t12:FormAccessPolicy 门面收敛——会话推导六处+角色常量三处收编,零语义变化

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