feat(appearance): 主题档存储/传输全面 JSON 化(读侧兼容旧紧凑值)+ LOGO 文字 sch_settings.logo_text 级联读写(LOGO文字与主题JSON化)
build / build-push (push) Successful in 2m1s
build / build-push (push) Successful in 2m1s
This commit is contained in:
@@ -28,8 +28,8 @@ import java.util.Optional;
|
|||||||
* <p>主题设置轮(2026-09-30)新增学校默认主题档三接口(契约见 YschTheme《主题设置与通道改动-执行方案》§3.5):
|
* <p>主题设置轮(2026-09-30)新增学校默认主题档三接口(契约见 YschTheme《主题设置与通道改动-执行方案》§3.5):
|
||||||
* schTheme 读(已登录可读,当时"可空不兜底"——S7 起改为学校→平台级联,见文末说明);
|
* schTheme 读(已登录可读,当时"可空不兜底"——S7 起改为学校→平台级联,见文末说明);
|
||||||
* saveSchThemeState 写(仅学校管理员/超管);clearSchTheme 清空(置空 theme 字段——sch_settings 行还承载
|
* saveSchThemeState 写(仅学校管理员/超管);clearSchTheme 清空(置空 theme 字段——sch_settings 行还承载
|
||||||
* logo 等字段不能删行,置空后读侧返回 null,效果等同"未配置")。存储形态:theme_name 列存紧凑四段编码
|
* logo 等字段不能删行,置空后读侧返回 null,效果等同"未配置")。存储形态(2026-10-09 JSON 化):
|
||||||
* {@code theme|sideDeep01|gradOn01|sideFollow01}(如 teal|0|1|1;不拆列、零 DDL,列宽未确认故不存整份 JSON;
|
* theme_name 列存规范化 ThemeState JSON 串(固定字段序;历史紧凑四段编码读侧兼容自动转 JSON;
|
||||||
* 旧格式值/非法值读侧一律视同未配置)。</p>
|
* 旧格式值/非法值读侧一律视同未配置)。</p>
|
||||||
* <p>默认头像轮(2026-09-30)同构新增学校默认头像样式三接口:schAvatarStyle 读(可空不兜底);
|
* <p>默认头像轮(2026-09-30)同构新增学校默认头像样式三接口:schAvatarStyle 读(可空不兜底);
|
||||||
* saveSchAvatarStyle 写(仅学校管理员/超管,值只接受 text|silhouette);clearSchAvatarStyle 清空(置空
|
* saveSchAvatarStyle 写(仅学校管理员/超管,值只接受 text|silhouette);clearSchAvatarStyle 清空(置空
|
||||||
@@ -121,7 +121,7 @@ public class SchSettingsController {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存学校默认主题档(仅学校管理员/超管):themeState 为整份 ThemeState JSON,服务端转紧凑编码存 theme_name。 */
|
/** 保存学校默认主题档(仅学校管理员/超管):themeState 为整份 ThemeState JSON,规范化后原形态存 theme_name(2026-10-09 JSON 化)。 */
|
||||||
@PostMapping("saveSchThemeState")
|
@PostMapping("saveSchThemeState")
|
||||||
public void saveSchThemeState(BoaUser authUserWithDept,
|
public void saveSchThemeState(BoaUser authUserWithDept,
|
||||||
@RequestParam("themeState") String themeState,
|
@RequestParam("themeState") String themeState,
|
||||||
@@ -150,6 +150,37 @@ public class SchSettingsController {
|
|||||||
schSettingsService.save(schSettings);
|
schSettingsService.save(schSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== LOGO 文字(平台/学校名称,LOGO 文字轮 2026-10-09):与主题三件套同模式 =====
|
||||||
|
|
||||||
|
/** LOGO 文字读(已登录可读):{logoText: string|null, schId=实际命中层}。本校空 → 平台层 → null。 */
|
||||||
|
@GetMapping("schLogoText")
|
||||||
|
public Map<String, Object> schLogoText(BoaUser authUserWithDept) {
|
||||||
|
ISchSettingsService.LayerValue hit =
|
||||||
|
schSettingsService.readLogoTextWithFallback(schSettingsService.resolveThemeSchId(authUserWithDept, null));
|
||||||
|
Map<String, Object> out = new HashMap<>();
|
||||||
|
out.put("logoText", hit.getValue());
|
||||||
|
out.put("schId", hit.getHitSchId());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存 LOGO 文字(仅学校管理员/超管):trim 后落本级(超管=平台层 0,校管=本校);空串存 null(=清空)。 */
|
||||||
|
@PostMapping("saveSchLogoText")
|
||||||
|
public void saveSchLogoText(BoaUser authUserWithDept,
|
||||||
|
@RequestParam("logoText") String logoText) {
|
||||||
|
requireThemeAdmin(authUserWithDept);
|
||||||
|
String text = logoText == null ? "" : logoText.trim();
|
||||||
|
if (text.length() > 64) {
|
||||||
|
throw new DException("LOGO 文字不能超过 64 个字符");
|
||||||
|
}
|
||||||
|
Integer targetSchId = schSettingsService.resolveThemeSchId(authUserWithDept, null);
|
||||||
|
SchSettings schSettings = schSettingsService.findBySchIdRaw(targetSchId).orElseGet(SchSettings::new);
|
||||||
|
schSettings.setCDate(new Date());
|
||||||
|
schSettings.setSchId(targetSchId);
|
||||||
|
schSettings.setCId(authUserWithDept.getId());
|
||||||
|
schSettings.setLogoText(text.isEmpty() ? null : text);
|
||||||
|
schSettingsService.save(schSettings);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("saveSchTheme")
|
@PostMapping("saveSchTheme")
|
||||||
public void saveSchTheme(BoaUser authUserWithDept, String themeName, @RequestParam(name = "schId", defaultValue = "0") Integer schId) {
|
public void saveSchTheme(BoaUser authUserWithDept, String themeName, @RequestParam(name = "schId", defaultValue = "0") Integer schId) {
|
||||||
if (schId == null || schId == 0) {
|
if (schId == null || schId == 0) {
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ public class SchSettings {
|
|||||||
@Column(name = "logo_url")
|
@Column(name = "logo_url")
|
||||||
private String logoUrl;
|
private String logoUrl;
|
||||||
|
|
||||||
|
/** LOGO 配套平台/学校名称文字(LOGO 图在 logo 表池,文字是单值配置随 settings;NULL=未配置回落上级) */
|
||||||
|
@Column(name = "logo_text")
|
||||||
|
private String logoText;
|
||||||
|
|
||||||
@Column(name = "home_img_url")
|
@Column(name = "home_img_url")
|
||||||
private String homeImgUrl;
|
private String homeImgUrl;
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ public interface ISchSettingsService {
|
|||||||
/** 头像样式读侧级联:语义与 {@link #readThemeWithFallback} 同构(本校空/脏 → 平台层 → null)。 */
|
/** 头像样式读侧级联:语义与 {@link #readThemeWithFallback} 同构(本校空/脏 → 平台层 → null)。 */
|
||||||
LayerValue readAvatarWithFallback(Integer schId);
|
LayerValue readAvatarWithFallback(Integer schId);
|
||||||
|
|
||||||
|
/** LOGO 文字读侧级联(LOGO 文字轮 2026-10-09):本校空 → 平台层 → null。 */
|
||||||
|
LayerValue readLogoTextWithFallback(Integer schId);
|
||||||
|
|
||||||
/** 级联读命中结果:{@code value}=命中的档位值(两级都无则 null),{@code hitSchId}=实际命中层的 schId
|
/** 级联读命中结果:{@code value}=命中的档位值(两级都无则 null),{@code hitSchId}=实际命中层的 schId
|
||||||
* (回落发生时是 0,不是解析出的本校 schId)。设置中心读端点的响应体 schId 即此语义——
|
* (回落发生时是 0,不是解析出的本校 schId)。设置中心读端点的响应体 schId 即此语义——
|
||||||
* 前端据此区分「本校已配置」与「本校未配置、正在回落平台默认」。 */
|
* 前端据此区分「本校已配置」与「本校未配置、正在回落平台默认」。 */
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
|||||||
try {
|
try {
|
||||||
Optional<UserAppearance> ua = userAppearanceRepository.findByUserId(user.getId());
|
Optional<UserAppearance> ua = userAppearanceRepository.findByUserId(user.getId());
|
||||||
if (ua.isPresent()) {
|
if (ua.isPresent()) {
|
||||||
out.put("myTheme", validCompactTheme(ua.get().getThemeState()));
|
out.put("myTheme", validThemeState(ua.get().getThemeState()));
|
||||||
out.put("avatarMy", validAvatarStyle(ua.get().getAvatarStyle()));
|
out.put("avatarMy", validAvatarStyle(ua.get().getAvatarStyle()));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -77,7 +77,7 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
|||||||
if (schId != null) {
|
if (schId != null) {
|
||||||
SchSettings sch = schSettingsService.findBySchIdRaw(schId).orElse(null);
|
SchSettings sch = schSettingsService.findBySchIdRaw(schId).orElse(null);
|
||||||
if (sch != null) {
|
if (sch != null) {
|
||||||
out.put("schTheme", validCompactTheme(sch.getThemeName()));
|
out.put("schTheme", validThemeState(sch.getThemeName()));
|
||||||
out.put("avatarSch", validAvatarStyle(sch.getAvatarStyle()));
|
out.put("avatarSch", validAvatarStyle(sch.getAvatarStyle()));
|
||||||
}
|
}
|
||||||
out.put("logoUrl", resolveLogoUrl(schId));
|
out.put("logoUrl", resolveLogoUrl(schId));
|
||||||
@@ -89,9 +89,10 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前用户生效外观(oapi 代理轮 2026-10-08,{@code /oapi/appearance} 出参真源):
|
* 当前用户生效外观(oapi 代理轮 2026-10-08,{@code /oapi/appearance} 出参真源;2026-10-09 JSON 化+LOGO 文字):
|
||||||
* {@code {theme, avatar, logoUrl}}——主题/头像为【合成后的生效值】(个人自选 → 学校默认 → 平台默认(schId=0)
|
* {@code {theme, avatar, logoText, logoUrl}}——theme 为【JSON 对象】(个人自选 → 学校默认 → 平台默认(schId=0)
|
||||||
* 级联,层级知识留在 UserHome 内部,对外不拆 my/sch);logoUrl 为本校 type=1 → 平台 type=0 择优。
|
* 级联,层级知识留在 UserHome 内部,对外不拆 my/sch);avatar 为生效样式;logoText 本校→平台级联;
|
||||||
|
* logoUrl 为本校 type=1 → 平台 type=0 择优。
|
||||||
*
|
*
|
||||||
* <p>schId 解析走本地 user_current_role(oapi 入参只有 userId,不引 identity 跨仓):roleId 当前角色学校,
|
* <p>schId 解析走本地 user_current_role(oapi 入参只有 userId,不引 identity 跨仓):roleId 当前角色学校,
|
||||||
* 无角色/跨校态回落 0(平台层)。与 /api/appearance 的 resolveThemeSchId(超管强制 0)仅在「超管临时切入
|
* 无角色/跨校态回落 0(平台层)。与 /api/appearance 的 resolveThemeSchId(超管强制 0)仅在「超管临时切入
|
||||||
@@ -102,6 +103,7 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
|||||||
Map<String, Object> out = new HashMap<>();
|
Map<String, Object> out = new HashMap<>();
|
||||||
out.put("theme", null);
|
out.put("theme", null);
|
||||||
out.put("avatar", null);
|
out.put("avatar", null);
|
||||||
|
out.put("logoText", null);
|
||||||
out.put("logoUrl", null);
|
out.put("logoUrl", null);
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
return out;
|
return out;
|
||||||
@@ -115,7 +117,7 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
|||||||
try {
|
try {
|
||||||
Optional<UserAppearance> ua = userAppearanceRepository.findByUserId(userId);
|
Optional<UserAppearance> ua = userAppearanceRepository.findByUserId(userId);
|
||||||
if (ua.isPresent()) {
|
if (ua.isPresent()) {
|
||||||
myTheme = validCompactTheme(ua.get().getThemeState());
|
myTheme = validThemeState(ua.get().getThemeState());
|
||||||
myAvatar = validAvatarStyle(ua.get().getAvatarStyle());
|
myAvatar = validAvatarStyle(ua.get().getAvatarStyle());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -125,25 +127,18 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
|||||||
String avatar = myAvatar;
|
String avatar = myAvatar;
|
||||||
try {
|
try {
|
||||||
if (theme == null) {
|
if (theme == null) {
|
||||||
// readThemeWithFallback 命中值是 ThemeState JSON(schTheme 端点口径);oapi 契约统一出
|
theme = schSettingsService.readThemeWithFallback(schId).getValue();
|
||||||
// 紧凑编码(init 侧 compactThemeToJson 只认紧凑形态),脏值宽松降 null 不抛
|
|
||||||
String hit = schSettingsService.readThemeWithFallback(schId).getValue();
|
|
||||||
if (hit != null) {
|
|
||||||
try {
|
|
||||||
theme = ThemeCodec.encodeThemeState(hit);
|
|
||||||
} catch (DException e) {
|
|
||||||
theme = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (avatar == null) {
|
if (avatar == null) {
|
||||||
avatar = schSettingsService.readAvatarWithFallback(schId).getValue();
|
avatar = schSettingsService.readAvatarWithFallback(schId).getValue();
|
||||||
}
|
}
|
||||||
|
out.put("logoText", schSettingsService.readLogoTextWithFallback(schId).getValue());
|
||||||
out.put("logoUrl", resolveLogoUrl(schId));
|
out.put("logoUrl", resolveLogoUrl(schId));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 学校/平台段降级:theme/avatar/logoUrl 保持已得值或 null
|
// 学校/平台段降级:theme/avatar/logoText/logoUrl 保持已得值或 null
|
||||||
}
|
}
|
||||||
out.put("theme", theme);
|
// theme 出 JSON 对象(2026-10-09 契约);值已过 decodeThemeState 校验,解析失败按 null 降级
|
||||||
|
out.put("theme", theme == null ? null : ThemeCodec.toJsonNode(theme));
|
||||||
out.put("avatar", avatar);
|
out.put("avatar", avatar);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -155,11 +150,8 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
|||||||
// 跟随学校默认:置 NULL(列语义见实体注释)
|
// 跟随学校默认:置 NULL(列语义见实体注释)
|
||||||
ua.setThemeState(null);
|
ua.setThemeState(null);
|
||||||
} else {
|
} else {
|
||||||
String compact = themeState.trim();
|
// 写侧从严:只收 JSON、产规范化 JSON 串(紧凑入参抛 DException,2026-10-09 JSON 化契约)
|
||||||
if (ThemeCodec.decodeThemeState(compact) == null) {
|
ua.setThemeState(ThemeCodec.encodeThemeState(themeState));
|
||||||
throw new DException("主题档不合法: " + themeState);
|
|
||||||
}
|
|
||||||
ua.setThemeState(compact);
|
|
||||||
}
|
}
|
||||||
ua.setUDate(new Date());
|
ua.setUDate(new Date());
|
||||||
userAppearanceRepository.save(ua);
|
userAppearanceRepository.save(ua);
|
||||||
@@ -190,11 +182,9 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 紧凑编码校验读(非法/脏值/旧格式 → null 视同未配置;合法返回原紧凑串) */
|
/** 主题档校验读(JSON 校验→规范化;旧紧凑四段兼容转 JSON;脏值 → null 视同未配置) */
|
||||||
private String validCompactTheme(String raw) {
|
private String validThemeState(String raw) {
|
||||||
if (!StringUtils.hasText(raw)) return null;
|
return StringUtils.hasText(raw) ? ThemeCodec.decodeThemeState(raw) : null;
|
||||||
String compact = raw.trim();
|
|
||||||
return ThemeCodec.decodeThemeState(compact) == null ? null : compact;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String validAvatarStyle(String raw) {
|
private String validAvatarStyle(String raw) {
|
||||||
|
|||||||
@@ -96,6 +96,25 @@ public class SchSettingsServiceImpl implements ISchSettingsService {
|
|||||||
ISchSettingsService.PLATFORM_SCH_ID);
|
ISchSettingsService.PLATFORM_SCH_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ISchSettingsService.LayerValue readLogoTextWithFallback(Integer schId) {
|
||||||
|
String text = readLogoTextAt(schId);
|
||||||
|
if (text != null || schId == null || schId == ISchSettingsService.PLATFORM_SCH_ID) {
|
||||||
|
return new ISchSettingsService.LayerValue(text, schId);
|
||||||
|
}
|
||||||
|
return new ISchSettingsService.LayerValue(readLogoTextAt(ISchSettingsService.PLATFORM_SCH_ID),
|
||||||
|
ISchSettingsService.PLATFORM_SCH_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 原样读某层 LOGO 文字(查无行/空白 → null;trim 后存储口径) */
|
||||||
|
private String readLogoTextAt(Integer schId) {
|
||||||
|
return schSettingsRepository.findFirstBySchId(schId)
|
||||||
|
.map(SchSettings::getLogoText)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
/** 原样读某层主题档并解码(查无行/旧格式/脏值 → null) */
|
/** 原样读某层主题档并解码(查无行/旧格式/脏值 → null) */
|
||||||
private String decodeTheme(Integer schId) {
|
private String decodeTheme(Integer schId) {
|
||||||
return schSettingsRepository.findFirstBySchId(schId)
|
return schSettingsRepository.findFirstBySchId(schId)
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
package com.yunhai.userhome.utils;
|
package com.yunhai.userhome.utils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.yunhai.platform.utils.exception.DException;
|
import com.yunhai.platform.utils.exception.DException;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题档/头像样式的编解码与校验(原 SchSettingsController 私有实现下沉,2026-10-08 外观偏好轮
|
* 主题档/头像样式的编解码与校验(原 SchSettingsController 私有实现下沉,2026-10-08 外观偏好轮;
|
||||||
* 供 AppearanceController 共享;行为与原私有版本逐字节一致,勿改语义)。
|
* 2026-10-09 存储与传输全面 JSON 化)。
|
||||||
* <p>紧凑编码形态:{@code theme|sideDeep01|gradOn01|sideFollow01}(如 {@code teal|0|1|1});
|
* <p>存储形态:{@code {"theme":"teal","sideDeep":false,"gradOn":false,"sideFollow":false}}
|
||||||
* 读侧宽松(脏值一律 null 视同未配置),写侧从严(非法抛 DException)。</p>
|
* (固定字段序的规范化 JSON 串;sch_settings.theme_name 与 user_appearance.theme_state 同形态)。
|
||||||
|
* 历史紧凑编码 {@code theme|sideDeep01|gradOn01|sideFollow01} 仅在【读侧】兼容——解码时自动转 JSON,
|
||||||
|
* 写侧一律产出 JSON(存量行靠迁移 UPDATE 或读时懒迁移消化)。
|
||||||
|
* 读侧宽松(脏值/不可解析一律 null 视同未配置),写侧从严(非法抛 DException)。</p>
|
||||||
*/
|
*/
|
||||||
public final class ThemeCodec {
|
public final class ThemeCodec {
|
||||||
|
|
||||||
@@ -17,39 +22,74 @@ public final class ThemeCodec {
|
|||||||
/** 合法头像样式(与前端 avatar.ts 的 AvatarStyle 一致;写入侧从严,对齐 encodeThemeState 做法) */
|
/** 合法头像样式(与前端 avatar.ts 的 AvatarStyle 一致;写入侧从严,对齐 encodeThemeState 做法) */
|
||||||
public static final String[] AVATAR_STYLES = {"text", "silhouette"};
|
public static final String[] AVATAR_STYLES = {"text", "silhouette"};
|
||||||
|
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
private ThemeCodec() {
|
private ThemeCodec() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 紧凑编码 → ThemeState JSON;非本编码形态(旧档名/脏值/字段非法)一律 null(视同未配置) */
|
/** 解析并校验 ThemeState JSON(四字段齐全、档名合法),合法返回【规范化 JSON 串】;非法 null */
|
||||||
public static String decodeThemeState(String encoded) {
|
private static String normalizeJson(String themeStateJson) {
|
||||||
if (!StringUtils.hasText(encoded)) return null;
|
try {
|
||||||
String[] parts = encoded.trim().split("\\|");
|
JsonNode n = MAPPER.readTree(themeStateJson);
|
||||||
if (parts.length != 4) return null;
|
if (n == null || !n.isObject() || !n.hasNonNull("theme") || !n.has("sideDeep")
|
||||||
if (!isThemeKey(parts[0])) return null;
|
|| !n.has("gradOn") || !n.has("sideFollow")) {
|
||||||
for (int i = 1; i < 4; i++) {
|
return null;
|
||||||
if (parts[i].length() != 1 || (parts[i].charAt(0) != '0' && parts[i].charAt(0) != '1')) return null;
|
}
|
||||||
|
String theme = n.get("theme").asText();
|
||||||
|
if (!isThemeKey(theme)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return "{\"theme\":\"" + theme + "\",\"sideDeep\":" + n.get("sideDeep").asBoolean()
|
||||||
|
+ ",\"gradOn\":" + n.get("gradOn").asBoolean()
|
||||||
|
+ ",\"sideFollow\":" + n.get("sideFollow").asBoolean() + "}";
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return "{\"theme\":\"" + parts[0] + "\",\"sideDeep\":" + parts[1]
|
|
||||||
+ ",\"gradOn\":" + parts[2] + ",\"sideFollow\":" + parts[3] + "}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ThemeState JSON 校验(四字段齐全、档名合法)+ → 紧凑编码;非法抛业务异常(写入侧从严) */
|
/** 旧紧凑编码 theme|01|01|01 → JSON;非本形态返回 null */
|
||||||
|
private static String fromCompact(String compact) {
|
||||||
|
String[] p = compact.trim().split("\\|");
|
||||||
|
if (p.length != 4 || !isThemeKey(p[0])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (int i = 1; i < 4; i++) {
|
||||||
|
if (p[i].length() != 1 || (p[i].charAt(0) != '0' && p[i].charAt(0) != '1')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "{\"theme\":\"" + p[0] + "\",\"sideDeep\":" + ("1".equals(p[1]))
|
||||||
|
+ ",\"gradOn\":" + ("1".equals(p[2]))
|
||||||
|
+ ",\"sideFollow\":" + ("1".equals(p[3])) + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读侧统一入口:JSON 校验 → 规范化 JSON;旧紧凑四段 → 转 JSON;其余(旧档名/脏值)null(视同未配置)。
|
||||||
|
* 返回值形态:ThemeState JSON 串(与 localStorage yh-theme 键形态一致)。
|
||||||
|
*/
|
||||||
|
public static String decodeThemeState(String raw) {
|
||||||
|
if (!StringUtils.hasText(raw)) return null;
|
||||||
|
String s = raw.trim();
|
||||||
|
return s.startsWith("{") ? normalizeJson(s) : fromCompact(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ThemeState JSON 校验(四字段齐全、档名合法)+ 规范化(固定字段序);非法抛业务异常(写入侧从严) */
|
||||||
public static String encodeThemeState(String themeStateJson) {
|
public static String encodeThemeState(String themeStateJson) {
|
||||||
if (!StringUtils.hasText(themeStateJson)) throw new DException("主题档不能为空");
|
if (!StringUtils.hasText(themeStateJson)) throw new DException("主题档不能为空");
|
||||||
com.fasterxml.jackson.databind.JsonNode n;
|
String normalized = normalizeJson(themeStateJson.trim());
|
||||||
|
if (normalized == null) {
|
||||||
|
throw new DException("主题档格式不合法");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ThemeState JSON 串 → 节点(oapi 出参对象形态用);入串须先过 decodeThemeState 校验 */
|
||||||
|
public static JsonNode toJsonNode(String themeStateJson) {
|
||||||
try {
|
try {
|
||||||
n = new com.fasterxml.jackson.databind.ObjectMapper().readTree(themeStateJson);
|
return MAPPER.readTree(themeStateJson);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new DException("主题档格式不合法");
|
return null;
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isThemeKey(String theme) {
|
public static boolean isThemeKey(String theme) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
@@ -76,19 +77,20 @@ class AppearanceServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("聚合:个人段+学校段+Logo 全存在 → 五字段齐全(紧凑编码原样、Logo 拼 FILE_WEB_PATH 绝对 URL)")
|
@DisplayName("聚合:个人段+学校段+Logo 全存在 → 五字段齐全(紧凑旧值读侧转 JSON、Logo 拼 FILE_WEB_PATH 绝对 URL)")
|
||||||
void aggregateFull() {
|
void aggregateFull() {
|
||||||
UserAppearance ua = new UserAppearance();
|
UserAppearance ua = new UserAppearance();
|
||||||
ua.setThemeState("ocean|1|0|1");
|
ua.setThemeState("ocean|1|0|1"); // 旧紧凑值(读侧兼容自动转 JSON)
|
||||||
ua.setAvatarStyle("silhouette");
|
ua.setAvatarStyle("silhouette");
|
||||||
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.of(ua));
|
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.of(ua));
|
||||||
when(schSettingsService.findBySchIdRaw(3)).thenReturn(Optional.of(schSettings("ink|0|1|1", "text")));
|
when(schSettingsService.findBySchIdRaw(3)).thenReturn(Optional.of(
|
||||||
|
schSettings("{\"theme\":\"ink\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":true}", "text")));
|
||||||
when(logoRepository.findBySchIdAndType(3, 1)).thenReturn(Collections.singletonList(logo("/upload/logo/sch.png")));
|
when(logoRepository.findBySchIdAndType(3, 1)).thenReturn(Collections.singletonList(logo("/upload/logo/sch.png")));
|
||||||
when(logoRepository.findBySchIdAndType(0, 0)).thenReturn(Collections.singletonList(logo("/upload/logo/sys.png")));
|
when(logoRepository.findBySchIdAndType(0, 0)).thenReturn(Collections.singletonList(logo("/upload/logo/sys.png")));
|
||||||
|
|
||||||
Map<String, Object> out = service.getAppearance(user);
|
Map<String, Object> out = service.getAppearance(user);
|
||||||
assertEquals("ocean|1|0|1", out.get("myTheme"));
|
assertEquals("{\"theme\":\"ocean\",\"sideDeep\":true,\"gradOn\":false,\"sideFollow\":true}", out.get("myTheme"));
|
||||||
assertEquals("ink|0|1|1", out.get("schTheme"));
|
assertEquals("{\"theme\":\"ink\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":true}", out.get("schTheme"));
|
||||||
assertEquals("silhouette", out.get("avatarMy"));
|
assertEquals("silhouette", out.get("avatarMy"));
|
||||||
assertEquals("text", out.get("avatarSch"));
|
assertEquals("text", out.get("avatarSch"));
|
||||||
assertEquals("http://base.yunhaixingtong.com/userhome/upload//logo/sch.png", out.get("logoUrl"));
|
assertEquals("http://base.yunhaixingtong.com/userhome/upload//logo/sch.png", out.get("logoUrl"));
|
||||||
@@ -121,7 +123,8 @@ class AppearanceServiceTest {
|
|||||||
Map<String, Object> out = service.getAppearance(user);
|
Map<String, Object> out = service.getAppearance(user);
|
||||||
assertNull(out.get("myTheme"));
|
assertNull(out.get("myTheme"));
|
||||||
assertNull(out.get("avatarMy"));
|
assertNull(out.get("avatarMy"));
|
||||||
assertEquals("teal|0|1|1", out.get("schTheme"));
|
assertEquals("{\"theme\":\"teal\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":true}", out.get("schTheme"),
|
||||||
|
"学校层紧凑旧值读侧转 JSON");
|
||||||
assertEquals("text", out.get("avatarSch"));
|
assertEquals("text", out.get("avatarSch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +138,8 @@ class AppearanceServiceTest {
|
|||||||
when(schSettingsService.resolveThemeSchId(any(), any())).thenThrow(new RuntimeException("userbase down"));
|
when(schSettingsService.resolveThemeSchId(any(), any())).thenThrow(new RuntimeException("userbase down"));
|
||||||
|
|
||||||
Map<String, Object> out = service.getAppearance(user);
|
Map<String, Object> out = service.getAppearance(user);
|
||||||
assertEquals("ocean|1|0|1", out.get("myTheme"));
|
assertEquals("{\"theme\":\"ocean\",\"sideDeep\":true,\"gradOn\":false,\"sideFollow\":true}", out.get("myTheme"),
|
||||||
|
"存量紧凑值读侧兼容转 JSON");
|
||||||
assertEquals("text", out.get("avatarMy"));
|
assertEquals("text", out.get("avatarMy"));
|
||||||
assertNull(out.get("schTheme"));
|
assertNull(out.get("schTheme"));
|
||||||
assertNull(out.get("avatarSch"));
|
assertNull(out.get("avatarSch"));
|
||||||
@@ -193,17 +197,19 @@ class AppearanceServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("saveMyTheme:非法紧凑编码抛 DException;合法值 trim 后存原样")
|
@DisplayName("saveMyTheme:非法值抛 DException(紧凑串不再可写/坏 JSON/缺字段/非法档名);合法 JSON trim 后规范化存")
|
||||||
void saveMyThemeValidation() {
|
void saveMyThemeValidation() {
|
||||||
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.empty());
|
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.empty());
|
||||||
assertThrows(DException.class, () -> service.saveMyTheme(10133, "red|0|1|1"));
|
assertThrows(DException.class, () -> service.saveMyTheme(10133, "red|0|1|1"));
|
||||||
assertThrows(DException.class, () -> service.saveMyTheme(10133, "{\"theme\":\"teal\"}"));
|
|
||||||
assertThrows(DException.class, () -> service.saveMyTheme(10133, "teal|2|1|1"));
|
assertThrows(DException.class, () -> service.saveMyTheme(10133, "teal|2|1|1"));
|
||||||
|
assertThrows(DException.class, () -> service.saveMyTheme(10133, "teal|0|1|1"), "紧凑串不再可写");
|
||||||
|
assertThrows(DException.class, () -> service.saveMyTheme(10133, "{\"theme\":\"teal\"}"));
|
||||||
|
|
||||||
service.saveMyTheme(10133, " teal|0|1|1 ");
|
service.saveMyTheme(10133, " {\"theme\":\"teal\",\"sideDeep\":0,\"gradOn\":true,\"sideFollow\":true} ");
|
||||||
ArgumentCaptor<UserAppearance> captor = ArgumentCaptor.forClass(UserAppearance.class);
|
ArgumentCaptor<UserAppearance> captor = ArgumentCaptor.forClass(UserAppearance.class);
|
||||||
verify(userAppearanceRepository).save(captor.capture());
|
verify(userAppearanceRepository).save(captor.capture());
|
||||||
assertEquals("teal|0|1|1", captor.getValue().getThemeState());
|
assertEquals("{\"theme\":\"teal\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":true}",
|
||||||
|
captor.getValue().getThemeState(), "trim + 0/1 布尔归一为规范化 JSON");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -236,41 +242,51 @@ class AppearanceServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("生效值:个人自选优先——theme/avatar 取 user_appearance,级联读不出站")
|
@DisplayName("生效值:个人自选优先(旧紧凑值兼容转 JSON)——theme/avatar 取 user_appearance,级联读不出站")
|
||||||
void effective_personalOverridesCascade() {
|
void effective_personalOverridesCascade() {
|
||||||
stubCurrentRole(3);
|
stubCurrentRole(3);
|
||||||
UserAppearance ua = new UserAppearance();
|
UserAppearance ua = new UserAppearance();
|
||||||
ua.setThemeState("ocean|1|0|1");
|
ua.setThemeState("ocean|1|0|1"); // 旧紧凑值:读侧兼容
|
||||||
ua.setAvatarStyle("silhouette");
|
ua.setAvatarStyle("silhouette");
|
||||||
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.of(ua));
|
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.of(ua));
|
||||||
when(logoRepository.findBySchIdAndType(any(), any())).thenReturn(Collections.emptyList());
|
when(logoRepository.findBySchIdAndType(any(), any())).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
Map<String, Object> out = service.getEffectiveAppearance(10133);
|
Map<String, Object> out = service.getEffectiveAppearance(10133);
|
||||||
assertEquals("ocean|1|0|1", out.get("theme"));
|
com.fasterxml.jackson.databind.JsonNode theme = (com.fasterxml.jackson.databind.JsonNode) out.get("theme");
|
||||||
|
assertEquals("ocean", theme.get("theme").asText());
|
||||||
|
assertTrue(theme.get("sideDeep").asBoolean());
|
||||||
|
assertFalse(theme.get("gradOn").asBoolean());
|
||||||
assertEquals("silhouette", out.get("avatar"));
|
assertEquals("silhouette", out.get("avatar"));
|
||||||
|
assertNull(out.get("logoText"));
|
||||||
assertNull(out.get("logoUrl"));
|
assertNull(out.get("logoUrl"));
|
||||||
verify(schSettingsService, never()).readThemeWithFallback(any());
|
verify(schSettingsService, never()).readThemeWithFallback(any());
|
||||||
verify(schSettingsService, never()).readAvatarWithFallback(any());
|
verify(schSettingsService, never()).readAvatarWithFallback(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("生效值:个人未设 → 学校/平台级联(缺陷 C 修复验证:合成链走 readThemeWithFallback),层间编码统一紧凑形态")
|
@DisplayName("生效值:个人未设 → 学校/平台级联(theme 出 JSON 对象)+ logoText 本校→平台级联")
|
||||||
void effective_fallsToCascade() {
|
void effective_fallsToCascade() {
|
||||||
stubCurrentRole(3);
|
stubCurrentRole(3);
|
||||||
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.empty());
|
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.empty());
|
||||||
// 级联命中值是 ThemeState JSON(schTheme 端点口径)——合成层须转回紧凑编码
|
|
||||||
when(schSettingsService.readThemeWithFallback(3))
|
when(schSettingsService.readThemeWithFallback(3))
|
||||||
.thenReturn(new ISchSettingsService.LayerValue("{\"theme\":\"ink\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":false}", 0));
|
.thenReturn(new ISchSettingsService.LayerValue("{\"theme\":\"ink\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":false}", 0));
|
||||||
when(schSettingsService.readAvatarWithFallback(3))
|
when(schSettingsService.readAvatarWithFallback(3))
|
||||||
.thenReturn(new ISchSettingsService.LayerValue("text", 3));
|
.thenReturn(new ISchSettingsService.LayerValue("text", 3));
|
||||||
|
when(schSettingsService.readLogoTextWithFallback(3))
|
||||||
|
.thenReturn(new ISchSettingsService.LayerValue("云海教育平台", 0));
|
||||||
when(logoRepository.findBySchIdAndType(any(), any())).thenReturn(Collections.emptyList());
|
when(logoRepository.findBySchIdAndType(any(), any())).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
Map<String, Object> out = service.getEffectiveAppearance(10133);
|
Map<String, Object> out = service.getEffectiveAppearance(10133);
|
||||||
assertEquals("ink|0|1|0", out.get("theme"), "级联 JSON 转紧凑编码,init 侧才可收敛");
|
com.fasterxml.jackson.databind.JsonNode theme = (com.fasterxml.jackson.databind.JsonNode) out.get("theme");
|
||||||
|
assertEquals("ink", theme.get("theme").asText(), "本校空回落平台档(hitSchId=0)");
|
||||||
|
assertFalse(theme.get("sideDeep").asBoolean());
|
||||||
|
assertTrue(theme.get("gradOn").asBoolean());
|
||||||
assertEquals("text", out.get("avatar"), "本校已配直接生效(hitSchId=本校)");
|
assertEquals("text", out.get("avatar"), "本校已配直接生效(hitSchId=本校)");
|
||||||
// 出参只三字段,不外露 my/sch 拆分
|
assertEquals("云海教育平台", out.get("logoText"), "logoText 本校空→平台层");
|
||||||
assertTrue(out.containsKey("theme") && out.containsKey("avatar") && out.containsKey("logoUrl"));
|
// 出参只四字段,不外露 my/sch 拆分
|
||||||
assertEquals(3, out.size());
|
assertEquals(4, out.size());
|
||||||
|
assertTrue(out.containsKey("theme") && out.containsKey("avatar")
|
||||||
|
&& out.containsKey("logoText") && out.containsKey("logoUrl"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -285,12 +301,16 @@ class AppearanceServiceTest {
|
|||||||
.thenReturn(new ISchSettingsService.LayerValue(null, 0));
|
.thenReturn(new ISchSettingsService.LayerValue(null, 0));
|
||||||
when(schSettingsService.readAvatarWithFallback(0))
|
when(schSettingsService.readAvatarWithFallback(0))
|
||||||
.thenReturn(new ISchSettingsService.LayerValue(null, 0));
|
.thenReturn(new ISchSettingsService.LayerValue(null, 0));
|
||||||
|
when(schSettingsService.readLogoTextWithFallback(0))
|
||||||
|
.thenReturn(new ISchSettingsService.LayerValue(null, 0));
|
||||||
when(logoRepository.findBySchIdAndType(any(), any())).thenReturn(Collections.emptyList());
|
when(logoRepository.findBySchIdAndType(any(), any())).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
Map<String, Object> out = service.getEffectiveAppearance(10133);
|
Map<String, Object> out = service.getEffectiveAppearance(10133);
|
||||||
assertNull(out.get("theme"), "脏值落级联且两级皆无 → null");
|
assertNull(out.get("theme"), "脏值落级联且两级皆无 → null");
|
||||||
assertNull(out.get("avatar"));
|
assertNull(out.get("avatar"));
|
||||||
|
assertNull(out.get("logoText"));
|
||||||
assertNull(out.get("logoUrl"));
|
assertNull(out.get("logoUrl"));
|
||||||
|
assertEquals(4, out.size());
|
||||||
verify(schSettingsService).readThemeWithFallback(0);
|
verify(schSettingsService).readThemeWithFallback(0);
|
||||||
verify(schSettingsService).readAvatarWithFallback(0);
|
verify(schSettingsService).readAvatarWithFallback(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,26 +12,39 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ThemeCodec 单测(外观偏好轮 2026-10-08 下沉共享后的行为冻结):
|
* ThemeCodec 单测(2026-10-08 下沉共享;2026-10-09 JSON 化行为冻结):
|
||||||
|
* 存储形态 = 规范化 ThemeState JSON 串(固定字段序);旧紧凑四段仅读侧兼容自动转 JSON;
|
||||||
* decode 宽松(脏值一律 null 视同未配置)、encode 从严(非法抛 DException)、往返一致。
|
* decode 宽松(脏值一律 null 视同未配置)、encode 从严(非法抛 DException)、往返一致。
|
||||||
*/
|
*/
|
||||||
@DisplayName("ThemeCodec - 紧凑编码编解码与校验")
|
@DisplayName("ThemeCodec - JSON 编解码与紧凑兼容")
|
||||||
class ThemeCodecTest {
|
class ThemeCodecTest {
|
||||||
|
|
||||||
|
private static final String TEAL_JSON =
|
||||||
|
"{\"theme\":\"teal\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":true}";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("decode:合法紧凑编码 → ThemeState JSON")
|
@DisplayName("decode:JSON 输入 → 规范化 JSON(字段序固定、布尔形态归一)")
|
||||||
void decodeValid() {
|
void decodeJson() {
|
||||||
assertEquals("{\"theme\":\"teal\",\"sideDeep\":0,\"gradOn\":1,\"sideFollow\":1}",
|
assertEquals(TEAL_JSON, ThemeCodec.decodeThemeState(TEAL_JSON));
|
||||||
ThemeCodec.decodeThemeState("teal|0|1|1"));
|
// 字段乱序/紧凑布尔(0/1)也归一为 false/true 规范形态
|
||||||
assertEquals("{\"theme\":\"ocean\",\"sideDeep\":1,\"gradOn\":0,\"sideFollow\":0}",
|
assertEquals(TEAL_JSON, ThemeCodec.decodeThemeState(
|
||||||
ThemeCodec.decodeThemeState("ocean|1|0|0"));
|
"{\"gradOn\":true,\"theme\":\"teal\",\"sideFollow\":true,\"sideDeep\":0}"));
|
||||||
|
assertNull(ThemeCodec.decodeThemeState("{\"theme\":\"teal\"}")); // 字段不齐
|
||||||
|
assertNull(ThemeCodec.decodeThemeState("{\"theme\":\"red\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":true}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("decode:空白容忍、脏值一律 null(旧档名/段数不对/开关非法/非 0 1)")
|
@DisplayName("decode:旧紧凑四段兼容 → 自动转 JSON(迁移期双读)")
|
||||||
|
void decodeLegacyCompact() {
|
||||||
|
assertEquals(TEAL_JSON, ThemeCodec.decodeThemeState("teal|0|1|1"));
|
||||||
|
assertEquals("{\"theme\":\"ocean\",\"sideDeep\":true,\"gradOn\":false,\"sideFollow\":false}",
|
||||||
|
ThemeCodec.decodeThemeState("ocean|1|0|0"));
|
||||||
|
assertEquals(TEAL_JSON, ThemeCodec.decodeThemeState(" teal|0|1|1 "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("decode:脏值一律 null(旧档名/段数不对/开关非法/非 0 1/坏 JSON)")
|
||||||
void decodeDirty() {
|
void decodeDirty() {
|
||||||
assertEquals("{\"theme\":\"teal\",\"sideDeep\":0,\"gradOn\":1,\"sideFollow\":1}",
|
|
||||||
ThemeCodec.decodeThemeState(" teal|0|1|1 "));
|
|
||||||
assertNull(ThemeCodec.decodeThemeState(null));
|
assertNull(ThemeCodec.decodeThemeState(null));
|
||||||
assertNull(ThemeCodec.decodeThemeState(""));
|
assertNull(ThemeCodec.decodeThemeState(""));
|
||||||
assertNull(ThemeCodec.decodeThemeState("default-theme")); // findBySchId 兜底遗留值
|
assertNull(ThemeCodec.decodeThemeState("default-theme")); // findBySchId 兜底遗留值
|
||||||
@@ -40,35 +53,48 @@ class ThemeCodecTest {
|
|||||||
assertNull(ThemeCodec.decodeThemeState("teal|0|1|1|1")); // 段数超出
|
assertNull(ThemeCodec.decodeThemeState("teal|0|1|1|1")); // 段数超出
|
||||||
assertNull(ThemeCodec.decodeThemeState("teal|2|1|1")); // 开关非 0/1
|
assertNull(ThemeCodec.decodeThemeState("teal|2|1|1")); // 开关非 0/1
|
||||||
assertNull(ThemeCodec.decodeThemeState("teal|true|1|1")); // 开关非单字符
|
assertNull(ThemeCodec.decodeThemeState("teal|true|1|1")); // 开关非单字符
|
||||||
|
assertNull(ThemeCodec.decodeThemeState("{not json")); // 坏 JSON
|
||||||
|
assertNull(ThemeCodec.decodeThemeState("[1,2]")); // 非对象
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("encode:ThemeState JSON → 紧凑编码;非法从严抛 DException")
|
@DisplayName("encode:校验从严 + 产出规范化 JSON(不再产紧凑);紧凑入参抛 DException")
|
||||||
void encode() {
|
void encode() {
|
||||||
assertEquals("teal|0|1|1", ThemeCodec.encodeThemeState(
|
assertEquals(TEAL_JSON, ThemeCodec.encodeThemeState(
|
||||||
"{\"theme\":\"teal\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":true}"));
|
"{\"theme\":\"teal\",\"sideDeep\":false,\"gradOn\":true,\"sideFollow\":true}"));
|
||||||
assertEquals("ocean|1|0|0", ThemeCodec.encodeThemeState(
|
// 0/1 布尔形态归一
|
||||||
"{\"theme\":\"ocean\",\"sideDeep\":1,\"gradOn\":0,\"sideFollow\":0}"));
|
assertEquals(TEAL_JSON, ThemeCodec.encodeThemeState(
|
||||||
|
"{\"theme\":\"teal\",\"sideDeep\":0,\"gradOn\":1,\"sideFollow\":1}"));
|
||||||
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState(null));
|
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState(null));
|
||||||
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState(""));
|
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState(""));
|
||||||
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState("teal|0|1|1"));
|
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState("teal|0|1|1")); // 紧凑不再可写
|
||||||
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState("{\"theme\":\"red\"}"));
|
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState("{\"theme\":\"red\"}"));
|
||||||
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState(
|
assertThrows(DException.class, () -> ThemeCodec.encodeThemeState(
|
||||||
"{\"theme\":\"teal\",\"sideDeep\":false}"));
|
"{\"theme\":\"teal\",\"sideDeep\":false}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("encode→decode 往返一致(合法域内)")
|
@DisplayName("encode→decode 往返一致(合法域内,含紧凑读侧转 JSON 闭环)")
|
||||||
void roundTrip() {
|
void roundTrip() {
|
||||||
for (String theme : new String[]{"blue", "ocean", "ink", "violet", "teal"}) {
|
for (String theme : new String[]{"blue", "ocean", "ink", "violet", "teal"}) {
|
||||||
for (int d = 0; d <= 1; d++) for (int g = 0; g <= 1; g++) for (int f = 0; f <= 1; f++) {
|
for (int d = 0; d <= 1; d++) for (int g = 0; g <= 1; g++) for (int f = 0; f <= 1; f++) {
|
||||||
String compact = theme + "|" + d + "|" + g + "|" + f;
|
String compact = theme + "|" + d + "|" + g + "|" + f;
|
||||||
String json = ThemeCodec.decodeThemeState(compact);
|
String json = ThemeCodec.decodeThemeState(compact);
|
||||||
assertEquals(compact, ThemeCodec.encodeThemeState(json));
|
assertEquals(json, ThemeCodec.encodeThemeState(json), "JSON 往返自洽");
|
||||||
|
assertEquals(json, ThemeCodec.decodeThemeState(json), "encode 产物可再读");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("toJsonNode:JSON 串 → 节点(oapi 对象出参用);坏串 null")
|
||||||
|
void toJsonNode() {
|
||||||
|
assertEquals("teal", ThemeCodec.toJsonNode(TEAL_JSON).get("theme").asText());
|
||||||
|
assertTrue(ThemeCodec.toJsonNode(TEAL_JSON).get("gradOn").asBoolean());
|
||||||
|
assertNull(ThemeCodec.toJsonNode("{bad"));
|
||||||
|
assertNull(ThemeCodec.toJsonNode(null));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("isThemeKey / isAvatarStyle")
|
@DisplayName("isThemeKey / isAvatarStyle")
|
||||||
void validators() {
|
void validators() {
|
||||||
@@ -79,7 +105,6 @@ class ThemeCodecTest {
|
|||||||
assertTrue(ThemeCodec.isAvatarStyle("silhouette"));
|
assertTrue(ThemeCodec.isAvatarStyle("silhouette"));
|
||||||
assertFalse(ThemeCodec.isAvatarStyle("circle"));
|
assertFalse(ThemeCodec.isAvatarStyle("circle"));
|
||||||
assertFalse(ThemeCodec.isAvatarStyle(null));
|
assertFalse(ThemeCodec.isAvatarStyle(null));
|
||||||
// 防御:encode 输出必须可被 isThemeKey 接受(档名域一致)
|
|
||||||
assertNotEquals(0, ThemeCodec.THEME_KEYS.length);
|
assertNotEquals(0, ThemeCodec.THEME_KEYS.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user