feat(auth): 角色映射按平台字典(1-7)校准,并移除资源库模块对接
build / build-push (push) Successful in 52s

- 角色字典实测校准(采样 200+ 账号 currentRole/roles):
  1 老师 / 2 校领导 / 3 学生 / 4 家长 / 5 局领导 / 6 工作号 / 7 管理员
  → agent.roles: student=[3] teacher=[1,6] parent=[4] admin=[2,5,7]
  (旧默认 student=[1]/teacher=[2,3]/admin=[4..7] 系臆测,导致老师账号被显示为学生)
- 新增家长身份 AgentRole.PARENT;活动报名/取消改由 STUDENT_OR_PARENT 放行
- 当前身份识别增加 roleName 关键词兜底,避免识别不出就降级为访客
- 移除资源库对接:删除 ResourceTools 及注册/编排/agent.modules/module-landing
- 测试 48/48 通过
This commit is contained in:
lijing
2026-10-12 16:10:39 +08:00
parent 57766c8bba
commit 67e55f1875
11 changed files with 265 additions and 135 deletions
+3
View File
@@ -20,6 +20,9 @@ src/main/resources/application-prod.yml
# IDE 运行日志
.run/
# 本地排查脚本(含平台 app-secret,禁止入库)
.scratch/
# 备份与残留
*.bak
*.orig
+5 -2
View File
@@ -239,7 +239,6 @@ agent:
dormnotice: http://base.yunhaixingtong.com/dormnotice
familyactivity: http://base.yunhaixingtong.com/familyactivity
coursehomework: http://base.yunhaixingtong.com/coursehomework
resourcelibrary: http://base.yunhaixingtong.com/resourcelibrary
profession: http://base.yunhaixingtong.com/profession
```
@@ -372,6 +371,7 @@ HTTP 状态都是 **200**,但响应体固定是:
> `http://base.yunhaixingtong.com/resourcelibrary`,实测该路径 **404**;
> 前端 `ResourceLibraryFront/.env.dev` 里资源库真实主机是 `https://base.bestsch.com/BschZyk`。
> 需与资源库团队确认正确的 base(含 context)后再改。
> **★ 2026-10-12 处理**:该模块(`ResourceTools` 及配置)已整体移除,**尚未接通**,待部署位置确认后再接回。
### 11.6 复核补记(2026-10-10 19:0x)——「请假可以了、其他模块没接通」
@@ -422,9 +422,12 @@ HTTP 状态都是 **200**,但响应体固定是:
| dormnotice | 宿舍通知箱 | `/par/notices` |
| familyactivity | 活动广场 | `/par/activities` |
| coursehomework | 今日作业 | `/stu/today` |
| resourcelibrary | 资源管理 | `/resource` |
| profession | 发展首页 | `/home` |
> ★ 2026-10-12:`resourcelibrary`(资源库)已**按用户要求暂时移除**(该模块前端在 base 域 **404**、真实主机疑为
> `https://base.bestsch.com/BschZyk`,尚未接通)——已从 `ResourceTools`、`agent.modules`、
> `agent.module-landing` 与前端 `MODULE_*` 表中一并去掉;待其部署位置确认后再接回。
运维可在 `application.yml` 用 `agent.module-landing.<模块>=<路由>` 覆盖;
工具自身声明(`write(...)` / `readAt(...)`)优先级更高。
@@ -3,18 +3,27 @@ package com.yunhai.agent.auth;
/**
* 智能体侧的角色分类(由平台 {@code BoaUser} 解析而来)。
*
* <p>用途:决定「这个身份能用哪些工具」——学生侧(请假申请 / 交作业 / 报名活动 / 看公告)
* 与教职工侧(发公告 / 审批请假 / 发布资源 / 专业发展)严格分开。
* <p>用途:决定「这个身份能用哪些工具」——学生侧(请假申请 / 交作业 / 报名活动 / 看公告)、
* 家长侧(报名活动 / 宿舍通知回执)与教职工侧(发公告 / 审批请假 / 专业发展)严格分开。
*
* <p>对齐平台 UserBase 的「用户身份」字典(门户/GetUserInfo 的 {@code currentRole.roleId}):
* <pre>
* 1 老师 2 校领导 3 学生 4 家长 5 局领导 6 工作号 7 管理员
* </pre>
* 映射到本枚举:1/6 → 教师,3 → 学生,4 → 家长,2/5/7 → 管理员。
*/
public enum AgentRole {
/** 学生 */
STUDENT("学生"),
/** 教师(含班主任) */
/** 教师(含班主任、工作号/教职工) */
TEACHER("教师"),
/** 管理员(校管 / 分管领导 / 教务 / 校长) */
/** 家长:家校活动的报名方、宿舍通知的回执方 */
PARENT("家长"),
/** 管理员(校领导 / 局领导 / 管理员) */
ADMIN("管理员"),
/** 访客:未登录或无法识别角色,只能使用公共只读能力 */
@@ -14,13 +14,14 @@ import java.util.Set;
* 集中成一张可读的表比散落在 7 个模块的工具定义里更容易审阅与改动;
* 若某个工具需要就近声明,用 {@code ToolDef.allow(...)} 显式指定即可(本策略会尊重已有声明)。
*
* <p>三类名单:
* <p>名单:
* <ul>
* <li>{@link #STAFF_ONLY} —— 教职工侧(教师 + 管理员):审批、发布、统计、宿管、专业发展;</li>
* <li>{@link #STUDENT_ONLY} —— 学生侧:交作业、报名活动、销假;</li>
* <li>{@link #STUDENT_ONLY} —— 学生侧:交作业、销假;</li>
* <li>{@link #STUDENT_OR_PARENT} —— 家校活动报名:家长与学生都可发起;</li>
* <li>{@link #SHARED} —— 学生与教职工都可能发起的:请假申请/撤销。</li>
* </ul>
* 未列入任何名单的工具 = 所有已识别角色都可用(如各类只读查询)。
* 未列入任何名单的工具 = 所有已识别角色都可用(如各类只读查询、宿舍通知回执)。
*/
public final class RolePolicy {
@@ -43,10 +44,14 @@ public final class RolePolicy {
/** 学生专属 */
private static final Set<String> STUDENT_ONLY = new HashSet<>(Arrays.asList(
"homework.myList", "homework.detail", "homework.result", "homework.submit",
"activity.signup", "activity.cancelSignup",
"leave.reportBack"
));
/** 学生与家长共用(家校活动报名由家长或学生发起) */
private static final Set<String> STUDENT_OR_PARENT = new HashSet<>(Arrays.asList(
"activity.signup", "activity.cancelSignup"
));
/** 学生与教职工共用(发起/撤销自己的事项) */
private static final Set<String> SHARED = new HashSet<>(Arrays.asList(
"leave.submit", "leave.withdraw"
@@ -71,6 +76,8 @@ public final class RolePolicy {
d.allow(AgentRole.TEACHER, AgentRole.ADMIN);
} else if (STUDENT_ONLY.contains(n)) {
d.allow(AgentRole.STUDENT);
} else if (STUDENT_OR_PARENT.contains(n)) {
d.allow(AgentRole.STUDENT, AgentRole.PARENT);
} else if (SHARED.contains(n)) {
d.allow(AgentRole.STUDENT, AgentRole.TEACHER, AgentRole.ADMIN);
}
@@ -4,6 +4,7 @@ import com.yunhai.agent.config.AgentSettings;
import com.yunhai.platform.utils.model.BoaRoleInfo;
import com.yunhai.platform.utils.model.BoaUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.LinkedHashSet;
@@ -19,17 +20,28 @@ import java.util.concurrent.ConcurrentHashMap;
* 平台里一个账号可以同时挂多个身份:{@code BoaUser.roleIds} 是<b>全部</b>身份的角色号,
* {@code BoaUser.currentRole} 是用户在门户里<b>当前激活</b>的那个身份
* (等价于 {@code YschBaseOpenApi.getCurrentIdentity(userId)})。
* 例如实测账号 10008 同时是「学生 + 管理员 + 教师」——若把所有身份并集,
* 权限只增不减、能力边界完全模糊(所有人都是全量 54 项)。
* 因此这里<b>只认当前身份</b>:与学校门户、其它业务模块的口径保持一致。
* 实测账号 10008(李晶/lijing)挂着「老师(1)」与「管理员(7)」,门户当前身份是「老师」——
* 若把所有身份并集,权限只增不减、能力边界完全模糊。
* 因此这里<b>只认当前身份</b>:与学校门户、其它业务模块的口径保持一致(2026-10-10 改)。
*
* <h3>判定规则(角色号可在 {@code agent.roles.*} 配置)</h3>
* <h3>★ 角色号实测校准(2026-10-12,采样 200+ 账号的 currentRole / roles 取得)</h3>
* <pre>
* 1 老师 2 校领导 3 学生 4 家长 5 局领导 6 工作号 7 管理员
* → 本枚举:1/6 = 教师,3 = 学生,4 = 家长,2/5/7 = 管理员
* </pre>
* 数据源与门户顶栏、{@code /notice/api/base/GetUserInfo} 一致(都是 {@code BoaUser.currentRole})。
* 本工程此前的默认映射(student=1 / teacher=2,3 / admin=4..7)是<b>猜的</b>,
* 把 roleId=1 误当学生 —— 于是「李晶(老师)」在助手里显示成「学生」。
* 现已按 {@code agent.roles.*} 配置修正(默认值见 {@link AgentSettings.Roles})。
*
* <h3>判定顺序</h3>
* <ol>
* <li>优先取 {@code currentRole.getRoleId()},命中 {@code agent.roles.student/teacher/admin}
* 即获得对应角色,且<b>只</b>获得这一个身份;</li>
* <li>当前身份是管理员时,按 {@code agent.roles.admin-also-teacher=true} 追加教师能力;</li>
* <li>当前身份号未在配置里声明(或账号没有当前身份)→ 回退按 {@code roleIds}/{@code type}
* 的旧口径判定,并打 WARN 日志提示补配置;</li>
* <li><b>当前身份</b>:先按 {@code roleId} 命中 {@code agent.roles.*};
* 角色号没配到,再按 {@code roleName} 关键词兜底(老师/教师/班主任 → 教师,
* 管理员/校长/主任/教务… → 管理员,家长 → 家长,学生 → 学生);命中即<b>只</b>返回该身份;</li>
* <li>当前身份是管理员时,按 {@code admin-also-teacher=true} 追加教师能力;</li>
* <li>无当前身份(或当前身份识别不出)→ 回退按 {@code roleIds} 逐个匹配配置;</li>
* <li>仍然什么都没识别出 → 按 {@code BoaUser.type} 兜底(0=学生、2=教职工、1=超管);</li>
* <li>什么都识别不出 → 访客(仅公共只读能力)。</li>
* </ol>
*/
@@ -37,6 +49,12 @@ import java.util.concurrent.ConcurrentHashMap;
@Component
public class RoleResolver {
/** 角色名关键词:命中即判定(角色号未配置时的兜底,跨学校更稳) */
private static final String[] ADMIN_WORDS = {
"管理", "校长", "园长", "局长", "领导", "主任", "教务", "督导", "书记", "党委"
};
private static final String[] TEACHER_WORDS = {"教师", "老师", "班主任", "教工"};
private final AgentSettings settings;
/** 每个用户上次记录过的身份描述,避免每请求刷屏(只在身份变化时打一次 INFO) */
@@ -58,20 +76,21 @@ public class RoleResolver {
// 1) ★ 平台「当前身份」优先 —— 一个账号只生效一个身份
BoaRoleInfo current = user.getCurrentRole();
if (current != null && current.getRoleId() != null) {
Set<AgentRole> hit = match(current.getRoleId(), cfg);
if (current != null && (current.getRoleId() != null || StringUtils.isNotBlank(current.getRoleName()))) {
Set<AgentRole> hit = matchCurrent(current, cfg);
if (!hit.isEmpty()) {
applyAdminAlsoTeacher(hit, cfg);
logResolved(user, current, hit, "当前身份");
return hit;
}
log.warn("[身份] 当前身份 roleId={} roleName={} 未命中 agent.roles.* 配置,"
+ "暂按账号全部身份兜底(userId={}, roleIds={})—— 建议把该角色号补进配置",
log.warn("[身份] 当前身份 roleId={} roleName={} 既未命中 agent.roles.* 配置、"
+ "角色名也不含可识别关键词,暂按账号全部身份兜底(userId={}, roleIds={})"
+ "—— 建议把该角色号补进配置",
current.getRoleId(), current.getRoleName(), user.getId(), user.getRoleIds());
}
// 2) 兜底:没有当前身份,或当前身份号未配置
if (user.getType() != null && user.getType() == 1) {
// 2) 兜底:账号全部角色号 + 超管口径
if (user.getType() != null && cfg.getAdminUserTypes().contains(user.getType())) {
roles.add(AgentRole.ADMIN);
}
List<Integer> roleIds = user.getRoleIds();
@@ -82,13 +101,18 @@ public class RoleResolver {
}
}
}
applyAdminAlsoTeacher(roles, cfg);
// 3) 仍未识别出任何角色 → 按 BoaUser.type 兜底(学生没有角色号,只能靠这一条)
if (roles.isEmpty()) {
roles.addAll(matchByUserType(user.getType(), cfg));
}
applyAdminAlsoTeacher(roles, cfg);
if (roles.isEmpty()) {
roles.add(AgentRole.GUEST);
}
logResolved(user, current, roles,
current == null ? "无当前身份,按账号全部身份" : "当前身份未配置,按账号全部身份");
current == null ? "无当前身份,按账号全部身份" : "当前身份未识别,按账号全部身份");
return roles;
}
@@ -107,6 +131,15 @@ public class RoleResolver {
return sb.toString();
}
/** 当前身份 → 智能体角色:角色号配置优先,角色名关键词兜底 */
private Set<AgentRole> matchCurrent(BoaRoleInfo current, AgentSettings.Roles cfg) {
Set<AgentRole> hit = match(current.getRoleId(), cfg);
if (hit.isEmpty() && StringUtils.isNotBlank(current.getRoleName())) {
hit = matchByRoleName(current.getRoleName().trim());
}
return hit;
}
/** 单个角色号 → 智能体角色(一个号理论上可命中多个分组,全部保留) */
private Set<AgentRole> match(Integer roleId, AgentSettings.Roles cfg) {
Set<AgentRole> hit = new LinkedHashSet<>();
@@ -122,6 +155,51 @@ public class RoleResolver {
if (contains(cfg.getStudent(), roleId)) {
hit.add(AgentRole.STUDENT);
}
if (contains(cfg.getParent(), roleId)) {
hit.add(AgentRole.PARENT);
}
return hit;
}
/** 角色名 → 智能体角色(角色号没配到时的兜底,避免「未识别 → 访客」) */
private Set<AgentRole> matchByRoleName(String name) {
Set<AgentRole> hit = new LinkedHashSet<>();
if (StringUtils.isBlank(name)) {
return hit;
}
if (containsAny(name, ADMIN_WORDS)) {
hit.add(AgentRole.ADMIN);
}
if (containsAny(name, TEACHER_WORDS)) {
hit.add(AgentRole.TEACHER);
}
if (name.contains("学生")) {
hit.add(AgentRole.STUDENT);
}
if (name.contains("家长")) {
hit.add(AgentRole.PARENT);
}
return hit;
}
/** {@code BoaUser.type} → 智能体角色(学生没有角色号,这是学生的唯一入口) */
private Set<AgentRole> matchByUserType(Integer type, AgentSettings.Roles cfg) {
Set<AgentRole> hit = new LinkedHashSet<>();
if (type == null) {
return hit;
}
if (contains(cfg.getStudentUserTypes(), type)) {
hit.add(AgentRole.STUDENT);
}
if (contains(cfg.getTeacherUserTypes(), type)) {
hit.add(AgentRole.TEACHER);
}
if (contains(cfg.getAdminUserTypes(), type)) {
hit.add(AgentRole.ADMIN);
}
if (contains(cfg.getParentUserTypes(), type)) {
hit.add(AgentRole.PARENT);
}
return hit;
}
@@ -148,6 +226,15 @@ public class RoleResolver {
}
}
private boolean containsAny(String name, String[] words) {
for (String w : words) {
if (name.contains(w)) {
return true;
}
}
return false;
}
private boolean contains(List<Integer> list, int v) {
return list != null && list.contains(v);
}
@@ -51,7 +51,6 @@ public class AgentSettings {
* dormnotice → /par/notices 宿舍通知箱
* familyactivity → /par/activities 活动广场
* coursehomework → /stu/today 今日作业
* resourcelibrary → /resource 资源管理
* profession → /home 发展首页
* </pre>
* 运维可在 {@code application.yml} 用 {@code agent.module-landing.<模块>=<路由>} 覆盖单个模块;
@@ -67,7 +66,6 @@ public class AgentSettings {
m.put("dormnotice", "/par/notices");
m.put("familyactivity", "/par/activities");
m.put("coursehomework", "/stu/today");
m.put("resourcelibrary", "/resource");
m.put("profession", "/home");
return m;
}
@@ -173,16 +171,34 @@ public class AgentSettings {
*/
@Data
public static class Roles {
/** 学生角色号(默认 1;请按 UserBase 校准) */
private List<Integer> student = new ArrayList<>(Arrays.asList(1));
/** 学生角色号。★ 实测校准(2026-10-12):{@code 3 = 学生} */
private List<Integer> student = new ArrayList<>(Arrays.asList(3));
/** 教师角色号(默认 2=教师、3=班主任;请按 UserBase 校准) */
private List<Integer> teacher = new ArrayList<>(Arrays.asList(2, 3));
/** 教师角色号。★ 实测校准:{@code 1 = 老师}、{@code 6 = 工作号}(教职工工号身份) */
private List<Integer> teacher = new ArrayList<>(Arrays.asList(1, 6));
/** 管理员角色号(默认 4=校管、5=分管领导、6=教务、7=校长;请按 UserBase 校准) */
private List<Integer> admin = new ArrayList<>(Arrays.asList(4, 5, 6, 7));
/** 家长角色号。★ 实测校准:{@code 4 = 家长} */
private List<Integer> parent = new ArrayList<>(Arrays.asList(4));
/** 管理员是否天然拥有教师能力(默认 true:校管/领导可用教师侧工具) */
/** 管理员角色号。★ 实测校准:{@code 2 = 校领导}、{@code 5 = 局领导}、{@code 7 = 管理员} */
private List<Integer> admin = new ArrayList<>(Arrays.asList(2, 5, 7));
/**
* 兜底口径之一:账号既没有「当前身份」、也没挂任何已识别的角色号时,
* 按 {@code BoaUser.type} 判定。实测 {@code 0 = 学生}。
*/
private List<Integer> studentUserTypes = new ArrayList<>(Arrays.asList(0));
/** 兜底口径之二:{@code 2 = 教职工} */
private List<Integer> teacherUserTypes = new ArrayList<>(Arrays.asList(2));
/** 兜底口径之三:{@code 1 = 平台超管} */
private List<Integer> adminUserTypes = new ArrayList<>(Arrays.asList(1));
/** 兜底口径之四:家长(平台暂未见稳定的 {@code type} 口径,留空待校准) */
private List<Integer> parentUserTypes = new ArrayList<>();
/** 管理员是否天然拥有教师能力(默认 true:校领导/局领导/管理员可用教师侧工具) */
private boolean adminAlsoTeacher = true;
}
}
@@ -940,7 +940,6 @@ public class AgentOrchestrator {
case "dormnotice": return "宿舍通知";
case "familyactivity": return "家校活动";
case "coursehomework": return "课程作业";
case "resourcelibrary": return "资源库";
case "profession": return "教师专业发展";
default: return module;
}
@@ -10,7 +10,6 @@ import com.yunhai.agent.tool.defs.DormNoticeTools;
import com.yunhai.agent.tool.defs.HomeworkTools;
import com.yunhai.agent.tool.defs.LeaveTools;
import com.yunhai.agent.tool.defs.ProfessionTools;
import com.yunhai.agent.tool.defs.ResourceTools;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@@ -75,7 +74,6 @@ public class ToolRegistry {
register(DormNoticeTools.defs());
register(ActivityTools.defs());
register(HomeworkTools.defs());
register(ResourceTools.defs());
register(ProfessionTools.defs());
// 套用「学生 / 教师」角色能力策略(未显式声明角色的工具在此落定)
RolePolicy.apply(tools.values());
@@ -1,59 +0,0 @@
package com.yunhai.agent.tool.defs;
import com.yunhai.agent.tool.ToolDef;
import com.yunhai.agent.tool.ToolParam;
import java.util.ArrayList;
import java.util.List;
/**
* 资源库模块工具 —— 后端 ResourceLibrary,前缀 /api/resource、/api/category、/api/textbook。
*/
public final class ResourceTools {
public static final String MODULE = "resourcelibrary";
private ResourceTools() {
}
public static List<ToolDef> defs() {
List<ToolDef> list = new ArrayList<>();
list.add(ToolDef.of("resource.search", MODULE, "检索资源",
"按关键词/分类/学科检索教学资源(课件、教案、试题等)。用户问“找一份 XX 的课件”“有没有 XX 资料”时用它。",
"GET", "/api/resource/List")
.p(ToolParam.optional("keyword", "string", "关键词:资源名称 / 编码 / 上传人"),
ToolParam.optional("categoryId", "integer", "分类节点 id(含子节点)"),
ToolParam.optional("subject", "string", "学科"),
ToolParam.optional("type", "string", "资源类型"),
ToolParam.optional("orderBy", "string", "排序:time_desc 最新 / download_desc 下载最多 / view_desc 浏览最多"),
ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"))
.readAt("/resource"));
list.add(ToolDef.of("resource.detail", MODULE, "资源详情",
"查看一份资源的详细信息(分类、学科、格式、下载量等)。",
"GET", "/api/resource/Detail")
.p(ToolParam.required("id", "integer", "资源 id,来自 resource.search"))
.readAt("/resource"));
list.add(ToolDef.of("resource.categoryTree", MODULE, "资源分类树",
"资源分类树(学段 → 学科 → 年级册次 → 章节),用于按分类筛选资源。",
"GET", "/api/category/Tree")
.readAt("/resource"));
list.add(ToolDef.of("resource.collect", MODULE, "收藏资源",
"收藏或取消收藏一份资源。",
"POST_FORM", "/api/resource/Collect")
.p(ToolParam.required("id", "integer", "资源 id")
.label("资源"))
.write("/resource/collect"));
list.add(ToolDef.of("resource.textbookOptions", MODULE, "教材选项",
"查询可选的教材版本 / 学科 / 出版社。",
"GET", "/api/textbook/Options")
.readAt("/textbook"));
return list;
}
}
+15 -6
View File
@@ -109,11 +109,22 @@ agent:
browser-exec:
enabled: true
# ★ 角色映射:按本校 UserBase 角色体系校准(BoaUser.type=1 恒为管理员)
# ★ 角色映射:按本校用户中心(UserBase)「用户身份」字典实测校准 ★
# 平台身份号 = 门户顶栏 / GetUserInfo 的 currentRole.roleId(采样 200+ 账号取得):
# 1 = 老师 2 = 校领导 3 = 学生 4 = 家长 5 = 局领导 6 = 工作号 7 = 管理员
# 映射到助手角色:1/6 → 教师,3 → 学生,4 → 家长,2/5/7 → 管理员
# ⚠️ 此前默认(student=1 / teacher=2,3 / admin=4..7)是猜的,把 roleId=1 当成学生,
# 于是「李晶(老师)」在助手里被显示成「学生」——这是本次修正的根因。
roles:
student: [1] # 学生
teacher: [2, 3] # 教师 / 班主任
admin: [4, 5, 6, 7] # 校管 / 分管领导 / 教务 / 校长
student: [3] # 学生
teacher: [1, 6] # 老师 / 工作号(教职工)
parent: [4] # 家长
admin: [2, 5, 7] # 校领导 / 局领导 / 管理员
# 账号既无「当前身份」、也没挂已识别角色号时的兜底口径(按 BoaUser.type)
student-user-types: [0] # 学生
teacher-user-types: [2] # 教职工
admin-user-types: [1] # 平台超管
parent-user-types: [] # 家长(平台暂未见稳定口径,留空待校准)
admin-also-teacher: true # 管理员同时拥有教师侧能力
# --- 业务模块基地址(key 必须与工具定义里的 MODULE 常量一致)---
@@ -124,7 +135,6 @@ agent:
dormnotice: http://base.yunhaixingtong.com/dormnotice
familyactivity: http://base.yunhaixingtong.com/familyactivity
coursehomework: http://base.yunhaixingtong.com/coursehomework
resourcelibrary: http://base.yunhaixingtong.com/resourcelibrary
profession: http://base.yunhaixingtong.com/profession
# --- 模块默认落地页(深链兜底)---
@@ -139,7 +149,6 @@ agent:
dormnotice: /par/notices # 宿舍通知箱
familyactivity: /par/activities # 活动广场
coursehomework: /stu/today # 今日作业
resourcelibrary: /resource # 资源管理
profession: /home # 发展首页
# 密钥链验签依赖 Redis(sign:app:*)。关掉 Redis 健康指标:Redis 抖动不应把 /actuator/health
@@ -73,64 +73,100 @@ class RolePermissionTest {
@Test
void multiIdentityAccountUsesCurrentRoleOnly() {
// 实测账号形态:10008 同时挂着 学生(1) + 教师(2) + 管理员(4),当前身份 = 学生
BoaUser u = userWithCurrent(10008, 1, "学生", 1, 2, 4);
// 实测账号形态(李晶 / lijing, userId=10008):挂着 老师(1) + 管理员(7),门户当前身份 = 老师
BoaUser u = userWithCurrent(10008, 1, "老师", 1, 7);
Set<AgentRole> roles = resolver.resolve(u);
assertTrue(roles.contains(AgentRole.STUDENT), "当前身份是学生,应识别为学生");
assertFalse(roles.contains(AgentRole.TEACHER), "不应因账号另挂教师身份而获得教师能力");
assertTrue(roles.contains(AgentRole.TEACHER), "当前身份是老师,应识别为教师");
assertFalse(roles.contains(AgentRole.ADMIN), "不应因账号另挂管理员身份而获得管理员能力");
assertFalse(roles.contains(AgentRole.STUDENT), "老师不是学生");
List<ToolDef> visible = registry.visibleTools(null, roles);
assertTrue(visible(visible, "homework.submit"), "学生当前身份应可用 homework.submit");
assertFalse(visible(visible, "notice.publish"), "学生当前身份不应看到 notice.publish");
assertFalse(visible(visible, "profession.myPoints"), "学生当前身份不应看到 profession.*");
assertTrue(visible(visible, "notice.publish"), "教师当前身份应可用 notice.publish");
assertTrue(visible(visible, "leave.audit"), "教师当前身份应可用 leave.audit");
assertFalse(visible(visible, "homework.submit"), "教师当前身份不应看到学生交作业");
assertFalse(visible(visible, "activity.signup"), "教师当前身份不应看到学生报名活动");
}
@Test
void switchingCurrentRoleSwitchesCapabilities() {
BoaUser u = userWithCurrent(10008, 1, "学生", 1, 2, 4);
assertTrue(resolver.resolve(u).contains(AgentRole.STUDENT));
// 同一账号,在门户里把当前身份从「老师」切到「管理员」→ 同一账号能力随之变化
BoaUser u = userWithCurrent(10008, 1, "老师", 1, 7);
assertTrue(resolver.resolve(u).contains(AgentRole.TEACHER));
// 门户把当前身份切成教师 → 同一账号能力随之变化
u.getCurrentRole().setRoleId(2);
u.getCurrentRole().setRoleName("教师");
Set<AgentRole> asTeacher = resolver.resolve(u);
assertTrue(asTeacher.contains(AgentRole.TEACHER));
assertFalse(asTeacher.contains(AgentRole.STUDENT));
List<ToolDef> teacherTools = registry.visibleTools(null, asTeacher);
assertTrue(visible(teacherTools, "notice.publish"), "教师当前身份应可用 notice.publish");
assertFalse(visible(teacherTools, "homework.submit"), "教师当前身份不应看到 homework.submit");
// 切成管理员 → 管理员 + 教师(admin-also-teacher 默认 true)
u.getCurrentRole().setRoleId(4);
u.getCurrentRole().setRoleId(7);
u.getCurrentRole().setRoleName("管理员");
Set<AgentRole> asAdmin = resolver.resolve(u);
assertTrue(asAdmin.contains(AgentRole.ADMIN));
assertTrue(asAdmin.contains(AgentRole.TEACHER));
assertTrue(asAdmin.contains(AgentRole.TEACHER), "管理员默认兼有教师能力(admin-also-teacher)");
assertFalse(asAdmin.contains(AgentRole.STUDENT), "管理员当前身份不应含学生专属能力");
// 切回老师 → 管理员能力消失
u.getCurrentRole().setRoleId(1);
u.getCurrentRole().setRoleName("老师");
Set<AgentRole> back = resolver.resolve(u);
assertTrue(back.contains(AgentRole.TEACHER));
assertFalse(back.contains(AgentRole.ADMIN), "切回老师后不应还留着管理员能力");
}
@Test
void unknownCurrentRoleFallsBackToAccountRoles() {
// 当前身份号未在 agent.roles.* 里声明 → 回退按账号全部身份判定(避免误判成访客)
BoaUser u = userWithCurrent(200, 999, "自定义角色", 1, 2);
// 当前身份号未在 agent.roles.* 里声明、角色名也不是常见词 → 回退按账号全部身份判定(避免误判成访客)
BoaUser u = userWithCurrent(200, 999, "自定义角色", 1, 7);
Set<AgentRole> roles = resolver.resolve(u);
assertTrue(roles.contains(AgentRole.STUDENT), "回退后应保留账号的学生身份");
assertTrue(roles.contains(AgentRole.TEACHER), "回退后应保留账号的教师身份");
assertTrue(roles.contains(AgentRole.ADMIN), "回退后应保留账号的管理员身份");
}
@Test
void roleNameKeywordFallbackWhenRoleIdNotConfigured() {
// 角色号没配到、但角色名可识别 → 按名称判定,不应掉成访客
assertTrue(resolver.resolve(userWithCurrent(300, 88, "班主任", 88)).contains(AgentRole.TEACHER),
"「班主任」应按名称识别为教师");
assertTrue(resolver.resolve(userWithCurrent(301, 99, "教务主任", 99)).contains(AgentRole.ADMIN),
"「教务主任」应按名称识别为管理员");
assertTrue(resolver.resolve(userWithCurrent(303, 77, "家长", 77)).contains(AgentRole.PARENT),
"「家长」应按名称识别为家长");
BoaUser stu = user(302);
stu.setType(0);
assertTrue(resolver.resolve(stu).contains(AgentRole.STUDENT), "type=0 应识别为学生");
}
@Test
void platformRoleDictionaryMapsToExpectedRoles() {
// 平台「用户身份」字典:1老师 2校领导 3学生 4家长 5局领导 6工作号 7管理员
assertTrue(resolver.resolve(userWithCurrent(11, 1, "老师", 1)).contains(AgentRole.TEACHER));
assertTrue(resolver.resolve(userWithCurrent(13, 3, "学生", 3)).contains(AgentRole.STUDENT));
assertTrue(resolver.resolve(userWithCurrent(14, 4, "家长", 4)).contains(AgentRole.PARENT));
// 6 = 工作号(教职工)
Set<AgentRole> gs = resolver.resolve(userWithCurrent(16, 6, "工作号", 6));
assertTrue(gs.contains(AgentRole.TEACHER), "roleId=6(工作号)应识别为教师");
assertFalse(gs.contains(AgentRole.ADMIN), "工作号不是管理员");
// 2=校领导、5=局领导、7=管理员 → 管理员,并兼有教师能力
for (int rid : new int[]{2, 5, 7}) {
Set<AgentRole> roles = resolver.resolve(userWithCurrent(400 + rid, rid, "领导", rid));
assertTrue(roles.contains(AgentRole.ADMIN), "roleId=" + rid + " 应识别为管理员");
assertTrue(roles.contains(AgentRole.TEACHER), "管理员应兼有教师能力");
assertFalse(roles.contains(AgentRole.STUDENT), "管理员不是学生");
}
}
@Test
void accountWithoutCurrentRoleStillResolvesByRoleIds() {
// 老口径:平台没下发 currentRole 时,仍按 roleIds 判定(不能变成访客)
Set<AgentRole> roles = resolver.resolve(user(300, 2));
assertTrue(roles.contains(AgentRole.TEACHER));
assertTrue(resolver.resolve(user(300, 1)).contains(AgentRole.TEACHER), "roleId=1 = 老师");
assertTrue(resolver.resolve(user(301, 3)).contains(AgentRole.STUDENT), "roleId=3 = 学生");
assertTrue(resolver.resolve(user(302, 7)).contains(AgentRole.ADMIN), "roleId=7 = 管理员");
}
@Test
void studentSeesOnlyStudentSideTools() {
Set<AgentRole> roles = resolver.resolve(user(101, 1));
assertTrue(roles.contains(AgentRole.STUDENT), "roleId=1 应识别为学生");
// 学生形态(实测):当前身份 roleId=3(平台「用户身份」字典:3=学生)
BoaUser stu = userWithCurrent(101, 3, "学生", 3);
Set<AgentRole> roles = resolver.resolve(stu);
assertTrue(roles.contains(AgentRole.STUDENT), "roleId=3 应识别为学生");
assertFalse(roles.contains(AgentRole.TEACHER), "学生不是教师");
assertFalse(roles.contains(AgentRole.PARENT), "学生不是家长");
List<ToolDef> visible = registry.visibleTools(null, roles);
// 学生能做:交作业、报名活动、请假申请
@@ -145,10 +181,32 @@ class RolePermissionTest {
assertFalse(visible(visible, "dorm.buildings"), "学生不应看到宿舍管理侧");
}
@Test
void parentSeesActivityToolsButNotStudentOrStaff() {
// 家长形态(实测):当前身份 roleId=4(平台字典:4=家长)
BoaUser p = userWithCurrent(201, 4, "家长", 4);
Set<AgentRole> roles = resolver.resolve(p);
assertTrue(roles.contains(AgentRole.PARENT), "roleId=4 应识别为家长");
assertFalse(roles.contains(AgentRole.STUDENT), "家长不是学生");
assertFalse(roles.contains(AgentRole.TEACHER), "家长不是教师");
List<ToolDef> visible = registry.visibleTools(null, roles);
// 家长能做:看活动广场、报名/取消报名、宿舍通知回执
assertTrue(visible(visible, "activity.plaza"), "家长应能看活动广场");
assertTrue(visible(visible, "activity.signup"), "家长应能报名活动");
assertTrue(visible(visible, "activity.cancelSignup"), "家长应能取消报名");
assertTrue(visible(visible, "dorm.submitReceipt"), "家长应能提交宿舍通知回执");
// 家长不做:学生作业、请假申请/审批、发布公告
assertFalse(visible(visible, "homework.submit"), "家长不应看到学生交作业");
assertFalse(visible(visible, "leave.submit"), "家长不应看到请假申请");
assertFalse(visible(visible, "leave.audit"), "家长不应看到审批");
assertFalse(visible(visible, "notice.publish"), "家长不应看到发布公告");
}
@Test
void teacherSeesStaffToolsButNotStudentHomework() {
Set<AgentRole> roles = resolver.resolve(user(102, 2));
assertTrue(roles.contains(AgentRole.TEACHER), "roleId=2 应识别为教师");
Set<AgentRole> roles = resolver.resolve(user(102, 1));
assertTrue(roles.contains(AgentRole.TEACHER), "roleId=1 应识别为教师(老师)");
List<ToolDef> visible = registry.visibleTools(null, roles);
// 教师能做:审批、发布、专业发展