From 67e55f1875f89aeef9e0b283fc4cf6b189799b80 Mon Sep 17 00:00:00 2001 From: lijing Date: Mon, 12 Oct 2026 16:10:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E8=A7=92=E8=89=B2=E6=98=A0?= =?UTF-8?q?=E5=B0=84=E6=8C=89=E5=B9=B3=E5=8F=B0=E5=AD=97=E5=85=B8(1-7)?= =?UTF-8?q?=E6=A0=A1=E5=87=86=EF=BC=8C=E5=B9=B6=E7=A7=BB=E9=99=A4=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=BA=93=E6=A8=A1=E5=9D=97=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 角色字典实测校准(采样 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 通过 --- .gitignore | 3 + README.md | 7 +- .../java/com/yunhai/agent/auth/AgentRole.java | 17 ++- .../com/yunhai/agent/auth/RolePolicy.java | 15 ++- .../com/yunhai/agent/auth/RoleResolver.java | 121 +++++++++++++++--- .../yunhai/agent/config/AgentSettings.java | 34 +++-- .../agent/service/AgentOrchestrator.java | 1 - .../com/yunhai/agent/tool/ToolRegistry.java | 2 - .../yunhai/agent/tool/defs/ResourceTools.java | 59 --------- src/main/resources/application.yml | 21 ++- .../com/yunhai/agent/RolePermissionTest.java | 120 ++++++++++++----- 11 files changed, 265 insertions(+), 135 deletions(-) delete mode 100644 src/main/java/com/yunhai/agent/tool/defs/ResourceTools.java diff --git a/.gitignore b/.gitignore index 3eab8f8..8b26b32 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ src/main/resources/application-prod.yml # IDE 运行日志 .run/ +# 本地排查脚本(含平台 app-secret,禁止入库) +.scratch/ + # 备份与残留 *.bak *.orig diff --git a/README.md b/README.md index 9d7399e..26d1036 100644 --- a/README.md +++ b/README.md @@ -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(...)`)优先级更高。 diff --git a/src/main/java/com/yunhai/agent/auth/AgentRole.java b/src/main/java/com/yunhai/agent/auth/AgentRole.java index cf54a05..1f0cd1c 100644 --- a/src/main/java/com/yunhai/agent/auth/AgentRole.java +++ b/src/main/java/com/yunhai/agent/auth/AgentRole.java @@ -3,18 +3,27 @@ package com.yunhai.agent.auth; /** * 智能体侧的角色分类(由平台 {@code BoaUser} 解析而来)。 * - *

用途:决定「这个身份能用哪些工具」——学生侧(请假申请 / 交作业 / 报名活动 / 看公告) - * 与教职工侧(发公告 / 审批请假 / 发布资源 / 专业发展)严格分开。 + *

用途:决定「这个身份能用哪些工具」——学生侧(请假申请 / 交作业 / 报名活动 / 看公告)、 + * 家长侧(报名活动 / 宿舍通知回执)与教职工侧(发公告 / 审批请假 / 专业发展)严格分开。 + * + *

对齐平台 UserBase 的「用户身份」字典(门户/GetUserInfo 的 {@code currentRole.roleId}): + *

+ * 1 老师   2 校领导   3 学生   4 家长   5 局领导   6 工作号   7 管理员
+ * 
+ * 映射到本枚举:1/6 → 教师,3 → 学生,4 → 家长,2/5/7 → 管理员。 */ public enum AgentRole { /** 学生 */ STUDENT("学生"), - /** 教师(含班主任) */ + /** 教师(含班主任、工作号/教职工) */ TEACHER("教师"), - /** 管理员(校管 / 分管领导 / 教务 / 校长) */ + /** 家长:家校活动的报名方、宿舍通知的回执方 */ + PARENT("家长"), + + /** 管理员(校领导 / 局领导 / 管理员) */ ADMIN("管理员"), /** 访客:未登录或无法识别角色,只能使用公共只读能力 */ diff --git a/src/main/java/com/yunhai/agent/auth/RolePolicy.java b/src/main/java/com/yunhai/agent/auth/RolePolicy.java index 48bf62f..2d597bb 100644 --- a/src/main/java/com/yunhai/agent/auth/RolePolicy.java +++ b/src/main/java/com/yunhai/agent/auth/RolePolicy.java @@ -14,13 +14,14 @@ import java.util.Set; * 集中成一张可读的表比散落在 7 个模块的工具定义里更容易审阅与改动; * 若某个工具需要就近声明,用 {@code ToolDef.allow(...)} 显式指定即可(本策略会尊重已有声明)。 * - *

三类名单: + *

名单: *

- * 未列入任何名单的工具 = 所有已识别角色都可用(如各类只读查询)。 + * 未列入任何名单的工具 = 所有已识别角色都可用(如各类只读查询、宿舍通知回执)。 */ public final class RolePolicy { @@ -43,10 +44,14 @@ public final class RolePolicy { /** 学生专属 */ private static final Set STUDENT_ONLY = new HashSet<>(Arrays.asList( "homework.myList", "homework.detail", "homework.result", "homework.submit", - "activity.signup", "activity.cancelSignup", "leave.reportBack" )); + /** 学生与家长共用(家校活动报名由家长或学生发起) */ + private static final Set STUDENT_OR_PARENT = new HashSet<>(Arrays.asList( + "activity.signup", "activity.cancelSignup" + )); + /** 学生与教职工共用(发起/撤销自己的事项) */ private static final Set 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); } diff --git a/src/main/java/com/yunhai/agent/auth/RoleResolver.java b/src/main/java/com/yunhai/agent/auth/RoleResolver.java index d0d8827..6301057 100644 --- a/src/main/java/com/yunhai/agent/auth/RoleResolver.java +++ b/src/main/java/com/yunhai/agent/auth/RoleResolver.java @@ -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} 是全部身份的角色号, * {@code BoaUser.currentRole} 是用户在门户里当前激活的那个身份 * (等价于 {@code YschBaseOpenApi.getCurrentIdentity(userId)})。 - * 例如实测账号 10008 同时是「学生 + 管理员 + 教师」——若把所有身份并集, - * 权限只增不减、能力边界完全模糊(所有人都是全量 54 项)。 - * 因此这里只认当前身份:与学校门户、其它业务模块的口径保持一致。 + * 实测账号 10008(李晶/lijing)挂着「老师(1)」与「管理员(7)」,门户当前身份是「老师」—— + * 若把所有身份并集,权限只增不减、能力边界完全模糊。 + * 因此这里只认当前身份:与学校门户、其它业务模块的口径保持一致(2026-10-10 改)。 * - *

判定规则(角色号可在 {@code agent.roles.*} 配置)

+ *

★ 角色号实测校准(2026-10-12,采样 200+ 账号的 currentRole / roles 取得)

+ *
+ * 1 老师   2 校领导   3 学生   4 家长   5 局领导   6 工作号   7 管理员
+ * → 本枚举:1/6 = 教师,3 = 学生,4 = 家长,2/5/7 = 管理员
+ * 
+ * 数据源与门户顶栏、{@code /notice/api/base/GetUserInfo} 一致(都是 {@code BoaUser.currentRole})。 + * 本工程此前的默认映射(student=1 / teacher=2,3 / admin=4..7)是猜的, + * 把 roleId=1 误当学生 —— 于是「李晶(老师)」在助手里显示成「学生」。 + * 现已按 {@code agent.roles.*} 配置修正(默认值见 {@link AgentSettings.Roles})。 + * + *

判定顺序

*
    - *
  1. 优先取 {@code currentRole.getRoleId()},命中 {@code agent.roles.student/teacher/admin} - * 即获得对应角色,且只获得这一个身份;
  2. - *
  3. 当前身份是管理员时,按 {@code agent.roles.admin-also-teacher=true} 追加教师能力;
  4. - *
  5. 当前身份号未在配置里声明(或账号没有当前身份)→ 回退按 {@code roleIds}/{@code type} - * 的旧口径判定,并打 WARN 日志提示补配置;
  6. + *
  7. 当前身份:先按 {@code roleId} 命中 {@code agent.roles.*}; + * 角色号没配到,再按 {@code roleName} 关键词兜底(老师/教师/班主任 → 教师, + * 管理员/校长/主任/教务… → 管理员,家长 → 家长,学生 → 学生);命中即只返回该身份;
  8. + *
  9. 当前身份是管理员时,按 {@code admin-also-teacher=true} 追加教师能力;
  10. + *
  11. 无当前身份(或当前身份识别不出)→ 回退按 {@code roleIds} 逐个匹配配置;
  12. + *
  13. 仍然什么都没识别出 → 按 {@code BoaUser.type} 兜底(0=学生、2=教职工、1=超管);
  14. *
  15. 什么都识别不出 → 访客(仅公共只读能力)。
  16. *
*/ @@ -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 hit = match(current.getRoleId(), cfg); + if (current != null && (current.getRoleId() != null || StringUtils.isNotBlank(current.getRoleName()))) { + Set 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 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 matchCurrent(BoaRoleInfo current, AgentSettings.Roles cfg) { + Set hit = match(current.getRoleId(), cfg); + if (hit.isEmpty() && StringUtils.isNotBlank(current.getRoleName())) { + hit = matchByRoleName(current.getRoleName().trim()); + } + return hit; + } + /** 单个角色号 → 智能体角色(一个号理论上可命中多个分组,全部保留) */ private Set match(Integer roleId, AgentSettings.Roles cfg) { Set 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 matchByRoleName(String name) { + Set 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 matchByUserType(Integer type, AgentSettings.Roles cfg) { + Set 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 list, int v) { return list != null && list.contains(v); } diff --git a/src/main/java/com/yunhai/agent/config/AgentSettings.java b/src/main/java/com/yunhai/agent/config/AgentSettings.java index c08f20f..aa6149e 100644 --- a/src/main/java/com/yunhai/agent/config/AgentSettings.java +++ b/src/main/java/com/yunhai/agent/config/AgentSettings.java @@ -51,7 +51,6 @@ public class AgentSettings { * dormnotice → /par/notices 宿舍通知箱 * familyactivity → /par/activities 活动广场 * coursehomework → /stu/today 今日作业 - * resourcelibrary → /resource 资源管理 * profession → /home 发展首页 * * 运维可在 {@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 student = new ArrayList<>(Arrays.asList(1)); + /** 学生角色号。★ 实测校准(2026-10-12):{@code 3 = 学生} */ + private List student = new ArrayList<>(Arrays.asList(3)); - /** 教师角色号(默认 2=教师、3=班主任;请按 UserBase 校准) */ - private List teacher = new ArrayList<>(Arrays.asList(2, 3)); + /** 教师角色号。★ 实测校准:{@code 1 = 老师}、{@code 6 = 工作号}(教职工工号身份) */ + private List teacher = new ArrayList<>(Arrays.asList(1, 6)); - /** 管理员角色号(默认 4=校管、5=分管领导、6=教务、7=校长;请按 UserBase 校准) */ - private List admin = new ArrayList<>(Arrays.asList(4, 5, 6, 7)); + /** 家长角色号。★ 实测校准:{@code 4 = 家长} */ + private List parent = new ArrayList<>(Arrays.asList(4)); - /** 管理员是否天然拥有教师能力(默认 true:校管/领导可用教师侧工具) */ + /** 管理员角色号。★ 实测校准:{@code 2 = 校领导}、{@code 5 = 局领导}、{@code 7 = 管理员} */ + private List admin = new ArrayList<>(Arrays.asList(2, 5, 7)); + + /** + * 兜底口径之一:账号既没有「当前身份」、也没挂任何已识别的角色号时, + * 按 {@code BoaUser.type} 判定。实测 {@code 0 = 学生}。 + */ + private List studentUserTypes = new ArrayList<>(Arrays.asList(0)); + + /** 兜底口径之二:{@code 2 = 教职工} */ + private List teacherUserTypes = new ArrayList<>(Arrays.asList(2)); + + /** 兜底口径之三:{@code 1 = 平台超管} */ + private List adminUserTypes = new ArrayList<>(Arrays.asList(1)); + + /** 兜底口径之四:家长(平台暂未见稳定的 {@code type} 口径,留空待校准) */ + private List parentUserTypes = new ArrayList<>(); + + /** 管理员是否天然拥有教师能力(默认 true:校领导/局领导/管理员可用教师侧工具) */ private boolean adminAlsoTeacher = true; } } diff --git a/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java b/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java index 7292aac..4750de2 100644 --- a/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java +++ b/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java @@ -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; } diff --git a/src/main/java/com/yunhai/agent/tool/ToolRegistry.java b/src/main/java/com/yunhai/agent/tool/ToolRegistry.java index eb523a7..e8e2afc 100644 --- a/src/main/java/com/yunhai/agent/tool/ToolRegistry.java +++ b/src/main/java/com/yunhai/agent/tool/ToolRegistry.java @@ -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()); diff --git a/src/main/java/com/yunhai/agent/tool/defs/ResourceTools.java b/src/main/java/com/yunhai/agent/tool/defs/ResourceTools.java deleted file mode 100644 index faf5c7c..0000000 --- a/src/main/java/com/yunhai/agent/tool/defs/ResourceTools.java +++ /dev/null @@ -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 defs() { - List 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; - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 32a20bd..daeab94 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/test/java/com/yunhai/agent/RolePermissionTest.java b/src/test/java/com/yunhai/agent/RolePermissionTest.java index 7bbb0c7..fbe7f8b 100644 --- a/src/test/java/com/yunhai/agent/RolePermissionTest.java +++ b/src/test/java/com/yunhai/agent/RolePermissionTest.java @@ -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 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 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 asTeacher = resolver.resolve(u); - assertTrue(asTeacher.contains(AgentRole.TEACHER)); - assertFalse(asTeacher.contains(AgentRole.STUDENT)); - List 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 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 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 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 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 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 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 roles = resolver.resolve(user(101, 1)); - assertTrue(roles.contains(AgentRole.STUDENT), "roleId=1 应识别为学生"); + // 学生形态(实测):当前身份 roleId=3(平台「用户身份」字典:3=学生) + BoaUser stu = userWithCurrent(101, 3, "学生", 3); + Set roles = resolver.resolve(stu); + assertTrue(roles.contains(AgentRole.STUDENT), "roleId=3 应识别为学生"); + assertFalse(roles.contains(AgentRole.TEACHER), "学生不是教师"); + assertFalse(roles.contains(AgentRole.PARENT), "学生不是家长"); List 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 roles = resolver.resolve(p); + assertTrue(roles.contains(AgentRole.PARENT), "roleId=4 应识别为家长"); + assertFalse(roles.contains(AgentRole.STUDENT), "家长不是学生"); + assertFalse(roles.contains(AgentRole.TEACHER), "家长不是教师"); + + List 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 roles = resolver.resolve(user(102, 2)); - assertTrue(roles.contains(AgentRole.TEACHER), "roleId=2 应识别为教师"); + Set roles = resolver.resolve(user(102, 1)); + assertTrue(roles.contains(AgentRole.TEACHER), "roleId=1 应识别为教师(老师)"); List visible = registry.visibleTools(null, roles); // 教师能做:审批、发布、专业发展