# 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, PageResult │ │ └── 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` 注解: ```java @RequireRole(UserRole.SCHOOL) // 只有学校管理员可以访问 ``` ### 2. 租户隔离 在学校/教师/家长端点中使用 `SecurityUtils.getCurrentTenantId()` 按当前租户过滤数据。 ### 3. 统一响应格式 ```java Result success(T data) // { code: 200, message: "success", data: ... } Result 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` 规范,所有接口必须符合统一响应模型。 **统一响应模型:** ```java // 普通接口 Result success(T data) // { code: 200, message: "success", data: ... } Result error(code, msg) // { code: xxx, message: "...", data: null } // 分页接口 Result> // { code: 200, message: "success", data: { items, total, page, pageSize } } ``` **响应结构说明:** | 接口类型 | 返回类型 | 分页字段命名 | |---------|---------|-------------| | 普通接口 | `Result` | - | | 分页接口 | `Result>` | `page`, `pageSize`, `total`, `items` | | 错误响应 | `Result` | 参考 `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 组件使用 (强制类型校验) ``` #### 实施步骤 **一、梳理并固化接口响应规范** 1. **确认统一响应模型** - 普通接口:`Result<业务 DTO>`,字段 `code/message/data` - 分页接口:`Result>`,分页字段命名(`page`, `pageSize`, `total` 等) - 错误响应:统一使用 `Result` 或类似 `ResultVoid` schema 2. **从后端提炼规范说明** - 位置:`reading-platform-java/common/response` 与 `common/enums/ErrorCode` - 输出:简短的"接口规范说明"文档,写入 `docs/` 目录 **二、从后端生成/校准 OpenAPI 规范** 1. **配置 SpringDoc/Knife4j 导出 OpenAPI** - 查看/完善后端 OpenAPI 配置类(如 `OpenApiConfig` 或 Knife4j 配置) - 指定 API 基本信息(title/description/version),与当前 `api-spec.yml` 对齐 - 扫描 `controller/*` 包下所有带 `@RestController` 的类 - 支持 `@Operation`、`@Parameter`、`@Schema` 注解 2. **规范化 Controller 注解与返回类型** - 检查 Controller 方法返回类型是否全部为 `Result` 或`Result>` - 为缺少注解的接口补全 `@Operation`、`@Parameter`、`@Schema` - 校准路径前缀与角色划分(`/api/v1/teacher/*`、`/api/v1/school/*` 等) 3. **替换/同步前端 api-spec.yml** - 使用导出的 OpenAPI JSON/YAML 覆盖/更新 `reading-platform-frontend/api-spec.yml` - 约定更新流程:修改后端 Controller → 查看/验证文档 → 导出并覆盖 api-spec.yml → 前端重新生成客户端 **三、将接口规范映射到前端 src/apis 体系** 1. **分析现有 src/apis 使用方式** - 搜索全项目对 `from 'src/apis/fetch'` 或`getRequests` 的引用 - 列出当前真实在用的 URL 列表及对应页面组件 - 对比这些 URL 与后端 Controller 路径以及 `api-spec.yml` 中的 paths 2. **设计 apis.ts 的"接口字典"结构** - 以 `SwaggerType` / `RequestType` 为基础 - 将真实接口按模块分类(教师端、学校端、家长端、管理员端) - 每个接口包含:路径、method、请求参数类型、响应类型 ### 6. DTO/VO 使用规范 #### 响应对象(Response/VO) - **必须创建独立的 VO 实体类**,不要使用 `Map`、`HashMap` 或 `JSONObject` 返回数据 - VO 类应放在 `com.reading.platform.dto.response` 包下 - 使用 `@Schema` 注解描述字段含义,便于生成 API 文档 - 使用 `@Data` 和 `@Builder` 注解简化代码 **示例:** ```java @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. 类级别注解** ```java // Entity 类 @Data @TableName("users") @Schema(description = "用户信息") // 必须添加 public class User { ... } // DTO/VO 类 @Data @Schema(description = "用户创建请求") // 必须添加 public class UserCreateRequest { ... } @Data @Schema(description = "用户信息响应") // 必须添加 public class UserResponse { ... } ``` **2. 字段级别注解** ```java @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 方法注解** ```java @Operation(summary = "创建用户", description = "创建新用户并返回用户信息") @ApiResponses({ @ApiResponse(responseCode = "200", description = "创建成功"), @ApiResponse(responseCode = "400", description = "参数错误"), @ApiResponse(responseCode = "409", description = "用户名已存在") }) @PostMapping public Result createUser(@Valid @RequestBody UserCreateRequest request) { return Result.success(userService.createUser(request)); } ``` **4. 参数注解** ```java @Operation(summary = "根据 ID 获取用户") @GetMapping("/{id}") public Result 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 类:** ```java 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 类:** ```java 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` 示例值 #### 注意事项 1. **导入正确的包** ```java import io.swagger.v3.oas.annotations.media.Schema; // Swagger 3.x ``` 2. **Lombok 与 Schema 的配合** - `@Data` 生成的 getter 方法会自动继承字段上的 `@Schema` 注解 - 但建议字段和方法都加上注解以确保兼容性 3. **必填校验** - `requiredMode = REQUIRED` 仅用于文档说明 - 实际校验需要配合 `@NotNull`、`@NotBlank` 等校验注解 4. **枚举类型** ```java @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`):** ```typescript 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 注解** ```java @Operation(summary = "获取用户信息") @GetMapping("/{id}") public Result getUser(@PathVariable Long id) { return Result.success(userService.getUser(id)); } ``` **2. 前端必须使用生成的类型和客户端** ```typescript // ✅ 正确:使用生成的类型和 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 约束规则** ```typescript // .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` 进行运行时数据校验: ```typescript import { UserInfoSchema } from '@/api/generated/schemas'; import { z } from 'zod'; // 运行时校验响应数据 const parsedData = UserInfoSchema.parse(apiResponse); ``` #### 开发命令 ```bash 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 中可添加类型检查步骤,类型不通过则构建失败。 ## 开发命令 ### 后端 ```bash # 使用 Docker Compose 运行(推荐) docker compose up --build # 本地运行(需要 MySQL 已启动) cd reading-platform-java mvn spring-boot:run # 构建 mvn clean package -DskipTests ``` ### 前端 ```bash 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 文档 - 访问地址:http://localhost:8080/doc.html(后端启动后) ## 近期补充的接口和字段(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` - 环节反馈 JSON - `pros` - 优点 - `suggestions` - 建议 - `activities_done` - 完成的活动 JSON **Lesson** - 课时 - `actual_duration` - 实际时长(分钟) - `overall_rating` - 整体评分 - `participation_rating` - 参与度评分 - `completion_note` - 完成说明 **Student** - 学生 - `class_id` - 班级 ID - `parent_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 语句