当前位置: 首页 > article >正文

从配置到运行时:Forge Admin 的动态 API 配置管理是怎么做的

问题同一个接口今天要加认证、明天要加加密、后天要限流这些行为散落在拦截器、过滤器、注解里改一次牵一发动全身怎么集中管理和动态刷新1. 这个问题在企业后台里为什么常见在企业后台开发中API 行为控制的需求随着业务发展不断变化。一个看似简单的接口可能涉及多种横切关注点认证鉴权哪些接口需要登录哪些接口可以匿名访问报文加解密敏感数据传输是否需要加密租户隔离是否自动追加tenant_id条件限流保护是否开启接口限流脱敏处理哪些字段需要脱敏后返回传统做法有三种做法代码形式问题硬编码拦截器里写if (path.contains(/public)) return true条件散落难以维护注解标注每个 Controller 方法加ApiEncrypt、SaCheckPermission接口多了注解满天飞修改要改源码配置文件YAML 里写auth.excludePaths: /login,/register无法运行时动态调整重启才能生效真实场景更复杂某接口上线时允许匿名访问运营后发现数据泄露风险紧急要求加认证某接口原本不限流用户量暴涨后触发雪崩需要立即开启限流新增租户后部分接口需要关闭租户隔离如公共查询接口不同模块的接口可能有不同的默认策略核心痛点API 行为配置散落在代码各处无法集中管理、无法运行时动态刷新、无法细粒度控制。2. Forge Admin 是怎么解决的Forge Admin 提供了forge-starter-api-config模块实现配置驱动 两级缓存 事件刷新的动态 API 配置管理。整体架构┌─────────────────────────────────────────────────────────────────┐ │ API 配置管理架构 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────┐ ┌───────────────┐ │ │ │ sys_api_config│ │ 拦截器链 │ │ │ │ (数据库表) │───────│ AuthInterceptor│ │ │ └───────────────┘ │ CryptoInterceptor│ │ │ │ │ TenantInterceptor│ │ │ ▼ │ LimitInterceptor │ │ │ ┌───────────────┐ └───────────────┘ │ │ │ ApiConfigManager│ │ │ │ │ (核心决策引擎) │ ▼ │ │ │ - Ant路径匹配 │ ┌───────────────┐ │ │ │ - 两级缓存 │ │ ThreadLocal │ │ │ │ - 事件刷新 │ │ (请求上下文) │ │ │ └───────────────┘ └───────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌───────────────┐ ┌───────────────┐ │ │ │ Caffeine (L1) │ │ 业务Service │ │ │ │ Redis (L2) │ │ (调用决策API) │ │ │ └───────────────┘ └───────────────┘ │ │ │ │ 配置变更 ───── ApiConfigRefreshEvent ───── 异步刷新缓存 │ │ │ └─────────────────────────────────────────────────────────────────┘核心能力表能力描述优势集中配置所有接口行为存储在sys_api_config表可通过后台页面管理无需改代码Ant路径匹配支持/api/user/**、/api/{id}等模式精确匹配和模糊匹配结合两级缓存Caffeine 本地缓存 Redis 分布式缓存高性能 集群一致性事件刷新配置变更发布事件异步刷新缓存运行时生效无需重启ThreadLocal 上下文请求级配置缓存避免重复查询拦截器和业务层共享配置模块结构forge-starter-api-config/ ├── config/ │ ├── ApiConfigAutoConfiguration.java # 自动配置引入即生效 │ └── ApiConfigProperties.java # 属性配置缓存大小、过期时间 ├── domain/ │ ├── entity/SysApiConfig.java # 数据库实体 │ ├── dto/ApiConfigInfo.java # 缓存和传输 DTO │ ├── event/ApiConfigRefreshEvent.java # 配置刷新事件 │ └── dto/ApiConfigQuery.java # 查询参数 ├── service/ │ ├── IApiConfigManager.java # 核心决策引擎接口 │ ├── impl/ApiConfigManagerImpl.java # 实现类缓存 匹配 刷新 │ ├── ISysApiConfigService.java # CRUD 服务接口 │ └── impl/SysApiConfigServiceImpl.java # CRUD 实现 ├── context/ │ └── ApiConfigContextHolder.java # ThreadLocal 上下文持有者 ├── listener/ │ └── ApiConfigRefreshListener.java # 事件监听器 ├── controller/ │ ├── SysApiConfigController.java # CRUD 接口 │ └── ApiConfigManageController.java # 管理接口刷新缓存 ├── mapper/ │ └── SysApiConfigMapper.java # MyBatis Mapper └── registry/ ├── ApiConfigScanner.java # 接口扫描器 └── ApiConfigAutoRegistrar.java # 自动注册器3. 核心数据结构 / 配置协议3.1 数据库表sys_api_configCREATE TABLE sys_api_config ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键ID, api_name VARCHAR(100) COMMENT 接口名称, api_code VARCHAR(100) COMMENT 接口编码程序引用, req_method VARCHAR(10) COMMENT 请求方式GET/POST/PUT/DELETE/ALL, url_path VARCHAR(500) COMMENT 接口路径支持Ant风格, api_version VARCHAR(20) COMMENT 接口版本号, module_code VARCHAR(50) COMMENT 所属模块, service_id VARCHAR(50) COMMENT 微服务ID, auth_flag TINYINT DEFAULT 1 COMMENT 是否需要认证1-需要, 0-不需要, encrypt_flag TINYINT DEFAULT 0 COMMENT 是否需要加解密1-需要, 0-不需要, tenant_flag TINYINT DEFAULT 1 COMMENT 是否启用租户隔离1-启用, 0-不启用, limit_flag TINYINT DEFAULT 0 COMMENT 是否开启限流1-开启, 0-关闭, sensitive_fields VARCHAR(500) COMMENT 需脱敏字段JSON数组, status TINYINT DEFAULT 1 COMMENT 状态1-正常, 0-停用, remark VARCHAR(500) COMMENT 备注说明, create_by BIGINT COMMENT 创建人, create_time DATETIME COMMENT 创建时间, update_by BIGINT COMMENT 更新人, update_time DATETIME COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTAPI配置管理表;3.2 核心字段说明字段类型示例用途api_nameString查询用户信息人工可读的接口名称便于管理api_codeStringuser_query程序引用编码可用于业务逻辑判断req_methodStringGET / ALL请求方式ALL表示所有方法url_pathString/api/user/**Ant 风格路径支持*、**、{变量}auth_flagInteger10不需要认证1需要认证encrypt_flagInteger00不需要加解密1需要tenant_flagInteger10不启用租户隔离1启用limit_flagInteger00不限流1开启限流sensitive_fieldsString[phone,id_card]JSON 数组指定脱敏字段3.3 配置示例精确匹配配置{ apiName: 登录接口, apiCode: auth_login, reqMethod: POST, urlPath: /auth/login, authFlag: 0, encryptFlag: 1, tenantFlag: 0, limitFlag: 1, moduleCode: auth }模糊匹配配置{ apiName: 公共查询接口, apiCode: public_query, reqMethod: GET, urlPath: /api/public/**, authFlag: 0, encryptFlag: 0, tenantFlag: 0, moduleCode: common }3.4 DTOApiConfigInfoData public class ApiConfigInfo implements Serializable { private Long id; private String apiName; private String apiCode; private String reqMethod; private String urlPath; private String apiVersion; private String moduleCode; private String serviceId; // 行为标志位Boolean 类型便于判断 private Boolean needAuth; private Boolean needEncrypt; private Boolean needTenant; private Boolean needLimit; private ListString sensitiveFields; private Boolean enabled; private String remark; private Long cacheTime; // 缓存时间戳 // 构建缓存 Key public String buildCacheKey() { return urlPath : reqMethod; } // 从实体转换 public static ApiConfigInfo fromEntity(SysApiConfig entity) { ApiConfigInfo info new ApiConfigInfo(); info.setNeedAuth(entity.getAuthFlag() 1); info.setNeedEncrypt(entity.getEncryptFlag() 1); // ... 其他字段转换 return info; } }3.5 配置属性ApiConfigPropertiesforge: api-config: enabled: true # 是否启用 API 配置管理 auto-register: true # 是否自动注册接口 cache-warm-up: true # 是否预热缓存 scan-packages: # 扫描的包路径 - com.mdframe.forge cache: local: max-size: 1000 # 本地缓存最大容量 expire-minutes: 10 # 本地缓存过期时间分钟 redis: enabled: true # 是否启用 Redis 缓存 expire-seconds: 1800 # Redis 缓存过期时间秒 key-prefix: api:config: # Redis Key 前缀4. 核心实现链路4.1 启动阶段自动配置和缓存预热入口ApiConfigAutoConfiguration.javaConfiguration ConditionalOnWebApplication ConditionalOnProperty(prefix forge.api-config, name enabled, havingValue true) EnableAsync public class ApiConfigAutoConfiguration { Bean public ApiConfigScanner apiConfigScanner(RequestMappingHandlerMapping mapping) { return new ApiConfigScanner(mapping); // 扫描已注册的接口 } Bean ConditionalOnProperty(prefix forge.api-config, name auto-register) public ApiConfigAutoRegistrar apiConfigAutoRegistrar( ApiConfigScanner scanner, SysApiConfigMapper mapper, ApiConfigProperties props, IApiConfigManager manager) { return new ApiConfigAutoRegistrar(scanner, mapper, props, manager); } Bean public ApiConfigRefreshListener apiConfigRefreshListener(IApiConfigManager manager) { return new ApiConfigRefreshListener(manager); // 配置刷新监听器 } }缓存预热ApiConfigManagerImpl.warmUpCache()Override public void warmUpCache() { log.info(开始预热API配置缓存...); long startTime System.currentTimeMillis(); ListSysApiConfig configs apiConfigMapper.selectAllEnabled(); for (SysApiConfig entity : configs) { ApiConfigInfo config ApiConfigInfo.fromEntity(entity); String cacheKey buildCacheKey(entity.getUrlPath(), entity.getReqMethod()); // 写入 L2 缓存Redis putToRedis(cacheKey, config); // 写入 L1 缓存Caffeine localCache.put(cacheKey, config); } // 初始化全量配置列表缓存用于 Ant 匹配 allEnabledConfigsCache configs.stream() .map(ApiConfigInfo::fromEntity) .collect(Collectors.toList()); log.info(API配置缓存预热完成共{}条配置耗时{}ms, configs.size(), elapsed); }4.2 请求阶段配置获取和上下文设置拦截器链调用假设有ApiConfigInterceptorpublic class ApiConfigInterceptor implements HandlerInterceptor { Autowired private IApiConfigManager apiConfigManager; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String urlPath request.getRequestURI(); String method request.getMethod(); // 获取 API 配置 ApiConfigInfo config apiConfigManager.getApiConfig(urlPath, method); // 设置到 ThreadLocal 上下文 ApiConfigContextHolder.setConfig(config); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 清除上下文 ApiConfigContextHolder.clear(); } }核心决策逻辑ApiConfigManagerImpl.getApiConfig()Override public ApiConfigInfo getApiConfig(String urlPath, String method) { if (!configProperties.isEnabled()) { return null; } // 使用 Ant 路径匹配查找最匹配的配置 ApiConfigInfo config findBestMatchConfig(urlPath, method); if (config ! null) { log.debug(API配置匹配成功: {} {} - {}, method, urlPath, config.getUrlPath()); } return config; } private ApiConfigInfo findBestMatchConfig(String urlPath, String method) { ApiConfigInfo bestMatch null; int bestScore -1; for (ApiConfigInfo config : allEnabledConfigsCache) { // 检查请求方法是否匹配 if (!method.equalsIgnoreCase(config.getReqMethod())) { continue; } // 使用 Ant 路径匹配 if (matcher.match(config.getUrlPath(), urlPath)) { // 计算匹配分数路径越精确分数越高 int score calculateMatchScore(urlPath, config.getUrlPath()); if (score bestScore) { bestScore score; bestMatch config; } } } return bestMatch; } private int calculateMatchScore(String requestPath, String configPath) { // 精确匹配完全相等 - 100分 if (requestPath.equals(configPath)) { return 100; } // 通配符匹配根据匹配段数计算分数 if (configPath.contains(*) || configPath.contains(**)) { int matchCount calculateMatchSegments(requestPath, configPath); return matchCount * 20; } // 前缀匹配 - 50分 if (requestPath.startsWith(configPath)) { return 50; } return 0; }上下文使用ApiConfigContextHolderpublic class ApiConfigContextHolder { private static final ThreadLocalApiConfigInfo CONTEXT_HOLDER new ThreadLocal(); public static void setConfig(ApiConfigInfo config) { CONTEXT_HOLDER.set(config); } public static ApiConfigInfo getConfig() { return CONTEXT_HOLDER.get(); } // 快捷判断方法 public static boolean needAuth() { ApiConfigInfo config CONTEXT_HOLDER.get(); return config ! null config.getNeedAuth(); } public static boolean needEncrypt() { ApiConfigInfo config CONTEXT_HOLDER.get(); return config ! null config.getNeedEncrypt(); } public static boolean needTenant() { ApiConfigInfo config CONTEXT_HOLDER.get(); return config ! null config.getNeedTenant(); } public static void clear() { CONTEXT_HOLDER.remove(); } }4.3 配置变更事件发布和缓存刷新Controller 发布事件SysApiConfigControllerPostMapping(/edit) public RespInfoVoid edit(RequestBody SysApiConfig config) { boolean result apiConfigService.updateConfig(config); if (result) { // 发布刷新事件异步处理 eventPublisher.publishEvent(new ApiConfigRefreshEvent(this, ApiConfigRefreshEvent.RefreshType.SINGLE, config.getId(), 修改API配置)); } return result ? RespInfo.success() : RespInfo.error(修改失败); }事件监听器ApiConfigRefreshListenerComponent public class ApiConfigRefreshListener { private final IApiConfigManager apiConfigManager; Async EventListener public void onApiConfigRefresh(ApiConfigRefreshEvent event) { log.info(收到API配置刷新事件: type{}, reason{}, event.getRefreshType(), event.getReason()); switch (event.getRefreshType()) { case SINGLE: // 刷新单个接口配置 if (event.getConfigId() ! null) { apiConfigManager.refreshApiConfigById(event.getConfigId()); } break; case ALL: // 刷新所有接口配置 apiConfigManager.refreshAllApiConfig(); break; case MODULE: // 刷新指定模块的配置 apiConfigManager.refreshApiConfigByModule(event.getModuleCode()); break; } } }缓存刷新实现ApiConfigManagerImplOverride public void refreshApiConfigById(Long configId) { SysApiConfig config apiConfigMapper.selectById(configId); if (config ! null) { String cacheKey buildCacheKey(config.getUrlPath(), config.getReqMethod()); // 清除 L1 缓存 localCache.invalidate(cacheKey); // 清除 L2 缓存Redis deleteFromRedis(cacheKey); // 重新加载配置到全量缓存 reloadAllEnabledConfigsCache(); log.info(刷新API配置缓存: id{}, key{}, configId, cacheKey); } } Override public void refreshAllApiConfig() { // 清除 L1 缓存 localCache.invalidateAll(); // 清除 L2 缓存Redis clearAllFromRedis(); // 重新预热缓存 warmUpCache(); log.info(刷新所有API配置缓存); }4.4 完整链路图┌─────────────────────────────────────────────────────────────────────┐ │ API 配置请求链路 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ HTTP Request │ │ (GET /api/user/123) │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ ApiConfigInterceptor│ │ │ │ - 获取 urlPath/method │ │ │ - 调用 apiConfigManager.getApiConfig() │ │ │ - 设置到 ThreadLocal │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ IApiConfigManager │ │ │ │ - 查询 L1 缓存Caffeine │ │ │ - Ant 路径匹配查找最匹配配置 │ │ │ - 返回 ApiConfigInfo │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ 后续拦截器链 │ │ │ │ - AuthInterceptor: 根据 needAuth 决定是否拦截 │ │ │ - CryptoInterceptor: 根据 needEncrypt 决定是否加解密 │ │ │ - TenantInterceptor: 根据 needTenant 决定是否追加租户条件 │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ Controller │ │ │ │ - 业务逻辑处理 │ │ │ - 可通过 ApiConfigContextHolder 获取配置 │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ afterCompletion │ │ │ │ - ApiConfigContextHolder.clear() │ │ └──────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────┐ │ 配置变更链路 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 用户修改配置后台页面 │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ SysApiConfigController.edit() │ │ │ - 更新数据库 │ │ │ - 发布 ApiConfigRefreshEvent │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ ApiConfigRefreshListener │ │ │ Async EventListener │ │ │ - 异步处理事件 │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ IApiConfigManager.refreshApiConfigById() │ │ │ - 清除 L1 缓存 │ │ │ - 清除 L2 缓存 │ │ │ - 重新加载配置 │ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ 下一次请求生效无需重启 │ │ │ └─────────────────────────────────────────────────────────────────────┘5. 关键取舍和坑5.1 为什么用两级缓存而不是只用 Redis问题如果只用 Redis每次请求都要网络调用性能损耗明显。取舍L1Caffeine本地内存缓存毫秒级响应适合高频访问的配置L2Redis分布式缓存保证集群一致性适合配置同步架构优势请求 - L1 缓存命中则返回 - L2 缓存命中则写入 L1 - 数据库坑点L1 和 L2 的过期时间要合理设置L1 过期时间 L2集群环境下配置变更必须清除所有节点的 L1 缓存通过 Redis Pub/Sub 或事件广播5.2 为什么用 Ant 路径匹配而不是精确匹配问题接口数量多时每个接口都配置一次太繁琐有些接口有路径参数如/api/user/{id}精确匹配无法覆盖。Ant 匹配规则模式匹配示例/api/user/*/api/user/123、/api/user/profile/api/user/**/api/user/123/profile、/api/user/123/roles/api/user/{id}/api/user/123Spring MVC 路径参数/api/public/**所有/api/public/下的路径匹配分数机制精确匹配100 分优先级最高通配符匹配根据匹配段数计算分数20 分/段前缀匹配50 分坑点配置顺序不重要但路径精确度决定优先级/api/user/**和/api/user/*同时配置时后者更精确优先匹配5.3 为什么用事件刷新而不是直接刷新缓存问题配置变更可能涉及多条缓存、多个节点直接刷新会导致阻塞请求处理。事件驱动优势Async异步处理不影响请求性能事件广播可扩展到集群通过 Redis Pub/Sub配置变更审计日志可在事件监听器中统一记录坑点异步刷新有短暂延迟通常 100ms极端情况下可能有几次请求使用旧配置如果 Redis 不可用事件广播可能失败需增加降级逻辑5.4 ThreadLocal 上下文的坑问题ThreadLocal 在异步线程池、线程复用场景下可能污染。解决方案拦截器afterCompletion必须调用clear()异步任务如Async方法不应依赖 ThreadLocal应从IApiConfigManager直接获取坑点不要在 Service 层长时间持有 ThreadLocal 配置可能在后续拦截器中已清除测试环境需注意 ThreadLocal 泄漏单元测试后未清除5.5 配置优先级的坑优先级规则数据库配置 注解配置 系统默认值坑点如果数据库配置和注解配置冲突数据库配置优先可能覆盖注解效果新增接口时如果未配置数据库记录会使用注解或默认值配置状态status0停用的记录不参与匹配但数据库中仍存在5.6 缓存预热 vs 懒加载预热优势启动时一次性加载所有配置避免首次请求延迟缓存统计信息准确命中率、加载次数懒加载优势启动速度更快只缓存实际访问的配置取舍配置数量 1000 时预热更合适配置数量巨大时懒加载更合适。5.7 常见错误配置示例错误 1urlPath未考虑 Spring MVC 路径参数// ❌ 错误无法匹配 /api/user/123 urlPath: /api/user/:id // ✅ 正确Spring MVC 路径参数用 Ant 风格 urlPath: /api/user/*错误 2reqMethod大小写不一致// ❌ 错误大小写不一致可能导致匹配失败 reqMethod: get // ✅ 正确统一使用大写 reqMethod: GET错误 3sensitive_fields格式错误// ❌ 错误非 JSON 数组格式 sensitiveFields: phone,id_card // ✅ 正确JSON 数组格式 sensitiveFields: [\phone\,\id_card\]6. 如何二开6.1 新增一个 API 配置步骤通过后台页面添加推荐访问系统管理 API 配置管理新增配置填写接口名称、路径、行为标志位保存后自动触发缓存刷新通过数据库脚本添加适合批量导入INSERT INTO sys_api_config ( api_name, api_code, req_method, url_path, auth_flag, encrypt_flag, tenant_flag, limit_flag, module_code, status, tenant_id ) VALUES ( 查询用户列表, user_list, GET, /api/user/page, 1, 0, 1, 0, system, 1, 1 );注意tenant_id必须为1默认租户插入后需手动刷新缓存调用/system/apiConfig/refreshAll通过 API 接口添加curl -X POST http://localhost:8580/system/apiConfig/add \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d { apiName: 查询用户列表, apiCode: user_list, reqMethod: GET, urlPath: /api/user/page, authFlag: 1, encryptFlag: 0, tenantFlag: 1, moduleCode: system }6.2 扩展拦截器使用 API 配置示例在CryptoInterceptor中使用配置决定是否加解密Component public class CryptoInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 从 ThreadLocal 获取配置 if (ApiConfigContextHolder.needEncrypt()) { // 标记请求需要解密 request.setAttribute(needEncrypt, true); } return true; } Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { // 从 ThreadLocal 获取配置 if (ApiConfigContextHolder.needEncrypt()) { // 标记响应需要加密 response.setHeader(X-Need-Encrypt, true); } } }6.3 新增一个行为标志位步骤修改数据库表ALTER TABLE sys_api_config ADD COLUMN audit_flag TINYINT DEFAULT 0 COMMENT 是否记录审计日志1-开启, 0-关闭;修改实体类Data public class SysApiConfig { // 新增字段 TransField(dictType yes_no) private Integer auditFlag; TableField(exist false) private String auditFlagName; }修改 DTOData public class ApiConfigInfo { private Boolean needAudit; public static ApiConfigInfo fromEntity(SysApiConfig entity) { info.setNeedAudit(entity.getAuditFlag() ! null entity.getAuditFlag() 1); return info; } }修改 ThreadLocal 上下文public static boolean needAudit() { ApiConfigInfo config CONTEXT_HOLDER.get(); return config ! null config.getNeedAudit(); }修改拦截器if (ApiConfigContextHolder.needAudit()) { auditLogger.log(request, response); }6.4 自定义缓存策略修改配置属性forge: api-config: cache: local: max-size: 5000 # 增大本地缓存容量 expire-minutes: 30 # 增长过期时间 redis: enabled: false # 禁用 Redis 缓存单机场景自定义缓存实现替换ApiConfigManagerImpl的缓存逻辑例如使用 Guava Cacheprivate final CacheString, ApiConfigInfo guavaCache CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .recordStats() .build();6.5 接入前端页面前端页面示例使用 AiCrudPagetemplate AiCrudPage :api-configapiConfig :columnscolumns :search-schemasearchSchema :edit-schemaeditSchema / /template script setup const apiConfig { pageUrl: GET/system/apiConfig/page, addUrl: POST/system/apiConfig/add, editUrl: POST/system/apiConfig/edit, deleteUrl: POST/system/apiConfig/remove, getByIdUrl: GET/system/apiConfig/getById?id:id } const columns computed(() [ { title: 接口名称, key: apiName }, { title: 接口编码, key: apiCode }, { title: 请求方式, key: reqMethod }, { title: 接口路径, key: urlPath }, { title: 所属模块, key: moduleCode }, { title: 认证, key: authFlag, render: h h(DictTag, { dictType: yes_no, value: row.authFlag }) }, { title: 加解密, key: encryptFlag, render: h h(DictTag, { dictType: yes_no, value: row.encryptFlag }) }, { title: 租户隔离, key: tenantFlag, render: h h(DictTag, { dictType: yes_no, value: row.tenantFlag }) }, { title: 限流, key: limitFlag, render: h h(DictTag, { dictType: yes_no, value: row.limitFlag }) }, { title: 状态, key: status, render: h h(DictTag, { dictType: enable_disable, value: row.status }) }, ]) const searchSchema computed(() [ { field: apiName, label: 接口名称, component: NInput }, { field: moduleCode, label: 所属模块, component: NInput }, { field: status, label: 状态, component: DictSelect, dictType: enable_disable }, ]) const editSchema computed(() [ { field: apiName, label: 接口名称, component: NInput, required: true }, { field: apiCode, label: 接口编码, component: NInput, required: true }, { field: reqMethod, label: 请求方式, component: DictSelect, dictType: req_method, required: true }, { field: urlPath, label: 接口路径, component: NInput, required: true }, { field: moduleCode, label: 所属模块, component: NInput }, { field: authFlag, label: 是否认证, component: NSwitch, defaultValue: 1 }, { field: encryptFlag, label: 是否加解密, component: NSwitch, defaultValue: 0 }, { field: tenantFlag, label: 是否租户隔离, component: NSwitch, defaultValue: 1 }, { field: limitFlag, label: 是否限流, component: NSwitch, defaultValue: 0 }, { field: status, label: 状态, component: DictSelect, dictType: enable_disable, defaultValue: 1 }, ]) /script6.6 新增配置管理接口示例新增刷新缓存接口RestController RequestMapping(/system/apiConfig) public class ApiConfigManageController { Autowired private IApiConfigManager apiConfigManager; /** * 刷新所有缓存 */ PostMapping(/refreshAll) OperationLog(module API配置管理, type OperationType.UPDATE, desc 刷新所有缓存) public RespInfoVoid refreshAll() { apiConfigManager.refreshAllApiConfig(); return RespInfo.success(); } /** * 刷新指定配置缓存 */ PostMapping(/refreshById) OperationLog(module API配置管理, type OperationType.UPDATE, desc 刷新指定缓存) public RespInfoVoid refreshById(RequestParam Long id) { apiConfigManager.refreshApiConfigById(id); return RespInfo.success(); } /** * 获取缓存统计信息 */ GetMapping(/cacheStats) public RespInfoString getCacheStats() { return RespInfo.success(apiConfigManager.getCacheStats()); } }7. 体验入口和下一篇预告体验 Forge Admin在线演示http://www.dlforgelab.com:8084/forge/login默认账号admin / 123456Giteehttps://gitee.com/ForgeLab/forge-adminGitHubhttps://github.com/yaomindong1996/forge-admin验证步骤登录后台进入系统管理 API 配置管理新增一条配置观察缓存刷新日志修改配置的authFlag或encryptFlag验证拦截器行为变化调用/system/apiConfig/cacheStats查看缓存命中率下一篇预告下一篇我们将继续拆解多租户后台怎么做数据隔离从 tenant_id 到拦截器的完整链路介绍 Forge Admin 如何通过TenantLineInnerInterceptor实现配置即生效的租户隔离能力避免业务代码散落租户判断。

