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 | 12 | |
| 13 | 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 | 45 | ## What each layer answers |
| 33 | 46 | ... | ... |
en/docs/concepts/index.md
| ... | ... | @@ -45,10 +45,13 @@ flowchart TB |
| 45 | 45 | XFLOW --> DB |
| 46 | 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 | 53 | AMQ --> XEJMSC |
| 51 | - XEJMSC --> REDIS | |
| 54 | + XEJMSC -- "PRO_ERPMERGEBASE*<br/>base-data merge" --> DB | |
| 52 | 55 | |
| 53 | 56 | XENTRY -. uses .-> XMSG |
| 54 | 57 | XIF -. uses .-> XMSG |
| ... | ... | @@ -60,6 +63,14 @@ The dashed cluster (`xlyPlat*` + MongoDB) is the B2B printing-platform |
| 60 | 63 | tier — present in the build, but [out of scope](../index.md#whats-out-of-scope) |
| 61 | 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 | 74 | For the library inventory behind each box, see the |
| 64 | 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 | 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 | 151 | ## The five-key composite |
| 91 | 152 | |
| 92 | 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 | 8 | different purpose (base-data merge); the two are easy to confuse and |
| 9 | 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 | 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 | 4 | deployable modules plus a few library-like WAR modules that are also used |
| 5 | 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 | 88 | ## The main modules |
| 8 | 89 | |
| 9 | 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 | 238 | re-pulls the grid via the same `getBusinessDataByFormcustomId` endpoint. |
| 239 | 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 | 285 | ## Concepts this slice introduces |
| 242 | 286 | |
| 243 | 287 | - [The data-driven thesis](../concepts/thesis.md) — why xly stores layouts as data. | ... | ... |