Commit 733ce1e2f6458674bbb67edb06de52ce1732bd95

Authored by zichun
1 parent 06ae2ff9

docs: verification pass + zh sync

en: every file:line citation, table-row count, endpoint, and diagram
entity re-verified against xly-src and the live DB. Numerous small
precision fixes across all wiki pages. api-reference/internal.md adds
a new section enumerating the 52 non-framework controllers so readers
can see what xly hardcoded vs. left to metadata.

zh: parallel translation sync (38 modified + 3 new maintainer pages).
Showing 77 changed files with 1967 additions and 465 deletions
en/docs/api-reference/external.md
... ... @@ -22,7 +22,10 @@ Content-Type: application/json
22 22 ```
23 23  
24 24 Handler: `ApiController.invoke()` at
25   -`xlyApi/src/main/java/com/xly/api/web/ApiController.java`.
  25 +`xlyApi/src/main/java/com/xly/api/web/ApiController.java:223`. The mapping
  26 +is `@RequestMapping` (method-agnostic at the framework level); the per-API
  27 +`sysapi.sMethod` column declares the expected verb for callers, and
  28 +`ApiCheckUtil` enforces it at dispatch.
26 29  
27 30 The flow:
28 31  
... ...
en/docs/api-reference/index.md
... ... @@ -10,7 +10,7 @@ overview lives in [concepts/api-surface](../concepts/api-surface.md).
10 10 | [External API](external.md) | `xlyApi` | `/xlyApi` | You are integrating an outside system that calls xly. |
11 11 | [Webhooks](webhooks.md) | `xlyInterface` | `/xlyInterface` | A third-party system needs to push events into xly. |
12 12 | [Messaging](messaging.md) | `xlyEntry` + `xlyErpJms*` | n/a (ActiveMQ / RocketMQ) | An asynchronous, fan-out integration is more appropriate than a synchronous HTTP call. |
13   -| [Notifications](notifications.md) | `xlyMsg` (consumed as a library by `xlyEntry`, `xlyBusinessService`, `xlyInterface`) | n/a (DingTalk / WeChat APIs) | A business event needs to push a chat-platform message to a user. |
  13 +| [Notifications](notifications.md) | `xlyMsg` (consumed as a library by `xlyEntry`, `xlyBusinessService`, `xlyInterface`, `xlyFlow`) | n/a (DingTalk / WeChat / email APIs) | A business event needs to push a chat-platform message or email to a user. |
14 14  
15 15 ## Reading order
16 16  
... ...
en/docs/api-reference/internal.md
... ... @@ -18,8 +18,8 @@ page is the catalog of HTTP entry points.
18 18  
19 19 | Endpoint | Method | Purpose |
20 20 |---|---|---|
21   -| `/business/getModelBysId/{moduleId}` | GET | Returns the form layout for a module — the five-key composite (`formData`, `gdsformconst`, `gdsjurisdiction`, `billnosetting`, `report`). |
22   -| `/business/getBusinessDataByFormcustomId/{formId}` | POST | Returns rows of business data for a form, paginated. Branches to `getBusinessDataByGroup` when `sGroupList` is set. |
  21 +| `/business/getModelBysId/{sModelsId}` | GET | Returns the form layout for a module — the five-key composite (`formData`, `gdsformconst`, `gdsjurisdiction`, `billnosetting`, `report`). |
  22 +| `/business/getBusinessDataByFormcustomId/{gdsconfigformmasterId}` | POST | Returns rows of business data for a form, paginated. Branches to `getBusinessDataByGroup` when `sGroupList` is set. |
23 23 | `/business/getBusinessDataByIndex` | POST | First / last / next / previous-record navigation. |
24 24 | `/business/addBusinessData` | POST | Single insert. |
25 25 | `/business/addUpdateDelBusinessData` | POST | Bundled add+update+delete in one transactional call. The frontend names the target table directly via `sTable`. |
... ... @@ -55,7 +55,7 @@ virtual tables) there is a parallel surface in
55 55 | `/treegrid/*` | `BusinessTreeGridController` | Tree-grid endpoints (the proc-backed path is implemented in this branch). |
56 56 | `/procedureCall/*` | `GenericProcedureCallController` | Generic stored-procedure invocation by name + parameters — see [generic procedure dispatch](../reference/maintainer/proc-dispatch.md). |
57 57 | `/panel/*` | `ConfigformPanelController` | Panel-layout persistence in `gdsconfigformpanel`. |
58   -| `/checkflow/*` | `CheckFlowController` | **Empty shell — returns 404.** The class declares the prefix but has zero handler methods. The actual workflow approve/reject/complete URLs come from xlyFlow's `CurrencyFlowController` (served at xlyEntry's context-path because xlyFlow is a library dep): `/complete/{taskId}/{sBrandsId}/{sSubsidiaryId}/{sUserId}`, `/completeerp/...`, plus `/modeler/*` for the BPMN modeler. See [Activiti integration](../reference/maintainer/activiti.md). |
  58 +| `/checkflow/*` | `CheckFlowController` | **Empty shell — returns 404.** The class declares the prefix but has zero handler methods. The actual workflow approve/reject/complete URLs come from xlyFlow's `CurrencyFlowController` (served at xlyEntry's context-path because xlyFlow is a library dep): `/currencyFlow/complete/{taskId}/{sBrandsId}/{sSubsidiaryId}/{sUserId}`, `/currencyFlow/completeerp/{sBrandsId}/{sSubsidiaryId}/{sUserName}`, plus `/modeler/*` (root-mapped) for the BPMN modeler. See [Activiti integration](../reference/maintainer/activiti.md). |
59 59 | `/modelCenter/getModelCenter`, `/modelCenter/getModelCenterCalculation` | `BusinessModelCenterController` | The FROUNT home-page **KPI Work Center** card (titled `KPI监控`). Aggregates open tasks across modules tagged `gdsmodule.bUnTask=1`, partitioned by role and business flow. **Not Activiti-driven.** See [The KPI Work Center](../reference/maintainer/runtime.md#the-kpi-work-center-front-end-home-dashboard). |
60 60  
61 61 ## Reporting and printing
... ... @@ -115,24 +115,144 @@ driven. **`图表配置`** is fully metadata-driven via two
115 115 `gdsconfigcharslave`; chart definitions there are consumed by xly's
116 116 dashboard rendering elsewhere in the SPA.
117 117  
118   -## Coverage policy — what this catalog includes
119   -
120   -`xlyEntry` hosts **~71 controllers** in total. This page enumerates
121   -the ~19 that are part of the framework's universal runtime:
122   -`/business/*`, `/configform/*`, `/treegrid/*`, `/procedureCall/*`,
123   -`/panel/*`, `/checkflow/*`, `/gdsmodule/*`, `/gdsconfigform/*`,
124   -`/gdsconfigtb/*`, plus the print surface. Every form in the system
125   -flows through these.
126   -
127   -The remaining ~52 controllers are **business-domain modules**
128   -(`/sysworkorder`, `/salesorder`, `/productionPlan`, `/oee`,
129   -`/eleMaterialsStock`, etc.) — they implement specific industry-tier
130   -flows on top of the framework primitives above. The wiki treats those
131   -as *illustrations of the framework at work*, not as catalogued surface
132   -of their own. Maintainers who need to find a specific business
133   -controller should grep `xlyEntry/src/main/java/com/xly/web/` for the
134   -URL prefix; the framework primitives on this page are what's worth
135   -reading first.
  118 +## Beyond the framework primitives — the rest of xlyEntry's surface
  119 +
  120 +`xlyEntry` hosts **70 controllers** in total. The 18 framework-side
  121 +controllers — the universal-runtime ones enumerated explicitly above
  122 +plus the seven `systemweb/` admin controllers backing the
  123 +[BACK builder sidebar](#back-builder-sidebar-admin-surface)
  124 +(`GdsformconstController`, `GdsjurisdictionController`,
  125 +`GdslogininfoController`, `GdsparameterController`, `LicenseController`,
  126 +`LoginController`, `SysbrandsController`) — are where every
  127 +metadata-driven form's lifecycle lives. The remaining 52 controllers
  128 +exist because the framework's universal CRUD + procedure-dispatch
  129 +path **wasn't enough** for the use case. Each one is, in effect, a
  130 +marker for where the data-driven thesis stopped scaling.
  131 +
  132 +That makes them worth enumerating even though they are out of the
  133 +framework's catalogued surface: they show what xly hardcodes in Java
  134 +versus what it leaves to metadata. A maintainer reading them gets a
  135 +direct read on the framework's escape-hatch shape.
  136 +
  137 +> **Namespace overlap.** `BusinessBaseController` (`/business/*`) and
  138 +> `QuoquotationController` (also `@RequestMapping("/business")`) share
  139 +> the URL prefix. Spring resolves by method-level path, so
  140 +> `/business/addQuotationsheet` and `/business/getQuoquotationProgress`
  141 +> live on `QuoquotationController` while every other `/business/*`
  142 +> endpoint is on `BusinessBaseController`. The collision is benign
  143 +> (no method-path overlap) but the convention is a foot-gun: a
  144 +> future contributor adding `@PostMapping("/addQuotationsheet")` to
  145 +> `BusinessBaseController` would silently shadow the quotation path.
  146 +
  147 +### Form-helper and SPA-extension controllers (22)
  148 +
  149 +Framework-adjacent endpoints — extensions to the universal CRUD path
  150 +that didn't fit the form-master / form-slave shape. Most are called
  151 +*alongside* `/business/*` from the same SPA screen.
  152 +
  153 +| Endpoint root | Controller | Role |
  154 +|---|---|---|
  155 +| `/bill/*` | `BillController` | Copy-a-document operations (`billCopyToCheck`, `billCopyToCheckWork`) — clone a master+slaves row-set into a new document. Not in `/business/*` because the SPA needs the new `sId` set returned in a single round-trip, which the universal save endpoint doesn't shape. |
  156 +| `/change/*` | `ChangeController` | Generic field-change recompute (`changeParam`) — the SPA fires this when a field's value triggers a derived recomputation that requires hitting a proc. |
  157 +| `/parameter/*` | `BusinessParameterController` | Per-module parameter reads. |
  158 +| `/treeclassify/*` | `BusinessTreeClassifyController` | Tree-grouped variant of the form data load — `/treeclassify/getTreeClassify/{gdsconfigformmasterId}`. Lives outside `/business/*` because the response shape is a nested tree, not a flat row-set. |
  159 +| `/calcprocedure/*` | `CalcProcedureController` | The runtime side of the [calculation-formula](../reference/builder/define-vtable.md) feature — `/calc` invokes a named calc proc. |
  160 +| `/calculationFormula/*` | `CalculationFormulaController` | Builder-side metadata for calculation formulas. |
  161 +| `/calculationStd/*` | `CalculationStdController` | Standard-calc lookup catalog. |
  162 +| `/char/*` | `CharController` | Chart-config CRUD for the BACK 图表配置 page — wraps `gdsconfigcharmaster`/`slave`. |
  163 +| `/checkModel/*` | `CheckmodelController` | Approval-model membership reads (`getUserListByModelId/{sCheckModeId}`). Used by the lightweight (non-Activiti) approval flow. |
  164 +| `/comparatorTree/*` | `ComparatorTreeController` | Comparator-tree reads for filterable hierarchical pickers. |
  165 +| `/excel/*` | `ExcelController` | Excel **export** of a grid — `/export/{gdsconfigformmasterId}`. Sibling to print, but data instead of report layout. |
  166 +| `/import/*` | `ImportExcelController` | Excel **import** (`/checkExcel`, then commit). Validates against the form's slaves before inserting. |
  167 +| `/filterTree/*` | `FilterTreeController` | Tree-shaped filter dropdown for grids. |
  168 +| `/notClear/*` | `NotClearController` | Barcode-scan "not yet cleared" save path (`doNotClearSave`, `getNotClearScanData/{sProcName}/{sId}`). Specific to scanner-driven warehouse flows. |
  169 +| `/notice/*` | `NoticeController` | In-app notice fetch / mark-read. |
  170 +| `/replaceField/*` | `ReplaceFieldController` | Bulk field-replace across rows. |
  171 +| `/searchgroupby/*` | `SearchgroupbyController` | Saved-search definitions with group-by. |
  172 +| `/searchupdown/*` | `SearchUpDownController` | Previous-/next-record-by-search navigation (variant of `/business/getBusinessDataByIndex`). |
  173 +| `/syssearch/*` | `SyssearchController` | Saved-search definitions CRUD. |
  174 +| `/syssystem/*` | `SyssystemController` | Variant of `getBusinessDataByFormcustomId` for system-table reads (`/getSyssystemDataByFormcustomId/{gdsconfigformmasterId}`) — bypasses tenant scoping for global metadata reads. |
  175 +| `/sqlfile/*` | `SqlFileController` | SQL-file load/save backing the "Mysql脚本配置" admin screen. |
  176 +| `/instruct/*` | `InstructController` | Direct SQL execution endpoints (`/exesql`, `/opensql`) — the admin-side query console. |
  177 +
  178 +### User and permission management (4)
  179 +
  180 +Multiple overlapping controllers for the same concern. The `New`
  181 +suffix and the presence of `sftlogininfo` *and* `userinfo` *and*
  182 +`gdslogininfo` (in `systemweb/`) suggest a refactor in progress —
  183 +the older path coexists with the new one until callers migrate.
  184 +
  185 +| Endpoint root | Controller | Role |
  186 +|---|---|---|
  187 +| `/userinfo/*` | `UserInfoController` | Current-user profile + session info. |
  188 +| `/sftlogininfo/*` | `SftlogininfoController` | User-account CRUD (newer of two paths). |
  189 +| `/sysjurisdiction/*` | `SysjurisdictionController` | Group/user permission reads (`getGroupData`, `getUserData`). |
  190 +| `/sysjurisdictionNew/*` | `SysjurisdictionNewController` | Newer parallel path (`getGroupDataNew`, `getGroupUserIdNew/{sUserId}`). Same concern, different shape. |
  191 +
  192 +### Manufacturing / MES (7)
  193 +
  194 +Industry-tier flows whose state machines, multi-table joins, or
  195 +hardware integration could not be expressed as `gdsconfigformmaster`
  196 +SQL. These are the framework's largest single concentration of
  197 +hardcoded business logic.
  198 +
  199 +| Endpoint root | Controller | Role |
  200 +|---|---|---|
  201 +| `/sysworkorder/*` | `WorkOrderController` | Work-order CRUD with side effects (`/add`, `/update/{sId}`) — a printing-industry work order spans many slaves and proc invocations the universal save can't chain atomically. |
  202 +| `/workOrderFlow/*` | `WorkOrderFlowController` | Work-order routing / process-flow reads (`getWorkOrderFlow`, `getSplitWorkOrderData/{sId}`). |
  203 +| `/workOrderPlan/*` | `WorkOrderPlanController` | Production-plan reads linking work orders to plan rows (`getControlProcess/{sProductionPlanId}`, `getProductionPlanInfo`). |
  204 +| `/splitWorkOrder/*` | `SplitWorkOrderController` | Splitting a master work order into multiple sub-orders (`getSplitWorkOrderData`). |
  205 +| `/productionPlan/*` | `ProductionPlanController` | Plan-tree reads (`getProductionPlanTree`). |
  206 +| `/process/*` | `ProcessController` | Manufacturing-process catalog reads. |
  207 +| `/oee/*` | `OeeController` | Overall Equipment Effectiveness — barcode-scan and MES status callbacks (`updateBarcode/{sBarCodeId}/{sBarCode}`, `doSysMesMsg/{sStatus}/{sMachineId}`). |
  208 +
  209 +### Sales, inventory, accounting, procurement, HR (9)
  210 +
  211 +| Endpoint root | Controller | Role |
  212 +|---|---|---|
  213 +| `/salesorder/*` | `SalesOrderController` | Sales-order specifics beyond the universal CRUD. |
  214 +| `/business/addQuotationsheet`, `/business/getQuoquotationProgress` | `QuoquotationController` | Quotation-sheet creation (long-running, hence the progress endpoint). **Note:** shares the `/business/*` prefix with `BusinessBaseController` — see the namespace-overlap note above. |
  215 +| `/eleMaterialsStock/*` | `EleMaterialsStockController` | Raw-material stock reads (`getEleMaterialsStock`, `getEleMaterialsStoreCurrQty`). |
  216 +| `/eleProductStock/*` | `EleProductStockController` | Finished-product stock reads. |
  217 +| `/costCenter/*` | `CostCenterController` | Cost-center data + voucher-import (`getCostCenterData`, `getCosvoucherImportData`). |
  218 +| `/sysAccountPeriod/*` | `SysAccountPeriodController` | Accounting-period open/close logic. |
  219 +| `/erpOrderProcurement/*` | `ErpOrderProcurementController` | Procurement-order specifics. |
  220 +| `/sisproductclassify/*` | `SisproductclassifyController` | Product-classification tree. |
  221 +| `/eleteamemployee/*` | `EleteamemployeeController` | Team/employee assignment for shop-floor flows. |
  222 +
  223 +### Integration and hardware (5)
  224 +
  225 +| Endpoint root | Controller | Role |
  226 +|---|---|---|
  227 +| `/file/*` | `FileController` | File upload (incl. WeChat mobile variant `mobileuploadwechat`). |
  228 +| `/plc/*` | `PlcController` | PLC-bridge entry points (`getplcMachine/{iOrder}/{sParentId}`) — see [Slice 6](../slices/06-hardware.md). |
  229 +| `/mobilephone/*` | `MobliePhoneController` | Mobile-app endpoints. (Typo `Moblie` is in the class name; the URL is `/mobilephone`.) |
  230 +| `/sysWebsocket/*` | `SysWebSocketController` | WebSocket setup/teardown for push notifications. |
  231 +| `/wechat/*` | `WechatController` | WeChat integration (in-app QR, OAuth callback). |
  232 +
  233 +### Out-of-scope per [the index](../index.md) (5)
  234 +
  235 +Listed for completeness — these are not part of the framework wiki's
  236 +in-scope surface, but they exist in the WAR.
  237 +
  238 +| Endpoint root | Controller | Status |
  239 +|---|---|---|
  240 +| `/ai/*` | `AiController` | AI assistant. Out of scope (index.md). |
  241 +| `/robot/*` | `ChatGptController` | ChatGPT integration. Out of scope (index.md). |
  242 +| `/test/*` | `TestController` | Dev scaffolding (`/file`, `/getDinkToken`). |
  243 +| (root paths) | `TestProcessController` | Dev scaffolding; no class-level `@RequestMapping`. |
  244 +| (commented out) | `XsController` | Dead file — `@RestController` and `@RequestMapping` are commented out; class exists but registers nothing. |
  245 +
  246 +### Reading these as a diagnostic
  247 +
  248 +Three patterns stand out when you scan the list:
  249 +
  250 +1. **Long-running or multi-step transactions** (`QuoquotationController.getQuoquotationProgress`, `BillController.billCopyToCheck`, `SplitWorkOrderController`) — the universal save is single-shot; any flow that needs a progress endpoint or a "clone then redirect" semantic needs its own controller.
  251 +2. **Industry-specific state machines** (`OeeController`, work-order family, `SysAccountPeriodController`) — when "the next legal state" can't be derived from a single column, the proc-dispatch path isn't enough and the controller wires up the orchestration in Java.
  252 +3. **Hardware or external systems** (`PlcController`, `WechatController`, `SysWebSocketController`, `FileController`) — anything that isn't "MySQL + HTTP" needs Java glue; metadata can't describe an inbound websocket or a serial-port handshake.
  253 +
  254 +The framework's universal runtime is what's *missing* from these
  255 +controllers' jobs.
136 256  
137 257 ## What this API is *not*
138 258  
... ...
en/docs/api-reference/messaging.md
... ... @@ -83,10 +83,12 @@ express well). RocketMQ topics are configured per environment.
83 83  
84 84 ## Manual cache-invalidation poke
85 85  
86   -If a metadata change happens via raw SQL (no JMS event), the cache
87   -across nodes will not bust automatically. The supported override is
88   -`BusinessCleanRedisDataImpl` in `xlyBusinessService/.../service/impl/` —
89   -it can publish an invalidation event directly. See
  86 +If a metadata change happens via raw SQL, the cache across nodes will not
  87 +bust automatically because no BACK save path calls `@CacheEvict`. The
  88 +supported override is to invoke the appropriate
  89 +`BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(...)` cleaner
  90 +from inside the application once; it evicts the shared Redis-backed Spring
  91 +cache directly. See
90 92 [cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md)
91 93 for the broader troubleshooting path.
92 94  
... ... @@ -95,6 +97,6 @@ for the broader troubleshooting path.
95 97 - **Not a public integration channel.** External integrators do not
96 98 publish or subscribe to these brokers. They are *internal* fan-out
97 99 for the cluster.
98   -- **Not the only way to invalidate caches.** The HTTP write paths in
99   - `xlyEntry` already publish JMS events when they should; the manual
100   - poke is for edge cases.
  100 +- **Not a cache-invalidation channel.** The HTTP write paths in
  101 + `xlyEntry` already evict Redis synchronously when they should; the
  102 + manual cleaner call is for raw-SQL or bypass paths.
... ...
en/docs/api-reference/notifications.md
1 1 # Notifications (xlyMsg)
2 2  
3   -Outbound notifications — DingTalk and WeChat — go through the `xlyMsg`
4   -module. This is *not* an HTTP surface that callers hit; it's an
5   -internal SDK that in-scope services call when a business event should
6   -push a message out to a chat platform.
  3 +Outbound notifications — DingTalk, WeChat, and email — go through the
  4 +`xlyMsg` module. This is *not* an HTTP surface that callers hit; it's
  5 +an internal SDK that in-scope services call when a business event
  6 +should push a message out to a chat platform or mailbox.
7 7  
8 8 ## What's inside
9 9  
... ... @@ -13,6 +13,7 @@ push a message out to a chat platform.
13 13 |---|---|
14 14 | `dingtalk/service/DingTalkService` + `dingtalk/util/SendDingTalkUtil`, `DingTalkMsgContentUtil`, `LocalCacheClient` | DingTalk corp-message dispatch. Wraps `com.aliyun:dingtalk:2.1.14` and `com.aliyun:alibaba-dingtalk-service-sdk:2.0.0`. |
15 15 | `wechat/service/WechatService` + `wechat/util/SendWxUtil`, `Wx_SignatureUtil`, `JedisMsgUtil`, `MsgContentUtil`, `Xml2JsonUtil` | WeChat work-platform dispatch — signature + send, including a Redis-backed access-token cache. |
  16 +| `emial/service/SendEmailService` + impl | Email dispatch (note the package typo `emial`). Used by `xlyFlow`'s `QuartzTask` for scheduled-job mail; `xlyEntry` also has its own `com.xly.web.email.SendEmailService` for `ScheduledTasks`-driven mail — same interface name, parallel implementation, kept for historical reasons. |
16 17 | `notice/service/NoticeService` | Provider-agnostic notice abstraction; routes a logical "notify user X about event Y" to the right backend. |
17 18  
18 19 `xlyMsg/build.gradle` — sole framework dependency is `xlyPersist`. The
... ...
en/docs/api-reference/webhooks.md
... ... @@ -37,7 +37,7 @@ for inbound calls:
37 37 | Endpoint | Method | Purpose |
38 38 |---|---|---|
39 39 | `/interfaceDefine/invoke/{interfaceInvoke}` | POST | Dispatch an inbound payload to the handler that `{interfaceInvoke}` names. |
40   -| `/interfaceDefine/callthirdparty/{interfaceInvoke}` | POST | Forward to a configured outbound endpoint. (Mirrors the `/interfaceDefine/callthirdparty/...` endpoint that also exists on `xlyApi`; the inbound side here is paired with the data-driven inbound dispatcher.) |
  40 +| `/interfaceDefine/callthirdparty/{interfaceInvoke}` | POST | Forward to a configured outbound endpoint. The same URL also exists on `xlyApi`'s `InterfaceController` — duplication is intentional, the two services share the data-driven dispatcher pattern. |
41 41  
42 42 Handler: `xlyInterface/src/main/java/com/xly/web/InterfaceController.java`.
43 43  
... ...
en/docs/concepts/customization-layers.md
... ... @@ -57,10 +57,13 @@ links are FK-enforced — see [no-FK reality](semantic-fk.md).
57 57 The framework reads each layer in order and merges by `sName` (the field
58 58 name). For a custom slave row with the same `sName` as a base slave:
59 59 override. For a new `sName`: append. For a base slave with no
60   -corresponding custom row: pass through unchanged. The merge happens
61   -inside `BusinessBaseServiceImpl.getModelBysId` (line 181) and the
62   -helpers it calls — `BusinessGdsconfigformsServiceImpl.getFormSlaveData`
63   -+ `getFormCustomSlaveData`.
  60 +corresponding custom row: pass through unchanged. Entry point is
  61 +`BusinessBaseServiceImpl.getModelBysId` (line 181), which calls
  62 +`BaseServiceImpl.getModelConfigByModleId` (line 55); the actual
  63 +slave-+-customslave merge runs in
  64 +`BusinessGdsconfigformsServiceImpl.getGdsconfigformslaveShow` (line 392),
  65 +combining `getFormSlaveData` (line 87) and `getFormCustomSlaveData`
  66 +(line 121), then optionally layering `getUserFormSlaveData` (line 156).
64 67  
65 68 Two database **views** support the merge by joining the form-master with
66 69 the relevant slave table:
... ...
en/docs/concepts/index.md
... ... @@ -87,6 +87,6 @@ a sign the content wants to be a slice instead.
87 87 - [No-FK, semantic-FK reality](semantic-fk.md) — how relations actually work.
88 88 - [Two customization channels](customization-channels.md) — metadata edits vs. SQL scripts.
89 89 - [Customization layers](customization-layers.md) — within Channel 1, how base / per-tenant / per-user overlays merge.
90   -- [Multi-tenancy and product editions](multi-tenancy.md) — the three scoping axes (`sBrandsId`, `sSubsidiaryId`, `sVersionFlowId`).
  90 +- [Multi-tenancy and product editions](multi-tenancy.md) — row scoping (`sBrandsId`, `sSubsidiaryId`) plus licence-gated module discovery.
91 91 - [The metadata-driven request lifecycle](request-lifecycle.md) — the diagram you'll come back to.
92 92 - [The three API tiers](api-surface.md) — internal (`xlyEntry`), external (`xlyApi`), inbound webhooks (`xlyInterface`).
... ...
en/docs/concepts/master-slave.md
... ... @@ -4,9 +4,10 @@
4 4 > This page is about the **document-row** pattern: one header row plus N
5 5 > detail rows for a quotation / sales order / work order. The
6 6 > **DataSource** master / slave (write-vs-read connection routing via
7   -> `MasterDataSourceConfig` / `SlaveDataSourceConfig` in `xlyApi`, paired
8   -> with `MasterBaseMapper.xml` / `SlaveBaseMapper.xml` in `xlyPersist`) is
9   -> a different concept covered in the [Tech-stack HikariCP row](../reference/maintainer/tech-stack.md#3-cache-in-memory)
  7 +> `MasterDataSourceConfig` / `SlaveDataSourceConfig` in `xlyApi` and
  8 +> `xlyInterface`, paired with `mastermapper/MasterBaseMapper.xml` /
  9 +> `slavemapper/SlaveBaseMapper.xml` co-located in those services) is
  10 +> a different concept covered in the [Tech-stack HikariCP row](../reference/maintainer/tech-stack.md#2-persistence)
10 11 > and indirectly in the runtime page. The two senses overlap in name only.
11 12  
12 13 Almost every business document in xly — a quotation, a sales order, a work
... ...
en/docs/concepts/modules-forms-vtables.md
... ... @@ -92,8 +92,8 @@ module / form / virtual-table combination through one universal
92 92 dispatch path. There is no per-module Java; PMs creating new modules
93 93 are creating new rows.
94 94  
95   -The flip side: that one engine has accumulated 3,500+ lines in
96   -`BusinessBaseServiceImpl` alone, plus another 800+ in
  95 +The flip side: that one engine has accumulated 3,900+ lines in
  96 +`BusinessBaseServiceImpl` alone, plus another 600+ in
97 97 `BusinessGdsconfigformsServiceImpl`. Edge cases, special-case
98 98 table handling (e.g., the `mftproductionplanslave` hardcode at
99 99 `BusinessBaseServiceImpl.java:1768`), per-tenant overlay merge
... ... @@ -112,17 +112,17 @@ any business-data table to its domain by the three-letter prefix:
112 112 | Prefix | Domain | Sample tables (live count) |
113 113 |---|---|---|
114 114 | `gds` | Framework metadata (modules, forms, fields, permissions, parameters, charts) | `gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave`, `gdsjurisdiction`, `gdsroute`, `gdsformconst`, `gdsparameter`, `gdsconfigcharmaster`/`slave` (chart definitions used by the BACK 图表配置 screen), … |
115   -| `sys` | Framework system (numbering, jurisdiction grants, reports, search, billing settings) — distinct from `gds*` "definition" tier | `sysjurisdiction`, `sysbillnosettings`, `sysreport`, `syssearch`, `sysapi`, `SysSystemSettings`, … (66 tables) |
116   -| `sis` | Shared lookup tables / classifiers backing dropdowns | `sisbank`, `siscolor`, `sisversionflow`, `sisjurisdictionclassify`, … (78 tables) |
117   -| `sft` | Login-session / group-permission link tables | `sftlogininfo*`, `sftlogininfojurisdictiongroup`, … (8 tables) |
118   -| `ele` | Master data ("element"): customer, employee, machine, materials, product, process, semigoods, costframe | `elecustomer*`, `eleemployee*`, `elemachine*`, `elematerials*`, `eleproduct*`, … (88 tables) |
119   -| `mft` | Manufacturing: work-order, production-plan, production-report | `mftworkordermaster`, `mftproductionplan*`, `mftproductionreport*`, … (72 tables) |
120   -| `sal` | Sales | `salsalesordermaster`, `salsalesorderslave`, `salsalesorderprocess`, … (65 tables) |
121   -| `quo` | Quotation | `quoquotationmaster`, `quoquotationslave`, `quoquotationcalc_tmp`, … (12 tables) |
  115 +| `sys` | Framework system (numbering, jurisdiction grants, reports, search, billing settings) — distinct from `gds*` "definition" tier | `sysjurisdiction`, `sysbillnosettings`, `sysreport`, `syssearch`, `sysapi`, `syssystemsettings`, … (68 tables) |
  116 +| `sis` | Shared lookup tables / classifiers backing dropdowns | `sisbank`, `siscolor`, `sisversionflow`, `sisjurisdictionclassify`, … (80 tables) |
  117 +| `sft` | Login-session / group-permission link tables | `sftlogininfo`, `sftlogininfojurisdictiongroup`, … (8 tables) |
  118 +| `ele` | Master data ("element"): customer, employee, machine, materials, product, process, semigoods, costframe | `elecustomer*`, `eleemployee*`, `elemachine*`, `elematerials*`, `eleproduct*`, … (89 tables) |
  119 +| `mft` | Manufacturing: work-order, production-plan, production-report | `mftworkordermaster`, `mftproductionplan*`, `mftproductionreport*`, … (82 tables) |
  120 +| `sal` | Sales | `salsalesordermaster`, `salsalesorderslave`, `salsalesorderprocess`, … (67 tables) |
  121 +| `quo` | Quotation | `quoquotationmaster`, `quoquotationslave`, `quoquotationcalc_tmp`, … (23 tables) |
122 122 | `acc` | Accounting | `accordercostanalysis`, `accordercostanalysisoperation`, … (31 tables) |
123 123 | `pur` | Purchasing | `purpurchaseapply`, `purpurchasearrive`, `purpurchasechecking`, … (28 tables) |
124 124 | `ops` | Outside-processing / outsourcing | `opsoutsidearrive`, `opsoutsidechecking`, `opsoutsideinstore`, … (23 tables) |
125   -| `cah` | Cashier / financial | `cahcashierinit`, `cahcostchange`, `cahpayment`, `cahreceipt`, … (22 tables) |
  125 +| `cah` | Cashier / financial | `cahcashierinit`, `cahcostchangemaster`, `cahpaymentmaster`, `cahreceiptmaster`, … (22 tables) |
126 126 | `sgd` | Semi-goods (半成品) | `sgdsemigoodscheck`, `sgdsemigoodsinstore`, `sgdsemigoodsmatchbill`, … (21 tables) |
127 127 | `ept` | Equipment / machine fixed assets | `eptmachinefixedborrow`, `eptmachinefixedchange`, `eptmachinefixedinstore`, … (21 tables) |
128 128 | `mit` | Materials inventory transactions | `mitmaterialsadjust`, `mitmaterialscheck`, `mitmaterialsinstore`, … (19 tables) |
... ...
en/docs/concepts/multi-tenancy.md
... ... @@ -14,7 +14,7 @@ two-paragraph summary you can link from anywhere.
14 14 |---|---|---|---|
15 15 | **`sBrandsId`** (加工商ID) | almost every business row | per-row | the user's session (`UserInfo.getsBrandsId()`) |
16 16 | **`sSubsidiaryId`** (子公司ID) | almost every business row | per-row | the user's session |
17   -| **`sVersionFlowId`** (版本流程ID) | `gdsmodule` only | per-module | the user's edition (against `sisversionflow`) |
  17 +| **`sVersionFlowId` / `sVersionFlowCode`** (版本流程ID / code) | `gdsmodule` only | per-module tag | edition catalogue metadata; the runtime menu gate uses the licence-derived `sVerifyLicense` module list |
18 18  
19 19 Per-row scoping is universal across business-data tables: both
20 20 `sBrandsId` and `sSubsidiaryId` appear on essentially every one. Most
... ... @@ -27,9 +27,11 @@ tables. In practice they hold a single sentinel tenant value shared
27 27 across all customers. Convention: "if a row represents tenant-owned
28 28 state, both columns are present *and populated from the session*."
29 29  
30   -Per-module gating (`sVersionFlowId`) is the opposite — it lives on
31   -`gdsmodule` only. So edition gating is a one-time filter at module-
32   -discovery time, not a per-row check.
  30 +Per-module edition metadata is the opposite — it lives on `gdsmodule`
  31 +only. The live runtime does not filter directly on `sVersionFlowId`;
  32 +module discovery is gated by the licence-derived `sVerifyLicense` list
  33 +of permitted `gdsmodule.sId` values. So edition gating is a one-time
  34 +module-discovery filter, not a per-row check.
33 35  
34 36 ## How it's enforced
35 37  
... ...
en/docs/concepts/request-lifecycle.md
... ... @@ -107,9 +107,10 @@ sequenceDiagram
107 107 Note over CTRL: AuthorizationInterceptor.preHandle<br/>resolves UserInfo from Redis<br/>RequestAddParamUtil.addParams (16 keys)
108 108  
109 109 CTRL->>SVC: getModelBysId(map)
110   - SVC->>FORMS: getModelConfigByModleId<br/>(form-master + slaves + overlays)
  110 + Note over SVC: getModelConfigByModleId (inherited from BaseServiceImpl)<br/>orchestrates the per-master form-master + slave loads
  111 + SVC->>FORMS: getFormmasterData / getGdsconfigformslaveShow<br/>(form-master + slaves + overlays)
111 112 REDIS-->>FORMS: cache hit?
112   - FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize ⋈ slave ⋈ customslave
  113 + FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize; per master row, gdsconfigformslave + gdsconfigformcustomslave
113 114 DB-->>FORMS: rows
114 115 FORMS-->>SVC: formData
115 116  
... ... @@ -172,12 +173,15 @@ A few things readers expect to find here but don&#39;t:
172 173 body — the handler branches on whether the body asks for one row or
173 174 many.
174 175 - **Workflow steps.** When a module has an active approval workflow
175   - (`bCheck = 1`, populated `sVersionFlowId`, deployed Activiti process),
176   - additional steps interleave. None of those tables are populated in
177   - this dev DB; see [Slice 7 (deferred)](../slices/07-workflow.md).
178   -- **Cache invalidation.** When BACK changes a metadata row, a JMS message
179   - invalidates cached copies on every running node — `ConsumerChangeGdsModuleThread`
180   - in `xlyErpJmsConsumer`. Outside the request flow but adjacent to it.
  176 + (`bCheck = 1`, `gdsmoduleflow` configured, deployed Activiti process,
  177 + and `ConstantUtils.bCheckflowCheck = true`), additional steps
  178 + interleave. None of those tables are populated in this dev DB; see
  179 + [Slice 7 (deferred)](../slices/07-workflow.md).
  180 +- **Cache invalidation.** When BACK changes a metadata row, the save path
  181 + synchronously calls `BusinessCleanRedisData` / `CleanRedisServiceImpl`,
  182 + which evicts Spring cache regions from shared Redis. The JMS
  183 + `ConsumerChangeGdsModuleThread` path is a separate base-data merge
  184 + channel, not cache invalidation.
181 185  
182 186 ## Variations covered by other slices
183 187  
... ...
en/docs/concepts/thesis.md
... ... @@ -66,7 +66,7 @@ Three costs are baked into this design and worth being explicit about:
66 66 - **Customizations are layered "cleanly"** ([Slice 4](../slices/04-custom-field.md)):
67 67 per-tenant overrides sit *on top of* the shared base without forking.
68 68 — *Limit:* the cleanliness is a Java-side property. The runtime
69   - merge logic in `BusinessBaseServiceImpl` is non-trivial (3,500+
  69 + merge logic in `BusinessBaseServiceImpl` is non-trivial (3,900+
70 70 lines), debugging "why does this tenant see field X but not Y"
71 71 involves chasing through `gdsconfigformpersonalize` +
72 72 `gdsconfigformcustomslave` + `gdsconfigformuserslave` interactions.
... ...
en/docs/contributing/index.md
... ... @@ -41,10 +41,10 @@ on each run. Hand-edits in those directories will be lost.
41 41  
42 42 ## Pre-commit hook (optional but recommended for local edits)
43 43  
44   -Install once:
  44 +Install once (run from `xly-wiki/en/`):
45 45  
46 46 ```bash
47   -ln -s ../../scripts/precommit.sh .git/hooks/pre-commit
  47 +ln -s ../../en/scripts/precommit.sh ../.git/hooks/pre-commit
48 48 chmod +x scripts/precommit.sh
49 49 ```
50 50  
... ...
en/docs/glossary/index.md
... ... @@ -23,3 +23,7 @@ preserves Chinese terms in the body text and translates only here.
23 23 | 流水号 | flow number / sequence | Document numbering — `sysbillnosettings`. |
24 24 | 抄送 | cc (carbon copy) | `biz_todo_copyto`. |
25 25 | 待办 | todo | `biz_todo_item`. |
  26 +| 审核 | audit / approve | The "approve" click in BACK that flips a document's `bCheck` flag and runs `Sp_<table>_check*` transition procs. The canonical workflow trigger. |
  27 +| 驳回 | reject | The counterpart to 审核 — reject a document; in customer overrides like `领班驳回.sql` it walks a multi-level rejection chain. |
  28 +| — | BACK | The admin / builder web app (`http://<host>:8597`) where PMs configure modules, forms, and permissions. Spelled uppercase to match the deployment artefact. |
  29 +| — | FROUNT | The end-user web app (`http://<host>:8598`) where day-to-day work happens. Misspelling of "Front" preserved from the deployment artefact. |
... ...
en/docs/index.md
... ... @@ -40,7 +40,7 @@ which Reference chapter you go deep on.
40 40 - Scheduler modules (`xlyErpTask`, `xlyPlatTask`) — commented out in `settings.gradle`; cron / Quartz wiring is not part of the wiki's framework runtime.
41 41 - Test scaffolding modules (`xlyTestService`, `xlyTestController`) — historical, not part of the framework runtime.
42 42 - Per-tenant schema drift between `xlyweberp_*` databases — wiki targets one schema.
43   -- Backup tables (`*_bak`, `*0302`, `*_copy1`, `*_history`, `*YYYYMMDD[HHMMSS]`-suffixed snapshots, etc.) — the auto-catalog generates a page for each because they exist on disk; the prose pages don't cover them as a family. ~56 such tables in the live schema.
  43 +- Backup tables (`*_bak`, `*0302`, `*_copy1`, `*_history`, `*YYYYMMDD[HHMMSS]`-suffixed snapshots, etc.) — the auto-catalog generates a page for each because they exist on disk; the prose pages don't cover them as a family. ~44 such tables in the live schema.
44 44 - The MongoDB document store (`spring.data.mongodb.uri` in the yaml profiles, document classes under `xlyEntity/.../mongo/`). Of 22 `@Document` classes there, 20 are `PLAT_*`-named — the only outliers are two `DIKE_TEST*` scratch classes. The single `MongoTemplate` caller is `xlyPersist/.../dao/platmongo/BaseMongoDao` (the `dao/platmongo/` package gives away its plat-tier intent), which has no in-tree consumers on the cleanup branch — the `xlyPlat*` modules that used to extend it are all commented out of `settings.gradle`. The framework layer this wiki covers is MySQL-only; the Mongo wiring stays compiled but dormant.
45 45  
46 46 > **Note on `xlyPlatConstant`.** It carries the `xlyPlat*` prefix but is in scope: `xlyPersist` imports two utility classes from it (`com.xly.xlyplatconstant.contant.thread.MultiThreadServer`, `com.xly.xlyplatconstant.contant.TimeContant`). Treat it as a misnamed shared-utility module, not a platform-tier module.
... ...
en/docs/reference/builder/attach-workflow.md
... ... @@ -5,7 +5,7 @@
5 5 > returns 0; `gdsmoduleflow = 0`; `gdsmodule WHERE bCheck = 1` matches
6 6 > 0 rows. The dispatch path itself is hard-disabled by
7 7 > `ConstantUtils.bCheckflowCheck = false` (see
8   -> [Activiti integration](../../reference/maintainer/activiti.md)). The
  8 +> [Activiti integration](../maintainer/activiti.md)). The
9 9 > recipe below is the **code-derived hypothesis** — it has not been
10 10 > exercised against a live deployment.
11 11  
... ... @@ -19,8 +19,10 @@
19 19 >
20 20 > 1. Set `gdsmodule.bCheck = 1` to flag the module as workflow-enabled.
21 21 > 2. Populate `gdsmoduleflow` with the flow's window configuration.
22   -> 3. Set `gdsmodule.sVersionFlowId` and `sVersionFlowCode` to the
23   -> Activiti process definition's ID and key.
  22 +> 3. Link the module/button to the process in `gdsmoduleflow` using
  23 +> the fields expected by `CheckExamineFlowServiceImpl`.
  24 +> `gdsmodule.sVersionFlowId` / `sVersionFlowCode` are edition tags,
  25 +> not the live Activiti binding.
24 26 > 4. Deploy the BPMN process definition through the Activiti REST API
25 27 > or whichever workflow deployment surface is enabled in the target
26 28 > environment.
... ...
en/docs/reference/builder/define-form.md
... ... @@ -19,7 +19,7 @@ One row registers the module&#39;s existence. Required columns:
19 19 | `sChinese` / `sEnglish` / `sBig5` | display name in three languages |
20 20 | `sParentId` | parent module's `sId` — places this module in the menu tree |
21 21 | `sBrandsId` / `sSubsidiaryId` | tenant scope — should be your tenant's IDs (or `'1111111111'` if standard / system-level) |
22   -| `sVersionFlowId` | the product edition this module belongs to (look up in `sisversionflow`) |
  22 +| `sVersionFlowId` / `sVersionFlowCode` | product-edition catalogue tags (look up in `sisversionflow` where populated); live menu visibility is still gated by the licence-derived module list |
23 23 | `bVisible` | `1` to show in the menu |
24 24 | `bInvalid` | `0` for active |
25 25  
... ...
en/docs/reference/builder/define-vtable.md
... ... @@ -98,7 +98,7 @@ A representative real row from the dev DB:
98 98 sId = 192116810113315231587698560
99 99 sChinese = 包装方式 (Packing method)
100 100 sTbName = SisPacking
101   -sParentId = (root)
  101 +sParentId = 192116810113315231564967560 (parent classification row)
102 102 ```
103 103  
104 104 **Slave columns** (`gdsconfigtbslave`, 10 rows under that `sParentId`)
... ...
en/docs/reference/builder/index.md
... ... @@ -12,5 +12,3 @@ metadata-table column names and a worked example.
12 12 - [How to set permissions](permissions.md) — `gdsjurisdiction` recipe.
13 13  
14 14 If you need details on a specific table or proc, see the [Auto-Catalog](../../auto-catalog/index.md).
15   -
16   -> **STUB.** Recipes will be filled in as their concepts are exercised by slices.
... ...
en/docs/reference/builder/permissions.md
... ... @@ -8,7 +8,7 @@ correctly here matters because the names look superficially similar.
8 8  
9 9 | Table | Granularity | What it stores | Loaded when |
10 10 |---|---|---|---|
11   -| [`gdsjurisdiction`](../../auto-catalog/tables/gdsjurisdiction.md) | per **module** | the **catalog** of actions/buttons that exist on each module (`BtnAdd`, `BtnUpd`, `BtnDel`, …) | every `getModelBysId` call (skipped for ADMIN — see [Slice 1](../../slices/01-hello-world.md)) |
  11 +| [`gdsjurisdiction`](../../auto-catalog/tables/gdsjurisdiction.md) | per **module** | the **catalog** of actions/buttons that exist on each module (`BtnAdd`, `BtnUpd`, `BtnDel`, `BtnPrint`, …) | every `getModelBysId` call (skipped for ADMIN — see [Slice 1](../../slices/01-hello-world.md)) |
12 12 | [`sysjurisdiction`](../../auto-catalog/tables/sysjurisdiction.md) | per **role** (and optionally per user) | the **grants** — which role (or user) can perform which action on which module | resolved on `getModelBysId` for the user's role; `BusinessGdsconfigformsServiceImpl.getJurisdictionData()` |
13 13  
14 14 The mental model: `gdsjurisdiction` is the *menu* of permission items
... ... @@ -22,7 +22,7 @@ A tree of permission items keyed by module:
22 22 | Column | Meaning |
23 23 |---|---|
24 24 | `sParentId` | the `gdsmodule.sId` this permission item belongs to |
25   -| `sName` | the action key — e.g., `BtnAdd`, `BtnUpd`, `BtnDel`, `BtnExport` |
  25 +| `sName` | the action key — e.g., `BtnAdd`, `BtnUpd`, `BtnDel`, `BtnPrint` |
26 26 | `sChinese` / `sEnglish` / `sBig5` | display label (e.g., `新增`) |
27 27 | `iOrder` | sort order in the permission UI |
28 28 | `sBrandsId` / `sSubsidiaryId` | tenant scope |
... ... @@ -89,7 +89,7 @@ For a new module, the typical path is:
89 89  
90 90 1. Define the module + form ([recipe](define-form.md)).
91 91 2. Insert `gdsjurisdiction` rows for each button/action the module
92   - exposes (`BtnAdd`, `BtnUpd`, `BtnExport`, …). One row per
  92 + exposes (`BtnAdd`, `BtnUpd`, `BtnPrint`, …). One row per
93 93 action; `sParentId = your module's sId`.
94 94 3. Decide which roles get which actions, and insert `sysjurisdiction`
95 95 rows: `sParentId = module sId`, `sJurisdictionClassifyId = role sId`,
... ...
en/docs/reference/maintainer/activiti.md
... ... @@ -158,7 +158,7 @@ the instance.
158 158  
159 159 ### A real Path-1 customisation example
160 160  
161   -[Slice 5](../../slices/05-customer-sql-override.md#worked-example-2-builds-a-multi-level-approval-workflow)
  161 +[Slice 5](../../slices/05-customer-sql-override.md#worked-example-2-万昌-builds-a-multi-level-approval-workflow)
162 162 walks through 万昌's `领班驳回.sql` — a customer-side multi-level
163 163 approval rejection. It's the canonical example of how customers
164 164 extend Path 1 when the single-`bCheck` flag isn't enough: they
... ... @@ -222,9 +222,10 @@ Despite the dev DB being idle, the engine boots with `xlyEntry`:
222 222 a `SpringProcessEngineConfiguration` bean.
223 223 - `xlyEntry/build.gradle` includes `xlyFlow` as `api project(':xlyFlow')`,
224 224 so the starter is on the runtime classpath of the `xlyEntry` WAR.
225   -- `xlyEntry/.../EntryApplicationBoot.java:23-24` excludes only
  225 +- `xlyEntry/.../EntryApplicationBoot.java:23-24` excludes
  226 + `DataSourceAutoConfiguration`,
226 227 `org.activiti.spring.boot.SecurityAutoConfiguration` (the
227   - REST-endpoint security adapter) and Spring's own
  228 + REST-endpoint security adapter), and Spring's own
228 229 `SecurityAutoConfiguration`. **Activiti's main engine
229 230 auto-config is NOT excluded** → the engine starts.
230 231 - `xlyFlow/.../activiti/config/ActivitiConfig.java` is a
... ... @@ -267,7 +268,7 @@ plus the modeler subpackage are real call sites. Selected anchors:
267 268 | Save a model in the modeler | `ModelerController.create()` :122 | `repositoryService.saveModel()` + `addModelEditorSource()` |
268 269 | Deploy a BPMN at runtime | `ModelerController.deploy()` :147 | `repositoryService.createDeployment().addString(name, bpmnXml).deploy()` |
269 270 | List process definitions | `ProcessDefinitionController` :135 | `repositoryService.createProcessDefinitionQuery()` |
270   -| Read engine config | `ProcessActController` :281 | `ProcessEngines.getDefaultProcessEngine()` |
  271 +| Read engine config | `BizTodoItemServiceImpl` :126 | `ProcessEngines.getDefaultProcessEngine()` |
271 272 | Bridge xly users into Activiti identity | `act_id_user` / `act_id_group` / `act_id_membership` are **views** projecting xly's `sftlogininfo*` schema | xly does not write to Activiti's identity tables; the views fake them |
272 273  
273 274 ## URLs the modeler exposes (xlyFlow controllers, on xlyEntry's port)
... ... @@ -291,7 +292,7 @@ authoritative.
291 292  
292 293 This is a wiki-internal correction worth flagging: the class file
293 294 exists at `xlyEntry/src/main/java/com/xly/web/businessweb/CheckFlowController.java`
294   -but its body is **22 lines, zero handler methods** — just a
  295 +but its body is **25 lines, zero handler methods** — just a
295 296 `@RestController @RequestMapping(value="/checkflow")` shell with no
296 297 content. Earlier versions of this wiki described `/checkflow/*` as
297 298 "Activiti workflow surface (approve / reject / view)"; that is not
... ...
en/docs/reference/maintainer/cache-invalidation.md
... ... @@ -17,7 +17,7 @@ flowchart TB
17 17  
18 18 PM[PM clicks 保存 in BACK]:::ok
19 19 SAVE["BusinessBaseServiceImpl<br/>add/update/deleteBusinessData"]
20   - EVICT["BusinessCleanRedisData.delCleanRedisData<br/>→ CleanRedisServiceImpl<br/>17 @CacheEvict methods"]:::ok
  20 + EVICT["BusinessCleanRedisData.delCleanRedisData<br/>→ CleanRedisServiceImpl<br/>18 cache regions for gdsmodule"]:::ok
21 21 REDIS[("Redis<br/>(shared across nodes)")]:::ok
22 22 DB[("MySQL<br/>row written")]:::ok
23 23  
... ... @@ -72,7 +72,7 @@ Next /business/getModelBysId call re-reads from DB and re-populates
72 72 The cleaner methods are in
73 73 `xlyBusinessService/src/main/java/com/xly/service/impl/CleanRedisServiceImpl.java`.
74 74 A representative one — invoked when `gdsmodule` rows change — evicts
75   -17 cache regions in a single call:
  75 +18 cache regions in a single call:
76 76  
77 77 ```
78 78 @CacheEvict(value = {
... ... @@ -130,7 +130,9 @@ not Spring Cache), Spring Boot 2.2.5 auto-configures
130 130 **Empirically verified** against the live dev Redis at
131 131 `118.178.19.35:16379` (database 0): 233 of 267 keys use Spring Cache's
132 132 default `<cacheName>::<key>` separator. Sample key shape matching the
133   -`@Cacheable` SpEL spec from `BusinessGdsconfigformsServiceImpl.java:209-211`:
  133 +`@Cacheable` annotation on `getFormconstData` at
  134 +`BusinessGdsconfigformsServiceImpl.java:189-190` (default key derived
  135 +from all params):
134 136  
135 137 ```
136 138 businessGdsconfigformsServiceGetFormconstData::{sLanguage=sChinese, sModelsId=…, sSubsidiaryId=1111111111, sUserId=…, sBrandsId=1111111111}
... ...
en/docs/reference/maintainer/deployment.md
... ... @@ -117,7 +117,7 @@ files. The active profile is selected at startup via
117 117  
118 118 ## Disabled in `settings.gradle`
119 119  
120   -The cleanup branch comments out 12 `include` lines. Three are non-Plat
  120 +The cleanup branch comments out 17 `include` lines. Three are non-Plat
121 121 modules present on disk:
122 122  
123 123 - `xlyErpTask` — long-running background tasks.
... ... @@ -127,7 +127,7 @@ modules present on disk:
127 127 - `xlyFile` — older file-management module, superseded by
128 128 `xlyPlatFileUpload` (also commented out).
129 129  
130   -The remaining nine commented-out includes are `xlyTestService`,
  130 +The remaining 14 commented-out includes are `xlyTestService`,
131 131 `xlyTestController`, and the full `xlyPlat*` family except
132 132 `xlyPlatConstant` — i.e. `xlyPlatTask`, `xlyPlatJmsProductor`,
133 133 `xlyPlatJmsConsumer`, `xlyPlatReportForm`, `xlyPlatFileUpload`,
... ... @@ -161,8 +161,9 @@ Three communication channels:
161 161 schema. Most cross-service "communication" is implicit through
162 162 shared tables.
163 163 2. **Messaging** — both ActiveMQ/JMS and RocketMQ exist in the codebase.
164   - Cache invalidation ([cache invalidation on metadata change](cache-invalidation.md))
165   - uses the ActiveMQ/JMS path.
  164 + ActiveMQ/JMS carries base-data merge and document fan-out jobs; Redis
  165 + cache invalidation is synchronous `@CacheEvict` in the BACK save path
  166 + (see [cache invalidation on metadata change](cache-invalidation.md)).
166 167 3. **HTTP REST** — for synchronous calls (e.g., xlyApi calling xlyEntry's
167 168 `/business/*` endpoints).
168 169  
... ... @@ -185,7 +186,7 @@ Profiles split by service:
185 186 `bgj` (lowercase). `dev` is the in-repo default.
186 187 - **xlyApi**: `local` (default in repo), `dev`, `linux`, `win`.
187 188 - **xlyInterface**: `dev` only.
188   -- **xlyFlow**: `dev` (empty file).
  189 +- **xlyFlow**: `dev` (datasource-only).
189 190 - **xlyFace**: `win` (default), `dev`, `linux`, `local`.
190 191 - **xlyPlc**: `dev` (default) plus 7 press-model profiles
191 192 (`15S`, `S10`, `T0`, `T1`, `CT`, `yt`, `pro` — uppercase / mixed-case,
... ...
en/docs/reference/maintainer/index.md
... ... @@ -9,10 +9,9 @@ metadata table to the `gds*` family, or wire in a new third-party integration.
9 9 - [Tech stack](tech-stack.md) — library inventory by category (versions, where each is used, and why).
10 10 - [The runtime: BusinessBaseController & friends](runtime.md) — the metadata-driven dispatch loop.
11 11 - [Generic procedure dispatch](proc-dispatch.md) — `GenericProcedureCallController` deep dive.
12   -- [Cache invalidation on metadata change](cache-invalidation.md) — `ConsumerChangeGdsModuleThread` and friends.
13   -- [SQL templates (`xlyEntry/templesql/`)](sql-templates.md) — runtime SQL generation.
  12 +- [Cache invalidation on metadata change](cache-invalidation.md) — synchronous `@CacheEvict`, and why similarly named JMS consumers are not cache-bust.
  13 +- [SQL templates (`xlyEntry/templesql/`)](sql-templates.md) — proc scaffolds engineers fill in.
14 14 - [Multi-service deployment](deployment.md) — `xlyApi` vs `xlyEntry` vs `xlyInterface`.
  15 +- [Metadata-management services (`xlyManage`)](management-services.md) — the `Gds*ServiceImpl` family behind the BACK builder.
  16 +- [BI / KPI / Charts engine](bi-engine.md) — the homebrewed dashboard layer.
15 17 - [Activiti integration](activiti.md) — version skew, schemas, custom delegates.
16   -
17   -> **STUB.** Each sub-page will be filled with a real code-trace once the matching
18   -> slice exercises it.
... ...
en/docs/reference/maintainer/management-services.md
... ... @@ -16,7 +16,7 @@ the read/write logic for every `gds*` table. The runtime
16 16  
17 17 | Service | Lines | Owns | BACK page |
18 18 |---|---:|---|---|
19   -| `GdsmoduleServiceImpl` | 729 | `gdsmodule` (modules), `gdsroute` (URL whitelist), module-tree CRUD, edition gating | 系统模块配置 |
  19 +| `GdsmoduleServiceImpl` | 729 | `gdsmodule` (modules), `gdsroute` (URL whitelist), module-tree CRUD | 系统模块配置 |
20 20 | `GdsconfigformServiceImpl` | 878 | `gdsconfigformmaster`, `gdsconfigformslave`, `gdsconfigformcustomslave`, `gdsconfigformpersonalize` (form definitions + per-tenant overlays) | 界面显示内容配置 |
21 21 | `GdsconfigtbServiceImpl` | 555 | `gdsconfigtbmaster`, `gdsconfigtbslave` (virtual-table definitions) | 数据表内容配置 |
22 22 | `SqlScriptsServiceImpl` | 489 | DDL / proc / view scripts authored in BACK; uses templates from [`templesql/`](sql-templates.md) | Mysql脚本配置 |
... ... @@ -63,12 +63,11 @@ own dedicated controller+service pair.
63 63 end-to-end usable: the BACK builder can also generate the
64 64 schema-migration SQL the overlay implies.
65 65 - **`GdsmoduleServiceImpl` includes `getModuleTreePro`** — the
66   - per-edition / per-tenant module-tree resolution called by the SPA at
67   - login (the first `/gdsmodule/getModuleTreePro` request you see in
68   - the live trace). Edition gating
69   - ([Slice 2](../../slices/02-multi-tenancy.md)) happens here, as a
70   - filter on `gdsmodule.sVersionFlowId` against the user's
71   - `sisversionflow` row.
  66 + module-tree resolution called by the SPA at login (the first
  67 + `/gdsmodule/getModuleTreePro` request you see in the live trace).
  68 + Edition visibility ([Slice 2](../../slices/02-multi-tenancy.md)) is
  69 + ultimately enforced by the licence-derived `sVerifyLicense` module-id
  70 + list used by the menu SQL, not by a direct `sVersionFlowId` predicate.
72 71 - **`SqlScriptsServiceImpl`** glues the
73 72 [`templesql/`](sql-templates.md) scaffolds into the BACK script
74 73 authoring screen. Engineers fill in the placeholder spec; the
... ... @@ -85,14 +84,18 @@ own dedicated controller+service pair.
85 84  
86 85 ## Cache-invalidation hookpoints
87 86  
88   -Every write through these services synchronously calls
89   -`BusinessCleanRedisData.delCleanRedisData*` on commit. This is why
  87 +Every write through these services carries its own `@CacheEvict`
  88 +annotations directly on the method (e.g., `GdsmoduleServiceImpl.java:96`
  89 +evicts nine cache regions on `addGdsmodule`). This is why
90 90 metadata edits in BACK take effect immediately on every node — the
91 91 shared Redis cache (RedisCacheManager, see
92 92 [cache-invalidation.md](cache-invalidation.md)) gets the relevant
93 93 regions evicted in the same transaction the write commits. There is
94 94 **no JMS fan-out here for cache-bust** — that's a common
95 95 misconception, addressed in detail on the cache-invalidation page.
  96 +(Business-data writes through `BusinessBaseServiceImpl` use the
  97 +separate `BusinessCleanRedisData.delCleanRedisData*` dispatcher; see
  98 +[cache-invalidation.md](cache-invalidation.md) for that path.)
96 99  
97 100 ## What's *not* in `xlyManage`
98 101  
... ... @@ -109,7 +112,7 @@ misconception, addressed in detail on the cache-invalidation page.
109 112 | Symptom | First place to look |
110 113 |---|---|
111 114 | BACK 修改/新增 against `gdsconfigform*` returns "操作失败" | `GdsconfigformServiceImpl` — check field validation + the matching DDL-script generation path |
112   -| Edition gating shows wrong modules | `GdsmoduleServiceImpl.getModuleTreePro` — verify the user's `sVersionFlowId` resolution |
  115 +| Edition gating shows wrong modules | menu/module-tree SQL plus `VerifyLicense.getModelAllList()` / `sVerifyLicense` — verify the permitted module-id list |
113 116 | BACK script-authoring screen produces broken SQL | `SqlScriptsServiceImpl` + the [templesql scaffolds](sql-templates.md) |
114 117 | Permission catalogue (BtnAdd / BtnUpd / …) missing for a module | `GdsjurisdictionServiceImpl` — check the rows under that `sParentId` |
115 118 | User can log in to BACK but FROUNT is empty | `GdslogininfoServiceImpl` — check the `sftlogininfo*` link tables |
... ...
en/docs/reference/maintainer/proc-dispatch.md
... ... @@ -119,15 +119,15 @@ templates](sql-templates.md) for the loader and the placeholder syntax.
119 119 The 1687 procedures in the live DB cluster around a few naming molds
120 120 beyond the bare `Sp_*` family:
121 121  
122   -| Mold | Approx count | Role |
  122 +| Mold | Live count | Role |
123 123 |---|---:|---|
124 124 | `Sp_*` | most | The dominant family, dispatched by `sSaveProName` / `sCalcProName` / `sProcName` etc. |
125   -| `Sp_*_BeforeSave` | ~62 | Pre-save hooks. Pair with `sSaveProNameBefore`. |
126   -| `Sp_*_AfterSave` / `Sp_*_SaveReturn` | ~62 / ~54 | Post-save hooks; `_SaveReturn` writes back into the parent transaction. |
127   -| `Sp_*_Calc` | ~178 | Calculation procs invoked by button-press flow (`sCalcProName` / `sButtonParam`). |
128   -| `sp_btn_*` | ~65 | Button-event sub-family — typically `sp_btn_calc*` / `sp_btn_validate*` (lowercase by convention). |
129   -| `PRO_ERPMERGE*` | ~11 | Data-migration utilities. **Not dispatched by the runtime** — engineer-only. |
130   -| `PRO_*` (other) | ~12 | Other one-off utilities. |
  125 +| `Sp_beforeSave*` | 78 | Pre-save hooks. Pair with `sSaveProNameBefore`. |
  126 +| `Sp_afterSave*` / `Sp_saveReturn*` | 62 / 54 | Post-save hooks; `Sp_saveReturn*` writes back into the parent transaction. |
  127 +| `Sp_Calc*` | 184 | Calculation procs invoked by button-press flow (`sCalcProName` / `sButtonParam`). |
  128 +| `Sp_Btn*` | 65 | Button-event sub-family — typically button calculations / validators. |
  129 +| `PRO_ERPMERGE*` | 22 | Data-merge utilities. **Not dispatched by the runtime** — engineer / JMS-consumer paths. |
  130 +| `PRO_*` (all) | 23 | `PRO_ERPMERGE*` plus one-off utilities. |
131 131 | `Get_*`, `del_*`, `Cal*`, `Tj_*` | small handfuls | Legacy / domain-specific helpers. Not part of the generic-dispatch contract. |
132 132  
133 133 A typo in any of the dispatched columns gets an `Sp_*`-shaped target,
... ...
en/docs/reference/maintainer/running-locally.md
... ... @@ -69,7 +69,7 @@ boot:
69 69 - `xlyApi` — start it as a second JVM if you need the external API surface.
70 70 - `xlyInterface` — same.
71 71 - `xlyPlc`, `xlyFlow`, `xlyFace` — same; each has its own application class and profile.
72   -- The `xlyErpJms*` consumers — they are standalone Spring Boot apps. Without them, JMS-driven cache invalidation will not happen across nodes (fine for a single-node dev box).
  72 +- The `xlyErpJms*` consumers — they are standalone Spring Boot apps. Without them, ActiveMQ base-data merge and document fan-out jobs will not run. Redis cache invalidation for BACK saves still works through synchronous `@CacheEvict`.
73 73  
74 74 For multi-service local development, see
75 75 [Multi-service deployment](deployment.md).
... ...
en/docs/reference/maintainer/runtime.md
... ... @@ -16,7 +16,7 @@ controllers and services that carry most of the generic form runtime.
16 16 | `BusinessTreeGridController` | `web/businessweb/` | Tree-grid endpoints. In this branch, the proc-backed path is implemented; the plain `getTreeGrid` service method is still a stub. | `/treegrid/getTreeGridByPro/{formId}` |
17 17 | `GenericProcedureCallController` | `web/businessweb/` | Generic stored-procedure invocation by name + parameters. | `/procedureCall/doGenericProcedureCall` |
18 18 | `ConfigformPanelController` | `web/businessweb/` | Panel-layout persistence in `gdsconfigformpanel`. | `/panel/get/{sFormId}`, `/panel/save/{sFormId}` |
19   -| `CheckFlowController` | `web/businessweb/` | **Empty shell.** The class file is 22 lines: just a `@RestController @RequestMapping(value="/checkflow")` with no handler methods. `/checkflow/*` returns 404. The actual workflow approve/reject/complete URLs live in `CurrencyFlowController` (in `xlyFlow`, served via xlyEntry's context-path) — see [Activiti integration](activiti.md#urls-the-modeler-exposes-xlyflow-controllers-on-xlyentrys-port). | (none — empty class) |
  19 +| `CheckFlowController` | `web/businessweb/` | **Empty shell.** The class file is 25 lines: just a `@RestController @RequestMapping(value="/checkflow")` with no handler methods. `/checkflow/*` returns 404. The actual workflow approve/reject/complete URLs live in `CurrencyFlowController` (in `xlyFlow`, served via xlyEntry's context-path) — see [Activiti integration](activiti.md#urls-the-modeler-exposes-xlyflow-controllers-on-xlyentrys-port). | (none — empty class) |
20 20 | `BusinessModelCenterController` | `web/businessweb/` | The "KPI Work Center" home dashboard on FROUNT — open-task aggregation across modules tagged with `gdsmodule.bUnTask=1`. **Not Activiti-driven** despite the workflow-flavoured UI; reads from `gdsmodule` rows partitioned by `sUnType` ∈ {`Pending`, `PendingCheck`, `MyWarning`}. Cached per user. See [The KPI Work Center](#the-kpi-work-center-front-end-home-dashboard) below. | `/modelCenter/getModelCenter`, `/modelCenter/getModelCenterCalculation` |
21 21  
22 22 Note that the controllers split across **two packages**: `businessweb/`
... ... @@ -226,7 +226,7 @@ Two flagged in slices that belong here permanently:
226 226 The "one controller writes any row in any table" pattern is the
227 227 core data-driven move. It also concentrates risk:
228 228  
229   -- **`BusinessBaseServiceImpl` is ~3,500 lines** of tightly
  229 +- **`BusinessBaseServiceImpl` is ~3,900 lines** of tightly
230 230 intertwined logic: per-tenant scope-bypass list, special-case
231 231 table hardcodes (`mftproductionplanslave` at line 1768),
232 232 pre/post-save hook dispatch, sTable-driven write routing. Every
... ...
en/docs/reference/maintainer/tech-stack.md
... ... @@ -48,7 +48,7 @@ page records facts only.
48 48  
49 49 | Library | Version | Where | In-scope source references |
50 50 |---|---|---|---|
51   -| MyBatis | 2.1.2 (`mybatis-spring-boot-starter`) | `xlyPersist/build.gradle`, `xlyApi/build.gradle`, `xlyFlow/build.gradle` | 102 Java files import `org.apache.ibatis.*` or `org.mybatis.*` — 76 in `xlyPersist`, the rest spread across xlyApi, xlyFlow, xlyInterface. Mapper XMLs live at `xlyPersist/src/main/resources/mapper/{erptable,business,test}/`. |
  51 +| MyBatis | 2.1.2 (`mybatis-spring-boot-starter`) | `xlyPersist/build.gradle`, `xlyApi/build.gradle`, `xlyFlow/build.gradle` | 98 Java files import `org.apache.ibatis.*` or `org.mybatis.*`, concentrated in `xlyPersist` with the rest spread across xlyApi, xlyFlow, xlyInterface. Mapper XMLs live at `xlyPersist/src/main/resources/mapper/{erptable,business,test}/`. |
52 52 | MyBatis-Plus | 3.3.0 | `xlyApi/build.gradle` | 2 files: `xlyApi/src/main/java/com/xly/api/util/SqlUtil.java`, `xlyApi/src/main/java/com/xly/api/web/BaseController.java`. Not used outside xlyApi. |
53 53 | MySQL Connector/J | 8.0.13 | `xlyPersist/build.gradle`, `xlyApi/build.gradle`, `xlyFlow/build.gradle` | yaml `spring.datasource.driverClassName: com.mysql.cj.jdbc.Driver` (e.g., `xlyEntry/.../application-local.yml:127`). |
54 54 | MSSQL JDBC | sqljdbc4 3.0 (Maven) + `mssql-jdbc-6.2.2.jre8.jar` (local jar in `xlyFlow/`, `xlyInterface/`) | `xlyApi/build.gradle`, `xlyInterface/build.gradle`, `xlyFlow/build.gradle` | 5 files: 3 in `xlyFlow/src/`, 2 in `xlyInterface/src/`. |
... ... @@ -143,7 +143,7 @@ third-party code.
143 143  
144 144 | Library | Version | Where | In-scope source references |
145 145 |---|---|---|---|
146   -| FastJson | 1.2.15 (`xlyPersist`, `xlyApi`) / 1.2.60 (`xlyFlow`) | `xlyPersist/build.gradle`, `xlyApi/build.gradle`, `xlyFlow/build.gradle` | 83 files import `com.alibaba.fastjson.*` across in-scope modules (xlyBusinessService=39, xlyEntry=11, xlyInterface=9, xlyPersist=9, xlyFlow=6, xlyMsg=5, xlyApi=4). |
  146 +| FastJson | 1.2.15 (`xlyPersist`, `xlyApi`) / 1.2.60 (`xlyFlow`) | `xlyPersist/build.gradle`, `xlyApi/build.gradle`, `xlyFlow/build.gradle` | 84 files import `com.alibaba.fastjson.*` across in-scope modules (xlyBusinessService=39, xlyEntry=11, xlyInterface=10, xlyPersist=9, xlyFlow=6, xlyMsg=5, xlyApi=4). |
147 147 | Jackson | `jackson-databind` 2.9.7 (`xlyFlow` explicit) + transitive via Spring | `xlyFlow/build.gradle` | 22 files import `com.fasterxml.jackson.*` (xlyFlow=8, xlyInterface=9, xlyApi=2, xlyPersist=2, xlyEntry=1). |
148 148 | Hutool | `hutool-all` 5.6.5 (`xlyPersist`) / 5.8.5 (`xlyApi`, `xlyFlow`) | `xlyPersist/build.gradle`, `xlyApi/build.gradle`, `xlyFlow/build.gradle` | 271 files import `cn.hutool.*` across every in-scope module (xlyBusinessService=93, xlyFlow=47, xlyApi=37, xlyPersist=33, xlyEntry=25, xlyInterface=23, xlyMsg=10, xlyManage=2, xlyPlc=1). |
149 149 | commons-lang3 | 3.6 (`xlyPersist`) / 3.8.1 (`xlyFlow`) | `xlyPersist/build.gradle`, `xlyFlow/build.gradle` | 39 files import `org.apache.commons.lang3.*` (xlyFlow=23, xlyPersist=7, xlyEntry=3, xlyApi=2, xlyBusinessService=3, xlyMsg=1). |
... ...
en/docs/slices/01-hello-world.md
... ... @@ -227,9 +227,11 @@ POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId}
227 227 ### 5. Cache invalidation
228 228  
229 229 A modified `gds*` row in any of the four metadata tables invalidates cached
230   -copies on every running node. xly does this through a JMS message:
231   -`xlyErpJmsConsumer/.../ConsumerChangeGdsModuleThread.java` listens for the
232   -"module changed" event and clears the relevant Redis keys. See
  230 +copies on every running node through the shared Redis store. The save path
  231 +calls `BusinessCleanRedisData.delCleanRedisData*`, which fires Spring
  232 +`@CacheEvict` synchronously in the BACK process. The similarly named JMS
  233 +`CHANGE_GDS_MODULE` path runs base-data merge procedures; it does not clear
  234 +Redis. See
233 235 [Cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md).
234 236  
235 237 ### 6. Browser confirms
... ...
en/docs/slices/02-multi-tenancy.md
... ... @@ -17,7 +17,7 @@ xly&#39;s tenancy has **three** dimensions, applied at different layers:
17 17 |---|---|---|---|
18 18 | **`sBrandsId`** (加工商ID) | Almost every business row | Per-row | "Which manufacturer/company owns this row?" |
19 19 | **`sSubsidiaryId`** (子公司ID) | Almost every business row | Per-row | "Which subsidiary within the company?" |
20   -| **`sVersionFlowId`** (版本流程ID) | `gdsmodule` only | Per-module | "Which product edition is this module part of?" |
  20 +| **`sVersionFlowId` / `sVersionFlowCode`** (版本流程ID / code) | `gdsmodule` only | Per-module tag | "Which product edition is this module tagged for?" Runtime gating uses the licence-derived module list. |
21 21  
22 22 The first two are **per-row** scoping. The third is **per-module** filtering
23 23 applied at module-list load time. Different mechanisms, different layers.
... ... @@ -174,9 +174,10 @@ Within `gdsmodule` (1358 rows in the dev DB), three tagging patterns coexist:
174 174 - *Multi-tenant scoping* (new concept page) — `sBrandsId`/`sSubsidiaryId`
175 175 as the per-row tenant boundary; the framework's universal injector
176 176 (`RequestAddParamUtil`).
177   -- *Product editions* (new concept page) — `sVersionFlowId` against
178   - `sisversionflow` as the per-module visibility filter; the difference
179   - between scoping (per-row) and gating (per-module).
  177 +- *Product editions* (concept page) — `sVersionFlowId` /
  178 + `sVersionFlowCode` as catalogue tags, with actual visibility gated
  179 + by the licence-derived module list; the difference between scoping
  180 + (per-row) and gating (per-module).
180 181  
181 182 These will be added to Concepts as part of the next backfill pass.
182 183  
... ... @@ -186,8 +187,9 @@ These will be added to Concepts as part of the next backfill pass.
186 187  
187 188 - [The runtime](../reference/maintainer/runtime.md) — `RequestAddParamUtil`
188 189 belongs in the runtime chapter as the universal tenant-context injector.
189   -- New page: *Multi-tenant query patterns* — the conventions every MyBatis
190   - mapper and every stored procedure must follow to stay tenant-safe.
  190 +- [Multi-tenancy and product editions](../concepts/multi-tenancy.md) —
  191 + the conventions every MyBatis mapper and stored procedure must follow
  192 + to stay tenant-safe.
191 193  
192 194 ## Open verification items
193 195  
... ...
en/docs/slices/04-custom-field.md
... ... @@ -109,8 +109,8 @@ configuration: there are no foreign keys enforcing that
109 109 `gdsconfigformcustomslave.sParentId` actually exists in
110 110 `gdsconfigformmaster.sId`. Orphan rows are possible and would be silently
111 111 ignored at merge time. A maintainer audit script that flags such orphans
112   -is on the [Maintainer Reference](../reference/maintainer/runtime.md)'s
113   -TODO list.
  112 +belongs in the maintainer toolkit; the current runtime does not enforce
  113 +that relationship.
114 114  
115 115 ## Why it works without code changes — and what that costs
116 116  
... ...
en/docs/slices/05-customer-sql-override.md
... ... @@ -248,7 +248,7 @@ Verified against the dev DB recon target (`xlyweberp_saas_ai`):
248 248 So 万昌's "Foreman Rejection" workflow is **a customer-built
249 249 state-machine atop xly's button primitive**: schema extension +
250 250 custom procs + custom audit log. The framework provides only the
251   -button-press dispatch (via `/business/genericProcedureCall*` or the
  251 +button-press dispatch (via `/procedureCall/doGenericProcedureCall` or the
252 252 button-param hook on the form-slave). Everything else — what state
253 253 the document is in, what flags toggle, what audit text gets logged —
254 254 is customer-side.
... ...
en/docs/slices/index.md
... ... @@ -14,9 +14,9 @@ Re-read earlier slices once you&#39;ve read later ones — the cross-refs back-fill.
14 14 | # | Slice | Concept(s) introduced |
15 15 |---|---|---|
16 16 | 1 | [a CRUD module (Hello World)](01-hello-world.md) | modules, forms, master/slave, jurisdiction |
17   -| 2 | [multi-tenancy and product editions](02-multi-tenancy.md) | sBrandsId/sSubsidiaryId scoping, sVersionFlowId editions |
  17 +| 2 | [multi-tenancy and product editions](02-multi-tenancy.md) | sBrandsId/sSubsidiaryId scoping, licence-gated editions |
18 18 | 3 | [a module with a report](03-report.md) | views, report templates, jxls |
19   -| 4 | [extending — a custom field](04-custom-field.md) | gdsconfigformuserslave, schema-less extension |
  19 +| 4 | [extending — a custom field](04-custom-field.md) | gdsconfigformcustomslave, schema-less extension |
20 20 | 5 | [extending — a per-customer SQL override](05-customer-sql-override.md) | script/客户/, override channel |
21 21 | 6 | [a hardware-integrated module](06-hardware.md) | xlyPlc, serial, RPC into the press |
22 22 | 7 | [a module with workflow](07-workflow.md) (deferred) | Activiti, biz_flow, approval — needs a deployment with active flows |
... ...
zh/docs/api-reference/external.md
... ... @@ -15,13 +15,13 @@ Content-Type: application/json
15 15 { ...request body, passed through as `sBody` to the handler... }
16 16 ```
17 17  
18   -处理器:`xlyApi/src/main/java/com/xly/api/web/ApiController.java` 中的 `ApiController.invoke()`
  18 +处理器:`xlyApi/src/main/java/com/xly/api/web/ApiController.java:223` 中的 `ApiController.invoke()`。该 mapping 是 `@RequestMapping`,框架层不限定 HTTP 方法;每个 API 行的 `sysapi.sMethod` 列声明调用方应使用的动词,并由 `ApiCheckUtil` 在分发时校验
19 19  
20 20 流程:
21 21  
22 22 1. 读取 `Authorization` header(回退:`authorizationt` 查询参数)。
23 23 2. 查找以 `sApiCode` 为键的 `sysapi` 行(通过 `ApiServiceImpl.invoke` → `SELECT … FROM sysapi WHERE sApiCode = #{sApiCode}`)。
24   -3. 如果该行的 `bHasToken` 标志已设置,就用 `sysapithirdtoken`(或该行指向的等价 token 存储)验证 token。
  24 +3. 如果该行的 `bHasToken` 标志已设置,就 AES 解密 bearer token 取回 `corpid`,再通过 `BrandServiceImpl.selectByCorpid` 到 `sysapibrand` 校验该 `corpid`。如果品牌行的 `iLossTime` 非 0,还会检查 token 内嵌时间戳是否过期。(`sysapithirdtoken` 用于**出站** token,也就是 xly 调第三方 API;这里不用于校验入站 bearer token。)
25 25 4. 使用请求 body 合并出的参数 map,执行 `sysapi.sDataSql` 中存放的 SQL 模板。
26 26 5. 将调用写入 `sysapilog`。
27 27 6. 用 `AjaxResult` 包装结果并返回。
... ... @@ -32,7 +32,7 @@ Content-Type: application/json
32 32  
33 33 | 列 | 含义 |
34 34 |---|---|
35   -| `sApiCode` | 消费方发送的 path variable。每个租户内必须唯一。 |
  35 +| `sApiCode` | 消费方发送的路径变量。每个租户内必须唯一。 |
36 36 | `sApiName` | 人类可读标签。 |
37 37 | `sApiUrl` / `sApiUrlRef` | 计算后的 URL,即 `sApiUrlRef + sApiCode`,用于出站分发。 |
38 38 | `sMethod` | 此 API 期望的 HTTP 方法(`GET`、`POST` 等)。 |
... ... @@ -47,9 +47,9 @@ Content-Type: application/json
47 47  
48 48 ## Token 端点:`/token/*`
49 49  
50   -| Endpoint | Method | 用途 |
  50 +| 端点 | 方法 | 用途 |
51 51 |---|---|---|
52   -| `/token/getToken?corpid=&corpsecret=` | POST | 为集成方的 `(corpid, corpsecret)` 组合签发 bearer token。 |
  52 +| `/token/getToken?corpid=&corpsecret=` | GET / POST | 为集成方的 `(corpid, corpsecret)` 组合签发 bearer token。(mapping 不限定方法。) |
53 53  
54 54 返回的 token 就是 `/api/invoke/{sApiCode}` 期望在 `Authorization` 中收到的值。完整实现位于 `xlyApi/src/main/java/com/xly/api/web/TokenController.java` 及其 service。
55 55  
... ... @@ -59,15 +59,17 @@ Content-Type: application/json
59 59  
60 60 这些是同一个 WAR 中托管的较小专用 API:
61 61  
62   -| Endpoint root | Controller | 用途 |
  62 +| 端点前缀 | Controller | 用途 |
63 63 |---|---|---|
64   -| `/online/api/{sApiCode}` | `OnlineController` | 只读执行某个 `sysapi` 行(不写入)。适合公开数据端点。 |
65   -| `/online/onlineword/{sApiCode}` | `OnlineController` | 返回“word”风格模板载荷的变体。 |
66   -| `/online/onlinelist`, `/online/getToken` | `OnlineController` | 在线 API 列表及其 token 签发。 |
67   -| `/pro/get/{sProName}` | `ProContentController` | 按名称获取存储过程元数据。 |
68   -| `/pro/getData/{sProName}` | `ProContentController` | 执行指定存储过程并返回其结果集。 |
69   -| `/pro/alert/{redisId}`, `/pro/getAlertValue/{redisId}` | `ProContentController` | 按 Redis id 读取预警 / 通知值。 |
70   -| `/pro/executeSql` | `ProContentController` | 执行参数化 SQL 模板(管理级)。 |
  64 +| `/online/api/{sApiCode}` | `OnlineController` | 为给定 `sysapi` 行渲染 BACK 浏览器内 API 调试 / 控制台页面(返回 Thymeleaf view,不执行 API)。 |
  65 +| `/online/onlineword/{sApiCode}` | `OnlineController` | 渲染“word”风格 API 文档页面。 |
  66 +| `/online/onlinelist` | `OnlineController` | 渲染在线 API 列表页面。 |
  67 +| `/online/getToken` | `OnlineController` | 渲染浏览器内 token 获取辅助页面。 |
  68 +| `/pro/get/{sProName}` | `ProContentController` | 渲染 BACK 页面,用于展示某个存储过程源码。 |
  69 +| `/pro/getData/{sProName}` | `ProContentController` | 返回存储过程源码文本(不是结果集)。 |
  70 +| `/pro/alert/{redisId}` | `ProContentController` | 为给定 Redis key 渲染预警 / 通知展示页面。 |
  71 +| `/pro/getAlertValue/{redisId}` | `ProContentController` | 返回 Redis 中 `redisId` 对应的缓存值。 |
  72 +| `/pro/executeSql` | `ProContentController` | 直接执行 `sSql` 载荷(管理级开发工具)。 |
71 73 | `/thirdparty/*` | `ThirdPartyController` | 第三方 API 定义 CRUD,以及 `checkPartyApi` 校验器。由 `sysapithirdparty` 支撑。 |
72 74 | `/thirdtoken/*` | `ThirdTokenController` | 出站 token 配置 CRUD。由 `sysapithirdtoken` 支撑。 |
73 75 | `/brand/*` | `BrandController` | 伙伴 / 供应商列表(`sysapibrand`)CRUD。 |
... ... @@ -86,8 +88,8 @@ Content-Type: application/json
86 88 | `sysapibrand` | 伙伴 / 供应商目录。 |
87 89 | `sysapithirdparty` | 出站第三方端点定义。 |
88 90 | `sysapithirdtoken` | 出站第三方 token 配置。 |
89   -| `sysapidbtodb` | DB-to-DB 同步 API 定义。 |
90   -| `sysapidbtodblog` | DB-to-DB 同步运行日志。 |
  91 +| `sysapidbtodb` | DB-to-DB 同步 API 定义。**由 `xlyFlow` 的 `DbToDbController` 拥有,不由 xlyApi 拥有**;列在这里是因为该表位于 xlyApi 的 `sysapi.sql` 中。 |
  92 +| `sysapidbtodblog` | DB-to-DB 同步运行日志。同上,由 xlyFlow 写入。 |
91 93  
92 94 ## 集成方如何使用
93 95  
... ...
zh/docs/api-reference/index.md
... ... @@ -8,7 +8,7 @@ xly 暴露了三个不同的 HTTP 接口面,分别由三个独立的 Spring Bo
8 8 | [外部 API](external.md) | `xlyApi` | `/xlyApi` | 你在接入会从外部调用 xly 的系统。 |
9 9 | [Webhook](webhooks.md) | `xlyInterface` | `/xlyInterface` | 第三方系统需要把事件推送进 xly。 |
10 10 | [消息](messaging.md) | `xlyEntry` + `xlyErpJms*` | 不适用(ActiveMQ / RocketMQ) | 异步、扇出式集成比同步 HTTP 调用更合适。 |
11   -| [通知](notifications.md) | `xlyMsg`(作为库被 `xlyEntry`、`xlyBusinessService`、`xlyInterface` 使用) | 不适用(钉钉 / 微信 API) | 业务事件需要向用户推送聊天平台消息。 |
  11 +| [通知](notifications.md) | `xlyMsg`(作为库被 `xlyEntry`、`xlyBusinessService`、`xlyInterface`、`xlyFlow` 使用) | 不适用(钉钉 / 微信 / 邮件 API) | 业务事件需要向用户推送聊天平台消息或邮件。 |
12 12  
13 13 ## 阅读顺序
14 14  
... ...
zh/docs/api-reference/internal.md
... ... @@ -8,15 +8,18 @@
8 8  
9 9 ## 通用 CRUD 接口面:`/business/*`
10 10  
11   -| Endpoint | Method | 用途 |
  11 +| 端点 | 方法 | 用途 |
12 12 |---|---|---|
13   -| `/business/getModelBysId/{moduleId}` | GET | 返回某个模块的表单布局,也就是五键组合(`formData`、`gdsformconst`、`gdsjurisdiction`、`billnosetting`、`report`)。 |
14   -| `/business/getBusinessDataByFormcustomId/{formId}` | POST | 返回某个表单的业务数据行,带分页。设置 `sGroupList` 时会分支到 `getBusinessDataByGroup`。 |
  13 +| `/business/getModelBysId/{sModelsId}` | GET | 返回某个模块的表单布局,也就是五键组合(`formData`、`gdsformconst`、`gdsjurisdiction`、`billnosetting`、`report`)。 |
  14 +| `/business/getBusinessDataByFormcustomId/{gdsconfigformmasterId}` | POST | 返回某个表单的业务数据行,带分页。设置 `sGroupList` 时会分支到 `getBusinessDataByGroup`。 |
15 15 | `/business/getBusinessDataByIndex` | POST | 首条 / 末条 / 下一条 / 上一条记录导航。 |
16 16 | `/business/addBusinessData` | POST | 单行新增。 |
17 17 | `/business/addUpdateDelBusinessData` | POST | 在一个事务调用里组合新增 + 修改 + 删除。前端通过 `sTable` 直接指定目标表。 |
18 18 | `/business/getSelectDataBysControlId/{sId}` | POST | 按控件 `sId` 为单个控件加载下拉选项。 |
19 19 | `/business/getSelectLimit/{sId}` | POST | 下拉加载调用的分页变体。 |
  20 +| `/business/addSysLocking` | POST | 用户开始编辑单据时获取乐观锁,在系统锁表中按 `(sFormGuid, sUserId)` 插入一行。SPA 进入编辑模式时会触发它,使并发编辑者收到冲突提示。处理器:`BusinessBaseController.java:400-407`。 |
  21 +| `/business/doExamine` | POST | 简单“审核”:通过 SQL 把指定行的 `bCheck` 翻为 `1`。**不会调用 Activiti**;这是不需要多步工作流的模块使用的 xly 轻量审批路径。处理器:`BusinessBaseController.java:384-391` → `BusinessBaseServiceImpl.doExamine` → `ExamineServiceImpl`。何时改用 Activiti,见 [xly 如何在不使用 Activiti 的情况下处理工作流](../reference/maintainer/activiti.md#xly-如何在不使用-activiti-的情况下处理工作流)。 |
  22 +| `/business/getProData` | POST | 面向模块的通用存储过程调用,是 `/procedureCall/doGenericProcedureCall` 的另一条路径。处理器:`BusinessBaseController.java:350-358` → `BusinessBaseServiceImpl.getProData`。FROUNT 会用它做模块级 proc 读取,例如首页看板的 `/getProData?sModelsId=...&sName=` 模式。 |
20 23  
21 24 这些端点在[切片 1](../slices/01-hello-world.md)(`getModelBysId` + 网格加载 + 保存)和[切片 3](../slices/03-report.md)(基于视图的读取变体)中有更详细说明。处理类位于 `xlyEntry/src/main/java/com/xly/web/businessweb/`。
22 25  
... ... @@ -24,7 +27,7 @@
24 27  
25 28 配置侧动作(创建模块、定义表单、声明虚拟表)在 `xlyEntry/src/main/java/com/xly/web/systemweb/` 下有一套并行接口面:
26 29  
27   -| Endpoint root | Controller | 用途 |
  30 +| 端点前缀 | Controller | 用途 |
28 31 |---|---|---|
29 32 | `/gdsmodule/*` | `GdsmoduleController` | 模块树 CRUD,包括 `getModuleTreePro`、`addGdsmodule`、`updateGdsmodule`。 |
30 33 | `/gdsconfigform/*` | `GdsconfigformController` | 表单主表和表单明细元数据 CRUD。 |
... ... @@ -32,20 +35,21 @@
32 35  
33 36 ## 专用运行时端点
34 37  
35   -| Endpoint root | Controller | 用途 |
  38 +| 端点前缀 | Controller | 用途 |
36 39 |---|---|---|
37 40 | `/configform/*` | `BusinessConfigformController` | 用户 / 用户组级显示定制。 |
38 41 | `/treegrid/*` | `BusinessTreeGridController` | 树表端点(当前分支实现的是存储过程支撑路径)。 |
39 42 | `/procedureCall/*` | `GenericProcedureCallController` | 按名称 + 参数通用调用存储过程;见[通用存储过程分发](../reference/maintainer/proc-dispatch.md)。 |
40 43 | `/panel/*` | `ConfigformPanelController` | `gdsconfigformpanel` 中的面板布局持久化。 |
41   -| `/checkFlow/*` | `CheckFlowController` | Activiti 工作流接口面(审批 / 驳回 / 查看),只在运行流程引擎的部署中有意义。 |
  44 +| `/checkflow/*` | `CheckFlowController` | **空壳,返回 404。** 该类只声明前缀,没有任何 handler 方法。真正的工作流审核 / 驳回 / 完成 URL 来自 xlyFlow 的 `CurrencyFlowController`(因为 xlyFlow 作为库依赖被编进 xlyEntry,所以仍挂在 xlyEntry context-path 下):`/currencyFlow/complete/{taskId}/{sBrandsId}/{sSubsidiaryId}/{sUserId}`、`/currencyFlow/completeerp/{sBrandsId}/{sSubsidiaryId}/{sUserName}`,以及根路径映射的 BPMN modeler `/modeler/*`。见 [Activiti 集成](../reference/maintainer/activiti.md)。 |
  45 +| `/modelCenter/getModelCenter`、`/modelCenter/getModelCenterCalculation` | `BusinessModelCenterController` | FROUNT 首页的 **KPI 工作中心**卡片(标题为 `KPI监控`)。它聚合标记为 `gdsmodule.bUnTask=1` 的模块上的未清任务,按角色和业务流程分组。**不是 Activiti 驱动。** 见 [KPI 工作中心](../reference/maintainer/runtime.md#kpi-工作中心front-端首页-dashboard)。 |
42 46  
43 47 ## 报表与打印
44 48  
45 49 打印接口面位于 `xlyEntry/src/main/java/com/xly/web/report/`:
46 50  
47 51 - `PrintReportController` — 当前 jxls / iText 打印路径。
48   -- `PrintReportControllerOld` — 为旧模板保留的历史打印路径
  52 +- `PrintReportControllerOld.java` — 文件存在,但类体已全部注释掉(而且注释内类名是 `PrintReportController`,不是 `*Old`)。这是保留作参考的死代码,不是活跃 controller
49 53  
50 54 前端的“打印” / “导出”按钮会调用这些控制器,控制器从 `sysreport` 加载模板,执行匹配的视图查询,并把二进制文件流回前端。流程见[切片 3](../slices/03-report.md)。
51 55  
... ... @@ -55,6 +59,135 @@
55 59  
56 60 如果某个请求未认证却进入了控制器,正常情况下会被 `@Authorization` 拦下;如果没有被拦下(例如某个方法未加注解),这个方法也会绕过 `RequestAddParamUtil` 中的通用租户注入,因此就是多租户 bug。
57 61  
  62 +## BACK 配置侧边栏(管理接口面) {#back-builder-sidebar-admin-surface}
  63 +
  64 +BACK 管理侧边栏的 10 个顶层项(登录 `admin`/`123`,版本 `基础版/8s`)都通过上面的框架原语接线成元数据驱动页面:
  65 +
  66 +| 侧边栏 | URL 片段 | 支撑 form-master `sTbName` | 所属 service |
  67 +|---|---|---|---|
  68 +| 系统模块配置 | `/xtmkpz` | `gdsmodule` | `GdsmoduleServiceImpl` |
  69 +| 数据表内容配置 | n/a | `gdsconfigtbmaster`/`slave` | `GdsconfigtbServiceImpl` |
  70 +| 界面显示内容配置 | n/a | `gdsconfigformmaster`/`slave`/`customslave`/`personalize` | `GdsconfigformServiceImpl` |
  71 +| 接口自定义配置 | `/sjbnrpz` | `sysapi` 表族 | xlyApi 侧管理接口 |
  72 +| 系统常量配置 | `/xtclpz` | `gdsformconst` | `GdsformconstServiceImpl`,切片 1 锚点 |
  73 +| 系统权限配置 | n/a | `gdsjurisdiction` | `GdsjurisdictionServiceImpl` |
  74 +| 常用操作配置 | n/a | 当前 dev DB 中没有对应 `gdsconfigformmaster` 行;页面是 SPA 中直接接线的管理特例。若通过元数据扩展,数据位于用户自定义按钮组层。 | n/a |
  75 +| 用户信息配置 | n/a | `sftlogininfo` 表族 | `GdslogininfoServiceImpl` |
  76 +| Mysql脚本配置 | n/a | BACK 对 [`templesql/` 脚手架](../reference/maintainer/sql-templates.md)的编辑器 | `SqlScriptsServiceImpl` |
  77 +| 图表配置 | 无 `gdsroute` 项;通过 SPA state 导航 | `gdsconfigcharmaster`/`slave` | `GdsconfigformServiceImpl`(图表子集) |
  78 +
  79 +10 项里有 8 项是本目录和[维护人员参考](../reference/maintainer/management-services.md)覆盖的框架原语。**`常用操作配置` 是 SPA 侧管理特例**:它出现在侧边栏,但 dev DB 中没有对应的 `gdsconfigformmaster` 行,说明该页在 BACK 中硬编码,而不是元数据驱动。**`图表配置`** 则完全由元数据驱动:两条 `gdsconfigformmaster` 行分别指向 `gdsconfigcharmaster` 和 `gdsconfigcharslave`,其中的图表定义由 SPA 其他位置的看板渲染消费。
  80 +
  81 +## 框架原语之外:xlyEntry 的其余接口面
  82 +
  83 +`xlyEntry` 总共托管 **70 个 controller**。其中 18 个属于框架侧:上面明确列出的通用运行时 controller,加上支撑 [BACK 配置侧边栏](#back-builder-sidebar-admin-surface) 的 7 个 `systemweb/` 管理 controller(`GdsformconstController`、`GdsjurisdictionController`、`GdslogininfoController`、`GdsparameterController`、`LicenseController`、`LoginController`、`SysbrandsController`)。每个元数据驱动表单的生命周期都在这些接口面里。
  84 +
  85 +剩余 52 个 controller 存在,是因为框架的通用 CRUD + 存储过程分发路径**不足以表达对应用例**。换句话说,每一个都是数据驱动论点停止扩展的位置标记。
  86 +
  87 +即使它们不属于框架目录化接口面,也值得枚举:它们展示了 xly 哪些内容硬编码在 Java 里,哪些内容留给元数据。维护人员读这些 controller,能直接看到框架逃生口的形状。
  88 +
  89 +> **命名空间重叠。** `BusinessBaseController`(`/business/*`)和 `QuoquotationController`(同样 `@RequestMapping("/business")`)共享 URL 前缀。Spring 会按方法级路径解析,所以 `/business/addQuotationsheet` 和 `/business/getQuoquotationProgress` 落在 `QuoquotationController`,其他 `/business/*` 端点落在 `BusinessBaseController`。当前没有方法路径冲突,因此能正常工作;但这个约定很容易踩坑:未来如果有人在 `BusinessBaseController` 里新增 `@PostMapping("/addQuotationsheet")`,就会悄悄遮蔽报价路径。
  90 +
  91 +### 表单辅助与 SPA 扩展 controller(22)
  92 +
  93 +这些是贴近框架的端点,是通用 CRUD 路径的扩展,但不适合放进 form-master / form-slave 的固定形状里。多数会在同一个 SPA 页面里和 `/business/*` 一起被调用。
  94 +
  95 +| 端点前缀 | Controller | 角色 |
  96 +|---|---|---|
  97 +| `/bill/*` | `BillController` | 复制单据操作(`billCopyToCheck`、`billCopyToCheckWork`):把一组 master + slave 行克隆成新单据。它不放在 `/business/*` 下,是因为 SPA 需要在一次请求里拿到新 `sId` 集合,而通用保存端点没有这种响应形状。 |
  98 +| `/change/*` | `ChangeController` | 通用字段变更重算(`changeParam`):字段值触发派生重算且需要调用存储过程时,SPA 会调用它。 |
  99 +| `/parameter/*` | `BusinessParameterController` | 每模块参数读取。 |
  100 +| `/treeclassify/*` | `BusinessTreeClassifyController` | 表单数据加载的树分组变体:`/treeclassify/getTreeClassify/{gdsconfigformmasterId}`。它位于 `/business/*` 之外,是因为响应形状是嵌套树,不是平铺行集。 |
  101 +| `/calcprocedure/*` | `CalcProcedureController` | [计算公式](../reference/builder/define-vtable.md) 功能的运行时侧:`/calc` 调用命名计算过程。 |
  102 +| `/calculationFormula/*` | `CalculationFormulaController` | 计算公式的构建侧元数据。 |
  103 +| `/calculationStd/*` | `CalculationStdController` | 标准计算查找目录。 |
  104 +| `/char/*` | `CharController` | BACK 图表配置页面的图表配置 CRUD,包装 `gdsconfigcharmaster` / `slave`。 |
  105 +| `/checkModel/*` | `CheckmodelController` | 审批模型成员读取(`getUserListByModelId/{sCheckModeId}`),供轻量级(非 Activiti)审批流使用。 |
  106 +| `/comparatorTree/*` | `ComparatorTreeController` | 可过滤层级选择器使用的比较树读取。 |
  107 +| `/excel/*` | `ExcelController` | 网格 **Excel 导出**:`/export/{gdsconfigformmasterId}`。它是打印的兄弟路径,但输出数据而不是报表版式。 |
  108 +| `/import/*` | `ImportExcelController` | Excel **导入**:先 `/checkExcel` 校验,再提交。插入前会按表单 slave 定义验证。 |
  109 +| `/filterTree/*` | `FilterTreeController` | 网格过滤用树形下拉。 |
  110 +| `/notClear/*` | `NotClearController` | 条码扫描“未清”保存路径(`doNotClearSave`、`getNotClearScanData/{sProcName}/{sId}`),特定于扫描驱动的仓库流程。 |
  111 +| `/notice/*` | `NoticeController` | 站内通知获取 / 标记已读。 |
  112 +| `/replaceField/*` | `ReplaceFieldController` | 跨行批量字段替换。 |
  113 +| `/searchgroupby/*` | `SearchgroupbyController` | 带 group-by 的保存搜索定义。 |
  114 +| `/searchupdown/*` | `SearchUpDownController` | 按搜索结果上一条 / 下一条导航,是 `/business/getBusinessDataByIndex` 的变体。 |
  115 +| `/syssearch/*` | `SyssearchController` | 保存搜索定义 CRUD。 |
  116 +| `/syssystem/*` | `SyssystemController` | 系统表读取专用的 `getBusinessDataByFormcustomId` 变体(`/getSyssystemDataByFormcustomId/{gdsconfigformmasterId}`),用于全局元数据读取时绕过租户作用域。 |
  117 +| `/sqlfile/*` | `SqlFileController` | “Mysql脚本配置”管理页面背后的 SQL 文件加载 / 保存。 |
  118 +| `/instruct/*` | `InstructController` | 直接执行 SQL 的端点(`/exesql`、`/opensql`):管理侧查询控制台。 |
  119 +
  120 +### 用户与权限管理(4)
  121 +
  122 +同一关注点下有多个重叠 controller。`New` 后缀,以及 `sftlogininfo`、`userinfo`、`gdslogininfo`(在 `systemweb/` 中)同时存在,说明这里有一次迁移中的重构:旧路径和新路径会并存,直到调用方迁移完成。
  123 +
  124 +| 端点前缀 | Controller | 角色 |
  125 +|---|---|---|
  126 +| `/userinfo/*` | `UserInfoController` | 当前用户资料与 session 信息。 |
  127 +| `/sftlogininfo/*` | `SftlogininfoController` | 用户账号 CRUD(两条路径中较新的一个)。 |
  128 +| `/sysjurisdiction/*` | `SysjurisdictionController` | 用户组 / 用户权限读取(`getGroupData`、`getUserData`)。 |
  129 +| `/sysjurisdictionNew/*` | `SysjurisdictionNewController` | 较新的并行路径(`getGroupDataNew`、`getGroupUserIdNew/{sUserId}`)。关注点相同,形状不同。 |
  130 +
  131 +### 生产 / MES(7)
  132 +
  133 +这些是行业层流程;它们的状态机、多表连接或硬件集成无法只靠 `gdsconfigformmaster` SQL 表达。这是框架中硬编码业务逻辑最集中的一组。
  134 +
  135 +| 端点前缀 | Controller | 角色 |
  136 +|---|---|---|
  137 +| `/sysworkorder/*` | `WorkOrderController` | 带副作用的工单 CRUD(`/add`、`/update/{sId}`):印刷行业工单会跨很多 slave 和存储过程调用,通用保存端点无法原子串起这些操作。 |
  138 +| `/workOrderFlow/*` | `WorkOrderFlowController` | 工单工艺路线 / 流程读取(`getWorkOrderFlow`、`getSplitWorkOrderData/{sId}`)。 |
  139 +| `/workOrderPlan/*` | `WorkOrderPlanController` | 连接工单与计划行的生产计划读取(`getControlProcess/{sProductionPlanId}`、`getProductionPlanInfo`)。 |
  140 +| `/splitWorkOrder/*` | `SplitWorkOrderController` | 把一个主工单拆成多个子工单(`getSplitWorkOrderData`)。 |
  141 +| `/productionPlan/*` | `ProductionPlanController` | 计划树读取(`getProductionPlanTree`)。 |
  142 +| `/process/*` | `ProcessController` | 生产工序目录读取。 |
  143 +| `/oee/*` | `OeeController` | OEE(Overall Equipment Effectiveness,设备综合效率):条码扫描和 MES 状态回调(`updateBarcode/{sBarCodeId}/{sBarCode}`、`doSysMesMsg/{sStatus}/{sMachineId}`)。 |
  144 +
  145 +### 销售、库存、财务、采购、人事(9)
  146 +
  147 +| 端点前缀 | Controller | 角色 |
  148 +|---|---|---|
  149 +| `/salesorder/*` | `SalesOrderController` | 超出通用 CRUD 的销售订单专用逻辑。 |
  150 +| `/business/addQuotationsheet`、`/business/getQuoquotationProgress` | `QuoquotationController` | 报价单创建(长运行,因此有进度端点)。**注意:**它和 `BusinessBaseController` 共享 `/business/*` 前缀,见上面的命名空间重叠说明。 |
  151 +| `/eleMaterialsStock/*` | `EleMaterialsStockController` | 原材料库存读取(`getEleMaterialsStock`、`getEleMaterialsStoreCurrQty`)。 |
  152 +| `/eleProductStock/*` | `EleProductStockController` | 成品库存读取。 |
  153 +| `/costCenter/*` | `CostCenterController` | 成本中心数据与凭证导入(`getCostCenterData`、`getCosvoucherImportData`)。 |
  154 +| `/sysAccountPeriod/*` | `SysAccountPeriodController` | 会计期间启用 / 关闭逻辑。 |
  155 +| `/erpOrderProcurement/*` | `ErpOrderProcurementController` | 采购订单专用逻辑。 |
  156 +| `/sisproductclassify/*` | `SisproductclassifyController` | 产品分类树。 |
  157 +| `/eleteamemployee/*` | `EleteamemployeeController` | 车间流程中的班组 / 员工分配。 |
  158 +
  159 +### 集成与硬件(5)
  160 +
  161 +| 端点前缀 | Controller | 角色 |
  162 +|---|---|---|
  163 +| `/file/*` | `FileController` | 文件上传,包括微信移动端变体 `mobileuploadwechat`。 |
  164 +| `/plc/*` | `PlcController` | PLC 桥接入口(`getplcMachine/{iOrder}/{sParentId}`),见[切片 6](../slices/06-hardware.md)。 |
  165 +| `/mobilephone/*` | `MobliePhoneController` | 移动 App 端点。(类名中的 `Moblie` 拼写错误来自源码;URL 是 `/mobilephone`。) |
  166 +| `/sysWebsocket/*` | `SysWebSocketController` | 推送通知的 WebSocket 建立 / 关闭。 |
  167 +| `/wechat/*` | `WechatController` | 微信集成(站内二维码、OAuth 回调)。 |
  168 +
  169 +### 按[首页范围说明](../index.md)排除(5)
  170 +
  171 +为完整性列出;它们不属于本框架 Wiki 的范围内接口面,但确实存在于 WAR 中。
  172 +
  173 +| 端点前缀 | Controller | 状态 |
  174 +|---|---|---|
  175 +| `/ai/*` | `AiController` | AI assistant。范围外(见 index.md)。 |
  176 +| `/robot/*` | `ChatGptController` | ChatGPT 集成。范围外(见 index.md)。 |
  177 +| `/test/*` | `TestController` | 开发脚手架(`/file`、`/getDinkToken`)。 |
  178 +| (根路径) | `TestProcessController` | 开发脚手架;没有类级 `@RequestMapping`。 |
  179 +| (已注释) | `XsController` | 死文件:`@RestController` 和 `@RequestMapping` 都被注释掉;类存在,但不会注册任何端点。 |
  180 +
  181 +### 把这些 controller 当作诊断线索
  182 +
  183 +扫完整个列表后,三个模式很明显:
  184 +
  185 +1. **长运行或多步骤事务**(`QuoquotationController.getQuoquotationProgress`、`BillController.billCopyToCheck`、`SplitWorkOrderController`):通用保存是一次性请求;任何需要进度端点,或“克隆后跳转”语义的流程,都需要自己的 controller。
  186 +2. **行业专用状态机**(`OeeController`、工单家族、`SysAccountPeriodController`):当“下一个合法状态”不能从单个列推出时,存储过程分发路径就不够用了,controller 会在 Java 中串起编排逻辑。
  187 +3. **硬件或外部系统**(`PlcController`、`WechatController`、`SysWebSocketController`、`FileController`):凡是不只是“MySQL + HTTP”的能力,都需要 Java 胶水;元数据无法描述入站 websocket 或串口握手。
  188 +
  189 +这些 controller 的职责,正好就是框架通用运行时没有覆盖的部分。
  190 +
58 191 ## 这个 API 不是什么
59 192  
60 193 - **不是稳定接口。** 端点形状会随框架变化。
... ...
zh/docs/api-reference/messaging.md
... ... @@ -4,25 +4,60 @@ xly 銝凋郊 HTTP 靚餈舅銝芣
4 4  
5 5 | Broker | | Producer | Consumer |
6 6 |---|---|---|---|
7   -| **ActiveMQ / JMS** | 蝻仃黎鈭辣頝荔蝻仃(../reference/maintainer/cache-invalidation.md)嚗 | `xlyErpJmsProductor` | `xlyErpJmsConsumer` |
  7 +| **ActiveMQ / JMS** | 黎鈭辣嚗蝖僎雿銵僎像霂Z”嚗誑 / **撠賜恣銝凋銝芷鈭 Redis 蝻仃**嚗楝敺仃(../reference/maintainer/cache-invalidation.md) | `xlyErpJmsProductor` | `xlyErpJmsConsumer` |
8 8 | **RocketMQ** | 隞 ActiveMQ 挽 | `RocketMQServiceImpl`嚗 `xlyBusinessService`嚗 | |
9 9  
10 10 憿萄瘛勗霂湔&蝸 `xlyErpJmsConsumer/src/main/java/com/xly/xlyerpjmsconsumer/` 銝 consumer thread 蝥批霈啣
11 11  
12   -## ActiveMQ / JMS嚗
  12 +## ActiveMQ / JMS嚗蝖僎 +
13 13  
14   -Producer 靘折ㄟ `xlyErpJmsProductor/src/main/java/com/xly/xlyerpjmsproductor/config/P2pQueue.java`雿輻蜓閬 destination嚗”霂瑁粉霂交辣嚗
  14 +Producer 靘折ㄟ `xlyErpJmsProductor/src/main/java/com/xly/xlyerpjmsproductor/config/P2pQueue.java` **24 銝 destination**嚗
  15 +
  16 +### 璅∪ / 嚗2嚗
  17 +
  18 +| Constant | |
  19 +|---|---|
  20 +| `ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` | 芋撌脣ConsumerChangeGdsModuleThread` 隡餈 `PRO_ERPMERGEBASEGDSMODULE`嚗 `gdsmodule` 銵僎像蝖霂Z”銝准**撠賜恣迨嚗 Redis 蝻**嚗edis 蝻 BACK 靽 `@CacheEvict` 郊摰仃(../reference/maintainer/cache-invalidation.md) |
  21 +| `ERP_JMS_ACTIVEMQ_CHANGE_WORK_ORDER_CONTROL` | 撌亙嚗 / 瘙餅鈭虜 |
  22 +
  23 +### 6嚗
15 24  
16 25 | Constant | |
17 26 |---|---|
18   -| `ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` | 芋撌脣圻 `ConsumerChangeGdsModuleThread` 皜 Redis 蝻仃(../reference/maintainer/cache-invalidation.md) |
19   -| `ERP_JMS_ACTIVEMQ_CHANGE_ELE_CUSTOMER` | 摰X銝餅 |
20   -| `ERP_JMS_ACTIVEMQ_CHANGE_ELE_EMPLOYEE` | 極銝餅 |
21   -| `ERP_JMS_ACTIVEMQ_CHANGE_ELE_MACHINE` | 頧阡銝餅 |
22   -| `ERP_JMS_ACTIVEMQ_UPD_SALE_ORDER`ERP_JMS_ACTIVEMQ_UPD_WORK_ORDER`ERP_JMS_ACTIVEMQ_UPD_PRODUCTION_REPORT` | 撌脫嚗 worker 瘨晶嚗恣虜憭望 |
23   -| `ERP_JMS_ACTIVEMQ_DEL_SALE_ORDER`ERP_JMS_ACTIVEMQ_DEL_WORK_ORDER`ERP_JMS_ACTIVEMQ_DEL_PRODUCTION_REPORT` | |
  27 +| `ERP_JMS_ACTIVEMQ_UPD_SALE_ORDER` / `_UPD_WORK_ORDER` / `_UPD_PRODUCTION_REPORT` | 撌脫嚗瘨晶恣虜憭望 |
  28 +| `ERP_JMS_ACTIVEMQ_DEL_SALE_ORDER` / `_DEL_WORK_ORDER` / `_DEL_PRODUCTION_REPORT` | |
  29 +
  30 +### 銝餅嚗7嚗CHANGE_ELE_*`
  31 +
  32 +| Constant | |
  33 +|---|---|
  34 +| `_CHANGE_ELE_CUSTOMER` | 摰X銝餅 |
  35 +| `_CHANGE_ELE_EMPLOYEE` | 極銝餅 |
  36 +| `_CHANGE_ELE_MACHINE` | 頧阡銝餅 |
  37 +| `_CHANGE_ELE_MATERIALS` | 蜓 |
  38 +| `_CHANGE_ELE_PRODUCT` | 鈭批蜓 |
  39 +| `_CHANGE_ELE_PROCESS` | 撌亙蜓 |
  40 +| `_CHANGE_ELE_TEAM` | 蝏蜓 |
  41 +
  42 +### 蝟餌縑 / 摮銵典嚗9嚗CHANGE_SIS_*`
  43 +
  44 +| Constant | |
  45 +|---|---|
  46 +| `_CHANGE_SIS_CUSTOMER_CLASSIFY` | 摰X掩 |
  47 +| `_CHANGE_SIS_DELIVER` | 揮撘 |
  48 +| `_CHANGE_SIS_FORMULA` | 霈∠撘 |
  49 +| `_CHANGE_SIS_PAYMENT` | 隞狡撘 |
  50 +| `_CHANGE_SIS_PROCESS_CLASSIFY` | 撌亙掩 |
  51 +| `_CHANGE_SIS_PRODUCT_CLASSIFY` | 鈭批掩 |
  52 +| `_CHANGE_SIS_SALES_MAN` | 鈭箏 |
  53 +| `_CHANGE_SIS_TAX` | 蝔 |
  54 +| `_CHANGE_SIS_WORK_CENTER` | 撌乩葉敹 |
  55 +
  56 +嚗洵銝撘”銋 constant 銝 `_圳嚗摮 `ERP_JMS_ACTIVEMQ_圳
  57 +
  58 +### Listener 靘
24 59  
25   -瘥葵 destination `xlyErpJmsConsumer/.../thread/` 銝笆摨 `Consumer*Thread` 蝐餃郊憭
  60 +`xlyErpJmsConsumer/.../consumer/Consumer.java` 頧** 24 銝 `@JmsListener` 瘜**蝐鳴葵 destination 銝銝芣瘜葵瘜蝸 `xlyErpJmsConsumer/.../thread/` 銝笆摨 `Consumer*Thread` 蝐鳴摰郊銵極雿虜靚銝銝 `PRO_ERPMERGEBASE*` 摮餈銵僎像蝖霂Z”嚗銝銝 listener 蝐餅 24 銝芣瘜**銝** 24 銝 listener 蝐颯onsumer thread 銝剜瓷 `@CacheEvict` `cleanRedis*`嚗edis 蝻仃 BACK 靽郊摰 [cache-invalidation.md](../reference/maintainer/cache-invalidation.md)
26 61  
27 62 ## RocketMQ嚗隞
28 63  
... ... @@ -30,9 +65,9 @@ Producer 靘折ㄟ `xlyErpJmsProductor/src/main/java/com/xly/xlyerpj
30 65  
31 66 ## 閫血仃
32 67  
33   -憒 SQL 摰瓷 JMS 鈭辣嚗蝻皜楝敺 `xlyBusinessService/.../service/impl/` 銝剔 `BusinessCleanRedisDataImpl`嚗隞亦仃辣摰頝臬仃(../reference/maintainer/cache-invalidation.md)
  68 +憒 SQL 摰瓷 BACK 靽楝敺蝻皜楝敺 `xlyBusinessService/.../service/impl/` 銝剔 `BusinessCleanRedisDataImpl`嚗隞亦靚皜瘜摰頝臬仃(../reference/maintainer/cache-invalidation.md)
34 69  
35 70 ## 餈葵銝隞銋
36 71  
37 72 - **銝撘** 憭銝 broker 嚗恥賑賑黎
38   -- **銝憭望銝撘** `xlyEntry` HTTP 楝敺歇蝏閬 JMS 鈭辣嚗閫血鈭器
  73 +- **銝蝻仃** `xlyEntry` HTTP 楝敺閬郊閫血 `@CacheEvict`嚗MS 提蝖僎郊撌乩★
... ...
zh/docs/api-reference/notifications.md
1 1 # 通知(xlyMsg)
2 2  
3   -出站通知(钉钉和微信)通过 `xlyMsg` 模块完成。它**不是**调用方直接访问的 HTTP 接口面,而是范围内服务在业务事件需要向聊天平台推送消息时调用的内部 SDK。
  3 +出站通知(钉钉、微信和邮件)通过 `xlyMsg` 模块完成。它**不是**调用方直接访问的 HTTP 接口面,而是范围内服务在业务事件需要向聊天平台或邮箱推送消息时调用的内部 SDK。
4 4  
5 5 ## 模块内容
6 6  
7 7 `xlyMsg/src/main/java/com/xly/`:
8 8  
9   -| Package | 角色 |
  9 +| | 角色 |
10 10 |---|---|
11 11 | `dingtalk/service/DingTalkService` + `dingtalk/util/SendDingTalkUtil`、`DingTalkMsgContentUtil`、`LocalCacheClient` | 钉钉企业消息分发。封装 `com.aliyun:dingtalk:2.1.14` 和 `com.aliyun:alibaba-dingtalk-service-sdk:2.0.0`。 |
12 12 | `wechat/service/WechatService` + `wechat/util/SendWxUtil`、`Wx_SignatureUtil`、`JedisMsgUtil`、`MsgContentUtil`、`Xml2JsonUtil` | 微信工作平台分发,包含签名和发送,以及 Redis 支撑的 access-token 缓存。 |
  13 +| `emial/service/SendEmailService` + impl | 邮件分发(包名拼成了 `emial`)。`xlyFlow` 的 `QuartzTask` 会用它发送定时任务邮件;`xlyEntry` 也有自己的 `com.xly.web.email.SendEmailService`,供 `ScheduledTasks` 驱动的邮件路径使用。两个接口名相同、实现并行保留,属于历史原因。 |
13 14 | `notice/service/NoticeService` | 与供应商无关的通知抽象;把“通知用户 X 某事件 Y”的逻辑路由到正确后端。 |
14 15  
15 16 `xlyMsg/build.gradle` 中唯一的框架依赖是 `xlyPersist`。该模块作为库被消费,不会作为自己的服务部署。
... ... @@ -18,12 +19,12 @@
18 19  
19 20 这些调用让 `xlyMsg` 成为框架相关内容,而不是 plat 层内容:
20 21  
21   -| Caller | 作用 |
  22 +| 调用方 | 作用 |
22 23 |---|---|
23 24 | `xlyEntry/.../web/businessweb/TestController.java` | 诊断端点,用于发送测试钉钉消息。 |
24   -| `xlyBusinessService/.../thread/UpdateDingTalkThread.java` | 单据变更后推送钉钉更新的异步 worker。 |
  25 +| `xlyBusinessService/.../thread/UpdateDingTalkThread.java` | 单据变更后推送钉钉更新的异步线程。 |
25 26 | `xlyBusinessService/.../service/impl/CheckExamineFlowServiceImpl.java` | 工作流审批通知;Activiti 任务被重新分配或完成时,待办人会收到聊天消息。 |
26   -| `xlyBusinessService/.../service/impl/GenericProcedureCallServiceImpl.java` | 通用存储过程 hook:任何选择接入的 `gdsmodule` 存储过程都可以通过这条路径发布通知。 |
  27 +| `xlyBusinessService/.../service/impl/GenericProcedureCallServiceImpl.java` | 通用存储过程钩子:任何选择接入的 `gdsmodule` 存储过程都可以通过这条路径发布通知。 |
27 28 | `xlyInterface/.../util/DingTalkUtil.java` + `scheduler/ScheduledTasks.java`、`ErpJobRunStatus.java` | 集成侧的定时任务心跳 / 失败告警。 |
28 29  
29 30 ## 配置
... ...
zh/docs/api-reference/webhooks.md
... ... @@ -10,30 +10,32 @@ http://&lt;host&gt;/xlyInterface/swagger-ui.html
10 10  
11 11 等价的 JSON 描述位于 `http://<host>/xlyInterface/v2/api-docs`。
12 12  
  13 +> **注意:** 项目拉取了 SpringFox jar,但没有注册 `Docket` bean(`xly-src` 中没有 `@EnableSwagger2`,也没有 `@Bean Docket api()`)。`swagger-ui.html` shell 会从 jar 的静态资源中被服务出来,但 `/v2/api-docs` 返回的描述基本为空。这里的 UI 是“依赖自带页面”,不是“已填充的 try-it-out 控制台”。如果维护人员希望列出真实 API,需要补一个 `Docket` bean。
  14 +
13 15 ## 数据驱动接收器:`/interfaceDefine/*`
14 16  
15 17 它和[外部 API 的 `/api/invoke`](external.md) 模式对应,但用于入站调用:
16 18  
17   -| Endpoint | Method | 用途 |
  19 +| 端点 | 方法 | 用途 |
18 20 |---|---|---|
19 21 | `/interfaceDefine/invoke/{interfaceInvoke}` | POST | 将入站载荷分发给 `{interfaceInvoke}` 命名的处理器。 |
20   -| `/interfaceDefine/callthirdparty/{interfaceInvoke}` | POST | 转发到已配置的出站端点。(对应 `xlyApi` 上同样存在的 `/interfaceDefine/callthirdparty/...` 端点;这里的入站侧和数据驱动入站分发器配套。) |
  22 +| `/interfaceDefine/callthirdparty/{interfaceInvoke}` | POST | 转发到已配置的出站端点。同一个 URL 在 `xlyApi` 的 `InterfaceController` 上也存在;这种重复是有意的,两个服务共享数据驱动分发器模式。 |
21 23  
22 24 处理器:`xlyInterface/src/main/java/com/xly/web/InterfaceController.java`。
23 25  
24   -`{interfaceInvoke}` path variable 会查找一条元数据行,该行声明要运行的 SQL 或存储过程、参数映射和响应形状。它和 xly 其他部分一样遵循数据驱动思路:新增入站端点靠插入数据行,而不是写 Java。
  26 +`{interfaceInvoke}` 路径变量会查找一条元数据行,该行声明要运行的 SQL 或存储过程、参数映射和响应形状。它和 xly 其他部分一样遵循数据驱动思路:新增入站端点靠插入数据行,而不是写 Java。
25 27  
26 28 ## 硬编码的供应商接收器
27 29  
28 30 少量接收器不会经过 `/interfaceDefine`,因为它们必须匹配合作方固定的 URL 规格。
29 31  
30   -| Endpoint | Method | 用途 |
  32 +| 端点 | 方法 | 用途 |
31 33 |---|---|---|
32 34 | `/Push` | POST | 供应商(微信 / 类似系统)推送接收器。 |
33 35 | `/Pull` | POST | 供应商拉取模式接收器。 |
34 36 | `/getKey/{key}` | GET | 按伙伴命名的 `key` 获取公钥。 |
35 37 | `/getKeyTest` | GET | `/getKey` 的测试模式变体。 |
36   -| `/send/sendQw` | POST | 企业微信出站消息。 |
  38 +| `/send/sendQw` | POST | 企业微信出站消息。**当前分支是空实现**:`SendQwController` 中方法体只是 `return "ok";`;已搭好 token 获取脚手架,但未完成。 |
37 39  
38 40 处理器:`xlyInterface/src/main/java/com/xly/web/WX_VendorWeb.java` 和 `xlyInterface/src/main/java/com/xly/wechat/test/SendQwController.java`。
39 41  
... ...
zh/docs/concepts/api-surface.md
... ... @@ -10,17 +10,26 @@ xly 不是只有一个 API,而是有**三层**,分别由三个独立的 Spri
10 10  
11 11 每个服务都会构建成自己的 WAR,并在自己的 JVM 中运行。它们不共享进程内状态;它们共享的是**数据库**。正是这个共享数据库让服务拆分成立:内部 API 写入的数据会自动被外部 API 读到,因为两者都连接同一个 schema。
12 12  
13   -## 为什么是三层,而不是一层
  13 +## 为什么是三层,以及拆分的代价
14 14  
15   -每一层回答的问题不同,合在一起会牺牲清晰度
  15 +每一层最初都是为了解答不同的问题
16 16  
17 17 - **内部层**很大(对所有元数据驱动模块做通用 CRUD)、易变(随框架变化)、且有意保持弱类型(SPA 决定要什么,服务端照元数据执行)。
18   -- **外部层**是收敛后的接口(只暴露允许集成方使用的端点),按 `sApiCode` 做版本化,并用 bearer token 认证。它能跨框架变化保持稳定,正是因为它小而明确。
  18 +- **外部层**是收敛后的接口(只暴露允许集成方使用的端点),按 `sApiCode` 做版本化,并用 bearer token 认证。
19 19 - **入站 webhook 层**接收来自第三方系统的不可信 body,并路由到 xly 处理器。Swagger UI 放在这里,因为这个受众最需要交互式文档。
20 20  
  21 +这个拆分有真实成本,Wiki 不应该略过:
  22 +
  23 +- **需要部署、监控、锁定版本的 WAR 有三个。** 一次发布必须协调 `xlyEntry`、`xlyApi`、`xlyInterface` 的构建。版本不匹配时(例如 `xlyEntry` 引入了 schema 变化,但 `xlyApi` 还没跟上),问题通常会静默存在,直到某条调用路径撞上它。
  24 +- **存在重复代码。** `RequestAddParamUtil` 在 `xlyPersist`(供 `xlyEntry` 使用)和 `xlyApi` 中各有一份,几乎是 56 行 / 57 行的拷贝。`InterfaceController` 在 `xlyApi` 和 `xlyInterface` 中也都存在,并且有重叠的 `/interfaceDefine/callthirdparty/*` 端点。让两边保持同步依赖运维纪律,不是编译期保证。
  25 +- **没有共享 session。** 在 BACK 登录的用户,在 `xlyApi` 中没有 session;外部调用方需要单独获取 bearer token。这对外部集成是正确的,但也意味着内部跨 WAR 调用如果发生,必须走公开 token 流程。
  26 +- **三个 context path 就意味着三条反向代理规则。** `BACK=:8597`、`FROUNT=:8598` 到具体 WAR 的映射在 nginx 配置中,不在本仓库里。代理配置错误是代码库本身捕捉不到的常见故障模式。
  27 +
  28 +这些层当然也可以做成一个部署物,只在内部用包边界隔离;Spring Boot 支持这种做法。那样的好处是:一次构建、一套依赖、一套 session 逻辑,也不会有重复工具类。代价是:各层更难独立扩容,也更难只限制外部调用方而不影响 SPA。xly 选择了部署期隔离;Wiki 的职责是把这个选择牺牲了什么写清楚。
  29 +
21 30 ## 每一层在运行时长什么样
22 31  
23   -- **内部层** — 见[四表读取](request-lifecycle.md)。一个端点(`/business/getModelBysId`)返回完整表单布局;另一个端点(`/business/addUpdateDelBusinessData`)写入元数据命名的任意表中的任意行。端点少,形状通用。
  32 +- **内部层** — 见[五键读取](../reference/maintainer/runtime.md#five-key-read)。一个端点(`/business/getModelBysId`)返回完整表单布局;另一个端点(`/business/addUpdateDelBusinessData`)写入元数据命名的任意表中的任意行。端点少,形状通用。
24 33 - **外部层** — 大多数调用走 `/api/invoke/{sApiCode}`。`sApiCode` 是 `sysapi` 元数据表中的一行,定义 SQL 模板、参数、认证要求和目标。新的外部 API 是**注册成数据**,不是写成代码;这和框架对自身表单采用的数据驱动基本论点一致。
25 34 - **入站 webhook 层** — `/interfaceDefine/invoke/{interfaceInvoke}` 接收载荷,查找元数据中的匹配处理器,运行配置好的 SQL 或存储过程。此外还有少量为特定伙伴准备的硬编码接收器(`/Push`、`/Pull`、`/send/sendQw`)。
26 35  
... ...
zh/docs/concepts/customization-channels.md
... ... @@ -15,7 +15,7 @@ xly 客户通过**两条不同路径**定制系统。理解区别很关键:它
15 15  
16 16 这些修改是**数据**。它们跟随客户数据库。它们在**后台**界面中可见,PM 可以审计。框架运行时在每次请求中读取它们(带缓存)。Java 代码不变;应用行为由这些行决定。
17 17  
18   -这是默认路径。**90% 以上客户定制都应该放在这里。**
  18 +这是架构希望客户优先使用的路径。至于真实比例是不是 90/10 偏向通道 1,代码库里没有统计;经验信号是 `script/客户/` 下已经有 18 个客户目录,说明有相当一部分客户需要通道 1 表达不了的东西。因此,“90% 以上应该在这里”更像目标,而不是实测事实。
19 19  
20 20 ## 通道 2 — 每客户 SQL 覆盖
21 21  
... ... @@ -40,7 +40,7 @@ xly 客户通过**两条不同路径**定制系统。理解区别很关键:它
40 40  
41 41 - 客户需要框架 Add/Update/Calc 过程无法表达的过程逻辑。
42 42 - 需要替换存储过程主体,而不是只在周围注入 SQL 片段。
43   -- 维护人员审查客户运行时时,需要能在源码控制的 SQL 文件中看到差异
  43 +- 运行时差异应放在源码控制的 `.sql` 文件中(`script/客户/<customer>/` 下),这样维护人员审查客户运行时时能一眼看到每客户变更,而不是只有连接到实时 DB 后才能发现
44 44  
45 45 通道 2 *几乎总是最后手段*。只有确认通道 1 做不到时才使用。
46 46  
... ...
zh/docs/concepts/customization-layers.md
... ... @@ -6,21 +6,33 @@
6 6  
7 7 ## 各层
8 8  
9   -```text
10   -gdsconfigformmaster ← 系统默认(表单)
11   - ↓ 被覆盖
12   -gdsconfigformpersonalize ← 每租户整表单覆盖
13   - (替换 sSqlStr/sWhere/sOrder)
14   - ↓ 然后是基础从表
15   -gdsconfigformslave ← 系统默认字段
16   - ↓ 被覆盖 / 扩展
17   -gdsconfigformcustomslave ← 每租户字段(新增、隐藏、覆盖)
18   - ↓ 可选地继续微调
19   -gdsconfigformuserslave ← 每用户视图偏好
20   - (列顺序、隐藏列)
  9 +```mermaid
  10 +flowchart TB
  11 + classDef sys fill:#e8f0fe,stroke:#4285f4
  12 + classDef tenant fill:#fef7e0,stroke:#fbbc04
  13 + classDef user fill:#f3e8fd,stroke:#a142f4
  14 +
  15 + M["gdsconfigformmaster<br/>系统默认:表单<br/>(sSqlStr · sWhere · sOrder)"]:::sys
  16 + P["gdsconfigformpersonalize<br/>每租户整表单覆盖<br/>(替换 sSqlStr / sWhere / sOrder)"]:::tenant
  17 + S["gdsconfigformslave<br/>系统默认字段"]:::sys
  18 + CS["gdsconfigformcustomslave<br/>每租户字段<br/>(按 sName 新增 · 隐藏 · 覆盖)"]:::tenant
  19 + US["gdsconfigformuserslave<br/>每用户视图微调<br/>(列顺序 · 隐藏列)"]:::user
  20 +
  21 + OUT["合并后的表单<br/>返回给 SPA"]
  22 +
  23 + M --> P
  24 + P --> S
  25 + S --> CS
  26 + CS --> US
  27 + US --> OUT
  28 +
  29 + M -. "总是加载" .-> OUT
  30 + P -. "租户有覆盖时加载" .-> OUT
  31 + CS -. "租户有覆盖时加载" .-> OUT
  32 + US -. "用户有偏好时加载" .-> OUT
21 33 ```
22 34  
23   -每一层都通过 `sParentId` 连接到上一层。没有任何连接由 FK 强制;见[无 FK 现实](semantic-fk.md)。
  35 +自上而下读这条链:**系统 → 租户 → 用户**。每一层都通过 `sParentId` 连接到上一层。没有任何连接由 FK 强制;见[无物理外键、语义外键的现实](semantic-fk.md)。
24 36  
25 37 ## 每层回答的问题
26 38  
... ... @@ -34,7 +46,7 @@ gdsconfigformuserslave ← 每用户视图偏好
34 46  
35 47 ## 合并如何发生
36 48  
37   -框架按顺序读取每层,并按 `sName`(字段名)合并。自定义从表行与基础从表拥有相同 `sName` 时:覆盖。新的 `sName`:追加。没有对应自定义行的基础从表:原样透传。合并发生在 `BusinessBaseServiceImpl.getModelBysId`(第 181 行)及其调用的 helper 中:`BusinessGdsconfigformsServiceImpl.getFormSlaveData` + `getFormCustomSlaveData`
  49 +框架按顺序读取每层,并按 `sName`(字段名)合并。自定义从表行与基础从表拥有相同 `sName` 时:覆盖。新的 `sName`:追加。没有对应自定义行的基础从表:原样透传。入口是 `BusinessBaseServiceImpl.getModelBysId`(第 181 行),它会调用 `BaseServiceImpl.getModelConfigByModleId`(第 55 行);真正的 slave + customslave 合并发生在 `BusinessGdsconfigformsServiceImpl.getGdsconfigformslaveShow`(第 392 行),组合 `getFormSlaveData`(第 87 行)和 `getFormCustomSlaveData`(第 121 行),随后可选叠加 `getUserFormSlaveData`(第 156 行)
38 50  
39 51 两个数据库**视图**通过连接 form-master 和相关 slave 表来支持合并:
40 52  
... ...
zh/docs/concepts/index.md
... ... @@ -24,7 +24,7 @@ flowchart TB
24 24 XMSG[/"xlyMsg<br/>库"/]
25 25 end
26 26  
27   - DB[("MySQL<br/>xlyweberp")]
  27 + DB[("MySQL<br/>xlyweberp_*")]
28 28 REDIS[(Redis)]
29 29 AMQ([ActiveMQ])
30 30 XEJMSC[xlyErpJmsConsumer]
... ... @@ -42,10 +42,13 @@ flowchart TB
42 42 XFLOW --> DB
43 43 XPLC --> DB
44 44  
45   - XENTRY <--> REDIS
46   - XENTRY -- "元数据变更" --> AMQ
  45 + XENTRY -- "保存时 @CacheEvict<br/>同步执行" --> REDIS
  46 + XENTRY <-- "缓存读取<br/>+ Shiro session" --> REDIS
  47 + XAPI <--> REDIS
  48 +
  49 + XENTRY -- "领域事件<br/>(不是缓存失效)" --> AMQ
47 50 AMQ --> XEJMSC
48   - XEJMSC --> REDIS
  51 + XEJMSC -- "PRO_ERPMERGEBASE*<br/>基础数据合并" --> DB
49 52  
50 53 XENTRY -. 使用 .-> XMSG
51 54 XIF -. 使用 .-> XMSG
... ... @@ -55,6 +58,10 @@ flowchart TB
55 58  
56 59 虚线簇(`xlyPlat*` + MongoDB)是 B2B 印刷平台层。它存在于构建中,但在本 Wiki 中[不属于覆盖范围](../index.md)。
57 60  
  61 +注意运行时到 Redis / ActiveMQ 有两条不同路径:**`@CacheEvict` 在保存流程中同步执行,直接清理共享 Redis 存储**(跨节点一致性依赖共享存储)。**JMS 路径是另一条基础数据合并通道**,不是缓存失效;`ConsumerChangeGdsModuleThread` 会运行 `PRO_ERPMERGEBASEGDSMODULE` 等过程。这两条路径在[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)中有完整说明。
  62 +
  63 +每个框背后的类库清单见[技术栈](../reference/maintainer/tech-stack.md)。
  64 +
58 65 ## 概念页
59 66  
60 67 这些页面刻意保持简短:每页解释一个概念,并链接到实际使用该概念的[垂直切片](../slices/index.md)。概念页应该短;如果一页长到超过一屏,通常说明它应该变成一个切片。
... ... @@ -65,6 +72,6 @@ flowchart TB
65 72 - [无物理外键、语义外键的现实](semantic-fk.md) — 关系实际如何工作。
66 73 - [两条定制通道](customization-channels.md) — 元数据编辑 vs. SQL 脚本。
67 74 - [定制层级](customization-layers.md) — 通道 1 内,基础 / 租户 / 用户覆盖如何合并。
68   -- [多租户与产品版本](multi-tenancy.md) — 三条作用域轴(`sBrandsId`、`sSubsidiaryId`、`sVersionFlowId`)
  75 +- [多租户与产品版本](multi-tenancy.md) — 行作用域(`sBrandsId`、`sSubsidiaryId`)加上许可证控制的模块发现
69 76 - [元数据驱动的请求生命周期](request-lifecycle.md) — 后续会反复回到这张图。
70 77 - [三层 API](api-surface.md) — 内部(`xlyEntry`)、外部(`xlyApi`)、入站 webhook(`xlyInterface`)。
... ...
zh/docs/concepts/master-slave.md
1 1 # 涓讳粠鍗曟嵁妯″紡
2 2  
  3 +> **杩欎釜浠g爜搴撻噷鏈変袱涓簰涓嶇浉鍏崇殑鈥滀富 / 浠庘濇蹇点**
  4 +> 鏈〉璁ㄨ鐨勬槸**鍗曟嵁琛**妯″紡锛氭姤浠枫侀攢鍞鍗曘佺敓浜у伐鍗曠瓑涓氬姟鍗曟嵁鐢 1 琛岃〃澶 + N 琛屾槑缁嗙粍鎴愩
  5 +> **DataSource** 灞傜殑 master / slave锛坄xlyApi` 鍜 `xlyInterface` 涓氳繃 `MasterDataSourceConfig` / `SlaveDataSourceConfig` 鍋氬啓搴 / 璇诲簱杩炴帴璺敱锛屽苟閰嶅杩欎簺鏈嶅姟鍐呯殑 `mastermapper/MasterBaseMapper.xml` / `slavemapper/SlaveBaseMapper.xml`锛夋槸鍙︿竴涓蹇碉紝瑙乕鎶鏈爤](../reference/maintainer/tech-stack.md#persistence)涓 HikariCP / 鏁版嵁婧愮浉鍏宠鏄庯紝涔熶細鍦ㄨ繍琛屾椂椤甸潰闂存帴娑夊強銆備袱鑰呭彧鏄悕瀛楅噸鍙犮
  6 +
3 7 xly 涓嚑涔庢墍鏈変笟鍔″崟鎹紝渚嬪鎶ヤ环鍗曘侀攢鍞鍗曘佸伐鍗曘佷粯娆惧嚟璇侊紝閮芥槸浠**涓琛岃〃澶 + N 琛屾槑缁**瀛樺偍銆倄ly 瀵规鐨勬湳璇槸 **master / slave**銆俶aster 淇濆瓨鍗曟嵁韬唤鍜屾眹鎬伙紱姣忎釜 slave 琛屾槸涓鏉℃槑缁嗐佷竴娈甸樁娈点佷竴椤规潗鏂欍佷竴涓骇鍝佹垨涓绗旇垂鐢ㄣ
4 8  
5 9 杩欎釜妯″紡鏃犲涓嶅湪锛
... ... @@ -29,9 +33,6 @@ xly 涓嚑涔庢墍鏈変笟鍔″崟鎹紝渚嬪鎶ヤ环鍗曘侀攢鍞鍗曘佸伐鍗曘佷粯
29 33  
30 34 ## 鈥淪lave鈥 鍛藉悕璇存槑
31 35  
32   -杩欎釜璇嶅湪鑻辨枃涓湁棰濆鍚箟锛岃屼腑鏂団滀富琛 / 浠庤〃鈥濇病鏈夈俉iki 淇濈暀 鈥渟lave鈥濓紝鍘熷洜鏄細
33   -
34   -1. 鏀瑰悕浼氱牬鍧忎唬鐮佸簱銆乻chema 鍜岃嚜鍔ㄧ洰褰曚腑鐨勫叏閮ㄤ氦鍙夊紩鐢紙14k+ 鏍囪瘑绗︼級銆
35   -2. 鎶婃瘡娆″嚭鐜伴兘鏄犲皠鎴 鈥渄etail鈥 鎴 鈥渃hild鈥 浼氭崯瀹冲彲鎼滅储鎬э紝骞朵娇 Wiki 鏂囨湰鍋忕寮鍙戣呭疄闄 grep 鍒扮殑鍐呭銆
  36 +杩欎釜璇嶅湪鑻辨枃涓甫鏈変腑鏂団滀富琛 / 浠庤〃鈥濇病鏈夌殑棰濆鍚箟銆俉iki 淇濈暀 鈥渟lave鈥 鍘熻瘝锛屾槸鍥犱负浠g爜搴撱乻chema 鍜岃嚜鍔ㄧ洰褰曚腑鏈 14k+ 鏍囪瘑绗﹂愬瓧浣跨敤瀹冿紱缈昏瘧鎴愬叾浠栬瘝浼氳鏂囨。鍋忕寮鍙戣呭疄闄 grep 鍒扮殑鍐呭銆
36 37  
37   -鏈潵 xly 鐗堟湰鍙兘鏀瑰悕涓 鈥渄etail鈥 / 鈥渉eader鈥濓紱鍦ㄦ涔嬪墠锛學iki 浣跨敤浠g爜搴撲腑鐨勫師璇嶏紝骞跺湪姝ゅ涓娆℃ц鏄庛
  38 +淇濈暀杩欎釜璇嶄篃鏈変唬浠枫傛渶鍒濆懡鍚嶆湰韬苟涓嶅ソ锛歚涓昏〃 / 浠庤〃` 瀹屽叏鍙互缈绘垚 `master / detail` 鎴 `header / line`锛屾棦绗﹀悎鑻辨枃寮鍙戜範鎯紝涔熸洿璐磋繎鍏崇郴璇箟銆傜户缁繚鐣 鈥渟lave鈥 鐨勬垚鏈紝浼氱敱姣忎釜闇瑕侀槄璇绘垨杈撳叆杩欎釜璇嶇殑鑻辨枃缁存姢鑰呮壙鎷咃紝涔熶細鐢辨湭鏉ヤ换浣曚竴娆″叏 schema 鏀瑰悕鎵挎媴銆俉iki 鍦ㄨ繖閲岃鏄庝竴娆★紝骞朵笉鑳芥秷闄ゆ垚鏈紝鍙槸鎶婂畠鏄庣‘鍐欏嚭鏉ャ
... ...
zh/docs/concepts/modules-forms-vtables.md
... ... @@ -54,3 +54,35 @@ gdsmodule.sFormId → (大多为空,历史字段)
54 54 ## 三个名词,一个引擎
55 55  
56 56 运行时(`BusinessBaseController` 和 `BusinessBaseServiceImpl`,见[切片 1](../slices/01-hello-world.md))知道如何渲染任意模块 / 表单 / 虚拟表组合。不存在每模块专用 Java 代码。PM 创建新模块是在创建新行,不是在创建新代码路径。
  57 +
  58 +## 业务数据表前缀
  59 +
  60 +本 Wiki 把业务模块当作示例,而不是章节主体;但 schema 的命名有规律。维护人员可以通过三字母前缀判断业务数据表所属领域:
  61 +
  62 +| 前缀 | 领域 | 示例表(实时数量) |
  63 +|---|---|---|
  64 +| `gds` | 框架元数据(模块、表单、字段、权限、参数、图表) | `gdsmodule`、`gdsconfigformmaster`、`gdsconfigformslave`、`gdsjurisdiction`、`gdsroute`、`gdsformconst`、`gdsparameter`、`gdsconfigcharmaster`/`slave`(BACK 图表配置使用的图表定义)等 |
  65 +| `sys` | 框架系统层(编号、授权、报表、搜索、账单设置),区别于 `gds*` 定义层 | `sysjurisdiction`、`sysbillnosettings`、`sysreport`、`syssearch`、`sysapi`、`syssystemsettings` 等(68 张表) |
  66 +| `sis` | 支撑下拉项的共享字典 / 分类表 | `sisbank`、`siscolor`、`sisversionflow`、`sisjurisdictionclassify` 等(80 张表) |
  67 +| `sft` | 登录 session / 用户组权限连接表 | `sftlogininfo`、`sftlogininfojurisdictiongroup` 等(8 张表) |
  68 +| `ele` | 主数据(element):客户、员工、机台、材料、产品、工序、半成品、成本框架 | `elecustomer*`、`eleemployee*`、`elemachine*`、`elematerials*`、`eleproduct*` 等(89 张表) |
  69 +| `mft` | 制造:工单、生产计划、生产报工 | `mftworkordermaster`、`mftproductionplan*`、`mftproductionreport*` 等(82 张表) |
  70 +| `sal` | 销售 | `salsalesordermaster`、`salsalesorderslave`、`salsalesorderprocess` 等(67 张表) |
  71 +| `quo` | 报价 | `quoquotationmaster`、`quoquotationslave`、`quoquotationcalc_tmp` 等(23 张表) |
  72 +| `acc` | 会计 / 成本 | `accordercostanalysis`、`accordercostanalysisoperation` 等(31 张表) |
  73 +| `pur` | 采购 | `purpurchaseapply`、`purpurchasearrive`、`purpurchasechecking` 等(28 张表) |
  74 +| `ops` | 外协 / 外发加工 | `opsoutsidearrive`、`opsoutsidechecking`、`opsoutsideinstore` 等(23 张表) |
  75 +| `cah` | 出纳 / 财务 | `cahcashierinit`、`cahcostchangemaster`、`cahpaymentmaster`、`cahreceiptmaster` 等(22 张表) |
  76 +| `sgd` | 半成品 | `sgdsemigoodscheck`、`sgdsemigoodsinstore`、`sgdsemigoodsmatchbill` 等(21 张表) |
  77 +| `ept` | 设备 / 机台固定资产 | `eptmachinefixedborrow`、`eptmachinefixedchange`、`eptmachinefixedinstore` 等(21 张表) |
  78 +| `mit` | 材料库存事务 | `mitmaterialsadjust`、`mitmaterialscheck`、`mitmaterialsinstore` 等(19 张表) |
  79 +| `pit` | 产品库存事务 | `pitproductadjust`、`pitproductbarcode`、`pitproductcheck`、`pitproductinstore` 等(18 张表) |
  80 +| `qly` | 质量检测 | `qlycomematerialstest`、`qlyproducttest`、`qlyprocesstest` 等(8 张表) |
  81 +| `kpi` | KPI 跟踪 | `kpimaster`、`kpidetail`、`kpimoduleuserday` 等(7 张表) |
  82 +| `udf` | 自定义 / 通用凭证框架 | `udfaccountno`、`udfvouchermaster`、`udfvouchertemplatemaster` 等(5 张表) |
  83 +| `viw_` / `Viw_` | 数据库**视图**(schema 中大小写不一致) | `viw_mftworkorderprocess`、`viw_corebusinessreport`、`viw_accordercostanalysisnew` 等(共 311 个视图) |
  84 +| `plat_` | B2B 印刷平台层(按[首页](../index.md)说明属于范围外) | 92 张表;本 Wiki 不展开 |
  85 +| `ai_` | AI / LLM 功能(范围外) | 7 张表;本 Wiki 不展开 |
  86 +| `act_`、`qrtz_` | 第三方 schema(Activiti 工作流、Quartz 调度) | 在 [Activiti](../reference/maintainer/activiti.md) 和[技术栈 Quartz](../reference/maintainer/tech-stack.md)中间接覆盖 |
  87 +
  88 +业务领域前缀(`ele`、`mft`、`sal`、`quo`、`acc`、`pur`、`ops`、`cah`、`sgd`、`ept`、`mit`、`pit`、`qly`、`kpi`、`udf`)及其从表都走同一套元数据驱动运行时:没有按前缀区分的 Java 代码,只有 `gdsconfigformmaster` / `gdsconfigformslave` 中指向各支撑表或视图的行。
... ...
zh/docs/concepts/multi-tenancy.md
... ... @@ -10,11 +10,11 @@ xly 憭 SaaS憟誨憟摨 schema
10 10 |---|---|---|---|
11 11 | **`sBrandsId`**嚗極D嚗 | 銝銵 | 瘥 | session嚗UserInfo.getsBrandsId()`嚗 |
12 12 | **`sSubsidiaryId`**嚗ID嚗 | 銝銵 | 瘥 | session |
13   -| **`sVersionFlowId`**嚗瘚D嚗 | 隞 `gdsmodule` | 瘥芋 | 撅漣嚗笆摨 `sisversionflow`嚗 |
  13 +| **`sVersionFlowId` / `sVersionFlowCode`**嚗瘚D / code嚗 | 隞 `gdsmodule` | 瘥芋倌 | 敶嚗閫找蝙霈詨霂漣 `sVerifyLicense` 璅∪” |
14 14  
15   -摰 DB 銝剜虜sBrandsId` 1,009 撘” / 閫銝sSubsidiaryId` 1,008 撘” / 閫銝銵典銵券撣行賑
  15 +銝銵其葉虜銵典撣行 `sBrandsId` 銝 `sSubsidiaryId`之憭獢銵其蒂舅銝芸”嚗gdsformconst`gdsmodule`gdsconfigformmaster`gdsconfigformslave`嚗&靘BusinessBaseServiceImpl.sTableNameList`嚗162-169 銵賑蛹閬摮”1078-1084 銵”頧質銝剖 `sBrandsId` / `sSubsidiaryId`蝙銝剖賑靽恥鈭怎蝏蝘潦靘圾銝綽銵誨銵函停舅銝芸賑 session 憛怠
16 16  
17   -瘥芋 gating嚗sVersionFlowId`嚗 **3** 撘”銝僎銝 `gdsmodule` 摰銵剁憭舅撘憭遢迨 gating 璅∪畾萇甈⊥扯誘嚗瘥
  17 +瘥芋摮鈭 `gdsmodule` 銝餈撟嗡 `sVersionFlowId` 餈誘嚗芋霈詨霂漣 `sVerifyLicense` 捂璅∪”迨璅∪畾蛛銝甈⊥扯誘嚗瘥
18 18  
19 19 ## 憒撩銵
20 20  
... ... @@ -34,6 +34,13 @@ RequestAddParamUtil.me().addParams(params, userInfo);
34 34 2. **摨** `viw_*` 閫隡蜓蝖銵典蒂蝘憒停輕摰∟恣摨扇甇斤掩閫
35 35 3. **靽垢**嚗addUpdateDelBusinessData`嚗捂垢 payload 銝剔 `sTable`銝撉砲銵冽撅”嚗停 1](../slices/01-hello-world.md#4-user-edits-a-row-clicks-save)
36 36  
37   -## 銝箔銋葵摰 DB 絲敺
  37 +## 餈葵霈曇恣憒撅誑撅
38 38  
39   -`xlyweberp_saas_ai` **銝銝**sBrandsId = '1111111111'`嚗**銝銝**摮嚗誑**銝銝**嚗8S_001 / 蝖嚗撌脩憟踝瓷蝙漣蝘隡葵葵葵摮嚗誑葵嚗挽霈⊿撅誨撅
  39 +獢霈曇恣**銵**撅誨撅銝芸銝芸蝘 SaaS 蝵莎葵葵摮葵蝵莎蝙憟 JavayBatis mapper 餈榆撘雿 `gdsmodule`sisversionflow` 銵其葉
  40 +
  41 +撅輕銝銋&嚗
  42 +
  43 +- **鈭怎 schema 鈭怨** 霂a銝 MySQL 摰銵典蝝W A 銵其 B buffer pool CPU瓷氖韏
  44 +- **瘥葵 WHERE 閬蒂蝘餈誘** 瘥霂餅霂a閬撣 `sBrandsId = ? AND sSubsidiaryId = ?`揣撘虜銋誑餈憭湔嚗ly 憭批銵冽漲摰輕鈭箏憓揣撘敹◆霈啣霂Z恣餈僎銵典之
  45 +- **瘝⊥′颲寧** 蝘銝瑪銝 drop 銝銝芣摨扇 `bInvalid`嚗◤嚗銝偶銋宏閬 GDPR / 撽餌漲葵蝘撌脩蝠摨仃霂
  46 +- **`sBrandsId` / `sSubsidiaryId` everywhere ** 移蝖桀蛹 `(sBrandsId, sSubsidiaryId)` 餈葵鈭隞嚗葵璅∪閬僎銵葵璅∪砲敶Y瘞貉恥嚗殿銝銝銝芰′霂箝
... ...
zh/docs/concepts/request-lifecycle.md
... ... @@ -7,27 +7,37 @@
7 7 ```text
8 8 Browser
9 9 1. 任意 URL 加载 SPA shell(服务端对每个路径返回同一 shell;
10   - gdsroute 是客户端侧边栏 / deep-link 白名单,不是服务端 404 gate
  10 + gdsroute 是客户端侧边栏 / deep-link 白名单,不是服务端 404 闸门
11 11 2. 用户点击侧边栏项或在 SPA 内导航
12 12 3. SPA 决定加载哪个模块 → 调用 /business/...
13 13  
14   -GET /xlyEntry/business/getModelBysId/{moduleId}?sModelsId={moduleId}
  14 +GET /xlyEntry/business/getModelBysId/{sModelsId}?sModelsId={sModelsId}
  15 +(模块 id 同时出现在 path 和 query 中;controller 绑定路径变量,
  16 +但 service 从 @RequestParam map 读取 sModelsId,所以 SPA 也必须在 query string 中传它)
15 17  
16 18 xlyEntry — BusinessBaseController.getModelBysId()
17 19 RequestAddParamUtil.addParams(params, userInfo)
18 20 → sBrandsId、sSubsidiaryId、sUserId、sLanguage 等
19   - → 租户作用域进入所有下游查询
  21 + → 共 16 个 key(详见 runtime.md)
  22 + → 租户作用域变成下游查询可用的参数。框架元数据读取按 form-id 过滤;
  23 + 每租户覆盖与业务数据读取才按 sBrandsId / sSubsidiaryId 过滤。
20 24  
21 25 BusinessBaseService.getModelBysId(map)
22   - ├── 加载 gdsmodule 行(模块)
23   - ├── 加载 gdsconfigformmaster 行(通过 sParentId 连接模块)
24   - ├── 加载 gdsconfigformslave 行(通过 sParentId 连接 form-master)
25   - ├── 合并 gdsconfigformpersonalize(每租户)
26   - ├── 合并 gdsconfigformcustomslave(每租户)
27   - ├── 加载 gdsjurisdiction(ADMIN 跳过)
28   - ├── 加载 gdsformconst(表单级常量)
29   - ├── 加载 sysbillnosettings(单据编号)
30   - └── 加载关联到该表单的 sysreport 行
  26 + ├── 1. formData
  27 + │ └── gdsconfigformmaster(按 sParentId = sModelsId 过滤;
  28 + │ gdsmodule 本身不被 SELECT,只通过 id 引用)
  29 + │ + LEFT JOIN gdsconfigformpersonalize(每租户覆盖)
  30 + │ + 每个 master 的 gdsconfigformslave
  31 + │ + 每个 master 的 gdsconfigformcustomslave(每租户覆盖)
  32 + ├── 2. gdsformconst(仅按 sParentId 过滤;不按租户过滤;
  33 + │ sLanguage 决定返回哪一列标签)
  34 + ├── 3. sysjurisdiction(按用户 / 用户组授权,JOIN
  35 + │ sftlogininfojurisdictiongroup + sisjurisdictionclassify;
  36 + │ ADMIN 跳过。返回 map key 仍叫 `gdsjurisdiction`,
  37 + │ 这个名字有误导性:gdsjurisdiction 表是配置侧动作目录,
  38 + │ 不是这里读取的授权表)
  39 + ├── 4. sysbillnosettings(每租户、每表单)
  40 + └── 5. sysreport(每租户、每表单)
31 41  
32 42 返回一个复合 map:
33 43 { formData, gdsformconst, gdsjurisdiction, billnosetting, report }
... ... @@ -44,24 +54,83 @@ xlyEntry — BusinessBaseController.getBusinessDataByFormcustomId()
44 54 用户看到表格
45 55 ```
46 56  
  57 +## 同一流程的时序图
  58 +
  59 +上面的 ASCII 图强调执行顺序;下面的时序图强调**谁调用谁**。排查真实请求时,后者通常更有用。
  60 +
  61 +```mermaid
  62 +sequenceDiagram
  63 + autonumber
  64 + participant SPA as Browser SPA
  65 + participant CTRL as BusinessBaseController
  66 + participant SVC as BusinessBaseServiceImpl
  67 + participant FORMS as BusinessGdsconfigformsServiceImpl
  68 + participant DB as MySQL
  69 + participant REDIS as Redis (RedisCacheManager)
  70 +
  71 + SPA->>CTRL: GET /business/getModelBysId/{sModelsId}<br/>?sModelsId=...&Authorization=<bearer>
  72 + Note over CTRL: AuthorizationInterceptor.preHandle<br/>从 Redis 解析 UserInfo<br/>RequestAddParamUtil.addParams(16 个 key)
  73 +
  74 + CTRL->>SVC: getModelBysId(map)
  75 + Note over SVC: getModelConfigByModleId(继承自 BaseServiceImpl)<br/>编排每个 form-master 的主表单 + 从表加载
  76 + SVC->>FORMS: getFormmasterData / getGdsconfigformslaveShow<br/>(form-master + slaves + overlays)
  77 + REDIS-->>FORMS: cache hit?
  78 + FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize;每个 master 再读 gdsconfigformslave + gdsconfigformcustomslave
  79 + DB-->>FORMS: rows
  80 + FORMS-->>SVC: formData
  81 +
  82 + SVC->>FORMS: getFormconstData(只按 form-id,不按租户)
  83 + FORMS->>DB: SELECT ... gdsformconst WHERE sParentId=...
  84 + DB-->>FORMS: rows
  85 + FORMS-->>SVC: gdsformconst
  86 +
  87 + alt sUserType != ADMIN
  88 + SVC->>FORMS: getJurisdictionData(每用户授权)
  89 + FORMS->>DB: SELECT ... sysjurisdiction ⋈ sftlogininfojurisdictiongroup
  90 + DB-->>FORMS: rows
  91 + FORMS-->>SVC: gdsjurisdiction(map key;源表是 sysjurisdiction)
  92 + else ADMIN
  93 + Note over SVC: 跳过权限加载
  94 + end
  95 +
  96 + SVC->>FORMS: getBillnosettingData
  97 + FORMS->>DB: SELECT ... sysbillnosettings WHERE sFormId=... AND tenant
  98 + DB-->>FORMS: row
  99 + FORMS-->>SVC: billnosetting
  100 +
  101 + SVC->>DB: SELECT ... sysreport WHERE sFormId=... AND tenant
  102 + DB-->>SVC: report rows
  103 +
  104 + SVC-->>CTRL: composite Map(5 个 key)
  105 + CTRL-->>SPA: AjaxResult{code:1, dataset:{...}}
  106 +
  107 + SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/{formId}<br/>?sModelsId=...
  108 + Note over CTRL,SVC: 同一次 RequestAddParamUtil 注入<br/>随后使用每表单 sSqlStr / sWhere / sOrder
  109 + CTRL->>DB: 对表单支撑 table/view/proc 执行参数化 SELECT
  110 + DB-->>CTRL: rows
  111 + CTRL-->>SPA: dataset
  112 +```
  113 +
  114 +图中第 1 行和第 22 行是两个 HTTP 往返。中间全部是服务端工作,SPA 看不到。
  115 +
47 116 ## 五键复合结果
48 117  
49 118 `getModelBysId` 返回一个 Java `Map`,按顺序包含这些 key:
50 119  
51 120 | Key | 来源 | SPA 用途 |
52 121 |---|---|---|
53   -| `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave`(+ 覆盖) | 表单布局本身:每个字段、控件、标签、校验规则 |
54   -| `gdsformconst` | 按租户 + 语言作用域过滤的 `gdsformconst` 行 | 表单级常量:标签、默认值、下拉文本 |
55   -| `gdsjurisdiction` | 用户角色的 `gdsjurisdiction` 行 | 按钮和数据权限 |
56   -| `billnosetting` | 该模块的 `sysbillnosettings` 行 | 单据编号规则(工单号、报价单号) |
57   -| `report` | 关联到该表单的 `sysreport` 行 | 打印模板(jxls Excel、iText PDF) |
  122 +| `formData` | `gdsconfigformmaster`(按 `sParentId = sModelsId` 过滤)⋈ `gdsconfigformpersonalize`(每租户覆盖);每个 master 行再加载 `gdsconfigformslave` + `gdsconfigformcustomslave` 覆盖。`gdsmodule` 只通过 id 引用。 | 表单布局本身:每个字段、控件、标签、校验规则 |
  123 +| `gdsformconst` | 仅按 `sParentId` 过滤的 `gdsformconst` 行;不按租户过滤;`sLanguage` 决定返回哪一列标签 | 表单级常量:标签、默认值、下拉文本 |
  124 +| `gdsjurisdiction` | 用户的 `sysjurisdiction` 行,或通过 `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify` 读取用户组授权;ADMIN 跳过。map key 名称 `gdsjurisdiction` 有误导性:那张表是配置侧动作目录,不是这里读取的授权表。 | 按钮和数据权限 |
  125 +| `billnosetting` | 该模块的 `sysbillnosettings` 行(每租户) | 单据编号规则(工单号、报价单号) |
  126 +| `report` | 关联到该表单的 `sysreport` 行(每租户) | 打印模板(jxls Excel、iText PDF) |
58 127  
59 128 ## 不在这个生命周期中的内容
60 129  
61 130 - **保存路径。** 保存有自己的端点 `POST /business/addUpdateDelBusinessData`,把新增 / 更新 / 删除打包进一个请求。见[切片 1](../slices/01-hello-world.md#4-user-edits-a-row-clicks-save)。
62 131 - **打开已有行编辑的读取。** 与表格加载使用同一端点 `getBusinessDataByFormcustomId`,但 body 中带行 `sId`,handler 按请求一行还是多行分支。
63   -- **工作流步骤。** 如果模块有活动审批流(`bCheck = 1`、填充 `sVersionFlowId`、已部署 Activiti 流程),会插入额外步骤。当前 dev DB 没有这些数据;见[切片 7(暂缓)](../slices/07-workflow.md)。
64   -- **缓存失效。** **后台**修改元数据行时,JMS 消息会让所有运行节点失效缓存副本,即 `xlyErpJmsConsumer` 中的 `ConsumerChangeGdsModuleThread`。它在请求流之外,但紧邻请求流。
  132 +- **工作流步骤。** 如果模块有活动审批流(`bCheck = 1`、`gdsmoduleflow` 已配置、Activiti 流程已部署,并且 `ConstantUtils.bCheckflowCheck = true`),会插入额外步骤。当前 dev DB 没有这些数据;见[切片 7(暂缓)](../slices/07-workflow.md)。
  133 +- **缓存失效。** **后台**修改元数据行时,保存服务会同步调用 `BusinessCleanRedisData` / `CleanRedisServiceImpl`,从共享 Redis 中驱逐 Spring cache region。JMS 的 `ConsumerChangeGdsModuleThread` 是另一条基础数据合并通道,不是缓存失效。
65 134  
66 135 ## 其他切片覆盖的变体
67 136  
... ...
zh/docs/concepts/semantic-fk.md
1 1 # 无物理外键、语义外键的现实
2 2  
3   -`xlyweberp_saas_ai` schema 在任何 xly 自研表(`gds*`、`ele*`、`mft*`、`quo*`、`sal*`、`acc*` 等表族)上都有 **0 个触发器**和 **0 个外键约束**。确实存在的 41 个 FK 约束都在捆绑的第三方 schema 上:Activiti 表(`act_*`)36 个、Quartz 调度表(`qrtz_*`)5 个;框架自身运行时不会通过这些表做核心连接。在 901 张基础表中,框架依赖的元数据和业务数据连接全部只是约定。
  3 +`xlyweberp_saas_ai` schema 在任何 xly 自研表(`gds*`、`ele*`、`mft*`、`quo*`、`sal*`、`acc*` 等表族)上都有 **0 个触发器**和 **0 个外键约束**。确实存在的 FK 约束都在捆绑的第三方 schema 上:Activiti 的 `act_*` 表和 Quartz 的 `qrtz_*` 表;框架自身运行时不会通过这些表做核心连接。框架依赖的元数据和业务数据连接全部只是约定。
4 4  
5 5 这是一个有意的设计选择,继续阅读前必须理解。
6 6  
7 7 ## 为什么 xly 禁用 FK
8 8  
9   -架构上给出的两个原因都很务实
  9 +架构上给出的两个原因
10 10  
11 11 1. **批量写入性能。** 大量插入(工单计算、月结、批量导入)一次会写入几十万行。启用 FK 后,MySQL 会在插入时验证每一行引用;以 xly 的数据量,这会成为限制因素。
12   -2. **schema 迁移敏捷性。** xly 演进很快:新模块、新字段、新表。启用 FK 时,每次 schema 变更都必须考虑约束图;没有 FK 时,`CREATE TABLE` 或 `ALTER TABLE` 是局部操作。代价由运行时应用代码承担。
  12 +2. **schema 迁移敏捷性。** xly 演进很快:新模块、新字段、新表。启用 FK 时,每次 schema 变更都必须考虑约束图;没有 FK 时,`CREATE TABLE` 或 `ALTER TABLE` 是局部操作。
  13 +
  14 +这两个考虑都真实存在,但都不足以推出“整个 schema 零 FK”:
  15 +
  16 +- **批量写入性能**可以更精细地处理:批处理期间临时关闭约束(`SET FOREIGN_KEY_CHECKS = 0`),写完后重新打开并校验。xly 选择的是完全不建 FK,这意味着每次读取也都要相信临时过程校验,而不是数据库强制的完整性。
  17 +- **schema 迁移敏捷性**确实会因为没有 FK 而提高,但代价是每个引用检查都要挪到应用代码或存储过程里,甚至可能被遗漏。实践中,原本 FK 自动完成的完整性工作被复制到数百个存储过程中,而且没有编译期保证某个过程真的做了检查(见下面的失效模式)。
  18 +
  19 +更准确的说法是:系统用 **DB 强制完整性**换取了**写入时和 DDL 时的运维便利**。这笔交易带来的 bug 面(孤儿行、未发现的跨租户引用、几周后才浮现的完整性错误)会在系统每天运行时持续付费。
13 20  
14 21 ## 什么是“语义 FK”
15 22  
... ...
zh/docs/concepts/thesis.md
... ... @@ -10,22 +10,24 @@ xly 逧婿譯育嶌蜿搾シ**蜊穂ク莉」遐∝コ薙∝黒荳驛ィ鄂イ梧ッ丞ョ「謌キ陦御クコ逕ィ謨ー謐
10 10  
11 11 霑吩クェ隶セ隶。譛我ク我クェ蜀スョ謌先悽悟シ蠕玲遑ョ蜀吝譚・
12 12  
13   -1. **豈乗ャ。隸キ豎る隕∬ッサ蜿門謨ー謐ョ縲** 豈乗ャ。鬘オ髱「蜉霓ス閾ウ蟆題ッサ蜿門屁蠑陦ィgdsmodule`縲〜gdsconfigformmaster`縲〜gdsconfigformslave`縲〜gdsjurisdiction`我サ・蜿顔ァ滓姐霑サ、譚。莉カ縲りソ占。梧慮莨夂ァッ譫∫シ灘ュ假シ御スシ灘ュ俶悴蜻ス荳ュ譌カ霑吩コ幄ッサ蜿紋ク榊庄驕ソ蜈阪
14   -2. **schema 莨壽戟扈ュ閹ィ閭縲** 譁ー讓。蝮 = `gdsmodule` 荳ュ荳陦 + `gdsconfigformslave` 荳ュ 1 蛻ー 50 陦 + 荳荳ェ謾ッ謦大ョ噪迚ゥ逅。ィ磯壼クク謖牙黒謐ョ邀サ蝙具シ峨ょス灘燕螳樊慮 DB 譛 901 蠑蝓コ遑陦ィ帷函莠ァ遘滓姐譖エ螟壹
  13 +1. **豈乗ャ。隸キ豎る隕∬ッサ蜿門謨ー謐ョ縲** 豈乗ャ。鬘オ髱「蜉霓ス蝨ィ郛灘ュ俶悴蜻ス荳ュ譌カ莨夊キ台コ皮アサ譟・隸「啻gdsconfigformmaster`亥ケカ荳コ蛹ケ驟咲噪譏守サ。悟匠蜉 personalize / customslave 隕尠峨〜gdsformconst`縲〜sysjurisdiction`育畑謌キ謗域揀幄ソ泌屓 map key 蜿ォ `gdsjurisdiction`御スョ樣刔隸サ蜿也噪譏ッ `sysjurisdiction`妁DMIN 莨夊キウ霑シ峨〜sysbillnosettings`縲〜sysreport`縲りソ占。梧慮莨夂ァッ譫∫シ灘ュ假シ御スシ灘ュ俶悴蜻ス荳ュ譌カ霑吩コ幄ッサ蜿紋ク榊庄驕ソ蜈阪
  14 +2. **schema 莨壽戟扈ュ閹ィ閭縲** 譁ー讓。蝮 = `gdsmodule` 荳ュ荳陦 + `gdsconfigformslave` 荳ュ 1 蛻ー 50 陦 + 荳荳ェ謾ッ謦大ョ噪迚ゥ逅。ィ磯壼クク謖牙黒謐ョ邀サ蝙具シ峨る囂逹荳壼苅讓。蝮怜「槫刈悟渕遑陦ィ謨ー驥丈シ夂サァ扈ュ蠅樣柄帷函莠ァ遘滓姐騾壼クク豈泌ケイ蜃逧 dev schema 蟶ヲ譖エ螟夊。ィ悟屏荳コ螳「謌キ螳壼宛讓。蝮嶺シ夐柄譛溽蕗蝨ィ蜈ア莠ォ schema 荳ュ縲
15 15 3. **蜈ウ邉サ譏ッ郤ヲ螳夲シ御ク肴弍郤ヲ譚溘** 荳コ莠ァ閭ス蜥瑚ソ∫ァサ轣オ豢サ諤ァ遖∫畑螟夜醗蜷趣シ御サ `gdsconfigformmaster.sParentId` 蛻ー `gdsmodule.sId` 逧ソ樊磁御サ・蜿贋ク顔卆荳ェ邀サ莨シ霑樊磁碁蜿ェ譏ッ[隸ュ荵牙、夜醗](semantic-fk.md)縲ょュ、蜆ソ陦梧弍蜿ッ閭ス蟄伜惠逧
16 16  
17   -## 謾カ逶
  17 +## 霑吩クェ隶セ隶。蟶ヲ譚・逧蜉幢シ御サ・蜿頑ッ冗ァ崎蜉帷噪莉」莉キ
18 18  
19   -菴應クコ莠、謐「警ly 蠕怜芦
  19 +- **荳荳ェ莉」遐∝コ捺恪蜉。蜃蜊∽クェ螳「謌キ縲** 豈丈クェ螳「謌キ遘滓姐諡・譛芽蟾ア逧謨ー謐ョ陦鯉シ妍ava 螳悟逶ク蜷後や披*髯仙宛*螳ケカ荳崎隕尠謇譛牙ョ「謌キ縲Ascript/螳「謌キ/` 荳狗噪 18 荳ェ逶ョ蠖包シ郁ァー蛻援 5](../slices/05-customer-sql-override.md)牙ーア譏ッ謨ー謐ョ鬩ア蜉ィ隶セ隶。謦槫「咏噪菴咲スョ壼ス灘ョ「謌キ髴隕∽ク榊酔逧ソィ矩サ霎第慮娯懷黒荳莉」遐∝コ凪晏ーア蜿俶莠懷黒荳 Java 莉」遐∝コ + 荳謇ケ逕ア謨ー謐ョ蠎馴撕鮟俶価霓ス逧ョ「謌キ荳灘ア SQL窶昴
  20 +- **PM 荳榊頃逕ィ蠑蜿第慮髣エ蟆ア閭ス貍碑ソ帛コ皮畑縲** 莉紋サャ謇灘シ BACK縲∵キサ蜉讓。蝮励∝ョ壻ケ芽。ィ蜊輔∬ョセ鄂ョ譚剞御ク倶ク荳ェ逕ィ謌キ蜉霓ス譌カ蜊ウ蜿ッ逵句芦蜿伜喧縲や披*髯仙宛*PM 閭ス陦ィ霎セ逧ッ肴アシ悟叙蜀ウ莠 `gdsconfigformmaster` / `gdsconfigformslave` 蟾イ扈乗垓髴イ逧縲ら悄豁」譁ー逧蜉幢シ郁螳壻ケ芽ョ。邂励撼譬譬。鬪後∽ク榊酔菫晏ュ倩キッ蠕シ我サ咲┯髴隕∝ュ伜お霑ィ具シ御ケ溷ーア驥肴眠髴隕∝キ・遞句ク茨シ悟宵譏ッ蠑蜿台ス咲スョ莉 Java 謐「謌蝉コ SQL縲よイ。譛 DB 隶ソ髣ョ譚噪 PM 荵溷セ磯埓蛻、譁ュ荳谺。蜈焚謐ョ謾ケ蜉ィ荳コ莉荵井コァ逕滉コ漠隸ッ霎灘悟屏荳コ霑ィ倶セァ騾サ霎大惠 BACK 荳ュ荳榊庄隗√
  21 +- **螳壼宛蜿ッ莉・窶懷ケイ蜃蝨ー窶晏螻**蛻援 4](../slices/04-custom-field.md)会シ壽ッ冗ァ滓姐隕尠蜿蜉蝨ィ蜈ア莠ォ蝓コ遑荵倶ク奇シ御ク埼怙隕 fork縲や披*髯仙宛*霑咏ァ榊ケイ蜃荳サ隕∵弍 Java 霑占。梧慮隗ァ剃ク狗噪縲ABusinessBaseServiceImpl` 荳ュ逧粋蟷カ騾サ霎第悽霄ォ蟷カ荳咲ョ蜊包シ3,900 螟夊。鯉シ会シ帶賜譟・窶應クコ莉荵郁ソ吩クェ遘滓姐閭ス逵句芦蟄玲ョオ X縲∫恚荳榊芦蟄玲ョオ Y窶晄慮碁怙隕∬ソス `gdsconfigformpersonalize`縲〜gdsconfigformcustomslave`縲〜gdsconfigformuserslave` 逧サ粋蜈ウ邉サ縲り御ク碑ヲ尠螻ゆク崎 `ALTER TABLE`帷悄豁」譁ー蠅樒黄逅莉咲┯髴隕∝刻隹 schema 霑∫ァサ縲
20 22  
21   -- **荳荳ェ莉」遐∝コ捺恪蜉。蜃蜊∽クェ螳「謌キ縲** 豈丈クェ螳「謌キ遘滓姐諡・譛芽蟾ア逧謨ー謐ョ陦鯉シ妍ava 螳悟逶ク蜷後
22   -- **PM 荳榊頃逕ィ蠑蜿第慮髣エ蟆ア閭ス貍碑ソ帛コ皮畑縲** 莉紋サャ謇灘シ**蜷主床**縲∵キサ蜉讓。蝮励∝ョ壻ケ芽。ィ蜊輔∬ョセ鄂ョ譚剞御ク倶ク荳ェ逕ィ謌キ蜉霓ス譌カ蜊ウ蜿ッ逵句芦蜿伜喧縲
23   -- **螳壼宛蜿ッ莉・蟷イ蜃蝨ー蛻ア**蛻援 4](../slices/04-custom-field.md)会シ壽ッ冗ァ滓姐隕尠蜿蜉蝨ィ蜈ア莠ォ蝓コ遑荵倶ク奇シ御ク埼怙隕 fork縲
  23 +譖エ逶エ逋ス蝨ー隸エ壽焚謐ョ鬩ア蜉ィ隶セ隶。謚雁、肴揩蠎ヲ莉 Java 謖ェ蛻ー莠焚謐ョ蠎灘柱 PM 譫サコ逧謨ー謐ョ驥後らウサ扈滓サ螟肴揩蠎ヲ豐。譛画カ亥、ア悟宵譏ッ霓ャ遘サ蛻ー莠。楔譌豕慕シ冶ッ第」譟・逧ココ蜥悟キ・蜈キ荳翫
24 24  
25 25 ## 菴墓慮螟ア謨
26 26  
27   -謨ー謐ョ鬩ア蜉ィ騾ら畑莠主ョ「謌キ髴豎り逕ィ蜈焚謐ョ陦ィ霎セ逧ュ蜀オ縲ゆク譌ヲ螳「謌キ髴隕∝謨ー謐ョ陦ィ霎セ荳堺コ噪陦御クコ梧ッ泌ヲゆク榊酔 SQL縲∽ク榊酔蟄伜お霑ィ倶クサ菴薙∵。楔隸肴ア裏豕墓緒霑ー逧★蜷郁ァ悟ーア莨夊ァヲ蜿願セケ逡後Yly 逧函蜿」譏ッ[豈丞ョ「謌キ SQL 隕尠騾夐%](../slices/05-customer-sql-override.md)壽滑謇句 SQL 謠蝉コ、蛻ー `script/螳「謌キ/<customer>/`悟ケカ逶エ謗・蠎皮畑蛻ー隸・螳「謌キ schema悟ョ悟扈戊ソ。楔縲りソ吩クェ騾夐%逵溷ョ槫ュ伜惠荳疲ュ」蝨ィ菴ソ逕ィ御スケ滓弍扈エ謚、謌先悽譛鬮倡噪螳壼宛譁ケ蠑上
  27 +謨ー謐ョ鬩ア蜉ィ騾ら畑莠主ョ「謌キ髴豎り逕ィ蜈焚謐ョ陦ィ霎セ逧ュ蜀オ縲ゆク譌ヲ螳「謌キ髴隕∝謨ー謐ョ陦ィ霎セ荳堺コ噪陦御クコ梧ッ泌ヲゆク榊酔 SQL縲∽ク榊酔蟄伜お霑ィ倶クサ菴薙∵。楔隸肴ア裏豕墓緒霑ー逧★蜷郁ァ悟ーア莨夊ァヲ蜿願セケ逡後Yly 逧コ泌ッケ譁ケ蠑乗弍[豈丞ョ「謌キ SQL 隕尠騾夐%](../slices/05-customer-sql-override.md)壽滑謇句 SQL 謠蝉コ、蛻ー `script/螳「謌キ/<customer>/`悟ケカ逶エ謗・蠎皮畑蛻ー隸・螳「謌キ schema悟ョ悟扈戊ソ。楔縲
  28 +
  29 +霑咎髴隕∬ッエ貂・夲シ壺懃サ戊ソ。楔窶晄э蜻ウ逹謨ー謐ョ鬩ア蜉ィ隶コ轤ケ蜿ェ蝨ィ邉サ扈溽噪荳驛ィ蛻遶九ょッケ `script/螳「謌キ/` 荳狗噪 18 荳ェ螳「謌キ譚・隸エ瑚ソ占。梧慮蟾イ扈丈ク榊譏ッ逵滓ュ」逧黒荳莉」遐∝コ難シ妍ava 譏ッ蜈ア莠ォ逧シ御スッ丈クェ螳「謌キ DB 驥悟ョ樣刔謇ァ陦檎噪霑ィ倶ス謎シ壼蜿会シ瑚御ク疲イ。譛芽蜉ィ譛コ蛻カ蜿醍鴫貍らァサ縲ょョ。譟・閠惠貅千∽クュ隸サ蛻ー逧 `Sp_SalSalesCheck`悟ケカ荳堺ソ晁ッ∝ーア譏ッ譟蝉クェ逕滉コァ螳「謌キ螳樣刔霑占。檎噪迚域悽縲よ滑螳ァー荳コ窶憺函蜿」窶晏キイ扈丞¥貂ゥ蜥鯉シ帛ョ櫁キオ荳ュ瑚ヲ尠騾夐%蟾イ扈丞序謌仙、炊驥榊、ァ荳壼苅騾サ霎大キョ蠑ら噪譬遲疲。茨シ瑚瑚ソ呎ュ」譏ッ謨ー謐ョ鬩ア蜉ィ隶セ隶。蜴滓悽諠ウ驕ソ蜈咲噪螟ア謨域ィ。蠑上
28 30  
29 31 ## 霑吝ッケ髦ッサ Wiki 諢丞袖逹莉荵
30 32  
31   -譛ャ Wiki 荳ュ逧ッ丈クェ蛻援驛ス隶ー蠖戊ソ吩クェ隶コ轤ケ逧ク谺。*蠎皮畑*縲ょ迚 1 譏ッ CRUD 讓。蝮嶺ク顔噪蝗幄。ィ隸サ蜿悶ょ迚 2 譏ッ雍ッ遨ソ豈丈ク螻ら噪螟夂ァ滓姐菴懃畑蝓溘ょ迚 3 譏ッ蜿ェ隸サ / 隗崟謾ッ謦醍噪蜿倅ス薙ょ迚 4 譏ッ螳壼宛隕尠縲ょ迚 5 譏ッ隕尠荳榊、溽畑譌カ逧函蜿」縲ょョサャ蜷郁オキ譚・莉惹クュ蠢芦霎ケ逡瑚ヲ尠謨ー謐ョ鬩ア蜉ィ隶セ隶。縲
  33 +譛ャ Wiki 荳ュ逧ッ丈クェ蛻援驛ス隶ー蠖戊ソ吩クェ隶コ轤ケ逧ク谺。*蠎皮畑*縲ょ迚 1 譏ッ CRUD 讓。蝮嶺ク顔噪蜈焚謐ョ隸サ蜿厄シ御ケ滓弍譬螳樔セ九ょ迚 2 譏ッ雍ッ遨ソ豈丈ク螻ら噪螟夂ァ滓姐菴懃畑蝓溘ょ迚 3 譏ッ蜿ェ隸サ / 隗崟謾ッ謦醍噪蜿倅ス薙ょ迚 4 譏ッ螳壼宛隕尠縲ょ迚 5 譏ッ隕尠荳榊、溽畑譌カ逧函蜿」縲ょョサャ蜷郁オキ譚・莉惹クュ蠢芦霎ケ逡瑚ヲ尠謨ー謐ョ鬩ア蜉ィ隶セ隶。縲
... ...
zh/docs/contributing/index.md
... ... @@ -32,7 +32,7 @@ DB_NAME=xlyweberp_some_other python scripts/gen_catalog.py
32 32  
33 33 生成器每次运行都会清空并重写 `docs/auto-catalog/{tables,views,procedures,functions}/` 下的全部文件。这些目录中的手工修改会丢失。
34 34  
35   -## Pre-commit hook(可选,建议本地编辑时使用)
  35 +## Pre-commit 钩子(可选,建议本地编辑时使用)
36 36  
37 37 安装一次:
38 38  
... ... @@ -41,7 +41,7 @@ ln -s ../../scripts/precommit.sh .git/hooks/pre-commit
41 41 chmod +x scripts/precommit.sh
42 42 ```
43 43  
44   -hook 会在每次提交时运行 `mkdocs build --strict`,以便在提交前发现断开的交叉链接。
  44 +钩子会在每次提交时运行 `mkdocs build --strict`,以便在提交前发现断开的交叉链接。
45 45  
46 46 ## 风格
47 47  
... ...
zh/docs/index.md
... ... @@ -21,13 +21,20 @@
21 21  
22 22 ## 不覆盖的范围
23 23  
24   -- B2B 印刷平台层(`plat_*` 表、`xlyPlat*` 模块)。
  24 +- B2B 印刷平台层(`plat_*` 表、除 `xlyPlatConstant` 外的所有 `xlyPlat*` 模块;见下方说明)。
25 25 - AI / LLM 功能(`ai_*` 表、`AiController`),太新且仍在变化。
26   -- 人脸识别(`xlyFace`),范围较窄。
  26 +- 人脸识别(`xlyFace`),范围较窄;它仍在 `settings.gradle` 中启用(会构建和部署),但本 Wiki 有意不展开。
  27 +- 文件管理模块(`xlyFile`)和串口模块(`xlyRxtx`),范围较窄 / 偏硬件。
  28 +- 调度模块(`xlyErpTask`、`xlyPlatTask`),在 `settings.gradle` 中已注释;cron / Quartz 接线不属于本 Wiki 覆盖的框架运行时。
  29 +- 测试脚手架模块(`xlyTestService`、`xlyTestController`),历史遗留,不属于框架运行时。
27 30 - `xlyweberp_*` 数据库之间的租户级 schema 漂移;本 Wiki 针对一个 schema。
28   -- 备份表(`*_bak`、`*0302` 等)。
29   -- MongoDB 文档存储(yaml profile 中的 `spring.data.mongodb.uri`,以及 `xlyEntity/.../mongo/` 下的文档类)。每个 `@Document` 类都是 `PLAT_*` 命名,每个 `MongoTemplate` 调用方都位于 `xlyPlat*` 模块中,因此 MongoDB 属于上面的 plat 层。本 Wiki 覆盖的框架层只讨论 MySQL。
  31 +- 备份表(`*_bak`、`*0302`、`*_copy1`、`*_history`、`*YYYYMMDD[HHMMSS]` 后缀快照等)。自动目录会为它们生成页面,因为它们真实存在;正文页面不会把它们作为一个家族展开。当前 schema 约有 56 张此类表。
  32 +- MongoDB 文档存储(yaml profile 中的 `spring.data.mongodb.uri`,以及 `xlyEntity/.../mongo/` 下的文档类)。其中 22 个 `@Document` 类里有 20 个是 `PLAT_*` 命名;仅有两个例外是 `DIKE_TEST*` 临时测试类。唯一的 `MongoTemplate` 调用方是 `xlyPersist/.../dao/platmongo/BaseMongoDao`(`dao/platmongo/` 包名已经表明其 plat 层意图),在 cleanup 分支没有树内消费者;曾经继承它的 `xlyPlat*` 模块都已从 `settings.gradle` 注释掉。本 Wiki 覆盖的框架层只讨论 MySQL;Mongo 接线仍可编译,但处于休眠状态。
  33 +
  34 +> **关于 `xlyPlatConstant`。** 它带有 `xlyPlat*` 前缀,但属于本 Wiki 范围:`xlyPersist` 从中导入了两个工具类(`com.xly.xlyplatconstant.contant.thread.MultiThreadServer`、`com.xly.xlyplatconstant.contant.TimeContant`)。把它视为命名不准的共享工具模块,而不是平台层模块。
  35 +
  36 +> **关于 `xlyPlc`。** PLC / 硬件桥接插件属于本 Wiki 范围,是非核心模块如何挂入框架的标准示例。见[切片 06:硬件](slices/06-hardware.md)。
30 37  
31 38 ## 如何修正这个 Wiki
32 39  
33   -编辑 Markdown 文件即可,这些文件就是 Wiki 的源。MkDocs Material 会从这些 `.md` 文件生成静态 HTML。重新生成命令和 pre-commit hook 见 [参与维护](contributing/index.md)。
  40 +编辑 Markdown 文件即可,这些文件就是 Wiki 的源。MkDocs Material 会从这些 `.md` 文件生成静态 HTML。重新生成命令和 pre-commit 钩子见 [参与维护](contributing/index.md)。
... ...
zh/docs/reference/builder/attach-workflow.md
1 1 # 如何挂接工作流
2 2  
  3 +> **暂缓:需要一个已部署 BPMN 的环境。** 已用 dev DB 实证确认:`SELECT COUNT(*) FROM act_re_procdef` 返回 0;`gdsmoduleflow = 0`;`gdsmodule WHERE bCheck = 1` 也为 0 行。分发路径本身还被 `ConstantUtils.bCheckflowCheck = false` 硬禁用(见 [Activiti 集成](../../reference/maintainer/activiti.md))。下面的配方是**从代码推导出的假设**,尚未在实时部署上跑通过。
  4 +
3 5 > **暂缓。** Activiti 已接入代码库,但当前实时 DB 中没有部署工作流。相同原因见[切片 7(暂缓)](../../slices/07-workflow.md)。
4 6 >
5 7 > 当有带活动流程的 DB 可用时,配方大致如下:
... ...
zh/docs/reference/builder/define-form.md
... ... @@ -88,4 +88,4 @@
88 88  
89 89 ## 缓存失效
90 90  
91   -插入后,运行时缓存仍可能持有*之前*的空状态,直到 JMS 消息触发。**后台**构建器保存变更时,xly 的缓存失效监听器(`ConsumerChangeGdsModuleThread`)会处理;如果你通过原始 SQL 插入,可能需要重启运行服务或等待 TTL 过期。见[元数据变更后的缓存失效](../maintainer/cache-invalidation.md)。
  91 +插入后,运行时缓存仍可能持有*之前*的空状态,直到某条路径清理它。BACK 保存变更时,保存 service 会同步调用 `BusinessCleanRedisData.delCleanRedisData*`,进而触发 `CleanRedisServiceImpl` 中相关 cache region 的 `@CacheEvict`。如果你通过原始 SQL 插入,**不会自动清理缓存**;需要从应用内部调用 `BusinessCleanRedisDataImpl` 的方法、重启运行服务,或等待 TTL 过期。(名字相近的 `ConsumerChangeGdsModuleThread` JMS 路径做的是存储过程基础数据合并,不是缓存失效;详见[元数据变更后的缓存失效](../maintainer/cache-invalidation.md)。)
... ...
zh/docs/reference/builder/define-vtable.md
... ... @@ -10,7 +10,7 @@ xly 中的*虚拟表*是作为元数据声明的“表”,不是 DDL 创建的
10 10 - 定义下游表单可以叠加使用的数据形状。
11 11 - 集中管理租户感知的列定义,使多个读取同一形状的表单共享默认值。
12 12  
13   -当前实时 DB 中,`gdsconfigtbmaster` 有 307 个虚拟表 master 行,`gdsconfigtbslave` 有 14,385 个虚拟列行。它们覆盖 lookup 表、分类树和可配置参数集
  13 +虚拟表覆盖 lookup 表、分类树和可配置参数集。随着 PM 增加新形状,目录会自由增长
14 14  
15 15 ## 配方
16 16  
... ... @@ -21,19 +21,42 @@ xly 中的*虚拟表*是作为元数据声明的“表”,不是 DDL 创建的
21 21 | 列 | 值 |
22 22 |---|---|
23 23 | `sId` | 唯一虚拟表 ID |
24   -| `sName` | 虚拟表逻辑名 |
25 24 | `sChinese` / `sEnglish` / `sBig5` | 显示名 |
26 25 | `sBrandsId` / `sSubsidiaryId` | 租户作用域 |
27   -| `sTbName` | 底层物理表名(如果有支撑表) |
28   -| 其他配置列 | 描述存储和索引 |
  26 +| `sTbName` | 底层物理名称。**实践中它可以指向表、视图或存储过程**;该列有唯一键,但没有更严格约束。运行时把它当成通用 SQL 标识符解析。 |
  27 +| `sParentId` | 树形分类的父虚拟表;平铺表为空 |
  28 +| `iOrder` | BACK 列表中的排序 |
29 29  
30 30 ### 2. 列 — `gdsconfigtbslave`
31 31  
32 32 每列一行。每行携带列名、类型、默认值、显示标签、校验规则,以及它是否属于主键。
33 33  
34   -## 未决:数据由什么支撑
  34 +## `sTbName` 实际指向什么,以及漂移
35 35  
36   -当前实时 DB 中,307 个 `gdsconfigtbmaster` 行都有非空 `sTbName`,但其中 11 个名称在 `information_schema.tables` 中找不到当前对象。因此安全表述是:元数据期望存在底层 SQL 对象,但实时 schema 并非对每个虚拟表行都完全对齐。
  36 +每个 `gdsconfigtbmaster` 行都有非空 `sTbName`,但该列只是带唯一键的字符串;框架不会强制它解析为基础表。已对实时 dev DB 验证:
  37 +
  38 +- `gdsconfigtbmaster` 总计 307 行。
  39 +- **296 行(96.4%)解析到 `information_schema.tables` 中真实存在的 `BASE TABLE`**。
  40 +- **11 行(3.6%)无法解析为基础表**,而它们的分布本身很有信息量:
  41 +
  42 +| 未解析的 `sTbName` 实际指向 | 数量 | 示例 |
  43 +|---|---:|---|
  44 +| 视图(`viw_*`),不是表 | 4 | `viw_mftproductionreport`、`viw_mftproductionreportEmployee1` |
  45 +| 存储过程(`Sp_*`) | 3 | `Sp_Cashier_BankJournal`、`Sp_Cashier_SumJournal`、`Sp_Sales_NotDeliverGoodNotifyList` |
  46 +| 大小写折叠后存在的真实表,或已重命名 / 删除对象 | 4 | `QlyProcessTestResult`(大小写漂移)等 |
  47 +
  48 +所以 `sTbName` **不严格等于“物理表名”**;它是运行时会替换进读取查询里的通用 SQL 标识符,也可能指向视图或可调用过程。早先把它写成“底层物理表名”的说法过窄。
  49 +
  50 +可用于暴露漂移的审计 SQL:
  51 +
  52 +```sql
  53 +SELECT sId, sChinese, sTbName
  54 +FROM gdsconfigtbmaster
  55 +WHERE sTbName NOT IN (
  56 + SELECT TABLE_NAME FROM information_schema.tables
  57 + WHERE TABLE_SCHEMA = DATABASE()
  58 +);
  59 +```
37 60  
38 61 ## 何时选择虚拟表、视图或真实表
39 62  
... ... @@ -45,6 +68,34 @@ xly 中的*虚拟表*是作为元数据声明的“表”,不是 DDL 创建的
45 68  
46 69 虚拟表通道是框架对数据驱动形状的“类型系统”;物理 schema 才是真正存储行的地方。两者有意解耦。
47 70  
48   -## 示例
  71 +## 示例:`包装方式` lookup
49 72  
50   -本页需要一个具体示例:从 `gdsconfigtbmaster` 选一张真实虚拟表,逐行解释它的 master 行和 slave 行。后续版本应补上。
  73 +dev DB 中的一条代表性真实记录:
  74 +
  75 +**Master**(`gdsconfigtbmaster`):
  76 +
  77 +```text
  78 +sId = 192116810113315231587698560
  79 +sChinese = 包装方式 (Packing method)
  80 +sTbName = SisPacking
  81 +sParentId = (root)
  82 +```
  83 +
  84 +**Slave 列**(`gdsconfigtbslave`,该 `sParentId` 下 10 行)声明的是*逻辑*形状:名称、显示标签、校验。*物理*形状位于真实的 `SisPacking` 表中:
  85 +
  86 +| Slave 行 | `SisPacking` 上的物理列 |
  87 +|---|---|
  88 +| `iIncrement`(自增列) | `iIncrement int auto_increment PK` |
  89 +| `sId`(标准ID) | `sId varchar(100) UNIQUE` |
  90 +| `sBrandsId`(加工商Id) | `sBrandsId varchar(100)` |
  91 +| `sSubsidiaryId`(子公司Id) | `sSubsidiaryId varchar(100)` |
  92 +| `tCreateDate`(制单日期) | `tCreateDate datetime DEFAULT CURRENT_TIMESTAMP` |
  93 +| `sMakePerson`(制单人) | `sMakePerson varchar(255)` |
  94 +| `iOrder`(排序号) | `iOrder int DEFAULT 0` |
  95 +| `sName`(名称) | `sName varchar(255)` |
  96 +| `sNo`(编号) | `sNo varchar(255)` |
  97 +| `bInvalid`(作废) | `bInvalid bit(1) DEFAULT b'0'` |
  98 +
  99 +`gdsconfigtbslave` 中的 10 行与物理 `SisPacking` 表上的 10 列逐一对应。PM 随后可以让某条 `gdsconfigformmaster` 指向 `sTbName='SisPacking'`,form-slave 行再按名称引用同一批列。运行时把这两层粘合起来,路径与切片 1 中的元数据驱动读取相同。
  100 +
  101 +本页之前把“补一个 worked example”列为 TODO;这里就是补上的示例。
... ...
zh/docs/reference/builder/permissions.md
... ... @@ -87,4 +87,4 @@ returnMap.put(&quot;gdsjurisdiction&quot;, jList);
87 87  
88 88 ## 关于 `plat_base_authority_*`
89 89  
90   -schema 中有三张 `plat_base_authority` 下的表(`plat_base_authority`、`plat_base_authority_button_type`、`plat_base_authority_data_type`),看起来像按钮类型和数据权限类型 lookup。**三张表在实时 DB 中都为空**;它们属于不在本 Wiki 范围内的 `xlyPlat*` B2B 平台层,不属于框架自身权限流。记录框架权限时不要引用它们;上面的 jurisdiction surface 是自包含的。
  90 +schema 中有三张 `plat_base_authority` 下的表(`plat_base_authority`、`plat_base_authority_button_type`、`plat_base_authority_data_type`),看起来像按钮类型和数据权限类型 lookup。**三张表在实时 DB 中都为空**;它们属于不在本 Wiki 范围内的 `xlyPlat*` B2B 平台层,不属于框架自身权限流。记录框架权限时不要引用它们;上面的权限接口面是自包含的。
... ...
zh/docs/reference/maintainer/activiti.md
1 1 # Activiti 集成
2 2  
3   -> **完整覆盖暂缓**:Activiti 已接入代码库,但当前实时 DB 中没有部署流程。见[切片 7(暂缓)](../../slices/07-workflow.md)。
  3 +> **TL;DR:Activiti 已接线,流程引擎会启动,但没有流量经过它。** 引擎已完成 bootstrap,`act_*` schema 已按 6.0.0.4 版本建好,BPMN modeler 可访问。但所有工作流表都是 0 行:没有 BPMN、没有 procdef、没有流程实例、没有任务;没有 `gdsmodule.bCheck=1`,也没有 `gdsmoduleflow` 关联。用户看到的审批按钮(`/business/doExamine`)**完全绕过 Activiti**,只是通过 SQL 把 `bCheck` 更新为 `1`。细节见下面的 [xly 如何在不使用 Activiti 的情况下处理工作流](#xly-如何在不使用-activiti-的情况下处理工作流)。
4 4  
5   -本页记录代码库中与 Activiti 相关的事实,避免未来维护人员在工作流真正启用时从零开始
  5 +本页记录实际已接线的内容(具体类、URL、流程引擎状态),以及要让它真正工作需要满足什么条件
6 6  
7   -## 两个 Activiti 版本
  7 +## xly 如何在不使用 Activiti 的情况下处理工作流 {#xly-如何在不使用-activiti-的情况下处理工作流}
8 8  
9   -依赖树携带**两个** Activiti 版本:
  9 +xly 有**三种类似工作流的机制**,按实际使用程度从高到低排序如下。
  10 +
  11 +### 路径 1:存储过程 + `bCheck` 标志的一步审批(主流模式)
  12 +
  13 +这是 xly 中 99% 审批的样子。这里**没有流程引擎,也没有状态机**;工作流就是那次过程调用本身。
  14 +
  15 +机制:
  16 +
  17 +1. 每张业务表携带同一组三个审计列:`bCheck`(审批布尔值)、`sCheckPerson`、`tCheckDate`。在当前 dev DB 中,`bCheck` 出现在 **426** 张表上,`tCheckDate` 出现在 400 张表上,`sCheckPerson` 出现在 398 张表上。也就是说,几乎每个业务单据都把审批审计轨迹放在自己行里。
  18 +2. 每个模块在 `gdsmodule.sProcName` 中声明一个**单一**审批过程名(列注释是“存储过程(审核)名称”)。例如报价模块的 `Sp_Quo_QuotationCheck`、销售订单的 `Sp_SalSalesCheck`。
  19 +3. 用户点击“审核”按钮时:
  20 + - `POST /business/doExamine` → `BusinessBaseController.java:384-391` → `BusinessBaseServiceImpl.doExamine()` → `ExamineServiceImpl.doExcuExamine()`。
  21 + - service 获取一个基于行 id 的 Redis 锁(`ws_update_*_{sGuid}_*`),避免两个用户并发审批。
  22 + - 它通过通用过程调用机制分发到 `sProcName` 命名的过程,见[通用存储过程分发](proc-dispatch.md)。
  23 + - **过程本身拥有业务逻辑**:校验必填字段、子行和主表合计是否平衡、相关单据是否锁定等。如果全部通过,过程更新该行:`bCheck = 1`、`sCheckPerson = <user>`、`tCheckDate = NOW()`。
  24 + - 过程返回 `OUT sCode INT`(1 表示成功,≤0 表示错误)和 `OUT sReturn LONGTEXT`(错误消息)。
  25 +4. 反审核是同一调用,只是 `iFlag = 0` 而不是 `1`;过程同时处理两个方向。
  26 +5. `Sp_System_CheckSave` 是每个带 `bCheck` 行保存后的通用钩子,由 `BusinessBaseServiceImpl.java:1828` 调用。它写入 `sFormId` 审计字段,并保留跨单据校验的占位逻辑(当前大多已注释)。
  27 +
  28 +这条路径没有“下一个审批人”或“审批队列”的概念。一个有权限的用户点击审核且过程成功后,该行就审批完成。
  29 +
  30 +### 路径 2:单据串联形成的隐式多步工作流
  31 +
  32 +多单据业务流程(报价 → 客户确认 → 销售订单 → 发货 → 发票)不是靠一个单据在状态之间推进,而是靠**多个独立模块和表单**实现。用户视角是:
  33 +
  34 +1. 模块 A(例如 `quoQuotationMaster`):填单,点击审核,行变成 `bCheck = 1`。
  35 +2. 模块 A 上有一个按钮(通过 `gdsconfigformslave.sButtonParam` 指向 `Sp_Quo_ToSalesOrder` 或类似过程)。点击后,过程创建模块 B(例如 `salSalesOrderMaster`)中的一行,并用模块 A 的数据预填。
  36 +3. 用户进入模块 B,补充字段,再在那里审核,依次推进。
  37 +
  38 +FROUNT [KPI 工作中心](runtime.md#kpi-工作中心front-端首页-dashboard) 上的 “01/04、02/04、03/04、04/04” 步骤编号反映的就是这个模型:每个“流程”是一个父模块,下面有 N 个有序子模块;步骤只是父 `gdsmodule` 条目下的子模块。没有流程引擎在跟踪“你处于四步中的第二步”;用户只是操作当前打开的单据,框架用父模块下子模块的 `iOrder` 提供上下文。
  39 +
  40 +因此,多步“工作流”来自:
  41 +
  42 +- 一个按主题分组步骤的父 `gdsmodule`(例如 `估价管理流程`,包含 4 个子模块)。
  43 +- 每个子模块自己的 `sProcName`(一步审批)。
  44 +- 每个子模块自己的 `sButtonParam` 过程,点击后创建下一张单据。
  45 +- 每张业务单据自己的 `bCheck` 标志。
  46 +
  47 +没有状态机,也没有 FSM 库;只是**通过表单按钮把一串存储过程接起来**。这是当前 dev DB 中能看到的机制。
  48 +
  49 +### 路径 3:Activiti BPMN 工作流(有闸门,目前代码中禁用) {#路径-3activiti-bpmn-工作流有闸门目前代码中禁用}
  50 +
  51 +路径 3 作为第三条通道存在于代码库中:启用后会经由 Activiti。但它没有在当前 dev DB 中运行,而且目前不重新编译无法启用。
  52 +
  53 +闸门硬编码在 `xlyPersist/.../utils/ConstantUtils.java`:
  54 +
  55 +```java
  56 +public static Boolean bCheckflowCheck = false;
  57 +```
  58 +
  59 +`ExamineServiceImpl.doExcuExamine()` 内部分发逻辑是:
  60 +
  61 +```java
  62 +if (ConstantUtils.bCheckflowCheck) {
  63 + Map<String,Object> reMap = checkExamineFlowService
  64 + .doSendCheckFolw(sGuid, sUserName, sBrandsId, sSubsidiaryId,
  65 + sFormId, map, searMap, sBtnName, request);
  66 + if (MapUtil.isNotEmpty(reMap)) { return reMap; }
  67 +}
  68 +```
  69 +
  70 +所以即使租户部署了 BPMN,并通过 `gdsmoduleflow` 关联了模块,这个 `if` 也会因为 `bCheckflowCheck` 是 Java 常量 `false` 而短路。要启用路径 3,需要:
  71 +
  72 +1. 把源码中的 `ConstantUtils.bCheckflowCheck` 改为 `true` 并重新构建 WAR,或在运行时 patch 该常量。
  73 +2. 插入一行以 `(sFormId, sBtnName)` 为 key 的 `gdsmoduleflow`,把表单上的审批按钮映射到已部署 BPMN。
  74 +3. 通过 modeler 部署 BPMN,让 `act_re_procdef` 有数据。
  75 +4. 确认相关模块的 `bCheck` 语义与 BPMN 起止事件对齐。
  76 +
  77 +启用后,`CheckExamineFlowServiceImpl.doSendCheckFolw` 会读取 `gdsmoduleflow` 行,调用 `checkExamineFlowDataService.doSendCheckFolwData` 预置数据,再经 `doSendFlowUrl` 跳到 xlyFlow controller。随后 `ProcessServiceImpl.submitApply()` 调用 `runtimeService.startProcessInstanceByKey(...)`,Activiti 接管流程;`biz_flow` + `biz_todo_item` 填充,审批人在收件箱中看到任务,`CurrencyFlowController.complete(...)` 推进流程实例。
  78 +
  79 +### 对比
  80 +
  81 +| 方面 | 路径 1(proc + bCheck) | 路径 2(单据链) | 路径 3(Activiti) |
  82 +|---|---|---|---|
  83 +| 状态存储 | 单据行上的 `bCheck` 列 | 无;状态等于用户打开哪张单据 | `act_ru_task`、`act_hi_*`、`biz_flow`、`biz_todo_item` |
  84 +| 步骤转换 | 每张单据一步 | 每个链路按钮触发“转下一张单据”的过程 | 流程引擎按 BPMN 图驱动转换 |
  85 +| 转派 / 委托 | 不支持 | 不支持 | Activiti 支持 |
  86 +| 并行分支 | 不支持 | 不支持 | BPMN 网关支持 |
  87 +| 当前是否活跃 | 是,每次“审核”点击都会用 | 是,多单据业务流都在用 | 否,代码中 `bCheckflowCheck = false` |
  88 +| 工具 | 只有存储过程 | 存储过程 + 模块树配置 | `/modeler/*` 下的 BPMN modeler |
  89 +
  90 +### 真实路径 1 定制示例
  91 +
  92 +[切片 5](../../slices/05-customer-sql-override.md#示例-2万昌构建多级审批工作流) 追踪了万昌的 `领班驳回.sql`:这是客户侧多级审批驳回的典型例子。它展示了客户在单个 `bCheck` 不够时如何扩展路径 1:`ALTER TABLE` 增加多个审批标志(`bManager`、`bIPQC`、`bDeputy` 等),按 `Sp_<table>_check<currentState>_<nextState>` 约定编写状态转换过程,并通过自定义 `sp_add_flow_log` 写审计。这是代码库中实证可见的定制通道;`script/客户/` 下没有任何目录部署 BPMN。
  93 +
  94 +### 为什么这个设计适合 xly 的用户
  95 +
  96 +印刷行业 ERP 客户的业务流程通常是规则驱动的(报价 → 订单 → 生产 → 发货 → 开票 → 收款),每一步按惯例都是**自己的单据和自己的表单**。用户预期的是“现在打开下一张表单继续填”,而不是“系统告诉我有一个 task 等我处理”。对这类用户:
  97 +
  98 +- 路径 1 + 路径 2 覆盖了当前 dev DB 中观察到的所有场景。
  99 +- 路径 3 的价值(BPMN 建模、转派、并行网关)留给极少数审批图确实需要它的租户。
  100 +
  101 +代价是:工作流逻辑**分散在存储过程中**,而不是集中声明在一个地方。给流程增加新步骤意味着写或改一个或多个过程,而不是编辑 BPMN 图。对复杂且频繁变化的流程,这会很脆弱;但对印刷厂现实中的 quote-to-cash 链条(每客户不常变化)来说,这是务实选择。
  102 +
  103 +## Activiti 已接线:流程引擎已开启
  104 +
  105 +尽管 dev DB 处于空转状态,流程引擎会随 `xlyEntry` 启动:
  106 +
  107 +- `xlyFlow/build.gradle:15` 引入 `org.activiti:activiti-spring-boot-starter-rest-api:6.0.0`。该 starter 传递引入 `activiti-spring-boot-starter`,触发 Spring Boot 的 `ProcessEngineAutoConfiguration` 创建 `SpringProcessEngineConfiguration` bean。
  108 +- `xlyEntry/build.gradle` 通过 `api project(':xlyFlow')` 引入 xlyFlow,因此 starter 在 `xlyEntry` WAR 的运行时 classpath 上。
  109 +- `xlyEntry/.../EntryApplicationBoot.java:23-24` 只排除了 `org.activiti.spring.boot.SecurityAutoConfiguration`(REST 端点安全适配器)和 Spring 自己的 `SecurityAutoConfiguration`。**Activiti 主流程引擎 auto-config 没有被排除**,所以引擎会启动。
  110 +- `xlyFlow/.../activiti/config/ActivitiConfig.java` 是一个 `@Configuration implements ProcessEngineConfigurationConfigurer`,只做两件事:把图生成器字体设为中文友好的 `宋体`(activity / annotation / label fonts),并安装自定义 `ICustomProcessDiagramGenerator`。
  111 +- `xlyApi` 的 `ApiApplicationBoot` 也没有排除 Activiti,但 xlyApi 不依赖 xlyFlow。因此 xlyApi classpath 上有流程引擎里的 `org.activiti.engine.identity.User` 类(仅由 `IdGen.java` 加密工具使用),但不会触发 Activiti auto-config。
  112 +
  113 +实时 schema 中看到的 24 张基础 `act_*` 表,是流程引擎首次启动时通过 auto-DDL 创建的。
  114 +
  115 +## classpath 上的两个 Activiti 版本
10 116  
11 117 | 模块 | 版本 | 说明 |
12 118 |---|---|---|
13   -| `xlyPersist` | `org.activiti:activiti-engine:5.17.0` | 较老的 5.x 线 |
14   -| `xlyFlow` | `org.activiti:activiti-spring-boot-starter-rest-api:6.0.0`、`activiti-json-converter:6.0.0` | 较新的 6.0 线 |
  119 +| `xlyPersist`、`xlyApi` | `org.activiti:activiti-engine:5.17.0` | 较老的 5.x 线,两个模块都有声明。**遗留依赖**;`xlyEntry` WAR 中实际运行的流程引擎是 xlyFlow starter 拉入的 6.0。5.17 声明只是 classpath 上的包袱。 |
  120 +| `xlyFlow` | `org.activiti:activiti-spring-boot-starter-rest-api:6.0.0`、`activiti-json-converter:6.0.0` | 较新的 6.0 线,这才是会运行的版本。 |
15 121  
16   -这是实际版本不匹配。Activiti 5.x 和 6.x schema 有重叠,但在部分 `act_*` 表和迁移路径上分叉。可能性包括:
  122 +做清理的维护人员应从 `xlyPersist` 和 `xlyApi` 的 `build.gradle` 中移除 `5.17.0`。已验证:这两个模块只有 `IdGen.java` 会触碰 `org.activiti.engine.identity.User`,而该类型签名由 6.0 流程引擎也能满足,因此移除是安全的。
  123 +
  124 +## 代码中实际调用了什么
  125 +
  126 +`xlyFlow/src/main/java/com/xly/activiti/` 下 154 个 Java 文件加 modeler 子包是真实调用点。选取锚点如下:
  127 +
  128 +| 活动 | 类 : 行 | 使用的 Activiti API |
  129 +|---|---|---|
  130 +| 启动流程实例 | `ProcessServiceImpl.submitApply()` :107 | `runtimeService.startProcessInstanceByKey(module, businessKey, variables)` |
  131 +| 完成任务 | `CurrencyFlowController.complete(...)` :167 / :200;`WechatFlowPostThread` :132 | `processService.complete(taskId, ...)` → `taskService.complete()` |
  132 +| 查询活跃任务 | `CurrencyFlowController` :409、:480 | `taskService.createTaskQuery().active().list()` |
  133 +| 查询运行中实例 | `CurrencyFlowController` :485、:659 | `runtimeService.createProcessInstanceQuery()` |
  134 +| 在 modeler 中保存模型 | `ModelerController.create()` :122 | `repositoryService.saveModel()` + `addModelEditorSource()` |
  135 +| 运行时部署 BPMN | `ModelerController.deploy()` :147 | `repositoryService.createDeployment().addString(name, bpmnXml).deploy()` |
  136 +| 列出流程定义 | `ProcessDefinitionController` :135 | `repositoryService.createProcessDefinitionQuery()` |
  137 +| 读取流程引擎配置 | `ProcessActController` :281 | `ProcessEngines.getDefaultProcessEngine()` |
  138 +| 把 xly 用户桥接到 Activiti identity | `act_id_user` / `act_id_group` / `act_id_membership` 是投影 xly `sftlogininfo*` schema 的**视图** | xly 不写 Activiti identity 表;这些视图伪装成 identity 表 |
17 139  
18   -1. 框架运行 Activiti 6.0(由 xlyFlow 驱动),而 xlyPersist 的 5.17 依赖是早期遗留。
19   -2. 不同服务因历史原因使用不同版本。
20   -3. 两者都在 classpath 中,但运行时只初始化一个。
  140 +## modeler 暴露的 URL(xlyFlow controller 挂在 xlyEntry 端口上) {#modeler-暴露的-urlxlyflow-controller-挂在-xlyentry-端口上}
21 141  
22   -未来维护人员应:(a) 移除未使用版本避免混淆,(b) 记录实时 schema 使用哪个版本,(c) 验证 `act_*` 表布局与该版本精确匹配。
  142 +xlyFlow 被 xlyEntry 作为库消费(`api project(':xlyFlow')`),因此 xlyFlow controller 会编译进 xlyEntry WAR,并在 xlyEntry context-path(`/xlyEntry`)下服务。重要 URL:
23 143  
24   -额外事实:`xlyFlow/build.gradle` 引入 Activiti 6 starter,但 `xlyFlow/src/main/java/com/xly/XlyFlowApplicationBoot.java` 被完全注释。因此代码存在,但本仓库当前并没有把 `xlyFlow` 呈现为明确可独立运行的 app。
  144 +- `POST /xlyEntry/modeler/model/{modelId}/save`:保存 BPMN modeler XML。
  145 +- `GET /xlyEntry/modeler/model/{modelId}/json`:为编辑器加载模型。
  146 +- `GET /xlyEntry/modeler/editor/stencilset`:modeler stencil 定义。
  147 +- `GET /xlyEntry/modeler/create` / `/modeler/deploy/{modelId}`:创建和部署。
  148 +- `POST /xlyEntry/currencyFlow/complete/{taskId}/{sBrandsId}/{sSubsidiaryId}/{sUserId}`:完成任务(`CurrencyFlowController`)。
  149 +- `POST /xlyEntry/currencyFlow/completeerp/{sBrandsId}/{sSubsidiaryId}/{sUserName}`:ERP 侧完成任务变体。
25 150  
26   -## `act_*` schema
  151 +这些 URL 不在[内部 API 页](../../api-reference/internal.md)中完整编目,因为它们是很少触碰的工作流接口面;维护时以源码为准。
27 152  
28   -实时 DB 当前有预期的 `act_*` 表,但本轮检查的关键表都是空的:
  153 +## `CheckFlowController.java` 实际包含什么
29 154  
30   -- `act_re_deployment` = 0
31   -- `act_re_procdef` = 0
32   -- `act_ru_task` = 0
33   -- `act_hi_procinst` = 0
  155 +这是一个需要明确标出的 wiki 内部纠正:`xlyEntry/src/main/java/com/xly/web/businessweb/CheckFlowController.java` 文件存在,但类体**只有 22 行、0 个 handler 方法**,只是一个带 `@RestController @RequestMapping(value="/checkflow")` 的空壳。早期 wiki 把 `/checkflow/*` 描述为“Activiti 工作流接口面(审批 / 驳回 / 查看)”,这不符合当前文件。**实时系统上任何 `/checkflow/*` 子路径都会返回 404。** 真正的审批 / 驳回 / 查看 URL 来自上面列出的 `CurrencyFlowController` 等 controller。
34 156  
35   -## xly 的包装层
  157 +## `act_*` schema 状态(当前 dev DB)
36 158  
37   -三个 xly 表包装 Activiti 集成:
  159 +| 表 | 行数 | 有数据时的含义 |
  160 +|---|---:|---|
  161 +| `act_re_model` | 0 | modeler 中保存的 BPMN 模型 |
  162 +| `act_re_procdef` | 0 | 已部署流程定义 |
  163 +| `act_ru_task` | 0 | 活跃等待任务 |
  164 +| `act_hi_procinst` | 0 | 历史流程实例 |
  165 +| `act_id_user` / `act_id_group` / `act_id_membership` | 视图 | 把 xly 的 `sftlogininfo*` 用户投影成 Activiti identity 形状 |
  166 +| `gdsmoduleflow` | 0 | xly 从 `gdsmodule` 到流程定义的关联 |
  167 +| `biz_flow` | 0 | xly 的每单据流程状态 |
  168 +| `biz_todo_item` | 0 | 待审批任务(xly wrapper,不是 Activiti 的 `act_ru_task`) |
  169 +| `biz_todo_copyto` | 0 | 流程抄送方 |
38 170  
39   -- [`biz_flow`](../../auto-catalog/tables/biz_flow.md):xly 每单据流程状态。
40   -- [`biz_todo_item`](../../auto-catalog/tables/biz_todo_item.md):待审批任务。
41   -- [`biz_todo_copyto`](../../auto-catalog/tables/biz_todo_copyto.md):流程抄送方。
42   -- [`gdsmoduleflow`](../../auto-catalog/tables/gdsmoduleflow.md) + `gdsmoduleflowslave`:模块流程窗口配置。
  171 +所以 Activiti 当前是**彻底空转**:流程引擎在运行,schema 已就绪,没有流量。
43 172  
44   -包装表在实时 DB 中也为空:`gdsmoduleflow = 0`、`biz_flow = 0`、`biz_todo_item = 0`、`biz_todo_copyto = 0`。
  173 +## 什么会让它动起来
45 174  
46   -启用时的模式:单据提交写入 `biz_flow` 行,同时启动 Activiti 流程实例;待审批人在 `biz_todo_item` 中看到任务;审批后流程实例推进并最终完成。
  175 +要让一个流程真正运行,大致顺序如下:
47 176  
48   -## 代码中接入位置
  177 +1. 工程师或 PM 打开 **modeler UI**(静态资源位于 `xlyFlow/src/main/resources/static/modeler/`,通过 `/modeler/*` 端点服务)。他们绘制 BPMN 并保存,`act_re_model` 填充。
  178 +2. 在 modeler 中点击 *Deploy* → `ModelerController.deploy()` 调用 `repositoryService.createDeployment().addString(name, bpmnXml).deploy()` → `act_re_procdef` 填充。
  179 +3. 某个 `gdsmodule` 行标记 `bCheck = 1`,并在 `gdsmoduleflow` 中插入一行,把该模块关联到已部署的 `act_re_procdef.KEY_`。
  180 +4. 用户在该模块保存一行时,保存 service 检测 `bCheck = 1`,调用 `ProcessServiceImpl.submitApply(applyUserId, businessKey, itemName, itemContent, module, variables)`。该方法执行 `runtimeService.startProcessInstanceByKey(module, businessKey, variables)`,于是 `act_ru_*` 表填充,`biz_flow` + `biz_todo_item` 也得到 xly 侧行。
  181 +5. 审批人在 FROUNT 收件箱中看到待办(很可能是“审批”tab,独立于 KPI 工作中心)。点击通过 / 驳回 → `CurrencyFlowController.complete()` → `taskService.complete()`。
  182 +6. 当流程实例到达 `endEvent`,行的 `bCheck` 发生转换;下游过滤 `bCheck = 1` 的查询开始看到它。
49 183  
50   -`xlyFlow/` 是专用模块。后续补全时应关注:
  184 +## xly 为什么要接 Activiti
51 185  
52   -- `xlyFlow` 的 Gradle build 引入 Activiti 6.0。
53   -- Activiti process engine 的 Spring Boot 配置。
54   -- `xlyEntry/com/xly/web/businessweb/` 中的 `CheckFlowController` 是 SPA 驱动工作流(审批 / 驳回 / 查看)的一个入口。
55   -- BPMN 流程定义若存在,应位于 `xlyFlow/src/main/resources/processes/` 或类似位置;当前代码库为空。
  186 +代码库已有自己的 `biz_flow` / `biz_todo_item` 表,理论上可以手写审批系统。把 Activiti 放在后面的收益是:
56 187  
57   -## 让 Activiti 工作需要什么
  188 +- 标准 BPMN 建模(JS modeler 使用 Activiti Explorer 同源的 stencilset)。
  189 +- 免费的状态机语义:流程引擎处理“task A 完成 → task B 可用”,xly 不需要用 SQL 维护 FSM。
  190 +- 图渲染能力(`ProcessActController` 中的页面转 PNG)。
58 191  
59   -使用工作流的部署需要:
  192 +成本是:JVM 中多一个流程引擎,DB 中多一套可能发生 DDL 漂移的 schema,以及一个额外认证接口面(xly 通过 `act_id_*` 视图把它遮住)。
60 193  
61   -1. 已部署 BPMN 流程定义(`act_re_procdef` 有数据)。
62   -2. 模块标记 `bCheck = 1`,并通过 `gdsmoduleflow` 关联到正确流程。
63   -3. 通过 `act_id_*` 或 xly 包装层分配审批用户。
64   -4. 保存端点在 `bCheck = 1` 模块上分支,启动流程实例,而不是(或除了)标准 add/update/delete。
  194 +## 本页不是什么
65 195  
66   -当有已部署流程的环境可用时,本页会成为正式参考;在此之前,把它视为“这里应该有什么”的清单。
  196 +- 不是切片 7 的替代品。切片 7(暂缓)应基于一个真实运行流程的部署做端到端追踪。
  197 +- 不是 modeler 教程。modeler 来自 Activiti 项目;xly 只是把它作为静态资源嵌入,没有修改。
  198 +- 不是从 Activiti 迁移到其他方案的计划。那会是更大的架构决策,不是 wiki finding。
... ...
zh/docs/reference/maintainer/bi-engine.md 0 → 100644
  1 +# BI / KPI / 图表引擎
  2 +
  3 +xly **没有**内置通用 OLAP / cube 引擎(没有 Mondrian、Saiku、MDX;随包里的 `olap4j-1.2.0.jar` 没有任何 Java import,属于 classpath 上的历史包袱,见[技术栈](tech-stack.md)里的 OLAP4J 说明)。
  4 +
  5 +xly 真正提供的是一套**自研的元数据驱动看板 + KPI 层**。它和框架其他部分使用同一组原语:`gds*` 元数据行指向 `Sp_*` 存储过程,再由通用 Java service 渲染。本页按端到端路径说明它。
  6 +
  7 +## 三个部分
  8 +
  9 +| 部分 | 入口 | 支撑表 | Service |
  10 +|---|---|---|---|
  11 +| **图表**(卡片、柱线饼仪表盘组件、看板) | FROUNT 的 `/indexPage/commonChar` 模块;BACK 的图表配置管理页 | `gdsconfigcharmaster`(3,006 行)、`gdsconfigcharslave`(1,951 行) | `CharServiceImpl`(2,219 行,是 xlyBusinessService 里最重的类之一) |
  12 +| **KPI**(按员工 / 模块做绩效评分) | BACK / FROUNT 的 KPI 页面,例如 `行为KPI项目设置`、`5.经营KPI分析`、`异常清除KPI任务表` | `kpimaster`(124,524 行)、`kpidetail`(1,308)、`kpimodule`(44)、`kpimoduleuser`、`kpimoduleuserday`、`kpislavel` | `KpiServiceImpl`(833 行,位于 `xlyBusinessService/.../KPIService/`)+ `BusinessModelKpiServiceImpl`(901 行)+ `FlushModleKpiThread`(后台刷新) |
  13 +| **预置聚合过程** | 由图表元数据行调用 | n/a | 20 个 `Sp_chart_*` 过程 + 2 个 `Sp_KPI_*` 过程 + `spKPImodule` |
  14 +
  15 +## 图表:看板如何渲染
  16 +
  17 +xly 中的一个图表就是 `gdsconfigcharmaster` 中的一行,关键列如下:
  18 +
  19 +| 列 | 作用 |
  20 +|---|---|
  21 +| `sId` | 图表 ID |
  22 +| `sParentId` | 所属模块(`gdsmodule.sId`) |
  23 +| `sChinese` / `sEnglish` / `sBig5` | 图表标题 |
  24 +| `sCharType` | 组件类型,见下方分布 |
  25 +| `sProcedureName` | 产出图表数据的存储过程 |
  26 +| `sProcedureParam` | 存储过程参数 JSON 规格 |
  27 +| `iWidth` | 布局跨度(24 列栅格) |
  28 +
  29 +实时 dev DB 中的 `sCharType` 分布:
  30 +
  31 +| `sCharType` | 数量 | 渲染内容 |
  32 +|---|---:|---|
  33 +| `Div` | 1558 | 容器 / 纯布局块 |
  34 +| `sLabel` | 1143 | 单值文本卡片,例如“今日销售额:¥X” |
  35 +| `Progress` | 137 | 进度条 |
  36 +| `sPie` | 52 | 饼图 |
  37 +| `commonList` | 45 | 内嵌数据表格(复用通用 grid) |
  38 +| `sColumnarGroup` | 30 | 分组柱状图 |
  39 +| `sColumnar` | 28 | 单系列柱状图 |
  40 +| `sBrokenLine` | 5 | 折线图 |
  41 +| `sBar` | 3 | 横向条形图 |
  42 +| `ColorBlock` | 3 | 类热力块的彩色块 |
  43 +| `sGauge` | 2 | 仪表盘组件 |
  44 +
  45 +从表行(`gdsconfigcharslave`)保存多系列 / 多列图表所需的系列或列拆分。
  46 +
  47 +运行时路径:
  48 +
  49 +```text
  50 +SPA 打开 /indexPage/commonChar?sModelsId=<看板模块 id>
  51 + │
  52 + ▼
  53 +GET /xlyEntry/business/getModelBysId/<id>
  54 + → 返回看板的元数据复合结果
  55 + (formData 中包含来自 gdsconfigcharmaster + slave 行的图表布局)
  56 + │
  57 + ▼
  58 +对每个图表:
  59 + POST /xlyEntry/business/getXxx(CharServiceImpl 方法)
  60 + 携带图表的 sProcedureName + sProcedureParam
  61 + → CharServiceImpl 通过通用存储过程分发调用对应的 Sp_chart_* 过程
  62 + │
  63 + ▼
  64 +SPA 用前端 ECharts 渲染每张卡片;一行图表元数据对应一张卡片
  65 +```
  66 +
  67 +## 20 个 `Sp_chart_*` 过程
  68 +
  69 +| 过程 | 计算内容 |
  70 +|---|---|
  71 +| `Sp_chart_home_11`、`Sp_chart_home_13` | 首页看板卡片 |
  72 +| `Sp_chart_TodayOrder`、`Sp_chart_TodayOrder_hm`、`Sp_chart_ThisMonthQty`、`Sp_chart_MonthOrder`、`Sp_chart_MonthTeamQty` | 当天 / 当月订单数、班组产量 |
  73 +| `Sp_chart_TodayProfit`、`Sp_chart_MonthProfit`、`Sp_chart_TodayReceivables`、`Sp_chart_TodayReceive`、`Sp_chart_expenses` | 财务:利润、应收、收款、费用汇总 |
  74 +| `Sp_chart_EquipmentLoad`、`Sp_chart_EquipmentLoad1`、`Sp_chart_EquipmentLod1`、`Sp_chart_EquipmentLast`、`Sp_chart_sMachine_speed`、`Sp_chart_Bottleneck` | 车间:设备负载、最后运行状态、当前瓶颈 |
  75 +| `Sp_chart_OrderProcess`、`Sp_chart_WorkOrderProcess` | 订单 / 工单进度时间线 |
  76 +
  77 +每个过程都遵循标准的 `(IN sLoginId, IN sBrId, IN sSuId, ...) → result-set` 形状,因此通用分发器可以直接调用。[多租户作用域](../../concepts/multi-tenancy.md)也会自然传入:每张图表都会自动按租户过滤。
  78 +
  79 +## dev DB 中的预置看板模块
  80 +
  81 +`/indexPage/commonChar` 是共享路由。dev DB 中有 6 个模块映射到它:
  82 +
  83 +| Module sId | 中文名称 |
  84 +|---|---|
  85 +| `19211681019715464089035510` | 销售图表分析 |
  86 +| `19211681019715481435115760` | 财务图表分析 |
  87 +| `19211681019715481435298200` | 生产图表分析 |
  88 +| `19211681019715708435449190` | 销售大数据分析 |
  89 +| `19211681019715708471874620` | 采购大数据分析 |
  90 +| `101251240115015889205266000` | 采购价格分析查询 |
  91 +
  92 +六个模块全部由 `gdsconfigcharmaster` 行驱动;新增或修改它们不需要改 Java 代码。
  93 +
  94 +## KPI 子系统
  95 +
  96 +> **先消歧。** FROUNT 首页也有一个标题为“**KPI监控**”的卡片,但它**不是**本页记录的 KPI。首页卡片是 `BusinessModelCenterController.getModelCenter` 提供的未清任务计数器,读取 `gdsmodule.bUnTask` / `sUnType`,没有目标值、评分和图表,只是名字容易误导。见运行时页的 [KPI 工作中心](runtime.md#kpi-工作中心front-端首页-dashboard)。下面的 `kpi*` 表族才是真正的员工绩效评分层。
  97 +
  98 +`kpi*` 是独立于图表渲染的**按员工绩效评分**层。形状如下:
  99 +
  100 +| 表 | 作用 | 实时行数 |
  101 +|---|---|---:|
  102 +| `kpimaster` | 按员工、按期间的 KPI 汇总。它是容量最大的表:每个员工的每次评分事件一行。 | 124,524 |
  103 +| `kpidetail` | 支撑每条 `kpimaster` 汇总的明细行。 | 1,308 |
  104 +| `kpimodule` | KPI 定义:哪些模块 / 指标参与评分。 | 44 |
  105 +| `kpimoduleuser` | 每用户 KPI 分配。 | 0(dev DB 未分配) |
  106 +| `kpimoduleuserday` | 每用户每日 KPI 桶。 | 1 |
  107 +| `kpislavel` | KPI 等级 / 档位定义。 | 0 |
  108 +
  109 +Java 侧:
  110 +
  111 +- `KpiServiceImpl.java`(833 行,位于 `xlyBusinessService/.../KPIService/`):KPI 事件的读写 API。
  112 +- `BusinessModelKpiServiceImpl.java`(901 行):把业务事件数据计算成 KPI 行的计算层。
  113 +- `FlushModleKpiThread.java`:后台重算线程。
  114 +- `KpimasterCloum.java` enum(xlyPersist):列名常量。
  115 +
  116 +存储过程:
  117 +
  118 +- `Sp_KPI_DetailByEmployee`:按员工的明细报表。
  119 +- `Sp_KPI_SumByEmployee`:按员工的汇总报表。
  120 +- `spKPImodule`:按模块重算 KPI。
  121 +
  122 +`script/客户/` 下也有较大的客户覆盖,例如 `script/标版/30100101/spKPImodule.sql` 以及多个 `Sp_SalesOrder_Kpi*` 过程。这与[每客户 SQL 覆盖通道](../../slices/05-customer-sql-override.md)一致:需要不同 KPI 规则的客户会交付自己的过程。
  123 +
  124 +## 自研方案的代价
  125 +
  126 +元数据 + 每图表一个过程的设计与 xly 的数据驱动论点一致,也避免了携带重型 OLAP 引擎。但代价很明确:
  127 +
  128 +1. **每张新图表都需要 SQL 作者。** “PM 添加一行元数据”只在工程师已经写好配套 `Sp_chart_*` 过程之后才成立。这里没有聚合构建器、字段选择器或自动生成查询;每个指标都是工程团队手写、评审和维护的存储过程。20 个过程和 11 种图表类型就是当前系统能渲染的全部形状。
  129 +2. **图表在 OLTP DB 上跑重 SQL。** 没有数仓、没有预聚合、没有增量汇总。“今日利润”图表就是在实时交易 schema 上做 SELECT。大客户加载图表时会和录单负载竞争同一个 MySQL 实例。缓存有帮助,但只对命中有效;元数据变更后的第一次加载仍要付完整成本。
  130 +3. **图表之间没有语义一致性保证。** 每个 `Sp_chart_*` 过程自行决定如何计算“月利润”“今日销售额”等指标。两个看似展示同一指标的图表可能因为过程体不同而悄悄不一致。真正的语义层可以避免这个问题,自研模型不能。
  131 +4. **不能钻取,也不能自由切片分析。** 每张图表都是固定查询形状。用户不能自由切换维度,也不能从汇总卡片钻取到底层交易,除非工程师为每条路径再写一个过程。
  132 +5. **KPI 逻辑会按客户分歧。** `script/客户/` 下的客户会交付自己的 `spKPImodule` 和 `Sp_SalesOrder_Kpi*` 覆盖;不同客户的 KPI 算法不同,而且代码只存在于该客户 DB 中。这会让“这个 KPI 到底是什么意思”取决于当前连接的是哪个 schema。
  133 +
  134 +这种简单设计足够支撑“展示 xly 一直展示的那 20 张卡片”。如果目标是即席分析或自助报表,它就不够了;那需要一套 xly 当前没有的独立语义层 / 数仓层。
  135 +
  136 +## 它不是什么
  137 +
  138 +- **不是自助 BI 工具。** 客户不能随便指向一张表并拖拽生成图表;新图表需要一个 SQL 存储过程,以及懂得注册元数据行的管理员。
  139 +- **不是实时分析基础设施。** 图表在缓存 miss 时会直接在 OLTP MySQL schema 上运行过程。没有独立数仓、没有增量聚合管道、没有流式层。大客户的大图表会在实时 DB 上执行重 SQL。
  140 +- **不是列存 / OLAP 引擎支撑的分析。** `xlyPersist/build.gradle` 中的 `olap4j` jar 没有任何 Java import,只是 classpath 上的历史包袱。xly 通过 MyBatis 和通用存储过程分发使用 MySQL 的普通行存。
... ...
zh/docs/reference/maintainer/cache-invalidation.md
1 1 # 元数据变更后的缓存失效
2 2  
3   -当 PM 在**后台**保存变更(给表单加列、更新权限、注册新模块)时,每个运行节点都必须丢弃对旧元数据的缓存解释。xly 通过 JMS 完成,而不是轮询
  3 +当 PM 在 BACK 保存变更(给表单加列、更新权限、注册新模块)时,框架必须丢弃对旧元数据的缓存解释。**缓存清理由 BACK 进程内的 Spring `@CacheEvict` 同步完成**,不是 JMS 扇出。代码里另有一条名字相近的 JMS 路径,但用途不同(基础数据合并);两者很容易混淆,本页专门拆开说明
4 4  
5   -## 路径
  5 +## 一个触发点,两条路径:差异在哪里
  6 +
  7 +```mermaid
  8 +flowchart TB
  9 + classDef ok fill:#e6f4ea,stroke:#34a853
  10 + classDef notcache fill:#fce8e6,stroke:#ea4335
  11 +
  12 + PM[PM 在 BACK 点击保存]:::ok
  13 + SAVE["BusinessBaseServiceImpl<br/>add/update/deleteBusinessData"]
  14 + EVICT["BusinessCleanRedisData.delCleanRedisData<br/>→ CleanRedisServiceImpl<br/>gdsmodule 相关 18 个 cache region"]:::ok
  15 + REDIS[("Redis<br/>(跨节点共享)")]:::ok
  16 + DB[("MySQL<br/>写入行")]:::ok
  17 +
  18 + PM --> SAVE
  19 + SAVE --> DB
  20 + SAVE -- "同步执行,<br/>同一事务路径" --> EVICT
  21 + EVICT --> REDIS
  22 + REDIS -. "任意节点下次读取<br/>都能看到新值" .-> ANY[其他节点]:::ok
  23 +
  24 + SAVE -. "发布 'gds module changed'" .-> AMQ([ActiveMQ])
  25 + AMQ --> CGM["ConsumerChangeGdsModuleThread<br/>(xlyErpJmsConsumer)"]:::notcache
  26 + CGM -- "调用<br/>PRO_ERPMERGEBASEGDSMODULE" --> DB2[("MySQL<br/>基础数据合并<br/>不是缓存")]:::notcache
  27 +
  28 + classDef title font-weight:bold
  29 +```
  30 +
  31 +**绿色路径**才是每次“修改元数据后刷新页面”实际依赖的路径。**红色路径**因为队列名(`CHANGE_GDS_MODULE`)和 consumer thread 名字,很容易被误认为缓存失效;但它不是。它通过存储过程做每租户 → base 数据合并。**两条路径互不依赖。**
  32 +
  33 +## 真实缓存失效路径(同步、进程内)
6 34  
7 35 ```text
8   -PM 在**后台**保存
9   - ↓
10   -**后台**控制器写入变更的 gds_* 行
11   - ↓
12   -Controller 发布 JMS “module changed” 消息
13   - ↓
14   -每个节点的 xlyErpJmsConsumer 收到消息
15   - ↓
16   -ConsumerChangeGdsModuleThread.run() 清除相关 Redis key
17   - ↓
18   -任意节点下一次 /business/getModelBysId 调用重新读表,
19   -并用新值重新填充缓存
  36 +PM 在 BACK 保存
  37 + │
  38 + ▼
  39 +BACK controller(如 /business/addUpdateDelBusinessData)调用
  40 +BusinessBaseServiceImpl.addBusinessData / updateBusinessData / deleteBusinessData
  41 + │
  42 + ▼
  43 +保存 service 调用 businessCleanRedisData.delCleanRedisData(...)
  44 +(如 BusinessBaseServiceImpl.java:1122、1224、1375、1441、1597、1677)
  45 + │
  46 + ▼
  47 +BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(<sTable>, ...)
  48 +按表名分发到 CleanRedisServiceImpl 上的某个清理方法
  49 + │
  50 + ▼
  51 +CleanRedisServiceImpl.cleanRedisByTableNameGdsModle()(或类似方法)
  52 +对固定的一组 cache region 触发 @CacheEvict
  53 + │
  54 + ▼
  55 +Spring CacheManager 清理命名条目
  56 + │
  57 + ▼
  58 +下一次 /business/getModelBysId 调用重新从 DB 读取并回填缓存
20 59 ```
21 60  
22   -处理器位于 `xlyErpJmsConsumer/src/main/java/com/xly/xlyerpjmsconsumer/thread/ConsumerChangeGdsModuleThread.java`。
  61 +清理方法位于 `xlyBusinessService/src/main/java/com/xly/service/impl/CleanRedisServiceImpl.java`。一个代表性方法(`gdsmodule` 行变更时调用)会一次性清理 18 个 cache region:
  62 +
  63 +```java
  64 +@CacheEvict(value = {
  65 + "getGdsmoduleTree", "getGdsmoduleList", "getModuleTreePro",
  66 + "getSysjurisdictionTreePro", "getsDisplayTypeAll",
  67 + "businessBaseServiceGetMenuList", "getBuMenu", "getMenu",
  68 + "getsAuthsId", "businessCommonServicegetModulelistAll",
  69 + "gdsmoduleById", "getSaveProName", "businessParameterGetParameter",
  70 + "getPrcName", "getKpiModelByUser", "getUserByFromId",
  71 + "getUserByActionId", "getModuleTreeProAll"
  72 +}, allEntries = true)
  73 +public void cleanRedisByTableNameGdsModle() { … }
  74 +```
  75 +
  76 +同一个类上还有其他按表命名的 cleaner,分别清理与 `gdsconfigformmaster`、`gdsconfigformslave`、`gdsconfigtbmaster`、`gdsformconst`、`gdsjurisdiction`、`gdsconfigcharmaster`、登录信息、billnosetting、kpimaster、`SysSystemSettings` 等相关的缓存区域。
  77 +
  78 +## JMS 的 `CHANGE_GDS_MODULE` 实际做什么(不是清缓存)
  79 +
  80 +框架中确实有 `P2pQueue.ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` 队列和 `ConsumerChangeGdsModuleThread` consumer thread,名字看上去像是做缓存失效,但事实不是。
23 81  
24   -## 为什么是 JMS,不是轮询后清除
  82 +`ConsumerChangeGdsModuleThread.run()` 解析 `changeGdsModuleService` bean(`ChangeGdsModuleServiceImpl`),调用 `changeTableData(sGdsModuleId, sJobId)`,进而调用存储过程 `PRO_ERPMERGEBASEGDSMODULE`(通过 `proDao.proErpMergeBaseGdsModule`,映射在 `ProMapper.xml`)。该存储过程把每租户 `gdsmodule` 行合并进扁平“base”查询表。这是基础数据合并作业,不是缓存清理。对 `xlyErpJmsConsumer/` grep `@CacheEvict` 或 `cleanRedis*` 结果为 0;consumer 侧不会清 Redis。
25 83  
26   -xly 经常跨多个节点运行(xlyEntry、xlyApi、xlyInterface 各自 JVM,有时水平扩展)。轮询“元数据是否变了”要么很慢(直到下一次轮询才可见),要么很吵(持续心跳)。JMS 可以在毫秒级把失效广播到每个节点
  84 +[`P2pQueue.java`](../../api-reference/messaging.md) 中另外 23 个 `ERP_JMS_ACTIVEMQ_*` 队列也一样:每个队列驱动一个领域特定的基础数据合并或扇出工作项,而不是缓存失效
27 85  
28   -代码库同时使用 **ActiveMQ** 和 **RocketMQ**,但这里记录的元数据变更路径是 **ActiveMQ / JMS**:`xlyErpJmsConsumer` 用 `@JmsListener` 监听 `P2pQueue.ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE`,`ConsumerChangeGdsModuleThread` 负责清缓存。`RocketMQServiceImpl` 用于其他集成流。
  86 +## 跨节点缓存一致性:Redis 支撑,已确认
29 87  
30   -## 会清哪些 key
  88 +`EntryApplicationBoot.java:22` 和 `ApiApplicationBoot.java:24` 上有 `@EnableCaching`。范围内源码中**没有自定义 `CacheManager` bean**(没有 `RedisCacheManager`、没有返回 `CacheManager` 的 `@Bean` 方法、没有 `implements CacheManager`、任何 `application*.yml` 中也没有 `spring.cache.*` 属性)。在存在 `spring-boot-starter-data-redis` 且 classpath 中没有其他缓存提供方(无 Caffeine、EhCache、Hazelcast、JCache;`xlyFlow` 中的 `shiro-ehcache` jar 是 Shiro 自己的 session cache,不是 Spring Cache)的情况下,Spring Boot 2.2.5 会自动配置 **`RedisCacheManager`**。
31 89  
32   -Redis 缓存包含
  90 +已在实时 dev Redis `118.178.19.35:16379`(database 0)验证:267 个 key 中有 233 个使用 Spring Cache 默认的 `<cacheName>::<key>` 分隔符。与 `BusinessGdsconfigformsServiceImpl.java:189-190` 中 `getFormconstData` 的 `@Cacheable` 注解(默认 key 来自所有参数)匹配的 key 形状示例
33 91  
34   -- 按 `sId` 的模块元数据。
35   -- 按 `sId` 的表单元数据。
36   -- 按表单 `sId` 的字段从表列表。
37   -- 每租户覆盖合并结果(派生缓存)。
38   -- 每(模块、角色)的权限规则。
  92 +```text
  93 +businessGdsconfigformsServiceGetFormconstData::{sLanguage=sChinese, sModelsId=…, sSubsidiaryId=1111111111, sUserId=…, sBrandsId=1111111111}
  94 +gdsmoduleById::gdsmoduleById_<sBrandsId>_<sSubsidiaryId>_<sLanguage>
  95 +```
39 96  
40   -consumer thread 收到变更行 ID 后,会清掉每个可能包含它的缓存 key 家族。**这里过度失效是安全选择**:下一次请求多读一次 DB 的成本,远小于继续服务陈旧元数据的成本
  97 +因此,任一节点上的 `@CacheEvict` 会清理共享 Redis 存储,其他节点下一次读取时也会看到失效。跨节点一致性来自 Redis,不来自 JMS
41 98  
42 99 ## 直接用 SQL 修改元数据时
43 100  
44   -通过 MyBatis 或通过**后台**做的 insert / update 会*触发* JMS 事件。工程师直接在生产 DB 执行 `UPDATE gdsmodule SET ...` 不会触发。缓存会持续提供旧元数据,直到:
  101 +通过 MyBatis 或 BACK 完成的 insert / update 会触发 `businessCleanRedisData.delCleanRedisData*`。直接在 DB 执行 `UPDATE gdsmodule SET …` 不会触发任何 cleaner。缓存会继续提供旧元数据,直到:
45 102  
46 103 1. 缓存 TTL 过期(实际 TTL 看缓存配置)。
47   -2. 重启应用服务器。
48   -3. 手工发送 JMS 消息(见 `xlyBusinessService` 中的 `BusinessCleanRedisDataImpl`)。
  104 +2. 重启应用服务器(由于缓存由 Redis 支撑且共享,重启一次即可,见上文)。
  105 +3. 从应用内部调用某个 `BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(<table>, …)` 方法;任意节点调用一次即可,因为它清的是共享 Redis 存储。
  106 +
  107 +## 这个设计的代价
  108 +
  109 +保存时同步 `@CacheEvict` 的模型运维上简单;在 Redis 支撑下,它也确实具备跨节点一致性。但它仍有几个脆弱点需要明确:
49 110  
50   -第三种是受支持的 workaround;第二种是粗暴 fallback。
  111 +- **两套名字很像的系统容易混淆。** JMS 路径 `CHANGE_GDS_MODULE` + `ConsumerChangeGdsModuleThread` 听起来像缓存失效,实际不是。这页存在的一部分原因,就是这种混淆反复造成 bug 和阅读误判。如果能把队列和过程重命名为类似 `MERGE_BASE_GDS_MODULE`,会更清楚,但改名成本不低。
  112 +- **驱逐和写入在同一条事务路径上。** 如果保存期间 Redis 调用失败,数据库行可能已经提交,但缓存仍旧是旧值。框架不会检测或自动恢复这种情况;保存时 Redis 短暂故障会让受影响行在 TTL 到期前一直读到旧缓存。
  113 +- **驱逐粒度是按 cache region 全量清。** `CleanRedisServiceImpl` 上大多数 `@CacheEvict` 都使用 `allEntries=true`,清掉整个 cache region,而不是只清受影响 key。元数据保存吞吐较高时,后续会出现一波 cache miss;小元数据缓存可以接受,但如果 region 有几千项就会变贵。
  114 +- **没有驱逐预算或批处理。** 批量元数据变更(例如一次改 100 个字段)会触发 100 次 `@CacheEvict`,每次都往返 Redis。没有把多次驱逐合并成一批的机制。
  115 +- **直接 DB 写入会绕过全部机制。** 任何绕开 `BusinessBaseServiceImpl` 的工具都会留下陈旧缓存,包括 DBA 脚本、通过 `mysql` 命令应用的 `script/客户/` 覆盖、以及通道 2 的 SQL 替换。对 xly 实际采用的部署模式来说,这是实打实的运维风险。
51 116  
52 117 ## 常见 bug:问题其实是缓存
53 118  
54 119 当现象像是“我改了但页面还是旧值”时,按顺序检查:
55 120  
56 121 1. 变更是否实际提交?(对 DB 执行 `SELECT` 确认。)
57   -2. **后台**节点能否访问 JMS broker?(不能访问时,失效事件不会发布。)
58   -3. 所有 consumer 节点是否在运行?(暂停的节点会继续服务旧元数据直到重启。)
59   -4. 变更是否通过原始 SQL 完成?(那就没有 JMS 事件,需要手工触发。)
  122 +2. 变更是否经过会调用 `BusinessCleanRedisData` 的路径?(直接 DB 写入或绕过 `BusinessBaseServiceImpl` 的 controller 不会。)
  123 +3. 保存提交时 Redis 是否可达?驱逐失败不会回滚保存。
  124 +4. 这个变更所在表是否映射到了会被清理的 cache region?`CleanRedisServiceImpl` 按写入表映射到具体 region;未映射的表不会让对应读取缓存失效。
60 125  
61   -[切片 1](../../slices/01-hello-world.md) 的“五表读取”会从缓存重新运行;理解哪一层陈旧是定位 bug 的关键。
  126 +[`getModelBysId` 在切片 1](../../slices/01-hello-world.md) 中返回的五键复合体会从缓存重跑;定位 bug 的关键是理解哪一层陈旧。
... ...
zh/docs/reference/maintainer/deployment.md
1 1 # 多服务部署
2 2  
3   -xly 不是单个 Spring Boot WAR。仓库包含多个可部署模块,以及一些也被 `xlyEntry` 作为依赖使用的类库型 WAR 模块。
  3 +xly 不是单个 Spring Boot WAR。仓库包含多个可部署模块,以及一些也被 `xlyEntry` 作为依赖使用的类库型模块。
  4 +
  5 +## 拓扑总览
  6 +
  7 +```mermaid
  8 +flowchart LR
  9 + classDef library fill:#f5f5f5,stroke:#999,stroke-dasharray:3 3
  10 + classDef boot fill:#e8f0fe,stroke:#4285f4
  11 +
  12 + subgraph clients [面向操作人员]
  13 + BACK["BACK SPA<br/>http://&lt;host&gt;:8597"]
  14 + FROUNT["FROUNT SPA<br/>http://&lt;host&gt;:8598"]
  15 + end
  16 + EXT([外部集成方])
  17 + HOOKS([第三方 webhook])
  18 +
  19 + subgraph boots [可部署 Spring Boot 应用]
  20 + direction TB
  21 + XENTRY["xlyEntry<br/>:8080 /xlyEntry<br/>EntryApplicationBoot"]:::boot
  22 + XAPI["xlyApi<br/>:8090 (local) / :8080<br/>/xlyApi · ApiApplicationBoot"]:::boot
  23 + XIF["xlyInterface<br/>:8080 /xlyInterface<br/>InterfaceApplicationBoot"]:::boot
  24 + XPLC["xlyPlc<br/>:8000 (dev) / :8080<br/>/xlyEntry · PlcApplicationBoot"]:::boot
  25 + XFACE["xlyFace<br/>:8091 (local) / :8080<br/>/xlyFace(文档范围外)"]:::boot
  26 + XEJMSC["xlyErpJmsConsumer<br/>(无端口;继承配置)<br/>JmsConsumerApplicationBoot"]:::boot
  27 + end
  28 +
  29 + subgraph libs [类库模块:不独立运行]
  30 + direction TB
  31 + XMANAGE[xlyManage]:::library
  32 + XBSERVICE[xlyBusinessService]:::library
  33 + XPERSIST[xlyPersist]:::library
  34 + XENTITY[xlyEntity]:::library
  35 + XFLOW[xlyFlow]:::library
  36 + XMSG[xlyMsg]:::library
  37 + XEJMSP[xlyErpJmsProductor]:::library
  38 + XPLATC[xlyPlatConstant]:::library
  39 + end
  40 +
  41 + subgraph infra [共享基础设施]
  42 + DB[("MySQL<br/>xlyweberp_*")]
  43 + REDIS[(Redis :16379<br/>共享缓存 + session)]
  44 + AMQ([ActiveMQ :61616])
  45 + MONGO[("MongoDB<br/>已接线但未使用")]
  46 + end
  47 +
  48 + BACK -->|nginx| XENTRY
  49 + FROUNT -->|nginx| XENTRY
  50 + FROUNT -->|nginx| XAPI
  51 + EXT --> XAPI
  52 + HOOKS --> XIF
  53 +
  54 + XENTRY --- XMANAGE
  55 + XENTRY --- XBSERVICE
  56 + XENTRY --- XFLOW
  57 + XBSERVICE --- XPERSIST
  58 + XAPI --- XPERSIST
  59 + XIF --- XPERSIST
  60 + XPERSIST --- XPLATC
  61 + XPERSIST --- XENTITY
  62 + XBSERVICE --- XMSG
  63 + XIF --- XMSG
  64 + XBSERVICE --- XEJMSP
  65 +
  66 + XENTRY --> DB
  67 + XAPI --> DB
  68 + XIF --> DB
  69 + XPLC --> DB
  70 + XFLOW --> DB
  71 +
  72 + XENTRY --> REDIS
  73 + XAPI --> REDIS
  74 + XEJMSC --> AMQ
  75 + XEJMSP -. 发布 .-> AMQ
  76 + XENTRY -. 发布 .-> AMQ
  77 +```
  78 +
  79 +反向代理把面向操作人员的端口(8597 / 8598)映射到内部 Spring Boot 应用(大多是 `:8080`,再通过不同 context path 区分)。类库模块不会独立运行,而是作为依赖打进可部署 WAR。`xlyFlow` 和 `xlyPlc` 都共享 xlyEntry 的 context-path `/xlyEntry`,所以真实部署需要按 host 或 upstream 路由,而不能只靠 path 区分。
4 80  
5 81 ## 主要模块
6 82  
7   -| 服务 / 模块 | 角色 | 代码事实 |
8   -|---|---|---|
9   -| **xlyEntry** | 主运行时与 builder/admin surface。承载 `/business/*`、`/gdsmodule/*`、`/gdsconfigform/*`、`/gdsconfigtb/*`、报表、登录和其他框架 controller。 | 依赖 `xlyManage`、`xlyBusinessService`、`xlyFlow`。 |
10   -| **xlyApi** | 面向 `/api/*`、`/online/*`、`/pro/*`、`/thirdparty/*` 等端点的 API 模块。 | 独立 Spring Boot 应用类 `ApiApplicationBoot`。 |
11   -| **xlyInterface** | 外部集成模块,带 Swagger 依赖和第三方集成代码。 | 独立 Spring Boot 应用类 `InterfaceApplicationBoot`。 |
12   -| **xlyPlc** | 车间 PLC 桥接([切片 6](../../slices/06-hardware.md))。 | 独立 Spring Boot 应用类 `PlcApplicationBoot`。 |
13   -| **xlyFace** | 可选人脸识别模块。 | 独立模块,仍存在于 Gradle build 中。 |
14   -| **xlyFlow** | 工作流 / Activiti 代码和 controller。 | 作为模块存在,但当前分支中 `XlyFlowApplicationBoot` 被注释,因此应视为通过 `xlyEntry` 消费的代码,而不是明确可独立运行的 app。 |
  83 +### 可部署的 Spring Boot 应用
  84 +
  85 +| 服务 / 模块 | 角色 | 默认 profile / 端口 | Boot 类 |
  86 +|---|---|---|---|
  87 +| **xlyEntry** | 主运行时与配置 / 管理接口面。承载 `/business/*`、`/gdsmodule/*`、`/gdsconfigform/*`、`/gdsconfigtb/*`、报表、登录和其他框架 controller。 | `dev` → 8080,context `/xlyEntry` | `EntryApplicationBoot` |
  88 +| **xlyApi** | 面向 `/api/*`、`/online/*`、`/pro/*`、`/thirdparty/*` 等端点的 API 模块。 | repo 默认 `local` → 8090,dev/win/linux → 8080,context `/xlyApi` | `ApiApplicationBoot` |
  89 +| **xlyInterface** | 外部集成模块,带 Swagger 依赖和第三方集成代码。 | `dev` → 8080,context `/xlyInterface` | `InterfaceApplicationBoot` |
  90 +| **xlyPlc** | 车间 PLC 桥接([切片 6](../../slices/06-hardware.md))。 | `dev` → 8000;命名 profile(15S、S10、T0、T1、CT、yt、pro)→ 8080,context `/xlyEntry`(与 xlyEntry 共享 context-path) | `PlcApplicationBoot` |
  91 +| **xlyFace** | 人脸识别模块。在 build 中(`settings.gradle` 仍按用户设置启用),但**不属于本 Wiki 文档范围**。 | repo 默认 `win` → 8080,local → 8091,context `/xlyFace` | `XlyFaceApplicationBoot` |
  92 +| **xlyErpJmsConsumer** | JMS consumer 后台服务。有 Boot main,但没有自己的 `application*.yml`;运行配置从同类服务继承。 | n/a(继承) | `JmsConsumerApplicationBoot` |
  93 +
  94 +### 类库模块(在 `settings.gradle` 中启用,但不独立运行)
  95 +
  96 +| 模块 | 角色 |
  97 +|---|---|
  98 +| **xlyManage** | 后台元数据管理服务(`Gds*ServiceImpl` 家族),被 xlyEntry 拉入。 |
  99 +| **xlyBusinessService** | 业务逻辑 service 层(`BusinessBaseServiceImpl` 和约 100 个兄弟 `*ServiceImpl` 类),被 xlyEntry 拉入。 |
  100 +| **xlyFlow** | 工作流 / Activiti 代码。当前分支 `XlyFlowApplicationBoot.java` 已完全注释掉;作为类库被 xlyEntry 使用。也共享 context-path `/xlyEntry`。 |
  101 +| **xlyEntity** | 共享实体 / DTO 类(约 83 个 Java 文件,包括 22 个 Mongo `@Document` 类)。 |
  102 +| **xlyPersist** | 持久化 helper(DAO、MyBatis mapper XML、`RequestAddParamUtil` 等)。 |
  103 +| **xlyMsg** | 通知 helper(钉钉、微信、邮件);没有 Boot main。 |
  104 +| **xlyErpJmsProductor** | JMS producer 代码(队列声明在 `P2pQueue.java`);没有 Boot main。 |
  105 +| **xlyPlatConstant** | 共享工具常量(`MultiThreadServer`、`TimeContant`),被 `xlyPersist` 消费。唯一仍启用的 Plat* 模块。 |
15 106  
16   -每个模块有自己的 `application.yml` 和多个 `application-<profile>.yml`。启动时通过 `-Dspring.profiles.active=...` 选择 profile。
  107 +每个可运行模块有自己的 `application.yml` 和多个 `application-<profile>.yml`。启动时通过 `-Dspring.profiles.active=...` 选择 profile。
17 108  
18 109 ## `settings.gradle` 中禁用的模块
19 110  
20   -```text
21   -//include 'xlyErpTask'
22   -//include 'xlyRxtx'
23   -//include 'xlyFile'
24   -```
  111 +cleanup 分支注释掉了 17 条 `include`。其中三个是磁盘上存在的非 Plat 模块:
25 112  
26   -三者在磁盘上存在,但被排除在当前 Gradle build 之外:
  113 +- `xlyErpTask`:长运行后台任务。
  114 +- `xlyRxtx`:原生串口库。xlyPlc 需要直接串口访问时可能重新启用;部分机型使用 TCP / Ethernet。
  115 +- `xlyFile`:旧文件管理模块,已被 `xlyPlatFileUpload`(也被注释)取代。
27 116  
28   -- `xlyErpTask`:长运行**后台**任务。
29   -- `xlyRxtx`:原生串口库。xlyPlc 不需要直接串口访问时会禁用;部分机型使用 TCP / Ethernet。
30   -- `xlyFile`:旧文件管理模块,已被 `xlyPlatFileUpload` 中的阿里云 OSS 集成取代。
  117 +其余 14 条被注释的 include 是 `xlyTestService`、`xlyTestController`,以及除 `xlyPlatConstant` 外的完整 `xlyPlat*` 家族:`xlyPlatTask`、`xlyPlatJmsProductor`、`xlyPlatJmsConsumer`、`xlyPlatReportForm`、`xlyPlatFileUpload`、`xlyPlatMarketingService`、`xlyPlatUserService`、`xlyPlatSmsService`、`xlyPlatMerchantController`、`xlyPlatWebsocket`、`xlyPlatPayService`、`xlyPlatCainiaoWaybillSevice`。(`xlyTestService` / `xlyTestController` 目录不在磁盘上;只有 `xlyEntry/.../businessweb/` 里存在一个空壳 `TestController.java`。)
31 118  
32   -维护人员清理代码库时应判断删除还是保留为历史参考。它们占用磁盘空间,但不影响 build。
  119 +维护人员清理代码库时应判断是否删除磁盘上但已排除的 `xlyErpTask` / `xlyRxtx` / `xlyFile` 目录,或保留为历史参考。它们占用磁盘空间,但不影响 build。
33 120  
34   -## Plat*
  121 +## Plat*
35 122  
36   -`xlyPlat*` 模块(`xlyPlatMerchantController`、`xlyPlatUserService`、`xlyPlatPayService`、`xlyPlatMarketingService`、`xlyPlatCainiaoWaybillSevice`、`xlyPlatSmsService`、`xlyPlatReportForm`、`xlyPlatFileUpload`、`xlyPlatJmsConsumer` / `Productor`、`xlyPlatTask`、`xlyPlatWebsocket`、`xlyPlatConstant`)属于 **B2B 印刷平台层**,不在本 Wiki 范围内
  123 +`xlyPlat*` 模块(`xlyPlatMerchantController`、`xlyPlatUserService`、`xlyPlatPayService`、`xlyPlatMarketingService`、`xlyPlatCainiaoWaybillSevice`、`xlyPlatSmsService`、`xlyPlatReportForm`、`xlyPlatFileUpload`、`xlyPlatJmsConsumer` / `Productor`、`xlyPlatTask`、`xlyPlatWebsocket`)属于 **B2B 印刷平台层**,不在本 Wiki 范围内。唯一例外是 `xlyPlatConstant`,它仍在 `settings.gradle` 中启用,并被 `xlyPersist` 当作共享常量工具使用(`MultiThreadServer`、`TimeContant`)
37 124  
38 125 ## 服务如何互相发现
39 126  
40 127 三种通信通道:
41 128  
42 129 1. **共享数据库**:每个服务读写同一个 MySQL schema。大多数跨服务“通信”是通过共享表隐式发生。
43   -2. **消息**:代码库中同时存在 ActiveMQ / JMS 和 RocketMQ。缓存失效([元数据变更后的缓存失效](cache-invalidation.md))使用 ActiveMQ / JMS 路径
  130 +2. **消息**:代码库中同时存在 ActiveMQ / JMS 和 RocketMQ。ActiveMQ / JMS 路径用于基础数据合并和异步扇出;缓存失效不走 JMS,见[元数据变更后的缓存失效](cache-invalidation.md)
44 131 3. **HTTP REST**:同步调用,例如 xlyApi 调用 xlyEntry 的 `/business/*` 端点。
45 132  
46 133 除 `application-<profile>.yml` 中按环境硬编码的 peer URL 外,没有服务注册 / 发现机制。
47 134  
48 135 ## 部署环境中的 URL 路由
49 136  
50   -工作区 `.env.local` 在实时环境中把 **后台** 指向端口 `8597`,**前台** 指向端口 `8598`。这足以识别两个面向操作人员的 surface;反向代理和 context-path 映射属于部署细节,不是本仓库中有代码依据的事实。
  137 +工作区 `.env.local` 在实时环境中把 **后台** 指向端口 `8597`,**前台** 指向端口 `8598`。这足以识别两个面向操作人员的接口面;反向代理和 context-path 映射属于部署细节,不是本仓库中有代码依据的事实。
51 138  
52 139 ## Profile 组合
53 140  
54   -`application-saas.yml`、`application-linux.yml`、`application-win.yml`、`application-15S.yml`、`application-S10.yml`、`application-pro.yml`、`application-T0.yml`、`application-T1.yml` 等覆盖以下组合:
  141 +Profile 按服务拆分:
  142 +
  143 +- **xlyEntry**:`dev`、`local`、`win`、`linux`、`15s`、`s10`、`saas`、`bgj`(小写)。`dev` 是 repo 内默认。
  144 +- **xlyApi**:`local`(repo 默认)、`dev`、`linux`、`win`。
  145 +- **xlyInterface**:仅 `dev`。
  146 +- **xlyFlow**:`dev`(空文件)。
  147 +- **xlyFace**:`win`(默认)、`dev`、`linux`、`local`。
  148 +- **xlyPlc**:`dev`(默认)加 7 个机型 profile(`15S`、`S10`、`T0`、`T1`、`CT`、`yt`、`pro`,大小写混合;不同于 xlyEntry 的小写 `15s` / `s10`)。
55 149  
56   -- 操作系统(linux / win)。
57   -- 客户类别(saas、15S、S10 等)。
58   -- 环境(dev、pro)。
59   -- 印刷机机型(xlyPlc)。
  150 +机型 profile(`T0`、`T1`、`CT`、`yt`、`pro`、`15S`、`S10`)是 **xlyPlc 专用**,其他服务没有这些 profile。跨服务 profile 覆盖以下组合:
  151 +
  152 +- 操作系统(`linux` / `win`)。
  153 +- 环境(`dev`、`local`、`saas`、`bgj`)。
  154 +- 客户 / 版本(xlyEntry 的 `15s`、`s10`)。
60 155  
61 156 每个部署为每个服务选择一个 profile。“某客户 → 哪些 profile”的映射位于运维部署文档,不在代码库中。
62 157  
63 158 ## 未决:生产 URL 路由
64 159  
  160 +> **暂缓(超出仓库可验证范围)。** 将公网 `:8597` / `:8598` 映射到内部 Spring Boot context-path 的 nginx / 反向代理配置位于部署运维基础设施中,不在本代码库里。Wiki 无法仅靠 src / DB / web 验证它;本节只是占位,等待部署侧配置被链接或纳入仓库。
  161 +
65 162 `8597` / `8598` 的精确 nginx / 反向代理配置不在仓库中。只有拿到部署侧配置后再补入本页。
... ...
zh/docs/reference/maintainer/index.md
... ... @@ -5,11 +5,13 @@
5 5 本章服务于会修改 `BusinessBaseController`、向 `gds*` 表族添加元数据表,或接入新第三方集成的人。
6 6  
7 7 - [本地运行 xlyEntry](running-locally.md) — 开发者首次启动配置(前置条件、profile、DB 覆盖、冒烟检查)。
  8 +- [技术栈](tech-stack.md) — 按类别整理的库清单(版本、声明位置、使用位置和原因)。
8 9 - [运行时:BusinessBaseController 及相关组件](runtime.md) — 元数据驱动分发循环。
9 10 - [通用存储过程分发](proc-dispatch.md) — `GenericProcedureCallController` 深入说明。
10   -- [元数据变更后的缓存失效](cache-invalidation.md) — `ConsumerChangeGdsModuleThread` 及相关组件
  11 +- [元数据变更后的缓存失效](cache-invalidation.md) — 同步 `@CacheEvict` 路径,以及名字相近但用途不同的 JMS 基础数据合并路径
11 12 - [SQL 模板(`xlyEntry/templesql/`)](sql-templates.md) — 运行时 SQL 生成相关脚手架。
12 13 - [多服务部署](deployment.md) — `xlyApi` vs `xlyEntry` vs `xlyInterface`。
13   -- [Activiti 集成](activiti.md) — 版本偏差、schema、自定义 delegate。
  14 +- [元数据管理服务(xlyManage)](management-services.md) — BACK 配置侧背后的 `Gds*ServiceImpl` 写入路径。
  15 +- [Activiti 集成](activiti.md) — 版本偏差、schema、自定义委托类。
14 16  
15 17 > **占位。** 每个子页会在相应切片实际覆盖后补充真实代码追踪。
... ...
zh/docs/reference/maintainer/management-services.md 0 → 100644
  1 +# 元数据管理服务(`xlyManage`)
  2 +
  3 +`xlyManage` 模块是 **BACK 配置器背后的 service 层**。PM 在系统模块配置、界面显示内容配置、数据表内容配置、系统权限配置、系统常量配置、用户信息配置、Mysql脚本配置等页面点击修改 / 新增 / 删除时,请求会先落到 `xlyEntry/.../web/systemweb/` 下的某个 `Gds*Controller`,再委托给 `xlyManage/src/main/java/com/xly/service/systeminfo/impl/` 下的 `Gds*ServiceImpl`。
  4 +
  5 +这些 service 是框架的**元数据 CRUD 主干**:它们拥有每张 `gds*` 表的读写逻辑。运行时([切片 1](../../slices/01-hello-world.md)、[runtime.md](runtime.md))负责**读取**元数据;`xlyManage` 负责**写入**元数据。
  6 +
  7 +## 服务总览
  8 +
  9 +| Service | 行数 | 拥有内容 | BACK 页面 |
  10 +|---|---:|---|---|
  11 +| `GdsmoduleServiceImpl` | 729 | `gdsmodule`(模块)、`gdsroute`(URL 白名单)、模块树 CRUD | 系统模块配置 |
  12 +| `GdsconfigformServiceImpl` | 878 | `gdsconfigformmaster`、`gdsconfigformslave`、`gdsconfigformcustomslave`、`gdsconfigformpersonalize`(表单定义 + 每租户覆盖) | 界面显示内容配置 |
  13 +| `GdsconfigtbServiceImpl` | 555 | `gdsconfigtbmaster`、`gdsconfigtbslave`(虚拟表定义) | 数据表内容配置 |
  14 +| `SqlScriptsServiceImpl` | 489 | BACK 中编写的 DDL / proc / view 脚本;使用 [`templesql/`](sql-templates.md) 模板 | Mysql脚本配置 |
  15 +| `GdsjurisdictionServiceImpl` | 362 | `gdsjurisdiction`(每模块动作**目录**;见[权限](../builder/permissions.md)) | 系统权限配置 + 配置流程的一部分 |
  16 +| `GdsparameterServiceImpl` | 319 | `gdsparameter`(系统级参数) | 参数页面 |
  17 +| `GdsformconstServiceImpl` | 243 | `gdsformconst`(每表单常量:标签、默认文本)。切片 1 锚点表。 | 系统常量配置 |
  18 +| `GdslogininfoServiceImpl` | 221 | `sftlogininfo*` 表族(用户 / 登录 / 用户组目录) | 用户信息配置 |
  19 +| `SysbrandsServiceImpl` | 125 | `sysbrands`(加工商 / 租户主数据) | 租户管理 |
  20 +| `CommonServiceImpl` | 56 | 其他 service 共用 helper | n/a |
  21 +
  22 +合计约 4000 行元数据 CRUD 逻辑,这是框架运行时里相当大的部分,之前 Wiki 没有覆盖。
  23 +
  24 +## 方法形状约定
  25 +
  26 +每个 `Gds*Service` 基本都遵循同一个五方法形状:
  27 +
  28 +```java
  29 +Feedback<Map<String,Object>> getXxx(Map<String, Object> params); // 列表 / 分页读取
  30 +Feedback<Map<String,Object>> getXxxBysId(Map<String, Object> params); // 单行读取
  31 +Feedback<Map<String,Object>> addXxx(Map<String, Object> params); // 插入
  32 +Feedback<Map<String,Object>> updateXxx(Map<String, Object> params); // 更新
  33 +Feedback<Map<String,Object>> deleteXxx(Map<String, Object> params); // 删除(常见为 bInvalid 软删)
  34 +```
  35 +
  36 +对应 `Gds*Controller` 的 endpoint 方法也几乎逐字镜像这个形状。因此 BACK 管理接口面本质上是一个薄 pass-through:controller 在 `xlyEntry/.../systemweb/`,service 在 `xlyManage/.../systeminfo/impl/`。这和运行时侧相反:运行时由一个通用 `BusinessBaseController` 处理任意表上的业务数据 CRUD;这里每类框架元数据 CRUD 都有自己的 controller + service 对。
  37 +
  38 +## 值得注意的细节
  39 +
  40 +- **`GdsconfigformServiceImpl` 最大**,因为它拥有四张强耦合表(form-master、form-slave、customslave、personalize)以及 **DDL 脚本生成**流程。`getFormslaveScriptSqlPro`、`getMasterSlaveScriptSqlPro` 等方法会生成工程师可应用到物理表上的 SQL,用于新增字段。这让定制覆盖模型([切片 4](../../slices/04-custom-field.md))端到端可用:BACK 配置器也能生成覆盖所暗示的 schema migration SQL。
  41 +- **`GdsmoduleServiceImpl` 包含 `getModuleTreePro`**,这是 SPA 登录后调用的模块树解析(实时 trace 中第一个 `/gdsmodule/getModuleTreePro` 请求)。版本可见性([切片 2](../../slices/02-multi-tenancy.md))最终由许可证产出的 `sVerifyLicense` 模块 id 列表控制,而不是直接按 `sVersionFlowId` 条件过滤。
  42 +- **`SqlScriptsServiceImpl`** 把 [`templesql/`](sql-templates.md) 脚手架接到 BACK 脚本编写页面。工程师填写占位规格后,service 会生成可编译的 proc / view body,并在连接的 schema 上执行。
  43 +- **`GdsjurisdictionServiceImpl` 写动作*目录***;`sysjurisdiction`(每用户授权表)由其他路径写入(`xlyBusinessService` 的权限管理流程)。目录 vs 授权的区别见[如何设置权限](../builder/permissions.md)。
  44 +- **`SysbrandsServiceImpl`** 写租户主数据(`sysbrands` + `sBrandsId` 行);新租户初始化基本就是这里插入一行,再种子化元数据。
  45 +
  46 +## 缓存失效钩子点
  47 +
  48 +这些 service 的写方法直接带有自己的 `@CacheEvict` 注解(例如 `GdsmoduleServiceImpl.java:96` 的 `addGdsmodule` 会清理 9 个 cache region)。这就是 BACK 中的元数据编辑能立即在各节点生效的原因:共享 Redis 缓存(RedisCacheManager,见 [cache-invalidation.md](cache-invalidation.md))会在写事务提交时清理相关 region。这里**没有 JMS 扇出用于清缓存**,这是常见误解,缓存失效页已有详细说明。(业务数据通过 `BusinessBaseServiceImpl` 写入时走另一条 `BusinessCleanRedisData.delCleanRedisData*` 分发路径;两条路径都在缓存失效页说明。)
  49 +
  50 +## `xlyManage` 不包含什么
  51 +
  52 +- **业务数据 CRUD。** 那是通用 `BusinessBaseController` + `BusinessBaseServiceImpl` 路径([runtime.md](runtime.md)、[切片 1](../../slices/01-hello-world.md))。
  53 +- **API 元数据**(`sysapi`)。那是 `xlyApi` 自己的管理接口面;见[外部 API](../../api-reference/external.md)。
  54 +- **工作流元数据**(`gdsmoduleflow`、`act_*`、`biz_flow`)。那在 `xlyFlow`;见 [activiti.md](activiti.md)。
  55 +
  56 +## 出问题时先看哪里
  57 +
  58 +| 症状 | 优先查看 |
  59 +|---|---|
  60 +| BACK 对 `gdsconfigform*` 修改 / 新增返回“操作失败” | `GdsconfigformServiceImpl`,检查字段校验和匹配的 DDL 脚本生成路径 |
  61 +| 版本可见性显示了错误模块 | 菜单 / 模块树 SQL,以及 `VerifyLicense.getModelAllList()` / `sVerifyLicense`,确认允许的模块 id 列表 |
  62 +| BACK 脚本编写页面生成了错误 SQL | `SqlScriptsServiceImpl` + [templesql 脚手架](sql-templates.md) |
  63 +| 某模块缺少权限动作目录(BtnAdd / BtnUpd / …) | `GdsjurisdictionServiceImpl`,检查该 `sParentId` 下的行 |
  64 +| 用户能登录 BACK 但 FROUNT 为空 | `GdslogininfoServiceImpl`,检查 `sftlogininfo*` 连接表 |
... ...
zh/docs/reference/maintainer/proc-dispatch.md
1 1 # 通用存储过程分发
2 2  
3   -当 `gdsmodule.sSaveProName`(或 `sDeleteProName`、`sCalcProName`、`sProcName`、`sSaveProNameBefore`)**非空**时,框架会调用指定存储过程,而不是落入默认 Add/Update 路径。同一套机制也处理按钮计算和按需自定义逻辑
  3 +当元数据命名一个存储过程时(例如通过 `gdsmodule.sSaveProName`、`sSaveProNameBefore`、`sDeleteProName`、`sCalcProName`、`sProcName`,或 `gdsconfigformslave.sButtonParam`),框架会按名称分发该过程。`sSaveProName` 和 `sSaveProNameBefore` 是**钩子点**:它们作为保存后 / 保存前阶段叠加在始终会运行的基础新增 / 更新路径(`BusinessBaseServiceImpl.addBusinessData` / `updateBusinessData`)之上,由 `BusinessBaseServiceImpl.java:1824` 与 `:1778` 的 `checkUpdate(...,"sSaveProName")` 调用。其他列驱动按需调用:`sCalcProName` 用于按钮计算,`sProcName` 用于自定义取数流等。同一套通用分发机制处理这些情况
4 4  
5 5 处理器是 `xlyEntry/com/xly/web/businessweb/` 中的 `GenericProcedureCallController`。
6 6  
7 7 ## 端点形状
8 8  
9   -前端向 `/business/genericProcedureCall*` 下的 URL POST:
  9 +前端向 `/procedureCall/doGenericProcedureCall` POST:
10 10  
11 11 - 存储过程名(通常从 `gdsmodule` 或 `gdsconfigformslave.sButtonParam` 解析)。
12 12 - 参数值(前端提供,框架注入租户身份)。
... ... @@ -49,8 +49,31 @@ xly 过程共享一套调用约定,以便通用分发可行:
49 49  
50 50 这些是工程师编写新过程时填充的模板;运行时不会用它们动态生成过程。loader 和占位符语法见 [SQL 模板](sql-templates.md)。
51 51  
  52 +## 实时 schema 中的过程命名模具
  53 +
  54 +实时 DB 的 1687 个存储过程围绕少数命名模具聚集,不只是裸 `Sp_*` 家族:
  55 +
  56 +| 模具 | 约数 | 角色 |
  57 +|---|---:|---|
  58 +| `Sp_*` | 大多数 | 主导家族,由 `sSaveProName` / `sCalcProName` / `sProcName` 等分发。 |
  59 +| `Sp_*_BeforeSave` | ~62 | 保存前钩子,对应 `sSaveProNameBefore`。 |
  60 +| `Sp_*_AfterSave` / `Sp_*_SaveReturn` | ~62 / ~54 | 保存后钩子;`_SaveReturn` 会写回父事务。 |
  61 +| `Sp_*_Calc` | ~178 | 按钮计算过程,由按钮流程(`sCalcProName` / `sButtonParam`)调用。 |
  62 +| `sp_btn_*` | ~65 | 按钮事件子家族,通常是 `sp_btn_calc*` / `sp_btn_validate*`(约定使用小写)。 |
  63 +| `PRO_ERPMERGE*` | ~11 | 数据迁移 / 合并工具。**不由运行时分发**,只给工程师直接使用。 |
  64 +| `PRO_*`(其他) | ~12 | 其他一次性工具。 |
  65 +| `Get_*`、`del_*`、`Cal*`、`Tj_*` | 少量 | 历史 / 领域特定 helper,不属于通用分发契约。 |
  66 +
  67 +被分发列里如果写错过程名,通常也会期望目标是 `Sp_*` 形状;其他模具不会通过 `sSaveProName` / `sCalcProName` 等解析。非 `Sp_*` 过程只能通过 mapper XML 或其他过程直接调用。
  68 +
  69 +## 函数层
  70 +
  71 +schema 还包含 **177 个用户自定义函数**,命名模具与过程平行:`Fun_*`(约 150)、`Fn_*`(约 8)、`get_*`(约 10)。
  72 +
  73 +这些函数**不由 Java 分发**。它们从其他存储过程、视图定义和 mapper XML 的 SELECT 语句中被调用。没有 `gdsmodule.sFunctionName` 之类的元数据列;函数由提到它们的 SQL 自行引入。维护人员排查慢报表时,应在过程和视图里 grep `Fun_*` / `Fn_*` / `get_*` 引用;框架 Java 侧看不到这些函数。
  74 +
52 75 ## 需要关注的失效模式
53 76  
54 77 1. **参数顺序不匹配。** 通用分发按位置绑定;IN 参数重排的过程会在运行时炸掉。
55 78 2. **缺少租户过滤。** 某过程在内部谓词中忘记 `sBrandsId` / `sSubsidiaryId`,就是多租户泄漏。过程不会被框架自动过滤,必须自己处理。
56   -3. **长运行过程阻塞请求线程。** 长计算应放到独立 worker(见 `xlyErpTask`,尽管它当前在 `settings.gradle` 中禁用)。
  79 +3. **长运行过程阻塞请求线程。** 长计算应放到独立后台线程或任务服务中(见 `xlyErpTask`,尽管它当前在 `settings.gradle` 中禁用)。
... ...
zh/docs/reference/maintainer/running-locally.md
... ... @@ -51,7 +51,7 @@ Wiki 的侦察脚本通过 `~/.my.cnf` 使用 `xlyweberp_saas_ai`,这和 `appl
51 51 - `xlyApi` — 如果需要外部 API 接口面,请作为第二个 JVM 启动。
52 52 - `xlyInterface` — 同上。
53 53 - `xlyPlc`、`xlyFlow`、`xlyFace` — 同上;每个服务都有自己的 application class 和 profile。
54   -- `xlyErpJms*` consumer — 它们是独立的 Spring Boot app。没有它们,JMS 驱动的缓存失效不会跨节点发生(单节点开发机通常可以接受)
  54 +- `xlyErpJms*` consumer — 它们是独立的 Spring Boot app。没有它们,跨节点的基础数据合并 / 消息扇出不会发生;元数据保存后的 Redis 缓存清理由 BACK 侧同步 `@CacheEvict` 完成,单节点开发机通常可以接受
55 55  
56 56 多服务本地开发见[多服务部署](deployment.md)。
57 57  
... ...
zh/docs/reference/maintainer/runtime.md
... ... @@ -6,29 +6,85 @@
6 6  
7 7 | 类 | 包 | 角色 | 最常引用端点 |
8 8 |---|---|---|---|
9   -| `BusinessBaseController` | `web/businessweb/` | 元数据驱动模块的通用 CRUD。每个表单的默认 API surface。 | `/business/getModelBysId/{moduleId}`、`/business/getBusinessDataByFormcustomId/{formId}`、`/business/addUpdateDelBusinessData`、`/business/getSelectDataBysControlId/{controlId}` |
  9 +| `BusinessBaseController` | `web/businessweb/` | 元数据驱动模块的通用 CRUD。每个表单的默认 API 接口面。 | `/business/getModelBysId/{moduleId}`、`/business/getBusinessDataByFormcustomId/{formId}`、`/business/addUpdateDelBusinessData`、`/business/getSelectDataBysControlId/{controlId}` |
10 10 | `BusinessConfigformController` | `web/businessweb/` | 已有表单的每用户 / 每组显示定制,不是基础表单定义 CRUD。 | `/configform/getConfigformData/{moduleId}`、`/configform/sHandleConfigform`、`/configform/sCopyConfigform` |
11 11 | `GdsmoduleController` | `web/systemweb/` | builder 侧使用的模块树和模块定义 CRUD。 | `/gdsmodule/getModuleTreePro`、`/gdsmodule/addGdsmodule`、`/gdsmodule/updateGdsmodule` |
12 12 | `GdsconfigformController` | `web/systemweb/` | form-master 和 form-slave 元数据 CRUD。 | `/gdsconfigform/*` 下端点 |
13 13 | `GdsconfigtbController` | `web/systemweb/` | 虚拟表 master / slave 元数据 CRUD。 | `/gdsconfigtb/*` 下端点 |
14   -| `BusinessTreeGridController` | `web/businessweb/` | 树表格端点。当前分支实现了 proc-backed 路径,普通 `getTreeGrid` service 方法仍是 stub。 | `/treegrid/getTreeGridByPro/{formId}` |
  14 +| `BusinessTreeGridController` | `web/businessweb/` | 树表格端点。当前分支实现了存储过程支撑路径,普通 `getTreeGrid` service 方法仍是空实现。 | `/treegrid/getTreeGridByPro/{formId}` |
15 15 | `GenericProcedureCallController` | `web/businessweb/` | 按名称 + 参数通用调用存储过程。 | `/procedureCall/doGenericProcedureCall` |
16 16 | `ConfigformPanelController` | `web/businessweb/` | `gdsconfigformpanel` 中的面板布局持久化。 | `/panel/get/{sFormId}`、`/panel/save/{sFormId}` |
17   -| `CheckFlowController` | `web/businessweb/` | Activiti 工作流 surface(审批 / 驳回 / 查看),仅在部署工作流时有意义。 | `/checkFlow/*` 下端点 |
  17 +| `CheckFlowController` | `web/businessweb/` | **空壳。** 类文件只有 22 行:一个 `@RestController @RequestMapping(value="/checkflow")`,没有 handler 方法。`/checkflow/*` 返回 404。真正的工作流审核 / 驳回 / 完成 URL 在 `CurrencyFlowController`(位于 `xlyFlow`,经 xlyEntry context-path 暴露)中;见 [Activiti 集成](activiti.md#modeler-暴露的-urlxlyflow-controller-挂在-xlyentry-端口上)。 | 无,空类 |
  18 +| `BusinessModelCenterController` | `web/businessweb/` | FROUNT 首页的“KPI 工作中心”看板:聚合标记为 `gdsmodule.bUnTask=1` 的模块上的未清任务。尽管 UI 像工作流,**它不是 Activiti 驱动**;它读取 `gdsmodule` 行,按 `sUnType` ∈ {`Pending`, `PendingCheck`, `MyWarning`} 分桶,并按用户缓存。见下面的 [KPI 工作中心](#kpi-工作中心front-端首页-dashboard)。 | `/modelCenter/getModelCenter`、`/modelCenter/getModelCenterCalculation` |
18 19  
19 20 注意 controller 分布在**两个包**中:`businessweb/` 承载运行时端点,`systemweb/` 承载 builder 侧元数据 CRUD 端点。两者都编译进同一个 `xlyEntry` WAR。
20 21  
21   -## 四表读取
  22 +## KPI 工作中心(FROUNT 首页看板) {#kpi-工作中心front-端首页-dashboard}
  23 +
  24 +用户进入 FROUNT(`http://<host>:8598/indexPage`)时,首页会显示一个标题为 **`KPI监控`** 的卡片。尽管名字叫 KPI,它不是分析意义上的 KPI 看板:没有目标、没有图表、没有指标度量。它是一个**未清任务聚合器**:把“需要你处理的事”按角色和业务流程汇总成统一入口。
  25 +
  26 +处理链路:
  27 +
  28 +- `POST /xlyEntry/modelCenter/getModelCenter` → `BusinessModelCenterController.java:44-48` → `BusinessModelCenterServiceImpl.getKPIModelList`(很薄的一层 wrapper)→ `xlyBusinessService` 中的 `BusinessModelKpiServiceImpl.getKPIModelList`。
  29 +- `POST /xlyEntry/modelCenter/getModelCenterCalculation` 会重新计算数量,并清掉 `getKpiModelByUser` 缓存区域。
  30 +
  31 +`getModelCenter` 的 Javadoc 称它为**“初次获取KPI工作中心”**,这是框架内部对该接口面的命名。
  32 +
  33 +### 数据来源:`gdsmodule` 未清任务标记
  34 +
  35 +KPI 工作中心读取的是 `gdsmodule`,不是 Activiti 的 `act_*` 表。参与看板的每个模块都会在自己的 `gdsmodule` 行上设置四个列:
  36 +
  37 +| 列 | 注释 | 含义 |
  38 +|---|---|---|
  39 +| `bUnTask` | `是否增加到未清` | `1` 表示纳入看板,`0` 表示忽略 |
  40 +| `sUnType` | `未清类型` | 分桶:`Pending`、`PendingCheck`、`MyWarning` 之一 |
  41 +| `sChineseUnMemo`、`sEnglishUnMemo`、`sBig5UnMemo` | `未清描述` | 各语言显示标签 |
  42 +
  43 +实时 dev DB 中,`bUnTask = 1` 的模块有 **92** 行,分布如下:
  44 +
  45 +| `sUnType` | 数量 | 前端分桶标签 |
  46 +|---|---:|---|
  47 +| `Pending` | 79 | 待处理事务 |
  48 +| `PendingCheck` | 1 | 发起新事务 |
  49 +| `MyWarning` | 3 | 我的预警报表 |
  50 +| NULL | 9 | 未标记,排除 |
  51 +
  52 +对每个已标记模块,`BusinessModelKpiServiceImpl` 会执行该模块的每用户数量查询,并按两种维度聚合:
  53 +
  54 +- **角色**(UI 中“按角色”):JOIN `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify`,把当前用户映射到角色,再按角色拆分数量。
  55 +- **流程**(UI 中“按流程”):`估价管理流程` / `订单生产流程` 这类标签来自 `gdsmodule` 树中的父模块。每个业务流程是一个父模块,下面包含 1 到 N 个有序子模块。UI 中 “01/04、02/04 …” 的步骤编号,就是子模块在父流程下的 `iOrder`。
  56 +
  57 +### 缓存
  58 +
  59 +`@Cacheable(value="getKpiModelByUser", key=...)` 会缓存每用户结果;`getModelCenterCalculation` 以及任何修改 `gdsmodule` 的路径都会通过 [`CleanRedisServiceImpl`](cache-invalidation.md) 失效这个区域。由于 `bUnTask` / `sUnType` 可通过 BACK 的“系统模块配置”页面编辑,维护人员要给看板加一个新的“未清任务”,改的是元数据,不是 Java。
  60 +
  61 +### 为什么不是 Activiti
  62 +
  63 +Activiti 的职责是**审批工作流**:当某行需要经过 N 步 `act_re_procdef` 图签核时,`act_ru_task` / `biz_todo_item` 表会按处理人保存待办任务。它和 KPI 工作中心是不同接口面,虽然两者都会向用户展示“需要处理的事”。
  64 +
  65 +当前 dev DB 中,`biz_todo_item` 和 `biz_flow` 都是空表(0 行),但 KPI 工作中心仍能显示非零数量。这是两套系统相互独立的实证。启用 Activiti 流程的部署会通过工作流 controller 和单独的流程面板暴露这些任务,而不是通过 KPI 工作中心。
  66 +
  67 +## 五键读取 {#five-key-read}
22 68  
23 69 对任何元数据驱动模块,请求生命周期(见[概念 → 请求生命周期](../../concepts/request-lifecycle.md))可归结为:
24 70  
25 71 ```java
26 72 public Map<String, Object> getModelBysId(Map<String, Object> map) {
27   - List<Map<String, Object>> formList = this.getModelConfigByModleId(map); // 1. join gdsmodule⋈form-master⋈form-slave
28   - List<Map<String, Object>> fList = businessGdsconfigformsService.getFormconstData(qMap); // 2. gdsformconst
29   - List<Map<String, Object>> jList = businessGdsconfigformsService.getJurisdictionData(qMap); // 3. gdsjurisdiction(ADMIN 跳过)
30   - Map<String, Object> billnosettingMap = businessGdsconfigformsService.getBillnosettingData(param); // 4. sysbillnosettings
31   - List<Map<String, Object>> reportList = printReportService.getReportData(qMap); // 5. sysreport
  73 + // 1. formData:gdsconfigformmaster 按 sParentId=sModelsId 过滤,
  74 + // LEFT JOIN gdsconfigformpersonalize(每租户),再为每个 master 行
  75 + // 加载 gdsconfigformslave + gdsconfigformcustomslave 覆盖。
  76 + // gdsmodule 本身只通过 id 引用,不被 SELECT。
  77 + List<Map<String, Object>> formList = this.getModelConfigByModleId(map);
  78 + // 2. gdsformconst:仅按 sParentId;sLanguage 决定返回哪列标签;不按租户过滤。
  79 + List<Map<String, Object>> fList = businessGdsconfigformsService.getFormconstData(qMap);
  80 + // 3. sysjurisdiction:每用户授权,JOIN sftlogininfojurisdictiongroup
  81 + // + sisjurisdictionclassify;ADMIN 跳过。
  82 + // 虽然返回 map key 叫 `gdsjurisdiction`,实际源表是 sysjurisdiction。
  83 + List<Map<String, Object>> jList = businessGdsconfigformsService.getJurisdictionData(qMap);
  84 + // 4. sysbillnosettings(每租户、每表单)。
  85 + Map<String, Object> billnosettingMap = businessGdsconfigformsService.getBillnosettingData(param);
  86 + // 5. sysreport(每租户、每表单)。
  87 + List<Map<String, Object>> reportList = printReportService.getReportData(qMap);
32 88 return composite(formList, fList, jList, billnosettingMap, reportList);
33 89 }
34 90 ```
... ... @@ -39,11 +95,11 @@ public Map&lt;String, Object&gt; getModelBysId(Map&lt;String, Object&gt; map) {
39 95  
40 96 | Key | 来源 | 前端用途 |
41 97 |---|---|---|
42   -| `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave`(+ 覆盖) | 表单布局 |
43   -| `gdsformconst` | `gdsformconst` | 表单级常量、下拉标签 |
44   -| `gdsjurisdiction` | `gdsjurisdiction` | 按钮 / 数据权限 |
45   -| `billnosetting` | `sysbillnosettings` | 单据编号规则 |
46   -| `report` | `sysreport` | 打印模板 |
  98 +| `formData` | `gdsconfigformmaster`(按 `sParentId = sModelsId` 过滤)⋈ `gdsconfigformpersonalize` 覆盖;每个 master 行再加载 `gdsconfigformslave` + `gdsconfigformcustomslave`。`gdsmodule` 只作为 id 来源被引用,不参与 join。 | 表单布局 |
  99 +| `gdsformconst` | `gdsformconst`(仅按 `sParentId` 过滤;`sLanguage` 决定返回哪列标签;不按租户过滤) | 表单级常量、下拉标签 |
  100 +| `gdsjurisdiction` | `sysjurisdiction`(JOIN `sftlogininfojurisdictiongroup` + `sisjurisdictionclassify` 得到每用户 / 用户组授权);ADMIN 跳过。**注意:** map key 名称 `gdsjurisdiction` 有误导性,`gdsjurisdiction` 是配置侧动作目录表;这里读取的每用户授权实际来自 `sysjurisdiction`。 | 按钮 / 数据权限 |
  101 +| `billnosetting` | `sysbillnosettings`(每租户、每表单) | 单据编号规则 |
  102 +| `report` | `sysreport`(每租户、每表单) | 打印模板 |
47 103  
48 104 ## 保存端点
49 105  
... ... @@ -57,7 +113,9 @@ public Map&lt;String, Object&gt; getModelBysId(Map&lt;String, Object&gt; map) {
57 113 }
58 114 ```
59 115  
60   -当 `gdsmodule.sSaveProName` 为空时,框架默认 Add/Update 路径运行,即 `AddDelUpdCommonServiceImpl.java`。非空时,调用指定存储过程。
  116 +基础新增 / 更新路径总是通过 `BusinessBaseServiceImpl.addBusinessData` / `updateBusinessData`(`xlyBusinessService/.../BusinessBaseServiceImpl.java:1014` 和 `:1250`),再委托 `businessBaseDao.add(map)` / `businessBaseDao.update(map)`,对 `sTable` 命名的表执行操作。
  117 +
  118 +`gdsmodule.sSaveProName`(及其兄弟列 `sSaveProNameBefore`)**不是**替换基础路径的二选一分支;它命名的是叠加在基础路径之上的额外存储过程钩子:保存后 / 保存前由 `BusinessBaseServiceImpl.java:1824` 的 `checkUpdate(...,"sSaveProName")` 和 `CheckSaveServiceImpl.java` 分发。`AddDelUpdCommonServiceImpl`(`@Service("addDelUpdCommonService")`)是另一套可复用 `insertByMap` / `updateByMap` / `delByMap` / `addBatch` helper,被工单计划、OEE、多报价、订单采购等领域 service 使用;它**不是** `addUpdateDelBusinessData` 的默认新增 / 更新路径。
61 119  
62 120 ## 多租户边界
63 121  
... ... @@ -73,9 +131,20 @@ RequestAddParamUtil.me().addParams(params, userInfo);
73 131  
74 132 ## 需要审计的安全关注点
75 133  
76   -1. **`addUpdateDelBusinessData` 中的 `sTable` 校验。** 前端直接命名目标表。运行时必须交叉检查传入表是否属于该表单授权的支撑表,否则是权限提升面。若不存在检查,应作为安全 ticket 提出。见[切片 1 v2 follow-up](../../slices/01-hello-world.md#open-verification-items)。
  134 +1. **`addUpdateDelBusinessData` 中的 `sTable` 校验:已确认缺失。** 前端直接命名目标表,而运行时**不会**交叉检查传入表是否属于该表单授权的支撑表。`BusinessBaseServiceImpl.sTableNameList`(162-169 行)是多租户作用域绕过列表(四张全局框架元数据表,写入时剥掉 `sBrandsId` / `sSubsidiaryId`;见 165 行 `//不需要公司子公司的表` 注释),不是支撑表白名单。整个流程里唯一的模块 / 表交叉检查是 1768 行的硬编码特例(`mftproductionplanslave`)。确实存在一些缓解措施(`RequestAddParamUtil` 的租户作用域、可选的保存前 / 后存储过程校验),但它们都不是支撑表白名单。完整追踪见[切片 1 follow-up](../../slices/01-hello-world.md#open-verification-items)。
77 135 2. **ADMIN 绕过权限。** `BusinessBaseServiceImpl` 对 `UserType.ADMIN` 完全跳过 `gdsjurisdiction` 加载。ADMIN 账号治理必须来自应用外部。
78 136  
  137 +## “通用 CRUD”在实践中意味着什么
  138 +
  139 +“一个 controller 写任意表中的任意行”是 xly 数据驱动设计的核心动作,也会把风险集中到少数路径上:
  140 +
  141 +- **`BusinessBaseServiceImpl` 已经约 3,900 行**,其中缠在一起的逻辑包括每租户作用域绕过列表、特定表硬编码(第 1768 行的 `mftproductionplanslave`)、保存前 / 保存后钩子分发、以及由 `sTable` 驱动的写入路由。每个 bug fix 都必须穿过这个大类。
  142 +- **它是整个业务运行时的单点故障。** `addUpdateDelBusinessData` 中的回归会同时破坏所有租户的所有表单保存。模块专用 controller 可以把爆炸半径限制在一个模块内;通用 controller 做不到。
  143 +- **`Map<String, Object>` 没有类型系统。** 前端传来一袋 key/value。运行时相信 key 与列名匹配、value 能转换成列类型。不匹配时通常在 DAO 层抛 `BadSqlGrammarException`,离错误来源已经很远。这里没有 schema-aware 的请求校验。
  144 +- **可发现性差。** “哪些端点会写 `mftproductionplanslave`?”不能靠 IDE find-usages 回答。真实答案是:任何调用 `BusinessBaseServiceImpl.addBusinessData` 且把 `sTable` 设为 `mftproductionplanslave` 的 controller,也就是几乎所有通用保存入口。
  145 +
  146 +通用模式让数据驱动论点成立;它也是新增模块几乎免费的原因,**同时**也是修改运行时几乎从不免费的原因。
  147 +
79 148 ## 缓存失效
80 149  
81   -**后台**保存元数据变更时会触发 JMS 消息,`xlyErpJmsConsumer` 中的 `ConsumerChangeGdsModuleThread` 会清除每个运行节点上的元数据缓存。见[元数据变更后的缓存失效](cache-invalidation.md)。
  150 +BACK 保存元数据变更时,保存 service 会同步调用 `BusinessCleanRedisData.delCleanRedisData*`,进而触发 `CleanRedisServiceImpl` 上相关缓存区域的 `@CacheEvict`。另有一个名字相近的 JMS 路径(`ConsumerChangeGdsModuleThread`),但它通过存储过程做基础数据合并,不做缓存失效。完整说明见[元数据变更后的缓存失效](cache-invalidation.md),包括跨节点一致性问题。
... ...
zh/docs/reference/maintainer/sql-templates.md
... ... @@ -62,6 +62,17 @@ END
62 62  
63 63 不遵守这些约定的过程无法通过通用分发调用,只能从自定义 Java 代码调用。
64 64  
  65 +## 两个 loader
  66 +
  67 +代码库里有**两个**名为 `FileSqlUtil` 的类,但可靠性完全不同:
  68 +
  69 +| Loader | 指向内容 | 状态 |
  70 +|---|---|---|
  71 +| `xlyFlow/src/main/java/com/xly/sqltemplate/util/FileSqlUtil.java` | 上述 8 个脚手架 | **8 个文件都存在**,这是 BACK 脚本编写页面实际使用的 loader。 |
  72 +| `xlyApi/src/main/java/com/xly/api/util/FileSqlUtil.java` | 7 个不同模板常量:`sInSqlStrTemple.sql`、`sOutSqlStrTemple.sql`、`sDataSqlTmpDef.sql`、`sDataSqlTmp.sql`、`sJsonSqlTmp.sql`、`sDbPro.sql`、`sJsonSqlTmpOut.sql` | **这些文件在当前树中都不存在**。这些常量是死代码,可能来自更早的 xlyApi 代码路径。通过它们调用会在运行时报 `FileNotFoundException`。 |
  73 +
  74 +维护人员如果要动 `xlyApi` 的 `FileSqlUtil`,应把它当作默认损坏:要么恢复缺失模板,要么删除这个 loader。
  75 +
65 76 ## 另见
66 77  
67 78 - [通用存储过程分发](proc-dispatch.md):过程写好后框架如何调用。
... ...
zh/docs/reference/maintainer/tech-stack.md 0 → 100644
  1 +# 技术栈
  2 +
  3 +本页是**范围内**框架的依赖清单:11 个框架核心模块(`xlyEntry`、`xlyApi`、`xlyManage`、`xlyBusinessService`、`xlyPersist`、`xlyEntity`、`xlyFlow`、`xlyInterface`、`xlyMsg`、`xlyErpJmsProductor`、`xlyErpJmsConsumer`)、一个插件(`xlyPlc`),以及一个共享工具模块(`xlyPlatConstant`;尽管名字带 `Plat*`,但 `xlyPersist` 依赖其中的 `MultiThreadServer` 和 `TimeContant`)。
  4 +
  5 +其他 plat 层模块(除 `xlyPlatConstant` 外的 `xlyPlat*`)、`xlyFace`(仍参与构建但不在文档范围内)和 AI 库均为[范围外](../../index.md),本页不列。
  6 +
  7 +## 如何读本页
  8 +
  9 +两个列提供证据:
  10 +
  11 +- **位置**:声明该库为 `api(...)` 或 `implementation(...)` 的 `build.gradle` 文件。多数库声明在 `xlyPersist/build.gradle`,并传递到依赖 `xlyPersist` 的模块。
  12 +- **范围内源码引用**:在上述范围内模块中对 `xly-src/<module>/src/` 执行 `grep -rln <package>` 的文件计数,并给出代表路径。“未发现源码引用”表示该库已声明,但范围内模块的 Java、HTML 或 yaml 源码没有直接引用。它仍可能作为传递依赖进入 classpath,或由 Spring Boot autoconfig 消费。
  13 +
  14 +本页不解释“为什么选择某个库”。如果 gradle 或 yaml 中有明确注释(例如 Netty 版本 pin),会引用该注释;否则只记录事实。
  15 +
  16 +## 1. 应用平台
  17 +
  18 +| Library | Version | 位置 | 范围内源码引用 |
  19 +|---|---|---|---|
  20 +| Spring Boot | 2.2.5 | `xlyPersist/build.gradle`(web、starter、aop、data-redis、data-mongodb、websocket、activemq、freemarker)、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 所有 `*ApplicationBoot.java` 类(`xlyEntry`、`xlyApi`、`xlyInterface`、`xlyPlc`、构建时的 `xlyFace`)都继承 `SpringBootServletInitializer`。 |
  21 +| Embedded Tomcat | Spring Boot 2.2.5 BOM | `spring-boot-starter-web` 传递依赖 | `xlyEntry/src/main/resources/application-local.yml` 10-29 行配置 `server.port: 8080`、`server.servlet.context-path: /xlyEntry`、`server.tomcat.*`。 |
  22 +| Netty | 4.1.65.Final | `xlyPersist/build.gradle` | 无直接 Java import。gradle 注释说明:“引入mq后netty-common-4.1.45.Final.jar、与netty-all-4.0.42.Final.jar冲突,所以引入”;该版本用于覆盖 MQ 库引入的冲突传递版本。 |
  23 +| AspectJ Weaver | 1.9.6 | `xlyApi/build.gradle`(声明两次) | 1 个文件在 `xlyFlow/src/main/java/...` import。主要消费者是 Spring Boot AOP starter;xlyApi 中的显式 pin 与 import 无关。 |
  24 +| Lombok | 1.18.8(`xlyPersist`、`xlyFlow`)/ 1.18.20(`xlyApi`) | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 19 个文件 import `lombok.*`。xlyApi 同时声明为 `implementation` 和 `annotationProcessor`。 |
  25 +
  26 +## 2. 持久化 {#persistence}
  27 +
  28 +| Library | Version | 位置 | 范围内源码引用 |
  29 +|---|---|---|---|
  30 +| MyBatis | 2.1.2 (`mybatis-spring-boot-starter`) | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 102 个 Java 文件 import `org.apache.ibatis.*` 或 `org.mybatis.*`;76 个在 `xlyPersist`。Mapper XML 位于 `xlyPersist/src/main/resources/mapper/{erptable,business,test}/`。 |
  31 +| MyBatis-Plus | 3.3.0 | `xlyApi/build.gradle` | 2 个文件:`xlyApi/.../SqlUtil.java`、`xlyApi/.../BaseController.java`。不在 xlyApi 之外使用。 |
  32 +| MySQL Connector/J | 8.0.13 | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | yaml 中 `spring.datasource.driverClassName: com.mysql.cj.jdbc.Driver`。 |
  33 +| MSSQL JDBC | sqljdbc4 3.0 + 本地 `mssql-jdbc-6.2.2.jre8.jar` | `xlyApi/build.gradle`、`xlyInterface/build.gradle`、`xlyFlow/build.gradle` | 5 个文件:xlyFlow 3 个,xlyInterface 2 个。 |
  34 +| Oracle JDBC | 本地 `ojdbc6-11.2.0.4.jar` | `xlyFlow/build.gradle` | xlyFlow 中 2 个文件。 |
  35 +| Druid | 1.2.16 | `xlyPersist/build.gradle`、`xlyApi/build.gradle` | 6 个 Java 文件 import `com.alibaba.druid.*`;16 个 `application-*.yml` 引用 Druid 配置。 |
  36 +| HikariCP | 4.0.3 | `xlyApi/build.gradle` | 8 个文件引用 `com.zaxxer.hikari`。Java 配置包括 `MasterDataSourceConfig.java`、`SlaveDataSourceConfig.java`。 |
  37 +| Flyway | 5.2.1 | `xlyPersist/build.gradle` | 无 Java import。通过 yaml `spring.flyway.*` 配置,`enabled: false`。脚本在 `xlyEntry/src/main/resources/flyway/V*__*.sql`。 |
  38 +| PageHelper | 4.1.1 | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 19 个文件 import `com.github.pagehelper.*`。 |
  39 +| jsqlparser | 3.2 | `xlyPersist/build.gradle` | 1 个 `xlyPersist/src/...` 文件 import `net.sf.jsqlparser`。 |
  40 +
  41 +## 3. 缓存与内存
  42 +
  43 +| Library | Version | 位置 | 范围内源码引用 |
  44 +|---|---|---|---|
  45 +| Spring Data Redis | 2.2.5 | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 3 个 Java 文件 import `org.springframework.data.redis.*`;所有模块 yaml 都有 `spring.redis.*` 配置块。 |
  46 +| Lettuce | Spring Data Redis 2.2.5 默认 driver | 传递依赖 | 无直接 Java import;yaml 中有 `spring.redis.lettuce.pool.*`。 |
  47 +| Jedis | 2.9.0 | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 5 个文件 import `redis.clients.jedis`,包括 `xlyMsg/.../wechat/util/JedisMsgUtil.java`。 |
  48 +| Guava | 18.0(`xlyPersist`、`xlyApi`);20.0(`xlyFlow`) | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 8 个文件 import `com.google.common.*`。 |
  49 +
  50 +## 4. 工作流与调度
  51 +
  52 +| Library | Version | 位置 | 范围内源码引用 |
  53 +|---|---|---|---|
  54 +| Activiti Engine | 5.17.0 | `xlyPersist/build.gradle`、`xlyApi/build.gradle`;由 `xlyFlow` 消费 | 35 个文件 import `org.activiti.*`。与 6.0 modeler 库的版本偏差见 [Activiti 集成](activiti.md)。 |
  55 +| Activiti Spring Boot REST API | 6.0.0 | `xlyFlow/build.gradle` | 由 Spring Boot autoconfig 和 xlyFlow 下 REST 端点消费。 |
  56 +| Activiti JSON Converter | 6.0.0 | `xlyFlow/build.gradle` | 用于 xlyFlow modeler 保存路径。 |
  57 +| Quartz | 2.3.0 | `xlyFlow/build.gradle` | 16 个文件 import `org.quartz.*`;xlyEntry yaml 通过 JDBC JobStore(`qrtz_*` 表)配置 `spring.quartz.*`。 |
  58 +
  59 +## 5. 消息
  60 +
  61 +| Library | Version | 位置 | 范围内源码引用 |
  62 +|---|---|---|---|
  63 +| Spring JMS + ActiveMQ starter | 2.2.5 | `xlyPersist/build.gradle` (`spring-boot-starter-activemq`) | 3 个文件 import `org.springframework.jms.*`;2 个文件 import `org.apache.activemq.*`。见[消息](../../api-reference/messaging.md)。 |
  64 +| RocketMQ Spring Boot Starter | 2.0.2 | `xlyPersist/build.gradle` | `xlyBusinessService/src/` 中 4 个文件 import `org.apache.rocketmq.*`。 |
  65 +
  66 +## 6. 视图 / 模板
  67 +
  68 +| Library | Version | 位置 | 范围内源码引用 |
  69 +|---|---|---|---|
  70 +| Thymeleaf | 3.0.15 | `xlyApi.build.gradle`、`xlyFlow.build.gradle`;也由 starter 传递 | 2 个 xlyFlow Java 文件 import;modeler 模板在 `xlyFlow/src/main/resources/templates/`。 |
  71 +| Freemarker | Spring starter 2.2.5 | `xlyPersist.build.gradle`、`xlyApi.build.gradle`、`xlyFlow.build.gradle` | 1 个 xlyFlow Java 文件 import。 |
  72 +| Apache Batik | 1.8 / 1.7 | `xlyPersist.build.gradle`、`xlyFlow.build.gradle` | 1 个 xlyFlow 文件 import;modeler 静态资源也携带 Batik 资产。 |
  73 +
  74 +## 7. 认证
  75 +
  76 +| Library | Version | 位置 | 范围内源码引用 |
  77 +|---|---|---|---|
  78 +| Apache Shiro (`shiro-spring`) | 1.3.2(`xlyPersist`)/ 1.4.2(`xlyApi`、`xlyFlow`) | `xlyPersist.build.gradle`、`xlyApi.build.gradle`、`xlyFlow.build.gradle` | 6 个 Java 文件 import。未发现 `@ConfigurationProperties("shiro")` 或 `shiro.*` 属性绑定,尽管 xlyEntry local yaml 有 `shiro:` 块。框架 HTTP auth 模式见 [API 参考](../../api-reference/index.md)。 |
  79 +| `shiro-ehcache` / `shiro-core` / `thymeleaf-extras-shiro` | 1.4.2 / 2.0.0 | `xlyFlow.build.gradle` | xlyFlow 模板中大量 Shiro tag;Java import 已计入上行。 |
  80 +| Bouncy Castle | `bcprov-jdk14:138` | `xlyApi.build.gradle` | 2 个 RSA 工具文件。 |
  81 +| commons-codec | 1.16.0 | `xlyPersist.build.gradle`、`xlyApi.build.gradle` | 18 个文件 import。 |
  82 +
  83 +## 8. 报表与导出
  84 +
  85 +打印 / 导出是框架第三方代码最大的消费面。
  86 +
  87 +| Library | Version / 形态 | 位置 / 引用 |
  88 +|---|---|---|
  89 +| iText 5.x 与 lowagie iText 2.x | `itextpdf` 5.5.0 + `com.lowagie:itext` 2.1.7 | 都在 `xlyPersist.build.gradle`;两条分支同时存在于 classpath。 |
  90 +| Aspose Cells / Words | 本地 jar | `xlyPersist.build.gradle`;多个 `xlyPersist/src/` 文件 import `com.aspose.*`。 |
  91 +| Apache POI | 4.1.2(`xlyPersist` / `xlyFlow`)/ 3.15(`xlyApi`) | 36 个文件 import。 |
  92 +| jxls + jxls-poi / jxls-jexcel | 2.8.1 / 1.0.9 | 22 个文件 import `org.jxls.*`。 |
  93 +| EasyExcel | 本地 4.0.3 jars | `xlyBusinessService` 中 10 个文件 import。 |
  94 +| JasperReports / OLAP4J | 本地 jar | 报表相关路径 import。 |
  95 +| ZXing / Barcode4J / Pinyin4j / PDFBox / Thumbnailator / jacob | 多版本 | 条码、拼音、PDF、缩略图、COM 自动化等报表辅助功能。 |
  96 +
  97 +## 9. 文件存储与 HTTP 客户端
  98 +
  99 +| Library | Version | 位置 / 引用 |
  100 +|---|---|---|
  101 +| Aliyun OSS SDK | 2.2.0 | `xlyPersist/src/main/java/com/xly/utils/OssUtil.java`。 |
  102 +| commons-fileupload / commons-io | 1.5 / 2.5 | xlyFlow、xlyPersist、xlyEntry、xlyBusinessService 中有 import。 |
  103 +| OkHttp + Okio | 4.10.0 / 2.10.0 | xlyApi 中 2 个文件 import。 |
  104 +| Apache HttpClient | 4.5.5 | xlyBusinessService 中 1 个文件 import。 |
  105 +| javax.mail | 1.6.2 | xlyPersist 中 1 个文件 import。 |
  106 +
  107 +## 10. JSON 与通用工具
  108 +
  109 +| Library | Version | 范围内引用 |
  110 +|---|---|---|
  111 +| FastJson | 1.2.15(`xlyPersist`、`xlyApi`)/ 1.2.60(`xlyFlow`) | 83 个文件 import。 |
  112 +| Jackson | 2.9.7(xlyFlow 显式)+ Spring 传递依赖 | 22 个文件 import。 |
  113 +| Hutool | 5.6.5(`xlyPersist`)/ 5.8.5(`xlyApi`、`xlyFlow`) | 271 个文件 import,覆盖所有范围内模块。 |
  114 +| commons-lang3 / commons-collections4 | 3.6 / 3.8.1;4.1 | 多模块 import。 |
  115 +| Groovy | `groovy-all` 3.0.2 | 5 个 Java 文件 import `groovy.util.logging.Slf4j`,看起来是遗留 import。 |
  116 +| Struts2 JSON plugin | 2.5.30 | 仅 `xlyPersist/src/main/java/com/xly/utils/FeedPage.java`;框架其他部分运行在 Spring MVC 上。 |
  117 +| SnakeYAML / JDOM / validation-api | 多版本 | xlyFlow、xlyApi、xlyInterface、xlyMsg 等少量引用。 |
  118 +
  119 +## 11. 硬件集成
  120 +
  121 +| Library | Version | 范围内源码引用 |
  122 +|---|---|---|
  123 +| HslCommunication | 本地 `HslCommunication.jar` | 9 个文件引用,分布在 xlyPersist、xlyBusinessService、xlyPlc。xlyPlc 是 PLC 桥;见[切片 6](../../slices/06-hardware.md)。 |
  124 +
  125 +## 12. 通知
  126 +
  127 +| Library | Version | 范围内源码引用 |
  128 +|---|---|---|
  129 +| Aliyun DingTalk SDK | `com.aliyun:dingtalk` 2.1.14 | `xlyMsg/src/main/java/com/xly/dingtalk/` 中 1 个文件;见[通知](../../api-reference/notifications.md)。 |
  130 +| `alibaba-dingtalk-service-sdk` | 2.0.0 | xlyMsg 中 1 个文件 import `com.dingtalk.api.*`。 |
  131 +| Jeewx-API(微信) | 本地 `jeewx-api-1.3.2.jar` | xlyInterface 中 5 个文件引用。 |
  132 +
  133 +## 13. 授权许可
  134 +
  135 +| Library | Version | 范围内源码引用 |
  136 +|---|---|---|
  137 +| TrueLicense | 本地 `trueswing.jar` + `truexml.jar` + `turelicense.jar` | `xlyBusinessService/src/main/java/com/xly/license/` 下 5 个文件;xlyEntry local yaml 中 `License:` 块默认 `checkLic: false`。 |
  138 +
  139 +## 14. 日志
  140 +
  141 +| Library | Version | 范围内源码引用 |
  142 +|---|---|---|
  143 +| Logback | `logback-classic` 1.2.3 | 5 个文件 import;配置在 `xlyEntry/src/main/resources/logback-spring.xml`。 |
  144 +| log4j 1.x | 1.2.17 | xlyFlow 中 1 个文件 import;Druid stat filter yaml 中也出现 `filters: stat,log4j2`。 |
  145 +
  146 +## 15. 构建与开发
  147 +
  148 +| Library | Version | 用途 |
  149 +|---|---|---|
  150 +| Gradle wrapper | 已提交 | 构建工具;见[本地运行](running-locally.md)。 |
  151 +| Spring Boot Gradle plugin | 2.2.5.RELEASE | repo 根 `build.gradle`,构建可运行 WAR。 |
  152 +| Spring Boot configuration processor | 2.2.5.RELEASE | xlyApi annotationProcessor,用于 `@ConfigurationProperties` IDE 元数据。 |
  153 +
  154 +## 已声明但未发现范围内源码引用
  155 +
  156 +以下库出现在 `build.gradle` 中,但在 `xly-src/<in-scope module>/src/` 下未发现 Java import、HTML 模板引用或 yaml 属性绑定。它们可能是传递依赖、Spring Boot autoconfig 消费的库、已删除代码遗留,或只是冗余声明。
  157 +
  158 +| Library | 声明位置 | 备注 |
  159 +|---|---|---|
  160 +| Kaptcha、JNA、oshi-core、UserAgentUtils | `xlyFlow.build.gradle` | 未发现 import;分别用于验证码、原生访问、系统信息、UA 解析。 |
  161 +| Barbecue、Gson、commons-pool2 | `xlyPersist.build.gradle` | 未发现 import;当前活跃条码路径是 Barcode4J + ZXing,JSON 路径是 FastJson + Jackson。 |
  162 +| Baidu SDK | `xlyInterface.build.gradle` 本地 jar | 未发现 `com.baidu` import。 |
  163 +| `mchange-commons-java` | `xlyFlow.build.gradle` | 未发现直接 import。 |
  164 +| Springfox | `xlyInterface.build.gradle` | 未发现直接 Java import;通过 jar 静态资源提供 `/swagger-ui.html`,但没有 Docket bean。见 [Webhook API](../../api-reference/webhooks.md)。 |
  165 +
  166 +## 值得注意的版本偏差与本地 jar
  167 +
  168 +| 项 | 细节 |
  169 +|---|---|
  170 +| Shiro | `xlyPersist` 为 1.3.2;`xlyApi` 和 `xlyFlow` 为 1.4.2。 |
  171 +| FastJson | `xlyPersist` / `xlyApi` 为 1.2.15;`xlyFlow` 为 1.2.60。 |
  172 +| Hutool | `xlyPersist` 为 5.6.5;`xlyApi` / `xlyFlow` 为 5.8.5。 |
  173 +| Apache POI | `xlyPersist` / `xlyFlow` 为 4.1.2;`xlyApi` 为 3.15。 |
  174 +| Guava | `xlyPersist` / `xlyApi` 为 18.0;`xlyFlow` 为 20.0。 |
  175 +| commons-lang3 | `xlyPersist` 为 3.6;`xlyFlow` 为 3.8.1。 |
  176 +| Lombok | `xlyPersist` / `xlyFlow` 为 1.18.8;`xlyApi` 为 1.18.20。 |
  177 +| iText | `xlyPersist` 同时声明 `itextpdf` 5.5.0 和 `com.lowagie:itext` 2.1.7。 |
  178 +| Activiti | 流程引擎 5.17.0(`xlyPersist` / `xlyApi`);rest-api 与 json-converter 6.0.0(`xlyFlow`)。见 [Activiti 集成](activiti.md)。 |
  179 +| 本地 jar | `xlyPersist/src/main/java/lib/` 下有 Aspose、jacob、HslCommunication、QRCode、OLAP4J、JasperReports、EasyExcel 等;`xlyFlow` 和 `xlyInterface` 下有 MSSQL / Oracle / Baidu / Jeewx jar;`xlyBusinessService` 下有 TrueLicense jar。 |
  180 +| Spring Boot | 2.2.5.RELEASE,固定在根 `build.gradle` plugin block,并由使用 starter 的各模块声明。 |
  181 +
  182 +## 本清单有意不包含
  183 +
  184 +- plat 层(`xlyPlat*` 模块)及仅在其中声明的依赖;按[首页](../../index.md)说明属于范围外。
  185 +- AI / LLM 库(`xlyApi/build.gradle` 中的 `com.theokanning.openai-gpt3-java:service` 0.11.1 和 `com.unfbx:chatgpt-java` 1.0.8),范围外。
  186 +- `xlyEntity/build.gradle` 中声明的 MongoDB starter(`spring-boot-starter-data-mongodb` 2.2.5)。`xlyEntity` 中有 22 个 `@Document` 类,20 个名为 `PLAT_*`,另 2 个是 `DIKE_TEST*` 测试夹具。对范围内模块 grep `MongoTemplate` / `MongoRepository` 只发现 `xlyPersist/.../dao/platmongo/BaseMongoDao.java`(服务 plat 层);没有范围内模块调用 Mongo API。见[首页范围说明](../../index.md)。
  187 +- `xlyFace`,范围外。
... ...
zh/docs/slices/01-hello-world.md
... ... @@ -66,13 +66,13 @@ GET /xlyEntry/business/getModelBysId/13?sModelsId=13 → 200 OK
66 66  
67 67 | Key | 来源 | 内容 |
68 68 |---|---|---|
69   -| `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave`(+ personalize) | 表单布局主干 |
70   -| `gdsformconst` | 按 `sBrandsId` / `sSubsidiaryId` / language 过滤的 `gdsformconst` 行 | 表单级常量、标签、默认值、下拉文本 |
71   -| `gdsjurisdiction` | 用户角色的 `gdsjurisdiction` 行 | 按钮和数据权限;`ADMIN` 用户跳过,管理员看到全部 |
72   -| `billnosetting` | 该模块的 `sysbillnosettings` 行 | 单据编号规则;对 `gdsformconst` 无关但总会加载 |
73   -| `report` | 关联到该表单的打印模板 | 打印报表定义 |
  69 +| `formData` | `gdsconfigformmaster`(按 `sParentId = sModelsId` 过滤)⋈ `gdsconfigformpersonalize`(每租户覆盖);每个 master 行再加载 `gdsconfigformslave` + `gdsconfigformcustomslave` 覆盖。`gdsmodule` 只通过 id 引用(slave 查询会用它解析 `sActiveName`),不参与 master 读取。 | 表单布局主干 |
  70 +| `gdsformconst` | 仅按 `sParentId` 过滤的 `gdsformconst` 行。**不按租户过滤**;该行识别表单,`sLanguage` 决定返回哪列标签。 | 表单级常量、标签、默认值、下拉文本 |
  71 +| `gdsjurisdiction` | 用户角色的 `sysjurisdiction` 行(JOIN `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify`)。`ADMIN` 用户跳过,管理员看到全部。**注意:** map key 叫 `gdsjurisdiction` 有误导性;`gdsjurisdiction` 是配置侧动作目录,实际每用户授权来自 `sysjurisdiction`。 | 按钮和数据权限 |
  72 +| `billnosetting` | 该模块的 `sysbillnosettings` 行(每租户) | 单据编号规则;对 `gdsformconst` 无关但总会加载 |
  73 +| `report` | 关联到该表单的 `sysreport` 行(每租户) | 打印报表定义 |
74 74  
75   -**多租户**在这次读取中执行:每个子查询都带有从认证 session 注入的 `sBrandsId` 和 `sSubsidiaryId`。租户之间看不到对方元数据
  75 +**多租户**在需要的地方执行:租户作用域读取(`gdsconfigformpersonalize`、`gdsconfigformcustomslave`、`sysbillnosettings`、`sysreport`)都会按认证 session 注入的 `sBrandsId` 和 `sSubsidiaryId` 过滤。框架基础元数据表(`gdsconfigformmaster`、`gdsconfigformslave`、`gdsformconst`)是全局的,只按 form-id 过滤。因此租户看不到其他租户的*个性化覆盖*或*业务数据*,但底层表单定义是共享的
76 76  
77 77 ### 3. SPA → 服务端(初始数据加载)
78 78  
... ... @@ -106,26 +106,58 @@ POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId}
106 106  
107 107 三种操作打包成一个原子请求。前端告诉后端要写**哪张表**和**哪些列**;没有每模块专用写 API,元数据驱动 UI 根据 `gdsconfigformmaster.sTbName` 和 `gdsconfigformslave` 字段列表生成 payload。
108 108  
109   -- `sSaveProName` 为空时,运行时走 `AddDelUpdCommonServiceImpl.java` 的默认 Add/Update 路径,生成参数化 `INSERT` / `UPDATE` / `DELETE`。
110   -- `sSaveProName` 非空时,运行时调用指定存储过程。`xlyEntry/src/main/resources/templates/templesql/sSaveProName.sql` 是工程师编写这类过程时使用的脚手架。
  109 +- `sSaveProName` 的作用:基础新增 / 更新路径总是通过 `BusinessBaseServiceImpl.addBusinessData` / `updateBusinessData`(`xlyBusinessService/.../BusinessBaseServiceImpl.java:1014` 和 `:1250`),再委托 `businessBaseDao.add(map)` / `businessBaseDao.update(map)`,对 `sTable` 命名的表执行操作。`sSaveProName`(及其兄弟列 `sSaveProNameBefore`)**不是**替换基础路径的二选一分支;它命名的是叠加在基础路径之上的额外存储过程钩子:保存后 / 保存前由 `BusinessBaseServiceImpl.java:1824` 的 `checkUpdate(...,"sSaveProName")` 分发。切片 1 的 `gdsformconst` 中 `sSaveProName` 为空,因此只运行基础路径。`xlyEntry/src/main/resources/templates/templesql/sSaveProName.sql` 是工程师编写这类钩子时使用的脚手架。非空 `sSaveProName` 的工作流闸门路径由切片 2 涉及。
111 110  
112   -> **待验证:** 真实保存请求体、响应体以及 `syslog4j` 中的实际 SQL 还未捕获。为了避免修改共享 dev DB 中的框架常量表,本轮没有执行保存。端点、handler 和 payload 形状已从源码确认
  111 +> **开放验证(需要真实保存):** 仍需捕获实时请求 body、响应 body,以及 `syslog4j` 中的实际 SQL,才能完全闭环
113 112  
114   -> **安全相关架构备注:** 前端在 payload 中直接提供 `sTable`。`BusinessBaseServiceImpl.addUpdateDelBusinessData` 会读取该值并分发删除 / 更新 / 新增。类级 `sTableNameList` 主要作为缓存失效 gate,而不是“该表是否被此表单授权”的 gate。已确认的缓解包括租户作用域自动注入,以及模块可通过 `sSaveProNameBefore` / `sSaveProName` 做验证;但权限规则是按钮级,不是表级。净结论:通用保存端点信任前端的 `sTable` 值,值得维护 ticket
  113 +> **安全相关架构备注:** 前端在 payload 中直接提供 `sTable`。`BusinessBaseServiceImpl.addUpdateDelBusinessData` 会读取该值并分发删除 / 更新 / 新增。类级 `sTableNameList`(`BusinessBaseServiceImpl.java:162-169`,只含 `gdsformconst`、`gdsmodule`、`gdsconfigformmaster`、`gdsconfigformslave`)在部分分支被使用,但只是**多租户作用域绕过**闸门(这四张表是全局框架元数据,写入时会剥掉 `sBrandsId` / `sSubsidiaryId`;见 `BusinessBaseServiceImpl.java:165` 的 `//不需要公司子公司的表` 注释),不是“该表是否被此表单授权”的闸门。整个流程里唯一的模块 / 表交叉检查是 1768 行的 `mftproductionplanslave` 硬编码特例。缓解措施包括租户作用域自动注入、模块可通过 `sSaveProNameBefore` / `sSaveProName` 做验证;但权限规则是按钮级,不是表级。净结论:通用保存端点信任前端的 `sTable` 值,值得维护工单
115 114  
116 115 ### 5. 缓存失效
117 116  
118   -修改任何四张元数据表中的 `gds*` 行,都会让每个运行节点失效缓存副本。xly 通过 JMS 消息做到这一点:`xlyErpJmsConsumer/.../ConsumerChangeGdsModuleThread.java` 监听“module changed”事件并清相关 Redis key。见[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)。
  117 +修改元数据行后,保存 service 会同步调用 `BusinessCleanRedisData.delCleanRedisData*`,进而触发 `CleanRedisServiceImpl` 上的 `@CacheEvict`。名字相近的 JMS 路径用于基础数据合并,不负责清缓存。见[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)。
119 118  
120 119 ### 6. 浏览器确认
121 120  
122 121 保存返回成功;前端要么就地 patch 该行,要么用同一 `getBusinessDataByFormcustomId` 端点重新拉取表格。追踪结束。
123 122  
  123 +## 保存流程时序图
  124 +
  125 +```mermaid
  126 +sequenceDiagram
  127 + autonumber
  128 + participant SPA as Browser SPA
  129 + participant CTRL as BusinessBaseController
  130 + participant SVC as BusinessBaseServiceImpl
  131 + participant CLEAN as BusinessCleanRedisData
  132 + participant DB as MySQL (xlyweberp_*)
  133 + participant REDIS as Redis (RedisCacheManager)
  134 +
  135 + Note over SPA: 用户在 sReopen 行点击修改,<br/>编辑 sChinese,点击保存
  136 + SPA->>CTRL: POST /business/addSysLocking?sModelsId=13<br/>(乐观锁占用)
  137 + CTRL-->>SPA: 200 OK
  138 + SPA->>CTRL: POST /business/addUpdateDelBusinessData?sModelsId=13<br/>{addData:[],updateData:[{sTable:"gdsformconst",column:{sId,sChinese,...}}],delData:[]}<br/>Authorization: <bearer>
  139 + Note over CTRL: AuthorizationInterceptor → 从 Redis 取得 UserInfo<br/>RequestAddParamUtil.addParams(16 个 key,含 sBrandsId/sSubsidiaryId)
  140 + CTRL->>SVC: addUpdateDelBusinessData(param)
  141 + Note over SVC: 按行分发:<br/>add → addBusinessData → businessBaseDao.add<br/>update → updateBusinessData → businessBaseDao.update<br/>del → deleteBusinessData → businessBaseDao.del<br/>(sTable 来自前端;没有白名单检查)
  142 + SVC->>DB: 对 sTable 命名的表执行 INSERT/UPDATE/DELETE
  143 + DB-->>SVC: rows affected
  144 + Note over SVC: 如果 sTable 在 sTableNameList 中<br/>(gdsformconst/gdsmodule/gdsconfigformmaster/<br/>gdsconfigformslave)→ 写入前移除 sBrandsId/sSubsidiaryId<br/>(4 张表的租户绕过)
  145 + SVC->>CLEAN: delCleanRedisData(sTable, sIds, sBrandsId, sSubsidiaryId, "update")
  146 + CLEAN->>REDIS: 对受影响 cache region 执行 @CacheEvict<br/>(同步,同一事务路径)
  147 + REDIS-->>CLEAN: evicted
  148 + SVC-->>CTRL: Feedback{code:1,msg:"操作成功"}
  149 + CTRL-->>SPA: AjaxResult{code:1,...}
  150 + SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/...<br/>(重新拉取表格;cache miss → 读取新 DB 数据)
  151 + CTRL->>DB: SELECT ...
  152 + DB-->>CTRL: rows
  153 + CTRL-->>SPA: dataset
  154 +```
  155 +
124 156 ## 本切片引入的概念
125 157  
126 158 - [数据驱动的基本论点](../concepts/thesis.md):为什么 xly 把布局存为数据。
127 159 - [模块、表单、虚拟表](../concepts/modules-forms-vtables.md):三个核心名词。
128   -- [元数据驱动的请求生命周期](../concepts/request-lifecycle.md):四表读取 + 五键结果 map。
  160 +- [元数据驱动的请求生命周期](../concepts/request-lifecycle.md):元数据读取 + 五键结果 map。
129 161 - [主从单据模式](../concepts/master-slave.md):`gdsconfigformmaster` / `slave` 本身就是该模式实例。
130 162 - [无物理外键、语义外键的现实](../concepts/semantic-fk.md):`gdsconfigformmaster.sParentId = gdsmodule.sId` 是语义 FK。
131 163  
... ... @@ -138,14 +170,20 @@ POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId}
138 170  
139 171 维护人员:
140 172  
141   -- [运行时:BusinessBaseController 及相关组件](../reference/maintainer/runtime.md):执行四表读取的 controller 和 service 层。
142   -- [元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md):JMS 驱动 Redis flush。
  173 +- [运行时:BusinessBaseController 及相关组件](../reference/maintainer/runtime.md):执行元数据读取的 controller 和 service 层。
  174 +- [元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md):同步 `@CacheEvict`(JMS 路径服务于另一个目的)。
143 175 - [多服务部署](../reference/maintainer/deployment.md):`xlyEntry` vs `xlyApi` vs `xlyInterface`;本切片完全运行在 `xlyEntry` 上。
144 176  
145 177 ## 待验证项 {#open-verification-items}
146 178  
147   -1. **真实捕获一次保存。** 端点、handler 和 payload 形状已从源码确认,但实际保存请求体尚未捕获。需要打开模块、点击新增、填写、保存,并捕获 JSON body 和响应。
148   -2. **保存 / 删除发出的精确 SQL**,从 `syslog4j` 或 MyBatis debug log 捕获。
149   -3. ~~**`addUpdateDelBusinessData` 中的 `sTable` 校验。**~~ **已关闭**:运行时不会把前端提供的 `sTable` 与表单授权支撑表交叉检查。已作为维护关注点记录。
150   -
151   -前两项属于切片 1 v2,需要对 dev DB 做实际写入;为避免修改共享状态而暂缓。
  179 +读路径已通过实时环境佐证;保存路径仍待继续捕获。
  180 +
  181 +1. ~~**实时捕获一次读取。**~~ **已关闭**:在 BACK(`http://118.178.19.35:8597`,admin/123,版本 `基础版/8s`)点击系统常量配置,产生了与文档一致的 HTTP 交换:
  182 + ```text
  183 + GET /xlyEntry/business/getModelBysId/13?sModelsId=13 → 200 OK
  184 + POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715574676360040?sModelsId=13 → 200 OK
  185 + ```
  186 + 两个 URL 都与 Wiki 完全匹配,包括路径变量旁边冗余的 `?sModelsId=13` query 参数。登录后 URL 停在 `/xtmkpz`,不会导航到 `/xtclpz`,这确认 URL fragment 是显示状态,不是路由驱动。
  187 +2. **实时捕获一次保存(部分)。** endpoint + URL + method 已在实时环境佐证:在 BACK 系统常量配置中点击新增 → 保存,会触发 `POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId=13` → 200 OK。(测试未产生 DB 变更,因为 audit-tag 值没有通过 SPA Vue model 传入,保存运行在原始行状态上。)下一步要捕获 SPA 发送的精确请求 **body**;目前文档中的 shape 仍来自 `BusinessBaseController.java:161-163` Javadoc。SPA 进入编辑模式时还会触发 `POST /business/addSysLocking`(乐观锁);见[内部 API](../api-reference/internal.md)。
  188 +3. **保存 / 删除发出的精确 SQL**,从 `syslog4j` 或 MyBatis debug log 捕获。
  189 +4. ~~**`addUpdateDelBusinessData` 中的 `sTable` 校验。**~~ **已关闭**:运行时不会把前端提供的 `sTable` 与表单授权支撑表交叉检查。已作为维护关注点记录。
... ...
zh/docs/slices/02-multi-tenancy.md
... ... @@ -10,7 +10,7 @@ xly 是多租户 SaaS。åŒä¸€å¥—代ç åº“ã€åŒä¸€å¥—æ•°æ®åº“ schemaã€åŒä¸€ç
10 10 |---|---|---|---|
11 11 | **`sBrandsId`**(加工商ID) | å‡ ä¹Žæ¯æ¡ä¸šåŠ¡è¡Œ | æ¯è¡Œ | “这行属于哪个加工商 / å…¬å¸ï¼Ÿâ€ |
12 12 | **`sSubsidiaryId`**(å­å…¬å¸ID) | å‡ ä¹Žæ¯æ¡ä¸šåŠ¡è¡Œ | æ¯è¡Œ | “公å¸å†…哪个å­å…¬å¸ï¼Ÿâ€ |
13   -| **`sVersionFlowId`**(版本æµç¨‹ID) | ä»… `gdsmodule` | æ¯æ¨¡å— | “这个模å—属于哪个产å“版本?†|
  13 +| **`sVersionFlowId` / `sVersionFlowCode`**(版本æµç¨‹ID / code) | ä»… `gdsmodule` | æ¯æ¨¡å—标签 | â€œè¿™ä¸ªæ¨¡å—æ ‡è®°ä¸ºå±žäºŽå“ªä¸ªäº§å“版本?â€è¿è¡Œæ—¶å¯è§æ€§ç”±è®¸å¯è¯äº§å‡ºçš„æ¨¡å—列表控制。 |
14 14  
15 15 å‰ä¸¤è€…是**æ¯è¡Œ**作用域。第三者是模å—列表加载时的**æ¯æ¨¡å—**过滤。机制ä¸åŒï¼Œå±‚级ä¸åŒã€‚
16 16  
... ... @@ -18,7 +18,7 @@ xly 是多租户 SaaS。åŒä¸€å¥—代ç åº“ã€åŒä¸€å¥—æ•°æ®åº“ schemaã€åŒä¸€ç
18 18  
19 19 ### 覆盖多广
20 20  
21   -`xlyweberp_saas_ai` çš„ 1,212 张表 / 视图(901 张基础表 + 311 个视图)中,**1,008 ä¸ªåŒæ—¶å¸¦æœ‰ `sBrandsId` å’Œ `sSubsidiaryId`**ã€‚å¦æœ‰ 1 个åªå¸¦ `sBrandsId`,0 个åªå¸¦ `sSubsidiaryId`。这几乎覆盖所有业务数æ®è¡¨å’Œæ¡†æž¶å…ƒæ•°æ®è¡¨ã€‚
  21 +几乎æ¯å¼ ä¸šåŠ¡æ•°æ®è¡¨å’Œè§†å›¾éƒ½å¸¦æœ‰ `sBrandsId` 与 `sSubsidiaryId`。**大多数框架元数æ®è¡¨ä¹Ÿå¸¦æœ‰è¿™ä¸¤ä¸ªåˆ—**,但四张表(`gdsformconst`ã€`gdsmodule`ã€`gdsconfigformmaster`ã€`gdsconfigformslave`)是明确例外:`BusinessBaseServiceImpl.sTableNameList`(162-169 行)把它们列为“ä¸éœ€è¦å…¬å¸å­å…¬å¸çš„表â€ï¼Œ1078-1084 行会从这些表的写入载è·ä¸­å‰¥æŽ‰ `sBrandsId` / `sSubsidiaryId`。实际使用中它们ä¿å­˜çš„æ˜¯æ‰€æœ‰å®¢æˆ·å…±äº«çš„一组哨兵租户值。其他缺少其中一个或两个列的表,多为å•租户共享字典或第三方 schema(`act_*`ã€`qrtz_*`)。
22 22  
23 23 ### 如何注入
24 24  
... ... @@ -36,13 +36,13 @@ xlyApi 在 `xlyApi/src/main/java/com/xly/api/util/RequestAddParamUtil.java` 中æ
36 36  
37 37 ### 查询中如何体现
38 38  
39   -切片 1 çš„ `getModelBysId` 调用最终在æ¯å¼ å…ƒæ•°æ®è¡¨ä¸Šéƒ½å¸¦æœ‰ï¼š
  39 +切片 1 çš„ `getModelBysId` 调用中,æ¯ç§Ÿæˆ·è°“è¯ï¼š
40 40  
41 41 ```sql
42 42 WHERE sBrandsId = #{sBrandsId} AND sSubsidiaryId = #{sSubsidiaryId}
43 43 ```
44 44  
45   -åŒæ ·è°“è¯å‡ºçŽ°åœ¨ä»£ç åº“中几乎æ¯ä¸ªä¸šåŠ¡æ•°æ®æŸ¥è¯¢é‡Œã€‚这就是è¿è¡Œæ—¶çš„多租户边界。
  45 +会加在**æ¯ç§Ÿæˆ·è¦†ç›–读å–**(`gdsconfigformpersonalize`ã€`gdsconfigformcustomslave`)以åŠ**业务状æ€è¯»å–**(`sysbillnosettings`ã€`sysreport`ï¼Œä»¥åŠ JOIN `sftlogininfojurisdictiongroup` çš„ `sysjurisdiction` 用户授æƒï¼›æ³¨æ„返回 map key å« `gdsjurisdiction`,但实际读å–的是 `sysjurisdiction`)上。它ä¸ä¼šåŠ åœ¨æ¡†æž¶åŸºç¡€å…ƒæ•°æ®è¯»å–(`gdsmodule`ã€`gdsconfigformmaster`ã€`gdsconfigformslave`ã€`gdsformconst`ï¼‰ä¸Šï¼›è¿™äº›è¡¨æ˜¯å…¨å±€çš„ï¼ŒåªæŒ‰ form-id è¿‡æ»¤ã€‚åŒæ ·è°“è¯å‡ºçŽ°åœ¨ä»£ç åº“中几乎æ¯ä¸ªä¸šåŠ¡æ•°æ®æŸ¥è¯¢é‡Œã€‚这是租户拥有状æ€çš„è¿è¡Œæ—¶è¾¹ç•Œï¼›æ¡†æž¶å…ƒæ•°æ®æœ‰æ„全局共享。
46 46  
47 47 ### 失效模å¼
48 48  
... ... @@ -56,7 +56,7 @@ xly 以多个版本销售:**基础版**ã€**EBC-MDM**ã€**EBC-SD**ã€**EBC-RD*
56 56  
57 57 ### 版本在哪里定义
58 58  
59   -`sisversionflow` 表(此 dev DB 中 1 行):
  59 +版本定义在 `sisversionflow` 字典表中,æ¯ä¸ªç‰ˆæœ¬ä¸€è¡Œã€‚关键列:
60 60  
61 61 | 列 | 值 | å«ä¹‰ |
62 62 |---|---|---|
... ... @@ -65,42 +65,33 @@ xly 以多个版本销售:**基础版**ã€**EBC-MDM**ã€**EBC-SD**ã€**EBC-RD*
65 65 | `sFlowName` | `基础版` | 显示å |
66 66 | `bEbcErpPremium`, `bEbcMes`, `bEbcMesStandard`, `bSass` | flags | 该版本属于哪些产å“å˜ä½“ |
67 67  
68   -真实 SaaS 中这里会有更多行,æ¯ä¸ªä¸åŒç‰ˆæœ¬ä¸€è¡Œã€‚`gdsmodule` 中看到的 `EBC-MDM-002`ã€`EBC-SD-002`ã€`EBC-RD-007` flow code,应对应多版本生产 DB 中的行。
  68 +> **å½“å‰ dev DB 状æ€ï¼š** `sisversionflow` ç›®å‰åªå®šä¹‰äº†ä¸€è¡Œï¼š`8S_001 / 基础版`。`gdsmodule.sVersionFlowCode` 中出现的其他版本ç ï¼ˆ`EBC-SD-002`ã€`EBC-RD-007`ã€`EBC-MDM-002` 等)作为模å—行标签存在,但在这里没有匹é…çš„ `sisversionflow` 行。SaaS 生产租户很å¯èƒ½ä¼šå¡«å……完整版本目录;dev DB 没有。
69 69  
70   -### 模å—如何按版本过滤
  70 +### 模å—如何按版本过滤(实际机制)
71 71  
72   -`sVersionFlowId` åªåœ¨ä¸‰å¼ è¡¨ä¸Šï¼š
  72 +`sVersionFlowId` / `sVersionFlowCode` 是 `gdsmodule` è¡Œä¸Šçš„æ ‡ç­¾ï¼Œç”¨æ¥æ ‡è®°æ¯ä¸ªæ¨¡å—属于哪个版本;**但这两个列没有出现在任何 Java æºç æˆ– MyBatis mapper 中**(已验è¯ï¼š`grep -r sVersionFlowId xly-src --include='*.java' --include='*.xml'` 在 mapper SQL 中没有命中)。è¿è¡Œæ—¶å¹¶ä¸ç›´æŽ¥æŒ‰è¿™ä¸¤ä¸ªåˆ—过滤。
73 73  
74   -- `gdsmodule`(实时模å—目录)。
75   -- `gdsmodule_0923bak`(备份快照)。
76   -- `gdsmodule_copy1`(å¦ä¸€ä¸ªå¿«ç…§ï¼‰ã€‚
  74 +真正的控制点由许å¯è¯é©±åŠ¨ï¼š`xly-src/xlyBusinessService/.../license/`(TrueLicense + xly çš„ `VerifyLicense.getModelAllList()`)返回租户许å¯è¯å…è®¸çš„æ¨¡å— `sId` 列表。该列表会以 `sVerifyLicense` å½¢å¼é€—å·æ›¿æ¢è¿›èœå• SQL:
77 75  
78   -å› æ­¤æ¯ç‰ˆæœ¬è¿‡æ»¤**åªå‘生在模å—å‘现时**ï¼Œä¸æ˜¯æ¯ä¸ªä¸šåŠ¡æŸ¥è¯¢ä¸Šã€‚ç”¨æˆ·ç™»å½•æ—¶ï¼Œæ¡†æž¶è§£æžç§Ÿæˆ·æ‰€å±žç‰ˆæœ¬ï¼Œç„¶åŽæŠŠå¯è§æ¨¡å—åˆ—è¡¨è¿‡æ»¤åˆ°åŒ¹é… `gdsmodule.sVersionFlowId` 的模å—。之åŽï¼Œæ¯ä¸ªåŠ è½½çš„æ¨¡å—照常用 `sBrandsId` / `sSubsidiaryId` è¯»å–æ•°æ®ã€‚
79   -
80   -`xlyweberp_saas_ai` 中按 `sVersionFlowId, sVersionFlowCode` 分组的图景:
  76 +```java
  77 +// MenuChildServiceImpl.java:38-65 — getBuMenuSql
  78 +sql.append(" AND m.sId in ("+sVerifyLicense+")");
  79 +```
81 80  
82   -| Flow code | æ¨¡å—æ•° | 覆盖内容 |
83   -|---|---|---|
84   -| 空 | 1,002 | 未标记,多为框架内部模å—å’Œä¸å—版本 gate 的项目 |
85   -| `8S_001`(基础版) | 322 | Essentials baseline |
86   -| `EBC-SD-002` | 15 | 销售 / 交付 |
87   -| `EBC-RD-007` | 6 | ç ”å‘ |
88   -| `EBC-MDM-002` | 5 | 主数æ®ç®¡ç† |
89   -| `EBC_001` | 4 | 基础 EBC bundle |
90   -| `EBC-SD-003` | 2 | SD å˜ä½“ |
91   -| `EBC-SD-001` | 1 | SD å˜ä½“ |
92   -| `EBC-COM-001` | 1 | 公共组件 |
  81 +`sVerifyLicense` è¦ä¹ˆç”± xlyApi çš„ `RequestAddParamUtil` 注入(50-52 行:`params.put("sVerifyLicense","'"+String.join("','",listModel)+"'")`),è¦ä¹ˆç”± xlyEntry 中的 controller æ‰‹å·¥ç»„è£…å‚æ•°ï¼ˆä¾‹å¦‚ `MobliePhoneController.java:57`)。因此æ¯ç‰ˆæœ¬è¿‡æ»¤çœŸæ­£å‘生在**模å—å‘现阶段,由许å¯è¯å±‚完æˆ**ï¼Œè€Œä¸æ˜¯é€šè¿‡ `sVersionFlowId`。`sVersionFlowId` / `sVersionFlowCode` 是给è¿ç»´å’Œ BACK 侧报表看的目录元数æ®ï¼›è¿è¡Œæ—¶æŽ§åˆ¶é“¾æ˜¯ `sVerifyLicense` → 许å¯è¯äº§å‡ºçš„æ¨¡å—列表 → `IN (...)`。
93 82  
94   -322 ä¸ªåŸºç¡€ç‰ˆæ ‡è®°æ¨¡å—æ˜¯é€šç”¨æŽˆæƒæ ¸å¿ƒï¼›1,002 个未标记行大多是框架内部模å—,ä¸å—版本 gate å½±å“。其余具å flow 是版本特定 add-on。
  83 +`gdsmodule`(dev DB 中 1358 è¡Œï¼‰é‡Œæœ‰ä¸‰ç§æ ‡ç­¾æ¨¡å¼ï¼š
95 84  
96   -## 为什么 dev 看起æ¥å°
  85 +- **未标记行**(`sVersionFlowCode` 为空):1002 行。框架内部模å—å’Œä¸å—版本å¯è§æ€§æŽ§åˆ¶å½±å“的页é¢ã€‚
  86 +- **基础版标记行**(`sVersionFlowCode = '8S_001'`):322 行。所有版本都会获得的通用核心。
  87 +- **版本特定行**:`EBC-SD-002`(15)ã€`EBC-RD-007`(6)ã€`EBC-MDM-002`(5)ã€`EBC_001`(4)ã€`EBC-SD-003`(2)ã€`EBC-SD-001`(1)ã€`EBC-COM-001`(1)。这些是由客户许å¯è¯æŽ§åˆ¶å¯è§æ€§çš„增购模å—。
97 88  
98   -本 Wiki 使用的 `xlyweberp_saas_ai` åªæœ‰ä¸€ä¸ªå“牌(`sBrandsId = '1111111111'`)ã€ä¸€ä¸ªå­å…¬å¸ï¼ˆåŒå€¼ï¼‰å’Œä¸€ä¸ªå¡«å……版本(`8S_001`)。多租户机制已接好,但几乎没有压力测试。生产环境应预期几å个å“牌 × æ¯å“牌几å个å­å…¬å¸ × å¤šä¸ªç‰ˆæœ¬ï¼Œå…¨éƒ¨é€šè¿‡åŒæ ·çš„æ¯è¡Œè¿‡æ»¤æ¨¡å¼éš”离。
  89 +补充:本 Wiki 使用的 `xlyweberp_saas_ai` åªæœ‰ä¸€ä¸ªå“牌(`sBrandsId = '1111111111'`)ã€ä¸€ä¸ªå­å…¬å¸ï¼ˆåŒå€¼ï¼‰å’Œä¸€ä¸ªå¡«å……版本(`8S_001`)。多租户机制已接好,但几乎没有压力测试。生产环境应预期几å个å“牌 × æ¯å“牌几å个å­å…¬å¸ × å¤šä¸ªç‰ˆæœ¬ï¼Œå…¨éƒ¨é€šè¿‡åŒæ ·çš„æ¯è¡Œè¿‡æ»¤æ¨¡å¼éš”离。
99 90  
100 91 ## 本切片引入的概念
101 92  
102 93 - *多租户作用域*:`sBrandsId` / `sSubsidiaryId` 是æ¯è¡Œç§Ÿæˆ·è¾¹ç•Œï¼›æ¡†æž¶çš„通用注入器是 `RequestAddParamUtil`。
103   -- *产å“版本*:`sVersionFlowId` 通过 `sisversionflow` å®žçŽ°æ¯æ¨¡å—å¯è§æ€§è¿‡æ»¤ï¼›åŒºåˆ† scoping(æ¯è¡Œï¼‰å’Œ gatingï¼ˆæ¯æ¨¡å—)。
  94 +- *产å“版本*:`sVersionFlowId` / `sVersionFlowCode` æ ‡è®°æ¨¡å—æ‰€å±žç‰ˆæœ¬ï¼Œä½†è¿è¡Œæ—¶æ¨¡å—å¯è§æ€§ç”±è®¸å¯è¯äº§å‡ºçš„ `sVerifyLicense` 模å—列表控制;区分行级作用域和模å—级å¯è§æ€§æŽ§åˆ¶ã€‚
104 95  
105 96 ## 本切片使用的å‚考
106 97  
... ... @@ -108,6 +99,6 @@ xly 以多个版本销售:**基础版**ã€**EBC-MDM**ã€**EBC-SD**ã€**EBC-RD*
108 99  
109 100 ## 待验è¯é¡¹
110 101  
111   -1. **按版本过滤模å—å‘现。** 机制åˆç†ï¼Œä½†å°šæœªå®šä½ç²¾ç¡®ä»£ç è·¯å¾„,候选是 `GdsmoduleController` 或 `GdsmoduleServiceImpl`。
112   -2. **Activiti 工作æµã€‚** `sVersionFlowId` 䏿˜¯å·¥ä½œæµ id(尽管å字里有 flow)。实际工作æµè¡¨å‡ä¸ºç©ºï¼›æœªæ¥åˆ‡ç‰‡ 7 在有活动æµç¨‹ DB 时记录。
113   -3. **session 级租户解æžã€‚** JWT / session 如何把登录用户映射到 `sBrandsId` / `sSubsidiaryId`,ä½äºŽ `RequestAddParamUtil` 下é¢ä¸€å±‚,值得在维护章节追踪。
  102 +1. ~~**按版本过滤模å—å‘现:定ä½ä»£ç è·¯å¾„。**~~ **已关闭。** 许å¯è¯é©±åŠ¨è¿‡æ»¤ä½äºŽ `xlyBusinessService/.../service/impl/MenuChildServiceImpl.java:38-65`(`getBuMenuSql`),SQL 以 `AND m.sId in (#{sVerifyLicense})` 结æŸã€‚`sVerifyLicense` æ¥è‡ª `VerifyLicense.getModelAllList()`(TrueLicense 绑定),并通过 `RequestAddParamUtil`(xlyApi)或 controller çº§å‚æ•°ç»„装(xlyEntry)注入。è§ä¸Šæ–¹å·²ä¿®æ­£çš„“模å—如何按版本过滤â€ç« èŠ‚ï¼›ä¹‹å‰æŠŠå®ƒè¯´æˆ `sVersionFlowId` 过滤是错误的。
  103 +2. ~~**Activiti å·¥ä½œæµ / `sVersionFlowId` 䏿˜¯å·¥ä½œæµ id。**~~ **已关闭。** 已记录在 [Activiti 集æˆ](../reference/maintainer/activiti.md):Activiti 已接线但 idle;未部署 BPMN;框架实际工作æµä½¿ç”¨ä¸‰æ¡éž Activiti 路径(一步 proc + bCheckã€å•æ®é“¾ã€å·²æŽ¥çº¿ä½†å½“å‰ç¡¬ç¦ç”¨çš„ Activiti 分å‘)。
  104 +3. ~~**session 级租户解æžï¼šJWT / session 查找链路。**~~ **已关闭。** 链路(å‡åœ¨ `xlyBusinessService/.../web/token/` 下):`AuthorizationInterceptor.preHandle` 用 `Authorization` header è°ƒ `RedisTokenManager.getToken`(AES 解密 bearer,æ¢å¤ `(userId, sBrandsId, sSubsidiaryId, …)`),å†ç”± `checkToken` 校验 Redis key `<sLoginType><userId>` 下缓存 token 并刷新 TTL。解æžå‡ºçš„ `UserInfo` 通过 `@CurrentUser`(`CurrentUserMethodArgumentResolver`)传入 controller,éšåŽ `RequestAddParamUtil.me().addParams(params, userInfo)` 在æ¯ä¸ªè®¤è¯æ–¹æ³•调用中注入 16 个 key:`sBrandsId`ã€`sSubsidiaryId`ã€`sBrId`ã€`sSuId`ã€`sLoginId`ã€`sIpAddress`ã€`sComputeName`ã€`sUserId`ã€`userId`ã€`sLanguage`ã€`sUserType`ã€`sUserName`ã€`sMakePerson`ã€`sTeamId`ã€`sMachineId`ã€`CURRENT_USER_LOGIN_TYPE`。
... ...
zh/docs/slices/03-report.md
... ... @@ -77,8 +77,10 @@ POST /xlyEntry/business/getBusinessDataByFormcustomId/{formId}?sModelsId={module
77 77 一些视图支撑模块确实有打印模板:Excel via jxls、PDF via iText。机制与表格分离:
78 78  
79 79 - `getModelBysId` 返回 `report` 数组,来自通过 `sFormId` 关联到表单的 `sysreport` 行。
80   -- 前端“打印” / “导出”按钮调用 `xlyEntry/com/xly/report/` 下的 controller,加载 jxls / iText 模板,使用同一视图查询的“取全部行”包装,并把二进制文件流回。
81   -- 本模块没有模板,因此不覆盖打印路径。未来修订应选一个确实有模板的模块;`print template` 值得单独成章。
  80 +- 前端“打印” / “导出”按钮调用 `xlyEntry/src/main/java/com/xly/web/report/` 下的 controller;`PrintReportController` 是活跃类。(同目录的 `PrintReportControllerOld.java` 文件仍存在,但类体已全部注释,是死代码。)controller 加载 jxls / iText 模板,使用同一视图查询的“取全部行”包装,并把二进制文件流回。
  81 +- 本模块没有模板,因此不覆盖打印路径。
  82 +
  83 +> **后续工作。** 如果后续修订选择一个确实带打印模板的模块,就能端到端追踪 jxls 导出。当前受 dev DB 状态阻塞(没有任何视图支撑表单挂接 `sysreport` 行;见下面的待验证项)。
82 84  
83 85 ## 本切片引入或强化的概念
84 86  
... ... @@ -86,8 +88,29 @@ POST /xlyEntry/business/getBusinessDataByFormcustomId/{formId}?sModelsId={module
86 88 - *共享模板 URL*:`/indexPage/commonList`、`/indexPage/commonBill`、`/indexPage/commonClassify`、`/indexPage/commonNewBill` 被数百个模块复用。URL 选择页面形状;模块身份来自 `sModelsId`。
87 89 - *报表模板*(仅预览):`sysreport` 通过 `sFormId` 关联,jxls / iText 模板由 `PrintReportController` 提供。
88 90  
  91 +## 本切片使用的参考
  92 +
  93 +- [配置人员:如何定义虚拟表](../reference/builder/define-vtable.md):对视图支撑模块来说,表单的 `sType` 和 `sTbName` 就是声明。
  94 +- [维护人员:运行时](../reference/maintainer/runtime.md):与切片 1 使用同一条 `BusinessBaseController` 路径;唯一新增的是 `sType` 分支。
  95 +- 新页:*视图与报表*(维护人员下):记录 `viw_` / `Viw_` 命名约定、视图必须带出租户列才能保持租户安全的规则,以及打印模板流程。
  96 +
89 97 ## 待验证项
90 98  
  99 +> **Item 1 — 暂缓(需要填充过的 dev DB)。** 截至最近审计,dev DB 中没有任何 view-backed form 挂接 `sysreport` 行:`SELECT … FROM gdsconfigformmaster m INNER JOIN sysreport r ON r.sFormId = m.sId WHERE m.sType='view'` 返回 0 行。该项仍是实际验证缺口,需要一个 `sysreport` 至少包含一个 view-backed form 的租户部署。
  100 +
91 101 1. **带打印模板的视图支撑模块**:选择一个并端到端追踪 jxls 导出。
92 102 2. **`sType = 'proc'` 变体**:209 个表单由存储过程支撑,应另开切片说明过程如何返回结果集和参数如何流动。
93   -3. **视图租户安全。** 需要脚本审计哪些视图没有带出 `sBrandsId` / `sSubsidiaryId`。
  103 +3. ~~**视图租户安全:审计哪些 `viw_*` 缺少 `sBrandsId`。**~~ **已关闭:305 个中有 19 个(约 6.2%)泄漏。** 对实时 DB 执行:
  104 +
  105 + ```sql
  106 + SELECT v.TABLE_NAME
  107 + FROM information_schema.views v
  108 + WHERE v.TABLE_SCHEMA = DATABASE()
  109 + AND v.TABLE_NAME LIKE 'viw_%'
  110 + AND v.TABLE_NAME NOT IN (
  111 + SELECT TABLE_NAME FROM information_schema.columns
  112 + WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = 'sBrandsId'
  113 + );
  114 + ```
  115 +
  116 + 此 dev DB 返回 19 行,包括 `viw_purorder_slave_detail`、`viw_qlyprocesstest`、`viw_accproductstoreinvoice*` 家族、`viw_hmwxjy*` 等。如果某个表单指向这些视图且 `gdsconfigformmaster.sWhere` 没有外层租户谓词,它们就是潜在跨租户泄漏点。下一步应审计这些具体视图对应的表单层谓词;裸视图审计已经变成一条一次性 SQL。
... ...
zh/docs/slices/04-custom-field.md
... ... @@ -22,7 +22,7 @@ xly 在每个基础表单之上叠加**三张**定制表。每张表作用域不
22 22  
23 23 假设租户“山东星海印务”想在客户列表表单上增加“客户内部编码”字段。客户或实施人员不修改 `gdsconfigformslave`,而是:
24 24  
25   -1. 打开**后台**中编辑 `gdsconfigformcustomslave` 行的模块(某个系统管理页面,可能是 `界面显示内容配置`;需在**后台**中点击验证)
  25 +1. 打开 BACK 中的 `界面显示内容配置`(`gdsmodule.sId=11`,`/jmnrpz`)。其第三个面板通过 `sId=19211681019715596285250620` 的 form-master 写入 `gdsconfigformcustomslave`,已在线验证。各 panel 映射见下面“待验证项”第 1 项
26 26 2. 新增一行:
27 27 - `sParentId` = 表单 `sId`(与基础 slave 指向同一个表单)。
28 28 - `sName = 'sInternalCode'`(字段列名)。
... ... @@ -33,7 +33,7 @@ xly 在每个基础表单之上叠加**三张**定制表。每张表作用域不
33 33  
34 34 下次该租户的任意用户加载表单时,会看到额外列。其他租户仍看到未修改的基础表单。
35 35  
36   -> **dev 中为空的重要说明。** `gdsconfigformcustomslave` 在 `xlyweberp_saas_ai` 中有 **0 行**。表已接入框架,但当前 dev DB 没有租户使用它。下面的追踪来自代码推导;实时观察仍待补
  36 +> **已用 dev DB 确认。** `gdsconfigformcustomslave` 在 `xlyweberp_saas_ai` 中当前**为空**(0 行)。表已接入框架,但当前 dev DB 没有租户注册字段级覆盖。下面的追踪来自代码推导;端到端**观察**需要一个真实填充该表的租户部署
37 37  
38 38 ## 运行时如何合并
39 39  
... ... @@ -78,7 +78,17 @@ gdsconfigformuserslave (每用户视图偏好)
78 78  
79 79 ## 待验证项
80 80  
81   -1. 找到实际编辑 `gdsconfigformcustomslave` 的 **后台** 页面。最可能是侧边栏中的 `界面显示内容配置`。
82   -2. **追踪合并代码。** 确认合并是在 MyBatis(两个视图)中完成,还是在 Java(`getFormSlaveData` + `getFormCustomSlaveData`)中完成。
83   -3. **`bVisible = false` 语义。** 它是隐藏已有基础字段,还是只抑制覆盖行本身?很可能是前者,但需要确认。
84   -4. **真实示例。** 在生产中找一个租户的实际 `gdsconfigformcustomslave` 行作为贯穿示例。
  81 +1. ~~**找到实际编辑 `gdsconfigformcustomslave` 的 BACK 页面。**~~ **已关闭:确认为 `界面显示内容配置`**(`gdsmodule.sId=11`,URL `/jmnrpz`)。该页在一个 screen 中渲染三个 form-master panel,分别对应表单定义栈的三层:
  82 +
  83 + | Panel | `gdsconfigformmaster.sId` | 写入的 `sTbName` |
  84 + |---|---|---|
  85 + | Form-master 编辑器 | `19211681019715574673782610` | `gdsconfigformmaster` |
  86 + | 基础 slave 编辑器 | `19211681019715596207594120` | `gdsconfigformslave` |
  87 + | 每租户覆盖 | `19211681019715596285250620` | `gdsconfigformcustomslave` |
  88 +
  89 + 第三个 panel 就是“给租户 X 添加自定义字段”的规范通道。在线验证:在 BACK(admin/123)点击 `界面显示内容配置` 会触发 `POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715596285250620?sModelsId=11` 加载现有 customslave 行;后续新增 / 修改操作的 `addUpdateDelBusinessData` POST 也会落到同一个 form-master,运行时按标准通用保存路径解析为对 `gdsconfigformcustomslave` 的写入。
  90 +2. ~~**追踪合并代码。**~~ **已关闭**:合并发生在 `BusinessBaseServiceImpl.java:246-248` 的 Java 中,先调用 `businessGdsconfigformsService.getFormSlaveData(map)`,再调用 `getFormCustomSlaveData(map)`,并把两者 `addAll` 到同一个 `slaveList`。两个视图(`gdsconfigformslavemasterview`、`gdsconfigformcustomslavemasterview`)提供 master-with-slave 的读取形状;**合并**在 Java,**master-with-slave join** 在 SQL。
  91 +3. ~~**`bVisible = false` 语义:隐藏基础字段,还是只抑制覆盖行?**~~ **已关闭:两者都会发生,但在不同层。** `BusinessGdsconfigformsServiceImpl.java:413-433` 中,当 `gdsconfigformcustomslave` 行按 `sControlName` 或 `sName` 匹配到基础 `gdsconfigformslave` 行时,customslave 行会完整替换基础行(`sList.removeAll(_cstlist); sList.addAll(_cList);`),所以 customslave 上的 `bVisible=false` 会按租户隐藏基础字段。用户级覆盖(`gdsconfigformuserslave`,446-468 行)随后叠加:只有用户行的 `bVisible` 为 true 且合并后行的 `bVisible` 也为 true 时,用户的 `iFitWidth` / `iOrder` 才生效;否则 464 行会显式 `cmap.put("bVisible", false)`,仅对该用户隐藏。因此 `bVisible=false` 在两层都能隐藏字段,只是作用域不同(每租户 vs 每用户)。
  92 +> **Item 4 — 暂缓(需要填充过的租户部署)。** 已对 dev DB 实证确认:`SELECT COUNT(*) FROM gdsconfigformcustomslave` 返回 0。该 DB 中没有租户注册每租户字段覆盖,因此无法从这里抽取真实 worked example。该项仍是真实缺口,等待有覆盖行的生产租户 DB。
  93 +
  94 +4. **真实示例。** 在生产中找一个租户的实际 `gdsconfigformcustomslave` 行作为贯穿示例。(dev DB 已确认为空,需要有覆盖行的租户部署。)
... ...
zh/docs/slices/05-customer-sql-override.md
... ... @@ -64,21 +64,132 @@ CREATE PROCEDURE `Sp_SalSalesCheck`(IN sLoginId varchar(100), ...)
64 64  
65 65 经验规则:优先选择切片 4 的元数据定制。只有元数据模型确实无法表达客户需求时,才使用切片 5 SQL 覆盖。
66 66  
67   -## 重庆展印 `Sp_SalSalesCheck` 的不同点
  67 +## 示例:重庆展印的 `Sp_SalSalesCheck` vs 标准过程
68 68  
69   -文件顶部显示
  69 +对实时 dev DB 做量化 diff 后
70 70  
71   -- 它从 `SysSystemSettings` 读取 `'CbxSrcNoCheck'` 行,决定哪些计费类型进入销售对账报表。这是标准过程可能没有暴露的客户特定开关。
72   -- 它调用全局 `Fun_GetLookCustomer(sLoginId, sBrId, sSuId)` helper 做权限作用域,与标准过程一致。
73   -- 它接受与标准过程相同的参数列表(`sLoginId` / `sCustomerId` / `sBrId` / `sSuId` / `bFilter` / `pageNum` / `pageSize` 等),因此框架调用点不变。
  71 +| 方面 | 标准 `Sp_SalSalesCheck`(DB 中) | 重庆展印覆盖(`script/客户/重庆展印/Sp_SalSalesCheck.sql`) |
  72 +|---|---|---|
  73 +| 主体长度 | 1714 行 | 723 行(约标准的 42%) |
  74 +| 参数签名 | 14 个参数:`sLoginId, sCustomerId, sBrId, sSuId, bFilter, sUnTaskFormId, pageNum, pageSize, totalCount(OUT), countCloumn, countMapJson(OUT), sFilterOrderBy, sGroupby_select_sql, sGroupby_group_sql` | **完全相同**:14 个参数、相同顺序 |
  75 +| `SysSystemSettings.CbxSrcNoCheck` 查询 | **未使用** | **使用**,驱动“未对账印件清单来源”,即哪些计费类型来源进入报表 |
  76 +| `Fun_GetLookCustomer(sLoginId, sBrId, sSuId)` 权限作用域 | 使用 | 使用(相同调用) |
  77 +| 临时表聚合流(`B1`、`B2` 等,多段 `DROP TEMPORARY TABLE` + `INSERT INTO`) | 很重,是 1714 行主体的大头 | 移除 / 简化 |
74 78  
75   -未来修订可以与标准 `Sp_SalSalesCheck` 做并排 diff,精确解释分歧业务规则。当前最重要的是结构事实:过程形状和参数列表与标准一致,主体不同。
  79 +因此重庆展印的覆盖:
  80 +
  81 +- 保持框架调用点不变(参数签名完全相同,所以元数据驱动分发器仍能正确调用;见[通用存储过程分发](../reference/maintainer/proc-dispatch.md))。
  82 +- 增加了标准过程未暴露的 `CbxSrcNoCheck` 系统设置分支。schema 中另有 12 个 `Sp_*` 过程也使用 `CbxSrcNoCheck`(`Sp_Manufacture_MftWorkOrderAround`、`Sp_OverdueNoCheck`、`Sp_Receivables_*` 家族,以及兄弟过程 `Sp_SalSalesCheck1` / `_1227` / `_YanBao` / `_ded_copy1`);该覆盖把这个模式引入客户的主过程。
  83 +- 去掉标准过程中较重的临时表聚合流。这不是更复杂的查询,而是一条更简单的查询路径;该客户的对账语义显然不需要标准完整聚合。
  84 +
  85 +如果维护人员需要精确业务规则差异,直接 diff 两个主体:
  86 +
  87 +```bash
  88 +mysql --defaults-file=$HOME/.my.cnf xlyweberp_saas_ai \
  89 + -BNe "SELECT ROUTINE_DEFINITION FROM information_schema.routines \
  90 + WHERE ROUTINE_NAME='Sp_SalSalesCheck'" > /tmp/std.sql
  91 +diff /tmp/std.sql script/客户/重庆展印/Sp_SalSalesCheck.sql | head -200
  92 +```
  93 +
  94 +## 示例 2:万昌构建多级审批工作流 {#示例-2万昌构建多级审批工作流}
  95 +
  96 +上面的重庆展印例子替换的是**一个**过程主体。**`script/客户/万昌/`** 目录展示了更进一步的模式:客户扩展 schema,并构建标准框架没有随附的多级审批工作流。
  97 +
  98 +定制树节选:
  99 +
  100 +```text
  101 +script/客户/万昌/
  102 +├── 计件工资/
  103 +│ ├── 日报审核/
  104 +│ │ └── 领班驳回.sql ← 本切片锚点
  105 +│ ├── 报表/
  106 +│ │ ├── 包装补时.sql
  107 +│ │ ├── 员工大废品.sql
  108 +│ │ ├── 班组大废品率查询报表.sql
  109 +│ │ ├── 手工质检组返工.sql
  110 +│ │ └── Sp_Manual_quality_inspection_rework.sql
  111 +│ └── 计件工资核算/
  112 +│ ├── 计件工资/
  113 +│ │ ├── sp_piece_rate_j.sql
  114 +│ │ ├── sp_piece_rate_JZ.sql
  115 +│ │ ├── sp_piece_rate_other.sql
  116 +│ │ └── sp_piece_rate_w.sql
  117 +│ ├── 员工工资汇总查询/员工工资汇总查询.sql
  118 +│ ├── Sp_BtnEven_CalcJsHs.sql
  119 +│ └── sp_btn_WorkOrderAssessmentPassRate.sql
  120 +├── Sp_getworkorder_calc_cb.sql
  121 +└── …
  122 +```
  123 +
  124 +这些中文目录(`计件工资` / `日报审核`)把客户组织流程直接编码进文件系统。维护人员只看 `ls`,就能知道每个脚本属于哪条业务流程。
  125 +
  126 +### 驳回脚本实际做了什么
  127 +
  128 +`领班驳回.sql` 共 185 行,定义 `Sp_mftproductionreportmaster_check1_0`。命名遵循 xly 的状态转换约定:`Sp_<table>_check<currentState>_<nextState>`,所以 `check1_0` 表示“从状态 1(已审核)回到状态 0(草稿)”,也就是驳回。
  129 +
  130 +核心 UPDATE 逻辑裁剪如下:
  131 +
  132 +```sql
  133 +SET p_setSql = CONCAT('bManager = 0,
  134 + bIPQC = 0,
  135 + bDeputy = 0,
  136 + bSubmit = 0,
  137 + bWorkshopManager = 0,
  138 + bCheck = 0,
  139 + sRejectMemo = ''', p_sRejectMemo, ''',
  140 + sMReserve1 = ', p_textareaValue);
  141 +
  142 +Set @sSqlStmt = CONCAT('Update mftproductionreportmaster
  143 + Set ', p_setSql, '
  144 + Where sId = ''', p_sTmpId, '''
  145 + AND sBrandsId = ''', sBrId, '''
  146 + AND sSubsidiaryId = ''', sSuId, '''');
  147 +PREPARE sSqlStmt FROM @sSqlStmt;
  148 +EXECUTE sSqlStmt;
  149 +
  150 +CALL sp_add_flow_log(p_sTmpId, p_sTmpId, '驳回', '驳回', '驳回',
  151 + sMakePerson, p_sRejectMemo, @sReturn, @sCode);
  152 +```
  153 +
  154 +所以一次按钮点击会同时重置**六个**审批标志,追加每行的驳回原因历史,并写入客户自定义审计日志。
  155 +
  156 +### 哪些是客户侧内容,不在标准 schema 中
  157 +
  158 +已对 dev DB 侦察目标(`xlyweberp_saas_ai`)验证:
  159 +
  160 +| 定制项 | 标准 schema 是否存在 | 万昌是否需要新增 |
  161 +|---|---|---|
  162 +| `mftproductionreportmaster` 上的多级审批列:`bManager`、`bIPQC`、`bDeputy`、`bSubmit`、`bWorkshopManager` | **不存在**;标准只有 `bCheck`、`sCheckPerson`、`tCheckDate` | 是,需要 `ALTER TABLE` 增加 5 个 boolean 列 |
  163 +| `sRejectMemo` 驳回原因历史列 | **不存在** | 是,需要 `ALTER TABLE` 增加 longtext |
  164 +| `sp_add_flow_log` 审计日志过程 | **不存在** | 是,完全由客户定义 |
  165 +| `Sp_<table>_check<n>_<m>` 命名约定 | **标准 DB 中没有过程使用该模式** | 是,万昌自己的约定 |
  166 +| 接入框架按钮机制 | 存在;`gdsconfigformslave.sButtonParam` 指向过程名 | 只需要配置 |
  167 +
  168 +因此,万昌的“领班驳回”工作流是**建立在 xly 按钮原语之上的客户自建状态机**:schema 扩展 + 自定义过程 + 自定义审计日志。框架只提供按钮点击分发(经 `/business/genericProcedureCall*` 或 form-slave 上的 button-param 钩子)。其余内容,包括单据处于什么状态、哪些标志翻转、写什么审计文本,都在客户侧。
  169 +
  170 +这与 Activiti 解决同一问题的方式完全不同(BPMN 图 + assignee model + Activiti 任务表)。xly 框架允许客户选择任一模型:
  171 +
  172 +- **Activiti 模式**:部署 BPMN,通过 `gdsmoduleflow` 关联,并把 `ConstantUtils.bCheckflowCheck` 改成 `true`;见 [activiti.md](../reference/maintainer/activiti.md#路径-3activiti-bpmn-工作流有闸门目前代码中禁用)。
  173 +- **万昌模式**:扩展 schema,编写状态转换过程,放到 `script/客户/<name>/<flow-name>/` 下,手工应用。
  174 +
  175 +代码库中生产相邻的定制展示的是万昌模式:Activiti 已接线,但 `script/客户/` 下没有任何客户目录部署 BPMN;而万昌式 schema-extending workflow 确实存在。这回答了“这个仓库里工作流如何定制”的实证问题:**通过每客户覆盖脚本交付 schema 扩展型存储过程**。
  176 +
  177 +### 客户定制模式一览
  178 +
  179 +18 个客户覆盖目录中,大多数并不是定制“工作流”本身,而是定制**计算和报表**。各目录大致内容:
  180 +
  181 +- `万昌`(14 个文件):包含 `领班驳回.sql` 工作流扩展,也包含计件工资计算过程。
  182 +- `千彩`(50 个文件):定制最重的客户。主要是每租户计算覆盖(`Sp_Calc_*`、`Sp_Inventory_*`、`Sp_Manufacture_*`)和一个工作流列表视图(`viw_NoSalSalesChecking`)。
  183 +- `重庆展印`(2 个文件):如上文所述,替换一个销售对账过程和一个配套视图。
  184 +- `朝阳`(8)、`金宣发`(8)、`无锡中江`(8)、`亚明威`(6)、`福雅`(5)、`金九`(5)、`快马`(4)等:较小的计算 / 报表覆盖。
  185 +
  186 +所以工作流定制模式(schema 扩展 + 状态转换过程 + 自定义审计)是**少见的**。只有客户流程确实不能被一步审批表达、标准框架的 `bCheck` toggle 不够时,才值得这样做。大部分客户分歧是计算逻辑,而不是工作流形状。
76 187  
77 188 配套视图 `viw_salsaleschecking_pro.sql` 也出于同一原因存在:当覆盖需要标准没有的 join 形状时,工程师编写客户特定视图,并与过程一起应用到该客户 schema。
78 189  
79 190 ## 本切片引入的概念
80 191  
81   -- *两条定制通道*:通过**后台**编辑元数据(切片 1、2、4)vs. 直接应用到客户 schema 的原始 SQL 覆盖。
  192 +- *两条定制通道*(细化[现有概念页](../concepts/customization-channels.md)):通过**后台**编辑元数据(切片 1、2、4)vs. 直接应用到客户 schema 的原始 SQL 覆盖。
82 193 - *客户间 schema 分歧*:同一过程名在不同客户 DB 中可能表示不同过程,影响维护人员分析运行时行为。
83 194  
84 195 ## 参考项
... ... @@ -87,7 +198,7 @@ CREATE PROCEDURE `Sp_SalSalesCheck`(IN sLoginId varchar(100), ...)
87 198  
88 199 ## 待验证项
89 200  
90   -1. **脚本应用是否真的完全手工?** 还是存在 Quartz job / `DbToDbController` 机制?需要读 `DbToDbServiceImpl.java` 确认。
91   -2. **审计。** 写小脚本连接客户 DB,把每个 `Sp_*` / `viw_*` 主体与标准 diff。意外分歧是运维风险。
92   -3. **并排 `Sp_SalSalesCheck` diff。** 当前只描述结构,未来应纳入实际主体差异,说明重庆展印改变了哪条业务规则以及原因。
  201 +1. ~~**脚本应用是手工,还是存在 Quartz / DbToDb 机制?**~~ **已关闭:手工。** `xlyFlow/.../dbtodb/service/impl/DbToDbServiceImpl.java` 是**库间同步**(公开方法包括 `getData`、`getDataDetail`、`getDataCount`、`addSave`、`execute`、`select`、`testConnect`,都通过 `DruidDataSource` + `DruidProperties` + `JdbcUtils` 操作客户自己的远程 DB)。它**不是**脚本应用器:in-scope 代码库中没有任何遍历 `script/客户/` 目录的步骤(`grep -rn "script/客户" xly-src/.../*.java` 无命中)。每个 `script/客户/<customer>/<file>.sql` 都是为了可追溯性提交,并由工程师 / DBA 通过 `mysql --defaults-file=… < the-file.sql` 手工应用。
  202 +2. **审计。** 写小脚本连接客户 DB,把每个 `Sp_*` / `viw_*` 主体与标准版本做 diff。意外分歧是运维风险。*后续工作:针对单个租户 DB 的审计查询只是一条语句;真正工作量在于把它自动化到全部客户环境。*
  203 +3. **并排 `Sp_SalSalesCheck` diff。** 上面的结构化 diff 表(大小、参数、关键 SQL 特征、`CbxSrcNoCheck` 分支)覆盖了分歧的*形状*;如果要进一步解释精确业务规则差异,可以补过程主体级 diff。*后续工作:上面的命令可按需生成;把完整 diff 嵌入 wiki 页面的收益不值得增加页面重量。*
93 204 4. **生命周期。** 客户升级、恢复、重建 schema 时,每个覆盖如何重新应用?部署章节需要 runbook。
... ...
zh/docs/slices/06-hardware.md
... ... @@ -12,7 +12,7 @@ xly 是印刷行业 ERP。在车间,印刷机由 PLC(programmable logic cont
12 12 |---|---|
13 13 | **模块** | `xlyPlc`(代码库中的兄弟 Spring Boot 服务) |
14 14 | **方向** | 单向:PLC → ERP DB(不向印刷机回发命令) |
15   -| **节奏** | 定时轮询(Quartz `PlcScheduledTasks`) |
  15 +| **节奏** | 定时轮询(`PlcScheduledTasks` 中的 Spring `@Scheduled` cron,例如 `0/30 * * * * ?` 和 `0/1 * * * * ?`) |
16 16 | **每机型区分** | Spring profile(`-S10`、`-T0`、`-T1`、`-15S`、`-CT`、`-yt`、`-pro`) |
17 17 | **写入表** | `mftProduceReportMachineState`(dev 中约 207k 行)及相关机器状态从表 |
18 18  
... ... @@ -23,7 +23,7 @@ xly 是印刷行业 ERP。在车间,印刷机由 PLC(programmable logic cont
23 23 | 文件 | 角色 |
24 24 |---|---|
25 25 | `PlcApplicationBoot.java` | Spring Boot 入口 |
26   -| `web/scheduler/PlcScheduledTasks.java` | Quartz 驱动轮询循环 |
  26 +| `web/scheduler/PlcScheduledTasks.java` | `@Component`,包含两个 `@Scheduled` cron 方法(每 30 秒和每 1 秒)驱动轮询循环 |
27 27 | `web/scheduler/PlcRunStatus.java` | 当前轮询周期的内存状态 |
28 28 | `web/scheduler/service/PlcToErpService.java` | 接口 |
29 29 | `web/scheduler/service/impl/PlcToErpServiceImpl.java` | 实现:读 PLC、写 DB |
... ... @@ -72,7 +72,7 @@ application-pro.yml (生产)
72 72  
73 73 - 可独立部署;多个客户把它运行在靠近印刷机的机器上,与中心 ERP 服务器分离。
74 74 - 可独立失败:桥崩溃时,框架继续在旧机器状态上运行;框架宕机时,桥继续写入,框架恢复后看到缓冲行。
75   -- 难以在没有真实印刷机的情况下端到端测试。多数 CI 测试会 stub PLC 读取。
  75 +- 难以在没有真实印刷机的情况下端到端测试。多数 CI 测试只能模拟 PLC 读取。
76 76  
77 77 ## 本切片引入的概念
78 78  
... ... @@ -84,6 +84,8 @@ application-pro.yml (生产)
84 84  
85 85 ## 待验证项
86 86  
  87 +> **Item 1 — 暂缓(超出仓库可验证范围)。** 字节协议本身来自各印刷机型厂商文档,不在 xly 源码树中。每个 `xlyPlc/src/main/resources/application-<model>.yml` 只携带**参数**(波特率、帧格式、寄存器地址、轮询调优项);**协议语义**属于印刷机厂商知识。完整记录这些内容是部署运维工作,不是针对 src / DB / web 的 wiki 审计能完成的。
  88 +
87 89 1. **线缆协议。** 每个机型有不同字节协议;每个 `application-<model>.yml` 携带参数。按机型记录协议是独立小众章节。
88   -2. **桥 → ERP DB 延迟。** 每个 profile 的轮询间隔是多少?如何影响车间看板刷新?值得记录。
89   -3. **为什么 `xlyRxtx` 在 `settings.gradle` 中禁用。** RXTX 是原生串口库;build 排除了它。需要确认 xlyPlc 当前是否不依赖串口,还是只在特定部署中需要。
  90 +2. ~~**桥 → ERP DB 延迟 / 轮询间隔。**~~ **已关闭。** `PlcScheduledTasks.java` 中有两个 Spring `@Scheduled` cron 方法(未观察到 cron 字符串按 profile 变化):`0/30 * * * * ?`(每 30 秒,74 行)和 `0/1 * * * * ?`(每 1 秒,105 行)。125 行还有一个已注释的第三个 cron(`0 */2 * * * ?`),当前休眠。每 profile 参数调优发生在轮询代码读取 `application-<model>.yml` 时,不在 cron 表达式本身。车间看板刷新独立于桥:它的 `viw_*` 聚合会在每次 FROUNT 请求时重读 `mftProduceReportMachineState`,所以印刷机发出状态后,看板最多约 30 秒能看到新行。
  91 +3. ~~**为什么 `xlyRxtx` 在 `settings.gradle` 中禁用。**~~ **已关闭。** `xly-src/settings.gradle` 的 git history 显示,`xlyRxtx` 最早在 commit `daf581311`(“1、添加串口功能 …”)中加入,用于串口能力。cleanup 分支把它注释掉,是源码裁剪的一部分:排除当前 dev DB 尚未覆盖的硬件模块。需要直接串口访问印刷机的部署可以重新启用 `settings.gradle` 中的 include。xlyPlc 本身不依赖 RXTX 也能运行;这里记录的机型依赖 TCP / Ethernet,只有串口型机型需要重新启用 RXTX。
... ...
zh/docs/slices/07-workflow.md
1 1 # 切片 7(暂缓)— 带工作流的模块
2 2  
  3 +> **暂缓:需要一个已部署 BPMN 且重新打开闸门的环境。** 已对 dev DB 实证确认:`act_re_procdef = 0`、`act_ru_task = 0`、`act_hi_procinst = 0`、`biz_flow = 0`、`biz_todo_item = 0`、`gdsmoduleflow = 0`,并且 `gdsmodule WHERE bCheck = 1` 返回 0 行。除空表状态外,分发路径本身还被 `ConstantUtils.bCheckflowCheck = false` 硬禁用(见 [Activiti 集成](../reference/maintainer/activiti.md))。因此本切片无法在当前代码库 / dev DB 上跑通;[Activiti 集成](../reference/maintainer/activiti.md)页已经记录了代码推导假设。
  4 +
3 5 > **占位,暂缓。** Activiti 已接入代码库(`xlyFlow` 模块、`act_*` schema、`xlyPersist` / `xlyFlow` 中两个 Activiti 版本),但当前 dev DB(`xlyweberp_saas_ai`)中每张工作流表都是空的:`act_re_procdef`、`act_ru_task`、`act_hi_procinst`、`biz_flow`、`biz_todo_item`、`gdsmoduleflow`、`sysflowsendtointerface` 都是 0 行。没有流程部署,没有任务运行。当前环境中工作流休眠。
4 6 >
5 7 > 这里原本是切片 2;该位置已重新分配给[多租户](02-multi-tenancy.md),因为多租户在此环境中可观察。等有活动流程的 DB 可用,或决定仅以代码推导假设记录 Activiti 时,本切片再补全。
... ...
zh/docs/slices/index.md
... ... @@ -13,7 +13,7 @@
13 13 | 3 | [带报表的模块](03-report.md) | 视图、报表模板、jxls |
14 14 | 4 | [扩展自定义字段](04-custom-field.md) | `gdsconfigformuserslave`、无 schema 扩展 |
15 15 | 5 | [每客户 SQL 覆盖](05-customer-sql-override.md) | `script/客户/`、覆盖通道 |
16   -| 6 | [硬件集成模块](06-hardware.md) | `xlyPlc`、串口、到印刷机的 RPC |
  16 +| 6 | [硬件集成模块](06-hardware.md) | `xlyPlc`、PLC 轮询、写入 ERP DB |
17 17 | 7 | [带工作流的模块](07-workflow.md)(暂缓) | Activiti、`biz_flow`、审批;dev 中休眠 |
18 18  
19 19 切片 1 到 5 是主线。切片 6 对不会接触硬件的读者可选。切片 7 暂缓:dev DB 中 Activiti 表为空。
... ...
zh/mkdocs.yml
... ... @@ -36,11 +36,14 @@ theme:
36 36 icon: material/brightness-4
37 37 name: 切换到浅色模式
38 38  
39   -# CJK 搜索:分隔符包含词边界和 CJK 标点。
40   -# 需要时由目录生成器处理真正的中文分词。
  39 +# 搜索分隔符:空白、常见标点、点号、HTML 实体和 CJK 标点。
  40 +# 已移除 CamelCase 拆分,避免 `BusinessBaseServiceImpl` 或 `MyBatis`
  41 +# 被切成多个普通词,导致精确代码标识符搜索被大量噪声淹没。
  42 +# 如需部分 token 搜索,lunr 支持后缀通配符(例如 `Service*`)。
  43 +# 真正的中文分词仍由目录生成器在索引阶段调用 jieba 处理。
41 44 plugins:
42 45 - search:
43   - separator: '[\s\-,;:!=\[\]()"`/]+|(?!\b)(?=[A-Z][a-z])|\.(?!\d)|&[lg]t;|[\u3000-\u303f\uff00-\uffef]'
  46 + separator: '[\s\-,;:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|[\u3000-\u303f\uff00-\uffef]'
44 47  
45 48 markdown_extensions:
46 49 - admonition
... ... @@ -100,11 +103,14 @@ nav:
100 103 - 4. 参考(维护人员):
101 104 - reference/maintainer/index.md
102 105 - "本地运行 xlyEntry": reference/maintainer/running-locally.md
  106 + - "技术栈": reference/maintainer/tech-stack.md
103 107 - "运行时:BusinessBaseController 及相关组件": reference/maintainer/runtime.md
104 108 - "通用存储过程分发": reference/maintainer/proc-dispatch.md
105 109 - "元数据变更后的缓存失效": reference/maintainer/cache-invalidation.md
106 110 - "SQL 模板(xlyEntry/templesql/)": reference/maintainer/sql-templates.md
107 111 - "多服务部署": reference/maintainer/deployment.md
  112 + - "元数据管理服务(xlyManage)": reference/maintainer/management-services.md
  113 + - "BI / KPI / 图表引擎": reference/maintainer/bi-engine.md
108 114 - "Activiti 集成": reference/maintainer/activiti.md
109 115 - 5. API 参考:
110 116 - api-reference/index.md
... ...