相关文章:

从配置到运行时:Forge Admin 的动态 API 配置管理是怎么做的

问题:同一个接口,今天要加认证、明天要加加密、后天要限流,这些行为散落在拦截器、过滤器、注解里,改一次牵一发动全身,怎么集中管理和动态刷新? 1. 这个问题在企业后台里为什么常见 在企业后台开发中&am…...

从零开始构建个人知识库:kepano-obsidian笔记模板完整指南

从零开始构建个人知识库:kepano-obsidian笔记模板完整指南 【免费下载链接】kepano-obsidian My personal Obsidian vault template. A bottom-up approach to note-taking and organizing things I am interested in. 项目地址: https://gitcode.com/gh_mirrors/…...

每日一书㉗ | 刻意练习:为什么有些人努力一辈子还是平庸?

“本文来自「乐想屋」公众号,系列更新[每日一书],每次5分钟,帮你把书读薄,把知识用活”先问你一个问题。你身边有没有这样的人:入行时间比你短,但能力已经甩你好几条街。他们好像没有特别刻苦,但…...

【小白快速上手】 OpenClaw 安装部署全流程(含安装包)

OpenClaw 一键安装包|一键部署,告别复杂环境配置 适配系统:Windows10/11 64 位当前版本:v2.7.5(虾壳云版)核心优势:全程可视化操作,无需命令行、无需手动配置 Python/Node.js&#…...

