一、项目概述


1.1 项目背景

SAQ-Flow-Backend 是一个基于 Spring Boot 开发的轻量级工作流后端服务。该项目起源于2021年我们承接的某区政府政务一体化办公平台信创改造任务。该平台下存在大量基于 .NET 框架开发的旧业务系统,这些系统多建于2010年左右,以实现定制化审批流程为核心,普遍存在流程结构单一、审批链路呈直线型、缺乏分支逻辑等特点。

在改造过程中,我们发现这些老系统通常借助大量枚举表和状态表来实现简单的流程控制,不仅开发效率低下,维护也极为不便。因此,我们迫切需要一款简洁、灵活的工作流引擎,将业务流程与控制逻辑分离,使开发人员能更专注于业务功能与前端交互,而不是重复编写流程状态管理的代码。

SAQ-Flow-Backend 由此应运而生。它虽设计简约,却有效解决了传统审批系统中流程硬编码、扩展困难等痛点。自投入使用以来,本系统持续为多个机关单位的定制化审批业务提供稳定支持,尽管未再做重大更新,仍以出色的适用性和可靠性,默默支撑着众多政务应用的日常运转,堪称“短小精悍”的典型代表。

1.2 技术栈

1.3 核心功能

  • 流程管理: 流程定义、节点配置、条件分支
  • 任务处理: 任务创建、分配、流转、退回、延期等操作
  • 权限控制: 基于角色的访问控制,支持多级权限管理
  • 监控统计: 流程执行监控、任务统计分析
  • 通知提醒: 任务到期提醒、状态变更通知

二、系统架构


2.1 整体架构

2.2 分层架构说明

API层 (Controller Layer)

负责接收HTTP请求,参数验证,调用业务服务,返回响应结果。主要包括:

  • TaskController - 任务相关接口
  • FlowController - 流程相关接口
  • UserController - 用户相关接口

业务逻辑层 (Service Layer)

实现核心业务逻辑,事务管理,业务规则验证。主要包括:

  • TaskService - 任务业务逻辑
  • FlowService - 流程业务逻辑
  • UserService - 用户业务逻辑

数据访问层 (DAO Layer)

数据持久化操作,SQL映射,数据库交互。基于MyBatis实现:

  • TaskMapper - 任务数据访问
  • FlowMapper - 流程数据访问
  • NodeMapper - 节点数据访问

数据库层 (Database Layer)

数据存储,包括核心业务表和系统配置表:

  • 流程相关表: FLOW, NODE, NODE_RELATION
  • 任务相关表: TASK, PROCINST
  • 用户相关表: USER, NODE_ROLE

三、 核心实体设计


3.1 主要实体类

Flow (流程定义)

  • id: Long - 主键ID
  • flowCode: String - 流程编码
  • flowName: String - 流程名称
  • flowDesc: String - 流程描述
  • version: String - 版本号
  • status: Integer - 状态
  • delFlag: Integer - 删除标志
package com.beyondbit.ias.flow.api.entity;

import lombok.Data;

import java.math.BigDecimal;
@Data
public class Flow {
    private String id;

    private String name;

    private String code;

    private BigDecimal version;

    private String description;

    private Integer isRun;

    private Integer delFlag;

}

Node (流程节点)

  • id: Long - 主键ID
  • nodeCode: String - 节点编码
  • nodeName: String - 节点名称
  • nodeType: NodeType - 节点类型
  • flowCode: String - 所属流程
  • orderNum: Integer - 排序号
  • delFlag: Integer - 删除标志
package com.beyondbit.ias.flow.api.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class Node {
    private String id;

    private String flowCode;

    private String nodeName;

    private String nodeAliasname;

    private String nodeCode;

    private String nodeType;

    private Integer delFlag;

    private Integer limitTime;

    private String limitTimeUnit;

    @JsonProperty
    private String lightColor;
    //时间单位跟limittimeUnit一致(工作日/自然日)
    @JsonProperty
    private Integer spendDay;

    private String routeUrl;
//    @JsonProperty
//    private String[] idList;
    //1:不需要部门保持,2:需要部门保持,即:给与的unitcode需要添加到task表中的opertor_unit_code
    private int keepType;

}

NodeRelation (流程节点关联表--流程图的联线)

  • id: Long - 主键ID
  • nodeCode: String - 当前节点code
  • nextNodeCode: String - 下一节点code
  • backNodeCode: NodeType - 退回节点code
  • delFlag: Integer - 删除标志
package com.beyondbit.ias.flow.api.entity;

import lombok.Data;

@Data
public class NodeRelation {
    private String id;

    private String nodeCode;

    private String nextNodeCode;

    private String backNodeCode;

    private Integer delFlag;


}

NodeRole(流程节点绑定的任务接收人角色)

  • id: Long - 主键ID
  • nodeCode: String - 当前节点code
  • roleColumn: String - 当前任务接收人角色集合(多个角色分号隔开)
  • delFlag: Integer - 删除标志
package com.beyondbit.ias.flow.api.entity;

import lombok.Data;

@Data
public class NodeRole {
    private String id;

    private String nodeCode;

    private String roleColumn;

    private Integer delFlag;

}

Task (任务)

  • id: Long - 主键ID
  • taskId: String - 任务ID
  • procinstId: String - 流程实例ID
  • nodeCode: String - 节点编码
  • assignee: String - 处理人
  • status: TaskStatus - 任务状态
  • createTime: Date - 创建时间
  • finishTime: Date - 完成时间
