From fa4318cfc27b791fe730e42e879d93d1df515691 Mon Sep 17 00:00:00 2001
From: chenxing
Date: Tue, 20 Oct 2026 12:00:46 +0800
Subject: [PATCH] =?UTF-8?q?t12:FormAccessPolicy=20=E9=97=A8=E9=9D=A2?=
=?UTF-8?q?=E6=94=B6=E6=95=9B=E2=80=94=E2=80=94=E4=BC=9A=E8=AF=9D=E6=8E=A8?=
=?UTF-8?q?=E5=AF=BC=E5=85=AD=E5=A4=84+=E8=A7=92=E8=89=B2=E5=B8=B8?=
=?UTF-8?q?=E9=87=8F=E4=B8=89=E5=A4=84=E6=94=B6=E7=BC=96=EF=BC=8C=E9=9B=B6?=
=?UTF-8?q?=E8=AF=AD=E4=B9=89=E5=8F=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
会话校推导/身份角色并集/超管判定收进门面单点,角色常量与校领导临时清单 {2,7}
同址(V58 单点);判定逻辑与拒绝文案逐点沿用原实现,既有 272 例测试一字未改全绿。
清单外补件:FormFileService 的 requireSessionSchId 是 B3 未列的第七处,一并收编。
---
.../controller/api/FormResolveController.java | 16 +-
.../formcenter/service/AnswerReadService.java | 48 +-----
.../formcenter/service/AnswerService.java | 16 +-
.../formcenter/service/DataScopeService.java | 44 +----
.../formcenter/service/FillAccessService.java | 50 ++----
.../service/FillableFormService.java | 15 +-
.../formcenter/service/FormAccessPolicy.java | 151 ++++++++++++++++++
.../formcenter/service/FormAdminGuard.java | 32 ++--
.../formcenter/service/FormFileService.java | 12 +-
9 files changed, 191 insertions(+), 193 deletions(-)
create mode 100644 src/main/java/com/yunhai/formcenter/service/FormAccessPolicy.java
diff --git a/src/main/java/com/yunhai/formcenter/controller/api/FormResolveController.java b/src/main/java/com/yunhai/formcenter/controller/api/FormResolveController.java
index f580759..37c2efd 100644
--- a/src/main/java/com/yunhai/formcenter/controller/api/FormResolveController.java
+++ b/src/main/java/com/yunhai/formcenter/controller/api/FormResolveController.java
@@ -1,9 +1,8 @@
package com.yunhai.formcenter.controller.api;
import com.yunhai.formcenter.model.FormModels;
+import com.yunhai.formcenter.service.FormAccessPolicy;
import com.yunhai.formcenter.service.FormDefService;
-import com.yunhai.platform.apistd.BizException;
-import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaUser;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
@@ -43,18 +42,7 @@ public class FormResolveController {
public FormModels.ResolvedFormView resolve(BoaUser user,
@RequestParam("code") String code,
@RequestParam(value = "appCode", required = false) String appCode) {
- return defService.resolve(requireSessionSchId(user),
+ return defService.resolve(FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属,无法解析表单"),
appCode == null || appCode.trim().isEmpty() ? DEFAULT_APP_CODE : appCode.trim(), code);
}
-
- /** 会话校(无学校归属的身份不能寻址表单——区域组/根直属用户不是填报主体) */
- private Long requireSessionSchId(BoaUser user) {
- if (user == null || user.getId() == null) {
- throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
- }
- if (user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) {
- throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属,无法解析表单");
- }
- return Long.valueOf(user.getCurrentRole().getSchId());
- }
}
diff --git a/src/main/java/com/yunhai/formcenter/service/AnswerReadService.java b/src/main/java/com/yunhai/formcenter/service/AnswerReadService.java
index 73a1d4e..8727f3c 100644
--- a/src/main/java/com/yunhai/formcenter/service/AnswerReadService.java
+++ b/src/main/java/com/yunhai/formcenter/service/AnswerReadService.java
@@ -85,7 +85,7 @@ public class AnswerReadService {
query.setScopes(scopeService.resolve(user, form));
query.setUserId(user == null ? null : user.getId());
// T10 方案 §5.1:SCHOOL 范围的谓词基准=会话学校(不是查询域,两者同源于会话校但语义不同)
- query.setViewerSchId(user == null ? null : sessionSchId(user));
+ query.setViewerSchId(FormAccessPolicy.sessionSchId(user));
query.setStatus(req == null ? null : trimToNull(req.getStatus()));
query.setFilters(resolveFilters(form, req == null ? null : req.getFilters()));
return query;
@@ -283,13 +283,7 @@ public class AnswerReadService {
* 普通用户只能看自己的答卷(submitter_id=会话用户),无数据管理权。
*/
public List listBySubmitter(BoaUser user, String formCode, String appCode) {
- if (user == null || user.getId() == null) {
- throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
- }
- Long schId = sessionSchId(user);
- if (schId == null) {
- throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
- }
+ Long schId = FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属");
String app = (appCode == null || appCode.trim().isEmpty()) ? "form-center" : appCode.trim();
// 与填报页加载同一条解析(契约 §1 resolve 是读接口唯一入口):不存在=PARAM_ERROR、未发布/停用=CUSTOM
Form form = defService.resolvePublished(schId, app, formCode).form();
@@ -313,13 +307,7 @@ public class AnswerReadService {
*/
@Transactional
public void softDeleteBySubmitter(BoaUser user, String answerId) {
- if (user == null || user.getId() == null) {
- throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
- }
- Long schId = sessionSchId(user);
- if (schId == null) {
- throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
- }
+ Long schId = FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属");
Answer answer = requireLiveAnswer(schId, answerId);
if (!answer.getSubmitterId().equals(user.getId())) {
throw new BizException(ErrorCode.FORBIDDEN, "只能删除自己提交的答卷");
@@ -341,13 +329,7 @@ public class AnswerReadService {
*/
@Transactional
public ReadModels.RowView editBySubmitter(BoaUser user, String answerId, JsonNode values, String summary) {
- if (user == null || user.getId() == null) {
- throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
- }
- Long schId = sessionSchId(user);
- if (schId == null) {
- throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
- }
+ Long schId = FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属");
Answer answer = requireLiveAnswer(schId, answerId);
if (!answer.getSubmitterId().equals(user.getId())) {
throw new BizException(ErrorCode.FORBIDDEN, "只能编辑自己提交的答卷");
@@ -395,13 +377,7 @@ public class AnswerReadService {
* 所以填表人(学生)不授权任何管理模块行也能用自己的两视图。
*/
public List listMine(BoaUser user, boolean bySubject) {
- if (user == null || user.getId() == null) {
- throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
- }
- Long schId = sessionSchId(user);
- if (schId == null) {
- throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
- }
+ Long schId = FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属");
List answers = bySubject
? answerRepository.findBySchIdAndSubjectIdOrderByIdDesc(schId, user.getId())
: answerRepository.findBySchIdAndSubmitterIdOrderByIdDesc(schId, user.getId());
@@ -427,13 +403,7 @@ public class AnswerReadService {
* 改状态在这一面压根没有入口——V14/V29。
*/
public ReadModels.DetailView myDetail(BoaUser user, String answerId) {
- if (user == null || user.getId() == null) {
- throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
- }
- Long schId = sessionSchId(user);
- if (schId == null) {
- throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
- }
+ Long schId = FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属");
Answer answer = requireLiveAnswer(schId, answerId);
boolean mine = user.getId().equals(answer.getSubmitterId()) || user.getId().equals(answer.getSubjectId());
if (!mine) {
@@ -468,12 +438,6 @@ public class AnswerReadService {
return item;
}
- /** 会话校(Integer→Long 换算走已有字段,契约 §7 身份行:外部不传) */
- private static Long sessionSchId(BoaUser user) {
- Integer schId = user.getCurrentRole() == null ? null : user.getCurrentRole().getSchId();
- return schId == null ? null : Long.valueOf(schId);
- }
-
/** 表单头表静默取数:答卷归属的表单理论上一定在(P32 连软删的都还在),取不到就让表名列降级为空 */
private Form findFormQuietly(Long schId, String formUuid) {
try {
diff --git a/src/main/java/com/yunhai/formcenter/service/AnswerService.java b/src/main/java/com/yunhai/formcenter/service/AnswerService.java
index 1c7578a..f0c6026 100644
--- a/src/main/java/com/yunhai/formcenter/service/AnswerService.java
+++ b/src/main/java/com/yunhai/formcenter/service/AnswerService.java
@@ -138,7 +138,7 @@ public class AnswerService {
}
private AnswerModels.SubmitView doSubmit(BoaUser user, AnswerModels.SubmitRequest req, String idempotencyKey) {
- Long schId = requireSessionSchId(user);
+ Long schId = FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属,不能提交答卷");
Integer submitterId = user.getId();
String appCode = req.getAppCode() == null || req.getAppCode().trim().isEmpty()
? DEFAULT_APP_CODE : req.getAppCode().trim();
@@ -359,21 +359,11 @@ public class AnswerService {
return clean;
}
- private Long requireSessionSchId(BoaUser user) {
- if (user == null || user.getId() == null) {
- throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
- }
- if (user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) {
- throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属,不能提交答卷");
- }
- return Long.valueOf(user.getCurrentRole().getSchId());
- }
-
private boolean belongsToCaller(Answer answer, BoaUser user) {
// 会话校 + 提交人共同界定"这条幂等记录是不是你的"(uk 是库级的,跨账号命中不能放行)
+ Long sessionSch = FormAccessPolicy.sessionSchId(user);
return answer.getSubmitterId() != null && answer.getSubmitterId().equals(user.getId())
- && user.getCurrentRole() != null && user.getCurrentRole().getSchId() != null
- && answer.getSchId() != null && answer.getSchId().longValue() == user.getCurrentRole().getSchId();
+ && sessionSch != null && answer.getSchId() != null && answer.getSchId().equals(sessionSch);
}
private static String newUuid() {
diff --git a/src/main/java/com/yunhai/formcenter/service/DataScopeService.java b/src/main/java/com/yunhai/formcenter/service/DataScopeService.java
index 1c972f2..c23b9b0 100644
--- a/src/main/java/com/yunhai/formcenter/service/DataScopeService.java
+++ b/src/main/java/com/yunhai/formcenter/service/DataScopeService.java
@@ -10,8 +10,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
@@ -53,14 +51,6 @@ import java.util.Set;
@RequiredArgsConstructor
public class DataScopeService {
- /** 超管用户类型(同 FormAdminGuard:不是 roleId,是 UserBase 的用户类型) */
- private static final int SUPER_ADMIN_TYPE = 1;
-
- private static final int ROLE_TEACHER = 1;
- private static final int ROLE_SCHOOL_LEADER = 2;
- private static final int ROLE_STUDENT = 3;
- private static final int ROLE_ADMIN = 7;
-
private final List implementors;
private final AdminScopeSource adminScopeSource;
@@ -74,7 +64,7 @@ public class DataScopeService {
if (user == null || user.getId() == null) {
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
}
- if (user.getType() != null && user.getType() == SUPER_ADMIN_TYPE) {
+ if (FormAccessPolicy.isSuperAdmin(user)) {
return EnumSet.of(DataScope.ALL);
}
Set scopes = new LinkedHashSet<>();
@@ -86,7 +76,7 @@ public class DataScopeService {
// 2) 身份默认矩阵,roleId 逐个取默认后并起来(多来源取最宽)
int audience = form == null || form.getAudience() == null
? FormEnums.Audience.STUDENT.getCode() : form.getAudience();
- for (Integer roleId : identityRoleIds(user)) {
+ for (Integer roleId : FormAccessPolicy.identityRoleIds(user)) {
scopes.add(defaultScope(roleId, audience));
}
if (scopes.isEmpty()) {
@@ -133,7 +123,7 @@ public class DataScopeService {
if (form == null || form.getSchId() == null) {
return null;
}
- Long ownSch = sessionSchId(user);
+ Long ownSch = FormAccessPolicy.sessionSchId(user);
if (ownSch == null) {
// 会话给不出学校:既没有"本校表单"可认,SCHOOL 也没有比对基准——不给任何升级
return null;
@@ -153,32 +143,16 @@ public class DataScopeService {
return null;
}
- /**
- * 参与矩阵解析的身份:{@code roleIds}(用户全部正常态角色)并上会话 {@code currentRole.roleId}
- * ——会话身份理论上必在 roleIds 内,并一次是防 UserBase 两侧不一致时漏掉当前身份那一格。
- */
- private List identityRoleIds(BoaUser user) {
- List ids = new ArrayList<>();
- if (user.getRoleIds() != null) {
- ids.addAll(user.getRoleIds());
- }
- Integer current = user.getCurrentRole() == null ? null : user.getCurrentRole().getRoleId();
- if (current != null && !ids.contains(current)) {
- ids.add(current);
- }
- return ids.isEmpty() ? Collections.emptyList() : ids;
- }
-
/** 矩阵取格(任教班级在一期落到"仅本人提交",见类注释表格) */
private DataScope defaultScope(Integer roleId, int audience) {
boolean studentFacing = FormEnums.Audience.STUDENT.getCode() == audience;
if (roleId == null) {
return DataScope.SELF_SUBMITTED;
}
- if (roleId == ROLE_ADMIN || roleId == ROLE_SCHOOL_LEADER) {
+ if (roleId == FormAccessPolicy.ROLE_ADMIN || roleId == FormAccessPolicy.ROLE_SCHOOL_LEADER) {
return DataScope.ALL;
}
- if (roleId == ROLE_TEACHER) {
+ if (roleId == FormAccessPolicy.ROLE_TEACHER) {
if (!studentFacing) {
return DataScope.SELF_SUBMITTED;
}
@@ -186,16 +160,10 @@ public class DataScopeService {
log.debug("[数据范围] 教师×面向学生:任教班级一期未开放,按矩阵注记生效为仅本人提交");
return DataScope.SELF_SUBMITTED;
}
- if (roleId == ROLE_STUDENT && studentFacing) {
+ if (roleId == FormAccessPolicy.ROLE_STUDENT && studentFacing) {
return DataScope.ABOUT_ME;
}
// 学生×面向教师、家长(4)/工作号(6)/局领导(5)、以及未知角色:兜底
return DataScope.SELF_SUBMITTED;
}
-
- /** 会话校(Integer→Long 换算;无当前身份或无学校归属=null,两表升级不参与) */
- private static Long sessionSchId(BoaUser user) {
- Integer schId = user.getCurrentRole() == null ? null : user.getCurrentRole().getSchId();
- return schId == null ? null : Long.valueOf(schId);
- }
}
diff --git a/src/main/java/com/yunhai/formcenter/service/FillAccessService.java b/src/main/java/com/yunhai/formcenter/service/FillAccessService.java
index f536a1e..f5324ae 100644
--- a/src/main/java/com/yunhai/formcenter/service/FillAccessService.java
+++ b/src/main/java/com/yunhai/formcenter/service/FillAccessService.java
@@ -8,8 +8,6 @@ import com.yunhai.platform.utils.model.BoaUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
@@ -50,14 +48,6 @@ import java.util.Set;
@RequiredArgsConstructor
public class FillAccessService {
- /** 超管用户类型(同 {@link FormAdminGuard}/{@link DataScopeService}:不是 roleId,是 UserBase 的用户类型) */
- private static final int SUPER_ADMIN_TYPE = 1;
-
- private static final int ROLE_TEACHER = 1;
- private static final int ROLE_SCHOOL_LEADER = 2;
- private static final int ROLE_STUDENT = 3;
- private static final int ROLE_ADMIN = 7;
-
private final AdminScopeSource adminScopeSource;
/** 填报入口层({@link #resolve} 的元素;未命中任何层=空集,语义即任务书里的 NONE) */
@@ -83,17 +73,18 @@ public class FillAccessService {
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
}
Set tiers = EnumSet.noneOf(Tier.class);
- List roleIds = identityRoleIds(user);
- if (isSuperAdmin(user) || grantedByAdminTables(user, form) || holdsAdminRole(roleIds)) {
+ List roleIds = FormAccessPolicy.identityRoleIds(user);
+ if (FormAccessPolicy.isSuperAdmin(user) || grantedByAdminTables(user, form) || holdsAdminRole(roleIds)) {
tiers.add(Tier.ADMIN);
}
boolean studentFacing = FormEnums.Audience.STUDENT.getCode()
== (form == null || form.getAudience() == null
? FormEnums.Audience.STUDENT.getCode() : form.getAudience());
- if (studentFacing && roleIds.contains(ROLE_TEACHER)) {
+ if (studentFacing && roleIds.contains(FormAccessPolicy.ROLE_TEACHER)) {
tiers.add(Tier.MANAGE);
}
- if (studentFacing ? roleIds.contains(ROLE_STUDENT) : roleIds.contains(ROLE_TEACHER)) {
+ if (studentFacing ? roleIds.contains(FormAccessPolicy.ROLE_STUDENT)
+ : roleIds.contains(FormAccessPolicy.ROLE_TEACHER)) {
tiers.add(Tier.FILL);
}
return tiers;
@@ -137,37 +128,20 @@ public class FillAccessService {
* 基础表单的管理员同样是管理员;校域不变式({@code answer.sch_id}=会话校)也不因代填而变。
*/
private boolean grantedByAdminTables(BoaUser user, Form form) {
- Long ownSch = sessionSchId(user);
+ Long ownSch = FormAccessPolicy.sessionSchId(user);
if (ownSch != null && adminScopeSource.isSchoolAdmin(ownSch, user.getId())) {
return true;
}
return form != null && adminScopeSource.isFormAdmin(form.getUuid(), user.getId());
}
- /** 校领导(2)/管理员(7):平台标准角色里的管理身份(契约 §8 矩阵那两行同一个因子) */
+ /** 校领导(2)/管理员(7):平台标准角色里的管理身份(清单单点在 {@link FormAccessPolicy#SCHOOL_LEADER_ROLES}) */
private static boolean holdsAdminRole(List roleIds) {
- return roleIds.contains(ROLE_ADMIN) || roleIds.contains(ROLE_SCHOOL_LEADER);
- }
-
- /** 参与解析的身份:{@code roleIds}(全部正常态角色)并上会话 {@code currentRole.roleId}(同 DataScopeService) */
- private static List identityRoleIds(BoaUser user) {
- List ids = new ArrayList<>();
- if (user.getRoleIds() != null) {
- ids.addAll(user.getRoleIds());
+ for (Integer roleId : roleIds) {
+ if (roleId != null && FormAccessPolicy.SCHOOL_LEADER_ROLES.contains(roleId)) {
+ return true;
+ }
}
- Integer current = user.getCurrentRole() == null ? null : user.getCurrentRole().getRoleId();
- if (current != null && !ids.contains(current)) {
- ids.add(current);
- }
- return ids.isEmpty() ? Collections.emptyList() : ids;
- }
-
- private static boolean isSuperAdmin(BoaUser user) {
- return user.getType() != null && user.getType() == SUPER_ADMIN_TYPE;
- }
-
- private static Long sessionSchId(BoaUser user) {
- Integer schId = user.getCurrentRole() == null ? null : user.getCurrentRole().getSchId();
- return schId == null ? null : Long.valueOf(schId);
+ return false;
}
}
diff --git a/src/main/java/com/yunhai/formcenter/service/FillableFormService.java b/src/main/java/com/yunhai/formcenter/service/FillableFormService.java
index 680c15f..ac6f516 100644
--- a/src/main/java/com/yunhai/formcenter/service/FillableFormService.java
+++ b/src/main/java/com/yunhai/formcenter/service/FillableFormService.java
@@ -3,8 +3,6 @@ package com.yunhai.formcenter.service;
import com.yunhai.formcenter.entity.Form;
import com.yunhai.formcenter.model.MyModels;
import com.yunhai.formcenter.repository.FormRepository;
-import com.yunhai.platform.apistd.BizException;
-import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaUser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -60,7 +58,7 @@ public class FillableFormService {
* @return 已按 deadline/code 排好序的行集;无权限的人拿到的是空集而不是异常
*/
public List listFillable(BoaUser user, String appCode) {
- Long schId = requireSessionSchId(user);
+ Long schId = FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属");
String cleanApp = appCode == null ? "" : appCode.trim();
List domain = Form.PLATFORM_SCH_ID == schId
? Collections.singletonList(Form.PLATFORM_SCH_ID)
@@ -121,15 +119,4 @@ public class FillableFormService {
}
return rows;
}
-
- /** 会话校(同 {@code AnswerService} 的口径:未登录与无学校归属两段分开拒,文案与 my 面其余端点一致) */
- private static Long requireSessionSchId(BoaUser user) {
- if (user == null || user.getId() == null) {
- throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
- }
- if (user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) {
- throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
- }
- return Long.valueOf(user.getCurrentRole().getSchId());
- }
}
diff --git a/src/main/java/com/yunhai/formcenter/service/FormAccessPolicy.java b/src/main/java/com/yunhai/formcenter/service/FormAccessPolicy.java
new file mode 100644
index 0000000..0c231b2
--- /dev/null
+++ b/src/main/java/com/yunhai/formcenter/service/FormAccessPolicy.java
@@ -0,0 +1,151 @@
+package com.yunhai.formcenter.service;
+
+import com.yunhai.formcenter.enums.FormEnums;
+import com.yunhai.platform.apistd.BizException;
+import com.yunhai.platform.utils.exception.ErrorCode;
+import com.yunhai.platform.utils.model.BoaUser;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 会话身份推导的唯一出口(契约 v2 §2.5 / T12):把"这个会话是谁、属于哪个校、算哪几种身份"
+ * 从原先六处各自抄一遍的私有实现收编成一处,角色魔法数字同样单点定义(B3/B5)。
+ *
+ * 本批次(T12)只做收编,不做语义:判定逻辑、拒绝文案与各调用点原有实现逐字等价,
+ * 语义切换(两档范围、audience 门、校管理员判定)在 T13 于本门面之上进行。
+ *
+ * 形态说明:会话/身份推导是静态单点方法——原实现本就是各调用类的 private static
+ * (纯字段换算,无协作者),做成实例方法要给每个消费方加构造器入参,会连带改动 T1~T11 期间
+ * 直接 new 这些服务的单测,与"零语义变化"批次的判据冲突。需要协作者(两表)的
+ * {@link #isSchoolAdmin(BoaUser)} 保持实例方法,由 Spring 装配消费方注入。
+ *
+ * 红线(V58):全库只允许本类持有角色数字常量与校领导清单;新守卫一律调本门面,
+ * 不许再抄 {@code type==1} / {@code roleIds.contains(2)} 这类字面量。
+ */
+@Component
+@RequiredArgsConstructor
+public class FormAccessPolicy {
+
+ /** 超管用户类型(UserBase {@code UserTypeEnum.SUPER_ADMIN};不是 roleId) */
+ public static final int SUPER_ADMIN_TYPE = 1;
+
+ /** 平台标准角色:教师 */
+ public static final int ROLE_TEACHER = 1;
+ /** 平台标准角色:校领导 */
+ public static final int ROLE_SCHOOL_LEADER = 2;
+ /** 平台标准角色:学生 */
+ public static final int ROLE_STUDENT = 3;
+ /** 平台标准角色:管理员 */
+ public static final int ROLE_ADMIN = 7;
+
+ /**
+ * 校领导角色清单——临时值,P-B 待 UserBase 正名(G2 裁决:现网三处既用值升格为单点常量,
+ * 正名后只改这一处,调用面与语义零变化)。
+ */
+ public static final Set SCHOOL_LEADER_ROLES =
+ Collections.unmodifiableSet(new HashSet<>(Arrays.asList(ROLE_SCHOOL_LEADER, ROLE_ADMIN)));
+
+ private final AdminScopeSource adminScopeSource;
+
+ /** 会话学校;无当前身份、无学校归属或会话为空 → null(调用侧自行决定拒绝口径) */
+ public static Long sessionSchId(BoaUser user) {
+ Integer schId = user == null || user.getCurrentRole() == null ? null : user.getCurrentRole().getSchId();
+ return schId == null ? null : Long.valueOf(schId);
+ }
+
+ /**
+ * 会话学校(必须给得出的形态):未登录 {@code UNAUTHORIZED},无学校归属 {@code FORBIDDEN}。
+ *
+ * @param noSchoolMessage 各调用面原有文案(T12 统一文案未开始,逐点沿用)
+ */
+ public static Long requireSessionSchId(BoaUser user, String noSchoolMessage) {
+ if (user == null || user.getId() == null) {
+ throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
+ }
+ Long schId = sessionSchId(user);
+ if (schId == null) {
+ throw new BizException(ErrorCode.FORBIDDEN, noSchoolMessage);
+ }
+ return schId;
+ }
+
+ /** 会话用户 id(平台全局身份,非本库自增主键);未登录为 null */
+ public static Integer uid(BoaUser user) {
+ return user == null ? null : user.getId();
+ }
+
+ /** 超管(UserBase 用户类型判定;不是 roleId) */
+ public static boolean isSuperAdmin(BoaUser user) {
+ return user != null && user.getType() != null && user.getType() == SUPER_ADMIN_TYPE;
+ }
+
+ /**
+ * 参与身份解析的角色集合:{@code roleIds}(全部正常态角色)并上会话 {@code currentRole.roleId}。
+ * 并一次是防 UserBase 两侧不一致时漏掉当前身份那一格(原 {@code DataScopeService} 口径)。
+ */
+ public static List identityRoleIds(BoaUser user) {
+ if (user == null) {
+ return Collections.emptyList();
+ }
+ List ids = new ArrayList<>();
+ if (user.getRoleIds() != null) {
+ ids.addAll(user.getRoleIds());
+ }
+ Integer current = user.getCurrentRole() == null ? null : user.getCurrentRole().getRoleId();
+ if (current != null && !ids.contains(current)) {
+ ids.add(current);
+ }
+ return ids;
+ }
+
+ /**
+ * 会话持有的填报身份类型(多身份取并集):含学生角色(3)→{@code STUDENT}、含教师角色(1)→{@code TEACHER}。
+ *
+ * 返回值永不含 {@code ANONYMOUS}——匿名只对匿名通道生效,登录会话不可能持有它(C1)。
+ * 家长(4)/局领导(5)/工作号(6) 与未知角色不产生任何填报身份。
+ */
+ public static Set identityTypes(BoaUser user) {
+ Set types = EnumSet.noneOf(FormEnums.Audience.class);
+ for (Integer roleId : identityRoleIds(user)) {
+ if (roleId == null) {
+ continue;
+ }
+ if (roleId == ROLE_STUDENT) {
+ types.add(FormEnums.Audience.STUDENT);
+ } else if (roleId == ROLE_TEACHER) {
+ types.add(FormEnums.Audience.TEACHER);
+ }
+ }
+ return types;
+ }
+
+ /**
+ * 是否校管理员(契约 v2 §2.2):超管 ∪ 校领导角色清单命中 ∪ {@code school_admin} 表命中会话校。
+ *
+ * T12 仅落定义,消费方在 T13 的两档范围解析与管理面守卫切换里接入;
+ * {@code school_admin} 表被删空时天然角色仍是兜底。
+ */
+ public boolean isSchoolAdmin(BoaUser user) {
+ if (user == null) {
+ return false;
+ }
+ if (isSuperAdmin(user)) {
+ return true;
+ }
+ for (Integer roleId : identityRoleIds(user)) {
+ if (roleId != null && SCHOOL_LEADER_ROLES.contains(roleId)) {
+ return true;
+ }
+ }
+ Long schId = sessionSchId(user);
+ return schId != null && user.getId() != null && adminScopeSource.isSchoolAdmin(schId, user.getId());
+ }
+}
diff --git a/src/main/java/com/yunhai/formcenter/service/FormAdminGuard.java b/src/main/java/com/yunhai/formcenter/service/FormAdminGuard.java
index dbb009f..8921c6d 100644
--- a/src/main/java/com/yunhai/formcenter/service/FormAdminGuard.java
+++ b/src/main/java/com/yunhai/formcenter/service/FormAdminGuard.java
@@ -35,9 +35,6 @@ import java.util.Set;
@RequiredArgsConstructor
public class FormAdminGuard {
- /** 超管用户类型(UserBase UserTypeEnum.SUPER_ADMIN) */
- private static final int SUPER_ADMIN_TYPE = 1;
-
/** 定义管理模块行 path(手册 §2.2:表单设计) */
public static final String MODULE_FORM_DESIGN = "/admin/forms";
/** 数据管理模块行 path */
@@ -63,17 +60,17 @@ public class FormAdminGuard {
if (user == null || user.getId() == null) {
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
}
- if (isSuperAdmin(user)) {
+ if (FormAccessPolicy.isSuperAdmin(user)) {
if (targetSchId != null) {
return targetSchId;
}
- Long current = currentSchId(user);
+ Long current = FormAccessPolicy.sessionSchId(user);
if (current != null) {
return current;
}
throw new BizException(ErrorCode.PARAM_ERROR, "超管请显式指定 schId(当前身份无学校归属)");
}
- Long current = currentSchId(user);
+ Long current = FormAccessPolicy.sessionSchId(user);
if (!hasModule(user, modulePath)) {
throw new BizException(ErrorCode.FORBIDDEN, "无该模块权限:" + modulePath);
}
@@ -105,10 +102,10 @@ public class FormAdminGuard {
if (user == null || user.getId() == null) {
return false;
}
- if (isSuperAdmin(user)) {
+ if (FormAccessPolicy.isSuperAdmin(user)) {
return true;
}
- if (currentSchId(user) == null) {
+ if (FormAccessPolicy.sessionSchId(user) == null) {
return false;
}
Set paths = authorizedPaths(user);
@@ -128,7 +125,7 @@ public class FormAdminGuard {
if (modulePaths == null || modulePaths.length == 0) {
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "模块行清单为空(调用方写错了)");
}
- if (isSuperAdmin(user)) {
+ if (FormAccessPolicy.isSuperAdmin(user)) {
return requireAdmin(user, targetSchId, modulePaths[0]);
}
Set held = authorizedPaths(user);
@@ -162,7 +159,7 @@ public class FormAdminGuard {
if (user == null || user.getId() == null) {
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
}
- if (isSuperAdmin(user)) {
+ if (FormAccessPolicy.isSuperAdmin(user)) {
// 超管的域语义原样交给 requireAdmin:显式目标域(含 0=平台级)优先,不在这里换默认值
return requireAdmin(user, targetSchId, MODULE_DATA_MANAGE);
}
@@ -170,7 +167,7 @@ public class FormAdminGuard {
// 取不到表单与"不是你域内的表单"同一口径:存在性本身就是探测面(契约 §1 uuid 不可枚举)
throw new BizException(ErrorCode.PARAM_ERROR, "表单不存在");
}
- Long current = currentSchId(user);
+ Long current = FormAccessPolicy.sessionSchId(user);
if (current == null) {
throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属,不能确定查询域");
}
@@ -189,17 +186,6 @@ public class FormAdminGuard {
/** 是否被授权某个模块行(path 精确匹配;超管直接放行) */
private boolean hasModule(BoaUser user, String modulePath) {
- return isSuperAdmin(user) || authorizedPaths(user).contains(modulePath);
- }
-
- private boolean isSuperAdmin(BoaUser user) {
- return user.getType() != null && user.getType() == SUPER_ADMIN_TYPE;
- }
-
- private Long currentSchId(BoaUser user) {
- if (user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) {
- return null;
- }
- return Long.valueOf(user.getCurrentRole().getSchId());
+ return FormAccessPolicy.isSuperAdmin(user) || authorizedPaths(user).contains(modulePath);
}
}
diff --git a/src/main/java/com/yunhai/formcenter/service/FormFileService.java b/src/main/java/com/yunhai/formcenter/service/FormFileService.java
index 1a24c0a..cbe5e07 100644
--- a/src/main/java/com/yunhai/formcenter/service/FormFileService.java
+++ b/src/main/java/com/yunhai/formcenter/service/FormFileService.java
@@ -103,7 +103,7 @@ public class FormFileService {
throw new BizException(ErrorCode.PARAM_ERROR,
"单文件不得超过 50MB(当前 " + (file.getSize() / 1024 / 1024) + "MB)");
}
- Long schId = requireSessionSchId(user);
+ Long schId = FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属,不能上传附件");
String original = sanitizeName(file.getOriginalFilename());
String ext = requireSafeExt(original);
String key = "form/" + LocalDate.now().format(YEAR) + "/" + LocalDate.now().format(MONTH)
@@ -197,14 +197,4 @@ public class FormFileService {
}
return sb.toString();
}
-
- private Long requireSessionSchId(BoaUser user) {
- if (user == null || user.getId() == null) {
- throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
- }
- if (user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) {
- throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属,不能上传附件");
- }
- return Long.valueOf(user.getCurrentRole().getSchId());
- }
}