Commit 06ae2ff955c77176be59135c6370c6185ff0e61d
1 parent
054efbef
docs: add five architecture diagrams (mermaid)
Five mermaid diagrams across the en wiki, plus fix one stale arrow in the existing concepts/index diagram. New: - reference/maintainer/deployment.md — full topology at a glance: 6 deployable Spring Boot apps with their default-profile ports + context paths + Boot main classes, the 8 library modules and what they're consumed by, the operator-facing SPAs (BACK :8597 / FROUNT :8598) traced through nginx into xlyEntry/xlyApi, and the shared infra cluster (MySQL / Redis :16379 / ActiveMQ :61616 / MongoDB wired-but-unused). Surfaces the "xlyFlow + xlyPlc share /xlyEntry context-path" subtlety and the "xlyErpJmsConsumer has no port, inherits" oddity in one picture. - reference/maintainer/cache-invalidation.md — dual-path diagram showing the synchronous @CacheEvict path (green) next to the JMS PRO_ERPMERGEBASEGDSMODULE base-data merge (red, labelled "NOT cache"). Anchors the page's main correction visually so a reader can't confuse the two systems again. - concepts/request-lifecycle.md — sequence diagram covering the full /getModelBysId + /getBusinessDataByFormcustomId round-trips, with the AuthorizationInterceptor + RequestAddParamUtil preamble, the ADMIN-skips-jurisdiction branch, and which service makes which DB call. Supplements (doesn't replace) the ASCII layout diagram above it. - concepts/customization-layers.md — colour-coded vertical stack (system → tenant → user) showing the five overlay tables (gdsconfigformmaster / personalize / slave / customslave / userslave) flowing into the merged form delivered to the SPA. - slices/01-hello-world.md — full save-flow sequence: addSysLocking optimistic-lock → addUpdateDelBusinessData → BusinessBaseServiceImpl per-row dispatch → DB write (with sTableNameList tenant-bypass callout) → BusinessCleanRedisData synchronous @CacheEvict → grid re-fetch. Built from the live capture in commit 054efbef. Fixed: - concepts/index.md — the existing "metadata change → AMQ → xlyErpJmsConsumer → REDIS" arrow chain implied JMS busts the cache, which the cache-invalidation pass already corrected in prose. Diagram updated to show the two distinct paths: synchronous @CacheEvict directly to Redis from xlyEntry, plus the separate domain-events JMS path that runs PRO_ERPMERGEBASE* against MySQL (NOT cache invalidation). Caption added pointing readers at cache-invalidation.md. All 6 diagrams render as <pre class="mermaid"> via Material for MkDocs's mermaid integration; mkdocs --strict green.
Showing
6 changed files
with
259 additions
and
17 deletions
en/docs/concepts/customization-layers.md
| @@ -12,22 +12,35 @@ overview. | @@ -12,22 +12,35 @@ overview. | ||
| 12 | 12 | ||
| 13 | ## The layers | 13 | ## The layers |
| 14 | 14 | ||
| 15 | -``` | ||
| 16 | -gdsconfigformmaster ← system default (the form) | ||
| 17 | - ↓ overlaid by | ||
| 18 | -gdsconfigformpersonalize ← per-tenant whole-form override | ||
| 19 | - (replaces sSqlStr/sWhere/sOrder) | ||
| 20 | - ↓ then base slaves | ||
| 21 | -gdsconfigformslave ← system default fields | ||
| 22 | - ↓ overlaid / extended by | ||
| 23 | -gdsconfigformcustomslave ← per-tenant fields (add, hide, override) | ||
| 24 | - ↓ optionally further tweaked by | ||
| 25 | -gdsconfigformuserslave ← per-user view preferences | ||
| 26 | - (column order, hidden columns) | 15 | +```mermaid |
| 16 | +flowchart TB | ||
| 17 | + classDef sys fill:#e8f0fe,stroke:#4285f4 | ||
| 18 | + classDef tenant fill:#fef7e0,stroke:#fbbc04 | ||
| 19 | + classDef user fill:#f3e8fd,stroke:#a142f4 | ||
| 20 | + | ||
| 21 | + M["gdsconfigformmaster<br/>system default — the form<br/>(sSqlStr · sWhere · sOrder)"]:::sys | ||
| 22 | + P["gdsconfigformpersonalize<br/>per-tenant whole-form override<br/>(replaces sSqlStr / sWhere / sOrder)"]:::tenant | ||
| 23 | + S["gdsconfigformslave<br/>system default fields"]:::sys | ||
| 24 | + CS["gdsconfigformcustomslave<br/>per-tenant fields<br/>(add · hide · override by sName)"]:::tenant | ||
| 25 | + US["gdsconfigformuserslave<br/>per-user view tweaks<br/>(column order · hidden columns)"]:::user | ||
| 26 | + | ||
| 27 | + OUT["Merged form<br/>delivered to SPA"] | ||
| 28 | + | ||
| 29 | + M --> P | ||
| 30 | + P --> S | ||
| 31 | + S --> CS | ||
| 32 | + CS --> US | ||
| 33 | + US --> OUT | ||
| 34 | + | ||
| 35 | + M -. "always loaded" .-> OUT | ||
| 36 | + P -. "loaded if tenant has overlay" .-> OUT | ||
| 37 | + CS -. "loaded if tenant has overlay" .-> OUT | ||
| 38 | + US -. "loaded if user has prefs" .-> OUT | ||
| 27 | ``` | 39 | ``` |
| 28 | 40 | ||
| 29 | -Each layer is keyed by `sParentId` linking up to the layer above. None of | ||
| 30 | -the links are FK-enforced — see [no-FK reality](semantic-fk.md). | 41 | +Read the chain top-to-bottom: **system → tenant → user**. Each layer |
| 42 | +is keyed by `sParentId` linking up to the layer above. None of the | ||
| 43 | +links are FK-enforced — see [no-FK reality](semantic-fk.md). | ||
| 31 | 44 | ||
| 32 | ## What each layer answers | 45 | ## What each layer answers |
| 33 | 46 |
en/docs/concepts/index.md
| @@ -45,10 +45,13 @@ flowchart TB | @@ -45,10 +45,13 @@ flowchart TB | ||
| 45 | XFLOW --> DB | 45 | XFLOW --> DB |
| 46 | XPLC --> DB | 46 | XPLC --> DB |
| 47 | 47 | ||
| 48 | - XENTRY <--> REDIS | ||
| 49 | - XENTRY -- "metadata change" --> AMQ | 48 | + XENTRY -- "@CacheEvict on save<br/>(synchronous)" --> REDIS |
| 49 | + XENTRY <-- "cache reads<br/>+ Shiro session" --> REDIS | ||
| 50 | + XAPI <--> REDIS | ||
| 51 | + | ||
| 52 | + XENTRY -- "domain events<br/>(NOT cache invalidation)" --> AMQ | ||
| 50 | AMQ --> XEJMSC | 53 | AMQ --> XEJMSC |
| 51 | - XEJMSC --> REDIS | 54 | + XEJMSC -- "PRO_ERPMERGEBASE*<br/>base-data merge" --> DB |
| 52 | 55 | ||
| 53 | XENTRY -. uses .-> XMSG | 56 | XENTRY -. uses .-> XMSG |
| 54 | XIF -. uses .-> XMSG | 57 | XIF -. uses .-> XMSG |
| @@ -60,6 +63,14 @@ The dashed cluster (`xlyPlat*` + MongoDB) is the B2B printing-platform | @@ -60,6 +63,14 @@ The dashed cluster (`xlyPlat*` + MongoDB) is the B2B printing-platform | ||
| 60 | tier — present in the build, but [out of scope](../index.md#whats-out-of-scope) | 63 | tier — present in the build, but [out of scope](../index.md#whats-out-of-scope) |
| 61 | for this wiki. | 64 | for this wiki. |
| 62 | 65 | ||
| 66 | +Note the two distinct paths between the runtime and Redis/ActiveMQ: | ||
| 67 | +**`@CacheEvict` is synchronous in the saving process and clears the | ||
| 68 | +shared Redis store directly** (cross-node coherence works through the | ||
| 69 | +shared store). **The JMS path is a separate base-data merge channel**, | ||
| 70 | +not cache invalidation — `ConsumerChangeGdsModuleThread` runs | ||
| 71 | +`PRO_ERPMERGEBASEGDSMODULE` and similar procs. Both are documented in | ||
| 72 | +[cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md). | ||
| 73 | + | ||
| 63 | For the library inventory behind each box, see the | 74 | For the library inventory behind each box, see the |
| 64 | [Tech stack](../reference/maintainer/tech-stack.md) page. | 75 | [Tech stack](../reference/maintainer/tech-stack.md) page. |
| 65 | 76 |
en/docs/concepts/request-lifecycle.md
| @@ -87,6 +87,67 @@ variations on a theme. | @@ -87,6 +87,67 @@ variations on a theme. | ||
| 87 | User sees the grid | 87 | User sees the grid |
| 88 | ``` | 88 | ``` |
| 89 | 89 | ||
| 90 | +## The same flow as a sequence | ||
| 91 | + | ||
| 92 | +The ASCII above shows the order of operations; the sequence diagram | ||
| 93 | +below shows *who calls whom*, which is what matters when you're | ||
| 94 | +tracing a real request through the runtime. | ||
| 95 | + | ||
| 96 | +```mermaid | ||
| 97 | +sequenceDiagram | ||
| 98 | + autonumber | ||
| 99 | + participant SPA as Browser SPA | ||
| 100 | + participant CTRL as BusinessBaseController | ||
| 101 | + participant SVC as BusinessBaseServiceImpl | ||
| 102 | + participant FORMS as BusinessGdsconfigformsServiceImpl | ||
| 103 | + participant DB as MySQL | ||
| 104 | + participant REDIS as Redis (RedisCacheManager) | ||
| 105 | + | ||
| 106 | + SPA->>CTRL: GET /business/getModelBysId/{sModelsId}<br/>?sModelsId=...&Authorization=<bearer> | ||
| 107 | + Note over CTRL: AuthorizationInterceptor.preHandle<br/>resolves UserInfo from Redis<br/>RequestAddParamUtil.addParams (16 keys) | ||
| 108 | + | ||
| 109 | + CTRL->>SVC: getModelBysId(map) | ||
| 110 | + SVC->>FORMS: getModelConfigByModleId<br/>(form-master + slaves + overlays) | ||
| 111 | + REDIS-->>FORMS: cache hit? | ||
| 112 | + FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize ⋈ slave ⋈ customslave | ||
| 113 | + DB-->>FORMS: rows | ||
| 114 | + FORMS-->>SVC: formData | ||
| 115 | + | ||
| 116 | + SVC->>FORMS: getFormconstData (form-id only, NOT tenant-scoped) | ||
| 117 | + FORMS->>DB: SELECT ... gdsformconst WHERE sParentId=... | ||
| 118 | + DB-->>FORMS: rows | ||
| 119 | + FORMS-->>SVC: gdsformconst | ||
| 120 | + | ||
| 121 | + alt sUserType != ADMIN | ||
| 122 | + SVC->>FORMS: getJurisdictionData (per-user grants) | ||
| 123 | + FORMS->>DB: SELECT ... sysjurisdiction ⋈ sftlogininfojurisdictiongroup | ||
| 124 | + DB-->>FORMS: rows | ||
| 125 | + FORMS-->>SVC: gdsjurisdiction (map-key; source table is sysjurisdiction) | ||
| 126 | + else ADMIN | ||
| 127 | + Note over SVC: skip jurisdiction load | ||
| 128 | + end | ||
| 129 | + | ||
| 130 | + SVC->>FORMS: getBillnosettingData | ||
| 131 | + FORMS->>DB: SELECT ... sysbillnosettings WHERE sFormId=... AND tenant | ||
| 132 | + DB-->>FORMS: row | ||
| 133 | + FORMS-->>SVC: billnosetting | ||
| 134 | + | ||
| 135 | + SVC->>DB: SELECT ... sysreport WHERE sFormId=... AND tenant | ||
| 136 | + DB-->>SVC: report rows | ||
| 137 | + | ||
| 138 | + SVC-->>CTRL: composite Map (5 keys) | ||
| 139 | + CTRL-->>SPA: AjaxResult{code:1, dataset:{...}} | ||
| 140 | + | ||
| 141 | + SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/{formId}<br/>?sModelsId=... | ||
| 142 | + Note over CTRL,SVC: same RequestAddParamUtil pass<br/>then per-form sSqlStr / sWhere / sOrder | ||
| 143 | + CTRL->>DB: parameterised SELECT against the form's backing table/view/proc | ||
| 144 | + DB-->>CTRL: rows | ||
| 145 | + CTRL-->>SPA: dataset | ||
| 146 | +``` | ||
| 147 | + | ||
| 148 | +The two HTTP round-trips are visible at lines 1 and 22 in the | ||
| 149 | +diagram. Everything between is server-side work the SPA never sees. | ||
| 150 | + | ||
| 90 | ## The five-key composite | 151 | ## The five-key composite |
| 91 | 152 | ||
| 92 | `getModelBysId` returns one Java `Map` with these keys, in this order: | 153 | `getModelBysId` returns one Java `Map` with these keys, in this order: |
en/docs/reference/maintainer/cache-invalidation.md
| @@ -8,6 +8,38 @@ A separate JMS path with similarly-named classes exists for a | @@ -8,6 +8,38 @@ A separate JMS path with similarly-named classes exists for a | ||
| 8 | different purpose (base-data merge); the two are easy to confuse and | 8 | different purpose (base-data merge); the two are easy to confuse and |
| 9 | this page calls them out explicitly. | 9 | this page calls them out explicitly. |
| 10 | 10 | ||
| 11 | +## Two paths, one trigger — what's actually different | ||
| 12 | + | ||
| 13 | +```mermaid | ||
| 14 | +flowchart TB | ||
| 15 | + classDef ok fill:#e6f4ea,stroke:#34a853 | ||
| 16 | + classDef notcache fill:#fce8e6,stroke:#ea4335 | ||
| 17 | + | ||
| 18 | + PM[PM clicks 保存 in BACK]:::ok | ||
| 19 | + SAVE["BusinessBaseServiceImpl<br/>add/update/deleteBusinessData"] | ||
| 20 | + EVICT["BusinessCleanRedisData.delCleanRedisData<br/>→ CleanRedisServiceImpl<br/>17 @CacheEvict methods"]:::ok | ||
| 21 | + REDIS[("Redis<br/>(shared across nodes)")]:::ok | ||
| 22 | + DB[("MySQL<br/>row written")]:::ok | ||
| 23 | + | ||
| 24 | + PM --> SAVE | ||
| 25 | + SAVE --> DB | ||
| 26 | + SAVE -- "synchronous,<br/>same transaction" --> EVICT | ||
| 27 | + EVICT --> REDIS | ||
| 28 | + REDIS -. "next read on<br/>any node sees fresh" .-> ANY[Other nodes]:::ok | ||
| 29 | + | ||
| 30 | + SAVE -. "publishes 'gds module changed'" .-> AMQ([ActiveMQ]) | ||
| 31 | + AMQ --> CGM["ConsumerChangeGdsModuleThread<br/>(xlyErpJmsConsumer)"]:::notcache | ||
| 32 | + CGM -- "calls<br/>PRO_ERPMERGEBASEGDSMODULE" --> DB2[("MySQL<br/>base-data merge<br/>NOT cache")]:::notcache | ||
| 33 | + | ||
| 34 | + classDef title font-weight:bold | ||
| 35 | +``` | ||
| 36 | + | ||
| 37 | +The **green path** is what every metadata-change-then-page-reload flow | ||
| 38 | +actually rides. The **red path** is what readers expect to be cache | ||
| 39 | +invalidation because of the queue's name (`CHANGE_GDS_MODULE`) and | ||
| 40 | +consumer-thread class — but it isn't. It does a per-tenant→base-data | ||
| 41 | +merge via stored procedure. **Neither path depends on the other.** | ||
| 42 | + | ||
| 11 | ## The actual cache-invalidation path (synchronous, in-process) | 43 | ## The actual cache-invalidation path (synchronous, in-process) |
| 12 | 44 | ||
| 13 | ``` | 45 | ``` |
en/docs/reference/maintainer/deployment.md
| @@ -4,6 +4,87 @@ xly is not a single Spring Boot WAR. The repository contains several | @@ -4,6 +4,87 @@ xly is not a single Spring Boot WAR. The repository contains several | ||
| 4 | deployable modules plus a few library-like WAR modules that are also used | 4 | deployable modules plus a few library-like WAR modules that are also used |
| 5 | as dependencies by `xlyEntry`. | 5 | as dependencies by `xlyEntry`. |
| 6 | 6 | ||
| 7 | +## Topology at a glance | ||
| 8 | + | ||
| 9 | +```mermaid | ||
| 10 | +flowchart LR | ||
| 11 | + classDef library fill:#f5f5f5,stroke:#999,stroke-dasharray:3 3 | ||
| 12 | + classDef boot fill:#e8f0fe,stroke:#4285f4 | ||
| 13 | + | ||
| 14 | + subgraph clients [Operator-facing] | ||
| 15 | + BACK["BACK SPA<br/>http://<host>:8597"] | ||
| 16 | + FROUNT["FROUNT SPA<br/>http://<host>:8598"] | ||
| 17 | + end | ||
| 18 | + EXT([External integrators]) | ||
| 19 | + HOOKS([Third-party webhooks]) | ||
| 20 | + | ||
| 21 | + subgraph boots [Deployable Spring Boot apps] | ||
| 22 | + direction TB | ||
| 23 | + XENTRY["xlyEntry<br/>:8080 /xlyEntry<br/>EntryApplicationBoot"]:::boot | ||
| 24 | + XAPI["xlyApi<br/>:8090 (local) / :8080<br/>/xlyApi · ApiApplicationBoot"]:::boot | ||
| 25 | + XIF["xlyInterface<br/>:8080 /xlyInterface<br/>InterfaceApplicationBoot"]:::boot | ||
| 26 | + XPLC["xlyPlc<br/>:8000 (dev) / :8080<br/>/xlyEntry · PlcApplicationBoot"]:::boot | ||
| 27 | + XFACE["xlyFace<br/>:8091 (local) / :8080<br/>/xlyFace (out of doc scope)"]:::boot | ||
| 28 | + XEJMSC["xlyErpJmsConsumer<br/>(no port; inherits)<br/>JmsConsumerApplicationBoot"]:::boot | ||
| 29 | + end | ||
| 30 | + | ||
| 31 | + subgraph libs [Library modules — not standalone runnable] | ||
| 32 | + direction TB | ||
| 33 | + XMANAGE[xlyManage]:::library | ||
| 34 | + XBSERVICE[xlyBusinessService]:::library | ||
| 35 | + XPERSIST[xlyPersist]:::library | ||
| 36 | + XENTITY[xlyEntity]:::library | ||
| 37 | + XFLOW[xlyFlow]:::library | ||
| 38 | + XMSG[xlyMsg]:::library | ||
| 39 | + XEJMSP[xlyErpJmsProductor]:::library | ||
| 40 | + XPLATC[xlyPlatConstant]:::library | ||
| 41 | + end | ||
| 42 | + | ||
| 43 | + subgraph infra [Shared infrastructure] | ||
| 44 | + DB[("MySQL<br/>xlyweberp_*")] | ||
| 45 | + REDIS[(Redis :16379<br/>shared cache + session)] | ||
| 46 | + AMQ([ActiveMQ :61616]) | ||
| 47 | + MONGO[("MongoDB<br/>(wired but unused)")] | ||
| 48 | + end | ||
| 49 | + | ||
| 50 | + BACK -->|nginx| XENTRY | ||
| 51 | + FROUNT -->|nginx| XENTRY | ||
| 52 | + FROUNT -->|nginx| XAPI | ||
| 53 | + EXT --> XAPI | ||
| 54 | + HOOKS --> XIF | ||
| 55 | + | ||
| 56 | + XENTRY --- XMANAGE | ||
| 57 | + XENTRY --- XBSERVICE | ||
| 58 | + XENTRY --- XFLOW | ||
| 59 | + XBSERVICE --- XPERSIST | ||
| 60 | + XAPI --- XPERSIST | ||
| 61 | + XIF --- XPERSIST | ||
| 62 | + XPERSIST --- XPLATC | ||
| 63 | + XPERSIST --- XENTITY | ||
| 64 | + XBSERVICE --- XMSG | ||
| 65 | + XIF --- XMSG | ||
| 66 | + XBSERVICE --- XEJMSP | ||
| 67 | + | ||
| 68 | + XENTRY --> DB | ||
| 69 | + XAPI --> DB | ||
| 70 | + XIF --> DB | ||
| 71 | + XPLC --> DB | ||
| 72 | + XFLOW --> DB | ||
| 73 | + | ||
| 74 | + XENTRY --> REDIS | ||
| 75 | + XAPI --> REDIS | ||
| 76 | + XEJMSC --> AMQ | ||
| 77 | + XEJMSP -. publishes .-> AMQ | ||
| 78 | + XENTRY -. publishes .-> AMQ | ||
| 79 | +``` | ||
| 80 | + | ||
| 81 | +The reverse-proxy maps the operator-facing ports (8597 / 8598) onto the | ||
| 82 | +internal Spring Boot apps (mostly :8080 with distinct context paths). | ||
| 83 | +The library modules don't run on their own — they're packaged into the | ||
| 84 | +deployable WARs as dependencies. `xlyFlow` and `xlyPlc` share xlyEntry's | ||
| 85 | +context-path `/xlyEntry`, so a real deployment routes by host or | ||
| 86 | +upstream rather than by path. | ||
| 87 | + | ||
| 7 | ## The main modules | 88 | ## The main modules |
| 8 | 89 | ||
| 9 | ### Deployable Spring Boot applications | 90 | ### Deployable Spring Boot applications |
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 | @@ -238,6 +238,50 @@ The save returns success; the front-end either patches the row in place or | ||
| 238 | re-pulls the grid via the same `getBusinessDataByFormcustomId` endpoint. | 238 | re-pulls the grid via the same `getBusinessDataByFormcustomId` endpoint. |
| 239 | End of trace. | 239 | End of trace. |
| 240 | 240 | ||
| 241 | +## The save flow as a sequence | ||
| 242 | + | ||
| 243 | +The full live-verified save round-trip, including the optimistic-lock | ||
| 244 | +preamble and the synchronous cache-bust that follows the DB write: | ||
| 245 | + | ||
| 246 | +```mermaid | ||
| 247 | +sequenceDiagram | ||
| 248 | + autonumber | ||
| 249 | + participant SPA as Browser SPA | ||
| 250 | + participant CTRL as BusinessBaseController | ||
| 251 | + participant SVC as BusinessBaseServiceImpl | ||
| 252 | + participant CLEAN as BusinessCleanRedisData | ||
| 253 | + participant DB as MySQL (xlyweberp_*) | ||
| 254 | + participant REDIS as Redis (RedisCacheManager) | ||
| 255 | + | ||
| 256 | + Note over SPA: User clicks 修改 on the sReopen row,<br/>edits sChinese, clicks 保存 | ||
| 257 | + SPA->>CTRL: POST /business/addSysLocking?sModelsId=13<br/>(optimistic-lock claim) | ||
| 258 | + CTRL-->>SPA: 200 OK | ||
| 259 | + SPA->>CTRL: POST /business/addUpdateDelBusinessData?sModelsId=13<br/>{addData:[],updateData:[{sTable:"gdsformconst",column:{sId,sChinese,...}}],delData:[]}<br/>Authorization: <bearer> | ||
| 260 | + Note over CTRL: AuthorizationInterceptor → UserInfo from Redis<br/>RequestAddParamUtil.addParams (16 keys incl. sBrandsId/sSubsidiaryId) | ||
| 261 | + CTRL->>SVC: addUpdateDelBusinessData(param) | ||
| 262 | + Note over SVC: per-row dispatch:<br/>add → addBusinessData → businessBaseDao.add<br/>update → updateBusinessData → businessBaseDao.update<br/>del → deleteBusinessData → businessBaseDao.del<br/>(sTable from frontend; NO whitelist check) | ||
| 263 | + SVC->>DB: INSERT/UPDATE/DELETE on the named sTable | ||
| 264 | + DB-->>SVC: rows affected | ||
| 265 | + Note over SVC: if sTable in sTableNameList<br/>(gdsformconst/gdsmodule/gdsconfigformmaster/<br/>gdsconfigformslave) → strip sBrandsId/sSubsidiaryId<br/>before write (4-table tenant-bypass) | ||
| 266 | + SVC->>CLEAN: delCleanRedisData(sTable, sIds, sBrandsId, sSubsidiaryId, "update") | ||
| 267 | + CLEAN->>REDIS: @CacheEvict over the affected cache regions<br/>(synchronous, same transaction) | ||
| 268 | + REDIS-->>CLEAN: evicted | ||
| 269 | + SVC-->>CTRL: Feedback{code:1,msg:"操作成功"} | ||
| 270 | + CTRL-->>SPA: AjaxResult{code:1,...} | ||
| 271 | + SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/...<br/>(re-pull the grid; cache miss → fresh DB read) | ||
| 272 | + CTRL->>DB: SELECT ... | ||
| 273 | + DB-->>CTRL: rows | ||
| 274 | + CTRL-->>SPA: dataset | ||
| 275 | +``` | ||
| 276 | + | ||
| 277 | +End-to-end live trace confirms: the SPA fires `addSysLocking` on | ||
| 278 | +edit-mode entry, then `addUpdateDelBusinessData` on save, then a | ||
| 279 | +follow-up grid re-fetch — three round-trips per save. The | ||
| 280 | +`@CacheEvict` runs synchronously in the same transaction as the DB | ||
| 281 | +write, so any node hitting Redis next sees the evicted region (see | ||
| 282 | +[cache invalidation](../reference/maintainer/cache-invalidation.md) | ||
| 283 | +for why this is Redis-backed and cross-node-coherent). | ||
| 284 | + | ||
| 241 | ## Concepts this slice introduces | 285 | ## Concepts this slice introduces |
| 242 | 286 | ||
| 243 | - [The data-driven thesis](../concepts/thesis.md) — why xly stores layouts as data. | 287 | - [The data-driven thesis](../concepts/thesis.md) — why xly stores layouts as data. |