feat(appearance): 开放接口 GET /oapi/appearance(生效值合成)+ 级联下沉 service(oapi 代理轮)
build / build-push (push) Successful in 3m18s
build / build-push (push) Successful in 3m18s
- 新增 controller/oapi/OapiAppearanceController:userId 入参,出参 {theme, avatar, logoUrl} 当前用户生效值
(个人→学校→平台级联合成,层级知识不外露);出参裸 payload,Map 禁 String
- readThemeWithFallback/readAvatarWithFallback 从 SchSettingsController 下沉 ISchSettingsService(行为等价),
供 schTheme 读端点与 oapi 合成共享——顺带修复「appearance 无学校→平台级联」审计缺陷 C
- OpenApiAuthEnforceConfiguration:/oapi/* 强制应用签名(无签名 401,对齐 UserMsg/UserBase 同名配置)
- oapi schId 解析走本地 user_current_role(不引 identity 跨仓);超管临时切校角色场景与 /api 版有差,登记偏差
- 单测:生效值合成 3 例(个人优先不触级联/级联命中/脏值+无角色落平台层),60/60 绿
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package com.yunhai.userhome.config;
|
||||
|
||||
import com.yunhai.platform.appauth.AppAuthFilter;
|
||||
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 javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 强制开放接口必须带应用签名(对齐 UserMsg/UserBase 同名配置)。<br>
|
||||
* yh-app-auth 的 {@link AppAuthFilter} 设计为「无签名头则放行」(以兼容浏览器 CAS 流),
|
||||
* 但 UserHome 的 /oapi/**(外观偏好开放接口)绝不允许匿名访问。本过滤器在 AppAuthFilter 之后运行,
|
||||
* 校验其写入的请求属性:已验签(属性存在)则放行,否则 401。<br>
|
||||
* 顺序须晚于 AppAuthFilter(HIGHEST_PRECEDENCE+10),故置为 +11。
|
||||
*/
|
||||
@Configuration
|
||||
public class OpenApiAuthEnforceConfiguration {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<RequireAppSignFilter> requireAppSignFilter() {
|
||||
FilterRegistrationBean<RequireAppSignFilter> bean = new FilterRegistrationBean<>();
|
||||
bean.setFilter(new RequireAppSignFilter());
|
||||
bean.addUrlPatterns("/oapi/*");
|
||||
bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 11);
|
||||
return bean;
|
||||
}
|
||||
|
||||
public static class RequireAppSignFilter implements Filter {
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
HttpServletResponse res = (HttpServletResponse) response;
|
||||
if (req.getAttribute(AppAuthFilter.APP_AUTH_ATTR) == null) {
|
||||
res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "missing app signature");
|
||||
return;
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,9 +37,9 @@ import java.util.Optional;
|
||||
* <p>外观偏好轮(2026-10-08):编解码下沉 {@link ThemeCodec}、schId 解析下沉
|
||||
* {@link ISchSettingsService#resolveThemeSchId}(行为等价重构,供 AppearanceController 共享)。</p>
|
||||
* <p>三级重构 S7(2026-10-08):{@code schTheme}/{@code schAvatarStyle} 读侧加"学校 → 平台默认(schId=0)"级联
|
||||
* ({@link #readThemeWithFallback}/{@link #readAvatarWithFallback})——本校未配置或脏值时回落平台档,
|
||||
* 平台也无配置才返回 null。写侧与接口签名、响应体结构均未变;{@code getSchSettings}/{@code findBySchId}
|
||||
* 的"合成默认对象"兜底语义也未变。</p>
|
||||
* (实现已随 oapi 代理轮下沉 {@link ISchSettingsService#readThemeWithFallback}/{@link ISchSettingsService#readAvatarWithFallback})
|
||||
* ——本校未配置或脏值时回落平台档,平台也无配置才返回 null。写侧与接口签名、响应体结构均未变;
|
||||
* {@code getSchSettings}/{@code findBySchId} 的"合成默认对象"兜底语义也未变。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH)
|
||||
@@ -62,69 +62,16 @@ public class SchSettingsController {
|
||||
}
|
||||
}
|
||||
|
||||
/** 平台默认层在 sch_settings 中的固定 schId(与 UsedAppController/壁纸/LOGO 的 0 层同口径) */
|
||||
private static final int PLATFORM_SCH_ID = 0;
|
||||
|
||||
/**
|
||||
* 主题档读侧级联(设置中心三级重构 S7,2026-10-08):本校行未配置/旧格式/脏值 → 再查平台默认层(schId=0),
|
||||
* 命中即返回平台档;两级都无 → null。schId 自身即平台层时不二次回落(避免自查两次)。
|
||||
* <p>写侧不受影响:超管仍写 schId=0、校管写本校。</p>
|
||||
*/
|
||||
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)。
|
||||
* <p>响应体的 {@code schId} 字段因此采用"实际命中层"语义(方案 §6.S7 允许二选一)——
|
||||
* 设置中心的"本校默认"层要据此区分"本校已配置"与"本校未配置、正在回落平台默认"。
|
||||
* 现有前端只消费 themeState/avatarStyle,不依赖该字段。</p>
|
||||
*/
|
||||
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
|
||||
* (前端"不写键、不报错")。 */
|
||||
* 未配置/脏值 → 回落平台默认层(级联见 {@link ISchSettingsService#readAvatarWithFallback});两级都无 →
|
||||
* avatarStyle=null(前端"不写键、不报错")。 */
|
||||
@GetMapping("schAvatarStyle")
|
||||
public Map<String, Object> schAvatarStyle(BoaUser authUserWithDept) {
|
||||
LayerValue hit = readAvatarWithFallback(schSettingsService.resolveThemeSchId(authUserWithDept, null));
|
||||
ISchSettingsService.LayerValue hit =
|
||||
schSettingsService.readAvatarWithFallback(schSettingsService.resolveThemeSchId(authUserWithDept, null));
|
||||
Map<String, Object> out = new HashMap<>();
|
||||
out.put("avatarStyle", hit.value);
|
||||
out.put("schId", hit.hitSchId);
|
||||
out.put("avatarStyle", hit.getValue());
|
||||
out.put("schId", hit.getHitSchId());
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -159,16 +106,18 @@ public class SchSettingsController {
|
||||
schSettingsService.save(schSettings);
|
||||
}
|
||||
|
||||
/** 学校默认主题档(已登录可读):{themeState: "<ThemeState JSON>" | null, schId};schId 为实际命中层(见 {@link LayerValue})。
|
||||
* 本校未配置/脏值 → 回落平台默认层(见 {@link #readThemeWithFallback});两级都无 → themeState=null
|
||||
/** 学校默认主题档(已登录可读):{themeState: "<ThemeState JSON>" | null, schId};schId 为实际命中层
|
||||
* (见 {@link ISchSettingsService.LayerValue})。本校未配置/脏值 → 回落平台默认层
|
||||
* (级联见 {@link ISchSettingsService#readThemeWithFallback});两级都无 → themeState=null
|
||||
* (前端"不写键、不报错")。
|
||||
*/
|
||||
@GetMapping("schTheme")
|
||||
public Map<String, Object> schTheme(BoaUser authUserWithDept) {
|
||||
LayerValue hit = readThemeWithFallback(schSettingsService.resolveThemeSchId(authUserWithDept, null));
|
||||
ISchSettingsService.LayerValue hit =
|
||||
schSettingsService.readThemeWithFallback(schSettingsService.resolveThemeSchId(authUserWithDept, null));
|
||||
Map<String, Object> out = new HashMap<>();
|
||||
out.put("themeState", hit.value);
|
||||
out.put("schId", hit.hitSchId);
|
||||
out.put("themeState", hit.getValue());
|
||||
out.put("schId", hit.getHitSchId());
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.yunhai.userhome.controller.oapi;
|
||||
|
||||
import com.yunhai.userhome.config.Config;
|
||||
import com.yunhai.userhome.service.IAppearanceService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 外观偏好开放接口(oapi 代理轮 2026-10-08):子应用后端经 open-gateway 签名直调(target=userhome),
|
||||
* 替代浏览器直连 /api/appearance + CAS iframe 引导。入站验签由 {@code OpenApiAuthEnforceConfiguration}
|
||||
* 强制(/oapi/* 无签名 401);appKey 准入归网关台账/限流管,本应用不维护 per-app 白名单。
|
||||
*
|
||||
* <p>出参 = 当前用户【生效值】{@code {theme, avatar, logoUrl}}(个人 → 学校 → 平台级联合成,
|
||||
* 层级知识留在 UserHome 内部不外露;null=未配置,消费端 init 侧清对应本地键回落默认观感)。
|
||||
* 返回裸 payload(平台 oapi 无信封包装),消费端整体透传给浏览器。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.OPEN_API_PATH)
|
||||
public class OapiAppearanceController {
|
||||
|
||||
@Autowired
|
||||
private IAppearanceService appearanceService;
|
||||
|
||||
/** 当前用户生效外观。userId 由调用方(子应用后端)从其登录态取,服务间签名保证调用方可信。 */
|
||||
@GetMapping("appearance")
|
||||
public Map<String, Object> appearance(@RequestParam("userId") Integer userId) {
|
||||
return appearanceService.getEffectiveAppearance(userId);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,13 @@ public interface IAppearanceService {
|
||||
*/
|
||||
Map<String, Object> getAppearance(BoaUser user);
|
||||
|
||||
/**
|
||||
* 当前用户生效外观(oapi 代理轮 2026-10-08):{theme, avatar, logoUrl}——主题/头像为合成后的
|
||||
* 【生效值】(个人 → 学校 → 平台级联,层级知识不外露);logoUrl 本校→平台择优。
|
||||
* 查不到/脏值/依赖故障的字段 null(init 侧清对应键回落默认观感)。
|
||||
*/
|
||||
Map<String, Object> getEffectiveAppearance(Integer userId);
|
||||
|
||||
/** 保存用户自选主题档(紧凑编码 theme|sideDeep|gradOn|sideFollow);themeState=null 表示跟随学校默认 */
|
||||
void saveMyTheme(Integer userId, String themeState);
|
||||
|
||||
|
||||
@@ -24,4 +24,38 @@ public interface ISchSettingsService {
|
||||
/** 主题/头像默认档的目标学校:超管=显式 schId 或 0(平台默认层);其余=当前角色学校(兜底主归属)。
|
||||
* 原为 SchSettingsController 私有方法,外观偏好轮(2026-10-08)下沉供 AppearanceController 共享。 */
|
||||
Integer resolveThemeSchId(BoaUser user, Integer explicitSchId);
|
||||
|
||||
/** 平台默认层在 sch_settings 中的固定 schId(与 UsedAppController/壁纸/LOGO 的 0 层同口径) */
|
||||
int PLATFORM_SCH_ID = 0;
|
||||
|
||||
/**
|
||||
* 主题档读侧级联(设置中心三级重构 S7,2026-10-08;oapi 代理轮下沉 service 供 schTheme 读端点与
|
||||
* /oapi/appearance 合成共享):本校行未配置/旧格式/脏值 → 再查平台默认层(schId=0),
|
||||
* 命中即返回平台档;两级都无 → value=null。schId 自身即平台层时不二次回落(避免自查两次)。
|
||||
*/
|
||||
LayerValue readThemeWithFallback(Integer schId);
|
||||
|
||||
/** 头像样式读侧级联:语义与 {@link #readThemeWithFallback} 同构(本校空/脏 → 平台层 → null)。 */
|
||||
LayerValue readAvatarWithFallback(Integer schId);
|
||||
|
||||
/** 级联读命中结果:{@code value}=命中的档位值(两级都无则 null),{@code hitSchId}=实际命中层的 schId
|
||||
* (回落发生时是 0,不是解析出的本校 schId)。设置中心读端点的响应体 schId 即此语义——
|
||||
* 前端据此区分「本校已配置」与「本校未配置、正在回落平台默认」。 */
|
||||
final class LayerValue {
|
||||
private final String value;
|
||||
private final Integer hitSchId;
|
||||
|
||||
public LayerValue(String value, Integer hitSchId) {
|
||||
this.value = value;
|
||||
this.hitSchId = hitSchId;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Integer getHitSchId() {
|
||||
return hitSchId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import com.yunhai.platform.utils.model.BoaUser;
|
||||
import com.yunhai.userhome.entity.Logo;
|
||||
import com.yunhai.userhome.entity.SchSettings;
|
||||
import com.yunhai.userhome.entity.UserAppearance;
|
||||
import com.yunhai.userhome.entity.UserCurrentRole;
|
||||
import com.yunhai.userhome.repository.LogoRepository;
|
||||
import com.yunhai.userhome.repository.UserAppearanceRepository;
|
||||
import com.yunhai.userhome.service.IAppearanceService;
|
||||
import com.yunhai.userhome.service.ISchSettingsService;
|
||||
import com.yunhai.userhome.service.IUserCurrentRoleService;
|
||||
import com.yunhai.userhome.utils.ThemeCodec;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -42,6 +44,9 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
||||
@Autowired
|
||||
private ISchSettingsService schSettingsService;
|
||||
|
||||
@Autowired
|
||||
private IUserCurrentRoleService userCurrentRoleService;
|
||||
|
||||
@Autowired
|
||||
private LogoRepository logoRepository;
|
||||
|
||||
@@ -83,6 +88,57 @@ public class AppearanceServiceImpl implements IAppearanceService {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户生效外观(oapi 代理轮 2026-10-08,{@code /oapi/appearance} 出参真源):
|
||||
* {@code {theme, avatar, logoUrl}}——主题/头像为【合成后的生效值】(个人自选 → 学校默认 → 平台默认(schId=0)
|
||||
* 级联,层级知识留在 UserHome 内部,对外不拆 my/sch);logoUrl 为本校 type=1 → 平台 type=0 择优。
|
||||
*
|
||||
* <p>schId 解析走本地 user_current_role(oapi 入参只有 userId,不引 identity 跨仓):roleId 当前角色学校,
|
||||
* 无角色/跨校态回落 0(平台层)。与 /api/appearance 的 resolveThemeSchId(超管强制 0)仅在「超管临时切入
|
||||
* 某校角色」场景不同——该场景按当前角色学校取默认反而更贴近其当下观感,属登记偏差而非回归。</p>
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> getEffectiveAppearance(Integer userId) {
|
||||
Map<String, Object> out = new HashMap<>();
|
||||
out.put("theme", null);
|
||||
out.put("avatar", null);
|
||||
out.put("logoUrl", null);
|
||||
if (userId == null) {
|
||||
return out;
|
||||
}
|
||||
Integer schId = userCurrentRoleService.findById(userId)
|
||||
.map(UserCurrentRole::getSchId)
|
||||
.filter(s -> s != null && s > 0)
|
||||
.orElse(ISchSettingsService.PLATFORM_SCH_ID);
|
||||
String myTheme = null;
|
||||
String myAvatar = null;
|
||||
try {
|
||||
Optional<UserAppearance> ua = userAppearanceRepository.findByUserId(userId);
|
||||
if (ua.isPresent()) {
|
||||
myTheme = validCompactTheme(ua.get().getThemeState());
|
||||
myAvatar = validAvatarStyle(ua.get().getAvatarStyle());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 个人段降级:myTheme/myAvatar 保持 null,不影响学校/平台段
|
||||
}
|
||||
String theme = myTheme;
|
||||
String avatar = myAvatar;
|
||||
try {
|
||||
if (theme == null) {
|
||||
theme = schSettingsService.readThemeWithFallback(schId).getValue();
|
||||
}
|
||||
if (avatar == null) {
|
||||
avatar = schSettingsService.readAvatarWithFallback(schId).getValue();
|
||||
}
|
||||
out.put("logoUrl", resolveLogoUrl(schId));
|
||||
} catch (Exception e) {
|
||||
// 学校/平台段降级:theme/avatar/logoUrl 保持已得值或 null
|
||||
}
|
||||
out.put("theme", theme);
|
||||
out.put("avatar", avatar);
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMyTheme(Integer userId, String themeState) {
|
||||
UserAppearance ua = loadOrCreate(userId);
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.yunhai.userhome.entity.UserCurrentRole;
|
||||
import com.yunhai.userhome.repository.SchSettingsRepository;
|
||||
import com.yunhai.userhome.service.ISchSettingsService;
|
||||
import com.yunhai.userhome.service.IUserCurrentRoleService;
|
||||
import com.yunhai.userhome.utils.ThemeCodec;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -74,4 +75,40 @@ public class SchSettingsServiceImpl implements ISchSettingsService {
|
||||
.filter(schId -> schId != null && schId > 0)
|
||||
.orElseGet(() -> resolveSchId(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ISchSettingsService.LayerValue readThemeWithFallback(Integer schId) {
|
||||
String themeState = decodeTheme(schId);
|
||||
if (themeState != null || schId == null || schId == ISchSettingsService.PLATFORM_SCH_ID) {
|
||||
return new ISchSettingsService.LayerValue(themeState, schId);
|
||||
}
|
||||
return new ISchSettingsService.LayerValue(decodeTheme(ISchSettingsService.PLATFORM_SCH_ID),
|
||||
ISchSettingsService.PLATFORM_SCH_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ISchSettingsService.LayerValue readAvatarWithFallback(Integer schId) {
|
||||
String avatarStyle = readAvatarAt(schId);
|
||||
if (avatarStyle != null || schId == null || schId == ISchSettingsService.PLATFORM_SCH_ID) {
|
||||
return new ISchSettingsService.LayerValue(avatarStyle, schId);
|
||||
}
|
||||
return new ISchSettingsService.LayerValue(readAvatarAt(ISchSettingsService.PLATFORM_SCH_ID),
|
||||
ISchSettingsService.PLATFORM_SCH_ID);
|
||||
}
|
||||
|
||||
/** 原样读某层主题档并解码(查无行/旧格式/脏值 → null) */
|
||||
private String decodeTheme(Integer schId) {
|
||||
return schSettingsRepository.findFirstBySchId(schId)
|
||||
.map(SchSettings::getThemeName)
|
||||
.map(ThemeCodec::decodeThemeState)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/** 原样读某层头像样式(查无行/非法值 → null) */
|
||||
private String readAvatarAt(Integer schId) {
|
||||
return schSettingsRepository.findFirstBySchId(schId)
|
||||
.map(SchSettings::getAvatarStyle)
|
||||
.filter(ThemeCodec::isAvatarStyle)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,4 +217,80 @@ class AppearanceServiceTest {
|
||||
verify(userAppearanceRepository).save(captor.capture());
|
||||
assertNull(captor.getValue().getAvatarStyle());
|
||||
}
|
||||
|
||||
// ===== 生效值合成(oapi 代理轮 2026-10-08,/oapi/appearance 出参真源)=====
|
||||
|
||||
private IUserCurrentRoleService userCurrentRoleService;
|
||||
|
||||
private void stubCurrentRole(Integer schId) {
|
||||
userCurrentRoleService = mock(IUserCurrentRoleService.class);
|
||||
ReflectionTestUtils.setField(service, "userCurrentRoleService", userCurrentRoleService);
|
||||
if (schId == null) {
|
||||
when(userCurrentRoleService.findById(10133)).thenReturn(Optional.empty());
|
||||
} else {
|
||||
com.yunhai.userhome.entity.UserCurrentRole role = new com.yunhai.userhome.entity.UserCurrentRole();
|
||||
role.setUserId(10133);
|
||||
role.setSchId(schId);
|
||||
when(userCurrentRoleService.findById(10133)).thenReturn(Optional.of(role));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("生效值:个人自选优先——theme/avatar 取 user_appearance,级联读不出站")
|
||||
void effective_personalOverridesCascade() {
|
||||
stubCurrentRole(3);
|
||||
UserAppearance ua = new UserAppearance();
|
||||
ua.setThemeState("ocean|1|0|1");
|
||||
ua.setAvatarStyle("silhouette");
|
||||
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.of(ua));
|
||||
when(logoRepository.findBySchIdAndType(any(), any())).thenReturn(Collections.emptyList());
|
||||
|
||||
Map<String, Object> out = service.getEffectiveAppearance(10133);
|
||||
assertEquals("ocean|1|0|1", out.get("theme"));
|
||||
assertEquals("silhouette", out.get("avatar"));
|
||||
assertNull(out.get("logoUrl"));
|
||||
verify(schSettingsService, never()).readThemeWithFallback(any());
|
||||
verify(schSettingsService, never()).readAvatarWithFallback(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("生效值:个人未设 → 学校/平台级联(缺陷 C 修复验证:合成链走 readThemeWithFallback)")
|
||||
void effective_fallsToCascade() {
|
||||
stubCurrentRole(3);
|
||||
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.empty());
|
||||
when(schSettingsService.readThemeWithFallback(3))
|
||||
.thenReturn(new ISchSettingsService.LayerValue("ink|0|1|1", 0));
|
||||
when(schSettingsService.readAvatarWithFallback(3))
|
||||
.thenReturn(new ISchSettingsService.LayerValue("text", 3));
|
||||
when(logoRepository.findBySchIdAndType(any(), any())).thenReturn(Collections.emptyList());
|
||||
|
||||
Map<String, Object> out = service.getEffectiveAppearance(10133);
|
||||
assertEquals("ink|0|1|1", out.get("theme"), "本校空回落平台档(hitSchId=0)");
|
||||
assertEquals("text", out.get("avatar"), "本校已配直接生效(hitSchId=本校)");
|
||||
// 出参只三字段,不外露 my/sch 拆分
|
||||
assertTrue(out.containsKey("theme") && out.containsKey("avatar") && out.containsKey("logoUrl"));
|
||||
assertEquals(3, out.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("生效值:个人脏值视同未配置 → 落级联;两级皆无 → null;无当前角色 → 平台层(0)")
|
||||
void effective_dirtyAndNoRole_fallsToPlatformLayer() {
|
||||
stubCurrentRole(null);
|
||||
UserAppearance ua = new UserAppearance();
|
||||
ua.setThemeState("default-theme");
|
||||
ua.setAvatarStyle("circle");
|
||||
when(userAppearanceRepository.findByUserId(10133)).thenReturn(Optional.of(ua));
|
||||
when(schSettingsService.readThemeWithFallback(0))
|
||||
.thenReturn(new ISchSettingsService.LayerValue(null, 0));
|
||||
when(schSettingsService.readAvatarWithFallback(0))
|
||||
.thenReturn(new ISchSettingsService.LayerValue(null, 0));
|
||||
when(logoRepository.findBySchIdAndType(any(), any())).thenReturn(Collections.emptyList());
|
||||
|
||||
Map<String, Object> out = service.getEffectiveAppearance(10133);
|
||||
assertNull(out.get("theme"), "脏值落级联且两级皆无 → null");
|
||||
assertNull(out.get("avatar"));
|
||||
assertNull(out.get("logoUrl"));
|
||||
verify(schSettingsService).readThemeWithFallback(0);
|
||||
verify(schSettingsService).readAvatarWithFallback(0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user