Merge branch 'spec58/integration' into spec58/t5

This commit is contained in:
2026-11-03 22:26:58 +08:00
6 changed files with 535 additions and 0 deletions
@@ -1,6 +1,7 @@
package com.gxwebsoft.gxmu.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.gxmu.service.ReviewFlowLevelValidator;
import com.gxwebsoft.gxmu.service.ReviewFlowService;
import com.gxwebsoft.gxmu.entity.ReviewFlow;
import com.gxwebsoft.gxmu.param.ReviewFlowParam;
@@ -16,8 +17,13 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* 审核流控制器
@@ -63,6 +69,7 @@ public class ReviewFlowController extends BaseController {
@ApiOperation("添加审核流")
@PostMapping()
public ApiResult<?> save(@RequestBody ReviewFlow reviewFlow) {
validateLevels(reviewFlow == null ? null : java.util.Collections.singletonList(reviewFlow));
// 记录当前登录用户id
// ReviewFlow checkByName = reviewFlowService.getByTitle(reviewFlow.getTitle());
// if (checkByName != null) {
@@ -77,6 +84,7 @@ public class ReviewFlowController extends BaseController {
@ApiOperation("修改审核流")
@PutMapping()
public ApiResult<?> update(@RequestBody ReviewFlow reviewFlow) {
validateUpdate(reviewFlow);
// ReviewFlow checkByName = reviewFlowService.getByTitle(reviewFlow.getTitle());
// if (checkByName != null && !Objects.equals(checkByName.getId(), reviewFlow.getId())) {
// return fail("该名称已存在");
@@ -99,6 +107,7 @@ public class ReviewFlowController extends BaseController {
@ApiOperation("批量添加审核流")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ReviewFlow> list) {
validateLevels(list);
if (reviewFlowService.saveBatch(list)) {
return success("添加成功");
}
@@ -108,12 +117,142 @@ public class ReviewFlowController extends BaseController {
@ApiOperation("批量修改审核流")
@PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<ReviewFlow> batchParam) {
validateBatchUpdate(batchParam);
if (batchParam.update(reviewFlowService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
/**
* 校验"保存后这条审批链的层级从 1 连续升序"(spec #58 行为约定)。
*
* <p>管理端是"先删旧节点、再逐级新增",因此单条保存时必须把库里同 title 的已有节点一起看,
* 否则逐级保存会被误判为缺级。
*/
private void validateLevels(List<ReviewFlow> incoming) {
if (incoming == null || incoming.isEmpty()) {
return;
}
List<ReviewFlow> proposed = new ArrayList<>();
Set<String> titles = new LinkedHashSet<>();
Set<Integer> replacedIds = new LinkedHashSet<>();
for (ReviewFlow flow : incoming) {
if (flow == null) {
continue;
}
proposed.add(flow);
if (flow.getId() != null) {
replacedIds.add(flow.getId());
}
if (flow.getTitle() != null) {
titles.add(flow.getTitle().trim());
}
}
for (String title : titles) {
List<ReviewFlow> existing = reviewFlowService.listByTitle(title);
if (existing == null) {
continue;
}
for (ReviewFlow flow : existing) {
if (flow != null && flow.getId() != null && !replacedIds.contains(flow.getId())) {
proposed.add(flow);
}
}
}
ReviewFlowLevelValidator.validate(proposed);
}
/**
* 修改单条节点:未传的字段沿用库里原值(MyBatis-Plus 同样忽略 null),再校验受影响链的连续性。
*/
private void validateUpdate(ReviewFlow reviewFlow) {
if (reviewFlow == null) {
return;
}
ReviewFlow existing = reviewFlow.getId() == null ? null : reviewFlowService.getById(reviewFlow.getId());
ReviewFlow merged = merge(existing, reviewFlow);
validateLevels(java.util.Collections.singletonList(merged));
if (existing != null && !sameTitle(existing.getTitle(), merged.getTitle())) {
validateRemaining(existing.getTitle(), java.util.Collections.singletonList(reviewFlow.getId()));
}
}
/**
* 批量修改:BatchParam 只带一个 data,id 集合里每行都按同样字段更新。
*/
private void validateBatchUpdate(BatchParam<ReviewFlow> batchParam) {
if (batchParam == null || batchParam.getData() == null
|| batchParam.getIds() == null || batchParam.getIds().isEmpty()) {
return;
}
List<Integer> ids = new ArrayList<>();
for (Serializable id : batchParam.getIds()) {
if (id instanceof Number) {
ids.add(((Number) id).intValue());
}
}
if (ids.isEmpty()) {
return;
}
List<ReviewFlow> existingRows = reviewFlowService.listByIds(ids);
if (existingRows == null || existingRows.isEmpty()) {
return;
}
List<ReviewFlow> mergedRows = new ArrayList<>();
List<String> oldTitles = new ArrayList<>();
for (ReviewFlow existing : existingRows) {
if (existing == null) {
continue;
}
mergedRows.add(merge(existing, batchParam.getData()));
oldTitles.add(existing.getTitle());
}
validateLevels(mergedRows);
Set<String> newTitles = new LinkedHashSet<>();
for (ReviewFlow row : mergedRows) {
newTitles.add(row.getTitle() == null ? "" : row.getTitle().trim());
}
for (String oldTitle : oldTitles) {
if (!newTitles.contains(oldTitle == null ? "" : oldTitle.trim())) {
validateRemaining(oldTitle, ids);
}
}
}
/**
* 校验改名/移走后原审批链剩下的节点仍连续。
*/
private void validateRemaining(String title, Collection<Integer> excludedIds) {
List<ReviewFlow> remaining = new ArrayList<>();
List<ReviewFlow> existing = title == null ? null : reviewFlowService.listByTitle(title);
if (existing != null) {
for (ReviewFlow flow : existing) {
if (flow != null && !excludedIds.contains(flow.getId())) {
remaining.add(flow);
}
}
}
ReviewFlowLevelValidator.validate(remaining);
}
private ReviewFlow merge(ReviewFlow existing, ReviewFlow incoming) {
if (existing == null) {
return incoming;
}
if (incoming.getTitle() != null) {
existing.setTitle(incoming.getTitle());
}
if (incoming.getLevel() != null) {
existing.setLevel(incoming.getLevel());
}
return existing;
}
private boolean sameTitle(String left, String right) {
return Objects.equals(left == null ? null : left.trim(), right == null ? null : right.trim());
}
@ApiOperation("批量删除审核流")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
@@ -0,0 +1,81 @@
package com.gxwebsoft.gxmu.service;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.gxmu.entity.ReviewFlow;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 审批链层级连续性校验(spec #58 行为约定:层级从 1 连续升序)。
*
* <p>一条审批链的层级必须恰好是 1..N,缺级会让申报卡在半路,重复级会让两个人拿到同一条待办。
* 按 title 分组校验,纯函数,不依赖数据库。
*
* @author Codex
* @since 2026-11-03
*/
public final class ReviewFlowLevelValidator {
private ReviewFlowLevelValidator() {
}
/**
* 校验一组审批流节点的层级;不合法时抛出 {@link BusinessException}。
*
* @param flows 同一轮保存涉及的节点,可包含多条审批链(按 title 分组)
*/
public static void validate(List<ReviewFlow> flows) {
if (flows == null || flows.isEmpty()) {
return;
}
Map<String, List<Integer>> levelsByTitle = new LinkedHashMap<>();
for (ReviewFlow flow : flows) {
if (flow == null) {
continue;
}
levelsByTitle.computeIfAbsent(titleOf(flow), key -> new ArrayList<>()).add(flow.getLevel());
}
for (Map.Entry<String, List<Integer>> entry : levelsByTitle.entrySet()) {
validateOne(entry.getKey(), entry.getValue());
}
}
private static void validateOne(String title, List<Integer> levels) {
for (Integer level : levels) {
if (level == null) {
throw new BusinessException("审批链「" + displayTitle(title) + "」存在未填写层级的节点,层级必须从 1 连续升序");
}
}
List<Integer> sorted = new ArrayList<>(levels);
Collections.sort(sorted);
for (int index = 0; index < sorted.size(); index++) {
if (sorted.get(index) != index + 1) {
throw new BusinessException("审批链「" + displayTitle(title) + "」的层级必须从 1 连续升序,当前层级为 "
+ join(sorted) + ",请调整后再保存");
}
}
}
private static String titleOf(ReviewFlow flow) {
return flow.getTitle() == null ? "" : flow.getTitle().trim();
}
private static String displayTitle(String title) {
return title.isEmpty() ? "未命名" : title;
}
private static String join(List<Integer> levels) {
StringBuilder builder = new StringBuilder();
for (int index = 0; index < levels.size(); index++) {
if (index > 0) {
builder.append(", ");
}
builder.append(levels.get(index));
}
return builder.toString();
}
}
@@ -1,5 +1,6 @@
package com.gxwebsoft.gxmu.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.gxmu.entity.ReviewFlow;
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
import com.gxwebsoft.gxmu.entity.ReviewList;
@@ -8,6 +9,7 @@ import com.gxwebsoft.gxmu.service.ReviewFlowService;
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
import com.gxwebsoft.gxmu.service.ReviewListService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
@@ -49,7 +51,12 @@ public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
saveFirstReviewNode(module, pk, firstOne);
}
/**
* 驳回后重走审批链:先清掉同一申报记录下的旧节点,再生成从第一级开始的新节点,
* 保证任一时刻只有一组活跃节点(spec #58 行为约定)。判定"上一条是驳回"必须在删除前完成。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void restartReviewWhenLastRejected(String module, Integer pk) {
if (module == null || pk == null) {
return;
@@ -75,6 +82,9 @@ public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
if (firstOne == null) {
return;
}
reviewListService.remove(new LambdaQueryWrapper<ReviewList>()
.eq(ReviewList::getModule, module)
.eq(ReviewList::getPk, pk));
saveFirstReviewNode(module, pk, firstOne);
}