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()); - } }