package com.beyondbit.ias.flow.api.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

import java.util.Date;
import java.util.List;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Task {
    private String id;

    @JsonProperty
    private List<String> ids;

    private String procinstId;

    private String nodeCode;

    private String nodeName;

    private String originTaskId;
    @JsonProperty
    private List<String> originTaskIds;

    private String operationDesc;

    private String operatorShowText;

    private Date startTime;

    private Date endTime;

    private String status;

    private Integer isCurrent;

    private String opinion;

    private String operatorRoleCode;

    private String operatorRoleName;

    private String operatorUnitCode;

    private String operatorUnitName;

    private String operatorUseruid;

    private String operatorUsername;

    private String createUserUid;
    private String createUserName;

    private Integer delFlag;

    private String operationName;

    @JsonProperty
    private List<String> statusList;
    @JsonProperty
    private String businessId;
    @JsonProperty
    private Node refNode;



}

3.2 枚举类型定义

NodeType (节点类型)

  • START: 开始节点
  • FLOW: 普通流程节点
  • COUNTERSIGN: 会签节点
  • PAUSE: 暂停节点
  • END: 结束节点

TaskStatus (任务状态)

  • WAIT: 等待处理
  • READY: 准备就绪
  • FINISH: 已完成

OperationName (操作类型)

  • Submit: 提交
  • Back: 退回
  • WithDraw: 撤回
  • Delay: 延期
  • Ending: 结束

四、 数据库设计


4.1 数据库ER图

4.2 核心数据表

4.3 表关系说明

主要关系:
  • FLOW → NODE: 一对多关系,一个流程包含多个节点
  • NODE → NODE_RELATION: 一对多关系,一个节点可以有多个后续节点
  • FLOW → PROCINST: 一对多关系,一个流程定义可以有多个实例
  • PROCINST → TASK: 一对多关系,一个流程实例包含多个任务
  • NODE → TASK: 一对多关系,一个节点可以生成多个任务
  • NODE → NODE_ROLE: 一对多关系,一个节点可以分配给多个角色

五、API接口设计


5.1 API流程图

5.2 任务管理接口

接口实现Controller


/**
 * 流程控制器
 */
@Slf4j
@CrossOrigin
@RestController
@RequestMapping("/task")
public class TaskController extends BaseApiController implements com.beyondbit.ias.flow.schemas.TaskService {
    @Resource
    private TaskService taskService;


    /**
     * 启动
     * @param params
     * @return
     */
    @Override
    public JsonResultMessage launch(LaunchParamsModel params) {
        LaunchParams launchParams = LaunchParamsModelConverter.to(params);
        String returnStr = this.taskService.launch(launchParams);
        if (returnStr == "-1") {
            return fail("没有开始节点");
        } else if(returnStr == "-2"){
            return fail("新增流程实例失败");
        } if (returnStr == "-3"){
            return fail("新增任务失败");
        }else {
            return success("启动成功!", returnStr);
        }
    }

    /**
     * 流转
     * @author gsj
     * @param params
     * @return
     */
    @Override
    public JsonResultMessage flowing(FlowingParamsModel params) {
        FlowingParams flowingParams = FlowingParamsModelConverter.to(params);
        String taskId = this.taskService.flowing(flowingParams);
        if (!taskId.equals("")) return success("流转成功!",taskId);
        else return fail("流转失败");
    }

    /**
     * 退回
     * @author gsj
     * @param params
     * @return
     */
    @Override
    public JsonResultMessage back(FlowingParamsModel params) {
        FlowingParams flowingParams = FlowingParamsModelConverter.to(params);
        String backToTaskId = this.taskService.back(flowingParams);
        return success("退回成功!",backToTaskId);
    }

    @Override
    public JsonResultMessage delay(Map reqParams) {
        return null;
    }


    @Override
    public JsonResultMessage ending(FlowingParamsModel params) {
        FlowingParams flowingParams = FlowingParamsModelConverter.to(params);
        String curTaskId = taskService.ending(flowingParams);
        if (!curTaskId.equals("")) return success("立即结束流程成功",curTaskId);
        else return fail("立即结束流程失败");
    }

    /**
     * 查询业务件的任务列表
     * @author gsj
     * @param params
     * @return
     */
    @Override
    public JsonResultMessage<List<TaskModel>> queryTaskList(TaskQueryParamsModel params) {
        TaskQueryParams taskQueryParams = TaskQueryParamsModelConverter.to(params);
        List<Task> resList = this.taskService.queryTaskList(taskQueryParams);
        if (resList == null) return fail("找不到该流程实例,请假查procinstId和businessId");
        else return success("获取任务列表成功!",TaskModelConverter.from(resList));
    }



    /**
     * 查询待办任务列表
     * @author gsj
     * @return
     */
    @Override
    public JsonResultMessage<Page<TaskModel>> queryToDoList(ToDoTaskQueyParamsModel params) {
        ToDoTaskQueyParams toDoTaskQueyParams = ToDoTaskQueyParamsModelConverter.to(params);
        Page<Task> resList = this.taskService.queryToDoList(toDoTaskQueyParams);
        return super.success(TaskModelConverter.from(resList));
    }

    /**
     * 查询已办任务列表
     * @author gsj
     * @param params
     * @return
     */
    @Override
    public JsonResultMessage<Page<TaskModel>> queryDoneList(ToDoTaskQueyParamsModel params){
        ToDoTaskQueyParams doneTaskQueryParams = ToDoTaskQueyParamsModelConverter.to(params);
        Page<Task> resList = this.taskService.queryDoneList(doneTaskQueryParams);
        return super.success(TaskModelConverter.from(resList));
    }

