大数跨境

SpringBoot3+Flowable 审批体验全链路:提交校验、连续选人、驳回重提与移动端一致性

SpringBoot3+Flowable 审批体验全链路:提交校验、连续选人、驳回重提与移动端一致性 企业软件源码
2026-10-10
3
导读:从一次真实审批旅程出发,串起提交前校验、连续自选审批人、PC 与移动待办、驳回重提、业务状态回调和审批轨迹,讲清 Flowable 审批体验如何保持一致。

SpringBoot+Flowable 审批体验全链路:提交校验、连续选人、驳回重提与移动端一致性

🌐 文档地址:https://ruoyioffice.com
 👇 文章底部获取源码和演示地址 👇
 💬 :17156169080(获取产品咨询)

审批系统的体验好坏,不取决于有多少按钮,而取决于一次“填写—选人—审批—驳回—修改—重提”的旅程里,业务校验、流程状态、可办动作和上下文能否始终保持一致。

▲ 审批地铁图用一条主线和一次驳回回环表达完整用户旅程。

引言:不是多做几个页面,而是统一事实

许多 Flowable 项目把流程启动成功当作终点:前端提交表单,后端 startProcessInstance,待办能出现就算完成。真正的业务问题往往发生在边界上:字段还没校验就弹选人框;用户选了第一个节点,却在第二个连续自选节点再次被打断;PC 能驳回,移动端按钮却不同;驳回后重新提交,之前的审批人上下文全部丢失。

要解决这些问题,必须把审批看成一条用户旅程,而不是若干孤立 API。服务端负责状态与权限真相,前端负责按终端组织交互;流程变量保存必要上下文,业务单据保存领域事实,审批轨迹保存谁在何时做了什么。

常见做法
直接后果
本文主张
点击提交先弹选人再校验
用户选完人才发现表单不合法
先业务校验,再解析需自选节点
连续自选节点逐个弹窗
提交过程被多次打断
启动前一次返回全部待选节点
按钮由 PC 写死
移动端出现越权或缺失动作
服务端按身份与状态下发可办动作
驳回只改流程任务
业务单仍显示审批中
流程动作与业务状态回调协同
重提创建全新上下文
审批人、附件或字段修改记录丢失
保存重提来源与可复用选人上下文

本文讨论的不是孤立按钮,也不会把前端、后端、表结构拆成三本说明书。我们先看产品闭环如何运转,再用 ER、时序和三段核心代码解释其中最关键的约束。文中的“现有实现”以当前系统与已提交能力为依据;需要因企业规则继续扩展的部分,会明确放在边界与对照中。

一、产品能力与特点

能力
主要角色
不止一张审批单的地方
提交校验
发起人
先校验业务字段、附件和状态,再进入选人
连续选人
发起人
一次选择多个连续自选节点的处理人
统一待办
审批人
PC 与 UniApp 读取同一任务、状态和可办动作
驳回策略
流程管理员
配置退回上一步、指定节点或发起人
重提上下文
发起人
带回上次字段与审批人,可按变化重新确认
审批轨迹
所有参与者
展示节点、处理人、意见、附件、时间与结果

这些能力的共同点,是每一步都留下可回查的业务身份、状态和时间。页面只是入口,真正决定系统可信度的是:上一环节的结果能否成为下一环节的明确输入;失败、撤回或重做时,能否沿原关系恢复,而不是人工改数。

二、业务流程怎么串

2.1 第一步:提交按钮先触发表单校验,再询问流程上下文

自定义表单和业务表单都应暴露统一的 validate/getFormValues 协议。只有字段、附件与业务状态通过后,前端才请求“启动前信息”,得到需要发起人选择的连续节点。这样取消选人不会产生脏单,校验失败也不会浪费一次选择。

▲ 发起页把条件与流程信息放在提交上下文中。

2.2 第二步:一次收集连续自选节点,不让用户半路停住

后端沿流程图向前解析连续的自选用户任务,遇到自动选人、网关不确定分支或非用户任务时停止。前端按节点分组展示候选人;最终把 nodeId -> userIds 作为结构化变量提交,而不是拼成一个字符串。

▲ 批量选人弹窗按节点组织候选人并一次确认。

2.3 第三步:PC 与移动端都从服务端读取可办动作

同一个任务在不同终端可以采用不同布局,但“能否通过、驳回、转办、委派、加签”必须由服务端计算。计算依据包括任务当前处理人、流程状态、节点配置、业务单状态与租户权限。移动端只是把动作渲染成底部按钮。

▲ 移动审批详情把业务表单、流程状态与动作放在同一页面。

2.4 第四步:驳回与重提形成闭环,历史轨迹不能被覆盖

驳回完成后,业务单状态同步为已退回,并记录目标节点与意见。发起人修改后重新提交,可以带回上次选人建议,但仍要重新校验业务字段。新一轮任务追加到轨迹,旧审批意见保留,用户能看清为什么退、改了什么、又交给了谁。

▲ 移动端重提页保留退回原因、历史轨迹和再次提交入口。

