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.annotation.OperationLog;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
||||
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 com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -38,11 +33,7 @@ public class ManuscriptController extends BaseController {
|
||||
@Resource
|
||||
private ManuscriptService manuscriptService;
|
||||
@Resource
|
||||
private ReviewFlowConfigService reviewFlowConfigService;
|
||||
@Resource
|
||||
private ReviewFlowService reviewFlowService;
|
||||
@Resource
|
||||
private ReviewListService reviewListService;
|
||||
private ReviewFlowStarterService reviewFlowStarterService;
|
||||
|
||||
@ApiOperation("分页查询稿件")
|
||||
@GetMapping("/page")
|
||||
@@ -75,23 +66,8 @@ public class ManuscriptController extends BaseController {
|
||||
manuscript.setUserId(loginUser.getUserId());
|
||||
}
|
||||
if (manuscriptService.save(manuscript)) {
|
||||
// 查找该模块的审核流
|
||||
ReviewFlowConfig reviewFlowConfig = reviewFlowConfigService.getByModule("cms_manuscript");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 首级审批人同样经 ReviewerResolver 解析(与其它模块同一条路径),不复制 review_user_id
|
||||
reviewFlowStarterService.startReview("cms_manuscript", manuscript.getId());
|
||||
return success("添加成功");
|
||||
}
|
||||
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 com.gxwebsoft.common.core.Constants;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
||||
import com.gxwebsoft.gxmu.constant.ReviewStatus;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
||||
import com.gxwebsoft.gxmu.service.DeclareGuardService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewListService;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||
import com.gxwebsoft.gxmu.param.ReviewListParam;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
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.annotation.OperationLog;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
@@ -23,11 +19,16 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审核列表控制器
|
||||
*
|
||||
* <p>审批动作(通过 / 驳回)的归属校验、状态校验留在这里(HTTP 层的准入判断);
|
||||
* 通过后「推进到下一级」属于审批链业务规则,交给 {@link ReviewFlowStarterService#advanceReview},
|
||||
* 审批人一律经 {@link com.gxwebsoft.gxmu.service.ReviewerResolver} 解析,不再复制 review_user_id。
|
||||
*
|
||||
* @author LX
|
||||
* @since 2026-03-18 15:50:51
|
||||
*/
|
||||
@@ -38,11 +39,9 @@ public class ReviewListController extends BaseController {
|
||||
@Resource
|
||||
private ReviewListService reviewListService;
|
||||
@Resource
|
||||
private ReviewFlowConfigService reviewFlowConfigService;
|
||||
@Resource
|
||||
private ReviewFlowService reviewFlowService;
|
||||
@Resource
|
||||
private DeclareGuardService declareGuardService;
|
||||
@Resource
|
||||
private ReviewFlowStarterService reviewFlowStarterService;
|
||||
|
||||
@ApiOperation("分页查询审核列表")
|
||||
@GetMapping("/page")
|
||||
@@ -100,8 +99,14 @@ public class ReviewListController extends BaseController {
|
||||
@ApiOperation("修改审核列表")
|
||||
@PutMapping()
|
||||
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) {
|
||||
return denied;
|
||||
}
|
||||
@@ -110,34 +115,10 @@ public class ReviewListController extends BaseController {
|
||||
return denied;
|
||||
}
|
||||
if (reviewListService.updateById(reviewList)) {
|
||||
// 通过进入下一个
|
||||
if (Integer.valueOf(1).equals(reviewList.getStatus())) {
|
||||
ReviewFlowConfig reviewFlowConfig = reviewFlowConfigService.getByModule(reviewList.getModule());
|
||||
if (reviewFlowConfig != null) {
|
||||
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 ()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
// 通过则推进到下一级;下一级的审批人由审批链配置解析,不复制本级的 review_user_id
|
||||
if (Integer.valueOf(ReviewStatus.APPROVED).equals(reviewList.getStatus())) {
|
||||
reviewFlowStarterService.advanceReview(reviewList.getModule(), reviewList.getPk(),
|
||||
existing == null ? null : existing.getSortNumber());
|
||||
}
|
||||
declareGuardService.syncReviewStatus(reviewList.getModule(), reviewList.getPk());
|
||||
return success("修改成功");
|
||||
@@ -166,11 +147,15 @@ public class ReviewListController extends BaseController {
|
||||
@ApiOperation("批量修改审核列表")
|
||||
@PutMapping("/batch")
|
||||
public ApiResult<?> updateBatch(@RequestBody BatchParam<ReviewList> batchParam) {
|
||||
// 批量审批逐行校验归属,只要有一行不是自己的就整体拒绝,避免部分落库
|
||||
// 批量审批逐行校验归属与状态,只要有一行不满足就整体拒绝,避免部分落库
|
||||
ApiResult<?> denied = denyIfBatchNotAssignee(batchParam);
|
||||
if (denied != null) {
|
||||
return denied;
|
||||
}
|
||||
denied = denyIfBatchAlreadyHandled(batchParam);
|
||||
if (denied != null) {
|
||||
return denied;
|
||||
}
|
||||
denied = denyIfRejectWithoutContent(batchParam == null ? null : batchParam.getData());
|
||||
if (denied != null) {
|
||||
return denied;
|
||||
@@ -191,7 +176,10 @@ public class ReviewListController extends BaseController {
|
||||
* @return null 表示校验通过,否则返回 403 结果
|
||||
*/
|
||||
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();
|
||||
if (existing == null || loginUserId == null || !loginUserId.equals(existing.getUserId())) {
|
||||
return new ApiResult<>(Constants.UNAUTHORIZED_CODE, Constants.UNAUTHORIZED_MSG);
|
||||
@@ -199,6 +187,25 @@ public class ReviewListController extends BaseController {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
for (java.io.Serializable id : batchParam.getIds()) {
|
||||
for (Serializable id : batchParam.getIds()) {
|
||||
ApiResult<?> denied = denyIfNotAssignee(id instanceof Number ? ((Number) id).intValue() : null);
|
||||
if (denied != null) {
|
||||
return denied;
|
||||
@@ -218,6 +225,26 @@ public class ReviewListController extends BaseController {
|
||||
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 表示校验通过,否则返回可读的失败结果
|
||||
*/
|
||||
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 null;
|
||||
@@ -236,7 +264,7 @@ public class ReviewListController extends BaseController {
|
||||
if (batchParam == null || batchParam.getIds() == null) {
|
||||
return;
|
||||
}
|
||||
for (java.io.Serializable id : batchParam.getIds()) {
|
||||
for (Serializable id : batchParam.getIds()) {
|
||||
ReviewList saved = reviewListService.getById(id);
|
||||
if (saved != null) {
|
||||
declareGuardService.syncReviewStatus(saved.getModule(), saved.getPk());
|
||||
@@ -283,4 +311,4 @@ public class ReviewListController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,16 @@ public interface ReviewFlowStarterService {
|
||||
* @param 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.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.gxwebsoft.gxmu.constant.ReviewStatus;
|
||||
import com.gxwebsoft.gxmu.entity.Declare;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
||||
@@ -160,7 +161,7 @@ public class DeclareGuardServiceImpl implements DeclareGuardService {
|
||||
return;
|
||||
}
|
||||
boolean pending = nodes.stream()
|
||||
.anyMatch(node -> node.getStatus() != null && node.getStatus() == 0);
|
||||
.anyMatch(node -> node.getStatus() != null && node.getStatus() == ReviewStatus.PENDING);
|
||||
if (pending) {
|
||||
declareRecordMapper.updateStatus(module, recordId, STATUS_PENDING, null);
|
||||
return;
|
||||
@@ -181,7 +182,7 @@ public class DeclareGuardServiceImpl implements DeclareGuardService {
|
||||
return;
|
||||
}
|
||||
declareRecordMapper.updateStatus(module, recordId,
|
||||
last.getStatus() == 2 ? STATUS_REJECTED : STATUS_PASSED, null);
|
||||
last.getStatus() == ReviewStatus.REJECTED ? STATUS_REJECTED : STATUS_PASSED, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -200,7 +201,7 @@ public class DeclareGuardServiceImpl implements DeclareGuardService {
|
||||
return "只有待审核的申报可以撤回";
|
||||
}
|
||||
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) {
|
||||
return "已有审核通过的记录不能撤回,请联系管理员处理";
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ package com.gxwebsoft.gxmu.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.ReviewFlowConfig;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
||||
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
||||
@@ -16,28 +20,22 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 审核流程发起服务实现
|
||||
*
|
||||
* <p>审批人不再写死:由 {@link ReviewerResolver} 按审批链配置的 reviewer_rule 解析,
|
||||
* 一期串行单审取 userId 最小的那个;解析不出来直接抛业务异常,不生成待办。</p>
|
||||
* 一期串行单审取 userId 最小的那个;解析不出来直接抛业务异常,不生成待办。
|
||||
* 首级(发起)、驳回重提、逐级推进三条路径都走同一套解析,第 2..N 级不再复制 review_user_id。</p>
|
||||
*
|
||||
* @author LX
|
||||
* @since 2026-03-18
|
||||
*/
|
||||
@Service
|
||||
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
|
||||
private ReviewFlowConfigService reviewFlowConfigService;
|
||||
@@ -49,6 +47,10 @@ public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
||||
private ReviewerResolver reviewerResolver;
|
||||
@Resource
|
||||
private DeclareRecordMapper declareRecordMapper;
|
||||
@Resource
|
||||
private ModuleRegistryService moduleRegistryService;
|
||||
@Resource
|
||||
private OrganizationService organizationService;
|
||||
|
||||
@Override
|
||||
public void startReview(String module, Integer pk) {
|
||||
@@ -59,15 +61,11 @@ public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
||||
if (reviewLists != null && !reviewLists.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ReviewFlowConfig reviewFlowConfig = reviewFlowConfigService.getByModule(module);
|
||||
if (reviewFlowConfig == null || reviewFlowConfig.getFlowId() == null) {
|
||||
List<ReviewFlow> flowList = loadFlowList(module);
|
||||
if (flowList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ReviewFlow firstOne = getFirstFlow(reviewFlowConfig);
|
||||
if (firstOne == null) {
|
||||
return;
|
||||
}
|
||||
saveFirstReviewNode(module, pk, firstOne);
|
||||
saveReviewNode(module, pk, flowList.get(0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,61 +88,125 @@ public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
||||
Comparator.nullsLast(LocalDateTime::compareTo))
|
||||
.thenComparing(ReviewList::getId, Comparator.nullsLast(Integer::compareTo)))
|
||||
.orElse(null);
|
||||
if (latestReview == null || latestReview.getStatus() == null || latestReview.getStatus() != 2) {
|
||||
if (latestReview == null || latestReview.getStatus() == null
|
||||
|| latestReview.getStatus() != ReviewStatus.REJECTED) {
|
||||
return;
|
||||
}
|
||||
ReviewFlowConfig reviewFlowConfig = reviewFlowConfigService.getByModule(module);
|
||||
if (reviewFlowConfig == null || reviewFlowConfig.getFlowId() == null) {
|
||||
return;
|
||||
}
|
||||
ReviewFlow firstOne = getFirstFlow(reviewFlowConfig);
|
||||
if (firstOne == null) {
|
||||
List<ReviewFlow> flowList = loadFlowList(module);
|
||||
if (flowList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
reviewListService.remove(new LambdaQueryWrapper<ReviewList>()
|
||||
.eq(ReviewList::getModule, module)
|
||||
.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());
|
||||
if (reviewFlow == null) {
|
||||
return null;
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
List<ReviewFlow> reviewFlowList = reviewFlowService.listByTitle(reviewFlow.getTitle());
|
||||
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);
|
||||
Integer collegeId = intValue(record, "collegeId");
|
||||
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()) {
|
||||
throw new BusinessException("模块 " + module + " 主键 " + pk + " 第 "
|
||||
+ (firstOne.getLevel() == null ? "?" : firstOne.getLevel())
|
||||
+ " 级审批人解析结果为空,未生成待办");
|
||||
// 兜底消息与 ReviewerResolver 同口径:哪个学院、第几级、哪个模块/主键
|
||||
String scope = collegeId == null ? "申报记录缺少学院标识," : "学院「" + collegeName(collegeId) + "」";
|
||||
throw new BusinessException(scope + "第 " + levelOf(flow)
|
||||
+ " 级审批人解析结果为空(模块 " + module + " 主键 " + pk + "),未生成待办");
|
||||
}
|
||||
ReviewList reviewList = new ReviewList();
|
||||
reviewList.setPk(pk);
|
||||
reviewList.setModule(module);
|
||||
reviewList.setUserId(reviewers.get(0));
|
||||
reviewList.setSortNumber(0);
|
||||
reviewList.setSortNumber(sortNumber);
|
||||
reviewListService.save(reviewList);
|
||||
}
|
||||
|
||||
/** 读申报记录的申报人与学院标识;只有 4 张在跑表有 college_id */
|
||||
/**
|
||||
* 读申报记录的申报人与学院标识。
|
||||
*
|
||||
* <p>哪张业务表有 college_id 由注册表 has_college 决定,后端不再维护模块白名单
|
||||
* (ADR 0006:新增表只改注册表一行)。
|
||||
*/
|
||||
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.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) {
|
||||
if (record == 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.metadata.IPage;
|
||||
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.TzbcyForm;
|
||||
import com.gxwebsoft.gxmu.mapper.TzbcyFormMapper;
|
||||
@@ -157,7 +158,7 @@ public class TzbcyFormServiceImpl extends ServiceImpl<TzbcyFormMapper, TzbcyForm
|
||||
}
|
||||
List<ReviewList> reviewList = form.getReviewList();
|
||||
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) {
|
||||
|
||||
@@ -6,8 +6,7 @@ import com.gxwebsoft.common.core.web.BatchParam;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||
import com.gxwebsoft.gxmu.service.DeclareGuardService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewListService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -54,10 +53,7 @@ class ReviewListControllerAuthTest {
|
||||
private ReviewListService reviewListService;
|
||||
|
||||
@Mock
|
||||
private ReviewFlowConfigService reviewFlowConfigService;
|
||||
|
||||
@Mock
|
||||
private ReviewFlowService reviewFlowService;
|
||||
private ReviewFlowStarterService reviewFlowStarterService;
|
||||
|
||||
@Mock
|
||||
private DeclareGuardService declareGuardService;
|
||||
@@ -117,6 +113,28 @@ class ReviewListControllerAuthTest {
|
||||
assertEquals(Constants.RESULT_OK_CODE, result.getCode());
|
||||
verify(reviewListService).updateById(any());
|
||||
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 为空/空白时拒绝,提示要可读。 */
|
||||
@@ -144,6 +162,8 @@ class ReviewListControllerAuthTest {
|
||||
|
||||
assertEquals(Constants.RESULT_OK_CODE, result.getCode());
|
||||
verify(reviewListService).updateById(any());
|
||||
// 驳回不推进下一级
|
||||
verify(reviewFlowStarterService, never()).advanceReview(any(), any(), any());
|
||||
}
|
||||
|
||||
/** 批量审批只要有一行不属于自己就整体拒绝,不能部分落库。 */
|
||||
|
||||
+87
-3
@@ -1,10 +1,13 @@
|
||||
package com.gxwebsoft.gxmu.service.impl;
|
||||
|
||||
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.ReviewFlowConfig;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
||||
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
||||
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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
@@ -36,10 +40,11 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 审批流发起与驳回重走(spec #58)。
|
||||
* 审批流发起、驳回重走与逐级推进(spec #58)。
|
||||
*
|
||||
* <p>接缝:{@link ReviewFlowStarterServiceImpl} 的公开方法,不启动 Spring 容器、不连数据库。
|
||||
* 断言「写进待办的人是谁」、驳回时「先清旧节点再建第一级」、以及解析不出来时「一个节点都不写」。</p>
|
||||
* 断言「写进待办的人是谁」、驳回时「先清旧节点再建第一级」、推进时「第 2..N 级同样经解析」,
|
||||
* 以及解析不出来时「一个节点都不写」。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-11-10
|
||||
@@ -52,6 +57,7 @@ class ReviewFlowStarterServiceImplTest {
|
||||
private static final Integer PK = 11;
|
||||
private static final Integer COLLEGE_ID = 100;
|
||||
private static final Integer DECLARE_USER_ID = 12;
|
||||
private static final String COLLEGE_NAME = "药学院";
|
||||
|
||||
@Mock
|
||||
private ReviewFlowConfigService reviewFlowConfigService;
|
||||
@@ -68,6 +74,12 @@ class ReviewFlowStarterServiceImplTest {
|
||||
@Mock
|
||||
private DeclareRecordMapper declareRecordMapper;
|
||||
|
||||
@Mock
|
||||
private ModuleRegistryService moduleRegistryService;
|
||||
|
||||
@Mock
|
||||
private OrganizationService organizationService;
|
||||
|
||||
@InjectMocks
|
||||
private ReviewFlowStarterServiceImpl reviewFlowStarterService;
|
||||
|
||||
@@ -87,6 +99,12 @@ class ReviewFlowStarterServiceImplTest {
|
||||
when(reviewListService.listByModuleNPK(MODULE, PK)).thenReturn(Collections.emptyList());
|
||||
when(declareRecordMapper.selectReviewContext(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
|
||||
when(reviewerResolver.resolve(any(), any(), any())).thenReturn(Collections.singletonList(7));
|
||||
}
|
||||
@@ -118,8 +136,12 @@ class ReviewFlowStarterServiceImplTest {
|
||||
void startReview_noReviewerThrowsAndWritesNoNode() {
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -136,6 +158,68 @@ class ReviewFlowStarterServiceImplTest {
|
||||
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