Spring Boot 表单挂 Flowable 审批:设计器绑模型后,业务侧用注解把单子跑起来

Spring Boot 表单挂 Flowable 审批:设计器绑模型后,业务侧用注解把单子跑起来

上一篇热帖讲的是:用 3 个注解在 Spring Boot 里接 Flowable,搭一条请假审批 (掘金原文,约 18 赞 / 44 藏)。很多人跟完之后卡在下一问------

低代码或应用中心里,表单页已经拖好了,审批节点也画完了。业务侧还要不要在 Service 里手写 RuntimeService.startProcessInstanceByKey?页面和流程模型到底怎么挂在一起,提交之后单子才会出现在待办里?

这篇只补这一段闭环:设计器侧挂模型 → 业务侧把单子跑起来 。方案载体仍是 Forge Admin;标题用 Spring Boot / Flowable / 表单等通用词,方便检索。文中路径相对仓库 forge-project,推断处标「按源码推演」,发布前建议在自己环境用一条请假或采购流复验。

查重:与已发的「审批公共节点合并」「超时终态 ENTITY_DELETED」,以及改期不发的「运行态 designPreview 卡死」都不是同一角度。本篇是热帖续篇,不重复讲 BPMN 节点合并或超时终态。

TL;DR

  1. 痛点:表单/页面做完,业务 Service 仍直接依赖 Flowable API,引擎一拆库就全改。
  2. 设计器侧 :业务对象绑定 flowModelKey + startMode(手动按钮 / 触发器 / 两者),节点列表从已发布模型读 userTask。
  3. 业务侧三件套 :类上 @FlowBind,方法上 @FlowStart(或显式 FlowClient.startProcess),回调上 @FlowCallback。
  4. 取舍 :需要立刻回写 processInstanceId 到业务表时,仓库请假/采购示例走显式 FlowClient;不需要回写可用 @FlowStart 切面。
  5. 低代码路径 :BusinessFlowService.startFlow 读绑定配置再 RPC 发起,类上 @FlowBind(modelKey = "*") 承接回调。

一、痛点:表单做完,还在手写 RuntimeService

你大概已经经历过这两种「半成品」:

  • 只做了表单 :字段、校验、列表页都有了,提交按钮后面空着,或直接 insert 业务表,审批靠人肉群消息。
  • 硬接了引擎 :Service 里 @Autowired RuntimeService,到处 startProcessInstanceByKey,流程一独立部署,业务模块跟着改编译依赖。

典型「搅在一起」的写法(反例,不要照抄):

java 复制代码
// 业务 Service 直接依赖引擎 API
Map<String, Object> vars = new HashMap<>();
vars.put("days", leave.getDays());
ProcessInstance pi = runtimeService.startProcessInstanceByKey(
    "leave_process", "leave:" + leave.getId(), vars);
leave.setProcessInstanceId(pi.getId());

问题有三:

  • 业务代码侵入流程引擎 API,引擎独立部署后到处改。
  • 表单页/业务对象和 modelKey 没有统一绑定入口,每个模块硬编码字符串。
  • 流程结束回写业务状态靠散落的监听器,请假、采购、报销各写一套,难复用。

Forge Admin 的拆法是:流程服务独立(forge-flow)+ 业务侧只认 FlowClient 与注解;低代码侧再把「哪个业务对象绑哪条流程」落到绑定配置,而不是散落在前端常量里。

二、架构一图:设计器挂模型 × 业务侧发起

先建立全景,再往下贴代码就不会懵。

text 复制代码
┌──────────────────────────────┐         ┌────────────────────────────┐
│ 应用中心 / 表单设计器(UI)     │         │ 流程服务 forge-flow         │
│                              │         │                            │
│ 选已发布流程模型 flowModelKey  │  发布    │  Flowable BPMN / 任务流转   │
│ startMode: MANUAL|TRIGGER    │ ───────►│  /start/{modelKey}         │
│ 节点权限从 userTask 解析       │         │  事件 → Redis/Webhook       │
└──────────────┬───────────────┘         └─────────────▲──────────────┘
               │ 绑定配置                                 │ HTTP
               ▼                                         │
