Activiti 集成
TL;DR:Activiti 已接线,流程引擎会启动,但没有流量经过它。 引擎已完成 bootstrap,
act_*schema 已按 6.0.0.4 版本建好,BPMN modeler 可访问。但所有工作流表都是 0 行:没有 BPMN、没有 procdef、没有流程实例、没有任务;没有gdsmodule.bCheck=1,也没有gdsmoduleflow关联。用户看到的审批按钮(/business/doExamine)完全绕过 Activiti,只是通过 SQL 把bCheck更新为1。细节见下面的 xly 如何在不使用 Activiti 的情况下处理工作流。
本页记录实际已接线的内容(具体类、URL、流程引擎状态),以及要让它真正工作需要满足什么条件。
xly 如何在不使用 Activiti 的情况下处理工作流 {#xly-如何在不使用-activiti-的情况下处理工作流}
xly 有三种类似工作流的机制,按实际使用程度从高到低排序如下。
路径 1:存储过程 + bCheck 标志的一步审批(主流模式)
这是 xly 中 99% 审批的样子。这里没有流程引擎,也没有状态机;工作流就是那次过程调用本身。
机制:
- 每张业务表携带同一组三个审计列:
bCheck(审批布尔值)、sCheckPerson、tCheckDate。在当前 dev DB 中,bCheck出现在 426 张表上,tCheckDate出现在 400 张表上,sCheckPerson出现在 398 张表上。也就是说,几乎每个业务单据都把审批审计轨迹放在自己行里。 - 每个模块在
gdsmodule.sProcName中声明一个单一审批过程名(列注释是“存储过程(审核)名称”)。例如报价模块的Sp_Quo_QuotationCheck、销售订单的Sp_SalSalesCheck。 - 用户点击“审核”按钮时:
-
POST /business/doExamine→BusinessBaseController.java:384-391→BusinessBaseServiceImpl.doExamine()→ExamineServiceImpl.doExcuExamine()。 - service 获取一个基于行 id 的 Redis 锁(
ws_update_*_{sGuid}_*),避免两个用户并发审批。 - 它通过通用过程调用机制分发到
sProcName命名的过程,见通用存储过程分发。 -
过程本身拥有业务逻辑:校验必填字段、子行和主表合计是否平衡、相关单据是否锁定等。如果全部通过,过程更新该行:
bCheck = 1、sCheckPerson = <user>、tCheckDate = NOW()。 - 过程返回
OUT sCode INT(1 表示成功,≤0 表示错误)和OUT sReturn LONGTEXT(错误消息)。
-
- 反审核是同一调用,只是
iFlag = 0而不是1;过程同时处理两个方向。 -
Sp_System_CheckSave是每个带bCheck行保存后的通用钩子,由BusinessBaseServiceImpl.java:1828调用。它写入sFormId审计字段,并保留跨单据校验的占位逻辑(当前大多已注释)。
这条路径没有“下一个审批人”或“审批队列”的概念。一个有权限的用户点击审核且过程成功后,该行就审批完成。
路径 2:单据串联形成的隐式多步工作流
多单据业务流程(报价 → 客户确认 → 销售订单 → 发货 → 发票)不是靠一个单据在状态之间推进,而是靠多个独立模块和表单实现。用户视角是:
- 模块 A(例如
quoQuotationMaster):填单,点击审核,行变成bCheck = 1。 - 模块 A 上有一个按钮(通过
gdsconfigformslave.sButtonParam指向Sp_Quo_ToSalesOrder或类似过程)。点击后,过程创建模块 B(例如salSalesOrderMaster)中的一行,并用模块 A 的数据预填。 - 用户进入模块 B,补充字段,再在那里审核,依次推进。
FROUNT KPI 工作中心 上的 “01/04、02/04、03/04、04/04” 步骤编号反映的就是这个模型:每个“流程”是一个父模块,下面有 N 个有序子模块;步骤只是父 gdsmodule 条目下的子模块。没有流程引擎在跟踪“你处于四步中的第二步”;用户只是操作当前打开的单据,框架用父模块下子模块的 iOrder 提供上下文。
因此,多步“工作流”来自:
- 一个按主题分组步骤的父
gdsmodule(例如估价管理流程,包含 4 个子模块)。 - 每个子模块自己的
sProcName(一步审批)。 - 每个子模块自己的
sButtonParam过程,点击后创建下一张单据。 - 每张业务单据自己的
bCheck标志。
没有状态机,也没有 FSM 库;只是通过表单按钮把一串存储过程接起来。这是当前 dev DB 中能看到的机制。
路径 3:Activiti BPMN 工作流(有闸门,目前代码中禁用) {#路径-3activiti-bpmn-工作流有闸门目前代码中禁用}
路径 3 作为第三条通道存在于代码库中:启用后会经由 Activiti。但它没有在当前 dev DB 中运行,而且目前不重新编译无法启用。
闸门硬编码在 xlyPersist/.../utils/ConstantUtils.java:
public static Boolean bCheckflowCheck = false;
ExamineServiceImpl.doExcuExamine() 内部分发逻辑是:
if (ConstantUtils.bCheckflowCheck) {
Map<String,Object> reMap = checkExamineFlowService
.doSendCheckFolw(sGuid, sUserName, sBrandsId, sSubsidiaryId,
sFormId, map, searMap, sBtnName, request);
if (MapUtil.isNotEmpty(reMap)) { return reMap; }
}
所以即使租户部署了 BPMN,并通过 gdsmoduleflow 关联了模块,这个 if 也会因为 bCheckflowCheck 是 Java 常量 false 而短路。要启用路径 3,需要:
- 把源码中的
ConstantUtils.bCheckflowCheck改为true并重新构建 WAR,或在运行时 patch 该常量。 - 插入一行以
(sFormId, sBtnName)为 key 的gdsmoduleflow,把表单上的审批按钮映射到已部署 BPMN。 - 通过 modeler 部署 BPMN,让
act_re_procdef有数据。 - 确认相关模块的
bCheck语义与 BPMN 起止事件对齐。
启用后,CheckExamineFlowServiceImpl.doSendCheckFolw 会读取 gdsmoduleflow 行,调用 checkExamineFlowDataService.doSendCheckFolwData 预置数据,再经 doSendFlowUrl 跳到 xlyFlow controller。随后 ProcessServiceImpl.submitApply() 调用 runtimeService.startProcessInstanceByKey(...),Activiti 接管流程;biz_flow + biz_todo_item 填充,审批人在收件箱中看到任务,CurrencyFlowController.complete(...) 推进流程实例。
对比
| 方面 | 路径 1(proc + bCheck) | 路径 2(单据链) | 路径 3(Activiti) |
|---|---|---|---|
| 状态存储 | 单据行上的 bCheck 列 |
无;状态等于用户打开哪张单据 |
act_ru_task、act_hi_*、biz_flow、biz_todo_item
|
| 步骤转换 | 每张单据一步 | 每个链路按钮触发“转下一张单据”的过程 | 流程引擎按 BPMN 图驱动转换 |
| 转派 / 委托 | 不支持 | 不支持 | Activiti 支持 |
| 并行分支 | 不支持 | 不支持 | BPMN 网关支持 |
| 当前是否活跃 | 是,每次“审核”点击都会用 | 是,多单据业务流都在用 | 否,代码中 bCheckflowCheck = false
|
| 工具 | 只有存储过程 | 存储过程 + 模块树配置 |
/modeler/* 下的 BPMN modeler |
真实路径 1 定制示例
切片 5 追踪了万昌的 领班驳回.sql:这是客户侧多级审批驳回的典型例子。它展示了客户在单个 bCheck 不够时如何扩展路径 1:ALTER TABLE 增加多个审批标志(bManager、bIPQC、bDeputy 等),按 Sp_<table>_check<currentState>_<nextState> 约定编写状态转换过程,并通过自定义 sp_add_flow_log 写审计。这是代码库中实证可见的定制通道;script/客户/ 下没有任何目录部署 BPMN。
为什么这个设计适合 xly 的用户
印刷行业 ERP 客户的业务流程通常是规则驱动的(报价 → 订单 → 生产 → 发货 → 开票 → 收款),每一步按惯例都是自己的单据和自己的表单。用户预期的是“现在打开下一张表单继续填”,而不是“系统告诉我有一个 task 等我处理”。对这类用户:
- 路径 1 + 路径 2 覆盖了当前 dev DB 中观察到的所有场景。
- 路径 3 的价值(BPMN 建模、转派、并行网关)留给极少数审批图确实需要它的租户。
代价是:工作流逻辑分散在存储过程中,而不是集中声明在一个地方。给流程增加新步骤意味着写或改一个或多个过程,而不是编辑 BPMN 图。对复杂且频繁变化的流程,这会很脆弱;但对印刷厂现实中的 quote-to-cash 链条(每客户不常变化)来说,这是务实选择。
Activiti 已接线:流程引擎已开启
尽管 dev DB 处于空转状态,流程引擎会随 xlyEntry 启动:
-
xlyFlow/build.gradle:15引入org.activiti:activiti-spring-boot-starter-rest-api:6.0.0。该 starter 传递引入activiti-spring-boot-starter,触发 Spring Boot 的ProcessEngineAutoConfiguration创建SpringProcessEngineConfigurationbean。 -
xlyEntry/build.gradle通过api project(':xlyFlow')引入 xlyFlow,因此 starter 在xlyEntryWAR 的运行时 classpath 上。 -
xlyEntry/.../EntryApplicationBoot.java:23-24只排除了org.activiti.spring.boot.SecurityAutoConfiguration(REST 端点安全适配器)和 Spring 自己的SecurityAutoConfiguration。Activiti 主流程引擎 auto-config 没有被排除,所以引擎会启动。 -
xlyFlow/.../activiti/config/ActivitiConfig.java是一个@Configuration implements ProcessEngineConfigurationConfigurer,只做两件事:把图生成器字体设为中文友好的宋体(activity / annotation / label fonts),并安装自定义ICustomProcessDiagramGenerator。 -
xlyApi的ApiApplicationBoot也没有排除 Activiti,但 xlyApi 不依赖 xlyFlow。因此 xlyApi classpath 上有流程引擎里的org.activiti.engine.identity.User类(仅由IdGen.java加密工具使用),但不会触发 Activiti auto-config。
实时 schema 中看到的 24 张基础 act_* 表,是流程引擎首次启动时通过 auto-DDL 创建的。
classpath 上的两个 Activiti 版本
| 模块 | 版本 | 说明 |
|---|---|---|
xlyPersist、xlyApi
|
org.activiti:activiti-engine:5.17.0 |
较老的 5.x 线,两个模块都有声明。遗留依赖;xlyEntry WAR 中实际运行的流程引擎是 xlyFlow starter 拉入的 6.0。5.17 声明只是 classpath 上的包袱。 |
xlyFlow |
org.activiti:activiti-spring-boot-starter-rest-api:6.0.0、activiti-json-converter:6.0.0
|
较新的 6.0 线,这才是会运行的版本。 |
做清理的维护人员应从 xlyPersist 和 xlyApi 的 build.gradle 中移除 5.17.0。已验证:这两个模块只有 IdGen.java 会触碰 org.activiti.engine.identity.User,而该类型签名由 6.0 流程引擎也能满足,因此移除是安全的。
代码中实际调用了什么
xlyFlow/src/main/java/com/xly/activiti/ 下 154 个 Java 文件加 modeler 子包是真实调用点。选取锚点如下:
| 活动 | 类 : 行 | 使用的 Activiti API |
|---|---|---|
| 启动流程实例 |
ProcessServiceImpl.submitApply() :107 |
runtimeService.startProcessInstanceByKey(module, businessKey, variables) |
| 完成任务 |
CurrencyFlowController.complete(...) :167 / :200;WechatFlowPostThread :132 |
processService.complete(taskId, ...) → taskService.complete()
|
| 查询活跃任务 |
CurrencyFlowController :409、:480 |
taskService.createTaskQuery().active().list() |
| 查询运行中实例 |
CurrencyFlowController :485、:659 |
runtimeService.createProcessInstanceQuery() |
| 在 modeler 中保存模型 |
ModelerController.create() :122 |
repositoryService.saveModel() + addModelEditorSource()
|
| 运行时部署 BPMN |
ModelerController.deploy() :147 |
repositoryService.createDeployment().addString(name, bpmnXml).deploy() |
| 列出流程定义 |
ProcessDefinitionController :135 |
repositoryService.createProcessDefinitionQuery() |
| 读取流程引擎配置 |
ProcessActController :281 |
ProcessEngines.getDefaultProcessEngine() |
| 把 xly 用户桥接到 Activiti identity |
act_id_user / act_id_group / act_id_membership 是投影 xly sftlogininfo* schema 的视图
|
xly 不写 Activiti identity 表;这些视图伪装成 identity 表 |
modeler 暴露的 URL(xlyFlow controller 挂在 xlyEntry 端口上) {#modeler-暴露的-urlxlyflow-controller-挂在-xlyentry-端口上}
xlyFlow 被 xlyEntry 作为库消费(api project(':xlyFlow')),因此 xlyFlow controller 会编译进 xlyEntry WAR,并在 xlyEntry context-path(/xlyEntry)下服务。重要 URL:
-
POST /xlyEntry/modeler/model/{modelId}/save:保存 BPMN modeler XML。 -
GET /xlyEntry/modeler/model/{modelId}/json:为编辑器加载模型。 -
GET /xlyEntry/modeler/editor/stencilset:modeler stencil 定义。 -
GET /xlyEntry/modeler/create//modeler/deploy/{modelId}:创建和部署。 -
POST /xlyEntry/currencyFlow/complete/{taskId}/{sBrandsId}/{sSubsidiaryId}/{sUserId}:完成任务(CurrencyFlowController)。 -
POST /xlyEntry/currencyFlow/completeerp/{sBrandsId}/{sSubsidiaryId}/{sUserName}:ERP 侧完成任务变体。
这些 URL 不在内部 API 页中完整编目,因为它们是很少触碰的工作流接口面;维护时以源码为准。
CheckFlowController.java 实际包含什么
这是一个需要明确标出的 wiki 内部纠正:xlyEntry/src/main/java/com/xly/web/businessweb/CheckFlowController.java 文件存在,但类体只有 22 行、0 个 handler 方法,只是一个带 @RestController @RequestMapping(value="/checkflow") 的空壳。早期 wiki 把 /checkflow/* 描述为“Activiti 工作流接口面(审批 / 驳回 / 查看)”,这不符合当前文件。实时系统上任何 /checkflow/* 子路径都会返回 404。 真正的审批 / 驳回 / 查看 URL 来自上面列出的 CurrencyFlowController 等 controller。
act_* schema 状态(当前 dev DB)
| 表 | 行数 | 有数据时的含义 |
|---|---|---|
act_re_model |
0 | modeler 中保存的 BPMN 模型 |
act_re_procdef |
0 | 已部署流程定义 |
act_ru_task |
0 | 活跃等待任务 |
act_hi_procinst |
0 | 历史流程实例 |
act_id_user / act_id_group / act_id_membership
|
视图 | 把 xly 的 sftlogininfo* 用户投影成 Activiti identity 形状 |
gdsmoduleflow |
0 | xly 从 gdsmodule 到流程定义的关联 |
biz_flow |
0 | xly 的每单据流程状态 |
biz_todo_item |
0 | 待审批任务(xly wrapper,不是 Activiti 的 act_ru_task) |
biz_todo_copyto |
0 | 流程抄送方 |
所以 Activiti 当前是彻底空转:流程引擎在运行,schema 已就绪,没有流量。
什么会让它动起来
要让一个流程真正运行,大致顺序如下:
- 工程师或 PM 打开 modeler UI(静态资源位于
xlyFlow/src/main/resources/static/modeler/,通过/modeler/*端点服务)。他们绘制 BPMN 并保存,act_re_model填充。 - 在 modeler 中点击 Deploy →
ModelerController.deploy()调用repositoryService.createDeployment().addString(name, bpmnXml).deploy()→act_re_procdef填充。 - 某个
gdsmodule行标记bCheck = 1,并在gdsmoduleflow中插入一行,把该模块关联到已部署的act_re_procdef.KEY_。 - 用户在该模块保存一行时,保存 service 检测
bCheck = 1,调用ProcessServiceImpl.submitApply(applyUserId, businessKey, itemName, itemContent, module, variables)。该方法执行runtimeService.startProcessInstanceByKey(module, businessKey, variables),于是act_ru_*表填充,biz_flow+biz_todo_item也得到 xly 侧行。 - 审批人在 FROUNT 收件箱中看到待办(很可能是“审批”tab,独立于 KPI 工作中心)。点击通过 / 驳回 →
CurrencyFlowController.complete()→taskService.complete()。 - 当流程实例到达
endEvent,行的bCheck发生转换;下游过滤bCheck = 1的查询开始看到它。
xly 为什么要接 Activiti
代码库已有自己的 biz_flow / biz_todo_item 表,理论上可以手写审批系统。把 Activiti 放在后面的收益是:
- 标准 BPMN 建模(JS modeler 使用 Activiti Explorer 同源的 stencilset)。
- 免费的状态机语义:流程引擎处理“task A 完成 → task B 可用”,xly 不需要用 SQL 维护 FSM。
- 图渲染能力(
ProcessActController中的页面转 PNG)。
成本是:JVM 中多一个流程引擎,DB 中多一套可能发生 DDL 漂移的 schema,以及一个额外认证接口面(xly 通过 act_id_* 视图把它遮住)。
本页不是什么
- 不是切片 7 的替代品。切片 7(暂缓)应基于一个真实运行流程的部署做端到端追踪。
- 不是 modeler 教程。modeler 来自 Activiti 项目;xly 只是把它作为静态资源嵌入,没有修改。
- 不是从 Activiti 迁移到其他方案的计划。那会是更大的架构决策,不是 wiki finding。