# 开发日志 - 2026-03-17 ## 多地点登录支持实现 ### 修改内容 #### 1. JwtTokenRedisService.java **文件路径**: `reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenRedisService.java` **修改内容**: - 修改 `validateToken()` 方法,移除了 token 一致性检查 - 现在只检查 token 是否在黑名单中 - 允许同一账号有多个有效的 token,支持多地点同时登录 **修改前逻辑**: ```java // 检查是否与存储的 token 一致 String storedToken = getStoredToken(username); if (storedToken == null) { return true; } boolean isValid = token.equals(storedToken); return isValid; // 如果不一致则返回 false,导致互踢下线 ``` **修改后逻辑**: ```java // 仅检查是否在黑名单中 if (isBlacklisted(token)) { return false; } // 不再检查是否与存储的 token 一致,允许同一账号有多个有效 token return true; ``` #### 2. JwtAuthenticationFilter.java **文件路径**: `reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java` **修改内容**: - 新增依赖注入:`AdminUserMapper`, `TenantMapper`, `TeacherMapper`, `ParentMapper` - 在 token 验证通过后,增加账户状态检查 - 新增 `isAccountActive()` 方法,根据用户角色查询对应表验证账户状态 **新增代码**: ```java // 检查账户状态是否为 active if (!isAccountActive(payload)) { log.debug("Account is not active for user: {}", payload.getUsername()); sendError(response, HttpStatus.UNAUTHORIZED, "账户已被禁用,请联系管理员"); return; } ``` **新增方法**: ```java private boolean isAccountActive(JwtPayload payload) { String role = payload.getRole(); Long userId = payload.getUserId(); return switch (role) { case "admin" -> { AdminUser adminUser = adminUserMapper.selectById(userId); yield adminUser != null && "active".equalsIgnoreCase(adminUser.getStatus()); } case "school" -> { Tenant tenant = tenantMapper.selectById(userId); yield tenant != null && "active".equalsIgnoreCase(tenant.getStatus()); } case "teacher" -> { Teacher teacher = teacherMapper.selectById(userId); yield teacher != null && "active".equalsIgnoreCase(teacher.getStatus()); } case "parent" -> { Parent parent = parentMapper.selectById(userId); yield parent != null && "active".equalsIgnoreCase(parent.getStatus()); } default -> false; }; } ``` #### 3. AuthServiceImpl.java **文件路径**: `reading-platform-java/src/main/java/com/reading/platform/service/impl/AuthServiceImpl.java` **修改内容**: - 更新 `logout()` 方法的注释,说明当前实现 #### 4. 状态判断忽略大小写修改 **修改内容**: - 将所有 `"active".equals(status)` 修改为 `"active".equalsIgnoreCase(status)` - 确保大小写不敏感的状态判断,如 "Active"、"ACTIVE"、"active" 都被识别为激活状态 **修改文件**: - `JwtAuthenticationFilter.java` - `isAccountActive()` 方法中的 4 处判断 - `AuthServiceImpl.java` - `login()` 方法中的 8 处判断(admin、teacher、parent、tenant 各 2 处) ### 功能说明 #### 多地点登录 - 同一账号可以在多个设备/浏览器同时登录 - 各个登录状态的 token 都有效,不会互踢下线 - JWT token 本身的过期时间(默认 24 小时)保证安全性 #### 账户状态验证 - 每次请求都会验证账户状态是否为 "active" - 如果管理员在后台禁用某个账号,该账号的所有已登录会话将立即失效 - 支持所有角色:admin、school、teacher、parent #### 黑名单机制 - 黑名单机制仍然有效 - 可以用于主动使特定 token 失效(如踢人下线场景) ### 验证步骤 1. **多地点登录测试**: - 使用同一账号在两个不同的浏览器登录 - 在两个浏览器都发起 API 请求 - 预期:两个登录都保持有效 2. **账户状态禁用测试**: - 使用账号 A 在浏览器 1 登录 - 在超管后台将账号 A 的状态修改为"非激活" - 在浏览器 1 再次发起请求,应返回"账户已被禁用" ### 安全性考虑 1. JWT token 有过期时间(默认 24 小时),过期后自动失效 2. 黑名单机制仍然有效,可以主动使特定 token 失效 3. 每次请求都会验证账户状态,确保禁用账号无法访问 ### 影响范围 - 所有角色的登录认证流程 - Token 验证流程 - 账户状态检查 ### 兼容性 - 此修改不影响现有功能 - 登出、黑名单等功能仍然正常工作 - 前端无需修改 --- ## Admin 控制器三层架构规范化 ### 修改内容 #### 1. AdminCourseCollectionController.java **文件路径**: `reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminCourseCollectionController.java` **问题**: - `findAll` 返回 `Result>` - 类型不明确 - `findOne` 返回 `Result` - 类型不明确 - `create/update` 返回 `Result` - 直接返回 Entity **修改后**: - 重命名为 `page` 方法,返回 `Result>` - `findOne` 返回 `Result` - `create` 返回 `Result` - `update` 返回 `Result` #### 2. CourseCollectionService.java **文件路径**: `reading-platform-java/src/main/java/com/reading/platform/service/CourseCollectionService.java` **新增方法**: - `pageCollections(Integer pageNum, Integer pageSize, String status)` - 分页查询并转换为 Response **修改方法**: - `createCollection()` - 返回类型从 `CourseCollection` 改为 `CourseCollectionResponse` - `updateCollection()` - 返回类型从 `CourseCollection` 改为 `CourseCollectionResponse` #### 3. CourseCollectionPageQueryRequest.java(新建) **文件路径**: `reading-platform-java/src/main/java/com/reading/platform/dto/request/CourseCollectionPageQueryRequest.java` ```java @Data @Schema(description = "课程套餐分页查询请求") public class CourseCollectionPageQueryRequest { @Schema(description = "页码", example = "1") private Integer pageNum = 1; @Schema(description = "每页数量", example = "10") private Integer pageSize = 10; @Schema(description = "状态") private String status; } ``` ### 修改文件列表 | 文件 | 类型 | 说明 | |------|------|------| | `AdminCourseCollectionController.java` | 修改 | 规范化返回类型 | | `CourseCollectionService.java` | 修改 | 新增分页方法,修改返回类型 | | `CourseCollectionPageQueryRequest.java` | 新增 | 分页查询请求 DTO | ### 验证 ```bash export JAVA_HOME="/f/Java/jdk-17" mvn clean compile -DskipTests # BUILD SUCCESS ``` ### 规范的控制器 | 控制器 | 状态 | |--------|------| | AdminCourseCollectionController | ✅ 已规范 | | AdminStatsController | ✅ 已规范 | | AdminTenantController | ✅ 已规范 | | AdminResourceController | ✅ 已规范 | | AdminCourseLessonController | ✅ 已规范 | | AdminCourseController | ✅ 已规范 | | AdminPackageController | ✅ 已规范 | | AdminThemeController | ✅ 已规范 | | AdminSettingsController | 🟡 可选(设置类接口允许使用 Map) | --- ## 排课计划参考功能实现 ### 背景 学校端排课功能需要显示排课计划参考数据,帮助老师了解课程包下的课程安排建议。用户说明:"因为我们一个套餐是一个课程体系,在这个课程体系下,有多个课程包,每个课程包下,有导入课、集体课、五大领域课,我们提供的排课计划参考是为了给学校老师指引,告诉他们课程包下的这三类课如何上,才能发挥最大的价值。" ### 新建排课流程 1. 选择课程套餐 → 选择课程包 → 显示排课计划参考 2. 单选课程类型(导入课/集体课/五大领域课) 3. 选择班级 → 指定授课教师 4. 设置授课时间 ### 后端修改 #### 1. CoursePackageResponse.java **文件路径**: `reading-platform-java/src/main/java/com/reading/platform/dto/response/CoursePackageResponse.java` **修改内容**: 在 `CoursePackageCourseItem` 内部类中添加 `scheduleRefData` 和 `lessonType` 字段 ```java @Schema(description = "排课计划参考数据(JSON)") private String scheduleRefData; @Schema(description = "课程类型") private String lessonType; ``` #### 2. CoursePackageService.java **文件路径**: `reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java` **修改内容**: 修改 `toResponse()` 方法,填充 `scheduleRefData` 和 `lessonType` ```java if (course != null) { item.setId(course.getId()); item.setName(course.getName()); item.setScheduleRefData(course.getScheduleRefData()); item.setLessonType(course.getLessonType()); } ``` #### 3. CourseCollectionService.java **文件路径**: `reading-platform-java/src/main/java/com/reading/platform/service/CourseCollectionService.java` **修改内容**: - 添加依赖注入:`CoursePackageCourseMapper`, `CourseMapper` - 添加导入:`Course`, `CoursePackageCourse` - 完全重写 `toPackageResponse()` 方法,包含课程列表查询逻辑 ```java private CoursePackageResponse toPackageResponse(CoursePackage pkg) { // 查询课程包关联的课程 List packageCourses = packageCourseMapper.selectList( new LambdaQueryWrapper() .eq(CoursePackageCourse::getPackageId, pkg.getId()) .orderByAsc(CoursePackageCourse::getSortOrder) ); List courseItems = ...; // 构建 courseItems 列表,包含 scheduleRefData } ``` ### 前端修改 #### 1. school.ts **文件路径**: `reading-platform-frontend/src/api/school.ts` **修改内容**: 在 `CoursePackage` 接口的 `courses` 数组项中添加 `scheduleRefData` 字段 ```typescript courses?: Array<{ id: number; name: string; gradeLevel: string; sortOrder: number; scheduleRefData?: string; // 新增字段 }>; ``` #### 2. CreateScheduleModal.vue **文件路径**: `reading-platform-frontend/src/views/school/schedule/components/CreateScheduleModal.vue` **修改内容**: 优化 `selectPackage()` 方法,直接从响应数据中提取排课计划参考 ```typescript const selectPackage = async (packageId: number) => { formData.packageId = packageId; formData.courseId = undefined; // 加载排课计划参考(从选中的课程包中获取) if (selectedCollection.value?.packages) { const selectedPkg = selectedCollection.value.packages.find((p: any) => p.id === packageId); if (selectedPkg?.courses && selectedPkg.courses.length > 0) { const firstCourse = selectedPkg.courses[0]; if (firstCourse.scheduleRefData) { try { const parsedData = JSON.parse(firstCourse.scheduleRefData); scheduleRefData.value = Array.isArray(parsedData) ? parsedData : []; } catch (e) { scheduleRefData.value = []; } } } } await loadLessonTypes(packageId); }; ``` ### API 验证 **请求**: ``` GET /api/v1/school/packages/5/packages Authorization: Bearer {token} ``` **响应**: ```json { "code": 200, "data": [{ "id": "5", "name": "完整阅读能力培养套餐", "courses": [ { "id": "6", "name": "小猪佩奇绘本阅读", "gradeLevel": "小班", "sortOrder": 1, "scheduleRefData": null, "lessonType": null } ] }] } ``` ### 下一步工作 1. 在数据库中添加示例排课计划参考数据(`course.schedule_ref_data` 字段) 2. 考虑添加 `collectionId` 存储(需要数据库迁移和 DTO 更新) ### Git 提交 ``` commit 9f89ce7 feat: 添加课程包排课计划参考数据返回 ```