SpringBoot+Flowable 审批体验全链路:提交校验、连续选人、驳回重提与移动端一致性
🌐 文档地址:https://ruoyioffice.com
👇 文章底部获取源码和演示地址 👇
💬 :17156169080(获取产品咨询)
审批系统的体验好坏,不取决于有多少按钮,而取决于一次“填写—选人—审批—驳回—修改—重提”的旅程里,业务校验、流程状态、可办动作和上下文能否始终保持一致。
▲ 审批地铁图用一条主线和一次驳回回环表达完整用户旅程。
引言:不是多做几个页面,而是统一事实
许多 Flowable 项目把流程启动成功当作终点:前端提交表单,后端 startProcessInstance,待办能出现就算完成。真正的业务问题往往发生在边界上:字段还没校验就弹选人框;用户选了第一个节点,却在第二个连续自选节点再次被打断;PC 能驳回,移动端按钮却不同;驳回后重新提交,之前的审批人上下文全部丢失。
要解决这些问题,必须把审批看成一条用户旅程,而不是若干孤立 API。服务端负责状态与权限真相,前端负责按终端组织交互;流程变量保存必要上下文,业务单据保存领域事实,审批轨迹保存谁在何时做了什么。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
本文讨论的不是孤立按钮,也不会把前端、后端、表结构拆成三本说明书。我们先看产品闭环如何运转,再用 ER、时序和三段核心代码解释其中最关键的约束。文中的“现有实现”以当前系统与已提交能力为依据;需要因企业规则继续扩展的部分,会明确放在边界与对照中。
一、产品能力与特点
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这些能力的共同点,是每一步都留下可回查的业务身份、状态和时间。页面只是入口,真正决定系统可信度的是:上一环节的结果能否成为下一环节的明确输入;失败、撤回或重做时,能否沿原关系恢复,而不是人工改数。
二、业务流程怎么串
2.1 第一步:提交按钮先触发表单校验,再询问流程上下文
自定义表单和业务表单都应暴露统一的 validate/getFormValues 协议。只有字段、附件与业务状态通过后,前端才请求“启动前信息”,得到需要发起人选择的连续节点。这样取消选人不会产生脏单,校验失败也不会浪费一次选择。
▲ 发起页把条件与流程信息放在提交上下文中。
2.2 第二步:一次收集连续自选节点,不让用户半路停住
后端沿流程图向前解析连续的自选用户任务,遇到自动选人、网关不确定分支或非用户任务时停止。前端按节点分组展示候选人;最终把 nodeId -> userIds 作为结构化变量提交,而不是拼成一个字符串。
▲ 批量选人弹窗按节点组织候选人并一次确认。
2.3 第三步:PC 与移动端都从服务端读取可办动作
同一个任务在不同终端可以采用不同布局,但“能否通过、驳回、转办、委派、加签”必须由服务端计算。计算依据包括任务当前处理人、流程状态、节点配置、业务单状态与租户权限。移动端只是把动作渲染成底部按钮。
▲ 移动审批详情把业务表单、流程状态与动作放在同一页面。
2.4 第四步:驳回与重提形成闭环,历史轨迹不能被覆盖
驳回完成后,业务单状态同步为已退回,并记录目标节点与意见。发起人修改后重新提交,可以带回上次选人建议,但仍要重新校验业务字段。新一轮任务追加到轨迹,旧审批意见保留,用户能看清为什么退、改了什么、又交给了谁。
▲ 移动端重提页保留退回原因、历史轨迹和再次提交入口。
走完这条主线,可以发现列表页只是入口。真正重要的是规则配置、业务表单、详情轨迹和最终台账,它们分别回答“为什么这样算”“这次提交了什么”“谁做了什么”“结果落在哪里”。
三、设计怎么落地:思路、ER、时序与核心代码
3.1 先把关键决策写清楚
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.2 核心实体只围绕闭环建模
▲ ER 图只保留支撑本文主链路的实体;字典、组织和通用审计表不在这里展开。
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.3 一条主时序比十个接口列表更容易验收
▲ 从一次用户或调度动作出发,串起端侧、领域服务、流程或账本,失败补偿在状态与幂等规则中处理。
3.4 把第一条关键规则收进服务端
前端提交顺序明确为“校验—保存—获取上下文—选人—启动”,取消选人不会误启动流程。
async function submitBill() {const values = await formRef.value?.validateAndGetValues();if (!values) return;await saveDraft(values);const context = await getStartContext(modelId, values);const assignees = context.selectableNodes.length? await openAssigneeDialog(context.selectableNodes): {};if (assignees === null) return;await startProcess({businessId: values.id,variables: context.variables,startUserSelectAssignees: assignees,clientRequestId: crypto.randomUUID(),});}
3.5 让核心写入可重试也不重复
后端只向前解析确定且连续的自选节点,遇到不确定网关立即停止,避免提前猜测未来路径。
public List<SelectableNode> findContinuousSelectableNodes(BpmnModel model, String startNodeId, Map<String, Object> variables) {List<SelectableNode> result = new ArrayList<>();FlowNode current = bpmnNavigator.next(model, startNodeId, variables);while (current instanceof UserTask task) {if (!candidateResolver.requiresStartUserSelect(task)) {break;}List<AdminUserRespDTO> candidates = candidateResolver.list(task, variables);result.add(new SelectableNode(task.getId(), task.getName(), candidates));FlowNode next = bpmnNavigator.singleDeterministicNext(model, task, variables);if (next == null) {break;}current = next;}return result;}
3.6 让回调与派生结果可追溯
驳回在同一业务事务中完成任务校验、流程动作、轨迹和业务状态回写,并保存重提建议。
public void rejectTask(Long userId, RejectReq req) {Task task = validateTaskAssignee(userId, req.getTaskId());BpmProcessInstanceExtDO instance = instanceService.getByProcessInstanceId(task.getProcessInstanceId());businessValidator.validateAction(instance.getBusinessKey(),ApprovalAction.REJECT);flowableRejectService.reject(task, req.getTargetNodeId(), req.getReason());commentService.create(task, userId, ApprovalAction.REJECT,req.getReason(), req.getAttachmentIds(), req.getClientType());businessStatusGateway.updateProcessStatus(instance.getBusinessKey(), ProcessStatus.REJECTED);resubmitContextService.saveSuggestion(instance.getId(), req.getTargetNodeId(), task.getAssignee());}
以上代码是围绕业务约束裁剪后的核心摘录,省略了权限注解、转换器、日志和通用异常包装。落地时仍应沿用项目现有的 Controller、Service、Mapper 分层,以及租户、数据权限和操作日志约定。
四、边界与对照:系统真正容易出错的地方
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这里有一个通用判断:不要用“最终数值看起来对”替代过程可解释。企业系统迟早会遇到撤回、补录、跨期、重试和历史规则变化。只有保存来源、版本、状态动作和结果引用,系统才具备修复能力。
五、把一次驳回重提当作验收剧本
一个有效的验收剧本应该跨终端、跨状态:员工在 PC 填写报销单,缺少发票时点击提交,系统先提示字段问题而不是弹审批人;补齐后一次选择“部门经理”和“财务复核”两个连续自选节点。经理在手机端驳回到发起人并上传说明,员工在移动端修改金额后重提,系统回显上次候选人但要求再次确认。财务最终通过,PC 详情同步显示已通过,轨迹保留两轮动作。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
为什么“按钮门禁”必须集中
按钮是否出现不是简单的 status == RUNNING。还要确认当前用户是否为任务处理人、任务是否已被他人完成、节点是否允许转办或加签、业务单是否仍允许当前动作、租户和数据权限是否匹配。服务端返回 action code、是否需要意见、是否需要目标节点和可编辑字段,端侧只负责交互,才能让 PC 与 UniApp 一致。
轨迹应当是证据,而不是装饰时间线
轨迹至少要包含节点名、处理人、动作、意见、时间、附件和终端;系统自动跳过、超时处理、转办和委派也要使用稳定动作码。展示层可以合并相邻自动节点,但底层记录不能丢,否则遇到审计或争议时无法解释。
五、快速体验
在线演示地址:https://ruoyioffice.com/web,可使用演示页提供的账号进入。建议不要只看列表,而是沿着本文主线完整走一次:
-
提交按钮先触发表单校验,再询问流程上下文 -
一次收集连续自选节点,不让用户半路停住 -
PC 与移动端都从服务端读取可办动作 -
驳回与重提形成闭环,历史轨迹不能被覆盖
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
常见问题(FAQ)
为什么提交前要先校验再弹选人?
因为选人是流程上下文,只有业务数据有效时才有意义;反过来会让用户重复劳动并产生脏草稿。
连续选人会不会提前穿过条件网关?
不应。只能解析条件已经确定的路径;遇到依赖后续审批结果的网关就停止。
PC 和移动端能有不同按钮吗?
布局可以不同,但可办动作集合应由同一服务端规则生成,端侧不能自行放宽权限。
驳回后应该复用原流程实例吗?
取决于策略。无论复用还是重启,都要保留原轨迹、业务轮次和重提来源,避免历史被覆盖。
结语
审批系统的体验好坏,不取决于有多少按钮,而取决于一次“填写—选人—审批—驳回—修改—重提”的旅程里,业务校验、流程状态、可办动作和上下文能否始终保持一致。 好的企业系统不会把复杂性藏在一条超长 SQL、一个万能表或一堆端侧 if/else 里,而是把业务日期、来源身份、状态迁移、版本和幂等边界设计成可观察、可验证的契约。
RuoYi Office 把流程、人力、财务、合同、项目、资产与移动端放在同一套企业管理平台中,适合用来验证本文的完整闭环,也便于开发者继续阅读 Spring Boot 3、Vue3、Flowable 与 UniApp 的实现。
如果这篇对你有用,点个「在看」或收藏。
🌐 演示地址:https://ruoyioffice.com/web
📦 GitHub 源码:https://github.com/yuqing2026/ruoyi-office
📦 Gitee 源码:https://gitee.com/yqzy1688/ruoyi-office
💬 微信:17156169080(获取产品咨询)
打开演示地址直接查看系统。

