字数: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 总结

这个示例表达了,共性业务可以进一步抽象,抽出其公共特征,然后只要简单定义表征,内核只实现一套即可,一是可以减少代码量;二是不用写多遍内核, 因为每写一遍内核都需要仔细检查其逻辑,这是不必要的思考浪费;三是内核和表征分开定义和实现,一处改处处有效,便于维护和扩展。