    @Override
    public JsonResultMessage<Page<TaskModel>> queryDoingList(@RequestBody ToDoTaskQueyParamsModel params){
        ToDoTaskQueyParams doneTaskQueryParams = ToDoTaskQueyParamsModelConverter.to(params);
        Page<Task> resList = this.taskService.queryDoingList(doneTaskQueryParams);
        return super.success(TaskModelConverter.from(resList));
    }



    /**
     * 获取指定流程的所有实例以及状态(当前任务节点的nodecode在参数nodecode中)
     */
    @Override
    public JsonResultMessage<Page<ProcinstModel>> queryProcinstByUser(ProcinstQueryModel params) {
        ProcinstQuery procinstQuery = ProcinstQueryModelConverter.to(params);
        Page<Procinst> resList = taskService.queryByUser(procinstQuery);
        return success(ProcinstModelConverter.from(resList) );
    }

    /**
     * 删除指定实例指定nodelist的任务
     */
    @Override
    public JsonResultMessage deleteTasksByNodeCodes(ProcinstNodesParamsModel paramsModel) {
        ProcinstNodesParams procinstNodesParams = ProcinstNodesParamsModelConverter.to(paramsModel);
        return success(taskService.deleteByNodeCodes(procinstNodesParams));

    }

    /**
     * 新增指定实例、下一个节点的任务
     * @param addTaskParamsModel
     * @return
     */
    @Override
    public JsonResultMessage addTaskByProcinstId(AddTaskParamsModel addTaskParamsModel) {
        AddTaskParams addTaskParams = AddTaskParamsModelConverter.to(addTaskParamsModel);
        return success(taskService.addTaskByProcinstId(addTaskParams));
    }

    @Override
    public JsonResultMessage delTaskByProcinstId(DelTaskParamsModel delTaskParamsModel) {
        DelTaskParams delTaskParams = DelTaskParamsModelConverter.to(delTaskParamsModel);
        return success(taskService.delTaskByProcinstId(delTaskParams));
    }
}

接口实现ServiceImpl


@Service
@Slf4j
public class TaskServiceImpl implements TaskService {

    @Resource
    private FlowMapper flowMapper;
    @Resource
    private NodeMapper nodeMapper;
    @Resource
    private ProcinstMapper procinstMapper;
    @Resource
    private TaskMapper taskMapper;
    @Resource
    private NodeRelationMapper nodeRelationMapper;
    @Resource
    private NodeRoleMapper nodeRoleMapper;
    @Value("${flowConfig.lightPercent.floor}")
    public float lightFloorNumber;

    @Value("${flowConfig.lightPercent.roof}")
    public float lightRoofNumber;