Claude Mythos Preview首月揪万余漏洞、拦截150万美元电诈,网络安全格局将变?

玻璃翼计划首战告捷A厂的玻璃翼计划首战告捷,Mythos 30天内就挖出1万个致命漏洞,甚至拦截了150万美元电诈。面对雪片式的报告,人类程序员崩溃求饶:「求别挖了,根本修不完啊!」就在刚刚,Anthropi…...

鼎讯AM-601光纤熔接机:交通通信建设与维护的可靠伙伴

在铁路、高速公路等交通基础设施的智能化建设中,稳定高效的光纤网络是指挥调度、安全监控等核心系统运行的生命线。鼎讯AM-601光纤熔接机,作为一款专为严苛环境设计的六马达便携式熔接设备,正成为保障这些关键通信链路畅通无阻的可靠选择。无…...

ESP32搭建TFT_LCD中文字库,附常用字库

(一)简介 在使用ESP32的时候,我们知道OLED屏幕是有中文库的,里面有非常多的常用字,但是LCD屏幕只有取模才能得到中文字体,那我们本期教程就来教大家如何搭建自己的字体库,使用中文字体更加方便快…...

H3C VSR路由器实战:用QoS策略给不同VLAN用户打DSCP标签(附配置命令详解)

H3C VSR路由器QoS实战:基于VLAN的DSCP标记与流量调度指南 在企业网络环境中,不同业务对网络质量的需求差异显著。普通办公流量可以容忍轻微延迟,但视频会议需要稳定的低延迟保障,而访客上网则可能消耗大量带宽却无需优先保障。本文…...

