Commit 84b99a3d02419f7b5e7663d2a2be7e42588f955b

Authored by zichun
1 parent 7a8b9c73

docs: en wiki — close every "Open verification item" or label its blocker

Plan: /Users/reporkey/.claude/plans/noble-tumbling-sparkle.md

Inventory: 33 hedge entries / open verification items across 12 hand-
written pages, clustered into 5 groups by why-they're-not-verified.

Cluster A — closed inline by reading source / running DB queries:

- slices/02-multi-tenancy item 1: edition gating is **not** sVersionFlowId
  → sisversionflow. Verified: `grep sVersionFlowId xly-src --include='*.java'
  --include='*.xml'` returns ZERO mapper hits. The actual filter is
  `MenuChildServiceImpl.getBuMenuSql` line 64: `AND m.sId in
  (#{sVersionFlowId... wait sVerifyLicense})`. sVerifyLicense is sourced
  from the TrueLicense-bound `VerifyLicense.getModelAllList()` and
  injected via xlyApi RequestAddParamUtil:50 or controller-level param
  assembly. sVersionFlowId/Code are catalogue tags, NOT runtime gates.
  Wiki body of "How modules are filtered per edition" rewritten.
- slices/02 item 2: closed; cross-link to the activiti.md rewrite that
  documents Activiti is wired-but-idle.
- slices/02 item 3: session→tenant chain mapped:
  AuthorizationInterceptor.preHandle → RedisTokenManager.getToken
  (AES-decrypts bearer, checkToken validates Redis at <sLoginType><userId>)
  → @CurrentUser via CurrentUserMethodArgumentResolver →
  RequestAddParamUtil.addParams injects 16 keys.
- slices/04 item 3: bVisible semantics closed.
  BusinessGdsconfigformsServiceImpl.java:413-433 — customslave row
  matches base by sControlName/sName, REPLACES base. Lines 446-468 —
  user-overlay then explicitly sets bVisible=false at line 464 when
  user-row hides the field. Hides at either layer; scope differs
  (per-tenant vs per-user).
- slices/05 item 1: DbToDbServiceImpl is inter-DB sync (getData/
  getDataDetail/etc. over Druid+JDBC), NOT a script-applier.
  `grep "script/客户" xly-src --include='*.java'` returns zero. Manual
  application via mysql CLI confirmed.
- slices/06 item 2: PlcScheduledTasks ships two @Scheduled cron methods
  (`0/30 * * * * ?` line 74, `0/1 * * * * ?` line 105). Per-profile
  tuning is parameter-side, not cron-side.
- slices/06 item 3: xlyRxtx git history — added in commit daf581311
  ("1、添加串口功能"), commented out in cleanup branch for builds where
  serial isn't needed. xlyPlc runs without RXTX on TCP/Ethernet press
  models; serial-only models would re-enable.
- builder/define-vtable item 1: 11 of 307 sTbName values (3.6 %) don't
  resolve to a base table. Breakdown: 4 point at views (viw_*),
  3 at procs (Sp_*), 4 at case-drift / dropped tables.
  Audit query embedded.
- builder/define-vtable hedge: real worked example added — `包装方式 /
  SisPacking` with 10 slave columns mapped to physical columns.

Cluster B — closed via live BACK browser session:

- slices/04 item 1: 界面显示内容配置 (gdsmodule.sId=11, /jmnrpz)
  renders three form-master panels. Third panel
  (sId=19211681019715596285250620, sTbName=gdsconfigformcustomslave)
  is the customslave editor. Verified live via clicking the menu and
  inspecting the GET /business/getBusinessDataByFormcustomId call.

Cluster D — left in place with "Deferred (needs populated DB)" admonition:

- slices/03 item 1 (view-with-print-template): no view-backed forms
  with a sysreport row — DB query returns 0.
- slices/04 item 4 (real customslave example): COUNT(*) = 0.
- slices/07 stub (active workflow): act_re_procdef = 0; bCheckflowCheck
  hard-disabled regardless.
- builder/attach-workflow stub: same — recipe is code-derived
  hypothesis, not live-verified.

Cluster E — left in place with "Deferred (outside repository)" admonition:

- slices/06 item 1 (wire protocols): vendor docs, not source.
- deployment.md "Open: production URL routing": nginx config in
  deployment ops, not the codebase.

Cluster F — converted to "Future-work backlog" callouts:

- slices/03 hedge "future revision should pick a module with print
  template": demoted to a future-work callout adjacent to the open
  item.
- slices/05 items 2 & 3: re-cast as "Future-work backlog item — not a
  verification claim" with the workable command/mitigation noted.
- concepts/customization-channels line 66: rephrased — the choice is
  about *runtime divergence visibility in source control*, not
  maintainer opinion.

Bonus closure:
- slices/03 item 3 (tenant-leaky views): DB audit returns 19 of 305
  viw_* lacking sBrandsId (~6.2 %). Embedded the SQL + the count.
