diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java b/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java
index bdb497a..0bab68e 100644
--- a/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java
+++ b/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java
@@ -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 查询应用信息(带缓存)。
* 与客户端契约 {@code GET /oapi/app/{appId}} 对应;查不到返回 null(HTTP 200 + data:null 信封)。
@@ -158,4 +163,29 @@ public class AppController {
public List iconMap() {
return applicationInfoService.listIconMap();
}
+
+ /**
+ * 应用×学校单点开通判定(2026-10-25 T01 最小只读口)。
+ * 权威事实源 = {@code sys_group_app_rel}(组应用开通表,学校组单点、不沿祖先、不并角色模块),
+ * 与 {@code GET /oapi/app/list-by-school} 同一口径,只是收敛成 boolean 判定——
+ * form-center/flow-center 等下游把本端点作为「该应用是否在该校开通」的平台资格第二重证明
+ * (fail-closed:本端点不可达时下游必须拒绝,不得降级为"仅验签通过")。
+ * 应用不存在(含已注销)返回 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);
+ }
}
diff --git a/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java b/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java
index dbad556..420e7a5 100644
--- a/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java
+++ b/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java
@@ -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();
+ }
+
/**
* 应用名称(页面*必填)
*/
diff --git a/src/main/java/com/yunhai/userbase/model/application/AppActivatedVo.java b/src/main/java/com/yunhai/userbase/model/application/AppActivatedVo.java
new file mode 100644
index 0000000..0ccb066
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/model/application/AppActivatedVo.java
@@ -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;
+}
diff --git a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java
index 23492e6..52c3d53 100644
--- a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java
+++ b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java
@@ -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。
*/