From bbcde3e6069b61413751a843e8b2616ed31bda07 Mon Sep 17 00:00:00 2001 From: zichun Date: Sat, 9 May 2026 09:01:48 +0800 Subject: [PATCH] docs: en wiki — five-pass verification audit, fix divergences inline --- en/docs/api-reference/external.md | 30 ++++++++++++++++++------------ en/docs/api-reference/internal.md | 8 +++++--- en/docs/api-reference/messaging.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- en/docs/api-reference/webhooks.md | 10 +++++++++- en/docs/concepts/api-surface.md | 2 +- en/docs/concepts/index.md | 2 +- en/docs/concepts/master-slave.md | 9 +++++++++ en/docs/concepts/modules-forms-vtables.md | 38 ++++++++++++++++++++++++++++++++++++++ en/docs/concepts/multi-tenancy.md | 14 ++++++++++---- en/docs/concepts/request-lifecycle.md | 56 +++++++++++++++++++++++++++++++++++++------------------- en/docs/index.md | 9 ++++++--- en/docs/reference/maintainer/activiti.md | 37 +++++++++++++++++++++++++------------ en/docs/reference/maintainer/cache-invalidation.md | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------- en/docs/reference/maintainer/deployment.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------- en/docs/reference/maintainer/proc-dispatch.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++------- en/docs/reference/maintainer/runtime.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- en/docs/reference/maintainer/tech-stack.md | 27 ++++++++++++++++----------- en/docs/slices/01-hello-world.md | 62 ++++++++++++++++++++++++++++++++++++++------------------------ en/docs/slices/03-report.md | 11 +++++++---- 19 files changed, 554 insertions(+), 231 deletions(-) diff --git a/en/docs/api-reference/external.md b/en/docs/api-reference/external.md index d79278e..806431c 100644 --- a/en/docs/api-reference/external.md +++ b/en/docs/api-reference/external.md @@ -31,8 +31,12 @@ The flow: 2. Look up the `sysapi` row keyed by `sApiCode` (via `ApiServiceImpl.invoke` → `SELECT … FROM sysapi WHERE sApiCode = #{sApiCode}`). -3. If the row's `bHasToken` flag is set, validate the token against - `sysapithirdtoken` (or the equivalent token store the row points at). +3. If the row's `bHasToken` flag is set, AES-decrypt the bearer token to + recover the `corpid`, then validate that `corpid` against `sysapibrand` + via `BrandServiceImpl.selectByCorpid`. If the brand row's `iLossTime` + is non-zero, also check the token's embedded timestamp hasn't expired. + (`sysapithirdtoken` is for *outbound* tokens — xly calling third-party + APIs — not for validating inbound bearer tokens here.) 4. Run the SQL template stored in `sysapi.sDataSql` with the request body merged into the parameter map. 5. Log the call to `sysapilog`. @@ -67,7 +71,7 @@ dry-run against the test fixture in `sDataTest`. | Endpoint | Method | Purpose | |---|---|---| -| `/token/getToken?corpid=&corpsecret=` | POST | Issue a bearer token for an integrator's `(corpid, corpsecret)` pair. | +| `/token/getToken?corpid=&corpsecret=` | GET / POST | Issue a bearer token for an integrator's `(corpid, corpsecret)` pair. (Mapping is method-agnostic.) | The token returned is what `/api/invoke/{sApiCode}` expects in `Authorization`. The full implementation is in @@ -84,13 +88,15 @@ These are smaller specialised APIs hosted in the same WAR: | Endpoint root | Controller | Purpose | |---|---|---| -| `/online/api/{sApiCode}` | `OnlineController` | Read-only execution of a `sysapi` row (no write). Useful for public-data endpoints. | -| `/online/onlineword/{sApiCode}` | `OnlineController` | Variant returning a "word"-style template payload. | -| `/online/onlinelist`, `/online/getToken` | `OnlineController` | Listing of online APIs and their token issuance. | -| `/pro/get/{sProName}` | `ProContentController` | Fetch metadata about a stored procedure by name. | -| `/pro/getData/{sProName}` | `ProContentController` | Execute the named stored procedure and return its result-set. | -| `/pro/alert/{redisId}`, `/pro/getAlertValue/{redisId}` | `ProContentController` | Read alert/notification values keyed by Redis id. | -| `/pro/executeSql` | `ProContentController` | Execute a parameterised SQL template (admin-tier). | +| `/online/api/{sApiCode}` | `OnlineController` | Renders the BACK in-browser API debug/console page for the given `sysapi` row (returns a Thymeleaf view, not API execution). | +| `/online/onlineword/{sApiCode}` | `OnlineController` | Renders the "word"-style API documentation page. | +| `/online/onlinelist` | `OnlineController` | Renders the online-API listing page. | +| `/online/getToken` | `OnlineController` | Renders the in-browser token-acquisition helper page. | +| `/pro/get/{sProName}` | `ProContentController` | Renders the BACK page that displays a stored procedure's source. | +| `/pro/getData/{sProName}` | `ProContentController` | Returns the stored procedure's source text (not its result-set). | +| `/pro/alert/{redisId}` | `ProContentController` | Renders the alert/notification display page for the given Redis key. | +| `/pro/getAlertValue/{redisId}` | `ProContentController` | Returns the value cached in Redis under `redisId`. | +| `/pro/executeSql` | `ProContentController` | Executes a `sSql` payload directly (admin-tier dev tool). | | `/thirdparty/*` | `ThirdPartyController` | CRUD over third-party-API definitions and the `checkPartyApi` validator. Backed by `sysapithirdparty`. | | `/thirdtoken/*` | `ThirdTokenController` | CRUD over outbound-token configs. Backed by `sysapithirdtoken`. | | `/brand/*` | `BrandController` | CRUD over the partner-supplier list (`sysapibrand`). | @@ -110,8 +116,8 @@ These tables hold the API metadata. All carry `sBrandsId` / | `sysapibrand` | Partner / supplier directory. | | `sysapithirdparty` | Outbound third-party endpoint definitions. | | `sysapithirdtoken` | Outbound third-party token configs. | -| `sysapidbtodb` | DB-to-DB sync API definitions. | -| `sysapidbtodblog` | DB-to-DB sync run log. | +| `sysapidbtodb` | DB-to-DB sync API definitions. **Owned by `xlyFlow`'s `DbToDbController`, not xlyApi** — listed here because the table lives in xlyApi's `sysapi.sql`. | +| `sysapidbtodblog` | DB-to-DB sync run log. Same — written by xlyFlow. | ## How an integrator uses this diff --git a/en/docs/api-reference/internal.md b/en/docs/api-reference/internal.md index 22cf55c..e6fb8c4 100644 --- a/en/docs/api-reference/internal.md +++ b/en/docs/api-reference/internal.md @@ -52,15 +52,17 @@ 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` | Activiti workflow surface (approve / reject / view) — only meaningful in deployments that run a flow. | +| `/checkflow/*` | `CheckFlowController` | Activiti workflow surface (approve / reject / view) — only meaningful in deployments that run a flow. The class file is `CheckFlowController.java` (camelCase) but the `@RequestMapping` value is all-lowercase `/checkflow`. | ## Reporting and printing The print surface lives under `xlyEntry/src/main/java/com/xly/web/report/`: - `PrintReportController` — current jxls / iText print path. -- `PrintReportControllerOld` — legacy print path retained for older - templates. +- `PrintReportControllerOld.java` — file exists but its class body is + fully commented out (and the commented-out class inside is named + `PrintReportController`, not `*Old`). It is dead source kept for + reference, not an active controller. The frontend's "打印" / "导出" buttons hit these controllers, which load a template from `sysreport`, run the matching view-backed query, and stream diff --git a/en/docs/api-reference/messaging.md b/en/docs/api-reference/messaging.md index 9ac6d13..9c1a8dc 100644 --- a/en/docs/api-reference/messaging.md +++ b/en/docs/api-reference/messaging.md @@ -5,31 +5,73 @@ runs two message brokers, each with a different role: | Broker | Used for | Producer | Consumer | |---|---|---|---| -| **ActiveMQ / JMS** | Cache invalidation, in-cluster fan-out events. The metadata-change pipeline ([cache invalidation](../reference/maintainer/cache-invalidation.md)) rides on this. | `xlyErpJmsProductor` | `xlyErpJmsConsumer` | +| **ActiveMQ / JMS** | In-cluster fan-out events: base-data merge jobs (consolidating per-tenant rows into flattened lookup tables) and document-update / document-delete notifications. **Despite the historical naming of one queue, this channel is NOT used for Redis cache invalidation** — see [Cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md) for the actual cache-evict path. | `xlyErpJmsProductor` | `xlyErpJmsConsumer` | | **RocketMQ** | Other integration flows where the ActiveMQ assumptions don't fit. | `RocketMQServiceImpl` (in `xlyBusinessService`) | (varies — service-specific) | This page is a pointer rather than a deep dive — exact queue names and payloads are documented at the consumer-thread level in `xlyErpJmsConsumer/src/main/java/com/xly/xlyerpjmsconsumer/`. -## ActiveMQ / JMS — the cache-invalidation channel +## ActiveMQ / JMS — base-data merge + fan-out channel Producer-side queue declarations live in `xlyErpJmsProductor/src/main/java/com/xly/xlyerpjmsproductor/config/P2pQueue.java`. -Notable destinations the framework uses today (read the file for the -full list): +The full set is **24 destinations**, grouped by intent: + +### Module / control (2) + +| Constant | Purpose | +|---|---| +| `ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` | "Module metadata changed" — `ConsumerChangeGdsModuleThread` runs the stored proc `PRO_ERPMERGEBASEGDSMODULE` to merge per-tenant `gdsmodule` rows into a flattened base lookup table. **Does not invalidate Redis caches** despite the name — Redis cache eviction happens synchronously via `@CacheEvict` in BACK during save. See [Cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md). | +| `ERP_JMS_ACTIVEMQ_CHANGE_WORK_ORDER_CONTROL` | Work-order control state changed (status/aggregate flags) — fan-out for downstream recalculation. | + +### Document operations (6) + +| Constant | Purpose | +|---|---| +| `ERP_JMS_ACTIVEMQ_UPD_SALE_ORDER` / `_UPD_WORK_ORDER` / `_UPD_PRODUCTION_REPORT` | "Document was updated" notifications consumed by background workers (totals recalculation, downstream invalidations). | +| `ERP_JMS_ACTIVEMQ_DEL_SALE_ORDER` / `_DEL_WORK_ORDER` / `_DEL_PRODUCTION_REPORT` | Document-delete notifications. | + +### Element-master change fan-out (7) — `CHANGE_ELE_*` + +| Constant | Purpose | +|---|---| +| `_CHANGE_ELE_CUSTOMER` | Customer-master change. | +| `_CHANGE_ELE_EMPLOYEE` | Employee-master change. | +| `_CHANGE_ELE_MACHINE` | Shop-floor machine-master change. | +| `_CHANGE_ELE_MATERIALS` | Materials-master change. | +| `_CHANGE_ELE_PRODUCT` | Product-master change. | +| `_CHANGE_ELE_PROCESS` | Process-master change. | +| `_CHANGE_ELE_TEAM` | Team-master change. | + +### System-info / lookup-table change fan-out (9) — `CHANGE_SIS_*` | Constant | Purpose | |---|---| -| `ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` | "Module metadata changed" — triggers `ConsumerChangeGdsModuleThread` to bust the relevant Redis caches across nodes. See [Cache invalidation on metadata change](../reference/maintainer/cache-invalidation.md). | -| `ERP_JMS_ACTIVEMQ_CHANGE_ELE_CUSTOMER` | Customer-master change fan-out. | -| `ERP_JMS_ACTIVEMQ_CHANGE_ELE_EMPLOYEE` | Employee-master change fan-out. | -| `ERP_JMS_ACTIVEMQ_CHANGE_ELE_MACHINE` | Shop-floor machine-master change fan-out. | -| `ERP_JMS_ACTIVEMQ_UPD_SALE_ORDER`, `ERP_JMS_ACTIVEMQ_UPD_WORK_ORDER`, `ERP_JMS_ACTIVEMQ_UPD_PRODUCTION_REPORT` | "Document was updated" notifications consumed by background workers (totals recalculation, downstream invalidations). | -| `ERP_JMS_ACTIVEMQ_DEL_SALE_ORDER`, `ERP_JMS_ACTIVEMQ_DEL_WORK_ORDER`, `ERP_JMS_ACTIVEMQ_DEL_PRODUCTION_REPORT` | Document-delete notifications. | - -Each destination has a corresponding `Consumer*Thread` class under -`xlyErpJmsConsumer/.../thread/` that handles the message asynchronously. +| `_CHANGE_SIS_CUSTOMER_CLASSIFY` | Customer-classification change. | +| `_CHANGE_SIS_DELIVER` | Delivery-method change. | +| `_CHANGE_SIS_FORMULA` | Calculation-formula change. | +| `_CHANGE_SIS_PAYMENT` | Payment-method change. | +| `_CHANGE_SIS_PROCESS_CLASSIFY` | Process-classification change. | +| `_CHANGE_SIS_PRODUCT_CLASSIFY` | Product-classification change. | +| `_CHANGE_SIS_SALES_MAN` | Sales-personnel change. | +| `_CHANGE_SIS_TAX` | Tax-rate change. | +| `_CHANGE_SIS_WORK_CENTER` | Work-centre change. | + +(Constant prefixes elided to `_…` after the first table — the full literal is `ERP_JMS_ACTIVEMQ_…`.) + +### Listener side + +`xlyErpJmsConsumer/.../consumer/Consumer.java` is a single class that +hosts **all 24 `@JmsListener` methods** — one per destination. Each +method dispatches the payload to a corresponding `Consumer*Thread` +class under `xlyErpJmsConsumer/.../thread/`, which executes the +domain-specific work (typically calling a `PRO_ERPMERGEBASE*` stored +proc that consolidates per-tenant rows into a flattened base lookup +table) asynchronously. There is *one* listener class with 24 methods, +*not* 24 listener classes. None of the consumer threads invoke +`@CacheEvict` or `cleanRedis*` — Redis cache invalidation is +synchronous in BACK during save, see [cache-invalidation.md](../reference/maintainer/cache-invalidation.md). ## RocketMQ — other flows diff --git a/en/docs/api-reference/webhooks.md b/en/docs/api-reference/webhooks.md index d570a7f..4789630 100644 --- a/en/docs/api-reference/webhooks.md +++ b/en/docs/api-reference/webhooks.md @@ -21,6 +21,14 @@ http:///xlyInterface/swagger-ui.html (or the equivalent JSON descriptor at `http:///xlyInterface/v2/api-docs`). +> **Caveat:** the project pulls the SpringFox jars but does **not** +> register a `Docket` bean (no `@EnableSwagger2` or +> `@Bean Docket api()` anywhere in `xly-src`). The `swagger-ui.html` +> shell is served from the jar's static resources, but `/v2/api-docs` +> returns an effectively empty descriptor — the UI is "the dependency +> ships" rather than "a populated try-it-out console". A maintainer +> who wants the live API listed has to add a `Docket` bean. + ## The data-driven receiver — `/interfaceDefine/*` Mirror of the [external API's `/api/invoke`](external.md) pattern, but @@ -49,7 +57,7 @@ they have to match a partner's fixed URL spec. | `/Pull` | POST | Vendor pull-pattern receiver. | | `/getKey/{key}` | GET | Public key fetch for a partner-named `key`. | | `/getKeyTest` | GET | Test-mode variant of `/getKey`. | -| `/send/sendQw` | POST | Enterprise-WeChat (企业微信) outbound message. | +| `/send/sendQw` | POST | Enterprise-WeChat (企业微信) outbound message. **Stub on this branch** — the method body in `SendQwController` is `return "ok";`; scaffolded for token-fetch but not finished. | Handlers: `xlyInterface/src/main/java/com/xly/web/WX_VendorWeb.java` and `xlyInterface/src/main/java/com/xly/wechat/test/SendQwController.java`. diff --git a/en/docs/concepts/api-surface.md b/en/docs/concepts/api-surface.md index d2b337b..05aab4b 100644 --- a/en/docs/concepts/api-surface.md +++ b/en/docs/concepts/api-surface.md @@ -35,7 +35,7 @@ sacrifice clarity: ## What each tier looks like at runtime -- **Internal** — see [the four-table read](../reference/maintainer/runtime.md#the-four-table-read). One +- **Internal** — see [the five-key read](../reference/maintainer/runtime.md#the-five-key-read). One endpoint (`/business/getModelBysId`) returns the entire form layout; another (`/business/addUpdateDelBusinessData`) writes any row in any table the metadata names. Few endpoints, generic shapes. diff --git a/en/docs/concepts/index.md b/en/docs/concepts/index.md index 5a860d1..38bcf8b 100644 --- a/en/docs/concepts/index.md +++ b/en/docs/concepts/index.md @@ -27,7 +27,7 @@ flowchart TB XMSG[/"xlyMsg
library"/] end - DB[("MySQL
xlyweberp")] + DB[("MySQL
xlyweberp_*")] REDIS[(Redis)] AMQ([ActiveMQ]) XEJMSC[xlyErpJmsConsumer] diff --git a/en/docs/concepts/master-slave.md b/en/docs/concepts/master-slave.md index 3b5f676..e647689 100644 --- a/en/docs/concepts/master-slave.md +++ b/en/docs/concepts/master-slave.md @@ -1,5 +1,14 @@ # The master / slave document pattern +> **Two unrelated "master / slave" concepts coexist in this codebase.** +> 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) +> 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 order, a payment voucher — is stored as **one header row plus N detail rows**. xly's term for this is **master / slave**. The master holds the diff --git a/en/docs/concepts/modules-forms-vtables.md b/en/docs/concepts/modules-forms-vtables.md index ad2e90b..1ea1a4b 100644 --- a/en/docs/concepts/modules-forms-vtables.md +++ b/en/docs/concepts/modules-forms-vtables.md @@ -91,3 +91,41 @@ documented in [Slice 1](../slices/01-hello-world.md) — knows how to render any module / form / virtual-table combination. There is no per-module Java code. PMs creating new modules are creating new rows; they are not creating new code paths. + +## Business-data table prefixes + +The wiki treats business modules as illustrations rather than subjects, +but the schema names them in a regular pattern. A maintainer can map +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) | `gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave`, `gdsjurisdiction`, `gdsroute`, `gdsformconst`, `gdsparameter`, … | +| `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) | +| `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) | +| `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) | +| `pit` | Product inventory transactions | `pitproductadjust`, `pitproductbarcode`, `pitproductcheck`, `pitproductinstore`, … (18 tables) | +| `qly` | Quality testing | `qlycomematerialstest`, `qlyproducttest`, `qlyprocesstest`, … (8 tables) | +| `kpi` | KPI tracking | `kpimaster`, `kpidetail`, `kpimoduleuserday`, … (7 tables) | +| `udf` | User-defined / generic-voucher framework | `udfaccountno`, `udfvouchermaster`, `udfvouchertemplatemaster`, … (5 tables) | +| `viw_` / `Viw_` | Database **views** (case inconsistent across schema) | `viw_mftworkorderprocess`, `viw_corebusinessreport`, `viw_accordercostanalysisnew`, … (311 views in total) | +| `plat_` | B2B printing-platform layer (out of scope per [index](../index.md#whats-out-of-scope)) | 92 tables — not documented here | +| `ai_` | AI / LLM features (out of scope) | 7 tables — not documented here | +| `act_`, `qrtz_` | Third-party schemas (Activiti workflow, Quartz scheduler) | covered transitively under [Activiti](../reference/maintainer/activiti.md) and [tech-stack Quartz](../reference/maintainer/tech-stack.md#4-workflow-scheduling) | + +The business-domain prefixes (`ele`, `mft`, `sal`, `quo`, `acc`, `pur`, +`ops`, `cah`, `sgd`, `ept`, `mit`, `pit`, `qly`, `kpi`, `udf`) and +their slaves all follow the same metadata-driven runtime — there is no +per-prefix Java code, just rows in `gdsconfigformmaster` / +`gdsconfigformslave` pointing at each backing table or view. diff --git a/en/docs/concepts/multi-tenancy.md b/en/docs/concepts/multi-tenancy.md index aa5dfb0..cf80bb4 100644 --- a/en/docs/concepts/multi-tenancy.md +++ b/en/docs/concepts/multi-tenancy.md @@ -16,10 +16,16 @@ two-paragraph summary you can link from anywhere. | **`sSubsidiaryId`** (子公司ID) | almost every business row | per-row | the user's session | | **`sVersionFlowId`** (版本流程ID) | `gdsmodule` only | per-module | the user's edition (against `sisversionflow`) | -Per-row scoping is universal: both `sBrandsId` and `sSubsidiaryId` appear -on essentially every business-data table and every framework-metadata -table. The convention is "if a row represents tenant-owned state, both -columns are present." +Per-row scoping is universal across business-data tables: both +`sBrandsId` and `sSubsidiaryId` appear on essentially every one. Most +framework-metadata tables also carry the columns, but four of them +(`gdsformconst`, `gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave`) +are an explicit exception — `BusinessBaseServiceImpl.sTableNameList` +(lines 162-169) lists them as "不需要公司子公司的表" and lines 1078-1084 +strip `sBrandsId`/`sSubsidiaryId` from the write payload for those +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- diff --git a/en/docs/concepts/request-lifecycle.md b/en/docs/concepts/request-lifecycle.md index 2f50d31..bc91cc8 100644 --- a/en/docs/concepts/request-lifecycle.md +++ b/en/docs/concepts/request-lifecycle.md @@ -16,8 +16,12 @@ variations on a theme. │ 3. SPA decides which module to load → calls /business/... │ └──────────────────────────────────────────────────────────────────────┘ │ - │ GET /xlyEntry/business/getModelBysId/{moduleId} - │ ?sModelsId={moduleId} + │ GET /xlyEntry/business/getModelBysId/{sModelsId} + │ ?sModelsId={sModelsId} + │ (the module's id appears in BOTH path and query — + │ the controller binds the path variable, but the + │ service reads sModelsId from the @RequestParam map, + │ so the SPA must include it in the query string too) ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ xlyEntry — BusinessBaseController.getModelBysId() │ @@ -25,22 +29,36 @@ variations on a theme. │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ RequestAddParamUtil.addParams(params, userInfo) │ │ │ │ → sBrandsId, sSubsidiaryId, sUserId, sLanguage, … │ │ -│ │ → tenant scope is now baked into every downstream query │ │ +│ │ (16 keys total — see runtime.md) │ │ +│ │ → tenant scope is now AVAILABLE for any downstream query │ │ +│ │ that wants it. Framework-metadata reads filter by │ │ +│ │ form-id only; per-tenant overlays + business data │ │ +│ │ reads filter by sBrandsId/sSubsidiaryId. │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ BusinessBaseService.getModelBysId(map) │ │ │ │ -│ ├── load gdsmodule row (the module) │ -│ ├── load gdsconfigformmaster row(s) │ -│ │ (joined to module via sParentId) │ -│ ├── load gdsconfigformslave rows │ -│ │ (joined to form-master via sParentId) │ -│ ├── merge gdsconfigformpersonalize (per tenant) │ -│ ├── merge gdsconfigformcustomslave (per tenant) │ -│ ├── load gdsjurisdiction (skipped for ADMIN) │ -│ ├── load gdsformconst (form-level constants) │ -│ ├── load sysbillnosettings (document-numbering) │ -│ └── load sysreport rows linked to this form │ +│ ├── 1. formData │ +│ │ └── gdsconfigformmaster (filtered by │ +│ │ sParentId = sModelsId; gdsmodule itself │ +│ │ is *not* SELECT-ed, only referenced by id) │ +│ │ + LEFT JOIN gdsconfigformpersonalize │ +│ │ (per-tenant overlay) │ +│ │ + per-master gdsconfigformslave │ +│ │ + per-master gdsconfigformcustomslave │ +│ │ (per-tenant overlay) │ +│ ├── 2. gdsformconst (filtered by sParentId only; │ +│ │ NOT tenant-scoped; sLanguage selects which │ +│ │ label column to return) │ +│ ├── 3. sysjurisdiction (per-user/group grants joined │ +│ │ to sftlogininfojurisdictiongroup + │ +│ │ sisjurisdictionclassify; skipped for ADMIN. │ +│ │ Returned under map key `gdsjurisdiction` — │ +│ │ misleading name, the gdsjurisdiction table is │ +│ │ the builder-side action catalogue, not what's │ +│ │ read here) │ +│ ├── 4. sysbillnosettings (per-tenant, per-form) │ +│ └── 5. sysreport (per-tenant, per-form) │ └──────────────────────────────────────────────────────────────────────┘ │ │ Returns one composite map: @@ -75,11 +93,11 @@ variations on a theme. | Key | Source | Used by the SPA for | |---|---|---| -| `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave` (+ overlays) | The form layout itself — every field, control, label, validation rule | -| `gdsformconst` | `gdsformconst` rows scoped by tenant + language | Form-level constants — labels, defaults, dropdown text | -| `gdsjurisdiction` | `gdsjurisdiction` rows for the user's role | Per-button and per-data permissions | -| `billnosetting` | `sysbillnosettings` row for this module | Document-numbering rules (work-order numbers, quotation numbers) | -| `report` | `sysreport` rows linked to this form | Print templates (Excel via jxls, PDF via iText) | +| `formData` | `gdsconfigformmaster` (filtered by `sParentId = sModelsId`) ⋈ `gdsconfigformpersonalize` (per-tenant overlay); per master row, `gdsconfigformslave` + `gdsconfigformcustomslave` overlays. `gdsmodule` is referenced only by id. | The form layout itself — every field, control, label, validation rule | +| `gdsformconst` | `gdsformconst` rows filtered by `sParentId` only — NOT tenant-scoped; `sLanguage` selects which label column to return | Form-level constants — labels, defaults, dropdown text | +| `gdsjurisdiction` | `sysjurisdiction` rows for the user (or for the user's group via `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify`); skipped for ADMIN. The map-key name `gdsjurisdiction` is misleading — that table is the builder-side action *catalogue*, not what's read here. | Per-button and per-data permissions | +| `billnosetting` | `sysbillnosettings` row for this module (per-tenant) | Document-numbering rules (work-order numbers, quotation numbers) | +| `report` | `sysreport` rows linked to this form (per-tenant) | Print templates (Excel via jxls, PDF via iText) | ## What's *not* in this lifecycle diff --git a/en/docs/index.md b/en/docs/index.md index 13feda4..de4b21f 100644 --- a/en/docs/index.md +++ b/en/docs/index.md @@ -35,15 +35,18 @@ which Reference chapter you go deep on. - The B2B printing-platform layer (`plat_*` tables, all `xlyPlat*` modules **except** `xlyPlatConstant` — see below). - AI / LLM features (`ai_*` tables, `AiController`) — too new, still moving. -- Face recognition (`xlyFace`) — niche. +- Face recognition (`xlyFace`) — niche; still an active include in `settings.gradle` (built and deployed) but intentionally undocumented here. - File-management module (`xlyFile`) and serial-port module (`xlyRxtx`) — niche / hardware-adjacent. +- 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`, etc.). -- The MongoDB document store (`spring.data.mongodb.uri` in the yaml profiles, document classes under `xlyEntity/.../mongo/`). Every `@Document` class is `PLAT_*`-named and every `MongoTemplate` caller lives in an `xlyPlat*` module — so MongoDB is part of the plat tier above. The framework layer this wiki covers is MySQL-only. +- 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. +- 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. +> **Note on `xlyPlc`.** The PLC / hardware-bridge plugin is in scope as the canonical example of how a non-core module hooks into the framework. See [Slice 06 — Hardware](slices/06-hardware.md). + ## How to fix something in this wiki Edit the markdown file. That is the wiki. Static HTML is generated from these `.md` diff --git a/en/docs/reference/maintainer/activiti.md b/en/docs/reference/maintainer/activiti.md index 8771ff4..3bb254c 100644 --- a/en/docs/reference/maintainer/activiti.md +++ b/en/docs/reference/maintainer/activiti.md @@ -14,7 +14,7 @@ The dependency tree carries **two** Activiti versions: | Module | Version | Notes | |---|---|---| -| `xlyPersist` | `org.activiti:activiti-engine:5.17.0` | Older 5.x line | +| `xlyPersist`, `xlyApi` | `org.activiti:activiti-engine:5.17.0` | Older 5.x line — declared in both modules | | `xlyFlow` | `org.activiti:activiti-spring-boot-starter-rest-api:6.0.0`, `activiti-json-converter:6.0.0` | Newer 6.0 line | This is a real version mismatch. Activiti's 5.x and 6.x schemas overlap @@ -26,9 +26,9 @@ but diverge in some `act_*` tables and migration paths. Possibilities: 3. Both are in the classpath but only one is initialised at runtime. A future maintainer attacking this should: (a) remove the unused -version to avoid confusion, (b) document which version the live -schema uses, (c) verify the `act_*` table layout matches that version -exactly. +version from both `xlyPersist` and `xlyApi` to avoid confusion, +(b) document which version the live schema uses, (c) verify the +`act_*` table layout matches that version exactly. One extra code fact matters in this branch: `xlyFlow/build.gradle` pulls in the Activiti 6 starter, but `xlyFlow/src/main/java/com/xly/XlyFlowApplicationBoot.java` @@ -37,10 +37,14 @@ does not currently present `xlyFlow` as a clearly runnable standalone app. ## The `act_*` schema -The framework ships the expected Activiti `act_*` tables (deployment, -process-definition, runtime task, history etc.) — they are present even -in deployments that don't yet run a flow. They populate only when a BPMN -process is deployed and a process instance is started. +The framework ships the expected Activiti `act_*` schema — 24 base +tables (deployment, process-definition, runtime task, history etc.) +plus 3 *views* (`act_id_user`, `act_id_group`, `act_id_membership`). +The base tables populate only when a BPMN process is deployed and a +process instance is started. The identity views are notable: xly does +not maintain real Activiti identity tables; it projects its own +user/group schema into the `act_id_*` shapes via views, so Activiti +sees xly's logins as if they were native Activiti users. ## xly's wrapper layer @@ -69,12 +73,21 @@ eventually completes. fleshed out): - `xlyFlow`'s `pom`-equivalent gradle build pulls in Activiti 6.0. -- The Spring Boot config for Activiti's process engine. +- `xlyFlow/src/main/java/com/xly/activiti/config/ActivitiConfig.java` — + `@Configuration` implementing `ProcessEngineConfigurationConfigurer`, + the Spring Boot wire-up for Activiti's process engine. - `CheckFlowController` in `xlyEntry/com/xly/web/businessweb/` is one surface the SPA hits to drive workflow (approve / reject / view). -- BPMN process definitions, when present, live under `xlyFlow/src/main/resources/` - (a `processes/` subdirectory or similar). Whether anything ships in - the codebase depends on the build profile. + Note: the URL prefix is `/checkflow` (lowercase), not the camelCase + class name. +- `xlyFlow/src/main/java/com/xly/XlyFlowApplicationBoot.java` is fully + commented out on this branch — the workflow code is consumed as a + library through xlyEntry rather than as a standalone runnable. +- **No BPMN definitions ship in this repo** under + `xlyFlow/src/main/resources/` (no `processes/` subdir, no `*.bpmn*` + files). Deployments must supply them at runtime, e.g. via the + Activiti modeler whose static assets live at + `xlyFlow/src/main/resources/static/modeler/`. ## What's needed to make Activiti work diff --git a/en/docs/reference/maintainer/cache-invalidation.md b/en/docs/reference/maintainer/cache-invalidation.md index 5a5e1e6..46931db 100644 --- a/en/docs/reference/maintainer/cache-invalidation.md +++ b/en/docs/reference/maintainer/cache-invalidation.md @@ -1,93 +1,137 @@ # Cache invalidation on metadata change When a PM saves a change in BACK — adds a column to a form, updates a -permission, registers a new module — every running node has to drop -its cached interpretation of the old metadata. xly does this through -JMS, not by polling. +permission, registers a new module — the framework drops the cached +interpretation of the old metadata. **The cache-clear is synchronous +in the BACK process via Spring's `@CacheEvict`**, NOT a JMS fan-out. +A separate JMS path with similarly-named classes exists for a +different purpose (base-data merge); the two are easy to confuse and +this page calls them out explicitly. -## The path +## The actual cache-invalidation path (synchronous, in-process) ``` PM saves in BACK │ ▼ -BACK controller writes the changed gds_* row +BACK controller (e.g. /business/addUpdateDelBusinessData) calls +BusinessBaseServiceImpl.addBusinessData / updateBusinessData / deleteBusinessData │ ▼ -Controller publishes a JMS "module changed" message +Save service calls businessCleanRedisData.delCleanRedisData(...) + (e.g., BusinessBaseServiceImpl.java:1122, 1224, 1375, 1441, 1597, 1677) │ ▼ -Every node's xlyErpJmsConsumer receives it +BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(, ...) + dispatches to one of the named cleaners on CleanRedisServiceImpl │ ▼ -ConsumerChangeGdsModuleThread.run() clears the relevant Redis keys +CleanRedisServiceImpl.cleanRedisByTableNameGdsModle() (or similar) + fires @CacheEvict against a fixed list of named cache regions │ ▼ -Next /business/getModelBysId call on any node re-reads the table - and re-populates the cache with the new value +Spring CacheManager evicts the named entries + │ + ▼ +Next /business/getModelBysId call re-reads from DB and re-populates + the cache. ``` -The handler is in -`xlyErpJmsConsumer/src/main/java/com/xly/xlyerpjmsconsumer/thread/ConsumerChangeGdsModuleThread.java`. - -## Why JMS, not poll-and-bust - -xly often runs across multiple nodes (xlyEntry, xlyApi, xlyInterface -each on their own JVM, sometimes scaled horizontally). Polling for -"has the metadata changed?" would either be slow (the change isn't -visible until the next poll) or chatty (constant heartbeats). JMS -fans out the invalidation to every node within milliseconds. - -xly uses both **ActiveMQ** and **RocketMQ** in the codebase, but the -metadata-change path documented here is the **ActiveMQ / JMS** one: -`xlyErpJmsConsumer` listens on `P2pQueue.ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` -with `@JmsListener`, and `ConsumerChangeGdsModuleThread` handles the -cache-bust work. `RocketMQServiceImpl` exists for other integration flows. - -## Which keys get cleared +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: -The Redis cache holds: - -- Module metadata by `sId`. -- Form metadata by `sId`. -- Field-list slaves keyed by form `sId`. -- Per-tenant overlay merges (a derived cache). -- Permission rules per (module, role). +``` +@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() { … } +``` -The consumer thread receives the changed row's IDs and clears each -cache key family that could plausibly include it. **Over-invalidating -is the safe option here** — the cost of an extra DB read on the next -request is far smaller than the cost of serving stale metadata. +Other table-named cleaners on the same class evict the regions +relevant to `gdsconfigformmaster`, `gdsconfigformslave`, +`gdsconfigtbmaster`, `gdsformconst`, `gdsjurisdiction`, +`gdsconfigcharmaster`, login-info, billnosetting, kpimaster, +`SysSystemSettings`, etc. + +## What the JMS `CHANGE_GDS_MODULE` queue actually does (NOT cache-bust) + +The framework has a JMS queue +`P2pQueue.ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` and a consumer thread +`ConsumerChangeGdsModuleThread`, both of which sound like they should +be doing cache invalidation — but they don't. +`ConsumerChangeGdsModuleThread.run()` resolves a +`changeGdsModuleService` bean (`ChangeGdsModuleServiceImpl`) and calls +`changeTableData(sGdsModuleId, sJobId)`, which invokes the stored +procedure `PRO_ERPMERGEBASEGDSMODULE` (via `proDao.proErpMergeBaseGdsModule`, +mapped in `ProMapper.xml`). That proc consolidates per-tenant +`gdsmodule` rows into a flattened "base" lookup table — a base-data +merge job, not a cache evict. A `grep` of `xlyErpJmsConsumer/` for +`@CacheEvict` or `cleanRedis*` returns zero hits — the consumer side +clears nothing in Redis. + +The same goes for the other 23 `ERP_JMS_ACTIVEMQ_*` queues in +[`P2pQueue.java`](../../api-reference/messaging.md): each one drives a +domain-specific base-data merge or fan-out work item, not cache +invalidation. + +## The cross-node coherence question (open) + +`@EnableCaching` is on `EntryApplicationBoot.java:22` and +`ApiApplicationBoot.java:24`. **No custom `CacheManager` bean is +declared anywhere in the in-scope source** (no `RedisCacheManager`, +no `@Bean CacheManager`-returning method, no `implements CacheManager`, +no `spring.cache.*` property in any `application*.yml`). Spring Boot +2.2.5 will then auto-pick a `CacheManager` based on what's on the +classpath; the most likely outcome with `spring-boot-starter-data-redis` +present is that Spring auto-configures Redis-backed caching, which +would make `@CacheEvict` clear the shared Redis store and therefore +fan out across nodes implicitly. **This has not been confirmed by live +inspection of a running node** — it's the natural reading of the +config but worth verifying when a deployment is available. If the +auto-picked manager is in fact `ConcurrentMapCacheManager` (in-memory, +per-JVM), the multi-node coherence story is broken under this code +path and would need a separate fix. ## When you change metadata directly via SQL -Inserts/updates done through MyBatis or BACK *trigger* the JMS event. -Inserts/updates done by an engineer running raw `UPDATE gdsmodule SET -...` against the production DB do **not** trigger it. The cache will -serve stale metadata until either: +Inserts/updates done through MyBatis or BACK trigger +`businessCleanRedisData.delCleanRedisData*`. Raw `UPDATE gdsmodule SET …` +against the DB does **not** trigger any cleaner. The cache will serve +stale metadata until either: 1. The cache TTL expires (check the cache config for the actual TTL). -2. A bounce of the application servers. -3. A manual JMS message is sent (see `BusinessCleanRedisDataImpl` in - `xlyBusinessService`). - -The third option is the supported workaround; option (2) is the -brute-force fallback. +2. A bounce of the application servers (one node at a time if the + cache is local; once if shared). +3. A manual call to one of the + `BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(, …)` + methods is invoked from inside the application (e.g., via a + maintenance endpoint). Note this clears whatever the local + `CacheManager` is bound to; if that turns out to be in-memory, + the cleanup must run on every node. ## Common bug: the cache is the bug When something looks like "I changed it but the page still shows the old value", check (in this order): -1. Did the change actually commit? (Confirm with `SELECT` against - the DB.) -2. Is the JMS broker reachable from the BACK node? (If not, the - invalidation event silently isn't published.) -3. Are all consumer nodes running? (If a node is paused, it'll serve - stale metadata until restarted.) -4. Did the change happen via raw SQL? (Then no JMS event was - published — manually trigger it.) - -The "five-table read" of [Slice 1](../../slices/01-hello-world.md) +1. Did the change actually commit? (Confirm with `SELECT` against the DB.) +2. Did the change go through a path that invokes + `BusinessCleanRedisData`? (Direct DB writes or controllers that + bypass `BusinessBaseServiceImpl` won't.) +3. Is the cache shared across nodes (Redis-backed) or local + (`ConcurrentMapCacheManager`)? Confirm by inspecting the active + `CacheManager` bean on a running node. +4. If the cache is local, did every node get the eviction call? + +The five-key composite returned by +[`getModelBysId` in Slice 1](../../slices/01-hello-world.md) re-runs from the cache; understanding which layer is stale is the key to the bug. diff --git a/en/docs/reference/maintainer/deployment.md b/en/docs/reference/maintainer/deployment.md index 8ff7223..d29dc98 100644 --- a/en/docs/reference/maintainer/deployment.md +++ b/en/docs/reference/maintainer/deployment.md @@ -6,14 +6,29 @@ as dependencies by `xlyEntry`. ## The main modules -| Service / module | Role | Code-backed notes | -|---|---|---| -| **xlyEntry** | Main runtime and builder/admin surface. Hosts `/business/*`, `/gdsmodule/*`, `/gdsconfigform/*`, `/gdsconfigtb/*`, reporting, login, and other framework controllers. | Depends on `xlyManage`, `xlyBusinessService`, and `xlyFlow`. | -| **xlyApi** | API-oriented module for `/api/*`, `/online/*`, `/pro/*`, `/thirdparty/*`, and related endpoints. | Separate Spring Boot application class in `ApiApplicationBoot`. | -| **xlyInterface** | External-integration module with Swagger dependencies and third-party integration code. | Separate Spring Boot application class in `InterfaceApplicationBoot`. | -| **xlyPlc** | Shop-floor PLC bridge ([Slice 6](../../slices/06-hardware.md)). | Separate Spring Boot application class in `PlcApplicationBoot`. | -| **xlyFace** | Optional face-recognition module. | Separate module; still present in the Gradle build. | -| **xlyFlow** | Workflow / Activiti code and controllers. | Present as its own module, but in this branch `XlyFlowApplicationBoot` is commented out, so treat it as code that is currently consumed through `xlyEntry` rather than a clearly runnable standalone app. | +### Deployable Spring Boot applications + +| Service / module | Role | Default profile / port | Boot class | +|---|---|---|---| +| **xlyEntry** | Main runtime and builder/admin surface. Hosts `/business/*`, `/gdsmodule/*`, `/gdsconfigform/*`, `/gdsconfigtb/*`, reporting, login, and other framework controllers. | `dev` → 8080, context `/xlyEntry` | `EntryApplicationBoot` | +| **xlyApi** | API-oriented module for `/api/*`, `/online/*`, `/pro/*`, `/thirdparty/*`, and related endpoints. | `local` (default in repo) → 8090, dev/win/linux → 8080, context `/xlyApi` | `ApiApplicationBoot` | +| **xlyInterface** | External-integration module with Swagger dependencies and third-party integration code. | `dev` → 8080, context `/xlyInterface` | `InterfaceApplicationBoot` | +| **xlyPlc** | Shop-floor PLC bridge ([Slice 6](../../slices/06-hardware.md)). | `dev` → 8000, named profiles (15S, S10, T0, T1, CT, yt, pro) → 8080, context `/xlyEntry` *(shares xlyEntry's context-path)* | `PlcApplicationBoot` | +| **xlyFace** | Face-recognition module. In build (`settings.gradle` keeps it active per user) but **out of documentation scope** for this wiki. | `win` (default in repo) → 8080, local → 8091, context `/xlyFace` | `XlyFaceApplicationBoot` | +| **xlyErpJmsConsumer** | JMS consumer worker. Has a Boot main but no `application*.yml` of its own — runtime config inherits from peer services. | n/a (inherits) | `JmsConsumerApplicationBoot` | + +### Library modules (in `settings.gradle`, not standalone runnable) + +| Module | Role | +|---|---| +| **xlyManage** | Backend metadata-management services (`Gds*ServiceImpl` family); pulled into xlyEntry. | +| **xlyBusinessService** | Business-logic service tier (`BusinessBaseServiceImpl` and ~100 sibling `*ServiceImpl` classes); pulled into xlyEntry. | +| **xlyFlow** | Workflow / Activiti code. `XlyFlowApplicationBoot.java` is fully commented out on this branch; consumed as a library through xlyEntry. Also shares context-path `/xlyEntry`. | +| **xlyEntity** | Shared entity / DTO classes (~83 Java files, including 22 Mongo `@Document` classes). | +| **xlyPersist** | Persistence helpers (DAOs, MyBatis mapper XMLs, `RequestAddParamUtil`, etc.). | +| **xlyMsg** | Notification helpers (DingTalk, WeChat, email); no Boot main. | +| **xlyErpJmsProductor** | JMS producer code (queue declarations in `P2pQueue.java`); no Boot main. | +| **xlyPlatConstant** | Shared utility constants (`MultiThreadServer`, `TimeContant`) consumed by `xlyPersist`. The single active Plat* module. | Each has its own `application.yml` + several `application-.yml` files. The active profile is selected at startup via @@ -21,33 +36,41 @@ files. The active profile is selected at startup via ## Disabled in `settings.gradle` -``` -//include 'xlyErpTask' -//include 'xlyRxtx' -//include 'xlyFile' -``` - -Three modules are present on disk but excluded from the active Gradle build: +The cleanup branch comments out 12 `include` lines. Three are non-Plat +modules present on disk: - `xlyErpTask` — long-running background tasks. -- `xlyRxtx` — native serial-port library. Disabled when xlyPlc doesn't - need direct serial access (some press models use TCP/Ethernet +- `xlyRxtx` — native serial-port library. May be re-enabled when xlyPlc + needs direct serial access (some press models use TCP/Ethernet instead). -- `xlyFile` — older file-management module, superseded by Aliyun OSS - integration in `xlyPlatFileUpload`. +- `xlyFile` — older file-management module, superseded by + `xlyPlatFileUpload` (also commented out). + +The remaining nine commented-out includes are `xlyTestService`, +`xlyTestController`, and the full `xlyPlat*` family except +`xlyPlatConstant` — i.e. `xlyPlatTask`, `xlyPlatJmsProductor`, +`xlyPlatJmsConsumer`, `xlyPlatReportForm`, `xlyPlatFileUpload`, +`xlyPlatMarketingService`, `xlyPlatUserService`, `xlyPlatSmsService`, +`xlyPlatMerchantController`, `xlyPlatWebsocket`, `xlyPlatPayService`, +`xlyPlatCainiaoWaybillSevice`. (`xlyTestService` / `xlyTestController` +directories are not on disk; only `TestController.java` exists inside +`xlyEntry/.../businessweb/` as a stub.) A maintainer cleaning up the codebase should consider whether to delete -these or keep them as historical reference. They take up disk space but -do not affect the build. +the on-disk-but-excluded `xlyErpTask` / `xlyRxtx` / `xlyFile` +directories or keep them as historical reference. They take up disk +space but do not affect the build. ## Plat* family The `xlyPlat*` modules (`xlyPlatMerchantController`, `xlyPlatUserService`, `xlyPlatPayService`, `xlyPlatMarketingService`, `xlyPlatCainiaoWaybillSevice`, `xlyPlatSmsService`, `xlyPlatReportForm`, `xlyPlatFileUpload`, -`xlyPlatJmsConsumer`/`Productor`, `xlyPlatTask`, `xlyPlatWebsocket`, -`xlyPlatConstant`) are the **B2B printing-platform layer** and remain -out-of-scope for this wiki. +`xlyPlatJmsConsumer`/`Productor`, `xlyPlatTask`, `xlyPlatWebsocket`) +are the **B2B printing-platform layer** and remain out-of-scope for +this wiki. The single exception is `xlyPlatConstant`, which is still +`include`d in `settings.gradle` and consumed as a shared constants +utility by `xlyPersist` (`MultiThreadServer`, `TimeContant`). ## How services find each other @@ -75,14 +98,25 @@ are deployment details rather than code-backed facts in this repository. ## Profile permutations -`application-saas.yml`, `application-linux.yml`, `application-win.yml`, -`application-15S.yml`, `application-S10.yml`, `application-pro.yml`, -`application-T0.yml`, `application-T1.yml`, … cover combinations of: - -- Operating system (linux / win) -- Customer category (saas, 15S, S10, …) -- Environment (dev, pro) -- Press-model (for xlyPlc) +Profiles split by service: + +- **xlyEntry**: `dev`, `local`, `win`, `linux`, `15s`, `s10`, `saas`, + `bgj` (lowercase). `dev` is the in-repo default. +- **xlyApi**: `local` (default in repo), `dev`, `linux`, `win`. +- **xlyInterface**: `dev` only. +- **xlyFlow**: `dev` (empty file). +- **xlyFace**: `win` (default), `dev`, `linux`, `local`. +- **xlyPlc**: `dev` (default) plus 7 press-model profiles + (`15S`, `S10`, `T0`, `T1`, `CT`, `yt`, `pro` — uppercase / mixed-case, + distinct from xlyEntry's lowercase `15s` / `s10`). + +The press-model profiles (`T0`, `T1`, `CT`, `yt`, `pro`, `15S`, `S10`) +are **xlyPlc-specific** — they don't exist for the other services. The +cross-service profiles cover combinations of: + +- Operating system (`linux` / `win`) +- Environment (`dev`, `local`, `saas`, `bgj`) +- Customer/edition (`15s`, `s10` for xlyEntry) A given deployment selects exactly one profile per service. The mapping from "this customer" → "these profiles" lives in deployment diff --git a/en/docs/reference/maintainer/proc-dispatch.md b/en/docs/reference/maintainer/proc-dispatch.md index c8cb3b5..3aeb7af 100644 --- a/en/docs/reference/maintainer/proc-dispatch.md +++ b/en/docs/reference/maintainer/proc-dispatch.md @@ -1,18 +1,24 @@ # Generic procedure dispatch -When `gdsmodule.sSaveProName` (or `sDeleteProName`, `sCalcProName`, -`sProcName`, `sSaveProNameBefore`) is **non-empty**, the framework -invokes the named stored procedure instead of falling through to its -default Add/Update path. The same machinery handles button-press -calculations and on-demand custom logic. +When the metadata names a stored procedure — via columns like +`gdsmodule.sSaveProName`, `sSaveProNameBefore`, `sDeleteProName`, +`sCalcProName`, `sProcName`, or `gdsconfigformslave.sButtonParam` — +the framework dispatches that proc by name. `sSaveProName` and +`sSaveProNameBefore` are **hooks**: they run as post-save / pre-save +phases on top of the always-running base add/update path +(`BusinessBaseServiceImpl.addBusinessData` / +`updateBusinessData`), invoked by `checkUpdate(...,"sSaveProName")` +at `BusinessBaseServiceImpl.java:1824` and `:1778`. The other +columns drive on-demand calls: `sCalcProName` for button-press +calculations, `sProcName` for custom-fetch flows, etc. The same +generic-dispatch machinery handles all of them. The handler is `GenericProcedureCallController` in `xlyEntry/com/xly/web/businessweb/`. ## Endpoint shape -The frontend POSTs to a URL under `/business/genericProcedureCall*` -with: +The frontend POSTs to `/procedureCall/doGenericProcedureCall` with: - The proc name (often resolved from `gdsmodule` or `gdsconfigformslave.sButtonParam`). - The parameter values (frontend supplies them, the framework injects @@ -79,6 +85,40 @@ These are templates an engineer fills in to author a new proc — they are not used by the runtime to generate procs on the fly. See [SQL templates](sql-templates.md) for the loader and the placeholder syntax. +## Proc-name molds in the live schema + +The 1687 procedures in the live DB cluster around a few naming molds +beyond the bare `Sp_*` family: + +| Mold | Approx 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. | +| `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, +so other molds never resolve via `sSaveProName` / `sCalcProName` etc. +The non-`Sp_*` procs are reachable only via direct invocation in +mapper XML or other procs. + +## The function layer + +The schema also ships **177 user-defined functions** following parallel +naming molds: `Fun_*` (~150), `Fn_*` (~8), `get_*` (~10). + +These are **not Java-dispatched**. They are invoked from inside other +procedures, view definitions, and mapper-XML SELECT statements. There +is no `gdsmodule.sFunctionName` column or analogous metadata — +functions are picked up by the SQL that mentions them. A maintainer +investigating a slow report should grep procs and views for `Fun_*` / +`Fn_*` / `get_*` references; the framework's Java side does not see +them at all. + ## Failure modes to watch 1. **Mismatched parameter order.** Generic dispatch binds positionally; diff --git a/en/docs/reference/maintainer/runtime.md b/en/docs/reference/maintainer/runtime.md index d8a93e1..dedf9a7 100644 --- a/en/docs/reference/maintainer/runtime.md +++ b/en/docs/reference/maintainer/runtime.md @@ -16,14 +16,14 @@ 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/` | Activiti workflow surface (approve / reject / view) — only meaningful when workflow is deployed. | endpoints under `/checkFlow/*` | +| `CheckFlowController` | `web/businessweb/` | Activiti workflow surface (approve / reject / view) — only meaningful when workflow is deployed. | endpoints under `/checkflow/*` (the class file is `CheckFlowController.java` in camelCase but the `@RequestMapping` value is all-lowercase) | Note that the controllers split across **two packages**: `businessweb/` hosts the runtime-facing endpoints, while `systemweb/` hosts the metadata-CRUD endpoints used by the builder side. Both compile into the same `xlyEntry` WAR. -## The four-table read +## The five-key read For any metadata-driven module, the request lifecycle (see [Concepts → request lifecycle](../../concepts/request-lifecycle.md)) @@ -31,11 +31,23 @@ boils down to: ```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 (skipped for ADMIN) - Map billnosettingMap = businessGdsconfigformsService.getBillnosettingData(param); // 4. sysbillnosettings - List> reportList = printReportService.getReportData(qMap); // 5. sysreport + // 1. formData — gdsconfigformmaster filtered by sParentId=sModelsId, + // LEFT JOIN gdsconfigformpersonalize (per-tenant), then for each + // master row load its gdsconfigformslave + gdsconfigformcustomslave + // overlays. gdsmodule itself is referenced only by id, not SELECT-ed. + List> formList = this.getModelConfigByModleId(map); + // 2. gdsformconst — by sParentId only; sLanguage selects which label + // column to return; NOT tenant-scoped. + List> fList = businessGdsconfigformsService.getFormconstData(qMap); + // 3. sysjurisdiction (per-user grants joined to sftlogininfojurisdictiongroup + // + sisjurisdictionclassify); skipped for ADMIN. + // Returned under map key `gdsjurisdiction` despite the source table + // being sysjurisdiction. + List> jList = businessGdsconfigformsService.getJurisdictionData(qMap); + // 4. sysbillnosettings (per-tenant, per-form). + Map billnosettingMap = businessGdsconfigformsService.getBillnosettingData(param); + // 5. sysreport (per-tenant, per-form). + List> reportList = printReportService.getReportData(qMap); return composite(formList, fList, jList, billnosettingMap, reportList); } ``` @@ -47,11 +59,11 @@ rest of the runtime is variations on it. | Key | Source | Frontend uses for | |---|---|---| -| `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave` (+ overlays) | Form layout | -| `gdsformconst` | `gdsformconst` | Form-level constants, dropdown labels | -| `gdsjurisdiction` | `gdsjurisdiction` | Per-button / per-data permissions | -| `billnosetting` | `sysbillnosettings` | Document numbering rules | -| `report` | `sysreport` | Print templates | +| `formData` | `gdsconfigformmaster` (filtered by `sParentId = sModelsId`) ⋈ `gdsconfigformpersonalize` overlay; per master row, `gdsconfigformslave` + `gdsconfigformcustomslave`. `gdsmodule` is referenced only as the id source, not joined. | Form layout | +| `gdsformconst` | `gdsformconst` (filtered by `sParentId` only; `sLanguage` selects which label column to return; NOT tenant-scoped) | Form-level constants, dropdown labels | +| `gdsjurisdiction` | `sysjurisdiction` (joined to `sftlogininfojurisdictiongroup` + `sisjurisdictionclassify` for per-user/group grants); skipped for ADMIN. **Note:** the map-key name `gdsjurisdiction` is misleading — `gdsjurisdiction` is the builder-side action *catalogue* table; the per-user *grant* read here actually queries `sysjurisdiction`. | Per-button / per-data permissions | +| `billnosetting` | `sysbillnosettings` (per-tenant, per-form) | Document numbering rules | +| `report` | `sysreport` (per-tenant, per-form) | Print templates | ## The save endpoint @@ -67,9 +79,22 @@ map per row, in this shape: } ``` -When `gdsmodule.sSaveProName` is empty, the framework's default -Add/Update path runs — `AddDelUpdCommonServiceImpl.java`. When it's -populated, the named stored proc is invoked instead. +The base add/update path always runs through +`BusinessBaseServiceImpl.addBusinessData` / `updateBusinessData` +(`xlyBusinessService/.../BusinessBaseServiceImpl.java:1014` and +`:1250`), which delegate to `businessBaseDao.add(map)` / +`businessBaseDao.update(map)` against the table named by `sTable`. +`gdsmodule.sSaveProName` (and its sibling `sSaveProNameBefore`) is +**not** an either/or branch that swaps the path; it names an extra +stored proc that runs as a post-save (or pre-save) hook on top of +the base path, dispatched by `checkUpdate(...,"sSaveProName")` at +`BusinessBaseServiceImpl.java:1824` and `CheckSaveServiceImpl.java`. +`AddDelUpdCommonServiceImpl` (`@Service("addDelUpdCommonService")`) +is a separate utility of reusable `insertByMap`/`updateByMap`/ +`delByMap`/`addBatch` helpers used by domain services +(work-order-plan, oee, many-quo, order-procurement, etc.); it is +**not** the runtime's default add/update path for +`addUpdateDelBusinessData`. ## Multi-tenant boundary @@ -95,12 +120,21 @@ A new controller method that doesn't call `RequestAddParamUtil` is a Two flagged in slices that belong here permanently: -1. **`sTable` validation in `addUpdateDelBusinessData`.** The frontend - names the target table directly. The runtime *must* cross-check - that the supplied table is one of the form's authorised backing - tables, or it's a privilege-escalation surface. Confirm the check - exists; if it doesn't, raise it as a security ticket. Tracked in - the [Slice 1 v2 follow-up](../../slices/01-hello-world.md#open-verification-items). +1. **`sTable` validation in `addUpdateDelBusinessData` — CONFIRMED MISSING.** + The frontend names the target table directly and the runtime does + **not** cross-check that the supplied table is one of the form's + authorised backing tables. `BusinessBaseServiceImpl.sTableNameList` + (lines 162-169) is a multi-tenant scope-bypass list (the four + framework-metadata tables that are global, so `sBrandsId` / + `sSubsidiaryId` are stripped from writes against them — see the + `//不需要公司子公司的表` comment at line 165), not a backing-table + whitelist. The hardcoded special case at line 1768 + (`mftproductionplanslave`) is the only module/table cross-check + in the whole flow. Mitigations exist (tenant scoping via + `RequestAddParamUtil`, optional pre/post-save proc validation), + but none of them is a backing-table whitelist. See + [Slice 1 follow-up](../../slices/01-hello-world.md#open-verification-items) + for the full trace. 2. **ADMIN bypasses permissions.** `BusinessBaseServiceImpl` skips the `gdsjurisdiction` load entirely for `UserType.ADMIN`. ADMIN account governance must @@ -108,7 +142,11 @@ Two flagged in slices that belong here permanently: ## Cache invalidation -When BACK saves a metadata change, a JMS message fires and -`ConsumerChangeGdsModuleThread` (in `xlyErpJmsConsumer`) clears the -cached metadata on every running node. See [cache invalidation on -metadata change](cache-invalidation.md). +When BACK saves a metadata change, the save service synchronously +calls `BusinessCleanRedisData.delCleanRedisData*`, which fires +`@CacheEvict` on the relevant cache regions in `CleanRedisServiceImpl`. +A separate JMS path (`ConsumerChangeGdsModuleThread`) exists with a +similar name but does base-data merging via stored proc, not cache +invalidation. See [cache invalidation on metadata change](cache-invalidation.md) +for the full story (including the open question about cross-node +coherence). diff --git a/en/docs/reference/maintainer/tech-stack.md b/en/docs/reference/maintainer/tech-stack.md index a5d084b..dd43e81 100644 --- a/en/docs/reference/maintainer/tech-stack.md +++ b/en/docs/reference/maintainer/tech-stack.md @@ -1,11 +1,15 @@ # Tech stack -A library inventory for the **in-scope** framework modules -(`xlyEntry`, `xlyApi`, `xlyManage`, `xlyBusinessService`, `xlyPersist`, -`xlyEntity`, `xlyFlow`, `xlyPlc`, `xlyInterface`, `xlyMsg`, -`xlyErpJms*`). - -The plat tier (`xlyPlat*` modules), `xlyFace`, and AI libraries are +A library inventory for the **in-scope** framework — 11 framework-core +modules (`xlyEntry`, `xlyApi`, `xlyManage`, `xlyBusinessService`, +`xlyPersist`, `xlyEntity`, `xlyFlow`, `xlyInterface`, `xlyMsg`, +`xlyErpJmsProductor`, `xlyErpJmsConsumer`), one plugin (`xlyPlc`), and +one shared utility (`xlyPlatConstant` — load-bearing for `xlyPersist`'s +use of `MultiThreadServer` and `TimeContant`, despite the `Plat*` +naming). + +The other plat-tier modules (`xlyPlat*` except `xlyPlatConstant`), +`xlyFace` (in build, out of documentation scope), and AI libraries are [out of scope](../../index.md#whats-out-of-scope) and are not listed here. @@ -49,7 +53,7 @@ page records facts only. | 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/`. | | Oracle JDBC | `ojdbc6-11.2.0.4.jar` (local jar in `xlyFlow/`) | `xlyFlow/build.gradle` | 2 files in `xlyFlow/src/`. | -| Druid | `druid-spring-boot-starter` 1.2.16; `druid` 1.2.16 | `xlyPersist/build.gradle`, `xlyApi/build.gradle` | 25 files import `com.alibaba.druid.*` (xlyEntry=8, xlyPlc=8, xlyFlow=5, xlyBusinessService=2, xlyInterface=2). yaml: `xlyEntry/.../application-local.yml:126` sets `spring.datasource.type: com.alibaba.druid.pool.DruidDataSource`; lines 308-313 configure the `/druid/*` stat-view servlet. | +| Druid | `druid-spring-boot-starter` 1.2.16; `druid` 1.2.16 | `xlyPersist/build.gradle`, `xlyApi/build.gradle` | 6 Java files import `com.alibaba.druid.*` (xlyBusinessService=2, xlyFlow=3, xlyInterface=1). 16 `application-*.yml` files reference Druid configuration (xlyEntry=8 yml profiles, xlyPlc=8 yml profiles). yaml: `xlyEntry/.../application-local.yml:126` sets `spring.datasource.type: com.alibaba.druid.pool.DruidDataSource`; lines 308-313 configure the `/druid/*` stat-view servlet. | | HikariCP | 4.0.3 | `xlyApi/build.gradle` | 8 files reference `com.zaxxer.hikari` (xlyApi=6, xlyInterface=2). Java config: `xlyApi/.../api/config/MasterDataSourceConfig.java`, `SlaveDataSourceConfig.java`. yaml: `xlyApi/.../application-{local,dev,linux,win}.yml`. | | Flyway | 5.2.1 | `xlyPersist/build.gradle` | No Java imports. Configured via yaml `spring.flyway.*` (e.g., `xlyEntry/.../application-local.yml:316-327`) with `enabled: false`. Migration scripts at `xlyEntry/src/main/resources/flyway/V*__*.sql`. | | PageHelper | 4.1.1 | `xlyPersist/build.gradle`, `xlyApi/build.gradle`, `xlyFlow/build.gradle` | 19 files import `com.github.pagehelper.*`. yaml `pagehelper.helperDialect: mysql` at `xlyEntry/.../application-local.yml:427`. | @@ -68,7 +72,7 @@ page records facts only. | Library | Version | Where | In-scope source references | |---|---|---|---| -| Activiti Engine | 5.17.0 | `xlyPersist/build.gradle`, `xlyApi/build.gradle`; consumed by `xlyFlow` | 35 files import `org.activiti.*` (xlyFlow=32, plus 1 each in xlyPersist, xlyApi, xlyEntry — the xlyEntry hit is the `SecurityAutoConfiguration` exclusion in `EntryApplicationBoot.java`; the xlyApi hit is `IdGen.java`; the xlyPersist hit is `BaseDao.java`). The version skew with the 6.0 modeler libs is documented in [Activiti integration](activiti.md). | +| Activiti Engine | 5.17.0 | `xlyPersist/build.gradle`, `xlyApi/build.gradle`; consumed by `xlyFlow` | 35 files import `org.activiti.*` (xlyFlow=32, plus 1 each in xlyPersist, xlyApi, xlyEntry — the xlyEntry hit is the `SecurityAutoConfiguration` exclusion in `EntryApplicationBoot.java`; the xlyApi and xlyPersist hits are both `IdGen.java`, near-identical crypto-utility copies that import `org.activiti.engine.identity.User`). The version skew with the 6.0 modeler libs is documented in [Activiti integration](activiti.md). | | Activiti Spring Boot REST API | 6.0.0 | `xlyFlow/build.gradle` | Consumed via Spring Boot autoconfig + REST endpoints under `xlyFlow`. | | Activiti JSON Converter | 6.0.0 | `xlyFlow/build.gradle` | (Used by xlyFlow's modeler save path.) | | Quartz | 2.3.0 | `xlyFlow/build.gradle` | 16 files import `org.quartz.*` (xlyEntry=8, xlyFlow=8). yaml: `xlyEntry/.../application-local.yml:329-365` configures `spring.quartz.*` with JDBC JobStore (`qrtz_*` tables), `instanceName: xlyflowScheduler`. | @@ -139,10 +143,10 @@ 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` | 84 files import `com.alibaba.fastjson.*` across all in-scope modules (xlyBusinessService=39, xlyEntry=11, xlyInterface=10, 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` | 83 files import `com.alibaba.fastjson.*` across in-scope modules (xlyBusinessService=39, xlyEntry=11, xlyInterface=9, 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` | 41 files import `org.apache.commons.lang3.*` (xlyFlow=24, xlyPersist=8, xlyEntry=3, xlyApi=2, xlyBusinessService=3, xlyMsg=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). | | commons-collections4 | 4.1 | `xlyPersist/build.gradle` | 1 file in `xlyBusinessService/src/`. | | Groovy | `groovy-all` 3.0.2 | `xlyPersist/build.gradle`, `xlyApi/build.gradle` | 5 Java files import `groovy.util.logging.Slf4j` (xlyPersist=3, xlyApi=2). The annotation is from Groovy's runtime; the Java files using it appear to be vestigial — the import is present but the annotation does not affect Java compilation. | | Struts2 JSON plugin | 2.5.30 | `xlyPersist/build.gradle` | 1 file: `xlyPersist/src/main/java/com/xly/utils/FeedPage.java`. The framework otherwise runs on Spring MVC. | @@ -205,6 +209,7 @@ since been removed, or may be vestigial. | commons-pool2 2.8.0 | `xlyPersist/build.gradle` | No direct imports. Likely transitive support for Jedis or similar. | | Baidu SDK (`baidu-sdk-1.4.5.jar`, local) | `xlyInterface/build.gradle` | No `com.baidu` imports found. | | `mchange-commons-java` 0.2.11 | `xlyFlow/build.gradle` | No direct imports. | +| Springfox (`springfox-swagger-ui` 2.9.2 + `springfox-swagger2` 2.9.2) | `xlyInterface/build.gradle` | No direct Java imports. Consumed via Spring Boot auto-config to serve `/swagger-ui.html` for the xlyInterface tier. Cited from [API Reference / webhooks](../../api-reference/webhooks.md). | ## Notable version skew & local jars @@ -228,5 +233,5 @@ Pulled directly from the `build.gradle` files. Each is a fact, not a recommendat - The plat tier (`xlyPlat*` modules) and dependencies declared only there — out of scope per [index](../../index.md#whats-out-of-scope). - AI / LLM libraries (`com.theokanning.openai-gpt3-java:service` 0.11.1 and `com.unfbx:chatgpt-java` 1.0.8 in `xlyApi/build.gradle`) — out of scope. -- The MongoDB starter declared in `xlyEntity/build.gradle` (`spring-boot-starter-data-mongodb` 2.2.5). The `xlyEntity` module contains 22 `@Document`-annotated classes under `xlyentity/mongo/`, all named `PLAT_*`. A grep for `MongoTemplate` and `MongoRepository` in in-scope modules returned only `xlyPersist/.../dao/platmongo/BaseMongoDao.java` (which serves the plat tier); no in-scope module invokes Mongo APIs. See the [out-of-scope note in index](../../index.md#whats-out-of-scope). +- The MongoDB starter declared in `xlyEntity/build.gradle` (`spring-boot-starter-data-mongodb` 2.2.5). The `xlyEntity` module contains 22 `@Document`-annotated classes under `xlyentity/mongo/` — 20 named `PLAT_*` plus 2 `DIKE_TEST*` test fixtures. A grep for `MongoTemplate` and `MongoRepository` in in-scope modules returned only `xlyPersist/.../dao/platmongo/BaseMongoDao.java` (which serves the plat tier); no in-scope module invokes Mongo APIs. See the [out-of-scope note in index](../../index.md#whats-out-of-scope). - `xlyFace` — out of scope. diff --git a/en/docs/slices/01-hello-world.md b/en/docs/slices/01-hello-world.md index d4f44f0..fd4a6cc 100644 --- a/en/docs/slices/01-hello-world.md +++ b/en/docs/slices/01-hello-world.md @@ -105,16 +105,21 @@ returns a single composite map with five keys: | Key | Source | What it contains | |---|---|---| -| `formData` | `gdsmodule` ⋈ `gdsconfigformmaster` ⋈ `gdsconfigformslave` (+ personalize) | The form layout — the spine. | -| `gdsformconst` | `gdsformconst` rows scoped by `sBrandsId`/`sSubsidiaryId`/language | Form-level constants — labels, defaults, dropdown text. | -| `gdsjurisdiction` | `gdsjurisdiction` rows for the user's role | Per-button and per-data permissions. **Skipped for `ADMIN` users** (line 196-198) — admins see everything. | -| `billnosetting` | `sysbillnosettings` row for this module | Document-numbering rule (irrelevant for `gdsformconst` but always loaded). | -| `report` | print templates linked to this form | Printable-report definitions, if any. | - -**Multi-tenancy** is enforced inside this read: every sub-query is scoped by -`sBrandsId` (manufacturer) and `sSubsidiaryId` (subsidiary), pulled from the -authenticated user's session via `RequestAddParamUtil.me().addParams()`. -Tenants do not see each other's metadata. +| `formData` | `gdsconfigformmaster` (filtered by `sParentId = sModelsId`) ⋈ `gdsconfigformpersonalize` (per-tenant overlay); per master row, `gdsconfigformslave` + `gdsconfigformcustomslave` overlays. `gdsmodule` is referenced only by id (sub-selected from the slave queries to resolve `sActiveName`), not joined into the master read. | The form layout — the spine. | +| `gdsformconst` | `gdsformconst` rows filtered by `sParentId` only. **Not tenant-scoped** — the row identifies the form, and `sLanguage` selects which label column to return. | Form-level constants — labels, defaults, dropdown text. | +| `gdsjurisdiction` | `sysjurisdiction` rows for the user's role (joined to `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify`). **Skipped for `ADMIN` users** (line 196-198) — admins see everything. **Note:** the map-key name `gdsjurisdiction` is misleading — `gdsjurisdiction` is the builder-side action *catalogue* table; the per-user grant read here actually queries `sysjurisdiction`. | Per-button and per-data permissions. | +| `billnosetting` | `sysbillnosettings` row for this module (per-tenant) | Document-numbering rule (irrelevant for `gdsformconst` but always loaded). | +| `report` | `sysreport` rows linked to this form (per-tenant) | Printable-report definitions, if any. | + +**Multi-tenancy** is enforced where it matters: tenant-scoped reads +(`gdsconfigformpersonalize`, `gdsconfigformcustomslave`, +`sysbillnosettings`, `sysreport`) all filter by `sBrandsId` (manufacturer) +and `sSubsidiaryId` (subsidiary), pulled from the authenticated user's +session via `RequestAddParamUtil.me().addParams()`. The framework-metadata +tables themselves (`gdsconfigformmaster`, `gdsconfigformslave`, +`gdsformconst`) are global — they are filtered by form-id only. So a +tenant cannot see another tenant's *personalized overlays* or +*business data*, but the underlying form definition is shared. ### 3. SPA → server (initial data load) @@ -166,17 +171,23 @@ POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId} per-module write-API; the metadata-driven UI generates the payload from `gdsconfigformmaster.sTbName` and the `gdsconfigformslave` field list. -- **What the framework does with empty `sSaveProName`:** the runtime takes - the default Add/Update path implemented in - `xlyBusinessService/.../AddDelUpdCommonServiceImpl.java`. That path - generates parameterised `INSERT`/`UPDATE`/`DELETE` against the table - named in the payload — no stored procedure invoked. - -- **What happens when `sSaveProName` is non-empty:** the runtime invokes - the named stored procedure (the SQL templates under - `xlyEntry/src/main/resources/templates/templesql/sSaveProName.sql` are - the *scaffold* engineers fill in when authoring such procs). That path - is exercised by Slice 2 (workflow), not this one. +- **What the framework does with `sSaveProName`:** the base add/update + path always runs through + `BusinessBaseServiceImpl.addBusinessData` / `updateBusinessData` + (`xlyBusinessService/.../BusinessBaseServiceImpl.java:1014` and + `:1250`), which delegate to `businessBaseDao.add(map)` / + `businessBaseDao.update(map)` against the table named by `sTable`. + `sSaveProName` (and its sibling `sSaveProNameBefore`) is **not** an + either/or branch that swaps the path — it names an extra stored proc + that the framework runs as a post-save (or pre-save) hook on top of + the base path, dispatched by `checkUpdate(...,"sSaveProName")` at + `BusinessBaseServiceImpl.java:1824`. For Slice 1's `gdsformconst`, + `sSaveProName` is empty, so only the base path runs. (The SQL + templates under + `xlyEntry/src/main/resources/templates/templesql/sSaveProName.sql` + are the *scaffold* engineers fill in when authoring such hooks.) The + workflow-gated path with a non-empty `sSaveProName` is exercised by + Slice 2. > **Open verification (would require an actual save):** capturing the live > request body, the response body, and the resulting SQL in `syslog4j` @@ -193,8 +204,11 @@ POST /xlyEntry/business/addUpdateDelBusinessData?sModelsId={moduleId} > `sTableNameList` (`BusinessBaseServiceImpl.java:162-169` — only > `gdsformconst`, `gdsmodule`, `gdsconfigformmaster`, `gdsconfigformslave`) > is consulted by some branches (e.g., line 1078, 1338, 1423, 1464) but -> only as a *cache-invalidation* gate, not as a "is this table authorised -> for this form?" gate. The hardcoded special case at line 1768 +> only as a *multi-tenant scope-bypass* gate (those four tables are global +> framework metadata, so `sBrandsId`/`sSubsidiaryId` are stripped from +> writes against them — see the `//不需要公司子公司的表` comment at +> `BusinessBaseServiceImpl.java:165`). It is **not** a "is this table +> authorised for this form?" gate. The hardcoded special case at line 1768 > (`mftproductionplanslave`) is the only module/table cross-check in the > whole flow. Mitigations that exist: > @@ -228,7 +242,7 @@ End of trace. - [The data-driven thesis](../concepts/thesis.md) — why xly stores layouts as data. - [Modules, forms, virtual tables](../concepts/modules-forms-vtables.md) — the three core nouns. -- [The metadata-driven request lifecycle](../concepts/request-lifecycle.md) — the four-table read + the three-key result map. +- [The metadata-driven request lifecycle](../concepts/request-lifecycle.md) — the metadata read + the five-key result map. - [Master / slave document pattern](../concepts/master-slave.md) — `gdsconfigformmaster`/`slave` is itself an instance of the pattern. - [No-FK, semantic-FK reality](../concepts/semantic-fk.md) — `gdsconfigformmaster.sParentId = gdsmodule.sId` is a semantic FK, not enforced. diff --git a/en/docs/slices/03-report.md b/en/docs/slices/03-report.md index c408ce2..856a51a 100644 --- a/en/docs/slices/03-report.md +++ b/en/docs/slices/03-report.md @@ -131,10 +131,13 @@ PDF-via-iText. The mechanism is separate from the grid: - `getModelBysId` returns the `report` array, populated from `sysreport` rows linked to the form via `sFormId`. -- The frontend's "打印" / "导出" buttons hit `xlyEntry/com/xly/report/` - controllers (`PrintReportController`, `PrintReportControllerOld`), - which load a jxls / iText template, run the same view-backed query - with a "fetch all rows" wrapper, and stream a binary file back. +- The frontend's "打印" / "导出" buttons hit + `xlyEntry/src/main/java/com/xly/web/report/` — `PrintReportController` + is the live class. (`PrintReportControllerOld.java` exists in the + same directory but its class body is fully commented out, dead source.) + The controller loads a jxls / iText template, runs the same + view-backed query with a "fetch all rows" wrapper, and streams a + binary file back. - This module (`工单工序明细`) has no template attached, so we don't exercise the print path here. **A future revision of this slice should pick a module that *does* — `print template` is a chapter of its own.** -- libgit2 0.22.2