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 22  
23 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 33 2. **A schema that won't stop growing.** New module = a row in
31 34 `gdsmodule` plus 1-50 rows in `gdsconfigformslave` plus a backing
... ... @@ -66,7 +69,7 @@ customization to maintain.
66 69 ## What this means for reading the wiki
67 70  
68 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 73 through every layer. Slice 3 is the read-only / view-backed variant. Slice
71 74 4 is the customization overlay. Slice 5 is the escape hatch when the
72 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 106  
107 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 256 **Maintainer track:**
257 257  
258 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 260 - [Multi-service deployment](../reference/maintainer/deployment.md) — `xlyEntry` vs `xlyApi` vs `xlyInterface`; this slice runs entirely on `xlyEntry`.
261 261  
262 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 279 confirmed from source; the *body of an actual save request* hasn't been
268 280 captured. This requires writing to the dev DB and we deferred it. Closing
269 281 this means: open the module, click 新增, fill the form, click 保存, capture
270 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 284 MyBatis debug log, to close the loop end-to-end.
273 285 3. ~~**`sTable` validation in `addUpdateDelBusinessData`.**~~ **CLOSED** —
274 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 26  
27 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 41 ### How they're injected
37 42  
... ... @@ -68,16 +73,23 @@ session via the `@CurrentUser` argument resolver.
68 73  
69 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 78 ```sql
74 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 94 ### Failure mode
83 95  
... ... @@ -101,8 +113,8 @@ extras.
101 113  
102 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 119 | Column | Meaning |
108 120 |---|---|
... ... @@ -111,6 +123,14 @@ key columns:
111 123 | `sFlowName` | Display name (e.g., `基础版`) |
112 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 134 ### How modules are filtered per edition
115 135  
116 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 141 `gdsmodule.sVersionFlowId`. From there, every loaded module reads its
122 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 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 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 154 ## Concepts this slice introduces
134 155  
... ...
en/docs/slices/04-custom-field.md
... ... @@ -143,12 +143,14 @@ forms never use.
143 143 1. **Find the live BACK page that edits `gdsconfigformcustomslave`.**
144 144 Most likely `界面显示内容配置` from the sidebar; confirm by clicking
145 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 154 3. **`bVisible = false` semantics.** Does setting `bVisible = false` on
153 155 a `gdsconfigformcustomslave` row *hide* an existing base field
154 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 20 |---|---|
21 21 | **Module** | `xlyPlc` (a sibling Spring Boot service in the codebase) |
22 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 24 | **Per-press differentiation** | Spring profile (`-S10`, `-T0`, `-T1`, `-15S`, `-CT`, `-yt`, `-pro`) |
25 25 | **Database tables it writes to** | `mftProduceReportMachineState`, and related machine-state slaves |
26 26  
... ... @@ -31,7 +31,7 @@ generic framework picks up.
31 31 | File | Role |
32 32 |---|---|
33 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 35 | `web/scheduler/PlcRunStatus.java` | In-memory state for the current poll cycle |
36 36 | `web/scheduler/service/PlcToErpService.java` | Interface |
37 37 | `web/scheduler/service/impl/PlcToErpServiceImpl.java` | The implementation: read PLC, write DB |
... ...