refactor: 代码合规性审查修复 - 三层架构、API路径、文档规范

## P0 三层架构违规修复 (4项)
- 创建 SchoolStatsService/TeacherStatsService,移除Controller直接调用Mapper
- 修复 AdminCourseController 使用 Service 层方法
- 修复 TeacherCourseController 使用 ClassService 获取班级
- 新增 ClassService.getActiveClassesByTenantId()
- 新增 CourseService.createSystemCourse()

## P1 API 路径统一 (8项)
后端路径统一为 /api/v1/admin/*:
- AdminCourseController: /api/admin/courses → /api/v1/admin/courses
- AdminTenantController: /api/admin/tenants → /api/v1/admin/tenants

前端配置调整:
- vite.config.ts: 移除代理重写规则
- src/api/index.ts: baseURL /api/v1 → /api
- 更新 admin.ts, lesson.ts, package.ts, theme.ts 使用 /v1/admin/* 路径

## P2 文档规范更新 (5项)
- 更新 CLAUDE.md 前端 API 调用文档
- 新增三种调用方式说明(http/适配层/Orval客户端)
- 新增 API 路径规范表格
- 更新前端目录结构说明

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Opus 4.6 2026-03-13 14:13:46 +08:00
parent 56508eb066
commit 066b1f2257
20 changed files with 592 additions and 305 deletions

View File

@ -135,8 +135,8 @@ reading-platform-frontend/
│ ├── App.vue # 根组件
│ ├── api/ # API 接口
│ │ ├── generated/ # Orval 自动生成(禁止手改)
│ │ ├── client.ts # 统一入口
│ │ └── *.ts # 业务适配层
│ │ ├── index.ts # 统一入口,导出 http 方法
│ │ └── *.ts # 业务适配层admin.ts, school.ts, teacher.ts 等)
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ ├── composables/ # 组合式函数
@ -174,9 +174,16 @@ reading-platform-frontend/
### Controller 层规范
**API 路径约定**
- 超管端:`/api/v1/admin/*`
- 学校端:`/api/school/*`
- 教师端:`/api/teacher/*`
- 家长端:`/api/parent/*`
- 认证:`/api/auth/*`
```java
@RestController
@RequestMapping("/api/v1/xxx")
@RequestMapping("/api/v1/admin/xxx") // 超管端使用 /api/v1/admin/
@Tag(name = "XXX管理", description = "XXX相关接口")
@RequiredArgsConstructor
public class XxxController {
@ -228,23 +235,64 @@ public interface XxxMapper extends BaseMapper<Xxx> {
## 前端开发规范
### API 开发规范Orval
### API 开发规范
1. **生成代码只读** - 不得在 `src/api/generated/` 内做任何手工修改
2. **以生成类型为准** - 参数/返回类型优先使用生成的类型
3. **统一调用入口** - 通过 `src/api/client.ts` 访问
3. **统一调用入口** - 通过 `src/api/index.ts` 导出的 `http` 方法
### 推荐调用方式
**方式一:使用 http 方法(推荐)**
```typescript
import { readingApi } from '@/api/client';
import { http } from '@/api';
async function loadTenant(id: number) {
const res = await readingApi.getTenant({ id });
return res.data;
return http.get<TenantDetail>(`/v1/admin/tenants/${id}`);
}
async function createTenant(data: CreateTenantDto) {
return http.post<Tenant>('/v1/admin/tenants', data);
}
```
**方式二:使用业务适配层(推荐)**
```typescript
import { getTenant, createTenant } from '@/api/admin';
async function loadTenant(id: number) {
return getTenant(id);
}
async function createNewTenant(data: CreateTenantDto) {
return createTenant(data);
}
```
**方式三:使用 Orval 生成的客户端(可选)**
```typescript
import { getReadingPlatformAPI } from '@/api/generated';
const api = getReadingPlatformAPI();
async function loadTenant(id: number) {
return api.getTenant({ id });
}
```
### API 路径规范
| 端 | 后端路径 | 前端路径 |
|----|----------|----------|
| 超管 | `/api/v1/admin/*` | `/v1/admin/*` |
| 学校 | `/api/school/*` | `/school/*` |
| 教师 | `/api/teacher/*` | `/teacher/*` |
| 家长 | `/api/parent/*` | `/parent/*` |
| 认证 | `/api/auth/*` | `/auth/*` |
### Vue SFC 约定
- 优先使用 `<script lang="ts" setup>`

View File

@ -167,49 +167,49 @@ export interface AdminSettings {
export const getTenants = (params: TenantQueryParams) =>
http.get<{ items: Tenant[]; total: number; page: number; pageSize: number; totalPages: number }>(
'/admin/tenants',
'/v1/admin/tenants',
{ params }
);
export const getTenant = (id: number) =>
http.get<TenantDetail>(`/admin/tenants/${id}`);
http.get<TenantDetail>(`/v1/admin/tenants/${id}`);
export const createTenant = (data: CreateTenantDto) =>
http.post<Tenant & { tempPassword: string }>('/admin/tenants', data);
http.post<Tenant & { tempPassword: string }>('/v1/admin/tenants', data);
export const updateTenant = (id: number, data: UpdateTenantDto) =>
http.put<Tenant>(`/admin/tenants/${id}`, data);
http.put<Tenant>(`/v1/admin/tenants/${id}`, data);
export const updateTenantQuota = (id: number, data: UpdateTenantQuotaDto) =>
http.put<Tenant>(`/admin/tenants/${id}/quota`, data);
http.put<Tenant>(`/v1/admin/tenants/${id}/quota`, data);
export const updateTenantStatus = (id: number, status: string) =>
http.put<{ id: number; name: string; status: string }>(`/admin/tenants/${id}/status`, { status });
http.put<{ id: number; name: string; status: string }>(`/v1/admin/tenants/${id}/status`, { status });
export const resetTenantPassword = (id: number) =>
http.post<{ tempPassword: string }>(`/admin/tenants/${id}/reset-password`);
http.post<{ tempPassword: string }>(`/v1/admin/tenants/${id}/reset-password`);
export const deleteTenant = (id: number) =>
http.delete<{ success: boolean }>(`/admin/tenants/${id}`);
http.delete<{ success: boolean }>(`/v1/admin/tenants/${id}`);
// ==================== 统计数据 ====================
export const getAdminStats = () =>
http.get<AdminStats>('/admin/stats');
http.get<AdminStats>('/v1/admin/stats');
export const getTrendData = () =>
http.get<TrendData[]>('/admin/stats/trend');
http.get<TrendData[]>('/v1/admin/stats/trend');
export const getActiveTenants = (limit?: number) =>
http.get<ActiveTenant[]>('/admin/stats/tenants/active', { params: { limit } });
http.get<ActiveTenant[]>('/v1/admin/stats/tenants/active', { params: { limit } });
export const getPopularCourses = (limit?: number) =>
http.get<PopularCourse[]>('/admin/stats/courses/popular', { params: { limit } });
http.get<PopularCourse[]>('/v1/admin/stats/courses/popular', { params: { limit } });
// ==================== 系统设置 ====================
export const getAdminSettings = () =>
http.get<AdminSettings>('/admin/settings');
http.get<AdminSettings>('/v1/admin/settings');
export const updateAdminSettings = (data: Record<string, any>) =>
http.put<AdminSettings>('/admin/settings', data);
http.put<AdminSettings>('/v1/admin/settings', data);

View File

@ -3,7 +3,7 @@ import { message } from 'ant-design-vue';
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: '/api/v1', // 使用 /api/v1,代理会保留完整路径
baseURL: '/api', // 使用 /api 作为统一前缀,超管端路径包含 /v1
timeout: 30000,
headers: {
'Content-Type': 'application/json',

View File

@ -86,64 +86,64 @@ export interface CreateStepData {
// 获取课程列表
export function getLessonList(courseId: number) {
return http.get(`/admin/courses/${courseId}/lessons`);
return http.get(`/v1/admin/courses/${courseId}/lessons`);
}
// 获取课程详情
export function getLessonDetail(courseId: number, lessonId: number) {
return http.get(`/admin/courses/${courseId}/lessons/${lessonId}`);
return http.get(`/v1/admin/courses/${courseId}/lessons/${lessonId}`);
}
// 按类型获取课程
export function getLessonByType(courseId: number, lessonType: string) {
return http.get(`/admin/courses/${courseId}/lessons/type/${lessonType}`);
return http.get(`/v1/admin/courses/${courseId}/lessons/type/${lessonType}`);
}
// 创建课程
export function createLesson(courseId: number, data: CreateLessonData) {
return http.post(`/admin/courses/${courseId}/lessons`, data);
return http.post(`/v1/admin/courses/${courseId}/lessons`, data);
}
// 更新课程
export function updateLesson(lessonId: number, data: Partial<CreateLessonData>) {
return http.put(`/admin/courses/0/lessons/${lessonId}`, data);
return http.put(`/v1/admin/courses/0/lessons/${lessonId}`, data);
}
// 删除课程
export function deleteLesson(courseId: number, lessonId: number) {
return http.delete(`/admin/courses/${courseId}/lessons/${lessonId}`);
return http.delete(`/v1/admin/courses/${courseId}/lessons/${lessonId}`);
}
// 重新排序课程
export function reorderLessons(courseId: number, lessonIds: number[]) {
return http.put(`/admin/courses/${courseId}/lessons/reorder`, { lessonIds });
return http.put(`/v1/admin/courses/${courseId}/lessons/reorder`, { lessonIds });
}
// ==================== 教学环节 API ====================
// 获取环节列表
export function getStepList(courseId: number, lessonId: number) {
return http.get(`/admin/courses/${courseId}/lessons/${lessonId}/steps`);
return http.get(`/v1/admin/courses/${courseId}/lessons/${lessonId}/steps`);
}
// 创建环节
export function createStep(courseId: number, lessonId: number, data: CreateStepData) {
return http.post(`/admin/courses/${courseId}/lessons/${lessonId}/steps`, data);
return http.post(`/v1/admin/courses/${courseId}/lessons/${lessonId}/steps`, data);
}
// 更新环节
export function updateStep(stepId: number, data: Partial<CreateStepData>) {
return http.put(`/admin/courses/0/lessons/steps/${stepId}`, data);
return http.put(`/v1/admin/courses/0/lessons/steps/${stepId}`, data);
}
// 删除环节
export function deleteStep(courseId: number, _lessonId: number, stepId: number) {
return http.delete(`/admin/courses/${courseId}/lessons/steps/${stepId}`);
return http.delete(`/v1/admin/courses/${courseId}/lessons/steps/${stepId}`);
}
// 重新排序环节
export function reorderSteps(courseId: number, lessonId: number, stepIds: number[]) {
return http.put(`/admin/courses/${courseId}/lessons/${lessonId}/steps/reorder`, { stepIds });
return http.put(`/v1/admin/courses/${courseId}/lessons/${lessonId}/steps/reorder`, { stepIds });
}
// ==================== 教师端 API ====================

View File

@ -49,27 +49,27 @@ export interface CreatePackageData {
// 获取套餐列表
export function getPackageList(params?: PackageListParams) {
return http.get('/admin/packages', { params });
return http.get('/v1/admin/packages', { params });
}
// 获取套餐详情
export function getPackageDetail(id: number) {
return http.get(`/admin/packages/${id}`);
return http.get(`/v1/admin/packages/${id}`);
}
// 创建套餐
export function createPackage(data: CreatePackageData) {
return http.post('/admin/packages', data);
return http.post('/v1/admin/packages', data);
}
// 更新套餐
export function updatePackage(id: number, data: Partial<CreatePackageData>) {
return http.put(`/admin/packages/${id}`, data);
return http.put(`/v1/admin/packages/${id}`, data);
}
// 删除套餐
export function deletePackage(id: number) {
return http.delete(`/admin/packages/${id}`);
return http.delete(`/v1/admin/packages/${id}`);
}
// 设置套餐课程
@ -77,7 +77,7 @@ export function setPackageCourses(
packageId: number,
courses: { courseId: number; gradeLevel: string; sortOrder?: number }[],
) {
return http.put(`/admin/packages/${packageId}/courses`, { courses });
return http.put(`/v1/admin/packages/${packageId}/courses`, { courses });
}
// 添加课程到套餐
@ -85,32 +85,32 @@ export function addCourseToPackage(
packageId: number,
data: { courseId: number; gradeLevel: string; sortOrder?: number },
) {
return http.post(`/admin/packages/${packageId}/courses`, data);
return http.post(`/v1/admin/packages/${packageId}/courses`, data);
}
// 从套餐移除课程
export function removeCourseFromPackage(packageId: number, courseId: number) {
return http.delete(`/admin/packages/${packageId}/courses/${courseId}`);
return http.delete(`/v1/admin/packages/${packageId}/courses/${courseId}`);
}
// 提交审核
export function submitPackage(id: number) {
return http.post(`/admin/packages/${id}/submit`);
return http.post(`/v1/admin/packages/${id}/submit`);
}
// 审核套餐
export function reviewPackage(id: number, data: { approved: boolean; comment?: string }) {
return http.post(`/admin/packages/${id}/review`, data);
return http.post(`/v1/admin/packages/${id}/review`, data);
}
// 发布套餐
export function publishPackage(id: number) {
return http.post(`/admin/packages/${id}/publish`);
return http.post(`/v1/admin/packages/${id}/publish`);
}
// 下架套餐
export function offlinePackage(id: number) {
return http.post(`/admin/packages/${id}/offline`);
return http.post(`/v1/admin/packages/${id}/offline`);
}
// ==================== 学校端套餐 ====================

View File

@ -29,30 +29,30 @@ export interface UpdateThemeData {
// 获取主题列表
export function getThemeList() {
return http.get('/admin/themes');
return http.get('/v1/admin/themes');
}
// 获取主题详情
export function getThemeDetail(id: number) {
return http.get(`/admin/themes/${id}`);
return http.get(`/v1/admin/themes/${id}`);
}
// 创建主题
export function createTheme(data: CreateThemeData) {
return http.post('/admin/themes', data);
return http.post('/v1/admin/themes', data);
}
// 更新主题
export function updateTheme(id: number, data: UpdateThemeData) {
return http.put(`/admin/themes/${id}`, data);
return http.put(`/v1/admin/themes/${id}`, data);
}
// 删除主题
export function deleteTheme(id: number) {
return http.delete(`/admin/themes/${id}`);
return http.delete(`/v1/admin/themes/${id}`);
}
// 重新排序主题
export function reorderThemes(ids: number[]) {
return http.put('/admin/themes/reorder', { ids });
return http.put('/v1/admin/themes/reorder', { ids });
}

View File

@ -56,7 +56,6 @@ export default defineConfig({
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v1/, '/api'),
},
'/uploads': {
target: 'http://localhost:8080',

View File

@ -8,7 +8,6 @@ import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.CourseCreateRequest;
import com.reading.platform.dto.request.CourseUpdateRequest;
import com.reading.platform.entity.Course;
import com.reading.platform.mapper.CourseMapper;
import com.reading.platform.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -18,21 +17,17 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "Admin - Course", description = "System Course Management APIs for Admin")
@RestController
@RequestMapping("/api/admin/courses")
@RequestMapping("/api/v1/admin/courses")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminCourseController {
private final CourseService courseService;
private final CourseMapper courseMapper;
@Operation(summary = "Create system course")
@PostMapping
public Result<Course> createCourse(@Valid @RequestBody CourseCreateRequest request) {
// System courses have null tenantId
Course course = courseService.createCourse(null, request);
course.setIsSystem(1);
courseMapper.updateById(course); // Save isSystem to database
Course course = courseService.createSystemCourse(request);
return Result.success(course);
}

View File

@ -20,7 +20,7 @@ import java.util.List;
@Tag(name = "Admin - Tenant", description = "Tenant Management APIs for Admin")
@RestController
@RequestMapping("/api/admin/tenants")
@RequestMapping("/api/v1/admin/tenants")
@RequiredArgsConstructor
@RequireRole(UserRole.ADMIN)
public class AdminTenantController {

View File

@ -1,22 +1,15 @@
package com.reading.platform.controller.school;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.entity.Clazz;
import com.reading.platform.entity.Student;
import com.reading.platform.entity.Teacher;
import com.reading.platform.mapper.ClazzMapper;
import com.reading.platform.mapper.StudentMapper;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.service.SchoolStatsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.List;
import java.util.Map;
/**
* 统计数据控制器学校端
@ -27,81 +20,50 @@ import java.util.*;
@Tag(name = "学校端 - 统计数据")
public class SchoolStatsController {
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
private final ClazzMapper clazzMapper;
private final SchoolStatsService schoolStatsService;
@GetMapping
@Operation(summary = "获取学校统计数据")
public Result<Map<String, Object>> getSchoolStats() {
Long tenantId = SecurityUtils.getCurrentTenantId();
Map<String, Object> stats = new HashMap<>();
stats.put("teacherCount", teacherMapper.selectCount(
new LambdaQueryWrapper<Teacher>().eq(Teacher::getTenantId, tenantId)
));
stats.put("studentCount", studentMapper.selectCount(
new LambdaQueryWrapper<Student>().eq(Student::getTenantId, tenantId)
));
stats.put("classCount", clazzMapper.selectCount(
new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId)
));
stats.put("lessonCount", 0); // TODO: implement lesson count
return Result.success(stats);
return Result.success(schoolStatsService.getSchoolStats(tenantId));
}
@GetMapping("/teachers")
@Operation(summary = "获取活跃教师排行")
public Result<List<Map<String, Object>>> getActiveTeachers(
@RequestParam(defaultValue = "5") int limit) {
List<Map<String, Object>> teachers = new ArrayList<>();
// For now, return empty list
return Result.success(teachers);
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getActiveTeachers(tenantId, limit));
}
@GetMapping("/courses")
@Operation(summary = "获取课程使用统计")
public Result<List<Map<String, Object>>> getCourseUsageStats() {
List<Map<String, Object>> courses = new ArrayList<>();
// For now, return empty list
return Result.success(courses);
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getCourseUsageStats(tenantId));
}
@GetMapping("/activities")
@Operation(summary = "获取近期活动")
public Result<List<Map<String, Object>>> getRecentActivities(
@RequestParam(defaultValue = "10") int limit) {
List<Map<String, Object>> activities = new ArrayList<>();
// For now, return empty list
return Result.success(activities);
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getRecentActivities(tenantId, limit));
}
@GetMapping("/lesson-trend")
@Operation(summary = "获取授课趋势")
public Result<List<Map<String, Object>>> getLessonTrend(
@RequestParam(defaultValue = "6") int months) {
List<Map<String, Object>> trend = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
for (int i = months - 1; i >= 0; i--) {
LocalDate date = LocalDate.now().minusMonths(i);
String month = date.format(formatter);
Map<String, Object> item = new HashMap<>();
item.put("month", month);
item.put("lessonCount", 0);
item.put("studentCount", 0);
trend.add(item);
}
return Result.success(trend);
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getLessonTrend(tenantId, months));
}
@GetMapping("/course-distribution")
@Operation(summary = "获取课程分布")
public Result<List<Map<String, Object>>> getCourseDistribution() {
List<Map<String, Object>> distribution = new ArrayList<>();
// For now, return empty list
return Result.success(distribution);
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getCourseDistribution(tenantId));
}
}

View File

@ -1,13 +1,12 @@
package com.reading.platform.controller.teacher;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.response.PageResult;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.entity.Clazz;
import com.reading.platform.entity.Course;
import com.reading.platform.mapper.ClazzMapper;
import com.reading.platform.service.ClassService;
import com.reading.platform.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -23,17 +22,13 @@ import java.util.List;
public class TeacherCourseController {
private final CourseService courseService;
private final ClazzMapper clazzMapper;
private final ClassService classService;
@Operation(summary = "Get teacher's classes")
@GetMapping("/classes")
public Result<List<Clazz>> getClasses() {
Long tenantId = SecurityUtils.getCurrentTenantId();
List<Clazz> classes = clazzMapper.selectList(
new LambdaQueryWrapper<Clazz>()
.eq(Clazz::getTenantId, tenantId)
.eq(Clazz::getStatus, "active")
);
List<Clazz> classes = classService.getActiveClassesByTenantId(tenantId);
return Result.success(classes);
}

View File

@ -1,27 +1,17 @@
package com.reading.platform.controller.teacher;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.entity.Clazz;
import com.reading.platform.entity.Course;
import com.reading.platform.entity.Lesson;
import com.reading.platform.entity.Student;
import com.reading.platform.mapper.ClazzMapper;
import com.reading.platform.mapper.CourseMapper;
import com.reading.platform.mapper.LessonMapper;
import com.reading.platform.mapper.StudentMapper;
import com.reading.platform.service.TeacherStatsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Slf4j
import java.util.List;
import java.util.Map;
/**
* 统计数据控制器教师端
@ -32,153 +22,35 @@ import java.util.*;
@Tag(name = "教师端 - 统计数据")
public class TeacherStatsController {
private final ClazzMapper clazzMapper;
private final StudentMapper studentMapper;
private final CourseMapper courseMapper;
private final LessonMapper lessonMapper;
private final TeacherStatsService teacherStatsService;
@GetMapping("/dashboard")
@Operation(summary = "获取教师端首页统计数据")
public Result<Map<String, Object>> getDashboard() {
Long teacherId = SecurityUtils.getCurrentUserId();
Long tenantId = SecurityUtils.getCurrentTenantId();
Map<String, Object> dashboard = new HashMap<>();
// 基础统计
Map<String, Object> stats = new HashMap<>();
stats.put("classCount", clazzMapper.selectCount(
new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId)
));
stats.put("studentCount", studentMapper.selectCount(
new LambdaQueryWrapper<Student>().eq(Student::getTenantId, tenantId)
));
stats.put("courseCount", courseMapper.selectCount(
new LambdaQueryWrapper<Course>().eq(Course::getTenantId, tenantId)
));
// Lesson count (handle missing table gracefully)
long lessonCount = 0;
try {
lessonCount = lessonMapper.selectCount(
new LambdaQueryWrapper<Lesson>().eq(Lesson::getTeacherId, teacherId)
);
} catch (Exception e) {
log.warn("Failed to query lessons table: {}", e.getMessage());
}
stats.put("lessonCount", lessonCount);
dashboard.put("stats", stats);
// 今日课程
LocalDate today = LocalDate.now();
List<Lesson> todayLessons = new ArrayList<>();
try {
todayLessons = lessonMapper.selectList(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.eq(Lesson::getLessonDate, today)
.orderByAsc(Lesson::getStartTime)
);
} catch (Exception e) {
log.warn("Failed to query today lessons: {}", e.getMessage());
}
dashboard.put("todayLessons", todayLessons);
// 推荐课程热门课程
List<Course> recommendedCourses = courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.eq(Course::getTenantId, tenantId)
.eq(Course::getStatus, "published")
.orderByDesc(Course::getUsageCount)
.last("LIMIT 5")
);
dashboard.put("recommendedCourses", recommendedCourses);
// 本周统计
Map<String, Object> weeklyStats = new HashMap<>();
LocalDate weekAgo = LocalDate.now().minusDays(7);
long weeklyLessonCount = 0;
try {
weeklyLessonCount = lessonMapper.selectCount(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.ge(Lesson::getLessonDate, weekAgo)
);
} catch (Exception e) {
log.warn("Failed to query weekly lessons: {}", e.getMessage());
}
weeklyStats.put("lessonCount", weeklyLessonCount);
weeklyStats.put("studentParticipation", 85); // TODO: calculate actual participation
weeklyStats.put("avgRating", 4.5); // TODO: calculate actual rating
weeklyStats.put("totalDuration", 300); // TODO: calculate actual duration
dashboard.put("weeklyStats", weeklyStats);
// 近期活动
List<Map<String, Object>> recentActivities = new ArrayList<>();
// For now, return empty list
dashboard.put("recentActivities", recentActivities);
return Result.success(dashboard);
return Result.success(teacherStatsService.getDashboard(teacherId, tenantId));
}
@GetMapping("/today-lessons")
@Operation(summary = "获取今日课程")
public Result<List<Lesson>> getTodayLessons() {
Long teacherId = SecurityUtils.getCurrentUserId();
LocalDate today = LocalDate.now();
List<Lesson> lessons = new ArrayList<>();
try {
lessons = lessonMapper.selectList(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.eq(Lesson::getLessonDate, today)
.orderByAsc(Lesson::getStartTime)
);
} catch (Exception e) {
log.warn("Failed to query today lessons: {}", e.getMessage());
}
return Result.success(lessons);
return Result.success(teacherStatsService.getTodayLessons(teacherId));
}
@GetMapping("/recommended-courses")
@Operation(summary = "获取推荐课程")
public Result<List<Course>> getRecommendedCourses() {
Long tenantId = SecurityUtils.getCurrentTenantId();
List<Course> courses = courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.eq(Course::getTenantId, tenantId)
.eq(Course::getStatus, "published")
.orderByDesc(Course::getUsageCount)
.last("LIMIT 10")
);
return Result.success(courses);
return Result.success(teacherStatsService.getRecommendedCourses(tenantId));
}
@GetMapping("/weekly-stats")
@Operation(summary = "获取本周统计")
public Result<Map<String, Object>> getWeeklyStats() {
Long teacherId = SecurityUtils.getCurrentUserId();
LocalDate weekAgo = LocalDate.now().minusDays(7);
Map<String, Object> stats = new HashMap<>();
long lessonCount = 0;
try {
lessonCount = lessonMapper.selectCount(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.ge(Lesson::getLessonDate, weekAgo)
);
} catch (Exception e) {
log.warn("Failed to query weekly lesson count: {}", e.getMessage());
}
stats.put("lessonCount", lessonCount);
stats.put("studentParticipation", 85);
stats.put("avgRating", 4.5);
stats.put("totalDuration", 300);
return Result.success(stats);
return Result.success(teacherStatsService.getWeeklyStats(teacherId));
}
@GetMapping("/lesson-trend")
@ -186,57 +58,13 @@ public class TeacherStatsController {
public Result<List<Map<String, Object>>> getLessonTrend(
@RequestParam(defaultValue = "6") int months) {
Long teacherId = SecurityUtils.getCurrentUserId();
List<Map<String, Object>> trend = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
for (int i = months - 1; i >= 0; i--) {
LocalDate date = LocalDate.now().minusMonths(i);
String month = date.format(formatter);
LocalDate monthStart = date.withDayOfMonth(1);
LocalDate monthEnd = date.plusMonths(1).withDayOfMonth(1);
long lessonCount = 0;
try {
lessonCount = lessonMapper.selectCount(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.ge(Lesson::getLessonDate, monthStart)
.lt(Lesson::getLessonDate, monthEnd)
);
} catch (Exception e) {
log.warn("Failed to query lesson trend for {}: {}", month, e.getMessage());
}
Map<String, Object> item = new HashMap<>();
item.put("month", month);
item.put("lessonCount", lessonCount);
item.put("avgRating", 4.5); // TODO: calculate actual rating
trend.add(item);
}
return Result.success(trend);
return Result.success(teacherStatsService.getLessonTrend(teacherId, months));
}
@GetMapping("/course-usage")
@Operation(summary = "获取课程使用统计")
public Result<List<Map<String, Object>>> getCourseUsage() {
Long tenantId = SecurityUtils.getCurrentTenantId();
List<Map<String, Object>> usage = new ArrayList<>();
List<Course> courses = courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.eq(Course::getTenantId, tenantId)
.orderByDesc(Course::getUsageCount)
.last("LIMIT 10")
);
for (Course course : courses) {
Map<String, Object> item = new HashMap<>();
item.put("name", course.getName());
item.put("value", course.getUsageCount() != null ? course.getUsageCount() : 0);
usage.add(item);
}
return Result.success(usage);
return Result.success(teacherStatsService.getCourseUsage(tenantId));
}
}

View File

@ -28,4 +28,6 @@ public interface ClassService {
List<Long> getTeacherIdsByClassId(Long classId);
List<Clazz> getActiveClassesByTenantId(Long tenantId);
}

View File

@ -14,6 +14,8 @@ public interface CourseService {
Course createCourse(Long tenantId, CourseCreateRequest request);
Course createSystemCourse(CourseCreateRequest request);
Course updateCourse(Long id, CourseUpdateRequest request);
Course getCourseById(Long id);

View File

@ -0,0 +1,40 @@
package com.reading.platform.service;
import java.util.List;
import java.util.Map;
/**
* School Statistics Service Interface
*/
public interface SchoolStatsService {
/**
* Get school statistics (teacher count, student count, class count)
*/
Map<String, Object> getSchoolStats(Long tenantId);
/**
* Get active teachers ranking
*/
List<Map<String, Object>> getActiveTeachers(Long tenantId, int limit);
/**
* Get course usage statistics
*/
List<Map<String, Object>> getCourseUsageStats(Long tenantId);
/**
* Get recent activities
*/
List<Map<String, Object>> getRecentActivities(Long tenantId, int limit);
/**
* Get lesson trend by month
*/
List<Map<String, Object>> getLessonTrend(Long tenantId, int months);
/**
* Get course distribution
*/
List<Map<String, Object>> getCourseDistribution(Long tenantId);
}

