Commit d21ed9699612096dee79bf341367aa99290b1302

Authored by zichun
1 parent bbcde3e6

docs: en wiki — finish remaining passes (B P1/P2/P3 gaps, D, E)

Second commit completing the verification plan after the first commit
covered Pass A + most of Pass B P0/P1 + Pass C1/C2.

P1 gaps:
- slices/02-multi-tenancy.md: add the four-table sTableNameList
  exception (gdsformconst, gdsmodule, gdsconfigformmaster,
  gdsconfigformslave strip tenant cols at write); rewrite query-shape
  section to distinguish global framework metadata from tenant-scoped
  overlays + business state; flag dev DB has only 8S_001 in
  sisversionflow despite gdsmodule.sVersionFlowCode referencing
  EBC-SD/EBC-RD/EBC-MDM/etc.; add live row counts (1002 untagged,
  322 8S_001, 15 EBC-SD-002, …).
- api-reference/internal.md, messaging.md, notifications.md verified
  as-is (Pass A surface fixes were sufficient, all file paths exist).

P2:
- slices/04-custom-field.md: close out the merge-code open item —
  merge is Java at BusinessBaseServiceImpl.java:246-248
  (getFormSlaveData + getFormCustomSlaveData); the two views supply
  the master⋈slave shape, the merge itself is in Java.
- slices/06-hardware.md: PlcScheduledTasks uses Spring @Scheduled cron
  (every 30s + every 1s), not Quartz directly.
- concepts/thesis.md: fix the "four-table read" framing — actual reads
  are from gdsconfigformmaster (with overlays), gdsformconst,
  sysjurisdiction, sysbillnosettings, sysreport. Soften slice-1
  reference to remove the "four-table" misframing.
- slices/03-report.md, concepts/api-surface.md, customization-channels,
  customization-layers, semantic-fk verified clean (script/客户/
  customer dirs = 18, 0 FK / 0 triggers on xly tables, both overlay
  views exist).

P3:
- reference/builder/define-form.md: rewrite cache-invalidation section
  to match the corrected architecture (synchronous @CacheEvict, not the
  JMS path).
- builder/define-vtable, builder/permissions, builder/index,
  builder/attach-workflow, slices/07-workflow, glossary/index,
  reference/maintainer/index, slices/index, contributing/index
  verified appropriately marked deferred / consistent.

Pass D consistency sweep:
- Ports (8080/8090/8000/8091/8597/8598) consistent across pages.
- Schema names consistently use xlyweberp_saas_ai;
  running-locally.md correctly documents the xlyweberp_saas vs _ai
  divergence.
- Counts (1358 gdsmodule rows, 311 views, 1687 procs, 177 functions)
  consistent.
- Module/Modle/Models codebase quirk consistently surfaced.
- xlyPlc plugin framing consistent across index.md, tech-stack.md,
  slice 06.
- vtable / virtual table — single canonical form ("virtual table") used
  in narrative; URL-style filename `define-vtable` preserved.
- proc / procedure / stored procedure usage clean (long form for
  emphasis, short for lists).

Pass E live traces (partial):
- Slice 1 read flow CORROBORATED end-to-end at BACK admin/123 edition
  基础版/8s. Both documented URLs captured exactly:
    GET  /xlyEntry/business/getModelBysId/13?sModelsId=13                                      → 200
    POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715574676360040?sModelsId=13 → 200
  including the redundant ?sModelsId=13 alongside the path variable.
  Updated slice-01 open verification items to mark the read item closed.
- Slice 1 save flow + slices 2-6 traces deferred (would mutate shared
  dev DB; FROUNT enumeration declined; monthly usage limit reached).

Pass C3 frontend inventory (partial):
- BACK admin sidebar = 10 framework-builder modules: 系统模块配置,
  数据表内容配置, 界面显示内容配置, 接口自定义配置, 系统常量配置,
  系统权限配置, 常用操作配置, 用户信息配置, Mysql脚本配置, 图表配置.
- 8/10 covered by existing wiki pages; 2 silent (常用操作配置,
  图表配置 — admin BACK utilities, not framework runtime).
