Compare commits

...
3 Commits
Author SHA1 Message Date
chenxing b621c959cf feat(appicon): 应用图标键下发出栏(YschAppIcons 10-04 轮 P2 后端)
build / build-push (push) Successful in 13m48s
- AppIconKeyService/AppIconEntryVo(新):icon_key/icon_color 资产库键值读取
- UserController:getUserApp 返回体加图标键注入(withIconKeys)
- UserSettingsController:用户设置应用列表注入 iconKey/iconColor(未配置 null 走原 img 回退)
- UsedAppController/UsedAppServiceImpl:常用应用列表同口径注入
依据:根目录《AppIcon资产库落地-执行报告-20261004.md》P2 数据侧/管理端
2026-10-06 20:21:04 +08:00
chenxing 564403b5ca feat(avatar): 学校默认头像样式端点三件套(默认头像轮后端)
- SchSettingsController:GET schAvatarStyle(登录可读,null=未配置)/ POST saveSchAvatarStyle(校管理员)/ POST clearSchAvatarStyle
- SchSettings 实体 + sql/sch_settings_avatar_style.sql:avatar_style 列(DDL 待人工执行)
前端配套=UserHome/front e2c937f(登录写 localStorage[yh-avatar-sch-default] + 设置页 avatar pane)
2026-10-06 20:21:02 +08:00
chenxing a3aa6d917e feat(user): 本人联系方式修改端点 POST /{platform}/user/UpdateContact——登录态取 userId(防替改),YschOpenCall Tier0 直调 UserBase /oapi/user/self/contact(服务目录取址+信封异常透传,零新增配置项);业务错误 DException 冒泡原样包信封,UserBase 不可达收口友好提示;成功返回最新 BoaUser 供前端刷新 store(任务B·UserHome 侧) 2026-10-02 08:54:45 +08:00
9 changed files with 322 additions and 3 deletions
@@ -31,6 +31,9 @@ import java.util.Optional;
* logo 等字段不能删行,置空后读侧返回 null,效果等同"未配置")。存储形态:theme_name 列存紧凑四段编码 * logo 等字段不能删行,置空后读侧返回 null,效果等同"未配置")。存储形态:theme_name 列存紧凑四段编码
* {@code theme|sideDeep01|gradOn01|sideFollow01}(如 teal|0|1|1;不拆列、零 DDL,列宽未确认故不存整份 JSON; * {@code theme|sideDeep01|gradOn01|sideFollow01}(如 teal|0|1|1;不拆列、零 DDL,列宽未确认故不存整份 JSON;
* 旧格式值/非法值读侧一律视同未配置)。</p> * 旧格式值/非法值读侧一律视同未配置)。</p>
* <p>默认头像轮(2026-09-30)同构新增学校默认头像样式三接口:schAvatarStyle 读(可空不兜底);
* saveSchAvatarStyle 写(仅学校管理员/超管,值只接受 text|silhouette);clearSchAvatarStyle 清空(置空
* avatar_style 字段)。</p>
*/ */
@RestController @RestController
@RequestMapping(value = Config.API_PATH) @RequestMapping(value = Config.API_PATH)
@@ -39,6 +42,9 @@ public class SchSettingsController {
/** 合法档位(与通道 THEME_NAMES 一致;防旧库脏值意外生效) */ /** 合法档位(与通道 THEME_NAMES 一致;防旧库脏值意外生效) */
private static final String[] THEME_KEYS = {"blue", "ocean", "ink", "violet", "teal"}; private static final String[] THEME_KEYS = {"blue", "ocean", "ink", "violet", "teal"};
/** 合法头像样式(与前端 avatar.ts 的 AvatarStyle 一致;写入侧从严,对齐 encodeThemeState 做法) */
private static final String[] AVATAR_STYLES = {"text", "silhouette"};
@Autowired @Autowired
private ISchSettingsService schSettingsService; private ISchSettingsService schSettingsService;
@@ -117,6 +123,62 @@ public class SchSettingsController {
return false; return false;
} }
private static boolean isAvatarStyle(String style) {
if (style == null) return false;
for (String k : AVATAR_STYLES) {
if (k.equals(style)) return true;
}
return false;
}
/**
* 学校默认头像样式(默认头像轮 2026-09-30,已登录可读):{avatarStyle: "text"|"silhouette"|null, schId}。
* 未配置/脏值 → avatarStyle=null(读侧可空不兜底,对齐 schTheme 语义;前端"不写键、不报错")。
*/
@GetMapping("schAvatarStyle")
public Map<String, Object> schAvatarStyle(BoaUser authUserWithDept) {
Integer schId = resolveThemeSchId(authUserWithDept, null);
String avatarStyle = schSettingsService.findBySchIdRaw(schId)
.map(SchSettings::getAvatarStyle)
.filter(SchSettingsController::isAvatarStyle)
.orElse(null);
Map<String, Object> out = new HashMap<>();
out.put("avatarStyle", avatarStyle);
out.put("schId", schId);
return out;
}
/** 保存学校默认头像样式(仅学校管理员/超管):值只接受 "text"/"silhouette",非法抛 DException。 */
@PostMapping("saveSchAvatarStyle")
public void saveSchAvatarStyle(BoaUser authUserWithDept,
@RequestParam("avatarStyle") String avatarStyle,
@RequestParam(name = "schId", required = false) Integer schId) {
requireThemeAdmin(authUserWithDept);
if (!isAvatarStyle(avatarStyle)) {
throw new DException("头像样式不合法: " + avatarStyle);
}
Integer targetSchId = resolveThemeSchId(authUserWithDept, schId);
SchSettings schSettings = schSettingsService.findBySchIdRaw(targetSchId).orElseGet(SchSettings::new);
schSettings.setCDate(new Date());
schSettings.setSchId(targetSchId);
schSettings.setCId(authUserWithDept.getId());
schSettings.setAvatarStyle(avatarStyle);
schSettingsService.save(schSettings);
}
/** 清空学校默认头像样式(仅管理员):置空 avatar_style 字段(行还承载 logo/theme 等配置不能删行,置空后读侧返回 null)。 */
@PostMapping("clearSchAvatarStyle")
public void clearSchAvatarStyle(BoaUser authUserWithDept,
@RequestParam(name = "schId", required = false) Integer schId) {
requireThemeAdmin(authUserWithDept);
Integer targetSchId = resolveThemeSchId(authUserWithDept, schId);
Optional<SchSettings> existing = schSettingsService.findBySchIdRaw(targetSchId);
if (!existing.isPresent()) return;
SchSettings schSettings = existing.get();
schSettings.setAvatarStyle(null);
schSettingsService.save(schSettings);
}
/** /**
* 学校默认主题档(已登录可读):{themeState: "<ThemeState JSON>" | null, schId}。 * 学校默认主题档(已登录可读):{themeState: "<ThemeState JSON>" | null, schId}。
* 未配置/旧格式值/脏数据 → themeState=null(前端"不写键、不报错")。 * 未配置/旧格式值/脏数据 → themeState=null(前端"不写键、不报错")。
@@ -5,7 +5,9 @@ import com.yunhai.platform.utils.model.BoaAppType;
import com.yunhai.platform.utils.model.BoaUser; import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userhome.config.Config; import com.yunhai.userhome.config.Config;
import com.yunhai.userhome.entity.UsedApp; import com.yunhai.userhome.entity.UsedApp;
import com.yunhai.userhome.model.AppIconEntryVo;
import com.yunhai.userhome.model.SetType; import com.yunhai.userhome.model.SetType;
import com.yunhai.userhome.service.AppIconKeyService;
import com.yunhai.userhome.service.AppRemoteService; import com.yunhai.userhome.service.AppRemoteService;
import com.yunhai.userhome.service.UsedAppService; import com.yunhai.userhome.service.UsedAppService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -36,6 +38,9 @@ public class UsedAppController {
@Autowired @Autowired
private AppRemoteService appRemoteService; private AppRemoteService appRemoteService;
@Autowired
private AppIconKeyService appIconKeyService;
/** /**
* 取当前角色生效的常用应用(BoaApp 详情,按 appIds 顺序)。 * 取当前角色生效的常用应用(BoaApp 详情,按 appIds 顺序)。
* *
@@ -112,7 +117,7 @@ public class UsedAppController {
usedAppService.saveUserId(usedApp); usedAppService.saveUserId(usedApp);
} }
/** 按 appIds(逗号分隔)取应用精简映射(id/name/icon/url),在全集中查找。 */ /** 按 appIds(逗号分隔)取应用精简映射(id/name/icon/url/iconKey/iconColor),在全集中查找。 */
@GetMapping("getAppByIds") @GetMapping("getAppByIds")
public List<Map<String, Object>> getAppByIds(BoaUser authUserWithDept, String appIds) { public List<Map<String, Object>> getAppByIds(BoaUser authUserWithDept, String appIds) {
List<Map<String, Object>> list = new ArrayList<>(); List<Map<String, Object>> list = new ArrayList<>();
@@ -132,6 +137,10 @@ public class UsedAppController {
m.put("name", app.getName()); m.put("name", app.getName());
m.put("icon", app.getIcon()); m.put("icon", app.getIcon());
m.put("url", app.getWebAddr()); m.put("url", app.getWebAddr());
// 矢量图标键(AppIcon 通道;未配置为 null,前端按空走原 img 回退)
AppIconEntryVo iconEntry = appIconKeyService.byId(app.getId());
m.put("iconKey", iconEntry == null ? null : iconEntry.getIconKey());
m.put("iconColor", iconEntry == null ? null : iconEntry.getIconColor());
list.add(m); list.add(m);
}); });
} catch (NumberFormatException ignore) { } catch (NumberFormatException ignore) {
@@ -1,15 +1,23 @@
package com.yunhai.userhome.controller.api; package com.yunhai.userhome.controller.api;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.openapi.base.OpenApiUserClient; import com.yunhai.platform.openapi.base.OpenApiUserClient;
import com.yunhai.platform.openapi.base.YschBaseOpenApi; import com.yunhai.platform.openapi.base.YschBaseOpenApi;
import com.yunhai.platform.openapi.core.OpenApiException;
import com.yunhai.platform.openapi.core.YschOpenCall;
import com.yunhai.platform.utils.exception.ErrorCode; import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaApp; import com.yunhai.platform.utils.model.BoaApp;
import com.yunhai.platform.utils.model.BoaAppType; import com.yunhai.platform.utils.model.BoaAppType;
import com.yunhai.platform.utils.model.BoaRoleInfo; import com.yunhai.platform.utils.model.BoaRoleInfo;
import com.yunhai.platform.utils.model.BoaUser; import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userhome.config.Config; import com.yunhai.userhome.config.Config;
import com.yunhai.userhome.model.AppIconEntryVo;
import com.yunhai.userhome.service.AppIconKeyService;
import com.yunhai.userhome.service.AppRemoteService; import com.yunhai.userhome.service.AppRemoteService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -18,7 +26,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 用户首页接口(CAS 登录态,路径 {@code /{platform}/user/})。 * 用户首页接口(CAS 登录态,路径 {@code /{platform}/user/})。
@@ -28,12 +38,26 @@ import java.util.List;
@RequestMapping(value = Config.API_PATH + "/user/") @RequestMapping(value = Config.API_PATH + "/user/")
public class UserController { public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
/** 服务目录目标名:userbase(yh.open.services / 枚举兜底 = base-host + /userbase,与 MsgCenterProxyService 同口径) */
private static final String TARGET_USERBASE = "userbase";
@Autowired @Autowired
private AppRemoteService appRemoteService; private AppRemoteService appRemoteService;
@Autowired @Autowired
private OpenApiUserClient openApiUserClient; private OpenApiUserClient openApiUserClient;
@Autowired
private YschOpenCall openCall;
@Autowired
private AppIconKeyService appIconKeyService;
@Autowired
private ObjectMapper objectMapper;
/** 当前登录用户信息(CAS 注入)。 */ /** 当前登录用户信息(CAS 注入)。 */
@GetMapping("GetUserInfo") @GetMapping("GetUserInfo")
@@ -58,9 +82,41 @@ public class UserController {
* @param userId 指定用户(缺省用登录用户) * @param userId 指定用户(缺省用登录用户)
*/ */
@GetMapping("GetUserApp") @GetMapping("GetUserApp")
public List<BoaAppType> getUserApp(BoaUser authUserWithDept, Integer schId, public JsonNode getUserApp(BoaUser authUserWithDept, Integer schId,
@RequestParam(name = "userId", required = false) Integer userId) { @RequestParam(name = "userId", required = false) Integer userId) {
return appRemoteService.getUserApps(authUserWithDept); return withIconKeys(appRemoteService.getUserApps(authUserWithDept));
}
/**
* 应用分组结果上合并注入矢量图标字段(iconKey/iconColor,AppIcon 资产库通道)。<br>
* BoaApp 属 BasePlatform SDK 本期不动:SDK 反序列化会丢弃 icon-map 的扩展字段,
* 故序列化前经 JsonNode 平铺注入;icon-map 不可达时按空映射跳过,出参结构与原值完全一致(零回归)。
*/
private JsonNode withIconKeys(List<BoaAppType> types) {
JsonNode root = objectMapper.valueToTree(types);
Map<Integer, AppIconEntryVo> iconMap = appIconKeyService.iconMap();
if (iconMap.isEmpty() || root == null || !root.isArray()) {
return root;
}
for (JsonNode type : root) {
JsonNode apps = type.get("apps");
if (apps == null || !apps.isArray()) {
continue;
}
for (JsonNode app : apps) {
JsonNode idNode = app.get("id");
if (idNode == null || !idNode.isNumber()) {
continue;
}
AppIconEntryVo entry = iconMap.get(idNode.asInt());
if (entry != null) {
((com.fasterxml.jackson.databind.node.ObjectNode) app)
.put("iconKey", entry.getIconKey())
.put("iconColor", entry.getIconColor());
}
}
}
return root;
} }
/** 按应用ID取单个应用(在已拉取全集中查找,免额外跨仓请求)。 */ /** 按应用ID取单个应用(在已拉取全集中查找,免额外跨仓请求)。 */
@@ -92,4 +148,43 @@ public class UserController {
} }
openApiUserClient.switchIdentity(authUserWithDept.getId(), roleInfo.getRoleId(), roleInfo.getSchId()); openApiUserClient.switchIdentity(authUserWithDept.getId(), roleInfo.getRoleId(), roleInfo.getSchId());
} }
/**
* 本人联系方式修改(手机号/邮箱):body {phone, email},userId 只取 CAS 会话(无视请求体,防替改他人),
* 经 {@link YschOpenCall} 泛化签名直调(Tier 0)转发 UserBase {@code POST /oapi/user/self/contact}
* (与 MsgCenterProxyService 直调 userbase 同范式:服务目录按名取址 + 信封异常透传,零新增配置项)。
* <ul>
* <li>phone/email 至少一项非空(前端与 UserBase 侧同口径校验,此处仅挡空请求);</li>
* <li>UserBase 业务失败(格式/查重等)以 DException(code,msg) 冒泡,由 GlobalExceptionHandler
* 原样包成信封返回(msg 即「手机号已被其他用户使用」等明确错误);</li>
* <li>UserBase 不可达(ioError/验签失败等 OpenApiException)收口为友好提示,不抛 500 裸栈;</li>
* <li>成功返回 UserBase 更新后的最新 {@link BoaUser}(含新 phone/email/groups),前端以此
* 覆盖 store.userInfo,规避 CAS principal 缓存导致的旧值回显。</li>
* </ul>
*/
@PostMapping("UpdateContact")
public BoaUser updateContact(BoaUser authUserWithDept, @RequestBody Map<String, String> body) {
String phone = body == null ? null : body.get("phone");
String email = body == null ? null : body.get("email");
boolean phoneBlank = phone == null || phone.trim().isEmpty();
boolean emailBlank = email == null || email.trim().isEmpty();
if (phoneBlank && emailBlank) {
throw new BizException(ErrorCode.PARAM_ERROR, "手机号与邮箱至少填写一项");
}
Map<String, String> payload = new LinkedHashMap<>();
payload.put("userId", String.valueOf(authUserWithDept.getId()));
if (!phoneBlank) {
payload.put("phone", phone.trim());
}
if (!emailBlank) {
payload.put("email", email.trim());
}
try {
return openCall.post(TARGET_USERBASE, "/oapi/user/self/contact", null, payload, BoaUser.class);
} catch (OpenApiException e) {
log.warn("[user] UpdateContact 直调 UserBase 失败 userId={}:{}", authUserWithDept.getId(), e.toString());
throw new BizException(ErrorCode.CUSTOM, "用户中心暂不可达,请稍后重试");
}
// DException(UserBase 业务错误,code!=0)刻意冒泡:GlobalExceptionHandler 按原 code/msg 包信封
}
} }
@@ -14,6 +14,8 @@ import com.yunhai.userhome.entity.*;
import com.yunhai.userhome.enums.DesktopAppType; import com.yunhai.userhome.enums.DesktopAppType;
import com.yunhai.userhome.enums.LightAppGroup; import com.yunhai.userhome.enums.LightAppGroup;
import com.yunhai.userhome.enums.LinkOpenType; import com.yunhai.userhome.enums.LinkOpenType;
import com.yunhai.userhome.model.AppIconEntryVo;
import com.yunhai.userhome.service.AppIconKeyService;
import com.yunhai.userhome.service.IUserCurrentRoleService; import com.yunhai.userhome.service.IUserCurrentRoleService;
import com.yunhai.userhome.service.IUserSettingsService; import com.yunhai.userhome.service.IUserSettingsService;
import com.yunhai.userhome.service.UsedAppService; import com.yunhai.userhome.service.UsedAppService;
@@ -59,6 +61,9 @@ public class UserSettingsController {
@Autowired @Autowired
private OpenApiUserClient openApiUserClient; private OpenApiUserClient openApiUserClient;
@Autowired
private AppIconKeyService appIconKeyService;
// ==================== 壁纸 ==================== // ==================== 壁纸 ====================
/** /**
@@ -376,6 +381,10 @@ public class UserSettingsController {
map.put("type", DesktopAppType.Bsch.getKey()); map.put("type", DesktopAppType.Bsch.getKey());
map.put("linkType", LinkOpenType.Open.getKey()); map.put("linkType", LinkOpenType.Open.getKey());
map.put("layout", null); map.put("layout", null);
// 矢量图标键(AppIcon 通道;未配置为 null,前端按空走原 img 回退)
AppIconEntryVo iconEntry = appIconKeyService.byId(optionalBoaApp.get().getId());
map.put("iconKey", iconEntry == null ? null : iconEntry.getIconKey());
map.put("iconColor", iconEntry == null ? null : iconEntry.getIconColor());
} else if (desktopApp.getAppType() == DesktopAppType.Light) { } else if (desktopApp.getAppType() == DesktopAppType.Light) {
Optional<LightApp> optionalLightApp = lightAllList.stream() Optional<LightApp> optionalLightApp = lightAllList.stream()
.filter(app -> desktopApp.getAppId().equals(app.getId())).findFirst(); .filter(app -> desktopApp.getAppId().equals(app.getId())).findFirst();
@@ -30,6 +30,10 @@ public class SchSettings {
@Column(name = "theme_name") @Column(name = "theme_name")
private String themeName; private String themeName;
/** 学校默认头像样式(默认头像轮 2026-09-30):text|silhouette;null=未配置(读侧可空不兜底) */
@Column(name = "avatar_style")
private String avatarStyle;
@Column(name = "logo_url") @Column(name = "logo_url")
private String logoUrl; private String logoUrl;
@@ -0,0 +1,42 @@
package com.yunhai.userhome.model;
/**
* UserBase 应用矢量图标映射条目({@code /oapi/app/icon-map} 出参镜像)。<br>
* BoaApp 属 BasePlatform SDK 本期不动,PC 通道的 iconKey/iconColor 经本 VO 独立补拉后
* 在 GetUserApp / 常用区出参上合并注入(加法式改造,不动既有 oapi 契约)。
*/
public class AppIconEntryVo {
/** 应用主键 ID(与 BoaApp.id 同源,按此合并) */
private Integer id;
/** 矢量图标键(AppIcon 资产库图标名) */
private String iconKey;
/** 矢量图标着色(CSS 色值);null = 端侧默认 */
private String iconColor;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getIconKey() {
return iconKey;
}
public void setIconKey(String iconKey) {
this.iconKey = iconKey;
}
public String getIconColor() {
return iconColor;
}
public void setIconColor(String iconColor) {
this.iconColor = iconColor;
}
}
@@ -0,0 +1,79 @@
package com.yunhai.userhome.service;
import com.yunhai.platform.openapi.core.YschOpenCall;
import com.yunhai.userhome.model.AppIconEntryVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 应用矢量图标映射补拉(AppIcon 资产库 P2/P3 数据通道)。<br>
* UserBase 的 {@code GET /oapi/app/icon-map} 返回全量已配置行 [{id, iconKey, iconColor}],
* 本服务拉取后建 id 索引供各出参组装点合并注入(BoaApp 属 SDK 本期不动,矢量字段走本通道)。<br>
* <ul>
* <li>60 秒本地缓存:多个出参点(GetUserApp/常用区/搜索)一次渲染只打一次跨仓调用;</li>
* <li>失败降级:返回上次成功值(无则空映射),绝不阻塞主应用列表——空映射下端侧
* 走原 关键词→URL→首字 回退链,表现与改造前一致(零回归要求);</li>
* <li>失败后 10 秒内不重试(简单防雪崩)。</li>
* </ul>
*/
@Service
public class AppIconKeyService {
private static final Logger log = LoggerFactory.getLogger(AppIconKeyService.class);
/** 服务目录目标名:userbase(与 UserController.TARGET_USERBASE 同口径) */
private static final String TARGET_USERBASE = "userbase";
private static final String PATH_ICON_MAP = "/oapi/app/icon-map";
/** 成功结果缓存时长 */
private static final long TTL_MS = 60_000L;
/** 拉取失败后的静默期 */
private static final long FAIL_BACKOFF_MS = 10_000L;
@Autowired
private YschOpenCall openCall;
private volatile Map<Integer, AppIconEntryVo> cached = Collections.emptyMap();
private volatile long fetchedAt = Long.MIN_VALUE;
/**
* id → 矢量图标条目 映射(含缓存与降级)。空映射 = 无任何应用配置矢量键或通道不可达。
*/
public Map<Integer, AppIconEntryVo> iconMap() {
long now = System.currentTimeMillis();
if (now - fetchedAt < TTL_MS) {
return cached;
}
try {
List<AppIconEntryVo> list = openCall.getList(TARGET_USERBASE, PATH_ICON_MAP, null,
AppIconEntryVo.class);
Map<Integer, AppIconEntryVo> map = (list == null) ? Collections.emptyMap()
: list.stream()
.filter(v -> v != null && v.getId() != null && v.getIconKey() != null)
.collect(Collectors.toMap(AppIconEntryVo::getId, Function.identity(), (a, b) -> a));
cached = Collections.unmodifiableMap(map);
fetchedAt = now;
return cached;
} catch (Exception e) {
// 降级:沿用上次值;10 秒内不再重试。应用列表主链路绝不因此报错
fetchedAt = now - TTL_MS + FAIL_BACKOFF_MS;
log.warn("[appicon] icon-map 拉取失败,按空映射降级:{}", e.toString());
return cached;
}
}
/** 按 id 取单条;无配置返回 null。 */
public AppIconEntryVo byId(Integer appId) {
return (appId == null) ? null : iconMap().get(appId);
}
}
@@ -4,8 +4,10 @@ import com.yunhai.platform.utils.model.BoaApp;
import com.yunhai.platform.utils.model.BoaAppType; import com.yunhai.platform.utils.model.BoaAppType;
import com.yunhai.platform.utils.model.BoaUser; import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userhome.entity.UsedApp; import com.yunhai.userhome.entity.UsedApp;
import com.yunhai.userhome.model.AppIconEntryVo;
import com.yunhai.userhome.model.SetType; import com.yunhai.userhome.model.SetType;
import com.yunhai.userhome.repository.UsedAppRepository; import com.yunhai.userhome.repository.UsedAppRepository;
import com.yunhai.userhome.service.AppIconKeyService;
import com.yunhai.userhome.service.AppRemoteService; import com.yunhai.userhome.service.AppRemoteService;
import com.yunhai.userhome.service.UsedAppService; import com.yunhai.userhome.service.UsedAppService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -34,6 +36,9 @@ public class UsedAppServiceImpl implements UsedAppService {
@Autowired @Autowired
private AppRemoteService appRemoteService; private AppRemoteService appRemoteService;
@Autowired
private AppIconKeyService appIconKeyService;
@Override @Override
public void save(UsedApp usedApp) { public void save(UsedApp usedApp) {
Optional<UsedApp> model = usedAppRepository Optional<UsedApp> model = usedAppRepository
@@ -138,6 +143,7 @@ public class UsedAppServiceImpl implements UsedAppService {
m.put("name", app.getName()); m.put("name", app.getName());
m.put("icon", app.getIcon()); m.put("icon", app.getIcon());
m.put("url", app.getWebAddr()); m.put("url", app.getWebAddr());
putIconKeys(m, app.getId());
list.add(m); list.add(m);
} }
} catch (NumberFormatException ignore) { } catch (NumberFormatException ignore) {
@@ -147,6 +153,13 @@ public class UsedAppServiceImpl implements UsedAppService {
return list; return list;
} }
/** 精简映射上补矢量图标键(iconKey/iconColor,AppIcon 通道;未配置时值为 null,前端按空走原回退链)。 */
private void putIconKeys(Map<String, Object> m, Integer appId) {
AppIconEntryVo entry = appIconKeyService.byId(appId);
m.put("iconKey", entry == null ? null : entry.getIconKey());
m.put("iconColor", entry == null ? null : entry.getIconColor());
}
/** BoaAppType 分组平铺为 BoaApp 列表。 */ /** BoaAppType 分组平铺为 BoaApp 列表。 */
private List<BoaApp> flattenApps(List<BoaAppType> types) { private List<BoaApp> flattenApps(List<BoaAppType> types) {
List<BoaApp> apps = new ArrayList<>(); List<BoaApp> apps = new ArrayList<>();
@@ -0,0 +1,6 @@
-- 学校默认头像样式(默认头像轮 2026-09-30):sch_settings 新增 avatar_style 列。
-- dev 环境由 JPA ddl-auto=update 自动加列;测试/生产等 ddl-auto 关闭的环境手工执行本文件。
ALTER TABLE sch_settings ADD COLUMN avatar_style VARCHAR(16) NULL COMMENT '学校默认头像样式 text|silhouette';
-- 回滚:
-- ALTER TABLE sch_settings DROP COLUMN avatar_style;