PDF差异对比神器diff-pdf:告别文档核对烦恼,提升工作效率的智能解决方案

PDF差异对比神器diff-pdf:告别文档核对烦恼,提升工作效率的智能解决方案 【免费下载链接】diff-pdf A simple tool for visually comparing two PDF files 项目地址: https://gitcode.com/gh_mirrors/di/diff-pdf 你是否曾在核对PDF文档时感到头疼…...

UnityExplorer:如何在游戏运行时实时调试和修改Unity项目

UnityExplorer:如何在游戏运行时实时调试和修改Unity项目 【免费下载链接】UnityExplorer An in-game UI for exploring, debugging and modifying IL2CPP and Mono Unity games. 项目地址: https://gitcode.com/gh_mirrors/un/UnityExplorer UnityExplorer是…...

智能知识学习平台

智能知识学习平台项目简介技术架构:问答驱动的开发模式前端架构后端架构核心功能:问答式交互贯穿始终1. 自定义构建知识库2.文档查看3.智能问答:知识触手可及4. 智能题目生成:严格遵循文档内容项目亮点用问答驱动的方式构建智慧学…...

面试官问LinkedBlockingQueue和ArrayBlockingQueue区别?别只答有界无界了,这3个实战坑才是重点

面试官追问LinkedBlockingQueue与ArrayBlockingQueue?别只答基础区别,这3个实战陷阱才是关键 当面试官抛出"LinkedBlockingQueue和ArrayBlockingQueue有什么区别"这个问题时,80%的候选人会条件反射般回答"一个有界一个无界&qu…...

