Activiti/Flowable 工作流实战:业务状态和流程状态怎么保持一致?再讲清 RBAC + 数据权限
工作流项目真正难的地方,往往不是“怎么发起流程”,而是“流程跑起来之后,状态别乱、权限别乱、数据别乱”。
这个项目里我能明显看到两条很重要的主线:
一条是act_z_business.status/result这类流程侧状态管理;
另一条是sys_permission、SysPermissionDataRule、jurisdiction、节点字段权限这些权限控制。
这两块如果一开始没设计好,系统前面跑得再快,后面也会越来越难维护。
文章目录
- Activiti/Flowable 工作流实战:业务状态和流程状态怎么保持一致?再讲清 RBAC + 数据权限
- 一、为什么“业务状态”和“流程状态”总是容易打架
- 二、这个项目里更稳的做法:流程状态和业务状态分层
- 2.1 流程侧状态,放在 `act_z_business`
- 2.2 业务侧状态,放在业务表
- 三、流程状态到底该怎么同步,才不容易乱
- 3.1 发起时,同步一次流程主状态
- 3.2 取消、办结、审批通过时,统一回写
- 3.3 业务表不要直接写成“经理待审”“财务待审”
- 四、我建议的“一致性设计原则”
- 4.1 流程节点只保留一份真相
- 4.2 业务表只保留业务语义状态
- 4.3 所有状态更新都走统一服务层
- 4.4 事务失败时,把外部副作用一起回滚
- 五、只做 RBAC 为什么不够
- 六、这个项目里的权限体系,其实是 4 层一起工作的
- 6.1 第一层:菜单/功能权限
- 6.2 第二层:数据权限
- 6.3 第三层:页面/按钮权限
- 6.4 第四层:节点字段权限
- 七、RBAC + 数据权限 + 节点权限,三者到底怎么分工
- 八、如果让我继续优化这套权限体系,我会补什么
- 8.1 把流程状态到业务状态的映射做成统一配置
- 8.2 让节点字段权限和按钮权限共用一套表达式能力
- 8.3 对“流程状态一致性”增加巡检机制
- 九、总结
一、为什么“业务状态”和“流程状态”总是容易打架
很多团队一开始做审批系统,都会这么写:
- 业务表里写一个状态字段
- 审批到哪个节点,就把状态改成“经理待审”“财务待审”“总监待审”
- 前端列表也直接根据这个状态做展示
短期看很省事,长期看问题很大。
因为流程引擎本身已经有:
- 当前任务
- 当前节点
- 流程实例状态
- 历史任务轨迹
如果业务表再自己维护一套“细颗粒节点状态”,很容易出现两边不一致:
- 引擎里已经到财务节点了,业务表还停在经理审批
- 引擎里流程被撤销了,业务表却还是审批中
- 引擎里任务已经办结,业务表列表仍显示待处理
这类问题在项目刚上线时不一定明显,但一旦涉及:
- 加签
- 退回
- 撤销
- 子流程
- 条件分支
状态漂移几乎一定会出现。
二、这个项目里更稳的做法:流程状态和业务状态分层
我比较认可这个项目里的思路,因为它没有把所有状态都塞进一处,而是做了分层。
2.1 流程侧状态,放在act_z_business
从ActivitiConstant和ActBusiness能看出,这套系统对流程状态做了统一抽象。
status负责描述流程生命周期:
0:待提交1:处理中2:处理结束3:已撤销
result负责描述流程结果:
0:待提交1:处理中2:通过3:驳回4:撤销5:删除6:办结
另外还有:
procInstIdnodeIdcurrTaskNameapplyTime
这些字段一起构成了“流程侧统一真相”。
2.2 业务侧状态,放在业务表
业务表仍然可以保留自己的业务语义状态,比如:
- 单据是否启用
- 计划是否终止
- 库存是否冻结
- 合同是否到期
这个项目里DataBaseConstant也专门定义了标准字段:
bpm_status
我会把它理解为“业务面向报表与业务判断的状态字段”,而不是“替代流程引擎的节点状态字段”。
也就是说:
- 流程状态负责描述流程跑到哪一步
- 业务状态负责描述业务当前是什么语义
这两者应该映射,不应该互相覆盖。
三、流程状态到底该怎么同步,才不容易乱
这套项目里,其实已经给了一个相对标准的同步思路。
3.1 发起时,同步一次流程主状态
在ActBusinessServiceImpl.apply里,发起流程成功之后,会更新:
procInstIdstatus = 1result = 1applyTimecurrTaskName
这一步非常关键。
它说明系统不是在业务表里随便写一句“已提交”,而是把流程实例真正跑起来之后,再统一更新桥表状态。
3.2 取消、办结、审批通过时,统一回写
在cancel、finish以及任务审批相关逻辑里,又会更新:
- 取消时:
status = STATUS_CANCELED - 办结时:
status = STATUS_FINISH - 通过时:
result = RESULT_PASS - 撤回/办结时:
result = RESULT_REVOKE
这说明桥表不是只在发起时写一次,而是整个流程生命周期都在维护。
3.3 业务表不要直接写成“经理待审”“财务待审”
这是我最想强调的一点。
如果你把业务表状态直接写成:
- 采购经理待审
- 财务负责人待审
- 总经理待审
那你其实是在把 BPMN 的节点语义复制一份到业务库里。
一旦流程图稍微调整:
- 增加一个会签节点
- 某个分支跳过财务
- 某个节点改名
你业务库里的语义就会开始变形。
更稳的做法应该是:
- 细粒度流程节点,以引擎和
act_z_business.nodeId/currTaskName为准 - 粗粒度业务语义,以业务表自己的
bpm_status或biz_status为准
比如业务表只保留:
- 草稿
- 审批中
- 已通过
- 已驳回
- 已撤销
这就足够了。
四、我建议的“一致性设计原则”
如果你也在做 Activiti/Flowable 项目,我建议你优先守住这 4 条。
4.1 流程节点只保留一份真相
“当前在谁手里”“当前是什么节点”“是否还有待办”,统一以:
- 引擎运行时任务
act_z_business.nodeIdact_z_business.currTaskName
为准。
不要业务表、缓存、前端页面各维护一份。
4.2 业务表只保留业务语义状态
业务表状态字段应该回答的是:
- 这张单据现在是不是审批中
- 它对业务来说是否生效
- 能不能继续后续业务动作
而不是回答:
- 现在卡在流程图第几个节点
4.3 所有状态更新都走统一服务层
不要:
- 控制器改一点
- 定时任务改一点
- SQL 脚本又改一点
流程状态同步一定要收口到统一服务层里,不然你永远查不清谁把状态改乱了。
4.4 事务失败时,把外部副作用一起回滚
这个项目里有一个我很喜欢的细节:如果保存失败,会把生成编码重新回填 Redis 队列。
这说明团队已经意识到:
事务回滚不只是数据库回滚,还包括:
- 编码占用回滚
- 草稿缓存清理
- 外部状态恢复
这类意识,才是流程系统能不能做稳的关键。
五、只做 RBAC 为什么不够
很多人一说权限,第一反应就是:
- 用户
- 角色
- 菜单
- 接口
也就是标准 RBAC。
但工作流系统里,RBAC 只能解决第一层问题:
- 你能不能进入某个功能
它解决不了:
- 你能看哪些数据
- 你在这个节点能编辑哪些字段
- 你能不能看到某个按钮
- 同一个页面里你和别人看到的是不是同一套内容
所以在审批系统里,权限至少要拆成 4 层。
六、这个项目里的权限体系,其实是 4 层一起工作的
6.1 第一层:菜单/功能权限
这一层还是经典 RBAC。
核心就是:
sys_rolesys_permissionsys_role_permission
它解决的是:
- 这个角色有没有这个菜单
- 这个用户能不能访问这个页面/接口
这是入口权限。
6.2 第二层:数据权限
这套项目里,数据权限不是口头概念,而是有完整实现的。
能看到几个很关键的点:
@PermissionData(pageComponent = "...")SysPermissionDataRuleJeecgDataAutorUtilsQueryGenerator
这套链路的意思非常清楚:
- 页面或接口声明自己使用哪个菜单组件的数据权限
- 系统查询该菜单对应的数据规则
- 数据规则放进请求上下文
QueryGenerator在组装QueryWrapper时自动把规则拼进去
这说明它做的不是“页面上藏一个筛选条件”,而是真正把数据权限下沉到了查询层。
这点非常重要。
因为真正可靠的数据权限,不是前端不展示,而是后端根本查不出来。
6.3 第三层:页面/按钮权限
在低代码页面这一层,项目又加了一套jurisdiction配置。
比如:
SysBaseApiImpl.checkUserJurisdictionSnDesignServiceImpl.processButton
它会根据当前用户的:
- 角色
- 部门
去过滤页面按钮、操作按钮、扩展按钮。
也就是说,同一个页面不是所有人都能看到同样的操作项。
这层权限特别适合处理:
- 某部门可见,别的部门不可见
- 某角色能点“审核通过”,别的角色只能看详情
- 某些快捷操作只在特定组织范围内开放
6.4 第四层:节点字段权限
这一层是审批系统最容易被忽略,但实际最重要的一层。
项目里专门有表:
sn_flow_node_procdef_promission
从实体能看出来,它是按:
formIdflowIdflowNodeIdfieldId
来配置字段权限的。
而promissionType语义也很清楚:
0:不使用1:隐藏2:可写3:只读
这就意味着同一张表单,在不同审批节点上,可以表现成完全不同的交互形态。
这才是真正的“流程驱动表单”。
七、RBAC + 数据权限 + 节点权限,三者到底怎么分工
如果让我用一句最容易记住的话来总结,我会这么说:
- RBAC 决定你能不能进来
- 数据权限决定你能看哪些记录
- 节点字段权限决定你能改哪些字段
再加上页面按钮权限,刚好构成一套比较完整的审批权限体系。
我比较反对的一种做法是:
- 所有权限都堆到角色里
因为角色一旦负责太多事,就会开始失控:
- 角色越来越多
- 角色语义越来越混乱
- 页面按钮、数据范围、字段可写性全绑在一起
最后没人敢改。
而这个项目现在的方向,至少是把这些维度分开了,这是对的。
八、如果让我继续优化这套权限体系,我会补什么
8.1 把流程状态到业务状态的映射做成统一配置
现在这套系统已经有流程统一状态和业务侧状态字段,但如果继续平台化,我会再往前走一步:
- 用统一映射表或统一枚举策略
- 明确“哪个流程结果映射成哪个业务状态”
这样跨业务线时就不会各写各的。
8.2 让节点字段权限和按钮权限共用一套表达式能力
现在字段权限、按钮权限、数据权限是分层的,这是优点。
但如果后面还能再抽一层统一表达式引擎,比如都支持:
- 角色
- 部门
- 表单字段值
- 当前节点
那整个系统的可配置性会更强。
8.3 对“流程状态一致性”增加巡检机制
比如定时检查:
act_z_business.procInstId是否存在- 流程已结束但桥表是否还在处理中
- 业务表
business_id/process_id/node_id是否为空
这类巡检对长期运行的系统非常有价值。
九、总结
我现在越来越觉得,审批系统做到后面,真正拉开差距的不是会不会发起流程,而是两件事:
- 状态能不能始终对得上
- 权限能不能分层控制好
结合这个项目,我比较认同的一套思路是:
- 流程状态,统一放在
act_z_business - 业务状态,留在业务表,表达业务语义
- RBAC 管入口
- 数据权限管记录范围
- 页面按钮权限管操作入口
- 节点字段权限管表单交互
如果只记一句话,我觉得可以记住这句:
工作流系统里最稳的做法,不是把所有状态和权限揉成一团,而是让“流程状态”“业务状态”“角色权限”“数据权限”“节点字段权限”各自分层,再通过统一服务把它们串起来。