View File

@ -0,0 +1,43 @@
package com.reading.platform.service;
import com.reading.platform.entity.Course;
import com.reading.platform.entity.Lesson;
import java.util.List;
import java.util.Map;
/**
* Teacher Statistics Service Interface
*/
public interface TeacherStatsService {
/**
* Get teacher dashboard statistics
*/
Map<String, Object> getDashboard(Long teacherId, Long tenantId);
/**
* Get today's lessons
*/
List<Lesson> getTodayLessons(Long teacherId);
/**
* Get recommended courses
*/
List<Course> getRecommendedCourses(Long tenantId);
/**
* Get weekly statistics
*/
Map<String, Object> getWeeklyStats(Long teacherId);
/**
* Get lesson trend by month
*/
List<Map<String, Object>> getLessonTrend(Long teacherId, int months);
/**
* Get course usage statistics
*/
List<Map<String, Object>> getCourseUsage(Long tenantId);
}

View File

@ -172,4 +172,13 @@ public class ClassServiceImpl implements ClassService {
return teacherIds;
}
@Override
public List<Clazz> getActiveClassesByTenantId(Long tenantId) {
return clazzMapper.selectList(
new LambdaQueryWrapper<Clazz>()
.eq(Clazz::getTenantId, tenantId)
.eq(Clazz::getStatus, "active")
);
}
}