    /**
     * 启动
     * 产生流程实例
     * 产生开始节点Task
     * @param params
     * @return 开始节点任务id
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String launch(LaunchParams params) {
        Map<String,Object> condition = new HashMap<String,Object>();

        log.info("启动===================================================>>>>"+params.toString());
        //流程元数据
        Flow flow = this.flowMapper.get(params.getFlowId());
        //开始节点元数据
        Node nodeCondition = new Node();
        nodeCondition.setFlowCode(flow.getCode());
        nodeCondition.setNodeType(NodeType.START.getCode());
        List<Node> nodes = this.nodeMapper.queryList(nodeCondition);
        log.info("查询到的流程节点:=====================================================>>>>"+nodes.size());
        Node startNode = nodes.stream().findAny().orElse(null);
        if (startNode != null) {
            NodeRole nodeRoleCondition = new NodeRole();
            nodeRoleCondition.setNodeCode(startNode.getNodeCode());
            NodeRole startNodeRole = this.nodeRoleMapper.queryList(nodeRoleCondition).stream().findAny().orElse(null);

            String procinstID = this.insertProcInst(flow.getId(), params.getStartUserUid(),params.getStartUserName(),startNode.getNodeCode(),params.getBusinessId());
            if (!procinstID.isEmpty()) {
                //开始任务实例
                String taskid = addFirstTask(procinstID,startNode,startNodeRole.getRoleColumn(),params);
                if (taskid.isEmpty()) {
                    //新增任务失败
                    return "-3";
                } else return taskid;
            } else {
                //新增流程实例失败
                return "-2";
            }

        } else {
            //没有开始节点
            return "-1";
        }
    }

    //新增流程实例
    public String insertProcInst(String flowId, String startUserUid,String startUserName, String startNodeCode, String bussinessId){
        //流程实例
        Procinst procinst = new Procinst();
        String procinstID = java.util.UUID.randomUUID().toString();
        procinst.setId(procinstID);
        procinst.setFlowId(flowId);
        procinst.setStartTime(new Date());
        procinst.setStartUseruid(startUserUid);
        procinst.setStartUserName(startUserName);
        procinst.setStartNodeCode(startNodeCode);
        procinst.setBusinessId(bussinessId);
        procinst.setDelFlag(0);
        int res = this.procinstMapper.insert(procinst);
        if (res > 0) return procinstID;
        else return "";
    }

    /**
     * 流转
     * 检查当前节点类型
     * 结束节点:完成
     * 其他节点:完成、产生下一步节点(检查流转类型)(产生下一步节点的时候需要检查节点类型,会签节点的话?检查是否已产生Task?更新(向上检查所有Task是否Finish来更新状态Wait->Ready))
     * 自动流转:产生下一步所有节点Task
     * 手动流转:根据选择的下一步节点产生Task
     * @param params
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String flowing(FlowingParams params) {
        String result="";

        //检查当前节点类型
        Task curTask = this.taskMapper.get(params.getTaskId());
        Node curNode = this.nodeMapper.getByCode(curTask.getNodeCode());
        System.out.println("task:"+curTask+";node:"+curNode);
        if (curNode.getNodeType().equals(NodeType.PAUSE.getCode())) {
            //停止节点,该流程线结束,没有下个节点,但是整个流程尚未结束
            curTask = finishCurrentTask(curTask, params, OperationName.Submit.getName());
            result = curTask.getId();
        }
        else if (curNode.getNodeType().equals(NodeType.END.getCode())){
//            //结束节点:完成
//            curTask = finishCurrentTask(curTask,params, OperationName.Submit.getName());
//
//            //更新实例表的结束时间等
//            Procinst procinst = this.procinstMapper.get(curTask.getProcinstId());
//            procinst.setEndTime(new Date());
//            //procinst.setDuation();//todo:应该调用排除节假日的一个计算方法
//            procinst.setEndNodeCode(curTask.getNodeCode());
//            this.procinstMapper.update(procinst);
//            result = curTask.getId();
            result = endingFlow(curTask,params);
        }else {
            //其他节点:完成、产生下一步节点
            //完成当前任务
            curTask = finishCurrentTask(curTask, params, OperationName.Submit.getName());
            //产生下一步节点
            List<String> arrNextNodeCodes = new ArrayList<String>();
            if (!StringUtil.isEmpty(params.getNextNodeCodes())) {
                //手动流转:根据接收的nextNodeCodes产生下一步节点Task
                String[] arrCustomNextNodeCodes = params.getNextNodeCodes().split(",");
                Collections.addAll(arrNextNodeCodes,arrCustomNextNodeCodes);
            } else {
                //自动流转:产生所有下一步节点Task
                var nodeRelationCondition = new NodeRelation();
                nodeRelationCondition.setNodeCode(curNode.getNodeCode());
                arrNextNodeCodes = nodeRelationMapper.queryList(nodeRelationCondition).stream().map(v -> v.getNextNodeCode()).collect(Collectors.toList());
                System.out.println("------自动流转"+arrNextNodeCodes);
            }
            //节点1->n逻辑,添加n条task
            for(String nodeCode : arrNextNodeCodes) {
                Node nextNode = nodeMapper.getByCode(nodeCode);
                System.out.println("------nextNode"+nextNode);
                //判断nextNodeCode会签节点CounterSign
                if (nextNode.getNodeType().equals(NodeType.COUNTERSIGN.getCode())) {
                    var taskCondition = new Task();
                    taskCondition.setProcinstId(curTask.getProcinstId());
                    taskCondition.setNodeCode(nextNode.getNodeCode());
                    //下一个节点是否已经存在task,如果存在task,那么状态一定是wait,如果所有上级节点都完成了,将task状态改为ready,否则依然还是wait
                    Task thisTask = taskMapper.queryList(taskCondition).stream().findAny().orElse(null);
                    if (thisTask != null) {
                        //会签节点需要所有上一个任务都完成才能让其Wait到Ready继续流转
                        Boolean canFlow = canFlow(curTask.getProcinstId(),nextNode.getNodeCode());
                        if (canFlow) {
                            thisTask.setStatus(TaskStatus.READY.getCode());
                            taskMapper.update(thisTask);
                            result = thisTask.getId();
                        }
                    } else {
                        //add nextNode newTask
                        result = addTask(nextNode, curTask, params);
                    }
                } else {
                    //add nextNode newTask
                    result = addTask(nextNode,curTask,params);
                }

            }

        }
        return result;
    }
    //完成当前任务
    public Task finishCurrentTask(Task curTask, FlowingParams params, String operationName){
        curTask.setOperationName(operationName);
        curTask.setCreateUserUid(params.getUserUid());
        curTask.setCreateUserName(params.getUserName());
        curTask.setOperatorShowText(params.getOperatorShowText());
        curTask.setOperationDesc(params.getOperationDesc());
        curTask.setOpinion(params.getOpinion());
        curTask.setStatus(TaskStatus.FINISH.getCode());
        curTask.setEndTime(new Date());
        curTask.setIsCurrent(0);
        this.taskMapper.update(curTask);
        return curTask;
    }

    //会签节点,需要判断之前的节点,是否全部流转结束,
    public boolean canFlow(String procinsId, String nodeCode) {
        //之前已经有其他上级节点完成,判断thisNode的来源节点个数,和上个节点的task是否都已经完成
        var nodeRelationCondition = new NodeRelation();
        nodeRelationCondition.setNextNodeCode(nodeCode);
        //查询来源节点
        List<String> originNodes = nodeRelationMapper.queryList(nodeRelationCondition).stream().map(v -> v.getNodeCode()).collect(Collectors.toList());
        //来源节点code在上个任务是否都已经是结束
        int notFinishTasksNumber = 0;
        for (String orginNodeCode:originNodes) {
            //获取没有finish的task
            var condition = new Task();
            condition.setProcinstId(procinsId);
            condition.setNodeCode(orginNodeCode);
            //是否已经流转到上个节点
            int hasFlowToLastNode = taskMapper.queryList(condition).size();
            //如果汇入节点还没有任务,需要等待,不能直接流转
            if (hasFlowToLastNode ==0) {
                return false;
            } else {
                //汇入节点已经有任务,并且都完成了,返回可以流转,有一个没有完成,仍需等待
                List<String> statusList = new ArrayList<String>();
                statusList.add(TaskStatus.WAIT.getCode());
                statusList.add(TaskStatus.READY.getCode());
                condition.setStatusList(statusList);
                notFinishTasksNumber += taskMapper.queryList(condition).size();
            }
        }
        if (notFinishTasksNumber == 0) return true;
        else return false;

    }

    //新增开始节点的任务
    public String addFirstTask(String procinstID, Node startNode,String nodeRoleColumn, LaunchParams params){
        Task startTask = new Task();
        String startTaskID = java.util.UUID.randomUUID().toString();
        startTask.setId(startTaskID);
        startTask.setProcinstId(procinstID);
        startTask.setNodeCode(startNode.getNodeCode());
        startTask.setNodeName(startNode.getNodeName());
        startTask.setIsCurrent(1);
        startTask.setOperatorRoleCode(nodeRoleColumn);
//            startTask.setOperatorRoleName();

        startTask.setStartTime(new Date());
        //startTask.setEndTime();//todo:应该调用一个排查节假日的计算方法
        startTask.setStatus(TaskStatus.READY.getCode());

        startTask.setDelFlag(0);
        int res = this.taskMapper.insert(startTask);
        if (res > 0) return startTaskID;
        else return "";
    }

    public String addTask(Node thisNode, Task curTask, FlowingParams params){
        int res = 0;
        //节点配置的角色
        NodeRole nodeRoleCondition = new NodeRole();
        nodeRoleCondition.setNodeCode(thisNode.getNodeCode());
        var refNodeRole = nodeRoleMapper.queryList(nodeRoleCondition);
        if (refNodeRole.isEmpty()) {
            return "-1";
        }else {
            NodeRole nodeRole = nodeRoleMapper.queryList(nodeRoleCondition).get(0);

            Task newTask = new Task();
            newTask.setId(java.util.UUID.randomUUID().toString());
            newTask.setProcinstId(curTask.getProcinstId());
            newTask.setNodeCode(thisNode.getNodeCode());
            newTask.setNodeName(thisNode.getNodeName());
            newTask.setOriginTaskId(curTask.getId());
            newTask.setStartTime(new Date());
            //startTask.setEndTime();//todo:应该调用一个排查节假日的计算方法
            if (thisNode.getNodeType().equals(NodeType.COUNTERSIGN.getCode())) {
                newTask.setStatus(TaskStatus.WAIT.getCode());
            } else {
                newTask.setStatus(TaskStatus.READY.getCode());
            }
            newTask.setIsCurrent(1);
            newTask.setOperatorRoleCode(nodeRole.getRoleColumn());
            //            startTask.setOperatorRoleName();
            //如果keepType==1,不需要部门保持,那么unitcode不需要添加,
            // 比如沟通评价后,一个任务到主办单位接收keepType=2,另一个任务到代表沟通反馈评价,keeyType=1,不需要添加operator_unit_code
            if (params.getToUnitCode() != null && !"".equals(params.getToUnitCode()) && thisNode.getKeepType() == 2) {
                newTask.setOperatorUnitCode(params.getToUnitCode());
            }
            if (params.getToUnitName() != null && !"".equals(params.getToUnitName())) {
                newTask.setOperatorUnitName(params.getToUnitName());
            }
            if (params.getToOperatorId() != null && !"".equals(params.getToOperatorId())) {
                newTask.setOperatorUseruid(params.getToOperatorId());
            }
            if (params.getToOperatorName() != null && !"".equals(params.getToOperatorName())) {
                newTask.setOperatorUsername(params.getToOperatorName());
            }
            newTask.setDelFlag(0);
            res += this.taskMapper.insert(newTask);
            if (res > 0)
                return newTask.getId();
            else return "";
        }
    }

    @Override
    public String ending(FlowingParams params) {
        Task curTask = this.taskMapper.get(params.getTaskId());
        String result = endingFlow(curTask,params);
        return result;
    }

    /**
     * 结束当前流程
     */
    public String endingFlow(Task curTask, FlowingParams params){
        //结束节点:完成
        curTask = finishCurrentTask(curTask,params, OperationName.Submit.getName());

        //更新实例表的结束时间等
        Procinst procinst = this.procinstMapper.get(curTask.getProcinstId());
        procinst.setEndTime(new Date());
        //procinst.setDuation();//todo:应该调用排除节假日的一个计算方法
        procinst.setEndNodeCode(curTask.getNodeCode());
        this.procinstMapper.update(procinst);
        String result = curTask.getId();
        return result;
    }