┌──────────────────────────────┐         ┌─────────────┴──────────────┐
│ 业务服务(admin / business)   │         │ forge-flow-client          │
│                              │  RPC    │  FlowClient                │
│  @FlowBind(modelKey=...)     │ ───────►│  FlowStartAspect           │
│  @FlowStart / startProcess   │         │  FlowEventSubscriber       │
│  @FlowCallback 回写状态       │◄────────│  Pub/Sub 路由回调           │
└──────────────────────────────┘         └────────────────────────────┘

对应源码落点:

层级 真实路径
注解定义 forge-server/forge-flow/forge-flow-client/src/main/java/com/mdframe/forge/flow/client/annotation/(FlowBind / FlowStart / FlowCallback)
发起切面 .../helper/FlowStartAspect.java
自动配置 .../helper/FlowClientHelperAutoConfiguration.java(有 AspectJ + FlowClient 才注册切面)
设计器绑定 UI forge-admin-ui/src/views/app-center/components/ApplicationFlowBindingBar.vue、designer/BusinessFlowBindingPanel.vue
节点策略 .../ApplicationFlowNodePanel.vue(按 flowModelKey 拉 userTask)
低代码发起 forge-plugin-generator/.../businessapp/BusinessFlowService.java 的 startFlow(...)
请假示例 forge-admin-server/.../leave/service/impl/LeaveRequestServiceImpl.java
采购示例 forge-business-core/.../purchase/service/impl/SamplePurchaseOrderServiceImpl.java

服务器入口也能对上:FlowInstanceController 提供 POST /start/{modelKey},客户端最终都是打到这条链,而不是业务进程内嵌 Flowable。

三、设计器侧:页面/业务对象挂上流程模型

打开应用中心业务对象的流程绑定。新入口在应用工作台「业务流程」;旧的 BusinessFlowBindingPanel 会提示「配置已整合到业务流程画布」,并提供跳转按钮------跟做时以新入口为准。

核心字段就两个:

  1. 流程模型 :下拉已发布模型,值为 modelKey(如 leave_process)。
  2. 谁来发起 / 启动方式 :MANUAL(用户点按钮)、TRIGGER(触发器自动)、BOTH。

还可以配 流程标题模板 (如 ${opportunityName}-流程)和 业务字段 → 流程变量 映射;未配置时系统会按字段编码同名补齐(面板文案与 resetVariableMappings 行为一致)。

前端绑定栏源码(节选):

vue 复制代码
<!-- ApplicationFlowBindingBar.vue -->
<n-select
  v-model:value="form.flowModelKey"
  :options="flowModelOptions"
  placeholder="选择已发布的审批流程"
/>
<n-select
  v-model:value="form.startMode"
  :options="startModeOptions"
  :disabled="!form.flowModelKey"
/>

未选模型时提示文案是:「尚未绑定流程模型;绑定后节点配置将自动读取审批节点。」节点策略面板 ApplicationFlowNodePanel 会 watch flowModelKey,去拉该模型的 userTask;解析失败则回退手填节点标识,并提示确认模型已发布且包含用户任务。

保存后写入业务对象的 FLOW 绑定(flowModelKey、titleTemplate、startMode、variableMapping...),编解码在 BusinessFlowBindingCodec。

按源码推演:绑定保存走 saveBusinessFlowBinding(objectCode, payload);低代码正式发起时 BusinessFlowService.startFlow 会 selectBindingByTypeAndCode(..., "FLOW"),缺绑定或缺少 flowModelKey 直接抛「未配置流程绑定 / 缺少 flowModelKey」。所以「先绑模型再点提交」是硬前提,不是文档建议。

拖审批节点仍在流程设计器里完成;本篇不展开 BPMN 与公共节点合并。你只要记住一句:页面挂的是已发布的 modelKey,不是设计器里未发布的草稿画布。

四、业务侧竖切:三注解把单子跑起来

4.1 类上绑定模型:@FlowBind

java 复制代码
// forge-flow-client/.../annotation/FlowBind.java 文档示例
@FlowBind(modelKey = "leave_process", businessType = "leave")
@Service
public class LeaveService { ... }

仓库请假实现同样挂了这行(LeaveRequestServiceImpl)。低代码通用回调服务则是:

java 复制代码
@FlowBind(modelKey = "*", businessType = "lowcode-business")
public class BusinessFlowService { ... }

modelKey = "*" 表示按事件里的 processDefKey / businessKey 动态匹配,适合「一个服务承接多种业务对象」。采购示例则绑死具体常量:@FlowBind(modelKey = SamplePurchaseOrderService.MODEL_KEY, businessType = ...)。