HKMG工艺的“阿喀琉斯之踵”:聊聊那个无法移除的SiON界面层与未来0.3nm的挑战

HKMG工艺的隐形枷锁:SiON界面层的物理宿命与亚纳米级突围战 在半导体工艺演进的史诗中,HKMG(高K金属栅)技术曾被寄予厚望——它用金属栅极替代传统多晶硅,搭配高K介质材料HfO₂,一举解决了栅极耗尽和漏电流…...

白嫖Codex!一行代码不花接入国产DeepSeek-v4-pro,从此告别ChatGPT月费

Codex 如何接入国产模型 DeepSeek-v4-pro 保姆级教程 使用 Claude Code、Codex 已经好几个月了,不得不感叹现在的 AI 工具真的太强大了。目前市面上很多 Claude Code 如何接入大模型的教程,但 Codex 却比较少,一方面因为 Codex 需要 ChatGPT …...

还在古法编程?OpenAI Codex 全自动编程!稳定中转 Token 保姆级教程

OpenAI Codex 从安装到进阶实战|终端 AI 编程完全指南(2026 最新) 摘要:OpenAI Codex 是目前最强大的终端 AI 编程工具,支持代码生成、项目重构、Bug 修复、脚本自动化、批量代码优化等全场景能力。本文从零起步&…...

