Commit 06ae2ff955c77176be59135c6370c6185ff0e61d

Authored by zichun
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.
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://&lt;host&gt;:8597"]
  16 + FROUNT["FROUNT SPA<br/>http://&lt;host&gt;: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.
... ...