首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >工作流引擎:从一条请假流程看审批流转的完整生命周期

工作流引擎:从一条请假流程看审批流转的完整生命周期

原创
作者头像
技术挖掘官
发布于 2026-09-22 14:27:08
发布于 2026-09-22 14:27:08
1510
举报

凡是做过企业应用的开发者,都绕不开审批流:请假要审批、报销要审批、采购要审批。自己实现一套审批流引擎,涉及流程定义、节点流转、待办推送、回退转发、会签加签,没有一个月下不来,还容易埋雷。geejing WebBuilder 快速开发平台把工作流做成了内置引擎:流程用 .flw 文件定义,业务模块只关心"表单长什么样、数据存哪里"两件事,其余的流转、调度、状态管理全部由引擎接管。本篇通过 example/workflow 目录下的请假流程(leave)与手动启动(manual-start)示例,把一条审批流从发起到归档的完整生命周期走一遍。

一、一个流程应用的文件地图

打开 example/workflow/leave 目录,你会看到一组职责分明的模块:start-form.xwl(发起表单)、handle-form.xwl(审批表单)、before-action.xwl 与 after-action.xwl(动作前后钩子)、actions.xwl(业务数据服务端)。再配上 example/leave.flw 流程定义文件和一个 reimburse(报销)目录的同构实现。这套文件结构本身就是最好的架构文档:

  • 流程定义(.flw):描述节点与流转规则,与业务代码完全分离;
  • start-form:发起人看到什么、填什么;
  • handle-form:审批人看到什么、能做什么动作;
  • before/after-action:每个动作执行前后的服务端钩子,负责日志、通知、业务联动;
  • actions:纯粹的业务 CRUD,与本系列第 14 篇讲的范式相同。

换句话说,平台把"流程"从业务代码里抽了出来——业务模块不感知流程走向,流程文件不包含业务逻辑,两者通过约定的接口(init、afterAction 这些钩子)对接。这是所有 BPM 设计的核心理念,但在这里它被简化成了一组浅显的文件命名约定。这份简单是有代价换来的:引擎需要预设大量约定(表单叫 start-form、数据绑定 flow_id、钩子叫 before/after-action),约定生效后开发者就不用为每个流程重复写调度代码。有团队会担心"约定不够灵活",从我们的经验看,真正会撞墙的是少数——流程引擎的价值不在能画出多复杂的流程图,而在把稳定性要求最高的那部分(状态一致性、待办正确性)从你的代码里拿走。复杂的会签、子流程、条件路由,引擎层面都有对应能力;真正要警惕的反而是把业务逻辑塞进流程定义里,让 .flw 文件变成第二个代码仓库。

二、发起表单:一个模块服务三种视角

截图是 start-form.xwl 打开后的样子:一个"Leave application"窗口,五个必填字段——标题、开始日期、结束日期、请假天数、事由。表单控件的定义也延续了系列一贯的简洁:required: 'true' 标必填,日期字段用 Wb.Date 自带日历下拉,请假天数用 Wb.Number 限定整数(decimalCount: '0')加最大长度,引擎的 Wb.verify 会在提交前统一把关。这些控件细节在第 9、10 篇里都讲过,此处只需知道:审批表单不是特殊物种,就是普通表单 + 工作流约定。它的客户端初始化代码值得细读:

代码语言:txt
复制
Wb.apply(app, {
  autoShow: false,
  init(flowId, type, record) {
    if (flowId) {
      Wb.ajax({
        url: xpath + '/../actions&xaction=selectLeave',
        params: { flowId },
        json: true,
        success(resp) {
          Wb.setValue(app.main, resp);
          app.main.show();
        }
      });
    } else {
      app.main.show();
    }
  }
});

init(flowId, type, record) 是平台工作流与业务表单之间最重要的约定:同一个表单模块会被引擎以三种形态调用——type 为 start 时是发起人填写的新表单(flowId 为空,直接显示空表);为 handle 时是审批人看到的表单(flowId 有值,先 ajax 拉取业务数据 Wb.setValue 回填再显示);为 view 时是查阅者的只读视图。一个模块服务三种视角,不需要写三个页面,这依赖的正是第 15 篇讲的"模块即单元"的加载机制:引擎按需加载 start-form,传不同参数,得到不同表现。

