Commit 4a3d6a2805f9c95836c8417d3839e21959dca568

Authored by zichun
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).
en/docs/api-reference/internal.md
@@ -26,7 +26,7 @@ page is the catalog of HTTP entry points. @@ -26,7 +26,7 @@ page is the catalog of HTTP entry points.
26 | `/business/getSelectDataBysControlId/{sId}` | POST | Dropdown population for a single control, by control `sId`. | 26 | `/business/getSelectDataBysControlId/{sId}` | POST | Dropdown population for a single control, by control `sId`. |
27 | `/business/getSelectLimit/{sId}` | POST | Paginated variant of the dropdown call. | 27 | `/business/getSelectLimit/{sId}` | POST | Paginated variant of the dropdown call. |
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`. | 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 | `/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). | 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 These endpoints are documented in detail by [Slice 1](../slices/01-hello-world.md) 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,32 +7,175 @@
7 > `gdsmodule.bCheck=1`. No `gdsmoduleflow` link. **The approval 7 > `gdsmodule.bCheck=1`. No `gdsmoduleflow` link. **The approval
8 > button users see (`/business/doExamine`) bypasses Activiti 8 > button users see (`/business/doExamine`) bypasses Activiti
9 > entirely** and just UPDATEs `bCheck=1` via SQL — see 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 This page documents what's actually wired (concrete classes, URLs, 12 This page documents what's actually wired (concrete classes, URLs,
13 engine state) and what would have to be true for it to do anything. 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 ## Activiti is wired — engine ON 180 ## Activiti is wired — engine ON
38 181