字数:9856
1 引文
以前做过很多审批业务,比如公司实名认证申请
,现在回想起来,那个做的还比较复杂,主要在于
-
(1)有审批历史记录,其实这个也没利用起来。
-
(2)正因为有历史,所以驳回缘由等管理就复杂化了
-
(3)有操作的有效时限管理,不过这个倒不复杂,只是一个字段的事情,不过业务理解上会稍微多了那么一些内容。
本文是想介绍一下一个简单的做法,代码量不多,就可以完成一个审批功能,如果关联信息有多个审批,甚至都能用这套思路。只是加个类型的区分而已。
2 需求
-
(1)社区馆开馆,需要负责人申请,这样就有一个开馆审批;
-
(2)对现有馆的地址和服务参数等修改,也需要审批;
-
(3)如果想退馆,也需要审批。 这样针对馆就有三种审批。
3 实现
分析一下需求,有以下几个特性:
-
(1)要保证有一个主状态,用于对外提供服务,只能有一个最新的状态,其来自于审批中的某一种的变更
-
(2)审批需要单独保存和处理,主状态与其有联动
-
(3)审批不会同时存在,有一定的发展顺序,可以复始,比如退馆之后,又能申请开馆
看一下表结构会更一目了然:
CREATE TABLE `ggh_library` (
`id` bigint(20) NOT NULL,
`id_user` bigint(20) DEFAULT NULL COMMENT '馆长用户编号',
`id_library_apply` bigint(20) DEFAULT NULL COMMENT '对应的最近审批,由于一个馆对应多个审批,但仅最近的审批有效,过去的是历史资料',
`name_library` varchar(30) DEFAULT NULL COMMENT '绘本馆名称',
`type_library` enum('全国馆','区域馆') DEFAULT NULL COMMENT '绘本馆类型,1全国馆,2区域馆',
`status_library` enum('申请开馆中','暂停营业中','营业中','申请修改中','申请退馆中','已退馆') DEFAULT '申请开馆中',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`last_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
`is_delete` tinyint(1) DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='绘本馆';
CREATE TABLE `ggh_library_apply` (
`id` bigint(20) NOT NULL COMMENT '审批记录',
`id_library` bigint(20) DEFAULT NULL COMMENT '操作的绘本馆',
`name_library` varchar(30) DEFAULT NULL COMMENT '绘本馆名称',
`id_user` bigint(20) DEFAULT NULL COMMENT '申请人',
`type_apply` enum('开馆','修改馆','退馆') DEFAULT NULL COMMENT '申请类型,其值与ggh_library表中的status_library一致',
`status_apply` enum('待提交','待审核','被拒','待支付','待退款','完成') DEFAULT NULL COMMENT '审批状态点',
`type_library` enum('全国馆','区域馆') DEFAULT '区域馆' COMMENT '绘本馆类型',
`radius` float DEFAULT NULL COMMENT '服务半径,精确到0.1千米',
`reject_type_library` varchar(50) DEFAULT NULL COMMENT '绘本馆类型或服务半径项不合规,驳回申请',
`name_user` varchar(30) DEFAULT NULL COMMENT '申请人姓名',
`reject_name_user` varchar(50) DEFAULT NULL COMMENT '姓名项内容不合规,被拒',
`id_ident` varchar(18) DEFAULT NULL COMMENT '身份证号',
`reject_id_ident` varchar(50) DEFAULT NULL COMMENT '身份证号不合规,被拒',
`name_pic_ident` varchar(40) DEFAULT NULL COMMENT '身份证扫描件',
`name_pic2_ident` varchar(40) DEFAULT NULL COMMENT '证件反面',
`reject_name_pic_ident` varchar(50) DEFAULT NULL COMMENT '身份证扫描件不合规,被拒',
`index_storage` enum('存储库1','存储库2','存储库3') DEFAULT '存储库1' COMMENT '存储库方案',
`location_library` varchar(100) DEFAULT NULL COMMENT '绘本馆详细地址',
`reject_location_library` varchar(50) DEFAULT NULL COMMENT '馆长地址不合规,被拒',
`phone_num` varchar(15) DEFAULT NULL COMMENT '馆长联系电话',
`reject_phone_num` varchar(50) DEFAULT NULL COMMENT '馆长联系方式不合规,被拒',
`create_time` timestamp NULL DEFAULT NULL,
`last_time` timestamp NULL DEFAULT NULL,
`is_delete` tinyint(1) DEFAULT NULL COMMENT '逻辑删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='绘本馆审批流程,同一审批类型,只允许有一条记录。比如退款审批有一条,成功后,可以复馆,则在另一条开馆审批上重复操作';
针对这样一些主要信息,特别要考虑双向联动
和约束
,因此这样设计:
-
(1)两个表,一个主状态表,有一些基本信息,比如馆名,大状态,其有一个对应字段,对应最近的审批,简单理解,名称来自于最近审批,能不能提供服务,也来自于审批。 一个审批表,有三种审批类型的数据,每一个馆只有一条某类型的审批数据。这样一个馆最多有三条审批数据,但只最新对应其中一条。
-
(2)约束和状态发展的关系用enum来定义,方便理解和调用。
下面具体看enum定义和一个示例业务接口:
/**
* 通过枚举来定义审批状态之间的约束,比如提交申请则之前的状态必须为'待提交'或'被拒'
* 审批有多种:开馆、修改、退馆
* 主要用法是:
* (1)controller层根据业务的不同,初始化这个对象的两个参数:statusLibrary和statusApply
* 此初始化也即意味着业务情景
* (2)业务接口在查询到db数据时,检查db数据的状态是否符合这两个状态的业务要求,而要求即在此enum中定义
* (3)一旦不符合要求,则抛出这里定义的异常信息
*
*/
public enum EnumStatusRestrict {
//保存草稿,审批状态变为'待提交',馆状态仍为'申请开馆中'
NEWONE_DRAFT("开馆", new String[]{"不存在", "待提交", "被拒"}, "待提交", new String[]{"不存在", "申请开馆中"}, "申请开馆中")
//提交审核,审批状态变为'待审核',馆状态仍为'申请开馆中'
, NEWONE_SUBMIT("开馆", new String[]{"不存在", "待提交", "被拒"}, "待审核", new String[]{"不存在", "申请开馆中"}, "申请开馆中")
//区域馆审核通过后,审批状态变为'待支付',馆状态仍为'申请开馆中'
, NEWONE_AREA_AUDIT_PASS("开馆", new String[]{"待审核"}, "待支付", new String[]{"申请开馆中"}, "申请开馆中")
//全国馆审核通过后,审批状态变为'完成',馆状态变为'暂停营业中'
, NEWONE_GLOBAL_AUDIT_PASS("开馆", new String[]{"待审核"}, "完成", new String[]{"申请开馆中"}, "暂停营业中")
//审核不通过,审批状态变为'被拒',馆状态仍为'申请开馆中'
, NEWONE_AUDIT_REJECT("开馆", new String[]{"待审核"}, "被拒", new String[]{"申请开馆中"}, "申请开馆中")
//修改馆保存草稿,审批状态变为'待提交',馆状态变为'申请修改中'
, MODIFY_DRAFT("修改馆", new String[]{"待提交", "被拒"}, "待提交", new String[]{"暂停营业中", "申请修改中"}, "申请修改中")
//修改馆,提交,审批状态变为'待提交',馆状态变为'申请修改中'
, MODIFY_SUBMIT("修改馆", new String[]{"待提交", "被拒"}, "待审核", new String[]{"申请修改中"}, "申请修改中")
//修改馆,审核通过,审批状态变为'完成',馆状态变为'暂停营业中'
, MODIFY_AUDIT_PASS("修改馆", new String[]{"待审核"}, "完成", new String[]{"申请修改中"}, "暂停营业中")
//修改馆,审核被拒,审批状态变为'完成',馆状态变为'暂停营业中'
, MODIFY_AUDIT_REJECT("修改馆", new String[]{"待审核"}, "被拒", new String[]{"申请修改中"}, "申请修改中")
;
String typeApply;
String[] allowLastApplyStatus;
String newStatusApply;
String[] allowLastLibraryStatus;
String newStatusLibrary;
/**
* 构造
* @param typeApply 审批类型,用于查当前数据的查询条件
* @param allowLastApplyStatus 仅允许审批现在处于何种状态
* @param newStatusApply 审批要变更成状态
* @param allowLastLibraryStatus 仅允许现在的馆的状态,才能做此业务
* @param newStatusLibrary 馆状态要变更成的状态,特别是ggh_library表
*/
EnumStatusRestrict(String typeApply
, String[] allowLastApplyStatus, String newStatusApply, String[] allowLastLibraryStatus, String newStatusLibrary) {
this.typeApply = typeApply;
this.newStatusLibrary = newStatusLibrary;
this.newStatusApply = newStatusApply;
this.allowLastApplyStatus = allowLastApplyStatus;
this.allowLastLibraryStatus = allowLastLibraryStatus;
}
/**
* 判断现审批的状态是否允许进行业务操作
* @param currentStatusApply
* @return
*/
public boolean isCurrentStatusApplyAllow(String currentStatusApply) {
return ArrayUtils.contains(this.allowLastApplyStatus, currentStatusApply);
}
public boolean isCurrentStatusLibraryAllow(String currentStatusLibrary) {
return ArrayUtils.contains(this.allowLastLibraryStatus, currentStatusLibrary);
}
/**
*申请类型,'开馆'、'修改馆'、'退馆'
*/
public String getTypeApply() {
return this.typeApply;
}
public String getNewStatusLibrary() {
return this.newStatusLibrary;
}
/**
*审批状态点:'待提交','待审核','被拒','待支付','待退款','完成'
*/
public String getNewStatusApply() {
return this.newStatusApply;
}
}
Controller类:
/**
* 申请绘本馆,第1步,保存草稿
*/
@ApiOperation("library new draft 申请绘本馆,第1步,普通用户,保存草稿(上传照片需要先保存草稿)")
@PostMapping("/newone/draft")
@ResponseStatus(HttpStatus.CREATED)
@JsonView(SimpleView.class)
@RequiresAuthentication
//@RequiresRoles(logical = Logical.OR, value = {"guest"})
public Map<String, Object> draftNewLibraryApply(@RequestBody FrmLibraryApply frmLibraryApply) {
return rtnSucc(onlyData(libraryMgrService.draftNewLibraryApply(frmLibraryApply)));
}
service接口实现
//开馆申请保存为草稿
@Override
@Transactional
public LibraryApply draftNewLibraryApply(FrmLibraryApply frmLibraryApply) {
EnumStatusRestrict enumStatus = EnumStatusRestrict.NEWONE_DRAFT;
return mayAddLibrary(enumStatus, frmLibraryApply);
}
private LibraryApply mayAddLibrary(EnumStatusRestrict enumStatus, FrmLibraryApply frmLibraryApply) {
//查用户是否合法
User existsUser = cacheService.findUserById(frmLibraryApply.getIdUser());
if (existsUser == null){
throw new MyException("用户编号非法");
}
Library existsLibrary = libraryRepository.findByIdUser(frmLibraryApply.getIdUser());
LibraryApply existsLibraryApply = libraryApplyRepository.findByIdUserAndTypeApply(frmLibraryApply.getIdUser()
, enumStatus.getTypeApply());
checkStatusAllow(enumStatus, existsLibrary, existsLibraryApply);
if (existsLibraryApply == null) { //不存在审批,理应指开馆审批,其它审批有一个触发接口,已经产生了数据
existsLibraryApply = utils.createEntity(LibraryApply.class);
existsLibraryApply.setTypeApply(enumStatus.getTypeApply());
if (existsLibrary == null) { //主表新增数据
existsLibrary = utils.createEntity(Library.class);
BeanUtils.copyProperties(frmLibraryApply, existsLibrary);//, FrmLibrary.class);
existsLibrary.setStatusLibrary(enumStatus.getNewStatusLibrary());
}
//重要,将馆记录关联上最新的审批记录,审批记录含馆详情
//TODO 原记录再次走流程暂时还没有关联上,这个由触发状态发展的接口去实现,见changeLibraryStatus,已实现部分
existsLibrary.setStatusLibrary(enumStatus.getNewStatusLibrary());
existsLibrary.setIdLibraryApply(existsLibraryApply.getId());
cacheMgrService.saveLibrary(existsLibrary);
}
BeanUtils.copyProperties(frmLibraryApply, existsLibraryApply, CommConst.DO_NOT_REPLACE);
existsLibraryApply.setIdLibrary(existsLibrary.getId());
//将审批状态字保存到数据库
existsLibraryApply.setStatusApply(enumStatus.getNewStatusApply());
return libraryApplyRepository.save(existsLibraryApply);
}
/**
* 这个约束检查
*/
private void checkStatusAllow(EnumStatusRestrict enumStatus
, Library existsLibrary, LibraryApply existsLibraryApply) {
if (existsLibraryApply != null && existsLibrary == null) {
throw new MyException("当前数据有误,不一致,存在馆数据但是没有审批数据");
}
String existsStatusLibrary = (existsLibrary == null) ? "不存在" : existsLibrary.getStatusLibrary();
boolean isLibraryStatusAllow = enumStatus.isCurrentStatusLibraryAllow(existsStatusLibrary);
if (!isLibraryStatusAllow) {
throw new MyException("当前的馆的数据不允许进行这样操作,馆的状态为:" + existsStatusLibrary);
}
String existsStatusApply = (existsLibraryApply == null) ? "不存在" : existsLibraryApply.getStatusApply();
boolean isApplayStatusAllow = enumStatus.isCurrentStatusApplyAllow(existsStatusApply);
if (!isApplayStatusAllow) {
throw new MyException("当前的馆存在的审批的状态不允许进行这样操作,状态为:" + existsStatusApply);
}
}
4 总结
这个示例表达了,共性业务可以进一步抽象,抽出其公共特征,然后只要简单定义表征,内核只实现一套即可,一是可以减少代码量;二是不用写多遍内核, 因为每写一遍内核都需要仔细检查其逻辑,这是不必要的思考浪费;三是内核和表征分开定义和实现,一处改处处有效,便于维护和扩展。