再看服务端部分。start-form 的 serverScript 里定义了 afterAction(action, flow)——它在"发起"、"通过"等动作执行后被引擎回调:

代码语言:txt
复制
afterAction(action, flow) {
  if (flow.restarting) {
    Params.$flow_id = flow.flowId;
    Wb.sync({ tableName: 'wb_leave', update: Params, whereFields: 'flow_id' });
  } else {
    Wb.apply(Params, { sid: Wb.getId(), flow_id: flow.flowId, user_id: flow.handleUser });
    Wb.sync({ tableName: 'wb_leave', insert: Params });
  }
}

这段代码处理了业务数据落库的两种情形:流程首次发起(else 分支),生成主键 sid、绑定流程实例号 flow_id 与发起人 flow.handleUser,插入 wb_leave 表;流程被驳回后修改重提(flow.restarting 为 true),改为按 whereFields: 'flow_id' 更新原记录。注意业务表与流程实例的关联方式——一张业务表加一个 flow_id 字段就完成了与引擎的对接,引擎侧的流程状态、审批记录都存在引擎自己的表里,业务表干干净净只有业务字段。tags: '{container: false}' 的声明则告诉引擎这个模块不作为容器加载。

三、审批表单:动作就是方法名

审批环节的 handle-form.xwl 展示了引擎提供的动作 API 全家福。先看初始化:

