activiti.md 16.9 KB

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 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/doExamineBusinessBaseController.java:384-391BusinessBaseServiceImpl.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).
    • 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 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:

public static Boolean bCheckflowCheck = false;

Inside ExamineServiceImpl.doExcuExamine(), the dispatch is:

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/*

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

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 only 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 ProcessActController :281 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 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 22 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 at all

The codebase has its own biz_flow / biz_todo_item tables that could implement a hand-rolled approval system. The decision to put Activiti behind them buys:

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

The cost: a second engine running in the JVM, a second DB schema with its own DDL drift, a second authentication surface (which xly papers over via the act_id_* views).

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.