    /**
     * 退回
     * 完成当前Task
     * 根据当前Task.OriginTaskId产生退回Task
     * @param params
     * @return
     */
    @Override
    public String back(FlowingParams params) {
        try {
            String res = "";

            //当前任务
            Task currentTask = this.taskMapper.get(params.getTaskId());
            currentTask = finishCurrentTask(currentTask, params, OperationName.Back.getName());

            //退回到上一个节点,如果nodeRelation中指定了退回节点,就用该节点,如果没有指定就是退回到来源节点(B->C,退回C->B,再退回B->C)
            NodeRelation nodeRelationCondition = new NodeRelation();
            nodeRelationCondition.setNodeCode(currentTask.getNodeCode());
            Task originTask = this.taskMapper.get(currentTask.getOriginTaskId());
            //有指定退回节点列表
            var templist = nodeRelationMapper.queryList(nodeRelationCondition);
            var backListInDB = templist.stream().filter(v -> v.getBackNodeCode() != null).map(back -> back.getBackNodeCode()).collect(Collectors.toList());
            var backNodeCodeList = new ArrayList<String>();
            var toOrigin = false;
            if (!backListInDB.isEmpty()) {
                //如果有多个指定返回code,那么匹配来源,如果都不匹配,两个都退回,匹配到来源,根据来源退回
                var isExistOriginCode = backListInDB.stream().filter(v -> v.equals(originTask.getNodeCode())).collect(Collectors.toList());
                if (isExistOriginCode != null && isExistOriginCode.size() > 0) {
                    backNodeCodeList.add(originTask.getNodeCode());
                    toOrigin = true;
                } else {
                    backNodeCodeList = (ArrayList<String>) backListInDB;
                }
            } else {
                backNodeCodeList.add(originTask.getNodeCode());
                toOrigin = true;
            }

            for (String backNodeCode : backNodeCodeList) {

                Task backTask = new Task();
                backTask.setId(java.util.UUID.randomUUID().toString());
                backTask.setProcinstId(currentTask.getProcinstId());
                backTask.setOriginTaskId(currentTask.getId());
                backTask.setNodeCode(backNodeCode);
                //根据nodeCode获取nodename
                backTask.setNodeName(nodeMapper.getByCode(backNodeCode).getNodeName());
                backTask.setIsCurrent(1);
                if (toOrigin) {
                    backTask.setOperatorRoleCode(originTask.getOperatorRoleCode());
                    //        backTask.setOperatorRoleName();
                    backTask.setOperatorUnitCode(originTask.getOperatorUnitCode());
                    //        backTask.setOperatorUnitName();
                    backTask.setOperatorUseruid(originTask.getOperatorUseruid());
                } else {
                    var nodeRoleCondition = new NodeRole();
                    nodeRoleCondition.setNodeCode(backNodeCode);
                    var backNode = nodeRoleMapper.queryList(nodeRoleCondition).get(0);
                    backTask.setOperatorRoleCode(backNode.getRoleColumn());
                }
                //        backTask.setOperatorUsername();
                backTask.setStartTime(new Date());
                backTask.setStatus(TaskStatus.READY.getCode());
                backTask.setDelFlag(0);
                int result =  this.taskMapper.insert(backTask);
                if (result >0) res = backTask.getId();
            }
            return res;
        }catch (Exception ex) {
            System.out.println("Recaught: " + ex);

        }
        return "";
    }

