kindergarten_java/docs/dev-logs/2026-03-21.md
En b361b1885b fix: 教师端首页今日课程 courseName 和 className 关联查询
问题:
- 今日课程功能只查询了 lesson 表,没有 JOIN 关联表
- TeacherLessonVO 的 courseName 和 className 字段为 null
- 前端无法显示课程名称和班级名称

修复:
- LessonMapper 新增 selectTodayLessonsWithDetails() 方法
- 通过 LEFT JOIN course_package 和 clazz 表获取名称
- TeacherStatsServiceImpl 重写 getTodayLessons() 方法
- 添加类型转换辅助方法 (getLong/getString/getLocalDate/getLocalTime/getLocalDateTime)

影响范围:
- 教师端首页 - 今日课程模块
- API: GET /api/v1/teacher/today-lessons
- API: GET /api/v1/teacher/dashboard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 18:43:47 +08:00

307 lines
11 KiB
Markdown
Raw 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-21
## 学校端 - 课程使用统计功能实现
### 需求背景
学校端 Dashboard 页面已有"课程使用统计"卡片组件,但后端返回空数据。需要实现该功能,让学校管理员能够查看本校各课程包的使用情况。
### 实现内容
#### 1. 后端修改
**Controller 层** (`SchoolStatsController.java`)
- 添加日期范围参数支持,允许前端传入 `startDate``endDate`
- 不传参数时默认统计本月数据
```java
@GetMapping("/courses")
@Operation(summary = "获取课程使用统计")
public Result<List<Map<String, Object>>> getCourseUsageStats(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getCourseUsageStats(tenantId, startDate, endDate));
}
```
**Service 接口** (`SchoolStatsService.java`)
- 更新方法签名,添加日期参数
- 添加 `LocalDate` 导入
```java
List<Map<String, Object>> getCourseUsageStats(Long tenantId, LocalDate startDate, LocalDate endDate);
```
**Service 实现** (`SchoolStatsServiceImpl.java`)
- 使用 `LessonMapper.getCourseUsageStats()` 查询实际数据
- 支持日期范围筛选,默认统计本月
-`CourseUsageStatsVO` 转换为 `Map` 格式返回
```java
@Override
public List<Map<String, Object>> getCourseUsageStats(Long tenantId, LocalDate startDate, LocalDate endDate) {
// 计算时间范围
LocalDateTime startTime;
LocalDateTime endTime;
if (startDate != null) {
startTime = startDate.atStartOfDay();
} else {
startTime = LocalDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
}
if (endDate != null) {
endTime = endDate.atTime(23, 59, 59);
} else {
endTime = LocalDateTime.now();
}
// 调用 Mapper 查询
List<CourseUsageStatsVO> stats = lessonMapper.getCourseUsageStats(
tenantId, null, startTime, endTime
);
// 转换为 Map 格式返回
return stats.stream().map(vo -> {
Map<String, Object> map = new HashMap<>();
map.put("courseId", vo.getCoursePackageId());
map.put("courseName", vo.getCoursePackageName());
map.put("usageCount", vo.getUsageCount());
map.put("studentCount", vo.getStudentCount() != null ? vo.getStudentCount() : 0);
map.put("avgDuration", vo.getAvgDuration() != null ? vo.getAvgDuration() : 0);
map.put("lastUsedAt", vo.getLastUsedAt());
return map;
}).collect(Collectors.toList());
}
```
#### 2. 前端修改
**API 层** (`src/api/school.ts`)
- 更新 `getCourseUsageStats` 支持日期参数
- 增强返回类型定义
```typescript
export const getCourseUsageStats = (startDate?: string, endDate?: string) => {
const params: Record<string, string> = {};
if (startDate) params.startDate = startDate;
if (endDate) params.endDate = endDate;
return http.get<Array<{
courseId: number;
courseName: string;
usageCount: number;
studentCount?: number;
avgDuration?: number;
lastUsedAt?: string;
}>>('/v1/school/stats/courses', { params });
};
```
**组件层** (`src/views/school/DashboardView.vue`)
- `loadCourseStats` 函数传递日期范围参数
- `onMounted` 中设置默认日期范围为当月
```typescript
const loadCourseStats = async () => {
courseStatsLoading.value = true;
try {
const startDate = dateRange.value?.[0]?.format('YYYY-MM-DD');
const endDate = dateRange.value?.[1]?.format('YYYY-MM-DD');
const data = await getCourseUsageStats(startDate, endDate);
courseStats.value = data.slice(0, 10);
} catch (error) {
console.error('Failed to load course stats:', error);
} finally {
courseStatsLoading.value = false;
}
};
onMounted(() => {
// ... 其他初始化
// 设置默认日期范围为当月
const now = dayjs();
const monthStart = now.startOf('month');
const monthEnd = now.endOf('month');
dateRange.value = [monthStart, monthEnd];
});
```
### 数据来源
课程使用统计基于 `lesson` 表的实际授课记录统计:
- **usageCount**: 统计周期内 COMPLETED 状态的 lesson 数量
- **studentCount**: 参与学生数(去重统计 student_record 中的 student_id
- **avgDuration**: 平均时长lesson 的 start_datetime 到 end_datetime 的分钟差平均值)
- **lastUsedAt**: 最后使用时间(最近一次授课完成时间)
### 文件变更列表
| 文件 | 变更说明 |
|------|---------|
| `reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStatsController.java` | 添加日期范围参数 |
| `reading-platform-java/src/main/java/com/reading/platform/service/SchoolStatsService.java` | 更新方法签名 |
| `reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolStatsServiceImpl.java` | 实现实际查询逻辑 |
| `reading-platform-frontend/src/api/school.ts` | 更新 API 函数和类型 |
| `reading-platform-frontend/src/views/school/DashboardView.vue` | 传递日期参数,设置默认日期范围 |
### 测试验证
- [ ] 后端编译通过
- [ ] 启动后端服务(端口 8480
- [ ] 启动前端服务(端口 5173
- [ ] 登录学校管理员账号
- [ ] 访问 Dashboard 页面,查看"课程使用统计"卡片
- [ ] 验证日期范围筛选功能
- [ ] 验证数据显示正确性
### 后续优化建议
1. **增加更多统计维度**:按班级、按教师统计
2. **可视化增强**:趋势图、热力图
3. **导出功能**Excel 导出课程使用明细
4. **性能优化**:大数据量时考虑缓存
---
## 学校端 - 数据导出功能实现
### 需求背景
学校端数据概览页面DashboardView.vue已有数据导出的 UI 界面和前端调用逻辑,但后端 `SchoolExportController.java` 中的 4 个接口只返回占位数据,没有实际的 Excel 导出功能。需要实现学校端数据概览的导出功能,包括:
1. **授课记录导出** - 导出指定时间范围内的授课记录
2. **教师绩效导出** - 导出教师绩效统计数据
3. **学生统计导出** - 导出学生统计数据
### 实现内容
#### 1. 添加依赖
`pom.xml` 中添加 EasyExcel 3.3.4
```xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version>
</dependency>
```
#### 2. 新建 DTO 类
- `dto/response/LessonExportVO.java` - 授课记录导出 VO9 个字段)
- `dto/response/TeacherPerformanceExportVO.java` - 教师绩效导出 VO7 个字段)
- `dto/response/StudentStatExportVO.java` - 学生统计导出 VO6 个字段)
#### 3. 新建 Service
- `service/SchoolExportService.java` - 导出服务接口
- `service/impl/SchoolExportServiceImpl.java` - 导出服务实现
#### 4. 扩展 Mapper
`LessonMapper.java` 中添加 3 个导出查询方法:
- `selectExportData()` - 授课记录查询
- `selectTeacherExportData()` - 教师绩效查询
- `selectStudentExportData()` - 学生统计查询
#### 5. 修改 Controller
`controller/school/SchoolExportController.java` - 实现 3 个导出接口:
- `GET /api/v1/school/export/lessons` - 授课记录导出
- `GET /api/v1/school/export/teacher-stats` - 教师绩效导出
- `GET /api/v1/school/export/student-stats` - 学生统计导出
#### 6. 修改前端 API
`src/api/school.ts` - 增强导出函数,处理空数据时返回的 JSON 响应
### 技术要点
1. **EasyExcel 导出**:使用 `@ExcelProperty` 注解定义 Excel 列名,支持自动列宽
2. **空数据处理**:当没有数据时返回 JSON 响应 `Result.error(404, "暂无数据")`
3. **响应类型判断**:前端通过 `content-type` 头区分 Excel 和 JSON 响应
4. **中文文件名**:使用 `URLEncoder` 编码确保中文文件名正确下载
5. **权限控制**:通过 `@RequireRole(UserRole.SCHOOL)` 确保只能导出当前租户数据
### 字段设计
#### 授课记录导出
| Excel 列名 | 数据来源 |
|-----------|----------|
| 授课日期 | lesson.lesson_date |
| 授课时间 | lesson.start_time ~ end_time |
| 班级名称 | clazz.name |
| 教师姓名 | teacher.name |
| 课程名称 | course_package.name |
| 学生人数 | COUNT(DISTINCT sr.student_id) |
| 平均参与度 | AVG(sr.participation) |
| 备注 | lesson.notes |
#### 教师绩效导出
| Excel 列名 | 数据来源 |
|-----------|----------|
| 教师姓名 | teacher.name |
| 所属班级 | GROUP_CONCAT(clazz.name) |
| 授课次数 | COUNT(lesson.id) |
| 课程数量 | COUNT(DISTINCT course_id) |
| 活跃等级 | CASE WHEN (HIGH/MEDIUM/LOW/INACTIVE) |
| 最后活跃时间 | MAX(end_datetime) |
| 平均参与度 | AVG(sr.participation) |
#### 学生统计导出
| Excel 列名 | 数据来源 |
|-----------|----------|
| 学生姓名 | student.name |
| 性别 | student.gender |
| 年级 | student.grade |
| 授课次数 | COUNT(DISTINCT lesson.id) |
| 平均参与度 | AVG(sr.participation) |
| 平均专注度 | AVG(sr.focus) |
### 问题修复
1. **lesson_type 字段不存在**
- 问题:`lesson` 表没有 `lesson_type` 字段
- 解决:改用 `l.status``l.notes`
2. **student 表 class_id 字段不存在**
- 问题:`student` 表没有 `class_id` 字段
- 解决:改用 `s.grade` 作为 `className` 显示
### 测试结果
| 测试项 | 结果 |
|--------|------|
| 编译通过 | ✅ |
| 授课记录导出 | ✅ HTTP 200 |
| 教师绩效导出 | ✅ HTTP 200 |
| 学生统计导出 | ✅ HTTP 200 |
| 空数据处理 | ✅ 返回 JSON |
### 文件变更列表
| 文件 | 变更说明 |
|------|---------|
| `reading-platform-java/pom.xml` | 添加 EasyExcel 依赖 |
| `reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonExportVO.java` | 新建 |
| `reading-platform-java/src/main/java/com/reading/platform/dto/response/TeacherPerformanceExportVO.java` | 新建 |
| `reading-platform-java/src/main/java/com/reading/platform/dto/response/StudentStatExportVO.java` | 新建 |
| `reading-platform-java/src/main/java/com/reading/platform/service/SchoolExportService.java` | 新建 |
| `reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolExportServiceImpl.java` | 新建 |
| `reading-platform-java/src/main/java/com/reading/platform/mapper/LessonMapper.java` | 添加 3 个导出查询方法 |
| `reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolExportController.java` | 实现导出接口 |
| `reading-platform-frontend/src/api/school.ts` | 增强导出函数 |
### 测试验证
- [x] 后端编译通过
- [x] 启动后端服务(端口 8481
- [x] 授课记录导出接口测试通过
- [x] 教师绩效导出接口测试通过
- [x] 学生统计导出接口测试通过
- [x] 空数据场景测试通过
- [x] 前端服务运行正常(端口 5174
---
**今日完成**: 学校端数据导出功能3 个导出接口)