TorchEasyRec:阿里巴巴开源的推荐系统深度学习框架详解

第一部分:项目概览与核心功能 一、项目简介:什么是 TorchEasyRec? TorchEasyRec 是阿里巴巴 PAI 团队开发的基于 PyTorch 的推荐系统框架,专门用于构建生产级别的深度学习推荐模型。简单来说,它就是一个让你能够快速…...

密码学入门:区块链中的密码学原理

密码学入门:区块链中的密码学原理 大家好,我是欧阳瑞(Rich Own)。今天想和大家聊聊密码学这个重要话题。作为一个Web3探索者,密码学是区块链的基础。今天就来分享一下区块链中常用的密码学原理。 为什么密码学很重要&a…...

LLM测试工程师必看,Claude E2E测试架构设计,从用例生成、黄金样本构建到回归基线告警闭环

更多请点击: https://codechina.net 第一章:LLM测试工程师必看,Claude E2E测试架构设计,从用例生成、黄金样本构建到回归基线告警闭环 核心架构概览 Claude端到端测试架构采用三层解耦设计:输入层(动态用…...

关于软件版本升级的故事

起因在群里有网友说软件的版本升级比较简单,俺就回了四个字母“PACS”,并补上了一个表情 然后看见开始细说了:一、PACS 属于哪一类?PACS 软件 第二类医疗器械(独立软件)国家药监局分类:Ⅱ 类 2…...