- FROUNT enumeration declined by user.
en/docs/concepts/thesis.md
@@ -22,10 +22,13 @@ engineers) own the metadata and therefore own the application. @@ -22,10 +22,13 @@ engineers) own the metadata and therefore own the application.
22 22
23 Three costs are baked into this design and worth being explicit about: 23 Three costs are baked into this design and worth being explicit about:
24 24
25 -1. **Per-request metadata reads.** Every page load runs at least four  
26 - table reads (`gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave`,  
27 - `gdsjurisdiction`) plus tenant filters. The runtime caches aggressively,  
28 - but those reads are unavoidable on cache miss. 25 +1. **Per-request metadata reads.** Every page load runs five queries
  26 + on cache miss: `gdsconfigformmaster` (with personalize/customslave
  27 + overlays for the matching slave rows), `gdsformconst`,
  28 + `sysjurisdiction` (per-user grants — the map key is named
  29 + `gdsjurisdiction` but the actual table read is `sysjurisdiction`;
  30 + skipped for ADMIN), `sysbillnosettings`, `sysreport`. The runtime
  31 + caches aggressively, but those reads are unavoidable on cache miss.
29 32
30 2. **A schema that won't stop growing.** New module = a row in 33 2. **A schema that won't stop growing.** New module = a row in
31 `gdsmodule` plus 1-50 rows in `gdsconfigformslave` plus a backing 34 `gdsmodule` plus 1-50 rows in `gdsconfigformslave` plus a backing
@@ -66,7 +69,7 @@ customization to maintain. @@ -66,7 +69,7 @@ customization to maintain.
66 ## What this means for reading the wiki 69 ## What this means for reading the wiki
67 70
68 Every slice in this wiki documents one *application of the thesis*. Slice 1 71 Every slice in this wiki documents one *application of the thesis*. Slice 1
69 -is the four-table read on a CRUD module. Slice 2 is multi-tenant scoping 72 +is the metadata read on a CRUD module — the canonical instance. Slice 2 is multi-tenant scoping
70 through every layer. Slice 3 is the read-only / view-backed variant. Slice 73 through every layer. Slice 3 is the read-only / view-backed variant. Slice
71 4 is the customization overlay. Slice 5 is the escape hatch when the 74 4 is the customization overlay. Slice 5 is the escape hatch when the
72 overlay isn't enough. Together they cover the data-driven design from 75 overlay isn't enough. Together they cover the data-driven design from
en/docs/reference/builder/define-form.md
@@ -106,9 +106,14 @@ Click the new sidebar item — the form should render. If it doesn't: @@ -106,9 +106,14 @@ Click the new sidebar item — the form should render. If it doesn't:
106 106
107 ## Cache invalidation 107 ## Cache invalidation
108 108
109 -After inserting, the runtime's cache holds the *previous* (empty) state  
110 -until a JMS message fires. xly's cache-invalidation listener  
111 -(`ConsumerChangeGdsModuleThread`) handles this when the BACK builder  
112 -saves changes; if you're inserting via raw SQL, you may need to bounce  
113 -the running services or wait for the TTL to expire. See  
114 -[Cache invalidation on metadata change](../maintainer/cache-invalidation.md). 109 +After inserting, the runtime's cache holds the *previous* (empty)
  110 +state until something clears it. When BACK saves the change, the save
  111 +service synchronously calls `BusinessCleanRedisData.delCleanRedisData*`,
  112 +which fires `@CacheEvict` on the relevant cache regions in
  113 +`CleanRedisServiceImpl`. If you're inserting via raw SQL, **no eviction
  114 +runs** — you'll need to either invoke `BusinessCleanRedisDataImpl`
  115 +methods directly from inside the application, bounce the running
  116 +services, or wait for the TTL to expire. (The JMS path with the
  117 +similarly-named `ConsumerChangeGdsModuleThread` does base-data merging
  118 +via stored proc, NOT cache invalidation despite the name — see
  119 +[Cache invalidation on metadata change](../maintainer/cache-invalidation.md).)
en/docs/slices/01-hello-world.md
@@ -256,19 +256,31 @@ End of trace. @@ -256,19 +256,31 @@ End of trace.
256 **Maintainer track:** 256 **Maintainer track:**
257 257
258 - [The runtime: BusinessBaseController & friends](../reference/maintainer/runtime.md) — the controller and service layer that did the four-table read. 258 - [The runtime: BusinessBaseController & friends](../reference/maintainer/runtime.md) — the controller and service layer that did the four-table read.
259 -- [Cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md) — JMS-driven Redis flush. 259 +- [Cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md) — synchronous `@CacheEvict` (the JMS path serves a different purpose).
260 - [Multi-service deployment](../reference/maintainer/deployment.md) — `xlyEntry` vs `xlyApi` vs `xlyInterface`; this slice runs entirely on `xlyEntry`. 260 - [Multi-service deployment](../reference/maintainer/deployment.md) — `xlyEntry` vs `xlyApi` vs `xlyInterface`; this slice runs entirely on `xlyEntry`.
261 261
262 ## Open verification items 262 ## Open verification items
263 263
264 -Two items still open; one is now closed.  
265 -  
266 -1. **A live captured save.** The endpoint, handler, and payload shape are 264 +Read flow live-corroborated; save flow still pending.
  265 +
  266 +1. ~~**A live captured read.**~~ **CLOSED** — clicking 系统常量配置 in BACK
  267 + (`http://118.178.19.35:8597`, admin/123, edition `基础版/8s`) produced
  268 + exactly the documented HTTP exchange:
  269 + ```
  270 + GET /xlyEntry/business/getModelBysId/13?sModelsId=13 → 200 OK
  271 + POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715574676360040?sModelsId=13 → 200 OK
  272 + ```
  273 + Both URLs match the wiki verbatim, including the redundant `?sModelsId=13`
  274 + query param alongside the path variable. The post-login URL stays at
  275 + `/xtmkpz` rather than navigating to `/xtclpz` — confirming that URL
  276 + fragments are display state, not route drivers (the SPA loads the
  277 + form on demand from the sidebar click).
  278 +2. **A live captured save.** The endpoint, handler, and payload shape are
