170 lines
5.7 KiB
Markdown
170 lines
5.7 KiB
Markdown
|
|
# 开发日志 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. **性能优化**:大数据量时考虑缓存
|