From d21ed9699612096dee79bf341367aa99290b1302 Mon Sep 17 00:00:00 2001 From: zichun Date: Sat, 9 May 2026 09:18:12 +0800 Subject: [PATCH] docs: en wiki — finish remaining passes (B P1/P2/P3 gaps, D, E) --- en/docs/concepts/thesis.md | 13 ++++++++----- en/docs/reference/builder/define-form.md | 17 +++++++++++------ en/docs/slices/01-hello-world.md | 22 +++++++++++++++++----- en/docs/slices/02-multi-tenancy.md | 57 +++++++++++++++++++++++++++++++++++++++------------------ en/docs/slices/04-custom-field.md | 14 ++++++++------ en/docs/slices/06-hardware.md | 4 ++-- 6 files changed, 85 insertions(+), 42 deletions(-) diff --git a/en/docs/concepts/thesis.md b/en/docs/concepts/thesis.md index 72ebebd..dbcbf78 100644 --- a/en/docs/concepts/thesis.md +++ b/en/docs/concepts/thesis.md @@ -22,10 +22,13 @@ engineers) own the metadata and therefore own the application. Three costs are baked into this design and worth being explicit about: -1. **Per-request metadata reads.** Every page load runs at least four - table reads (`gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave`, - `gdsjurisdiction`) plus tenant filters. The runtime caches aggressively, - but those reads are unavoidable on cache miss. +1. **Per-request metadata reads.** Every page load runs five queries + on cache miss: `gdsconfigformmaster` (with personalize/customslave + overlays for the matching slave rows), `gdsformconst`, + `sysjurisdiction` (per-user grants — the map key is named + `gdsjurisdiction` but the actual table read is `sysjurisdiction`; + skipped for ADMIN), `sysbillnosettings`, `sysreport`. The runtime + caches aggressively, but those reads are unavoidable on cache miss. 2. **A schema that won't stop growing.** New module = a row in `gdsmodule` plus 1-50 rows in `gdsconfigformslave` plus a backing @@ -66,7 +69,7 @@ customization to maintain. ## What this means for reading the wiki Every slice in this wiki documents one *application of the thesis*. Slice 1 -is the four-table read on a CRUD module. Slice 2 is multi-tenant scoping +is the metadata read on a CRUD module — the canonical instance. Slice 2 is multi-tenant scoping through every layer. Slice 3 is the read-only / view-backed variant. Slice 4 is the customization overlay. Slice 5 is the escape hatch when the overlay isn't enough. Together they cover the data-driven design from diff --git a/en/docs/reference/builder/define-form.md b/en/docs/reference/builder/define-form.md index 781f501..4e19598 100644 --- a/en/docs/reference/builder/define-form.md +++ b/en/docs/reference/builder/define-form.md @@ -106,9 +106,14 @@ Click the new sidebar item — the form should render. If it doesn't: ## Cache invalidation -After inserting, the runtime's cache holds the *previous* (empty) state -until a JMS message fires. xly's cache-invalidation listener -(`ConsumerChangeGdsModuleThread`) handles this when the BACK builder -saves changes; if you're inserting via raw SQL, you may need to bounce -the running services or wait for the TTL to expire. See -[Cache invalidation on metadata change](../maintainer/cache-invalidation.md). +After inserting, the runtime's cache holds the *previous* (empty) +state until something clears it. When BACK saves the change, the save +service synchronously calls `BusinessCleanRedisData.delCleanRedisData*`, +which fires `@CacheEvict` on the relevant cache regions in +`CleanRedisServiceImpl`. If you're inserting via raw SQL, **no eviction +runs** — you'll need to either invoke `BusinessCleanRedisDataImpl` +methods directly from inside the application, bounce the running +services, or wait for the TTL to expire. (The JMS path with the +similarly-named `ConsumerChangeGdsModuleThread` does base-data merging +via stored proc, NOT cache invalidation despite the name — see +[Cache invalidation on metadata change](../maintainer/cache-invalidation.md).) diff --git a/en/docs/slices/01-hello-world.md b/en/docs/slices/01-hello-world.md index fd4a6cc..2200523 100644 --- a/en/docs/slices/01-hello-world.md +++ b/en/docs/slices/01-hello-world.md @@ -256,19 +256,31 @@ End of trace. **Maintainer track:** - [The runtime: BusinessBaseController & friends](../reference/maintainer/runtime.md) — the controller and service layer that did the four-table read. -- [Cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md) — JMS-driven Redis flush. +- [Cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md) — synchronous `@CacheEvict` (the JMS path serves a different purpose). - [Multi-service deployment](../reference/maintainer/deployment.md) — `xlyEntry` vs `xlyApi` vs `xlyInterface`; this slice runs entirely on `xlyEntry`. ## Open verification items -Two items still open; one is now closed. - -1. **A live captured save.** The endpoint, handler, and payload shape are +Read flow live-corroborated; save flow still pending. + +1. ~~**A live captured read.**~~ **CLOSED** — clicking 系统常量配置 in BACK + (`http://118.178.19.35:8597`, admin/123, edition `基础版/8s`) produced + exactly the documented HTTP exchange: + ``` + GET /xlyEntry/business/getModelBysId/13?sModelsId=13 → 200 OK + POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715574676360040?sModelsId=13 → 200 OK + ``` + Both URLs match the wiki verbatim, including the redundant `?sModelsId=13` + query param alongside the path variable. The post-login URL stays at + `/xtmkpz` rather than navigating to `/xtclpz` — confirming that URL + fragments are display state, not route drivers (the SPA loads the + form on demand from the sidebar click). +2. **A live captured save.** The endpoint, handler, and payload shape are confirmed from source; the *body of an actual save request* hasn't been captured. This requires writing to the dev DB and we deferred it. Closing this means: open the module, click 新增, fill the form, click 保存, capture the JSON body and the response. -2. **The exact SQL emitted by save/delete**, captured from `syslog4j` or a +3. **The exact SQL emitted by save/delete**, captured from `syslog4j` or a MyBatis debug log, to close the loop end-to-end. 3. ~~**`sTable` validation in `addUpdateDelBusinessData`.**~~ **CLOSED** — the runtime does **not** cross-check the frontend-supplied `sTable` diff --git a/en/docs/slices/02-multi-tenancy.md b/en/docs/slices/02-multi-tenancy.md index e5839f7..1941c23 100644 --- a/en/docs/slices/02-multi-tenancy.md +++ b/en/docs/slices/02-multi-tenancy.md @@ -26,12 +26,17 @@ applied at module-list load time. Different mechanisms, different layers. ### How wide -Essentially every base table and every view in the schema carries both -`sBrandsId` and `sSubsidiaryId` — both business-data tables and -framework-metadata tables. The convention is followed almost without -exception; the few tables that lack one or both columns are either -single-tenant lookups (e.g., shared dictionary tables) or third-party -schemas the framework doesn't author. +Essentially every business-data table and view in the schema carries +both `sBrandsId` and `sSubsidiaryId`. **Most framework-metadata tables +also carry the columns**, but four of them — `gdsformconst`, +`gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave` — are an +explicit exception: `BusinessBaseServiceImpl.sTableNameList` (lines +162-169) lists them as "不需要公司子公司的表", and lines 1078-1084 strip +`sBrandsId`/`sSubsidiaryId` from the write payload for those tables. +In practice they hold a single sentinel tenant value shared across +all customers. The other tables that lack one or both columns are +single-tenant shared dictionaries or third-party schemas +(`act_*`, `qrtz_*`). ### How they're injected @@ -68,16 +73,23 @@ session via the `@CurrentUser` argument resolver. ### How it shows up in queries -The Slice-1 `getModelBysId` call ended up reading metadata via: +In the Slice-1 `getModelBysId` call, the per-tenant predicate ```sql WHERE sBrandsId = #{sBrandsId} AND sSubsidiaryId = #{sSubsidiaryId} ``` -…on every metadata table (`gdsmodule`, `gdsconfigformmaster`, -`gdsconfigformslave`, `gdsformconst`, `gdsjurisdiction`, `sysbillnosettings`). -The same predicate appears in essentially every business-data query in the -codebase. This is the multi-tenancy boundary at runtime. +is added on **per-tenant overlay reads** (`gdsconfigformpersonalize`, +`gdsconfigformcustomslave`) and on the **business-state** reads +(`sysbillnosettings`, `sysreport`, plus `sysjurisdiction` joined to +`sftlogininfojurisdictiongroup` for per-user grants — note the +returned map key is `gdsjurisdiction` even though the actual table +read is `sysjurisdiction`). It is *not* added on the framework-metadata +reads (`gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave`, +`gdsformconst`) — those are global and filtered by form-id only. The +same per-tenant predicate appears in essentially every business-data +query in the codebase. This is the multi-tenancy boundary at runtime +for tenant-owned state; framework metadata is intentionally global. ### Failure mode @@ -101,8 +113,8 @@ extras. ### Where editions are defined -Editions live in the `sisversionflow` table — one row per edition. The -key columns: +Editions are defined in the `sisversionflow` lookup table — one row +per edition. The key columns: | Column | Meaning | |---|---| @@ -111,6 +123,14 @@ key columns: | `sFlowName` | Display name (e.g., `基础版`) | | `bEbcErpPremium`, `bEbcMes`, `bEbcMesStandard`, `bSass` | Flags marking which product variants this edition belongs to | +> **State of this dev DB:** `sisversionflow` currently defines only +> one row — `8S_001 / 基础版`. The other edition codes shown in +> `gdsmodule.sVersionFlowCode` (`EBC-SD-002`, `EBC-RD-007`, +> `EBC-MDM-002`, etc.) are referenced as tags on module rows but have +> **no matching row in the `sisversionflow` lookup table** here. A +> production tenant of the SaaS likely populates the lookup table with +> the full edition catalog; the dev DB doesn't. + ### How modules are filtered per edition `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 `gdsmodule.sVersionFlowId`. From there, every loaded module reads its data with `sBrandsId`/`sSubsidiaryId` scoping as normal. -Within `gdsmodule`, three tagging patterns coexist: +Within `gdsmodule` (1358 rows in the dev DB), three tagging patterns coexist: -- **Untagged rows** (`sVersionFlowId` empty) — framework-internal modules +- **Untagged rows** (`sVersionFlowCode` empty) — 1002 rows. Framework-internal modules and screens the edition gate does not apply to. The bulk of the catalog. -- **Essentials-tagged rows** (`sVersionFlowCode = '8S_001'`) — the +- **Essentials-tagged rows** (`sVersionFlowCode = '8S_001'`) — 322 rows. The universally-licensed core every edition gets. -- **Edition-specific rows** (`EBC-SD-*`, `EBC-MDM-*`, `EBC-RD-*`, - `EBC-COM-*`, `EBC_*`) — add-ons gated by the customer's licence. +- **Edition-specific rows** — `EBC-SD-002` (15), `EBC-RD-007` (6), + `EBC-MDM-002` (5), `EBC_001` (4), `EBC-SD-003` (2), `EBC-SD-001` (1), + `EBC-COM-001` (1) — add-ons gated by the customer's licence. ## Concepts this slice introduces diff --git a/en/docs/slices/04-custom-field.md b/en/docs/slices/04-custom-field.md index 9f6ced2..9d510cd 100644 --- a/en/docs/slices/04-custom-field.md +++ b/en/docs/slices/04-custom-field.md @@ -143,12 +143,14 @@ forms never use. 1. **Find the live BACK page that edits `gdsconfigformcustomslave`.** Most likely `界面显示内容配置` from the sidebar; confirm by clicking in BACK and checking which table the save endpoint writes to. -2. **Trace the merge code.** Locate the exact join logic — is the merge - done in MyBatis (the two views) or in Java - (`BusinessGdsconfigformsServiceImpl.getFormSlaveData` + - `getFormCustomSlaveData`, called in - `BusinessBaseServiceImpl.java:246-248`)? Both look plausible from - the source we've seen. +2. ~~**Trace the merge code.**~~ **CLOSED** — the merge happens in + Java at `BusinessBaseServiceImpl.java:246-248`: it calls + `businessGdsconfigformsService.getFormSlaveData(map)` then + `getFormCustomSlaveData(map)` and `addAll`s them into one + `slaveList`. The two views (`gdsconfigformslavemasterview`, + `gdsconfigformcustomslavemasterview`) supply the joined-with-master + shape that each call reads — the *merge* is Java; the + *master-with-slave join* is SQL. 3. **`bVisible = false` semantics.** Does setting `bVisible = false` on a `gdsconfigformcustomslave` row *hide* an existing base field (an override-to-remove pattern), or only suppress the override diff --git a/en/docs/slices/06-hardware.md b/en/docs/slices/06-hardware.md index 12e10d6..aeda3b9 100644 --- a/en/docs/slices/06-hardware.md +++ b/en/docs/slices/06-hardware.md @@ -20,7 +20,7 @@ generic framework picks up. |---|---| | **Module** | `xlyPlc` (a sibling Spring Boot service in the codebase) | | **Direction** | One-way: PLC → ERP DB (no commands sent back to the press) | -| **Cadence** | Polled on a schedule (Quartz `PlcScheduledTasks`) | +| **Cadence** | Polled on a schedule (Spring `@Scheduled` cron in `PlcScheduledTasks`, e.g. `0/30 * * * * ?` and `0/1 * * * * ?`) | | **Per-press differentiation** | Spring profile (`-S10`, `-T0`, `-T1`, `-15S`, `-CT`, `-yt`, `-pro`) | | **Database tables it writes to** | `mftProduceReportMachineState`, and related machine-state slaves | @@ -31,7 +31,7 @@ generic framework picks up. | File | Role | |---|---| | `PlcApplicationBoot.java` | Spring Boot entry point | -| `web/scheduler/PlcScheduledTasks.java` | Quartz-driven poll loop | +| `web/scheduler/PlcScheduledTasks.java` | `@Component` with two `@Scheduled` cron methods (every 30 s and every 1 s) driving the poll loop | | `web/scheduler/PlcRunStatus.java` | In-memory state for the current poll cycle | | `web/scheduler/service/PlcToErpService.java` | Interface | | `web/scheduler/service/impl/PlcToErpServiceImpl.java` | The implementation: read PLC, write DB | -- libgit2 0.22.2