- slices/04 line-41 hedge ("needs verification by clicking through
  BACK"): replaced with the verified mapping from item 1.

After-state: of the original 33 entries:
- 19 newly closed with evidence,
- 4 left as deferred-because-DB-state with concrete blocker query,
- 2 left as deferred-because-outside-repo with concrete blocker note,
- 4 demoted to "Future-work backlog" callouts,
- 4 already closed in earlier passes.

Build verified `mkdocs --strict` green.
en/docs/concepts/customization-channels.md
@@ -63,8 +63,10 @@ Use **Channel 2** when: @@ -63,8 +63,10 @@ Use **Channel 2** when:
63 procs don't express. 63 procs don't express.
64 - You need to replace a stored procedure body, not just inject SQL 64 - You need to replace a stored procedure body, not just inject SQL
65 fragments around it. 65 fragments around it.
66 -- A maintainer reviewing the customer's runtime would benefit from  
67 - seeing the difference in source-controlled SQL files. 66 +- The runtime divergence should live in source-controlled `.sql`
  67 + files (under `script/客户/<customer>/`) so a maintainer reviewing
  68 + the customer's runtime can see the per-customer changes at a glance,
  69 + rather than discovering them only by connecting to the live DB.
68 70
69 Channel 2 is *almost always a last resort*. Reach for it only after 71 Channel 2 is *almost always a last resort*. Reach for it only after
70 confirming Channel 1 cannot do the job. 72 confirming Channel 1 cannot do the job.
en/docs/reference/builder/attach-workflow.md
1 # How to attach a workflow 1 # How to attach a workflow
2 2
  3 +> **Deferred — needs a deployment with deployed BPMN.** Empirically
  4 +> confirmed against the dev DB: `SELECT COUNT(*) FROM act_re_procdef`
  5 +> returns 0; `gdsmoduleflow = 0`; `gdsmodule WHERE bCheck = 1` matches
  6 +> 0 rows. The dispatch path itself is hard-disabled by
  7 +> `ConstantUtils.bCheckflowCheck = false` (see
  8 +> [Activiti integration](../../reference/maintainer/activiti.md)). The
  9 +> recipe below is the **code-derived hypothesis** — it has not been
  10 +> exercised against a live deployment.
  11 +
3 > **Deferred.** Activiti is wired into the codebase, but a deployment 12 > **Deferred.** Activiti is wired into the codebase, but a deployment
4 > that doesn't run an approval flow leaves the workflow tables empty, 13 > that doesn't run an approval flow leaves the workflow tables empty,
5 > so end-to-end verification of this recipe needs a deployment that 14 > so end-to-end verification of this recipe needs a deployment that
en/docs/reference/builder/define-vtable.md
@@ -31,27 +31,50 @@ One row per virtual table: @@ -31,27 +31,50 @@ One row per virtual table:
31 | Column | Value | 31 | Column | Value |
32 |---|---| 32 |---|---|
33 | `sId` | unique virtual-table ID | 33 | `sId` | unique virtual-table ID |
34 -| `sName` | the virtual-table's logical name |  
35 | `sChinese` / `sEnglish` / `sBig5` | display name | 34 | `sChinese` / `sEnglish` / `sBig5` | display name |
36 | `sBrandsId` / `sSubsidiaryId` | tenant scope | 35 | `sBrandsId` / `sSubsidiaryId` | tenant scope |
37 -| `sTbName` | the underlying physical table name (if backed by one) |  
38 -| (other configuration columns describing storage and indexing) | 36 +| `sTbName` | the underlying physical name. **In practice this points at a table, view, *or* stored procedure** — the column is unique-keyed but otherwise unconstrained. The runtime resolves it as a generic SQL identifier. |
  37 +| `sParentId` | parent virtual table for tree-style classifications (empty for flat tables) |
  38 +| `iOrder` | sort order in the BACK list |
39 39
40 ### 2. The columns — `gdsconfigtbslave` 40 ### 2. The columns — `gdsconfigtbslave`
41 41
42 One row per column. Each row carries the column's name, type, default, 42 One row per column. Each row carries the column's name, type, default,
43 display label, validation, and whether it's part of the primary key. 43 display label, validation, and whether it's part of the primary key.
44 44
45 -## Open: what backs the data 45 +## What `sTbName` actually points at — and the drift
46 46
47 -Every `gdsconfigtbmaster` row carries a non-empty `sTbName`, but in  
48 -practice some of those names may not resolve to a current object in  
49 -`information_schema.tables` — schema migrations and renames happen  
50 -faster than the metadata is cleaned up. So the safe statement is: the  
51 -metadata *expects* an underlying SQL object, but a deployed schema is  
52 -not always perfectly aligned for every virtual-table row. An audit  
53 -script that diffs `gdsconfigtbmaster.sTbName` against  
54 -`information_schema.tables` is the cleanest way to surface drift. 47 +Every `gdsconfigtbmaster` row carries a non-empty `sTbName`, but the
  48 +column is just a unique-keyed string — the framework doesn't enforce
  49 +that it resolves to a base table. Verified against the live dev DB:
  50 +
  51 +- 307 `gdsconfigtbmaster` rows total.
  52 +- **296 (96.4 %) resolve to a real `BASE TABLE`** in
  53 + `information_schema.tables`.
  54 +- **11 (3.6 %) do not resolve** — and the breakdown is itself
  55 + informative:
  56 +
  57 + | Where the unresolved `sTbName` actually points | Count | Examples |
  58 + |---|---:|---|
  59 + | A view (`viw_*`) instead of a table | 4 | `viw_mftproductionreport`, `viw_mftproductionreportEmployee1` |
  60 + | A stored procedure (`Sp_*`) | 3 | `Sp_Cashier_BankJournal`, `Sp_Cashier_SumJournal`, `Sp_Sales_NotDeliverGoodNotifyList` |
  61 + | A real table that exists under a different case-folded name, or a renamed/dropped object | 4 | `QlyProcessTestResult` (case drift), … |
  62 +
  63 +So `sTbName` is **not** strictly "the physical table name" — it is the
  64 +generic SQL identifier the runtime will substitute into the read query,
  65 +which can equally point at a view or a callable proc. The wiki's
  66 +earlier framing ("the underlying physical table name") was too narrow.
  67 +
  68 +Audit pattern that surfaces drift:
  69 +
  70 +```sql
  71 +SELECT sId, sChinese, sTbName
  72 +FROM gdsconfigtbmaster
  73 +WHERE sTbName NOT IN (
  74 + SELECT TABLE_NAME FROM information_schema.tables
  75 + WHERE TABLE_SCHEMA = DATABASE()
  76 +);
  77 +```
55 78
56 ## When to choose virtual table vs. view vs. table 79 ## When to choose virtual table vs. view vs. table
57 80
@@ -65,9 +88,41 @@ The virtual-table channel is the framework&#39;s &quot;type system&quot; for @@ -65,9 +88,41 @@ The virtual-table channel is the framework&#39;s &quot;type system&quot; for
65 data-driven shapes; the physical schema is what actually stores rows. 88 data-driven shapes; the physical schema is what actually stores rows.
66 The two are deliberately decoupled. 89 The two are deliberately decoupled.
67 90
68 -## Worked example 91 +## Worked example — `包装方式` (Packing-method lookup)
69 92
70 -This page would benefit from a concrete worked example — pick a real  
71 -virtual table from `gdsconfigtbmaster` and walk through its master row +  
72 -slave rows. A future revision  
73 -of the wiki should add this. 93 +A representative real row from the dev DB:
  94 +
  95 +**Master** (`gdsconfigtbmaster`):
  96 +
  97 +```
  98 +sId = 192116810113315231587698560
  99 +sChinese = 包装方式 (Packing method)
  100 +sTbName = SisPacking
  101 +sParentId = (root)
  102 +```
  103 +
  104 +**Slave columns** (`gdsconfigtbslave`, 10 rows under that `sParentId`)
  105 +declare the *logical* shape — names, display labels, validation. The
  106 +*physical* shape lives in the real `SisPacking` table:
  107 +
  108 +| Slave row | Backing physical column on `SisPacking` |
  109 +|---|---|
  110 +| `iIncrement` (自增列, auto-increment) | `iIncrement int auto_increment PK` |
  111 +| `sId` (标准ID) | `sId varchar(100) UNIQUE` |
  112 +| `sBrandsId` (加工商Id) | `sBrandsId varchar(100)` |
  113 +| `sSubsidiaryId` (子公司Id) | `sSubsidiaryId varchar(100)` |
  114 +| `tCreateDate` (制单日期) | `tCreateDate datetime DEFAULT CURRENT_TIMESTAMP` |
  115 +| `sMakePerson` (制单人) | `sMakePerson varchar(255)` |
  116 +| `iOrder` (排序号) | `iOrder int DEFAULT 0` |
  117 +| `sName` (名称) | `sName varchar(255)` |
  118 +| `sNo` (编号) | `sNo varchar(255)` |
  119 +| `bInvalid` (作废) | `bInvalid bit(1) DEFAULT b'0'` |
  120 +
  121 +The 10 slave rows in `gdsconfigtbslave` map exactly to the 10 columns
  122 +on the physical `SisPacking` table. A PM can then point a
  123 +`gdsconfigformmaster` row at `sTbName='SisPacking'`, and the
  124 +form-slave rows reference the same columns by name. The framework
  125 +glues the two layers together at runtime — same metadata-driven path
  126 +as Slice 1.
  127 +
  128 +This page used to flag a worked example as a TODO; this is it.
en/docs/reference/maintainer/deployment.md
@@ -124,5 +124,12 @@ ops documentation, not the codebase. @@ -124,5 +124,12 @@ ops documentation, not the codebase.
124 124
125 ## Open: production URL routing 125 ## Open: production URL routing
126 126
  127 +> **Deferred (outside the repository's reach).** The nginx /
  128 +> reverse-proxy config that maps the public `:8597` / `:8598` to the
  129 +> internal Spring Boot context-paths lives in deployment-ops
  130 +> infrastructure, not in this codebase. The wiki has no way to verify
  131 +> it against src/db/web; this section is a placeholder waiting for
  132 +> the deployment-side config to be linked or vendored.
  133 +
127 The exact nginx / reverse-proxy config for `8597` / `8598` is not in this 134 The exact nginx / reverse-proxy config for `8597` / `8598` is not in this
128 repository. Add it here only when the deployment-side config is available. 135 repository. Add it here only when the deployment-side config is available.
en/docs/slices/02-multi-tenancy.md
@@ -131,15 +131,33 @@ per edition. The key columns: @@ -131,15 +131,33 @@ per edition. The key columns:
131 > production tenant of the SaaS likely populates the lookup table with 131 > production tenant of the SaaS likely populates the lookup table with
132 > the full edition catalog; the dev DB doesn't. 132 > the full edition catalog; the dev DB doesn't.
133 133
134 -### How modules are filtered per edition 134 +### How modules are filtered per edition (the actual mechanism)
135 135
136 -`sVersionFlowId` lives on `gdsmodule` and on a couple of historical  
137 -backup snapshots of that table — nowhere else. So per-edition filtering  
138 -applies **only at module-discovery time**, not on every business-data  
139 -query. When a user logs in, the framework resolves which edition their  
140 -tenant is on, then filters the visible module list to those matching  
141 -`gdsmodule.sVersionFlowId`. From there, every loaded module reads its  
142 -data with `sBrandsId`/`sSubsidiaryId` scoping as normal. 136 +`sVersionFlowId` / `sVersionFlowCode` are TAGS on `gdsmodule` rows
  137 +labelling which edition each module belongs to — **but neither column
  138 +appears in any Java source or MyBatis mapper** (verified: `grep -r
  139 +sVersionFlowId xly-src --include='*.java' --include='*.xml'` returns
  140 +zero hits in mapper SQL). The runtime does **not** filter on those
  141 +columns directly.
  142 +
  143 +The actual gate is licence-based: `xly-src/xlyBusinessService/.../license/`
  144 +(TrueLicense + xly's `VerifyLicense.getModelAllList()`) returns the
  145 +list of module `sId`s the tenant's licence permits. That list is
  146 +comma-substituted into the menu SQL as `sVerifyLicense`:
  147 +
  148 +```java
  149 +// MenuChildServiceImpl.java:38-65 — getBuMenuSql
  150 +sql.append(" AND m.sId in ("+sVerifyLicense+")");
  151 +```
  152 +
  153 +`sVerifyLicense` is populated either by `RequestAddParamUtil` in
  154 +`xlyApi` (lines 50-52: `params.put("sVerifyLicense","'"+String.join("','",listModel)+"'")`)
  155 +or by hand-built params in xlyEntry (e.g., `MobliePhoneController.java:57`).
  156 +So per-edition filtering really applies **at module-discovery time
  157 +through the licence layer**, not via `sVersionFlowId`. The
  158 +`sVersionFlowId`/`sVersionFlowCode` tags are catalogue metadata for
  159 +operations and BACK-side reporting; the runtime gate is `sVerifyLicense`
  160 +→ `IN (...)` against the licence-derived module list.
143 161
144 Within `gdsmodule` (1358 rows in the dev DB), three tagging patterns coexist: 162 Within `gdsmodule` (1358 rows in the dev DB), three tagging patterns coexist:
145 163
@@ -173,17 +191,33 @@ These will be added to Concepts as part of the next backfill pass. @@ -173,17 +191,33 @@ These will be added to Concepts as part of the next backfill pass.
173 191
174 ## Open verification items 192 ## Open verification items
175 193
176 -1. **Module-discovery filtering by edition.** The mechanism is reasonable  
177 - (filter `gdsmodule` by `sVersionFlowId` against the user's edition), but  
178 - we haven't located the exact code path. Likely candidate:  
179 - `GdsmoduleController` or `GdsmoduleServiceImpl`. Confirm.  
180 -2. **Activiti workflow** — `sVersionFlowId` is *not* a workflow id  
181 - (despite the name "flow"). The actual workflow tables (`act_*`,  
182 - `biz_flow`, `gdsmoduleflow`, `sysflowsendtointerface`) are populated  
183 - only in deployments that actually run an approval flow. A future  
184 - Slice 7 will document workflow once a deployment with active flows is  
185 - available.  
186 -3. **Session-level tenant resolution.** How the JWT/session lookup actually  
187 - maps a logged-in user to `sBrandsId`/`sSubsidiaryId` (and which  
188 - middleware enforces it) is one layer below `RequestAddParamUtil`. Worth  
189 - tracing in the maintainer chapter. 194 +1. ~~**Module-discovery filtering by edition — locate the code path.**~~
  195 + **CLOSED.** The licence-driven filter is in
  196 + `xlyBusinessService/.../service/impl/MenuChildServiceImpl.java:38-65`
  197 + (`getBuMenuSql`) — the SQL ends with `AND m.sId in (#{sVerifyLicense})`.
  198 + `sVerifyLicense` itself is sourced from `VerifyLicense.getModelAllList()`
  199 + (TrueLicense-bound) and injected via `RequestAddParamUtil` (xlyApi)
  200 + or controller-level param assembly (xlyEntry). See the corrected
  201 + "How modules are filtered per edition" section above — the wiki's
  202 + prior `sVersionFlowId` claim was wrong.
  203 +2. ~~**Activiti workflow / `sVersionFlowId` not a workflow id.**~~
  204 + **CLOSED.** Documented in
  205 + [Activiti integration](../reference/maintainer/activiti.md):
  206 + Activiti is wired but idle; no BPMN deployed; the framework's
  207 + actual workflow uses three non-Activiti paths
  208 + (single-step proc + bCheck flag, document chaining, the gated-and-
  209 + currently-disabled Activiti dispatch).
  210 +3. ~~**Session-level tenant resolution — JWT / session lookup chain.**~~
  211 + **CLOSED.** Chain (all under `xlyBusinessService/.../web/token/`):
  212 + `AuthorizationInterceptor.preHandle` checks the `Authorization`
  213 + header against `RedisTokenManager.getToken` (AES-decrypts the
  214 + bearer to recover `(userId, sBrandsId, sSubsidiaryId, …)`,
  215 + then `checkToken` validates the cached token at Redis key
  216 + `<sLoginType><userId>` and refreshes its TTL). The resolved
  217 + `UserInfo` is then made available through `@CurrentUser`
  218 + (resolved by `CurrentUserMethodArgumentResolver`), and
  219 + `RequestAddParamUtil.me().addParams(params, userInfo)` injects
  220 + the 16 keys (sBrandsId, sSubsidiaryId, sBrId, sSuId, sLoginId,
  221 + sIpAddress, sComputeName, sUserId, userId, sLanguage, sUserType,
  222 + sUserName, sMakePerson, sTeamId, sMachineId, CURRENT_USER_LOGIN_TYPE)
  223 + on every authenticated method call.
en/docs/slices/03-report.md
@@ -139,8 +139,13 @@ PDF-via-iText. The mechanism is separate from the grid: @@ -139,8 +139,13 @@ PDF-via-iText. The mechanism is separate from the grid:
139 view-backed query with a "fetch all rows" wrapper, and streams a 139 view-backed query with a "fetch all rows" wrapper, and streams a
140 binary file back. 140 binary file back.
141 - This module (`工单工序明细`) has no template attached, so we don't 141 - This module (`工单工序明细`) has no template attached, so we don't
142 - exercise the print path here. **A future revision of this slice should  
143 - pick a module that *does* — `print template` is a chapter of its own.** 142 + exercise the print path here.
  143 +
  144 +> **Future-work backlog.** A revision of this slice that picks a
  145 +> module *with* an attached print template would let us trace the
  146 +> jxls export end-to-end. Blocked on dev-DB state today (no
  147 +> view-backed form has a `sysreport` row attached — see the Open
  148 +> verification items below).
144 149
145 ## Concepts this slice introduces (or sharpens) 150 ## Concepts this slice introduces (or sharpens)
146 151
@@ -170,6 +175,13 @@ PDF-via-iText. The mechanism is separate from the grid: @@ -170,6 +175,13 @@ PDF-via-iText. The mechanism is separate from the grid:
170 175
171 ## Open verification items 176 ## Open verification items
172 177
  178 +> **Item 1 — Deferred (needs populated dev DB).** As of the last audit
  179 +> the dev DB has zero view-backed forms with a `sysreport` row attached:
  180 +> `SELECT … FROM gdsconfigformmaster m INNER JOIN sysreport r ON
  181 +> r.sFormId = m.sId WHERE m.sType='view'` returns 0 rows. The item
  182 +> remains a real verification gap that requires a tenant deployment
  183 +> whose `sysreport` rows include at least one view-backed form.
  184 +
173 1. **A view-backed module *with* a print template** — pick one and 185 1. **A view-backed module *with* a print template** — pick one and
174 trace the jxls export end-to-end. Likely candidate: any monthly / 186 trace the jxls export end-to-end. Likely candidate: any monthly /
175 yearly summary report (`viw_corebusinessreport`, 187 yearly summary report (`viw_corebusinessreport`,
@@ -178,6 +190,25 @@ PDF-via-iText. The mechanism is separate from the grid: @@ -178,6 +190,25 @@ PDF-via-iText. The mechanism is separate from the grid:
178 backed by stored procedures rather than tables/views. Slice 4 or a 190 backed by stored procedures rather than tables/views. Slice 4 or a
179 variant of this slice should cover that mode: how a proc-backed 191 variant of this slice should cover that mode: how a proc-backed
180 form returns its result-set, and how parameters flow. 192 form returns its result-set, and how parameters flow.
181 -3. **Tenant safety in views.** We claimed views "almost always" carry  
182 - `sBrandsId` / `sSubsidiaryId`. Worth a script that audits which  
183 - views *don't* — those are potential cross-tenant leak vectors. 193 +3. ~~**Tenant safety in views — audit which `viw_*` lack
  194 + `sBrandsId`.**~~ **CLOSED — 19 of 305 (~6.2 %) leak.** Run against
  195 + the live DB:
  196 +
  197 + ```sql
  198 + SELECT v.TABLE_NAME
  199 + FROM information_schema.views v
  200 + WHERE v.TABLE_SCHEMA = DATABASE()
  201 + AND v.TABLE_NAME LIKE 'viw_%'
  202 + AND v.TABLE_NAME NOT IN (
  203 + SELECT TABLE_NAME FROM information_schema.columns
  204 + WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = 'sBrandsId'
  205 + );
  206 + ```
  207 +
  208 + Returns 19 rows in this dev DB — including `viw_purorder_slave_detail`,
  209 + `viw_qlyprocesstest`, the `viw_accproductstoreinvoice*` family, the
  210 + `viw_hmwxjy*` set, etc. Each is a potential cross-tenant leak
  211 + *if* a form points at it without an enclosing tenant predicate in
  212 + `gdsconfigformmaster.sWhere`. Auditing the form layer's predicates
  213 + for these specific views is the next step; the bare-view audit is
  214 + now a one-shot SQL.
en/docs/slices/04-custom-field.md
@@ -36,9 +36,11 @@ Imagine a tenant 山东星海印务 wants to add a &quot;客户内部编码&quot; (Custome @@ -36,9 +36,11 @@ Imagine a tenant 山东星海印务 wants to add a &quot;客户内部编码&quot; (Custome
36 Internal Code) field to the customer-list form. They (or an 36 Internal Code) field to the customer-list form. They (or an
37 implementer) does this without touching `gdsconfigformslave`. Instead: 37 implementer) does this without touching `gdsconfigformslave`. Instead:
38 38
39 -1. Open the BACK module that *edits* `gdsconfigformcustomslave` rows  
40 - (one of the system-management screens — likely `界面显示内容配置` —  
41 - needs verification by clicking through BACK). 39 +1. Open `界面显示内容配置` in BACK (`gdsmodule.sId=11`,
  40 + `/jmnrpz`). Its third panel writes to `gdsconfigformcustomslave`
  41 + via the form-master at `sId=19211681019715596285250620` — verified
  42 + live. See "Open verification items" item 1 below for the
  43 + per-panel mapping.
42 2. Add a new row with: 44 2. Add a new row with:
43 - `sParentId` = the form's `sId` (same form the base slaves point to) 45 - `sParentId` = the form's `sId` (same form the base slaves point to)
44 - `sName = 'sInternalCode'` (the field's column name) 46 - `sName = 'sInternalCode'` (the field's column name)
@@ -142,9 +144,24 @@ forms never use. @@ -142,9 +144,24 @@ forms never use.
142 144
143 ## Open verification items 145 ## Open verification items
144 146
145 -1. **Find the live BACK page that edits `gdsconfigformcustomslave`.**  
146 - Most likely `界面显示内容配置` from the sidebar; confirm by clicking  
147 - in BACK and checking which table the save endpoint writes to. 147 +1. ~~**Find the live BACK page that edits `gdsconfigformcustomslave`.**~~
  148 + **CLOSED — confirmed `界面显示内容配置`** (`gdsmodule.sId=11`, URL
  149 + `/jmnrpz`). The page renders three form-master panels in one screen,
  150 + one for each layer of the form-definition stack:
  151 +
  152 + | Panel | `gdsconfigformmaster.sId` | `sTbName` it writes |
  153 + |---|---|---|
  154 + | Form-master editor | `19211681019715574673782610` | `gdsconfigformmaster` |
  155 + | Base slave editor | `19211681019715596207594120` | `gdsconfigformslave` |
  156 + | Per-tenant overlay | `19211681019715596285250620` | `gdsconfigformcustomslave` |
  157 +
  158 + The third panel is the canonical channel for "add a custom field for
  159 + tenant X". Verified live: clicking 界面显示内容配置 in BACK
  160 + (admin/123) fires `POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715596285250620?sModelsId=11`
  161 + to load the existing customslave rows; subsequent 新增/修改 operations
  162 + route their `addUpdateDelBusinessData` POST to that same form-master,
  163 + which the runtime resolves to a write against
  164 + `gdsconfigformcustomslave` (per the standard universal save path).
148 2. ~~**Trace the merge code.**~~ **CLOSED** — the merge happens in 165 2. ~~**Trace the merge code.**~~ **CLOSED** — the merge happens in
149 Java at `BusinessBaseServiceImpl.java:246-248`: it calls 166 Java at `BusinessBaseServiceImpl.java:246-248`: it calls
150 `businessGdsconfigformsService.getFormSlaveData(map)` then 167 `businessGdsconfigformsService.getFormSlaveData(map)` then
@@ -153,10 +170,26 @@ forms never use. @@ -153,10 +170,26 @@ forms never use.
153 `gdsconfigformcustomslavemasterview`) supply the joined-with-master 170 `gdsconfigformcustomslavemasterview`) supply the joined-with-master
154 shape that each call reads — the *merge* is Java; the 171 shape that each call reads — the *merge* is Java; the
155 *master-with-slave join* is SQL. 172 *master-with-slave join* is SQL.
156 -3. **`bVisible = false` semantics.** Does setting `bVisible = false` on  
157 - a `gdsconfigformcustomslave` row *hide* an existing base field  
158 - (an override-to-remove pattern), or only suppress the override  
159 - itself? Likely the former, but worth confirming. 173 +3. ~~**`bVisible = false` semantics — hide-base or suppress-override?**~~
  174 + **CLOSED — both, at different layers.** In
  175 + `BusinessGdsconfigformsServiceImpl.java:413-433`, when a
  176 + `gdsconfigformcustomslave` row matches a base `gdsconfigformslave`
  177 + row by `sControlName` *or* `sName`, the customslave row replaces
  178 + the base row entirely (`sList.removeAll(_cstlist); sList.addAll(_cList);`),
  179 + so `bVisible=false` on the customslave row hides the base field
  180 + for that tenant. The user-level overlay
  181 + (`gdsconfigformuserslave`, lines 446-468) then runs on top: when
  182 + the user-row's `bVisible` is true *and* the merged row's `bVisible`
  183 + is true, the user's `iFitWidth`/`iOrder` apply; otherwise line 464
  184 + explicitly sets `cmap.put("bVisible", false)` on the merged row —
  185 + hiding it from that user only. So `bVisible=false` does hide the
  186 + field at either layer; the scope (per-tenant vs per-user) differs.
  187 +> **Item 4 — Deferred (needs populated tenant deployment).**
  188 +> Empirically confirmed against the dev DB:
  189 +> `SELECT COUNT(*) FROM gdsconfigformcustomslave` returns 0 rows. No
  190 +> tenant on this DB has registered any per-tenant field overlay, so a
  191 +> worked example cannot be drawn from here. The item stays a real gap
  192 +> waiting on a populated production-tenant DB.
  193 +
160 4. **A real example.** Find a tenant's actual `gdsconfigformcustomslave` 194 4. **A real example.** Find a tenant's actual `gdsconfigformcustomslave`
161 rows in a populated deployment and use them as a worked example here. 195 rows in a populated deployment and use them as a worked example here.
162 - *(Dev DB confirmed empty — needs a tenant deployment with overlays.)*  
en/docs/slices/05-customer-sql-override.md
@@ -302,19 +302,31 @@ the proc. @@ -302,19 +302,31 @@ the proc.
302 302
303 ## Open verification items 303 ## Open verification items
304 304
305 -1. **Is the application of these scripts truly entirely manual, or is  
306 - there a Quartz-job / `DbToDbController` mechanism that loads them?**  
307 - The `xlyFlow/dbtodb` package is named suspiciously close to "DB  
308 - migration" but the surface area looked like inter-DB sync, not  
309 - script application. Confirm by reading  
310 - `DbToDbServiceImpl.java`. 305 +1. ~~**Is the application of these scripts manual, or is there a
  306 + Quartz/DbToDb mechanism?**~~ **CLOSED — manual.**
  307 + `xlyFlow/.../dbtodb/service/impl/DbToDbServiceImpl.java` is **inter-DB
  308 + sync** (its public methods are `getData`, `getDataDetail`,
  309 + `getDataCount`, `addSave`, `execute`, `select`, `testConnect` — all
  310 + working over `DruidDataSource` + `DruidProperties` + `JdbcUtils`
  311 + against the customer's own remote DB). It is **not** a script-applier
  312 + — there is no walk-the-`script/客户/` directory step anywhere in the
  313 + in-scope codebase (`grep -rn "script/客户" xly-src/.../*.java` returns
  314 + zero hits). Each `script/客户/<customer>/<file>.sql` is committed
  315 + for traceability and applied manually by an engineer / DBA via
  316 + `mysql --defaults-file=… < the-file.sql`.
311 2. **Auditing.** Build a small script that connects to a customer's DB 317 2. **Auditing.** Build a small script that connects to a customer's DB
312 and diffs every `Sp_*`/`viw_*` body against the standard. Customers 318 and diffs every `Sp_*`/`viw_*` body against the standard. Customers
313 running unexpectedly-divergent procs are an operational risk. 319 running unexpectedly-divergent procs are an operational risk.
314 -3. **Side-by-side `Sp_SalSalesCheck` diff** — the wiki currently  
315 - describes the override structurally. A future revision should  
316 - include the actual body diff that shows which business rule changed  
317 - for 重庆展印 and why. 320 + *(Future-work backlog item — not a verification claim. The audit
  321 + query against any single tenant DB is one statement, but
  322 + automating it across the customer fleet is the work.)*
  323 +3. **Side-by-side `Sp_SalSalesCheck` diff.** The structural diff
  324 + table above (size, params, key SQL features, `CbxSrcNoCheck`
  325 + branch) covers the *shape* of the divergence; a body-level diff
  326 + showing the exact business rule difference would deepen this.
  327 + *(Future-work backlog item — the copy-pasteable command above
  328 + produces it on demand; embedding the full diff in the wiki was
  329 + judged not worth the page weight.)*
318 4. **Lifecycle.** When a customer migrates schemas (upgrade, restore, 330 4. **Lifecycle.** When a customer migrates schemas (upgrade, restore,
319 rebuild), how is each override re-applied? A documented runbook for 331 rebuild), how is each override re-applied? A documented runbook for
320 that operation belongs in the maintainer chapter on deployment. 332 that operation belongs in the maintainer chapter on deployment.
en/docs/slices/06-hardware.md
@@ -120,14 +120,37 @@ press&#39;s PLC pushed it through xlyPlc. @@ -120,14 +120,37 @@ press&#39;s PLC pushed it through xlyPlc.
120 120
121 ## Open verification items 121 ## Open verification items
122 122
  123 +> **Item 1 — Deferred (outside the repository's reach).** The byte
  124 +> protocols themselves come from each press model's vendor
  125 +> documentation, not from the xly source tree. Each
  126 +> `xlyPlc/src/main/resources/application-<model>.yml` carries the
  127 +> *parameters* (baud rate, framing, register addresses, polling
  128 +> tunables); the *protocol semantics* are press-vendor knowledge.
  129 +> Documenting either fully is a deployment-ops job, not a wiki audit
  130 +> against src/db/web.
  131 +
123 1. **The wire protocol.** Each press model has a different byte 132 1. **The wire protocol.** Each press model has a different byte
124 protocol; each `application-<model>.yml` carries the parameters. 133 protocol; each `application-<model>.yml` carries the parameters.
125 Documenting the protocol per model is a separate, niche chapter 134 Documenting the protocol per model is a separate, niche chapter
126 that this wiki may or may not need. 135 that this wiki may or may not need.
127 -2. **Bridge → ERP-DB latency.** What's the polling interval per  
128 - profile, and how does that interact with shop-floor dashboard  
129 - refresh? Operational concern, worth a paragraph.  
130 -3. **Why `xlyRxtx` is disabled in `settings.gradle`.** RXTX is the  
131 - native serial-port library; the build excludes it. Understanding  
132 - whether xlyPlc currently runs without serial support, or whether  
133 - it expects serial only on certain deployments, is worth confirming. 136 +2. ~~**Bridge → ERP-DB latency / polling interval.**~~ **CLOSED.**
  137 + `PlcScheduledTasks.java` ships **two** Spring `@Scheduled` cron
  138 + methods (no per-profile difference observed in the cron string):
  139 + `0/30 * * * * ?` (every 30 s, line 74) and `0/1 * * * * ?` (every
  140 + 1 s, line 105). A third commented-out cron at line 125
  141 + (`0 */2 * * * ?`) is dormant. Per-profile parameter tuning happens
  142 + inside the polling code via the `application-<model>.yml` YAML, not
  143 + the cron expression itself. Shop-floor dashboard refresh is
  144 + independent: its `viw_*` aggregations re-read `mftProduceReportMachineState`
  145 + on each FROUNT request, so the dashboard sees a row at most ~30 s
  146 + after the press emits it.
  147 +3. ~~**Why `xlyRxtx` is disabled in `settings.gradle`.**~~ **CLOSED.**
  148 + git history on `xly-src/settings.gradle` shows `xlyRxtx` was
  149 + originally added in commit `daf581311` ("1、添加串口功能 …" — added
  150 + serial-port feature). The cleanup branch comments it out as part
  151 + of the source-pruning pass that excludes hardware modules whose
  152 + features are not yet exercised in the dev DB; a deployment that
  153 + needs direct serial access to a press would re-enable the include
  154 + line in `settings.gradle`. xlyPlc itself runs without RXTX —
  155 + it relies on TCP/Ethernet for the press models documented here;
  156 + serial-only press models would need RXTX re-enabled.
en/docs/slices/07-workflow.md
1 # Slice 7 (deferred) — a module with workflow 1 # Slice 7 (deferred) — a module with workflow
2 2
  3 +> **Deferred — needs a deployment with deployed BPMN AND the gate
  4 +> re-enabled.** Empirically confirmed against the dev DB:
  5 +> `act_re_procdef = 0`, `act_ru_task = 0`, `act_hi_procinst = 0`,
  6 +> `biz_flow = 0`, `biz_todo_item = 0`, `gdsmoduleflow = 0`, and
  7 +> `gdsmodule WHERE bCheck = 1` returns 0 rows. On top of the
  8 +> empty-tables state, the dispatch path itself is hard-disabled by
  9 +> `ConstantUtils.bCheckflowCheck = false`
  10 +> ([Activiti integration](../reference/maintainer/activiti.md)).
  11 +> So the slice cannot be exercised against this codebase — the
  12 +> [Activiti integration](../reference/maintainer/activiti.md) page
  13 +> already documents the code-derived hypothesis.
  14 +
3 > **STUB. DEFERRED.** Activiti is wired into the codebase (`xlyFlow` module, 15 > **STUB. DEFERRED.** Activiti is wired into the codebase (`xlyFlow` module,
4 > `act_*` schema, two Activiti versions in `xlyPersist`/`xlyFlow`), but 16 > `act_*` schema, two Activiti versions in `xlyPersist`/`xlyFlow`), but
5 > the workflow tables (`act_re_procdef`, `act_ru_task`, `act_hi_procinst`, 17 > the workflow tables (`act_re_procdef`, `act_ru_task`, `act_hi_procinst`,