diff --git a/en/docs/concepts/customization-layers.md b/en/docs/concepts/customization-layers.md
index 5f06b38..a8d37ac 100644
--- a/en/docs/concepts/customization-layers.md
+++ b/en/docs/concepts/customization-layers.md
@@ -12,22 +12,35 @@ overview.
## The layers
-```
-gdsconfigformmaster ← system default (the form)
- ↓ overlaid by
-gdsconfigformpersonalize ← per-tenant whole-form override
- (replaces sSqlStr/sWhere/sOrder)
- ↓ then base slaves
-gdsconfigformslave ← system default fields
- ↓ overlaid / extended by
-gdsconfigformcustomslave ← per-tenant fields (add, hide, override)
- ↓ optionally further tweaked by
-gdsconfigformuserslave ← per-user view preferences
- (column order, hidden columns)
+```mermaid
+flowchart TB
+ classDef sys fill:#e8f0fe,stroke:#4285f4
+ classDef tenant fill:#fef7e0,stroke:#fbbc04
+ classDef user fill:#f3e8fd,stroke:#a142f4
+
+ M["gdsconfigformmaster
system default — the form
(sSqlStr · sWhere · sOrder)"]:::sys
+ P["gdsconfigformpersonalize
per-tenant whole-form override
(replaces sSqlStr / sWhere / sOrder)"]:::tenant
+ S["gdsconfigformslave
system default fields"]:::sys
+ CS["gdsconfigformcustomslave
per-tenant fields
(add · hide · override by sName)"]:::tenant
+ US["gdsconfigformuserslave
per-user view tweaks
(column order · hidden columns)"]:::user
+
+ OUT["Merged form
delivered to SPA"]
+
+ M --> P
+ P --> S
+ S --> CS
+ CS --> US
+ US --> OUT
+
+ M -. "always loaded" .-> OUT
+ P -. "loaded if tenant has overlay" .-> OUT
+ CS -. "loaded if tenant has overlay" .-> OUT
+ US -. "loaded if user has prefs" .-> OUT
```
-Each layer is keyed by `sParentId` linking up to the layer above. None of
-the links are FK-enforced — see [no-FK reality](semantic-fk.md).
+Read the chain top-to-bottom: **system → tenant → user**. Each layer
+is keyed by `sParentId` linking up to the layer above. None of the
+links are FK-enforced — see [no-FK reality](semantic-fk.md).
## What each layer answers
diff --git a/en/docs/concepts/index.md b/en/docs/concepts/index.md
index 38bcf8b..95a93a9 100644
--- a/en/docs/concepts/index.md
+++ b/en/docs/concepts/index.md
@@ -45,10 +45,13 @@ flowchart TB
XFLOW --> DB
XPLC --> DB
- XENTRY <--> REDIS
- XENTRY -- "metadata change" --> AMQ
+ XENTRY -- "@CacheEvict on save
(synchronous)" --> REDIS
+ XENTRY <-- "cache reads
+ Shiro session" --> REDIS
+ XAPI <--> REDIS
+
+ XENTRY -- "domain events
(NOT cache invalidation)" --> AMQ
AMQ --> XEJMSC
- XEJMSC --> REDIS
+ XEJMSC -- "PRO_ERPMERGEBASE*
base-data merge" --> DB
XENTRY -. uses .-> XMSG
XIF -. uses .-> XMSG
@@ -60,6 +63,14 @@ The dashed cluster (`xlyPlat*` + MongoDB) is the B2B printing-platform
tier — present in the build, but [out of scope](../index.md#whats-out-of-scope)
for this wiki.
+Note the two distinct paths between the runtime and Redis/ActiveMQ:
+**`@CacheEvict` is synchronous in the saving process and clears the
+shared Redis store directly** (cross-node coherence works through the
+shared store). **The JMS path is a separate base-data merge channel**,
+not cache invalidation — `ConsumerChangeGdsModuleThread` runs
+`PRO_ERPMERGEBASEGDSMODULE` and similar procs. Both are documented in
+[cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md).
+
For the library inventory behind each box, see the
[Tech stack](../reference/maintainer/tech-stack.md) page.
diff --git a/en/docs/concepts/request-lifecycle.md b/en/docs/concepts/request-lifecycle.md
index bc91cc8..d9c5d9e 100644
--- a/en/docs/concepts/request-lifecycle.md
+++ b/en/docs/concepts/request-lifecycle.md
@@ -87,6 +87,67 @@ variations on a theme.
User sees the grid
```
+## The same flow as a sequence
+
+The ASCII above shows the order of operations; the sequence diagram
+below shows *who calls whom*, which is what matters when you're
+tracing a real request through the runtime.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant SPA as Browser SPA
+ participant CTRL as BusinessBaseController
+ participant SVC as BusinessBaseServiceImpl
+ participant FORMS as BusinessGdsconfigformsServiceImpl
+ participant DB as MySQL
+ participant REDIS as Redis (RedisCacheManager)
+
+ SPA->>CTRL: GET /business/getModelBysId/{sModelsId}
?sModelsId=...&Authorization=
+ Note over CTRL: AuthorizationInterceptor.preHandle
resolves UserInfo from Redis
RequestAddParamUtil.addParams (16 keys)
+
+ CTRL->>SVC: getModelBysId(map)
+ SVC->>FORMS: getModelConfigByModleId
(form-master + slaves + overlays)
+ REDIS-->>FORMS: cache hit?
+ FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize ⋈ slave ⋈ customslave
+ DB-->>FORMS: rows
+ FORMS-->>SVC: formData
+
+ SVC->>FORMS: getFormconstData (form-id only, NOT tenant-scoped)
+ FORMS->>DB: SELECT ... gdsformconst WHERE sParentId=...
+ DB-->>FORMS: rows
+ FORMS-->>SVC: gdsformconst
+
+ alt sUserType != ADMIN
+ SVC->>FORMS: getJurisdictionData (per-user grants)
+ FORMS->>DB: SELECT ... sysjurisdiction ⋈ sftlogininfojurisdictiongroup
+ DB-->>FORMS: rows
+ FORMS-->>SVC: gdsjurisdiction (map-key; source table is sysjurisdiction)
+ else ADMIN
+ Note over SVC: skip jurisdiction load
+ end
+
+ SVC->>FORMS: getBillnosettingData
+ FORMS->>DB: SELECT ... sysbillnosettings WHERE sFormId=... AND tenant
+ DB-->>FORMS: row
+ FORMS-->>SVC: billnosetting
+
+ SVC->>DB: SELECT ... sysreport WHERE sFormId=... AND tenant
+ DB-->>SVC: report rows
+
+ SVC-->>CTRL: composite Map (5 keys)
+ CTRL-->>SPA: AjaxResult{code:1, dataset:{...}}
+
+ SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/{formId}
?sModelsId=...
+ Note over CTRL,SVC: same RequestAddParamUtil pass
then per-form sSqlStr / sWhere / sOrder
+ CTRL->>DB: parameterised SELECT against the form's backing table/view/proc
+ DB-->>CTRL: rows
+ CTRL-->>SPA: dataset
+```
+
+The two HTTP round-trips are visible at lines 1 and 22 in the
+diagram. Everything between is server-side work the SPA never sees.
+
## The five-key composite
`getModelBysId` returns one Java `Map` with these keys, in this order:
diff --git a/en/docs/reference/maintainer/cache-invalidation.md b/en/docs/reference/maintainer/cache-invalidation.md
index bde1d5d..91926c7 100644
--- a/en/docs/reference/maintainer/cache-invalidation.md
+++ b/en/docs/reference/maintainer/cache-invalidation.md
@@ -8,6 +8,38 @@ A separate JMS path with similarly-named classes exists for a
different purpose (base-data merge); the two are easy to confuse and
this page calls them out explicitly.
+## Two paths, one trigger — what's actually different
+
+```mermaid
+flowchart TB
+ classDef ok fill:#e6f4ea,stroke:#34a853
+ classDef notcache fill:#fce8e6,stroke:#ea4335
+
+ PM[PM clicks 保存 in BACK]:::ok
+ SAVE["BusinessBaseServiceImpl
add/update/deleteBusinessData"]
+ EVICT["BusinessCleanRedisData.delCleanRedisData
→ CleanRedisServiceImpl
17 @CacheEvict methods"]:::ok
+ REDIS[("Redis
(shared across nodes)")]:::ok
+ DB[("MySQL
row written")]:::ok
+
+ PM --> SAVE
+ SAVE --> DB
+ SAVE -- "synchronous,
same transaction" --> EVICT
+ EVICT --> REDIS
+ REDIS -. "next read on
any node sees fresh" .-> ANY[Other nodes]:::ok
+
+ SAVE -. "publishes 'gds module changed'" .-> AMQ([ActiveMQ])
+ AMQ --> CGM["ConsumerChangeGdsModuleThread
(xlyErpJmsConsumer)"]:::notcache
+ CGM -- "calls
PRO_ERPMERGEBASEGDSMODULE" --> DB2[("MySQL
base-data merge
NOT cache")]:::notcache
+
+ classDef title font-weight:bold
+```
+
+The **green path** is what every metadata-change-then-page-reload flow
+actually rides. The **red path** is what readers expect to be cache
+invalidation because of the queue's name (`CHANGE_GDS_MODULE`) and
+consumer-thread class — but it isn't. It does a per-tenant→base-data
+merge via stored procedure. **Neither path depends on the other.**
+
## The actual cache-invalidation path (synchronous, in-process)
```
diff --git a/en/docs/reference/maintainer/deployment.md b/en/docs/reference/maintainer/deployment.md
index 3261153..d5f3a34 100644
--- a/en/docs/reference/maintainer/deployment.md
+++ b/en/docs/reference/maintainer/deployment.md
@@ -4,6 +4,87 @@ xly is not a single Spring Boot WAR. The repository contains several
deployable modules plus a few library-like WAR modules that are also used
as dependencies by `xlyEntry`.
+## Topology at a glance
+
+```mermaid
+flowchart LR
+ classDef library fill:#f5f5f5,stroke:#999,stroke-dasharray:3 3
+ classDef boot fill:#e8f0fe,stroke:#4285f4
+
+ subgraph clients [Operator-facing]
+ BACK["BACK SPA
http://<host>:8597"]
+ FROUNT["FROUNT SPA
http://<host>:8598"]
+ end
+ EXT([External integrators])
+ HOOKS([Third-party webhooks])
+
+ subgraph boots [Deployable Spring Boot apps]
+ direction TB
+ XENTRY["xlyEntry
:8080 /xlyEntry
EntryApplicationBoot"]:::boot
+ XAPI["xlyApi
:8090 (local) / :8080
/xlyApi · ApiApplicationBoot"]:::boot
+ XIF["xlyInterface
:8080 /xlyInterface
InterfaceApplicationBoot"]:::boot
+ XPLC["xlyPlc
:8000 (dev) / :8080
/xlyEntry · PlcApplicationBoot"]:::boot
+ XFACE["xlyFace
:8091 (local) / :8080
/xlyFace (out of doc scope)"]:::boot
+ XEJMSC["xlyErpJmsConsumer
(no port; inherits)
JmsConsumerApplicationBoot"]:::boot
+ end
+
+ subgraph libs [Library modules — not standalone runnable]
+ direction TB
+ XMANAGE[xlyManage]:::library
+ XBSERVICE[xlyBusinessService]:::library
+ XPERSIST[xlyPersist]:::library
+ XENTITY[xlyEntity]:::library
+ XFLOW[xlyFlow]:::library
+ XMSG[xlyMsg]:::library
+ XEJMSP[xlyErpJmsProductor]:::library
+ XPLATC[xlyPlatConstant]:::library
+ end
+
+ subgraph infra [Shared infrastructure]
+ DB[("MySQL
xlyweberp_*")]
+ REDIS[(Redis :16379
shared cache + session)]
+ AMQ([ActiveMQ :61616])
+ MONGO[("MongoDB
(wired but unused)")]
+ end
+
+ BACK -->|nginx| XENTRY
+ FROUNT -->|nginx| XENTRY
+ FROUNT -->|nginx| XAPI
+ EXT --> XAPI
+ HOOKS --> XIF
+
+ XENTRY --- XMANAGE
+ XENTRY --- XBSERVICE
+ XENTRY --- XFLOW
+ XBSERVICE --- XPERSIST
+ XAPI --- XPERSIST
+ XIF --- XPERSIST
+ XPERSIST --- XPLATC
+ XPERSIST --- XENTITY
+ XBSERVICE --- XMSG
+ XIF --- XMSG
+ XBSERVICE --- XEJMSP
+
+ XENTRY --> DB
+ XAPI --> DB
+ XIF --> DB
+ XPLC --> DB
+ XFLOW --> DB
+
+ XENTRY --> REDIS
+ XAPI --> REDIS
+ XEJMSC --> AMQ
+ XEJMSP -. publishes .-> AMQ
+ XENTRY -. publishes .-> AMQ
+```
+
+The reverse-proxy maps the operator-facing ports (8597 / 8598) onto the
+internal Spring Boot apps (mostly :8080 with distinct context paths).
+The library modules don't run on their own — they're packaged into the
+deployable WARs as dependencies. `xlyFlow` and `xlyPlc` share xlyEntry's
+context-path `/xlyEntry`, so a real deployment routes by host or
+upstream rather than by path.
+
## The main modules
### Deployable Spring Boot applications
diff --git a/en/docs/slices/01-hello-world.md b/en/docs/slices/01-hello-world.md
index 41845a1..c3e1619 100644
--- a/en/docs/slices/01-hello-world.md
+++ b/en/docs/slices/01-hello-world.md
@@ -238,6 +238,50 @@ The save returns success; the front-end either patches the row in place or
re-pulls the grid via the same `getBusinessDataByFormcustomId` endpoint.
End of trace.
+## The save flow as a sequence
+
+The full live-verified save round-trip, including the optimistic-lock
+preamble and the synchronous cache-bust that follows the DB write:
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant SPA as Browser SPA
+ participant CTRL as BusinessBaseController
+ participant SVC as BusinessBaseServiceImpl
+ participant CLEAN as BusinessCleanRedisData
+ participant DB as MySQL (xlyweberp_*)
+ participant REDIS as Redis (RedisCacheManager)
+
+ Note over SPA: User clicks 修改 on the sReopen row,
edits sChinese, clicks 保存
+ SPA->>CTRL: POST /business/addSysLocking?sModelsId=13
(optimistic-lock claim)
+ CTRL-->>SPA: 200 OK
+ SPA->>CTRL: POST /business/addUpdateDelBusinessData?sModelsId=13
{addData:[],updateData:[{sTable:"gdsformconst",column:{sId,sChinese,...}}],delData:[]}
Authorization:
+ Note over CTRL: AuthorizationInterceptor → UserInfo from Redis
RequestAddParamUtil.addParams (16 keys incl. sBrandsId/sSubsidiaryId)
+ CTRL->>SVC: addUpdateDelBusinessData(param)
+ Note over SVC: per-row dispatch:
add → addBusinessData → businessBaseDao.add
update → updateBusinessData → businessBaseDao.update
del → deleteBusinessData → businessBaseDao.del
(sTable from frontend; NO whitelist check)
+ SVC->>DB: INSERT/UPDATE/DELETE on the named sTable
+ DB-->>SVC: rows affected
+ Note over SVC: if sTable in sTableNameList
(gdsformconst/gdsmodule/gdsconfigformmaster/
gdsconfigformslave) → strip sBrandsId/sSubsidiaryId
before write (4-table tenant-bypass)
+ SVC->>CLEAN: delCleanRedisData(sTable, sIds, sBrandsId, sSubsidiaryId, "update")
+ CLEAN->>REDIS: @CacheEvict over the affected cache regions
(synchronous, same transaction)
+ REDIS-->>CLEAN: evicted
+ SVC-->>CTRL: Feedback{code:1,msg:"操作成功"}
+ CTRL-->>SPA: AjaxResult{code:1,...}
+ SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/...
(re-pull the grid; cache miss → fresh DB read)
+ CTRL->>DB: SELECT ...
+ DB-->>CTRL: rows
+ CTRL-->>SPA: dataset
+```
+
+End-to-end live trace confirms: the SPA fires `addSysLocking` on
+edit-mode entry, then `addUpdateDelBusinessData` on save, then a
+follow-up grid re-fetch — three round-trips per save. The
+`@CacheEvict` runs synchronously in the same transaction as the DB
+write, so any node hitting Redis next sees the evicted region (see
+[cache invalidation](../reference/maintainer/cache-invalidation.md)
+for why this is Redis-backed and cross-node-coherent).
+
## Concepts this slice introduces
- [The data-driven thesis](../concepts/thesis.md) — why xly stores layouts as data.