前言
本文代码实现是仿照工作流实现的,由于业务需求的特殊性,没有直接使用工作流。
功能实现
功能实现 | |
功能 | 说明 |
任务查询 | 1.个人任务查询 2.节点任务查询 3.组内任务查询 上述3种查询可组合查询 |
任务驳回 | 1.驳回到任意节点 2.节点跳跃(可忽略驳回点与驳回至节点之间的任意节点) |
任务分配(拾取) | 将任务分配给某个人 |
任务归还 | 这个操作在项目中没有具体使用,不过已经实现相关功能 |
任务删除 | 1.删除当前任务 2.删除任务流转记录 支持强制删除任务(默认进行中的任务无法删除),自由设置任务流程记录是否删除 |
流程记录 | 1.流程记录查询 2.流程记录添加 3.流程记录删除 |
1.责任人与组为一对多关系 2.不支持网关相关逻辑 |
1.需求及实现
以下根据实际项目进行需求举例说明,以下截图为功能部分截图,仅供参考
需求 | 说明 | 实现效果 |
1️⃣业务流程设置 | 1.管理员可以对任意流程节点进行开启关闭操作 2.节点设置,仅对之后的流程实例生效,进行中的仍按之前的流程节点进行 | |
2️⃣项目权限(节点任务组)设置 | 1.为不同的人员 ,在项目节点下设置不同的任务组权限 将这个功能带入到工作流中,可以理解为在某个节点设置任务组,把人员分到不同的组中,只有当前组下的人员才能看到任务 | 说明: 此图仅供参考,为本人实际项目部分图,项目权限为流程权限,其中包含了非流程节点。 1.项目权限:表示流程节点 2.类别名称:可以理解为任务 组。 |
2.代码实现
1.流程设置
这里的流程设置,相当于画任务流程节点图,我是将节点都放在了数据字典中
然后将需要的节点存储即可,这里比较简单,就不列代码了。
2.项目权限设置
流程节点下任务组,责任人
设置
1.表设计
operator_auth,用于存放当前检测项目(任务组
)下不同节点的权限。这个不是本文的重点,这里就不列出具体的代码了。
3.任务流程处理
1.表设计
1.detection_flow_task
detection_flow_task | 任务主表 | |
名 | 类型 | 注释 |
model_type | varchar(255) | 流程节点 |
test_code | varchar(255) | 检测项目编号(任务组) |
business_key | bigint(20) | 业务id |
signee | bigint(20) | 责任人 |
variable | varchar(1000) | 变量 |
next_node | varchar(255) | 下个节点 |
operate | int(11) | 操作类型 0驳回 1通过 |
2.detection_flow_nodes
存储任务流程实时节点数据
detection_flow_task | 任务主表 | |
名 | 类型 | 注释 |
model_type | varchar(255) | 任务节点 |
business_key | bigint(20) | 业务id |
3.detection_flow_his
detection_flow_his | 任务流程历史记录 | |
名 | 类型 | 注释 |
model_type | varchar(255) | 流程节点 |
business_key | bigint(20) | 业务id |
variable | varchar(1000) | 变量 |
next_node | varchar(255) | 下个节点 |
operate | int(11) | 操作类型 0驳回 1通过 |
2.代码实现
代码较多,这里只贴出核心代码
1.FlowTaskController
@Tag(name = "管理后台 - 任务流程-主表-当前任务")
@RestController
@RequestMapping("/detection/flow-task")
@Validated
public class FlowTaskController {
@Resource
private FlowTaskService flowTaskService;
/**
* @description发起流程 ,默认从起始节点开始
* @author lvyq
* @throws
* @time 2023-09-15 10:10
*/
@PostMapping("/startFlow")
@Operation(summary = "发起任务流程")
public CommonResult startFlow(@Valid @RequestBody FlowTaskCreateReqVO completeVO){
flowTaskService.startFlow(completeVO.getBusinessKey(),completeVO.getSignee(),completeVO.getStartNode(),completeVO.getTestCode());
return success(true);
}
/**
* @description 完成当前流程
* @author lvyq
* @param[1] createReqVO
* @throws
* @return CommonResult<Long>
* @time 2023-08-15 11:50
*/
@PutMapping("/complete")
@Operation(summary = "完成任务")
public CommonResult<Boolean> complete(@Valid @RequestBody FlowTaskCompleteVO completeVO) {
flowTaskService.complete(completeVO);
return success(true);
}
/**
* @description 任务驳回
* @author lvyq
* @param[1] completeVO
* @throws
* @return CommonResult<Boolean>
* @time 2023-08-22 13:33
*/
@PutMapping("/reject")
@Operation(summary = "任务驳回")
public CommonResult<Boolean> reject(@Valid @RequestBody FlowTaskRejectVO vo) {
flowTaskService.reject(vo);
return success(true);
}
/**
* @description 获取当前节点的待办任务
* @author lvyq
* @param[1] pageVO
* @throws
* @return CommonResult<PageResult<FlowTaskRespVO>>
* @time 2023-08-15 15:03
*/
@GetMapping("/page")
@Operation(summary = "获得任务流程-主表-当前任务分页")
@TaskFlowAuth
public CommonResult getFlowTaskPage(@Valid FlowTaskPageReqVO pageVO) {
return success(flowTaskService.getFlowTaskPage(pageVO));
}
/**
* @description
* @author lvyq
* @param[1] pageVO 指派任务
* @throws
* @return CommonResult
* @time 2023-09-25 14:25
*/
@PutMapping("/assignTask")
public CommonResult assignTask(@Valid @RequestBody AssignTaskVo completeVO){
flowTaskService.assignTask(completeVO);
return success(true);
}
/**
* @description 重新发起流程
* @author lvyq
* @param[1] completeVO
* @throws
* @return CommonResult
* @time 2023-09-26 15:05
*/
@PutMapping("/reStartFlow")
@Operation(summary = "重新发起任务流程")
public void reStartFlow(@Valid @RequestBody ReStartFlowTaskVo vo){
flowTaskService.reStartFlow(vo.getBusinessKey(),vo.getDelHis());
}
}
2.FlowTaskServiceImpl
@Service
@Validated
public class FlowTaskServiceImpl implements FlowTaskService {
@Resource
private FlowTaskMapper flowTaskMapper;
@Resource
private FlowHisMapper flowHisMapper;
@Resource
private FlowHisService flowHisService;
@Resource
private FlowNodesMapper flowNodesMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void deleteFlowTask(Long id) {
// 校验存在
validateFlowTaskExists(id);
// 删除
flowTaskMapper.deleteById(id);
}
private void validateFlowTaskExists(Long id) {
if (flowTaskMapper.selectById(id) == null) {
throw exception(FLOW_TASK_NOT_EXISTS);
}
}
@Override
public FlowTaskDO getFlowTask(Long id) {
return flowTaskMapper.selectById(id);
}
@Override
public List<FlowTaskDO> getFlowTaskList(Collection<Long> ids) {
return flowTaskMapper.selectBatchIds(ids);
}
@Override
public PageResult<FlowTaskRespVO> getFlowTaskPage(FlowTaskPageReqVO pageReqVO) {
IPage<FlowTaskRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
List<FlowTaskRespVO> flowTaskDOList = flowTaskMapper.getFlowTaskPageForDetection(page,pageReqVO);
List<String> nodes= getNodes();
flowTaskDOList.forEach(ls->{
Map<String,Object> map = new HashMap<>();
//历史节点
String flowNodes = ls.getFlowNodes();
if (!StringUtils.isEmpty(flowNodes)){
String[] split = flowNodes.split(",");
List<String> list = Arrays.asList(split);
nodes.forEach(no->{
if (list.contains(no)){
map.put(no,1);
}else {
map.put(no,0);
}
});
} else {
nodes.forEach(node->{
map.put(node,0);
});
}
//设置当前节点为否
map.put(ls.getModelType(),0);
ls.setFlowStatus(map);
});
return new PageResult<>(flowTaskDOList,page.getTotal());
}
/**
* @return boolean
* @throws
* @description 检测任务是否存在
* @author lvyq
* @time 2023-08-15 13:42
*/
public boolean checkTask(Long businessKey) {
boolean state = false;
FlowTaskDO flowTaskDO = flowTaskMapper.selectOne(FlowTaskDO::getBusinessKey, businessKey);
if (flowTaskDO != null) state = true;
return state;
}
/**
* @throws
* @description
* @author lvyq 审核通过
* @param[1] completeVO
* @time 2023-08-22 10:52
*/
@Override
@Transactional
public void complete(FlowTaskCompleteVO completeVO) {
//获取当前任务
FlowTaskDO taskDO = validateFlowTask(completeVO.getBusinessKey());
//信息补全
taskDO.setId(null);
taskDO.setVariable(completeVO.getVariable());
taskDO.setRemark(completeVO.getRemark());
taskDO.setSignee(completeVO.getSignee());
taskDO.setOperate(1);
//流程节点
List<String> nodes = flowNodesMapper.selectOne(FlowNodesDO::getBusinessKey, completeVO.getBusinessKey()).getModelType();
FlowHisCreateReqVO flowHisCreateReqVO = BeanUtil.copyProperties(taskDO, FlowHisCreateReqVO.class);
//当前流程没有下个环节,则表示流程通过后即结束当前流程
flowTaskMapper.deleteByBusinessKey(taskDO.getBusinessKey());
if (StringUtils.isEmpty(taskDO.getNextNode())) {
flowHisCreateReqVO.setRemark("流程结束");
} else {
//前往下个流程
completeTask(nodes, taskDO);
}
flowHisService.createFlowHis(flowHisCreateReqVO);
}
/**
* @throws
* @description 审核通过处理
* @author lvyq
* @time 2023-09-15 9:47
*/
private void completeTask(List<String> nodes, FlowTaskDO taskDO) {
//即将前往的流程节点
int currentNodeIndex = getNodeIndex(nodes,taskDO.getNextNode());
taskDO.setModelType(taskDO.getNextNode());
if (currentNodeIndex >= nodes.size() - 1) {
//即将前往的节点为流程的最后一个节点,则不存在下个节点
taskDO.setNextNode(null);
} else {
taskDO.setNextNode(nodes.get(currentNodeIndex + 1));
}
flowTaskMapper.insert(taskDO);
}
/**
* @throws
* @description 审核驳回
* @author lvyq
* @param[1] completeVO
* @time 2023-08-22 13:45
*/
@Override
@Transactional
public void reject(FlowTaskRejectVO completeVO) {
taskValCheck(completeVO);
//获取当前任务
FlowTaskDO taskDO = validateFlowTask(completeVO.getBusinessKey());
//处理最新的流程节点集合
List<String> nodes = checkCurrentNodes(completeVO);
// variable 设置
taskDO.setVariable(completeVO.getVariable());
taskDO.setId(null);
taskDO.setRemark(completeVO.getRemark());
taskDO.setOperate(0);//驳回
FlowHisCreateReqVO flowHisCreateReqVO = BeanUtil.copyProperties(taskDO, FlowHisCreateReqVO.class);
//历史记录节点处理
//1.发起节点 为未变更时taskDO的当前任务节点,下个节点为变更后的taskDo的当前节点
if (!StringUtils.isEmpty(completeVO.getRejectModelType())) {
if (!nodes.contains(completeVO.getRejectModelType())) {
throw exception(new ErrorCode(10001, "驳回节点不存在,驳回失败"));
}
//指定回退节点
rejectSpecifyNode(nodes, taskDO, completeVO.getRejectModelType(), completeVO.getIsInOrder());
} else {
//按序驳回,禁止忽略流程节点操作,不对相应数据进行处理
rejectInOrder(nodes, taskDO);
}
//删除当前任务数据
flowTaskMapper.deleteByBusinessKey(taskDO.getBusinessKey());
flowTaskMapper.insert(taskDO);
//flowHisCreateReqVO.setOperate(0);
flowHisCreateReqVO.setNextNode(taskDO.getModelType());
flowHisService.createFlowHis(flowHisCreateReqVO);
}
/**
* @return
* @throws
* @description 发起流程
* @author lvyq
* @param[1] businessKey 业务id
* @param[2] signee 责任人
* @param[3] startNode 流程起始节点,缺省时默认第一个节点
* @time 2023-09-15 10:43
*/
@Override
public void startFlow(Long businessKey, Long signee, String startNode, String testCode) {
//查询当前流程是否存在
if (checkTask(businessKey)){
throw exception(FLOW_IS_CREATED);
}
List<String> nodes = getNodes();
FlowNodesDO nodesDO = new FlowNodesDO();
//流程节点存储
nodesDO.setBusinessKey(businessKey);
nodesDO.setModelType(nodes);
//任务主表
FlowTaskDO flowTask = new FlowTaskDO();
flowTask.setSignee(signee);
flowTask.setBusinessKey(businessKey);
//通过
flowTask.setOperate(1);
//检测项目
flowTask.setTestCode(testCode);
FlowHisCreateReqVO flowHisCreateReqVO = BeanUtil.copyProperties(flowTask, FlowHisCreateReqVO.class);
if (startNode != null) {
flowTask.setModelType(startNode);
//下个节点获取 ,即将前往的流程节点
int currentNodeIndex = getNodeIndex(nodes,startNode);
if (currentNodeIndex >= nodes.size() - 1) {
//即将前往的节点为流程的最后一个节点,则不存在下个节点
flowTask.setNextNode(null);
} else {
flowTask.setNextNode(nodes.get(currentNodeIndex + 1));
}
} else {
flowTask.setModelType(nodes.get(0));
flowTask.setNextNode(nodes.get(1));
}
//创建任务记录
flowTaskMapper.insert(flowTask);
flowNodesMapper.insert(nodesDO);
//流程历史记录处理补全
//起始环节为空
flowHisCreateReqVO.setModelType(null);
//历史记录中的下个环节设置为当前所在的环节-需求设计
flowHisCreateReqVO.setNextNode(flowTask.getModelType());
flowHisCreateReqVO.setRemark("发起流程");
flowHisService.createFlowHis(flowHisCreateReqVO);
}
/**
* @description
* @author lvyq
* @param[1] completeVO
* @throw 接口增强,追加判断,当前判断方法可有可无,之后的实现已根据该方法中的逻辑进行了相应处理,
* 此方法仅做为接口增强
* @time 2023-09-15 9:20
*/
private void taskValCheck(FlowTaskRejectVO completeVO) {
Integer isInOrder = completeVO.getIsInOrder();
if (completeVO.getRejectModelType() != null) {
//指定节点退回,
//isOrder允许缺省,缺省时默认为1,按序流转 0 非按序流转
if (isInOrder != null && isInOrder != 1 && isInOrder != 0) {
throw exception(new ErrorCode(20230915, "非法参数{isInOrder}"));
}
} else {
//非指定节点退回,判断,【需求设计】不允许移除流程节点,isInOrder缺省处理.以下逻辑已在相应实现中进行了处理,此处仅作为提示使用
if (completeVO.getExcludeModelType() != null && completeVO.getExcludeModelType().size() > 0) {
throw exception(new ErrorCode(20230915, "非法参数{excludeModelType}"));
}
//isInOrder 缺省处理即可,{1} 按序
if (isInOrder != null) {
throw exception(new ErrorCode(20230915, "非法参数{isInOrder}"));
}
}
}
/**
* @throws
* @description
* @author lvyq
* 按顺驳回
* @time 2023-09-15 9:10
*/
private void rejectInOrder(List<String> nodes, FlowTaskDO taskDO) {
//非指定回退 默认退回上一步 下标位置
int oldNodeIndex = getNodeIndex(nodes,taskDO.getModelType());
//当前节点
if (oldNodeIndex == 0) {
//首节点,无法驳回
throw exception(FLOW_ILLEGAL_OPERATION);
} else {
taskDO.setNextNode(nodes.get(oldNodeIndex));
taskDO.setModelType(nodes.get(oldNodeIndex - 1));
}
}
/**
* @throws
* @description
* @author lvyq
* 驳回到指定节点
* @time 2023-09-15 8:45
*/
private void rejectSpecifyNode(List<String> nodes, FlowTaskDO taskDO, String rejectModelType, Integer isInOrder) {
//将要驳回至的节点
String nextNode = taskDO.getModelType();
taskDO.setModelType(rejectModelType);
if (isInOrder == 0) {
//非顺序 即回到驳回点
taskDO.setNextNode(nextNode);
} else {
//按序
int nodeIndex = getNodeIndex(nodes,rejectModelType);
taskDO.setNextNode(nodes.get(nodeIndex + 1));
}
}
/**
* @return
* @throws
* @description
* @author lvyq
* 校验数据是否存在,存在则返回信息
* @time 2023-09-14 17:24
*/
private FlowTaskDO validateFlowTask(@NotNull(message = "businessKey不能为空") Long businessKey) {
FlowTaskDO taskDO = flowTaskMapper.selectOne(FlowTaskDO::getBusinessKey, businessKey);
if (taskDO == null) throw exception(FLOW_TASK_NOT_EXISTS);
return taskDO;
}
/**
* @return List<String>
* @throws
* @description
* @author lvyq
* 最新流程节点处理
* @time 2023-09-14 17:03
*/
private List<String> checkCurrentNodes(FlowTaskRejectVO completeVO) {
//TODO 流程节点,获取全部流程节点
List<String> nodesBefore = getNodes();
List<String> currentNodes = new ArrayList<>();
if (completeVO.getExcludeModelType() != null && completeVO.getExcludeModelType().size() > 0) {
//更新流程节点
nodesBefore.stream().filter(str -> !completeVO.getExcludeModelType().contains(str)).forEach(currentNodes::add);
} else {
currentNodes = nodesBefore;
}
//bug -2023.09.18 解决通过其它节点,未设置移除的节点时,仍使用上一个驳回所设置的流程节点问题。每次驳回都进行流程节点设置
updateFlowNodes(currentNodes,completeVO.getBusinessKey());
return currentNodes;
}
/**
* @throws
* @description
* @author lvyq
* 更新FlowNodes ,用于在审核通过时查询最新的流程
* @time 2023-09-14 17:10
*/
private void updateFlowNodes(@NotNull(message = "非法操作,流程节点不可为空") List<String> nodes, @NotNull(message = "onlineDataId不能为空") Long onlineDataId) {
FlowNodesDO nodesDO = flowNodesMapper.selectOne(FlowNodesDO::getBusinessKey, onlineDataId);
nodesDO.setModelType(nodes);
flowNodesMapper.updateById(nodesDO);
}
private List<String> getNodes() {
List<String> nodes = new ArrayList<>();
String flowNodes = stringRedisTemplate.opsForValue().get("flowNode:"+SecurityFrameworkUtils.getLoginUser().getTenantId().toString());
String[] split = flowNodes.split(",");
for (String s : split) {
nodes.add(s);
}
return nodes;
}
/**
* @description 任务指派 -当前节点任务指派
* @author lvyq
* @param[1] completeVO
* @throws
* @time 2023-09-25 14:26
*/
@Override
public void assignTask(AssignTaskVo completeVO) {
FlowTaskDO taskDO = validateFlowTask(completeVO.getBusinessKey());
//修改数据
taskDO.setSignee(completeVO.getSignee());
flowTaskMapper.updateById(taskDO);
}
/**
* @description 获取节点所在的下标
* @author lvyq
* @throws
* @return Integer
* @time 2023-09-26 11:40
*/
public Integer getNodeIndex(List<String> nodes,String node){
int[] nodeIndexArr = ListUtil.indexOfAll(nodes, node::equals);
int nodeIndex = nodeIndexArr[0];
return nodeIndex;
}
/**
* @description 重新发起流程-逻辑抽离
* @author lvyq
* @param[1] businessKey
* @param[2] delHis
* @throws
* @time 2023-09-26 15:23
*/
@Override
@Transactional
public void reStartFlow(Long businessKey, Integer delHis) {
//TODO 判断节点, 需求 - 审核节点,待优化成可配选项
String reSetNode="doExamine";
//获取当前任务节点
FlowTaskDO taskDO = flowTaskMapper.selectOne(FlowTaskDO::getBusinessKey, businessKey);
FlowHisCreateReqVO flowHisCreateReqVO = BeanUtil.copyProperties(taskDO, FlowHisCreateReqVO.class);
//存在任务节点,判断是否需要修改节点
if (taskDO!=null){
String modelType = taskDO.getModelType();
//判断当前节点是否是审核后的节点,如果是审核后的则将流程数据流转至审核节点上,同时将业务流转节点重置
List<String> nodes = flowNodesMapper.selectOne(FlowNodesDO::getBusinessKey, businessKey).getModelType();
Integer nodeIndex = getNodeIndex(nodes, modelType);
Integer reSetIndex = getNodeIndex(nodes, reSetNode);
if (delHis==1){
flowHisMapper.deleteByBusinessKey(businessKey);
}
if (nodeIndex<=reSetIndex){
flowHisCreateReqVO.setNextNode(taskDO.getModelType());
flowHisCreateReqVO.setModelType(null);
flowHisCreateReqVO.setRemark("试验修改【流程不变】");
}else {
//重置
flowTaskMapper.deleteByBusinessKey(businessKey);
flowNodesMapper.deleteByBusinessKey(businessKey);
//流程节点存储
List<String> newNodes = getNodes();
FlowNodesDO nodesDO = new FlowNodesDO();
nodesDO.setBusinessKey(businessKey);
nodesDO.setModelType(newNodes);
//任务主表
FlowTaskDO flowTask = new FlowTaskDO();
flowTask.setBusinessKey(businessKey);
flowTask.setTestCode(taskDO.getTestCode());
Integer nodeIndex1 = getNodeIndex(newNodes, reSetNode);
flowTask.setModelType(newNodes.get(nodeIndex1));
flowTask.setNextNode(newNodes.get(nodeIndex1+1));
//创建任务记录
flowTaskMapper.insert(flowTask);
flowNodesMapper.insert(nodesDO);
//起始环节为老数据的当前环节
flowHisCreateReqVO.setModelType(taskDO.getModelType());
//下个环节设置为当前所在的环节-需求设计
flowHisCreateReqVO.setNextNode(flowTask.getModelType());
flowHisCreateReqVO.setRemark("试验修改【流程变更】");
}
flowHisService.createFlowHis(flowHisCreateReqVO);
}
}
}
4.任务流程记录
任务流程记录比较简单,只是简单的增删改查。就不具体列举了。
5.接口说明
主要接口功能说明,更多操作看service
1️⃣ 发起流程
/startFlow | 发起流程 |
参数 | 说明 |
businessKey | 业务key |
signee | 处理人 |
startNode | 流程起始节点,缺省 nodes[0] |
testCode | 项目编号(任务组) |
/complete | 完成当前流程 |
参数 | 说明 |
businessKey | 业务key |
signee | 处理人(可缺省) |
startNode | 流程起始节点,缺省 nodes[0] |
variable | 变量 |
/reject | 任务驳回 |
参数 | 说明 |
businessKey | 业务key |
variable | 变量 |
isInOrder | 是否按序流转 0 否 ,退回至某个节点>完成>流转至退回前节点 权重为1 1.是, 按序流转,配合 excludeModelType 为按序流转节点中需要剔除的流程节点 |
rejectModelType | 指定退回节点 缺省,驳回到上个节点,并按序流转 |
excludeModelType | 剔除节点,与 isInOrder=1 时,搭配使用 缺省时按1处理 |
/page | 任务代办列表 |
参数 | 说明 |
businessKey | 业务key |
operate | 操作类型 0 驳回 1 通过 |
modelType | 流程节点 |
testCode | 任务组 |
/assignTask | 任务指派 |
参数 | 说明 |
businessKey | 业务key |
signee | 被指派人 |