基于jeecg-boot的flowable流程收回功能实现(全网首创功能)

更多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、首先前端功能

前端比较简单,只要在已办功能里增加收回菜单功能,同时调用后端代码来实现。

增加一个菜单按钮

增加一个收回任务函数

完整的代码如下:

javascript 复制代码
<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,具体代码如下:

java 复制代码
/**
	 *  发起人收回流程
	 *  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

java 复制代码
/**
     * 删除跳转的历史节点信息
     *
     * @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 复制代码
<?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、自定义业务与其它流程做分别处理

其它流程直接删除相关用户任务历史信息,保留初始发送,用户可以直接进行流程重新编辑发送。

而自定义业务则删除所有实例相关的任务历史信息,不保留任务相关信息,同时删除自定义业务发起时候的写入的关联表,以便用户可以再次发起流程。

相关推荐
morris1316 分钟前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
七星静香30 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员31 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU32 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie635 分钟前
在IDEA中使用Git
java·git
Elaine2023911 小时前
06 网络编程基础
java·网络
G丶AEOM1 小时前
分布式——BASE理论
java·分布式·八股
落落鱼20131 小时前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀1 小时前
LRU缓存算法
java·算法·缓存
镰刀出海1 小时前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试