1. 业务场景:
公司为开发一套适用性强的审批系统,并且灵活性可以高一点,灵活性高指的是:我们审批流通常是xxx人发起审批单,A审批=>B审批=》C审批通过,这个审批单才算结束,但防中间有一个审批人拒绝,这审批单也结束,我们想制作成,可以自定义的选择审批顺序,已达到灵活的选择审批人的目的。
例如:项目场景:项目中有用户表oa_user,底下关联着角色表oa_role,另有客户表,我们登录的用户这边指的是业务员,也有可能登录的这个用户是客户,区分以这个用户的一个字段customer_type如果是2的话他就是客户,等于1的话就是业务员。
具体的表关系如下:
2. 数据库设计:
1. 流程主表
2. 流程节点表
3. 流程线表
4. 流程节点角色表
5. 审批单
5. 审批单明细
3. 开发流程分析:
3.1 模块1:Flow的设计
- Flow的新增
Flow表的新增涉及到三张表同步新增,分别是flow,flow_node,flow_line。
我们新增一条数据:
flow表:flow_no应该做限制不能重复
flow_node表:这里的数据是flow的审核人数据
flow_line表:这里的数据是审核人具体的审核顺序,第一个审核人的pre_node_id设置为0,最后审核人的next_node_id设置成-1
VO的设计原则:尽可能让更多的地方能够共用同一个VO
考虑到我们单个Flow的详情需要展示:flow_node,flow_node_role,而插入只需要插入flow_node和flow,于是VO可以设计成FlowInfoVO ,其中FlowVO-Flow,FlowNodeVO-FlowNode,FlowNodeRoleVO-FlowNodeRole
以Bean一一对应
//同时包含flow,flow_node,flow_node_role信息
public class FlowInfoVO extends FlowVO{
List<FlowInfoVO> flowInfoVO ;
}
FlowInfoVO extends FowVO{
//包含flow信息
List<FlowNodeInfoVO> flowNodeInfoVO ;
}
FlowNodeInfoVO extend FlowNodeVO{
//包含角色信息
List<FlowNodeRoleVO> flowNodeRoleVO;
}
插入的细节:
public void insertFlowNodeInfoVO(FlowInfoVO flowInfoVO) {
//flow的插入
Flow flow = new Flow();
BeanUtils.copyProperties(flowInfoVO,flow);
this.save(flow);
//flow_node的擦插入
List<FlowNode> flowNodeList = new ArrayList<>();
List<FlowNodeInfoVO> flowNodeInfoVOList = flowInfoVO.getFlowNodeInfoVOList();
for (FlowNodeInfoVO flowNodeInfoVO : flowNodeInfoVOList) {
FlowNode flowNode = new FlowNode();
BeanUtils.copyProperties(flowNodeInfoVO,flowNode);
flowNode.setFlowNo(flow.getFlowNo());
flowNodeList.add(flowNode);
}
flowNodeService.saveBatch(flowNodeList);
//flow_line的插入
flowNodeList = flowNodeService.list(new QueryWrapper<FlowNode>().eq("flow_no",flowInfoVO.getFlowNo()));
List<FlowLine> flowLineList = new ArrayList<>();
for (int i = 0; i < flowNodeList.size(); i++) {
FlowLine flowLine = new FlowLine();
flowLine.setFlowNo(flow.getFlowNo());
flowLine.setFlowNodeId(flowNodeList.get(i).getFlowNodeId());
if(i == 0){
flowLine.setPrevNodeId(0);
}else{
flowLine.setPrevNodeId(flowNodeList.get(i-1).getFlowNodeId());
}
if( i == flowNodeList.size() - 1){
flowLine.setNextNodeId(-1);
}else{
flowLine.setNextNodeId(flowNodeList.get(i + 1).getFlowNodeId());
}
flowLineList.add(flowLine);
}
flowLineService.saveBatch(flowLineList);
}
- Flow的分页查询
分页查询这边再多查询一些内容,开始只是查询flow的数据,这边多展示一下flow下面的flow_node,当然是根据flow_line排序好了,为了是为了到时候修改的时候不需要重新请求接口插叙这些节点的顺序。
public IPage<FlowInfoVO> selectPageFlow(int pageNum, int pageSize,String flowNode,String flowName) {
//获取流程表
IPage<Flow> page = this.page(new Page<>(pageNum, pageSize));
List<Flow> records = page.getRecords();
if(records.size() == 0){
return new Page<FlowInfoVO>();
}
//取出流程代码集合
List<String> collect = records.stream().map(a -> a.getFlowNo()).collect(Collectors.toList());
//查询带有顺序的流程节点
List<FlowNodeInfoVO> flowNodeVOS = flowNodeMapper.selectFlowNodeVOList(collect);
Map<String, List<FlowNodeInfoVO>> map = flowNodeVOS.stream().collect(Collectors.groupingBy(FlowNodeInfoVO::getFlowNo));
IPage<FlowInfoVO> iPage = new Page<>(pageNum, pageSize);
iPage.setTotal(page.getTotal());
iPage.setCurrent(page.getCurrent());
//流式编程
List<FlowInfoVO> flowInfoVOList = records.stream().map(i -> {
FlowInfoVO flowInfoVO = new FlowInfoVO();
BeanUtils.copyProperties(i, flowInfoVO);
flowInfoVO.setFlowNodeInfoVOList(map.get(flowInfoVO.getFlowNo()));
return flowInfoVO;
}).collect(Collectors.toList());
iPage.setRecords(flowInfoVOList);
return iPage;
}
- 单个Flow的顺序修改
修改这边更新的是flow_line,所以我们就必须传递flow_node_id,分页查询也必须查出来,内部逻辑是flow_line对应flow_no对应的全部清掉,重新插入新的flow_line数据
public void updateFlowLineVO(List<FlowLineVO> flowLineVOList) {
List<FlowLine> flowLineList = new ArrayList<>();
//如果只有一个,那就没修改的必要的了
if(flowLineVOList.size() == 1) return;
//将修改后的节点的前驱和后继节点进行更新
for (int i = 0; i < flowLineVOList.size(); i++) {
FlowLine flowLine = new FlowLine();
if(i == 0){
flowLineVOList.get(i).setPrevNodeId(0);
flowLineVOList.get(i).setNextNodeId(flowLineVOList.get(i + 1).getFlowNodeId());
}else if(i == flowLineVOList.size()-1){
flowLineVOList.get(i).setNextNodeId(-1);
flowLineVOList.get(i).setPrevNodeId(flowLineVOList.get(i - 1).getFlowNodeId());
}else{
flowLineVOList.get(i).setNextNodeId(flowLineVOList.get(i + 1).getFlowNodeId());
flowLineVOList.get(i).setPrevNodeId(flowLineVOList.get(i - 1).getFlowNodeId());
}
BeanUtils.copyProperties(flowLineVOList.get(i),flowLine);
flowLineList.add(flowLine);
}
List<Integer> collectIds = flowLineVOList.stream().map(a -> a.getFlowLineId()).collect(Collectors.toList());
//删除原来表中的顺序
flowLineService.removeByIds(collectIds);
//批量保存,修改后的流程顺序
flowLineService.saveBatch(flowLineList);
}
- 单个Flow的详情
这边的查询嵌套就比较多了,条件我们应该是flowId,查询步骤:
先查询当前flowId对应的节点(有顺序的),再根据这些节点id查询对应的角色,最后分组保存!
public FlowInfoVO selectFlowInfoVO(Integer flowId) {
/*step1:查询流程详情*/
FlowInfoVO flowInfoVO = new FlowInfoVO();
Flow flow = baseMapper.selectById(flowId);
flowInfoVO.setFlowNodeInfoVOList( baseMapper.selectFlowInfoVOById(flowId));
//获取流程节点id
List<Integer> collect = flowInfoVO.getFlowNodeInfoVOList().stream().map(a -> a.getFlowNodeId()).collect(Collectors.toList());
/*step2:查询节点id对应的节点角色信息*/
//取出个节点对应的角色
List<FlowNodeRoleVO> flowNodeRoles = flowNodeRoleMapper.selectFlowNodeRoleVO(collect);
/*step3:查询节点id对应的节点角色信息*/
//分组节点id分组角色
Map<Integer,List<FlowNodeRoleVO>> map =flowNodeRoles.stream().collect(Collectors.groupingBy(FlowNodeRoleVO::getFlowNodeId));
//各节点角色数据填充
flowInfoVO.getFlowNodeInfoVOList().stream().forEach(i->{
flowInfoVO.setFlowNo(flow.getFlowNo());
flowInfoVO.setFlowName(flow.getFlowName());
flowInfoVO.setRemark(flow.getRemark());
flowInfoVO.setFlowId(flow.getFlowId());
i.setFlowNodeRoleVOList(map.get(i.getFlowNodeId()));
});
return flowInfoVO;
}
- 节点角色的插入
这里要做的是插入每个节点对应的角色,操作的是flow_node_role表,看似插入,其实重置该节点对应的角色,也就是说先删除,再插入。
public void addRoles(Integer flowNodeId, List<Integer> roleIds) {
List<FlowNodeRole> flowNodeRoles = new ArrayList<>();
for (Integer roleId : roleIds) {
FlowNodeRole flowNodeRole1 = new FlowNodeRole();
flowNodeRole1.setFlowNodeId(flowNodeId);
flowNodeRole1.setRoleId(roleId);
flowNodeRoles.add(flowNodeRole1);
}
flowNodeRoleService.remove(new QueryWrapper<FlowNodeRole>().eq("flow_node_id",flowNodeId));
flowNodeRoleService.saveOrUpdateBatch(flowNodeRoles);
}
注意:如果对于主键关联性比较强的,应尽量避免先删除,后插入!!
3.2 模块2:审批单的接入
- 审批单提交
审批单提交对于客户的话,只需要往审批单表中插入一条数据即可,但是对应业务员申请的话,需要走审批流程,就需要同时去更新审批单,审批单明细这两张表的数据,而这这两张表的一些数据是来自于Flow流程中的节点数据。
/***************************************************************** * 函数名: insertUrgentOrder * 功能 : {toDo:插入到急件记录表中} * 作者 :lzy 2020/10/23 * 参数表 : * @param projectCardId :工程卡id * @param customerDataId : 客户id * @param type : 1客户 2业务员 * @param salesmanId : 业务员id * 返回值: * @return : void * * 修改记录: * 日期 修改人 修改说明 ******************************************************************/
public void insertUrgentOder(Integer projectCardId, Integer customerDataId, Integer salesmanId,Integer type) {
//获取该审批的第一个节点
FlowNodeVO urgentFlow = flowNodeMapper.selectFirstOrLast("urgent_flow", 0);
ProjectCardUrgent projectCardUrgent = new ProjectCardUrgent();
projectCardUrgent.setProjectCardId(projectCardId);
projectCardUrgent.setCustomerDataId(customerDataId);
projectCardUrgent.setStatus(1);
projectCardUrgent.setCreateId(ShiroUtils.getUserInfo().getUserId());
projectCardUrgent.setCreateTime(new Date());
//设置客户或业务员申请急件
if (type == 1) {
projectCardUrgent.setApplyMan(customerDataMapper.selectById(customerDataId).getCustomerFullName()); //设置为客户为自己申请
} else {
projectCardUrgent.setFlowNo("urgent_flow");
projectCardUrgent.setCurrentNode(urgentFlow.getFlowNodeId());
projectCardUrgent.setDocumentStatus(0); //未审核
projectCardUrgent.setApplyMan(oaUserMapper.selectById(salesmanId).getUserName()); //设置为业务员申请
}
projectCardUrgent.setType(type);
//插入急件
projectCardUrgentMapper.insert(projectCardUrgent);
//如果是业务员审批,则需要插入审批单的明细记录
if(type == 2){
ProjectUrgentReviewRecord projectUrgentReviewRecord = new ProjectUrgentReviewRecord();
projectUrgentReviewRecord.setProjectCardUrgentId(projectCardUrgent.getProjectCardUrgentId());
projectUrgentReviewRecord.setApprovalStatus(urgentFlow.getFlowNodeId());
projectUrgentReviewRecord.setApprovalText(urgentFlow.getFlowNodeName());
projectUrgentReviewRecord.setCreateId(ShiroUtils.getUserInfo().getUserId());
projectUrgentReviewRecord.setCreateTime(new Date());
projectUrgentReviewRecord.setProjectCardUrgentId(projectCardUrgent.getProjectCardUrgentId());
projectUrgentReviewRecord.setType(1);
projectUrgentReviewRecordMapper.insert(projectUrgentReviewRecord);
}
}
- 审核记录单查询
这边审核记录单的查询,展示的是当前用户角色是否和节点的角色有包含关系,换句话说,当前用户具备的角色是否有审核单子的权限。
/***************************************************************** * 函数名: toExamineList * 功能 : {toDo: 审核列表} * 作者 :cx139 2020/10/29 * 参数表 : * @param pageNum : * @param pageSize : * @param projectCardNumber : * @param customerData : * 返回值: * @return : com.baomidou.mybatisplus.core.metadata.IPage<com.hgfzp.textile.erp.vo.ProjectCardUrgentVOS> * * 修改记录: * 日期 修改人 修改说明 ******************************************************************/
public IPage<ProjectCardUrgentVO> toExamineList(Integer pageNum, Integer pageSize, String projectCardNumber, String customerData) {
//获取当前登录的用户的角色信息
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("user_id", ShiroUtils.getUserInfo().getUserId());
queryWrapper.eq("status", 1);
List<OaUserRole> oaUserRoles = oaUserRoleMapper.selectList(queryWrapper);
List<Integer> integers = oaUserRoles.stream().map(a -> a.getRoleId()).collect(Collectors.toList());
QueryWrapper<ProjectCardUrgent> qw = new QueryWrapper();
qw.eq("t.type",2);
Page page = new Page(pageNum, pageSize);
IPage<ProjectCardUrgentVO> iPage= baseMapper.toExamineList(page, customerData, projectCardNumber, qw,integers);
return iPage;
}
Mapper层的xml:
<!--审核列表-->
<select id="toExamineList" resultType="com.hgfzp.textile.erp.vo.ProjectCardUrgentVO">
SELECT
t.*,
d.customer_code,
d.customer_abbreviation,
d.customer_full_name,
d.customer_abbreviation,
e.cargo_name,
a.project_card_number,
f.customer_color_number,
f.colour_number_name,
g.user_name oaUser,
fn.flow_node_name,
fn2.flow_node_name nextFlowNodeName,fn2.flow_node_id nextFlowNodeId
FROM
project_card_urgent t
LEFT JOIN project_card a ON a.project_card_id = t.project_card_id
LEFT JOIN dyeing_notice_serial_number b ON a.dyeing_notice_serial_number_id = b.dyeing_notice_serial_number_id
LEFT JOIN embryo_cloth_warehousing c ON b.embryo_cloth_warehousing_id = c.embryo_cloth_warehousing_id
LEFT JOIN customer_data d ON t.customer_data_id = d.customer_data_id
LEFT JOIN cargo_name_information e ON c.cargo_name_information_id = e.cargo_name_information_id
LEFT JOIN colour_number f ON b.colour_number_id = f.colour_number_id
LEFT JOIN oa_user g ON g.user_id = d.salesman_id
LEFT JOIN flow_node fn ON fn.flow_node_id = t.current_node
LEFT JOIN flow_line fl ON fl.flow_node_id = t.current_node
LEFT JOIN flow_node_role fnr ON fl.next_node_id = fnr.flow_node_id
LEFT JOIN flow_node fn2 ON fn2.flow_node_id = fl.next_node_id
<where>
<if test="projectCardNumber != null">
and a.project_card_number like concat(#{projectCardNumber},'%')
</if>
<if test="customerData != null">
and d.customer_full_name like concat(#{customerData},'%')
or d.customer_abbreviation like concat(#{customerData},'%')
or d.customer_code like concat(#{customerData},'%')
</if>
<if test="ew != null">
<if test="ew.SqlSegment != null">
AND ${ew.SqlSegment}
</if>
</if>
AND fnr.role_id IN
<foreach collection="roleIds" item="roleId" index="index" open="(" close=")" separator=",">
#{roleId}
</foreach>
</where>
order by t.project_card_urgent_id desc
</select>
- 审批单的审核
审核需要传递上一个审核通过的人节点currentNode ,和即将要来审核节点nextFlowNodeId ,一定要做一个判断的是:按照正常的流程来说,我们是必不需要判断currentNode != projectCardUrgent.getCurrentNode()
因为它们是一定相等的,但是也有可能出现不等的情况下,如果传入一个已经审批过的节点,此时就会出现审批重复的局面!!
每提交一次审核都会向审批单明细插入一条数据
审批单的current_node也会跟着改变
当全部的流程节点都完成审批,则会去更新主表的result_status
/***************************************************************** * 函数名: toExamine * 功能 : {toDo: 审核} * 作者 :cx139 2020/10/29 * 参数表 : * @param projectCardUrgentId : 工程卡急件id * @param type : 通过1 拒绝2 * @param currentNode : 审批单最近的审核人节点 * @param nextFlowNodeId : 下一个审核人节点 * 返回值: * @return : void * * 修改记录: * 日期 修改人 修改说明 ******************************************************************/
public Result toExamine(Integer projectCardUrgentId, Integer type,Integer currentNode,Integer nextFlowNodeId) throws Exception {
//查询单据信息
ProjectCardUrgent projectCardUrgent = projectCardUrgentService.getById(projectCardUrgentId);
//单据节点与传入节是否一致 projectCardUrgent.getCurrentNode()已审批过的节点
if(currentNode != projectCardUrgent.getCurrentNode()){
return ResultGenerator.genFailResult("当前已审批");
}
/*step1:查询用户角色并查看权限*/
QueryWrapper qw = new QueryWrapper();
qw.eq("user_id", ShiroUtils.getUserInfo().getUserId());
qw.eq("status", 1);
List<OaUserRole> oaUserRoles = oaUserRoleMapper.selectList(qw);
//取出当前登录用户的角色id
List<Integer> integers = oaUserRoles.stream().map(a -> a.getRoleId()).collect(Collectors.toList());
//取出当前审批人的角色
List<FlowNodeRoleVO> flowNodeRoleVOS = flowNodeRoleMapper.selectFlowNodeRoleByFlowNodeId(nextFlowNodeId);
List<Integer> flowRoleIds = flowNodeRoleVOS.stream().map(i->i.getRoleId()).collect(Collectors.toList());
boolean flag = false;
//判断当前审批人是否有做当前审批动作的角色的权限
for (Integer integer : integers) {
if(flowRoleIds.contains(integer)){
flag = true;
break;
}
}
if(!flag){
return ResultGenerator.genFailResult("用户没有审批权限");
}
/*step2:更新审核记录表*/
//下一个节点信息
FlowNodeVO flowNodeVO = flowNodeMapper.selectFlowNodeVOById(nextFlowNodeId);
//审核记录表
ProjectUrgentReviewRecord projectUrgentReviewRecord = new ProjectUrgentReviewRecord();
projectUrgentReviewRecord.setProjectCardUrgentId(projectCardUrgentId);
projectUrgentReviewRecord.setCreateId(ShiroUtils.getUserInfo().getUserId());
projectUrgentReviewRecord.setCreateTime(new Date());
projectUrgentReviewRecord.setType(type);
projectUrgentReviewRecord.setApprovalStatus(nextFlowNodeId);
projectUrgentReviewRecord.setApprovalText(flowNodeVO.getFlowNodeName());
this.save(projectUrgentReviewRecord);
/*step3:审核单更新*/
//主表更新
projectCardUrgent.setProjectCardUrgentId(projectCardUrgentId);
projectCardUrgent.setCurrentNode(nextFlowNodeId);
//如果是最后一个节点或者拒绝
if(flowNodeVO.getNextNodeId() == -1 || type == 2){
//单据结束
projectCardUrgent.setDocumentStatus(2);
}else{
//否则在审批中
projectCardUrgent.setDocumentStatus(1);
}
//设置单据结果状态
projectCardUrgent.setResultStatus(type);
projectCardUrgentMapper.updateById(projectCardUrgent);
/*step4:急件审核单通过,调用排程接口*/
//如果是审批结束,调用排程接口
if(flowNodeVO.getNextNodeId() == -1 && type == 1){
//更新工程卡急件状态
projectCardService.update(new UpdateWrapper<ProjectCard>()
.eq("project_card_id",projectCardUrgent.getProjectCardId()).set("urgent_type",1));
projectCardScheduleCommon.projectCardChangeUrgent(projectCardUrgent.getProjectCardId(),projectCardUrgent.getCustomerDataId());
}
return ResultGenerator.genSuccessResult();
}
- 审批单提交记录查询
记录查询展示的是该单子的审核进度和单子的基本信息
/***************************************************************** * 函数名: selectProjectCardUrgentPage * 功能 : {toDo:工程卡急件分页查询+条件查询} * 作者 : liuzeyu 2020/8/29 * 参数表 : * @param pageNum :页数 * @param pageSize :页大小 * 返回值: * @return : com.hgfzp.textile.common.utils.result.Result<com.baomidou.mybatisplus.core.metadata.IPage<com.hgfzp.textile.erp.vo.ProjectCardUrgentVOS>> * * 修改记录: * 日期 修改人 修改说明 ******************************************************************/
@Override
public Result<IPage<ProjectCardUrgentVO>> selectProjectCardUrgentPages(Integer pageNum, Integer pageSize,
String projectCardNumber, String customerData,Integer isAll) {
List<ProjectCardUrgent> projectCardUrgents = projectCardUrgentMapper.selectList(null);
List<Integer> projectCardIds = projectCardUrgents.stream().map(group -> group.getProjectCardId()).collect(Collectors.toList());
IPage<ProjectCardUrgentVO> projectCardUrgentVOSIPage = null;
QueryWrapper queryWrapper = new QueryWrapper();
if (projectCardNumber != null) {
queryWrapper.likeRight("a.project_card_number", projectCardNumber);
}
if (customerData != null) {
queryWrapper.likeRight("d.customer_full_name", customerData);
}
//默认值为1全查,但是如果等于0的时候就查询业务员的
if(isAll == 0){
queryWrapper.eq("t.create_id",ShiroUtils.getUserInfo().getUserId());
}
projectCardUrgentVOSIPage = projectCardUrgentMapper.selectProjectCardUrgentPages(new Page(pageNum, pageSize), projectCardIds, queryWrapper);
return ResultGenerator.genSuccessResult(projectCardUrgentVOSIPage);
}
对应的xml文件
<!-- 工程卡急件分页记录查询-->
<select id="selectProjectCardUrgentPages" resultType="com.hgfzp.textile.erp.vo.ProjectCardUrgentVO">
SELECT
t.*,
d.customer_code,
d.customer_abbreviation,
d.customer_full_name,
e.cargo_name,
a.project_card_number,
f.customer_color_number,
f.colour_number_name,
g.user_name oaUser,
fn.flow_node_name,
fn2.flow_node_name nextFlowNodeName,fn2.flow_node_id nextFlowNodeId
FROM
project_card_urgent t
LEFT JOIN project_card a ON a.project_card_id = t.project_card_id
LEFT JOIN dyeing_notice_serial_number b ON a.dyeing_notice_serial_number_id = b.dyeing_notice_serial_number_id
LEFT JOIN embryo_cloth_warehousing c ON b.embryo_cloth_warehousing_id = c.embryo_cloth_warehousing_id
LEFT JOIN customer_data d ON t.customer_data_id = d.customer_data_id
LEFT JOIN cargo_name_information e ON c.cargo_name_information_id = e.cargo_name_information_id
LEFT JOIN colour_number f ON b.colour_number_id = f.colour_number_id
LEFT JOIN oa_user g ON g.user_id = d.salesman_id
LEFT JOIN flow_node fn ON fn.flow_node_id = t.current_node
LEFT JOIN flow_line fl ON fl.flow_node_id = t.current_node
LEFT JOIN flow_node_role fnr ON fl.next_node_id = fnr.flow_node_id
LEFT JOIN flow_node fn2 ON fn2.flow_node_id = fl.next_node_id
<where>
<if test="projectCardIds.size() > 0">
a.project_card_id in
<foreach collection="projectCardIds" item="projectCardId" index="index" open="(" close=")" separator=",">
#{projectCardId}
</foreach>
</if>
<if test="ew != null">
<if test="ew.SqlSegment != null">
AND ${ew.SqlSegment}
</if>
</if>
</where>
group by a.project_card_number
order by t.project_card_urgent_id desc
#先分组后排序
</select>
4. 小结
审批系统的内部实现逻辑比较复杂,考虑的问题也会比较多,难点在于审批单的接入。系统也存在很多需要改善的地方,例如:该系统没有修改节点的功能,实则是因为如果存在单子正在审批过程,一修改节点就会导致审批错乱,难以维护。正常的审批系统应该还会有单子驳回的功能,随着学习的深入会继续完善该系统!!