kindergarten_java/docs/dev-logs/2026-03-17.md
Claude Opus 4.6 d3cf4fd43b feat: 添加课程包课程列表查询API
- 新增 GET /api/v1/school/packages/{packageId}/courses 接口
- 返回课程包详情,包含课程列表和排课计划参考数据
- 更新开发日志和变更日志

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 09:39:27 +08:00

12 KiB
Raw Blame History

开发日志 - 2026-03-17

多地点登录支持实现

修改内容

1. JwtTokenRedisService.java

文件路径: reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenRedisService.java

修改内容:

  • 修改 validateToken() 方法,移除了 token 一致性检查
  • 现在只检查 token 是否在黑名单中
  • 允许同一账号有多个有效的 token支持多地点同时登录

修改前逻辑:

// 检查是否与存储的 token 一致
String storedToken = getStoredToken(username);
if (storedToken == null) {
    return true;
}
boolean isValid = token.equals(storedToken);
return isValid;  // 如果不一致则返回 false导致互踢下线

修改后逻辑:

// 仅检查是否在黑名单中
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() 方法,根据用户角色查询对应表验证账户状态

新增代码:

// 检查账户状态是否为 active
if (!isAccountActive(payload)) {
    log.debug("Account is not active for user: {}", payload.getUsername());
    sendError(response, HttpStatus.UNAUTHORIZED, "账户已被禁用,请联系管理员");
    return;
}

新增方法:

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<List<?>> - 类型不明确
  • findOne 返回 Result<?> - 类型不明确
  • create/update 返回 Result<CourseCollection> - 直接返回 Entity

修改后:

  • 重命名为 page 方法,返回 Result<PageResult<CourseCollectionResponse>>
  • findOne 返回 Result<CourseCollectionResponse>
  • create 返回 Result<CourseCollectionResponse>
  • update 返回 Result<CourseCollectionResponse>

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

@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

验证

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 内部类中添加 scheduleRefDatalessonType 字段

@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() 方法,填充 scheduleRefDatalessonType

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() 方法,包含课程列表查询逻辑
private CoursePackageResponse toPackageResponse(CoursePackage pkg) {
    // 查询课程包关联的课程
    List<CoursePackageCourse> packageCourses = packageCourseMapper.selectList(
            new LambdaQueryWrapper<CoursePackageCourse>()
                    .eq(CoursePackageCourse::getPackageId, pkg.getId())
                    .orderByAsc(CoursePackageCourse::getSortOrder)
    );

    List<CoursePackageResponse.CoursePackageCourseItem> courseItems = ...;
    // 构建 courseItems 列表,包含 scheduleRefData
}

前端修改

1. school.ts

文件路径: reading-platform-frontend/src/api/school.ts

修改内容: 在 CoursePackage 接口的 courses 数组项中添加 scheduleRefData 字段

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() 方法,直接从响应数据中提取排课计划参考

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}

响应:

{
  "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: 添加课程包排课计划参考数据返回