4.2 方法成功后自动发起:@FlowStart

注解把 SpEL 约定写在源码注释里(FlowStart.java):

java 复制代码
@FlowStart(
    modelKey        = "leave_process",  // 可省略,取 @FlowBind.modelKey
    businessKeySpEl = "'leave:' + #leave.id",
    titleSpEl       = "#leave.name + ' 的请假申请'",
    userIdSpEl      = "#leave.applicantId",
    userNameSpEl    = "#leave.applicantName",
    variablesSpEl   = "{'days': #leave.days}"
)
public LeaveApply submit(LeaveApply leave) { ... }

切面 FlowStartAspect 在方法 proceed() 成功后解析 SpEL,调用 flowClient.startProcess(...);businessKeySpEl 为空会直接跳过并发 warn。引入 forge-flow-client、容器里有 FlowClient、类路径有 AspectJ 时,由 FlowClientHelperAutoConfiguration 注册该切面。

仓库里的真实取舍(跟做必看): 请假与采购示例目前没有 在 submit 方法上贴 @FlowStart,而是显式调用 FlowClient.startProcess。请假代码注释写得很直白------

显式发起流程并回写 processInstanceId(@FlowStart 切面无法回写业务表字段)

java 复制代码
// LeaveRequestServiceImpl.submitLeave(真实代码)
FlowResult<String> startResult = flowClient.startProcess(
        PROCESS_KEY, businessKey, BUSINESS_TYPE, title,
        Collections.emptyMap(),
        leave.getApplyUserId(), leave.getApplyUserName(),
        leave.getApplyDeptId(), leave.getApplyDeptName());
if (!startResult.isSuccess() || !StringUtils.hasText(startResult.getData())) {
    throw new BusinessException(...);
}
leave.setProcessInstanceId(startResult.getData());
updateById(leave);

采购单 SamplePurchaseOrderServiceImpl.submit 同理:先组装变量(金额、会签人、部门负责人等),再 RPC 启动,成功后写 processInstanceId 并 markInProcess。注释还强调「流程启动(远程 RPC)在事务外执行,避免长事务占用 DB 连接」------这是生产向细节,纯 @FlowStart 默认在业务方法返回后触发,细控事务边界时反而显式调用更清晰。

跟做决策表:

场景 推荐
演示/简单单据,不需要把 instanceId 落业务表 @FlowStart 零侵入
要落 process_instance_id,或要卡启动失败回滚语义 显式 FlowClient.startProcess(与仓库示例一致)
低代码按钮 / 触发器 走绑定 + BusinessFlowService.startFlow

两种写法都依赖同一条 HTTP 发起链,都不要再注入 RuntimeService。

4.3 结束与节点事件:@FlowCallback

java 复制代码
@FlowCallback(on = FlowCallback.ON_COMPLETED)
public void onLeaveApproved(FlowEventContext ctx) {
    LeaveRequest leave = getByBusinessKey(ctx.getBusinessKey());
    if (leave != null) {
        leave.setStatus(LeaveRequestStatus.APPROVED.getCode());
        leave.setApproveComment(ctx.getLastComment());
        updateById(leave);
    }
}

事件常量:PROCESS_COMPLETED / PROCESS_REJECTED / PROCESS_CANCELED,以及任务级 TASK_CREATED / TASK_COMPLETED / TASK_ASSIGNED。请假示例拆成三个方法;采购示例一个方法里 on = { ... } 多事件,再按 context.getEvent() 分支。分发靠 FlowEventSubscriber(默认订阅 Redis 频道 flow:event:all,也可 Webhook)。

若回调对不上,先查两件事:类上是否有匹配的 @FlowBind;forge.flow.client.redis-subscribe 是否为 true(自动配置默认 true)。

4.4 低代码:绑定配置驱动的发起

java 复制代码
// BusinessFlowService.startFlow(真实逻辑摘要)
AiBusinessBinding flowBinding = bindingMapper.selectBindingByTypeAndCode(
        tenantId, "OBJECT", objectCode, "FLOW");
String flowModelKey = resolveFlowModelKey(bindingConfig);
String businessKey = objectCode + ":" + recordId;
FlowResult<String> result = flowClient.startProcess(
        flowModelKey, businessKey, title,
        flowVariables, String.valueOf(userId), userName, null, null);

