Files
UserhomeService/src/main/java/com/yunhai/userhome/service/impl/AppearanceServiceImpl.java
T

215 lines
9.2 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.userhome.service.impl;
import com.yunhai.platform.utils.exception.DException;
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;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 用户外观偏好聚合实现(外观偏好服务端化 2026-10-08)。
*
* <p>消费方是各子应用 init 的异步补齐段(挂载后 fetch,失败静默回落默认观感),因此本服务
* 对外承诺:聚合读永不抛异常——个人段/学校段各自独立降级(一段故障不影响另一段),
* 查不到/脏值/依赖故障的字段一律 null。</p>
*
* <p>数据来源:myTheme/avatarMy ← user_appearance(纯本地,per-user 一行);
* schTheme/avatarSch/logoUrl ← sch_settings + logo 表(schId 复用 resolveThemeSchId——
* 超管=平台层 0,其余=当前角色学校;identity 跨仓查询失败时学校段整体降级 null)。
* Logo 择优复用 UserSettingsServiceImpl.findLogoList 口径:本校 type=1 优先 → 系统 type=0 → null。</p>
*/
@Service
public class AppearanceServiceImpl implements IAppearanceService {
@Autowired
private UserAppearanceRepository userAppearanceRepository;
@Autowired
private ISchSettingsService schSettingsService;
@Autowired
private IUserCurrentRoleService userCurrentRoleService;
@Autowired
private LogoRepository logoRepository;
@Value("${FILE_WEB_PATH:}")
private String fileWebPath;
@Override
public Map<String, Object> getAppearance(BoaUser user) {
Map<String, Object> out = new HashMap<>();
out.put("myTheme", null);
out.put("schTheme", null);
out.put("avatarMy", null);
out.put("avatarSch", null);
out.put("logoUrl", null);
// 个人段(纯本地查询)
try {
Optional<UserAppearance> ua = userAppearanceRepository.findByUserId(user.getId());
if (ua.isPresent()) {
out.put("myTheme", validThemeState(ua.get().getThemeState()));
out.put("avatarMy", validAvatarStyle(ua.get().getAvatarStyle()));
}
} catch (Exception e) {
// 降级:个人段 null,不影响学校段
}
// 学校段(schId 解析含 identity 跨仓调用;失败整体降级 null)
try {
Integer schId = schSettingsService.resolveThemeSchId(user, null);
if (schId != null) {
SchSettings sch = schSettingsService.findBySchIdRaw(schId).orElse(null);
if (sch != null) {
out.put("schTheme", validThemeState(sch.getThemeName()));
out.put("avatarSch", validAvatarStyle(sch.getAvatarStyle()));
}
out.put("logoUrl", resolveLogoUrl(schId));
}
} catch (Exception e) {
// 降级:学校段 null(含 logo),功能不受影响
}
return out;
}
/**
* 当前用户生效外观(oapi 代理轮 2026-10-08,{@code /oapi/appearance} 出参真源;2026-10-09 JSON 化+LOGO 文字):
* {@code {theme, avatar, logoText, logoUrl}}——theme 为【JSON 对象】(个人自选 → 学校默认 → 平台默认(schId=0)
* 级联,层级知识留在 UserHome 内部,对外不拆 my/sch);avatar 为生效样式;logoText 本校→平台级联;
* 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("logoText", 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 = validThemeState(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("logoText", schSettingsService.readLogoTextWithFallback(schId).getValue());
out.put("logoUrl", resolveLogoUrl(schId));
} catch (Exception e) {
// 学校/平台段降级:theme/avatar/logoText/logoUrl 保持已得值或 null
}
// theme 出 JSON 对象(2026-10-09 契约);值已过 decodeThemeState 校验,解析失败按 null 降级
out.put("theme", theme == null ? null : ThemeCodec.toJsonNode(theme));
out.put("avatar", avatar);
return out;
}
@Override
public void saveMyTheme(Integer userId, String themeState) {
UserAppearance ua = loadOrCreate(userId);
if (themeState == null) {
// 跟随学校默认:置 NULL(列语义见实体注释)
ua.setThemeState(null);
} else {
// 写侧从严:只收 JSON、产规范化 JSON 串(紧凑入参抛 DException,2026-10-09 JSON 化契约)
ua.setThemeState(ThemeCodec.encodeThemeState(themeState));
}
ua.setUDate(new Date());
userAppearanceRepository.save(ua);
}
@Override
public void saveMyAvatarStyle(Integer userId, String avatarStyle) {
UserAppearance ua = loadOrCreate(userId);
if (avatarStyle == null) {
ua.setAvatarStyle(null);
} else {
String style = avatarStyle.trim();
if (!ThemeCodec.isAvatarStyle(style)) {
throw new DException("头像样式不合法: " + avatarStyle);
}
ua.setAvatarStyle(style);
}
ua.setUDate(new Date());
userAppearanceRepository.save(ua);
}
private UserAppearance loadOrCreate(Integer userId) {
return userAppearanceRepository.findByUserId(userId).orElseGet(() -> {
UserAppearance ua = new UserAppearance();
ua.setUserId(userId);
ua.setCDate(new Date());
return ua;
});
}
/** 主题档校验读(JSON 校验→规范化;旧紧凑四段兼容转 JSON;脏值 → null 视同未配置) */
private String validThemeState(String raw) {
return StringUtils.hasText(raw) ? ThemeCodec.decodeThemeState(raw) : null;
}
private String validAvatarStyle(String raw) {
return ThemeCodec.isAvatarStyle(raw) ? raw : null;
}
/** Logo 择优:本校(type=1)优先 → 系统(type=0) → null;URL 拼法对齐 UserSettingsController.getLogoList */
private String resolveLogoUrl(Integer schId) {
if (schId != null) {
String url = firstLogoUrl(logoRepository.findBySchIdAndType(schId, 1));
if (url != null) return url;
}
return firstLogoUrl(logoRepository.findBySchIdAndType(0, 0));
}
private String firstLogoUrl(List<Logo> list) {
if (list == null) return null;
for (Logo logo : list) {
String path = logo == null ? null : logo.getLogoUrl();
if (!StringUtils.hasText(path)) continue;
// 本地上传路径拼 FILE_WEB_PATH 绝对 URL;已是绝对地址的(预留跨仓存储)原样透传
if (path.startsWith("http://") || path.startsWith("https://")) return path;
return fileWebPath + path.replace("/upload", "");
}
return null;
}
}