代码语言:txt
复制
Wb.apply(app, {
  hideActionButtons: true,
  leaveData: _$leaveData$_,
  init(flowId, type, record) {
    Wb.setValue(app.main, app.leaveData);
    app.radioGroup1.visible = type == 'handle';
    Wb.log(record.data.node_name);
  },
  ...

三处细节:hideActionButtons: true 隐藏引擎默认的"通过/驳回"按钮,改由表单自定义 UI;leaveData: _$leaveData$_ 是平台的模板占位语法——引擎加载模块时会把审批单数据直接注入源码占位处,比 ajax 回取少一次往返(两种取数方式并存,按场景选);app.radioGroup1.visible = type == 'handle' 让操作选项只在审批视角出现,查阅视角自动隐藏。

核心的 onAction 是一个 switch 分发,每个 case 调用一个 doXxx 方法:

代码语言:txt
复制
switch (app.radioGroup1.value) {
  case 0: app.doPass(); break;           // 通过
  case 1: value = app.nodeSelect.value;
          if (value) app.doBack(value);  // 退回到指定节点
          else { app.nodeSelect.focus(); Wb.tipSelect(); }
          break;
  case 2: value = app.userSelect.value;
          if (value) app.doTransfer(value); // 转办给指定人
          break;
  case 3: app.doReject(); break;         // 驳回终止
  case 4: value = app.userSelect.value;
          if (value) app.doSignBefore(value); // 前加签
          break;
}

doPass、doBack(节点)、doTransfer(用户)、doReject、doSignBefore(用户) 五个方法由引擎注入到审批表单的作用域,覆盖了审批操作的主干:通过、退回、转办、驳回、加签。方法内部自动处理流转逻辑——更新引擎状态表、计算下一节点处理人、写审批记录,业务代码只在动作完成后通过 afterAction 钩子拿到通知。这五个动作与 .flw 文件里的节点定义是联动的:doBack 能退到哪些节点,取决于流程图里画了哪些可回退路径;doTransfer 的候选用户来自节点上配置的处理人规则。表单代码只负责"发起动作调用",规则解释权始终在引擎手里——这也是审批系统审计合规的关键:动作从来不是页面说了算,页面只是触发器。特别注意示例里的参数校验写法:选了"退回"但没选目标节点时,app.nodeSelect.focus() 聚焦控件 + Wb.tipSelect() 弹本地化提示,第 17 篇讲的反馈体系在这里派上了用场。

动作执行的前后还有两个全局钩子模块 before-action.xwl 与 after-action.xwl,内容极简却是理解引擎的关键:

代码语言:txt
复制
Wb.info('before flow action: ' + app.action); // action name
Wb.info('before flow id: ' + app.flow.flowId); // workflow id

app.action 是动作名,app.flow.flowId 是流程实例号,两个钩子在每个动作执行前后都会被调用(示例里只做了日志,注释显示也可以 return 值给客户端)。发放通知、写操作日志、联动外部系统,都挂在这里,一处配置、全流程生效。钩子还提供了一个鲜为人知的扩展点:before 钩子在某些场景下可以中断流程——比如发现金额超限需要走另一条审批线时,可以在钩子里改写流转参数。这类能力要用得克制:钩子里的逻辑越重,流程行为就越难预测,一般原则是"钩子做观察与记录,实在必要才做干预"。

四、手动启动:两行代码发起一个流程

前面讲的都是"用户在界面上填表发起",manual-start.xwl 演示了另一条路:代码直接发起流程。页面只有一个按钮,服务端逻辑在 start.xwl:

代码语言:txt
复制
flow = new Wb.Workflow({ file: 'example/leave.flw', title: 'My leave application' });
flow.start();
Wb.sync({
  tableName: 'wb_leave', insert: {
    sid: Wb.getId(), begin_date: now.addDay(2), end_date: now.addDay(5),
    flow_id: flow.flowId, user_id: Wb.userid, leave_days: 8, leave_reason: 'Go on vacation'
  }
});

new Wb.Workflow({ file, title }) 指定流程定义文件并实例化,flow.start() 启动实例,flow.flowId 拿到实例号写进业务表——发起一个流程只需要这三步,和 new 一个普通对象没有任何区别。示例数据里的 user_id: Wb.userid 取当前登录用户,now.addDay(2) 展示了平台对 Date 的扩展方法(addDay、dateText 这些在原生 JS 里都要自己写)。这条路径的典型用途是系统自动发起的流程:合同到期自动触发续签审批、库存低于阈值自动发起采购申请——没有人在界面上点"发起",流程照样转起来。从工程视角看,"API 发起"与"表单发起"走的是同一条引擎管道,后续的审批、回退、归档行为完全一致,你不需要为自动发起的流程单独维护一套处理逻辑——很多自研工作流恰恰是在这条边界上裂开的:人工发起的流程好好的,定时任务发起的流程就丢待办、丢通知。

客户端拿到结果后的引导也讲究:Wb.tip('The workflow started, click to view it.', null, f => Wb.openNormal({ url: 'my-flow', success(scope) { scope.grid1.reload(); } }))——提示条本身就是可点击的,点一下直接跳到"我的流程"页面并刷新列表。反馈与导航合二为一,比弹一个"操作成功"再让用户自己找入口高明得多。

五、把示例变成你的第一套审批流

把四个模块的要点串成一张落地清单,照着做就能搭出自己的第一条流程。清单里的每一步都对应本篇讲过的示例文件,做的时候把示例源码摆在旁边对照,比任何文档都直观:

  1. 画流程:在流程设计器里定义 leave.flw 的节点与流转规则(示例自带,可以先改再建);
  2. 建业务表:一张表 + 一个 flow_id 字段,其余全是业务字段;
  3. 写 start-form:表单控件 + init(flowId, type, record) 三态处理 + 服务端 afterAction 落库(insert 与 restarting 两个分支);
  4. 写 handle-form:自定义审批 UI + 五个 doXxx 动作调用 + _$data$_ 或 ajax 取数;
  5. 挂钩子:before/after-action 里写通知与日志;
  6. 配入口:菜单挂 start-form,或用 Wb.Workflow 在业务代码里自动发起。

三条实践忠告配套奉上:第一,业务表里的 flow_id 字段从第一天就要建索引,流程列表页的查询全靠它;第二,afterAction 里的落库逻辑要幂等——重试、重复回调在企业环境里是常态而不是异常;第三,流程定义文件的版本管理要像代码一样严肃,改 .flw 不留记录,三个月后没人说得清"为什么这一步要抄送总监"。另外,示例目录里的 reimburse(报销)流程与 leave 结构完全同构,读会了请假流程,报销流程就是免费的复习课——顺便观察它多出的金额字段与额度校验,看看同一套骨架如何承载不同的业务约束。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、一个流程应用的文件地图
  • 二、发起表单:一个模块服务三种视角
  • 三、审批表单:动作就是方法名
  • 四、手动启动:两行代码发起一个流程
  • 五、把示例变成你的第一套审批流
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档