走完这条主线,可以发现列表页只是入口。真正重要的是规则配置、业务表单、详情轨迹和最终台账,它们分别回答“为什么这样算”“这次提交了什么”“谁做了什么”“结果落在哪里”。

三、设计怎么落地:思路、ER、时序与核心代码

3.1 先把关键决策写清楚

决策点
落地方式
为什么这样做
校验顺序固定
业务校验通过后才查询启动前选人
避免选人结果与无效表单绑定
选人变量结构化
Map<nodeId, userIds>
支持连续节点、多人节点和回显
可办动作服务端化
详情接口返回 action list
双端不复制权限判断
状态双轨但单向回调
流程状态驱动业务状态更新
业务表不自行猜测 Flowable 任务
轨迹追加不覆盖
每个动作写意见、终端和时间
驳回重提仍保留完整证据

3.2 核心实体只围绕闭环建模

▲ ER 图只保留支撑本文主链路的实体;字典、组织和通用审计表不在这里展开。

关键字段
业务含义
business_key
流程实例关联业务单据
process_status
业务侧待提交、审批中、通过、驳回
task_definition_key
当前用户任务节点
start_user_select_assignees
连续自选节点与人员映射
action_code
通过、驳回、转办、委派等动作
reject_target_node
驳回落点
resubmit_from_instance_id
重提来源实例或轮次
client_request_id
双端幂等提交请求号

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 让回调与派生结果可追溯

驳回在同一业务事务中完成任务校验、流程动作、轨迹和业务状态回写,并保存重提建议。

@Transactional(rollbackFor = Exception.class)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 分层,以及租户、数据权限和操作日志约定。

四、边界与对照:系统真正容易出错的地方

边界场景
推荐处理
拒绝与字段校验
拒绝通常不应强制校验审批节点可编辑的业务字段
多人会签
可办动作还需结合会签完成条件与当前实例状态
并发点击
clientRequestId + 当前任务状态双重幂等
驳回落点
必须验证目标节点位于合法历史路径或配置范围
附件共享
业务附件按业务 ID 关联,审批意见附件按任务/评论关联
终端一致性
状态码与动作码统一,文案和布局可以按端适配

这里有一个通用判断:不要用“最终数值看起来对”替代过程可解释。企业系统迟早会遇到撤回、补录、跨期、重试和历史规则变化。只有保存来源、版本、状态动作和结果引用,系统才具备修复能力。

五、把一次驳回重提当作验收剧本

一个有效的验收剧本应该跨终端、跨状态:员工在 PC 填写报销单,缺少发票时点击提交,系统先提示字段问题而不是弹审批人;补齐后一次选择“部门经理”和“财务复核”两个连续自选节点。经理在手机端驳回到发起人并上传说明,员工在移动端修改金额后重提,系统回显上次候选人但要求再次确认。财务最终通过,PC 详情同步显示已通过,轨迹保留两轮动作。

检查点
预期行为
反例
提交前校验
错误聚焦到业务字段,不进入选人
先选完人再报必填错误
连续选人
一个弹窗按节点分组收集
每流转一步再次打断发起人
移动端驳回
只展示服务端允许的目标节点
端侧写死“驳回上一步”
重提
带回业务内容、退回原因和选人建议
创建一张看不出来源的新单
轨迹
原任务、驳回、修改、重提全部追加
重提覆盖第一次审批意见
状态回写
业务单与流程实例最终一致
流程已结束,业务仍审批中

为什么“按钮门禁”必须集中

按钮是否出现不是简单的 status == RUNNING。还要确认当前用户是否为任务处理人、任务是否已被他人完成、节点是否允许转办或加签、业务单是否仍允许当前动作、租户和数据权限是否匹配。服务端返回 action code、是否需要意见、是否需要目标节点和可编辑字段,端侧只负责交互,才能让 PC 与 UniApp 一致。

轨迹应当是证据,而不是装饰时间线

轨迹至少要包含节点名、处理人、动作、意见、时间、附件和终端;系统自动跳过、超时处理、转办和委派也要使用稳定动作码。展示层可以合并相邻自动节点,但底层记录不能丢,否则遇到审计或争议时无法解释。

五、快速体验

在线演示地址:https://ruoyioffice.com/web,可使用演示页提供的账号进入。建议不要只看列表,而是沿着本文主线完整走一次:

  1. 提交按钮先触发表单校验,再询问流程上下文
  2. 一次收集连续自选节点,不让用户半路停住
  3. PC 与移动端都从服务端读取可办动作
  4. 驳回与重提形成闭环,历史轨迹不能被覆盖
资源
地址
适合谁
在线演示
https://ruoyioffice.com/web
产品、实施、技术负责人先验证交互与业务闭环
GitHub 源码
https://github.com/yuqing2026/ruoyi-office
需要阅读 Spring Boot、Vue3 与流程实现的开发者
Gitee 源码
https://gitee.com/yqzy1688/ruoyi-office
国内网络环境下拉取与对照代码

常见问题(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(获取产品咨询)

打开演示地址直接查看系统。

【声明】内容源于网络
0
0
企业软件源码
RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
内容 150
粉丝 0
企业软件源码 RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
总阅读2.8k
粉丝0
内容150