    /**
     * 查询业务件的任务列表
     * @param params
     * @return
     */
    @Override
    public List<Task> queryTaskList(TaskQueryParams params) {
        String procinstId = params.getProcinstId();
        if (procinstId == null) {
            Procinst procinstCondition = new Procinst();
            procinstCondition.setFlowId(params.getFlowId());
            procinstCondition.setBusinessId(params.getBusinessId());
            Procinst procinst = this.procinstMapper.queryList(procinstCondition).stream().findAny().orElse(null);//一个业务件基本对应一个流程实例
            if (procinst != null && procinst.getId()!=null) {
                procinstId = procinst.getId();
            }
        }
        if (procinstId == null) {
            return null;
        } else {
            Task condition = new Task();
            condition.setProcinstId(procinstId);
            var list = this.taskMapper.queryList(condition);
            for (Task item:list) {
                //受否有限制时间
                var curNode = nodeMapper.getByCode(item.getNodeCode());
                if (curNode !=null && curNode.getLimitTime() != null && curNode.getLimitTime() >0 ) {
                    int spentDay = 0;
                    //如果已经结束,算耗时应该是开始时间到结束时间,如果没有结束,耗时是开始时间到now
                    var endTime = new Date();
                    if (item.getEndTime() != null) endTime = item.getEndTime();
                    if ( curNode.getLimitTimeUnit().equals(LimitTimeUnit.WorkDay.getName())) {
                        //工作日用节假日管理计算花费了多少天
                        spentDay = taskMapper.getSpentWorkDay(item.getStartTime(), endTime);
                    } else if(curNode.getLimitTimeUnit().equals(LimitTimeUnit.NatureDay.getName())) {
                        //自然日直接计算
                        spentDay = differentDaysByMillisecond(item.getStartTime(), endTime);
                    }
                    curNode.setSpendDay(spentDay);
                    //亮灯的有颜色, 花费时间/限制时间
                    float percent = (float) spentDay/curNode.getLimitTime();
                    String lightColor = null;
                    if(percent <= lightFloorNumber){      //小于等于百分之70为绿色
                        lightColor = LightType.GREEN.getCode();
                    }else if(percent > lightFloorNumber && percent < lightRoofNumber){   //大于百分之70,小于百分之百为黄色
                        lightColor = LightType.YELLOW.getCode();
                    }else if(percent >= lightRoofNumber){   //大于等于百分之百为红色
                        lightColor = LightType.RED.getCode();
                    }
//                    if (percent >= lightFloorNumber) {
//                        lightColor = LightType.RED.getCode();
//                    } else {
//                        lightColor = LightType.YELLOW.getCode();
//                    }
                    curNode.setLightColor(lightColor);
                }
                item.setRefNode(curNode);
            }
            return list;
        }
    }


