feat: 提交校园智能助手后端工程源码(Spring Boot 2.7.18 + 平台 starter + JPA + Nacos)
build / build-push (push) Successful in 40s

- com.yunhai.agent,端口 8093,context-path /agent,制品 target/agent.jar;与 NoticeService 同构
- CAS 单点登录 + 三层角色权限(学生/教师/管理员/访客)+ 会话与工具调用审计落库
- DeepSeek 大模型编排 + 7 个业务模块工具(请假/公告/宿舍/活动/作业/资源库/教师发展)
- 浏览器代执行协议(服务端挂起 → 前端用登录态代调 → toolResult 续跑)+ 调用失败深链兜底
- 登出修复:服务端真正 session.invalidate() + 前端两步登出 + logout-guard
- 保留 projman 注入的 .gitea/workflows/build.yml 与 docker/Dockerfile;docker/application-prod.yml、run-app.sh 随工程入库
This commit is contained in:
lijing
2026-10-12 11:18:29 +08:00
parent 8589afd69a
commit 8303b857e9
58 changed files with 6883 additions and 2 deletions
@@ -0,0 +1,218 @@
package com.yunhai.agent;
import com.yunhai.agent.auth.AgentRole;
import com.yunhai.agent.auth.RoleResolver;
import com.yunhai.agent.config.AgentSettings;
import com.yunhai.agent.tool.CallerContext;
import com.yunhai.agent.tool.ToolDef;
import com.yunhai.agent.tool.ToolExecutor;
import com.yunhai.agent.tool.ToolRegistry;
import com.yunhai.agent.tool.ToolResult;
import com.yunhai.platform.utils.model.BoaRoleInfo;
import com.yunhai.platform.utils.model.BoaUser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 「分学生、教师权限」的核心断言(纯单元,不依赖 Spring / SSO / 数据库)。
*
* <p>验证两件事:① 模型侧过滤(哪些工具可见);② 服务端强校验(越权直接拒绝执行)。
*/
class RolePermissionTest {
private AgentSettings settings;
private ToolRegistry registry;
private RoleResolver resolver;
@BeforeEach
void setUp() {
settings = new AgentSettings();
registry = new ToolRegistry(settings);
registry.init();
resolver = new RoleResolver(settings);
}
/** 伪造一个指定角色号的平台用户(type=null 表示非超管) */
private BoaUser user(Integer id, Integer... roleIds) {
BoaUser u = new BoaUser();
u.setId(id);
u.setName("测试用户" + id);
u.setAccount("test" + id);
u.setRoleIds(Arrays.asList(roleIds));
return u;
}
/** 伪造一个带「当前身份」的平台用户 */
private BoaUser userWithCurrent(Integer id, Integer currentRoleId, String currentRoleName, Integer... allRoleIds) {
BoaUser u = user(id, allRoleIds);
if (currentRoleId != null) {
BoaRoleInfo cur = new BoaRoleInfo();
cur.setRoleId(currentRoleId);
cur.setRoleName(currentRoleName);
cur.setSchId(3);
cur.setSchName("测试学校");
u.setCurrentRole(cur);
}
return u;
}
private boolean visible(List<ToolDef> tools, String name) {
return tools.stream().anyMatch(t -> t.getName().equals(name));
}
@Test
void multiIdentityAccountUsesCurrentRoleOnly() {
// 实测账号形态:10008 同时挂着 学生(1) + 教师(2) + 管理员(4),当前身份 = 学生
BoaUser u = userWithCurrent(10008, 1, "学生", 1, 2, 4);
Set<AgentRole> roles = resolver.resolve(u);
assertTrue(roles.contains(AgentRole.STUDENT), "当前身份是学生,应识别为学生");
assertFalse(roles.contains(AgentRole.TEACHER), "不应因账号另挂教师身份而获得教师能力");
assertFalse(roles.contains(AgentRole.ADMIN), "不应因账号另挂管理员身份而获得管理员能力");
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.*");
}
@Test
void switchingCurrentRoleSwitchesCapabilities() {
BoaUser u = userWithCurrent(10008, 1, "学生", 1, 2, 4);
assertTrue(resolver.resolve(u).contains(AgentRole.STUDENT));
// 门户把当前身份切成教师 → 同一账号能力随之变化
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().setRoleName("管理员");
Set<AgentRole> asAdmin = resolver.resolve(u);
assertTrue(asAdmin.contains(AgentRole.ADMIN));
assertTrue(asAdmin.contains(AgentRole.TEACHER));
assertFalse(asAdmin.contains(AgentRole.STUDENT), "管理员当前身份不应含学生专属能力");
}
@Test
void unknownCurrentRoleFallsBackToAccountRoles() {
// 当前身份号未在 agent.roles.* 里声明 → 回退按账号全部身份判定(避免误判成访客)
BoaUser u = userWithCurrent(200, 999, "自定义角色", 1, 2);
Set<AgentRole> roles = resolver.resolve(u);
assertTrue(roles.contains(AgentRole.STUDENT), "回退后应保留账号的学生身份");
assertTrue(roles.contains(AgentRole.TEACHER), "回退后应保留账号的教师身份");
}
@Test
void accountWithoutCurrentRoleStillResolvesByRoleIds() {
// 老口径:平台没下发 currentRole 时,仍按 roleIds 判定(不能变成访客)
Set<AgentRole> roles = resolver.resolve(user(300, 2));
assertTrue(roles.contains(AgentRole.TEACHER));
}
@Test
void studentSeesOnlyStudentSideTools() {
Set<AgentRole> roles = resolver.resolve(user(101, 1));
assertTrue(roles.contains(AgentRole.STUDENT), "roleId=1 应识别为学生");
List<ToolDef> visible = registry.visibleTools(null, roles);
// 学生能做:交作业、报名活动、请假申请
assertTrue(visible(visible, "homework.submit"), "学生应可用 homework.submit");
assertTrue(visible(visible, "activity.signup"), "学生应可用 activity.signup");
assertTrue(visible(visible, "leave.submit"), "学生应可用 leave.submit");
// 学生不能做:审批、发布、专业发展、宿管管理
assertFalse(visible(visible, "leave.audit"), "学生不应看到 leave.audit");
assertFalse(visible(visible, "leave.batchPass"), "学生不应看到 leave.batchPass");
assertFalse(visible(visible, "notice.publish"), "学生不应看到 notice.publish");
assertFalse(visible(visible, "profession.myPoints"), "学生不应看到 profession.*");
assertFalse(visible(visible, "dorm.buildings"), "学生不应看到宿舍管理侧");
}
@Test
void teacherSeesStaffToolsButNotStudentHomework() {
Set<AgentRole> roles = resolver.resolve(user(102, 2));
assertTrue(roles.contains(AgentRole.TEACHER), "roleId=2 应识别为教师");
List<ToolDef> visible = registry.visibleTools(null, roles);
// 教师能做:审批、发布、专业发展
assertTrue(visible(visible, "leave.audit"), "教师应可用 leave.audit");
assertTrue(visible(visible, "notice.publish"), "教师应可用 notice.publish");
assertTrue(visible(visible, "profession.myPoints"), "教师应可用 profession.*");
assertTrue(visible(visible, "leave.stats"), "教师应可用 leave.stats");
// 教师不做:学生交作业、报名活动
assertFalse(visible(visible, "homework.submit"), "教师不应看到 homework.submit");
assertFalse(visible(visible, "activity.signup"), "教师不应看到 activity.signup");
}
@Test
void adminByTypeGetsStaffCapabilities() {
BoaUser u = user(1);
u.setType(1); // 平台超管口径
Set<AgentRole> roles = resolver.resolve(u);
assertTrue(roles.contains(AgentRole.ADMIN), "type=1 应识别为管理员");
assertTrue(roles.contains(AgentRole.TEACHER), "管理员默认兼有教师能力");
List<ToolDef> visible = registry.visibleTools(null, roles);
assertTrue(visible(visible, "leave.audit"));
assertTrue(visible(visible, "notice.publish"));
}
@Test
void guestSeesOnlyUnrestrictedTools() {
Set<AgentRole> roles = resolver.resolve(null);
assertTrue(roles.contains(AgentRole.GUEST));
List<ToolDef> visible = registry.visibleTools(null, roles);
assertTrue(visible(visible, "notice.received"), "公共只读能力应对访客可见");
assertFalse(visible(visible, "leave.submit"), "访客不应看到请假申请");
assertFalse(visible(visible, "leave.audit"), "访客不应看到审批");
}
@Test
void executorRejectsUnauthorizedToolAtServerSide() {
// 即便模型绕过过滤硬调用,服务端也必须拒绝
ToolExecutor executor = new ToolExecutor(settings, new RestTemplate());
ToolDef audit = registry.get("leave.audit");
assertTrue(audit != null && !audit.allowsAny(Collections.singleton(AgentRole.STUDENT)),
"leave.audit 应限制学生使用");
CallerContext student = new CallerContext();
student.setRoles(new LinkedHashSet<>(Collections.singletonList(AgentRole.STUDENT)));
ToolResult tr = executor.execute(audit, new LinkedHashMap<>(), student);
assertFalse(tr.isSuccess(), "学生调用审批工具必须失败");
assertTrue(tr.getError() != null && tr.getError().contains("无权"),
"拒绝理由应说明无权,实际:" + tr.getError());
}
@Test
void executorAllowsAuthorizedTool() {
// 教师调用审批工具不会被角色闸门拦截(因未配置模块地址,会在网关层报"未配置地址",不是"无权")
ToolExecutor executor = new ToolExecutor(settings, new RestTemplate());
ToolDef audit = registry.get("leave.audit");
CallerContext teacher = new CallerContext();
teacher.setRoles(new LinkedHashSet<>(Collections.singletonList(AgentRole.TEACHER)));
ToolResult tr = executor.execute(audit, new LinkedHashMap<>(), teacher);
assertTrue(tr.getError() == null || !tr.getError().contains("无权"),
"教师调用不应被角色闸门拒绝,实际:" + tr.getError());
}
}