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.