外观偏好异步补齐A:用户外观偏好服务端真源(user_appearance 实体+Appearance 三端点)
- 新实体 user_appearance(per-user 一行,theme_state/avatar_style 紧凑编码,NULL=跟随学校默认) - 新增 GET appearance 聚合出参 + saveMyTheme/saveMyAvatarStyle 双写端点(登录态 /api/** 链路) - 聚合读永不抛:个人段/学校段独立降级(identity 跨仓故障→学校段 null),脏值视同未配置 - Logo 择优复用 findLogoList 口径(本校 type=1 → 系统 type=0 → null),URL 拼法对齐 getLogoList - ThemeCodec 编解码下沉 + resolveThemeSchId/resolveSchId 下沉 SchSettingsService(行为等价重构,供共享) - 单测:ThemeCodecTest 5 例 + AppearanceServiceTest 9 例(聚合降级/upsert 置 NULL/择优/校验从严)
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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.repository.LogoRepository;
|
||||
import com.yunhai.userhome.repository.UserAppearanceRepository;
|
||||
import com.yunhai.userhome.service.IAppearanceService;
|
||||
import com.yunhai.userhome.service.ISchSettingsService;
|
||||
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 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", validCompactTheme(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", validCompactTheme(sch.getThemeName()));
|
||||
out.put("avatarSch", validAvatarStyle(sch.getAvatarStyle()));
|
||||
}
|
||||
out.put("logoUrl", resolveLogoUrl(schId));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 降级:学校段 null(含 logo),功能不受影响
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMyTheme(Integer userId, String themeState) {
|
||||
UserAppearance ua = loadOrCreate(userId);
|
||||
if (themeState == null) {
|
||||
// 跟随学校默认:置 NULL(列语义见实体注释)
|
||||
ua.setThemeState(null);
|
||||
} else {
|
||||
String compact = themeState.trim();
|
||||
if (ThemeCodec.decodeThemeState(compact) == null) {
|
||||
throw new DException("主题档不合法: " + themeState);
|
||||
}
|
||||
ua.setThemeState(compact);
|
||||
}
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
/** 紧凑编码校验读(非法/脏值/旧格式 → null 视同未配置;合法返回原紧凑串) */
|
||||
private String validCompactTheme(String raw) {
|
||||
if (!StringUtils.hasText(raw)) return null;
|
||||
String compact = raw.trim();
|
||||
return ThemeCodec.decodeThemeState(compact) == null ? null : compact;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user