# 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 的情况下处理工作流](#xly-如何在不使用-activiti-的情况下处理工作流)。 本页记录实际已接线的内容(具体类、URL、流程引擎状态),以及要让它真正工作需要满足什么条件。 ## xly 如何在不使用 Activiti 的情况下处理工作流 {#xly-如何在不使用-activiti-的情况下处理工作流} xly 有**三种类似工作流的机制**,按实际使用程度从高到低排序如下。 ### 路径 1:存储过程 + `bCheck` 标志的一步审批(主流模式) 这是 xly 中 99% 审批的样子。这里**没有流程引擎,也没有状态机**;工作流就是那次过程调用本身。 机制: 1. 每张业务表携带同一组三个审计列:`bCheck`(审批布尔值)、`sCheckPerson`、`tCheckDate`。在当前 dev DB 中,`bCheck` 出现在 **426** 张表上,`tCheckDate` 出现在 400 张表上,`sCheckPerson` 出现在 398 张表上。也就是说,几乎每个业务单据都把审批审计轨迹放在自己行里。 2. 每个模块在 `gdsmodule.sProcName` 中声明一个**单一**审批过程名(列注释是“存储过程(审核)名称”)。例如报价模块的 `Sp_Quo_QuotationCheck`、销售订单的 `Sp_SalSalesCheck`。 3. 用户点击“审核”按钮时: - `POST /business/doExamine` → `BusinessBaseController.java:384-391` → `BusinessBaseServiceImpl.doExamine()` → `ExamineServiceImpl.doExcuExamine()`。 - service 获取一个基于行 id 的 Redis 锁(`ws_update_*_{sGuid}_*`),避免两个用户并发审批。 - 它通过通用过程调用机制分发到 `sProcName` 命名的过程,见[通用存储过程分发](proc-dispatch.md)。 - **过程本身拥有业务逻辑**:校验必填字段、子行和主表合计是否平衡、相关单据是否锁定等。如果全部通过,过程更新该行:`bCheck = 1`、`sCheckPerson = `、`tCheckDate = NOW()`。 - 过程返回 `OUT sCode INT`(1 表示成功,≤0 表示错误)和 `OUT sReturn LONGTEXT`(错误消息)。 4. 反审核是同一调用,只是 `iFlag = 0` 而不是 `1`;过程同时处理两个方向。 5. `Sp_System_CheckSave` 是每个带 `bCheck` 行保存后的通用钩子,由 `BusinessBaseServiceImpl.java:1828` 调用。它写入 `sFormId` 审计字段,并保留跨单据校验的占位逻辑(当前大多已注释)。 这条路径没有“下一个审批人”或“审批队列”的概念。一个有权限的用户点击审核且过程成功后,该行就审批完成。 ### 路径 2:单据串联形成的隐式多步工作流 多单据业务流程(报价 → 客户确认 → 销售订单 → 发货 → 发票)不是靠一个单据在状态之间推进,而是靠**多个独立模块和表单**实现。用户视角是: 1. 模块 A(例如 `quoQuotationMaster`):填单,点击审核,行变成 `bCheck = 1`。 2. 模块 A 上有一个按钮(通过 `gdsconfigformslave.sButtonParam` 指向 `Sp_Quo_ToSalesOrder` 或类似过程)。点击后,过程创建模块 B(例如 `salSalesOrderMaster`)中的一行,并用模块 A 的数据预填。 3. 用户进入模块 B,补充字段,再在那里审核,依次推进。 FROUNT [KPI 工作中心](runtime.md#kpi-工作中心front-端首页-dashboard) 上的 “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`: ```java public static Boolean bCheckflowCheck = false; ``` `ExamineServiceImpl.doExcuExamine()` 内部分发逻辑是: ```java if (ConstantUtils.bCheckflowCheck) { Map 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,需要: 1. 把源码中的 `ConstantUtils.bCheckflowCheck` 改为 `true` 并重新构建 WAR,或在运行时 patch 该常量。 2. 插入一行以 `(sFormId, sBtnName)` 为 key 的 `gdsmoduleflow`,把表单上的审批按钮映射到已部署 BPMN。 3. 通过 modeler 部署 BPMN,让 `act_re_procdef` 有数据。 4. 确认相关模块的 `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](../../slices/05-customer-sql-override.md#示例-2万昌构建多级审批工作流) 追踪了万昌的 `领班驳回.sql`:这是客户侧多级审批驳回的典型例子。它展示了客户在单个 `bCheck` 不够时如何扩展路径 1:`ALTER TABLE` 增加多个审批标志(`bManager`、`bIPQC`、`bDeputy` 等),按 `Sp__check_` 约定编写状态转换过程,并通过自定义 `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` 创建 `SpringProcessEngineConfiguration` bean。 - `xlyEntry/build.gradle` 通过 `api project(':xlyFlow')` 引入 xlyFlow,因此 starter 在 `xlyEntry` WAR 的运行时 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 页](../../api-reference/internal.md)中完整编目,因为它们是很少触碰的工作流接口面;维护时以源码为准。 ## `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 已就绪,没有流量。 ## 什么会让它动起来 要让一个流程真正运行,大致顺序如下: 1. 工程师或 PM 打开 **modeler UI**(静态资源位于 `xlyFlow/src/main/resources/static/modeler/`,通过 `/modeler/*` 端点服务)。他们绘制 BPMN 并保存,`act_re_model` 填充。 2. 在 modeler 中点击 *Deploy* → `ModelerController.deploy()` 调用 `repositoryService.createDeployment().addString(name, bpmnXml).deploy()` → `act_re_procdef` 填充。 3. 某个 `gdsmodule` 行标记 `bCheck = 1`,并在 `gdsmoduleflow` 中插入一行,把该模块关联到已部署的 `act_re_procdef.KEY_`。 4. 用户在该模块保存一行时,保存 service 检测 `bCheck = 1`,调用 `ProcessServiceImpl.submitApply(applyUserId, businessKey, itemName, itemContent, module, variables)`。该方法执行 `runtimeService.startProcessInstanceByKey(module, businessKey, variables)`,于是 `act_ru_*` 表填充,`biz_flow` + `biz_todo_item` 也得到 xly 侧行。 5. 审批人在 FROUNT 收件箱中看到待办(很可能是“审批”tab,独立于 KPI 工作中心)。点击通过 / 驳回 → `CurrencyFlowController.complete()` → `taskService.complete()`。 6. 当流程实例到达 `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。