feat(app): 应用稳定来源主体 UUID + /oapi/app/activated 开通判定(FL 授权门禁依赖)
build / build-push (push) Successful in 2m10s

This commit is contained in:
chenxing
2026-10-28 11:45:41 +08:00
parent 809d88c838
commit 6f60d65195
4 changed files with 68 additions and 0 deletions
@@ -5,11 +5,13 @@ import com.yunhai.platform.utils.enums.IEnum;
import com.yunhai.platform.utils.model.BoaAppType;
import com.yunhai.userbase.config.Config;
import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.model.application.AppActivatedVo;
import com.yunhai.userbase.model.application.AppIconMapVo;
import com.yunhai.userbase.model.application.ApplicationInfoVo;
import com.yunhai.userbase.service.IAppCategoryService;
import com.yunhai.userbase.service.IApplicationInfoService;
import com.yunhai.userbase.service.IGroupAuthzService;
import com.yunhai.platform.utils.exception.DException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -38,6 +40,9 @@ public class AppController {
@Autowired
private IGroupAuthzService groupAuthzService;
@Autowired
private com.yunhai.userbase.service.ISysGroupService sysGroupService;
/**
* 按主键 ID 查询应用信息(带缓存)。<br>
* 与客户端契约 {@code GET /oapi/app/{appId}} 对应;查不到返回 null(HTTP 200 + data:null 信封)。<br>
@@ -158,4 +163,29 @@ public class AppController {
public List<AppIconMapVo> iconMap() {
return applicationInfoService.listIconMap();
}
/**
* 应用×学校单点开通判定(2026-10-25 T01 最小只读口)。<br>
* 权威事实源 = {@code sys_group_app_rel}(组应用开通表,学校组单点、不沿祖先、不并角色模块),
* 与 {@code GET /oapi/app/list-by-school} 同一口径,只是收敛成 boolean 判定——
* form-center/flow-center 等下游把本端点作为「该应用是否在该校开通」的平台资格第二重证明
* (fail-closed:本端点不可达时下游必须拒绝,不得降级为"仅验签通过")。<br>
* 应用不存在(含已注销)返回 activated=false,不区分"没开通"与"没这应用",
* 避免把应用存在性做成探测面;schoolId 非法(非学校组锚点)按既有 {@code assertSchoolId}
* 口径抛业务错误(code=5)。只读、无敏感字段。
*
* @param appId 应用主键 ID(必填;稳定主体,非 appKey 凭据索引)
* @param schoolId 学校组 ID(必填)
*/
@GetMapping("activated")
public AppActivatedVo activated(@RequestParam("appId") Integer appId,
@RequestParam("schoolId") Integer schoolId) {
if (appId == null || schoolId == null) {
throw new DException("appId 与 schoolId 必填");
}
sysGroupService.assertSchoolId(schoolId);
boolean appExists = applicationInfoService.getById(appId) != null;
boolean activated = appExists && groupAuthzService.findAuthorizedAppIds(schoolId).contains(appId);
return new AppActivatedVo(appId, schoolId, activated);
}
}
@@ -26,6 +26,16 @@ import java.util.List;
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class ApplicationInfo extends BaseEntity {
/** Immutable application incarnation; never derive it from reusable numeric IDs or credentials. */
@Column(name = "source_principal_id", unique = true, updatable = false, length = 36)
private String sourcePrincipalId;
@javax.persistence.PrePersist
void assignSourcePrincipal() {
// Always generate server-side, including when an entity supplied a value.
sourcePrincipalId = java.util.UUID.randomUUID().toString();
}
/**
* 应用名称(页面*必填)
*/
@@ -0,0 +1,25 @@
package com.yunhai.userbase.model.application;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 应用×学校单点开通判定结果(2026-10-25 T01 最小只读口)。
* 只回判定所需三件,不透出 secret/别名等管理信息;消费方(form-center/flow-center)
* 把它作为「该应用是否在该校开通」的平台事实第二重证明,fail-closed 消费。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppActivatedVo {
/** 应用主键 ID(稳定主体,与入参一致) */
private Integer appId;
/** 学校组 ID(与入参一致) */
private Integer schoolId;
/** true=该校单点开通集合包含该应用(sys_group_app_rel 口径,不沿祖先) */
private boolean activated;
}
@@ -12,6 +12,9 @@ import java.util.List;
*/
@Data
public class ApplicationInfoVo {
/** Stable, non-secret application incarnation used for source isolation. */
private String sourcePrincipalId;
/**
* 主键ID。<br>
*/