最近在做一个企业管理系统的外包,该管理系统主要分为两个端,管理端(web端)和生产端(移动端)。管理端的功能有人员管理、项目管理、工作量管理、审批流程管理等,生产端就是给员工用的,功能有上报工作量,请假,调岗,打卡等。生产端的一个特色就是有一个审批的功能,员工提交的各个模块的申请都要走审批流程,审批流程通过后,该申请才算生效。
一个完整的审批过程需要这几类角色:申请人、申请详情、审批流程、审批规则。
申请人点击app上面的某个申请,比如请假,然后填入必要的内容作为申请详情,比如请假原因,请假时间等(不同模块的申请,有着不同的申请详情),接着申请人指定一个审批流程后提交。审批流程中的每个审批人收到待审批的待办后依次按规则审批,通过后申请生效,驳回后申请失效。通过我的申请,已审批等页面可以查看审批详情。
无论是申请人,还是审批人,对应到数据库都算是一个用户,所以理所当然要有用户表。
-- users表
CREATE TABLE `users` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '员工编码',
`username` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户名/员工号',
`password` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '密码',
`phone` char(11) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '手机号',
`name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '员工姓名',
`dept_id` bigint(20) DEFAULT NULL COMMENT '部门编码',
`dept_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '部门名称',
`postion_id` bigint(20) DEFAULT NULL COMMENT '职位编码',
`postion_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '职位名',
`base_salary` decimal(10,2) DEFAULT NULL COMMENT '基础薪资',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`modify_time` datetime DEFAULT NULL COMMENT '修改时间',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登陆时间',
`status` tinyint(4) DEFAULT '0' COMMENT '员工状态 0有效 1无效',
PRIMARY KEY (`user_id`) USING BTREE,
UNIQUE KEY `users_username` (`username`) USING BTREE,
KEY `users_dept_id` (`dept_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
由于审批详情和功能模块绑定,且与审批一一对应,所以使用相同的id就可以定位到某个审批及其审批详情。而数据库就需要一张审批表(用来记录某一个审批的全部信息)和多张审批详情表(请假详情,工作量详情等)来存储数据。
审批表
-- approval表
CREATE TABLE `approval` (
`approval_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '审批编码',
`module_id` tinyint(4) NOT NULL COMMENT '模块编码',
`applicant_id` bigint(20) DEFAULT NULL COMMENT '申请人编码',
`applicant_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '申请人名字',
`approver_id` bigint(20) DEFAULT NULL COMMENT '审批人编码',
`approver_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '审批人名字',
`process_id` bigint(20) DEFAULT NULL COMMENT '审批流程编码',
`levels` tinyint(4) DEFAULT NULL COMMENT '等级',
`reject_reason` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '审批驳回原因',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`modify_time` datetime DEFAULT NULL COMMENT '修改时间',
`complete_time` datetime DEFAULT NULL COMMENT '审批完成时间',
`status` tinyint(4) DEFAULT '0' COMMENT '审批状态 0审批中 1审批通过 2审批驳回',
PRIMARY KEY (`approval_id`) USING BTREE,
KEY `index_approval_applicant` (`applicant_id`) USING BTREE,
KEY `index_approval_approver` (`approver_id`) USING BTREE,
KEY `index_approval_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=168 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
审批详情表(工作量)
在上面审批表显示的数据中其审批id为142,对应的模块id为0,也就是工作量模块(可以自定),其对应的审批详情表为work_report,而这条数据的id也是142。相应的,其他审批详情表(比如请假,调岗,离职等)也是一样。
-- work_report表 该表为工作量表,
CREATE TABLE `work_report` (
`work_report_id` bigint(20) NOT NULL COMMENT '报工编码',
`applicant_id` bigint(20) DEFAULT NULL COMMENT '报工申请人编码',
`applicant_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '申请人名字',
`sum_workload_price` decimal(10,2) DEFAULT NULL COMMENT '报工总价',
`project_id` bigint(20) DEFAULT NULL COMMENT '项目编码',
`project_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '项目名称',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`modify_time` datetime DEFAULT NULL COMMENT '修改时间',
`complete_time` datetime DEFAULT NULL COMMENT '审批完成时间',
`status` tinyint(4) DEFAULT NULL COMMENT '报工状态 0审批中 1审批通过 2审批驳回',
PRIMARY KEY (`work_report_id`) USING BTREE,
KEY `index_work_report_user_id` (`applicant_id`) USING BTREE,
KEY `index_work_report_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
审批流程实际上是一个个的审批人还有标志其先后顺序的审批等级。可以通过id和top_id来组织这张表。每一个process_id确定一个审批人,每一个top_id确定一个审批记录,如下图所示。
-- process表
CREATE TABLE `process` (
`process_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '流程id',
`top_id` bigint(20) DEFAULT NULL COMMENT '最顶级id',
`approver_id` bigint(20) DEFAULT NULL COMMENT '审核人id',
`approver_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '审核人名字',
`next_id` bigint(20) DEFAULT NULL COMMENT '下一流程id(null表示无后序流程)',
`levels` tinyint(4) DEFAULT '0' COMMENT '流程级别(从1开始)',
PRIMARY KEY (`process_id`) USING BTREE,
KEY `process_top_id` (`top_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=127 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
我们看到无论是审批表还是详情表,都有一个status字段,来标志审批状态。
通过模块化的思想,可以将审批详情分为多张表。这是从审批系统的角度思考,而从各个模块的角度去想,因其都需要进行审批,所以只用一张表来存储就行,不需要为其单独的建表。
而所谓的审批规则是根据业务场景确定的,比如我这个项目,审批规则就是最简单的分级审批,每个申请只有一个审批流程,审批流程中每一级只有一个审批人,任何一级审批驳回后整个申请就宣告结束。其实只要有了基础的审批表、审批详情表、审批流程表,任何审批规则都是可以实现的,无非是复杂程度不同。
如果有查看每一级审批记录的需求,也可以再建立一张审批历史表,其中记录了每一级审批的审批人、时间等信息。
-- approval_history表
CREATE TABLE `approval_history` (
`approval_id` bigint(20) NOT NULL COMMENT '审批编码',
`approver_id` bigint(20) NOT NULL COMMENT '审批人编码',
`approver_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '审批人名字',
`process_id` bigint(20) DEFAULT NULL COMMENT '审批流程编码',
`levels` tinyint(4) DEFAULT NULL COMMENT '等级',
`complete_time` datetime DEFAULT NULL COMMENT '审批完成时间',
`status` tinyint(4) DEFAULT '0' COMMENT '审批状态 0审批通过 1审批驳回',
PRIMARY KEY (`approval_id`,`approver_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
通过对用例的分析,设计出了五张表。分别是用户表、审批表、审批详情表、审批流程表和审批历史表。
在审批表和审批详情表之间通过相同的id进行数据绑定,并且根据模块id来确定不同的审批详情。接下来就是设计功能,来支撑起整个审批系统了。
对于整个审批的操作大致可以分为四块,即:1 上报申请,2 通过申请,3 驳回申请,4 查看申请详情。
对应到数据库层面的操作就是:1 开启一级审批流程;生成审批表记录;审批详情表记录,2 修改审批状态为通过,3 修改审批状态为驳回,4 查询审批表及其详情。
最难的是上报,其次是查询,最后是通过/审批,体现在代码层面就是其代码行数的减少。而整个线性的审批流程,也正对应着其开发流程。
抽象出来的四个操作,完全适用于每个功能模块,对应到代码层面就是四个接口。前端完全可以通过这四个接口来完成不同功能模块审批操作,只需要向后端传输相应的模块id即可。该抽象思想,也可以用在数据校验上,后面做数据校验时也会用到类似抽象的思想。
话不多说上代码
上报的流程大致分为三个:1 生成审批,2 生成审批详情,3 通知第一审批人审批
/**
* 审批上报
*
* @param input
* @return
*/
@RequestMapping(value = "/reporting", method = RequestMethod.POST)
@Approval(opType = Approval.REPORTING)
@Transactional
public RespBean reporting(@RequestBody Input input) {
// 开启审批流程
approvalService.addApproval(input);
// 上报
input.getService().reporting(input);
// 生成待办
todoService.generateTodo(input);
return RespBean.ok("上报成功");
}
approvalService:用来操作审批,主要有以下四个操作
public interface IApprovalService {
// 生成审批
void addApproval(Input input);
// 查询审批详情
JSONObject getApprovalDetail(Input input);
// 审批通过
void passApproval(Input input);
// 审批驳回
void rejectApproval(Input input);
}
在上报接口中,调用addApproval方法使用其来生成审批,并返回其id,来供下游生成同id的审批详情。
接着就是模块的上报流程,其实就是从入参中获取对用service层的类,调用reporting方法。而不同的service都会实现同一个接口:IApprovalDetailService,实现该接口的业务类,执行不同的业务操作。
public interface IApprovalDetailService {
// 执行上报
void reporting(Input input);
// 查询审批详情
JSONObject todoDetail(JSONObject res);
// 完成整个审批
void pass(Input input);
// 审批驳回
void reject(Input input);
}
上报完成后,就可以调用todoService生成待办给审批人看了
public interface ITodoService {
// 生成待办
void generateTodo(Input input);
// 审批通过 完成代办
void pass(Input input);
// 审批驳回 完成待办
void reject(Input input);
// 查询待办
Todo getTodo(Long todoId, Long userId);
}
其余三个操作的流程附上
/**
* 查看审批详情
*
* @param input
* @return
*/
@RequestMapping(value = "/todoDetail", method = RequestMethod.POST)
@Approval(opType = Approval.TODO_DETAIL)
public RespBean todoDetail(@RequestBody Input input) {
// 查询审批详情
JSONObject approvalRes = approvalService.getApprovalDetail(input);
// 查询业务详情
JSONObject res = input.getService().todoDetail(approvalRes);
return RespBean.ok("查询成功", res);
}
/**
* 审批通过
*
* @param input
* @return
*/
@RequestMapping(value = "/pass", method = RequestMethod.POST)
@Approval(opType = Approval.PASS)
@Transactional
public RespBean pass(@RequestBody Input input) {
// 完成待办
todoService.pass(input);
// 通过审批 并 尝试推进下一审批
approvalService.passApproval(input);
if (null == input.getApproval()) {
// 完成整个审批流程
input.getService().pass(input);
} else {
// 如果审批未完成 就生成待办(新审批)
todoService.generateTodo(input);
}
return RespBean.ok("审批通过成功");
}
/**
* 审批驳回
*
* @param input
* @return
*/
@RequestMapping(value = "/reject", method = RequestMethod.POST)
@Approval(opType = Approval.REJECT)
public RespBean reject(@RequestBody Input input) {
// 完成代办
todoService.reject(input);
// 结束审批流程
approvalService.rejectApproval(input);
// 结束业务流程
input.getService().reject(input);
return RespBean.ok("审批驳回成功");
}
在四大接口执行之前,需要做的事也有很多,如参数校验,参数注入。而这些操作通过AOP可以统一去做。
只需要根据前端传入的模块id,就能够确定相应的校验器和业务类。
AOP中的代码复用,使用到了java反射,通过获取到运行时触发方法上的注解,来判断调用了哪个接口,从而注入不同的参数,供下游使用。
比如在校验时,通过实现定义好的枚举类,使用前端传入的模块id找到相应的beanName,然后通过ioc容器获取对应的校验器bean,执行相应的校验方法。而不同模块的校验器实现已经做好了参数校验,下游直接执行业务代码即可。
Valid接口
/**
* @author: theTian
* @date: 2020/11/12 15:02
*/
public interface Valid {
/**
* 上报验证
* @param input
*/
void validReporting(Input input);
/**
* 查询审批验证
* @param input
*/
default void validTodoDetail(Input input){};
/**
* 审批通过验证
* @param input
*/
default void validPass(Input input){};
/**
* 审批驳回验证
* @param input
*/
default void validReject(Input input){};
}
在获取相应的业务bean时,也用到了相同的想法,即通过模块id找到对应的业务bean。其中获取bean借助到了ioc容器,所以枚举类中的beanName要和ioc中的beanName一致。
/**
* 审批四个接口的参数校验及参数注入
*
* @param point
* @throws Throwable
*/
@Around("execution(* x.x.controller.ApprovalController.*(x.x.x.Input))")
public Object approvalValid(ProceedingJoinPoint point) throws Throwable {
Input input = (Input) point.getArgs()[0];
logger.info(point.getSignature().getName() + "入参:" + input);
// 根据注解注入参数
Signature signature = point.getSignature();
MethodSignature msg = (MethodSignature) signature;
Method method = point.getTarget().getClass().getMethod(msg.getName(), msg.getParameterTypes());
Approval annotation = method.getAnnotation(Approval.class);
String opType = annotation.opType();
if (StringUtils.isNotEmpty(opType)) {
input.setUser(UserUtils.getUser());
input.setValidMethod(opType);
}
// 校验
ValidUtils.approvalValid(input);
// 获取service
ApprovalService service = ServiceFactory.getService(input.getModule().getModuleId());
JgerpAssert.notNull(service, "不存在该服务");
// 装填service
input.setService(service);
// 执行后序方法
RespBean res = (RespBean) point.proceed();
logger.info(point.getSignature().getName() + "出参:" + JSONObject.toJSONString(res));
return res;
}
核心代码:
// 校验
ValidUtils.approvalValid(input);
// 获取service
ApprovalService service = ServiceFactory.getService(input.getModule().getModuleId());
在参考网上的博客,结合项目实际情况后逐渐找到了思路。其实就是在设计各种功能的时候考虑到要做模块化,把审批系统,各个功能模块独立出来,一定要降低模块之间的耦合度,并提升自身模块的复用性,这样的好处就是在开发了审批系统之后开发新的功能模块时,就可以直接接入已经做好的审批系统,从而达到代码复用。
可是知易行难,真正落实到代码层面,需要注意很多的细节。
比如,由于是前后端分离开发,要考虑到各个功能模块的数据校验。起码不能因为字段过长而导致插入数据库失败,与此同时也要考虑特殊字符处理,业务规则校验等。
再比如审批系统设计的要足够独立,对于新模块的开发,要做到一次开发,处处使用。这就需要运用到OOP以及AOP的思想。
随着项目的推进,如今一期的基本功能都已经实现了,但仅仅只做到了够用,自己的设计肯定不是业界最佳方案,也可能隐藏着诸多的漏洞,但是从需求分析,功能设计,数据库设计,功能开发和测试,在这个过程中通过思考问题并实践验证,确实收获很多。