t12:FormAccessPolicy 门面收敛——会话推导六处+角色常量三处收编,零语义变化
会话校推导/身份角色并集/超管判定收进门面单点,角色常量与校领导临时清单 {2,7}
同址(V58 单点);判定逻辑与拒绝文案逐点沿用原实现,既有 272 例测试一字未改全绿。
清单外补件:FormFileService 的 requireSessionSchId 是 B3 未列的第七处,一并收编。
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ReadModels.RowView> 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 {
|
||||
* 所以填表人(学生)不授权任何管理模块行也能用自己的两视图。</p>
|
||||
*/
|
||||
public List<MyModels.ItemView> 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<Answer> 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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<AnswerScopeImplementor> 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<DataScope> 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<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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Tier> tiers = EnumSet.noneOf(Tier.class);
|
||||
List<Integer> roleIds = identityRoleIds(user);
|
||||
if (isSuperAdmin(user) || grantedByAdminTables(user, form) || holdsAdminRole(roleIds)) {
|
||||
List<Integer> 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}=会话校)也不因代填而变。</p>
|
||||
*/
|
||||
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<Integer> roleIds) {
|
||||
return roleIds.contains(ROLE_ADMIN) || roleIds.contains(ROLE_SCHOOL_LEADER);
|
||||
}
|
||||
|
||||
/** 参与解析的身份:{@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());
|
||||
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.<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);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MyModels.FillableRow> listFillable(BoaUser user, String appCode) {
|
||||
Long schId = requireSessionSchId(user);
|
||||
Long schId = FormAccessPolicy.requireSessionSchId(user, "当前身份无学校归属");
|
||||
String cleanApp = appCode == null ? "" : appCode.trim();
|
||||
List<Long> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user