主要变更: 1. 所有 Entity/DTO/VO 添加 @Schema 注解,完善 API 文档 2. 新增前端 API 封装模块 (src/apis),包含 fetch.ts 和 apis.ts 3. 生成完整的 TypeScript 类型定义(100+ 个模型) 4. pom.xml 添加 Maven 编译配置和 UTF-8 编码支持 5. 更新 CLAUDE.md 开发文档,新增接口规范和 Swagger 注解规范 6. 清理旧的文档文件和 Flyway 迁移脚本 技术细节: - 后端:27 个实体类 + 所有 DTO/Response 添加 Swagger 注解 - 前端:新增 orval 生成的 API 客户端类型 - 构建:配置 Maven compiler plugin 和 Spring Boot 插件的 JVM 参数 - 数据库:新增 schema 导出文件,删除旧 Flyway 迁移脚本 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
24 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 中可添加类型检查步骤,类型不通过则构建失败。
开发命令
后端
# 使用 Docker Compose 运行(推荐)
docker compose up --build
# 本地运行(需要 MySQL 已启动)
cd reading-platform-java
mvn spring-boot:run
# 构建
mvn 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 语句