Commit 4a3d6a2805f9c95836c8417d3839e21959dca568
1 parent
07f72f6c
docs: en wiki — document xly's three workflow mechanisms
User asked: if Activiti isn't used, what implements workflow? Investigated and replaced the original two-path table with a proper three-path explainer. Findings: - Path 1 (single-step approval via stored proc + bCheck flag) is the dominant pattern. Each business table carries bCheck (426 tables), tCheckDate (400), sCheckPerson (398) — virtually universal. The 审核 button POSTs to /business/doExamine which calls the proc named in gdsmodule.sProcName; the proc owns the business validation and flips bCheck=1 directly. No engine, no state machine, no queue. - Path 2 (document chaining) is how multi-document business processes work: separate gdsmodule entries arranged as parent + ordered children, each with its own form and approval button. A "convert-to-next-doc" proc on form A creates the document for form B. The "01/04, 02/04…" KPI Work Center step numbering is just iOrder of children under a parent gdsmodule. No state machine; the workflow is the chain of procs wired by buttons. - Path 3 (Activiti BPMN) is GATED by ConstantUtils.bCheckflowCheck which is hard-coded false. So even if a tenant configured gdsmoduleflow rows and deployed BPMN, the gate in ExamineServiceImpl.doExcuExamine would short-circuit. To activate Path 3 a maintainer would have to recompile xly with the constant flipped, OR runtime-patch it. The page now leads with all three paths and a comparison table covering state storage, step transitions, reassignment/delegation, parallel branches, current activation status, and tooling. Concluding section explains why this design fits xly's audience (printing-industry ERP, where each business step is naturally its own document) and the trade-off (workflow logic scattered across stored procs rather than visualised in BPMN).
Showing
2 changed files
with
165 additions
and
22 deletions
en/docs/api-reference/internal.md
| ... | ... | @@ -26,7 +26,7 @@ page is the catalog of HTTP entry points. |
| 26 | 26 | | `/business/getSelectDataBysControlId/{sId}` | POST | Dropdown population for a single control, by control `sId`. | |
| 27 | 27 | | `/business/getSelectLimit/{sId}` | POST | Paginated variant of the dropdown call. | |
| 28 | 28 | | `/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`. | |
| 29 | -| `/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. | | |
| 29 | +| `/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. | | |
| 30 | 30 | | `/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). | |
| 31 | 31 | |
| 32 | 32 | These endpoints are documented in detail by [Slice 1](../slices/01-hello-world.md) | ... | ... |
en/docs/reference/maintainer/activiti.md
| ... | ... | @@ -7,32 +7,175 @@ |
| 7 | 7 | > `gdsmodule.bCheck=1`. No `gdsmoduleflow` link. **The approval |
| 8 | 8 | > button users see (`/business/doExamine`) bypasses Activiti |
| 9 | 9 | > entirely** and just UPDATEs `bCheck=1` via SQL — see |
| 10 | -> [Two approval paths](#two-approval-paths) below. | |
| 10 | +> [How xly handles workflow without Activiti](#how-xly-handles-workflow-without-activiti) below. | |
| 11 | 11 | |
| 12 | 12 | This page documents what's actually wired (concrete classes, URLs, |
| 13 | 13 | engine state) and what would have to be true for it to do anything. |
| 14 | 14 | |
| 15 | -## Two approval paths | |
| 16 | - | |
| 17 | -xly ships two parallel approval mechanisms. They look similar from | |
| 18 | -the UI but go through completely different code: | |
| 19 | - | |
| 20 | -| Path | Triggered by | Backend | Activiti involvement | | |
| 15 | +## How xly handles workflow without Activiti | |
| 16 | + | |
| 17 | +xly has **three workflow-like mechanisms**, in decreasing order of how | |
| 18 | +much they're actually used: | |
| 19 | + | |
| 20 | +### Path 1 — Single-step approval via stored proc + `bCheck` flag (the dominant pattern) | |
| 21 | + | |
| 22 | +This is what 99% of approvals in xly look like. There is **no engine | |
| 23 | +and no state machine** — the workflow is the procedure call itself. | |
| 24 | + | |
| 25 | +The mechanics: | |
| 26 | + | |
| 27 | +1. Each business table carries the same three audit columns: | |
| 28 | + `bCheck` (the approval boolean), `sCheckPerson`, `tCheckDate`. | |
| 29 | + Empirical reach in this dev DB: `bCheck` on **426 tables**, | |
| 30 | + `tCheckDate` on 400, `sCheckPerson` on 398. So nearly every | |
| 31 | + business document has the audit trail built into its own row. | |
| 32 | +2. Each module declares a *single* approval procedure name in | |
| 33 | + `gdsmodule.sProcName` (column comment: "存储过程(审核)名称" — | |
| 34 | + "stored-procedure (approval) name"). For example | |
| 35 | + `Sp_Quo_QuotationCheck` for the quotation module, | |
| 36 | + `Sp_SalSalesCheck` for sales orders. | |
| 37 | +3. When the user clicks the 审核 button: | |
| 38 | + - `POST /business/doExamine` → `BusinessBaseController.java:384-391` | |
| 39 | + → `BusinessBaseServiceImpl.doExamine()` → `ExamineServiceImpl.doExcuExamine()` | |
| 40 | + - The service acquires a Redis lock keyed on the row id | |
| 41 | + (`ws_update_*_{sGuid}_*`) so two users can't approve concurrently. | |
| 42 | + - It dispatches to the proc named in `sProcName` via the generic | |
| 43 | + procedure-call machinery | |
| 44 | + ([proc-dispatch.md](proc-dispatch.md)). | |
| 45 | + - **The proc itself owns the business logic**: validate that all | |
| 46 | + required fields are populated, that child rows balance with | |
| 47 | + parent totals, that no related document is locked, etc. | |
| 48 | + If all checks pass, the proc UPDATEs the row to set | |
| 49 | + `bCheck = 1`, `sCheckPerson = <user>`, `tCheckDate = NOW()`. | |
| 50 | + - The proc returns `OUT sCode INT` (1 = success, ≤0 = error) | |
| 51 | + and `OUT sReturn LONGTEXT` (error message). | |
| 52 | +4. Cancel-approval is the same call with `iFlag = 0` instead of `1`; | |
| 53 | + the proc handles both directions. | |
| 54 | +5. `Sp_System_CheckSave` is a generic post-approval hook that | |
| 55 | + `BusinessBaseServiceImpl.java:1828` calls after every save of an | |
| 56 | + `bCheck`-tracked row — it writes a `sFormId` audit field and | |
| 57 | + contains placeholder slots for cross-document validation | |
| 58 | + (currently mostly commented out). | |
| 59 | + | |
| 60 | +There's no notion of "next assignee" or "approval queue" in this | |
| 61 | +path — once a single user with permission has clicked 审核 and the | |
| 62 | +proc has succeeded, the row is approved. Period. | |
| 63 | + | |
| 64 | +### Path 2 — Document-chaining as implicit multi-step workflow | |
| 65 | + | |
| 66 | +Multi-document business processes (quote → customer-confirm → | |
| 67 | +sales-order → delivery → invoice) are implemented as **separate | |
| 68 | +modules with their own forms**, not as a single document advancing | |
| 69 | +through states. The user's workflow: | |
| 70 | + | |
| 71 | +1. Module A (e.g., `quoQuotationMaster`) — fill it in, click 审核, | |
| 72 | + row gets `bCheck = 1`. | |
| 73 | +2. Module A has a button (configured via `gdsconfigformslave.sButtonParam` | |
| 74 | + pointing at `Sp_Quo_ToSalesOrder` or similar) that, when clicked, | |
| 75 | + creates a row in module B (e.g., `salSalesOrderMaster`) | |
| 76 | + pre-populated from module A. | |
| 77 | +3. The user navigates to module B, fills any extra fields, clicks | |
| 78 | + approve there too, etc. | |
| 79 | + | |
| 80 | +The "01/04, 02/04, 03/04, 04/04" step numbering on the FROUNT | |
| 81 | +[KPI Work Center](runtime.md#the-kpi-work-center-front-end-home-dashboard) | |
| 82 | +reflects this: each "flow" is a parent module containing N ordered | |
| 83 | +child modules; the steps are *just the children of a parent | |
| 84 | +gdsmodule entry*. There's no engine tracking "you're at step 2 of 4"; | |
| 85 | +the user is always working on whichever document is currently | |
| 86 | +in front of them, and the framework displays `iOrder` of the | |
| 87 | +parent's children for context. | |
| 88 | + | |
| 89 | +So multi-step "workflow" emerges from: | |
| 90 | + | |
| 91 | +- A parent gdsmodule grouping the steps thematically (`估价管理流程` = | |
| 92 | + "pricing management flow" with 4 children). | |
| 93 | +- Each child module's `sProcName` (single-step approval). | |
| 94 | +- Each child module's `sButtonParam` proc that, when fired, creates | |
| 95 | + the next document. | |
| 96 | +- Each business document's `bCheck` flag tracking its own row's state. | |
| 97 | + | |
| 98 | +No state machine, no FSM library — just a **chain of stored procs | |
| 99 | +wired together by buttons on forms**. This is the mechanism you'll | |
| 100 | +see in the dev DB. | |
| 101 | + | |
| 102 | +### Path 3 — Activiti BPMN workflow (gated, currently disabled in code) | |
| 103 | + | |
| 104 | +Path 3 exists in the codebase as a **third channel** that *would* | |
| 105 | +route through Activiti when activated. It is not running in this | |
| 106 | +dev DB and currently can't be activated without recompiling. | |
| 107 | + | |
| 108 | +The gate is hard-coded in `xlyPersist/.../utils/ConstantUtils.java`: | |
| 109 | + | |
| 110 | +```java | |
| 111 | +public static Boolean bCheckflowCheck = false; | |
| 112 | +``` | |
| 113 | + | |
| 114 | +Inside `ExamineServiceImpl.doExcuExamine()`, the dispatch is: | |
| 115 | + | |
| 116 | +```java | |
| 117 | +if (ConstantUtils.bCheckflowCheck) { | |
| 118 | + Map<String,Object> reMap = checkExamineFlowService | |
| 119 | + .doSendCheckFolw(sGuid, sUserName, sBrandsId, sSubsidiaryId, | |
| 120 | + sFormId, map, searMap, sBtnName, request); | |
| 121 | + if (MapUtil.isNotEmpty(reMap)) { return reMap; } | |
| 122 | +} | |
| 123 | +``` | |
| 124 | + | |
| 125 | +So even if a tenant deployed BPMN and linked modules via | |
| 126 | +`gdsmoduleflow`, the `if` would short-circuit because | |
| 127 | +`bCheckflowCheck` is a Java constant `false`. To activate Path 3 | |
| 128 | +you have to: | |
| 129 | + | |
| 130 | +1. Change `ConstantUtils.bCheckflowCheck = true` in source and | |
| 131 | + rebuild the WAR, **or** patch the constant at runtime. | |
| 132 | +2. Insert a `gdsmoduleflow` row keyed on `(sFormId, sBtnName)` — | |
| 133 | + this maps the form's approval button to a deployed BPMN. | |
| 134 | +3. Deploy the BPMN through the modeler so `act_re_procdef` is populated. | |
| 135 | +4. Make sure modules involved have `bCheck` semantics that align with | |
| 136 | + the BPMN's start/end events. | |
| 137 | + | |
| 138 | +Once activated, `CheckExamineFlowServiceImpl.doSendCheckFolw` reads | |
| 139 | +the `gdsmoduleflow` row, calls | |
| 140 | +`checkExamineFlowDataService.doSendCheckFolwData` to pre-stage the | |
| 141 | +data, then `doSendFlowUrl` which kicks the request to xlyFlow's | |
| 142 | +controllers. From there `ProcessServiceImpl.submitApply()` calls | |
| 143 | +`runtimeService.startProcessInstanceByKey(...)` and Activiti is in | |
| 144 | +charge — `biz_flow` + `biz_todo_item` populate, approvers see tasks | |
| 145 | +in their inbox, and `CurrencyFlowController.complete(...)` advances | |
| 146 | +the instance. | |
| 147 | + | |
| 148 | +### Comparison | |
| 149 | + | |
| 150 | +| Aspect | Path 1 (proc + bCheck) | Path 2 (doc chain) | Path 3 (Activiti) | | |
| 21 | 151 | |---|---|---|---| |
| 22 | -| **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). | | |
| 23 | -| **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`). | | |
| 24 | - | |
| 25 | -In this dev DB, **path 1 is the only path that runs**. Path 2 has zero | |
| 26 | -configuration anywhere — no BPMN deployed, no `gdsmoduleflow` row, no | |
| 27 | -`gdsmodule.bCheck=1` modules. So when a user clicks 审核 today, the | |
| 28 | -record gets approved via the simple-toggle path and Activiti is not | |
| 29 | -involved. | |
| 30 | - | |
| 31 | -Why the second path even exists: a customer that *does* need a real | |
| 32 | -multi-step approval (warehouse → finance → GM, with reassignment and | |
| 33 | -delegation) gets it by deploying a BPMN through the modeler and | |
| 34 | -linking modules via `gdsmoduleflow`. That activates path 2 for those | |
| 35 | -modules; path 1 still handles everything else. | |
| 152 | +| 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` | | |
| 153 | +| 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 | | |
| 154 | +| Reassignment / delegation | Not supported | Not supported | Supported via Activiti | | |
| 155 | +| Parallel branches | Not supported | Not supported | Supported via BPMN gateways | | |
| 156 | +| Currently active | Yes, on every 审核 click | Yes, in every multi-document business flow | No — `bCheckflowCheck = false` in code | | |
| 157 | +| Tooling | Just a stored proc | Stored procs + module-tree configuration | BPMN modeler at `/modeler/*` | | |
| 158 | + | |
| 159 | +### Why this design works for xly's audience | |
| 160 | + | |
| 161 | +The printing-industry ERP customers run rule-driven business | |
| 162 | +processes (quote → order → production → delivery → invoice → payment) | |
| 163 | +where each step is **its own document with its own form** by | |
| 164 | +convention. A user expects "Now I open the next form and fill it in" | |
| 165 | +rather than "the system tells me a task is waiting for me." For | |
| 166 | +that audience: | |
| 167 | + | |
| 168 | +- Path 1 + Path 2 cover every observed scenario in this dev DB. | |
| 169 | +- Path 3's value (BPMN modeling, reassignment, parallel gateways) is | |
| 170 | + reserved for the rare tenant whose approval graph genuinely needs | |
| 171 | + it. | |
| 172 | + | |
| 173 | +The trade-off: workflow logic is **scattered across stored procedures** | |
| 174 | +rather than declarable in one place. Adding a new step to a flow | |
| 175 | +means writing or editing one or more procs, not editing a BPMN | |
| 176 | +diagram. For complex, frequently-changing flows, this is brittle. | |
| 177 | +For the printing-shop reality (quote-to-cash chain that doesn't | |
| 178 | +change much per customer), it's pragmatic. | |
| 36 | 179 | |
| 37 | 180 | ## Activiti is wired — engine ON |
| 38 | 181 | ... | ... |