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)。 * *

两件事不满足就在启动期直接失败,而不是等运行到详情页才发现空白: *

    *
  1. 注册表 table_name 与 module 一致(module 同时是业务表名);
  2. *
  3. 注册表引用的业务表在库里真实存在。
  4. *
* *

反方向只告警不阻断:{@code gxmu_review_flow_config} 里有、注册表里没有的 module * 打 warn(这种模块的标题会回退成裸 module 串、也不会出现在模块下拉里,但存量数据仍要能查)。 * 不阻断启动是刻意的——存量脏数据不该挡住上线,但必须留下可检索的线索。 * *

允许 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(); } /** * 反向校验:审批链配置里有、注册表里没有的 module 打 warn,不阻断启动。 * *

这类模块的标题会回退成裸 module 串、也不会出现在模块下拉里, * 但存量申报记录仍要能查——所以只提示补注册表行,不抛异常。 * * @param modules 注册表里的全部模块 */ private void warnModulesMissingFromRegistry(List modules) { Set registered = new HashSet<>(); for (ModuleRegistry item : modules) { if (item.getModule() != null) { registered.add(item.getModule()); } } List configured = moduleRegistryMapper.selectConfiguredModules(); if (configured == null || configured.isEmpty()) { return; } for (String module : configured) { if (module != null && !module.isEmpty() && !registered.contains(module)) { log.warn("gxmu_review_flow_config 里的模块 {} 未在 gxmu_module_registry 登记:" + "该模块的标题会回退成 module 原串、也不会出现在模块下拉里;" + "请补一行注册表数据(enabled 按是否新开申报入口决定)", module); } } } /** * 执行校验;不一致时抛 {@link IllegalStateException} 让启动失败。 */ public void check() { if (!strict) { log.warn("gxmu.module-registry.strict=false,跳过模块注册表一致性校验(仅限本地无库启动)"); return; } List modules = moduleRegistryService.listAll(); if (modules == null || modules.isEmpty()) { log.warn("模块注册表为空,跳过一致性校验;请确认 gxmu_module_registry 已初始化"); return; } List mismatches = new ArrayList<>(); Set 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 existing = new HashSet<>(moduleRegistryMapper.selectExistingTableNames(tableNames)); List 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"); } warnModulesMissingFromRegistry(modules); log.info("模块注册表一致性校验通过:{} 个模块,{} 张业务表", modules.size(), tableNames.size()); } }