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 com.yunhai.userhome.utils.ThemeCodec; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; 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 读(已登录可读,当时"可空不兜底"——S7 起改为学校→平台级联,见文末说明); * saveSchThemeState 写(仅学校管理员/超管);clearSchTheme 清空(置空 theme 字段——sch_settings 行还承载 * logo 等字段不能删行,置空后读侧返回 null,效果等同"未配置")。存储形态:theme_name 列存紧凑四段编码 * {@code theme|sideDeep01|gradOn01|sideFollow01}(如 teal|0|1|1;不拆列、零 DDL,列宽未确认故不存整份 JSON; * 旧格式值/非法值读侧一律视同未配置)。

*

默认头像轮(2026-09-30)同构新增学校默认头像样式三接口:schAvatarStyle 读(可空不兜底); * saveSchAvatarStyle 写(仅学校管理员/超管,值只接受 text|silhouette);clearSchAvatarStyle 清空(置空 * avatar_style 字段)。

*

外观偏好轮(2026-10-08):编解码下沉 {@link ThemeCodec}、schId 解析下沉 * {@link ISchSettingsService#resolveThemeSchId}(行为等价重构,供 AppearanceController 共享)。

*

三级重构 S7(2026-10-08):{@code schTheme}/{@code schAvatarStyle} 读侧加"学校 → 平台默认(schId=0)"级联 * ({@link #readThemeWithFallback}/{@link #readAvatarWithFallback})——本校未配置或脏值时回落平台档, * 平台也无配置才返回 null。写侧与接口签名、响应体结构均未变;{@code getSchSettings}/{@code findBySchId} * 的"合成默认对象"兜底语义也未变。

*/ @RestController @RequestMapping(value = Config.API_PATH) public class SchSettingsController { @Autowired private ISchSettingsService schSettingsService; @Autowired private IUserCurrentRoleService userCurrentRoleService; /** 主题档写权限:超管或学校管理员(先例: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("仅学校管理员可配置本校默认主题"); } } } /** 平台默认层在 sch_settings 中的固定 schId(与 UsedAppController/壁纸/LOGO 的 0 层同口径) */ private static final int PLATFORM_SCH_ID = 0; /** * 主题档读侧级联(设置中心三级重构 S7,2026-10-08):本校行未配置/旧格式/脏值 → 再查平台默认层(schId=0), * 命中即返回平台档;两级都无 → null。schId 自身即平台层时不二次回落(避免自查两次)。 *

写侧不受影响:超管仍写 schId=0、校管写本校。

*/ private LayerValue readThemeWithFallback(Integer schId) { String themeState = decodeTheme(schId); if (themeState != null || schId == null || schId == PLATFORM_SCH_ID) return new LayerValue(themeState, schId); return new LayerValue(decodeTheme(PLATFORM_SCH_ID), PLATFORM_SCH_ID); } /** 头像样式读侧级联:语义与 {@link #readThemeWithFallback} 同构(本校空/脏 → 平台层 → null)。 */ private LayerValue readAvatarWithFallback(Integer schId) { String avatarStyle = readAvatar(schId); if (avatarStyle != null || schId == null || schId == PLATFORM_SCH_ID) return new LayerValue(avatarStyle, schId); return new LayerValue(readAvatar(PLATFORM_SCH_ID), PLATFORM_SCH_ID); } /** * 级联读命中结果:{@code value}=命中的档位值(两级都无则 null),{@code hitSchId}=**实际命中层**的 schId * (回落发生时是 0,不是解析出的本机 schId)。 *

响应体的 {@code schId} 字段因此采用"实际命中层"语义(方案 §6.S7 允许二选一)—— * 设置中心的"本校默认"层要据此区分"本校已配置"与"本校未配置、正在回落平台默认"。 * 现有前端只消费 themeState/avatarStyle,不依赖该字段。

*/ private static final class LayerValue { final String value; final Integer hitSchId; LayerValue(String value, Integer hitSchId) { this.value = value; this.hitSchId = hitSchId; } } /** 原样读某层主题档并解码(查无行/脏值 → null) */ private String decodeTheme(Integer schId) { return schSettingsService.findBySchIdRaw(schId) .map(SchSettings::getThemeName) .map(ThemeCodec::decodeThemeState) .orElse(null); } /** 原样读某层头像样式(查无行/脏值 → null) */ private String readAvatar(Integer schId) { return schSettingsService.findBySchIdRaw(schId) .map(SchSettings::getAvatarStyle) .filter(ThemeCodec::isAvatarStyle) .orElse(null); } /** 学校默认头像样式(默认头像轮 2026-09-30,已登录可读):{avatarStyle: "text"|"silhouette"|null, schId}。 * 未配置/脏值 → 回落平台默认层(见 {@link #readAvatarWithFallback});两级都无 → avatarStyle=null * (前端"不写键、不报错")。 */ @GetMapping("schAvatarStyle") public Map schAvatarStyle(BoaUser authUserWithDept) { LayerValue hit = readAvatarWithFallback(schSettingsService.resolveThemeSchId(authUserWithDept, null)); Map out = new HashMap<>(); out.put("avatarStyle", hit.value); out.put("schId", hit.hitSchId); return out; } /** 保存学校默认头像样式(仅学校管理员/超管):值只接受 "text"/"silhouette",非法抛 DException。 */ @PostMapping("saveSchAvatarStyle") public void saveSchAvatarStyle(BoaUser authUserWithDept, @RequestParam("avatarStyle") String avatarStyle, @RequestParam(name = "schId", required = false) Integer schId) { requireThemeAdmin(authUserWithDept); if (!ThemeCodec.isAvatarStyle(avatarStyle)) { throw new DException("头像样式不合法: " + avatarStyle); } Integer targetSchId = schSettingsService.resolveThemeSchId(authUserWithDept, schId); SchSettings schSettings = schSettingsService.findBySchIdRaw(targetSchId).orElseGet(SchSettings::new); schSettings.setCDate(new Date()); schSettings.setSchId(targetSchId); schSettings.setCId(authUserWithDept.getId()); schSettings.setAvatarStyle(avatarStyle); schSettingsService.save(schSettings); } /** 清空学校默认头像样式(仅管理员):置空 avatar_style 字段(行还承载 logo/theme 等配置不能删行;置空后读侧回落平台层,平台也无则 null)。 */ @PostMapping("clearSchAvatarStyle") public void clearSchAvatarStyle(BoaUser authUserWithDept, @RequestParam(name = "schId", required = false) Integer schId) { requireThemeAdmin(authUserWithDept); Integer targetSchId = schSettingsService.resolveThemeSchId(authUserWithDept, schId); Optional existing = schSettingsService.findBySchIdRaw(targetSchId); if (!existing.isPresent()) return; SchSettings schSettings = existing.get(); schSettings.setAvatarStyle(null); schSettingsService.save(schSettings); } /** 学校默认主题档(已登录可读):{themeState: "<ThemeState JSON>" | null, schId};schId 为实际命中层(见 {@link LayerValue})。 * 本校未配置/脏值 → 回落平台默认层(见 {@link #readThemeWithFallback});两级都无 → themeState=null * (前端"不写键、不报错")。 */ @GetMapping("schTheme") public Map schTheme(BoaUser authUserWithDept) { LayerValue hit = readThemeWithFallback(schSettingsService.resolveThemeSchId(authUserWithDept, null)); Map out = new HashMap<>(); out.put("themeState", hit.value); out.put("schId", hit.hitSchId); 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 = schSettingsService.resolveThemeSchId(authUserWithDept, schId); String encoded = ThemeCodec.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 = schSettingsService.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) { schId = schSettingsService.resolveSchId(authUserWithDept); } SchSettings schSettings = new SchSettings(); Optional model = schSettingsService.findBySchId(schId); if (model.isPresent()) { schSettings = model.get(); } schSettings.setCDate(new Date()); schSettings.setSchId(schId); schSettings.setCId(authUserWithDept.getId()); schSettings.setThemeName(themeName); schSettingsService.save(schSettings); } @PostMapping("saveSchLogo") public void saveSchLogo(BoaUser authUserWithDept, String logoUrl, @RequestParam(name = "schId", defaultValue = "0") Integer schId) { if (schId == null || schId == 0) { schId = schSettingsService.resolveSchId(authUserWithDept); } SchSettings schSettings = newSchSettingsOrLoad(schId, authUserWithDept); schSettings.setLogoUrl(logoUrl); schSettingsService.save(schSettings); } /** getSchSettings:schId=0 时按 主学校→主归属组→0 推断(对齐旧 SchSettingsController)。 */ @GetMapping("getSchSettings") public Optional getSchSettings(BoaUser authUserWithDept, @RequestParam(name = "schId", defaultValue = "0") Integer schId) { if (schId == null || schId == 0) { schId = schSettingsService.resolveSchId(authUserWithDept); } return schSettingsService.findBySchId(schId); } private SchSettings newSchSettingsOrLoad(Integer schId, BoaUser user) { Optional model = schSettingsService.findBySchId(schId); SchSettings schSettings = model.orElseGet(SchSettings::new); schSettings.setCDate(new Date()); schSettings.setSchId(schId); schSettings.setCId(user.getId()); return schSettings; } }