From 71adaf6acbb9b88c52d8d6a46ca5d4969b9c355d Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 1 Sep 2026 11:50:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/application-prod.yml | 14 +++ pom.xml | 6 ++ .../config/EnvConfigPublicConfig.java | 51 ++++++++++ .../yunhai/userbase/config/SwaggerConfig.java | 32 ++++++- .../controller/api/EnvConfigController.java | 35 +++++++ .../service/impl/EnvConfigService.java | 92 +++++++++++++++++++ src/main/resources/application.yml | 31 ++++++- .../sys_user_role_rel_add_is_main_default.sql | 13 +++ 8 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/yunhai/userbase/config/EnvConfigPublicConfig.java create mode 100644 src/main/java/com/yunhai/userbase/controller/api/EnvConfigController.java create mode 100644 src/main/java/com/yunhai/userbase/service/impl/EnvConfigService.java create mode 100644 src/main/resources/sql/sys_user_role_rel_add_is_main_default.sql diff --git a/docker/application-prod.yml b/docker/application-prod.yml index 80bb6d3..7718c2f 100644 --- a/docker/application-prod.yml +++ b/docker/application-prod.yml @@ -62,6 +62,20 @@ spring: simple: auto-startup: false +# 健康检查(projman 健康总览探活口径:容器 Running + HTTP 200 即健康): +# 仅暴露 health;DB/Redis 计入总体状态(连不上即 DOWN=503,projman 显示 fail),rabbit 推送链路不算 +management: + endpoints: + web: + exposure: + include: health + endpoint: + health: + show-details: never + health: + rabbit: + enabled: false + # 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth) yh: sso: diff --git a/pom.xml b/pom.xml index 2c11623..4907139 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,12 @@ test + + + org.springframework.boot + spring-boot-starter-actuator + + com.alibaba.cloud diff --git a/src/main/java/com/yunhai/userbase/config/EnvConfigPublicConfig.java b/src/main/java/com/yunhai/userbase/config/EnvConfigPublicConfig.java new file mode 100644 index 0000000..17efd4f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/EnvConfigPublicConfig.java @@ -0,0 +1,51 @@ +package com.yunhai.userbase.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * {@code GET /api/env-config} 免登录放行(方案:前端动态配置方案-env-config接入落地.md §3.3)。 + *