TigerVNC跨平台远程桌面解决方案:构建企业级安全连接的技术实践

TigerVNC跨平台远程桌面解决方案:构建企业级安全连接的技术实践 【免费下载链接】tigervnc High performance, multi-platform VNC client and server 项目地址: https://gitcode.com/gh_mirrors/ti/tigervnc 在数字化转型浪潮中,远程桌面访问已成…...

如何永久保存微信聊天记录?WeChatMsg终极数据导出指南

如何永久保存微信聊天记录?WeChatMsg终极数据导出指南 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeCha…...

别再手动改代码了!用Vivado的VIO IP核实时调试你的FPGA设计(附UART实例)

实时交互式FPGA调试革命:Vivado VIO核的UART实战指南 调试FPGA设计时,你是否经历过这样的痛苦循环:修改一行代码→全编译→下载比特流→测试→发现问题→再修改...这种"石器时代"的工作流正在吞噬工程师的创造力。Xilinx Vivado中的…...

关于内卷,几个值得深想的洞察

首先声明:这篇不劝躺平,也不教内卷——只是想说清楚,你到底在一个什么样的游戏里。 你以为内卷是“资源不足”,其实是“分配方式” 很多人对内卷有个本质上的认知错误:以为内卷是因为资源不够,大家为了抢资…...