    public static int differentDaysByMillisecond(Date date1,Date date2)
    {
        int days = (int) ((date2.getTime() - date1.getTime()) / (1000*3600*24));
        return days;
    }

    /**
     * 查询待办任务列表
     * @return
     * @param params
     */
    @Override
    public Page<Task> queryToDoList(ToDoTaskQueyParams params) {
        List<String> nodeStatus = new ArrayList<String>();
        nodeStatus.add(TaskStatus.READY.getCode());
        params.setNodeStatus(nodeStatus);

        PageHelper.startPage(params.getPageIndex(),params.getPageSize());
        List<Task> taskList = this.taskMapper.queryDoList(params);
        return Page.of(taskList);
    }


    /**
     * 查询已办任务列表
     * @param params
     * @return
     */
    @Override
    public Page<Task> queryDoneList(ToDoTaskQueyParams params) {
        List<String> nodeStatus = new ArrayList<String>();
        nodeStatus.add(TaskStatus.FINISH.getCode());
        params.setNodeStatus(nodeStatus);
        PageHelper.startPage(params.getPageIndex(),params.getPageSize());
        List<Task> taskList = this.taskMapper.queryDoList(params);
        return Page.of(taskList);
    }

    @Override
    public Page<Task> queryDoingList(ToDoTaskQueyParams params) {
        PageHelper.startPage(params.getPageIndex(),params.getPageSize());
        List<Task> taskList = this.taskMapper.queryDoList(params);
        return Page.of(taskList);
    }

    /***
     * 查询跟我相关的任务、包括某些节点筛选
     * @param procinstQuery
     * @return
     */
    @Override
    public Page<Procinst> queryByUser(ProcinstQuery procinstQuery) {
        PageHelper.startPage(procinstQuery.getPageIndex(),procinstQuery.getPageSize());

        List<Procinst> procinstList = procinstMapper.queryListByNodeCode(procinstQuery);
        //根据proinstId查询这个实例当前的节点:iscurrent = 1
        for (Procinst procinst: procinstList) {
            var refNodeTaskCondition = new Task();
            refNodeTaskCondition.setProcinstId(procinst.getId());
            refNodeTaskCondition.setIsCurrent(1);
            List<Node> refNodeList=nodeMapper.queryRefCurrentNodeByTask(refNodeTaskCondition);
            procinst.setRefNodes(NodeModelConverter.from(refNodeList));

            //当前任务列表
            var taskCondition = new Task();
            taskCondition.setProcinstId(procinst.getId());
            taskCondition.setIsCurrent(1);
            taskCondition.setStatus(TaskStatus.READY.getCode());
            List<Task> curTaskList = this.taskMapper.queryList(taskCondition);
            procinst.setCurTasks(TaskModelConverter.from(curTaskList));
        }
        return Page.of(procinstList);
    }

    @Override
    public int deleteByNodeCodes(ProcinstNodesParams procinstNodesParams) {
        return taskMapper.deleteByNodeCodes(procinstNodesParams);
    }

    @Override
    public String addTaskByProcinstId(AddTaskParams addTaskParams) {
//        String nodeCode = "OtherDeptAccept";
//        String originNodeCode = "Assign";
//        String toUnitCode = "FX_FGW";
//        String toUserUid = "fgw";
//        String operatorRoleCode = "DBJY_r_rddb";
        Node nextNode = nodeMapper.getByCode(addTaskParams.getNextNodeCodes());
        //获取最近一次originNodeCode的task,例如多次分办、退回,取最近一次分办
        Task condition = new Task();
        condition.setNodeCode(addTaskParams.getOriginNodeCode());
        condition.setProcinstId(addTaskParams.getProcinstId());
        List<Task> refTaskList = taskMapper.queryList(condition);
        Task recentTask = refTaskList.get(refTaskList.size()-1);

        String result = this.addTask(nextNode, recentTask,addTaskParams.getFlowingParams());
        return result;
    }

    /**
     * 删除某个节点后的,某个条件的任务,以及只有所有的任务,例如会办单位接收、tounitcode是哪个单位
     * @param
     * @return
     */
    @Override
    public int delTaskByProcinstId(DelTaskParams delTaskParams) {
        //先找到第一个需要删除的任务,然后根据orginTaskid来将所有需要删除的任务id放入数组中
        Task delCondition = new Task();
        delCondition.setProcinstId(delTaskParams.getDelTaskCondition().getProcinstId());
        List<String> toDelTaskIds = new ArrayList<>();
        List<Task> taskList = taskMapper.queryList(delTaskParams.getDelTaskCondition());
        if(delTaskParams.getIsDeleteAfterAll()>0) {
            //循环查询下面的任务
            Task condition = new Task();
            while (taskList.size() > 0) {
                toDelTaskIds.addAll(taskList.stream().map(v -> v.getId()).collect(Collectors.toList()));
                condition.setOriginTaskIds(taskList.stream().map(v -> v.getId()).collect(Collectors.toList()));
                taskList = taskMapper.queryList(condition);
            }

            //根据id来批量删除
            delCondition.setIds(toDelTaskIds);

        } else {

            delCondition.setIds(taskList.stream().map(v -> v.getId()).collect(Collectors.toList()));
        }
        if (delCondition.getIds().size()>0)
            return taskMapper.batchDelete(delCondition);
        return 0;
    }
}

5.3 任务查询接口

接口实现controller


 */
