kindergarten_java/docs/dev-logs/2026-03-21.md
En c1f5b5085e feat: 学校端数据报告功能实现
主要变更:
1. 新建 ReportMapper - 数据报告统计查询
   - getOverviewStats: 概览统计(教师/学生/班级总数、本月授课次数)
   - getTeacherReports: 教师教学数据统计
   - getCourseReports: 课程使用排行统计
   - getStudentReports: 学生学习数据统计

2. 新建 SchoolReportService - 数据报告服务层
   - 4 个报告查询接口实现

3. 修改 SchoolStatsController - 调整统计接口参数
   - getLessonTrend 改为支持 startDate 和 endDate 参数

4. 前端更新 ReportView.vue
   - 对接 4 个报告接口
   - 优化图表展示和数据表格
   - 支持日期范围筛选

5. 更新开发日志和测试记录

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

529 lines
18 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-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 个导出接口)
---
## 学校端 - 数据报告功能实现
### 需求背景
学校端"数据中心"菜单下的"数据报告"子菜单功能之前未实现。前端 `ReportView.vue` 页面已存在,但调用的 API 返回空数据。需要实现完整的数据报告功能,让学校管理员能够查看:
1. **整体概览** - 教师总数、学生总数、班级总数、本月授课次数
2. **教师报告** - 教师教学数据统计(授课次数、任务数、评分)
3. **课程报告** - 课程使用排行(授课次数、学生数、完成率)
4. **学生报告** - 学生学习数据(完成任务、成长记录、出勤率)
### 实现内容
#### 1. 新建后端 Mapper
**ReportMapper.java** - 数据报告 Mapper
```java
@Mapper
public interface ReportMapper extends BaseMapper<Lesson> {
// 4 个统计查询方法
Map<String, Object> getOverviewStats(...) // 概览统计
List<Map<String, Object>> getTeacherReports(...) // 教师报告
List<Map<String, Object>> getCourseReports(...) // 课程报告
List<Map<String, Object>> getStudentReports(...) // 学生报告
}
```
**SQL 查询逻辑**
- **概览统计**: 统计教师数、学生数、班级数、本月授课次数、本月任务完成数
- **教师报告**: 按教师分组,统计授课次数、任务完成数、平均评分、最后活跃时间
- **课程报告**: 按课程分组,统计授课次数、学生数、平均评分、完成率
- **学生报告**: 按学生分组,统计任务完成数、成长记录数、出勤率
#### 2. 新建后端 Service
**SchoolReportService.java** - 服务接口
```java
public interface SchoolReportService {
ReportOverviewResponse getOverview(Long tenantId, LocalDate startDate, LocalDate endDate);
List<TeacherReportResponse> getTeacherReports(Long tenantId, LocalDate startDate, LocalDate endDate, int limit);
List<CourseReportResponse> getCourseReports(Long tenantId, LocalDate startDate, LocalDate endDate, int limit);
List<StudentReportResponse> getStudentReports(Long tenantId, LocalDate startDate, LocalDate endDate, int limit);
}
```
**SchoolReportServiceImpl.java** - 服务实现
```java
@Slf4j
@Service
@RequiredArgsConstructor
public class SchoolReportServiceImpl implements SchoolReportService {
private final ReportMapper reportMapper;
@Override
public ReportOverviewResponse getOverview(Long tenantId, LocalDate startDate, LocalDate endDate) {
// 计算时间范围(默认本月)
// 调用 Mapper 查询
// 转换为 ReportOverviewResponse 返回
}
// ... 其他方法
}
```
#### 3. 修改 Controller
**SchoolReportController.java** - 添加日期范围参数
```java
@GetMapping("/overview")
public Result<ReportOverviewResponse> getOverview(
@RequestParam(required = false) LocalDate startDate,
@RequestParam(required = false) LocalDate endDate) {
return Result.success(schoolReportService.getOverview(tenantId, startDate, endDate));
}
// ... 其他接口
```
#### 4. 前端 API 对齐
**src/api/school.ts** - 更新类型定义和 API 函数
```typescript
// 更新接口定义
export interface ReportOverview {
reportDate: string;
totalTeachers: number;
totalStudents: number;
totalClasses: number;
monthlyLessons: number;
monthlyTasksCompleted: number;
}
export interface TeacherReport {
teacherId: number;
teacherName: string;
lessonCount: number;
taskCount: number;
averageRating: number;
lastLessonTime?: string;
}
// ... CourseReport, StudentReport
// 更新 API 函数支持日期参数
export const getReportOverview = (startDate?: string, endDate?: string) => {
const params: Record<string, string> = {};
if (startDate) params.startDate = startDate;
if (endDate) params.endDate = endDate;
return http.get<ReportOverview>('/v1/school/reports/overview', { params });
};
// ... 其他 API
```
#### 5. 前端页面完善
**ReportView.vue** - 更新数据绑定和显示
```typescript
// 概览数据
const overviewData = ref<ReportOverview>({
reportDate: '',
totalTeachers: 0,
totalStudents: 0,
totalClasses: 0,
monthlyLessons: 0,
monthlyTasksCompleted: 0,
});
// 加载数据时传递日期参数
const loadData = async () => {
const startDate = dateRange.value?.[0]?.format('YYYY-MM-DD');
const endDate = dateRange.value?.[1]?.format('YYYY-MM-DD');
const [overview, teachers, courses, students] = await Promise.all([
getReportOverview(startDate, endDate),
getTeacherReports(startDate, endDate),
getCourseReports(startDate, endDate),
getStudentReports(startDate, endDate),
]);
// ...
};
// 设置默认日期范围为当月
onMounted(() => {
setDefaultDateRange();
loadData();
});
```
### 字段映射
#### 概览卡片
| 显示项 | 数据字段 |
|--------|---------|
| 教师总数 | totalTeachers |
| 学生总数 | totalStudents |
| 班级总数 | totalClasses |
| 本月授课 | monthlyLessons |
#### 教师报告
| 显示项 | 数据字段 |
|--------|---------|
| 教师姓名 | teacherName |
| 授课次数 | lessonCount |
| 任务完成数 | taskCount |
| 平均评分 | averageRating |
| 最后活跃 | lastLessonTime |
#### 课程报告
| 显示项 | 数据字段 |
|--------|---------|
| 课程名称 | courseName |
| 授课次数 | lessonCount |
| 参与学生 | studentCount |
| 完成率 | completionRate |
| 平均评分 | averageRating |
#### 学生报告
| 显示项 | 数据字段 |
|--------|---------|
| 学生姓名 | studentName |
| 班级 | className |
| 完成任务 | taskCount |
| 成长记录 | growthRecordCount |
| 出勤率 | attendanceRate |
### 功能特性
1. **日期范围筛选**: 支持选择日期范围,默认统计当月数据
2. **实时刷新**: 选择日期范围后自动刷新数据
3. **详情弹窗**: 点击教师/课程可查看详细信息
4. **响应式设计**: 支持不同屏幕尺寸
### 文件变更列表
| 文件 | 变更说明 |
|------|---------|
| `reading-platform-java/src/main/java/com/reading/platform/mapper/ReportMapper.java` | 新建4 个统计查询方法 |
| `reading-platform-java/src/main/java/com/reading/platform/service/SchoolReportService.java` | 新建,服务接口 |
| `reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolReportServiceImpl.java` | 新建,服务实现 |
| `reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolReportController.java` | 修改,调用 Service |
| `reading-platform-frontend/src/api/school.ts` | 更新,类型定义和 API 函数 |
| `reading-platform-frontend/src/views/school/ReportView.vue` | 更新,数据绑定和显示 |
### 测试验证
- [x] 后端编译通过
- [x] ReportView.vue 类型检查通过
- [ ] 启动后端服务(端口 8480
- [ ] 启动前端服务(端口 5173
- [ ] 登录学校管理员账号
- [ ] 访问数据报告页面
- [ ] 验证日期范围筛选
- [ ] 验证各 Tab 数据显示
---
**今日完成**: 学校端数据报告功能4 个统计接口 + 前端页面)