215 lines
9.2 KiB
Java
215 lines
9.2 KiB
Java
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;
|
||
}
|
||
}
|