267 confirmed from source; the *body of an actual save request* hasn't been 279 confirmed from source; the *body of an actual save request* hasn't been
268 captured. This requires writing to the dev DB and we deferred it. Closing 280 captured. This requires writing to the dev DB and we deferred it. Closing
269 this means: open the module, click 新增, fill the form, click 保存, capture 281 this means: open the module, click 新增, fill the form, click 保存, capture
270 the JSON body and the response. 282 the JSON body and the response.
271 -2. **The exact SQL emitted by save/delete**, captured from `syslog4j` or a 283 +3. **The exact SQL emitted by save/delete**, captured from `syslog4j` or a
272 MyBatis debug log, to close the loop end-to-end. 284 MyBatis debug log, to close the loop end-to-end.
273 3. ~~**`sTable` validation in `addUpdateDelBusinessData`.**~~ **CLOSED** — 285 3. ~~**`sTable` validation in `addUpdateDelBusinessData`.**~~ **CLOSED** —
274 the runtime does **not** cross-check the frontend-supplied `sTable` 286 the runtime does **not** cross-check the frontend-supplied `sTable`
en/docs/slices/02-multi-tenancy.md
@@ -26,12 +26,17 @@ applied at module-list load time. Different mechanisms, different layers. @@ -26,12 +26,17 @@ applied at module-list load time. Different mechanisms, different layers.
26 26
27 ### How wide 27 ### How wide
28 28
29 -Essentially every base table and every view in the schema carries both  
30 -`sBrandsId` and `sSubsidiaryId` — both business-data tables and  
31 -framework-metadata tables. The convention is followed almost without  
32 -exception; the few tables that lack one or both columns are either  
33 -single-tenant lookups (e.g., shared dictionary tables) or third-party  
34 -schemas the framework doesn't author. 29 +Essentially every business-data table and view in the schema carries
  30 +both `sBrandsId` and `sSubsidiaryId`. **Most framework-metadata tables
  31 +also carry the columns**, but four of them — `gdsformconst`,
  32 +`gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave` — are an
  33 +explicit exception: `BusinessBaseServiceImpl.sTableNameList` (lines
  34 +162-169) lists them as "不需要公司子公司的表", and lines 1078-1084 strip
  35 +`sBrandsId`/`sSubsidiaryId` from the write payload for those tables.
  36 +In practice they hold a single sentinel tenant value shared across
  37 +all customers. The other tables that lack one or both columns are
  38 +single-tenant shared dictionaries or third-party schemas
  39 +(`act_*`, `qrtz_*`).
35 40
36 ### How they're injected 41 ### How they're injected
37 42
@@ -68,16 +73,23 @@ session via the `@CurrentUser` argument resolver. @@ -68,16 +73,23 @@ session via the `@CurrentUser` argument resolver.
68 73
69 ### How it shows up in queries 74 ### How it shows up in queries
70 75
71 -The Slice-1 `getModelBysId` call ended up reading metadata via: 76 +In the Slice-1 `getModelBysId` call, the per-tenant predicate
72 77
73 ```sql 78 ```sql
74 WHERE sBrandsId = #{sBrandsId} AND sSubsidiaryId = #{sSubsidiaryId} 79 WHERE sBrandsId = #{sBrandsId} AND sSubsidiaryId = #{sSubsidiaryId}
75 ``` 80 ```
76 81
77 -…on every metadata table (`gdsmodule`, `gdsconfigformmaster`,  
78 -`gdsconfigformslave`, `gdsformconst`, `gdsjurisdiction`, `sysbillnosettings`).  
79 -The same predicate appears in essentially every business-data query in the  
80 -codebase. This is the multi-tenancy boundary at runtime. 82 +is added on **per-tenant overlay reads** (`gdsconfigformpersonalize`,
  83 +`gdsconfigformcustomslave`) and on the **business-state** reads
  84 +(`sysbillnosettings`, `sysreport`, plus `sysjurisdiction` joined to
  85 +`sftlogininfojurisdictiongroup` for per-user grants — note the
  86 +returned map key is `gdsjurisdiction` even though the actual table
  87 +read is `sysjurisdiction`). It is *not* added on the framework-metadata
  88 +reads (`gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave`,
  89 +`gdsformconst`) — those are global and filtered by form-id only. The
  90 +same per-tenant predicate appears in essentially every business-data
  91 +query in the codebase. This is the multi-tenancy boundary at runtime
  92 +for tenant-owned state; framework metadata is intentionally global.
