diff --git a/src/main/java/com/yunhai/cms/controller/oapi/site/SiteOapiController.java b/src/main/java/com/yunhai/cms/controller/oapi/site/SiteOapiController.java index 408d353..57b83f2 100644 --- a/src/main/java/com/yunhai/cms/controller/oapi/site/SiteOapiController.java +++ b/src/main/java/com/yunhai/cms/controller/oapi/site/SiteOapiController.java @@ -2,6 +2,7 @@ package com.yunhai.cms.controller.oapi.site; import com.yunhai.cms.entity.CmsSite; import com.yunhai.cms.repository.CmsSiteRepository; +import com.yunhai.cms.utils.ExtJsonUtils; import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.utils.exception.ErrorCode; import lombok.Data; @@ -11,7 +12,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * 站点目录开放接口(/oapi/site/list,appKey 签名访问;V2.0 Part C.3)。 @@ -51,14 +54,34 @@ public class SiteOapiController { vo.setSiteCode(site.getCode()); vo.setName(site.getName()); vo.setSchId(site.getSchId() == null ? null : site.getSchId().longValue()); + // T1 主题联动(2026-10-03):theme 空=default;themeParams=config_json.theme_params + // 原样键值(解析写法对齐 PortalSiteSnapshot.of),未配=空对象(消费方免判空两写) + vo.setTheme(site.getTheme() == null || site.getTheme().isEmpty() + ? "default" : site.getTheme()); + Map config = ExtJsonUtils.parse(site.getConfigJson()); + Object tp = config == null ? null : config.get("theme_params"); + Map params = new LinkedHashMap<>(); + if (tp instanceof Map) { + for (Map.Entry e : ((Map) tp).entrySet()) { + if (e.getKey() != null && e.getValue() != null) { + params.put(String.valueOf(e.getKey()), String.valueOf(e.getValue())); + } + } + } + vo.setThemeParams(params); return vo; } - /** 站点选项(siteCode 为办事大厅侧定位键;schId 用于权限与租户定界) */ + /** 站点选项(siteCode 为办事大厅侧定位键;schId 用于权限与租户定界; + * theme/themeParams raw 透传——消费方(svc-hall hall 用户面)按皮肤声明自行过滤) */ @Data public static class SiteOptionVo { private String siteCode; private String name; private Long schId; + /** CMS 皮肤包名(空已兜底为 default) */ + private String theme; + /** CMS config_json.theme_params 原样键值(未配置=空对象) */ + private Map themeParams = new LinkedHashMap<>(); } } diff --git a/src/test/java/com/yunhai/cms/controller/oapi/SiteOapiIT.java b/src/test/java/com/yunhai/cms/controller/oapi/SiteOapiIT.java index 11a7acb..4b4b6c6 100644 --- a/src/test/java/com/yunhai/cms/controller/oapi/SiteOapiIT.java +++ b/src/test/java/com/yunhai/cms/controller/oapi/SiteOapiIT.java @@ -23,7 +23,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. *
    *
  • 鉴权:/oapi/** 无签名 401(RequireAppSignFilter)、错密钥 401;
  • *
  • 契约:schId 可选——不传=全部启用站点(含跨校),传了=按校过滤;
  • - *
  • 出参形态:data[] 元素键=siteCode/name/schId(消费方 mapToVos 按这三键解析)。
  • + *
  • 出参形态:data[] 元素键=siteCode/name/schId/theme/themeParams(消费方 mapToVos 按前 + * 三键解析;theme/themeParams 为 T1 主题联动新增,raw 透传——theme 空=default, + * themeParams=config_json.theme_params 原样键值,未配=空对象,消费方自行过滤)。
  • *
* 签名 canonical 与平台 AppSignInterceptor 一致:METHOD + "\n" + path[?sortedQuery] + "\n" + ts, * HmacSHA256 输出 Base64(见 SignUtil.signRequest / AppAuthFilter.buildSignedTarget)。 @@ -42,6 +44,11 @@ class SiteOapiIT extends CmsIntegrationTestBase { void seed() { purgeTables(); CmsSite demo1 = persistSite("demo1", 1, "演示站点一"); + // T1 主题联动(2026-10-03):demo1 配置皮肤包与主题参数(经 /oapi/site/list raw 透传) + demo1.setTheme("campus-glow"); + demo1.setConfigJson("{\"theme_params\":{\"primary\":\"#0e8a5f\",\"colorScheme\":\"custom\"}," + + "\"svc_hall_base\":\"http://localhost:8089\"}"); + siteRepository.saveAndFlush(demo1); // schx 为 demo1 的院系子站:《院系子站方案》接入口径——子站不进办事大厅, // 按校下发(svc-hall 站点选择器 listSites(schId))只含主站;反解全量仍含子站。 persistSite("schx", 1, "主站X子站", demo1.getId()); @@ -111,6 +118,22 @@ class SiteOapiIT extends CmsIntegrationTestBase { assertThat(body).contains("\"name\":\"演示站点一\"").contains("\"schId\":1"); } + @Test + @DisplayName("T1 主题联动:theme/themeParams raw 透传;未配置站点 theme=default、themeParams=空对象") + void themeFields_dispatchedRawWithFallback() throws Exception { + MvcResult result = mockMvc.perform(get("/oapi/site/list").headers(signHeaders("/oapi/site/list"))) + .andExpect(status().isOk()) + .andReturn(); + String body = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + // demo1:theme 原样下发,theme_params 内的键值原样透传(不解析别名、不注 colorScheme) + assertThat(body).contains("\"siteCode\":\"demo1\"") + .contains("\"theme\":\"campus-glow\"") + .contains("\"themeParams\":{\"primary\":\"#0e8a5f\",\"colorScheme\":\"custom\"}"); + // demo2 未配置主题参数:theme 兜底 default,themeParams 空对象(消费方免判空两写) + assertThat(body).contains("\"siteCode\":\"demo2\"").contains("\"theme\":\"default\""); + assertThat(body).doesNotContain("\"themeParams\":null"); + } + /** 按平台 AppSignInterceptor 规则生成签名头(path + 排序 query,与 AppAuthFilter 服务端验签对齐) */ private static org.springframework.http.HttpHeaders signHeaders(String target) { String ts = String.valueOf(System.currentTimeMillis());