diff --git a/en/docs/api-reference/external.md b/en/docs/api-reference/external.md index 806431c..b4a1110 100644 --- a/en/docs/api-reference/external.md +++ b/en/docs/api-reference/external.md @@ -22,7 +22,10 @@ Content-Type: application/json ``` Handler: `ApiController.invoke()` at -`xlyApi/src/main/java/com/xly/api/web/ApiController.java`. +`xlyApi/src/main/java/com/xly/api/web/ApiController.java:223`. The mapping +is `@RequestMapping` (method-agnostic at the framework level); the per-API +`sysapi.sMethod` column declares the expected verb for callers, and +`ApiCheckUtil` enforces it at dispatch. The flow: diff --git a/en/docs/api-reference/index.md b/en/docs/api-reference/index.md index 3d917d6..5f83b5a 100644 --- a/en/docs/api-reference/index.md +++ b/en/docs/api-reference/index.md @@ -10,7 +10,7 @@ overview lives in [concepts/api-surface](../concepts/api-surface.md). | [External API](external.md) | `xlyApi` | `/xlyApi` | You are integrating an outside system that calls xly. | | [Webhooks](webhooks.md) | `xlyInterface` | `/xlyInterface` | A third-party system needs to push events into xly. | | [Messaging](messaging.md) | `xlyEntry` + `xlyErpJms*` | n/a (ActiveMQ / RocketMQ) | An asynchronous, fan-out integration is more appropriate than a synchronous HTTP call. | -| [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. | +| [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. | ## Reading order diff --git a/en/docs/api-reference/internal.md b/en/docs/api-reference/internal.md index 2e5936a..b48b038 100644 --- a/en/docs/api-reference/internal.md +++ b/en/docs/api-reference/internal.md @@ -18,8 +18,8 @@ page is the catalog of HTTP entry points. | Endpoint | Method | Purpose | |---|---|---| -| `/business/getModelBysId/{moduleId}` | GET | Returns the form layout for a module — the five-key composite (`formData`, `gdsformconst`, `gdsjurisdiction`, `billnosetting`, `report`). | -| `/business/getBusinessDataByFormcustomId/{formId}` | POST | Returns rows of business data for a form, paginated. Branches to `getBusinessDataByGroup` when `sGroupList` is set. | +| `/business/getModelBysId/{sModelsId}` | GET | Returns the form layout for a module — the five-key composite (`formData`, `gdsformconst`, `gdsjurisdiction`, `billnosetting`, `report`). | +| `/business/getBusinessDataByFormcustomId/{gdsconfigformmasterId}` | POST | Returns rows of business data for a form, paginated. Branches to `getBusinessDataByGroup` when `sGroupList` is set. | | `/business/getBusinessDataByIndex` | POST | First / last / next / previous-record navigation. | | `/business/addBusinessData` | POST | Single insert. | | `/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 | `/treegrid/*` | `BusinessTreeGridController` | Tree-grid endpoints (the proc-backed path is implemented in this branch). | | `/procedureCall/*` | `GenericProcedureCallController` | Generic stored-procedure invocation by name + parameters — see [generic procedure dispatch](../reference/maintainer/proc-dispatch.md). | | `/panel/*` | `ConfigformPanelController` | Panel-layout persistence in `gdsconfigformpanel`. | -| `/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). | +| `/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). | | `/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). | ## Reporting and printing @@ -115,24 +115,144 @@ driven. **`图表配置`** is fully metadata-driven via two `gdsconfigcharslave`; chart definitions there are consumed by xly's dashboard rendering elsewhere in the SPA. -## Coverage policy — what this catalog includes - -`xlyEntry` hosts **~71 controllers** in total. This page enumerates -the ~19 that are part of the framework's universal runtime: -`/business/*`, `/configform/*`, `/treegrid/*`, `/procedureCall/*`, -`/panel/*`, `/checkflow/*`, `/gdsmodule/*`, `/gdsconfigform/*`, -`/gdsconfigtb/*`, plus the print surface. Every form in the system -flows through these. - -The remaining ~52 controllers are **business-domain modules** -(`/sysworkorder`, `/salesorder`, `/productionPlan`, `/oee`, -`/eleMaterialsStock`, etc.) — they implement specific industry-tier -flows on top of the framework primitives above. The wiki treats those -as *illustrations of the framework at work*, not as catalogued surface -of their own. Maintainers who need to find a specific business -controller should grep `xlyEntry/src/main/java/com/xly/web/` for the -URL prefix; the framework primitives on this page are what's worth -reading first. +## Beyond the framework primitives — the rest of xlyEntry's surface + +`xlyEntry` hosts **70 controllers** in total. The 18 framework-side +controllers — the universal-runtime ones enumerated explicitly above +plus the seven `systemweb/` admin controllers backing the +[BACK builder sidebar](#back-builder-sidebar-admin-surface) +(`GdsformconstController`, `GdsjurisdictionController`, +`GdslogininfoController`, `GdsparameterController`, `LicenseController`, +`LoginController`, `SysbrandsController`) — are where every +metadata-driven form's lifecycle lives. The remaining 52 controllers +exist because the framework's universal CRUD + procedure-dispatch +path **wasn't enough** for the use case. Each one is, in effect, a +marker for where the data-driven thesis stopped scaling. + +That makes them worth enumerating even though they are out of the +framework's catalogued surface: they show what xly hardcodes in Java +versus what it leaves to metadata. A maintainer reading them gets a +direct read on the framework's escape-hatch shape. + +> **Namespace overlap.** `BusinessBaseController` (`/business/*`) and +> `QuoquotationController` (also `@RequestMapping("/business")`) share +> the URL prefix. Spring resolves by method-level path, so +> `/business/addQuotationsheet` and `/business/getQuoquotationProgress` +> live on `QuoquotationController` while every other `/business/*` +> endpoint is on `BusinessBaseController`. The collision is benign +> (no method-path overlap) but the convention is a foot-gun: a +> future contributor adding `@PostMapping("/addQuotationsheet")` to +> `BusinessBaseController` would silently shadow the quotation path. + +### Form-helper and SPA-extension controllers (22) + +Framework-adjacent endpoints — extensions to the universal CRUD path +that didn't fit the form-master / form-slave shape. Most are called +*alongside* `/business/*` from the same SPA screen. + +| Endpoint root | Controller | Role | +|---|---|---| +| `/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. | +| `/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. | +| `/parameter/*` | `BusinessParameterController` | Per-module parameter reads. | +| `/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. | +| `/calcprocedure/*` | `CalcProcedureController` | The runtime side of the [calculation-formula](../reference/builder/define-vtable.md) feature — `/calc` invokes a named calc proc. | +| `/calculationFormula/*` | `CalculationFormulaController` | Builder-side metadata for calculation formulas. | +| `/calculationStd/*` | `CalculationStdController` | Standard-calc lookup catalog. | +| `/char/*` | `CharController` | Chart-config CRUD for the BACK 图表配置 page — wraps `gdsconfigcharmaster`/`slave`. | +| `/checkModel/*` | `CheckmodelController` | Approval-model membership reads (`getUserListByModelId/{sCheckModeId}`). Used by the lightweight (non-Activiti) approval flow. | +| `/comparatorTree/*` | `ComparatorTreeController` | Comparator-tree reads for filterable hierarchical pickers. | +| `/excel/*` | `ExcelController` | Excel **export** of a grid — `/export/{gdsconfigformmasterId}`. Sibling to print, but data instead of report layout. | +| `/import/*` | `ImportExcelController` | Excel **import** (`/checkExcel`, then commit). Validates against the form's slaves before inserting. | +| `/filterTree/*` | `FilterTreeController` | Tree-shaped filter dropdown for grids. | +| `/notClear/*` | `NotClearController` | Barcode-scan "not yet cleared" save path (`doNotClearSave`, `getNotClearScanData/{sProcName}/{sId}`). Specific to scanner-driven warehouse flows. | +| `/notice/*` | `NoticeController` | In-app notice fetch / mark-read. | +| `/replaceField/*` | `ReplaceFieldController` | Bulk field-replace across rows. | +| `/searchgroupby/*` | `SearchgroupbyController` | Saved-search definitions with group-by. | +| `/searchupdown/*` | `SearchUpDownController` | Previous-/next-record-by-search navigation (variant of `/business/getBusinessDataByIndex`). | +| `/syssearch/*` | `SyssearchController` | Saved-search definitions CRUD. | +| `/syssystem/*` | `SyssystemController` | Variant of `getBusinessDataByFormcustomId` for system-table reads (`/getSyssystemDataByFormcustomId/{gdsconfigformmasterId}`) — bypasses tenant scoping for global metadata reads. | +| `/sqlfile/*` | `SqlFileController` | SQL-file load/save backing the "Mysql脚本配置" admin screen. | +| `/instruct/*` | `InstructController` | Direct SQL execution endpoints (`/exesql`, `/opensql`) — the admin-side query console. | + +### User and permission management (4) + +Multiple overlapping controllers for the same concern. The `New` +suffix and the presence of `sftlogininfo` *and* `userinfo` *and* +`gdslogininfo` (in `systemweb/`) suggest a refactor in progress — +the older path coexists with the new one until callers migrate. + +| Endpoint root | Controller | Role | +|---|---|---| +| `/userinfo/*` | `UserInfoController` | Current-user profile + session info. | +| `/sftlogininfo/*` | `SftlogininfoController` | User-account CRUD (newer of two paths). | +| `/sysjurisdiction/*` | `SysjurisdictionController` | Group/user permission reads (`getGroupData`, `getUserData`). | +| `/sysjurisdictionNew/*` | `SysjurisdictionNewController` | Newer parallel path (`getGroupDataNew`, `getGroupUserIdNew/{sUserId}`). Same concern, different shape. | + +### Manufacturing / MES (7) + +Industry-tier flows whose state machines, multi-table joins, or +hardware integration could not be expressed as `gdsconfigformmaster` +SQL. These are the framework's largest single concentration of +hardcoded business logic. + +| Endpoint root | Controller | Role | +|---|---|---| +| `/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. | +| `/workOrderFlow/*` | `WorkOrderFlowController` | Work-order routing / process-flow reads (`getWorkOrderFlow`, `getSplitWorkOrderData/{sId}`). | +| `/workOrderPlan/*` | `WorkOrderPlanController` | Production-plan reads linking work orders to plan rows (`getControlProcess/{sProductionPlanId}`, `getProductionPlanInfo`). | +| `/splitWorkOrder/*` | `SplitWorkOrderController` | Splitting a master work order into multiple sub-orders (`getSplitWorkOrderData`). | +| `/productionPlan/*` | `ProductionPlanController` | Plan-tree reads (`getProductionPlanTree`). | +| `/process/*` | `ProcessController` | Manufacturing-process catalog reads. | +| `/oee/*` | `OeeController` | Overall Equipment Effectiveness — barcode-scan and MES status callbacks (`updateBarcode/{sBarCodeId}/{sBarCode}`, `doSysMesMsg/{sStatus}/{sMachineId}`). | + +### Sales, inventory, accounting, procurement, HR (9) + +| Endpoint root | Controller | Role | +|---|---|---| +| `/salesorder/*` | `SalesOrderController` | Sales-order specifics beyond the universal CRUD. | +| `/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. | +| `/eleMaterialsStock/*` | `EleMaterialsStockController` | Raw-material stock reads (`getEleMaterialsStock`, `getEleMaterialsStoreCurrQty`). | +| `/eleProductStock/*` | `EleProductStockController` | Finished-product stock reads. | +| `/costCenter/*` | `CostCenterController` | Cost-center data + voucher-import (`getCostCenterData`, `getCosvoucherImportData`). | +| `/sysAccountPeriod/*` | `SysAccountPeriodController` | Accounting-period open/close logic. | +| `/erpOrderProcurement/*` | `ErpOrderProcurementController` | Procurement-order specifics. | +| `/sisproductclassify/*` | `SisproductclassifyController` | Product-classification tree. | +| `/eleteamemployee/*` | `EleteamemployeeController` | Team/employee assignment for shop-floor flows. | + +### Integration and hardware (5) + +| Endpoint root | Controller | Role | +|---|---|---| +| `/file/*` | `FileController` | File upload (incl. WeChat mobile variant `mobileuploadwechat`). | +| `/plc/*` | `PlcController` | PLC-bridge entry points (`getplcMachine/{iOrder}/{sParentId}`) — see [Slice 6](../slices/06-hardware.md). | +| `/mobilephone/*` | `MobliePhoneController` | Mobile-app endpoints. (Typo `Moblie` is in the class name; the URL is `/mobilephone`.) | +| `/sysWebsocket/*` | `SysWebSocketController` | WebSocket setup/teardown for push notifications. | +| `/wechat/*` | `WechatController` | WeChat integration (in-app QR, OAuth callback). | + +### Out-of-scope per [the index](../index.md) (5) + +Listed for completeness — these are not part of the framework wiki's +in-scope surface, but they exist in the WAR. + +| Endpoint root | Controller | Status | +|---|---|---| +| `/ai/*` | `AiController` | AI assistant. Out of scope (index.md). | +| `/robot/*` | `ChatGptController` | ChatGPT integration. Out of scope (index.md). | +| `/test/*` | `TestController` | Dev scaffolding (`/file`, `/getDinkToken`). | +| (root paths) | `TestProcessController` | Dev scaffolding; no class-level `@RequestMapping`. | +| (commented out) | `XsController` | Dead file — `@RestController` and `@RequestMapping` are commented out; class exists but registers nothing. | + +### Reading these as a diagnostic + +Three patterns stand out when you scan the list: + +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. +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. +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. + +The framework's universal runtime is what's *missing* from these +controllers' jobs. ## What this API is *not* diff --git a/en/docs/api-reference/messaging.md b/en/docs/api-reference/messaging.md index 9c1a8dc..ab2d965 100644 --- a/en/docs/api-reference/messaging.md +++ b/en/docs/api-reference/messaging.md @@ -83,10 +83,12 @@ express well). RocketMQ topics are configured per environment. ## Manual cache-invalidation poke -If a metadata change happens via raw SQL (no JMS event), the cache -across nodes will not bust automatically. The supported override is -`BusinessCleanRedisDataImpl` in `xlyBusinessService/.../service/impl/` — -it can publish an invalidation event directly. See +If a metadata change happens via raw SQL, the cache across nodes will not +bust automatically because no BACK save path calls `@CacheEvict`. The +supported override is to invoke the appropriate +`BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(...)` cleaner +from inside the application once; it evicts the shared Redis-backed Spring +cache directly. See [cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md) for the broader troubleshooting path. @@ -95,6 +97,6 @@ for the broader troubleshooting path. - **Not a public integration channel.** External integrators do not publish or subscribe to these brokers. They are *internal* fan-out for the cluster. -- **Not the only way to invalidate caches.** The HTTP write paths in - `xlyEntry` already publish JMS events when they should; the manual - poke is for edge cases. +- **Not a cache-invalidation channel.** The HTTP write paths in + `xlyEntry` already evict Redis synchronously when they should; the + manual cleaner call is for raw-SQL or bypass paths. diff --git a/en/docs/api-reference/notifications.md b/en/docs/api-reference/notifications.md index e8261a6..09caff4 100644 --- a/en/docs/api-reference/notifications.md +++ b/en/docs/api-reference/notifications.md @@ -1,9 +1,9 @@ # Notifications (xlyMsg) -Outbound notifications — DingTalk and WeChat — go through the `xlyMsg` -module. This is *not* an HTTP surface that callers hit; it's an -internal SDK that in-scope services call when a business event should -push a message out to a chat platform. +Outbound notifications — DingTalk, WeChat, and email — go through the +`xlyMsg` module. This is *not* an HTTP surface that callers hit; it's +an internal SDK that in-scope services call when a business event +should push a message out to a chat platform or mailbox. ## What's inside @@ -13,6 +13,7 @@ push a message out to a chat platform. |---|---| | `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`. | | `wechat/service/WechatService` + `wechat/util/SendWxUtil`, `Wx_SignatureUtil`, `JedisMsgUtil`, `MsgContentUtil`, `Xml2JsonUtil` | WeChat work-platform dispatch — signature + send, including a Redis-backed access-token cache. | +| `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. | | `notice/service/NoticeService` | Provider-agnostic notice abstraction; routes a logical "notify user X about event Y" to the right backend. | `xlyMsg/build.gradle` — sole framework dependency is `xlyPersist`. The diff --git a/en/docs/api-reference/webhooks.md b/en/docs/api-reference/webhooks.md index 4789630..a2ce257 100644 --- a/en/docs/api-reference/webhooks.md +++ b/en/docs/api-reference/webhooks.md @@ -37,7 +37,7 @@ for inbound calls: | Endpoint | Method | Purpose | |---|---|---| | `/interfaceDefine/invoke/{interfaceInvoke}` | POST | Dispatch an inbound payload to the handler that `{interfaceInvoke}` names. | -| `/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.) | +| `/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. | Handler: `xlyInterface/src/main/java/com/xly/web/InterfaceController.java`. diff --git a/en/docs/concepts/customization-layers.md b/en/docs/concepts/customization-layers.md index a8d37ac..739ed18 100644 --- a/en/docs/concepts/customization-layers.md +++ b/en/docs/concepts/customization-layers.md @@ -57,10 +57,13 @@ links are FK-enforced — see [no-FK reality](semantic-fk.md). The framework reads each layer in order and merges by `sName` (the field name). For a custom slave row with the same `sName` as a base slave: override. For a new `sName`: append. For a base slave with no -corresponding custom row: pass through unchanged. The merge happens -inside `BusinessBaseServiceImpl.getModelBysId` (line 181) and the -helpers it calls — `BusinessGdsconfigformsServiceImpl.getFormSlaveData` -+ `getFormCustomSlaveData`. +corresponding custom row: pass through unchanged. Entry point is +`BusinessBaseServiceImpl.getModelBysId` (line 181), which calls +`BaseServiceImpl.getModelConfigByModleId` (line 55); the actual +slave-+-customslave merge runs in +`BusinessGdsconfigformsServiceImpl.getGdsconfigformslaveShow` (line 392), +combining `getFormSlaveData` (line 87) and `getFormCustomSlaveData` +(line 121), then optionally layering `getUserFormSlaveData` (line 156). Two database **views** support the merge by joining the form-master with the relevant slave table: diff --git a/en/docs/concepts/index.md b/en/docs/concepts/index.md index 95a93a9..361b925 100644 --- a/en/docs/concepts/index.md +++ b/en/docs/concepts/index.md @@ -87,6 +87,6 @@ a sign the content wants to be a slice instead. - [No-FK, semantic-FK reality](semantic-fk.md) — how relations actually work. - [Two customization channels](customization-channels.md) — metadata edits vs. SQL scripts. - [Customization layers](customization-layers.md) — within Channel 1, how base / per-tenant / per-user overlays merge. -- [Multi-tenancy and product editions](multi-tenancy.md) — the three scoping axes (`sBrandsId`, `sSubsidiaryId`, `sVersionFlowId`). +- [Multi-tenancy and product editions](multi-tenancy.md) — row scoping (`sBrandsId`, `sSubsidiaryId`) plus licence-gated module discovery. - [The metadata-driven request lifecycle](request-lifecycle.md) — the diagram you'll come back to. - [The three API tiers](api-surface.md) — internal (`xlyEntry`), external (`xlyApi`), inbound webhooks (`xlyInterface`). diff --git a/en/docs/concepts/master-slave.md b/en/docs/concepts/master-slave.md index e412b0f..a98ddd7 100644 --- a/en/docs/concepts/master-slave.md +++ b/en/docs/concepts/master-slave.md @@ -4,9 +4,10 @@ > This page is about the **document-row** pattern: one header row plus N > detail rows for a quotation / sales order / work order. The > **DataSource** master / slave (write-vs-read connection routing via -> `MasterDataSourceConfig` / `SlaveDataSourceConfig` in `xlyApi`, paired -> with `MasterBaseMapper.xml` / `SlaveBaseMapper.xml` in `xlyPersist`) is -> a different concept covered in the [Tech-stack HikariCP row](../reference/maintainer/tech-stack.md#3-cache-in-memory) +> `MasterDataSourceConfig` / `SlaveDataSourceConfig` in `xlyApi` and +> `xlyInterface`, paired with `mastermapper/MasterBaseMapper.xml` / +> `slavemapper/SlaveBaseMapper.xml` co-located in those services) is +> a different concept covered in the [Tech-stack HikariCP row](../reference/maintainer/tech-stack.md#2-persistence) > and indirectly in the runtime page. The two senses overlap in name only. Almost every business document in xly — a quotation, a sales order, a work diff --git a/en/docs/concepts/modules-forms-vtables.md b/en/docs/concepts/modules-forms-vtables.md index 02a46f8..07e9566 100644 --- a/en/docs/concepts/modules-forms-vtables.md +++ b/en/docs/concepts/modules-forms-vtables.md @@ -92,8 +92,8 @@ module / form / virtual-table combination through one universal dispatch path. There is no per-module Java; PMs creating new modules are creating new rows. -The flip side: that one engine has accumulated 3,500+ lines in -`BusinessBaseServiceImpl` alone, plus another 800+ in +The flip side: that one engine has accumulated 3,900+ lines in +`BusinessBaseServiceImpl` alone, plus another 600+ in `BusinessGdsconfigformsServiceImpl`. Edge cases, special-case table handling (e.g., the `mftproductionplanslave` hardcode at `BusinessBaseServiceImpl.java:1768`), per-tenant overlay merge @@ -112,17 +112,17 @@ any business-data table to its domain by the three-letter prefix: | Prefix | Domain | Sample tables (live count) | |---|---|---| | `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), … | -| `sys` | Framework system (numbering, jurisdiction grants, reports, search, billing settings) — distinct from `gds*` "definition" tier | `sysjurisdiction`, `sysbillnosettings`, `sysreport`, `syssearch`, `sysapi`, `SysSystemSettings`, … (66 tables) | -| `sis` | Shared lookup tables / classifiers backing dropdowns | `sisbank`, `siscolor`, `sisversionflow`, `sisjurisdictionclassify`, … (78 tables) | -| `sft` | Login-session / group-permission link tables | `sftlogininfo*`, `sftlogininfojurisdictiongroup`, … (8 tables) | -| `ele` | Master data ("element"): customer, employee, machine, materials, product, process, semigoods, costframe | `elecustomer*`, `eleemployee*`, `elemachine*`, `elematerials*`, `eleproduct*`, … (88 tables) | -| `mft` | Manufacturing: work-order, production-plan, production-report | `mftworkordermaster`, `mftproductionplan*`, `mftproductionreport*`, … (72 tables) | -| `sal` | Sales | `salsalesordermaster`, `salsalesorderslave`, `salsalesorderprocess`, … (65 tables) | -| `quo` | Quotation | `quoquotationmaster`, `quoquotationslave`, `quoquotationcalc_tmp`, … (12 tables) | +| `sys` | Framework system (numbering, jurisdiction grants, reports, search, billing settings) — distinct from `gds*` "definition" tier | `sysjurisdiction`, `sysbillnosettings`, `sysreport`, `syssearch`, `sysapi`, `syssystemsettings`, … (68 tables) | +| `sis` | Shared lookup tables / classifiers backing dropdowns | `sisbank`, `siscolor`, `sisversionflow`, `sisjurisdictionclassify`, … (80 tables) | +| `sft` | Login-session / group-permission link tables | `sftlogininfo`, `sftlogininfojurisdictiongroup`, … (8 tables) | +| `ele` | Master data ("element"): customer, employee, machine, materials, product, process, semigoods, costframe | `elecustomer*`, `eleemployee*`, `elemachine*`, `elematerials*`, `eleproduct*`, … (89 tables) | +| `mft` | Manufacturing: work-order, production-plan, production-report | `mftworkordermaster`, `mftproductionplan*`, `mftproductionreport*`, … (82 tables) | +| `sal` | Sales | `salsalesordermaster`, `salsalesorderslave`, `salsalesorderprocess`, … (67 tables) | +| `quo` | Quotation | `quoquotationmaster`, `quoquotationslave`, `quoquotationcalc_tmp`, … (23 tables) | | `acc` | Accounting | `accordercostanalysis`, `accordercostanalysisoperation`, … (31 tables) | | `pur` | Purchasing | `purpurchaseapply`, `purpurchasearrive`, `purpurchasechecking`, … (28 tables) | | `ops` | Outside-processing / outsourcing | `opsoutsidearrive`, `opsoutsidechecking`, `opsoutsideinstore`, … (23 tables) | -| `cah` | Cashier / financial | `cahcashierinit`, `cahcostchange`, `cahpayment`, `cahreceipt`, … (22 tables) | +| `cah` | Cashier / financial | `cahcashierinit`, `cahcostchangemaster`, `cahpaymentmaster`, `cahreceiptmaster`, … (22 tables) | | `sgd` | Semi-goods (半成品) | `sgdsemigoodscheck`, `sgdsemigoodsinstore`, `sgdsemigoodsmatchbill`, … (21 tables) | | `ept` | Equipment / machine fixed assets | `eptmachinefixedborrow`, `eptmachinefixedchange`, `eptmachinefixedinstore`, … (21 tables) | | `mit` | Materials inventory transactions | `mitmaterialsadjust`, `mitmaterialscheck`, `mitmaterialsinstore`, … (19 tables) | diff --git a/en/docs/concepts/multi-tenancy.md b/en/docs/concepts/multi-tenancy.md index 25633a2..e7d446f 100644 --- a/en/docs/concepts/multi-tenancy.md +++ b/en/docs/concepts/multi-tenancy.md @@ -14,7 +14,7 @@ two-paragraph summary you can link from anywhere. |---|---|---|---| | **`sBrandsId`** (加工商ID) | almost every business row | per-row | the user's session (`UserInfo.getsBrandsId()`) | | **`sSubsidiaryId`** (子公司ID) | almost every business row | per-row | the user's session | -| **`sVersionFlowId`** (版本流程ID) | `gdsmodule` only | per-module | the user's edition (against `sisversionflow`) | +| **`sVersionFlowId` / `sVersionFlowCode`** (版本流程ID / code) | `gdsmodule` only | per-module tag | edition catalogue metadata; the runtime menu gate uses the licence-derived `sVerifyLicense` module list | Per-row scoping is universal across business-data tables: both `sBrandsId` and `sSubsidiaryId` appear on essentially every one. Most @@ -27,9 +27,11 @@ tables. In practice they hold a single sentinel tenant value shared across all customers. Convention: "if a row represents tenant-owned state, both columns are present *and populated from the session*." -Per-module gating (`sVersionFlowId`) is the opposite — it lives on -`gdsmodule` only. So edition gating is a one-time filter at module- -discovery time, not a per-row check. +Per-module edition metadata is the opposite — it lives on `gdsmodule` +only. The live runtime does not filter directly on `sVersionFlowId`; +module discovery is gated by the licence-derived `sVerifyLicense` list +of permitted `gdsmodule.sId` values. So edition gating is a one-time +module-discovery filter, not a per-row check. ## How it's enforced diff --git a/en/docs/concepts/request-lifecycle.md b/en/docs/concepts/request-lifecycle.md index d9c5d9e..e931dc0 100644 --- a/en/docs/concepts/request-lifecycle.md +++ b/en/docs/concepts/request-lifecycle.md @@ -107,9 +107,10 @@ sequenceDiagram Note over CTRL: AuthorizationInterceptor.preHandle
resolves UserInfo from Redis
RequestAddParamUtil.addParams (16 keys) CTRL->>SVC: getModelBysId(map) - SVC->>FORMS: getModelConfigByModleId
(form-master + slaves + overlays) + Note over SVC: getModelConfigByModleId (inherited from BaseServiceImpl)
orchestrates the per-master form-master + slave loads + SVC->>FORMS: getFormmasterData / getGdsconfigformslaveShow
(form-master + slaves + overlays) REDIS-->>FORMS: cache hit? - FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize ⋈ slave ⋈ customslave + FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize; per master row, gdsconfigformslave + gdsconfigformcustomslave DB-->>FORMS: rows FORMS-->>SVC: formData @@ -172,12 +173,15 @@ A few things readers expect to find here but don't: body — the handler branches on whether the body asks for one row or many. - **Workflow steps.** When a module has an active approval workflow - (`bCheck = 1`, populated `sVersionFlowId`, deployed Activiti process), - additional steps interleave. None of those tables are populated in - this dev DB; see [Slice 7 (deferred)](../slices/07-workflow.md). -- **Cache invalidation.** When BACK changes a metadata row, a JMS message - invalidates cached copies on every running node — `ConsumerChangeGdsModuleThread` - in `xlyErpJmsConsumer`. Outside the request flow but adjacent to it. + (`bCheck = 1`, `gdsmoduleflow` configured, deployed Activiti process, + and `ConstantUtils.bCheckflowCheck = true`), additional steps + interleave. None of those tables are populated in this dev DB; see + [Slice 7 (deferred)](../slices/07-workflow.md). +- **Cache invalidation.** When BACK changes a metadata row, the save path + synchronously calls `BusinessCleanRedisData` / `CleanRedisServiceImpl`, + which evicts Spring cache regions from shared Redis. The JMS + `ConsumerChangeGdsModuleThread` path is a separate base-data merge + channel, not cache invalidation. ## Variations covered by other slices diff --git a/en/docs/concepts/thesis.md b/en/docs/concepts/thesis.md index ba89ebe..eacd318 100644 --- a/en/docs/concepts/thesis.md +++ b/en/docs/concepts/thesis.md @@ -66,7 +66,7 @@ Three costs are baked into this design and worth being explicit about: - **Customizations are layered "cleanly"** ([Slice 4](../slices/04-custom-field.md)): per-tenant overrides sit *on top of* the shared base without forking. — *Limit:* the cleanliness is a Java-side property. The runtime - merge logic in `BusinessBaseServiceImpl` is non-trivial (3,500+ + merge logic in `BusinessBaseServiceImpl` is non-trivial (3,900+ lines), debugging "why does this tenant see field X but not Y" involves chasing through `gdsconfigformpersonalize` + `gdsconfigformcustomslave` + `gdsconfigformuserslave` interactions. diff --git a/en/docs/contributing/index.md b/en/docs/contributing/index.md index d939905..cc07e36 100644 --- a/en/docs/contributing/index.md +++ b/en/docs/contributing/index.md @@ -41,10 +41,10 @@ on each run. Hand-edits in those directories will be lost. ## Pre-commit hook (optional but recommended for local edits) -Install once: +Install once (run from `xly-wiki/en/`): ```bash -ln -s ../../scripts/precommit.sh .git/hooks/pre-commit +ln -s ../../en/scripts/precommit.sh ../.git/hooks/pre-commit chmod +x scripts/precommit.sh ``` diff --git a/en/docs/glossary/index.md b/en/docs/glossary/index.md index d1449d2..4b8e525 100644 --- a/en/docs/glossary/index.md +++ b/en/docs/glossary/index.md @@ -23,3 +23,7 @@ preserves Chinese terms in the body text and translates only here. | 流水号 | flow number / sequence | Document numbering — `sysbillnosettings`. | | 抄送 | cc (carbon copy) | `biz_todo_copyto`. | | 待办 | todo | `biz_todo_item`. | +| 审核 | audit / approve | The "approve" click in BACK that flips a document's `bCheck` flag and runs `Sp__check*` transition procs. The canonical workflow trigger. | +| 驳回 | reject | The counterpart to 审核 — reject a document; in customer overrides like `领班驳回.sql` it walks a multi-level rejection chain. | +| — | BACK | The admin / builder web app (`http://:8597`) where PMs configure modules, forms, and permissions. Spelled uppercase to match the deployment artefact. | +| — | FROUNT | The end-user web app (`http://:8598`) where day-to-day work happens. Misspelling of "Front" preserved from the deployment artefact. | diff --git a/en/docs/index.md b/en/docs/index.md index de4b21f..6a56f42 100644 --- a/en/docs/index.md +++ b/en/docs/index.md @@ -40,7 +40,7 @@ which Reference chapter you go deep on. - Scheduler modules (`xlyErpTask`, `xlyPlatTask`) — commented out in `settings.gradle`; cron / Quartz wiring is not part of the wiki's framework runtime. - Test scaffolding modules (`xlyTestService`, `xlyTestController`) — historical, not part of the framework runtime. - Per-tenant schema drift between `xlyweberp_*` databases — wiki targets one schema. -- 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. +- 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. - 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. > **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. diff --git a/en/docs/reference/builder/attach-workflow.md b/en/docs/reference/builder/attach-workflow.md index 8e5a5d9..516059a 100644 --- a/en/docs/reference/builder/attach-workflow.md +++ b/en/docs/reference/builder/attach-workflow.md @@ -5,7 +5,7 @@ > returns 0; `gdsmoduleflow = 0`; `gdsmodule WHERE bCheck = 1` matches > 0 rows. The dispatch path itself is hard-disabled by > `ConstantUtils.bCheckflowCheck = false` (see -> [Activiti integration](../../reference/maintainer/activiti.md)). The +> [Activiti integration](../maintainer/activiti.md)). The > recipe below is the **code-derived hypothesis** — it has not been > exercised against a live deployment. @@ -19,8 +19,10 @@ > > 1. Set `gdsmodule.bCheck = 1` to flag the module as workflow-enabled. > 2. Populate `gdsmoduleflow` with the flow's window configuration. -> 3. Set `gdsmodule.sVersionFlowId` and `sVersionFlowCode` to the -> Activiti process definition's ID and key. +> 3. Link the module/button to the process in `gdsmoduleflow` using +> the fields expected by `CheckExamineFlowServiceImpl`. +> `gdsmodule.sVersionFlowId` / `sVersionFlowCode` are edition tags, +> not the live Activiti binding. > 4. Deploy the BPMN process definition through the Activiti REST API > or whichever workflow deployment surface is enabled in the target > environment. diff --git a/en/docs/reference/builder/define-form.md b/en/docs/reference/builder/define-form.md index 4e19598..11dc6f4 100644 --- a/en/docs/reference/builder/define-form.md +++ b/en/docs/reference/builder/define-form.md @@ -19,7 +19,7 @@ One row registers the module's existence. Required columns: | `sChinese` / `sEnglish` / `sBig5` | display name in three languages | | `sParentId` | parent module's `sId` — places this module in the menu tree | | `sBrandsId` / `sSubsidiaryId` | tenant scope — should be your tenant's IDs (or `'1111111111'` if standard / system-level) | -| `sVersionFlowId` | the product edition this module belongs to (look up in `sisversionflow`) | +| `sVersionFlowId` / `sVersionFlowCode` | product-edition catalogue tags (look up in `sisversionflow` where populated); live menu visibility is still gated by the licence-derived module list | | `bVisible` | `1` to show in the menu | | `bInvalid` | `0` for active | diff --git a/en/docs/reference/builder/define-vtable.md b/en/docs/reference/builder/define-vtable.md index 2b943bf..a40be9a 100644 --- a/en/docs/reference/builder/define-vtable.md +++ b/en/docs/reference/builder/define-vtable.md @@ -98,7 +98,7 @@ A representative real row from the dev DB: sId = 192116810113315231587698560 sChinese = 包装方式 (Packing method) sTbName = SisPacking -sParentId = (root) +sParentId = 192116810113315231564967560 (parent classification row) ``` **Slave columns** (`gdsconfigtbslave`, 10 rows under that `sParentId`) diff --git a/en/docs/reference/builder/index.md b/en/docs/reference/builder/index.md index dbafe42..58771c6 100644 --- a/en/docs/reference/builder/index.md +++ b/en/docs/reference/builder/index.md @@ -12,5 +12,3 @@ metadata-table column names and a worked example. - [How to set permissions](permissions.md) — `gdsjurisdiction` recipe. If you need details on a specific table or proc, see the [Auto-Catalog](../../auto-catalog/index.md). - -> **STUB.** Recipes will be filled in as their concepts are exercised by slices. diff --git a/en/docs/reference/builder/permissions.md b/en/docs/reference/builder/permissions.md index b937e4f..29e5a28 100644 --- a/en/docs/reference/builder/permissions.md +++ b/en/docs/reference/builder/permissions.md @@ -8,7 +8,7 @@ correctly here matters because the names look superficially similar. | Table | Granularity | What it stores | Loaded when | |---|---|---|---| -| [`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)) | +| [`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)) | | [`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()` | The mental model: `gdsjurisdiction` is the *menu* of permission items @@ -22,7 +22,7 @@ A tree of permission items keyed by module: | Column | Meaning | |---|---| | `sParentId` | the `gdsmodule.sId` this permission item belongs to | -| `sName` | the action key — e.g., `BtnAdd`, `BtnUpd`, `BtnDel`, `BtnExport` | +| `sName` | the action key — e.g., `BtnAdd`, `BtnUpd`, `BtnDel`, `BtnPrint` | | `sChinese` / `sEnglish` / `sBig5` | display label (e.g., `新增`) | | `iOrder` | sort order in the permission UI | | `sBrandsId` / `sSubsidiaryId` | tenant scope | @@ -89,7 +89,7 @@ For a new module, the typical path is: 1. Define the module + form ([recipe](define-form.md)). 2. Insert `gdsjurisdiction` rows for each button/action the module - exposes (`BtnAdd`, `BtnUpd`, `BtnExport`, …). One row per + exposes (`BtnAdd`, `BtnUpd`, `BtnPrint`, …). One row per action; `sParentId = your module's sId`. 3. Decide which roles get which actions, and insert `sysjurisdiction` rows: `sParentId = module sId`, `sJurisdictionClassifyId = role sId`, diff --git a/en/docs/reference/maintainer/activiti.md b/en/docs/reference/maintainer/activiti.md index b4cddd6..24fd422 100644 --- a/en/docs/reference/maintainer/activiti.md +++ b/en/docs/reference/maintainer/activiti.md @@ -158,7 +158,7 @@ the instance. ### A real Path-1 customisation example -[Slice 5](../../slices/05-customer-sql-override.md#worked-example-2-builds-a-multi-level-approval-workflow) +[Slice 5](../../slices/05-customer-sql-override.md#worked-example-2-万昌-builds-a-multi-level-approval-workflow) walks through 万昌's `领班驳回.sql` — a customer-side multi-level approval rejection. It's the canonical example of how customers 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`: a `SpringProcessEngineConfiguration` bean. - `xlyEntry/build.gradle` includes `xlyFlow` as `api project(':xlyFlow')`, so the starter is on the runtime classpath of the `xlyEntry` WAR. -- `xlyEntry/.../EntryApplicationBoot.java:23-24` excludes only +- `xlyEntry/.../EntryApplicationBoot.java:23-24` excludes + `DataSourceAutoConfiguration`, `org.activiti.spring.boot.SecurityAutoConfiguration` (the - REST-endpoint security adapter) and Spring's own + REST-endpoint security adapter), and Spring's own `SecurityAutoConfiguration`. **Activiti's main engine auto-config is NOT excluded** → the engine starts. - `xlyFlow/.../activiti/config/ActivitiConfig.java` is a @@ -267,7 +268,7 @@ plus the modeler subpackage are real call sites. Selected anchors: | Save a model in the modeler | `ModelerController.create()` :122 | `repositoryService.saveModel()` + `addModelEditorSource()` | | Deploy a BPMN at runtime | `ModelerController.deploy()` :147 | `repositoryService.createDeployment().addString(name, bpmnXml).deploy()` | | List process definitions | `ProcessDefinitionController` :135 | `repositoryService.createProcessDefinitionQuery()` | -| Read engine config | `ProcessActController` :281 | `ProcessEngines.getDefaultProcessEngine()` | +| Read engine config | `BizTodoItemServiceImpl` :126 | `ProcessEngines.getDefaultProcessEngine()` | | 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 | ## URLs the modeler exposes (xlyFlow controllers, on xlyEntry's port) @@ -291,7 +292,7 @@ authoritative. This is a wiki-internal correction worth flagging: the class file exists at `xlyEntry/src/main/java/com/xly/web/businessweb/CheckFlowController.java` -but its body is **22 lines, zero handler methods** — just a +but its body is **25 lines, zero handler methods** — just a `@RestController @RequestMapping(value="/checkflow")` shell with no content. Earlier versions of this wiki described `/checkflow/*` as "Activiti workflow surface (approve / reject / view)"; that is not diff --git a/en/docs/reference/maintainer/cache-invalidation.md b/en/docs/reference/maintainer/cache-invalidation.md index 91926c7..82ddc3d 100644 --- a/en/docs/reference/maintainer/cache-invalidation.md +++ b/en/docs/reference/maintainer/cache-invalidation.md @@ -17,7 +17,7 @@ flowchart TB PM[PM clicks 保存 in BACK]:::ok SAVE["BusinessBaseServiceImpl
add/update/deleteBusinessData"] - EVICT["BusinessCleanRedisData.delCleanRedisData
→ CleanRedisServiceImpl
17 @CacheEvict methods"]:::ok + EVICT["BusinessCleanRedisData.delCleanRedisData
→ CleanRedisServiceImpl
18 cache regions for gdsmodule"]:::ok REDIS[("Redis
(shared across nodes)")]:::ok DB[("MySQL
row written")]:::ok @@ -72,7 +72,7 @@ Next /business/getModelBysId call re-reads from DB and re-populates The cleaner methods are in `xlyBusinessService/src/main/java/com/xly/service/impl/CleanRedisServiceImpl.java`. A representative one — invoked when `gdsmodule` rows change — evicts -17 cache regions in a single call: +18 cache regions in a single call: ``` @CacheEvict(value = { @@ -130,7 +130,9 @@ not Spring Cache), Spring Boot 2.2.5 auto-configures **Empirically verified** against the live dev Redis at `118.178.19.35:16379` (database 0): 233 of 267 keys use Spring Cache's default `::` separator. Sample key shape matching the -`@Cacheable` SpEL spec from `BusinessGdsconfigformsServiceImpl.java:209-211`: +`@Cacheable` annotation on `getFormconstData` at +`BusinessGdsconfigformsServiceImpl.java:189-190` (default key derived +from all params): ``` businessGdsconfigformsServiceGetFormconstData::{sLanguage=sChinese, sModelsId=…, sSubsidiaryId=1111111111, sUserId=…, sBrandsId=1111111111} diff --git a/en/docs/reference/maintainer/deployment.md b/en/docs/reference/maintainer/deployment.md index d5f3a34..35e240d 100644 --- a/en/docs/reference/maintainer/deployment.md +++ b/en/docs/reference/maintainer/deployment.md @@ -117,7 +117,7 @@ files. The active profile is selected at startup via ## Disabled in `settings.gradle` -The cleanup branch comments out 12 `include` lines. Three are non-Plat +The cleanup branch comments out 17 `include` lines. Three are non-Plat modules present on disk: - `xlyErpTask` — long-running background tasks. @@ -127,7 +127,7 @@ modules present on disk: - `xlyFile` — older file-management module, superseded by `xlyPlatFileUpload` (also commented out). -The remaining nine commented-out includes are `xlyTestService`, +The remaining 14 commented-out includes are `xlyTestService`, `xlyTestController`, and the full `xlyPlat*` family except `xlyPlatConstant` — i.e. `xlyPlatTask`, `xlyPlatJmsProductor`, `xlyPlatJmsConsumer`, `xlyPlatReportForm`, `xlyPlatFileUpload`, @@ -161,8 +161,9 @@ Three communication channels: schema. Most cross-service "communication" is implicit through shared tables. 2. **Messaging** — both ActiveMQ/JMS and RocketMQ exist in the codebase. - Cache invalidation ([cache invalidation on metadata change](cache-invalidation.md)) - uses the ActiveMQ/JMS path. + ActiveMQ/JMS carries base-data merge and document fan-out jobs; Redis + cache invalidation is synchronous `@CacheEvict` in the BACK save path + (see [cache invalidation on metadata change](cache-invalidation.md)). 3. **HTTP REST** — for synchronous calls (e.g., xlyApi calling xlyEntry's `/business/*` endpoints). @@ -185,7 +186,7 @@ Profiles split by service: `bgj` (lowercase). `dev` is the in-repo default. - **xlyApi**: `local` (default in repo), `dev`, `linux`, `win`. - **xlyInterface**: `dev` only. -- **xlyFlow**: `dev` (empty file). +- **xlyFlow**: `dev` (datasource-only). - **xlyFace**: `win` (default), `dev`, `linux`, `local`. - **xlyPlc**: `dev` (default) plus 7 press-model profiles (`15S`, `S10`, `T0`, `T1`, `CT`, `yt`, `pro` — uppercase / mixed-case, diff --git a/en/docs/reference/maintainer/index.md b/en/docs/reference/maintainer/index.md index 92329c9..c362978 100644 --- a/en/docs/reference/maintainer/index.md +++ b/en/docs/reference/maintainer/index.md @@ -9,10 +9,9 @@ metadata table to the `gds*` family, or wire in a new third-party integration. - [Tech stack](tech-stack.md) — library inventory by category (versions, where each is used, and why). - [The runtime: BusinessBaseController & friends](runtime.md) — the metadata-driven dispatch loop. - [Generic procedure dispatch](proc-dispatch.md) — `GenericProcedureCallController` deep dive. -- [Cache invalidation on metadata change](cache-invalidation.md) — `ConsumerChangeGdsModuleThread` and friends. -- [SQL templates (`xlyEntry/templesql/`)](sql-templates.md) — runtime SQL generation. +- [Cache invalidation on metadata change](cache-invalidation.md) — synchronous `@CacheEvict`, and why similarly named JMS consumers are not cache-bust. +- [SQL templates (`xlyEntry/templesql/`)](sql-templates.md) — proc scaffolds engineers fill in. - [Multi-service deployment](deployment.md) — `xlyApi` vs `xlyEntry` vs `xlyInterface`. +- [Metadata-management services (`xlyManage`)](management-services.md) — the `Gds*ServiceImpl` family behind the BACK builder. +- [BI / KPI / Charts engine](bi-engine.md) — the homebrewed dashboard layer. - [Activiti integration](activiti.md) — version skew, schemas, custom delegates. - -> **STUB.** Each sub-page will be filled with a real code-trace once the matching -> slice exercises it. diff --git a/en/docs/reference/maintainer/management-services.md b/en/docs/reference/maintainer/management-services.md index 9e25bfd..52023c8 100644 --- a/en/docs/reference/maintainer/management-services.md +++ b/en/docs/reference/maintainer/management-services.md @@ -16,7 +16,7 @@ the read/write logic for every `gds*` table. The runtime | Service | Lines | Owns | BACK page | |---|---:|---|---| -| `GdsmoduleServiceImpl` | 729 | `gdsmodule` (modules), `gdsroute` (URL whitelist), module-tree CRUD, edition gating | 系统模块配置 | +| `GdsmoduleServiceImpl` | 729 | `gdsmodule` (modules), `gdsroute` (URL whitelist), module-tree CRUD | 系统模块配置 | | `GdsconfigformServiceImpl` | 878 | `gdsconfigformmaster`, `gdsconfigformslave`, `gdsconfigformcustomslave`, `gdsconfigformpersonalize` (form definitions + per-tenant overlays) | 界面显示内容配置 | | `GdsconfigtbServiceImpl` | 555 | `gdsconfigtbmaster`, `gdsconfigtbslave` (virtual-table definitions) | 数据表内容配置 | | `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. end-to-end usable: the BACK builder can also generate the schema-migration SQL the overlay implies. - **`GdsmoduleServiceImpl` includes `getModuleTreePro`** — the - per-edition / per-tenant module-tree resolution called by the SPA at - login (the first `/gdsmodule/getModuleTreePro` request you see in - the live trace). Edition gating - ([Slice 2](../../slices/02-multi-tenancy.md)) happens here, as a - filter on `gdsmodule.sVersionFlowId` against the user's - `sisversionflow` row. + module-tree resolution called by the SPA at login (the first + `/gdsmodule/getModuleTreePro` request you see in the live trace). + Edition visibility ([Slice 2](../../slices/02-multi-tenancy.md)) is + ultimately enforced by the licence-derived `sVerifyLicense` module-id + list used by the menu SQL, not by a direct `sVersionFlowId` predicate. - **`SqlScriptsServiceImpl`** glues the [`templesql/`](sql-templates.md) scaffolds into the BACK script authoring screen. Engineers fill in the placeholder spec; the @@ -85,14 +84,18 @@ own dedicated controller+service pair. ## Cache-invalidation hookpoints -Every write through these services synchronously calls -`BusinessCleanRedisData.delCleanRedisData*` on commit. This is why +Every write through these services carries its own `@CacheEvict` +annotations directly on the method (e.g., `GdsmoduleServiceImpl.java:96` +evicts nine cache regions on `addGdsmodule`). This is why metadata edits in BACK take effect immediately on every node — the shared Redis cache (RedisCacheManager, see [cache-invalidation.md](cache-invalidation.md)) gets the relevant regions evicted in the same transaction the write commits. There is **no JMS fan-out here for cache-bust** — that's a common misconception, addressed in detail on the cache-invalidation page. +(Business-data writes through `BusinessBaseServiceImpl` use the +separate `BusinessCleanRedisData.delCleanRedisData*` dispatcher; see +[cache-invalidation.md](cache-invalidation.md) for that path.) ## What's *not* in `xlyManage` @@ -109,7 +112,7 @@ misconception, addressed in detail on the cache-invalidation page. | Symptom | First place to look | |---|---| | BACK 修改/新增 against `gdsconfigform*` returns "操作失败" | `GdsconfigformServiceImpl` — check field validation + the matching DDL-script generation path | -| Edition gating shows wrong modules | `GdsmoduleServiceImpl.getModuleTreePro` — verify the user's `sVersionFlowId` resolution | +| Edition gating shows wrong modules | menu/module-tree SQL plus `VerifyLicense.getModelAllList()` / `sVerifyLicense` — verify the permitted module-id list | | BACK script-authoring screen produces broken SQL | `SqlScriptsServiceImpl` + the [templesql scaffolds](sql-templates.md) | | Permission catalogue (BtnAdd / BtnUpd / …) missing for a module | `GdsjurisdictionServiceImpl` — check the rows under that `sParentId` | | User can log in to BACK but FROUNT is empty | `GdslogininfoServiceImpl` — check the `sftlogininfo*` link tables | diff --git a/en/docs/reference/maintainer/proc-dispatch.md b/en/docs/reference/maintainer/proc-dispatch.md index ec6d767..beff792 100644 --- a/en/docs/reference/maintainer/proc-dispatch.md +++ b/en/docs/reference/maintainer/proc-dispatch.md @@ -119,15 +119,15 @@ templates](sql-templates.md) for the loader and the placeholder syntax. The 1687 procedures in the live DB cluster around a few naming molds beyond the bare `Sp_*` family: -| Mold | Approx count | Role | +| Mold | Live count | Role | |---|---:|---| | `Sp_*` | most | The dominant family, dispatched by `sSaveProName` / `sCalcProName` / `sProcName` etc. | -| `Sp_*_BeforeSave` | ~62 | Pre-save hooks. Pair with `sSaveProNameBefore`. | -| `Sp_*_AfterSave` / `Sp_*_SaveReturn` | ~62 / ~54 | Post-save hooks; `_SaveReturn` writes back into the parent transaction. | -| `Sp_*_Calc` | ~178 | Calculation procs invoked by button-press flow (`sCalcProName` / `sButtonParam`). | -| `sp_btn_*` | ~65 | Button-event sub-family — typically `sp_btn_calc*` / `sp_btn_validate*` (lowercase by convention). | -| `PRO_ERPMERGE*` | ~11 | Data-migration utilities. **Not dispatched by the runtime** — engineer-only. | -| `PRO_*` (other) | ~12 | Other one-off utilities. | +| `Sp_beforeSave*` | 78 | Pre-save hooks. Pair with `sSaveProNameBefore`. | +| `Sp_afterSave*` / `Sp_saveReturn*` | 62 / 54 | Post-save hooks; `Sp_saveReturn*` writes back into the parent transaction. | +| `Sp_Calc*` | 184 | Calculation procs invoked by button-press flow (`sCalcProName` / `sButtonParam`). | +| `Sp_Btn*` | 65 | Button-event sub-family — typically button calculations / validators. | +| `PRO_ERPMERGE*` | 22 | Data-merge utilities. **Not dispatched by the runtime** — engineer / JMS-consumer paths. | +| `PRO_*` (all) | 23 | `PRO_ERPMERGE*` plus one-off utilities. | | `Get_*`, `del_*`, `Cal*`, `Tj_*` | small handfuls | Legacy / domain-specific helpers. Not part of the generic-dispatch contract. | A typo in any of the dispatched columns gets an `Sp_*`-shaped target, diff --git a/en/docs/reference/maintainer/running-locally.md b/en/docs/reference/maintainer/running-locally.md index 339768e..e956727 100644 --- a/en/docs/reference/maintainer/running-locally.md +++ b/en/docs/reference/maintainer/running-locally.md @@ -69,7 +69,7 @@ boot: - `xlyApi` — start it as a second JVM if you need the external API surface. - `xlyInterface` — same. - `xlyPlc`, `xlyFlow`, `xlyFace` — same; each has its own application class and profile. -- 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). +- 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`. For multi-service local development, see [Multi-service deployment](deployment.md). diff --git a/en/docs/reference/maintainer/runtime.md b/en/docs/reference/maintainer/runtime.md index db8005d..7125f12 100644 --- a/en/docs/reference/maintainer/runtime.md +++ b/en/docs/reference/maintainer/runtime.md @@ -16,7 +16,7 @@ controllers and services that carry most of the generic form runtime. | `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}` | | `GenericProcedureCallController` | `web/businessweb/` | Generic stored-procedure invocation by name + parameters. | `/procedureCall/doGenericProcedureCall` | | `ConfigformPanelController` | `web/businessweb/` | Panel-layout persistence in `gdsconfigformpanel`. | `/panel/get/{sFormId}`, `/panel/save/{sFormId}` | -| `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) | +| `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) | | `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` | Note that the controllers split across **two packages**: `businessweb/` @@ -226,7 +226,7 @@ Two flagged in slices that belong here permanently: The "one controller writes any row in any table" pattern is the core data-driven move. It also concentrates risk: -- **`BusinessBaseServiceImpl` is ~3,500 lines** of tightly +- **`BusinessBaseServiceImpl` is ~3,900 lines** of tightly intertwined logic: per-tenant scope-bypass list, special-case table hardcodes (`mftproductionplanslave` at line 1768), pre/post-save hook dispatch, sTable-driven write routing. Every diff --git a/en/docs/reference/maintainer/tech-stack.md b/en/docs/reference/maintainer/tech-stack.md index 30e9a69..9e366b2 100644 --- a/en/docs/reference/maintainer/tech-stack.md +++ b/en/docs/reference/maintainer/tech-stack.md @@ -48,7 +48,7 @@ page records facts only. | Library | Version | Where | In-scope source references | |---|---|---|---| -| 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}/`. | +| 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}/`. | | 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. | | 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`). | | 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. | Library | Version | Where | In-scope source references | |---|---|---|---| -| 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). | +| 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). | | 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). | | 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). | | 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). | diff --git a/en/docs/slices/01-hello-world.md b/en/docs/slices/01-hello-world.md index c3e1619..04ffc4b 100644 --- a/en/docs/slices/01-hello-world.md +++ b/en/docs/slices/01-hello-world.md @@ -227,9 +227,11 @@ POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId} ### 5. Cache invalidation A modified `gds*` row in any of the four metadata tables invalidates cached -copies on every running node. xly does this through a JMS message: -`xlyErpJmsConsumer/.../ConsumerChangeGdsModuleThread.java` listens for the -"module changed" event and clears the relevant Redis keys. See +copies on every running node through the shared Redis store. The save path +calls `BusinessCleanRedisData.delCleanRedisData*`, which fires Spring +`@CacheEvict` synchronously in the BACK process. The similarly named JMS +`CHANGE_GDS_MODULE` path runs base-data merge procedures; it does not clear +Redis. See [Cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md). ### 6. Browser confirms diff --git a/en/docs/slices/02-multi-tenancy.md b/en/docs/slices/02-multi-tenancy.md index 1674b1c..1505e80 100644 --- a/en/docs/slices/02-multi-tenancy.md +++ b/en/docs/slices/02-multi-tenancy.md @@ -17,7 +17,7 @@ xly's tenancy has **three** dimensions, applied at different layers: |---|---|---|---| | **`sBrandsId`** (加工商ID) | Almost every business row | Per-row | "Which manufacturer/company owns this row?" | | **`sSubsidiaryId`** (子公司ID) | Almost every business row | Per-row | "Which subsidiary within the company?" | -| **`sVersionFlowId`** (版本流程ID) | `gdsmodule` only | Per-module | "Which product edition is this module part of?" | +| **`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. | The first two are **per-row** scoping. The third is **per-module** filtering 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: - *Multi-tenant scoping* (new concept page) — `sBrandsId`/`sSubsidiaryId` as the per-row tenant boundary; the framework's universal injector (`RequestAddParamUtil`). -- *Product editions* (new concept page) — `sVersionFlowId` against - `sisversionflow` as the per-module visibility filter; the difference - between scoping (per-row) and gating (per-module). +- *Product editions* (concept page) — `sVersionFlowId` / + `sVersionFlowCode` as catalogue tags, with actual visibility gated + by the licence-derived module list; the difference between scoping + (per-row) and gating (per-module). These will be added to Concepts as part of the next backfill pass. @@ -186,8 +187,9 @@ These will be added to Concepts as part of the next backfill pass. - [The runtime](../reference/maintainer/runtime.md) — `RequestAddParamUtil` belongs in the runtime chapter as the universal tenant-context injector. -- New page: *Multi-tenant query patterns* — the conventions every MyBatis - mapper and every stored procedure must follow to stay tenant-safe. +- [Multi-tenancy and product editions](../concepts/multi-tenancy.md) — + the conventions every MyBatis mapper and stored procedure must follow + to stay tenant-safe. ## Open verification items diff --git a/en/docs/slices/04-custom-field.md b/en/docs/slices/04-custom-field.md index a85d527..34f3667 100644 --- a/en/docs/slices/04-custom-field.md +++ b/en/docs/slices/04-custom-field.md @@ -109,8 +109,8 @@ configuration: there are no foreign keys enforcing that `gdsconfigformcustomslave.sParentId` actually exists in `gdsconfigformmaster.sId`. Orphan rows are possible and would be silently ignored at merge time. A maintainer audit script that flags such orphans -is on the [Maintainer Reference](../reference/maintainer/runtime.md)'s -TODO list. +belongs in the maintainer toolkit; the current runtime does not enforce +that relationship. ## Why it works without code changes — and what that costs diff --git a/en/docs/slices/05-customer-sql-override.md b/en/docs/slices/05-customer-sql-override.md index ec5e86c..eda3145 100644 --- a/en/docs/slices/05-customer-sql-override.md +++ b/en/docs/slices/05-customer-sql-override.md @@ -248,7 +248,7 @@ Verified against the dev DB recon target (`xlyweberp_saas_ai`): So 万昌's "Foreman Rejection" workflow is **a customer-built state-machine atop xly's button primitive**: schema extension + custom procs + custom audit log. The framework provides only the -button-press dispatch (via `/business/genericProcedureCall*` or the +button-press dispatch (via `/procedureCall/doGenericProcedureCall` or the button-param hook on the form-slave). Everything else — what state the document is in, what flags toggle, what audit text gets logged — is customer-side. diff --git a/en/docs/slices/index.md b/en/docs/slices/index.md index 7709a8b..81771d5 100644 --- a/en/docs/slices/index.md +++ b/en/docs/slices/index.md @@ -14,9 +14,9 @@ Re-read earlier slices once you've read later ones — the cross-refs back-fill. | # | Slice | Concept(s) introduced | |---|---|---| | 1 | [a CRUD module (Hello World)](01-hello-world.md) | modules, forms, master/slave, jurisdiction | -| 2 | [multi-tenancy and product editions](02-multi-tenancy.md) | sBrandsId/sSubsidiaryId scoping, sVersionFlowId editions | +| 2 | [multi-tenancy and product editions](02-multi-tenancy.md) | sBrandsId/sSubsidiaryId scoping, licence-gated editions | | 3 | [a module with a report](03-report.md) | views, report templates, jxls | -| 4 | [extending — a custom field](04-custom-field.md) | gdsconfigformuserslave, schema-less extension | +| 4 | [extending — a custom field](04-custom-field.md) | gdsconfigformcustomslave, schema-less extension | | 5 | [extending — a per-customer SQL override](05-customer-sql-override.md) | script/客户/, override channel | | 6 | [a hardware-integrated module](06-hardware.md) | xlyPlc, serial, RPC into the press | | 7 | [a module with workflow](07-workflow.md) (deferred) | Activiti, biz_flow, approval — needs a deployment with active flows | diff --git a/zh/docs/api-reference/external.md b/zh/docs/api-reference/external.md index 970bc89..e933daa 100644 --- a/zh/docs/api-reference/external.md +++ b/zh/docs/api-reference/external.md @@ -15,13 +15,13 @@ Content-Type: application/json { ...request body, passed through as `sBody` to the handler... } ``` -处理器:`xlyApi/src/main/java/com/xly/api/web/ApiController.java` 中的 `ApiController.invoke()`。 +处理器:`xlyApi/src/main/java/com/xly/api/web/ApiController.java:223` 中的 `ApiController.invoke()`。该 mapping 是 `@RequestMapping`,框架层不限定 HTTP 方法;每个 API 行的 `sysapi.sMethod` 列声明调用方应使用的动词,并由 `ApiCheckUtil` 在分发时校验。 流程: 1. 读取 `Authorization` header(回退:`authorizationt` 查询参数)。 2. 查找以 `sApiCode` 为键的 `sysapi` 行(通过 `ApiServiceImpl.invoke` → `SELECT … FROM sysapi WHERE sApiCode = #{sApiCode}`)。 -3. 如果该行的 `bHasToken` 标志已设置,就用 `sysapithirdtoken`(或该行指向的等价 token 存储)验证 token。 +3. 如果该行的 `bHasToken` 标志已设置,就 AES 解密 bearer token 取回 `corpid`,再通过 `BrandServiceImpl.selectByCorpid` 到 `sysapibrand` 校验该 `corpid`。如果品牌行的 `iLossTime` 非 0,还会检查 token 内嵌时间戳是否过期。(`sysapithirdtoken` 用于**出站** token,也就是 xly 调第三方 API;这里不用于校验入站 bearer token。) 4. 使用请求 body 合并出的参数 map,执行 `sysapi.sDataSql` 中存放的 SQL 模板。 5. 将调用写入 `sysapilog`。 6. 用 `AjaxResult` 包装结果并返回。 @@ -32,7 +32,7 @@ Content-Type: application/json | 列 | 含义 | |---|---| -| `sApiCode` | 消费方发送的 path variable。每个租户内必须唯一。 | +| `sApiCode` | 消费方发送的路径变量。每个租户内必须唯一。 | | `sApiName` | 人类可读标签。 | | `sApiUrl` / `sApiUrlRef` | 计算后的 URL,即 `sApiUrlRef + sApiCode`,用于出站分发。 | | `sMethod` | 此 API 期望的 HTTP 方法(`GET`、`POST` 等)。 | @@ -47,9 +47,9 @@ Content-Type: application/json ## Token 端点:`/token/*` -| Endpoint | Method | 用途 | +| 端点 | 方法 | 用途 | |---|---|---| -| `/token/getToken?corpid=&corpsecret=` | POST | 为集成方的 `(corpid, corpsecret)` 组合签发 bearer token。 | +| `/token/getToken?corpid=&corpsecret=` | GET / POST | 为集成方的 `(corpid, corpsecret)` 组合签发 bearer token。(mapping 不限定方法。) | 返回的 token 就是 `/api/invoke/{sApiCode}` 期望在 `Authorization` 中收到的值。完整实现位于 `xlyApi/src/main/java/com/xly/api/web/TokenController.java` 及其 service。 @@ -59,15 +59,17 @@ Content-Type: application/json 这些是同一个 WAR 中托管的较小专用 API: -| Endpoint root | Controller | 用途 | +| 端点前缀 | Controller | 用途 | |---|---|---| -| `/online/api/{sApiCode}` | `OnlineController` | 只读执行某个 `sysapi` 行(不写入)。适合公开数据端点。 | -| `/online/onlineword/{sApiCode}` | `OnlineController` | 返回“word”风格模板载荷的变体。 | -| `/online/onlinelist`, `/online/getToken` | `OnlineController` | 在线 API 列表及其 token 签发。 | -| `/pro/get/{sProName}` | `ProContentController` | 按名称获取存储过程元数据。 | -| `/pro/getData/{sProName}` | `ProContentController` | 执行指定存储过程并返回其结果集。 | -| `/pro/alert/{redisId}`, `/pro/getAlertValue/{redisId}` | `ProContentController` | 按 Redis id 读取预警 / 通知值。 | -| `/pro/executeSql` | `ProContentController` | 执行参数化 SQL 模板(管理级)。 | +| `/online/api/{sApiCode}` | `OnlineController` | 为给定 `sysapi` 行渲染 BACK 浏览器内 API 调试 / 控制台页面(返回 Thymeleaf view,不执行 API)。 | +| `/online/onlineword/{sApiCode}` | `OnlineController` | 渲染“word”风格 API 文档页面。 | +| `/online/onlinelist` | `OnlineController` | 渲染在线 API 列表页面。 | +| `/online/getToken` | `OnlineController` | 渲染浏览器内 token 获取辅助页面。 | +| `/pro/get/{sProName}` | `ProContentController` | 渲染 BACK 页面,用于展示某个存储过程源码。 | +| `/pro/getData/{sProName}` | `ProContentController` | 返回存储过程源码文本(不是结果集)。 | +| `/pro/alert/{redisId}` | `ProContentController` | 为给定 Redis key 渲染预警 / 通知展示页面。 | +| `/pro/getAlertValue/{redisId}` | `ProContentController` | 返回 Redis 中 `redisId` 对应的缓存值。 | +| `/pro/executeSql` | `ProContentController` | 直接执行 `sSql` 载荷(管理级开发工具)。 | | `/thirdparty/*` | `ThirdPartyController` | 第三方 API 定义 CRUD,以及 `checkPartyApi` 校验器。由 `sysapithirdparty` 支撑。 | | `/thirdtoken/*` | `ThirdTokenController` | 出站 token 配置 CRUD。由 `sysapithirdtoken` 支撑。 | | `/brand/*` | `BrandController` | 伙伴 / 供应商列表(`sysapibrand`)CRUD。 | @@ -86,8 +88,8 @@ Content-Type: application/json | `sysapibrand` | 伙伴 / 供应商目录。 | | `sysapithirdparty` | 出站第三方端点定义。 | | `sysapithirdtoken` | 出站第三方 token 配置。 | -| `sysapidbtodb` | DB-to-DB 同步 API 定义。 | -| `sysapidbtodblog` | DB-to-DB 同步运行日志。 | +| `sysapidbtodb` | DB-to-DB 同步 API 定义。**由 `xlyFlow` 的 `DbToDbController` 拥有,不由 xlyApi 拥有**;列在这里是因为该表位于 xlyApi 的 `sysapi.sql` 中。 | +| `sysapidbtodblog` | DB-to-DB 同步运行日志。同上,由 xlyFlow 写入。 | ## 集成方如何使用 diff --git a/zh/docs/api-reference/index.md b/zh/docs/api-reference/index.md index 0927053..ccd0424 100644 --- a/zh/docs/api-reference/index.md +++ b/zh/docs/api-reference/index.md @@ -8,7 +8,7 @@ xly 暴露了三个不同的 HTTP 接口面,分别由三个独立的 Spring Bo | [外部 API](external.md) | `xlyApi` | `/xlyApi` | 你在接入会从外部调用 xly 的系统。 | | [Webhook](webhooks.md) | `xlyInterface` | `/xlyInterface` | 第三方系统需要把事件推送进 xly。 | | [消息](messaging.md) | `xlyEntry` + `xlyErpJms*` | 不适用(ActiveMQ / RocketMQ) | 异步、扇出式集成比同步 HTTP 调用更合适。 | -| [通知](notifications.md) | `xlyMsg`(作为库被 `xlyEntry`、`xlyBusinessService`、`xlyInterface` 使用) | 不适用(钉钉 / 微信 API) | 业务事件需要向用户推送聊天平台消息。 | +| [通知](notifications.md) | `xlyMsg`(作为库被 `xlyEntry`、`xlyBusinessService`、`xlyInterface`、`xlyFlow` 使用) | 不适用(钉钉 / 微信 / 邮件 API) | 业务事件需要向用户推送聊天平台消息或邮件。 | ## 阅读顺序 diff --git a/zh/docs/api-reference/internal.md b/zh/docs/api-reference/internal.md index 4493b6f..64c66d2 100644 --- a/zh/docs/api-reference/internal.md +++ b/zh/docs/api-reference/internal.md @@ -8,15 +8,18 @@ ## 通用 CRUD 接口面:`/business/*` -| Endpoint | Method | 用途 | +| 端点 | 方法 | 用途 | |---|---|---| -| `/business/getModelBysId/{moduleId}` | GET | 返回某个模块的表单布局,也就是五键组合(`formData`、`gdsformconst`、`gdsjurisdiction`、`billnosetting`、`report`)。 | -| `/business/getBusinessDataByFormcustomId/{formId}` | POST | 返回某个表单的业务数据行,带分页。设置 `sGroupList` 时会分支到 `getBusinessDataByGroup`。 | +| `/business/getModelBysId/{sModelsId}` | GET | 返回某个模块的表单布局,也就是五键组合(`formData`、`gdsformconst`、`gdsjurisdiction`、`billnosetting`、`report`)。 | +| `/business/getBusinessDataByFormcustomId/{gdsconfigformmasterId}` | POST | 返回某个表单的业务数据行,带分页。设置 `sGroupList` 时会分支到 `getBusinessDataByGroup`。 | | `/business/getBusinessDataByIndex` | POST | 首条 / 末条 / 下一条 / 上一条记录导航。 | | `/business/addBusinessData` | POST | 单行新增。 | | `/business/addUpdateDelBusinessData` | POST | 在一个事务调用里组合新增 + 修改 + 删除。前端通过 `sTable` 直接指定目标表。 | | `/business/getSelectDataBysControlId/{sId}` | POST | 按控件 `sId` 为单个控件加载下拉选项。 | | `/business/getSelectLimit/{sId}` | POST | 下拉加载调用的分页变体。 | +| `/business/addSysLocking` | POST | 用户开始编辑单据时获取乐观锁,在系统锁表中按 `(sFormGuid, sUserId)` 插入一行。SPA 进入编辑模式时会触发它,使并发编辑者收到冲突提示。处理器:`BusinessBaseController.java:400-407`。 | +| `/business/doExamine` | POST | 简单“审核”:通过 SQL 把指定行的 `bCheck` 翻为 `1`。**不会调用 Activiti**;这是不需要多步工作流的模块使用的 xly 轻量审批路径。处理器:`BusinessBaseController.java:384-391` → `BusinessBaseServiceImpl.doExamine` → `ExamineServiceImpl`。何时改用 Activiti,见 [xly 如何在不使用 Activiti 的情况下处理工作流](../reference/maintainer/activiti.md#xly-如何在不使用-activiti-的情况下处理工作流)。 | +| `/business/getProData` | POST | 面向模块的通用存储过程调用,是 `/procedureCall/doGenericProcedureCall` 的另一条路径。处理器:`BusinessBaseController.java:350-358` → `BusinessBaseServiceImpl.getProData`。FROUNT 会用它做模块级 proc 读取,例如首页看板的 `/getProData?sModelsId=...&sName=` 模式。 | 这些端点在[切片 1](../slices/01-hello-world.md)(`getModelBysId` + 网格加载 + 保存)和[切片 3](../slices/03-report.md)(基于视图的读取变体)中有更详细说明。处理类位于 `xlyEntry/src/main/java/com/xly/web/businessweb/`。 @@ -24,7 +27,7 @@ 配置侧动作(创建模块、定义表单、声明虚拟表)在 `xlyEntry/src/main/java/com/xly/web/systemweb/` 下有一套并行接口面: -| Endpoint root | Controller | 用途 | +| 端点前缀 | Controller | 用途 | |---|---|---| | `/gdsmodule/*` | `GdsmoduleController` | 模块树 CRUD,包括 `getModuleTreePro`、`addGdsmodule`、`updateGdsmodule`。 | | `/gdsconfigform/*` | `GdsconfigformController` | 表单主表和表单明细元数据 CRUD。 | @@ -32,20 +35,21 @@ ## 专用运行时端点 -| Endpoint root | Controller | 用途 | +| 端点前缀 | Controller | 用途 | |---|---|---| | `/configform/*` | `BusinessConfigformController` | 用户 / 用户组级显示定制。 | | `/treegrid/*` | `BusinessTreeGridController` | 树表端点(当前分支实现的是存储过程支撑路径)。 | | `/procedureCall/*` | `GenericProcedureCallController` | 按名称 + 参数通用调用存储过程;见[通用存储过程分发](../reference/maintainer/proc-dispatch.md)。 | | `/panel/*` | `ConfigformPanelController` | `gdsconfigformpanel` 中的面板布局持久化。 | -| `/checkFlow/*` | `CheckFlowController` | Activiti 工作流接口面(审批 / 驳回 / 查看),只在运行流程引擎的部署中有意义。 | +| `/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)。 | +| `/modelCenter/getModelCenter`、`/modelCenter/getModelCenterCalculation` | `BusinessModelCenterController` | FROUNT 首页的 **KPI 工作中心**卡片(标题为 `KPI监控`)。它聚合标记为 `gdsmodule.bUnTask=1` 的模块上的未清任务,按角色和业务流程分组。**不是 Activiti 驱动。** 见 [KPI 工作中心](../reference/maintainer/runtime.md#kpi-工作中心front-端首页-dashboard)。 | ## 报表与打印 打印接口面位于 `xlyEntry/src/main/java/com/xly/web/report/`: - `PrintReportController` — 当前 jxls / iText 打印路径。 -- `PrintReportControllerOld` — 为旧模板保留的历史打印路径。 +- `PrintReportControllerOld.java` — 文件存在,但类体已全部注释掉(而且注释内类名是 `PrintReportController`,不是 `*Old`)。这是保留作参考的死代码,不是活跃 controller。 前端的“打印” / “导出”按钮会调用这些控制器,控制器从 `sysreport` 加载模板,执行匹配的视图查询,并把二进制文件流回前端。流程见[切片 3](../slices/03-report.md)。 @@ -55,6 +59,135 @@ 如果某个请求未认证却进入了控制器,正常情况下会被 `@Authorization` 拦下;如果没有被拦下(例如某个方法未加注解),这个方法也会绕过 `RequestAddParamUtil` 中的通用租户注入,因此就是多租户 bug。 +## BACK 配置侧边栏(管理接口面) {#back-builder-sidebar-admin-surface} + +BACK 管理侧边栏的 10 个顶层项(登录 `admin`/`123`,版本 `基础版/8s`)都通过上面的框架原语接线成元数据驱动页面: + +| 侧边栏 | URL 片段 | 支撑 form-master `sTbName` | 所属 service | +|---|---|---|---| +| 系统模块配置 | `/xtmkpz` | `gdsmodule` | `GdsmoduleServiceImpl` | +| 数据表内容配置 | n/a | `gdsconfigtbmaster`/`slave` | `GdsconfigtbServiceImpl` | +| 界面显示内容配置 | n/a | `gdsconfigformmaster`/`slave`/`customslave`/`personalize` | `GdsconfigformServiceImpl` | +| 接口自定义配置 | `/sjbnrpz` | `sysapi` 表族 | xlyApi 侧管理接口 | +| 系统常量配置 | `/xtclpz` | `gdsformconst` | `GdsformconstServiceImpl`,切片 1 锚点 | +| 系统权限配置 | n/a | `gdsjurisdiction` | `GdsjurisdictionServiceImpl` | +| 常用操作配置 | n/a | 当前 dev DB 中没有对应 `gdsconfigformmaster` 行;页面是 SPA 中直接接线的管理特例。若通过元数据扩展,数据位于用户自定义按钮组层。 | n/a | +| 用户信息配置 | n/a | `sftlogininfo` 表族 | `GdslogininfoServiceImpl` | +| Mysql脚本配置 | n/a | BACK 对 [`templesql/` 脚手架](../reference/maintainer/sql-templates.md)的编辑器 | `SqlScriptsServiceImpl` | +| 图表配置 | 无 `gdsroute` 项;通过 SPA state 导航 | `gdsconfigcharmaster`/`slave` | `GdsconfigformServiceImpl`(图表子集) | + +10 项里有 8 项是本目录和[维护人员参考](../reference/maintainer/management-services.md)覆盖的框架原语。**`常用操作配置` 是 SPA 侧管理特例**:它出现在侧边栏,但 dev DB 中没有对应的 `gdsconfigformmaster` 行,说明该页在 BACK 中硬编码,而不是元数据驱动。**`图表配置`** 则完全由元数据驱动:两条 `gdsconfigformmaster` 行分别指向 `gdsconfigcharmaster` 和 `gdsconfigcharslave`,其中的图表定义由 SPA 其他位置的看板渲染消费。 + +## 框架原语之外:xlyEntry 的其余接口面 + +`xlyEntry` 总共托管 **70 个 controller**。其中 18 个属于框架侧:上面明确列出的通用运行时 controller,加上支撑 [BACK 配置侧边栏](#back-builder-sidebar-admin-surface) 的 7 个 `systemweb/` 管理 controller(`GdsformconstController`、`GdsjurisdictionController`、`GdslogininfoController`、`GdsparameterController`、`LicenseController`、`LoginController`、`SysbrandsController`)。每个元数据驱动表单的生命周期都在这些接口面里。 + +剩余 52 个 controller 存在,是因为框架的通用 CRUD + 存储过程分发路径**不足以表达对应用例**。换句话说,每一个都是数据驱动论点停止扩展的位置标记。 + +即使它们不属于框架目录化接口面,也值得枚举:它们展示了 xly 哪些内容硬编码在 Java 里,哪些内容留给元数据。维护人员读这些 controller,能直接看到框架逃生口的形状。 + +> **命名空间重叠。** `BusinessBaseController`(`/business/*`)和 `QuoquotationController`(同样 `@RequestMapping("/business")`)共享 URL 前缀。Spring 会按方法级路径解析,所以 `/business/addQuotationsheet` 和 `/business/getQuoquotationProgress` 落在 `QuoquotationController`,其他 `/business/*` 端点落在 `BusinessBaseController`。当前没有方法路径冲突,因此能正常工作;但这个约定很容易踩坑:未来如果有人在 `BusinessBaseController` 里新增 `@PostMapping("/addQuotationsheet")`,就会悄悄遮蔽报价路径。 + +### 表单辅助与 SPA 扩展 controller(22) + +这些是贴近框架的端点,是通用 CRUD 路径的扩展,但不适合放进 form-master / form-slave 的固定形状里。多数会在同一个 SPA 页面里和 `/business/*` 一起被调用。 + +| 端点前缀 | Controller | 角色 | +|---|---|---| +| `/bill/*` | `BillController` | 复制单据操作(`billCopyToCheck`、`billCopyToCheckWork`):把一组 master + slave 行克隆成新单据。它不放在 `/business/*` 下,是因为 SPA 需要在一次请求里拿到新 `sId` 集合,而通用保存端点没有这种响应形状。 | +| `/change/*` | `ChangeController` | 通用字段变更重算(`changeParam`):字段值触发派生重算且需要调用存储过程时,SPA 会调用它。 | +| `/parameter/*` | `BusinessParameterController` | 每模块参数读取。 | +| `/treeclassify/*` | `BusinessTreeClassifyController` | 表单数据加载的树分组变体:`/treeclassify/getTreeClassify/{gdsconfigformmasterId}`。它位于 `/business/*` 之外,是因为响应形状是嵌套树,不是平铺行集。 | +| `/calcprocedure/*` | `CalcProcedureController` | [计算公式](../reference/builder/define-vtable.md) 功能的运行时侧:`/calc` 调用命名计算过程。 | +| `/calculationFormula/*` | `CalculationFormulaController` | 计算公式的构建侧元数据。 | +| `/calculationStd/*` | `CalculationStdController` | 标准计算查找目录。 | +| `/char/*` | `CharController` | BACK 图表配置页面的图表配置 CRUD,包装 `gdsconfigcharmaster` / `slave`。 | +| `/checkModel/*` | `CheckmodelController` | 审批模型成员读取(`getUserListByModelId/{sCheckModeId}`),供轻量级(非 Activiti)审批流使用。 | +| `/comparatorTree/*` | `ComparatorTreeController` | 可过滤层级选择器使用的比较树读取。 | +| `/excel/*` | `ExcelController` | 网格 **Excel 导出**:`/export/{gdsconfigformmasterId}`。它是打印的兄弟路径,但输出数据而不是报表版式。 | +| `/import/*` | `ImportExcelController` | Excel **导入**:先 `/checkExcel` 校验,再提交。插入前会按表单 slave 定义验证。 | +| `/filterTree/*` | `FilterTreeController` | 网格过滤用树形下拉。 | +| `/notClear/*` | `NotClearController` | 条码扫描“未清”保存路径(`doNotClearSave`、`getNotClearScanData/{sProcName}/{sId}`),特定于扫描驱动的仓库流程。 | +| `/notice/*` | `NoticeController` | 站内通知获取 / 标记已读。 | +| `/replaceField/*` | `ReplaceFieldController` | 跨行批量字段替换。 | +| `/searchgroupby/*` | `SearchgroupbyController` | 带 group-by 的保存搜索定义。 | +| `/searchupdown/*` | `SearchUpDownController` | 按搜索结果上一条 / 下一条导航,是 `/business/getBusinessDataByIndex` 的变体。 | +| `/syssearch/*` | `SyssearchController` | 保存搜索定义 CRUD。 | +| `/syssystem/*` | `SyssystemController` | 系统表读取专用的 `getBusinessDataByFormcustomId` 变体(`/getSyssystemDataByFormcustomId/{gdsconfigformmasterId}`),用于全局元数据读取时绕过租户作用域。 | +| `/sqlfile/*` | `SqlFileController` | “Mysql脚本配置”管理页面背后的 SQL 文件加载 / 保存。 | +| `/instruct/*` | `InstructController` | 直接执行 SQL 的端点(`/exesql`、`/opensql`):管理侧查询控制台。 | + +### 用户与权限管理(4) + +同一关注点下有多个重叠 controller。`New` 后缀,以及 `sftlogininfo`、`userinfo`、`gdslogininfo`(在 `systemweb/` 中)同时存在,说明这里有一次迁移中的重构:旧路径和新路径会并存,直到调用方迁移完成。 + +| 端点前缀 | Controller | 角色 | +|---|---|---| +| `/userinfo/*` | `UserInfoController` | 当前用户资料与 session 信息。 | +| `/sftlogininfo/*` | `SftlogininfoController` | 用户账号 CRUD(两条路径中较新的一个)。 | +| `/sysjurisdiction/*` | `SysjurisdictionController` | 用户组 / 用户权限读取(`getGroupData`、`getUserData`)。 | +| `/sysjurisdictionNew/*` | `SysjurisdictionNewController` | 较新的并行路径(`getGroupDataNew`、`getGroupUserIdNew/{sUserId}`)。关注点相同,形状不同。 | + +### 生产 / MES(7) + +这些是行业层流程;它们的状态机、多表连接或硬件集成无法只靠 `gdsconfigformmaster` SQL 表达。这是框架中硬编码业务逻辑最集中的一组。 + +| 端点前缀 | Controller | 角色 | +|---|---|---| +| `/sysworkorder/*` | `WorkOrderController` | 带副作用的工单 CRUD(`/add`、`/update/{sId}`):印刷行业工单会跨很多 slave 和存储过程调用,通用保存端点无法原子串起这些操作。 | +| `/workOrderFlow/*` | `WorkOrderFlowController` | 工单工艺路线 / 流程读取(`getWorkOrderFlow`、`getSplitWorkOrderData/{sId}`)。 | +| `/workOrderPlan/*` | `WorkOrderPlanController` | 连接工单与计划行的生产计划读取(`getControlProcess/{sProductionPlanId}`、`getProductionPlanInfo`)。 | +| `/splitWorkOrder/*` | `SplitWorkOrderController` | 把一个主工单拆成多个子工单(`getSplitWorkOrderData`)。 | +| `/productionPlan/*` | `ProductionPlanController` | 计划树读取(`getProductionPlanTree`)。 | +| `/process/*` | `ProcessController` | 生产工序目录读取。 | +| `/oee/*` | `OeeController` | OEE(Overall Equipment Effectiveness,设备综合效率):条码扫描和 MES 状态回调(`updateBarcode/{sBarCodeId}/{sBarCode}`、`doSysMesMsg/{sStatus}/{sMachineId}`)。 | + +### 销售、库存、财务、采购、人事(9) + +| 端点前缀 | Controller | 角色 | +|---|---|---| +| `/salesorder/*` | `SalesOrderController` | 超出通用 CRUD 的销售订单专用逻辑。 | +| `/business/addQuotationsheet`、`/business/getQuoquotationProgress` | `QuoquotationController` | 报价单创建(长运行,因此有进度端点)。**注意:**它和 `BusinessBaseController` 共享 `/business/*` 前缀,见上面的命名空间重叠说明。 | +| `/eleMaterialsStock/*` | `EleMaterialsStockController` | 原材料库存读取(`getEleMaterialsStock`、`getEleMaterialsStoreCurrQty`)。 | +| `/eleProductStock/*` | `EleProductStockController` | 成品库存读取。 | +| `/costCenter/*` | `CostCenterController` | 成本中心数据与凭证导入(`getCostCenterData`、`getCosvoucherImportData`)。 | +| `/sysAccountPeriod/*` | `SysAccountPeriodController` | 会计期间启用 / 关闭逻辑。 | +| `/erpOrderProcurement/*` | `ErpOrderProcurementController` | 采购订单专用逻辑。 | +| `/sisproductclassify/*` | `SisproductclassifyController` | 产品分类树。 | +| `/eleteamemployee/*` | `EleteamemployeeController` | 车间流程中的班组 / 员工分配。 | + +### 集成与硬件(5) + +| 端点前缀 | Controller | 角色 | +|---|---|---| +| `/file/*` | `FileController` | 文件上传,包括微信移动端变体 `mobileuploadwechat`。 | +| `/plc/*` | `PlcController` | PLC 桥接入口(`getplcMachine/{iOrder}/{sParentId}`),见[切片 6](../slices/06-hardware.md)。 | +| `/mobilephone/*` | `MobliePhoneController` | 移动 App 端点。(类名中的 `Moblie` 拼写错误来自源码;URL 是 `/mobilephone`。) | +| `/sysWebsocket/*` | `SysWebSocketController` | 推送通知的 WebSocket 建立 / 关闭。 | +| `/wechat/*` | `WechatController` | 微信集成(站内二维码、OAuth 回调)。 | + +### 按[首页范围说明](../index.md)排除(5) + +为完整性列出;它们不属于本框架 Wiki 的范围内接口面,但确实存在于 WAR 中。 + +| 端点前缀 | Controller | 状态 | +|---|---|---| +| `/ai/*` | `AiController` | AI assistant。范围外(见 index.md)。 | +| `/robot/*` | `ChatGptController` | ChatGPT 集成。范围外(见 index.md)。 | +| `/test/*` | `TestController` | 开发脚手架(`/file`、`/getDinkToken`)。 | +| (根路径) | `TestProcessController` | 开发脚手架;没有类级 `@RequestMapping`。 | +| (已注释) | `XsController` | 死文件:`@RestController` 和 `@RequestMapping` 都被注释掉;类存在,但不会注册任何端点。 | + +### 把这些 controller 当作诊断线索 + +扫完整个列表后,三个模式很明显: + +1. **长运行或多步骤事务**(`QuoquotationController.getQuoquotationProgress`、`BillController.billCopyToCheck`、`SplitWorkOrderController`):通用保存是一次性请求;任何需要进度端点,或“克隆后跳转”语义的流程,都需要自己的 controller。 +2. **行业专用状态机**(`OeeController`、工单家族、`SysAccountPeriodController`):当“下一个合法状态”不能从单个列推出时,存储过程分发路径就不够用了,controller 会在 Java 中串起编排逻辑。 +3. **硬件或外部系统**(`PlcController`、`WechatController`、`SysWebSocketController`、`FileController`):凡是不只是“MySQL + HTTP”的能力,都需要 Java 胶水;元数据无法描述入站 websocket 或串口握手。 + +这些 controller 的职责,正好就是框架通用运行时没有覆盖的部分。 + ## 这个 API 不是什么 - **不是稳定接口。** 端点形状会随框架变化。 diff --git a/zh/docs/api-reference/messaging.md b/zh/docs/api-reference/messaging.md index 39e05b5..977c61c 100644 --- a/zh/docs/api-reference/messaging.md +++ b/zh/docs/api-reference/messaging.md @@ -4,25 +4,60 @@ xly 中不是所有集成都适合同步 HTTP 调用。框架运行两个消息 | Broker | 用途 | Producer | Consumer | |---|---|---|---| -| **ActiveMQ / JMS** | 缓存失效、集群内扇出事件。元数据变更链路([缓存失效](../reference/maintainer/cache-invalidation.md))依赖它。 | `xlyErpJmsProductor` | `xlyErpJmsConsumer` | +| **ActiveMQ / JMS** | 集群内扇出事件:基础数据合并作业(把每租户行合并成扁平查询表)以及单据更新 / 单据删除通知。**尽管其中一个队列有历史命名,这条通道不用于 Redis 缓存失效**;真实缓存清理路径见[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)。 | `xlyErpJmsProductor` | `xlyErpJmsConsumer` | | **RocketMQ** | 其他不适合 ActiveMQ 假设的集成流程。 | `RocketMQServiceImpl`(位于 `xlyBusinessService`) | 因服务而异。 | 本页只是指针而不是深入说明;准确的队列名和载荷在 `xlyErpJmsConsumer/src/main/java/com/xly/xlyerpjmsconsumer/` 下按 consumer thread 级别记录。 -## ActiveMQ / JMS:缓存失效通道 +## ActiveMQ / JMS:基础数据合并 + 扇出通道 -Producer 侧队列声明位于 `xlyErpJmsProductor/src/main/java/com/xly/xlyerpjmsproductor/config/P2pQueue.java`。当前框架使用的主要 destination(完整列表请读该文件): +Producer 侧队列声明位于 `xlyErpJmsProductor/src/main/java/com/xly/xlyerpjmsproductor/config/P2pQueue.java`。完整集合是 **24 个 destination**,可按意图分组: + +### 模块 / 控制(2) + +| Constant | 用途 | +|---|---| +| `ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` | “模块元数据已变更”,`ConsumerChangeGdsModuleThread` 会运行存储过程 `PRO_ERPMERGEBASEGDSMODULE`,把每租户 `gdsmodule` 行合并到扁平基础查询表中。**尽管名字如此,它不清 Redis 缓存**;Redis 缓存清理由 BACK 保存时通过 `@CacheEvict` 同步完成。见[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)。 | +| `ERP_JMS_ACTIVEMQ_CHANGE_WORK_ORDER_CONTROL` | 工单控制状态变更(状态 / 汇总标志),用于下游重算扇出。 | + +### 单据操作(6) | Constant | 用途 | |---|---| -| `ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` | “模块元数据已变更”,触发 `ConsumerChangeGdsModuleThread` 清理各节点相关 Redis 缓存。见[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)。 | -| `ERP_JMS_ACTIVEMQ_CHANGE_ELE_CUSTOMER` | 客户主数据变更扇出。 | -| `ERP_JMS_ACTIVEMQ_CHANGE_ELE_EMPLOYEE` | 员工主数据变更扇出。 | -| `ERP_JMS_ACTIVEMQ_CHANGE_ELE_MACHINE` | 车间机台主数据变更扇出。 | -| `ERP_JMS_ACTIVEMQ_UPD_SALE_ORDER`、`ERP_JMS_ACTIVEMQ_UPD_WORK_ORDER`、`ERP_JMS_ACTIVEMQ_UPD_PRODUCTION_REPORT` | “单据已更新”通知,由后台 worker 消费(合计重算、下游失效等)。 | -| `ERP_JMS_ACTIVEMQ_DEL_SALE_ORDER`、`ERP_JMS_ACTIVEMQ_DEL_WORK_ORDER`、`ERP_JMS_ACTIVEMQ_DEL_PRODUCTION_REPORT` | 单据删除通知。 | +| `ERP_JMS_ACTIVEMQ_UPD_SALE_ORDER` / `_UPD_WORK_ORDER` / `_UPD_PRODUCTION_REPORT` | “单据已更新”通知,由后台消费者处理(合计重算、下游失效等)。 | +| `ERP_JMS_ACTIVEMQ_DEL_SALE_ORDER` / `_DEL_WORK_ORDER` / `_DEL_PRODUCTION_REPORT` | 单据删除通知。 | + +### 主数据变更扇出(7):`CHANGE_ELE_*` + +| Constant | 用途 | +|---|---| +| `_CHANGE_ELE_CUSTOMER` | 客户主数据变更。 | +| `_CHANGE_ELE_EMPLOYEE` | 员工主数据变更。 | +| `_CHANGE_ELE_MACHINE` | 车间机台主数据变更。 | +| `_CHANGE_ELE_MATERIALS` | 材料主数据变更。 | +| `_CHANGE_ELE_PRODUCT` | 产品主数据变更。 | +| `_CHANGE_ELE_PROCESS` | 工序主数据变更。 | +| `_CHANGE_ELE_TEAM` | 班组主数据变更。 | + +### 系统信息 / 字典表变更扇出(9):`CHANGE_SIS_*` + +| Constant | 用途 | +|---|---| +| `_CHANGE_SIS_CUSTOMER_CLASSIFY` | 客户分类变更。 | +| `_CHANGE_SIS_DELIVER` | 送货方式变更。 | +| `_CHANGE_SIS_FORMULA` | 计算公式变更。 | +| `_CHANGE_SIS_PAYMENT` | 付款方式变更。 | +| `_CHANGE_SIS_PROCESS_CLASSIFY` | 工序分类变更。 | +| `_CHANGE_SIS_PRODUCT_CLASSIFY` | 产品分类变更。 | +| `_CHANGE_SIS_SALES_MAN` | 销售人员变更。 | +| `_CHANGE_SIS_TAX` | 税率变更。 | +| `_CHANGE_SIS_WORK_CENTER` | 工作中心变更。 | + +(第一张表之后省略 constant 前缀为 `_…`;完整字面量是 `ERP_JMS_ACTIVEMQ_…`。) + +### Listener 侧 -每个 destination 在 `xlyErpJmsConsumer/.../thread/` 下都有对应的 `Consumer*Thread` 类异步处理消息。 +`xlyErpJmsConsumer/.../consumer/Consumer.java` 是承载**全部 24 个 `@JmsListener` 方法**的单一类,每个 destination 一个方法。每个方法把载荷分发给 `xlyErpJmsConsumer/.../thread/` 下对应的 `Consumer*Thread` 类,由它异步执行领域工作(通常是调用一个 `PRO_ERPMERGEBASE*` 存储过程,把每租户行合并成扁平基础查询表)。这里是一个 listener 类有 24 个方法,**不是** 24 个 listener 类。consumer thread 中没有调用 `@CacheEvict` 或 `cleanRedis*`;Redis 缓存失效在 BACK 保存时同步完成,见 [cache-invalidation.md](../reference/maintainer/cache-invalidation.md)。 ## RocketMQ:其他流程 @@ -30,9 +65,9 @@ Producer 侧队列声明位于 `xlyErpJmsProductor/src/main/java/com/xly/xlyerpj ## 手动触发缓存失效 -如果元数据变更是通过原始 SQL 完成的(没有 JMS 事件),各节点缓存不会自动清理。支持的覆盖路径是 `xlyBusinessService/.../service/impl/` 中的 `BusinessCleanRedisDataImpl`,它可以直接发布失效事件。更完整的排查路径见[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)。 +如果元数据变更是通过原始 SQL 完成的(没有 BACK 保存路径),各节点缓存不会自动清理。支持的覆盖路径是 `xlyBusinessService/.../service/impl/` 中的 `BusinessCleanRedisDataImpl`,它可以直接调用清理方法。更完整的排查路径见[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)。 ## 这个机制不是什么 - **不是公开集成通道。** 外部集成方不应向这些 broker 发布消息,也不应订阅它们。它们是集群内部的扇出机制。 -- **不是失效缓存的唯一方式。** `xlyEntry` 的 HTTP 写路径已经会在需要时发布 JMS 事件;手动触发只用于边界情况。 +- **不是缓存失效通道。** `xlyEntry` 的 HTTP 写路径会在需要时同步触发 `@CacheEvict`;JMS 队列负责基础数据合并和异步工作项。 diff --git a/zh/docs/api-reference/notifications.md b/zh/docs/api-reference/notifications.md index a0a34af..83608c6 100644 --- a/zh/docs/api-reference/notifications.md +++ b/zh/docs/api-reference/notifications.md @@ -1,15 +1,16 @@ # 通知(xlyMsg) -出站通知(钉钉和微信)通过 `xlyMsg` 模块完成。它**不是**调用方直接访问的 HTTP 接口面,而是范围内服务在业务事件需要向聊天平台推送消息时调用的内部 SDK。 +出站通知(钉钉、微信和邮件)通过 `xlyMsg` 模块完成。它**不是**调用方直接访问的 HTTP 接口面,而是范围内服务在业务事件需要向聊天平台或邮箱推送消息时调用的内部 SDK。 ## 模块内容 `xlyMsg/src/main/java/com/xly/`: -| Package | 角色 | +| 包 | 角色 | |---|---| | `dingtalk/service/DingTalkService` + `dingtalk/util/SendDingTalkUtil`、`DingTalkMsgContentUtil`、`LocalCacheClient` | 钉钉企业消息分发。封装 `com.aliyun:dingtalk:2.1.14` 和 `com.aliyun:alibaba-dingtalk-service-sdk:2.0.0`。 | | `wechat/service/WechatService` + `wechat/util/SendWxUtil`、`Wx_SignatureUtil`、`JedisMsgUtil`、`MsgContentUtil`、`Xml2JsonUtil` | 微信工作平台分发,包含签名和发送,以及 Redis 支撑的 access-token 缓存。 | +| `emial/service/SendEmailService` + impl | 邮件分发(包名拼成了 `emial`)。`xlyFlow` 的 `QuartzTask` 会用它发送定时任务邮件;`xlyEntry` 也有自己的 `com.xly.web.email.SendEmailService`,供 `ScheduledTasks` 驱动的邮件路径使用。两个接口名相同、实现并行保留,属于历史原因。 | | `notice/service/NoticeService` | 与供应商无关的通知抽象;把“通知用户 X 某事件 Y”的逻辑路由到正确后端。 | `xlyMsg/build.gradle` 中唯一的框架依赖是 `xlyPersist`。该模块作为库被消费,不会作为自己的服务部署。 @@ -18,12 +19,12 @@ 这些调用让 `xlyMsg` 成为框架相关内容,而不是 plat 层内容: -| Caller | 作用 | +| 调用方 | 作用 | |---|---| | `xlyEntry/.../web/businessweb/TestController.java` | 诊断端点,用于发送测试钉钉消息。 | -| `xlyBusinessService/.../thread/UpdateDingTalkThread.java` | 单据变更后推送钉钉更新的异步 worker。 | +| `xlyBusinessService/.../thread/UpdateDingTalkThread.java` | 单据变更后推送钉钉更新的异步线程。 | | `xlyBusinessService/.../service/impl/CheckExamineFlowServiceImpl.java` | 工作流审批通知;Activiti 任务被重新分配或完成时,待办人会收到聊天消息。 | -| `xlyBusinessService/.../service/impl/GenericProcedureCallServiceImpl.java` | 通用存储过程 hook:任何选择接入的 `gdsmodule` 存储过程都可以通过这条路径发布通知。 | +| `xlyBusinessService/.../service/impl/GenericProcedureCallServiceImpl.java` | 通用存储过程钩子:任何选择接入的 `gdsmodule` 存储过程都可以通过这条路径发布通知。 | | `xlyInterface/.../util/DingTalkUtil.java` + `scheduler/ScheduledTasks.java`、`ErpJobRunStatus.java` | 集成侧的定时任务心跳 / 失败告警。 | ## 配置 diff --git a/zh/docs/api-reference/webhooks.md b/zh/docs/api-reference/webhooks.md index 8b8df72..00d2b26 100644 --- a/zh/docs/api-reference/webhooks.md +++ b/zh/docs/api-reference/webhooks.md @@ -10,30 +10,32 @@ http:///xlyInterface/swagger-ui.html 等价的 JSON 描述位于 `http:///xlyInterface/v2/api-docs`。 +> **注意:** 项目拉取了 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。 + ## 数据驱动接收器:`/interfaceDefine/*` 它和[外部 API 的 `/api/invoke`](external.md) 模式对应,但用于入站调用: -| Endpoint | Method | 用途 | +| 端点 | 方法 | 用途 | |---|---|---| | `/interfaceDefine/invoke/{interfaceInvoke}` | POST | 将入站载荷分发给 `{interfaceInvoke}` 命名的处理器。 | -| `/interfaceDefine/callthirdparty/{interfaceInvoke}` | POST | 转发到已配置的出站端点。(对应 `xlyApi` 上同样存在的 `/interfaceDefine/callthirdparty/...` 端点;这里的入站侧和数据驱动入站分发器配套。) | +| `/interfaceDefine/callthirdparty/{interfaceInvoke}` | POST | 转发到已配置的出站端点。同一个 URL 在 `xlyApi` 的 `InterfaceController` 上也存在;这种重复是有意的,两个服务共享数据驱动分发器模式。 | 处理器:`xlyInterface/src/main/java/com/xly/web/InterfaceController.java`。 -`{interfaceInvoke}` path variable 会查找一条元数据行,该行声明要运行的 SQL 或存储过程、参数映射和响应形状。它和 xly 其他部分一样遵循数据驱动思路:新增入站端点靠插入数据行,而不是写 Java。 +`{interfaceInvoke}` 路径变量会查找一条元数据行,该行声明要运行的 SQL 或存储过程、参数映射和响应形状。它和 xly 其他部分一样遵循数据驱动思路:新增入站端点靠插入数据行,而不是写 Java。 ## 硬编码的供应商接收器 少量接收器不会经过 `/interfaceDefine`,因为它们必须匹配合作方固定的 URL 规格。 -| Endpoint | Method | 用途 | +| 端点 | 方法 | 用途 | |---|---|---| | `/Push` | POST | 供应商(微信 / 类似系统)推送接收器。 | | `/Pull` | POST | 供应商拉取模式接收器。 | | `/getKey/{key}` | GET | 按伙伴命名的 `key` 获取公钥。 | | `/getKeyTest` | GET | `/getKey` 的测试模式变体。 | -| `/send/sendQw` | POST | 企业微信出站消息。 | +| `/send/sendQw` | POST | 企业微信出站消息。**当前分支是空实现**:`SendQwController` 中方法体只是 `return "ok";`;已搭好 token 获取脚手架,但未完成。 | 处理器:`xlyInterface/src/main/java/com/xly/web/WX_VendorWeb.java` 和 `xlyInterface/src/main/java/com/xly/wechat/test/SendQwController.java`。 diff --git a/zh/docs/concepts/api-surface.md b/zh/docs/concepts/api-surface.md index a56ac98..d472eda 100644 --- a/zh/docs/concepts/api-surface.md +++ b/zh/docs/concepts/api-surface.md @@ -10,17 +10,26 @@ xly 不是只有一个 API,而是有**三层**,分别由三个独立的 Spri 每个服务都会构建成自己的 WAR,并在自己的 JVM 中运行。它们不共享进程内状态;它们共享的是**数据库**。正是这个共享数据库让服务拆分成立:内部 API 写入的数据会自动被外部 API 读到,因为两者都连接同一个 schema。 -## 为什么是三层,而不是一层 +## 为什么是三层,以及拆分的代价 -每一层回答的问题不同,合在一起会牺牲清晰度: +每一层最初都是为了解答不同的问题: - **内部层**很大(对所有元数据驱动模块做通用 CRUD)、易变(随框架变化)、且有意保持弱类型(SPA 决定要什么,服务端照元数据执行)。 -- **外部层**是收敛后的接口(只暴露允许集成方使用的端点),按 `sApiCode` 做版本化,并用 bearer token 认证。它能跨框架变化保持稳定,正是因为它小而明确。 +- **外部层**是收敛后的接口(只暴露允许集成方使用的端点),按 `sApiCode` 做版本化,并用 bearer token 认证。 - **入站 webhook 层**接收来自第三方系统的不可信 body,并路由到 xly 处理器。Swagger UI 放在这里,因为这个受众最需要交互式文档。 +这个拆分有真实成本,Wiki 不应该略过: + +- **需要部署、监控、锁定版本的 WAR 有三个。** 一次发布必须协调 `xlyEntry`、`xlyApi`、`xlyInterface` 的构建。版本不匹配时(例如 `xlyEntry` 引入了 schema 变化,但 `xlyApi` 还没跟上),问题通常会静默存在,直到某条调用路径撞上它。 +- **存在重复代码。** `RequestAddParamUtil` 在 `xlyPersist`(供 `xlyEntry` 使用)和 `xlyApi` 中各有一份,几乎是 56 行 / 57 行的拷贝。`InterfaceController` 在 `xlyApi` 和 `xlyInterface` 中也都存在,并且有重叠的 `/interfaceDefine/callthirdparty/*` 端点。让两边保持同步依赖运维纪律,不是编译期保证。 +- **没有共享 session。** 在 BACK 登录的用户,在 `xlyApi` 中没有 session;外部调用方需要单独获取 bearer token。这对外部集成是正确的,但也意味着内部跨 WAR 调用如果发生,必须走公开 token 流程。 +- **三个 context path 就意味着三条反向代理规则。** `BACK=:8597`、`FROUNT=:8598` 到具体 WAR 的映射在 nginx 配置中,不在本仓库里。代理配置错误是代码库本身捕捉不到的常见故障模式。 + +这些层当然也可以做成一个部署物,只在内部用包边界隔离;Spring Boot 支持这种做法。那样的好处是:一次构建、一套依赖、一套 session 逻辑,也不会有重复工具类。代价是:各层更难独立扩容,也更难只限制外部调用方而不影响 SPA。xly 选择了部署期隔离;Wiki 的职责是把这个选择牺牲了什么写清楚。 + ## 每一层在运行时长什么样 -- **内部层** — 见[四表读取](request-lifecycle.md)。一个端点(`/business/getModelBysId`)返回完整表单布局;另一个端点(`/business/addUpdateDelBusinessData`)写入元数据命名的任意表中的任意行。端点少,形状通用。 +- **内部层** — 见[五键读取](../reference/maintainer/runtime.md#five-key-read)。一个端点(`/business/getModelBysId`)返回完整表单布局;另一个端点(`/business/addUpdateDelBusinessData`)写入元数据命名的任意表中的任意行。端点少,形状通用。 - **外部层** — 大多数调用走 `/api/invoke/{sApiCode}`。`sApiCode` 是 `sysapi` 元数据表中的一行,定义 SQL 模板、参数、认证要求和目标。新的外部 API 是**注册成数据**,不是写成代码;这和框架对自身表单采用的数据驱动基本论点一致。 - **入站 webhook 层** — `/interfaceDefine/invoke/{interfaceInvoke}` 接收载荷,查找元数据中的匹配处理器,运行配置好的 SQL 或存储过程。此外还有少量为特定伙伴准备的硬编码接收器(`/Push`、`/Pull`、`/send/sendQw`)。 diff --git a/zh/docs/concepts/customization-channels.md b/zh/docs/concepts/customization-channels.md index 2ff9e73..50bbe45 100644 --- a/zh/docs/concepts/customization-channels.md +++ b/zh/docs/concepts/customization-channels.md @@ -15,7 +15,7 @@ xly 客户通过**两条不同路径**定制系统。理解区别很关键:它 这些修改是**数据**。它们跟随客户数据库。它们在**后台**界面中可见,PM 可以审计。框架运行时在每次请求中读取它们(带缓存)。Java 代码不变;应用行为由这些行决定。 -这是默认路径。**90% 以上客户定制都应该放在这里。** +这是架构希望客户优先使用的路径。至于真实比例是不是 90/10 偏向通道 1,代码库里没有统计;经验信号是 `script/客户/` 下已经有 18 个客户目录,说明有相当一部分客户需要通道 1 表达不了的东西。因此,“90% 以上应该在这里”更像目标,而不是实测事实。 ## 通道 2 — 每客户 SQL 覆盖 @@ -40,7 +40,7 @@ xly 客户通过**两条不同路径**定制系统。理解区别很关键:它 - 客户需要框架 Add/Update/Calc 过程无法表达的过程逻辑。 - 需要替换存储过程主体,而不是只在周围注入 SQL 片段。 -- 维护人员审查客户运行时时,需要能在源码控制的 SQL 文件中看到差异。 +- 运行时差异应放在源码控制的 `.sql` 文件中(`script/客户//` 下),这样维护人员审查客户运行时时能一眼看到每客户变更,而不是只有连接到实时 DB 后才能发现。 通道 2 *几乎总是最后手段*。只有确认通道 1 做不到时才使用。 diff --git a/zh/docs/concepts/customization-layers.md b/zh/docs/concepts/customization-layers.md index 2345d28..d94f877 100644 --- a/zh/docs/concepts/customization-layers.md +++ b/zh/docs/concepts/customization-layers.md @@ -6,21 +6,33 @@ ## 各层 -```text -gdsconfigformmaster ← 系统默认(表单) - ↓ 被覆盖 -gdsconfigformpersonalize ← 每租户整表单覆盖 - (替换 sSqlStr/sWhere/sOrder) - ↓ 然后是基础从表 -gdsconfigformslave ← 系统默认字段 - ↓ 被覆盖 / 扩展 -gdsconfigformcustomslave ← 每租户字段(新增、隐藏、覆盖) - ↓ 可选地继续微调 -gdsconfigformuserslave ← 每用户视图偏好 - (列顺序、隐藏列) +```mermaid +flowchart TB + classDef sys fill:#e8f0fe,stroke:#4285f4 + classDef tenant fill:#fef7e0,stroke:#fbbc04 + classDef user fill:#f3e8fd,stroke:#a142f4 + + M["gdsconfigformmaster
系统默认:表单
(sSqlStr · sWhere · sOrder)"]:::sys + P["gdsconfigformpersonalize
每租户整表单覆盖
(替换 sSqlStr / sWhere / sOrder)"]:::tenant + S["gdsconfigformslave
系统默认字段"]:::sys + CS["gdsconfigformcustomslave
每租户字段
(按 sName 新增 · 隐藏 · 覆盖)"]:::tenant + US["gdsconfigformuserslave
每用户视图微调
(列顺序 · 隐藏列)"]:::user + + OUT["合并后的表单
返回给 SPA"] + + M --> P + P --> S + S --> CS + CS --> US + US --> OUT + + M -. "总是加载" .-> OUT + P -. "租户有覆盖时加载" .-> OUT + CS -. "租户有覆盖时加载" .-> OUT + US -. "用户有偏好时加载" .-> OUT ``` -每一层都通过 `sParentId` 连接到上一层。没有任何连接由 FK 强制;见[无 FK 现实](semantic-fk.md)。 +自上而下读这条链:**系统 → 租户 → 用户**。每一层都通过 `sParentId` 连接到上一层。没有任何连接由 FK 强制;见[无物理外键、语义外键的现实](semantic-fk.md)。 ## 每层回答的问题 @@ -34,7 +46,7 @@ gdsconfigformuserslave ← 每用户视图偏好 ## 合并如何发生 -框架按顺序读取每层,并按 `sName`(字段名)合并。自定义从表行与基础从表拥有相同 `sName` 时:覆盖。新的 `sName`:追加。没有对应自定义行的基础从表:原样透传。合并发生在 `BusinessBaseServiceImpl.getModelBysId`(第 181 行)及其调用的 helper 中:`BusinessGdsconfigformsServiceImpl.getFormSlaveData` + `getFormCustomSlaveData`。 +框架按顺序读取每层,并按 `sName`(字段名)合并。自定义从表行与基础从表拥有相同 `sName` 时:覆盖。新的 `sName`:追加。没有对应自定义行的基础从表:原样透传。入口是 `BusinessBaseServiceImpl.getModelBysId`(第 181 行),它会调用 `BaseServiceImpl.getModelConfigByModleId`(第 55 行);真正的 slave + customslave 合并发生在 `BusinessGdsconfigformsServiceImpl.getGdsconfigformslaveShow`(第 392 行),组合 `getFormSlaveData`(第 87 行)和 `getFormCustomSlaveData`(第 121 行),随后可选叠加 `getUserFormSlaveData`(第 156 行)。 两个数据库**视图**通过连接 form-master 和相关 slave 表来支持合并: diff --git a/zh/docs/concepts/index.md b/zh/docs/concepts/index.md index 2d96189..5be8d5c 100644 --- a/zh/docs/concepts/index.md +++ b/zh/docs/concepts/index.md @@ -24,7 +24,7 @@ flowchart TB XMSG[/"xlyMsg
库"/] end - DB[("MySQL
xlyweberp")] + DB[("MySQL
xlyweberp_*")] REDIS[(Redis)] AMQ([ActiveMQ]) XEJMSC[xlyErpJmsConsumer] @@ -42,10 +42,13 @@ flowchart TB XFLOW --> DB XPLC --> DB - XENTRY <--> REDIS - XENTRY -- "元数据变更" --> AMQ + XENTRY -- "保存时 @CacheEvict
同步执行" --> REDIS + XENTRY <-- "缓存读取
+ Shiro session" --> REDIS + XAPI <--> REDIS + + XENTRY -- "领域事件
(不是缓存失效)" --> AMQ AMQ --> XEJMSC - XEJMSC --> REDIS + XEJMSC -- "PRO_ERPMERGEBASE*
基础数据合并" --> DB XENTRY -. 使用 .-> XMSG XIF -. 使用 .-> XMSG @@ -55,6 +58,10 @@ flowchart TB 虚线簇(`xlyPlat*` + MongoDB)是 B2B 印刷平台层。它存在于构建中,但在本 Wiki 中[不属于覆盖范围](../index.md)。 +注意运行时到 Redis / ActiveMQ 有两条不同路径:**`@CacheEvict` 在保存流程中同步执行,直接清理共享 Redis 存储**(跨节点一致性依赖共享存储)。**JMS 路径是另一条基础数据合并通道**,不是缓存失效;`ConsumerChangeGdsModuleThread` 会运行 `PRO_ERPMERGEBASEGDSMODULE` 等过程。这两条路径在[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)中有完整说明。 + +每个框背后的类库清单见[技术栈](../reference/maintainer/tech-stack.md)。 + ## 概念页 这些页面刻意保持简短:每页解释一个概念,并链接到实际使用该概念的[垂直切片](../slices/index.md)。概念页应该短;如果一页长到超过一屏,通常说明它应该变成一个切片。 @@ -65,6 +72,6 @@ flowchart TB - [无物理外键、语义外键的现实](semantic-fk.md) — 关系实际如何工作。 - [两条定制通道](customization-channels.md) — 元数据编辑 vs. SQL 脚本。 - [定制层级](customization-layers.md) — 通道 1 内,基础 / 租户 / 用户覆盖如何合并。 -- [多租户与产品版本](multi-tenancy.md) — 三条作用域轴(`sBrandsId`、`sSubsidiaryId`、`sVersionFlowId`)。 +- [多租户与产品版本](multi-tenancy.md) — 行作用域(`sBrandsId`、`sSubsidiaryId`)加上许可证控制的模块发现。 - [元数据驱动的请求生命周期](request-lifecycle.md) — 后续会反复回到这张图。 - [三层 API](api-surface.md) — 内部(`xlyEntry`)、外部(`xlyApi`)、入站 webhook(`xlyInterface`)。 diff --git a/zh/docs/concepts/master-slave.md b/zh/docs/concepts/master-slave.md index cca22ca..7e5313b 100644 --- a/zh/docs/concepts/master-slave.md +++ b/zh/docs/concepts/master-slave.md @@ -1,5 +1,9 @@ # 主从单据模式 +> **这个代码库里有两个互不相关的“主 / 从”概念。** +> 本页讨论的是**单据行**模式:报价、销售订单、生产工单等业务单据由 1 行表头 + N 行明细组成。 +> **DataSource** 层的 master / slave(`xlyApi` 和 `xlyInterface` 中通过 `MasterDataSourceConfig` / `SlaveDataSourceConfig` 做写库 / 读库连接路由,并配套这些服务内的 `mastermapper/MasterBaseMapper.xml` / `slavemapper/SlaveBaseMapper.xml`)是另一个概念,见[技术栈](../reference/maintainer/tech-stack.md#persistence)中 HikariCP / 数据源相关说明,也会在运行时页面间接涉及。两者只是名字重叠。 + xly 中几乎所有业务单据,例如报价单、销售订单、工单、付款凭证,都是以**一行表头 + N 行明细**存储。xly 对此的术语是 **master / slave**。master 保存单据身份和汇总;每个 slave 行是一条明细、一段阶段、一项材料、一个产品或一笔费用。 这个模式无处不在: @@ -29,9 +33,6 @@ xly 中几乎所有业务单据,例如报价单、销售订单、工单、付 ## “Slave” 命名说明 -这个词在英文中有额外含义,而中文“主表 / 从表”没有。Wiki 保留 “slave”,原因是: - -1. 改名会破坏代码库、schema 和自动目录中的全部交叉引用(14k+ 标识符)。 -2. 把每次出现都映射成 “detail” 或 “child” 会损害可搜索性,并使 Wiki 文本偏离开发者实际 grep 到的内容。 +这个词在英文中带有中文“主表 / 从表”没有的额外含义。Wiki 保留 “slave” 原词,是因为代码库、schema 和自动目录中有 14k+ 标识符逐字使用它;翻译成其他词会让文档偏离开发者实际 grep 到的内容。 -未来 xly 版本可能改名为 “detail” / “header”;在此之前,Wiki 使用代码库中的原词,并在此处一次性说明。 +保留这个词也有代价。最初命名本身并不好:`主表 / 从表` 完全可以翻成 `master / detail` 或 `header / line`,既符合英文开发习惯,也更贴近关系语义。继续保留 “slave” 的成本,会由每个需要阅读或输入这个词的英文维护者承担,也会由未来任何一次全 schema 改名承担。Wiki 在这里说明一次,并不能消除成本,只是把它明确写出来。 diff --git a/zh/docs/concepts/modules-forms-vtables.md b/zh/docs/concepts/modules-forms-vtables.md index e239b79..42dbe3c 100644 --- a/zh/docs/concepts/modules-forms-vtables.md +++ b/zh/docs/concepts/modules-forms-vtables.md @@ -54,3 +54,35 @@ gdsmodule.sFormId → (大多为空,历史字段) ## 三个名词,一个引擎 运行时(`BusinessBaseController` 和 `BusinessBaseServiceImpl`,见[切片 1](../slices/01-hello-world.md))知道如何渲染任意模块 / 表单 / 虚拟表组合。不存在每模块专用 Java 代码。PM 创建新模块是在创建新行,不是在创建新代码路径。 + +## 业务数据表前缀 + +本 Wiki 把业务模块当作示例,而不是章节主体;但 schema 的命名有规律。维护人员可以通过三字母前缀判断业务数据表所属领域: + +| 前缀 | 领域 | 示例表(实时数量) | +|---|---|---| +| `gds` | 框架元数据(模块、表单、字段、权限、参数、图表) | `gdsmodule`、`gdsconfigformmaster`、`gdsconfigformslave`、`gdsjurisdiction`、`gdsroute`、`gdsformconst`、`gdsparameter`、`gdsconfigcharmaster`/`slave`(BACK 图表配置使用的图表定义)等 | +| `sys` | 框架系统层(编号、授权、报表、搜索、账单设置),区别于 `gds*` 定义层 | `sysjurisdiction`、`sysbillnosettings`、`sysreport`、`syssearch`、`sysapi`、`syssystemsettings` 等(68 张表) | +| `sis` | 支撑下拉项的共享字典 / 分类表 | `sisbank`、`siscolor`、`sisversionflow`、`sisjurisdictionclassify` 等(80 张表) | +| `sft` | 登录 session / 用户组权限连接表 | `sftlogininfo`、`sftlogininfojurisdictiongroup` 等(8 张表) | +| `ele` | 主数据(element):客户、员工、机台、材料、产品、工序、半成品、成本框架 | `elecustomer*`、`eleemployee*`、`elemachine*`、`elematerials*`、`eleproduct*` 等(89 张表) | +| `mft` | 制造:工单、生产计划、生产报工 | `mftworkordermaster`、`mftproductionplan*`、`mftproductionreport*` 等(82 张表) | +| `sal` | 销售 | `salsalesordermaster`、`salsalesorderslave`、`salsalesorderprocess` 等(67 张表) | +| `quo` | 报价 | `quoquotationmaster`、`quoquotationslave`、`quoquotationcalc_tmp` 等(23 张表) | +| `acc` | 会计 / 成本 | `accordercostanalysis`、`accordercostanalysisoperation` 等(31 张表) | +| `pur` | 采购 | `purpurchaseapply`、`purpurchasearrive`、`purpurchasechecking` 等(28 张表) | +| `ops` | 外协 / 外发加工 | `opsoutsidearrive`、`opsoutsidechecking`、`opsoutsideinstore` 等(23 张表) | +| `cah` | 出纳 / 财务 | `cahcashierinit`、`cahcostchangemaster`、`cahpaymentmaster`、`cahreceiptmaster` 等(22 张表) | +| `sgd` | 半成品 | `sgdsemigoodscheck`、`sgdsemigoodsinstore`、`sgdsemigoodsmatchbill` 等(21 张表) | +| `ept` | 设备 / 机台固定资产 | `eptmachinefixedborrow`、`eptmachinefixedchange`、`eptmachinefixedinstore` 等(21 张表) | +| `mit` | 材料库存事务 | `mitmaterialsadjust`、`mitmaterialscheck`、`mitmaterialsinstore` 等(19 张表) | +| `pit` | 产品库存事务 | `pitproductadjust`、`pitproductbarcode`、`pitproductcheck`、`pitproductinstore` 等(18 张表) | +| `qly` | 质量检测 | `qlycomematerialstest`、`qlyproducttest`、`qlyprocesstest` 等(8 张表) | +| `kpi` | KPI 跟踪 | `kpimaster`、`kpidetail`、`kpimoduleuserday` 等(7 张表) | +| `udf` | 自定义 / 通用凭证框架 | `udfaccountno`、`udfvouchermaster`、`udfvouchertemplatemaster` 等(5 张表) | +| `viw_` / `Viw_` | 数据库**视图**(schema 中大小写不一致) | `viw_mftworkorderprocess`、`viw_corebusinessreport`、`viw_accordercostanalysisnew` 等(共 311 个视图) | +| `plat_` | B2B 印刷平台层(按[首页](../index.md)说明属于范围外) | 92 张表;本 Wiki 不展开 | +| `ai_` | AI / LLM 功能(范围外) | 7 张表;本 Wiki 不展开 | +| `act_`、`qrtz_` | 第三方 schema(Activiti 工作流、Quartz 调度) | 在 [Activiti](../reference/maintainer/activiti.md) 和[技术栈 Quartz](../reference/maintainer/tech-stack.md)中间接覆盖 | + +业务领域前缀(`ele`、`mft`、`sal`、`quo`、`acc`、`pur`、`ops`、`cah`、`sgd`、`ept`、`mit`、`pit`、`qly`、`kpi`、`udf`)及其从表都走同一套元数据驱动运行时:没有按前缀区分的 Java 代码,只有 `gdsconfigformmaster` / `gdsconfigformslave` 中指向各支撑表或视图的行。 diff --git a/zh/docs/concepts/multi-tenancy.md b/zh/docs/concepts/multi-tenancy.md index 83b7385..10bbad8 100644 --- a/zh/docs/concepts/multi-tenancy.md +++ b/zh/docs/concepts/multi-tenancy.md @@ -10,11 +10,11 @@ xly 是多租户 SaaS。同一套代码库、同一套数据库 schema、同一 |---|---|---|---| | **`sBrandsId`**(加工商ID) | 几乎每条业务行 | 每行 | 用户 session(`UserInfo.getsBrandsId()`) | | **`sSubsidiaryId`**(子公司ID) | 几乎每条业务行 | 每行 | 用户 session | -| **`sVersionFlowId`**(版本流程ID) | 仅 `gdsmodule` | 每模块 | 用户所属产品版本(对应 `sisversionflow`) | +| **`sVersionFlowId` / `sVersionFlowCode`**(版本流程ID / code) | 仅 `gdsmodule` | 每模块标签 | 版本目录元数据;运行时菜单可见性使用许可证产出的 `sVerifyLicense` 模块列表 | -实时 DB 中每行作用域非常普遍:`sBrandsId` 出现在 1,009 张表 / 视图上,`sSubsidiaryId` 出现在 1,008 张表 / 视图上。几乎所有业务数据表和框架元数据表都带有它们。 +逐行作用域在业务数据表中非常普遍:几乎每张业务数据表和视图都带有 `sBrandsId` 与 `sSubsidiaryId`。大多数框架元数据表也带有这两个列,但四张表(`gdsformconst`、`gdsmodule`、`gdsconfigformmaster`、`gdsconfigformslave`)是明确例外:`BusinessBaseServiceImpl.sTableNameList`(162-169 行)把它们列为“不需要公司子公司的表”,1078-1084 行会从这些表的写入载荷中剥掉 `sBrandsId` / `sSubsidiaryId`。实际使用中它们保存的是所有客户共享的一组哨兵租户值。惯例应理解为:如果一行代表租户拥有的状态,就有这两个列,且它们由 session 填充。 -每模块 gating(`sVersionFlowId`)只出现在 **3** 张表上,并且只有 `gdsmodule` 是实时表,另外两张是备份。因此版本 gating 是模块发现阶段的一次性过滤,不是每行检查。 +每模块版本元数据则相反:它只存在于 `gdsmodule` 上。实时运行时并不直接按 `sVersionFlowId` 过滤;模块发现由许可证产出的 `sVerifyLicense` 允许模块列表控制。因此版本控制发生在模块发现阶段,是一次性过滤,不是每行检查。 ## 如何强制执行 @@ -34,6 +34,13 @@ RequestAddParamUtil.me().addParams(params, userInfo); 2. **数据库视图。** 几乎所有 `viw_*` 视图都会从主基础表带出租户列,但手写视图如果漏掉这些列,就可能造成按行泄漏。维护审计脚本应标记此类视图。 3. **保存端点**(`addUpdateDelBusinessData`),它允许前端在 payload 中直接提供 `sTable`。如果运行时不校验该表是否属于表单授权范围,这就是权限提升面。见[切片 1](../slices/01-hello-world.md#4-user-edits-a-row-clicks-save)。 -## 为什么这个实时 DB 看起来很小 +## 这个设计如何扩展,以及哪里扩展不了 -`xlyweberp_saas_ai` 只有**一个**品牌(`sBrandsId = '1111111111'`)、**一个**子公司,以及**一个**有数据的版本(`8S_001 / 基础版`)。多租户机制已经接好,但这里没有充分使用。生产租户会有几十个品牌、每个品牌下几十个子公司,以及多个版本;设计通过行数扩展,而不是通过代码分支扩展。 +框架的多租户设计靠**行数**扩展,而不是靠代码分支扩展。只有一个品牌、一个子公司、一种版本的小型 SaaS 部署,和拥有几十个品牌、几十个子公司、多个版本的部署,使用的是同一套 Java、MyBatis mapper 和存储过程;差异只体现在 `gdsmodule`、`sisversionflow` 和业务数据表中的行分布。 + +按行数扩展运维上简单,但限制也很明确: + +- **共享物理 schema 意味着共享资源竞争。** 所有租户的查询都打到同一个 MySQL 实例、同一批表和同一批索引上。租户 A 的重报表会和租户 B 的录单竞争 buffer pool 和 CPU。这里没有按租户隔离资源。 +- **每个 WHERE 都要带租户过滤。** 每条读查询都要携带 `sBrandsId = ? AND sSubsidiaryId = ?`。索引通常也要以这些列开头才有用;xly 大多数表按约定这样做,但维护人员新增索引时必须记得这一点。漏掉后,查询计划会扫过所有租户的行,并在表变大后悄悄变慢。 +- **没有物理硬删除边界。** 租户下线不会 drop 一个数据库;行会留在原处,有的标记 `bInvalid`,有的被删除,有的完全不动。永久移除需要按租户写清理脚本。从 GDPR / 数据驻留角度看,“这个租户已经彻底消失”很难证明。 +- **`sBrandsId` / `sSubsidiaryId` everywhere 固化了租户单位。** “租户”精确定义为 `(sBrandsId, sSubsidiaryId)` 这个二元组。其他切法(例如按区域授权、按部门授权但不拆子公司)不适合这个模型,需要并行的作用域列。这个模型假定该形状永远适合所有客户;目前实践上基本成立,但这是一个硬承诺。 diff --git a/zh/docs/concepts/request-lifecycle.md b/zh/docs/concepts/request-lifecycle.md index b5ce5f1..caa0d7e 100644 --- a/zh/docs/concepts/request-lifecycle.md +++ b/zh/docs/concepts/request-lifecycle.md @@ -7,27 +7,37 @@ ```text Browser 1. 任意 URL 加载 SPA shell(服务端对每个路径返回同一 shell; - gdsroute 是客户端侧边栏 / deep-link 白名单,不是服务端 404 gate) + gdsroute 是客户端侧边栏 / deep-link 白名单,不是服务端 404 闸门) 2. 用户点击侧边栏项或在 SPA 内导航 3. SPA 决定加载哪个模块 → 调用 /business/... -GET /xlyEntry/business/getModelBysId/{moduleId}?sModelsId={moduleId} +GET /xlyEntry/business/getModelBysId/{sModelsId}?sModelsId={sModelsId} +(模块 id 同时出现在 path 和 query 中;controller 绑定路径变量, +但 service 从 @RequestParam map 读取 sModelsId,所以 SPA 也必须在 query string 中传它) xlyEntry — BusinessBaseController.getModelBysId() RequestAddParamUtil.addParams(params, userInfo) → sBrandsId、sSubsidiaryId、sUserId、sLanguage 等 - → 租户作用域进入所有下游查询 + → 共 16 个 key(详见 runtime.md) + → 租户作用域变成下游查询可用的参数。框架元数据读取按 form-id 过滤; + 每租户覆盖与业务数据读取才按 sBrandsId / sSubsidiaryId 过滤。 BusinessBaseService.getModelBysId(map) - ├── 加载 gdsmodule 行(模块) - ├── 加载 gdsconfigformmaster 行(通过 sParentId 连接模块) - ├── 加载 gdsconfigformslave 行(通过 sParentId 连接 form-master) - ├── 合并 gdsconfigformpersonalize(每租户) - ├── 合并 gdsconfigformcustomslave(每租户) - ├── 加载 gdsjurisdiction(ADMIN 跳过) - ├── 加载 gdsformconst(表单级常量) - ├── 加载 sysbillnosettings(单据编号) - └── 加载关联到该表单的 sysreport 行 + ├── 1. formData + │ └── gdsconfigformmaster(按 sParentId = sModelsId 过滤; + │ gdsmodule 本身不被 SELECT,只通过 id 引用) + │ + LEFT JOIN gdsconfigformpersonalize(每租户覆盖) + │ + 每个 master 的 gdsconfigformslave + │ + 每个 master 的 gdsconfigformcustomslave(每租户覆盖) + ├── 2. gdsformconst(仅按 sParentId 过滤;不按租户过滤; + │ sLanguage 决定返回哪一列标签) + ├── 3. sysjurisdiction(按用户 / 用户组授权,JOIN + │ sftlogininfojurisdictiongroup + sisjurisdictionclassify; + │ ADMIN 跳过。返回 map key 仍叫 `gdsjurisdiction`, + │ 这个名字有误导性:gdsjurisdiction 表是配置侧动作目录, + │ 不是这里读取的授权表) + ├── 4. sysbillnosettings(每租户、每表单) + └── 5. sysreport(每租户、每表单) 返回一个复合 map: { formData, gdsformconst, gdsjurisdiction, billnosetting, report } @@ -44,24 +54,83 @@ xlyEntry — BusinessBaseController.getBusinessDataByFormcustomId() 用户看到表格 ``` +## 同一流程的时序图 + +上面的 ASCII 图强调执行顺序;下面的时序图强调**谁调用谁**。排查真实请求时,后者通常更有用。 + +```mermaid +sequenceDiagram + autonumber + participant SPA as Browser SPA + participant CTRL as BusinessBaseController + participant SVC as BusinessBaseServiceImpl + participant FORMS as BusinessGdsconfigformsServiceImpl + participant DB as MySQL + participant REDIS as Redis (RedisCacheManager) + + SPA->>CTRL: GET /business/getModelBysId/{sModelsId}
?sModelsId=...&Authorization= + Note over CTRL: AuthorizationInterceptor.preHandle
从 Redis 解析 UserInfo
RequestAddParamUtil.addParams(16 个 key) + + CTRL->>SVC: getModelBysId(map) + Note over SVC: getModelConfigByModleId(继承自 BaseServiceImpl)
编排每个 form-master 的主表单 + 从表加载 + SVC->>FORMS: getFormmasterData / getGdsconfigformslaveShow
(form-master + slaves + overlays) + REDIS-->>FORMS: cache hit? + FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize;每个 master 再读 gdsconfigformslave + gdsconfigformcustomslave + DB-->>FORMS: rows + FORMS-->>SVC: formData + + SVC->>FORMS: getFormconstData(只按 form-id,不按租户) + FORMS->>DB: SELECT ... gdsformconst WHERE sParentId=... + DB-->>FORMS: rows + FORMS-->>SVC: gdsformconst + + alt sUserType != ADMIN + SVC->>FORMS: getJurisdictionData(每用户授权) + FORMS->>DB: SELECT ... sysjurisdiction ⋈ sftlogininfojurisdictiongroup + DB-->>FORMS: rows + FORMS-->>SVC: gdsjurisdiction(map key;源表是 sysjurisdiction) + else ADMIN + Note over SVC: 跳过权限加载 + end + + SVC->>FORMS: getBillnosettingData + FORMS->>DB: SELECT ... sysbillnosettings WHERE sFormId=... AND tenant + DB-->>FORMS: row + FORMS-->>SVC: billnosetting + + SVC->>DB: SELECT ... sysreport WHERE sFormId=... AND tenant + DB-->>SVC: report rows + + SVC-->>CTRL: composite Map(5 个 key) + CTRL-->>SPA: AjaxResult{code:1, dataset:{...}} + + SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/{formId}
?sModelsId=... + Note over CTRL,SVC: 同一次 RequestAddParamUtil 注入
随后使用每表单 sSqlStr / sWhere / sOrder + CTRL->>DB: 对表单支撑 table/view/proc 执行参数化 SELECT + DB-->>CTRL: rows + CTRL-->>SPA: dataset +``` + +图中第 1 行和第 22 行是两个 HTTP 往返。中间全部是服务端工作,SPA 看不到。 + ## 五键复合结果 `getModelBysId` 返回一个 Java `Map`,按顺序包含这些 key: | Key | 来源 | SPA 用途 | |---|---|---| -| `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave`(+ 覆盖) | 表单布局本身:每个字段、控件、标签、校验规则 | -| `gdsformconst` | 按租户 + 语言作用域过滤的 `gdsformconst` 行 | 表单级常量:标签、默认值、下拉文本 | -| `gdsjurisdiction` | 用户角色的 `gdsjurisdiction` 行 | 按钮和数据权限 | -| `billnosetting` | 该模块的 `sysbillnosettings` 行 | 单据编号规则(工单号、报价单号) | -| `report` | 关联到该表单的 `sysreport` 行 | 打印模板(jxls Excel、iText PDF) | +| `formData` | `gdsconfigformmaster`(按 `sParentId = sModelsId` 过滤)⋈ `gdsconfigformpersonalize`(每租户覆盖);每个 master 行再加载 `gdsconfigformslave` + `gdsconfigformcustomslave` 覆盖。`gdsmodule` 只通过 id 引用。 | 表单布局本身:每个字段、控件、标签、校验规则 | +| `gdsformconst` | 仅按 `sParentId` 过滤的 `gdsformconst` 行;不按租户过滤;`sLanguage` 决定返回哪一列标签 | 表单级常量:标签、默认值、下拉文本 | +| `gdsjurisdiction` | 用户的 `sysjurisdiction` 行,或通过 `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify` 读取用户组授权;ADMIN 跳过。map key 名称 `gdsjurisdiction` 有误导性:那张表是配置侧动作目录,不是这里读取的授权表。 | 按钮和数据权限 | +| `billnosetting` | 该模块的 `sysbillnosettings` 行(每租户) | 单据编号规则(工单号、报价单号) | +| `report` | 关联到该表单的 `sysreport` 行(每租户) | 打印模板(jxls Excel、iText PDF) | ## 不在这个生命周期中的内容 - **保存路径。** 保存有自己的端点 `POST /business/addUpdateDelBusinessData`,把新增 / 更新 / 删除打包进一个请求。见[切片 1](../slices/01-hello-world.md#4-user-edits-a-row-clicks-save)。 - **打开已有行编辑的读取。** 与表格加载使用同一端点 `getBusinessDataByFormcustomId`,但 body 中带行 `sId`,handler 按请求一行还是多行分支。 -- **工作流步骤。** 如果模块有活动审批流(`bCheck = 1`、填充 `sVersionFlowId`、已部署 Activiti 流程),会插入额外步骤。当前 dev DB 没有这些数据;见[切片 7(暂缓)](../slices/07-workflow.md)。 -- **缓存失效。** **后台**修改元数据行时,JMS 消息会让所有运行节点失效缓存副本,即 `xlyErpJmsConsumer` 中的 `ConsumerChangeGdsModuleThread`。它在请求流之外,但紧邻请求流。 +- **工作流步骤。** 如果模块有活动审批流(`bCheck = 1`、`gdsmoduleflow` 已配置、Activiti 流程已部署,并且 `ConstantUtils.bCheckflowCheck = true`),会插入额外步骤。当前 dev DB 没有这些数据;见[切片 7(暂缓)](../slices/07-workflow.md)。 +- **缓存失效。** **后台**修改元数据行时,保存服务会同步调用 `BusinessCleanRedisData` / `CleanRedisServiceImpl`,从共享 Redis 中驱逐 Spring cache region。JMS 的 `ConsumerChangeGdsModuleThread` 是另一条基础数据合并通道,不是缓存失效。 ## 其他切片覆盖的变体 diff --git a/zh/docs/concepts/semantic-fk.md b/zh/docs/concepts/semantic-fk.md index 6be72bd..53deae1 100644 --- a/zh/docs/concepts/semantic-fk.md +++ b/zh/docs/concepts/semantic-fk.md @@ -1,15 +1,22 @@ # 无物理外键、语义外键的现实 -`xlyweberp_saas_ai` schema 在任何 xly 自研表(`gds*`、`ele*`、`mft*`、`quo*`、`sal*`、`acc*` 等表族)上都有 **0 个触发器**和 **0 个外键约束**。确实存在的 41 个 FK 约束都在捆绑的第三方 schema 上:Activiti 表(`act_*`)36 个、Quartz 调度表(`qrtz_*`)5 个;框架自身运行时不会通过这些表做核心连接。在 901 张基础表中,框架依赖的元数据和业务数据连接全部只是约定。 +`xlyweberp_saas_ai` schema 在任何 xly 自研表(`gds*`、`ele*`、`mft*`、`quo*`、`sal*`、`acc*` 等表族)上都有 **0 个触发器**和 **0 个外键约束**。确实存在的 FK 约束都在捆绑的第三方 schema 上:Activiti 的 `act_*` 表和 Quartz 的 `qrtz_*` 表;框架自身运行时不会通过这些表做核心连接。框架依赖的元数据和业务数据连接全部只是约定。 这是一个有意的设计选择,继续阅读前必须理解。 ## 为什么 xly 禁用 FK -架构上给出的两个原因都很务实: +架构上给出的两个原因是: 1. **批量写入性能。** 大量插入(工单计算、月结、批量导入)一次会写入几十万行。启用 FK 后,MySQL 会在插入时验证每一行引用;以 xly 的数据量,这会成为限制因素。 -2. **schema 迁移敏捷性。** xly 演进很快:新模块、新字段、新表。启用 FK 时,每次 schema 变更都必须考虑约束图;没有 FK 时,`CREATE TABLE` 或 `ALTER TABLE` 是局部操作。代价由运行时应用代码承担。 +2. **schema 迁移敏捷性。** xly 演进很快:新模块、新字段、新表。启用 FK 时,每次 schema 变更都必须考虑约束图;没有 FK 时,`CREATE TABLE` 或 `ALTER TABLE` 是局部操作。 + +这两个考虑都真实存在,但都不足以推出“整个 schema 零 FK”: + +- **批量写入性能**可以更精细地处理:批处理期间临时关闭约束(`SET FOREIGN_KEY_CHECKS = 0`),写完后重新打开并校验。xly 选择的是完全不建 FK,这意味着每次读取也都要相信临时过程校验,而不是数据库强制的完整性。 +- **schema 迁移敏捷性**确实会因为没有 FK 而提高,但代价是每个引用检查都要挪到应用代码或存储过程里,甚至可能被遗漏。实践中,原本 FK 自动完成的完整性工作被复制到数百个存储过程中,而且没有编译期保证某个过程真的做了检查(见下面的失效模式)。 + +更准确的说法是:系统用 **DB 强制完整性**换取了**写入时和 DDL 时的运维便利**。这笔交易带来的 bug 面(孤儿行、未发现的跨租户引用、几周后才浮现的完整性错误)会在系统每天运行时持续付费。 ## 什么是“语义 FK” diff --git a/zh/docs/concepts/thesis.md b/zh/docs/concepts/thesis.md index a2c0642..75ed051 100644 --- a/zh/docs/concepts/thesis.md +++ b/zh/docs/concepts/thesis.md @@ -10,22 +10,24 @@ xly 的方案相反:**单一代码库、单一部署,每客户行为用数 这个设计有三个内置成本,值得明确写出来: -1. **每次请求都要读取元数据。** 每次页面加载至少读取四张表(`gdsmodule`、`gdsconfigformmaster`、`gdsconfigformslave`、`gdsjurisdiction`)以及租户过滤条件。运行时会积极缓存,但缓存未命中时这些读取不可避免。 -2. **schema 会持续膨胀。** 新模块 = `gdsmodule` 中一行 + `gdsconfigformslave` 中 1 到 50 行 + 一个支撑它的物理表(通常按单据类型)。当前实时 DB 有 901 张基础表;生产租户更多。 +1. **每次请求都要读取元数据。** 每次页面加载在缓存未命中时会跑五类查询:`gdsconfigformmaster`(并为匹配的明细行叠加 personalize / customslave 覆盖)、`gdsformconst`、`sysjurisdiction`(用户授权;返回 map key 叫 `gdsjurisdiction`,但实际读取的是 `sysjurisdiction`;ADMIN 会跳过)、`sysbillnosettings`、`sysreport`。运行时会积极缓存,但缓存未命中时这些读取不可避免。 +2. **schema 会持续膨胀。** 新模块 = `gdsmodule` 中一行 + `gdsconfigformslave` 中 1 到 50 行 + 一个支撑它的物理表(通常按单据类型)。随着业务模块增加,基础表数量会继续增长;生产租户通常比干净的 dev schema 带更多表,因为客户定制模块会长期留在共享 schema 中。 3. **关系是约定,不是约束。** 为了性能和迁移灵活性禁用外键后,从 `gdsconfigformmaster.sParentId` 到 `gdsmodule.sId` 的连接,以及上百个类似连接,都只是[语义外键](semantic-fk.md)。孤儿行是可能存在的。 -## 收益 +## 这个设计带来的能力,以及每种能力的代价 -作为交换,xly 得到: +- **一个代码库服务几十个客户。** 每个客户租户拥有自己的元数据行;Java 完全相同。——*限制:*它并不能覆盖所有客户。`script/客户/` 下的 18 个目录(见[切片 5](../slices/05-customer-sql-override.md))就是数据驱动设计撞墙的位置:当客户需要不同的过程逻辑时,“单一代码库”就变成了“单一 Java 代码库 + 一批由数据库静默承载的客户专属 SQL”。 +- **PM 不占用开发时间就能演进应用。** 他们打开 BACK、添加模块、定义表单、设置权限,下一个用户加载时即可看到变化。——*限制:*PM 能表达的词汇,取决于 `gdsconfigformmaster` / `gdsconfigformslave` 已经暴露的列。真正新的能力(自定义计算、非标准校验、不同保存路径)仍然需要存储过程,也就重新需要工程师,只是开发位置从 Java 换成了 SQL。没有 DB 访问权的 PM 也很难判断一次元数据改动为什么产生了错误输出,因为过程侧逻辑在 BACK 中不可见。 +- **定制可以“干净地”分层**([切片 4](../slices/04-custom-field.md)):每租户覆盖叠加在共享基础之上,不需要 fork。——*限制:*这种干净主要是 Java 运行时视角下的。`BusinessBaseServiceImpl` 中的合并逻辑本身并不简单(3,900 多行);排查“为什么这个租户能看到字段 X、看不到字段 Y”时,需要追 `gdsconfigformpersonalize`、`gdsconfigformcustomslave`、`gdsconfigformuserslave` 的组合关系。而且覆盖层不能 `ALTER TABLE`;真正新增物理列仍然需要协调 schema 迁移。 -- **一个代码库服务几十个客户。** 每个客户租户拥有自己的元数据行;Java 完全相同。 -- **PM 不占用开发时间就能演进应用。** 他们打开**后台**、添加模块、定义表单、设置权限,下一个用户加载时即可看到变化。 -- **定制可以干净地分层**([切片 4](../slices/04-custom-field.md)):每租户覆盖叠加在共享基础之上,不需要 fork。 +更直白地说:数据驱动设计把复杂度从 Java 挪到了数据库和 PM 构建的元数据里。系统总复杂度没有消失,只是转移到了框架无法编译检查的人和工具上。 ## 何时失效 -数据驱动适用于客户需求能用元数据表达的情况。一旦客户需要元数据表达不了的行为,比如不同 SQL、不同存储过程主体、框架词汇无法描述的聚合规则,就会触及边界。xly 的逃生口是[每客户 SQL 覆盖通道](../slices/05-customer-sql-override.md):把手写 SQL 提交到 `script/客户//`,并直接应用到该客户 schema,完全绕过框架。这个通道真实存在且正在使用,但也是维护成本最高的定制方式。 +数据驱动适用于客户需求能用元数据表达的情况。一旦客户需要元数据表达不了的行为,比如不同 SQL、不同存储过程主体、框架词汇无法描述的聚合规则,就会触及边界。xly 的应对方式是[每客户 SQL 覆盖通道](../slices/05-customer-sql-override.md):把手写 SQL 提交到 `script/客户//`,并直接应用到该客户 schema,完全绕过框架。 + +这里需要说清楚:“绕过框架”意味着数据驱动论点只在系统的一部分成立。对 `script/客户/` 下的 18 个客户来说,运行时已经不再是真正的单一代码库;Java 是共享的,但每个客户 DB 里实际执行的过程体会分叉,而且没有自动机制发现漂移。审查者在源码中读到的 `Sp_SalSalesCheck`,并不保证就是某个生产客户实际运行的版本。把它称为“逃生口”已经偏温和;实践中,覆盖通道已经变成处理重大业务逻辑差异的标准答案,而这正是数据驱动设计原本想避免的失效模式。 ## 这对阅读 Wiki 意味着什么 -本 Wiki 中的每个切片都记录这个论点的一次*应用*。切片 1 是 CRUD 模块上的四表读取。切片 2 是贯穿每一层的多租户作用域。切片 3 是只读 / 视图支撑的变体。切片 4 是定制覆盖。切片 5 是覆盖不够用时的逃生口。它们合起来从中心到边界覆盖数据驱动设计。 +本 Wiki 中的每个切片都记录这个论点的一次*应用*。切片 1 是 CRUD 模块上的元数据读取,也是标准实例。切片 2 是贯穿每一层的多租户作用域。切片 3 是只读 / 视图支撑的变体。切片 4 是定制覆盖。切片 5 是覆盖不够用时的逃生口。它们合起来从中心到边界覆盖数据驱动设计。 diff --git a/zh/docs/contributing/index.md b/zh/docs/contributing/index.md index daa411e..77fd3d9 100644 --- a/zh/docs/contributing/index.md +++ b/zh/docs/contributing/index.md @@ -32,7 +32,7 @@ DB_NAME=xlyweberp_some_other python scripts/gen_catalog.py 生成器每次运行都会清空并重写 `docs/auto-catalog/{tables,views,procedures,functions}/` 下的全部文件。这些目录中的手工修改会丢失。 -## Pre-commit hook(可选,建议本地编辑时使用) +## Pre-commit 钩子(可选,建议本地编辑时使用) 安装一次: @@ -41,7 +41,7 @@ ln -s ../../scripts/precommit.sh .git/hooks/pre-commit chmod +x scripts/precommit.sh ``` -hook 会在每次提交时运行 `mkdocs build --strict`,以便在提交前发现断开的交叉链接。 +钩子会在每次提交时运行 `mkdocs build --strict`,以便在提交前发现断开的交叉链接。 ## 风格 diff --git a/zh/docs/index.md b/zh/docs/index.md index 76068c9..882d56f 100644 --- a/zh/docs/index.md +++ b/zh/docs/index.md @@ -21,13 +21,20 @@ ## 不覆盖的范围 -- B2B 印刷平台层(`plat_*` 表、`xlyPlat*` 模块)。 +- B2B 印刷平台层(`plat_*` 表、除 `xlyPlatConstant` 外的所有 `xlyPlat*` 模块;见下方说明)。 - AI / LLM 功能(`ai_*` 表、`AiController`),太新且仍在变化。 -- 人脸识别(`xlyFace`),范围较窄。 +- 人脸识别(`xlyFace`),范围较窄;它仍在 `settings.gradle` 中启用(会构建和部署),但本 Wiki 有意不展开。 +- 文件管理模块(`xlyFile`)和串口模块(`xlyRxtx`),范围较窄 / 偏硬件。 +- 调度模块(`xlyErpTask`、`xlyPlatTask`),在 `settings.gradle` 中已注释;cron / Quartz 接线不属于本 Wiki 覆盖的框架运行时。 +- 测试脚手架模块(`xlyTestService`、`xlyTestController`),历史遗留,不属于框架运行时。 - `xlyweberp_*` 数据库之间的租户级 schema 漂移;本 Wiki 针对一个 schema。 -- 备份表(`*_bak`、`*0302` 等)。 -- MongoDB 文档存储(yaml profile 中的 `spring.data.mongodb.uri`,以及 `xlyEntity/.../mongo/` 下的文档类)。每个 `@Document` 类都是 `PLAT_*` 命名,每个 `MongoTemplate` 调用方都位于 `xlyPlat*` 模块中,因此 MongoDB 属于上面的 plat 层。本 Wiki 覆盖的框架层只讨论 MySQL。 +- 备份表(`*_bak`、`*0302`、`*_copy1`、`*_history`、`*YYYYMMDD[HHMMSS]` 后缀快照等)。自动目录会为它们生成页面,因为它们真实存在;正文页面不会把它们作为一个家族展开。当前 schema 约有 56 张此类表。 +- 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 接线仍可编译,但处于休眠状态。 + +> **关于 `xlyPlatConstant`。** 它带有 `xlyPlat*` 前缀,但属于本 Wiki 范围:`xlyPersist` 从中导入了两个工具类(`com.xly.xlyplatconstant.contant.thread.MultiThreadServer`、`com.xly.xlyplatconstant.contant.TimeContant`)。把它视为命名不准的共享工具模块,而不是平台层模块。 + +> **关于 `xlyPlc`。** PLC / 硬件桥接插件属于本 Wiki 范围,是非核心模块如何挂入框架的标准示例。见[切片 06:硬件](slices/06-hardware.md)。 ## 如何修正这个 Wiki -编辑 Markdown 文件即可,这些文件就是 Wiki 的源。MkDocs Material 会从这些 `.md` 文件生成静态 HTML。重新生成命令和 pre-commit hook 见 [参与维护](contributing/index.md)。 +编辑 Markdown 文件即可,这些文件就是 Wiki 的源。MkDocs Material 会从这些 `.md` 文件生成静态 HTML。重新生成命令和 pre-commit 钩子见 [参与维护](contributing/index.md)。 diff --git a/zh/docs/reference/builder/attach-workflow.md b/zh/docs/reference/builder/attach-workflow.md index 0984f9e..e487726 100644 --- a/zh/docs/reference/builder/attach-workflow.md +++ b/zh/docs/reference/builder/attach-workflow.md @@ -1,5 +1,7 @@ # 如何挂接工作流 +> **暂缓:需要一个已部署 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))。下面的配方是**从代码推导出的假设**,尚未在实时部署上跑通过。 + > **暂缓。** Activiti 已接入代码库,但当前实时 DB 中没有部署工作流。相同原因见[切片 7(暂缓)](../../slices/07-workflow.md)。 > > 当有带活动流程的 DB 可用时,配方大致如下: diff --git a/zh/docs/reference/builder/define-form.md b/zh/docs/reference/builder/define-form.md index 936db24..5125106 100644 --- a/zh/docs/reference/builder/define-form.md +++ b/zh/docs/reference/builder/define-form.md @@ -88,4 +88,4 @@ ## 缓存失效 -插入后,运行时缓存仍可能持有*之前*的空状态,直到 JMS 消息触发。**后台**构建器保存变更时,xly 的缓存失效监听器(`ConsumerChangeGdsModuleThread`)会处理;如果你通过原始 SQL 插入,可能需要重启运行服务或等待 TTL 过期。见[元数据变更后的缓存失效](../maintainer/cache-invalidation.md)。 +插入后,运行时缓存仍可能持有*之前*的空状态,直到某条路径清理它。BACK 保存变更时,保存 service 会同步调用 `BusinessCleanRedisData.delCleanRedisData*`,进而触发 `CleanRedisServiceImpl` 中相关 cache region 的 `@CacheEvict`。如果你通过原始 SQL 插入,**不会自动清理缓存**;需要从应用内部调用 `BusinessCleanRedisDataImpl` 的方法、重启运行服务,或等待 TTL 过期。(名字相近的 `ConsumerChangeGdsModuleThread` JMS 路径做的是存储过程基础数据合并,不是缓存失效;详见[元数据变更后的缓存失效](../maintainer/cache-invalidation.md)。) diff --git a/zh/docs/reference/builder/define-vtable.md b/zh/docs/reference/builder/define-vtable.md index 09f4002..7fe9b1c 100644 --- a/zh/docs/reference/builder/define-vtable.md +++ b/zh/docs/reference/builder/define-vtable.md @@ -10,7 +10,7 @@ xly 中的*虚拟表*是作为元数据声明的“表”,不是 DDL 创建的 - 定义下游表单可以叠加使用的数据形状。 - 集中管理租户感知的列定义,使多个读取同一形状的表单共享默认值。 -当前实时 DB 中,`gdsconfigtbmaster` 有 307 个虚拟表 master 行,`gdsconfigtbslave` 有 14,385 个虚拟列行。它们覆盖 lookup 表、分类树和可配置参数集。 +虚拟表覆盖 lookup 表、分类树和可配置参数集。随着 PM 增加新形状,目录会自由增长。 ## 配方 @@ -21,19 +21,42 @@ xly 中的*虚拟表*是作为元数据声明的“表”,不是 DDL 创建的 | 列 | 值 | |---|---| | `sId` | 唯一虚拟表 ID | -| `sName` | 虚拟表逻辑名 | | `sChinese` / `sEnglish` / `sBig5` | 显示名 | | `sBrandsId` / `sSubsidiaryId` | 租户作用域 | -| `sTbName` | 底层物理表名(如果有支撑表) | -| 其他配置列 | 描述存储和索引 | +| `sTbName` | 底层物理名称。**实践中它可以指向表、视图或存储过程**;该列有唯一键,但没有更严格约束。运行时把它当成通用 SQL 标识符解析。 | +| `sParentId` | 树形分类的父虚拟表;平铺表为空 | +| `iOrder` | BACK 列表中的排序 | ### 2. 列 — `gdsconfigtbslave` 每列一行。每行携带列名、类型、默认值、显示标签、校验规则,以及它是否属于主键。 -## 未决:数据由什么支撑 +## `sTbName` 实际指向什么,以及漂移 -当前实时 DB 中,307 个 `gdsconfigtbmaster` 行都有非空 `sTbName`,但其中 11 个名称在 `information_schema.tables` 中找不到当前对象。因此安全表述是:元数据期望存在底层 SQL 对象,但实时 schema 并非对每个虚拟表行都完全对齐。 +每个 `gdsconfigtbmaster` 行都有非空 `sTbName`,但该列只是带唯一键的字符串;框架不会强制它解析为基础表。已对实时 dev DB 验证: + +- `gdsconfigtbmaster` 总计 307 行。 +- **296 行(96.4%)解析到 `information_schema.tables` 中真实存在的 `BASE TABLE`**。 +- **11 行(3.6%)无法解析为基础表**,而它们的分布本身很有信息量: + +| 未解析的 `sTbName` 实际指向 | 数量 | 示例 | +|---|---:|---| +| 视图(`viw_*`),不是表 | 4 | `viw_mftproductionreport`、`viw_mftproductionreportEmployee1` | +| 存储过程(`Sp_*`) | 3 | `Sp_Cashier_BankJournal`、`Sp_Cashier_SumJournal`、`Sp_Sales_NotDeliverGoodNotifyList` | +| 大小写折叠后存在的真实表,或已重命名 / 删除对象 | 4 | `QlyProcessTestResult`(大小写漂移)等 | + +所以 `sTbName` **不严格等于“物理表名”**;它是运行时会替换进读取查询里的通用 SQL 标识符,也可能指向视图或可调用过程。早先把它写成“底层物理表名”的说法过窄。 + +可用于暴露漂移的审计 SQL: + +```sql +SELECT sId, sChinese, sTbName +FROM gdsconfigtbmaster +WHERE sTbName NOT IN ( + SELECT TABLE_NAME FROM information_schema.tables + WHERE TABLE_SCHEMA = DATABASE() +); +``` ## 何时选择虚拟表、视图或真实表 @@ -45,6 +68,34 @@ xly 中的*虚拟表*是作为元数据声明的“表”,不是 DDL 创建的 虚拟表通道是框架对数据驱动形状的“类型系统”;物理 schema 才是真正存储行的地方。两者有意解耦。 -## 示例 +## 示例:`包装方式` lookup -本页需要一个具体示例:从 `gdsconfigtbmaster` 选一张真实虚拟表,逐行解释它的 master 行和 slave 行。后续版本应补上。 +dev DB 中的一条代表性真实记录: + +**Master**(`gdsconfigtbmaster`): + +```text +sId = 192116810113315231587698560 +sChinese = 包装方式 (Packing method) +sTbName = SisPacking +sParentId = (root) +``` + +**Slave 列**(`gdsconfigtbslave`,该 `sParentId` 下 10 行)声明的是*逻辑*形状:名称、显示标签、校验。*物理*形状位于真实的 `SisPacking` 表中: + +| Slave 行 | `SisPacking` 上的物理列 | +|---|---| +| `iIncrement`(自增列) | `iIncrement int auto_increment PK` | +| `sId`(标准ID) | `sId varchar(100) UNIQUE` | +| `sBrandsId`(加工商Id) | `sBrandsId varchar(100)` | +| `sSubsidiaryId`(子公司Id) | `sSubsidiaryId varchar(100)` | +| `tCreateDate`(制单日期) | `tCreateDate datetime DEFAULT CURRENT_TIMESTAMP` | +| `sMakePerson`(制单人) | `sMakePerson varchar(255)` | +| `iOrder`(排序号) | `iOrder int DEFAULT 0` | +| `sName`(名称) | `sName varchar(255)` | +| `sNo`(编号) | `sNo varchar(255)` | +| `bInvalid`(作废) | `bInvalid bit(1) DEFAULT b'0'` | + +`gdsconfigtbslave` 中的 10 行与物理 `SisPacking` 表上的 10 列逐一对应。PM 随后可以让某条 `gdsconfigformmaster` 指向 `sTbName='SisPacking'`,form-slave 行再按名称引用同一批列。运行时把这两层粘合起来,路径与切片 1 中的元数据驱动读取相同。 + +本页之前把“补一个 worked example”列为 TODO;这里就是补上的示例。 diff --git a/zh/docs/reference/builder/permissions.md b/zh/docs/reference/builder/permissions.md index adf7260..0350863 100644 --- a/zh/docs/reference/builder/permissions.md +++ b/zh/docs/reference/builder/permissions.md @@ -87,4 +87,4 @@ returnMap.put("gdsjurisdiction", jList); ## 关于 `plat_base_authority_*` -schema 中有三张 `plat_base_authority` 下的表(`plat_base_authority`、`plat_base_authority_button_type`、`plat_base_authority_data_type`),看起来像按钮类型和数据权限类型 lookup。**三张表在实时 DB 中都为空**;它们属于不在本 Wiki 范围内的 `xlyPlat*` B2B 平台层,不属于框架自身权限流。记录框架权限时不要引用它们;上面的 jurisdiction surface 是自包含的。 +schema 中有三张 `plat_base_authority` 下的表(`plat_base_authority`、`plat_base_authority_button_type`、`plat_base_authority_data_type`),看起来像按钮类型和数据权限类型 lookup。**三张表在实时 DB 中都为空**;它们属于不在本 Wiki 范围内的 `xlyPlat*` B2B 平台层,不属于框架自身权限流。记录框架权限时不要引用它们;上面的权限接口面是自包含的。 diff --git a/zh/docs/reference/maintainer/activiti.md b/zh/docs/reference/maintainer/activiti.md index 62983ba..bdda1df 100644 --- a/zh/docs/reference/maintainer/activiti.md +++ b/zh/docs/reference/maintainer/activiti.md @@ -1,66 +1,198 @@ # Activiti 集成 -> **完整覆盖暂缓**:Activiti 已接入代码库,但当前实时 DB 中没有部署流程。见[切片 7(暂缓)](../../slices/07-workflow.md)。 +> **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-的情况下处理工作流)。 -本页记录代码库中与 Activiti 相关的事实,避免未来维护人员在工作流真正启用时从零开始。 +本页记录实际已接线的内容(具体类、URL、流程引擎状态),以及要让它真正工作需要满足什么条件。 -## 两个 Activiti 版本 +## xly 如何在不使用 Activiti 的情况下处理工作流 {#xly-如何在不使用-activiti-的情况下处理工作流} -依赖树携带**两个** Activiti 版本: +xly 有**三种类似工作流的机制**,按实际使用程度从高到低排序如下。 + +### 路径 1:存储过程 + `bCheck` 标志的一步审批(主流模式) + +这是 xly 中 99% 审批的样子。这里**没有流程引擎,也没有状态机**;工作流就是那次过程调用本身。 + +机制: + +1. 每张业务表携带同一组三个审计列:`bCheck`(审批布尔值)、`sCheckPerson`、`tCheckDate`。在当前 dev DB 中,`bCheck` 出现在 **426** 张表上,`tCheckDate` 出现在 400 张表上,`sCheckPerson` 出现在 398 张表上。也就是说,几乎每个业务单据都把审批审计轨迹放在自己行里。 +2. 每个模块在 `gdsmodule.sProcName` 中声明一个**单一**审批过程名(列注释是“存储过程(审核)名称”)。例如报价模块的 `Sp_Quo_QuotationCheck`、销售订单的 `Sp_SalSalesCheck`。 +3. 用户点击“审核”按钮时: + - `POST /business/doExamine` → `BusinessBaseController.java:384-391` → `BusinessBaseServiceImpl.doExamine()` → `ExamineServiceImpl.doExcuExamine()`。 + - service 获取一个基于行 id 的 Redis 锁(`ws_update_*_{sGuid}_*`),避免两个用户并发审批。 + - 它通过通用过程调用机制分发到 `sProcName` 命名的过程,见[通用存储过程分发](proc-dispatch.md)。 + - **过程本身拥有业务逻辑**:校验必填字段、子行和主表合计是否平衡、相关单据是否锁定等。如果全部通过,过程更新该行:`bCheck = 1`、`sCheckPerson = `、`tCheckDate = NOW()`。 + - 过程返回 `OUT sCode INT`(1 表示成功,≤0 表示错误)和 `OUT sReturn LONGTEXT`(错误消息)。 +4. 反审核是同一调用,只是 `iFlag = 0` 而不是 `1`;过程同时处理两个方向。 +5. `Sp_System_CheckSave` 是每个带 `bCheck` 行保存后的通用钩子,由 `BusinessBaseServiceImpl.java:1828` 调用。它写入 `sFormId` 审计字段,并保留跨单据校验的占位逻辑(当前大多已注释)。 + +这条路径没有“下一个审批人”或“审批队列”的概念。一个有权限的用户点击审核且过程成功后,该行就审批完成。 + +### 路径 2:单据串联形成的隐式多步工作流 + +多单据业务流程(报价 → 客户确认 → 销售订单 → 发货 → 发票)不是靠一个单据在状态之间推进,而是靠**多个独立模块和表单**实现。用户视角是: + +1. 模块 A(例如 `quoQuotationMaster`):填单,点击审核,行变成 `bCheck = 1`。 +2. 模块 A 上有一个按钮(通过 `gdsconfigformslave.sButtonParam` 指向 `Sp_Quo_ToSalesOrder` 或类似过程)。点击后,过程创建模块 B(例如 `salSalesOrderMaster`)中的一行,并用模块 A 的数据预填。 +3. 用户进入模块 B,补充字段,再在那里审核,依次推进。 + +FROUNT [KPI 工作中心](runtime.md#kpi-工作中心front-端首页-dashboard) 上的 “01/04、02/04、03/04、04/04” 步骤编号反映的就是这个模型:每个“流程”是一个父模块,下面有 N 个有序子模块;步骤只是父 `gdsmodule` 条目下的子模块。没有流程引擎在跟踪“你处于四步中的第二步”;用户只是操作当前打开的单据,框架用父模块下子模块的 `iOrder` 提供上下文。 + +因此,多步“工作流”来自: + +- 一个按主题分组步骤的父 `gdsmodule`(例如 `估价管理流程`,包含 4 个子模块)。 +- 每个子模块自己的 `sProcName`(一步审批)。 +- 每个子模块自己的 `sButtonParam` 过程,点击后创建下一张单据。 +- 每张业务单据自己的 `bCheck` 标志。 + +没有状态机,也没有 FSM 库;只是**通过表单按钮把一串存储过程接起来**。这是当前 dev DB 中能看到的机制。 + +### 路径 3:Activiti BPMN 工作流(有闸门,目前代码中禁用) {#路径-3activiti-bpmn-工作流有闸门目前代码中禁用} + +路径 3 作为第三条通道存在于代码库中:启用后会经由 Activiti。但它没有在当前 dev DB 中运行,而且目前不重新编译无法启用。 + +闸门硬编码在 `xlyPersist/.../utils/ConstantUtils.java`: + +```java +public static Boolean bCheckflowCheck = false; +``` + +`ExamineServiceImpl.doExcuExamine()` 内部分发逻辑是: + +```java +if (ConstantUtils.bCheckflowCheck) { + Map reMap = checkExamineFlowService + .doSendCheckFolw(sGuid, sUserName, sBrandsId, sSubsidiaryId, + sFormId, map, searMap, sBtnName, request); + if (MapUtil.isNotEmpty(reMap)) { return reMap; } +} +``` + +所以即使租户部署了 BPMN,并通过 `gdsmoduleflow` 关联了模块,这个 `if` 也会因为 `bCheckflowCheck` 是 Java 常量 `false` 而短路。要启用路径 3,需要: + +1. 把源码中的 `ConstantUtils.bCheckflowCheck` 改为 `true` 并重新构建 WAR,或在运行时 patch 该常量。 +2. 插入一行以 `(sFormId, sBtnName)` 为 key 的 `gdsmoduleflow`,把表单上的审批按钮映射到已部署 BPMN。 +3. 通过 modeler 部署 BPMN,让 `act_re_procdef` 有数据。 +4. 确认相关模块的 `bCheck` 语义与 BPMN 起止事件对齐。 + +启用后,`CheckExamineFlowServiceImpl.doSendCheckFolw` 会读取 `gdsmoduleflow` 行,调用 `checkExamineFlowDataService.doSendCheckFolwData` 预置数据,再经 `doSendFlowUrl` 跳到 xlyFlow controller。随后 `ProcessServiceImpl.submitApply()` 调用 `runtimeService.startProcessInstanceByKey(...)`,Activiti 接管流程;`biz_flow` + `biz_todo_item` 填充,审批人在收件箱中看到任务,`CurrencyFlowController.complete(...)` 推进流程实例。 + +### 对比 + +| 方面 | 路径 1(proc + bCheck) | 路径 2(单据链) | 路径 3(Activiti) | +|---|---|---|---| +| 状态存储 | 单据行上的 `bCheck` 列 | 无;状态等于用户打开哪张单据 | `act_ru_task`、`act_hi_*`、`biz_flow`、`biz_todo_item` | +| 步骤转换 | 每张单据一步 | 每个链路按钮触发“转下一张单据”的过程 | 流程引擎按 BPMN 图驱动转换 | +| 转派 / 委托 | 不支持 | 不支持 | Activiti 支持 | +| 并行分支 | 不支持 | 不支持 | BPMN 网关支持 | +| 当前是否活跃 | 是,每次“审核”点击都会用 | 是,多单据业务流都在用 | 否,代码中 `bCheckflowCheck = false` | +| 工具 | 只有存储过程 | 存储过程 + 模块树配置 | `/modeler/*` 下的 BPMN modeler | + +### 真实路径 1 定制示例 + +[切片 5](../../slices/05-customer-sql-override.md#示例-2万昌构建多级审批工作流) 追踪了万昌的 `领班驳回.sql`:这是客户侧多级审批驳回的典型例子。它展示了客户在单个 `bCheck` 不够时如何扩展路径 1:`ALTER TABLE` 增加多个审批标志(`bManager`、`bIPQC`、`bDeputy` 等),按 `Sp_
_check_` 约定编写状态转换过程,并通过自定义 `sp_add_flow_log` 写审计。这是代码库中实证可见的定制通道;`script/客户/` 下没有任何目录部署 BPMN。 + +### 为什么这个设计适合 xly 的用户 + +印刷行业 ERP 客户的业务流程通常是规则驱动的(报价 → 订单 → 生产 → 发货 → 开票 → 收款),每一步按惯例都是**自己的单据和自己的表单**。用户预期的是“现在打开下一张表单继续填”,而不是“系统告诉我有一个 task 等我处理”。对这类用户: + +- 路径 1 + 路径 2 覆盖了当前 dev DB 中观察到的所有场景。 +- 路径 3 的价值(BPMN 建模、转派、并行网关)留给极少数审批图确实需要它的租户。 + +代价是:工作流逻辑**分散在存储过程中**,而不是集中声明在一个地方。给流程增加新步骤意味着写或改一个或多个过程,而不是编辑 BPMN 图。对复杂且频繁变化的流程,这会很脆弱;但对印刷厂现实中的 quote-to-cash 链条(每客户不常变化)来说,这是务实选择。 + +## Activiti 已接线:流程引擎已开启 + +尽管 dev DB 处于空转状态,流程引擎会随 `xlyEntry` 启动: + +- `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。 +- `xlyEntry/build.gradle` 通过 `api project(':xlyFlow')` 引入 xlyFlow,因此 starter 在 `xlyEntry` WAR 的运行时 classpath 上。 +- `xlyEntry/.../EntryApplicationBoot.java:23-24` 只排除了 `org.activiti.spring.boot.SecurityAutoConfiguration`(REST 端点安全适配器)和 Spring 自己的 `SecurityAutoConfiguration`。**Activiti 主流程引擎 auto-config 没有被排除**,所以引擎会启动。 +- `xlyFlow/.../activiti/config/ActivitiConfig.java` 是一个 `@Configuration implements ProcessEngineConfigurationConfigurer`,只做两件事:把图生成器字体设为中文友好的 `宋体`(activity / annotation / label fonts),并安装自定义 `ICustomProcessDiagramGenerator`。 +- `xlyApi` 的 `ApiApplicationBoot` 也没有排除 Activiti,但 xlyApi 不依赖 xlyFlow。因此 xlyApi classpath 上有流程引擎里的 `org.activiti.engine.identity.User` 类(仅由 `IdGen.java` 加密工具使用),但不会触发 Activiti auto-config。 + +实时 schema 中看到的 24 张基础 `act_*` 表,是流程引擎首次启动时通过 auto-DDL 创建的。 + +## classpath 上的两个 Activiti 版本 | 模块 | 版本 | 说明 | |---|---|---| -| `xlyPersist` | `org.activiti:activiti-engine:5.17.0` | 较老的 5.x 线 | -| `xlyFlow` | `org.activiti:activiti-spring-boot-starter-rest-api:6.0.0`、`activiti-json-converter:6.0.0` | 较新的 6.0 线 | +| `xlyPersist`、`xlyApi` | `org.activiti:activiti-engine:5.17.0` | 较老的 5.x 线,两个模块都有声明。**遗留依赖**;`xlyEntry` WAR 中实际运行的流程引擎是 xlyFlow starter 拉入的 6.0。5.17 声明只是 classpath 上的包袱。 | +| `xlyFlow` | `org.activiti:activiti-spring-boot-starter-rest-api:6.0.0`、`activiti-json-converter:6.0.0` | 较新的 6.0 线,这才是会运行的版本。 | -这是实际版本不匹配。Activiti 5.x 和 6.x schema 有重叠,但在部分 `act_*` 表和迁移路径上分叉。可能性包括: +做清理的维护人员应从 `xlyPersist` 和 `xlyApi` 的 `build.gradle` 中移除 `5.17.0`。已验证:这两个模块只有 `IdGen.java` 会触碰 `org.activiti.engine.identity.User`,而该类型签名由 6.0 流程引擎也能满足,因此移除是安全的。 + +## 代码中实际调用了什么 + +`xlyFlow/src/main/java/com/xly/activiti/` 下 154 个 Java 文件加 modeler 子包是真实调用点。选取锚点如下: + +| 活动 | 类 : 行 | 使用的 Activiti API | +|---|---|---| +| 启动流程实例 | `ProcessServiceImpl.submitApply()` :107 | `runtimeService.startProcessInstanceByKey(module, businessKey, variables)` | +| 完成任务 | `CurrencyFlowController.complete(...)` :167 / :200;`WechatFlowPostThread` :132 | `processService.complete(taskId, ...)` → `taskService.complete()` | +| 查询活跃任务 | `CurrencyFlowController` :409、:480 | `taskService.createTaskQuery().active().list()` | +| 查询运行中实例 | `CurrencyFlowController` :485、:659 | `runtimeService.createProcessInstanceQuery()` | +| 在 modeler 中保存模型 | `ModelerController.create()` :122 | `repositoryService.saveModel()` + `addModelEditorSource()` | +| 运行时部署 BPMN | `ModelerController.deploy()` :147 | `repositoryService.createDeployment().addString(name, bpmnXml).deploy()` | +| 列出流程定义 | `ProcessDefinitionController` :135 | `repositoryService.createProcessDefinitionQuery()` | +| 读取流程引擎配置 | `ProcessActController` :281 | `ProcessEngines.getDefaultProcessEngine()` | +| 把 xly 用户桥接到 Activiti identity | `act_id_user` / `act_id_group` / `act_id_membership` 是投影 xly `sftlogininfo*` schema 的**视图** | xly 不写 Activiti identity 表;这些视图伪装成 identity 表 | -1. 框架运行 Activiti 6.0(由 xlyFlow 驱动),而 xlyPersist 的 5.17 依赖是早期遗留。 -2. 不同服务因历史原因使用不同版本。 -3. 两者都在 classpath 中,但运行时只初始化一个。 +## modeler 暴露的 URL(xlyFlow controller 挂在 xlyEntry 端口上) {#modeler-暴露的-urlxlyflow-controller-挂在-xlyentry-端口上} -未来维护人员应:(a) 移除未使用版本避免混淆,(b) 记录实时 schema 使用哪个版本,(c) 验证 `act_*` 表布局与该版本精确匹配。 +xlyFlow 被 xlyEntry 作为库消费(`api project(':xlyFlow')`),因此 xlyFlow controller 会编译进 xlyEntry WAR,并在 xlyEntry context-path(`/xlyEntry`)下服务。重要 URL: -额外事实:`xlyFlow/build.gradle` 引入 Activiti 6 starter,但 `xlyFlow/src/main/java/com/xly/XlyFlowApplicationBoot.java` 被完全注释。因此代码存在,但本仓库当前并没有把 `xlyFlow` 呈现为明确可独立运行的 app。 +- `POST /xlyEntry/modeler/model/{modelId}/save`:保存 BPMN modeler XML。 +- `GET /xlyEntry/modeler/model/{modelId}/json`:为编辑器加载模型。 +- `GET /xlyEntry/modeler/editor/stencilset`:modeler stencil 定义。 +- `GET /xlyEntry/modeler/create` / `/modeler/deploy/{modelId}`:创建和部署。 +- `POST /xlyEntry/currencyFlow/complete/{taskId}/{sBrandsId}/{sSubsidiaryId}/{sUserId}`:完成任务(`CurrencyFlowController`)。 +- `POST /xlyEntry/currencyFlow/completeerp/{sBrandsId}/{sSubsidiaryId}/{sUserName}`:ERP 侧完成任务变体。 -## `act_*` schema +这些 URL 不在[内部 API 页](../../api-reference/internal.md)中完整编目,因为它们是很少触碰的工作流接口面;维护时以源码为准。 -实时 DB 当前有预期的 `act_*` 表,但本轮检查的关键表都是空的: +## `CheckFlowController.java` 实际包含什么 -- `act_re_deployment` = 0 -- `act_re_procdef` = 0 -- `act_ru_task` = 0 -- `act_hi_procinst` = 0 +这是一个需要明确标出的 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。 -## xly 的包装层 +## `act_*` schema 状态(当前 dev DB) -三个 xly 表包装 Activiti 集成: +| 表 | 行数 | 有数据时的含义 | +|---|---:|---| +| `act_re_model` | 0 | modeler 中保存的 BPMN 模型 | +| `act_re_procdef` | 0 | 已部署流程定义 | +| `act_ru_task` | 0 | 活跃等待任务 | +| `act_hi_procinst` | 0 | 历史流程实例 | +| `act_id_user` / `act_id_group` / `act_id_membership` | 视图 | 把 xly 的 `sftlogininfo*` 用户投影成 Activiti identity 形状 | +| `gdsmoduleflow` | 0 | xly 从 `gdsmodule` 到流程定义的关联 | +| `biz_flow` | 0 | xly 的每单据流程状态 | +| `biz_todo_item` | 0 | 待审批任务(xly wrapper,不是 Activiti 的 `act_ru_task`) | +| `biz_todo_copyto` | 0 | 流程抄送方 | -- [`biz_flow`](../../auto-catalog/tables/biz_flow.md):xly 每单据流程状态。 -- [`biz_todo_item`](../../auto-catalog/tables/biz_todo_item.md):待审批任务。 -- [`biz_todo_copyto`](../../auto-catalog/tables/biz_todo_copyto.md):流程抄送方。 -- [`gdsmoduleflow`](../../auto-catalog/tables/gdsmoduleflow.md) + `gdsmoduleflowslave`:模块流程窗口配置。 +所以 Activiti 当前是**彻底空转**:流程引擎在运行,schema 已就绪,没有流量。 -包装表在实时 DB 中也为空:`gdsmoduleflow = 0`、`biz_flow = 0`、`biz_todo_item = 0`、`biz_todo_copyto = 0`。 +## 什么会让它动起来 -启用时的模式:单据提交写入 `biz_flow` 行,同时启动 Activiti 流程实例;待审批人在 `biz_todo_item` 中看到任务;审批后流程实例推进并最终完成。 +要让一个流程真正运行,大致顺序如下: -## 代码中接入位置 +1. 工程师或 PM 打开 **modeler UI**(静态资源位于 `xlyFlow/src/main/resources/static/modeler/`,通过 `/modeler/*` 端点服务)。他们绘制 BPMN 并保存,`act_re_model` 填充。 +2. 在 modeler 中点击 *Deploy* → `ModelerController.deploy()` 调用 `repositoryService.createDeployment().addString(name, bpmnXml).deploy()` → `act_re_procdef` 填充。 +3. 某个 `gdsmodule` 行标记 `bCheck = 1`,并在 `gdsmoduleflow` 中插入一行,把该模块关联到已部署的 `act_re_procdef.KEY_`。 +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 侧行。 +5. 审批人在 FROUNT 收件箱中看到待办(很可能是“审批”tab,独立于 KPI 工作中心)。点击通过 / 驳回 → `CurrencyFlowController.complete()` → `taskService.complete()`。 +6. 当流程实例到达 `endEvent`,行的 `bCheck` 发生转换;下游过滤 `bCheck = 1` 的查询开始看到它。 -`xlyFlow/` 是专用模块。后续补全时应关注: +## xly 为什么要接 Activiti -- `xlyFlow` 的 Gradle build 引入 Activiti 6.0。 -- Activiti process engine 的 Spring Boot 配置。 -- `xlyEntry/com/xly/web/businessweb/` 中的 `CheckFlowController` 是 SPA 驱动工作流(审批 / 驳回 / 查看)的一个入口。 -- BPMN 流程定义若存在,应位于 `xlyFlow/src/main/resources/processes/` 或类似位置;当前代码库为空。 +代码库已有自己的 `biz_flow` / `biz_todo_item` 表,理论上可以手写审批系统。把 Activiti 放在后面的收益是: -## 让 Activiti 工作需要什么 +- 标准 BPMN 建模(JS modeler 使用 Activiti Explorer 同源的 stencilset)。 +- 免费的状态机语义:流程引擎处理“task A 完成 → task B 可用”,xly 不需要用 SQL 维护 FSM。 +- 图渲染能力(`ProcessActController` 中的页面转 PNG)。 -使用工作流的部署需要: +成本是:JVM 中多一个流程引擎,DB 中多一套可能发生 DDL 漂移的 schema,以及一个额外认证接口面(xly 通过 `act_id_*` 视图把它遮住)。 -1. 已部署 BPMN 流程定义(`act_re_procdef` 有数据)。 -2. 模块标记 `bCheck = 1`,并通过 `gdsmoduleflow` 关联到正确流程。 -3. 通过 `act_id_*` 或 xly 包装层分配审批用户。 -4. 保存端点在 `bCheck = 1` 模块上分支,启动流程实例,而不是(或除了)标准 add/update/delete。 +## 本页不是什么 -当有已部署流程的环境可用时,本页会成为正式参考;在此之前,把它视为“这里应该有什么”的清单。 +- 不是切片 7 的替代品。切片 7(暂缓)应基于一个真实运行流程的部署做端到端追踪。 +- 不是 modeler 教程。modeler 来自 Activiti 项目;xly 只是把它作为静态资源嵌入,没有修改。 +- 不是从 Activiti 迁移到其他方案的计划。那会是更大的架构决策,不是 wiki finding。 diff --git a/zh/docs/reference/maintainer/bi-engine.md b/zh/docs/reference/maintainer/bi-engine.md new file mode 100644 index 0000000..11e6461 --- /dev/null +++ b/zh/docs/reference/maintainer/bi-engine.md @@ -0,0 +1,140 @@ +# BI / KPI / 图表引擎 + +xly **没有**内置通用 OLAP / cube 引擎(没有 Mondrian、Saiku、MDX;随包里的 `olap4j-1.2.0.jar` 没有任何 Java import,属于 classpath 上的历史包袱,见[技术栈](tech-stack.md)里的 OLAP4J 说明)。 + +xly 真正提供的是一套**自研的元数据驱动看板 + KPI 层**。它和框架其他部分使用同一组原语:`gds*` 元数据行指向 `Sp_*` 存储过程,再由通用 Java service 渲染。本页按端到端路径说明它。 + +## 三个部分 + +| 部分 | 入口 | 支撑表 | Service | +|---|---|---|---| +| **图表**(卡片、柱线饼仪表盘组件、看板) | FROUNT 的 `/indexPage/commonChar` 模块;BACK 的图表配置管理页 | `gdsconfigcharmaster`(3,006 行)、`gdsconfigcharslave`(1,951 行) | `CharServiceImpl`(2,219 行,是 xlyBusinessService 里最重的类之一) | +| **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`(后台刷新) | +| **预置聚合过程** | 由图表元数据行调用 | n/a | 20 个 `Sp_chart_*` 过程 + 2 个 `Sp_KPI_*` 过程 + `spKPImodule` | + +## 图表:看板如何渲染 + +xly 中的一个图表就是 `gdsconfigcharmaster` 中的一行,关键列如下: + +| 列 | 作用 | +|---|---| +| `sId` | 图表 ID | +| `sParentId` | 所属模块(`gdsmodule.sId`) | +| `sChinese` / `sEnglish` / `sBig5` | 图表标题 | +| `sCharType` | 组件类型,见下方分布 | +| `sProcedureName` | 产出图表数据的存储过程 | +| `sProcedureParam` | 存储过程参数 JSON 规格 | +| `iWidth` | 布局跨度(24 列栅格) | + +实时 dev DB 中的 `sCharType` 分布: + +| `sCharType` | 数量 | 渲染内容 | +|---|---:|---| +| `Div` | 1558 | 容器 / 纯布局块 | +| `sLabel` | 1143 | 单值文本卡片,例如“今日销售额:¥X” | +| `Progress` | 137 | 进度条 | +| `sPie` | 52 | 饼图 | +| `commonList` | 45 | 内嵌数据表格(复用通用 grid) | +| `sColumnarGroup` | 30 | 分组柱状图 | +| `sColumnar` | 28 | 单系列柱状图 | +| `sBrokenLine` | 5 | 折线图 | +| `sBar` | 3 | 横向条形图 | +| `ColorBlock` | 3 | 类热力块的彩色块 | +| `sGauge` | 2 | 仪表盘组件 | + +从表行(`gdsconfigcharslave`)保存多系列 / 多列图表所需的系列或列拆分。 + +运行时路径: + +```text +SPA 打开 /indexPage/commonChar?sModelsId=<看板模块 id> + │ + ▼ +GET /xlyEntry/business/getModelBysId/ + → 返回看板的元数据复合结果 + (formData 中包含来自 gdsconfigcharmaster + slave 行的图表布局) + │ + ▼ +对每个图表: + POST /xlyEntry/business/getXxx(CharServiceImpl 方法) + 携带图表的 sProcedureName + sProcedureParam + → CharServiceImpl 通过通用存储过程分发调用对应的 Sp_chart_* 过程 + │ + ▼ +SPA 用前端 ECharts 渲染每张卡片;一行图表元数据对应一张卡片 +``` + +## 20 个 `Sp_chart_*` 过程 + +| 过程 | 计算内容 | +|---|---| +| `Sp_chart_home_11`、`Sp_chart_home_13` | 首页看板卡片 | +| `Sp_chart_TodayOrder`、`Sp_chart_TodayOrder_hm`、`Sp_chart_ThisMonthQty`、`Sp_chart_MonthOrder`、`Sp_chart_MonthTeamQty` | 当天 / 当月订单数、班组产量 | +| `Sp_chart_TodayProfit`、`Sp_chart_MonthProfit`、`Sp_chart_TodayReceivables`、`Sp_chart_TodayReceive`、`Sp_chart_expenses` | 财务:利润、应收、收款、费用汇总 | +| `Sp_chart_EquipmentLoad`、`Sp_chart_EquipmentLoad1`、`Sp_chart_EquipmentLod1`、`Sp_chart_EquipmentLast`、`Sp_chart_sMachine_speed`、`Sp_chart_Bottleneck` | 车间:设备负载、最后运行状态、当前瓶颈 | +| `Sp_chart_OrderProcess`、`Sp_chart_WorkOrderProcess` | 订单 / 工单进度时间线 | + +每个过程都遵循标准的 `(IN sLoginId, IN sBrId, IN sSuId, ...) → result-set` 形状,因此通用分发器可以直接调用。[多租户作用域](../../concepts/multi-tenancy.md)也会自然传入:每张图表都会自动按租户过滤。 + +## dev DB 中的预置看板模块 + +`/indexPage/commonChar` 是共享路由。dev DB 中有 6 个模块映射到它: + +| Module sId | 中文名称 | +|---|---| +| `19211681019715464089035510` | 销售图表分析 | +| `19211681019715481435115760` | 财务图表分析 | +| `19211681019715481435298200` | 生产图表分析 | +| `19211681019715708435449190` | 销售大数据分析 | +| `19211681019715708471874620` | 采购大数据分析 | +| `101251240115015889205266000` | 采购价格分析查询 | + +六个模块全部由 `gdsconfigcharmaster` 行驱动;新增或修改它们不需要改 Java 代码。 + +## KPI 子系统 + +> **先消歧。** FROUNT 首页也有一个标题为“**KPI监控**”的卡片,但它**不是**本页记录的 KPI。首页卡片是 `BusinessModelCenterController.getModelCenter` 提供的未清任务计数器,读取 `gdsmodule.bUnTask` / `sUnType`,没有目标值、评分和图表,只是名字容易误导。见运行时页的 [KPI 工作中心](runtime.md#kpi-工作中心front-端首页-dashboard)。下面的 `kpi*` 表族才是真正的员工绩效评分层。 + +`kpi*` 是独立于图表渲染的**按员工绩效评分**层。形状如下: + +| 表 | 作用 | 实时行数 | +|---|---|---:| +| `kpimaster` | 按员工、按期间的 KPI 汇总。它是容量最大的表:每个员工的每次评分事件一行。 | 124,524 | +| `kpidetail` | 支撑每条 `kpimaster` 汇总的明细行。 | 1,308 | +| `kpimodule` | KPI 定义:哪些模块 / 指标参与评分。 | 44 | +| `kpimoduleuser` | 每用户 KPI 分配。 | 0(dev DB 未分配) | +| `kpimoduleuserday` | 每用户每日 KPI 桶。 | 1 | +| `kpislavel` | KPI 等级 / 档位定义。 | 0 | + +Java 侧: + +- `KpiServiceImpl.java`(833 行,位于 `xlyBusinessService/.../KPIService/`):KPI 事件的读写 API。 +- `BusinessModelKpiServiceImpl.java`(901 行):把业务事件数据计算成 KPI 行的计算层。 +- `FlushModleKpiThread.java`:后台重算线程。 +- `KpimasterCloum.java` enum(xlyPersist):列名常量。 + +存储过程: + +- `Sp_KPI_DetailByEmployee`:按员工的明细报表。 +- `Sp_KPI_SumByEmployee`:按员工的汇总报表。 +- `spKPImodule`:按模块重算 KPI。 + +`script/客户/` 下也有较大的客户覆盖,例如 `script/标版/30100101/spKPImodule.sql` 以及多个 `Sp_SalesOrder_Kpi*` 过程。这与[每客户 SQL 覆盖通道](../../slices/05-customer-sql-override.md)一致:需要不同 KPI 规则的客户会交付自己的过程。 + +## 自研方案的代价 + +元数据 + 每图表一个过程的设计与 xly 的数据驱动论点一致,也避免了携带重型 OLAP 引擎。但代价很明确: + +1. **每张新图表都需要 SQL 作者。** “PM 添加一行元数据”只在工程师已经写好配套 `Sp_chart_*` 过程之后才成立。这里没有聚合构建器、字段选择器或自动生成查询;每个指标都是工程团队手写、评审和维护的存储过程。20 个过程和 11 种图表类型就是当前系统能渲染的全部形状。 +2. **图表在 OLTP DB 上跑重 SQL。** 没有数仓、没有预聚合、没有增量汇总。“今日利润”图表就是在实时交易 schema 上做 SELECT。大客户加载图表时会和录单负载竞争同一个 MySQL 实例。缓存有帮助,但只对命中有效;元数据变更后的第一次加载仍要付完整成本。 +3. **图表之间没有语义一致性保证。** 每个 `Sp_chart_*` 过程自行决定如何计算“月利润”“今日销售额”等指标。两个看似展示同一指标的图表可能因为过程体不同而悄悄不一致。真正的语义层可以避免这个问题,自研模型不能。 +4. **不能钻取,也不能自由切片分析。** 每张图表都是固定查询形状。用户不能自由切换维度,也不能从汇总卡片钻取到底层交易,除非工程师为每条路径再写一个过程。 +5. **KPI 逻辑会按客户分歧。** `script/客户/` 下的客户会交付自己的 `spKPImodule` 和 `Sp_SalesOrder_Kpi*` 覆盖;不同客户的 KPI 算法不同,而且代码只存在于该客户 DB 中。这会让“这个 KPI 到底是什么意思”取决于当前连接的是哪个 schema。 + +这种简单设计足够支撑“展示 xly 一直展示的那 20 张卡片”。如果目标是即席分析或自助报表,它就不够了;那需要一套 xly 当前没有的独立语义层 / 数仓层。 + +## 它不是什么 + +- **不是自助 BI 工具。** 客户不能随便指向一张表并拖拽生成图表;新图表需要一个 SQL 存储过程,以及懂得注册元数据行的管理员。 +- **不是实时分析基础设施。** 图表在缓存 miss 时会直接在 OLTP MySQL schema 上运行过程。没有独立数仓、没有增量聚合管道、没有流式层。大客户的大图表会在实时 DB 上执行重 SQL。 +- **不是列存 / OLAP 引擎支撑的分析。** `xlyPersist/build.gradle` 中的 `olap4j` jar 没有任何 Java import,只是 classpath 上的历史包袱。xly 通过 MyBatis 和通用存储过程分发使用 MySQL 的普通行存。 diff --git a/zh/docs/reference/maintainer/cache-invalidation.md b/zh/docs/reference/maintainer/cache-invalidation.md index ab22d80..1af36dc 100644 --- a/zh/docs/reference/maintainer/cache-invalidation.md +++ b/zh/docs/reference/maintainer/cache-invalidation.md @@ -1,61 +1,126 @@ # 元数据变更后的缓存失效 -当 PM 在**后台**保存变更(给表单加列、更新权限、注册新模块)时,每个运行节点都必须丢弃对旧元数据的缓存解释。xly 通过 JMS 完成,而不是轮询。 +当 PM 在 BACK 保存变更(给表单加列、更新权限、注册新模块)时,框架必须丢弃对旧元数据的缓存解释。**缓存清理由 BACK 进程内的 Spring `@CacheEvict` 同步完成**,不是 JMS 扇出。代码里另有一条名字相近的 JMS 路径,但用途不同(基础数据合并);两者很容易混淆,本页专门拆开说明。 -## 路径 +## 一个触发点,两条路径:差异在哪里 + +```mermaid +flowchart TB + classDef ok fill:#e6f4ea,stroke:#34a853 + classDef notcache fill:#fce8e6,stroke:#ea4335 + + PM[PM 在 BACK 点击保存]:::ok + SAVE["BusinessBaseServiceImpl
add/update/deleteBusinessData"] + EVICT["BusinessCleanRedisData.delCleanRedisData
→ CleanRedisServiceImpl
gdsmodule 相关 18 个 cache region"]:::ok + REDIS[("Redis
(跨节点共享)")]:::ok + DB[("MySQL
写入行")]:::ok + + PM --> SAVE + SAVE --> DB + SAVE -- "同步执行,
同一事务路径" --> EVICT + EVICT --> REDIS + REDIS -. "任意节点下次读取
都能看到新值" .-> ANY[其他节点]:::ok + + SAVE -. "发布 'gds module changed'" .-> AMQ([ActiveMQ]) + AMQ --> CGM["ConsumerChangeGdsModuleThread
(xlyErpJmsConsumer)"]:::notcache + CGM -- "调用
PRO_ERPMERGEBASEGDSMODULE" --> DB2[("MySQL
基础数据合并
不是缓存")]:::notcache + + classDef title font-weight:bold +``` + +**绿色路径**才是每次“修改元数据后刷新页面”实际依赖的路径。**红色路径**因为队列名(`CHANGE_GDS_MODULE`)和 consumer thread 名字,很容易被误认为缓存失效;但它不是。它通过存储过程做每租户 → base 数据合并。**两条路径互不依赖。** + +## 真实缓存失效路径(同步、进程内) ```text -PM 在**后台**保存 - ↓ -**后台**控制器写入变更的 gds_* 行 - ↓ -Controller 发布 JMS “module changed” 消息 - ↓ -每个节点的 xlyErpJmsConsumer 收到消息 - ↓ -ConsumerChangeGdsModuleThread.run() 清除相关 Redis key - ↓ -任意节点下一次 /business/getModelBysId 调用重新读表, -并用新值重新填充缓存 +PM 在 BACK 保存 + │ + ▼ +BACK controller(如 /business/addUpdateDelBusinessData)调用 +BusinessBaseServiceImpl.addBusinessData / updateBusinessData / deleteBusinessData + │ + ▼ +保存 service 调用 businessCleanRedisData.delCleanRedisData(...) +(如 BusinessBaseServiceImpl.java:1122、1224、1375、1441、1597、1677) + │ + ▼ +BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(, ...) +按表名分发到 CleanRedisServiceImpl 上的某个清理方法 + │ + ▼ +CleanRedisServiceImpl.cleanRedisByTableNameGdsModle()(或类似方法) +对固定的一组 cache region 触发 @CacheEvict + │ + ▼ +Spring CacheManager 清理命名条目 + │ + ▼ +下一次 /business/getModelBysId 调用重新从 DB 读取并回填缓存 ``` -处理器位于 `xlyErpJmsConsumer/src/main/java/com/xly/xlyerpjmsconsumer/thread/ConsumerChangeGdsModuleThread.java`。 +清理方法位于 `xlyBusinessService/src/main/java/com/xly/service/impl/CleanRedisServiceImpl.java`。一个代表性方法(`gdsmodule` 行变更时调用)会一次性清理 18 个 cache region: + +```java +@CacheEvict(value = { + "getGdsmoduleTree", "getGdsmoduleList", "getModuleTreePro", + "getSysjurisdictionTreePro", "getsDisplayTypeAll", + "businessBaseServiceGetMenuList", "getBuMenu", "getMenu", + "getsAuthsId", "businessCommonServicegetModulelistAll", + "gdsmoduleById", "getSaveProName", "businessParameterGetParameter", + "getPrcName", "getKpiModelByUser", "getUserByFromId", + "getUserByActionId", "getModuleTreeProAll" +}, allEntries = true) +public void cleanRedisByTableNameGdsModle() { … } +``` + +同一个类上还有其他按表命名的 cleaner,分别清理与 `gdsconfigformmaster`、`gdsconfigformslave`、`gdsconfigtbmaster`、`gdsformconst`、`gdsjurisdiction`、`gdsconfigcharmaster`、登录信息、billnosetting、kpimaster、`SysSystemSettings` 等相关的缓存区域。 + +## JMS 的 `CHANGE_GDS_MODULE` 实际做什么(不是清缓存) + +框架中确实有 `P2pQueue.ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` 队列和 `ConsumerChangeGdsModuleThread` consumer thread,名字看上去像是做缓存失效,但事实不是。 -## 为什么是 JMS,不是轮询后清除 +`ConsumerChangeGdsModuleThread.run()` 解析 `changeGdsModuleService` bean(`ChangeGdsModuleServiceImpl`),调用 `changeTableData(sGdsModuleId, sJobId)`,进而调用存储过程 `PRO_ERPMERGEBASEGDSMODULE`(通过 `proDao.proErpMergeBaseGdsModule`,映射在 `ProMapper.xml`)。该存储过程把每租户 `gdsmodule` 行合并进扁平“base”查询表。这是基础数据合并作业,不是缓存清理。对 `xlyErpJmsConsumer/` grep `@CacheEvict` 或 `cleanRedis*` 结果为 0;consumer 侧不会清 Redis。 -xly 经常跨多个节点运行(xlyEntry、xlyApi、xlyInterface 各自 JVM,有时水平扩展)。轮询“元数据是否变了”要么很慢(直到下一次轮询才可见),要么很吵(持续心跳)。JMS 可以在毫秒级把失效广播到每个节点。 +[`P2pQueue.java`](../../api-reference/messaging.md) 中另外 23 个 `ERP_JMS_ACTIVEMQ_*` 队列也一样:每个队列驱动一个领域特定的基础数据合并或扇出工作项,而不是缓存失效。 -代码库同时使用 **ActiveMQ** 和 **RocketMQ**,但这里记录的元数据变更路径是 **ActiveMQ / JMS**:`xlyErpJmsConsumer` 用 `@JmsListener` 监听 `P2pQueue.ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE`,`ConsumerChangeGdsModuleThread` 负责清缓存。`RocketMQServiceImpl` 用于其他集成流。 +## 跨节点缓存一致性:Redis 支撑,已确认 -## 会清哪些 key +`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`**。 -Redis 缓存包含: +已在实时 dev Redis `118.178.19.35:16379`(database 0)验证:267 个 key 中有 233 个使用 Spring Cache 默认的 `::` 分隔符。与 `BusinessGdsconfigformsServiceImpl.java:189-190` 中 `getFormconstData` 的 `@Cacheable` 注解(默认 key 来自所有参数)匹配的 key 形状示例: -- 按 `sId` 的模块元数据。 -- 按 `sId` 的表单元数据。 -- 按表单 `sId` 的字段从表列表。 -- 每租户覆盖合并结果(派生缓存)。 -- 每(模块、角色)的权限规则。 +```text +businessGdsconfigformsServiceGetFormconstData::{sLanguage=sChinese, sModelsId=…, sSubsidiaryId=1111111111, sUserId=…, sBrandsId=1111111111} +gdsmoduleById::gdsmoduleById___ +``` -consumer thread 收到变更行 ID 后,会清掉每个可能包含它的缓存 key 家族。**这里过度失效是安全选择**:下一次请求多读一次 DB 的成本,远小于继续服务陈旧元数据的成本。 +因此,任一节点上的 `@CacheEvict` 会清理共享 Redis 存储,其他节点下一次读取时也会看到失效。跨节点一致性来自 Redis,不来自 JMS。 ## 直接用 SQL 修改元数据时 -通过 MyBatis 或通过**后台**做的 insert / update 会*触发* JMS 事件。工程师直接在生产 DB 执行 `UPDATE gdsmodule SET ...` 不会触发。缓存会持续提供旧元数据,直到: +通过 MyBatis 或 BACK 完成的 insert / update 会触发 `businessCleanRedisData.delCleanRedisData*`。直接在 DB 执行 `UPDATE gdsmodule SET …` 不会触发任何 cleaner。缓存会继续提供旧元数据,直到: 1. 缓存 TTL 过期(实际 TTL 看缓存配置)。 -2. 重启应用服务器。 -3. 手工发送 JMS 消息(见 `xlyBusinessService` 中的 `BusinessCleanRedisDataImpl`)。 +2. 重启应用服务器(由于缓存由 Redis 支撑且共享,重启一次即可,见上文)。 +3. 从应用内部调用某个 `BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(
, …)` 方法;任意节点调用一次即可,因为它清的是共享 Redis 存储。 + +## 这个设计的代价 + +保存时同步 `@CacheEvict` 的模型运维上简单;在 Redis 支撑下,它也确实具备跨节点一致性。但它仍有几个脆弱点需要明确: -第三种是受支持的 workaround;第二种是粗暴 fallback。 +- **两套名字很像的系统容易混淆。** JMS 路径 `CHANGE_GDS_MODULE` + `ConsumerChangeGdsModuleThread` 听起来像缓存失效,实际不是。这页存在的一部分原因,就是这种混淆反复造成 bug 和阅读误判。如果能把队列和过程重命名为类似 `MERGE_BASE_GDS_MODULE`,会更清楚,但改名成本不低。 +- **驱逐和写入在同一条事务路径上。** 如果保存期间 Redis 调用失败,数据库行可能已经提交,但缓存仍旧是旧值。框架不会检测或自动恢复这种情况;保存时 Redis 短暂故障会让受影响行在 TTL 到期前一直读到旧缓存。 +- **驱逐粒度是按 cache region 全量清。** `CleanRedisServiceImpl` 上大多数 `@CacheEvict` 都使用 `allEntries=true`,清掉整个 cache region,而不是只清受影响 key。元数据保存吞吐较高时,后续会出现一波 cache miss;小元数据缓存可以接受,但如果 region 有几千项就会变贵。 +- **没有驱逐预算或批处理。** 批量元数据变更(例如一次改 100 个字段)会触发 100 次 `@CacheEvict`,每次都往返 Redis。没有把多次驱逐合并成一批的机制。 +- **直接 DB 写入会绕过全部机制。** 任何绕开 `BusinessBaseServiceImpl` 的工具都会留下陈旧缓存,包括 DBA 脚本、通过 `mysql` 命令应用的 `script/客户/` 覆盖、以及通道 2 的 SQL 替换。对 xly 实际采用的部署模式来说,这是实打实的运维风险。 ## 常见 bug:问题其实是缓存 当现象像是“我改了但页面还是旧值”时,按顺序检查: 1. 变更是否实际提交?(对 DB 执行 `SELECT` 确认。) -2. **后台**节点能否访问 JMS broker?(不能访问时,失效事件不会发布。) -3. 所有 consumer 节点是否在运行?(暂停的节点会继续服务旧元数据直到重启。) -4. 变更是否通过原始 SQL 完成?(那就没有 JMS 事件,需要手工触发。) +2. 变更是否经过会调用 `BusinessCleanRedisData` 的路径?(直接 DB 写入或绕过 `BusinessBaseServiceImpl` 的 controller 不会。) +3. 保存提交时 Redis 是否可达?驱逐失败不会回滚保存。 +4. 这个变更所在表是否映射到了会被清理的 cache region?`CleanRedisServiceImpl` 按写入表映射到具体 region;未映射的表不会让对应读取缓存失效。 -[切片 1](../../slices/01-hello-world.md) 的“五表读取”会从缓存重新运行;理解哪一层陈旧是定位 bug 的关键。 +[`getModelBysId` 在切片 1](../../slices/01-hello-world.md) 中返回的五键复合体会从缓存重跑;定位 bug 的关键是理解哪一层陈旧。 diff --git a/zh/docs/reference/maintainer/deployment.md b/zh/docs/reference/maintainer/deployment.md index 54d865d..dcfb616 100644 --- a/zh/docs/reference/maintainer/deployment.md +++ b/zh/docs/reference/maintainer/deployment.md @@ -1,65 +1,162 @@ # 多服务部署 -xly 不是单个 Spring Boot WAR。仓库包含多个可部署模块,以及一些也被 `xlyEntry` 作为依赖使用的类库型 WAR 模块。 +xly 不是单个 Spring Boot WAR。仓库包含多个可部署模块,以及一些也被 `xlyEntry` 作为依赖使用的类库型模块。 + +## 拓扑总览 + +```mermaid +flowchart LR + classDef library fill:#f5f5f5,stroke:#999,stroke-dasharray:3 3 + classDef boot fill:#e8f0fe,stroke:#4285f4 + + subgraph clients [面向操作人员] + BACK["BACK SPA
http://<host>:8597"] + FROUNT["FROUNT SPA
http://<host>:8598"] + end + EXT([外部集成方]) + HOOKS([第三方 webhook]) + + subgraph boots [可部署 Spring Boot 应用] + direction TB + XENTRY["xlyEntry
:8080 /xlyEntry
EntryApplicationBoot"]:::boot + XAPI["xlyApi
:8090 (local) / :8080
/xlyApi · ApiApplicationBoot"]:::boot + XIF["xlyInterface
:8080 /xlyInterface
InterfaceApplicationBoot"]:::boot + XPLC["xlyPlc
:8000 (dev) / :8080
/xlyEntry · PlcApplicationBoot"]:::boot + XFACE["xlyFace
:8091 (local) / :8080
/xlyFace(文档范围外)"]:::boot + XEJMSC["xlyErpJmsConsumer
(无端口;继承配置)
JmsConsumerApplicationBoot"]:::boot + end + + subgraph libs [类库模块:不独立运行] + direction TB + XMANAGE[xlyManage]:::library + XBSERVICE[xlyBusinessService]:::library + XPERSIST[xlyPersist]:::library + XENTITY[xlyEntity]:::library + XFLOW[xlyFlow]:::library + XMSG[xlyMsg]:::library + XEJMSP[xlyErpJmsProductor]:::library + XPLATC[xlyPlatConstant]:::library + end + + subgraph infra [共享基础设施] + DB[("MySQL
xlyweberp_*")] + REDIS[(Redis :16379
共享缓存 + session)] + AMQ([ActiveMQ :61616]) + MONGO[("MongoDB
已接线但未使用")] + end + + BACK -->|nginx| XENTRY + FROUNT -->|nginx| XENTRY + FROUNT -->|nginx| XAPI + EXT --> XAPI + HOOKS --> XIF + + XENTRY --- XMANAGE + XENTRY --- XBSERVICE + XENTRY --- XFLOW + XBSERVICE --- XPERSIST + XAPI --- XPERSIST + XIF --- XPERSIST + XPERSIST --- XPLATC + XPERSIST --- XENTITY + XBSERVICE --- XMSG + XIF --- XMSG + XBSERVICE --- XEJMSP + + XENTRY --> DB + XAPI --> DB + XIF --> DB + XPLC --> DB + XFLOW --> DB + + XENTRY --> REDIS + XAPI --> REDIS + XEJMSC --> AMQ + XEJMSP -. 发布 .-> AMQ + XENTRY -. 发布 .-> AMQ +``` + +反向代理把面向操作人员的端口(8597 / 8598)映射到内部 Spring Boot 应用(大多是 `:8080`,再通过不同 context path 区分)。类库模块不会独立运行,而是作为依赖打进可部署 WAR。`xlyFlow` 和 `xlyPlc` 都共享 xlyEntry 的 context-path `/xlyEntry`,所以真实部署需要按 host 或 upstream 路由,而不能只靠 path 区分。 ## 主要模块 -| 服务 / 模块 | 角色 | 代码事实 | -|---|---|---| -| **xlyEntry** | 主运行时与 builder/admin surface。承载 `/business/*`、`/gdsmodule/*`、`/gdsconfigform/*`、`/gdsconfigtb/*`、报表、登录和其他框架 controller。 | 依赖 `xlyManage`、`xlyBusinessService`、`xlyFlow`。 | -| **xlyApi** | 面向 `/api/*`、`/online/*`、`/pro/*`、`/thirdparty/*` 等端点的 API 模块。 | 独立 Spring Boot 应用类 `ApiApplicationBoot`。 | -| **xlyInterface** | 外部集成模块,带 Swagger 依赖和第三方集成代码。 | 独立 Spring Boot 应用类 `InterfaceApplicationBoot`。 | -| **xlyPlc** | 车间 PLC 桥接([切片 6](../../slices/06-hardware.md))。 | 独立 Spring Boot 应用类 `PlcApplicationBoot`。 | -| **xlyFace** | 可选人脸识别模块。 | 独立模块,仍存在于 Gradle build 中。 | -| **xlyFlow** | 工作流 / Activiti 代码和 controller。 | 作为模块存在,但当前分支中 `XlyFlowApplicationBoot` 被注释,因此应视为通过 `xlyEntry` 消费的代码,而不是明确可独立运行的 app。 | +### 可部署的 Spring Boot 应用 + +| 服务 / 模块 | 角色 | 默认 profile / 端口 | Boot 类 | +|---|---|---|---| +| **xlyEntry** | 主运行时与配置 / 管理接口面。承载 `/business/*`、`/gdsmodule/*`、`/gdsconfigform/*`、`/gdsconfigtb/*`、报表、登录和其他框架 controller。 | `dev` → 8080,context `/xlyEntry` | `EntryApplicationBoot` | +| **xlyApi** | 面向 `/api/*`、`/online/*`、`/pro/*`、`/thirdparty/*` 等端点的 API 模块。 | repo 默认 `local` → 8090,dev/win/linux → 8080,context `/xlyApi` | `ApiApplicationBoot` | +| **xlyInterface** | 外部集成模块,带 Swagger 依赖和第三方集成代码。 | `dev` → 8080,context `/xlyInterface` | `InterfaceApplicationBoot` | +| **xlyPlc** | 车间 PLC 桥接([切片 6](../../slices/06-hardware.md))。 | `dev` → 8000;命名 profile(15S、S10、T0、T1、CT、yt、pro)→ 8080,context `/xlyEntry`(与 xlyEntry 共享 context-path) | `PlcApplicationBoot` | +| **xlyFace** | 人脸识别模块。在 build 中(`settings.gradle` 仍按用户设置启用),但**不属于本 Wiki 文档范围**。 | repo 默认 `win` → 8080,local → 8091,context `/xlyFace` | `XlyFaceApplicationBoot` | +| **xlyErpJmsConsumer** | JMS consumer 后台服务。有 Boot main,但没有自己的 `application*.yml`;运行配置从同类服务继承。 | n/a(继承) | `JmsConsumerApplicationBoot` | + +### 类库模块(在 `settings.gradle` 中启用,但不独立运行) + +| 模块 | 角色 | +|---|---| +| **xlyManage** | 后台元数据管理服务(`Gds*ServiceImpl` 家族),被 xlyEntry 拉入。 | +| **xlyBusinessService** | 业务逻辑 service 层(`BusinessBaseServiceImpl` 和约 100 个兄弟 `*ServiceImpl` 类),被 xlyEntry 拉入。 | +| **xlyFlow** | 工作流 / Activiti 代码。当前分支 `XlyFlowApplicationBoot.java` 已完全注释掉;作为类库被 xlyEntry 使用。也共享 context-path `/xlyEntry`。 | +| **xlyEntity** | 共享实体 / DTO 类(约 83 个 Java 文件,包括 22 个 Mongo `@Document` 类)。 | +| **xlyPersist** | 持久化 helper(DAO、MyBatis mapper XML、`RequestAddParamUtil` 等)。 | +| **xlyMsg** | 通知 helper(钉钉、微信、邮件);没有 Boot main。 | +| **xlyErpJmsProductor** | JMS producer 代码(队列声明在 `P2pQueue.java`);没有 Boot main。 | +| **xlyPlatConstant** | 共享工具常量(`MultiThreadServer`、`TimeContant`),被 `xlyPersist` 消费。唯一仍启用的 Plat* 模块。 | -每个模块有自己的 `application.yml` 和多个 `application-.yml`。启动时通过 `-Dspring.profiles.active=...` 选择 profile。 +每个可运行模块有自己的 `application.yml` 和多个 `application-.yml`。启动时通过 `-Dspring.profiles.active=...` 选择 profile。 ## `settings.gradle` 中禁用的模块 -```text -//include 'xlyErpTask' -//include 'xlyRxtx' -//include 'xlyFile' -``` +cleanup 分支注释掉了 17 条 `include`。其中三个是磁盘上存在的非 Plat 模块: -三者在磁盘上存在,但被排除在当前 Gradle build 之外: +- `xlyErpTask`:长运行后台任务。 +- `xlyRxtx`:原生串口库。xlyPlc 需要直接串口访问时可能重新启用;部分机型使用 TCP / Ethernet。 +- `xlyFile`:旧文件管理模块,已被 `xlyPlatFileUpload`(也被注释)取代。 -- `xlyErpTask`:长运行**后台**任务。 -- `xlyRxtx`:原生串口库。xlyPlc 不需要直接串口访问时会禁用;部分机型使用 TCP / Ethernet。 -- `xlyFile`:旧文件管理模块,已被 `xlyPlatFileUpload` 中的阿里云 OSS 集成取代。 +其余 14 条被注释的 include 是 `xlyTestService`、`xlyTestController`,以及除 `xlyPlatConstant` 外的完整 `xlyPlat*` 家族:`xlyPlatTask`、`xlyPlatJmsProductor`、`xlyPlatJmsConsumer`、`xlyPlatReportForm`、`xlyPlatFileUpload`、`xlyPlatMarketingService`、`xlyPlatUserService`、`xlyPlatSmsService`、`xlyPlatMerchantController`、`xlyPlatWebsocket`、`xlyPlatPayService`、`xlyPlatCainiaoWaybillSevice`。(`xlyTestService` / `xlyTestController` 目录不在磁盘上;只有 `xlyEntry/.../businessweb/` 里存在一个空壳 `TestController.java`。) -维护人员清理代码库时应判断删除还是保留为历史参考。它们占用磁盘空间,但不影响 build。 +维护人员清理代码库时应判断是否删除磁盘上但已排除的 `xlyErpTask` / `xlyRxtx` / `xlyFile` 目录,或保留为历史参考。它们占用磁盘空间,但不影响 build。 -## Plat* 表族 +## Plat* 家族 -`xlyPlat*` 模块(`xlyPlatMerchantController`、`xlyPlatUserService`、`xlyPlatPayService`、`xlyPlatMarketingService`、`xlyPlatCainiaoWaybillSevice`、`xlyPlatSmsService`、`xlyPlatReportForm`、`xlyPlatFileUpload`、`xlyPlatJmsConsumer` / `Productor`、`xlyPlatTask`、`xlyPlatWebsocket`、`xlyPlatConstant`)属于 **B2B 印刷平台层**,不在本 Wiki 范围内。 +`xlyPlat*` 模块(`xlyPlatMerchantController`、`xlyPlatUserService`、`xlyPlatPayService`、`xlyPlatMarketingService`、`xlyPlatCainiaoWaybillSevice`、`xlyPlatSmsService`、`xlyPlatReportForm`、`xlyPlatFileUpload`、`xlyPlatJmsConsumer` / `Productor`、`xlyPlatTask`、`xlyPlatWebsocket`)属于 **B2B 印刷平台层**,不在本 Wiki 范围内。唯一例外是 `xlyPlatConstant`,它仍在 `settings.gradle` 中启用,并被 `xlyPersist` 当作共享常量工具使用(`MultiThreadServer`、`TimeContant`)。 ## 服务如何互相发现 三种通信通道: 1. **共享数据库**:每个服务读写同一个 MySQL schema。大多数跨服务“通信”是通过共享表隐式发生。 -2. **消息**:代码库中同时存在 ActiveMQ / JMS 和 RocketMQ。缓存失效([元数据变更后的缓存失效](cache-invalidation.md))使用 ActiveMQ / JMS 路径。 +2. **消息**:代码库中同时存在 ActiveMQ / JMS 和 RocketMQ。ActiveMQ / JMS 路径用于基础数据合并和异步扇出;缓存失效不走 JMS,见[元数据变更后的缓存失效](cache-invalidation.md)。 3. **HTTP REST**:同步调用,例如 xlyApi 调用 xlyEntry 的 `/business/*` 端点。 除 `application-.yml` 中按环境硬编码的 peer URL 外,没有服务注册 / 发现机制。 ## 部署环境中的 URL 路由 -工作区 `.env.local` 在实时环境中把 **后台** 指向端口 `8597`,**前台** 指向端口 `8598`。这足以识别两个面向操作人员的 surface;反向代理和 context-path 映射属于部署细节,不是本仓库中有代码依据的事实。 +工作区 `.env.local` 在实时环境中把 **后台** 指向端口 `8597`,**前台** 指向端口 `8598`。这足以识别两个面向操作人员的接口面;反向代理和 context-path 映射属于部署细节,不是本仓库中有代码依据的事实。 ## Profile 组合 -`application-saas.yml`、`application-linux.yml`、`application-win.yml`、`application-15S.yml`、`application-S10.yml`、`application-pro.yml`、`application-T0.yml`、`application-T1.yml` 等覆盖以下组合: +Profile 按服务拆分: + +- **xlyEntry**:`dev`、`local`、`win`、`linux`、`15s`、`s10`、`saas`、`bgj`(小写)。`dev` 是 repo 内默认。 +- **xlyApi**:`local`(repo 默认)、`dev`、`linux`、`win`。 +- **xlyInterface**:仅 `dev`。 +- **xlyFlow**:`dev`(空文件)。 +- **xlyFace**:`win`(默认)、`dev`、`linux`、`local`。 +- **xlyPlc**:`dev`(默认)加 7 个机型 profile(`15S`、`S10`、`T0`、`T1`、`CT`、`yt`、`pro`,大小写混合;不同于 xlyEntry 的小写 `15s` / `s10`)。 -- 操作系统(linux / win)。 -- 客户类别(saas、15S、S10 等)。 -- 环境(dev、pro)。 -- 印刷机机型(xlyPlc)。 +机型 profile(`T0`、`T1`、`CT`、`yt`、`pro`、`15S`、`S10`)是 **xlyPlc 专用**,其他服务没有这些 profile。跨服务 profile 覆盖以下组合: + +- 操作系统(`linux` / `win`)。 +- 环境(`dev`、`local`、`saas`、`bgj`)。 +- 客户 / 版本(xlyEntry 的 `15s`、`s10`)。 每个部署为每个服务选择一个 profile。“某客户 → 哪些 profile”的映射位于运维部署文档,不在代码库中。 ## 未决:生产 URL 路由 +> **暂缓(超出仓库可验证范围)。** 将公网 `:8597` / `:8598` 映射到内部 Spring Boot context-path 的 nginx / 反向代理配置位于部署运维基础设施中,不在本代码库里。Wiki 无法仅靠 src / DB / web 验证它;本节只是占位,等待部署侧配置被链接或纳入仓库。 + `8597` / `8598` 的精确 nginx / 反向代理配置不在仓库中。只有拿到部署侧配置后再补入本页。 diff --git a/zh/docs/reference/maintainer/index.md b/zh/docs/reference/maintainer/index.md index e178b23..83e17f0 100644 --- a/zh/docs/reference/maintainer/index.md +++ b/zh/docs/reference/maintainer/index.md @@ -5,11 +5,13 @@ 本章服务于会修改 `BusinessBaseController`、向 `gds*` 表族添加元数据表,或接入新第三方集成的人。 - [本地运行 xlyEntry](running-locally.md) — 开发者首次启动配置(前置条件、profile、DB 覆盖、冒烟检查)。 +- [技术栈](tech-stack.md) — 按类别整理的库清单(版本、声明位置、使用位置和原因)。 - [运行时:BusinessBaseController 及相关组件](runtime.md) — 元数据驱动分发循环。 - [通用存储过程分发](proc-dispatch.md) — `GenericProcedureCallController` 深入说明。 -- [元数据变更后的缓存失效](cache-invalidation.md) — `ConsumerChangeGdsModuleThread` 及相关组件。 +- [元数据变更后的缓存失效](cache-invalidation.md) — 同步 `@CacheEvict` 路径,以及名字相近但用途不同的 JMS 基础数据合并路径。 - [SQL 模板(`xlyEntry/templesql/`)](sql-templates.md) — 运行时 SQL 生成相关脚手架。 - [多服务部署](deployment.md) — `xlyApi` vs `xlyEntry` vs `xlyInterface`。 -- [Activiti 集成](activiti.md) — 版本偏差、schema、自定义 delegate。 +- [元数据管理服务(xlyManage)](management-services.md) — BACK 配置侧背后的 `Gds*ServiceImpl` 写入路径。 +- [Activiti 集成](activiti.md) — 版本偏差、schema、自定义委托类。 > **占位。** 每个子页会在相应切片实际覆盖后补充真实代码追踪。 diff --git a/zh/docs/reference/maintainer/management-services.md b/zh/docs/reference/maintainer/management-services.md new file mode 100644 index 0000000..0dbd6a9 --- /dev/null +++ b/zh/docs/reference/maintainer/management-services.md @@ -0,0 +1,64 @@ +# 元数据管理服务(`xlyManage`) + +`xlyManage` 模块是 **BACK 配置器背后的 service 层**。PM 在系统模块配置、界面显示内容配置、数据表内容配置、系统权限配置、系统常量配置、用户信息配置、Mysql脚本配置等页面点击修改 / 新增 / 删除时,请求会先落到 `xlyEntry/.../web/systemweb/` 下的某个 `Gds*Controller`,再委托给 `xlyManage/src/main/java/com/xly/service/systeminfo/impl/` 下的 `Gds*ServiceImpl`。 + +这些 service 是框架的**元数据 CRUD 主干**:它们拥有每张 `gds*` 表的读写逻辑。运行时([切片 1](../../slices/01-hello-world.md)、[runtime.md](runtime.md))负责**读取**元数据;`xlyManage` 负责**写入**元数据。 + +## 服务总览 + +| Service | 行数 | 拥有内容 | BACK 页面 | +|---|---:|---|---| +| `GdsmoduleServiceImpl` | 729 | `gdsmodule`(模块)、`gdsroute`(URL 白名单)、模块树 CRUD | 系统模块配置 | +| `GdsconfigformServiceImpl` | 878 | `gdsconfigformmaster`、`gdsconfigformslave`、`gdsconfigformcustomslave`、`gdsconfigformpersonalize`(表单定义 + 每租户覆盖) | 界面显示内容配置 | +| `GdsconfigtbServiceImpl` | 555 | `gdsconfigtbmaster`、`gdsconfigtbslave`(虚拟表定义) | 数据表内容配置 | +| `SqlScriptsServiceImpl` | 489 | BACK 中编写的 DDL / proc / view 脚本;使用 [`templesql/`](sql-templates.md) 模板 | Mysql脚本配置 | +| `GdsjurisdictionServiceImpl` | 362 | `gdsjurisdiction`(每模块动作**目录**;见[权限](../builder/permissions.md)) | 系统权限配置 + 配置流程的一部分 | +| `GdsparameterServiceImpl` | 319 | `gdsparameter`(系统级参数) | 参数页面 | +| `GdsformconstServiceImpl` | 243 | `gdsformconst`(每表单常量:标签、默认文本)。切片 1 锚点表。 | 系统常量配置 | +| `GdslogininfoServiceImpl` | 221 | `sftlogininfo*` 表族(用户 / 登录 / 用户组目录) | 用户信息配置 | +| `SysbrandsServiceImpl` | 125 | `sysbrands`(加工商 / 租户主数据) | 租户管理 | +| `CommonServiceImpl` | 56 | 其他 service 共用 helper | n/a | + +合计约 4000 行元数据 CRUD 逻辑,这是框架运行时里相当大的部分,之前 Wiki 没有覆盖。 + +## 方法形状约定 + +每个 `Gds*Service` 基本都遵循同一个五方法形状: + +```java +Feedback> getXxx(Map params); // 列表 / 分页读取 +Feedback> getXxxBysId(Map params); // 单行读取 +Feedback> addXxx(Map params); // 插入 +Feedback> updateXxx(Map params); // 更新 +Feedback> deleteXxx(Map params); // 删除(常见为 bInvalid 软删) +``` + +对应 `Gds*Controller` 的 endpoint 方法也几乎逐字镜像这个形状。因此 BACK 管理接口面本质上是一个薄 pass-through:controller 在 `xlyEntry/.../systemweb/`,service 在 `xlyManage/.../systeminfo/impl/`。这和运行时侧相反:运行时由一个通用 `BusinessBaseController` 处理任意表上的业务数据 CRUD;这里每类框架元数据 CRUD 都有自己的 controller + service 对。 + +## 值得注意的细节 + +- **`GdsconfigformServiceImpl` 最大**,因为它拥有四张强耦合表(form-master、form-slave、customslave、personalize)以及 **DDL 脚本生成**流程。`getFormslaveScriptSqlPro`、`getMasterSlaveScriptSqlPro` 等方法会生成工程师可应用到物理表上的 SQL,用于新增字段。这让定制覆盖模型([切片 4](../../slices/04-custom-field.md))端到端可用:BACK 配置器也能生成覆盖所暗示的 schema migration SQL。 +- **`GdsmoduleServiceImpl` 包含 `getModuleTreePro`**,这是 SPA 登录后调用的模块树解析(实时 trace 中第一个 `/gdsmodule/getModuleTreePro` 请求)。版本可见性([切片 2](../../slices/02-multi-tenancy.md))最终由许可证产出的 `sVerifyLicense` 模块 id 列表控制,而不是直接按 `sVersionFlowId` 条件过滤。 +- **`SqlScriptsServiceImpl`** 把 [`templesql/`](sql-templates.md) 脚手架接到 BACK 脚本编写页面。工程师填写占位规格后,service 会生成可编译的 proc / view body,并在连接的 schema 上执行。 +- **`GdsjurisdictionServiceImpl` 写动作*目录***;`sysjurisdiction`(每用户授权表)由其他路径写入(`xlyBusinessService` 的权限管理流程)。目录 vs 授权的区别见[如何设置权限](../builder/permissions.md)。 +- **`SysbrandsServiceImpl`** 写租户主数据(`sysbrands` + `sBrandsId` 行);新租户初始化基本就是这里插入一行,再种子化元数据。 + +## 缓存失效钩子点 + +这些 service 的写方法直接带有自己的 `@CacheEvict` 注解(例如 `GdsmoduleServiceImpl.java:96` 的 `addGdsmodule` 会清理 9 个 cache region)。这就是 BACK 中的元数据编辑能立即在各节点生效的原因:共享 Redis 缓存(RedisCacheManager,见 [cache-invalidation.md](cache-invalidation.md))会在写事务提交时清理相关 region。这里**没有 JMS 扇出用于清缓存**,这是常见误解,缓存失效页已有详细说明。(业务数据通过 `BusinessBaseServiceImpl` 写入时走另一条 `BusinessCleanRedisData.delCleanRedisData*` 分发路径;两条路径都在缓存失效页说明。) + +## `xlyManage` 不包含什么 + +- **业务数据 CRUD。** 那是通用 `BusinessBaseController` + `BusinessBaseServiceImpl` 路径([runtime.md](runtime.md)、[切片 1](../../slices/01-hello-world.md))。 +- **API 元数据**(`sysapi`)。那是 `xlyApi` 自己的管理接口面;见[外部 API](../../api-reference/external.md)。 +- **工作流元数据**(`gdsmoduleflow`、`act_*`、`biz_flow`)。那在 `xlyFlow`;见 [activiti.md](activiti.md)。 + +## 出问题时先看哪里 + +| 症状 | 优先查看 | +|---|---| +| BACK 对 `gdsconfigform*` 修改 / 新增返回“操作失败” | `GdsconfigformServiceImpl`,检查字段校验和匹配的 DDL 脚本生成路径 | +| 版本可见性显示了错误模块 | 菜单 / 模块树 SQL,以及 `VerifyLicense.getModelAllList()` / `sVerifyLicense`,确认允许的模块 id 列表 | +| BACK 脚本编写页面生成了错误 SQL | `SqlScriptsServiceImpl` + [templesql 脚手架](sql-templates.md) | +| 某模块缺少权限动作目录(BtnAdd / BtnUpd / …) | `GdsjurisdictionServiceImpl`,检查该 `sParentId` 下的行 | +| 用户能登录 BACK 但 FROUNT 为空 | `GdslogininfoServiceImpl`,检查 `sftlogininfo*` 连接表 | diff --git a/zh/docs/reference/maintainer/proc-dispatch.md b/zh/docs/reference/maintainer/proc-dispatch.md index 06f44c3..5392295 100644 --- a/zh/docs/reference/maintainer/proc-dispatch.md +++ b/zh/docs/reference/maintainer/proc-dispatch.md @@ -1,12 +1,12 @@ # 通用存储过程分发 -当 `gdsmodule.sSaveProName`(或 `sDeleteProName`、`sCalcProName`、`sProcName`、`sSaveProNameBefore`)**非空**时,框架会调用指定存储过程,而不是落入默认 Add/Update 路径。同一套机制也处理按钮计算和按需自定义逻辑。 +当元数据命名一个存储过程时(例如通过 `gdsmodule.sSaveProName`、`sSaveProNameBefore`、`sDeleteProName`、`sCalcProName`、`sProcName`,或 `gdsconfigformslave.sButtonParam`),框架会按名称分发该过程。`sSaveProName` 和 `sSaveProNameBefore` 是**钩子点**:它们作为保存后 / 保存前阶段叠加在始终会运行的基础新增 / 更新路径(`BusinessBaseServiceImpl.addBusinessData` / `updateBusinessData`)之上,由 `BusinessBaseServiceImpl.java:1824` 与 `:1778` 的 `checkUpdate(...,"sSaveProName")` 调用。其他列驱动按需调用:`sCalcProName` 用于按钮计算,`sProcName` 用于自定义取数流等。同一套通用分发机制处理这些情况。 处理器是 `xlyEntry/com/xly/web/businessweb/` 中的 `GenericProcedureCallController`。 ## 端点形状 -前端向 `/business/genericProcedureCall*` 下的 URL POST: +前端向 `/procedureCall/doGenericProcedureCall` POST: - 存储过程名(通常从 `gdsmodule` 或 `gdsconfigformslave.sButtonParam` 解析)。 - 参数值(前端提供,框架注入租户身份)。 @@ -49,8 +49,31 @@ xly 过程共享一套调用约定,以便通用分发可行: 这些是工程师编写新过程时填充的模板;运行时不会用它们动态生成过程。loader 和占位符语法见 [SQL 模板](sql-templates.md)。 +## 实时 schema 中的过程命名模具 + +实时 DB 的 1687 个存储过程围绕少数命名模具聚集,不只是裸 `Sp_*` 家族: + +| 模具 | 约数 | 角色 | +|---|---:|---| +| `Sp_*` | 大多数 | 主导家族,由 `sSaveProName` / `sCalcProName` / `sProcName` 等分发。 | +| `Sp_*_BeforeSave` | ~62 | 保存前钩子,对应 `sSaveProNameBefore`。 | +| `Sp_*_AfterSave` / `Sp_*_SaveReturn` | ~62 / ~54 | 保存后钩子;`_SaveReturn` 会写回父事务。 | +| `Sp_*_Calc` | ~178 | 按钮计算过程,由按钮流程(`sCalcProName` / `sButtonParam`)调用。 | +| `sp_btn_*` | ~65 | 按钮事件子家族,通常是 `sp_btn_calc*` / `sp_btn_validate*`(约定使用小写)。 | +| `PRO_ERPMERGE*` | ~11 | 数据迁移 / 合并工具。**不由运行时分发**,只给工程师直接使用。 | +| `PRO_*`(其他) | ~12 | 其他一次性工具。 | +| `Get_*`、`del_*`、`Cal*`、`Tj_*` | 少量 | 历史 / 领域特定 helper,不属于通用分发契约。 | + +被分发列里如果写错过程名,通常也会期望目标是 `Sp_*` 形状;其他模具不会通过 `sSaveProName` / `sCalcProName` 等解析。非 `Sp_*` 过程只能通过 mapper XML 或其他过程直接调用。 + +## 函数层 + +schema 还包含 **177 个用户自定义函数**,命名模具与过程平行:`Fun_*`(约 150)、`Fn_*`(约 8)、`get_*`(约 10)。 + +这些函数**不由 Java 分发**。它们从其他存储过程、视图定义和 mapper XML 的 SELECT 语句中被调用。没有 `gdsmodule.sFunctionName` 之类的元数据列;函数由提到它们的 SQL 自行引入。维护人员排查慢报表时,应在过程和视图里 grep `Fun_*` / `Fn_*` / `get_*` 引用;框架 Java 侧看不到这些函数。 + ## 需要关注的失效模式 1. **参数顺序不匹配。** 通用分发按位置绑定;IN 参数重排的过程会在运行时炸掉。 2. **缺少租户过滤。** 某过程在内部谓词中忘记 `sBrandsId` / `sSubsidiaryId`,就是多租户泄漏。过程不会被框架自动过滤,必须自己处理。 -3. **长运行过程阻塞请求线程。** 长计算应放到独立 worker(见 `xlyErpTask`,尽管它当前在 `settings.gradle` 中禁用)。 +3. **长运行过程阻塞请求线程。** 长计算应放到独立后台线程或任务服务中(见 `xlyErpTask`,尽管它当前在 `settings.gradle` 中禁用)。 diff --git a/zh/docs/reference/maintainer/running-locally.md b/zh/docs/reference/maintainer/running-locally.md index e78cb18..ccbf686 100644 --- a/zh/docs/reference/maintainer/running-locally.md +++ b/zh/docs/reference/maintainer/running-locally.md @@ -51,7 +51,7 @@ Wiki 的侦察脚本通过 `~/.my.cnf` 使用 `xlyweberp_saas_ai`,这和 `appl - `xlyApi` — 如果需要外部 API 接口面,请作为第二个 JVM 启动。 - `xlyInterface` — 同上。 - `xlyPlc`、`xlyFlow`、`xlyFace` — 同上;每个服务都有自己的 application class 和 profile。 -- `xlyErpJms*` consumer — 它们是独立的 Spring Boot app。没有它们,JMS 驱动的缓存失效不会跨节点发生(单节点开发机通常可以接受)。 +- `xlyErpJms*` consumer — 它们是独立的 Spring Boot app。没有它们,跨节点的基础数据合并 / 消息扇出不会发生;元数据保存后的 Redis 缓存清理由 BACK 侧同步 `@CacheEvict` 完成,单节点开发机通常可以接受。 多服务本地开发见[多服务部署](deployment.md)。 diff --git a/zh/docs/reference/maintainer/runtime.md b/zh/docs/reference/maintainer/runtime.md index 8bbef34..0f0ade9 100644 --- a/zh/docs/reference/maintainer/runtime.md +++ b/zh/docs/reference/maintainer/runtime.md @@ -6,29 +6,85 @@ | 类 | 包 | 角色 | 最常引用端点 | |---|---|---|---| -| `BusinessBaseController` | `web/businessweb/` | 元数据驱动模块的通用 CRUD。每个表单的默认 API surface。 | `/business/getModelBysId/{moduleId}`、`/business/getBusinessDataByFormcustomId/{formId}`、`/business/addUpdateDelBusinessData`、`/business/getSelectDataBysControlId/{controlId}` | +| `BusinessBaseController` | `web/businessweb/` | 元数据驱动模块的通用 CRUD。每个表单的默认 API 接口面。 | `/business/getModelBysId/{moduleId}`、`/business/getBusinessDataByFormcustomId/{formId}`、`/business/addUpdateDelBusinessData`、`/business/getSelectDataBysControlId/{controlId}` | | `BusinessConfigformController` | `web/businessweb/` | 已有表单的每用户 / 每组显示定制,不是基础表单定义 CRUD。 | `/configform/getConfigformData/{moduleId}`、`/configform/sHandleConfigform`、`/configform/sCopyConfigform` | | `GdsmoduleController` | `web/systemweb/` | builder 侧使用的模块树和模块定义 CRUD。 | `/gdsmodule/getModuleTreePro`、`/gdsmodule/addGdsmodule`、`/gdsmodule/updateGdsmodule` | | `GdsconfigformController` | `web/systemweb/` | form-master 和 form-slave 元数据 CRUD。 | `/gdsconfigform/*` 下端点 | | `GdsconfigtbController` | `web/systemweb/` | 虚拟表 master / slave 元数据 CRUD。 | `/gdsconfigtb/*` 下端点 | -| `BusinessTreeGridController` | `web/businessweb/` | 树表格端点。当前分支实现了 proc-backed 路径,普通 `getTreeGrid` service 方法仍是 stub。 | `/treegrid/getTreeGridByPro/{formId}` | +| `BusinessTreeGridController` | `web/businessweb/` | 树表格端点。当前分支实现了存储过程支撑路径,普通 `getTreeGrid` service 方法仍是空实现。 | `/treegrid/getTreeGridByPro/{formId}` | | `GenericProcedureCallController` | `web/businessweb/` | 按名称 + 参数通用调用存储过程。 | `/procedureCall/doGenericProcedureCall` | | `ConfigformPanelController` | `web/businessweb/` | `gdsconfigformpanel` 中的面板布局持久化。 | `/panel/get/{sFormId}`、`/panel/save/{sFormId}` | -| `CheckFlowController` | `web/businessweb/` | Activiti 工作流 surface(审批 / 驳回 / 查看),仅在部署工作流时有意义。 | `/checkFlow/*` 下端点 | +| `CheckFlowController` | `web/businessweb/` | **空壳。** 类文件只有 22 行:一个 `@RestController @RequestMapping(value="/checkflow")`,没有 handler 方法。`/checkflow/*` 返回 404。真正的工作流审核 / 驳回 / 完成 URL 在 `CurrencyFlowController`(位于 `xlyFlow`,经 xlyEntry context-path 暴露)中;见 [Activiti 集成](activiti.md#modeler-暴露的-urlxlyflow-controller-挂在-xlyentry-端口上)。 | 无,空类 | +| `BusinessModelCenterController` | `web/businessweb/` | FROUNT 首页的“KPI 工作中心”看板:聚合标记为 `gdsmodule.bUnTask=1` 的模块上的未清任务。尽管 UI 像工作流,**它不是 Activiti 驱动**;它读取 `gdsmodule` 行,按 `sUnType` ∈ {`Pending`, `PendingCheck`, `MyWarning`} 分桶,并按用户缓存。见下面的 [KPI 工作中心](#kpi-工作中心front-端首页-dashboard)。 | `/modelCenter/getModelCenter`、`/modelCenter/getModelCenterCalculation` | 注意 controller 分布在**两个包**中:`businessweb/` 承载运行时端点,`systemweb/` 承载 builder 侧元数据 CRUD 端点。两者都编译进同一个 `xlyEntry` WAR。 -## 四表读取 +## KPI 工作中心(FROUNT 首页看板) {#kpi-工作中心front-端首页-dashboard} + +用户进入 FROUNT(`http://:8598/indexPage`)时,首页会显示一个标题为 **`KPI监控`** 的卡片。尽管名字叫 KPI,它不是分析意义上的 KPI 看板:没有目标、没有图表、没有指标度量。它是一个**未清任务聚合器**:把“需要你处理的事”按角色和业务流程汇总成统一入口。 + +处理链路: + +- `POST /xlyEntry/modelCenter/getModelCenter` → `BusinessModelCenterController.java:44-48` → `BusinessModelCenterServiceImpl.getKPIModelList`(很薄的一层 wrapper)→ `xlyBusinessService` 中的 `BusinessModelKpiServiceImpl.getKPIModelList`。 +- `POST /xlyEntry/modelCenter/getModelCenterCalculation` 会重新计算数量,并清掉 `getKpiModelByUser` 缓存区域。 + +`getModelCenter` 的 Javadoc 称它为**“初次获取KPI工作中心”**,这是框架内部对该接口面的命名。 + +### 数据来源:`gdsmodule` 未清任务标记 + +KPI 工作中心读取的是 `gdsmodule`,不是 Activiti 的 `act_*` 表。参与看板的每个模块都会在自己的 `gdsmodule` 行上设置四个列: + +| 列 | 注释 | 含义 | +|---|---|---| +| `bUnTask` | `是否增加到未清` | `1` 表示纳入看板,`0` 表示忽略 | +| `sUnType` | `未清类型` | 分桶:`Pending`、`PendingCheck`、`MyWarning` 之一 | +| `sChineseUnMemo`、`sEnglishUnMemo`、`sBig5UnMemo` | `未清描述` | 各语言显示标签 | + +实时 dev DB 中,`bUnTask = 1` 的模块有 **92** 行,分布如下: + +| `sUnType` | 数量 | 前端分桶标签 | +|---|---:|---| +| `Pending` | 79 | 待处理事务 | +| `PendingCheck` | 1 | 发起新事务 | +| `MyWarning` | 3 | 我的预警报表 | +| NULL | 9 | 未标记,排除 | + +对每个已标记模块,`BusinessModelKpiServiceImpl` 会执行该模块的每用户数量查询,并按两种维度聚合: + +- **角色**(UI 中“按角色”):JOIN `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify`,把当前用户映射到角色,再按角色拆分数量。 +- **流程**(UI 中“按流程”):`估价管理流程` / `订单生产流程` 这类标签来自 `gdsmodule` 树中的父模块。每个业务流程是一个父模块,下面包含 1 到 N 个有序子模块。UI 中 “01/04、02/04 …” 的步骤编号,就是子模块在父流程下的 `iOrder`。 + +### 缓存 + +`@Cacheable(value="getKpiModelByUser", key=...)` 会缓存每用户结果;`getModelCenterCalculation` 以及任何修改 `gdsmodule` 的路径都会通过 [`CleanRedisServiceImpl`](cache-invalidation.md) 失效这个区域。由于 `bUnTask` / `sUnType` 可通过 BACK 的“系统模块配置”页面编辑,维护人员要给看板加一个新的“未清任务”,改的是元数据,不是 Java。 + +### 为什么不是 Activiti + +Activiti 的职责是**审批工作流**:当某行需要经过 N 步 `act_re_procdef` 图签核时,`act_ru_task` / `biz_todo_item` 表会按处理人保存待办任务。它和 KPI 工作中心是不同接口面,虽然两者都会向用户展示“需要处理的事”。 + +当前 dev DB 中,`biz_todo_item` 和 `biz_flow` 都是空表(0 行),但 KPI 工作中心仍能显示非零数量。这是两套系统相互独立的实证。启用 Activiti 流程的部署会通过工作流 controller 和单独的流程面板暴露这些任务,而不是通过 KPI 工作中心。 + +## 五键读取 {#five-key-read} 对任何元数据驱动模块,请求生命周期(见[概念 → 请求生命周期](../../concepts/request-lifecycle.md))可归结为: ```java public Map getModelBysId(Map map) { - List> formList = this.getModelConfigByModleId(map); // 1. join gdsmodule⋈form-master⋈form-slave - List> fList = businessGdsconfigformsService.getFormconstData(qMap); // 2. gdsformconst - List> jList = businessGdsconfigformsService.getJurisdictionData(qMap); // 3. gdsjurisdiction(ADMIN 跳过) - Map billnosettingMap = businessGdsconfigformsService.getBillnosettingData(param); // 4. sysbillnosettings - List> reportList = printReportService.getReportData(qMap); // 5. sysreport + // 1. formData:gdsconfigformmaster 按 sParentId=sModelsId 过滤, + // LEFT JOIN gdsconfigformpersonalize(每租户),再为每个 master 行 + // 加载 gdsconfigformslave + gdsconfigformcustomslave 覆盖。 + // gdsmodule 本身只通过 id 引用,不被 SELECT。 + List> formList = this.getModelConfigByModleId(map); + // 2. gdsformconst:仅按 sParentId;sLanguage 决定返回哪列标签;不按租户过滤。 + List> fList = businessGdsconfigformsService.getFormconstData(qMap); + // 3. sysjurisdiction:每用户授权,JOIN sftlogininfojurisdictiongroup + // + sisjurisdictionclassify;ADMIN 跳过。 + // 虽然返回 map key 叫 `gdsjurisdiction`,实际源表是 sysjurisdiction。 + List> jList = businessGdsconfigformsService.getJurisdictionData(qMap); + // 4. sysbillnosettings(每租户、每表单)。 + Map billnosettingMap = businessGdsconfigformsService.getBillnosettingData(param); + // 5. sysreport(每租户、每表单)。 + List> reportList = printReportService.getReportData(qMap); return composite(formList, fList, jList, billnosettingMap, reportList); } ``` @@ -39,11 +95,11 @@ public Map getModelBysId(Map map) { | Key | 来源 | 前端用途 | |---|---|---| -| `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave`(+ 覆盖) | 表单布局 | -| `gdsformconst` | `gdsformconst` | 表单级常量、下拉标签 | -| `gdsjurisdiction` | `gdsjurisdiction` | 按钮 / 数据权限 | -| `billnosetting` | `sysbillnosettings` | 单据编号规则 | -| `report` | `sysreport` | 打印模板 | +| `formData` | `gdsconfigformmaster`(按 `sParentId = sModelsId` 过滤)⋈ `gdsconfigformpersonalize` 覆盖;每个 master 行再加载 `gdsconfigformslave` + `gdsconfigformcustomslave`。`gdsmodule` 只作为 id 来源被引用,不参与 join。 | 表单布局 | +| `gdsformconst` | `gdsformconst`(仅按 `sParentId` 过滤;`sLanguage` 决定返回哪列标签;不按租户过滤) | 表单级常量、下拉标签 | +| `gdsjurisdiction` | `sysjurisdiction`(JOIN `sftlogininfojurisdictiongroup` + `sisjurisdictionclassify` 得到每用户 / 用户组授权);ADMIN 跳过。**注意:** map key 名称 `gdsjurisdiction` 有误导性,`gdsjurisdiction` 是配置侧动作目录表;这里读取的每用户授权实际来自 `sysjurisdiction`。 | 按钮 / 数据权限 | +| `billnosetting` | `sysbillnosettings`(每租户、每表单) | 单据编号规则 | +| `report` | `sysreport`(每租户、每表单) | 打印模板 | ## 保存端点 @@ -57,7 +113,9 @@ public Map getModelBysId(Map map) { } ``` -当 `gdsmodule.sSaveProName` 为空时,框架默认 Add/Update 路径运行,即 `AddDelUpdCommonServiceImpl.java`。非空时,调用指定存储过程。 +基础新增 / 更新路径总是通过 `BusinessBaseServiceImpl.addBusinessData` / `updateBusinessData`(`xlyBusinessService/.../BusinessBaseServiceImpl.java:1014` 和 `:1250`),再委托 `businessBaseDao.add(map)` / `businessBaseDao.update(map)`,对 `sTable` 命名的表执行操作。 + +`gdsmodule.sSaveProName`(及其兄弟列 `sSaveProNameBefore`)**不是**替换基础路径的二选一分支;它命名的是叠加在基础路径之上的额外存储过程钩子:保存后 / 保存前由 `BusinessBaseServiceImpl.java:1824` 的 `checkUpdate(...,"sSaveProName")` 和 `CheckSaveServiceImpl.java` 分发。`AddDelUpdCommonServiceImpl`(`@Service("addDelUpdCommonService")`)是另一套可复用 `insertByMap` / `updateByMap` / `delByMap` / `addBatch` helper,被工单计划、OEE、多报价、订单采购等领域 service 使用;它**不是** `addUpdateDelBusinessData` 的默认新增 / 更新路径。 ## 多租户边界 @@ -73,9 +131,20 @@ RequestAddParamUtil.me().addParams(params, userInfo); ## 需要审计的安全关注点 -1. **`addUpdateDelBusinessData` 中的 `sTable` 校验。** 前端直接命名目标表。运行时必须交叉检查传入表是否属于该表单授权的支撑表,否则是权限提升面。若不存在检查,应作为安全 ticket 提出。见[切片 1 v2 follow-up](../../slices/01-hello-world.md#open-verification-items)。 +1. **`addUpdateDelBusinessData` 中的 `sTable` 校验:已确认缺失。** 前端直接命名目标表,而运行时**不会**交叉检查传入表是否属于该表单授权的支撑表。`BusinessBaseServiceImpl.sTableNameList`(162-169 行)是多租户作用域绕过列表(四张全局框架元数据表,写入时剥掉 `sBrandsId` / `sSubsidiaryId`;见 165 行 `//不需要公司子公司的表` 注释),不是支撑表白名单。整个流程里唯一的模块 / 表交叉检查是 1768 行的硬编码特例(`mftproductionplanslave`)。确实存在一些缓解措施(`RequestAddParamUtil` 的租户作用域、可选的保存前 / 后存储过程校验),但它们都不是支撑表白名单。完整追踪见[切片 1 follow-up](../../slices/01-hello-world.md#open-verification-items)。 2. **ADMIN 绕过权限。** `BusinessBaseServiceImpl` 对 `UserType.ADMIN` 完全跳过 `gdsjurisdiction` 加载。ADMIN 账号治理必须来自应用外部。 +## “通用 CRUD”在实践中意味着什么 + +“一个 controller 写任意表中的任意行”是 xly 数据驱动设计的核心动作,也会把风险集中到少数路径上: + +- **`BusinessBaseServiceImpl` 已经约 3,900 行**,其中缠在一起的逻辑包括每租户作用域绕过列表、特定表硬编码(第 1768 行的 `mftproductionplanslave`)、保存前 / 保存后钩子分发、以及由 `sTable` 驱动的写入路由。每个 bug fix 都必须穿过这个大类。 +- **它是整个业务运行时的单点故障。** `addUpdateDelBusinessData` 中的回归会同时破坏所有租户的所有表单保存。模块专用 controller 可以把爆炸半径限制在一个模块内;通用 controller 做不到。 +- **`Map` 没有类型系统。** 前端传来一袋 key/value。运行时相信 key 与列名匹配、value 能转换成列类型。不匹配时通常在 DAO 层抛 `BadSqlGrammarException`,离错误来源已经很远。这里没有 schema-aware 的请求校验。 +- **可发现性差。** “哪些端点会写 `mftproductionplanslave`?”不能靠 IDE find-usages 回答。真实答案是:任何调用 `BusinessBaseServiceImpl.addBusinessData` 且把 `sTable` 设为 `mftproductionplanslave` 的 controller,也就是几乎所有通用保存入口。 + +通用模式让数据驱动论点成立;它也是新增模块几乎免费的原因,**同时**也是修改运行时几乎从不免费的原因。 + ## 缓存失效 -**后台**保存元数据变更时会触发 JMS 消息,`xlyErpJmsConsumer` 中的 `ConsumerChangeGdsModuleThread` 会清除每个运行节点上的元数据缓存。见[元数据变更后的缓存失效](cache-invalidation.md)。 +BACK 保存元数据变更时,保存 service 会同步调用 `BusinessCleanRedisData.delCleanRedisData*`,进而触发 `CleanRedisServiceImpl` 上相关缓存区域的 `@CacheEvict`。另有一个名字相近的 JMS 路径(`ConsumerChangeGdsModuleThread`),但它通过存储过程做基础数据合并,不做缓存失效。完整说明见[元数据变更后的缓存失效](cache-invalidation.md),包括跨节点一致性问题。 diff --git a/zh/docs/reference/maintainer/sql-templates.md b/zh/docs/reference/maintainer/sql-templates.md index 800fe68..00622a7 100644 --- a/zh/docs/reference/maintainer/sql-templates.md +++ b/zh/docs/reference/maintainer/sql-templates.md @@ -62,6 +62,17 @@ END 不遵守这些约定的过程无法通过通用分发调用,只能从自定义 Java 代码调用。 +## 两个 loader + +代码库里有**两个**名为 `FileSqlUtil` 的类,但可靠性完全不同: + +| Loader | 指向内容 | 状态 | +|---|---|---| +| `xlyFlow/src/main/java/com/xly/sqltemplate/util/FileSqlUtil.java` | 上述 8 个脚手架 | **8 个文件都存在**,这是 BACK 脚本编写页面实际使用的 loader。 | +| `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`。 | + +维护人员如果要动 `xlyApi` 的 `FileSqlUtil`,应把它当作默认损坏:要么恢复缺失模板,要么删除这个 loader。 + ## 另见 - [通用存储过程分发](proc-dispatch.md):过程写好后框架如何调用。 diff --git a/zh/docs/reference/maintainer/tech-stack.md b/zh/docs/reference/maintainer/tech-stack.md new file mode 100644 index 0000000..81ab7b2 --- /dev/null +++ b/zh/docs/reference/maintainer/tech-stack.md @@ -0,0 +1,187 @@ +# 技术栈 + +本页是**范围内**框架的依赖清单:11 个框架核心模块(`xlyEntry`、`xlyApi`、`xlyManage`、`xlyBusinessService`、`xlyPersist`、`xlyEntity`、`xlyFlow`、`xlyInterface`、`xlyMsg`、`xlyErpJmsProductor`、`xlyErpJmsConsumer`)、一个插件(`xlyPlc`),以及一个共享工具模块(`xlyPlatConstant`;尽管名字带 `Plat*`,但 `xlyPersist` 依赖其中的 `MultiThreadServer` 和 `TimeContant`)。 + +其他 plat 层模块(除 `xlyPlatConstant` 外的 `xlyPlat*`)、`xlyFace`(仍参与构建但不在文档范围内)和 AI 库均为[范围外](../../index.md),本页不列。 + +## 如何读本页 + +两个列提供证据: + +- **位置**:声明该库为 `api(...)` 或 `implementation(...)` 的 `build.gradle` 文件。多数库声明在 `xlyPersist/build.gradle`,并传递到依赖 `xlyPersist` 的模块。 +- **范围内源码引用**:在上述范围内模块中对 `xly-src//src/` 执行 `grep -rln ` 的文件计数,并给出代表路径。“未发现源码引用”表示该库已声明,但范围内模块的 Java、HTML 或 yaml 源码没有直接引用。它仍可能作为传递依赖进入 classpath,或由 Spring Boot autoconfig 消费。 + +本页不解释“为什么选择某个库”。如果 gradle 或 yaml 中有明确注释(例如 Netty 版本 pin),会引用该注释;否则只记录事实。 + +## 1. 应用平台 + +| Library | Version | 位置 | 范围内源码引用 | +|---|---|---|---| +| 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`。 | +| 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.*`。 | +| 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 库引入的冲突传递版本。 | +| AspectJ Weaver | 1.9.6 | `xlyApi/build.gradle`(声明两次) | 1 个文件在 `xlyFlow/src/main/java/...` import。主要消费者是 Spring Boot AOP starter;xlyApi 中的显式 pin 与 import 无关。 | +| 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`。 | + +## 2. 持久化 {#persistence} + +| Library | Version | 位置 | 范围内源码引用 | +|---|---|---|---| +| 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}/`。 | +| MyBatis-Plus | 3.3.0 | `xlyApi/build.gradle` | 2 个文件:`xlyApi/.../SqlUtil.java`、`xlyApi/.../BaseController.java`。不在 xlyApi 之外使用。 | +| MySQL Connector/J | 8.0.13 | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | yaml 中 `spring.datasource.driverClassName: com.mysql.cj.jdbc.Driver`。 | +| 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 个。 | +| Oracle JDBC | 本地 `ojdbc6-11.2.0.4.jar` | `xlyFlow/build.gradle` | xlyFlow 中 2 个文件。 | +| Druid | 1.2.16 | `xlyPersist/build.gradle`、`xlyApi/build.gradle` | 6 个 Java 文件 import `com.alibaba.druid.*`;16 个 `application-*.yml` 引用 Druid 配置。 | +| HikariCP | 4.0.3 | `xlyApi/build.gradle` | 8 个文件引用 `com.zaxxer.hikari`。Java 配置包括 `MasterDataSourceConfig.java`、`SlaveDataSourceConfig.java`。 | +| Flyway | 5.2.1 | `xlyPersist/build.gradle` | 无 Java import。通过 yaml `spring.flyway.*` 配置,`enabled: false`。脚本在 `xlyEntry/src/main/resources/flyway/V*__*.sql`。 | +| PageHelper | 4.1.1 | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 19 个文件 import `com.github.pagehelper.*`。 | +| jsqlparser | 3.2 | `xlyPersist/build.gradle` | 1 个 `xlyPersist/src/...` 文件 import `net.sf.jsqlparser`。 | + +## 3. 缓存与内存 + +| Library | Version | 位置 | 范围内源码引用 | +|---|---|---|---| +| 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.*` 配置块。 | +| Lettuce | Spring Data Redis 2.2.5 默认 driver | 传递依赖 | 无直接 Java import;yaml 中有 `spring.redis.lettuce.pool.*`。 | +| Jedis | 2.9.0 | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 5 个文件 import `redis.clients.jedis`,包括 `xlyMsg/.../wechat/util/JedisMsgUtil.java`。 | +| Guava | 18.0(`xlyPersist`、`xlyApi`);20.0(`xlyFlow`) | `xlyPersist/build.gradle`、`xlyApi/build.gradle`、`xlyFlow/build.gradle` | 8 个文件 import `com.google.common.*`。 | + +## 4. 工作流与调度 + +| Library | Version | 位置 | 范围内源码引用 | +|---|---|---|---| +| Activiti Engine | 5.17.0 | `xlyPersist/build.gradle`、`xlyApi/build.gradle`;由 `xlyFlow` 消费 | 35 个文件 import `org.activiti.*`。与 6.0 modeler 库的版本偏差见 [Activiti 集成](activiti.md)。 | +| Activiti Spring Boot REST API | 6.0.0 | `xlyFlow/build.gradle` | 由 Spring Boot autoconfig 和 xlyFlow 下 REST 端点消费。 | +| Activiti JSON Converter | 6.0.0 | `xlyFlow/build.gradle` | 用于 xlyFlow modeler 保存路径。 | +| Quartz | 2.3.0 | `xlyFlow/build.gradle` | 16 个文件 import `org.quartz.*`;xlyEntry yaml 通过 JDBC JobStore(`qrtz_*` 表)配置 `spring.quartz.*`。 | + +## 5. 消息 + +| Library | Version | 位置 | 范围内源码引用 | +|---|---|---|---| +| 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)。 | +| RocketMQ Spring Boot Starter | 2.0.2 | `xlyPersist/build.gradle` | `xlyBusinessService/src/` 中 4 个文件 import `org.apache.rocketmq.*`。 | + +## 6. 视图 / 模板 + +| Library | Version | 位置 | 范围内源码引用 | +|---|---|---|---| +| Thymeleaf | 3.0.15 | `xlyApi.build.gradle`、`xlyFlow.build.gradle`;也由 starter 传递 | 2 个 xlyFlow Java 文件 import;modeler 模板在 `xlyFlow/src/main/resources/templates/`。 | +| Freemarker | Spring starter 2.2.5 | `xlyPersist.build.gradle`、`xlyApi.build.gradle`、`xlyFlow.build.gradle` | 1 个 xlyFlow Java 文件 import。 | +| Apache Batik | 1.8 / 1.7 | `xlyPersist.build.gradle`、`xlyFlow.build.gradle` | 1 个 xlyFlow 文件 import;modeler 静态资源也携带 Batik 资产。 | + +## 7. 认证 + +| Library | Version | 位置 | 范围内源码引用 | +|---|---|---|---| +| 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)。 | +| `shiro-ehcache` / `shiro-core` / `thymeleaf-extras-shiro` | 1.4.2 / 2.0.0 | `xlyFlow.build.gradle` | xlyFlow 模板中大量 Shiro tag;Java import 已计入上行。 | +| Bouncy Castle | `bcprov-jdk14:138` | `xlyApi.build.gradle` | 2 个 RSA 工具文件。 | +| commons-codec | 1.16.0 | `xlyPersist.build.gradle`、`xlyApi.build.gradle` | 18 个文件 import。 | + +## 8. 报表与导出 + +打印 / 导出是框架第三方代码最大的消费面。 + +| Library | Version / 形态 | 位置 / 引用 | +|---|---|---| +| iText 5.x 与 lowagie iText 2.x | `itextpdf` 5.5.0 + `com.lowagie:itext` 2.1.7 | 都在 `xlyPersist.build.gradle`;两条分支同时存在于 classpath。 | +| Aspose Cells / Words | 本地 jar | `xlyPersist.build.gradle`;多个 `xlyPersist/src/` 文件 import `com.aspose.*`。 | +| Apache POI | 4.1.2(`xlyPersist` / `xlyFlow`)/ 3.15(`xlyApi`) | 36 个文件 import。 | +| jxls + jxls-poi / jxls-jexcel | 2.8.1 / 1.0.9 | 22 个文件 import `org.jxls.*`。 | +| EasyExcel | 本地 4.0.3 jars | `xlyBusinessService` 中 10 个文件 import。 | +| JasperReports / OLAP4J | 本地 jar | 报表相关路径 import。 | +| ZXing / Barcode4J / Pinyin4j / PDFBox / Thumbnailator / jacob | 多版本 | 条码、拼音、PDF、缩略图、COM 自动化等报表辅助功能。 | + +## 9. 文件存储与 HTTP 客户端 + +| Library | Version | 位置 / 引用 | +|---|---|---| +| Aliyun OSS SDK | 2.2.0 | `xlyPersist/src/main/java/com/xly/utils/OssUtil.java`。 | +| commons-fileupload / commons-io | 1.5 / 2.5 | xlyFlow、xlyPersist、xlyEntry、xlyBusinessService 中有 import。 | +| OkHttp + Okio | 4.10.0 / 2.10.0 | xlyApi 中 2 个文件 import。 | +| Apache HttpClient | 4.5.5 | xlyBusinessService 中 1 个文件 import。 | +| javax.mail | 1.6.2 | xlyPersist 中 1 个文件 import。 | + +## 10. JSON 与通用工具 + +| Library | Version | 范围内引用 | +|---|---|---| +| FastJson | 1.2.15(`xlyPersist`、`xlyApi`)/ 1.2.60(`xlyFlow`) | 83 个文件 import。 | +| Jackson | 2.9.7(xlyFlow 显式)+ Spring 传递依赖 | 22 个文件 import。 | +| Hutool | 5.6.5(`xlyPersist`)/ 5.8.5(`xlyApi`、`xlyFlow`) | 271 个文件 import,覆盖所有范围内模块。 | +| commons-lang3 / commons-collections4 | 3.6 / 3.8.1;4.1 | 多模块 import。 | +| Groovy | `groovy-all` 3.0.2 | 5 个 Java 文件 import `groovy.util.logging.Slf4j`,看起来是遗留 import。 | +| Struts2 JSON plugin | 2.5.30 | 仅 `xlyPersist/src/main/java/com/xly/utils/FeedPage.java`;框架其他部分运行在 Spring MVC 上。 | +| SnakeYAML / JDOM / validation-api | 多版本 | xlyFlow、xlyApi、xlyInterface、xlyMsg 等少量引用。 | + +## 11. 硬件集成 + +| Library | Version | 范围内源码引用 | +|---|---|---| +| HslCommunication | 本地 `HslCommunication.jar` | 9 个文件引用,分布在 xlyPersist、xlyBusinessService、xlyPlc。xlyPlc 是 PLC 桥;见[切片 6](../../slices/06-hardware.md)。 | + +## 12. 通知 + +| Library | Version | 范围内源码引用 | +|---|---|---| +| Aliyun DingTalk SDK | `com.aliyun:dingtalk` 2.1.14 | `xlyMsg/src/main/java/com/xly/dingtalk/` 中 1 个文件;见[通知](../../api-reference/notifications.md)。 | +| `alibaba-dingtalk-service-sdk` | 2.0.0 | xlyMsg 中 1 个文件 import `com.dingtalk.api.*`。 | +| Jeewx-API(微信) | 本地 `jeewx-api-1.3.2.jar` | xlyInterface 中 5 个文件引用。 | + +## 13. 授权许可 + +| Library | Version | 范围内源码引用 | +|---|---|---| +| TrueLicense | 本地 `trueswing.jar` + `truexml.jar` + `turelicense.jar` | `xlyBusinessService/src/main/java/com/xly/license/` 下 5 个文件;xlyEntry local yaml 中 `License:` 块默认 `checkLic: false`。 | + +## 14. 日志 + +| Library | Version | 范围内源码引用 | +|---|---|---| +| Logback | `logback-classic` 1.2.3 | 5 个文件 import;配置在 `xlyEntry/src/main/resources/logback-spring.xml`。 | +| log4j 1.x | 1.2.17 | xlyFlow 中 1 个文件 import;Druid stat filter yaml 中也出现 `filters: stat,log4j2`。 | + +## 15. 构建与开发 + +| Library | Version | 用途 | +|---|---|---| +| Gradle wrapper | 已提交 | 构建工具;见[本地运行](running-locally.md)。 | +| Spring Boot Gradle plugin | 2.2.5.RELEASE | repo 根 `build.gradle`,构建可运行 WAR。 | +| Spring Boot configuration processor | 2.2.5.RELEASE | xlyApi annotationProcessor,用于 `@ConfigurationProperties` IDE 元数据。 | + +## 已声明但未发现范围内源码引用 + +以下库出现在 `build.gradle` 中,但在 `xly-src//src/` 下未发现 Java import、HTML 模板引用或 yaml 属性绑定。它们可能是传递依赖、Spring Boot autoconfig 消费的库、已删除代码遗留,或只是冗余声明。 + +| Library | 声明位置 | 备注 | +|---|---|---| +| Kaptcha、JNA、oshi-core、UserAgentUtils | `xlyFlow.build.gradle` | 未发现 import;分别用于验证码、原生访问、系统信息、UA 解析。 | +| Barbecue、Gson、commons-pool2 | `xlyPersist.build.gradle` | 未发现 import;当前活跃条码路径是 Barcode4J + ZXing,JSON 路径是 FastJson + Jackson。 | +| Baidu SDK | `xlyInterface.build.gradle` 本地 jar | 未发现 `com.baidu` import。 | +| `mchange-commons-java` | `xlyFlow.build.gradle` | 未发现直接 import。 | +| Springfox | `xlyInterface.build.gradle` | 未发现直接 Java import;通过 jar 静态资源提供 `/swagger-ui.html`,但没有 Docket bean。见 [Webhook API](../../api-reference/webhooks.md)。 | + +## 值得注意的版本偏差与本地 jar + +| 项 | 细节 | +|---|---| +| Shiro | `xlyPersist` 为 1.3.2;`xlyApi` 和 `xlyFlow` 为 1.4.2。 | +| FastJson | `xlyPersist` / `xlyApi` 为 1.2.15;`xlyFlow` 为 1.2.60。 | +| Hutool | `xlyPersist` 为 5.6.5;`xlyApi` / `xlyFlow` 为 5.8.5。 | +| Apache POI | `xlyPersist` / `xlyFlow` 为 4.1.2;`xlyApi` 为 3.15。 | +| Guava | `xlyPersist` / `xlyApi` 为 18.0;`xlyFlow` 为 20.0。 | +| commons-lang3 | `xlyPersist` 为 3.6;`xlyFlow` 为 3.8.1。 | +| Lombok | `xlyPersist` / `xlyFlow` 为 1.18.8;`xlyApi` 为 1.18.20。 | +| iText | `xlyPersist` 同时声明 `itextpdf` 5.5.0 和 `com.lowagie:itext` 2.1.7。 | +| Activiti | 流程引擎 5.17.0(`xlyPersist` / `xlyApi`);rest-api 与 json-converter 6.0.0(`xlyFlow`)。见 [Activiti 集成](activiti.md)。 | +| 本地 jar | `xlyPersist/src/main/java/lib/` 下有 Aspose、jacob、HslCommunication、QRCode、OLAP4J、JasperReports、EasyExcel 等;`xlyFlow` 和 `xlyInterface` 下有 MSSQL / Oracle / Baidu / Jeewx jar;`xlyBusinessService` 下有 TrueLicense jar。 | +| Spring Boot | 2.2.5.RELEASE,固定在根 `build.gradle` plugin block,并由使用 starter 的各模块声明。 | + +## 本清单有意不包含 + +- plat 层(`xlyPlat*` 模块)及仅在其中声明的依赖;按[首页](../../index.md)说明属于范围外。 +- AI / LLM 库(`xlyApi/build.gradle` 中的 `com.theokanning.openai-gpt3-java:service` 0.11.1 和 `com.unfbx:chatgpt-java` 1.0.8),范围外。 +- `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)。 +- `xlyFace`,范围外。 diff --git a/zh/docs/slices/01-hello-world.md b/zh/docs/slices/01-hello-world.md index baab4b5..35e1089 100644 --- a/zh/docs/slices/01-hello-world.md +++ b/zh/docs/slices/01-hello-world.md @@ -66,13 +66,13 @@ GET /xlyEntry/business/getModelBysId/13?sModelsId=13 → 200 OK | Key | 来源 | 内容 | |---|---|---| -| `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave`(+ personalize) | 表单布局主干 | -| `gdsformconst` | 按 `sBrandsId` / `sSubsidiaryId` / language 过滤的 `gdsformconst` 行 | 表单级常量、标签、默认值、下拉文本 | -| `gdsjurisdiction` | 用户角色的 `gdsjurisdiction` 行 | 按钮和数据权限;`ADMIN` 用户跳过,管理员看到全部 | -| `billnosetting` | 该模块的 `sysbillnosettings` 行 | 单据编号规则;对 `gdsformconst` 无关但总会加载 | -| `report` | 关联到该表单的打印模板 | 打印报表定义 | +| `formData` | `gdsconfigformmaster`(按 `sParentId = sModelsId` 过滤)⋈ `gdsconfigformpersonalize`(每租户覆盖);每个 master 行再加载 `gdsconfigformslave` + `gdsconfigformcustomslave` 覆盖。`gdsmodule` 只通过 id 引用(slave 查询会用它解析 `sActiveName`),不参与 master 读取。 | 表单布局主干 | +| `gdsformconst` | 仅按 `sParentId` 过滤的 `gdsformconst` 行。**不按租户过滤**;该行识别表单,`sLanguage` 决定返回哪列标签。 | 表单级常量、标签、默认值、下拉文本 | +| `gdsjurisdiction` | 用户角色的 `sysjurisdiction` 行(JOIN `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify`)。`ADMIN` 用户跳过,管理员看到全部。**注意:** map key 叫 `gdsjurisdiction` 有误导性;`gdsjurisdiction` 是配置侧动作目录,实际每用户授权来自 `sysjurisdiction`。 | 按钮和数据权限 | +| `billnosetting` | 该模块的 `sysbillnosettings` 行(每租户) | 单据编号规则;对 `gdsformconst` 无关但总会加载 | +| `report` | 关联到该表单的 `sysreport` 行(每租户) | 打印报表定义 | -**多租户**在这次读取中执行:每个子查询都带有从认证 session 注入的 `sBrandsId` 和 `sSubsidiaryId`。租户之间看不到对方元数据。 +**多租户**在需要的地方执行:租户作用域读取(`gdsconfigformpersonalize`、`gdsconfigformcustomslave`、`sysbillnosettings`、`sysreport`)都会按认证 session 注入的 `sBrandsId` 和 `sSubsidiaryId` 过滤。框架基础元数据表(`gdsconfigformmaster`、`gdsconfigformslave`、`gdsformconst`)是全局的,只按 form-id 过滤。因此租户看不到其他租户的*个性化覆盖*或*业务数据*,但底层表单定义是共享的。 ### 3. SPA → 服务端(初始数据加载) @@ -106,26 +106,58 @@ POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId} 三种操作打包成一个原子请求。前端告诉后端要写**哪张表**和**哪些列**;没有每模块专用写 API,元数据驱动 UI 根据 `gdsconfigformmaster.sTbName` 和 `gdsconfigformslave` 字段列表生成 payload。 -- `sSaveProName` 为空时,运行时走 `AddDelUpdCommonServiceImpl.java` 的默认 Add/Update 路径,生成参数化 `INSERT` / `UPDATE` / `DELETE`。 -- `sSaveProName` 非空时,运行时调用指定存储过程。`xlyEntry/src/main/resources/templates/templesql/sSaveProName.sql` 是工程师编写这类过程时使用的脚手架。 +- `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 涉及。 -> **待验证:** 真实保存请求体、响应体以及 `syslog4j` 中的实际 SQL 还未捕获。为了避免修改共享 dev DB 中的框架常量表,本轮没有执行保存。端点、handler 和 payload 形状已从源码确认。 +> **开放验证(需要真实保存):** 仍需捕获实时请求 body、响应 body,以及 `syslog4j` 中的实际 SQL,才能完全闭环。 -> **安全相关架构备注:** 前端在 payload 中直接提供 `sTable`。`BusinessBaseServiceImpl.addUpdateDelBusinessData` 会读取该值并分发删除 / 更新 / 新增。类级 `sTableNameList` 主要作为缓存失效 gate,而不是“该表是否被此表单授权”的 gate。已确认的缓解包括租户作用域自动注入,以及模块可通过 `sSaveProNameBefore` / `sSaveProName` 做验证;但权限规则是按钮级,不是表级。净结论:通用保存端点信任前端的 `sTable` 值,值得维护 ticket。 +> **安全相关架构备注:** 前端在 payload 中直接提供 `sTable`。`BusinessBaseServiceImpl.addUpdateDelBusinessData` 会读取该值并分发删除 / 更新 / 新增。类级 `sTableNameList`(`BusinessBaseServiceImpl.java:162-169`,只含 `gdsformconst`、`gdsmodule`、`gdsconfigformmaster`、`gdsconfigformslave`)在部分分支被使用,但只是**多租户作用域绕过**闸门(这四张表是全局框架元数据,写入时会剥掉 `sBrandsId` / `sSubsidiaryId`;见 `BusinessBaseServiceImpl.java:165` 的 `//不需要公司子公司的表` 注释),不是“该表是否被此表单授权”的闸门。整个流程里唯一的模块 / 表交叉检查是 1768 行的 `mftproductionplanslave` 硬编码特例。缓解措施包括租户作用域自动注入、模块可通过 `sSaveProNameBefore` / `sSaveProName` 做验证;但权限规则是按钮级,不是表级。净结论:通用保存端点信任前端的 `sTable` 值,值得维护工单。 ### 5. 缓存失效 -修改任何四张元数据表中的 `gds*` 行,都会让每个运行节点失效缓存副本。xly 通过 JMS 消息做到这一点:`xlyErpJmsConsumer/.../ConsumerChangeGdsModuleThread.java` 监听“module changed”事件并清相关 Redis key。见[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)。 +修改元数据行后,保存 service 会同步调用 `BusinessCleanRedisData.delCleanRedisData*`,进而触发 `CleanRedisServiceImpl` 上的 `@CacheEvict`。名字相近的 JMS 路径用于基础数据合并,不负责清缓存。见[元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md)。 ### 6. 浏览器确认 保存返回成功;前端要么就地 patch 该行,要么用同一 `getBusinessDataByFormcustomId` 端点重新拉取表格。追踪结束。 +## 保存流程时序图 + +```mermaid +sequenceDiagram + autonumber + participant SPA as Browser SPA + participant CTRL as BusinessBaseController + participant SVC as BusinessBaseServiceImpl + participant CLEAN as BusinessCleanRedisData + participant DB as MySQL (xlyweberp_*) + participant REDIS as Redis (RedisCacheManager) + + Note over SPA: 用户在 sReopen 行点击修改,
编辑 sChinese,点击保存 + SPA->>CTRL: POST /business/addSysLocking?sModelsId=13
(乐观锁占用) + CTRL-->>SPA: 200 OK + SPA->>CTRL: POST /business/addUpdateDelBusinessData?sModelsId=13
{addData:[],updateData:[{sTable:"gdsformconst",column:{sId,sChinese,...}}],delData:[]}
Authorization: + Note over CTRL: AuthorizationInterceptor → 从 Redis 取得 UserInfo
RequestAddParamUtil.addParams(16 个 key,含 sBrandsId/sSubsidiaryId) + CTRL->>SVC: addUpdateDelBusinessData(param) + Note over SVC: 按行分发:
add → addBusinessData → businessBaseDao.add
update → updateBusinessData → businessBaseDao.update
del → deleteBusinessData → businessBaseDao.del
(sTable 来自前端;没有白名单检查) + SVC->>DB: 对 sTable 命名的表执行 INSERT/UPDATE/DELETE + DB-->>SVC: rows affected + Note over SVC: 如果 sTable 在 sTableNameList 中
(gdsformconst/gdsmodule/gdsconfigformmaster/
gdsconfigformslave)→ 写入前移除 sBrandsId/sSubsidiaryId
(4 张表的租户绕过) + SVC->>CLEAN: delCleanRedisData(sTable, sIds, sBrandsId, sSubsidiaryId, "update") + CLEAN->>REDIS: 对受影响 cache region 执行 @CacheEvict
(同步,同一事务路径) + REDIS-->>CLEAN: evicted + SVC-->>CTRL: Feedback{code:1,msg:"操作成功"} + CTRL-->>SPA: AjaxResult{code:1,...} + SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/...
(重新拉取表格;cache miss → 读取新 DB 数据) + CTRL->>DB: SELECT ... + DB-->>CTRL: rows + CTRL-->>SPA: dataset +``` + ## 本切片引入的概念 - [数据驱动的基本论点](../concepts/thesis.md):为什么 xly 把布局存为数据。 - [模块、表单、虚拟表](../concepts/modules-forms-vtables.md):三个核心名词。 -- [元数据驱动的请求生命周期](../concepts/request-lifecycle.md):四表读取 + 五键结果 map。 +- [元数据驱动的请求生命周期](../concepts/request-lifecycle.md):元数据读取 + 五键结果 map。 - [主从单据模式](../concepts/master-slave.md):`gdsconfigformmaster` / `slave` 本身就是该模式实例。 - [无物理外键、语义外键的现实](../concepts/semantic-fk.md):`gdsconfigformmaster.sParentId = gdsmodule.sId` 是语义 FK。 @@ -138,14 +170,20 @@ POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId} 维护人员: -- [运行时:BusinessBaseController 及相关组件](../reference/maintainer/runtime.md):执行四表读取的 controller 和 service 层。 -- [元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md):JMS 驱动 Redis flush。 +- [运行时:BusinessBaseController 及相关组件](../reference/maintainer/runtime.md):执行元数据读取的 controller 和 service 层。 +- [元数据变更后的缓存失效](../reference/maintainer/cache-invalidation.md):同步 `@CacheEvict`(JMS 路径服务于另一个目的)。 - [多服务部署](../reference/maintainer/deployment.md):`xlyEntry` vs `xlyApi` vs `xlyInterface`;本切片完全运行在 `xlyEntry` 上。 ## 待验证项 {#open-verification-items} -1. **真实捕获一次保存。** 端点、handler 和 payload 形状已从源码确认,但实际保存请求体尚未捕获。需要打开模块、点击新增、填写、保存,并捕获 JSON body 和响应。 -2. **保存 / 删除发出的精确 SQL**,从 `syslog4j` 或 MyBatis debug log 捕获。 -3. ~~**`addUpdateDelBusinessData` 中的 `sTable` 校验。**~~ **已关闭**:运行时不会把前端提供的 `sTable` 与表单授权支撑表交叉检查。已作为维护关注点记录。 - -前两项属于切片 1 v2,需要对 dev DB 做实际写入;为避免修改共享状态而暂缓。 +读路径已通过实时环境佐证;保存路径仍待继续捕获。 + +1. ~~**实时捕获一次读取。**~~ **已关闭**:在 BACK(`http://118.178.19.35:8597`,admin/123,版本 `基础版/8s`)点击系统常量配置,产生了与文档一致的 HTTP 交换: + ```text + GET /xlyEntry/business/getModelBysId/13?sModelsId=13 → 200 OK + POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715574676360040?sModelsId=13 → 200 OK + ``` + 两个 URL 都与 Wiki 完全匹配,包括路径变量旁边冗余的 `?sModelsId=13` query 参数。登录后 URL 停在 `/xtmkpz`,不会导航到 `/xtclpz`,这确认 URL fragment 是显示状态,不是路由驱动。 +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)。 +3. **保存 / 删除发出的精确 SQL**,从 `syslog4j` 或 MyBatis debug log 捕获。 +4. ~~**`addUpdateDelBusinessData` 中的 `sTable` 校验。**~~ **已关闭**:运行时不会把前端提供的 `sTable` 与表单授权支撑表交叉检查。已作为维护关注点记录。 diff --git a/zh/docs/slices/02-multi-tenancy.md b/zh/docs/slices/02-multi-tenancy.md index 8338dae..f5d7e78 100644 --- a/zh/docs/slices/02-multi-tenancy.md +++ b/zh/docs/slices/02-multi-tenancy.md @@ -10,7 +10,7 @@ xly 是多租户 SaaS。同一套代码库、同一套数据库 schema、同一 |---|---|---|---| | **`sBrandsId`**(加工商ID) | 几乎每条业务行 | 每行 | “这行属于哪个加工商 / 公司?” | | **`sSubsidiaryId`**(子公司ID) | 几乎每条业务行 | 每行 | “公司内哪个子公司?” | -| **`sVersionFlowId`**(版本流程ID) | 仅 `gdsmodule` | 每模块 | “这个模块属于哪个产品版本?” | +| **`sVersionFlowId` / `sVersionFlowCode`**(版本流程ID / code) | 仅 `gdsmodule` | 每模块标签 | “这个模块标记为属于哪个产品版本?”运行时可见性由许可证产出的模块列表控制。 | 前两者是**每行**作用域。第三者是模块列表加载时的**每模块**过滤。机制不同,层级不同。 @@ -18,7 +18,7 @@ xly 是多租户 SaaS。同一套代码库、同一套数据库 schema、同一 ### 覆盖多广 -`xlyweberp_saas_ai` 的 1,212 张表 / 视图(901 张基础表 + 311 个视图)中,**1,008 个同时带有 `sBrandsId` 和 `sSubsidiaryId`**。另有 1 个只带 `sBrandsId`,0 个只带 `sSubsidiaryId`。这几乎覆盖所有业务数据表和框架元数据表。 +几乎每张业务数据表和视图都带有 `sBrandsId` 与 `sSubsidiaryId`。**大多数框架元数据表也带有这两个列**,但四张表(`gdsformconst`、`gdsmodule`、`gdsconfigformmaster`、`gdsconfigformslave`)是明确例外:`BusinessBaseServiceImpl.sTableNameList`(162-169 行)把它们列为“不需要公司子公司的表”,1078-1084 行会从这些表的写入载荷中剥掉 `sBrandsId` / `sSubsidiaryId`。实际使用中它们保存的是所有客户共享的一组哨兵租户值。其他缺少其中一个或两个列的表,多为单租户共享字典或第三方 schema(`act_*`、`qrtz_*`)。 ### 如何注入 @@ -36,13 +36,13 @@ xlyApi 在 `xlyApi/src/main/java/com/xly/api/util/RequestAddParamUtil.java` 中 ### 查询中如何体现 -切片 1 的 `getModelBysId` 调用最终在每张元数据表上都带有: +切片 1 的 `getModelBysId` 调用中,每租户谓词: ```sql WHERE sBrandsId = #{sBrandsId} AND sSubsidiaryId = #{sSubsidiaryId} ``` -同样谓词出现在代码库中几乎每个业务数据查询里。这就是运行时的多租户边界。 +会加在**每租户覆盖读取**(`gdsconfigformpersonalize`、`gdsconfigformcustomslave`)以及**业务状态读取**(`sysbillnosettings`、`sysreport`,以及 JOIN `sftlogininfojurisdictiongroup` 的 `sysjurisdiction` 用户授权;注意返回 map key 叫 `gdsjurisdiction`,但实际读取的是 `sysjurisdiction`)上。它不会加在框架基础元数据读取(`gdsmodule`、`gdsconfigformmaster`、`gdsconfigformslave`、`gdsformconst`)上;这些表是全局的,只按 form-id 过滤。同样谓词出现在代码库中几乎每个业务数据查询里。这是租户拥有状态的运行时边界;框架元数据有意全局共享。 ### 失效模式 @@ -56,7 +56,7 @@ xly 以多个版本销售:**基础版**、**EBC-MDM**、**EBC-SD**、**EBC-RD* ### 版本在哪里定义 -`sisversionflow` 表(此 dev DB 中 1 行): +版本定义在 `sisversionflow` 字典表中,每个版本一行。关键列: | 列 | 值 | 含义 | |---|---|---| @@ -65,42 +65,33 @@ xly 以多个版本销售:**基础版**、**EBC-MDM**、**EBC-SD**、**EBC-RD* | `sFlowName` | `基础版` | 显示名 | | `bEbcErpPremium`, `bEbcMes`, `bEbcMesStandard`, `bSass` | flags | 该版本属于哪些产品变体 | -真实 SaaS 中这里会有更多行,每个不同版本一行。`gdsmodule` 中看到的 `EBC-MDM-002`、`EBC-SD-002`、`EBC-RD-007` flow code,应对应多版本生产 DB 中的行。 +> **当前 dev DB 状态:** `sisversionflow` 目前只定义了一行:`8S_001 / 基础版`。`gdsmodule.sVersionFlowCode` 中出现的其他版本码(`EBC-SD-002`、`EBC-RD-007`、`EBC-MDM-002` 等)作为模块行标签存在,但在这里没有匹配的 `sisversionflow` 行。SaaS 生产租户很可能会填充完整版本目录;dev DB 没有。 -### 模块如何按版本过滤 +### 模块如何按版本过滤(实际机制) -`sVersionFlowId` 只在三张表上: +`sVersionFlowId` / `sVersionFlowCode` 是 `gdsmodule` 行上的标签,用来标记每个模块属于哪个版本;**但这两个列没有出现在任何 Java 源码或 MyBatis mapper 中**(已验证:`grep -r sVersionFlowId xly-src --include='*.java' --include='*.xml'` 在 mapper SQL 中没有命中)。运行时并不直接按这两个列过滤。 -- `gdsmodule`(实时模块目录)。 -- `gdsmodule_0923bak`(备份快照)。 -- `gdsmodule_copy1`(另一个快照)。 +真正的控制点由许可证驱动:`xly-src/xlyBusinessService/.../license/`(TrueLicense + xly 的 `VerifyLicense.getModelAllList()`)返回租户许可证允许的模块 `sId` 列表。该列表会以 `sVerifyLicense` 形式逗号替换进菜单 SQL: -因此每版本过滤**只发生在模块发现时**,不是每个业务查询上。用户登录时,框架解析租户所属版本,然后把可见模块列表过滤到匹配 `gdsmodule.sVersionFlowId` 的模块。之后,每个加载的模块照常用 `sBrandsId` / `sSubsidiaryId` 读取数据。 - -`xlyweberp_saas_ai` 中按 `sVersionFlowId, sVersionFlowCode` 分组的图景: +```java +// MenuChildServiceImpl.java:38-65 — getBuMenuSql +sql.append(" AND m.sId in ("+sVerifyLicense+")"); +``` -| Flow code | 模块数 | 覆盖内容 | -|---|---|---| -| 空 | 1,002 | 未标记,多为框架内部模块和不受版本 gate 的项目 | -| `8S_001`(基础版) | 322 | Essentials baseline | -| `EBC-SD-002` | 15 | 销售 / 交付 | -| `EBC-RD-007` | 6 | 研发 | -| `EBC-MDM-002` | 5 | 主数据管理 | -| `EBC_001` | 4 | 基础 EBC bundle | -| `EBC-SD-003` | 2 | SD 变体 | -| `EBC-SD-001` | 1 | SD 变体 | -| `EBC-COM-001` | 1 | 公共组件 | +`sVerifyLicense` 要么由 xlyApi 的 `RequestAddParamUtil` 注入(50-52 行:`params.put("sVerifyLicense","'"+String.join("','",listModel)+"'")`),要么由 xlyEntry 中的 controller 手工组装参数(例如 `MobliePhoneController.java:57`)。因此每版本过滤真正发生在**模块发现阶段,由许可证层完成**,而不是通过 `sVersionFlowId`。`sVersionFlowId` / `sVersionFlowCode` 是给运维和 BACK 侧报表看的目录元数据;运行时控制链是 `sVerifyLicense` → 许可证产出的模块列表 → `IN (...)`。 -322 个基础版标记模块是通用授权核心;1,002 个未标记行大多是框架内部模块,不受版本 gate 影响。其余具名 flow 是版本特定 add-on。 +`gdsmodule`(dev DB 中 1358 行)里有三种标签模式: -## 为什么 dev 看起来小 +- **未标记行**(`sVersionFlowCode` 为空):1002 行。框架内部模块和不受版本可见性控制影响的页面。 +- **基础版标记行**(`sVersionFlowCode = '8S_001'`):322 行。所有版本都会获得的通用核心。 +- **版本特定行**:`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)。这些是由客户许可证控制可见性的增购模块。 -本 Wiki 使用的 `xlyweberp_saas_ai` 只有一个品牌(`sBrandsId = '1111111111'`)、一个子公司(同值)和一个填充版本(`8S_001`)。多租户机制已接好,但几乎没有压力测试。生产环境应预期几十个品牌 × 每品牌几十个子公司 × 多个版本,全部通过同样的每行过滤模式隔离。 +补充:本 Wiki 使用的 `xlyweberp_saas_ai` 只有一个品牌(`sBrandsId = '1111111111'`)、一个子公司(同值)和一个填充版本(`8S_001`)。多租户机制已接好,但几乎没有压力测试。生产环境应预期几十个品牌 × 每品牌几十个子公司 × 多个版本,全部通过同样的每行过滤模式隔离。 ## 本切片引入的概念 - *多租户作用域*:`sBrandsId` / `sSubsidiaryId` 是每行租户边界;框架的通用注入器是 `RequestAddParamUtil`。 -- *产品版本*:`sVersionFlowId` 通过 `sisversionflow` 实现每模块可见性过滤;区分 scoping(每行)和 gating(每模块)。 +- *产品版本*:`sVersionFlowId` / `sVersionFlowCode` 标记模块所属版本,但运行时模块可见性由许可证产出的 `sVerifyLicense` 模块列表控制;区分行级作用域和模块级可见性控制。 ## 本切片使用的参考 @@ -108,6 +99,6 @@ xly 以多个版本销售:**基础版**、**EBC-MDM**、**EBC-SD**、**EBC-RD* ## 待验证项 -1. **按版本过滤模块发现。** 机制合理,但尚未定位精确代码路径,候选是 `GdsmoduleController` 或 `GdsmoduleServiceImpl`。 -2. **Activiti 工作流。** `sVersionFlowId` 不是工作流 id(尽管名字里有 flow)。实际工作流表均为空;未来切片 7 在有活动流程 DB 时记录。 -3. **session 级租户解析。** JWT / session 如何把登录用户映射到 `sBrandsId` / `sSubsidiaryId`,位于 `RequestAddParamUtil` 下面一层,值得在维护章节追踪。 +1. ~~**按版本过滤模块发现:定位代码路径。**~~ **已关闭。** 许可证驱动过滤位于 `xlyBusinessService/.../service/impl/MenuChildServiceImpl.java:38-65`(`getBuMenuSql`),SQL 以 `AND m.sId in (#{sVerifyLicense})` 结束。`sVerifyLicense` 来自 `VerifyLicense.getModelAllList()`(TrueLicense 绑定),并通过 `RequestAddParamUtil`(xlyApi)或 controller 级参数组装(xlyEntry)注入。见上方已修正的“模块如何按版本过滤”章节;之前把它说成 `sVersionFlowId` 过滤是错误的。 +2. ~~**Activiti 工作流 / `sVersionFlowId` 不是工作流 id。**~~ **已关闭。** 已记录在 [Activiti 集成](../reference/maintainer/activiti.md):Activiti 已接线但 idle;未部署 BPMN;框架实际工作流使用三条非 Activiti 路径(一步 proc + bCheck、单据链、已接线但当前硬禁用的 Activiti 分发)。 +3. ~~**session 级租户解析:JWT / session 查找链路。**~~ **已关闭。** 链路(均在 `xlyBusinessService/.../web/token/` 下):`AuthorizationInterceptor.preHandle` 用 `Authorization` header 调 `RedisTokenManager.getToken`(AES 解密 bearer,恢复 `(userId, sBrandsId, sSubsidiaryId, …)`),再由 `checkToken` 校验 Redis key `` 下缓存 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`。 diff --git a/zh/docs/slices/03-report.md b/zh/docs/slices/03-report.md index b995f81..716b303 100644 --- a/zh/docs/slices/03-report.md +++ b/zh/docs/slices/03-report.md @@ -77,8 +77,10 @@ POST /xlyEntry/business/getBusinessDataByFormcustomId/{formId}?sModelsId={module 一些视图支撑模块确实有打印模板:Excel via jxls、PDF via iText。机制与表格分离: - `getModelBysId` 返回 `report` 数组,来自通过 `sFormId` 关联到表单的 `sysreport` 行。 -- 前端“打印” / “导出”按钮调用 `xlyEntry/com/xly/report/` 下的 controller,加载 jxls / iText 模板,使用同一视图查询的“取全部行”包装,并把二进制文件流回。 -- 本模块没有模板,因此不覆盖打印路径。未来修订应选一个确实有模板的模块;`print template` 值得单独成章。 +- 前端“打印” / “导出”按钮调用 `xlyEntry/src/main/java/com/xly/web/report/` 下的 controller;`PrintReportController` 是活跃类。(同目录的 `PrintReportControllerOld.java` 文件仍存在,但类体已全部注释,是死代码。)controller 加载 jxls / iText 模板,使用同一视图查询的“取全部行”包装,并把二进制文件流回。 +- 本模块没有模板,因此不覆盖打印路径。 + +> **后续工作。** 如果后续修订选择一个确实带打印模板的模块,就能端到端追踪 jxls 导出。当前受 dev DB 状态阻塞(没有任何视图支撑表单挂接 `sysreport` 行;见下面的待验证项)。 ## 本切片引入或强化的概念 @@ -86,8 +88,29 @@ POST /xlyEntry/business/getBusinessDataByFormcustomId/{formId}?sModelsId={module - *共享模板 URL*:`/indexPage/commonList`、`/indexPage/commonBill`、`/indexPage/commonClassify`、`/indexPage/commonNewBill` 被数百个模块复用。URL 选择页面形状;模块身份来自 `sModelsId`。 - *报表模板*(仅预览):`sysreport` 通过 `sFormId` 关联,jxls / iText 模板由 `PrintReportController` 提供。 +## 本切片使用的参考 + +- [配置人员:如何定义虚拟表](../reference/builder/define-vtable.md):对视图支撑模块来说,表单的 `sType` 和 `sTbName` 就是声明。 +- [维护人员:运行时](../reference/maintainer/runtime.md):与切片 1 使用同一条 `BusinessBaseController` 路径;唯一新增的是 `sType` 分支。 +- 新页:*视图与报表*(维护人员下):记录 `viw_` / `Viw_` 命名约定、视图必须带出租户列才能保持租户安全的规则,以及打印模板流程。 + ## 待验证项 +> **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 的租户部署。 + 1. **带打印模板的视图支撑模块**:选择一个并端到端追踪 jxls 导出。 2. **`sType = 'proc'` 变体**:209 个表单由存储过程支撑,应另开切片说明过程如何返回结果集和参数如何流动。 -3. **视图租户安全。** 需要脚本审计哪些视图没有带出 `sBrandsId` / `sSubsidiaryId`。 +3. ~~**视图租户安全:审计哪些 `viw_*` 缺少 `sBrandsId`。**~~ **已关闭:305 个中有 19 个(约 6.2%)泄漏。** 对实时 DB 执行: + + ```sql + SELECT v.TABLE_NAME + FROM information_schema.views v + WHERE v.TABLE_SCHEMA = DATABASE() + AND v.TABLE_NAME LIKE 'viw_%' + AND v.TABLE_NAME NOT IN ( + SELECT TABLE_NAME FROM information_schema.columns + WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = 'sBrandsId' + ); + ``` + + 此 dev DB 返回 19 行,包括 `viw_purorder_slave_detail`、`viw_qlyprocesstest`、`viw_accproductstoreinvoice*` 家族、`viw_hmwxjy*` 等。如果某个表单指向这些视图且 `gdsconfigformmaster.sWhere` 没有外层租户谓词,它们就是潜在跨租户泄漏点。下一步应审计这些具体视图对应的表单层谓词;裸视图审计已经变成一条一次性 SQL。 diff --git a/zh/docs/slices/04-custom-field.md b/zh/docs/slices/04-custom-field.md index 939f13c..273eb0e 100644 --- a/zh/docs/slices/04-custom-field.md +++ b/zh/docs/slices/04-custom-field.md @@ -22,7 +22,7 @@ xly 在每个基础表单之上叠加**三张**定制表。每张表作用域不 假设租户“山东星海印务”想在客户列表表单上增加“客户内部编码”字段。客户或实施人员不修改 `gdsconfigformslave`,而是: -1. 打开**后台**中编辑 `gdsconfigformcustomslave` 行的模块(某个系统管理页面,可能是 `界面显示内容配置`;需在**后台**中点击验证)。 +1. 打开 BACK 中的 `界面显示内容配置`(`gdsmodule.sId=11`,`/jmnrpz`)。其第三个面板通过 `sId=19211681019715596285250620` 的 form-master 写入 `gdsconfigformcustomslave`,已在线验证。各 panel 映射见下面“待验证项”第 1 项。 2. 新增一行: - `sParentId` = 表单 `sId`(与基础 slave 指向同一个表单)。 - `sName = 'sInternalCode'`(字段列名)。 @@ -33,7 +33,7 @@ xly 在每个基础表单之上叠加**三张**定制表。每张表作用域不 下次该租户的任意用户加载表单时,会看到额外列。其他租户仍看到未修改的基础表单。 -> **dev 中为空的重要说明。** `gdsconfigformcustomslave` 在 `xlyweberp_saas_ai` 中有 **0 行**。表已接入框架,但当前 dev DB 没有租户使用它。下面的追踪来自代码推导;实时观察仍待补。 +> **已用 dev DB 确认。** `gdsconfigformcustomslave` 在 `xlyweberp_saas_ai` 中当前**为空**(0 行)。表已接入框架,但当前 dev DB 没有租户注册字段级覆盖。下面的追踪来自代码推导;端到端**观察**需要一个真实填充该表的租户部署。 ## 运行时如何合并 @@ -78,7 +78,17 @@ gdsconfigformuserslave (每用户视图偏好) ## 待验证项 -1. 找到实际编辑 `gdsconfigformcustomslave` 的 **后台** 页面。最可能是侧边栏中的 `界面显示内容配置`。 -2. **追踪合并代码。** 确认合并是在 MyBatis(两个视图)中完成,还是在 Java(`getFormSlaveData` + `getFormCustomSlaveData`)中完成。 -3. **`bVisible = false` 语义。** 它是隐藏已有基础字段,还是只抑制覆盖行本身?很可能是前者,但需要确认。 -4. **真实示例。** 在生产中找一个租户的实际 `gdsconfigformcustomslave` 行作为贯穿示例。 +1. ~~**找到实际编辑 `gdsconfigformcustomslave` 的 BACK 页面。**~~ **已关闭:确认为 `界面显示内容配置`**(`gdsmodule.sId=11`,URL `/jmnrpz`)。该页在一个 screen 中渲染三个 form-master panel,分别对应表单定义栈的三层: + + | Panel | `gdsconfigformmaster.sId` | 写入的 `sTbName` | + |---|---|---| + | Form-master 编辑器 | `19211681019715574673782610` | `gdsconfigformmaster` | + | 基础 slave 编辑器 | `19211681019715596207594120` | `gdsconfigformslave` | + | 每租户覆盖 | `19211681019715596285250620` | `gdsconfigformcustomslave` | + + 第三个 panel 就是“给租户 X 添加自定义字段”的规范通道。在线验证:在 BACK(admin/123)点击 `界面显示内容配置` 会触发 `POST /xlyEntry/business/getBusinessDataByFormcustomId/19211681019715596285250620?sModelsId=11` 加载现有 customslave 行;后续新增 / 修改操作的 `addUpdateDelBusinessData` POST 也会落到同一个 form-master,运行时按标准通用保存路径解析为对 `gdsconfigformcustomslave` 的写入。 +2. ~~**追踪合并代码。**~~ **已关闭**:合并发生在 `BusinessBaseServiceImpl.java:246-248` 的 Java 中,先调用 `businessGdsconfigformsService.getFormSlaveData(map)`,再调用 `getFormCustomSlaveData(map)`,并把两者 `addAll` 到同一个 `slaveList`。两个视图(`gdsconfigformslavemasterview`、`gdsconfigformcustomslavemasterview`)提供 master-with-slave 的读取形状;**合并**在 Java,**master-with-slave join** 在 SQL。 +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 每用户)。 +> **Item 4 — 暂缓(需要填充过的租户部署)。** 已对 dev DB 实证确认:`SELECT COUNT(*) FROM gdsconfigformcustomslave` 返回 0。该 DB 中没有租户注册每租户字段覆盖,因此无法从这里抽取真实 worked example。该项仍是真实缺口,等待有覆盖行的生产租户 DB。 + +4. **真实示例。** 在生产中找一个租户的实际 `gdsconfigformcustomslave` 行作为贯穿示例。(dev DB 已确认为空,需要有覆盖行的租户部署。) diff --git a/zh/docs/slices/05-customer-sql-override.md b/zh/docs/slices/05-customer-sql-override.md index ef67a34..141f13e 100644 --- a/zh/docs/slices/05-customer-sql-override.md +++ b/zh/docs/slices/05-customer-sql-override.md @@ -64,21 +64,132 @@ CREATE PROCEDURE `Sp_SalSalesCheck`(IN sLoginId varchar(100), ...) 经验规则:优先选择切片 4 的元数据定制。只有元数据模型确实无法表达客户需求时,才使用切片 5 SQL 覆盖。 -## 重庆展印 `Sp_SalSalesCheck` 的不同点 +## 示例:重庆展印的 `Sp_SalSalesCheck` vs 标准过程 -文件顶部显示: +对实时 dev DB 做量化 diff 后: -- 它从 `SysSystemSettings` 读取 `'CbxSrcNoCheck'` 行,决定哪些计费类型进入销售对账报表。这是标准过程可能没有暴露的客户特定开关。 -- 它调用全局 `Fun_GetLookCustomer(sLoginId, sBrId, sSuId)` helper 做权限作用域,与标准过程一致。 -- 它接受与标准过程相同的参数列表(`sLoginId` / `sCustomerId` / `sBrId` / `sSuId` / `bFilter` / `pageNum` / `pageSize` 等),因此框架调用点不变。 +| 方面 | 标准 `Sp_SalSalesCheck`(DB 中) | 重庆展印覆盖(`script/客户/重庆展印/Sp_SalSalesCheck.sql`) | +|---|---|---| +| 主体长度 | 1714 行 | 723 行(约标准的 42%) | +| 参数签名 | 14 个参数:`sLoginId, sCustomerId, sBrId, sSuId, bFilter, sUnTaskFormId, pageNum, pageSize, totalCount(OUT), countCloumn, countMapJson(OUT), sFilterOrderBy, sGroupby_select_sql, sGroupby_group_sql` | **完全相同**:14 个参数、相同顺序 | +| `SysSystemSettings.CbxSrcNoCheck` 查询 | **未使用** | **使用**,驱动“未对账印件清单来源”,即哪些计费类型来源进入报表 | +| `Fun_GetLookCustomer(sLoginId, sBrId, sSuId)` 权限作用域 | 使用 | 使用(相同调用) | +| 临时表聚合流(`B1`、`B2` 等,多段 `DROP TEMPORARY TABLE` + `INSERT INTO`) | 很重,是 1714 行主体的大头 | 移除 / 简化 | -未来修订可以与标准 `Sp_SalSalesCheck` 做并排 diff,精确解释分歧业务规则。当前最重要的是结构事实:过程形状和参数列表与标准一致,主体不同。 +因此重庆展印的覆盖: + +- 保持框架调用点不变(参数签名完全相同,所以元数据驱动分发器仍能正确调用;见[通用存储过程分发](../reference/maintainer/proc-dispatch.md))。 +- 增加了标准过程未暴露的 `CbxSrcNoCheck` 系统设置分支。schema 中另有 12 个 `Sp_*` 过程也使用 `CbxSrcNoCheck`(`Sp_Manufacture_MftWorkOrderAround`、`Sp_OverdueNoCheck`、`Sp_Receivables_*` 家族,以及兄弟过程 `Sp_SalSalesCheck1` / `_1227` / `_YanBao` / `_ded_copy1`);该覆盖把这个模式引入客户的主过程。 +- 去掉标准过程中较重的临时表聚合流。这不是更复杂的查询,而是一条更简单的查询路径;该客户的对账语义显然不需要标准完整聚合。 + +如果维护人员需要精确业务规则差异,直接 diff 两个主体: + +```bash +mysql --defaults-file=$HOME/.my.cnf xlyweberp_saas_ai \ + -BNe "SELECT ROUTINE_DEFINITION FROM information_schema.routines \ + WHERE ROUTINE_NAME='Sp_SalSalesCheck'" > /tmp/std.sql +diff /tmp/std.sql script/客户/重庆展印/Sp_SalSalesCheck.sql | head -200 +``` + +## 示例 2:万昌构建多级审批工作流 {#示例-2万昌构建多级审批工作流} + +上面的重庆展印例子替换的是**一个**过程主体。**`script/客户/万昌/`** 目录展示了更进一步的模式:客户扩展 schema,并构建标准框架没有随附的多级审批工作流。 + +定制树节选: + +```text +script/客户/万昌/ +├── 计件工资/ +│ ├── 日报审核/ +│ │ └── 领班驳回.sql ← 本切片锚点 +│ ├── 报表/ +│ │ ├── 包装补时.sql +│ │ ├── 员工大废品.sql +│ │ ├── 班组大废品率查询报表.sql +│ │ ├── 手工质检组返工.sql +│ │ └── Sp_Manual_quality_inspection_rework.sql +│ └── 计件工资核算/ +│ ├── 计件工资/ +│ │ ├── sp_piece_rate_j.sql +│ │ ├── sp_piece_rate_JZ.sql +│ │ ├── sp_piece_rate_other.sql +│ │ └── sp_piece_rate_w.sql +│ ├── 员工工资汇总查询/员工工资汇总查询.sql +│ ├── Sp_BtnEven_CalcJsHs.sql +│ └── sp_btn_WorkOrderAssessmentPassRate.sql +├── Sp_getworkorder_calc_cb.sql +└── … +``` + +这些中文目录(`计件工资` / `日报审核`)把客户组织流程直接编码进文件系统。维护人员只看 `ls`,就能知道每个脚本属于哪条业务流程。 + +### 驳回脚本实际做了什么 + +`领班驳回.sql` 共 185 行,定义 `Sp_mftproductionreportmaster_check1_0`。命名遵循 xly 的状态转换约定:`Sp_
_check_`,所以 `check1_0` 表示“从状态 1(已审核)回到状态 0(草稿)”,也就是驳回。 + +核心 UPDATE 逻辑裁剪如下: + +```sql +SET p_setSql = CONCAT('bManager = 0, + bIPQC = 0, + bDeputy = 0, + bSubmit = 0, + bWorkshopManager = 0, + bCheck = 0, + sRejectMemo = ''', p_sRejectMemo, ''', + sMReserve1 = ', p_textareaValue); + +Set @sSqlStmt = CONCAT('Update mftproductionreportmaster + Set ', p_setSql, ' + Where sId = ''', p_sTmpId, ''' + AND sBrandsId = ''', sBrId, ''' + AND sSubsidiaryId = ''', sSuId, ''''); +PREPARE sSqlStmt FROM @sSqlStmt; +EXECUTE sSqlStmt; + +CALL sp_add_flow_log(p_sTmpId, p_sTmpId, '驳回', '驳回', '驳回', + sMakePerson, p_sRejectMemo, @sReturn, @sCode); +``` + +所以一次按钮点击会同时重置**六个**审批标志,追加每行的驳回原因历史,并写入客户自定义审计日志。 + +### 哪些是客户侧内容,不在标准 schema 中 + +已对 dev DB 侦察目标(`xlyweberp_saas_ai`)验证: + +| 定制项 | 标准 schema 是否存在 | 万昌是否需要新增 | +|---|---|---| +| `mftproductionreportmaster` 上的多级审批列:`bManager`、`bIPQC`、`bDeputy`、`bSubmit`、`bWorkshopManager` | **不存在**;标准只有 `bCheck`、`sCheckPerson`、`tCheckDate` | 是,需要 `ALTER TABLE` 增加 5 个 boolean 列 | +| `sRejectMemo` 驳回原因历史列 | **不存在** | 是,需要 `ALTER TABLE` 增加 longtext | +| `sp_add_flow_log` 审计日志过程 | **不存在** | 是,完全由客户定义 | +| `Sp_
_check_` 命名约定 | **标准 DB 中没有过程使用该模式** | 是,万昌自己的约定 | +| 接入框架按钮机制 | 存在;`gdsconfigformslave.sButtonParam` 指向过程名 | 只需要配置 | + +因此,万昌的“领班驳回”工作流是**建立在 xly 按钮原语之上的客户自建状态机**:schema 扩展 + 自定义过程 + 自定义审计日志。框架只提供按钮点击分发(经 `/business/genericProcedureCall*` 或 form-slave 上的 button-param 钩子)。其余内容,包括单据处于什么状态、哪些标志翻转、写什么审计文本,都在客户侧。 + +这与 Activiti 解决同一问题的方式完全不同(BPMN 图 + assignee model + Activiti 任务表)。xly 框架允许客户选择任一模型: + +- **Activiti 模式**:部署 BPMN,通过 `gdsmoduleflow` 关联,并把 `ConstantUtils.bCheckflowCheck` 改成 `true`;见 [activiti.md](../reference/maintainer/activiti.md#路径-3activiti-bpmn-工作流有闸门目前代码中禁用)。 +- **万昌模式**:扩展 schema,编写状态转换过程,放到 `script/客户///` 下,手工应用。 + +代码库中生产相邻的定制展示的是万昌模式:Activiti 已接线,但 `script/客户/` 下没有任何客户目录部署 BPMN;而万昌式 schema-extending workflow 确实存在。这回答了“这个仓库里工作流如何定制”的实证问题:**通过每客户覆盖脚本交付 schema 扩展型存储过程**。 + +### 客户定制模式一览 + +18 个客户覆盖目录中,大多数并不是定制“工作流”本身,而是定制**计算和报表**。各目录大致内容: + +- `万昌`(14 个文件):包含 `领班驳回.sql` 工作流扩展,也包含计件工资计算过程。 +- `千彩`(50 个文件):定制最重的客户。主要是每租户计算覆盖(`Sp_Calc_*`、`Sp_Inventory_*`、`Sp_Manufacture_*`)和一个工作流列表视图(`viw_NoSalSalesChecking`)。 +- `重庆展印`(2 个文件):如上文所述,替换一个销售对账过程和一个配套视图。 +- `朝阳`(8)、`金宣发`(8)、`无锡中江`(8)、`亚明威`(6)、`福雅`(5)、`金九`(5)、`快马`(4)等:较小的计算 / 报表覆盖。 + +所以工作流定制模式(schema 扩展 + 状态转换过程 + 自定义审计)是**少见的**。只有客户流程确实不能被一步审批表达、标准框架的 `bCheck` toggle 不够时,才值得这样做。大部分客户分歧是计算逻辑,而不是工作流形状。 配套视图 `viw_salsaleschecking_pro.sql` 也出于同一原因存在:当覆盖需要标准没有的 join 形状时,工程师编写客户特定视图,并与过程一起应用到该客户 schema。 ## 本切片引入的概念 -- *两条定制通道*:通过**后台**编辑元数据(切片 1、2、4)vs. 直接应用到客户 schema 的原始 SQL 覆盖。 +- *两条定制通道*(细化[现有概念页](../concepts/customization-channels.md)):通过**后台**编辑元数据(切片 1、2、4)vs. 直接应用到客户 schema 的原始 SQL 覆盖。 - *客户间 schema 分歧*:同一过程名在不同客户 DB 中可能表示不同过程,影响维护人员分析运行时行为。 ## 参考项 @@ -87,7 +198,7 @@ CREATE PROCEDURE `Sp_SalSalesCheck`(IN sLoginId varchar(100), ...) ## 待验证项 -1. **脚本应用是否真的完全手工?** 还是存在 Quartz job / `DbToDbController` 机制?需要读 `DbToDbServiceImpl.java` 确认。 -2. **审计。** 写小脚本连接客户 DB,把每个 `Sp_*` / `viw_*` 主体与标准 diff。意外分歧是运维风险。 -3. **并排 `Sp_SalSalesCheck` diff。** 当前只描述结构,未来应纳入实际主体差异,说明重庆展印改变了哪条业务规则以及原因。 +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/客户//.sql` 都是为了可追溯性提交,并由工程师 / DBA 通过 `mysql --defaults-file=… < the-file.sql` 手工应用。 +2. **审计。** 写小脚本连接客户 DB,把每个 `Sp_*` / `viw_*` 主体与标准版本做 diff。意外分歧是运维风险。*后续工作:针对单个租户 DB 的审计查询只是一条语句;真正工作量在于把它自动化到全部客户环境。* +3. **并排 `Sp_SalSalesCheck` diff。** 上面的结构化 diff 表(大小、参数、关键 SQL 特征、`CbxSrcNoCheck` 分支)覆盖了分歧的*形状*;如果要进一步解释精确业务规则差异,可以补过程主体级 diff。*后续工作:上面的命令可按需生成;把完整 diff 嵌入 wiki 页面的收益不值得增加页面重量。* 4. **生命周期。** 客户升级、恢复、重建 schema 时,每个覆盖如何重新应用?部署章节需要 runbook。 diff --git a/zh/docs/slices/06-hardware.md b/zh/docs/slices/06-hardware.md index 2a1d394..a90188b 100644 --- a/zh/docs/slices/06-hardware.md +++ b/zh/docs/slices/06-hardware.md @@ -12,7 +12,7 @@ xly 是印刷行业 ERP。在车间,印刷机由 PLC(programmable logic cont |---|---| | **模块** | `xlyPlc`(代码库中的兄弟 Spring Boot 服务) | | **方向** | 单向:PLC → ERP DB(不向印刷机回发命令) | -| **节奏** | 定时轮询(Quartz `PlcScheduledTasks`) | +| **节奏** | 定时轮询(`PlcScheduledTasks` 中的 Spring `@Scheduled` cron,例如 `0/30 * * * * ?` 和 `0/1 * * * * ?`) | | **每机型区分** | Spring profile(`-S10`、`-T0`、`-T1`、`-15S`、`-CT`、`-yt`、`-pro`) | | **写入表** | `mftProduceReportMachineState`(dev 中约 207k 行)及相关机器状态从表 | @@ -23,7 +23,7 @@ xly 是印刷行业 ERP。在车间,印刷机由 PLC(programmable logic cont | 文件 | 角色 | |---|---| | `PlcApplicationBoot.java` | Spring Boot 入口 | -| `web/scheduler/PlcScheduledTasks.java` | Quartz 驱动轮询循环 | +| `web/scheduler/PlcScheduledTasks.java` | `@Component`,包含两个 `@Scheduled` cron 方法(每 30 秒和每 1 秒)驱动轮询循环 | | `web/scheduler/PlcRunStatus.java` | 当前轮询周期的内存状态 | | `web/scheduler/service/PlcToErpService.java` | 接口 | | `web/scheduler/service/impl/PlcToErpServiceImpl.java` | 实现:读 PLC、写 DB | @@ -72,7 +72,7 @@ application-pro.yml (生产) - 可独立部署;多个客户把它运行在靠近印刷机的机器上,与中心 ERP 服务器分离。 - 可独立失败:桥崩溃时,框架继续在旧机器状态上运行;框架宕机时,桥继续写入,框架恢复后看到缓冲行。 -- 难以在没有真实印刷机的情况下端到端测试。多数 CI 测试会 stub PLC 读取。 +- 难以在没有真实印刷机的情况下端到端测试。多数 CI 测试只能模拟 PLC 读取。 ## 本切片引入的概念 @@ -84,6 +84,8 @@ application-pro.yml (生产) ## 待验证项 +> **Item 1 — 暂缓(超出仓库可验证范围)。** 字节协议本身来自各印刷机型厂商文档,不在 xly 源码树中。每个 `xlyPlc/src/main/resources/application-.yml` 只携带**参数**(波特率、帧格式、寄存器地址、轮询调优项);**协议语义**属于印刷机厂商知识。完整记录这些内容是部署运维工作,不是针对 src / DB / web 的 wiki 审计能完成的。 + 1. **线缆协议。** 每个机型有不同字节协议;每个 `application-.yml` 携带参数。按机型记录协议是独立小众章节。 -2. **桥 → ERP DB 延迟。** 每个 profile 的轮询间隔是多少?如何影响车间看板刷新?值得记录。 -3. **为什么 `xlyRxtx` 在 `settings.gradle` 中禁用。** RXTX 是原生串口库;build 排除了它。需要确认 xlyPlc 当前是否不依赖串口,还是只在特定部署中需要。 +2. ~~**桥 → ERP DB 延迟 / 轮询间隔。**~~ **已关闭。** `PlcScheduledTasks.java` 中有两个 Spring `@Scheduled` cron 方法(未观察到 cron 字符串按 profile 变化):`0/30 * * * * ?`(每 30 秒,74 行)和 `0/1 * * * * ?`(每 1 秒,105 行)。125 行还有一个已注释的第三个 cron(`0 */2 * * * ?`),当前休眠。每 profile 参数调优发生在轮询代码读取 `application-.yml` 时,不在 cron 表达式本身。车间看板刷新独立于桥:它的 `viw_*` 聚合会在每次 FROUNT 请求时重读 `mftProduceReportMachineState`,所以印刷机发出状态后,看板最多约 30 秒能看到新行。 +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。 diff --git a/zh/docs/slices/07-workflow.md b/zh/docs/slices/07-workflow.md index 9224128..7d0b493 100644 --- a/zh/docs/slices/07-workflow.md +++ b/zh/docs/slices/07-workflow.md @@ -1,5 +1,7 @@ # 切片 7(暂缓)— 带工作流的模块 +> **暂缓:需要一个已部署 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)页已经记录了代码推导假设。 + > **占位,暂缓。** 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 行。没有流程部署,没有任务运行。当前环境中工作流休眠。 > > 这里原本是切片 2;该位置已重新分配给[多租户](02-multi-tenancy.md),因为多租户在此环境中可观察。等有活动流程的 DB 可用,或决定仅以代码推导假设记录 Activiti 时,本切片再补全。 diff --git a/zh/docs/slices/index.md b/zh/docs/slices/index.md index 13e4f50..048d240 100644 --- a/zh/docs/slices/index.md +++ b/zh/docs/slices/index.md @@ -13,7 +13,7 @@ | 3 | [带报表的模块](03-report.md) | 视图、报表模板、jxls | | 4 | [扩展自定义字段](04-custom-field.md) | `gdsconfigformuserslave`、无 schema 扩展 | | 5 | [每客户 SQL 覆盖](05-customer-sql-override.md) | `script/客户/`、覆盖通道 | -| 6 | [硬件集成模块](06-hardware.md) | `xlyPlc`、串口、到印刷机的 RPC | +| 6 | [硬件集成模块](06-hardware.md) | `xlyPlc`、PLC 轮询、写入 ERP DB | | 7 | [带工作流的模块](07-workflow.md)(暂缓) | Activiti、`biz_flow`、审批;dev 中休眠 | 切片 1 到 5 是主线。切片 6 对不会接触硬件的读者可选。切片 7 暂缓:dev DB 中 Activiti 表为空。 diff --git a/zh/mkdocs.yml b/zh/mkdocs.yml index c1fe98d..166d7e9 100644 --- a/zh/mkdocs.yml +++ b/zh/mkdocs.yml @@ -36,11 +36,14 @@ theme: icon: material/brightness-4 name: 切换到浅色模式 -# CJK 搜索:分隔符包含词边界和 CJK 标点。 -# 需要时由目录生成器处理真正的中文分词。 +# 搜索分隔符:空白、常见标点、点号、HTML 实体和 CJK 标点。 +# 已移除 CamelCase 拆分,避免 `BusinessBaseServiceImpl` 或 `MyBatis` +# 被切成多个普通词,导致精确代码标识符搜索被大量噪声淹没。 +# 如需部分 token 搜索,lunr 支持后缀通配符(例如 `Service*`)。 +# 真正的中文分词仍由目录生成器在索引阶段调用 jieba 处理。 plugins: - search: - separator: '[\s\-,;:!=\[\]()"`/]+|(?!\b)(?=[A-Z][a-z])|\.(?!\d)|&[lg]t;|[\u3000-\u303f\uff00-\uffef]' + separator: '[\s\-,;:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|[\u3000-\u303f\uff00-\uffef]' markdown_extensions: - admonition @@ -100,11 +103,14 @@ nav: - 4. 参考(维护人员): - reference/maintainer/index.md - "本地运行 xlyEntry": reference/maintainer/running-locally.md + - "技术栈": reference/maintainer/tech-stack.md - "运行时:BusinessBaseController 及相关组件": reference/maintainer/runtime.md - "通用存储过程分发": reference/maintainer/proc-dispatch.md - "元数据变更后的缓存失效": reference/maintainer/cache-invalidation.md - "SQL 模板(xlyEntry/templesql/)": reference/maintainer/sql-templates.md - "多服务部署": reference/maintainer/deployment.md + - "元数据管理服务(xlyManage)": reference/maintainer/management-services.md + - "BI / KPI / 图表引擎": reference/maintainer/bi-engine.md - "Activiti 集成": reference/maintainer/activiti.md - 5. API 参考: - api-reference/index.md