View File

@ -84,6 +84,64 @@ public class CourseServiceImpl implements CourseService {
return course;
}
@Override
@Transactional
public Course createSystemCourse(CourseCreateRequest request) {
Course course = new Course();
course.setTenantId(null); // System courses have null tenantId
course.setName(request.getName());
course.setCode(request.getCode());
course.setDescription(request.getDescription());
course.setCoverUrl(request.getCoverUrl());
course.setCoverImagePath(request.getCoverImagePath());
course.setCategory(request.getCategory());
course.setAgeRange(request.getAgeRange());
course.setDifficultyLevel(request.getDifficultyLevel());
course.setDurationMinutes(request.getDurationMinutes());
course.setObjectives(request.getObjectives());
course.setStatus("draft");
course.setIsSystem(1);
// Course Package Fields
course.setCoreContent(request.getCoreContent());
course.setIntroSummary(request.getIntroSummary());
course.setIntroHighlights(request.getIntroHighlights());
course.setIntroGoals(request.getIntroGoals());
course.setIntroSchedule(request.getIntroSchedule());
course.setIntroKeyPoints(request.getIntroKeyPoints());
course.setIntroMethods(request.getIntroMethods());
course.setIntroEvaluation(request.getIntroEvaluation());
course.setIntroNotes(request.getIntroNotes());
course.setScheduleRefData(request.getScheduleRefData());
course.setEnvironmentConstruction(request.getEnvironmentConstruction());
course.setThemeId(request.getThemeId());
course.setPictureBookName(request.getPictureBookName());
course.setEbookPaths(request.getEbookPaths());
course.setAudioPaths(request.getAudioPaths());
course.setVideoPaths(request.getVideoPaths());
course.setOtherResources(request.getOtherResources());
course.setPptPath(request.getPptPath());
course.setPptName(request.getPptName());
course.setPosterPaths(request.getPosterPaths());
course.setTools(request.getTools());
course.setStudentMaterials(request.getStudentMaterials());
course.setLessonPlanData(request.getLessonPlanData());
course.setActivitiesData(request.getActivitiesData());
course.setAssessmentData(request.getAssessmentData());
course.setGradeTags(request.getGradeTags());
course.setDomainTags(request.getDomainTags());
course.setHasCollectiveLesson(request.getHasCollectiveLesson() != null && request.getHasCollectiveLesson() ? 1 : 0);
course.setVersion("1.0");
course.setIsLatest(1);
course.setUsageCount(0);
course.setTeacherCount(0);
courseMapper.insert(course);
log.info("System course created: id={}, name={}", course.getId(), course.getName());
return course;
}
@Override
@Transactional
public Course updateCourse(Long id, CourseUpdateRequest request) {

View File

@ -0,0 +1,91 @@
package com.reading.platform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.entity.Clazz;
import com.reading.platform.entity.Student;
import com.reading.platform.entity.Teacher;
import com.reading.platform.mapper.ClazzMapper;
import com.reading.platform.mapper.StudentMapper;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.service.SchoolStatsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* School Statistics Service Implementation
*/
@Service
@RequiredArgsConstructor
public class SchoolStatsServiceImpl implements SchoolStatsService {
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
private final ClazzMapper clazzMapper;
@Override
public Map<String, Object> getSchoolStats(Long tenantId) {
Map<String, Object> stats = new HashMap<>();
stats.put("teacherCount", teacherMapper.selectCount(
new LambdaQueryWrapper<Teacher>().eq(Teacher::getTenantId, tenantId)
));
stats.put("studentCount", studentMapper.selectCount(
new LambdaQueryWrapper<Student>().eq(Student::getTenantId, tenantId)
));
stats.put("classCount", clazzMapper.selectCount(
new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId)
));
stats.put("lessonCount", 0); // TODO: implement lesson count
return stats;
}
@Override
public List<Map<String, Object>> getActiveTeachers(Long tenantId, int limit) {
List<Map<String, Object>> teachers = new ArrayList<>();
// For now, return empty list
return teachers;
}
@Override
public List<Map<String, Object>> getCourseUsageStats(Long tenantId) {
List<Map<String, Object>> courses = new ArrayList<>();
// For now, return empty list
return courses;
}
@Override
public List<Map<String, Object>> getRecentActivities(Long tenantId, int limit) {
List<Map<String, Object>> activities = new ArrayList<>();
// For now, return empty list
return activities;
}
@Override
public List<Map<String, Object>> getLessonTrend(Long tenantId, int months) {
List<Map<String, Object>> trend = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
for (int i = months - 1; i >= 0; i--) {
LocalDate date = LocalDate.now().minusMonths(i);
String month = date.format(formatter);
Map<String, Object> item = new HashMap<>();
item.put("month", month);
item.put("lessonCount", 0);
item.put("studentCount", 0);
trend.add(item);
}
return trend;
}
@Override
public List<Map<String, Object>> getCourseDistribution(Long tenantId) {
List<Map<String, Object>> distribution = new ArrayList<>();
// For now, return empty list
return distribution;
}
}

