本规范基于 warm-flow 工作流框架,适用于所有工作流相关的开发任务。warm-flow 是 dromara 社区开源的轻量级工作流引擎,与 Spring Boot 和若依框架无缝集成。
<!-- warm-flow核心依赖 -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
</dependency>
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
</dependency>
# application.yml
warm-flow:
enabled: true # 设置为 true 时,启用工作流功能
// 所有工作流相关类必须添加此注解
@ConditionalOnEnable
@Service / @Controller / @Component
public class WorkflowRelatedClass {
// 实现逻辑
}
@Data
@EqualsAndHashCode(callSuper = true)
public class TestLeaveBo extends BaseEntity {
private Long id;
private String leaveType;
private String startDate;
private String endDate;
private Integer leaveDays;
private String remark;
private String status; // 工作流状态字段(必需)
}
@Data
@ExcelIgnoreUnannotated
public class TestLeaveVo {
// 业务字段
private Long id;
private String leaveType;
private String startDate;
private String endDate;
private Integer leaveDays;
private String remark;
private String status;
// 🔴 基础字段(必须包含,避免MyBatis映射错误)
private Long createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private Long updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
⚠️ 重要提醒: 所有工作流业务 VO 类必须包含四个基础字段,否则会出现 Could not set property 'createBy' 错误。
@ConditionalOnEnable // 🔴 必须添加
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/workflow/业务模块")
public class WorkflowController extends BaseController {
private final IWorkflowService workflowService;
// 🔴 所有接口必须添加权限注解
@SaCheckPermission("workflow:业务模块:list")
@GetMapping("/list")
public TableDataInfo<WorkflowVo> list(WorkflowBo bo, PageQuery pageQuery) {
return workflowService.queryPageList(bo, pageQuery);
}
@SaCheckPermission("workflow:业务模块:add")
@PostMapping()
public R<WorkflowVo> add(@Validated(AddGroup.class) @RequestBody WorkflowBo bo) {
return R.ok(workflowService.insertByBo(bo));
}
@SaCheckPermission("workflow:业务模块:edit")
@PutMapping()
public R<WorkflowVo> edit(@Validated(EditGroup.class) @RequestBody WorkflowBo bo) {
return R.ok(workflowService.updateByBo(bo));
}
@SaCheckPermission("workflow:业务模块:remove")
@DeleteMapping("/{ids}")
public R<Void> remove(@PathVariable Long[] ids) {
return toAjax(workflowService.deleteWithValidByIds(List.of(ids)));
}
}
@ConditionalOnEnable // 🔴 必须添加
@Service
@RequiredArgsConstructor
public class WorkflowServiceImpl implements IWorkflowService {
private final WorkflowMapper baseMapper;
@Override
@Transactional(rollbackFor = Exception.class) // 🔴 重要操作需要事务
public WorkflowVo insertByBo(WorkflowBo bo) {
Workflow entity = BeanUtil.toBean(bo, Workflow.class);
// 🔴 新建时必须设置状态为草稿
entity.setStatus("draft");
validEntityBeforeSave(entity);
boolean flag = baseMapper.insert(entity) > 0;
if (flag) {
bo.setId(entity.getId());
return BeanUtil.toBean(entity, WorkflowVo.class);
}
return null;
}
// 其他CRUD方法...
}
-- 所有工作流业务表必须包含以下字段
CREATE TABLE workflow_business (
id BIGINT(20) NOT NULL PRIMARY KEY COMMENT '主键ID',
-- 业务字段
-- ...
-- 🔴 工作流状态字段(必需)
status VARCHAR(20) DEFAULT 'draft' COMMENT '流程状态',
-- 🔴 标准基础字段(必需)
tenant_id VARCHAR(20) DEFAULT '000000' COMMENT '租户编号',
create_dept BIGINT(20) DEFAULT NULL COMMENT '创建部门',
create_by BIGINT(20) DEFAULT NULL COMMENT '创建者',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_by BIGINT(20) DEFAULT NULL COMMENT '更新者',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT(1) DEFAULT 0 COMMENT '删除标志'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流业务表';
| 状态值 | 状态名称 | 说明 | 可用操作 |
|---|---|---|---|
| draft | 草稿 | 新建但未提交 | 编辑、删除、提交 |
| waiting | 待审批 | 流程进行中 | 查看、撤销 |
| complete | 已完成 | 流程正常结束 | 查看 |
| cancel | 已取消 | 申请人撤销 | 编辑、删除、重新提交 |
| back | 已退回 | 审批人退回 | 编辑、删除、重新提交 |
| invalid | 已作废 | 管理员作废 | 查看 |
| terminate | 已终止 | 流程异常终止 | 查看 |
<template>
<div class="p-2">
<!-- 搜索表单 -->
<transition
:enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave"
>
<div v-show="showSearch" class="search">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<!-- 搜索字段 -->
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">
搜索
</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
</transition>
<!-- 数据表格 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
v-hasPermi="['workflow:业务:add']"
type="primary"
plain
icon="Plus"
@click="handleAdd"
>
新增
</el-button>
</el-col>
</el-row>
</template>
<el-table v-loading="loading" border :data="dataList">
<el-table-column type="selection" width="55" align="center" />
<!-- 业务字段 -->
<!-- 🔴 工作流状态列(必需) -->
<el-table-column label="流程状态" align="center">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.status" />
</template>
</el-table-column>
<!-- 🔴 操作列(根据状态显示不同按钮) -->
<el-table-column label="操作" align="center">
<template #default="scope">
<el-row :gutter="10" class="mb8">
<el-col
:span="1.5"
v-if="['draft', 'cancel', 'back'].includes(scope.row.status)"
>
<el-button
type="primary"
size="small"
icon="Edit"
@click="handleUpdate(scope.row)"
>
修改
</el-button>
</el-col>
<el-col
:span="1.5"
v-if="['draft', 'cancel', 'back'].includes(scope.row.status)"
>
<el-button
type="primary"
size="small"
icon="Delete"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</el-col>
</el-row>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
size="small"
icon="View"
@click="handleView(scope.row)"
>
查看
</el-button>
</el-col>
<el-col :span="1.5" v-if="scope.row.status === 'waiting'">
<el-button
size="small"
type="primary"
icon="Notification"
@click="handleCancel(scope.row.id)"
>
撤销
</el-button>
</el-col>
</el-row>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { list业务, del业务 } from '@/api/workflow/业务模块';
import { cancelProcessApply } from '@/api/workflow/instance';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
// 组件逻辑...
</script>
<template>
<div class="p-2">
<!-- 🔴 1. 审批按钮组件(必需) -->
<el-card shadow="never">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.id"
:status="form.status"
:pageType="routeParams.type"
/>
</el-card>
<!-- 2. 业务表单 -->
<el-card shadow="never" style="height: 78vh; overflow-y: auto">
<el-form
ref="formRef"
v-loading="loading"
:disabled="routeParams.type === 'view'"
:model="form"
:rules="rules"
label-width="80px"
>
<!-- 业务字段 -->
</el-form>
</el-card>
<!-- 🔴 3. 工作流组件(必需) -->
<submitVerify
ref="submitVerifyRef"
:task-variables="taskVariables"
@submit-callback="submitCallback"
/>
<approvalRecord ref="approvalRecordRef" />
<!-- 4. 流程选择对话框 -->
<el-dialog v-model="dialogVisible.visible" title="流程定义" width="500">
<el-select v-model="flowCode" placeholder="Select">
<el-option
v-for="item in flowCodeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="submitFlow()">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
// 🔴 必需的组件引入
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
// 🔴 必需的API引入
import { add业务, get业务, update业务 } from '@/api/workflow/业务模块';
import { startWorkFlow } from '@/api/workflow/task';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 🔴 必需的ref引用
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
// 🔴 必需的变量定义
const buttonLoading = ref(false);
const loading = ref(true);
const routeParams = ref<Record<string, any>>({});
const taskVariables = ref<Record<string, any>>({});
// 🔴 流程定义选项(根据实际业务配置)
const flowCodeOptions = [{ value: 'workflow_code', label: '工作流名称' }];
// 🔴 必需的事件处理函数
const submitForm = async (status: string) => {
try {
// 1. 表单验证
await formRef.value?.validate();
// 2. 保存业务数据
buttonLoading.value = true;
let res;
if (form.value.id) {
res = await update业务(form.value);
} else {
res = await add业务(form.value);
}
form.value = res.data;
// 3. 处理不同状态
if (status === 'draft') {
proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
return;
}
// 4. 启动工作流
if (!flowCode.value) {
dialogVisible.visible = true;
return;
}
await handleStartWorkFlow(res.data);
} catch (error) {
console.error('提交失败:', error);
} finally {
buttonLoading.value = false;
}
};
const handleStartWorkFlow = async (data) => {
const submitFormData = {
flowCode: flowCode.value,
businessId: data.id,
variables: taskVariables.value,
};
const resp = await startWorkFlow(submitFormData);
submitVerifyRef.value.openDialog(resp.data.taskId);
};
const approvalVerifyOpen = () => {
submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.id);
};
const submitCallback = async () => {
await proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
// 🔴 页面初始化
onMounted(() => {
nextTick(async () => {
routeParams.value = proxy.$route.query;
// 初始化逻辑
if (['update', 'view', 'approval'].includes(routeParams.value.type)) {
getInfo();
}
});
});
</script>
// api/workflow/业务模块/types.ts
export interface WorkflowForm extends BaseEntity {
id?: string | number;
// 业务字段
status?: string; // 🔴 工作流状态字段
}
export interface WorkflowVO extends BaseEntity {
id?: string | number;
// 业务字段
status?: string; // 🔴 工作流状态字段
}
export interface WorkflowQuery extends PageQuery {
// 查询字段
status?: string;
}
// api/workflow/业务模块/index.ts
import request from '@/utils/request';
import { WorkflowQuery, WorkflowForm, WorkflowVO } from './types';
export const list业务 = (query: WorkflowQuery): AxiosPromise<WorkflowVO[]> => {
return request({
url: '/workflow/业务模块/list',
method: 'get',
params: query,
});
};
export const get业务 = (id: string | number): AxiosPromise<WorkflowVO> => {
return request({
url: `/workflow/业务模块/${id}`,
method: 'get',
});
};
export const add业务 = (data: WorkflowForm): AxiosPromise<WorkflowVO> => {
return request({
url: '/workflow/业务模块',
method: 'post',
data: data,
});
};
export const update业务 = (data: WorkflowForm): AxiosPromise<WorkflowVO> => {
return request({
url: '/workflow/业务模块',
method: 'put',
data: data,
});
};
export const del业务 = (id: string | number | Array<string | number>): AxiosPromise<void> => {
return request({
url: `/workflow/业务模块/${id}`,
method: 'delete',
});
};
// router配置
{
path: '/workflow/业务模块',
component: Layout,
redirect: '/workflow/业务模块/index',
name: 'Workflow业务模块',
meta: {
title: '业务模块管理',
icon: 'workflow'
},
children: [
{
path: 'index',
component: () => import('@/views/workflow/业务模块/index.vue'),
name: 'Workflow业务模块List',
meta: { title: '业务模块列表', activeMenu: '/workflow/业务模块/index' }
},
{
path: 'edit',
component: () => import('@/views/workflow/业务模块/edit.vue'),
name: 'Workflow业务模块Edit',
hidden: true,
meta: { title: '业务模块编辑', activeMenu: '/workflow/业务模块/index' }
}
]
}
// 启动工作流标准代码
const handleStartWorkFlow = async (businessData: any) => {
const submitFormData = {
flowCode: 'your_flow_code', // 🔴 流程定义编码
businessId: businessData.id, // 🔴 业务数据ID
variables: {
// 🔴 流程变量
// 条件判断变量
amount: businessData.amount,
// 会签用户列表
userList: ['1', '3', '4'],
// 其他业务变量
...otherVariables,
},
};
try {
const resp = await startWorkFlow(submitFormData);
// 打开提交确认对话框
submitVerifyRef.value.openDialog(resp.data.taskId);
} catch (error) {
console.error('启动工作流失败:', error);
}
};
// 在 submitVerify 组件中自动处理
// 支持的操作:
// - 提交审批
// - 委托 (delegateTask)
// - 转办 (transferTask)
// - 加签 (addSignature)
// - 减签 (reductionSignature)
// - 终止 (terminationTask)
// - 退回 (backProcess)
🔴 退回残留任务清理(强制性,2026-10-15):
backProcess 在 skip(REJECT) 后必须清理同实例下非目标节点残留 flow_task/flow_user(cleanResidualTasksAfterBack)startWorkFlow 需自愈多任务脏数据并按实例状态解析正确 taskId(healResidualTasksOnRestart)// 申请人撤销流程
const handleCancelProcessApply = async (businessId: string) => {
await proxy?.$modal.confirm('是否确认撤销当前单据?');
const data = {
businessId: businessId,
message: '申请人撤销流程!',
};
await cancelProcessApply(data);
proxy?.$modal.msgSuccess('撤销成功');
getList(); // 刷新列表
};
// 流程编码:simple_approval
// 特点:线性审批,一步一步进行
const taskVariables = {
// 基本业务数据即可
businessData: formData,
};
// 流程编码:exclusive_gateway
// 特点:根据条件选择不同审批分支
const taskVariables = {
amount: formData.amount, // 金额条件
urgentLevel: formData.urgent, // 紧急程度
applicantLevel: currentUser.level, // 申请人级别
};
// 流程编码:parallel_gateway
// 特点:多个分支并行执行
const taskVariables = {
parallelBranches: ['finance', 'hr', 'legal'], // 并行分支
businessData: formData,
};
// 流程编码:countersign
// 特点:多人会签,所有人都需要审批
const taskVariables = {
userList: ['1', '3', '4', '5'], // 会签用户列表
signType: 'all', // all-全部同意 any-任一同意
businessData: formData,
};
在系统管理 → 菜单管理中配置:
父菜单:工作流管理
├── 业务模块管理
├── 菜单权限:workflow:业务模块:list
├── 按钮权限:
├── 新增:workflow:业务模块:add
├── 修改:workflow:业务模块:edit
├── 删除:workflow:业务模块:remove
├── 查询:workflow:业务模块:query
└── 导出:workflow:业务模块:export
工作流通用权限:
├── 任务管理:workflow:task:list
├── 实例管理:workflow:instance:list
├── 定义管理:workflow:definition:list
└── 审批权限:workflow:approval:handle
问题: Could not set property 'createBy'
解决: 确保 VO 类包含完整的基础字段:
// 🔴 必须包含的四个基础字段
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
问题: 工作流相关功能无法使用
检查清单:
warm-flow.enabled=true@ConditionalOnEnable@SaCheckPermission常见原因:
解决: 统一使用标准格式
<el-date-picker
v-model="dateRange"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
/>
规范版本: v1.0
最后更新: 2025-01-09
适用范围: 所有基于 warm-flow 的工作流开发任务