From d71901db359c08c51b0b5edb5c2c8622dafc2dc9 Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 30 Sep 2026 12:52:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(theme):=20=E5=AD=A6=E6=A0=A1=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E4=B8=BB=E9=A2=98=E6=A1=A3=E4=B8=89=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E2=80=94=E2=80=94schTheme=20=E8=AF=BB=20/=20saveSchThemeState?= =?UTF-8?q?=20=E5=86=99=20/=20clearSchTheme=20=E6=B8=85=E7=A9=BA=EF=BC=88?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E8=AE=BE=E7=BD=AE=20S3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 契约对齐 YschTheme《主题设置与通道改动》§3.5:读已登录可开放且可空不兜底(与 getSchSettings 的 default-theme 兜底语义刻意区分);写仅学校管理员/超管(权限先例照抄 UserSettingsServiceImpl.findSchWallpaperManageList:type==1 或 OtherUtil.isAdminById(roleId));清空=置空 theme 字段(sch_settings 行还承载 logo 等配置不能删行,置空后读侧 null,效果与删行等价) - 存储形态:theme_name 列存紧凑四段编码 theme|sideDeep01|gradOn01|sideFollow01(如 teal|0|1|1)——不拆列零 DDL;列宽未确认故不存整份 JSON;读侧只认该格式,旧档名/脏值一律视同未配置(防旧库值意外生效);写入侧四字段校验从严 - schId 解析:超管=显式参数或 0(平台默认层);其余=当前角色学校(UserCurrentRole),兜底主归属 - ISchSettingsService 新增 findBySchIdRaw(无兜底原样读,可空语义专用);既有 saveSchTheme/saveSchLogo/getSchSettings 不动(无其他调用方,影响面零) --- .../controller/api/SchSettingsController.java | 127 ++++++++++++++++++ .../userhome/service/ISchSettingsService.java | 3 + .../service/impl/SchSettingsServiceImpl.java | 5 + 3 files changed, 135 insertions(+) diff --git a/src/main/java/com/yunhai/userhome/controller/api/SchSettingsController.java b/src/main/java/com/yunhai/userhome/controller/api/SchSettingsController.java index 3a5d9d0..08dcd43 100644 --- a/src/main/java/com/yunhai/userhome/controller/api/SchSettingsController.java +++ b/src/main/java/com/yunhai/userhome/controller/api/SchSettingsController.java @@ -1,10 +1,15 @@ package com.yunhai.userhome.controller.api; +import com.yunhai.platform.utils.exception.DException; import com.yunhai.platform.utils.model.BoaUser; import com.yunhai.userhome.config.Config; import com.yunhai.userhome.entity.SchSettings; +import com.yunhai.userhome.entity.UserCurrentRole; import com.yunhai.userhome.service.ISchSettingsService; +import com.yunhai.userhome.service.IUserCurrentRoleService; +import com.yunhai.userhome.utils.OtherUtil; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -12,20 +17,34 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; /** * 学校设置接口(CAS 登录态,路径 {@code /{platform}/}),对齐旧 UserHome SchSettingsController。 *

getSchSettings 查本地 sch_settings 表(不再依赖跨仓 schConfig); * saveSchTheme/saveSchLogo 写本地 sch_settings 表。

+ *

主题设置轮(2026-09-30)新增学校默认主题档三接口(契约见 YschTheme《主题设置与通道改动-执行方案》§3.5): + * schTheme 读(已登录可读,可空不兜底——与 getSchSettings 的兜底语义刻意区分); + * saveSchThemeState 写(仅学校管理员/超管);clearSchTheme 清空(置空 theme 字段——sch_settings 行还承载 + * logo 等字段不能删行,置空后读侧返回 null,效果等同"未配置")。存储形态:theme_name 列存紧凑四段编码 + * {@code theme|sideDeep01|gradOn01|sideFollow01}(如 teal|0|1|1;不拆列、零 DDL,列宽未确认故不存整份 JSON; + * 旧格式值/非法值读侧一律视同未配置)。

*/ @RestController @RequestMapping(value = Config.API_PATH) public class SchSettingsController { + /** 合法档位(与通道 THEME_NAMES 一致;防旧库脏值意外生效) */ + private static final String[] THEME_KEYS = {"blue", "ocean", "ink", "violet", "teal"}; + @Autowired private ISchSettingsService schSettingsService; + @Autowired + private IUserCurrentRoleService userCurrentRoleService; + private Integer resolveSchId(BoaUser user) { if (user.getSchool() != null) { return user.getSchool().getId(); @@ -36,6 +55,114 @@ public class SchSettingsController { return 0; } + /** 主题档的目标学校:超管=显式 schId 或 0(平台默认层);其余=当前角色学校(兜底主归属) */ + private Integer resolveThemeSchId(BoaUser user, Integer explicitSchId) { + if (user.getType() != null && user.getType() == 1) { + return explicitSchId == null ? 0 : explicitSchId; + } + return userCurrentRoleService.findById(user.getId()) + .map(UserCurrentRole::getSchId) + .filter(schId -> schId != null && schId > 0) + .orElseGet(() -> resolveSchId(user)); + } + + /** 主题档写权限:超管或学校管理员(先例:UserSettingsServiceImpl.findSchWallpaperManageList) */ + private void requireThemeAdmin(BoaUser user) { + boolean superAdmin = user.getType() != null && user.getType() == 1; + if (!superAdmin) { + Optional currentRole = userCurrentRoleService.findById(user.getId()); + if (!currentRole.isPresent() || !OtherUtil.isAdminById(currentRole.get().getRoleId())) { + throw new DException("仅学校管理员可配置本校默认主题"); + } + } + } + + /** 紧凑编码 → ThemeState JSON;非本编码形态(旧档名/脏值/字段非法)一律 null(视同未配置) */ + private static String decodeThemeState(String encoded) { + if (!StringUtils.hasText(encoded)) return null; + String[] parts = encoded.trim().split("\\|"); + if (parts.length != 4) return null; + if (!isThemeKey(parts[0])) return null; + for (int i = 1; i < 4; i++) { + if (parts[i].length() != 1 || (parts[i].charAt(0) != '0' && parts[i].charAt(0) != '1')) return null; + } + return "{\"theme\":\"" + parts[0] + "\",\"sideDeep\":" + parts[1] + + ",\"gradOn\":" + parts[2] + ",\"sideFollow\":" + parts[3] + "}"; + } + + /** ThemeState JSON 校验(四字段齐全、档名合法)+ → 紧凑编码;非法抛业务异常(写入侧从严) */ + private static String encodeThemeState(String themeStateJson) { + if (!StringUtils.hasText(themeStateJson)) throw new DException("主题档不能为空"); + com.fasterxml.jackson.databind.JsonNode n; + try { + n = new com.fasterxml.jackson.databind.ObjectMapper().readTree(themeStateJson); + } catch (Exception e) { + throw new DException("主题档格式不合法"); + } + if (n == null || !n.isObject() || !n.hasNonNull("theme") || !n.has("sideDeep") || !n.has("gradOn") || !n.has("sideFollow")) { + throw new DException("主题档格式不合法"); + } + String theme = n.get("theme").asText(); + if (!isThemeKey(theme)) throw new DException("主题档名不合法: " + theme); + return theme + "|" + (n.get("sideDeep").asBoolean() ? 1 : 0) + + "|" + (n.get("gradOn").asBoolean() ? 1 : 0) + + "|" + (n.get("sideFollow").asBoolean() ? 1 : 0); + } + + private static boolean isThemeKey(String theme) { + if (theme == null) return false; + for (String k : THEME_KEYS) { + if (k.equals(theme)) return true; + } + return false; + } + + /** + * 学校默认主题档(已登录可读):{themeState: "" | null, schId}。 + * 未配置/旧格式值/脏数据 → themeState=null(前端"不写键、不报错")。 + */ + @GetMapping("schTheme") + public Map schTheme(BoaUser authUserWithDept) { + Integer schId = resolveThemeSchId(authUserWithDept, null); + String themeState = schSettingsService.findBySchIdRaw(schId) + .map(SchSettings::getThemeName) + .map(SchSettingsController::decodeThemeState) + .orElse(null); + Map out = new HashMap<>(); + out.put("themeState", themeState); + out.put("schId", schId); + return out; + } + + /** 保存学校默认主题档(仅学校管理员/超管):themeState 为整份 ThemeState JSON,服务端转紧凑编码存 theme_name。 */ + @PostMapping("saveSchThemeState") + public void saveSchThemeState(BoaUser authUserWithDept, + @RequestParam("themeState") String themeState, + @RequestParam(name = "schId", required = false) Integer schId) { + requireThemeAdmin(authUserWithDept); + Integer targetSchId = resolveThemeSchId(authUserWithDept, schId); + String encoded = encodeThemeState(themeState); + SchSettings schSettings = schSettingsService.findBySchIdRaw(targetSchId).orElseGet(SchSettings::new); + schSettings.setCDate(new Date()); + schSettings.setSchId(targetSchId); + schSettings.setCId(authUserWithDept.getId()); + schSettings.setThemeName(encoded); + schSettingsService.save(schSettings); + } + + /** 清空学校默认主题(仅管理员):置空 theme 字段(行还承载 logo 等配置不能删行;置空后读侧返回 null)。 */ + @PostMapping("clearSchTheme") + public void clearSchTheme(BoaUser authUserWithDept, + @RequestParam(name = "schId", required = false) Integer schId) { + requireThemeAdmin(authUserWithDept); + Integer targetSchId = resolveThemeSchId(authUserWithDept, schId); + Optional existing = schSettingsService.findBySchIdRaw(targetSchId); + if (!existing.isPresent()) return; + SchSettings schSettings = existing.get(); + schSettings.setThemeName(null); + schSettingsService.save(schSettings); + } + @PostMapping("saveSchTheme") public void saveSchTheme(BoaUser authUserWithDept, String themeName, @RequestParam(name = "schId", defaultValue = "0") Integer schId) { if (schId == null || schId == 0) { diff --git a/src/main/java/com/yunhai/userhome/service/ISchSettingsService.java b/src/main/java/com/yunhai/userhome/service/ISchSettingsService.java index f318d36..ec0742d 100644 --- a/src/main/java/com/yunhai/userhome/service/ISchSettingsService.java +++ b/src/main/java/com/yunhai/userhome/service/ISchSettingsService.java @@ -12,5 +12,8 @@ public interface ISchSettingsService { Optional findBySchId(Integer schId); + /** 原样读(无兜底):学校默认主题档等"可空语义"场景专用——查无返回 empty,不合成默认对象 */ + Optional findBySchIdRaw(Integer schId); + List findBySchIds(List schIds); } diff --git a/src/main/java/com/yunhai/userhome/service/impl/SchSettingsServiceImpl.java b/src/main/java/com/yunhai/userhome/service/impl/SchSettingsServiceImpl.java index d36243e..cb73406 100644 --- a/src/main/java/com/yunhai/userhome/service/impl/SchSettingsServiceImpl.java +++ b/src/main/java/com/yunhai/userhome/service/impl/SchSettingsServiceImpl.java @@ -23,6 +23,11 @@ public class SchSettingsServiceImpl implements ISchSettingsService { schSettingsRepository.save(schSettings); } + @Override + public Optional findBySchIdRaw(Integer schId) { + return schSettingsRepository.findFirstBySchId(schId); + } + @Override public Optional findBySchId(Integer schId) { Optional local = schSettingsRepository.findFirstBySchId(schId);