feat(spec58): 模块注册表后端与模块硬编码收敛(#60)
- 新增 gxmu/module 包:ModuleRegistry 实体/Mapper/Service,listEnabled 按 sort_number 升序,titleOf 未注册回退 module 原串 - 新增 GET /api/gxmu/module-registry/list(需登录),返回启用模块元数据 - ReviewFlowConfigServiceImpl / ReviewListServiceImpl 的模块名映射改读注册表,查不到回退原串 - DeclareGuardServiceImpl 的模块集合改为注册表驱动,含 enabled=0(停用模块存量仍可撤回/作废) - 启动期一致性校验:table_name≠module 或业务表缺失即启动失败,gxmu.module-registry.strict=false 可关闭 - 单测 13 例:注册表加载与排序、titleOf 回退、启动校验、module 名回退
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
package com.gxwebsoft.gxmu.module;
|
||||||
|
|
||||||
|
import com.gxwebsoft.gxmu.module.entity.ModuleRegistry;
|
||||||
|
import com.gxwebsoft.gxmu.module.mapper.ModuleRegistryMapper;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块注册表启动期一致性校验(spec #58 用户故事 21)。
|
||||||
|
*
|
||||||
|
* <p>两件事不满足就在启动期直接失败,而不是等运行到详情页才发现空白:
|
||||||
|
* <ol>
|
||||||
|
* <li>注册表 table_name 与 module 一致(module 同时是业务表名);</li>
|
||||||
|
* <li>注册表引用的业务表在库里真实存在。</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>允许 gxmu.module-registry.strict=false 关闭校验,便于无库本地启动;
|
||||||
|
* 生产环境必须保持默认的 true。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-10
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ModuleRegistryIntegrityChecker implements ApplicationRunner {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ModuleRegistryIntegrityChecker.class);
|
||||||
|
|
||||||
|
@Value("${gxmu.module-registry.strict:true}")
|
||||||
|
private boolean strict;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ModuleRegistryService moduleRegistryService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ModuleRegistryMapper moduleRegistryMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行校验;不一致时抛 {@link IllegalStateException} 让启动失败。
|
||||||
|
*/
|
||||||
|
public void check() {
|
||||||
|
if (!strict) {
|
||||||
|
log.warn("gxmu.module-registry.strict=false,跳过模块注册表一致性校验(仅限本地无库启动)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<ModuleRegistry> modules = moduleRegistryService.listAll();
|
||||||
|
if (modules == null || modules.isEmpty()) {
|
||||||
|
log.warn("模块注册表为空,跳过一致性校验;请确认 gxmu_module_registry 已初始化");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> mismatches = new ArrayList<>();
|
||||||
|
Set<String> tableNames = new LinkedHashSet<>();
|
||||||
|
for (ModuleRegistry item : modules) {
|
||||||
|
String module = item.getModule();
|
||||||
|
String tableName = item.getTableName();
|
||||||
|
if (tableName == null || !tableName.equals(module)) {
|
||||||
|
mismatches.add(module + " -> " + tableName);
|
||||||
|
} else {
|
||||||
|
tableNames.add(tableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mismatches.isEmpty()) {
|
||||||
|
throw new IllegalStateException("模块注册表 table_name 与 module 不一致:" + String.join(", ", mismatches)
|
||||||
|
+ "。module 同时是业务表名,必须与 table_name 相同(见 gxmu_module_registry 表注释);"
|
||||||
|
+ "请修正注册表数据后重启,或本地无库启动时设置 gxmu.module-registry.strict=false");
|
||||||
|
}
|
||||||
|
Set<String> existing = new HashSet<>(moduleRegistryMapper.selectExistingTableNames(tableNames));
|
||||||
|
List<String> missing = new ArrayList<>();
|
||||||
|
for (String tableName : tableNames) {
|
||||||
|
if (!existing.contains(tableName)) {
|
||||||
|
missing.add(tableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!missing.isEmpty()) {
|
||||||
|
throw new IllegalStateException("模块注册表引用的业务表不存在:" + String.join(", ", missing)
|
||||||
|
+ "。请先执行建表脚本或修正注册表数据;本地无库启动时可设置 gxmu.module-registry.strict=false");
|
||||||
|
}
|
||||||
|
log.info("模块注册表一致性校验通过:{} 个模块,{} 张业务表", modules.size(), tableNames.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.gxwebsoft.gxmu.module.controller;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
|
import com.gxwebsoft.gxmu.module.entity.ModuleRegistry;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块注册表控制器
|
||||||
|
*
|
||||||
|
* <p>前端启动时一次性拉取启用模块清单并缓存,模块下拉、详情页、导出模板都从这里驱动。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-10
|
||||||
|
*/
|
||||||
|
@Api(tags = "模块注册表")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/gxmu/module-registry")
|
||||||
|
public class ModuleRegistryController extends BaseController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ModuleRegistryService moduleRegistryService;
|
||||||
|
|
||||||
|
@ApiOperation("启用模块清单")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public ApiResult<List<ModuleRegistry>> list() {
|
||||||
|
return success(moduleRegistryService.listEnabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.gxwebsoft.gxmu.module.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块注册表:业务模块元数据的唯一来源,见 docs/adr/0006-approval-chain-convergence.md。
|
||||||
|
*
|
||||||
|
* <p>module 同时是业务表名,因此 tableName 必须与它一致,
|
||||||
|
* 不一致会在启动期直接失败(见 ModuleRegistryIntegrityChecker)。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-10
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@ApiModel(value = "ModuleRegistry对象", description = "模块注册表")
|
||||||
|
@TableName("gxmu_module_registry")
|
||||||
|
public class ModuleRegistry implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "模块标识,同时是业务表名")
|
||||||
|
private String module;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "业务表名,必须与 module 一致")
|
||||||
|
private String tableName;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "模块标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "详情页配置键(moduleDetailConfig.ts)")
|
||||||
|
private String detailKey;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "导出模板标识,空表示走默认导出")
|
||||||
|
private String exportTemplate;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "是否启用:0否 1是")
|
||||||
|
private Integer enabled;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "排序号,小的在前")
|
||||||
|
private Integer sortNumber;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "是否删除, 0否, 1是")
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "租户id")
|
||||||
|
private Integer tenantId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "创建时间")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "修改时间")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
private Integer userId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.gxwebsoft.gxmu.module.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.gxwebsoft.gxmu.module.entity.ModuleRegistry;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块注册表 Mapper
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-10
|
||||||
|
*/
|
||||||
|
public interface ModuleRegistryMapper extends BaseMapper<ModuleRegistry> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前库中真实存在的表名,用于启动期校验注册表引用的业务表。
|
||||||
|
*
|
||||||
|
* @param names 待校验的表名集合
|
||||||
|
* @return 存在的表名
|
||||||
|
*/
|
||||||
|
@Select("<script>"
|
||||||
|
+ "SELECT TABLE_NAME FROM information_schema.TABLES "
|
||||||
|
+ "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN "
|
||||||
|
+ "<foreach collection='names' item='name' open='(' separator=',' close=')'>#{name}</foreach>"
|
||||||
|
+ "</script>")
|
||||||
|
List<String> selectExistingTableNames(@Param("names") Collection<String> names);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.gxwebsoft.gxmu.module.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.gxwebsoft.gxmu.module.entity.ModuleRegistry;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块注册表服务:模块元数据的唯一读取入口。
|
||||||
|
*
|
||||||
|
* <p>调用方不要再维护模块名常量或标题映射,一律从这里取;
|
||||||
|
* 注册表里查不到的历史 module 由调用方回退 module 原串,不抛错。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-10
|
||||||
|
*/
|
||||||
|
public interface ModuleRegistryService extends IService<ModuleRegistry> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用模块清单,按 sort_number 升序。
|
||||||
|
*/
|
||||||
|
List<ModuleRegistry> listEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全部未删除模块(含停用),按 sort_number 升序。
|
||||||
|
*/
|
||||||
|
List<ModuleRegistry> listAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 module 查注册行,未注册返回 null。
|
||||||
|
*/
|
||||||
|
ModuleRegistry getByModule(String module);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块标题;未注册或标题为空时回退 module 原串(module 为 null 时返回 null)。
|
||||||
|
*/
|
||||||
|
String titleOf(String module);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全部模块的 module -> title 映射(含停用),供列表页一次性填充标题。
|
||||||
|
*/
|
||||||
|
Map<String, String> titleMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册表里存在的全部 module(含停用)。
|
||||||
|
*/
|
||||||
|
Set<String> allModules();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* module 是否在注册表里存在(含停用)。
|
||||||
|
*
|
||||||
|
* <p>口径:停用只影响入口与列表,不影响存量记录的撤回/作废能力,
|
||||||
|
* 所以停用模块也必须返回 true。
|
||||||
|
*/
|
||||||
|
boolean containsModule(String module);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.gxwebsoft.gxmu.module.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.gxwebsoft.gxmu.module.entity.ModuleRegistry;
|
||||||
|
import com.gxwebsoft.gxmu.module.mapper.ModuleRegistryMapper;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块注册表 Service 实现
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-10
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ModuleRegistryServiceImpl extends ServiceImpl<ModuleRegistryMapper, ModuleRegistry>
|
||||||
|
implements ModuleRegistryService {
|
||||||
|
|
||||||
|
private static final int ENABLED = 1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ModuleRegistry> listEnabled() {
|
||||||
|
return listAll().stream()
|
||||||
|
.filter(item -> item.getEnabled() != null && item.getEnabled() == ENABLED)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ModuleRegistry> listAll() {
|
||||||
|
// 用字符串列名而非 Lambda:注册表服务是纯读路径,且要在无 MyBatis 上下文的单测里可用
|
||||||
|
List<ModuleRegistry> list = baseMapper.selectList(new QueryWrapper<ModuleRegistry>()
|
||||||
|
.orderByAsc("sort_number")
|
||||||
|
.orderByAsc("id"));
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return list.stream().sorted(sortOrder()).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModuleRegistry getByModule(String module) {
|
||||||
|
if (module == null || module.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return baseMapper.selectOne(new QueryWrapper<ModuleRegistry>()
|
||||||
|
.eq("module", module)
|
||||||
|
.last("limit 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String titleOf(String module) {
|
||||||
|
if (module == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ModuleRegistry item = getByModule(module);
|
||||||
|
if (item == null || item.getTitle() == null || item.getTitle().isEmpty()) {
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
return item.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> titleMap() {
|
||||||
|
Map<String, String> titleMap = new LinkedHashMap<>();
|
||||||
|
for (ModuleRegistry item : listAll()) {
|
||||||
|
if (item.getModule() == null || item.getTitle() == null || item.getTitle().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
titleMap.putIfAbsent(item.getModule(), item.getTitle());
|
||||||
|
}
|
||||||
|
return titleMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> allModules() {
|
||||||
|
Set<String> modules = new LinkedHashSet<>();
|
||||||
|
for (ModuleRegistry item : listAll()) {
|
||||||
|
if (item.getModule() != null) {
|
||||||
|
modules.add(item.getModule());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsModule(String module) {
|
||||||
|
return module != null && !module.isEmpty() && allModules().contains(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Comparator<ModuleRegistry> sortOrder() {
|
||||||
|
return Comparator.comparingInt((ModuleRegistry item) ->
|
||||||
|
item.getSortNumber() == null ? Integer.MAX_VALUE : item.getSortNumber())
|
||||||
|
.thenComparingInt(item -> item.getId() == null ? Integer.MAX_VALUE : item.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ 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;
|
||||||
import com.gxwebsoft.gxmu.model.DeclareCheckResult;
|
import com.gxwebsoft.gxmu.model.DeclareCheckResult;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||||
import com.gxwebsoft.gxmu.service.DeclareGuardService;
|
import com.gxwebsoft.gxmu.service.DeclareGuardService;
|
||||||
import com.gxwebsoft.gxmu.service.DeclareService;
|
import com.gxwebsoft.gxmu.service.DeclareService;
|
||||||
import com.gxwebsoft.gxmu.service.ReviewListService;
|
import com.gxwebsoft.gxmu.service.ReviewListService;
|
||||||
@@ -17,7 +18,6 @@ import java.time.LocalDate;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -40,12 +40,6 @@ public class DeclareGuardServiceImpl implements DeclareGuardService {
|
|||||||
public static final int STATUS_REJECTED = 3;
|
public static final int STATUS_REJECTED = 3;
|
||||||
public static final int STATUS_VOID = 4;
|
public static final int STATUS_VOID = 4;
|
||||||
|
|
||||||
/** 走申报项目判重的 9 个模块,模块名同时是表名 */
|
|
||||||
private static final Set<String> MODULES = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(
|
|
||||||
"gxmu_sjqn", "gxmu_sjtbzbsj", "gxmu_wshqtw", "gxmu_wshqtzb",
|
|
||||||
"gxmu_yxgqtdgb", "gxmu_yxgqty", "gxmu_tzbcy_form", "gxmu_qmgc_form",
|
|
||||||
"gxmu_wxxzx_project")));
|
|
||||||
|
|
||||||
private static final String TZBCY_MODULE = "gxmu_tzbcy_form";
|
private static final String TZBCY_MODULE = "gxmu_tzbcy_form";
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
@@ -57,10 +51,18 @@ public class DeclareGuardServiceImpl implements DeclareGuardService {
|
|||||||
private DeclareService declareService;
|
private DeclareService declareService;
|
||||||
@Resource
|
@Resource
|
||||||
private ReviewListService reviewListService;
|
private ReviewListService reviewListService;
|
||||||
|
@Resource
|
||||||
|
private ModuleRegistryService moduleRegistryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册表驱动的模块集合:注册表里存在的 module 一律支持(含 enabled=0)。
|
||||||
|
*
|
||||||
|
* <p>口径:停用只影响入口与列表,不影响存量记录的撤回/作废能力;
|
||||||
|
* 若只认启用模块,停用模块的历史数据会失去撤回、作废与状态同步。
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(String module) {
|
public boolean supports(String module) {
|
||||||
return module != null && MODULES.contains(module);
|
return module != null && moduleRegistryService.containsModule(module);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import com.gxwebsoft.common.system.entity.Organization;
|
|||||||
import com.gxwebsoft.common.system.service.OrganizationService;
|
import com.gxwebsoft.common.system.service.OrganizationService;
|
||||||
import com.gxwebsoft.common.core.web.PageParam;
|
import com.gxwebsoft.common.core.web.PageParam;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -31,27 +30,13 @@ import java.util.stream.Collectors;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ReviewFlowConfigServiceImpl extends ServiceImpl<ReviewFlowConfigMapper, ReviewFlowConfig> implements ReviewFlowConfigService {
|
public class ReviewFlowConfigServiceImpl extends ServiceImpl<ReviewFlowConfigMapper, ReviewFlowConfig> implements ReviewFlowConfigService {
|
||||||
private static final Map<String, String> MODULE_NAME_MAP;
|
|
||||||
|
|
||||||
static {
|
|
||||||
Map<String, String> moduleNameMap = new LinkedHashMap<>();
|
|
||||||
moduleNameMap.put("cms_manuscript", "稿件");
|
|
||||||
moduleNameMap.put("gxmu_sjqn", "十佳青年岗位能手");
|
|
||||||
moduleNameMap.put("gxmu_sjtbzbsj", "十佳团支部书记");
|
|
||||||
moduleNameMap.put("gxmu_wshqtw", "五四红旗团委");
|
|
||||||
moduleNameMap.put("gxmu_wshqtzb", "五四红旗团支部");
|
|
||||||
moduleNameMap.put("gxmu_yxgqtdgb", "优秀共青团干部");
|
|
||||||
moduleNameMap.put("gxmu_yxgqty", "优秀共青团员");
|
|
||||||
moduleNameMap.put("gxmu_tzbcy_form", "挑战杯");
|
|
||||||
moduleNameMap.put("gxmu_qmgc_form", "青马工程");
|
|
||||||
moduleNameMap.put("gxmu_wxxzx_project", "未来学术之星");
|
|
||||||
MODULE_NAME_MAP = Collections.unmodifiableMap(moduleNameMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ReviewFlowService reviewFlowService;
|
private ReviewFlowService reviewFlowService;
|
||||||
@Resource
|
@Resource
|
||||||
private OrganizationService organizationService;
|
private OrganizationService organizationService;
|
||||||
|
@Resource
|
||||||
|
private ModuleRegistryService moduleRegistryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageResult<ReviewFlowConfig> pageRel(ReviewFlowConfigParam param) {
|
public PageResult<ReviewFlowConfig> pageRel(ReviewFlowConfigParam param) {
|
||||||
@@ -110,11 +95,21 @@ public class ReviewFlowConfigServiceImpl extends ServiceImpl<ReviewFlowConfigMap
|
|||||||
flowNameMap = reviewFlowService.listByIds(flowIds).stream()
|
flowNameMap = reviewFlowService.listByIds(flowIds).stream()
|
||||||
.collect(Collectors.toMap(ReviewFlow::getId, ReviewFlow::getTitle, (left, right) -> left));
|
.collect(Collectors.toMap(ReviewFlow::getId, ReviewFlow::getTitle, (left, right) -> left));
|
||||||
}
|
}
|
||||||
|
// 模块标题以注册表为唯一来源;注册表里查不到的历史 module 回退原串,不让列表报错
|
||||||
|
Map<String, String> moduleTitleMap = moduleRegistryService.titleMap();
|
||||||
for (ReviewFlowConfig item : list) {
|
for (ReviewFlowConfig item : list) {
|
||||||
item.setFlowName(flowNameMap.get(item.getFlowId()));
|
item.setFlowName(flowNameMap.get(item.getFlowId()));
|
||||||
item.setModuleName(MODULE_NAME_MAP.get(item.getModule()));
|
item.setModuleName(moduleTitleOf(moduleTitleMap, item.getModule()));
|
||||||
item.setOrganizationName(organizationNameMap.get(item.getOrganizationId()));
|
item.setOrganizationName(organizationNameMap.get(item.getOrganizationId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String moduleTitleOf(Map<String, String> moduleTitleMap, String module) {
|
||||||
|
if (module == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String title = moduleTitleMap.get(module);
|
||||||
|
return title == null || title.isEmpty() ? module : title;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import com.gxwebsoft.gxmu.mapper.ReviewListMapper;
|
|||||||
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.gxmu.module.service.ModuleRegistryService;
|
||||||
import com.gxwebsoft.common.core.web.PageParam;
|
import com.gxwebsoft.common.core.web.PageParam;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -25,25 +24,11 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ReviewListServiceImpl extends ServiceImpl<ReviewListMapper, ReviewList> implements ReviewListService {
|
public class ReviewListServiceImpl extends ServiceImpl<ReviewListMapper, ReviewList> implements ReviewListService {
|
||||||
private static final Map<String, String> MODULE_NAME_MAP;
|
|
||||||
|
|
||||||
static {
|
|
||||||
Map<String, String> moduleNameMap = new LinkedHashMap<>();
|
|
||||||
moduleNameMap.put("cms_manuscript", "稿件");
|
|
||||||
moduleNameMap.put("gxmu_sjqn", "十佳青年岗位能手");
|
|
||||||
moduleNameMap.put("gxmu_sjtbzbsj", "十佳团支部书记");
|
|
||||||
moduleNameMap.put("gxmu_wshqtw", "五四红旗团委");
|
|
||||||
moduleNameMap.put("gxmu_wshqtzb", "五四红旗团支部");
|
|
||||||
moduleNameMap.put("gxmu_yxgqtdgb", "优秀共青团干部");
|
|
||||||
moduleNameMap.put("gxmu_yxgqty", "优秀共青团员");
|
|
||||||
moduleNameMap.put("gxmu_tzbcy_form", "挑战杯");
|
|
||||||
moduleNameMap.put("gxmu_qmgc_form", "青马工程");
|
|
||||||
moduleNameMap.put("gxmu_wxxzx_project", "未来学术之星");
|
|
||||||
MODULE_NAME_MAP = Collections.unmodifiableMap(moduleNameMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
@Resource
|
||||||
|
private ModuleRegistryService moduleRegistryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageResult<ReviewList> pageRel(ReviewListParam param) {
|
public PageResult<ReviewList> pageRel(ReviewListParam param) {
|
||||||
@@ -98,11 +83,21 @@ public class ReviewListServiceImpl extends ServiceImpl<ReviewListMapper, ReviewL
|
|||||||
if (list == null || list.isEmpty()) {
|
if (list == null || list.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 模块标题以注册表为唯一来源;注册表里查不到的历史 module 回退原串,不让列表报错
|
||||||
|
Map<String, String> moduleTitleMap = moduleRegistryService.titleMap();
|
||||||
for (ReviewList item : list) {
|
for (ReviewList item : list) {
|
||||||
item.setModuleName(MODULE_NAME_MAP.get(item.getModule()));
|
item.setModuleName(moduleTitleOf(moduleTitleMap, item.getModule()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String moduleTitleOf(Map<String, String> moduleTitleMap, String module) {
|
||||||
|
if (module == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String title = moduleTitleMap.get(module);
|
||||||
|
return title == null || title.isEmpty() ? module : title;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ReviewList> listByModuleNPK(String module, Integer pk){
|
public List<ReviewList> listByModuleNPK(String module, Integer pk){
|
||||||
return list(
|
return list(
|
||||||
|
|||||||
@@ -204,6 +204,11 @@ gxmu:
|
|||||||
timeout-ms: 10000
|
timeout-ms: 10000
|
||||||
# 名册外的账号默认拒绝登录,见 ADR-0001:学生须先同步进名册
|
# 名册外的账号默认拒绝登录,见 ADR-0001:学生须先同步进名册
|
||||||
allow-unknown-user: false
|
allow-unknown-user: false
|
||||||
|
# 模块注册表(spec #58 / ADR 0006):启动期校验注册表 table_name 与 module 一致、
|
||||||
|
# 且注册表引用的业务表在库里存在,不一致直接启动失败(用户故事 21)。
|
||||||
|
# strict 默认 true;本地无库启动可临时设为 false 跳过校验。
|
||||||
|
module-registry:
|
||||||
|
strict: true
|
||||||
openplat-sync:
|
openplat-sync:
|
||||||
enabled: true
|
enabled: true
|
||||||
# 每天 03:30
|
# 每天 03:30
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package com.gxwebsoft.gxmu.module;
|
||||||
|
|
||||||
|
import com.gxwebsoft.gxmu.module.entity.ModuleRegistry;
|
||||||
|
import com.gxwebsoft.gxmu.module.mapper.ModuleRegistryMapper;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
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.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块注册表启动期一致性校验的测试(不启 Spring、不连库)。
|
||||||
|
*
|
||||||
|
* <p>对应用户故事 21:接入新模块时「表名与 module 名不一致」要启动期直接失败,
|
||||||
|
* 而不是运行到详情页才发现空白;同时保留 strict=false 供无库本地启动。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-10
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
|
class ModuleRegistryIntegrityCheckerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ModuleRegistryService moduleRegistryService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ModuleRegistryMapper moduleRegistryMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ModuleRegistryIntegrityChecker checker;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void strictOn() {
|
||||||
|
ReflectionTestUtils.setField(checker, "strict", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void failsWhenTableNameDiffersFromModule() {
|
||||||
|
when(moduleRegistryService.listAll()).thenReturn(Collections.singletonList(
|
||||||
|
module("gxmu_new_form", "gxmu_new_form_table", 1)));
|
||||||
|
|
||||||
|
IllegalStateException error = assertThrows(IllegalStateException.class, () -> checker.check());
|
||||||
|
|
||||||
|
assertTrue(error.getMessage().contains("gxmu_new_form"), error.getMessage());
|
||||||
|
assertTrue(error.getMessage().contains("gxmu_new_form_table"), error.getMessage());
|
||||||
|
verify(moduleRegistryMapper, never()).selectExistingTableNames(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void failsWhenRegisteredTableDoesNotExist() {
|
||||||
|
when(moduleRegistryService.listAll()).thenReturn(Arrays.asList(
|
||||||
|
module("gxmu_sjqn", "gxmu_sjqn", 1),
|
||||||
|
module("gxmu_ghost_form", "gxmu_ghost_form", 0)));
|
||||||
|
when(moduleRegistryMapper.selectExistingTableNames(any()))
|
||||||
|
.thenReturn(Collections.singletonList("gxmu_sjqn"));
|
||||||
|
|
||||||
|
IllegalStateException error = assertThrows(IllegalStateException.class, () -> checker.check());
|
||||||
|
|
||||||
|
assertTrue(error.getMessage().contains("gxmu_ghost_form"), error.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void passesWhenRegistryMatchesSchema() {
|
||||||
|
when(moduleRegistryService.listAll()).thenReturn(Collections.singletonList(
|
||||||
|
module("gxmu_sjqn", "gxmu_sjqn", 1)));
|
||||||
|
when(moduleRegistryMapper.selectExistingTableNames(any()))
|
||||||
|
.thenReturn(Collections.singletonList("gxmu_sjqn"));
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> checker.check());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void strictFalseSkipsValidationForLocalStartupWithoutDatabase() {
|
||||||
|
ReflectionTestUtils.setField(checker, "strict", false);
|
||||||
|
when(moduleRegistryService.listAll()).thenReturn(Collections.singletonList(
|
||||||
|
module("gxmu_new_form", "gxmu_new_form_table", 1)));
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> checker.check());
|
||||||
|
verify(moduleRegistryService, never()).listAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleRegistry module(String name, String tableName, int enabled) {
|
||||||
|
ModuleRegistry item = new ModuleRegistry();
|
||||||
|
item.setModule(name);
|
||||||
|
item.setTableName(tableName);
|
||||||
|
item.setTitle(name);
|
||||||
|
item.setDetailKey(name);
|
||||||
|
item.setEnabled(enabled);
|
||||||
|
item.setSortNumber(1);
|
||||||
|
item.setDeleted(0);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package com.gxwebsoft.gxmu.module;
|
||||||
|
|
||||||
|
import com.gxwebsoft.gxmu.module.entity.ModuleRegistry;
|
||||||
|
import com.gxwebsoft.gxmu.module.mapper.ModuleRegistryMapper;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.impl.ModuleRegistryServiceImpl;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块注册表服务的单元测试(mock 掉 Mapper,不启 Spring、不连库)。
|
||||||
|
*
|
||||||
|
* <p>注册表是模块元数据的唯一来源,这里锁住三件对外可观察的事:
|
||||||
|
* 启用清单按 sort_number 升序、标题查询在未注册时回退 module 原串、
|
||||||
|
* 模块集合包含已停用模块(停用模块的历史数据仍要能撤回/作废)。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-10
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
|
class ModuleRegistryServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ModuleRegistryMapper moduleRegistryMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ModuleRegistryServiceImpl moduleRegistryService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listEnabledReturnsOnlyEnabledModulesOrderedBySortNumber() {
|
||||||
|
when(moduleRegistryMapper.selectList(any())).thenReturn(Arrays.asList(
|
||||||
|
module("gxmu_qmgc_form", "青马工程", 1, 8),
|
||||||
|
module("gxmu_sjtbzbsj", "十佳团支部书记", 0, 2),
|
||||||
|
module("gxmu_sjqn", "十佳青年岗位能手", 1, 1)));
|
||||||
|
|
||||||
|
List<ModuleRegistry> enabled = moduleRegistryService.listEnabled();
|
||||||
|
|
||||||
|
assertEquals(Arrays.asList("gxmu_sjqn", "gxmu_qmgc_form"),
|
||||||
|
enabled.stream().map(ModuleRegistry::getModule).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void titleOfReturnsRegisteredTitle() {
|
||||||
|
when(moduleRegistryMapper.selectOne(any())).thenReturn(module("gxmu_tzbcy_form", "挑战杯", 1, 7));
|
||||||
|
|
||||||
|
assertEquals("挑战杯", moduleRegistryService.titleOf("gxmu_tzbcy_form"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void titleOfFallsBackToModuleStringWhenNotRegistered() {
|
||||||
|
when(moduleRegistryMapper.selectOne(any())).thenReturn(null);
|
||||||
|
|
||||||
|
assertEquals("cms_legacy_module", moduleRegistryService.titleOf("cms_legacy_module"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void titleOfKeepsNullModuleNull() {
|
||||||
|
assertNull(moduleRegistryService.titleOf(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void titleMapCoversDisabledModulesSoHistoricalRowsKeepTitles() {
|
||||||
|
when(moduleRegistryMapper.selectList(any())).thenReturn(Arrays.asList(
|
||||||
|
module("gxmu_sjqn", "十佳青年岗位能手", 1, 1),
|
||||||
|
module("gxmu_yxgqty", "优秀共青团员", 0, 6)));
|
||||||
|
|
||||||
|
Map<String, String> titles = moduleRegistryService.titleMap();
|
||||||
|
|
||||||
|
assertEquals("十佳青年岗位能手", titles.get("gxmu_sjqn"));
|
||||||
|
assertEquals("优秀共青团员", titles.get("gxmu_yxgqty"));
|
||||||
|
assertEquals(2, titles.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void moduleSetContainsDisabledModules() {
|
||||||
|
when(moduleRegistryMapper.selectList(any())).thenReturn(Arrays.asList(
|
||||||
|
module("gxmu_sjqn", "十佳青年岗位能手", 1, 1),
|
||||||
|
module("gxmu_yxgqty", "优秀共青团员", 0, 6)));
|
||||||
|
|
||||||
|
assertTrue(moduleRegistryService.containsModule("gxmu_yxgqty"));
|
||||||
|
assertFalse(moduleRegistryService.containsModule("gxmu_unknown"));
|
||||||
|
assertFalse(moduleRegistryService.containsModule(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleRegistry module(String name, String title, int enabled, int sortNumber) {
|
||||||
|
ModuleRegistry item = new ModuleRegistry();
|
||||||
|
item.setModule(name);
|
||||||
|
item.setTableName(name);
|
||||||
|
item.setTitle(title);
|
||||||
|
item.setDetailKey(name);
|
||||||
|
item.setEnabled(enabled);
|
||||||
|
item.setSortNumber(sortNumber);
|
||||||
|
item.setDeleted(0);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.gxwebsoft.gxmu.service.impl;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.system.service.OrganizationService;
|
||||||
|
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
|
||||||
|
import com.gxwebsoft.gxmu.mapper.ReviewFlowConfigMapper;
|
||||||
|
import com.gxwebsoft.gxmu.module.service.ModuleRegistryService;
|
||||||
|
import com.gxwebsoft.gxmu.param.ReviewFlowConfigParam;
|
||||||
|
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批链配置列表的模块名填充测试(mock 掉 Mapper 与注册表,不启 Spring、不连库)。
|
||||||
|
*
|
||||||
|
* <p>模块名改读注册表后仍要保住一条底线:注册表里查不到的老 module
|
||||||
|
* 不能让列表报错或空白,必须回退 module 原串。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-11-10
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
|
class ReviewFlowConfigServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ReviewFlowConfigMapper reviewFlowConfigMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ReviewFlowService reviewFlowService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private OrganizationService organizationService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ModuleRegistryService moduleRegistryService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ReviewFlowConfigServiceImpl reviewFlowConfigService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fillsModuleNameFromRegistry() {
|
||||||
|
ReviewFlowConfig config = config("gxmu_sjqn");
|
||||||
|
when(reviewFlowConfigMapper.selectListRel(any())).thenReturn(Collections.singletonList(config));
|
||||||
|
when(moduleRegistryService.titleMap())
|
||||||
|
.thenReturn(Collections.singletonMap("gxmu_sjqn", "十佳青年岗位能手"));
|
||||||
|
|
||||||
|
List<ReviewFlowConfig> list = reviewFlowConfigService.listRel(new ReviewFlowConfigParam());
|
||||||
|
|
||||||
|
assertEquals("十佳青年岗位能手", list.get(0).getModuleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fallsBackToModuleStringWhenRegistryHasNoSuchModule() {
|
||||||
|
ReviewFlowConfig config = config("cms_legacy_module");
|
||||||
|
when(reviewFlowConfigMapper.selectListRel(any())).thenReturn(Collections.singletonList(config));
|
||||||
|
when(moduleRegistryService.titleMap()).thenReturn(Collections.emptyMap());
|
||||||
|
|
||||||
|
List<ReviewFlowConfig> list = reviewFlowConfigService.listRel(new ReviewFlowConfigParam());
|
||||||
|
|
||||||
|
assertEquals("cms_legacy_module", list.get(0).getModuleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsNullModuleNull() {
|
||||||
|
ReviewFlowConfig config = config(null);
|
||||||
|
when(reviewFlowConfigMapper.selectListRel(any())).thenReturn(Collections.singletonList(config));
|
||||||
|
when(moduleRegistryService.titleMap()).thenReturn(Collections.emptyMap());
|
||||||
|
|
||||||
|
List<ReviewFlowConfig> list = reviewFlowConfigService.listRel(new ReviewFlowConfigParam());
|
||||||
|
|
||||||
|
assertNull(list.get(0).getModuleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReviewFlowConfig config(String module) {
|
||||||
|
ReviewFlowConfig config = new ReviewFlowConfig();
|
||||||
|
config.setId(1);
|
||||||
|
config.setModule(module);
|
||||||
|
config.setTitle("配置");
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user