View File

@ -0,0 +1,215 @@
package com.reading.platform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.entity.Clazz;
import com.reading.platform.entity.Course;
import com.reading.platform.entity.Lesson;
import com.reading.platform.entity.Student;
import com.reading.platform.mapper.ClazzMapper;
import com.reading.platform.mapper.CourseMapper;
import com.reading.platform.mapper.LessonMapper;
import com.reading.platform.mapper.StudentMapper;
import com.reading.platform.service.TeacherStatsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* Teacher Statistics Service Implementation
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TeacherStatsServiceImpl implements TeacherStatsService {
private final ClazzMapper clazzMapper;
private final StudentMapper studentMapper;
private final CourseMapper courseMapper;
private final LessonMapper lessonMapper;
@Override
public Map<String, Object> getDashboard(Long teacherId, Long tenantId) {
Map<String, Object> dashboard = new HashMap<>();
// 基础统计
Map<String, Object> stats = new HashMap<>();
stats.put("classCount", clazzMapper.selectCount(
new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId)
));
stats.put("studentCount", studentMapper.selectCount(
new LambdaQueryWrapper<Student>().eq(Student::getTenantId, tenantId)
));
stats.put("courseCount", courseMapper.selectCount(
new LambdaQueryWrapper<Course>().eq(Course::getTenantId, tenantId)
));
// Lesson count (handle missing table gracefully)
long lessonCount = 0;
try {
lessonCount = lessonMapper.selectCount(
new LambdaQueryWrapper<Lesson>().eq(Lesson::getTeacherId, teacherId)
);
} catch (Exception e) {
log.warn("Failed to query lessons table: {}", e.getMessage());
}
stats.put("lessonCount", lessonCount);
dashboard.put("stats", stats);
// 今日课程
LocalDate today = LocalDate.now();
List<Lesson> todayLessons = new ArrayList<>();
try {
todayLessons = lessonMapper.selectList(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.eq(Lesson::getLessonDate, today)
.orderByAsc(Lesson::getStartTime)
);
} catch (Exception e) {
log.warn("Failed to query today lessons: {}", e.getMessage());
}
dashboard.put("todayLessons", todayLessons);
// 推荐课程热门课程
List<Course> recommendedCourses = courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.eq(Course::getTenantId, tenantId)
.eq(Course::getStatus, "published")
.orderByDesc(Course::getUsageCount)
.last("LIMIT 5")
);
dashboard.put("recommendedCourses", recommendedCourses);
// 本周统计
Map<String, Object> weeklyStats = new HashMap<>();
LocalDate weekAgo = LocalDate.now().minusDays(7);
long weeklyLessonCount = 0;
try {
weeklyLessonCount = lessonMapper.selectCount(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.ge(Lesson::getLessonDate, weekAgo)
);
} catch (Exception e) {
log.warn("Failed to query weekly lessons: {}", e.getMessage());
}
weeklyStats.put("lessonCount", weeklyLessonCount);
weeklyStats.put("studentParticipation", 85); // TODO: calculate actual participation
weeklyStats.put("avgRating", 4.5); // TODO: calculate actual rating
weeklyStats.put("totalDuration", 300); // TODO: calculate actual duration
dashboard.put("weeklyStats", weeklyStats);
// 近期活动
List<Map<String, Object>> recentActivities = new ArrayList<>();
dashboard.put("recentActivities", recentActivities);
return dashboard;
}
@Override
public List<Lesson> getTodayLessons(Long teacherId) {
LocalDate today = LocalDate.now();
List<Lesson> lessons = new ArrayList<>();
try {
lessons = lessonMapper.selectList(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.eq(Lesson::getLessonDate, today)
.orderByAsc(Lesson::getStartTime)
);
} catch (Exception e) {
log.warn("Failed to query today lessons: {}", e.getMessage());
}
return lessons;
}
@Override
public List<Course> getRecommendedCourses(Long tenantId) {
return courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.eq(Course::getTenantId, tenantId)
.eq(Course::getStatus, "published")
.orderByDesc(Course::getUsageCount)
.last("LIMIT 10")
);
}
@Override
public Map<String, Object> getWeeklyStats(Long teacherId) {
LocalDate weekAgo = LocalDate.now().minusDays(7);
Map<String, Object> stats = new HashMap<>();
long lessonCount = 0;
try {
lessonCount = lessonMapper.selectCount(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.ge(Lesson::getLessonDate, weekAgo)
);
} catch (Exception e) {
log.warn("Failed to query weekly lesson count: {}", e.getMessage());
}
stats.put("lessonCount", lessonCount);
stats.put("studentParticipation", 85);
stats.put("avgRating", 4.5);
stats.put("totalDuration", 300);
return stats;
}
@Override
public List<Map<String, Object>> getLessonTrend(Long teacherId, int months) {
List<Map<String, Object>> trend = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
for (int i = months - 1; i >= 0; i--) {
LocalDate date = LocalDate.now().minusMonths(i);
String month = date.format(formatter);
LocalDate monthStart = date.withDayOfMonth(1);
LocalDate monthEnd = date.plusMonths(1).withDayOfMonth(1);
long lessonCount = 0;
try {
lessonCount = lessonMapper.selectCount(
new LambdaQueryWrapper<Lesson>()
.eq(Lesson::getTeacherId, teacherId)
.ge(Lesson::getLessonDate, monthStart)
.lt(Lesson::getLessonDate, monthEnd)
);
} catch (Exception e) {
log.warn("Failed to query lesson trend for {}: {}", month, e.getMessage());
}
Map<String, Object> item = new HashMap<>();
item.put("month", month);
item.put("lessonCount", lessonCount);
item.put("avgRating", 4.5); // TODO: calculate actual rating
trend.add(item);
}
return trend;
}
@Override
public List<Map<String, Object>> getCourseUsage(Long tenantId) {
List<Map<String, Object>> usage = new ArrayList<>();
List<Course> courses = courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.eq(Course::getTenantId, tenantId)
.orderByDesc(Course::getUsageCount)
.last("LIMIT 10")
);
for (Course course : courses) {
Map<String, Object> item = new HashMap<>();
item.put("name", course.getName());
item.put("value", course.getUsageCount() != null ? course.getUsageCount() : 0);
usage.add(item);
}
return usage;
}
}