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

386 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 开发日志 - 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<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`
```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<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` 字段
```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: 添加课程包排课计划参考数据返回
```