终极鸣潮优化指南:WaveTools工具箱让你的游戏体验飞起来

终极鸣潮优化指南:WaveTools工具箱让你的游戏体验飞起来 【免费下载链接】WaveTools 🧰鸣潮工具箱 项目地址: https://gitcode.com/gh_mirrors/wa/WaveTools 对于《鸣潮》玩家来说,流畅的游戏体验和个性化的配置管理是提升游戏乐趣的关…...

TII投稿避坑指南:LaTeX模板编译报错‘xxx-eps-converted-to.pdf not found’的终极解决方案

TII投稿LaTeX避坑实战:从编译报错到完美PDF生成的终极指南 凌晨三点的实验室,屏幕上闪烁的xxx-eps-converted-to.pdf not found错误提示仿佛在嘲笑你连续八小时的徒劳尝试。这不是科幻场景,而是每位用LaTeX撰写TII论文的研究者都可能遭遇的真…...

公共卫生机器学习项目中的算法公平性实践:ACAR框架详解

1. 项目概述:当机器学习遇见公共卫生,公平性为何成为“必答题”?在公共卫生领域,机器学习(ML)正以前所未有的速度渗透到疾病监测、风险分层和资源分配等核心环节。想象一下,一个模型被用来预测某…...

Python多智能体建模终极指南:用Mesa轻松构建复杂系统仿真

Python多智能体建模终极指南:用Mesa轻松构建复杂系统仿真 【免费下载链接】mesa Mesa is an open-source Python library for agent-based modeling, ideal for simulating complex systems and exploring emergent behaviors. 项目地址: https://gitcode.com/gh_…...

第 2 期:广告视觉提效:FastAPI+LangChain 对接豆包图片模型(附完整代码)

https://mp.weixin.qq.com/s/El8_eV3wYCW-OPungbt7ng...

OpenCore Legacy Patcher完整指南:如何让老旧Mac重获新生运行最新macOS

OpenCore Legacy Patcher完整指南:如何让老旧Mac重获新生运行最新macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 想让你的老旧Mac设备重获新…...

SafeExamBrowser安全绕过实战:虚拟机检测绕过与日志伪装技术架构深度解析

SafeExamBrowser安全绕过实战:虚拟机检测绕过与日志伪装技术架构深度解析 【免费下载链接】safe-exam-browser-bypass A VM and display detection bypass for SEB. 项目地址: https://gitcode.com/gh_mirrors/sa/safe-exam-browser-bypass SafeExamBrowser&…...