diff --git a/en/docs/api-reference/internal.md b/en/docs/api-reference/internal.md index 36ae0e2..2e5936a 100644 --- a/en/docs/api-reference/internal.md +++ b/en/docs/api-reference/internal.md @@ -26,7 +26,7 @@ page is the catalog of HTTP entry points. | `/business/getSelectDataBysControlId/{sId}` | POST | Dropdown population for a single control, by control `sId`. | | `/business/getSelectLimit/{sId}` | POST | Paginated variant of the dropdown call. | | `/business/addSysLocking` | POST | Optimistic-lock acquisition when a user starts editing a document — inserts a row in the system lock table keyed by `(sFormGuid, sUserId)`. The SPA fires this when entering edit mode so concurrent editors get a conflict warning. Handler: `BusinessBaseController.java:400-407`. | -| `/business/doExamine` | POST | Simple "approve" — flips `bCheck = 1` on the named row via SQL. **Does NOT invoke Activiti**; this is xly's lightweight approval path used by every module that doesn't need multi-step workflow. Handler: `BusinessBaseController.java:384-391` → `BusinessBaseServiceImpl.doExamine` → `ExamineServiceImpl`. See [Two approval paths](../reference/maintainer/activiti.md#two-approval-paths) for when Activiti is used instead. | +| `/business/doExamine` | POST | Simple "approve" — flips `bCheck = 1` on the named row via SQL. **Does NOT invoke Activiti**; this is xly's lightweight approval path used by every module that doesn't need multi-step workflow. Handler: `BusinessBaseController.java:384-391` → `BusinessBaseServiceImpl.doExamine` → `ExamineServiceImpl`. See [How xly handles workflow without Activiti](../reference/maintainer/activiti.md#how-xly-handles-workflow-without-activiti) for when Activiti is used instead. | | `/business/getProData` | POST | Generic stored-procedure invocation for a module — alternate path to `/procedureCall/doGenericProcedureCall`. Handler: `BusinessBaseController.java:350-358` → `BusinessBaseServiceImpl.getProData`. Used by FROUNT for module-level proc reads (the home dashboard `/getProData?sModelsId=...&sName=` pattern). | These endpoints are documented in detail by [Slice 1](../slices/01-hello-world.md) diff --git a/en/docs/reference/maintainer/activiti.md b/en/docs/reference/maintainer/activiti.md index 9ac1343..c252c6a 100644 --- a/en/docs/reference/maintainer/activiti.md +++ b/en/docs/reference/maintainer/activiti.md @@ -7,32 +7,175 @@ > `gdsmodule.bCheck=1`. No `gdsmoduleflow` link. **The approval > button users see (`/business/doExamine`) bypasses Activiti > entirely** and just UPDATEs `bCheck=1` via SQL — see -> [Two approval paths](#two-approval-paths) below. +> [How xly handles workflow without Activiti](#how-xly-handles-workflow-without-activiti) below. This page documents what's actually wired (concrete classes, URLs, engine state) and what would have to be true for it to do anything. -## Two approval paths - -xly ships two parallel approval mechanisms. They look similar from -the UI but go through completely different code: - -| Path | Triggered by | Backend | Activiti involvement | +## How xly handles workflow without Activiti + +xly has **three workflow-like mechanisms**, in decreasing order of how +much they're actually used: + +### Path 1 — Single-step approval via stored proc + `bCheck` flag (the dominant pattern) + +This is what 99% of approvals in xly look like. There is **no engine +and no state machine** — the workflow is the procedure call itself. + +The mechanics: + +1. Each business table carries the same three audit columns: + `bCheck` (the approval boolean), `sCheckPerson`, `tCheckDate`. + Empirical reach in this dev DB: `bCheck` on **426 tables**, + `tCheckDate` on 400, `sCheckPerson` on 398. So nearly every + business document has the audit trail built into its own row. +2. Each module declares a *single* approval procedure name in + `gdsmodule.sProcName` (column comment: "存储过程(审核)名称" — + "stored-procedure (approval) name"). For example + `Sp_Quo_QuotationCheck` for the quotation module, + `Sp_SalSalesCheck` for sales orders. +3. When the user clicks the 审核 button: + - `POST /business/doExamine` → `BusinessBaseController.java:384-391` + → `BusinessBaseServiceImpl.doExamine()` → `ExamineServiceImpl.doExcuExamine()` + - The service acquires a Redis lock keyed on the row id + (`ws_update_*_{sGuid}_*`) so two users can't approve concurrently. + - It dispatches to the proc named in `sProcName` via the generic + procedure-call machinery + ([proc-dispatch.md](proc-dispatch.md)). + - **The proc itself owns the business logic**: validate that all + required fields are populated, that child rows balance with + parent totals, that no related document is locked, etc. + If all checks pass, the proc UPDATEs the row to set + `bCheck = 1`, `sCheckPerson = `, `tCheckDate = NOW()`. + - The proc returns `OUT sCode INT` (1 = success, ≤0 = error) + and `OUT sReturn LONGTEXT` (error message). +4. Cancel-approval is the same call with `iFlag = 0` instead of `1`; + the proc handles both directions. +5. `Sp_System_CheckSave` is a generic post-approval hook that + `BusinessBaseServiceImpl.java:1828` calls after every save of an + `bCheck`-tracked row — it writes a `sFormId` audit field and + contains placeholder slots for cross-document validation + (currently mostly commented out). + +There's no notion of "next assignee" or "approval queue" in this +path — once a single user with permission has clicked 审核 and the +proc has succeeded, the row is approved. Period. + +### Path 2 — Document-chaining as implicit multi-step workflow + +Multi-document business processes (quote → customer-confirm → +sales-order → delivery → invoice) are implemented as **separate +modules with their own forms**, not as a single document advancing +through states. The user's workflow: + +1. Module A (e.g., `quoQuotationMaster`) — fill it in, click 审核, + row gets `bCheck = 1`. +2. Module A has a button (configured via `gdsconfigformslave.sButtonParam` + pointing at `Sp_Quo_ToSalesOrder` or similar) that, when clicked, + creates a row in module B (e.g., `salSalesOrderMaster`) + pre-populated from module A. +3. The user navigates to module B, fills any extra fields, clicks + approve there too, etc. + +The "01/04, 02/04, 03/04, 04/04" step numbering on the FROUNT +[KPI Work Center](runtime.md#the-kpi-work-center-front-end-home-dashboard) +reflects this: each "flow" is a parent module containing N ordered +child modules; the steps are *just the children of a parent +gdsmodule entry*. There's no engine tracking "you're at step 2 of 4"; +the user is always working on whichever document is currently +in front of them, and the framework displays `iOrder` of the +parent's children for context. + +So multi-step "workflow" emerges from: + +- A parent gdsmodule grouping the steps thematically (`估价管理流程` = + "pricing management flow" with 4 children). +- Each child module's `sProcName` (single-step approval). +- Each child module's `sButtonParam` proc that, when fired, creates + the next document. +- Each business document's `bCheck` flag tracking its own row's state. + +No state machine, no FSM library — just a **chain of stored procs +wired together by buttons on forms**. This is the mechanism you'll +see in the dev DB. + +### Path 3 — Activiti BPMN workflow (gated, currently disabled in code) + +Path 3 exists in the codebase as a **third channel** that *would* +route through Activiti when activated. It is not running in this +dev DB and currently can't be activated without recompiling. + +The gate is hard-coded in `xlyPersist/.../utils/ConstantUtils.java`: + +```java +public static Boolean bCheckflowCheck = false; +``` + +Inside `ExamineServiceImpl.doExcuExamine()`, the dispatch is: + +```java +if (ConstantUtils.bCheckflowCheck) { + Map reMap = checkExamineFlowService + .doSendCheckFolw(sGuid, sUserName, sBrandsId, sSubsidiaryId, + sFormId, map, searMap, sBtnName, request); + if (MapUtil.isNotEmpty(reMap)) { return reMap; } +} +``` + +So even if a tenant deployed BPMN and linked modules via +`gdsmoduleflow`, the `if` would short-circuit because +`bCheckflowCheck` is a Java constant `false`. To activate Path 3 +you have to: + +1. Change `ConstantUtils.bCheckflowCheck = true` in source and + rebuild the WAR, **or** patch the constant at runtime. +2. Insert a `gdsmoduleflow` row keyed on `(sFormId, sBtnName)` — + this maps the form's approval button to a deployed BPMN. +3. Deploy the BPMN through the modeler so `act_re_procdef` is populated. +4. Make sure modules involved have `bCheck` semantics that align with + the BPMN's start/end events. + +Once activated, `CheckExamineFlowServiceImpl.doSendCheckFolw` reads +the `gdsmoduleflow` row, calls +`checkExamineFlowDataService.doSendCheckFolwData` to pre-stage the +data, then `doSendFlowUrl` which kicks the request to xlyFlow's +controllers. From there `ProcessServiceImpl.submitApply()` calls +`runtimeService.startProcessInstanceByKey(...)` and Activiti is in +charge — `biz_flow` + `biz_todo_item` populate, approvers see tasks +in their inbox, and `CurrencyFlowController.complete(...)` advances +the instance. + +### Comparison + +| Aspect | Path 1 (proc + bCheck) | Path 2 (doc chain) | Path 3 (Activiti) | |---|---|---|---| -| **Simple "approve" toggle** | The "审核" button on a single-row form | `POST /business/doExamine` → `BusinessBaseController.java:384-391` → `BusinessBaseServiceImpl.doExamine()` → `ExamineServiceImpl` (xlyBusinessService). The service runs an UPDATE that flips `bCheck = 1` (and writes audit fields). | **None.** `grep` confirms `ExamineServiceImpl` calls neither `runtimeService`, `taskService`, `processService`, nor any other Activiti API. The `bCheck` flag is xly's own boolean, owned by `BusinessBaseServiceImpl`. Used widely as a list filter (`SalesOrderServiceImpl` and friends do `WHERE bCheck = 1` on every "show approved-only" query). | -| **Multi-step BPMN workflow** | A user submits a row whose module has `bCheck = 1` AND a populated `gdsmoduleflow` row pointing at a deployed `act_re_procdef` | `ProcessServiceImpl.submitApply()` → `runtimeService.startProcessInstanceByKey(...)`. Subsequent approvers act through `CurrencyFlowController.complete(...)` → `taskService.complete()`. xly mirrors state in `biz_flow` + `biz_todo_item`. | **Full Activiti.** Requires a deployed BPMN (in `act_re_procdef`), an active process instance (in `act_ru_*`), and an assigned task (`act_ru_task`). | - -In this dev DB, **path 1 is the only path that runs**. Path 2 has zero -configuration anywhere — no BPMN deployed, no `gdsmoduleflow` row, no -`gdsmodule.bCheck=1` modules. So when a user clicks 审核 today, the -record gets approved via the simple-toggle path and Activiti is not -involved. - -Why the second path even exists: a customer that *does* need a real -multi-step approval (warehouse → finance → GM, with reassignment and -delegation) gets it by deploying a BPMN through the modeler and -linking modules via `gdsmoduleflow`. That activates path 2 for those -modules; path 1 still handles everything else. +| State storage | `bCheck` column on the document row | None (state = which document the user opened) | `act_ru_task`, `act_hi_*`, `biz_flow`, `biz_todo_item` | +| Step transitions | Single step per document | One button click per chain link, fires a "convert-to-next-doc" proc | Engine drives transitions per BPMN graph | +| Reassignment / delegation | Not supported | Not supported | Supported via Activiti | +| Parallel branches | Not supported | Not supported | Supported via BPMN gateways | +| Currently active | Yes, on every 审核 click | Yes, in every multi-document business flow | No — `bCheckflowCheck = false` in code | +| Tooling | Just a stored proc | Stored procs + module-tree configuration | BPMN modeler at `/modeler/*` | + +### Why this design works for xly's audience + +The printing-industry ERP customers run rule-driven business +processes (quote → order → production → delivery → invoice → payment) +where each step is **its own document with its own form** by +convention. A user expects "Now I open the next form and fill it in" +rather than "the system tells me a task is waiting for me." For +that audience: + +- Path 1 + Path 2 cover every observed scenario in this dev DB. +- Path 3's value (BPMN modeling, reassignment, parallel gateways) is + reserved for the rare tenant whose approval graph genuinely needs + it. + +The trade-off: workflow logic is **scattered across stored procedures** +rather than declarable in one place. Adding a new step to a flow +means writing or editing one or more procs, not editing a BPMN +diagram. For complex, frequently-changing flows, this is brittle. +For the printing-shop reality (quote-to-cash chain that doesn't +change much per customer), it's pragmatic. ## Activiti is wired — engine ON