# 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 = <user>`、`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<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，需要：

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_<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` 创建 `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。