跟做可以压成四步:

  1. 在流程设计器发布模型,记下 modelKey。
  2. 在业务对象绑定该 modelKey,选好 startMode,保存。
  3. 代码优先模块:@FlowBind +(@FlowStart 或显式 startProcess)+ @FlowCallback。
  4. 低代码模块:保存业务记录后点「提交审批」或走触发器,后台进 startFlow。

文档技能 code-first-workflow.md 也建议:仅 DRAFT 可提交、启动成功后再置 IN_PROCESS、回调状态迁移要幂等------和采购示例一致。

五、对照表 & 自测清单

注解 / 入口对照

能力 位置 作用 仓库证据
@FlowBind 类 声明 modelKey / businessType Leave、Purchase、BusinessFlowService
@FlowStart 方法 成功后自动 startProcess 注解 + FlowStartAspect(业务示例多改显式调用)
@FlowCallback 方法 收流程/任务事件回写业务 Leave 三回调;Purchase 合并回调
设计器绑定 UI + Codec flowModelKey / startMode / 变量映射 BindingBar / BindingPanel / BindingCodec
低代码发起 Service 读绑定后 RPC BusinessFlowService.startFlow

自测清单(建议收藏)

  • 流程模型已发布 ,绑定下拉里能选到,且 modelKey 与 @FlowBind / 绑定 JSON 一致。
  • startMode 与入口一致:按钮发起用 MANUAL/BOTH;只靠触发器不要指望列表按钮出现。
  • 业务键全局唯一(请假 LEAVE_... / 低代码 objectCode:recordId / 采购按定义拼接)。
  • 若业务表有 process_instance_id,用显式 FlowClient 回写;不要假设 @FlowStart 会改实体字段。
  • Redis 或 Webhook 已通:通过/驳回后业务状态有变;日志里能看到回调命中对应 @FlowBind Bean。
  • 业务 Service 未 直接注入 RuntimeService / TaskService。
  • 启动失败时有明确业务异常(请假/采购都校验 FlowResult.isSuccess()),避免「表有数据、待办没有」。

六、小结 & CTA

表单挂审批不是再写一遍 Flowable 教程,而是两段对接:

  1. 设计器 :业务对象 ↔ 已发布 modelKey(再配启动方式与变量);
  2. 业务侧 :forge-flow-client 三注解(或显式 FlowClient)把单子跑起来、把状态收回来。

上一篇解决「注解怎么写」;这篇解决「页面怎么挂、单子怎么真的进待办」。Forge Admin 开源仓库与演示:

评论区想看下一篇哪条?例如「触发器自动发起的变量映射踩坑」或「任务表单字段权限如何跟节点走」------留言出题,少踩一个坑。

(系列:上篇「3 个注解搭请假」→ 本篇「表单挂模型 + 发起闭环」。这里只谈 Spring Boot + Flowable + 表单绑定的跟做路径。)

相关推荐
wno7041 小时前
Spring Boot中JdbcTemplate与Mybatis使用PostgreSQL
spring boot·postgresql·mybatis
云上工程笔记1 小时前
2026年大模型Token成本测算推荐清单:输入输出、缓存、并发与峰值用量
人工智能·spring boot·缓存·token
RuoyiOffice2 小时前
UniApp企业办公:把审批、客户和项目装进手机,能做些什么?
spring boot·vue3·uniapp·crm·flowable·移动办公·企业应用
mister_guo2 小时前
Spring Boot 配置文件读取的几种方式:用法、原理与常见误区
java·spring boot
余槐i2 小时前
虚拟线程上线后吞吐反而掉了:Spring Boot 3 里 synchronized 锁住载体线程的定位与修复
java·spring boot·性能优化·虚拟线程·jdk 21
独泪了无痕10 小时前
Spring Data Redis 快速上手
spring boot·redis·后端
Thomas.Sir13 小时前
第9课:SpringBoot4.x Web开发基础:WebMvc自动配置、静态资源、请求参数接收
java·spring boot
VimorphTech18 小时前
一段字幕太长,怎样拆句并安排显示时间?
java·spring boot
万联WANFLOW18 小时前
Kimi K3.1 Pro 接口接入指南:模型选型、费用计算与零数据保留
java·spring boot·python·安全·spring·大模型