+ * 前端 SPA 启动阶段(用户尚未登录)就要拉运行时配置,而 yh-auth 的 CAS 过滤器 + * (HIGHEST+4..+9,url-pattern /api/*)会把未登录请求 302 到 CAS 登录页,导致配置拉取失败。 + * 本过滤器注册在所有 CAS 过滤器之前(HIGHEST+3),精确匹配该端点路径后直接 forward: + * forward 属 FORWARD 派发,只经过为 FORWARD 注册的过滤器(CAS 四件套均仅注册 REQUEST 派发), + * 请求直达 DispatcherServlet 的 {@link com.yunhai.userbase.controller.api.EnvConfigController}。 + * 其余 /api/** 请求完全不受影响,登录态拦截行为逐字节不变。 + */ +@Configuration +public class EnvConfigPublicConfig { + + /** 免登录端点的 context 相对路径(与 EnvConfigController 的 /{platform}/env-config 中 platform=api 对应) */ + public static final String PUBLIC_PATH = "/api/env-config"; + + @Bean + public FilterRegistrationBean envConfigSsoBypassFilter() { + FilterRegistrationBean bean = new FilterRegistrationBean<>(); + bean.setFilter(new EnvConfigSsoBypassFilter()); + // 精确 URL 匹配,只作用于这一个端点;须早于 CasSsoApiRedirectFilter(HIGHEST+4) + bean.addUrlPatterns(PUBLIC_PATH); + bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 3); + return bean; + } + + static class EnvConfigSsoBypassFilter implements Filter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + // forward 同一 URI(query 参数随原请求保留);本过滤器仅注册 REQUEST 派发,不会被再次触发 + ((HttpServletRequest) request).getRequestDispatcher(PUBLIC_PATH).forward(request, response); + // 不调用 chain.doFilter:REQUEST 链上排在后面的 CAS 过滤器因此不会执行 + } + } +} diff --git a/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java b/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java index 3018a12..09bfba8 100644 --- a/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java +++ b/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java @@ -2,6 +2,7 @@ package com.yunhai.userbase.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import org.springdoc.core.GroupedOpenApi; import org.springdoc.core.customizers.GlobalOperationCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,7 +11,14 @@ import org.springframework.web.method.HandlerMethod; /** * 接口文档(springdoc-openapi,替代旧 Springfox)。

* 提供统一文档元信息;并通过全局定制器隐藏由切面注入的 {@code BoaUser authUserWithDept} 参数, - * 避免其作为请求体出现在文档中(该参数不参与 HTTP 入参,由 {@code BoaUserArgumentResolver} 解析)。 + * 避免其作为请求体出现在文档中(该参数不参与 HTTP 入参,由 {@code BoaUserArgumentResolver} 解析)。

+ * 文档分组: + *

+ * 生产环境建议关闭文档入口(见 application.yml 的 springdoc 注释)。 */ @Configuration public class SwaggerConfig { @@ -24,6 +32,28 @@ public class SwaggerConfig { .version("0.0.1")); } + /** + * 开放接口分组:/oapi/** 但排除 /oapi/internal/**(内部密钥分发端点,仅内网 + IP 白名单,不对外暴露契约)。 + * 导出快照:GET /v3/api-docs/oapi + */ + @Bean + public GroupedOpenApi oapiGroup() { + return GroupedOpenApi.builder() + .group("oapi") + .pathsToMatch("/oapi/**") + .pathsToExclude("/oapi/internal/**") + .build(); + } + + /** 业务接口分组:/api/**(会话鉴权的面向前端接口,与开放接口契约隔离)。 */ + @Bean + public GroupedOpenApi apiGroup() { + return GroupedOpenApi.builder() + .group("api") + .pathsToMatch("/api/**") + .build(); + } + /** 隐藏切面注入的登录用户参数,保持文档整洁。 */ @Bean public GlobalOperationCustomizer hideAuthUserParam() { diff --git a/src/main/java/com/yunhai/userbase/controller/api/EnvConfigController.java b/src/main/java/com/yunhai/userbase/controller/api/EnvConfigController.java new file mode 100644 index 0000000..1a926f6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/EnvConfigController.java @@ -0,0 +1,35 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.service.impl.EnvConfigService; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 前端运行时配置端点(免登录)。 + *

+ * 返回 projman 配置管理维护、双写 Nacos 的 env-config-{front}.json 原文(JSON), + * 前端 SPA 启动时请求 {@code GET /userbase/api/env-config?front=front},替代 config/index.js + * 与构建期 VITE_* 环境变量,实现接口地址等配置在线改、免发版生效。
+ * {@code /api/*} 前缀是 Caddy 反代到后端的既有路由,免登录放行由 + * {@link com.yunhai.userbase.config.EnvConfigPublicConfig} 的旁路过滤器实现。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/env-config") +public class EnvConfigController { + + @Autowired + private EnvConfigService envConfigService; + + /** 配置内容为非敏感信息(前端 JS 最终也会持有),按方案约定不做登录鉴权 */ + @Operation(summary = "前端运行时配置(projman 双写 Nacos,免登录)", operationId = "envConfig") + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public String get(@RequestParam(value = "front", defaultValue = "front") String front) { + return envConfigService.get(front); + } +} diff --git a/src/main/java/com/yunhai/userbase/service/impl/EnvConfigService.java b/src/main/java/com/yunhai/userbase/service/impl/EnvConfigService.java new file mode 100644 index 0000000..1422c75 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/impl/EnvConfigService.java @@ -0,0 +1,92 @@ +package com.yunhai.userbase.service.impl; + +import com.alibaba.cloud.nacos.NacosConfigManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 前端运行时配置读取(方案:Projman/前端动态配置方案-env-config接入落地.md)。 + *

+ * projman 配置管理中的 env-config-{前端目标名}.json 会双写到 Nacos + * (dataId = front-{应用名}-{目标名},group = FRONT_GROUP,type = json), + * 本服务按前端目标名读取并原样返回 JSON 串,经 {@code GET /api/env-config?front=xxx} 提供给前端 SPA, + * 替代构建期写死的 VITE_* 环境变量与 config/index.js 静态文件。 + *

+ * 容错口径:读不到(本地未开 Nacos / dataId 不存在 / 超时)一律返回 {@code "{}"}, + * 前端拿空对象后走 import.meta.env 构建期兜底,不影响启动。 + */ +@Service +public class EnvConfigService { + + private static final Logger log = LoggerFactory.getLogger(EnvConfigService.class); + + /** 前端目标名白名单:projman 前端目标创建时即限制为简单标识符,此处同口径校验,防 dataId 拼接注入 */ + private static final String FRONT_NAME_PATTERN = "^[A-Za-z0-9_-]{1,64}$"; + + /** Nacos dataId 前缀:front-{应用名}-,与 projman AppConfigFileService 双写规则一致 */ + @Value("${yh.env-config.data-id-prefix:front-userbase-}") + private String dataIdPrefix; + + /** Nacos group,与 projman 双写侧 FRONT_GROUP 常量一致 */ + @Value("${yh.env-config.group:FRONT_GROUP}") + private String group; + + /** 结果缓存 TTL:前端 SPA 每次打开页面都会拉一次,缓存削峰避免轮询打穿 Nacos */ + private static final long CACHE_TTL_MS = 30_000L; + + private final Map cache = new ConcurrentHashMap<>(); + + private static class CacheEntry { + final String content; + final long ts; + CacheEntry(String content, long ts) { + this.content = content; + this.ts = ts; + } + } + + /** 本地 profile 未启用 Nacos 时为 null(NACOS_CONFIG_ENABLED=false 时不注册该 bean) */ + @Autowired(required = false) + private NacosConfigManager nacosConfigManager; + + /** 读取前端目标配置 JSON;任何失败返回 "{}"(前端走构建期兜底) */ + public String get(String front) { + if (front == null || !front.trim().matches(FRONT_NAME_PATTERN)) { + return "{}"; + } + String key = front.trim(); + CacheEntry cached = cache.get(key); + long now = System.currentTimeMillis(); + if (cached != null && now - cached.ts < CACHE_TTL_MS) { + return cached.content; + } + String content = readFromNacos(key); + cache.put(key, new CacheEntry(content, now)); + return content; + } + + private String readFromNacos(String front) { + if (nacosConfigManager == null) { + // 本地开发未接 Nacos:无运行时配置,前端走 import.meta.env 兜底 + return "{}"; + } + try { + String content = nacosConfigManager.getConfigService() + .getConfig(dataIdPrefix + front, group, 3000); + if (content == null || content.trim().isEmpty()) { + return "{}"; + } + return content.trim(); + } catch (Exception e) { + // Nacos 不可达时 getConfig 自身有本地快照兜底;仍失败则返回空对象,不抛错不影响前端启动 + log.warn("[env-config] 读取 Nacos 配置失败 dataId={} group={}: {}", dataIdPrefix + front, group, e.getMessage()); + return "{}"; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 97e719e..53b14d4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -78,14 +78,35 @@ yh: - 172.16.0.0/12 - 192.168.10.0/200 -ADMIN_TAG_ID: 290 +# 接口文档(springdoc):分组见 SwaggerConfig(oapi=开放接口契约权威源 / api=业务接口) +# 契约快照导出:GET /v3/api-docs/oapi +# 生产环境关闭文档入口(spec JSON 与 UI 一并关,避免契约与内部端点外泄): +# SPRINGDOC_API_DOCS_ENABLED=false SPRINGDOC_SWAGGER_UI_ENABLED=false +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true + + +# 健康检查(供 projman 健康总览探活,容器间直连 http://userbase-service:8060/userbase/actuator/health): +# 只暴露 health 一个端点;show-details=never 不外泄 DB/Redis 等组件细节,仅返回总体状态 {"status":"UP"}; +# rabbit 连通性不计入总体健康(推送链路故障不应判定用户中心整体不可用),本地无 rabbit 时同理避免误报 DOWN +management: + endpoints: + web: + exposure: + include: health + endpoint: + health: + show-details: never + health: + rabbit: + enabled: false # 上传文件本地存储根目录(Linux 部署请通过环境变量/配置中心外置,避免写死 Windows 盘符) FILE_PATH: D://bsch//bschlearn # 上传文件对外访问的 Web 根地址,须与 MyWebAppConfigurer 中的 /upload/{子目录}/ 映射保持一致 FILE_WEB_PATH: http://localhost:8060/upload/ - -ZIYUAN_WEB_PATH_HTTP: http://base.bestsch.com/ziyuanku/file/ -UPLOAD_WEB_PATH_HTTP: http://localhost:8060/upload/file/ -ORC_SERVICE_URL: http://oa.bestsch.com/OrcService/api/Ocr/Upload +UPLOAD_WEB_PATH_HTTP: http://localhost:8060/upload/file/ \ No newline at end of file diff --git a/src/main/resources/sql/sys_user_role_rel_add_is_main_default.sql b/src/main/resources/sql/sys_user_role_rel_add_is_main_default.sql new file mode 100644 index 0000000..3ce1f5f --- /dev/null +++ b/src/main/resources/sql/sys_user_role_rel_add_is_main_default.sql @@ -0,0 +1,13 @@ +-- 为 sys_user_role_rel.is_main 补默认值,修复「去字段化后 insert 缺列」报错。 +-- 背景:is_main 已去字段化(实体不再映射该列、insert 不再写它,主身份改由主组→主校→最小 sortNo/id 读时推导), +-- 但物理列保留至阶段 3(对账任务 MainIdentityConsistencyCheckTask 读完遗留列确认差异清零后才 DROP COLUMN, +-- 见《is_main去字段化_实施自检与审计.md》)。 +-- 该列当前为 NOT NULL 且无默认值,MySQL 严格模式下 insert 缺列即报: +-- ERROR 1364 (HY000): Field 'is_main' doesn't have a default value +-- 影响入口:SysUserService.saveSchoolIdentity(/api/sysuser/Edit 全量覆盖身份)、IdentityBackfillTask 回填。 +-- 存量 is_main=1 数据保留不动(对账任务仍需读),仅新增行取默认 0,与「读时计算」语义一致。 +-- 注意:实体已不映射该列,ddl-auto=update 不会为其补默认值,dev/prod 均需手工执行本脚本。 +-- 执行方式:由 DBA 在 dev / prod 各自手工执行,AI 不代跑。 + +ALTER TABLE sys_user_role_rel + ALTER COLUMN is_main SET DEFAULT 0;