81 93
82 ### Failure mode 94 ### Failure mode
83 95
@@ -101,8 +113,8 @@ extras. @@ -101,8 +113,8 @@ extras.
101 113
102 ### Where editions are defined 114 ### Where editions are defined
103 115
104 -Editions live in the `sisversionflow` table — one row per edition. The  
105 -key columns: 116 +Editions are defined in the `sisversionflow` lookup table — one row
  117 +per edition. The key columns:
106 118
107 | Column | Meaning | 119 | Column | Meaning |
108 |---|---| 120 |---|---|
@@ -111,6 +123,14 @@ key columns: @@ -111,6 +123,14 @@ key columns:
111 | `sFlowName` | Display name (e.g., `基础版`) | 123 | `sFlowName` | Display name (e.g., `基础版`) |
112 | `bEbcErpPremium`, `bEbcMes`, `bEbcMesStandard`, `bSass` | Flags marking which product variants this edition belongs to | 124 | `bEbcErpPremium`, `bEbcMes`, `bEbcMesStandard`, `bSass` | Flags marking which product variants this edition belongs to |
113 125
  126 +> **State of this dev DB:** `sisversionflow` currently defines only
  127 +> one row — `8S_001 / 基础版`. The other edition codes shown in
  128 +> `gdsmodule.sVersionFlowCode` (`EBC-SD-002`, `EBC-RD-007`,
  129 +> `EBC-MDM-002`, etc.) are referenced as tags on module rows but have
  130 +> **no matching row in the `sisversionflow` lookup table** here. A
  131 +> production tenant of the SaaS likely populates the lookup table with
  132 +> the full edition catalog; the dev DB doesn't.
  133 +
114 ### How modules are filtered per edition 134 ### How modules are filtered per edition
115 135
116 `sVersionFlowId` lives on `gdsmodule` and on a couple of historical 136 `sVersionFlowId` lives on `gdsmodule` and on a couple of historical
@@ -121,14 +141,15 @@ tenant is on, then filters the visible module list to those matching @@ -121,14 +141,15 @@ tenant is on, then filters the visible module list to those matching
121 `gdsmodule.sVersionFlowId`. From there, every loaded module reads its 141 `gdsmodule.sVersionFlowId`. From there, every loaded module reads its
122 data with `sBrandsId`/`sSubsidiaryId` scoping as normal. 142 data with `sBrandsId`/`sSubsidiaryId` scoping as normal.
123 143
124 -Within `gdsmodule`, three tagging patterns coexist: 144 +Within `gdsmodule` (1358 rows in the dev DB), three tagging patterns coexist:
125 145
126 -- **Untagged rows** (`sVersionFlowId` empty) — framework-internal modules 146 +- **Untagged rows** (`sVersionFlowCode` empty) — 1002 rows. Framework-internal modules
127 and screens the edition gate does not apply to. The bulk of the catalog. 147 and screens the edition gate does not apply to. The bulk of the catalog.
128 -- **Essentials-tagged rows** (`sVersionFlowCode = '8S_001'`) — the 148 +- **Essentials-tagged rows** (`sVersionFlowCode = '8S_001'`) — 322 rows. The
129 universally-licensed core every edition gets. 149 universally-licensed core every edition gets.
130 -- **Edition-specific rows** (`EBC-SD-*`, `EBC-MDM-*`, `EBC-RD-*`,  
131 - `EBC-COM-*`, `EBC_*`) — add-ons gated by the customer's licence. 150 +- **Edition-specific rows** — `EBC-SD-002` (15), `EBC-RD-007` (6),
  151 + `EBC-MDM-002` (5), `EBC_001` (4), `EBC-SD-003` (2), `EBC-SD-001` (1),
  152 + `EBC-COM-001` (1) — add-ons gated by the customer's licence.
