# Activiti integration

> **TL;DR — Activiti is wired, the engine runs, no traffic flows
> through it.** Engine bootstrapped, `act_*` schema provisioned at
> 6.0.0.4, BPMN modeler reachable. But every workflow table is 0
> rows: no BPMN, no procdefs, no instances, no tasks. No
> `gdsmodule.bCheck=1`. No `gdsmoduleflow` link. **The approval
> button users see (`/business/doExamine`) bypasses Activiti
> entirely** and just UPDATEs `bCheck=1` via SQL — see
> [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.

## 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 = <user>`, `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<String,Object> 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) |
|---|---|---|---|
| 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/*` |

### A real Path-1 customisation example

[Slice 5](../../slices/05-customer-sql-override.md#worked-example-2-万昌-builds-a-multi-level-approval-workflow)
walks through 万昌's `领班驳回.sql` — a customer-side multi-level
approval rejection. It's the canonical example of how customers
extend Path 1 when the single-`bCheck` flag isn't enough: they
`ALTER TABLE` to add multiple approval flags (`bManager`, `bIPQC`,
`bDeputy`, …), write transition procs following the
`Sp_<table>_check<currentState>_<nextState>` naming convention, and
emit audit entries via a custom `sp_add_flow_log`. This is the
empirically-observed customisation channel — Activiti deployment
is not seen in any `script/客户/` directory.

### Why xly avoided Activiti — and what that costs

The printing-industry ERP customers run rule-driven business
processes (quote → order → production → delivery → invoice → payment)
where each step is conventionally its own document with its own form.
The audience-fit argument: 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," so Path 1 + Path 2 cover every observed scenario in this
dev DB, and Path 3 is held in reserve.

The costs of going proc-based instead of BPMN-based:

- **Workflow logic is scattered across stored procedures, not
  declarable in one place.** Adding a step to "what happens after a
  quote is approved" means writing or editing one or more `Sp_*` procs,
  re-grepping every other proc that references the affected document,
  and hoping nothing was missed. A BPMN engine would have one diagram
  to look at.
- **No central audit trail of who approved what when.** `bCheck = 1`
  records that *some* approval happened, plus who approved it via the
  `sCheckPerson` column — but the *path the document took* (which
  steps, in which order, with what comments) lives only in proc-side
  status flags, not in a queryable workflow history.
- **No parallel-branch or reassignment semantics.** Path 1 + 2 cover
  linear single-approver flows. The first time a customer needs
  "two people must approve in parallel", or "if person A is on
  vacation, route to person B", the system has to either fall back
  to Path 3 (Activiti, currently disabled) or hand-code the routing
  in stored procs.
- **Flow-graph evolution is invisible.** Changing the steps of a
  workflow means editing procs and document chains. There is no
  diff that says "the order-approval flow changed from N steps to
  N+1 steps on date X" — only commit history of individual procs.
- **The Activiti engine is on the classpath and booted at runtime
  for nothing.** Memory + JAR + schema (24 `act_*` base tables + 3
  identity views) are paid for in every deployment whether they're
  used or not.

For the printing-shop reality the trade has been viable. It would
not scale to a domain with frequently-changing approval flows or
strict audit requirements.

## Activiti is wired — engine ON

Despite the dev DB being idle, the engine boots with `xlyEntry`:

- `xlyFlow/build.gradle:15` pulls
  `org.activiti:activiti-spring-boot-starter-rest-api:6.0.0`. That
  starter transitively pulls `activiti-spring-boot-starter`, which
  triggers Spring Boot's `ProcessEngineAutoConfiguration` to create
  a `SpringProcessEngineConfiguration` bean.
- `xlyEntry/build.gradle` includes `xlyFlow` as `api project(':xlyFlow')`,
  so the starter is on the runtime classpath of the `xlyEntry` WAR.
- `xlyEntry/.../EntryApplicationBoot.java:23-24` excludes
  `DataSourceAutoConfiguration`,
  `org.activiti.spring.boot.SecurityAutoConfiguration` (the
  REST-endpoint security adapter), and Spring's own
  `SecurityAutoConfiguration`. **Activiti's main engine
  auto-config is NOT excluded** → the engine starts.
- `xlyFlow/.../activiti/config/ActivitiConfig.java` is a
  `@Configuration implements ProcessEngineConfigurationConfigurer`
  whose only job is to set Chinese-friendly fonts on the diagram
  generator (`宋体` for activity / annotation / label fonts) and
  install a custom `ICustomProcessDiagramGenerator`.
- `xlyApi`'s `ApiApplicationBoot` does NOT exclude Activiti either,
  but xlyApi doesn't include xlyFlow as a dep, so xlyApi has the
  engine's `org.activiti.engine.identity.User` class on classpath
  (used only by `IdGen.java` for crypto utilities) but no Activiti
  auto-config kicks in there.

The 24 base `act_*` tables you see in the live schema are created by
the engine's auto-DDL on first boot.

## Two Activiti versions on the classpath

| Module | Version | Notes |
|---|---|---|
| `xlyPersist`, `xlyApi` | `org.activiti:activiti-engine:5.17.0` | Older 5.x line — declared in both modules. **Vestigial** — the runtime engine in `xlyEntry`'s WAR is the 6.0 one pulled by xlyFlow's starter. The 5.17 declarations are dead weight on the classpath. |
| `xlyFlow` | `org.activiti:activiti-spring-boot-starter-rest-api:6.0.0`, `activiti-json-converter:6.0.0` | Newer 6.0 line — this is what runs. |

A maintainer cleaning up should drop `5.17.0` from `xlyPersist` and
`xlyApi`'s `build.gradle`. Verified: only `IdGen.java` in each of
those modules touches `org.activiti.engine.identity.User`, and the
type signature is satisfied by the 6.0 engine too — removal is safe.

## What's actually invoked from code

The 154 Java files under `xlyFlow/src/main/java/com/xly/activiti/`
plus the modeler subpackage are real call sites. Selected anchors:

| Activity | Class : line | Activiti API used |
|---|---|---|
| Start a process instance | `ProcessServiceImpl.submitApply()` :107 | `runtimeService.startProcessInstanceByKey(module, businessKey, variables)` |
| Complete a task | `CurrencyFlowController.complete(...)` :167 / `:200`; `WechatFlowPostThread` :132 | `processService.complete(taskId, ...)` → `taskService.complete()` |
| Query active tasks | `CurrencyFlowController` :409, :480 | `taskService.createTaskQuery().active().list()` |
| Query running instances | `CurrencyFlowController` :485, :659 | `runtimeService.createProcessInstanceQuery()` |
| Save a model in the modeler | `ModelerController.create()` :122 | `repositoryService.saveModel()` + `addModelEditorSource()` |
| Deploy a BPMN at runtime | `ModelerController.deploy()` :147 | `repositoryService.createDeployment().addString(name, bpmnXml).deploy()` |
| List process definitions | `ProcessDefinitionController` :135 | `repositoryService.createProcessDefinitionQuery()` |
| Read engine config | `BizTodoItemServiceImpl` :126 | `ProcessEngines.getDefaultProcessEngine()` |
| Bridge xly users into Activiti identity | `act_id_user` / `act_id_group` / `act_id_membership` are **views** projecting xly's `sftlogininfo*` schema | xly does not write to Activiti's identity tables; the views fake them |

## URLs the modeler exposes (xlyFlow controllers, on xlyEntry's port)

Because xlyFlow is consumed as a library by xlyEntry (`api project(':xlyFlow')`),
all xlyFlow controllers compile into the xlyEntry WAR and serve at
xlyEntry's context-path (`/xlyEntry`). Notable URLs:

- `POST /xlyEntry/modeler/model/{modelId}/save` — save BPMN-modeler XML
- `GET  /xlyEntry/modeler/model/{modelId}/json` — load model for editor
- `GET  /xlyEntry/modeler/editor/stencilset` — modeler stencil definitions
- `GET  /xlyEntry/modeler/create` / `/modeler/deploy/{modelId}` — create + deploy
- `POST /xlyEntry/complete/{taskId}/{sBrandsId}/{sSubsidiaryId}/{sUserId}` — complete a task (CurrencyFlowController)
- `POST /xlyEntry/completeerp/{sBrandsId}/{sSubsidiaryId}/{sUserName}` — ERP-side completion variant

These are not catalogued on the [Internal API page](../../api-reference/internal.md)
because they're rarely-touched workflow surface; treat the source as
authoritative.

## What `CheckFlowController.java` actually contains

This is a wiki-internal correction worth flagging: the class file
exists at `xlyEntry/src/main/java/com/xly/web/businessweb/CheckFlowController.java`
but its body is **25 lines, zero handler methods** — just a
`@RestController @RequestMapping(value="/checkflow")` shell with no
content. Earlier versions of this wiki described `/checkflow/*` as
"Activiti workflow surface (approve / reject / view)"; that is not
what the file currently contains. **`/checkflow/*` returns 404 for
any sub-path on the live system.** The actual approve/reject/view
URLs come from `CurrencyFlowController` and friends listed above.

## The `act_*` schema state (this dev DB)

| Table | Rows | Meaning when populated |
|---|---:|---|
| `act_re_model` | 0 | BPMN models saved in the modeler |
| `act_re_procdef` | 0 | Deployed process definitions |
| `act_ru_task` | 0 | Active (waiting) tasks |
| `act_hi_procinst` | 0 | Historical process instances |
| `act_id_user` / `act_id_group` / `act_id_membership` | (views) | Project xly's `sftlogininfo*` users into Activiti identity shape |
| `gdsmoduleflow` | 0 | xly's link from `gdsmodule` to a process definition |
| `biz_flow` | 0 | xly's per-document flow state |
| `biz_todo_item` | 0 | Pending approver tasks (xly wrapper, not Activiti's `act_ru_task`) |
| `biz_todo_copyto` | 0 | CC'd parties on a flow |

So Activiti is **stone-cold idle**. Engine running, schemas ready, no traffic.

## What would make it move

For a flow to actually run, in roughly this order:

1. An engineer or PM opens the **modeler UI** (modeler static assets at
   `xlyFlow/src/main/resources/static/modeler/`, served via the
   `/modeler/*` endpoints). They draw a BPMN, save it
   (`act_re_model` populated).
2. They click *Deploy* in the modeler →
   `ModelerController.deploy()` calls
   `repositoryService.createDeployment().addString(name, bpmnXml).deploy()`
   → `act_re_procdef` populated.
3. A `gdsmodule` row is tagged `bCheck = 1` and a row in
   `gdsmoduleflow` links the module to the deployed
   `act_re_procdef.KEY_`.
4. When a user saves a row on that module, the save service detects
   `bCheck = 1` and calls `ProcessServiceImpl.submitApply(applyUserId, businessKey, itemName, itemContent, module, variables)`.
   That fires `runtimeService.startProcessInstanceByKey(module, businessKey, variables)`
   → `act_ru_*` tables populate, `biz_flow` + `biz_todo_item` get xly-side rows.
5. Approvers see pending tasks in their FROUNT inbox (probably the
   "审批" tab, separate from KPI Work Center). They click 通过/驳回 →
   `CurrencyFlowController.complete()` → `taskService.complete()`.
6. When the proc instance reaches `endEvent`, the row's `bCheck`
   transitions; downstream queries that filter on `bCheck = 1` start
   seeing it.

## Why xly bothered with Activiti — and whether it was worth it

The codebase has its own `biz_flow` / `biz_todo_item` tables that
*could* implement a hand-rolled approval system. The arguments for
putting Activiti behind them:

- Standard BPMN modeling (the JS modeler pulls the same stencilset as
  Activiti Explorer).
- Engine-managed state-machine semantics — "task A done → task B
  available" without xly maintaining the FSM in SQL.
- Diagram rendering (the page-as-PNG in `ProcessActController`).

The costs are not minor:

- A second engine running in the JVM, with its own startup cost,
  memory footprint, and operational surface.
- A second DB schema (24 `act_*` tables + 3 identity views) that
  diverges from xly's `gds*`/`biz*` conventions and needs its own
  DDL migrations across Activiti versions (and indeed: see the
  5.17 vs 6.0 version skew elsewhere on this page).
- A second authentication surface that xly papers over via the
  `act_id_*` views projecting xly's own users into Activiti's shape
  — a hack that works but creates two-way coupling between user-
  table changes and Activiti correctness.
- A modeler UI (Angular 1.x era) that maintainers have to learn
  separately from BACK.
- And — the most damning cost — **on this dev DB the engine is
  idle**. The `act_re_procdef` and `biz_flow` tables are empty, and
  Path 1 / Path 2 handle every observed workflow scenario. The
  Activiti dependency is paid for at every startup whether it's
  exercised or not.

A more honest framing: Activiti was bet on as the "real" workflow
solution; in practice the simpler proc-driven paths covered the
actual demand. The wiring stayed because removing it isn't free
either, but the value the engine delivers in the current deployment
is approximately zero. A future cleanup could plausibly remove
Activiti entirely and consolidate on the document-chain pattern,
trading away the *option* of BPMN-style flows for a smaller
codebase and one fewer schema to maintain.

## What this page is *not*

- A Slice 7 substitute. Slice 7 (deferred) would document an
  end-to-end traced flow against a deployment that actually runs one.
- A modeler tutorial. The modeler comes from the Activiti project; xly
  embeds it as static assets without modification.
- A migration plan from Activiti to anything else. That would be a
  larger architectural decision, not a wiki finding.
