fix(gxmu): 审批人解析覆盖第 2..N 级,已处理节点再提交直接拒绝
- ReviewFlowStarterService 新增 advanceReview(module, pk, currentSortNumber):
通过后推进下一级也走 ReviewerResolver,不再 setUserId(nextOne.getReviewUserId());
解析为空抛可读 BusinessException 且不写节点,最后一级则不生成节点
- ReviewListController 的推进改为调用 advanceReview;ManuscriptController 的首级
改为 startReview("cms_manuscript", id),cms 稿件不再复制 review_user_id
- 推进前校验当前节点 status == 0(ReviewStatus.PENDING),已处理节点再 PUT 直接拒绝,
避免重复通过造出第二组活跃节点;批量审批同样逐行校验
- 新增 ReviewStatus 常量(gxmu_review_list.status:0待审 1通过 2驳回),
把 controller / starter / DeclareGuard / TzbcyFormServiceImpl 里的裸 1、2 换成常量
- 解析失败的兜底消息补上学院与层级,与 ReviewerResolver 口径一致
- 审批动作注释里的「撤销」改成实际语义(通过 / 驳回)
refs #58
This commit is contained in:
@@ -10,12 +10,7 @@ import com.gxwebsoft.common.core.web.PageParam;
|
|||||||
import com.gxwebsoft.common.core.web.BatchParam;
|
import com.gxwebsoft.common.core.web.BatchParam;
|
||||||
import com.gxwebsoft.common.core.annotation.OperationLog;
|
import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||||
import com.gxwebsoft.common.system.entity.User;
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
|
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
|
||||||
import com.gxwebsoft.gxmu.service.ReviewListService;
|
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -38,11 +33,7 @@ public class ManuscriptController extends BaseController {
|
|||||||
@Resource
|
@Resource
|
||||||
private ManuscriptService manuscriptService;
|
private ManuscriptService manuscriptService;
|
||||||
@Resource
|
@Resource
|
||||||
private ReviewFlowConfigService reviewFlowConfigService;
|
private ReviewFlowStarterService reviewFlowStarterService;
|
||||||
@Resource
|
|
||||||
private ReviewFlowService reviewFlowService;
|
|
||||||
@Resource
|
|
||||||
private ReviewListService reviewListService;
|
|
||||||
|
|
||||||
@ApiOperation("分页查询稿件")
|
@ApiOperation("分页查询稿件")
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@@ -75,23 +66,8 @@ public class ManuscriptController extends BaseController {
|
|||||||
manuscript.setUserId(loginUser.getUserId());
|
manuscript.setUserId(loginUser.getUserId());
|
||||||
}
|
}
|
||||||
if (manuscriptService.save(manuscript)) {
|
if (manuscriptService.save(manuscript)) {
|
||||||
// 查找该模块的审核流
|
// 首级审批人同样经 ReviewerResolver 解析(与其它模块同一条路径),不复制 review_user_id
|
||||||
ReviewFlowConfig reviewFlowConfig = reviewFlowConfigService.getByModule("cms_manuscript");
|
reviewFlowStarterService.startReview("cms_manuscript", manuscript.getId());
|
||||||
if (reviewFlowConfig != null) {
|
|
||||||
ReviewFlow reviewFlow = reviewFlowService.getById(reviewFlowConfig.getFlowId());
|
|
||||||
if (reviewFlow != null) {
|
|
||||||
List<ReviewFlow> reviewFlowList = reviewFlowService.listByTitle(reviewFlow.getTitle());
|
|
||||||
if (reviewFlowList != null && !reviewFlowList.isEmpty()) {
|
|
||||||
ReviewFlow firstOne = reviewFlowList.get(0);
|
|
||||||
ReviewList reviewList = new ReviewList();
|
|
||||||
reviewList.setPk(manuscript.getId());
|
|
||||||
reviewList.setModule("cms_manuscript");
|
|
||||||
reviewList.setUserId(firstOne.getReviewUserId());
|
|
||||||
reviewList.setSortNumber(0);
|
|
||||||
reviewListService.save(reviewList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return success("添加成功");
|
return success("添加成功");
|
||||||
}
|
}
|
||||||
return fail("添加失败");
|
return fail("添加失败");
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.gxwebsoft.gxmu.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核待办(gxmu_review_list.status)的状态取值。
|
||||||
|
*
|
||||||
|
* <p>这套取值与申报记录的 declare_status(0草稿 1待审核 2通过 3驳回 4作废,见
|
||||||
|
* {@link com.gxwebsoft.gxmu.service.impl.DeclareGuardServiceImpl} 的 STATUS_* 常量)
|
||||||
|
* 是两套语义不同的编码:同为 "2",这里是驳回、那里是通过,因此不要混用字面量。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-11
|
||||||
|
*/
|
||||||
|
public final class ReviewStatus {
|
||||||
|
|
||||||
|
/** 待审核:当前一级的责任人可以审批 */
|
||||||
|
public static final int PENDING = 0;
|
||||||
|
|
||||||
|
/** 通过:节点已处理,链条推进到下一级 */
|
||||||
|
public static final int APPROVED = 1;
|
||||||
|
|
||||||
|
/** 驳回:节点已处理,申报退回申报人 */
|
||||||
|
public static final int REJECTED = 2;
|
||||||
|
|
||||||
|
private ReviewStatus() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,19 +3,15 @@ package com.gxwebsoft.gxmu.controller;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.gxwebsoft.common.core.Constants;
|
import com.gxwebsoft.common.core.Constants;
|
||||||
import com.gxwebsoft.common.core.web.BaseController;
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
import com.gxwebsoft.gxmu.constant.ReviewStatus;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
|
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
|
||||||
import com.gxwebsoft.gxmu.service.DeclareGuardService;
|
import com.gxwebsoft.gxmu.service.DeclareGuardService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewListService;
|
import com.gxwebsoft.gxmu.service.ReviewListService;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||||
import com.gxwebsoft.gxmu.param.ReviewListParam;
|
import com.gxwebsoft.gxmu.param.ReviewListParam;
|
||||||
import com.gxwebsoft.common.core.web.ApiResult;
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
import com.gxwebsoft.common.core.web.PageParam;
|
|
||||||
import com.gxwebsoft.common.core.web.BatchParam;
|
import com.gxwebsoft.common.core.web.BatchParam;
|
||||||
import com.gxwebsoft.common.core.annotation.OperationLog;
|
|
||||||
import com.gxwebsoft.common.system.entity.User;
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
@@ -23,11 +19,16 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审核列表控制器
|
* 审核列表控制器
|
||||||
*
|
*
|
||||||
|
* <p>审批动作(通过 / 驳回)的归属校验、状态校验留在这里(HTTP 层的准入判断);
|
||||||
|
* 通过后「推进到下一级」属于审批链业务规则,交给 {@link ReviewFlowStarterService#advanceReview},
|
||||||
|
* 审批人一律经 {@link com.gxwebsoft.gxmu.service.ReviewerResolver} 解析,不再复制 review_user_id。
|
||||||
|
*
|
||||||
* @author LX
|
* @author LX
|
||||||
* @since 2026-03-18 15:50:51
|
* @since 2026-03-18 15:50:51
|
||||||
*/
|
*/
|
||||||
@@ -38,11 +39,9 @@ public class ReviewListController extends BaseController {
|
|||||||
@Resource
|
@Resource
|
||||||
private ReviewListService reviewListService;
|
private ReviewListService reviewListService;
|
||||||
@Resource
|
@Resource
|
||||||
private ReviewFlowConfigService reviewFlowConfigService;
|
|
||||||
@Resource
|
|
||||||
private ReviewFlowService reviewFlowService;
|
|
||||||
@Resource
|
|
||||||
private DeclareGuardService declareGuardService;
|
private DeclareGuardService declareGuardService;
|
||||||
|
@Resource
|
||||||
|
private ReviewFlowStarterService reviewFlowStarterService;
|
||||||
|
|
||||||
@ApiOperation("分页查询审核列表")
|
@ApiOperation("分页查询审核列表")
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@@ -100,8 +99,14 @@ public class ReviewListController extends BaseController {
|
|||||||
@ApiOperation("修改审核列表")
|
@ApiOperation("修改审核列表")
|
||||||
@PutMapping()
|
@PutMapping()
|
||||||
public ApiResult<?> update(@RequestBody ReviewList reviewList) {
|
public ApiResult<?> update(@RequestBody ReviewList reviewList) {
|
||||||
// 审批动作(通过/驳回/撤销)只能由待办的责任人本人执行
|
// 审批动作(通过 / 驳回)只能由待办的责任人本人执行
|
||||||
ApiResult<?> denied = denyIfNotAssignee(reviewList.getId());
|
ReviewList existing = reviewList.getId() == null ? null : reviewListService.getById(reviewList.getId());
|
||||||
|
ApiResult<?> denied = denyIfNotAssignee(existing);
|
||||||
|
if (denied != null) {
|
||||||
|
return denied;
|
||||||
|
}
|
||||||
|
// 已处理过的节点再提交直接拒绝:否则会再插一条下一级待办,同一申报出现第二组活跃节点
|
||||||
|
denied = denyIfAlreadyHandled(existing);
|
||||||
if (denied != null) {
|
if (denied != null) {
|
||||||
return denied;
|
return denied;
|
||||||
}
|
}
|
||||||
@@ -110,34 +115,10 @@ public class ReviewListController extends BaseController {
|
|||||||
return denied;
|
return denied;
|
||||||
}
|
}
|
||||||
if (reviewListService.updateById(reviewList)) {
|
if (reviewListService.updateById(reviewList)) {
|
||||||
// 通过进入下一个
|
// 通过则推进到下一级;下一级的审批人由审批链配置解析,不复制本级的 review_user_id
|
||||||
if (Integer.valueOf(1).equals(reviewList.getStatus())) {
|
if (Integer.valueOf(ReviewStatus.APPROVED).equals(reviewList.getStatus())) {
|
||||||
ReviewFlowConfig reviewFlowConfig = reviewFlowConfigService.getByModule(reviewList.getModule());
|
reviewFlowStarterService.advanceReview(reviewList.getModule(), reviewList.getPk(),
|
||||||
if (reviewFlowConfig != null) {
|
existing == null ? null : existing.getSortNumber());
|
||||||
ReviewFlow reviewFlow = reviewFlowService.getById(reviewFlowConfig.getFlowId());
|
|
||||||
if (reviewFlow != null) {
|
|
||||||
List<ReviewFlow> reviewFlowList = reviewFlowService.listByTitle(reviewFlow.getTitle());
|
|
||||||
if (reviewFlowList != null && !reviewFlowList.isEmpty()) {
|
|
||||||
if (reviewList.getSortNumber() + 1 < reviewFlowList.size()) {
|
|
||||||
ReviewFlow nextOne = reviewFlowList.get(reviewList.getSortNumber() + 1);
|
|
||||||
if (nextOne != null) {
|
|
||||||
ReviewList nextReviewList = new ReviewList();
|
|
||||||
nextReviewList.setPk(reviewList.getPk());
|
|
||||||
nextReviewList.setModule(reviewList.getModule());
|
|
||||||
nextReviewList.setUserId(nextOne.getReviewUserId());
|
|
||||||
nextReviewList.setSortNumber(reviewList.getSortNumber() + 1);
|
|
||||||
reviewListService.save(nextReviewList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// switch (reviewList.getModule()) {
|
|
||||||
// case "cms_manuscript": {
|
|
||||||
// // 社团指导老师/二级学院团委书记-团委社团管理部/团委办公室-学校团委组织宣传部,拟发布后,再提交给团委副书记-团委书记
|
|
||||||
// if ()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
declareGuardService.syncReviewStatus(reviewList.getModule(), reviewList.getPk());
|
declareGuardService.syncReviewStatus(reviewList.getModule(), reviewList.getPk());
|
||||||
return success("修改成功");
|
return success("修改成功");
|
||||||
@@ -166,11 +147,15 @@ public class ReviewListController extends BaseController {
|
|||||||
@ApiOperation("批量修改审核列表")
|
@ApiOperation("批量修改审核列表")
|
||||||
@PutMapping("/batch")
|
@PutMapping("/batch")
|
||||||
public ApiResult<?> updateBatch(@RequestBody BatchParam<ReviewList> batchParam) {
|
public ApiResult<?> updateBatch(@RequestBody BatchParam<ReviewList> batchParam) {
|
||||||
// 批量审批逐行校验归属,只要有一行不是自己的就整体拒绝,避免部分落库
|
// 批量审批逐行校验归属与状态,只要有一行不满足就整体拒绝,避免部分落库
|
||||||
ApiResult<?> denied = denyIfBatchNotAssignee(batchParam);
|
ApiResult<?> denied = denyIfBatchNotAssignee(batchParam);
|
||||||
if (denied != null) {
|
if (denied != null) {
|
||||||
return denied;
|
return denied;
|
||||||
}
|
}
|
||||||
|
denied = denyIfBatchAlreadyHandled(batchParam);
|
||||||
|
if (denied != null) {
|
||||||
|
return denied;
|
||||||
|
}
|
||||||
denied = denyIfRejectWithoutContent(batchParam == null ? null : batchParam.getData());
|
denied = denyIfRejectWithoutContent(batchParam == null ? null : batchParam.getData());
|
||||||
if (denied != null) {
|
if (denied != null) {
|
||||||
return denied;
|
return denied;
|
||||||
@@ -191,7 +176,10 @@ public class ReviewListController extends BaseController {
|
|||||||
* @return null 表示校验通过,否则返回 403 结果
|
* @return null 表示校验通过,否则返回 403 结果
|
||||||
*/
|
*/
|
||||||
private ApiResult<?> denyIfNotAssignee(Integer id) {
|
private ApiResult<?> denyIfNotAssignee(Integer id) {
|
||||||
ReviewList existing = id == null ? null : reviewListService.getById(id);
|
return denyIfNotAssignee(id == null ? null : reviewListService.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApiResult<?> denyIfNotAssignee(ReviewList existing) {
|
||||||
Integer loginUserId = getLoginUserId();
|
Integer loginUserId = getLoginUserId();
|
||||||
if (existing == null || loginUserId == null || !loginUserId.equals(existing.getUserId())) {
|
if (existing == null || loginUserId == null || !loginUserId.equals(existing.getUserId())) {
|
||||||
return new ApiResult<>(Constants.UNAUTHORIZED_CODE, Constants.UNAUTHORIZED_MSG);
|
return new ApiResult<>(Constants.UNAUTHORIZED_CODE, Constants.UNAUTHORIZED_MSG);
|
||||||
@@ -199,6 +187,25 @@ public class ReviewListController extends BaseController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前节点必须仍是「待审核」才允许提交审批动作。
|
||||||
|
*
|
||||||
|
* <p>重复通过会给同一申报再造一组待办(gxmu_review_list 没有 (module, pk, level) 唯一键兜底),
|
||||||
|
* 双击或多标签页即可复现,所以在落库前就拒绝。
|
||||||
|
*
|
||||||
|
* @param existing 库里现有的待办行
|
||||||
|
* @return null 表示仍是待审核,否则返回可读的失败结果
|
||||||
|
*/
|
||||||
|
private ApiResult<?> denyIfAlreadyHandled(ReviewList existing) {
|
||||||
|
if (existing == null || existing.getStatus() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (existing.getStatus() != ReviewStatus.PENDING) {
|
||||||
|
return fail("该审批已处理(通过 / 驳回),请勿重复提交");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量审批的归属校验:逐行确认都属于当前用户。
|
* 批量审批的归属校验:逐行确认都属于当前用户。
|
||||||
*
|
*
|
||||||
@@ -209,7 +216,7 @@ public class ReviewListController extends BaseController {
|
|||||||
if (batchParam == null || batchParam.getIds() == null) {
|
if (batchParam == null || batchParam.getIds() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (java.io.Serializable id : batchParam.getIds()) {
|
for (Serializable id : batchParam.getIds()) {
|
||||||
ApiResult<?> denied = denyIfNotAssignee(id instanceof Number ? ((Number) id).intValue() : null);
|
ApiResult<?> denied = denyIfNotAssignee(id instanceof Number ? ((Number) id).intValue() : null);
|
||||||
if (denied != null) {
|
if (denied != null) {
|
||||||
return denied;
|
return denied;
|
||||||
@@ -218,6 +225,26 @@ public class ReviewListController extends BaseController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量审批的状态校验:任意一行已处理就整体拒绝。
|
||||||
|
*
|
||||||
|
* @param batchParam 批量参数
|
||||||
|
* @return null 表示全部仍是待审核,否则返回可读的失败结果
|
||||||
|
*/
|
||||||
|
private ApiResult<?> denyIfBatchAlreadyHandled(BatchParam<ReviewList> batchParam) {
|
||||||
|
if (batchParam == null || batchParam.getIds() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (Serializable id : batchParam.getIds()) {
|
||||||
|
ReviewList existing = id instanceof Number ? reviewListService.getById(((Number) id).intValue()) : null;
|
||||||
|
ApiResult<?> denied = denyIfAlreadyHandled(existing);
|
||||||
|
if (denied != null) {
|
||||||
|
return denied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 驳回必须携带意见,否则申报人不知道要改什么。
|
* 驳回必须携带意见,否则申报人不知道要改什么。
|
||||||
*
|
*
|
||||||
@@ -225,7 +252,8 @@ public class ReviewListController extends BaseController {
|
|||||||
* @return null 表示校验通过,否则返回可读的失败结果
|
* @return null 表示校验通过,否则返回可读的失败结果
|
||||||
*/
|
*/
|
||||||
private ApiResult<?> denyIfRejectWithoutContent(ReviewList data) {
|
private ApiResult<?> denyIfRejectWithoutContent(ReviewList data) {
|
||||||
if (data != null && Integer.valueOf(2).equals(data.getStatus()) && StrUtil.isBlank(data.getContent())) {
|
if (data != null && Integer.valueOf(ReviewStatus.REJECTED).equals(data.getStatus())
|
||||||
|
&& StrUtil.isBlank(data.getContent())) {
|
||||||
return fail("驳回必须填写审核意见");
|
return fail("驳回必须填写审核意见");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -236,7 +264,7 @@ public class ReviewListController extends BaseController {
|
|||||||
if (batchParam == null || batchParam.getIds() == null) {
|
if (batchParam == null || batchParam.getIds() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (java.io.Serializable id : batchParam.getIds()) {
|
for (Serializable id : batchParam.getIds()) {
|
||||||
ReviewList saved = reviewListService.getById(id);
|
ReviewList saved = reviewListService.getById(id);
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
declareGuardService.syncReviewStatus(saved.getModule(), saved.getPk());
|
declareGuardService.syncReviewStatus(saved.getModule(), saved.getPk());
|
||||||
@@ -283,4 +311,4 @@ public class ReviewListController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,16 @@ public interface ReviewFlowStarterService {
|
|||||||
* @param pk 业务主键
|
* @param pk 业务主键
|
||||||
*/
|
*/
|
||||||
void restartReviewWhenLastRejected(String module, Integer pk);
|
void restartReviewWhenLastRejected(String module, Integer pk);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前一级通过后推进到下一级:审批人同样走 {@link ReviewerResolver} 解析,不复制上一级的 review_user_id。
|
||||||
|
*
|
||||||
|
* <p>解析不出审批人时抛可读的业务异常且一个节点都不写(宁可卡住,也不生成无人认领的待办);
|
||||||
|
* 已经是最后一级则什么都不做,链条自然结束。
|
||||||
|
*
|
||||||
|
* @param module 模块编码
|
||||||
|
* @param pk 业务主键
|
||||||
|
* @param currentSortNumber 当前已通过节点的顺序号(首级为 0,与 review_list.sort_number 同口径)
|
||||||
|
*/
|
||||||
|
void advanceReview(String module, Integer pk, Integer currentSortNumber);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.gxwebsoft.gxmu.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.gxwebsoft.gxmu.constant.ReviewStatus;
|
||||||
import com.gxwebsoft.gxmu.entity.Declare;
|
import com.gxwebsoft.gxmu.entity.Declare;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||||
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
||||||
@@ -160,7 +161,7 @@ public class DeclareGuardServiceImpl implements DeclareGuardService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean pending = nodes.stream()
|
boolean pending = nodes.stream()
|
||||||
.anyMatch(node -> node.getStatus() != null && node.getStatus() == 0);
|
.anyMatch(node -> node.getStatus() != null && node.getStatus() == ReviewStatus.PENDING);
|
||||||
if (pending) {
|
if (pending) {
|
||||||
declareRecordMapper.updateStatus(module, recordId, STATUS_PENDING, null);
|
declareRecordMapper.updateStatus(module, recordId, STATUS_PENDING, null);
|
||||||
return;
|
return;
|
||||||
@@ -181,7 +182,7 @@ public class DeclareGuardServiceImpl implements DeclareGuardService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
declareRecordMapper.updateStatus(module, recordId,
|
declareRecordMapper.updateStatus(module, recordId,
|
||||||
last.getStatus() == 2 ? STATUS_REJECTED : STATUS_PASSED, null);
|
last.getStatus() == ReviewStatus.REJECTED ? STATUS_REJECTED : STATUS_PASSED, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -200,7 +201,7 @@ public class DeclareGuardServiceImpl implements DeclareGuardService {
|
|||||||
return "只有待审核的申报可以撤回";
|
return "只有待审核的申报可以撤回";
|
||||||
}
|
}
|
||||||
boolean approved = reviewListService.listByModuleNPK(module, recordId).stream()
|
boolean approved = reviewListService.listByModuleNPK(module, recordId).stream()
|
||||||
.anyMatch(node -> node.getStatus() != null && node.getStatus() == 1);
|
.anyMatch(node -> node.getStatus() != null && node.getStatus() == ReviewStatus.APPROVED);
|
||||||
if (approved) {
|
if (approved) {
|
||||||
return "已有审核通过的记录不能撤回,请联系管理员处理";
|
return "已有审核通过的记录不能撤回,请联系管理员处理";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ package com.gxwebsoft.gxmu.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
|
import com.gxwebsoft.common.system.entity.Organization;
|
||||||
|
import com.gxwebsoft.common.system.service.OrganizationService;
|
||||||
|
import com.gxwebsoft.gxmu.constant.ReviewStatus;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
|
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||||
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
||||||
@@ -16,28 +20,22 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审核流程发起服务实现
|
* 审核流程发起服务实现
|
||||||
*
|
*
|
||||||
* <p>审批人不再写死:由 {@link ReviewerResolver} 按审批链配置的 reviewer_rule 解析,
|
* <p>审批人不再写死:由 {@link ReviewerResolver} 按审批链配置的 reviewer_rule 解析,
|
||||||
* 一期串行单审取 userId 最小的那个;解析不出来直接抛业务异常,不生成待办。</p>
|
* 一期串行单审取 userId 最小的那个;解析不出来直接抛业务异常,不生成待办。
|
||||||
|
* 首级(发起)、驳回重提、逐级推进三条路径都走同一套解析,第 2..N 级不再复制 review_user_id。</p>
|
||||||
*
|
*
|
||||||
* @author LX
|
* @author LX
|
||||||
* @since 2026-03-18
|
* @since 2026-03-18
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
||||||
/** 有 college_id 的 4 张在跑业务表(#59);其余模块的申报记录只取申报人 */
|
|
||||||
private static final Set<String> COLLEGE_MODULES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
||||||
"gxmu_sjqn", "gxmu_tzbcy_form", "gxmu_qmgc_form", "gxmu_wxxzx_project")));
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ReviewFlowConfigService reviewFlowConfigService;
|
private ReviewFlowConfigService reviewFlowConfigService;
|
||||||
@@ -49,6 +47,10 @@ public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
|||||||
private ReviewerResolver reviewerResolver;
|
private ReviewerResolver reviewerResolver;
|
||||||
@Resource
|
@Resource
|
||||||
private DeclareRecordMapper declareRecordMapper;
|
private DeclareRecordMapper declareRecordMapper;
|
||||||
|
@Resource
|
||||||
|
private ModuleRegistryService moduleRegistryService;
|
||||||
|
@Resource
|
||||||
|
private OrganizationService organizationService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startReview(String module, Integer pk) {
|
public void startReview(String module, Integer pk) {
|
||||||
@@ -59,15 +61,11 @@ public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
|||||||
if (reviewLists != null && !reviewLists.isEmpty()) {
|
if (reviewLists != null && !reviewLists.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ReviewFlowConfig reviewFlowConfig = reviewFlowConfigService.getByModule(module);
|
List<ReviewFlow> flowList = loadFlowList(module);
|
||||||
if (reviewFlowConfig == null || reviewFlowConfig.getFlowId() == null) {
|
if (flowList.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ReviewFlow firstOne = getFirstFlow(reviewFlowConfig);
|
saveReviewNode(module, pk, flowList.get(0), 0);
|
||||||
if (firstOne == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveFirstReviewNode(module, pk, firstOne);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,61 +88,125 @@ public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
|||||||
Comparator.nullsLast(LocalDateTime::compareTo))
|
Comparator.nullsLast(LocalDateTime::compareTo))
|
||||||
.thenComparing(ReviewList::getId, Comparator.nullsLast(Integer::compareTo)))
|
.thenComparing(ReviewList::getId, Comparator.nullsLast(Integer::compareTo)))
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
if (latestReview == null || latestReview.getStatus() == null || latestReview.getStatus() != 2) {
|
if (latestReview == null || latestReview.getStatus() == null
|
||||||
|
|| latestReview.getStatus() != ReviewStatus.REJECTED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ReviewFlowConfig reviewFlowConfig = reviewFlowConfigService.getByModule(module);
|
List<ReviewFlow> flowList = loadFlowList(module);
|
||||||
if (reviewFlowConfig == null || reviewFlowConfig.getFlowId() == null) {
|
if (flowList.isEmpty()) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
ReviewFlow firstOne = getFirstFlow(reviewFlowConfig);
|
|
||||||
if (firstOne == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reviewListService.remove(new LambdaQueryWrapper<ReviewList>()
|
reviewListService.remove(new LambdaQueryWrapper<ReviewList>()
|
||||||
.eq(ReviewList::getModule, module)
|
.eq(ReviewList::getModule, module)
|
||||||
.eq(ReviewList::getPk, pk));
|
.eq(ReviewList::getPk, pk));
|
||||||
saveFirstReviewNode(module, pk, firstOne);
|
saveReviewNode(module, pk, flowList.get(0), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReviewFlow getFirstFlow(ReviewFlowConfig reviewFlowConfig) {
|
/**
|
||||||
|
* 当前一级通过后推进到下一级。
|
||||||
|
*
|
||||||
|
* <p>顺序号与审批链下标一一对应(首级 sort_number = 0),所以下一级就是 flowList 的
|
||||||
|
* {@code currentSortNumber + 1};越界说明这一级已是最后一级,链条结束。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void advanceReview(String module, Integer pk, Integer currentSortNumber) {
|
||||||
|
if (module == null || pk == null || currentSortNumber == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<ReviewFlow> flowList = loadFlowList(module);
|
||||||
|
if (flowList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int nextIndex = currentSortNumber + 1;
|
||||||
|
if (nextIndex >= flowList.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ReviewFlow nextOne = flowList.get(nextIndex);
|
||||||
|
if (nextOne == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveReviewNode(module, pk, nextOne, nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读模块的审批链节点(按 level 升序,下标即 sort_number)。
|
||||||
|
*
|
||||||
|
* @param module 模块编码
|
||||||
|
* @return 审批链节点;没有配置时返回空列表
|
||||||
|
*/
|
||||||
|
private List<ReviewFlow> loadFlowList(String module) {
|
||||||
|
ReviewFlowConfig reviewFlowConfig = reviewFlowConfigService.getByModule(module);
|
||||||
|
if (reviewFlowConfig == null || reviewFlowConfig.getFlowId() == null) {
|
||||||
|
return java.util.Collections.emptyList();
|
||||||
|
}
|
||||||
ReviewFlow reviewFlow = reviewFlowService.getById(reviewFlowConfig.getFlowId());
|
ReviewFlow reviewFlow = reviewFlowService.getById(reviewFlowConfig.getFlowId());
|
||||||
if (reviewFlow == null) {
|
if (reviewFlow == null) {
|
||||||
return null;
|
return java.util.Collections.emptyList();
|
||||||
}
|
}
|
||||||
List<ReviewFlow> reviewFlowList = reviewFlowService.listByTitle(reviewFlow.getTitle());
|
List<ReviewFlow> reviewFlowList = reviewFlowService.listByTitle(reviewFlow.getTitle());
|
||||||
if (reviewFlowList == null || reviewFlowList.isEmpty()) {
|
if (reviewFlowList == null || reviewFlowList.isEmpty()) {
|
||||||
return null;
|
return java.util.Collections.emptyList();
|
||||||
}
|
}
|
||||||
return reviewFlowList.get(0);
|
return reviewFlowList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveFirstReviewNode(String module, Integer pk, ReviewFlow firstOne) {
|
/**
|
||||||
|
* 解析该级审批人并写一个待办节点。
|
||||||
|
*
|
||||||
|
* @param module 模块编码
|
||||||
|
* @param pk 业务主键
|
||||||
|
* @param flow 该级审批链配置
|
||||||
|
* @param sortNumber 待办顺序号(0 起)
|
||||||
|
*/
|
||||||
|
private void saveReviewNode(String module, Integer pk, ReviewFlow flow, int sortNumber) {
|
||||||
Map<String, Object> record = selectDeclareRecord(module, pk);
|
Map<String, Object> record = selectDeclareRecord(module, pk);
|
||||||
Integer collegeId = intValue(record, "collegeId");
|
Integer collegeId = intValue(record, "collegeId");
|
||||||
Integer declareUserId = intValue(record, "userId");
|
Integer declareUserId = intValue(record, "userId");
|
||||||
List<Integer> reviewers = reviewerResolver.resolve(firstOne, collegeId, declareUserId);
|
List<Integer> reviewers = reviewerResolver.resolve(flow, collegeId, declareUserId);
|
||||||
if (reviewers == null || reviewers.isEmpty()) {
|
if (reviewers == null || reviewers.isEmpty()) {
|
||||||
throw new BusinessException("模块 " + module + " 主键 " + pk + " 第 "
|
// 兜底消息与 ReviewerResolver 同口径:哪个学院、第几级、哪个模块/主键
|
||||||
+ (firstOne.getLevel() == null ? "?" : firstOne.getLevel())
|
String scope = collegeId == null ? "申报记录缺少学院标识," : "学院「" + collegeName(collegeId) + "」";
|
||||||
+ " 级审批人解析结果为空,未生成待办");
|
throw new BusinessException(scope + "第 " + levelOf(flow)
|
||||||
|
+ " 级审批人解析结果为空(模块 " + module + " 主键 " + pk + "),未生成待办");
|
||||||
}
|
}
|
||||||
ReviewList reviewList = new ReviewList();
|
ReviewList reviewList = new ReviewList();
|
||||||
reviewList.setPk(pk);
|
reviewList.setPk(pk);
|
||||||
reviewList.setModule(module);
|
reviewList.setModule(module);
|
||||||
reviewList.setUserId(reviewers.get(0));
|
reviewList.setUserId(reviewers.get(0));
|
||||||
reviewList.setSortNumber(0);
|
reviewList.setSortNumber(sortNumber);
|
||||||
reviewListService.save(reviewList);
|
reviewListService.save(reviewList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 读申报记录的申报人与学院标识;只有 4 张在跑表有 college_id */
|
/**
|
||||||
|
* 读申报记录的申报人与学院标识。
|
||||||
|
*
|
||||||
|
* <p>哪张业务表有 college_id 由注册表 has_college 决定,后端不再维护模块白名单
|
||||||
|
* (ADR 0006:新增表只改注册表一行)。
|
||||||
|
*/
|
||||||
private Map<String, Object> selectDeclareRecord(String module, Integer pk) {
|
private Map<String, Object> selectDeclareRecord(String module, Integer pk) {
|
||||||
if (COLLEGE_MODULES.contains(module)) {
|
if (moduleRegistryService.hasCollege(module)) {
|
||||||
return declareRecordMapper.selectReviewContext(module, pk);
|
return declareRecordMapper.selectReviewContext(module, pk);
|
||||||
}
|
}
|
||||||
return declareRecordMapper.selectRecord(module, pk);
|
return declareRecordMapper.selectRecord(module, pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String collegeName(Integer collegeId) {
|
||||||
|
if (collegeId == null) {
|
||||||
|
return "学院标识为空";
|
||||||
|
}
|
||||||
|
Organization organization = organizationService.getById(collegeId);
|
||||||
|
if (organization == null || organization.getOrganizationName() == null
|
||||||
|
|| organization.getOrganizationName().isEmpty()) {
|
||||||
|
return "未知学院(" + collegeId + ")";
|
||||||
|
}
|
||||||
|
return organization.getOrganizationName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String levelOf(ReviewFlow flow) {
|
||||||
|
return flow == null || flow.getLevel() == null ? "?" : String.valueOf(flow.getLevel());
|
||||||
|
}
|
||||||
|
|
||||||
private Integer intValue(Map<String, Object> record, String key) {
|
private Integer intValue(Map<String, Object> record, String key) {
|
||||||
if (record == null) {
|
if (record == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.gxwebsoft.gxmu.constant.ReviewStatus;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||||
import com.gxwebsoft.gxmu.entity.TzbcyForm;
|
import com.gxwebsoft.gxmu.entity.TzbcyForm;
|
||||||
import com.gxwebsoft.gxmu.mapper.TzbcyFormMapper;
|
import com.gxwebsoft.gxmu.mapper.TzbcyFormMapper;
|
||||||
@@ -157,7 +158,7 @@ public class TzbcyFormServiceImpl extends ServiceImpl<TzbcyFormMapper, TzbcyForm
|
|||||||
}
|
}
|
||||||
List<ReviewList> reviewList = form.getReviewList();
|
List<ReviewList> reviewList = form.getReviewList();
|
||||||
ReviewList latest = reviewList.get(reviewList.size() - 1);
|
ReviewList latest = reviewList.get(reviewList.size() - 1);
|
||||||
return latest != null && latest.getStatus() != null && latest.getStatus() == 1;
|
return latest != null && latest.getStatus() != null && latest.getStatus() == ReviewStatus.APPROVED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildPersonKey(String name, String phone) {
|
private String buildPersonKey(String name, String phone) {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import com.gxwebsoft.common.core.web.BatchParam;
|
|||||||
import com.gxwebsoft.common.system.entity.User;
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||||
import com.gxwebsoft.gxmu.service.DeclareGuardService;
|
import com.gxwebsoft.gxmu.service.DeclareGuardService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
|
||||||
import com.gxwebsoft.gxmu.service.ReviewListService;
|
import com.gxwebsoft.gxmu.service.ReviewListService;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -54,10 +53,7 @@ class ReviewListControllerAuthTest {
|
|||||||
private ReviewListService reviewListService;
|
private ReviewListService reviewListService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ReviewFlowConfigService reviewFlowConfigService;
|
private ReviewFlowStarterService reviewFlowStarterService;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ReviewFlowService reviewFlowService;
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private DeclareGuardService declareGuardService;
|
private DeclareGuardService declareGuardService;
|
||||||
@@ -117,6 +113,28 @@ class ReviewListControllerAuthTest {
|
|||||||
assertEquals(Constants.RESULT_OK_CODE, result.getCode());
|
assertEquals(Constants.RESULT_OK_CODE, result.getCode());
|
||||||
verify(reviewListService).updateById(any());
|
verify(reviewListService).updateById(any());
|
||||||
verify(declareGuardService).syncReviewStatus("jhqn", PK);
|
verify(declareGuardService).syncReviewStatus("jhqn", PK);
|
||||||
|
// 通过后推进下一级:交给审批链服务按规则解析审批人
|
||||||
|
verify(reviewFlowStarterService).advanceReview("jhqn", PK, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已处理过的节点再 PUT 一次直接拒绝:否则会再插一条下一级待办,
|
||||||
|
* 同一申报出现第二组活跃节点(双击 / 多标签页即可复现)。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void approveIsDeniedWhenNodeIsAlreadyHandled() {
|
||||||
|
ReviewList handled = row(ROW_ID, OWNER_ID);
|
||||||
|
handled.setStatus(1);
|
||||||
|
when(reviewListService.getById(ROW_ID)).thenReturn(handled);
|
||||||
|
loginAs(OWNER_ID);
|
||||||
|
|
||||||
|
ApiResult<?> result = controller.update(request(ROW_ID, 1, "同意"));
|
||||||
|
|
||||||
|
assertNotEquals(Constants.RESULT_OK_CODE, result.getCode());
|
||||||
|
assertTrue(result.getMessage() != null && result.getMessage().contains("已处理"),
|
||||||
|
"重复提交要给出可读提示,实际:" + result.getMessage());
|
||||||
|
verify(reviewListService, never()).updateById(any());
|
||||||
|
verify(reviewFlowStarterService, never()).advanceReview(any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 驳回必须写意见:status=2 且 content 为空/空白时拒绝,提示要可读。 */
|
/** 驳回必须写意见:status=2 且 content 为空/空白时拒绝,提示要可读。 */
|
||||||
@@ -144,6 +162,8 @@ class ReviewListControllerAuthTest {
|
|||||||
|
|
||||||
assertEquals(Constants.RESULT_OK_CODE, result.getCode());
|
assertEquals(Constants.RESULT_OK_CODE, result.getCode());
|
||||||
verify(reviewListService).updateById(any());
|
verify(reviewListService).updateById(any());
|
||||||
|
// 驳回不推进下一级
|
||||||
|
verify(reviewFlowStarterService, never()).advanceReview(any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量审批只要有一行不属于自己就整体拒绝,不能部分落库。 */
|
/** 批量审批只要有一行不属于自己就整体拒绝,不能部分落库。 */
|
||||||
|
|||||||
+87
-3
@@ -1,10 +1,13 @@
|
|||||||
package com.gxwebsoft.gxmu.service.impl;
|
package com.gxwebsoft.gxmu.service.impl;
|
||||||
|
|
||||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
|
import com.gxwebsoft.common.system.entity.Organization;
|
||||||
|
import com.gxwebsoft.common.system.service.OrganizationService;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
|
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
|
||||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||||
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewListService;
|
import com.gxwebsoft.gxmu.service.ReviewListService;
|
||||||
@@ -28,6 +31,7 @@ import java.util.Map;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.inOrder;
|
import static org.mockito.Mockito.inOrder;
|
||||||
@@ -36,10 +40,11 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批流发起与驳回重走(spec #58)。
|
* 审批流发起、驳回重走与逐级推进(spec #58)。
|
||||||
*
|
*
|
||||||
* <p>接缝:{@link ReviewFlowStarterServiceImpl} 的公开方法,不启动 Spring 容器、不连数据库。
|
* <p>接缝:{@link ReviewFlowStarterServiceImpl} 的公开方法,不启动 Spring 容器、不连数据库。
|
||||||
* 断言「写进待办的人是谁」、驳回时「先清旧节点再建第一级」、以及解析不出来时「一个节点都不写」。</p>
|
* 断言「写进待办的人是谁」、驳回时「先清旧节点再建第一级」、推进时「第 2..N 级同样经解析」,
|
||||||
|
* 以及解析不出来时「一个节点都不写」。</p>
|
||||||
*
|
*
|
||||||
* @author Codex
|
* @author Codex
|
||||||
* @since 2026-11-10
|
* @since 2026-11-10
|
||||||
@@ -52,6 +57,7 @@ class ReviewFlowStarterServiceImplTest {
|
|||||||
private static final Integer PK = 11;
|
private static final Integer PK = 11;
|
||||||
private static final Integer COLLEGE_ID = 100;
|
private static final Integer COLLEGE_ID = 100;
|
||||||
private static final Integer DECLARE_USER_ID = 12;
|
private static final Integer DECLARE_USER_ID = 12;
|
||||||
|
private static final String COLLEGE_NAME = "药学院";
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ReviewFlowConfigService reviewFlowConfigService;
|
private ReviewFlowConfigService reviewFlowConfigService;
|
||||||
@@ -68,6 +74,12 @@ class ReviewFlowStarterServiceImplTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private DeclareRecordMapper declareRecordMapper;
|
private DeclareRecordMapper declareRecordMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ModuleRegistryService moduleRegistryService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private OrganizationService organizationService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private ReviewFlowStarterServiceImpl reviewFlowStarterService;
|
private ReviewFlowStarterServiceImpl reviewFlowStarterService;
|
||||||
|
|
||||||
@@ -87,6 +99,12 @@ class ReviewFlowStarterServiceImplTest {
|
|||||||
when(reviewListService.listByModuleNPK(MODULE, PK)).thenReturn(Collections.emptyList());
|
when(reviewListService.listByModuleNPK(MODULE, PK)).thenReturn(Collections.emptyList());
|
||||||
when(declareRecordMapper.selectReviewContext(MODULE, PK)).thenReturn(recordContext());
|
when(declareRecordMapper.selectReviewContext(MODULE, PK)).thenReturn(recordContext());
|
||||||
when(declareRecordMapper.selectRecord(MODULE, PK)).thenReturn(recordContext());
|
when(declareRecordMapper.selectRecord(MODULE, PK)).thenReturn(recordContext());
|
||||||
|
// 学院标识由注册表 has_college 决定是否读
|
||||||
|
when(moduleRegistryService.hasCollege(MODULE)).thenReturn(true);
|
||||||
|
Organization college = new Organization();
|
||||||
|
college.setOrganizationId(COLLEGE_ID);
|
||||||
|
college.setOrganizationName(COLLEGE_NAME);
|
||||||
|
when(organizationService.getById(COLLEGE_ID)).thenReturn(college);
|
||||||
// 默认:解析出第一级审批人 7
|
// 默认:解析出第一级审批人 7
|
||||||
when(reviewerResolver.resolve(any(), any(), any())).thenReturn(Collections.singletonList(7));
|
when(reviewerResolver.resolve(any(), any(), any())).thenReturn(Collections.singletonList(7));
|
||||||
}
|
}
|
||||||
@@ -118,8 +136,12 @@ class ReviewFlowStarterServiceImplTest {
|
|||||||
void startReview_noReviewerThrowsAndWritesNoNode() {
|
void startReview_noReviewerThrowsAndWritesNoNode() {
|
||||||
when(reviewerResolver.resolve(any(), any(), any())).thenReturn(Collections.emptyList());
|
when(reviewerResolver.resolve(any(), any(), any())).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
assertThrows(BusinessException.class, () -> reviewFlowStarterService.startReview(MODULE, PK));
|
BusinessException error = assertThrows(BusinessException.class,
|
||||||
|
() -> reviewFlowStarterService.startReview(MODULE, PK));
|
||||||
|
|
||||||
|
// 兜底消息与 ReviewerResolver 同口径:哪个学院、第几级
|
||||||
|
assertTrue(error.getMessage().contains(COLLEGE_NAME), error.getMessage());
|
||||||
|
assertTrue(error.getMessage().contains("第 1 级"), error.getMessage());
|
||||||
verify(reviewListService, never()).save(any());
|
verify(reviewListService, never()).save(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +158,68 @@ class ReviewFlowStarterServiceImplTest {
|
|||||||
verify(reviewListService, never()).save(any());
|
verify(reviewListService, never()).save(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 逐级推进:第 2..N 级同样经 ReviewerResolver,不复制 review_user_id
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** 通过后推进:下一级节点的人来自解析,而不是上一级配置里写死的 review_user_id。 */
|
||||||
|
@Test
|
||||||
|
void advanceReview_writesResolvedReviewerAsNextNode() {
|
||||||
|
when(reviewFlowService.listByTitle("十佳青年")).thenReturn(Arrays.asList(
|
||||||
|
flow("十佳青年", 1, 7), flow("十佳青年", 2, 8)));
|
||||||
|
when(reviewerResolver.resolve(any(), any(), any())).thenReturn(Collections.singletonList(43));
|
||||||
|
|
||||||
|
reviewFlowStarterService.advanceReview(MODULE, PK, 0);
|
||||||
|
|
||||||
|
ArgumentCaptor<ReviewList> captor = ArgumentCaptor.forClass(ReviewList.class);
|
||||||
|
verify(reviewListService).save(captor.capture());
|
||||||
|
ReviewList saved = captor.getValue();
|
||||||
|
assertEquals(MODULE, saved.getModule());
|
||||||
|
assertEquals(PK, saved.getPk());
|
||||||
|
assertEquals(Integer.valueOf(1), saved.getSortNumber());
|
||||||
|
assertEquals(Integer.valueOf(43), saved.getUserId(), "下一级应落在解析出的人上,而不是 review_user_id=8");
|
||||||
|
// 解析的是第 2 级那条配置,并且带上了学院上下文
|
||||||
|
verify(reviewerResolver).resolve(
|
||||||
|
org.mockito.ArgumentMatchers.argThat(item -> item != null && Integer.valueOf(2).equals(item.getLevel())),
|
||||||
|
eq(COLLEGE_ID), eq(DECLARE_USER_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已是最后一级:链条结束,不再生成节点。 */
|
||||||
|
@Test
|
||||||
|
void advanceReview_lastLevelWritesNothing() {
|
||||||
|
when(reviewFlowService.listByTitle("十佳青年")).thenReturn(Arrays.asList(
|
||||||
|
flow("十佳青年", 1, 7), flow("十佳青年", 2, 8)));
|
||||||
|
|
||||||
|
reviewFlowStarterService.advanceReview(MODULE, PK, 1);
|
||||||
|
|
||||||
|
verify(reviewListService, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 推进时解析为空:抛可读异常(带学院与层级)且不写节点,避免第二组无人认领的待办。 */
|
||||||
|
@Test
|
||||||
|
void advanceReview_emptyReviewerThrowsAndWritesNoNode() {
|
||||||
|
when(reviewFlowService.listByTitle("十佳青年")).thenReturn(Arrays.asList(
|
||||||
|
flow("十佳青年", 1, 7), flow("十佳青年", 2, 8)));
|
||||||
|
when(reviewerResolver.resolve(any(), any(), any())).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
BusinessException error = assertThrows(BusinessException.class,
|
||||||
|
() -> reviewFlowStarterService.advanceReview(MODULE, PK, 0));
|
||||||
|
|
||||||
|
assertTrue(error.getMessage().contains(COLLEGE_NAME), error.getMessage());
|
||||||
|
assertTrue(error.getMessage().contains("第 2 级"), error.getMessage());
|
||||||
|
verify(reviewListService, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 没有审批链配置时推进是 no-op,不写节点也不报错。 */
|
||||||
|
@Test
|
||||||
|
void advanceReview_withoutFlowConfigDoesNothing() {
|
||||||
|
when(reviewFlowConfigService.getByModule(MODULE)).thenReturn(null);
|
||||||
|
|
||||||
|
reviewFlowStarterService.advanceReview(MODULE, PK, 0);
|
||||||
|
|
||||||
|
verify(reviewListService, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 驳回重走:先清旧节点,再解析第一级
|
// 驳回重走:先清旧节点,再解析第一级
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user