# 开发日志 2026-03-21 ## 学校端 - 课程使用统计功能实现 ### 需求背景 学校端 Dashboard 页面已有"课程使用统计"卡片组件,但后端返回空数据。需要实现该功能,让学校管理员能够查看本校各课程包的使用情况。 ### 实现内容 #### 1. 后端修改 **Controller 层** (`SchoolStatsController.java`) - 添加日期范围参数支持,允许前端传入 `startDate` 和 `endDate` - 不传参数时默认统计本月数据 ```java @GetMapping("/courses") @Operation(summary = "获取课程使用统计") public Result>> 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> getCourseUsageStats(Long tenantId, LocalDate startDate, LocalDate endDate); ``` **Service 实现** (`SchoolStatsServiceImpl.java`) - 使用 `LessonMapper.getCourseUsageStats()` 查询实际数据 - 支持日期范围筛选,默认统计本月 - 将 `CourseUsageStatsVO` 转换为 `Map` 格式返回 ```java @Override public List> 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 stats = lessonMapper.getCourseUsageStats( tenantId, null, startTime, endTime ); // 转换为 Map 格式返回 return stats.stream().map(vo -> { Map 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 = {}; if (startDate) params.startDate = startDate; if (endDate) params.endDate = endDate; return http.get>('/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 com.alibaba easyexcel 3.3.4 ``` #### 2. 新建 DTO 类 - `dto/response/LessonExportVO.java` - 授课记录导出 VO(9 个字段) - `dto/response/TeacherPerformanceExportVO.java` - 教师绩效导出 VO(7 个字段) - `dto/response/StudentStatExportVO.java` - 学生统计导出 VO(6 个字段) #### 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 个导出接口)