feat(oapi): 学校维度应用清单端点 GET /oapi/app/list-by-school(流程中心应用设置源池)
build / build-push (push) Successful in 54s
build / build-push (push) Successful in 54s
- 开通口径=学校组单点已开通(sys_group_app_rel where group_id=schId,不沿祖先/不并子树角色,拍板 D7); 基石应用过滤/端掩码过滤/convertToBoaApp 不透 secret/applyAlias 学校别名合并,骨架复用 listBoaAppType - 新缓存 schoolAppList('s:'+schId+':c:'+clientType,60s,学校 id 不可枚举→授权/组/应用/别名事件整清) - AppAuthzChangedEvent/GroupChangedEvent/ApplicationInfoCacheChangedEvent 三事件挂接整清 - 仅含本功能 5 文件;工作区其余未提交改动(icon-map/channel-bind 实验线)未纳入
This commit is contained in:
@@ -119,6 +119,20 @@ public final class CacheNames {
|
||||
*/
|
||||
public static final String USER_APP_LIST = "userAppList";
|
||||
|
||||
/**
|
||||
* 学校维度应用清单(GET /oapi/app/list-by-school 读路径,2026-10-10;流程中心 flw_app 源池)。<br>
|
||||
* key 规则:{@code 's:' + schoolId + ':c:' + (clientType 为 null ? 'all' : ClientEnum.name())}——
|
||||
* 学校 id 不可枚举,用户级变更不涉及(学校层无用户),授权/组/应用/别名事件一律整清,
|
||||
* 其余直改 DB 场景靠 TTL 自愈(同 USER_APP_LIST 显式学校后缀策略);<br>
|
||||
* TTL 60s(同 USER_APP_LIST 档位:脏了=流程中心显示过期应用池,短档足够);<br>
|
||||
* 存放:Redis;依赖表:sys_group_app_rel、app_category、application_info、
|
||||
* sys_school_app_override、sys_school_category_override;<br>
|
||||
* 失效责任:{@link com.yunhai.userbase.event.AppAuthzChangedEvent}(授权域写口整清,含学校别名)+
|
||||
* {@link com.yunhai.userbase.event.GroupChangedEvent} / {@link com.yunhai.userbase.event.ApplicationInfoCacheChangedEvent}
|
||||
* (组开通面变化 / 应用启停排序删除整清)。
|
||||
*/
|
||||
public static final String SCHOOL_APP_LIST = "schoolAppList";
|
||||
|
||||
/**
|
||||
* 用户组关系状态 Map(GET /oapi/user/groupState 数据源,groupId→status,status null 归一 0=正常)。<br>
|
||||
* key 规则:{@code 'u:' + userId}(按用户整张 Map,不做 (userId,groupId) 多维 key——changeGroups
|
||||
@@ -234,6 +248,12 @@ public final class CacheNames {
|
||||
"'u:'+userId(整张 groupId→status Map)",
|
||||
"UserChangedEvent(精确驱逐)+ GroupChangedEvent(整清)→ CacheInvalidationListener",
|
||||
"sys_user_group_rel"));
|
||||
m.put(SCHOOL_APP_LIST, new Meta(SCHOOL_APP_LIST, Store.REDIS, Duration.ofSeconds(60), 0.1,
|
||||
"'s:'+schoolId+':c:'+(clientType?.name()?:'all')(8 client × 学校 id,不可枚举→整清)",
|
||||
"AppAuthzChangedEvent(整清,含学校别名写口)+ GroupChangedEvent / ApplicationInfoCacheChangedEvent"
|
||||
+ "(整清)→ CacheInvalidationListener;直改 DB 靠 TTL 自愈",
|
||||
"sys_group_app_rel, app_category, application_info, sys_group,"
|
||||
+ " sys_school_app_override, sys_school_category_override"));
|
||||
REGISTRY = Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
|
||||
@@ -124,4 +124,24 @@ public class AppController {
|
||||
: IEnum.getByKey(ClientEnum.class, clientType);
|
||||
return appCategoryService.listBoaAppType(userId, clientEnum, schoolId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按学校维度查询该校已开通的应用列表(按应用类型分组,已合并学校别名,2026-10-10)。<br>
|
||||
* 开通口径 = 学校组<b>单点</b>已开通(sys_group_app_rel where group_id=schoolId;开通不继承,
|
||||
* 不并入子组/角色模块应用)——学校把应用只开给子组时本端点看不到,属口径自然结果。<br>
|
||||
* 基石应用(UserBase 自身 / BASE)不返回;不透出 appSecret。<br>
|
||||
* schoolId 必填且须为合法学校组锚点,否则 code=5。<br>
|
||||
* 消费方(flow-center 应用设置)可传 clientType=null 不过滤端,避免漏纯移动端应用。
|
||||
*
|
||||
* @param schoolId 学校组ID(必填)
|
||||
* @param clientType 客户端类型 key(可选;null=不按端过滤)
|
||||
* @return 应用类型分组列表;未开通应用的类型 apps 为空数组
|
||||
*/
|
||||
@GetMapping("list-by-school")
|
||||
public List<BoaAppType> listBySchool(@RequestParam("schoolId") Integer schoolId,
|
||||
@RequestParam(value = "clientType", required = false) Integer clientType) {
|
||||
ClientEnum clientEnum = (clientType == null) ? null
|
||||
: IEnum.getByKey(ClientEnum.class, clientType);
|
||||
return appCategoryService.listBoaAppTypeBySchool(schoolId, clientEnum);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,4 +55,21 @@ public interface IAppCategoryService {
|
||||
*/
|
||||
List<BoaAppType> listBoaAppType(Integer userId, ClientEnum clientType, Integer schoolId);
|
||||
|
||||
/**
|
||||
* 开放接口专用:按学校维度查询该校已开通的应用列表(按应用类型分组,2026-10-10 流程中心应用设置源池)。<br>
|
||||
* 规则:<br>
|
||||
* ① 开通口径:<b>仅学校组自身已开通</b>(sys_group_app_rel where group_id=schoolId,开通不继承、
|
||||
* 不并入子组/角色模块应用)——学校层无用户,这是"学校维度应用清单"的唯一定义;<br>
|
||||
* ② 基石应用过滤:同 {@link #listBoaAppType}(UserBase 自身 / BASE 应用不出现);<br>
|
||||
* ③ 客户端过滤:clientType 位掩码(null=不按端过滤);<br>
|
||||
* ④ 不透出 appSecret;<br>
|
||||
* ⑤ 学校别名:按该校合并 sys_school_app_override / sys_school_category_override(同 listBoaAppType ④)。<br>
|
||||
* schoolId 必填且须为合法学校锚点(assertSchoolId),非法抛 DException(code=5)。
|
||||
*
|
||||
* @param schoolId 学校组ID(必填)
|
||||
* @param clientType 客户端类型(null=不按端过滤)
|
||||
* @return 应用类型 + 该校已开通应用列表(已合并学校别名)
|
||||
*/
|
||||
List<BoaAppType> listBoaAppTypeBySchool(Integer schoolId, ClientEnum clientType);
|
||||
|
||||
}
|
||||
|
||||
@@ -245,6 +245,64 @@ public class AppCategoryService implements IAppCategoryService {
|
||||
return schoolAppOverrideService.applyAlias(result, categoryBaseSortNo, effectiveSchool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 学校维度应用清单(2026-10-10,GET /oapi/app/list-by-school 读路径;流程中心 flw_app 源池)。<br>
|
||||
* 开通口径 = 学校组<b>单点</b>已开通(findAuthorizedAppIds(schId),不沿祖先/不并角色模块):
|
||||
* 学校层无用户,"学校开了什么"就是 sys_group_app_rel 的单点事实——学校把应用只开给子组时
|
||||
* 本端点看不到它,属口径自然结果(组开通继承的是模块权限池,不是学校清单)。<br>
|
||||
* 骨架复用 {@link #listBoaAppType}(分类分组/基石过滤/端过滤/convertToBoaApp 不透 secret/
|
||||
* applyAlias 别名合并),仅权限集合来源不同;返回前合并学校别名。<br>
|
||||
* 缓存 key 规则:{@code 's:'+schoolId+':c:'+(clientType?'all')}——学校 id 不可枚举,
|
||||
* 一律整清(授权/组/应用/别名事件)+ 60s TTL 自愈(同 USER_APP_LIST 显式学校后缀策略)。
|
||||
*/
|
||||
@Override
|
||||
@Cacheable(value = CacheNames.SCHOOL_APP_LIST,
|
||||
key = "'s:' + #schoolId + ':c:' + (#clientType == null ? 'all' : #clientType.name())",
|
||||
sync = true)
|
||||
public List<BoaAppType> listBoaAppTypeBySchool(Integer schoolId, ClientEnum clientType) {
|
||||
if (schoolId == null) {
|
||||
throw new DException("schoolId 必填");
|
||||
}
|
||||
sysGroupService.assertSchoolId(schoolId);
|
||||
Set<Integer> openedAppIds = new HashSet<>(groupAuthzService.findAuthorizedAppIds(schoolId));
|
||||
List<AppCategory> categories = appCategoryRepository.findAllByOrderBySortNoAsc();
|
||||
List<ApplicationInfo> apps = applicationInfoRepository.findAll();
|
||||
apps.sort(Comparator.comparing(ApplicationInfo::getSortNo, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(ApplicationInfo::getId, Comparator.nullsLast(Integer::compareTo)));
|
||||
Map<Integer, List<ApplicationInfo>> appMap = new LinkedHashMap<>();
|
||||
for (ApplicationInfo app : apps) {
|
||||
if (app.getCategoryId() == null) {
|
||||
continue;
|
||||
}
|
||||
appMap.computeIfAbsent(app.getCategoryId(), k -> new ArrayList<>()).add(app);
|
||||
}
|
||||
Map<Integer, Integer> categoryBaseSortNo = new HashMap<>();
|
||||
List<BoaAppType> result = new ArrayList<>();
|
||||
for (AppCategory category : categories) {
|
||||
categoryBaseSortNo.put(category.getId(), category.getSortNo());
|
||||
BoaAppType type = new BoaAppType();
|
||||
type.setId(category.getId());
|
||||
type.setName(category.getName());
|
||||
List<BoaApp> boaApps = new ArrayList<>();
|
||||
for (ApplicationInfo app : appMap.getOrDefault(category.getId(), Collections.emptyList())) {
|
||||
if (AppNature.isBaseApp(app.getAppKey(), app.getAppNature())) {
|
||||
continue;
|
||||
}
|
||||
// 学校组单点开通过滤(与用户维度的权限过滤唯一差异)
|
||||
if (!openedAppIds.contains(app.getId())) {
|
||||
continue;
|
||||
}
|
||||
if (clientType != null && !containsClient(app.getClients(), clientType.getKey())) {
|
||||
continue;
|
||||
}
|
||||
boaApps.add(convertToBoaApp(app));
|
||||
}
|
||||
type.setApps(boaApps);
|
||||
result.add(type);
|
||||
}
|
||||
return schoolAppOverrideService.applyAlias(result, categoryBaseSortNo, schoolId);
|
||||
}
|
||||
|
||||
/**
|
||||
* oapi 生效学校三级兜底:<br>
|
||||
* ① 显式 {@code schoolId}——复用开放接口公共锚点校验 {@code assertSchoolId}(非空/存在/须学校组),
|
||||
|
||||
@@ -64,7 +64,8 @@ public class CacheInvalidationListener {
|
||||
clearQuietly(CacheNames.APP_INFO);
|
||||
clearQuietly(CacheNames.APP_KEY_TO_ID);
|
||||
clearQuietly(CacheNames.USER_APP_LIST);
|
||||
log.info("[Cache失效] 应用变更 appKey={} type={} → 清 appInfo/appKeyToId/userAppList",
|
||||
clearQuietly(CacheNames.SCHOOL_APP_LIST);
|
||||
log.info("[Cache失效] 应用变更 appKey={} type={} → 清 appInfo/appKeyToId/userAppList/schoolAppList",
|
||||
event.getAppKey(), event.getType());
|
||||
}
|
||||
|
||||
@@ -77,8 +78,9 @@ public class CacheInvalidationListener {
|
||||
clearQuietly(CacheNames.BOA_USER_NULL);
|
||||
clearQuietly(CacheNames.IDENTITY_CURRENT);
|
||||
clearQuietly(CacheNames.USER_APP_LIST);
|
||||
clearQuietly(CacheNames.SCHOOL_APP_LIST);
|
||||
clearQuietly(CacheNames.USER_GROUP_REL);
|
||||
log.info("[Cache失效] 组变更 op={} → 驱逐 groupSnapshot,清 boaUser/identityCurrent/userAppList/userGroupRel",
|
||||
log.info("[Cache失效] 组变更 op={} → 驱逐 groupSnapshot,清 boaUser/identityCurrent/userAppList/schoolAppList/userGroupRel",
|
||||
event.getSourceOp());
|
||||
}
|
||||
|
||||
@@ -132,7 +134,8 @@ public class CacheInvalidationListener {
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
|
||||
public void onAppAuthzChanged(AppAuthzChangedEvent event) {
|
||||
clearQuietly(CacheNames.USER_APP_LIST);
|
||||
log.info("[Cache失效] 应用授权变更 op={} → 清 userAppList", event.getSourceOp());
|
||||
clearQuietly(CacheNames.SCHOOL_APP_LIST);
|
||||
log.info("[Cache失效] 应用授权变更 op={} → 清 userAppList/schoolAppList", event.getSourceOp());
|
||||
}
|
||||
|
||||
// ===================== 驱逐工具(error 可观测,TTL 自愈兜底) =====================
|
||||
|
||||
Reference in New Issue
Block a user