主要变更: 1. 新增学校报告服务 (SchoolReportService) - 学校概览统计 (getOverviewStats) - 教师统计报表 (getTeacherStats) - 课程统计报表 (getCourseStats) - 学生统计报表 (getStudentStats) - 课时趋势分析 (getLessonTrend) 2. 新增学校端 Controller - SchoolReportController: 学校统计报告接口 - SchoolResourceController: 学校资源管理接口 - SchoolFeedbackController: 学校反馈管理接口 3. 完善实体类字段 - CourseLesson: 添加 lessonOrder 字段 - ResourceItem: 添加 tenantId、type 字段 - Task: 添加 name 字段 - LessonFeedback: 添加 courseId、tenantId、overallRating 字段 4. 完善服务层实现 - ResourceServiceImpl: 实现资源库和资源项管理方法 - SchoolReportServiceImpl: 实现学校统计报表逻辑 - TeacherDashboardServiceImpl: 修复时间类型转换 - AdminStatsServiceImpl: 完善统计逻辑 5. 新增 Flyway 迁移脚本 (V2) - 添加 ORM 实体类缺失字段的数据库迁移 6. 修复路由冲突 - 移除 AdminCourseController 中重复的 getCourseLessons 方法 7. 添加测试工具类 - CheckDatabase, CheckClazzTable: 数据库检查工具 - InitDatabase, InitClasses: 数据初始化工具 - GeneratePasswordHash: 密码哈希生成工具 8. 配置 Maven Wrapper - 添加 maven-wrapper.properties 和 mvnw.cmd - 确保使用 Java 17 编译
42 KiB
CLAUDE.md
本文档为 Claude Code (claude.ai/code) 在本项目中工作时提供指导。
项目概述
这是一个少儿智慧阅读平台(Kindergarten Course Management System),采用 Spring Boot 后端 + Vue 3 前端架构。系统管理幼儿园的课程、课时、任务和学生成长记录。
技术架构
后端 (reading-platform-java)
- 框架: Spring Boot 3.2.3 + Java 17
- 持久层: MyBatis-Plus 3.5.5
- 安全: Spring Security + JWT
- API 文档: Knife4j (Swagger OpenAPI 3)
- 数据库: MySQL 8.0
- 数据库迁移: Flyway
前端 (reading-platform-frontend)
- 框架: Vue 3 + TypeScript + Vite
- UI 组件库: Ant Design Vue
- 状态管理: Pinia
- API: Axios + Orval 自动生成的 TypeScript 客户端
多租户架构
系统支持多个幼儿园(租户):
admin角色:超级管理员(无租户,管理全系统课程)school角色:学校管理员(管理本校的教师、学生、班级)teacher角色:教师(管理本校的课时和任务)parent角色:家长(查看孩子的进度和任务)
除 admin_users 外,每个实体都有 tenant_id 字段。系统课程的 tenant_id = NULL。
项目结构
kindergarten_java/
├── reading-platform-java/ # Spring Boot 后端
│ ├── src/main/java/.../controller/
│ │ ├── admin/ # 超级管理员端点 (/api/v1/admin/*)
│ │ ├── school/ # 学校管理员端点 (/api/v1/school/*)
│ │ ├── teacher/ # 教师端点 (/api/v1/teacher/*)
│ │ └── parent/ # 家长端点 (/api/v1/parent/*)
│ ├── entity/ # 数据库实体(27张表)
│ ├── mapper/ # MyBatis-Plus 映射器
│ ├── service/ # 服务层接口 + 实现
│ ├── common/
│ │ ├── annotation/RequireRole # 基于角色的访问控制
│ │ ├── security/ # JWT 认证
│ │ ├── enums/ # UserRole, CourseStatus 等枚举
│ │ ├── response/ # Result<T>, PageResult<T>
│ │ └── config/ # Security, MyBatis, OpenAPI 配置
│ └── resources/
│ ├── db/migration/ # Flyway 迁移脚本
│ └── mapper/ # MyBatis XML 文件
│
├── reading-platform-frontend/ # Vue 3 前端
│ ├── src/views/
│ │ ├── admin/ # 超级管理员页面
│ │ ├── school/ # 学校管理员页面
│ │ ├── teacher/ # 教师页面
│ │ └── parent/ # 家长页面
│ ├── api/generated/ # 自动生成的 API 客户端
│ ├── api-spec.yml # OpenAPI 规范
│ └── router/index.ts # Vue Router 配置
│
├── docker-compose.yml # 后端 + 前端服务
└── docs/开发协作指南.md # 开发指南(中文)
关键模式
1. 基于角色的访问控制
在 Controller/Service 上使用 @RequireRole 注解:
@RequireRole(UserRole.SCHOOL) // 只有学校管理员可以访问
2. 租户隔离
在学校/教师/家长端点中使用 SecurityUtils.getCurrentTenantId() 按当前租户过滤数据。
3. 统一响应格式
Result<T> success(T data) // { code: 200, message: "success", data: ... }
Result<T> error(code, msg) // { code: xxx, message: "...", data: null }
4. OpenAPI 驱动开发
- 后端:在 Controller 上使用
@Operation、@Parameter、@Schema注解 - 前端:运行
npm run api:update从api-spec.yml重新生成 TypeScript 客户端
5. 前后端接口规范
后端:以 Controller 为"唯一真源"
以后端 Spring Boot Controller 为接口定义的唯一真源,通过 SpringDoc/Knife4j 导出OpenAPI 规范,所有接口必须符合统一响应模型。
统一响应模型:
// 普通接口
Result<T> success(T data) // { code: 200, message: "success", data: ... }
Result<T> error(code, msg) // { code: xxx, message: "...", data: null }
// 分页接口
Result<PageResult<T>> // { code: 200, message: "success", data: { items, total, page, pageSize } }
响应结构说明:
| 接口类型 | 返回类型 | 分页字段命名 |
|---|---|---|
| 普通接口 | Result<T> |
- |
| 分页接口 | Result<PageResult<T>> |
page, pageSize, total, items |
| 错误响应 | Result<Void> |
参考 ErrorCode 枚举 |
请求参数规范:
| 参数类型 | 注解 | 说明 |
|---|---|---|
| 路径变量 | @PathVariable |
与 OpenAPI path 模板保持一致 |
| 查询参数 | @RequestParam |
分页:page, pageSize;过滤:keyword, category 等 |
| 请求体 | @RequestBody DTO |
使用 *Request 类,不直接暴露实体 |
前端:src/apis + fetch.ts 调用模式
围绕 src/apis + fetch.ts 的调用模式,将真实接口规范(路径、method、参数、响应结构)维护到 apis.ts。
工作流程:
后端 Controller (带 @Schema 注解)
↓
Knife4j/Swagger → /v3/api-docs
↓
api-spec.yml (OpenAPI 规范)
↓
orval (npm run api:gen)
↓
生成的 TypeScript 类型 + API 客户端
↓
Vue 组件使用 (强制类型校验)
实施步骤
一、梳理并固化接口响应规范
-
确认统一响应模型
- 普通接口:
Result<业务 DTO>,字段code/message/data - 分页接口:
Result<PageResult<业务 DTO>>,分页字段命名(page,pageSize,total等) - 错误响应:统一使用
Result<Void>或类似ResultVoidschema
- 普通接口:
-
从后端提炼规范说明
- 位置:
reading-platform-java/common/response与common/enums/ErrorCode - 输出:简短的"接口规范说明"文档,写入
docs/目录
- 位置:
二、从后端生成/校准 OpenAPI 规范
-
配置 SpringDoc/Knife4j 导出 OpenAPI
- 查看/完善后端 OpenAPI 配置类(如
OpenApiConfig或 Knife4j 配置) - 指定 API 基本信息(title/description/version),与当前
api-spec.yml对齐 - 扫描
controller/*包下所有带@RestController的类 - 支持
@Operation、@Parameter、@Schema注解
- 查看/完善后端 OpenAPI 配置类(如
-
规范化 Controller 注解与返回类型
- 检查 Controller 方法返回类型是否全部为
Result<T>或Result<PageResult<T>> - 为缺少注解的接口补全
@Operation、@Parameter、@Schema - 校准路径前缀与角色划分(
/api/v1/teacher/*、/api/v1/school/*等)
- 检查 Controller 方法返回类型是否全部为
-
替换/同步前端 api-spec.yml
- 使用导出的 OpenAPI JSON/YAML 覆盖/更新
reading-platform-frontend/api-spec.yml - 约定更新流程:修改后端 Controller → 查看/验证文档 → 导出并覆盖 api-spec.yml → 前端重新生成客户端
- 使用导出的 OpenAPI JSON/YAML 覆盖/更新
三、将接口规范映射到前端 src/apis 体系
-
分析现有 src/apis 使用方式
- 搜索全项目对
from 'src/apis/fetch'或getRequests的引用 - 列出当前真实在用的 URL 列表及对应页面组件
- 对比这些 URL 与后端 Controller 路径以及
api-spec.yml中的 paths
- 搜索全项目对
-
设计 apis.ts 的"接口字典"结构
- 以
SwaggerType/RequestType为基础 - 将真实接口按模块分类(教师端、学校端、家长端、管理员端)
- 每个接口包含:路径、method、请求参数类型、响应类型
- 以
6. DTO/VO 使用规范
响应对象(Response/VO)
- 必须创建独立的 VO 实体类,不要使用
Map、HashMap或JSONObject返回数据 - VO 类应放在
com.reading.platform.dto.response包下 - 使用
@Schema注解描述字段含义,便于生成 API 文档 - 使用
@Data和@Builder注解简化代码
示例:
@Data
@Builder
@Schema(description = "用户信息响应")
public class UserInfoResponse {
@Schema(description = "用户 ID")
private Long id;
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "角色")
private String role;
}
请求对象(Request/DTO)
- 复杂请求参数应创建 DTO 类,放在
com.reading.platform.dto.request包下 - 使用
@Valid和校验注解确保参数合法性
为什么不用 Map?
- ✅ VO 实体类:类型安全、IDE 智能提示、API 文档自动生成、易于维护
- ❌ Map:类型不安全、无文档、易出错、难以重构
7. Swagger/OpenAPI 注解规范
强制要求
所有实体类(Entity)、DTO、VO 都必须添加 @Schema 注解,确保 API 文档完整性和前端类型生成准确性。
注解使用规范
1. 类级别注解
// Entity 类
@Data
@TableName("users")
@Schema(description = "用户信息") // 必须添加
public class User { ... }
// DTO/VO 类
@Data
@Schema(description = "用户创建请求") // 必须添加
public class UserCreateRequest { ... }
@Data
@Schema(description = "用户信息响应") // 必须添加
public class UserResponse { ... }
2. 字段级别注解
@Schema(description = "用户 ID", example = "1", requiredMode = Schema.RequiredMode.READ_ONLY)
private Long id;
@Schema(description = "用户名", example = "zhangsan", requiredMode = Schema.RequiredMode.REQUIRED)
private String username;
@Schema(description = "年龄", example = "18", minimum = "1", maximum = "150")
private Integer age;
@Schema(description = "创建时间", example = "2024-01-01 12:00:00")
private LocalDateTime createdAt;
3. Controller 方法注解
@Operation(summary = "创建用户", description = "创建新用户并返回用户信息")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "400", description = "参数错误"),
@ApiResponse(responseCode = "409", description = "用户名已存在")
})
@PostMapping
public Result<UserResponse> createUser(@Valid @RequestBody UserCreateRequest request) {
return Result.success(userService.createUser(request));
}
4. 参数注解
@Operation(summary = "根据 ID 获取用户")
@GetMapping("/{id}")
public Result<UserResponse> getUser(
@Parameter(description = "用户 ID", required = true)
@PathVariable Long id,
@Parameter(description = "是否包含详细信息")
@RequestParam(defaultValue = "false")
Boolean includeDetails
) {
return Result.success(userService.getUser(id, includeDetails));
}
常用注解说明
| 注解 | 位置 | 说明 |
|---|---|---|
@Schema(description = "...") |
类/字段 | 描述类或字段的含义 |
@Schema(example = "...") |
字段 | 示例值 |
@Schema(requiredMode = REQUIRED) |
字段 | 必填字段 |
@Schema(requiredMode = READ_ONLY) |
字段 | 只读字段(如 ID、创建时间) |
@Schema(minimum = "1", maximum = "100") |
字段 | 数值范围 |
@Schema(minLength = 1, maxLength = 50) |
字段 | 字符串长度 |
@Schema(pattern = "^[a-zA-Z]+$") |
字段 | 正则表达式 |
@Operation(summary = "...") |
方法 | 接口摘要 |
@Parameter(description = "...") |
参数 | 参数描述 |
完整示例
Entity 类:
package com.reading.platform.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("courses")
@Schema(description = "课程信息")
public class Course {
@Schema(description = "课程 ID", requiredMode = Schema.RequiredMode.READ_ONLY)
@TableId(type = IdType.AUTO)
private Long id;
@Schema(description = "租户 ID", requiredMode = Schema.RequiredMode.READ_ONLY)
private Long tenantId;
@Schema(description = "课程名称", example = "绘本阅读入门", requiredMode = Schema.RequiredMode.REQUIRED)
private String name;
@Schema(description = "课程编码", example = "READ001")
private String code;
@Schema(description = "课程描述")
private String description;
@Schema(description = "封面图片 URL")
private String coverUrl;
@Schema(description = "分类", example = "language")
private String category;
@Schema(description = "适用年龄段", example = "5-6 岁")
private String ageRange;
@Schema(description = "难度等级", example = "beginner")
private String difficultyLevel;
@Schema(description = "课程时长(分钟)", example = "30")
private Integer durationMinutes;
@Schema(description = "状态", example = "published")
private String status;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.READ_ONLY)
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.READ_ONLY)
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
DTO 类:
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Data
@Schema(description = "课程创建请求")
public class CourseCreateRequest {
@Schema(description = "课程名称", example = "绘本阅读入门", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "课程名称不能为空")
@Size(max = 100, message = "课程名称不能超过 100 个字符")
private String name;
@Schema(description = "课程描述", example = "适合大班幼儿的绘本阅读课程")
private String description;
@Schema(description = "分类", example = "language")
private String category;
@Schema(description = "适用年龄段", example = "5-6 岁")
private String ageRange;
}
代码审查要点
- Entity 类是否添加了
@Schema(description = "...") - Entity 字段是否添加了
@Schema注解描述字段含义 - DTO/VO 类是否添加了
@Schema(description = "...") - DTO/VO 字段是否添加了
@Schema注解 - Controller 方法是否添加了
@Operation注解 - Controller 参数是否添加了
@Parameter注解 - 必填字段是否标注了
requiredMode = REQUIRED - 只读字段(ID、时间戳)是否标注了
requiredMode = READ_ONLY - 是否有合适的
example示例值
注意事项
- 导入正确的包
import io.swagger.v3.oas.annotations.media.Schema; // Swagger 3.x
- Lombok 与 Schema 的配合
@Data生成的 getter 方法会自动继承字段上的@Schema注解- 但建议字段和方法都加上注解以确保兼容性
- 必填校验
requiredMode = REQUIRED仅用于文档说明- 实际校验需要配合
@NotNull、@NotBlank等校验注解
- 枚举类型
@Schema(description = "状态", example = "active", allowableValues = {"active", "inactive"})
private String status;
8. 前端接口校验规范
基于 OpenAPI + orval + TypeScript 的强制校验链路
本项目前端采用从接口文档到页面代码的强制类型校验链路,确保前后端接口一致性和类型安全。
技术栈:
- OpenAPI 3.0:统一的 API 接口规范
- orval:从 OpenAPI 规范生成 TypeScript 类型和 API 客户端
- TypeScript:静态类型检查
- ESLint:代码规范约束
- axios:HTTP 请求(由 orval 自动生成)
工作流程:
后端 Controller (带 @Schema 注解)
↓
Knife4j/Swagger
↓
api-spec.yml (OpenAPI 规范)
↓
orval (npm run api:gen)
↓
生成的 TypeScript 类型 + API 客户端
↓
Vue 组件使用 (强制类型校验)
配置说明
orval 配置 (orval.config.ts 或 vite.config.ts):
export default {
'api': {
input: {
target: './api-spec.yml',
filters: {
tags: ['default'], // 只生成指定标签的接口
},
},
output: {
mode: 'split', // 分离模式:每个接口一个文件
target: './src/api/generated/client.ts',
schemas: './src/api/generated/schemas',
client: 'axios',
mock: false,
override: {
fetch: {
includeHttpRequestHeader: true,
},
useTypeOverInterfaces: true, // 优先使用 type
useDate: true,
},
},
},
};
使用规范
1. 后端必须添加 @Schema 注解
@Operation(summary = "获取用户信息")
@GetMapping("/{id}")
public Result<UserInfoResponse> getUser(@PathVariable Long id) {
return Result.success(userService.getUser(id));
}
2. 前端必须使用生成的类型和客户端
// ✅ 正确:使用生成的类型和 API
import { getUser } from '@/api/generated/client';
import type { UserInfoResponse } from '@/api/generated/schemas';
const user: UserInfoResponse = await getUser(id);
// ❌ 错误:不要手写类型或手动调用 axios
interface ManualUser { id: number; name: string; }
const user = await axios.get(`/api/users/${id}`);
3. ESLint 约束规则
// .eslintrc.cjs 中添加
rules: {
// 禁止手动调用 axios,必须使用生成的 API 客户端
'no-restricted-imports': [
'error',
{
patterns: ['axios', '!@/api/generated/*'],
},
],
// 强制使用生成的类型定义
'@typescript-eslint/no-explicit-any': 'error',
// 禁止使用 any 类型(特殊情况需 eslint-disable 注释)
}
4. 运行时校验(可选)
使用 zod 或 yup 进行运行时数据校验:
import { UserInfoSchema } from '@/api/generated/schemas';
import { z } from 'zod';
// 运行时校验响应数据
const parsedData = UserInfoSchema.parse(apiResponse);
开发命令
cd reading-platform-frontend
# 从后端更新 API 规范并生成客户端
npm run api:gen
# 或者完整流程(包含从后端导出 swagger)
npm run api:update
代码审查要点
- 前端是否使用了 orval 生成的类型,而非手写 interface
- 是否使用生成的 API 客户端,而非手动 axios 调用
- TypeScript 编译是否通过(无类型错误)
- 后端 Controller 是否正确添加 @Schema 注解
- API 变更后是否重新运行了
npm run api:gen
常见问题
Q: 为什么要强制使用生成的类型? A: 手写类型容易与后端实际返回不一致,导致运行时错误。生成类型确保前后端一致性。
Q: 如何添加自定义逻辑? A: 在生成的客户端外封装业务逻辑层,不要修改生成的文件。
Q: API 变更后忘记更新怎么办? A: CI/CD 中可添加类型检查步骤,类型不通过则构建失败。
开发命令
后端
Java 环境配置
项目要求 Java 17(Spring Boot 3.2.3 强制要求),本地同时安装了 Java 8 和 Java 17 时,必须使用 Maven Wrapper 确保使用正确的 Java 版本。
Java 安装路径:
- Java 17:
F:\Java\jdk-17 - Java 8:
F:\Java\jdk1.8.0_202
编译/构建命令(必须使用 Maven Wrapper):
# Windows 命令行(在项目目录下)
.\mvnw.cmd clean install -DskipTests
# 或者使用编译脚本
.\compile.bat
⚠️ 重要: 不要直接使用
mvn命令,因为它会使用系统JAVA_HOME环境变量(可能是 Java 8)。必须使用.\mvnw.cmd,它内置了 Java 17 路径配置。
# 使用 Docker Compose 运行(推荐)
docker compose up --build
# 本地运行(需要 MySQL 已启动,且确保使用 Java 17)
cd reading-platform-java
.\mvnw.cmd spring-boot:run
# 构建
.\mvnw.cmd clean package -DskipTests
前端
cd reading-platform-frontend
npm install
npm run dev
npm run build
# 从后端规范更新 API 客户端
npm run api:update
数据库迁移
- 将新的迁移脚本添加到
reading-platform-java/src/main/resources/db/migration/V{n}__description.sql - Flyway 会在后端启动时自动运行(仅开发模式)
数据库表结构(27张表)
- 租户: tenants, tenant_courses
- 用户: admin_users, teachers, students, parents, parent_students
- 班级: classes, class_teachers, student_class_history
- 课程: courses, course_versions, course_resources, course_scripts, course_script_pages, course_activities
- 课时: lessons, lesson_feedbacks, student_records
- 任务: tasks, task_targets, task_completions, task_templates
- 成长: growth_records
- 资源: resource_libraries, resource_items
- 日程: schedule_plans, schedule_templates
- 系统: system_settings, notifications, operation_logs, tags
测试账号
| 角色 | 用户名 | 密码 |
|---|---|---|
| 管理员 | admin | admin123 |
| 学校 | school | 123456 |
| 教师 | teacher1 | 123456 |
| 家长 | parent1 | 123456 |
API 文档
近期补充的接口和字段(2026-03-10)
实体类新增字段
StudentRecord - 学生评价记录
focus- 专注力评分 (1-5)participation- 参与度评分 (1-5)interest- 兴趣评分 (1-5)understanding- 理解度评分 (1-5)
LessonFeedback - 课时反馈
design_quality- 设计质量评分 (1-5)participation- 参与度评分 (1-5)goal_achievement- 目标达成度评分 (1-5)step_feedbacks- 环节反馈 JSONpros- 优点suggestions- 建议activities_done- 完成的活动 JSON
Lesson - 课时
actual_duration- 实际时长(分钟)overall_rating- 整体评分participation_rating- 参与度评分completion_note- 完成说明
Student - 学生
class_id- 班级 IDparent_name- 家长姓名parent_phone- 家长手机号reading_count- 阅读次数lesson_count- 课时数
ClassTeacher - 班级教师
is_primary- 是否主教sort_order- 排序
StudentClassHistory - 学生班级历史
reason- 调班原因
新增 Service 方法
ClassService
getClassList(Long tenantId)- 获取班级列表(无分页)getClassStudents(Long classId, ...)- 获取班级学生分页getClassTeachers(Long classId)- 获取班级教师列表addClassTeacher(...)- 添加班级教师updateClassTeacher(...)- 更新班级教师角色removeClassTeacher(...)- 移除班级教师
LessonService
finishLesson(...)- 结束课时saveStudentRecord(...)- 保存学生评价记录getStudentRecords(Long lessonId)- 获取课程所有学生记录batchSaveStudentRecords(...)- 批量保存学生评价记录saveLessonFeedback(...)- 提交课程反馈getLessonFeedback(Long lessonId)- 获取课程反馈
StudentService
transferStudent(...)- 学生调班getStudentClassHistory(Long studentId)- 获取学生调班历史
TaskService
getTaskCompletion(Long taskId, Long studentId)- 获取任务完成记录
新增 Controller 接口
学校端 (/api/v1/school/*)
GET /classes/list- 获取班级列表(无分页)GET /classes/{id}/students- 获取班级学生分页GET /classes/{id}/teachers- 获取班级教师列表POST /classes/{id}/teachers- 添加班级教师PUT /classes/{id}/teachers/{teacherId}- 更新班级教师角色DELETE /classes/{id}/teachers/{teacherId}- 移除班级教师POST /students/{id}/transfer- 学生调班GET /students/{id}/history- 获取学生调班历史
教师端 (/api/v1/teacher/*)
GET /courses/classes- 获取教师的班级列表GET /courses/students- 获取教师所有学生分页GET /courses/classes/{classId}/students- 获取班级学生分页GET /courses/classes/{classId}/teachers- 获取班级教师列表POST /lessons/{id}/finish- 结束课时POST /lessons/{lessonId}/students/{studentId}/record- 保存学生评价记录GET /lessons/{lessonId}/student-records- 获取课程所有学生记录POST /lessons/{lessonId}/student-records/batch- 批量保存学生评价记录POST /lessons/{lessonId}/feedback- 提交课程反馈GET /lessons/{lessonId}/feedback- 获取课程反馈GET /schedules/timetable- 获取课表(按日期范围)GET /schedules/today- 获取今日课表POST /schedules- 创建课表计划PUT /schedules/{id}- 更新课表计划DELETE /schedules/{id}- 取消课表计划
家长端 (/api/v1/parent/*)
GET /children- 获取我的孩子(增强返回格式)GET /children/{id}- 获取孩子详情(增强返回格式)GET /children/{childId}/lessons- 获取孩子的课时记录GET /children/{childId}/tasks- 获取孩子的任务(带完成状态)PUT /children/{childId}/tasks/{taskId}/feedback- 提交任务家长反馈
数据库迁移
- 迁移脚本:``
- 包含上述所有实体类新增字段的 ALTER TABLE 语句
统一开发规范(完整版)
核心原则
- OpenAPI 规范驱动 - 前后端通过接口规范对齐,零沟通成本
- 类型安全优先 - TypeScript 强制类型校验,早发现早修复
- 约定大于配置 - 统一代码风格和目录结构,降低认知负担
- 自动化优先 - 能自动化的绝不手动(代码生成、部署、测试)
- 三层架构分离 - Controller、Service、Mapper 职责清晰
一、三层架构规范
1.1 各层职责
┌─────────────────────────────────────────────────────────┐
│ Controller 层(入口) │
│ • 接收 HTTP 请求参数(DTO/Request) │
│ • 参数校验(@Valid) │
│ • 调用 Service 层(传入 DTO) │
│ • 接收 Service 返回的 Entity 或 VO │
│ • 转换为响应 VO(如需要) │
│ • 返回 Result<VO> │
│ • 不包含业务逻辑 │
└─────────────────────────────────────────────────────────┘
↓ 使用 DTO/Entity
┌─────────────────────────────────────────────────────────┐
│ Service 层(业务) │
│ • 处理业务逻辑 │
│ • 事务控制(@Transactional) │
│ • 调用 Mapper 层(传入/返回 Entity) │
│ • 调用其他 Service │
│ • 返回 Entity 或 Entity 列表(给 Controller 转换) │
│ • 不包含业务逻辑 │
└─────────────────────────────────────────────────────────┘
↓ 只使用 Entity
┌─────────────────────────────────────────────────────────┐
│ Mapper 层(数据访问) │
│ • 数据库 CRUD 操作 │
│ • 继承 BaseMapper<Entity> │
│ • 接收/返回 Entity 或 Entity 列表 │
│ • 复杂查询返回 Entity(通过 ResultMap 映射) │
│ • 禁止返回 Map/JSONObject/自定义 DTO │
└─────────────────────────────────────────────────────────┘
1.2 统一响应格式
// 普通接口
Result<T> success(T data) // { code: 200, message: "success", data: ... }
Result<T> error(code, msg) // { code: xxx, message: "...", data: null }
// 分页接口
Result<PageResult<T>> // { code: 200, message: "success", data: { items, total, page, pageSize } }
二、Service/Mapper 层数据使用规范
核心原则:Service 层和 Mapper 层必须使用实体类(Entity)接收和返回数据,严禁在 Service 层和 Mapper 层之间使用 DTO/VO 转换。
黄金法则
| 层级间通信 | 数据类型 |
|---|---|
| Service ↔ Mapper | 只用 Entity |
| Controller ↔ Service | 可以 DTO/Entity 混用 |
| Controller ↔ HTTP | DTO 进,VO 出 |
错误示例
// ❌ 错误:不要在 Service 层和 Mapper 层之间使用 DTO
@Service
public class UserServiceImpl implements UserService {
public UserInfoDTO getUserById(Long userId) {
UserInfoDTO dto = userMapper.selectUserDTO(userId); // 错误!
return dto;
}
}
@Mapper
public interface UserMapper extends BaseMapper<User> {
UserInfoDTO selectUserDTO(Long id); // 错误!应返回 User
}
正确示例
// ✅ 正确:Service 层和 Mapper 层使用 Entity
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public User getUserById(Long userId) {
return this.getById(userId); // 直接返回 Entity
}
}
// Controller 层负责 Entity → VO 转换
@GetMapping("/{id}")
public Result<UserInfoVO> getUser(@PathVariable Long id) {
User user = userService.getUserById(id); // Service 返回 Entity
UserInfoVO vo = convertToVO(user); // Controller 转换为 VO
return Result.success(vo);
}
三、Service 层继承规范
所有 Service 接口必须继承 IService<T>,实现类必须继承 ServiceImpl<Mapper, Entity>
// Service 接口
public interface UserService extends IService<User> {
User createUser(UserCreateRequest request);
Page<User> pageUsers(Integer page, Integer size, String keyword);
}
// Service 实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// 自动拥有:save(), remove(), update(), getById(), list(), page(), count() 等方法
}
继承 IService 的好处:
- 减少样板代码:基础 CRUD 方法无需手动编写
- 统一接口规范:所有 Service 层接口一致
- 类型安全:泛型确保类型正确
- 链式调用:支持
lambdaQuery()等链式操作 - 批量操作:内置
saveBatch(),removeBatch()等方法
四、查询分页规范
所有返回列表的查询接口,默认必须分页处理
// ❌ 错误:不分页返回所有数据
@GetMapping("/list")
public Result<List<User>> listUsers() {
List<User> users = userService.list(); // 可能返回成千上万条
return Result.success(users);
}
// ✅ 正确:分页返回
@GetMapping("/page")
public Result<PageResult<UserInfoVO>> pageUsers(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String keyword) {
Page<User> userPage = this.page(
new Page<>(page, size),
Wrappers.<User>lambdaQuery()
.like(StringUtils.hasText(keyword), User::getUsername, keyword)
.orderByDesc(User::getCreateTime)
);
return Result.success(buildPageResult(userPage));
}
不分页的例外场景:
- 下拉选项数据(如角色列表、部门列表)
- 数据量固定且很小(< 100 条)
- 导出接口(全量导出)
五、查询方式选择规范
| 场景 | 推荐方式 | 示例方法 |
|---|---|---|
| 单表按 ID 查询 | 通用方法 | getById(id) |
| 单表条件查询 | QueryWrapper | list(wrapper) / getOne(wrapper) |
| 单表分页查询 | QueryWrapper + Page | page(new Page<>(p, s), wrapper) |
| 单表统计 | QueryWrapper | count(wrapper) |
| 两表联查 | 自定义 SQL | mapper.selectWithXxx() |
| 三表及以上 | 自定义 SQL | mapper.selectWithXxxAndYyy() |
| 聚合统计 | 自定义 SQL | mapper.selectStats() |
| 子查询 | 自定义 SQL | mapper.selectBySubQuery() |
| 复杂动态条件 | 自定义 SQL(XML) | mapper.selectByCondition() |
决策树
开始查询
│
▼
┌────────────────┐
│ 是否单表查询? │
└────────────────┘
│ │
是 否
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 是否需要分页? │ │ 使用自定义 SQL │
└──────────────────┘ │ (XML 或@Select) │
│ │ └──────────────────┘
是 否
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ page(Page, │ │ list(QueryWrapper) │
│ QueryWrapper)│ │ getOne(QueryWrapper)│
└──────────────┘ │ count(QueryWrapper) │
└──────────────────┘
六、QueryWrapper 使用规范
// 使用 Lambda 表达式构建类型安全的查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 等值查询
wrapper.eq(User::getStatus, 1);
// 模糊查询
wrapper.like(User::getUsername, "张");
wrapper.likeLeft(User::getUsername, "三"); // %三
wrapper.likeRight(User::getUsername, "张"); // 张%
// 范围查询
wrapper.between(User::getAge, 18, 30);
wrapper.in(User::getStatus, Arrays.asList(1, 2));
// 比较查询
wrapper.gt(User::getAge, 18); // >
wrapper.ge(User::getAge, 18); // >=
wrapper.lt(User::getAge, 60); // <
wrapper.le(User::getAge, 60); // <=
// 空值判断
wrapper.isNull(User::getDeletedAt);
wrapper.isNotNull(User::getEmail);
// 排序
wrapper.orderByDesc(User::getCreateTime);
wrapper.orderByAsc(User::getSortOrder);
// 条件查询(第一个参数为 true 时才添加条件)
wrapper.eq(StringUtils.hasText(keyword), User::getUsername, keyword);
六、日志打印规范
核心原则:所有日志打印内容必须使用中文。
错误示例:
// ❌ 错误:使用英文日志
log.info("User created successfully, id: {}", userId);
log.debug("Query user by id: {}", userId);
log.error("Failed to create user", e);
正确示例:
// ✅ 正确:使用中文日志
log.info("用户创建成功,ID: {}", userId);
log.debug("查询用户,ID: {}", userId);
log.error("创建用户失败", e);
log.warn("用户不存在,ID: {}", userId);
日志格式规范:
| 场景 | 推荐格式 | 示例 |
|---|---|---|
| 操作开始 | "开始{操作},{关键参数}" | 开始创建用户,用户名:zhangsan |
| 操作成功 | "{操作}成功,{关键结果}" | 用户创建成功,ID: 123 |
| 操作失败 | "{操作}失败,{关键参数}" | 用户删除失败,ID: 123 |
| 查询操作 | "{动作}{对象},{关键参数}" | 查询用户,ID: 123 |
| 状态检查 | "{对象}不存在/已存在,{关键参数}" | 用户不存在,ID: 123 |
| 异常日志 | "{操作}异常,{关键参数}" + e | 创建用户异常,ID: 123 + e |
完整示例:
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public User createUser(UserCreateRequest request) {
log.info("开始创建用户,用户名:{}", request.getUsername());
boolean exists = userMapper.existsByUsername(request.getUsername());
if (exists) {
log.warn("用户名已存在:{}", request.getUsername());
throw new BusinessException("用户名已存在");
}
User user = User.builder()
.username(request.getUsername())
.email(request.getEmail())
.build();
userMapper.insert(user);
log.info("用户创建成功,ID: {}", user.getId());
return user;
}
@Override
public User getUserById(Long userId) {
log.debug("查询用户,ID: {}", userId);
User user = this.getById(userId);
if (user == null) {
log.warn("用户不存在,ID: {}", userId);
throw new BusinessException("用户不存在");
}
log.info("查询用户成功,用户名:{}", user.getUsername());
return user;
}
}
七、工具函数使用规范
核心原则:工具函数必须集中管理,禁止在 Controller 层直接编写工具方法。
存放位置决策
| 场景 | 存放位置 | 调用方式 |
|---|---|---|
| 多个地方调用(≥2 处) | 统一工具类(如 CommonUtil) |
静态方法调用 |
| 仅在一个地方调用 | Service 层内部私有方法 | 本类内调用 |
| 业务无关的通用工具 | 独立工具类(如 DateUtil, FileUtil) |
静态方法调用 |
错误示例
// ❌ 错误:工具方法不应该写在 Controller 层
@RestController
public class UserController {
private String formatUsername(String username) {
return username.trim().toLowerCase();
}
}
// ❌ 错误:工具逻辑散落在各处,导致代码重复
@Service
public class UserServiceImpl {
private String generateOrderNo() { /* ... */ }
}
@Service
public class OrderServiceImpl {
private String generateOrderNo() { /* ... */ } // 重复代码
}
正确示例
多处调用的工具函数 → 统一工具类
// ✅ 正确:统一工具类
package com.reading.platform.common.util;
public class CommonUtil {
private CommonUtil() {} // 私有构造,防止实例化
/**
* 生成订单号
* 格式:ORD + 年月日时分秒 + 4 位随机数
*/
public static String generateOrderNo() {
String timestamp = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
int random = (int) (Math.random() * 9000) + 1000;
return "ORD" + timestamp + random;
}
/**
* 格式化用户名
*/
public static String formatUsername(String username) {
if (username == null) return null;
return username.trim().toLowerCase();
}
}
仅一处调用的工具函数 → Service 层内部方法
// ✅ 正确:仅在本 Service 内使用的工具方法写在 Service 层
@Service
public class LessonServiceImpl extends ServiceImpl<LessonMapper, Lesson> implements LessonService {
@Override
public void finishLesson(Long lessonId, List<StudentRecord> records) {
for (StudentRecord record : records) {
record.setLessonId(lessonId);
processRecordBeforeSave(record); // 调用内部方法
studentRecordMapper.insert(record);
}
}
/**
* 保存记录前处理数据
* 此方法仅在本类中使用,不需要抽取到工具类
*/
private void processRecordBeforeSave(StudentRecord record) {
// 计算综合评分
int totalScore = record.getFocus() + record.getParticipation()
+ record.getInterest() + record.getUnderstanding();
record.setTotalScore(totalScore);
// 设置评价等级
if (totalScore >= 17) {
record.setGrade("A");
} else if (totalScore >= 13) {
record.setGrade("B");
} else {
record.setGrade("C");
}
}
}
工具类设计原则
- 私有构造:防止实例化
- 静态方法:所有方法都是
static的 - 无状态:工具类不应持有状态
- 线程安全:工具方法必须是线程安全的
- 充分注释:每个方法都要有 JavaDoc 注释
代码审查要点
- 工具函数是否抽取到工具类或 Service 内部
- 是否存在 Controller 层直接编写工具方法的情况
- 多处使用的工具是否统一到工具类中
- 工具类是否有私有构造防止实例化
- 工具方法是否为静态方法
八、Swagger/OpenAPI 注解规范
所有 Entity、DTO、VO 都必须添加 @Schema 注解
// Entity 类
@Data
@TableName("users")
@Schema(description = "用户信息")
public class User {
@Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.READ_ONLY)
private Long id;
@Schema(description = "用户名", example = "zhangsan", requiredMode = Schema.RequiredMode.REQUIRED)
private String username;
}
// DTO 类
@Data
@Schema(description = "用户创建请求")
public class UserCreateRequest {
@Schema(description = "用户名", example = "zhangsan", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "用户名不能为空")
private String username;
}
// VO 类
@Data
@Schema(description = "用户信息响应")
public class UserInfoVO {
@Schema(description = "用户 ID")
private Long id;
@Schema(description = "用户名")
private String username;
}
九、代码审查要点
Service/Mapper 层
- Service 接口是否继承
IService<T> - Service 实现类是否继承
ServiceImpl<Mapper, Entity> - Service ↔ Mapper 之间是否只使用 Entity(无 DTO 转换)
- 查询列表接口是否进行了分页处理
- 简单查询是否优先使用 QueryWrapper + 通用方法
- 复杂联表查询是否使用自定义 SQL
Controller 层
- 是否使用
@RestController注解 - 返回类型是否为
Result<T>或Result<PageResult<T>> - 是否使用
@Operation描述接口 - 是否使用
@Parameter描述参数 - 是否使用
@Valid校验请求参数
Entity/DTO/VO
- Entity 类是否添加
@Schema(description = "...") - Entity 字段是否添加
@Schema注解 - DTO/VO 类是否添加
@Schema(description = "...") - DTO/VO 字段是否添加
@Schema注解 - 必填字段是否标注
requiredMode = REQUIRED - 只读字段(ID、时间戳)是否标注
requiredMode = READ_ONLY