@Slf4j
@CrossOrigin
@RestController
@RequestMapping("/user")
public class UserController extends BaseApiController implements com.beyondbit.ias.flow.schemas.UserService {

    @Resource
    private UserService userService;

    @Override
    public JsonResultMessage<Page<UserModel>> query(UserQueryModel userQueryModel) {
        val userQuery = UserQueryModelConverter.to(userQueryModel);
        val userPage = userService.query(userQuery);

        return success(UserModelConverter.from(userPage));
    }

    @Override
    public JsonResultMessage<UserModel> get(String id) {
        var user = userService.get(id);

        return success(UserModelConverter.from(user));
    }

    @Override
    public JsonResultMessage add(@Valid UserModel userModel) {
        try {
            val user = UserModelConverter.to(userModel);
            userService.add(user);
        } catch (ServiceException e) {
            if (e instanceof UserExistsException) {
                return error(ErrorCode.USER_EXISTS_EXCEPTION);
            }

            return fail(e.getMessage());
        }

        return success("新增成功");
    }

    @Override
    public JsonResultMessage update(String id, UserModel userModel) {
        val user = UserModelConverter.to(userModel);
        user.setId(id);
        userService.update(user);

        return success("修改成功");
    }

    @Override
    public JsonResultMessage delete(String id) {
        userService.delete(id);

        return success("删除成功");
    }

    @Resource
    private NodeService nodeService;
    @Override
    public JsonResultMessage queryList(NodeModel params) {
        Node nodeCondition = NodeModelConverter.to(params);
        return success(nodeService.queryList(nodeCondition));
    }
}

接口实现ServiceImpl


/**
 * 用户服务默认实现
 *
 * @Author: lvfahai
 * @Date: 2020/3/10 11:28
 */
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Resource
    private IdGenerator idGenerator;

    @Override
    public Page<User> query(UserQuery query) {
        PageHelper.startPage(query.getCurrentPage(), query.getPageSize());
        List<User> list = userMapper.query(query);

        return Page.of(list);
    }

    @Override
    public User get(String id) {
        return userMapper.get(id);
    }

    @Override
    public void add(User user) throws ServiceException {
        User existUser = userMapper.getByUserUid(user.getUserUid());

        if (existUser != null) {
            throw new UserExistsException(user.getUserUid());
        } else {
            user.setId(idGenerator.newId());
            user.setDateCreated(new Date());
            userMapper.insert(user);
        }
    }

    @Override
    public void update(User user) {
        userMapper.update(user);
    }

    @Override
    public void delete(String id) {
        userMapper.delete(id);
    }
}

六、业务流程

6.1 流程启动流程

  1. 接收启动请求: 客户端调用 /task/launch 接口
  2. 参数验证: 验证 LaunchParams 参数完整性和有效性
  3. 创建流程实例: 在 PROCINST 表中创建新的流程实例记录
  4. 获取起始节点: 根据流程定义获取 START 类型的起始节点
  5. 创建首个任务: 在 TASK 表中创建起始任务
  6. 返回结果: 返回任务ID和流程实例ID

6.2 任务流转流程

  1. 获取当前任务: 根据 taskId 查询当前任务信息
  2. 权限验证: 验证当前用户是否有权限处理该任务
  3. 节点类型判断:
    • PAUSE节点: 暂停流程,等待外部触发
    • END节点: 结束流程,更新流程实例状态
    • FLOW节点: 继续流转到下一节点
    • COUNTERSIGN节点: 会签处理,创建多个并行任务
  4. 完成当前任务: 更新任务状态为 FINISH
  5. 生成下一步任务: 根据节点关系创建后续任务
  6. 任务分配: 根据节点角色配置分配任务处理人

6.3 任务退回流程

  1. 验证退回权限: 检查当前用户是否可以执行退回操作
  2. 获取退回节点: 根据 backNodeCode 获取目标节点信息
  3. 删除当前任务: 删除当前节点的所有未完成任务
  4. 创建退回任务: 在目标节点创建新的任务
  5. 记录操作历史: 记录退回操作的详细信息

6.4 会签节点处理

会签节点特殊处理逻辑:
  • 并行任务创建: 为每个会签人员创建独立的任务
  • 完成条件检查: 检查是否所有会签任务都已完成
  • 结果汇总: 汇总所有会签人员的处理结果
  • 流程继续: 满足条件后继续流转到下一节点

七. 安全设计

7.1 权限控制

基于角色的访问控制 (RBAC)

  • 用户认证: 基于JWT的用户身份验证
  • 角色管理: 通过 NODE_ROLE 表配置节点处理权限
  • 权限验证: 在任务操作前验证用户权限
  • 数据隔离: 基于用户所属单位进行数据隔离

7.2 数据安全

  • SQL注入防护: 使用MyBatis参数化查询
  • XSS防护: 对用户输入进行转义处理
  • 敏感数据加密: 对关键业务数据进行加密存储
  • 操作日志: 记录所有关键操作的审计日志

7.3 接口安全

  • 请求限流: 防止恶意请求攻击
  • 参数验证: 严格验证所有输入参数
  • 异常处理: 统一异常处理,避免敏感信息泄露
  • HTTPS传输: 强制使用HTTPS加密传输

八、结束语
时至今日,SAQ-Flow-Backend 依然平稳运行于多个政务场景中,它以极简的设计承载着不简单的使命,不仅大幅提升了开发效率,也成为了政务数字化进程中的一块稳健基石。未来,我们相信这样轻量、专注的工具仍会在合适的场景中持续发挥其独特价值。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