132 153
133 ## Concepts this slice introduces 154 ## Concepts this slice introduces
134 155
en/docs/slices/04-custom-field.md
@@ -143,12 +143,14 @@ forms never use. @@ -143,12 +143,14 @@ forms never use.
143 1. **Find the live BACK page that edits `gdsconfigformcustomslave`.** 143 1. **Find the live BACK page that edits `gdsconfigformcustomslave`.**
144 Most likely `界面显示内容配置` from the sidebar; confirm by clicking 144 Most likely `界面显示内容配置` from the sidebar; confirm by clicking
145 in BACK and checking which table the save endpoint writes to. 145 in BACK and checking which table the save endpoint writes to.
146 -2. **Trace the merge code.** Locate the exact join logic — is the merge  
147 - done in MyBatis (the two views) or in Java  
148 - (`BusinessGdsconfigformsServiceImpl.getFormSlaveData` +  
149 - `getFormCustomSlaveData`, called in  
150 - `BusinessBaseServiceImpl.java:246-248`)? Both look plausible from  
151 - the source we've seen. 146 +2. ~~**Trace the merge code.**~~ **CLOSED** — the merge happens in
  147 + Java at `BusinessBaseServiceImpl.java:246-248`: it calls
  148 + `businessGdsconfigformsService.getFormSlaveData(map)` then
  149 + `getFormCustomSlaveData(map)` and `addAll`s them into one
  150 + `slaveList`. The two views (`gdsconfigformslavemasterview`,
  151 + `gdsconfigformcustomslavemasterview`) supply the joined-with-master
  152 + shape that each call reads — the *merge* is Java; the
  153 + *master-with-slave join* is SQL.
152 3. **`bVisible = false` semantics.** Does setting `bVisible = false` on 154 3. **`bVisible = false` semantics.** Does setting `bVisible = false` on
153 a `gdsconfigformcustomslave` row *hide* an existing base field 155 a `gdsconfigformcustomslave` row *hide* an existing base field
154 (an override-to-remove pattern), or only suppress the override 156 (an override-to-remove pattern), or only suppress the override
en/docs/slices/06-hardware.md
@@ -20,7 +20,7 @@ generic framework picks up. @@ -20,7 +20,7 @@ generic framework picks up.
20 |---|---| 20 |---|---|
21 | **Module** | `xlyPlc` (a sibling Spring Boot service in the codebase) | 21 | **Module** | `xlyPlc` (a sibling Spring Boot service in the codebase) |
22 | **Direction** | One-way: PLC → ERP DB (no commands sent back to the press) | 22 | **Direction** | One-way: PLC → ERP DB (no commands sent back to the press) |
23 -| **Cadence** | Polled on a schedule (Quartz `PlcScheduledTasks`) | 23 +| **Cadence** | Polled on a schedule (Spring `@Scheduled` cron in `PlcScheduledTasks`, e.g. `0/30 * * * * ?` and `0/1 * * * * ?`) |
24 | **Per-press differentiation** | Spring profile (`-S10`, `-T0`, `-T1`, `-15S`, `-CT`, `-yt`, `-pro`) | 24 | **Per-press differentiation** | Spring profile (`-S10`, `-T0`, `-T1`, `-15S`, `-CT`, `-yt`, `-pro`) |
25 | **Database tables it writes to** | `mftProduceReportMachineState`, and related machine-state slaves | 25 | **Database tables it writes to** | `mftProduceReportMachineState`, and related machine-state slaves |
26 26
@@ -31,7 +31,7 @@ generic framework picks up. @@ -31,7 +31,7 @@ generic framework picks up.
31 | File | Role | 31 | File | Role |
32 |---|---| 32 |---|---|
33 | `PlcApplicationBoot.java` | Spring Boot entry point | 33 | `PlcApplicationBoot.java` | Spring Boot entry point |
34 -| `web/scheduler/PlcScheduledTasks.java` | Quartz-driven poll loop | 34 +| `web/scheduler/PlcScheduledTasks.java` | `@Component` with two `@Scheduled` cron methods (every 30 s and every 1 s) driving the poll loop |
35 | `web/scheduler/PlcRunStatus.java` | In-memory state for the current poll cycle | 35 | `web/scheduler/PlcRunStatus.java` | In-memory state for the current poll cycle |
36 | `web/scheduler/service/PlcToErpService.java` | Interface | 36 | `web/scheduler/service/PlcToErpService.java` | Interface |
37 | `web/scheduler/service/impl/PlcToErpServiceImpl.java` | The implementation: read PLC, write DB | 37 | `web/scheduler/service/impl/PlcToErpServiceImpl.java` | The implementation: read PLC, write DB |