更多nbcio-boot功能请看演示系统
gitee源代码地址
后端代码: https://gitee.com/nbacheng/nbcio-boot
前端代码:https://gitee.com/nbacheng/nbcio-vue.git
在线演示(包括H5) : http://122.227.135.243:9888
对于之前的flowable流程,之前有撤回,拒绝,退回等功能,但都不能满足发起人对于流程收回的功能,发起人收回后可以重新进行流程发起,同时能够支持自定义业务的收回功能。
从目前开源项目与全网的资料看都没有找到相关资料,所以只能自己来写相应的功能,满足用户的需求了。
版权声明:大家要是单独用我的代码,请注明作者。
1、首先前端功能
前端比较简单,只要在已办功能里增加收回菜单功能,同时调用后端代码来实现。
增加一个菜单按钮
增加一个收回任务函数
完整的代码如下:
<template> <a-card :bordered="false"> <!-- 查询区域 --> <div class="table-page-search-wrapper"> <a-form layout="inline" @keyup.enter.native="handleQuery"> <a-row :gutter="24"> <a-col :md="6" :sm="8"> <a-form-item label="流程名称"> <a-input placeholder="请输入流程名称" v-model="queryParams.procDefName"></a-input> </a-form-item> </a-col> <a-col :md="8" :sm="24"> <a-form-item label="接收日期"> <a-date-picker v-model="queryParams.createTime" style="width: 100%" placeholder="请输入接收日期"/> </a-form-item> </a-col> <a-col :md="6" :sm="8"> <span style="float: left;overflow: hidden;" class="table-page-search-submitButtons"> <a-button type="primary" @click="handleQuery" icon="search">查询</a-button> <a-button type="primary" @click="searchReset" icon="reload" style="margin-left: 8px">重置</a-button> </span> </a-col> </a-row> </a-form> </div> <!-- 查询区域-END --> <!-- 操作按钮区域 --> <div class="table-operator"> <a-button type="primary" icon="download" @click="handleExportXls('待办任务')">导出</a-button> <a-dropdown v-if="selectedRowKeys.length > 0"> <a-menu slot="overlay"> <a-menu-item key="1" @click="batchDel"><a-icon type="delete"/>删除</a-menu-item> </a-menu> <a-button style="margin-left: 8px"> 批量操作 <a-icon type="down" /></a-button> </a-dropdown> </div> <!-- table区域-begin --> <div> <div class="ant-alert ant-alert-info" style="margin-bottom: 16px;"> <i class="anticon anticon-info-circle ant-alert-icon"></i> 已选择 <a style="font-weight: 600">{{ selectedRowKeys.length }}</a>项 <a style="margin-left: 24px" @click="onClearSelected">清空</a> </div> <a-table ref="table" size="middle" :scroll="{x:true}" bordered rowKey="procInsId" :columns="columns" :dataSource="dataSource" :pagination="ipagination" :loading="loading" :rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}" class="j-table-force-nowrap" @change="handleTableChange"> <template slot="procDefVersion" slot-scope="text, record, index"> <el-tag size="medium" >V{{ record.procDefVersion }}</el-tag> </template> <template slot="startUserName" slot-scope="text, record, index"> <label>{{record.startUserName}} <el-tag type="info" size="mini">{{record.startDeptName}}</el-tag></label> </template> <template slot="htmlSlot" slot-scope="text"> <div v-html="text"></div> </template> <template slot="imgSlot" slot-scope="text"> <span v-if="!text" style="font-size: 12px;font-style: italic;">无图片</span> <img v-else :src="getImgView(text)" height="25px" alt="" style="max-width:80px;font-size: 12px;font-style: italic;"/> </template> <template slot="fileSlot" slot-scope="text"> <span v-if="!text" style="font-size: 12px;font-style: italic;">无文件</span> <a-button v-else :ghost="true" type="primary" icon="download" size="small" @click="downloadFile(text)"> 下载 </a-button> </template> <span slot="action" slot-scope="text, record"> <a-dropdown> <a class="ant-dropdown-link">更多 <a-icon type="down" /></a> <a-menu slot="overlay"> <a-menu-item> <a @click="handleFlowRecord(record)">流转记录</a> </a-menu-item> <a-menu-item> <a @click="handleRecall(record)"> 收回</a> </a-menu-item> <a-menu-item> <a @click="handleRevoke(record)"> 撤回</a> </a-menu-item> </a-menu> </a-dropdown> </span> </a-table> </div> </a-card> </template> <script> import '@/assets/less/TableExpand.less' import { mixinDevice } from '@/utils/mixin' import { JeecgListMixin } from '@/mixins/JeecgListMixin' import { finishedList, finishedListNew, getDeployment, delDeployment, addDeployment, updateDeployment, exportDeployment, revokeProcess, recallProcess } from "@/views/flowable/api/finished"; import moment from 'moment'; export default { name: "finishedIndex", mixins:[JeecgListMixin, mixinDevice], components: { }, data() { return { // 表头 columns: [ { title: '#', dataIndex: '', key:'rowIndex', width:60, align:"center", customRender:function (t,r,index) { return parseInt(index)+1; } }, { title:'任务编号', align:"center", dataIndex: 'procInsId', }, { title:'流程名称', align:"center", dataIndex: 'procDefName', }, { title:'任务节点', align:"center", dataIndex: 'taskName', }, { title:'流程类别', align:"center", dataIndex: 'category' }, { title:'流程版本', align:"center", dataIndex: 'procDefVersion', scopedSlots: { customRender: 'procDefVersion' } }, { title:'业务主键', align:"center", dataIndex: 'businessKey' }, { title:'流程发起人', align:"center", dataIndex: 'startUserName', scopedSlots: { customRender: 'startUserName' } }, { title:'接收时间', align:"center", dataIndex: 'createTime' }, { title:'审批时间', align:"center", dataIndex: 'finishTime' }, { title:'耗时', align:"center", dataIndex: 'duration' }, { title: '操作', dataIndex: 'action', align:"center", fixed:"right", width:147, scopedSlots: { customRender: 'action' } } ], // 查询参数 queryParams: { pageNo: 1, pageSize: 10, name: null, category: null, key: null, tenantId: null, deployTime: null, derivedFrom: null, derivedFromRoot: null, parentDeploymentId: null, engineVersion: null, procDefName: null, createTime: null }, url: { list: "/flowable/task/finishedListNew", deleteBatch: "/flowable/task/deleteBatch", exportXlsUrl: "/flowable/task/finishedExportXls", }, dataSource: [], //表格数据源 /* 表格分页参数 */ ipagination:{ current: 1, pageSize: 10, pageSizeOptions: ['10', '20', '30'], showTotal: (total, range) => { return range[0] + "-" + range[1] + " 共" + total + "条" }, showQuickJumper: true, showSizeChanger: true, total: 0 }, // 遮罩层 loading: true, // 选中数组 ids: [], // 非单个禁用 single: true, // 非多个禁用 multiple: true, // 显示搜索条件 showSearch: true, // 总条数 total: 0, // 已办任务列表数据 finishedList: [], // 弹出层标题 title: "", // 是否显示弹出层 open: false, src: "", // 查询参数 queryParams: { pageNo: 1, pageSize: 10, name: null, category: null, key: null, tenantId: null, deployTime: null, derivedFrom: null, derivedFromRoot: null, parentDeploymentId: null, engineVersion: null }, // 表单参数 form: {}, // 表单校验 rules: { } }; }, created() { this.getSuperFieldList(); //this.getList(); }, methods: { /** 查询流程定义列表 */ getList() { this.loading = true; finishedListNew(this.queryParams).then(response => { if(response.success) { this.dataSource = response.result.records; this.total = response.result.total; this.ipagination.total = response.result.total; this.loading = false; } else { this.$message.error(response.message) this.loading = false; } }); }, // 取消按钮 cancel() { this.open = false; this.reset(); }, // 表单重置 reset() { this.form = { id: null, name: null, category: null, key: null, tenantId: null, deployTime: null, derivedFrom: null, derivedFromRoot: null, parentDeploymentId: null, engineVersion: null }; this.resetForm("form"); }, setIcon(val){ if (val){ return "el-icon-check"; }else { return "el-icon-time"; } }, setColor(val){ if (val){ return "#2bc418"; }else { return "#b3bdbb"; } }, initDictConfig(){ }, getSuperFieldList(){ let fieldList=[]; fieldList.push({type:'string',value:'procInsId',text:'任务编号'}) fieldList.push({type:'string',value:'procDefName',text:'流程名称'}) fieldList.push({type:'string',value:'taskName',text:'任务节点'}) fieldList.push({type:'string',value:'category',text:'流程类别'}) fieldList.push({type:'string',value: 'procDefVersion',text:'流程版本'}) fieldList.push({type:'string',value: 'businessKey',text:'业务主键'}) fieldList.push({type:'string',value:'startUserName',text:'流程发起人'}) fieldList.push({type:'datetime',value:'createTime',text:'接收时间'}) fieldList.push({type:'datetime',value:'finishTime',text:'审批时间'}) fieldList.push({type:'string',value:'duration',text:'耗时'}) this.superFieldList = fieldList }, /** 搜索按钮操作 */ handleQuery() { this.queryParams.pageNum = 1; this.getList(); }, /** 重置按钮操作 */ resetQuery() { this.resetForm("queryForm"); this.handleQuery(); }, // 多选框选中数据 handleSelectionChange(selection) { this.ids = selection.map(item => item.id) this.single = selection.length!==1 this.multiple = !selection.length }, /** 新增按钮操作 */ handleAdd() { this.reset(); this.open = true; this.title = "添加流程定义"; }, /** 流程流转记录 */ handleFlowRecord(row){ this.$router.push({ path: '/flowable/task/record/index', query: { procInsId: row.procInsId, deployId: row.deployId, taskId: row.taskId, businessKey: row.businessKey, category: row.category, finished: false }}) }, /** 撤回任务 */ handleRevoke(row){ const params = { instanceId: row.procInsId, dataId: row.businessKey } revokeProcess(params).then( res => { this.$message.success(res.message); this.getList(); }); }, /** 收回任务 */ handleRecall(row){ const params = { instanceId: row.procInsId, dataId: row.businessKey } recallProcess(params).then( res => { this.$message.success(res.message); this.getList(); }); }, /** 提交按钮 */ submitForm() { this.$refs["form"].validate(valid => { if (valid) { if (this.form.id != null) { updateDeployment(this.form).then(response => { this.$message.success("修改成功"); this.open = false; this.getList(); }); } else { addDeployment(this.form).then(response => { this.$message.success("新增成功"); this.open = false; this.getList(); }); } } }); }, /** 删除按钮操作 */ handleDelete(row) { const ids = row.id || this.ids; const dataid = row.businessKey; this.$confirm('是否确认删除流程定义编号为"' + ids + '"的数据项?', "警告", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }).then(function() { return delDeployment(ids,dataid); }).then(() => { this.getList(); this.$message.success("删除成功"); }) }, /** 导出按钮操作 */ handleExport() { const queryParams = this.queryParams; this.$confirm('是否确认导出所有流程定义数据项?', "警告", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }).then(function() { return exportDeployment(queryParams); }).then(response => { this.download(response.message); }) } } }; </script>
2、后端代码
2.1 增加一个收回流程功能recallProcess,具体代码如下:
/** * 发起人收回流程 * add by nbacheng * * @param FlowTaskVo taskVo * * @return */ @Override @Transactional public Result recallProcess(FlowTaskVo flowTaskVo) { // 当前任务 listtask List<Task> listtask = taskService.createTaskQuery().processInstanceId(flowTaskVo.getInstanceId()).active().list(); if (listtask == null) { throw new CustomException("流程未启动或已执行完成,无法收回"); } if (taskService.createTaskQuery().taskId(listtask.get(0).getId()).singleResult().isSuspended()) { throw new CustomException("任务处于挂起状态"); } List<Task> procInsId = taskService.createNativeTaskQuery().sql("select * from ACT_HI_TASKINST where PROC_INST_ID_ = #{procInsId} ORDER BY START_TIME_ desc").parameter("procInsId", flowTaskVo.getInstanceId()).list(); SysUser loginUser = iFlowThirdService.getLoginUser(); String processInstanceId = listtask.get(0).getProcessInstanceId(); // 获取所有历史任务(按创建时间升序) List<HistoricTaskInstance> hisTaskList = historyService.createHistoricTaskInstanceQuery() .processInstanceId(processInstanceId).orderByTaskCreateTime() .asc() .list(); if (CollectionUtil.isEmpty(hisTaskList) || hisTaskList.size() < 2) { log.error("当前流程 【{}】 审批节点 【{}】正在初始节点无法收回", processInstanceId, listtask.get(0).getName()); throw new FlowableException(String.format("当前流程 【%s】 审批节点【%s】正在初始节点无法收回", processInstanceId, listtask.get(0).getName())); } // 第一个任务 HistoricTaskInstance startTask = hisTaskList.get(0); //若操作用户不是发起人,不能收回 if(!StringUtils.equalsAnyIgnoreCase(loginUser.getUsername(), startTask.getAssignee())) { throw new CustomException("操作用户不是发起人,不能收回"); } // 当前任务 HistoricTaskInstance currentTask = hisTaskList.get(hisTaskList.size() - 1); BpmnModel bpmnModel = repositoryService.getBpmnModel(listtask.get(0).getProcessDefinitionId()); // 获取第一个活动节点 FlowNode startFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(startTask.getTaskDefinitionKey()); // 获取当前活动节点 FlowNode currentFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(currentTask.getTaskDefinitionKey()); // 临时保存当前活动的原始方向 List<SequenceFlow> originalSequenceFlowList = new ArrayList<>(currentFlowNode.getOutgoingFlows()); // 清理活动方向 currentFlowNode.getOutgoingFlows().clear(); // 建立新方向 SequenceFlow newSequenceFlow = new SequenceFlow(); newSequenceFlow.setId("newSequenceFlowId"); newSequenceFlow.setSourceFlowElement(currentFlowNode); newSequenceFlow.setTargetFlowElement(startFlowNode); List<SequenceFlow> newSequenceFlowList = new ArrayList<>(); newSequenceFlowList.add(newSequenceFlow); // 当前节点指向新的方向 currentFlowNode.setOutgoingFlows(newSequenceFlowList); // 完成当前任务 for(Task task : listtask) { taskService.addComment(task.getId(), listtask.get(0).getProcessInstanceId(),FlowComment.RECALL.getType(), "发起人收回"); taskService.setAssignee(task.getId(), startTask.getAssignee()); taskService.complete(task.getId()); } // 重新查询当前任务 Task nextTask = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult(); if (ObjectUtil.isNotNull(nextTask)) { taskService.setAssignee(nextTask.getId(), startTask.getAssignee()); //taskService.complete(nextTask.getId());;//跳过流程发起节点 } //自定义业务处理id String dataId = flowTaskVo.getDataId(); // 删除运行和历史的节点信息 this.deleteActivity(procInsId.get(1).getTaskDefinitionKey(), flowTaskVo.getInstanceId(), dataId); // 恢复原始方向 currentFlowNode.setOutgoingFlows(originalSequenceFlowList); //自定义业务处理 if(StrUtil.isNotBlank(flowTaskVo.getDataId()) && !Objects.equals(flowTaskVo.getDataId(), "null")){ //如果保存数据前未调用必调的FlowCommonService.initActBusiness方法,就会有问题 FlowMyBusiness business = flowMyBusinessService.getByDataId(dataId); //删除自定义业务任务关联表,以便可以重新发起流程 if (business != null) { flowMyBusinessService.removeById(business); } } return Result.OK("发起人收回成功"); }
2.2 调用 删除历史节点信息deleteActivity
/** * 删除跳转的历史节点信息 * * @param disActivityId 跳转的节点id * @param processInstanceId 流程实例id * @param dataId 自定义业务id */ protected void deleteActivity(String disActivityId, String processInstanceId, String dataId) { List<ActivityInstance> disActivities = flowTaskMapper .queryActivityInstance(disActivityId, processInstanceId, null); //删除运行时和历史节点信息 if (CollectionUtils.isNotEmpty(disActivities)) { ActivityInstance activityInstance = disActivities.get(0); List<ActivityInstance> datas = flowTaskMapper .queryActivityInstance(disActivityId, processInstanceId, activityInstance.getEndTime()); //datas.remove(0); //保留流程发起节点信息 List<String> runActivityIds = new ArrayList<>(); if (CollectionUtils.isNotEmpty(datas)) { datas.forEach(ai -> runActivityIds.add(ai.getId())); flowTaskMapper.deleteRunActinstsByIds(runActivityIds); flowTaskMapper.deleteHisActinstsByIds(runActivityIds); } if(dataId != null) {//对于自定义业务, 删除所有相关流程信息 //flowTaskMapper.deleteAllHisAndRun(processInstanceId); //根据流程实例id 删除去ACT_RU_*与ACT_HI_*流程实例数据 ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); if (null != processInstance) { runtimeService.deleteProcessInstance(processInstanceId, "流程实例删除"); historyService.deleteHistoricProcessInstance(processInstanceId); } } } }
2.3 FlowTaskMapper.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.nbcio.modules.flowable.mapper.FlowTaskMapper"> <select id="queryActivityInstance" resultType="org.flowable.engine.impl.persistence.entity.ActivityInstanceEntityImpl"> select t.* from act_ru_actinst t <where> <if test="processInstanceId !=null and processInstanceId != ''" > t.PROC_INST_ID_=#{processInstanceId} and ACT_TYPE_ = 'userTask' and END_TIME_ is not null </if> </where> order by t.END_TIME_ ASC </select> <delete id="deleteRunActinstsByIds" parameterType="java.util.List"> delete from act_ru_actinst where ID_ in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> </delete> <delete id="deleteHisActinstsByIds" parameterType="java.util.List"> delete from act_hi_actinst where ID_ in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> </delete> <delete id="deleteAllHisAndRun" parameterType="String"> delete from act_ru_actinst where proc_inst_id_ = #{processInstanceId}; delete from act_ru_identitylink where proc_inst_id_ = #{processInstanceId}; delete from act_ru_task where proc_inst_id_ = #{processInstanceId}; delete from act_ru_variable where proc_inst_id_ = #{processInstanceId}; delete from act_ru_execution where proc_inst_id_ = #{processInstanceId}; delete from act_hi_actinst where proc_inst_id_ = #{processInstanceId}; delete from act_hi_comment where proc_inst_id_ = #{processInstanceId}; delete from act_hi_identitylink where proc_inst_id_ = #{processInstanceId}; delete from act_hi_procinst where proc_inst_id_ = #{processInstanceId}; delete from act_hi_taskinst where proc_inst_id_ = #{processInstanceId}; delete from act_hi_varinst where proc_inst_id_ = #{processInstanceId}; </delete> </mapper>
3、自定义业务与其它流程做分别处理
其它流程直接删除相关用户任务历史信息,保留初始发送,用户可以直接进行流程重新编辑发送。
而自定义业务则删除所有实例相关的任务历史信息,不保留任务相关信息,同时删除自定义业务发起时候的写入的关联表,以便用户可以再次发起流程。