From 353db50860157a038caa2d4aa87923f57a5b0b9a Mon Sep 17 00:00:00 2001 From: En Date: Thu, 12 Mar 2026 14:00:47 +0800 Subject: [PATCH] new Tree --- CLAUDE.md | 1269 ----------------------------------------------------- dev.db | Bin 458752 -> 0 bytes 2 files changed, 1269 deletions(-) delete mode 100644 CLAUDE.md delete mode 100644 dev.db diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index c04c9b3..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,1269 +0,0 @@ -# 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 中可添加类型检查步骤,类型不通过则构建失败。 - -## 开发命令 - -### 后端 - -#### 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):** -```bash -# Windows 命令行(在项目目录下) -.\mvnw.cmd clean install -DskipTests - -# 或者使用编译脚本 -.\compile.bat -``` - -> ⚠️ **重要:** 不要直接使用 `mvn` 命令,因为它会使用系统 `JAVA_HOME` 环境变量(可能是 Java 8)。必须使用 `.\mvnw.cmd`,它内置了 Java 17 路径配置。 - -```bash -# 使用 Docker Compose 运行(推荐) -docker compose up --build - -# 本地运行(需要 MySQL 已启动,且确保使用 Java 17) -cd reading-platform-java -.\mvnw.cmd spring-boot:run - -# 构建 -.\mvnw.cmd 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 语句 - ---- - -## 统一开发规范(完整版) - -### 核心原则 - -1. **OpenAPI 规范驱动** - 前后端通过接口规范对齐,零沟通成本 -2. **类型安全优先** - TypeScript 强制类型校验,早发现早修复 -3. **约定大于配置** - 统一代码风格和目录结构,降低认知负担 -4. **自动化优先** - 能自动化的绝不手动(代码生成、部署、测试) -5. **三层架构分离** - Controller、Service、Mapper 职责清晰 - ---- - -### 一、三层架构规范 - -#### 1.1 各层职责 - -``` -┌─────────────────────────────────────────────────────────┐ -│ Controller 层(入口) │ -│ • 接收 HTTP 请求参数(DTO/Request) │ -│ • 参数校验(@Valid) │ -│ • 调用 Service 层(传入 DTO) │ -│ • 接收 Service 返回的 Entity 或 VO │ -│ • 转换为响应 VO(如需要) │ -│ • 返回 Result │ -│ • 不包含业务逻辑 │ -└─────────────────────────────────────────────────────────┘ - ↓ 使用 DTO/Entity -┌─────────────────────────────────────────────────────────┐ -│ Service 层(业务) │ -│ • 处理业务逻辑 │ -│ • 事务控制(@Transactional) │ -│ • 调用 Mapper 层(传入/返回 Entity) │ -│ • 调用其他 Service │ -│ • 返回 Entity 或 Entity 列表(给 Controller 转换) │ -│ • 不包含业务逻辑 │ -└─────────────────────────────────────────────────────────┘ - ↓ 只使用 Entity -┌─────────────────────────────────────────────────────────┐ -│ Mapper 层(数据访问) │ -│ • 数据库 CRUD 操作 │ -│ • 继承 BaseMapper │ -│ • 接收/返回 Entity 或 Entity 列表 │ -│ • 复杂查询返回 Entity(通过 ResultMap 映射) │ -│ • 禁止返回 Map/JSONObject/自定义 DTO │ -└─────────────────────────────────────────────────────────┘ -``` - -#### 1.2 统一响应格式 - -```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 } } -``` - ---- - -### 二、Service/Mapper 层数据使用规范 - -**核心原则:Service 层和 Mapper 层必须使用实体类(Entity)接收和返回数据,严禁在 Service 层和 Mapper 层之间使用 DTO/VO 转换。** - -#### 黄金法则 - -| 层级间通信 | 数据类型 | -|-----------|---------| -| Service ↔ Mapper | **只用 Entity** | -| Controller ↔ Service | 可以 DTO/Entity 混用 | -| Controller ↔ HTTP | **DTO 进,VO 出** | - -#### 错误示例 - -```java -// ❌ 错误:不要在 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 { - UserInfoDTO selectUserDTO(Long id); // 错误!应返回 User -} -``` - -#### 正确示例 - -```java -// ✅ 正确:Service 层和 Mapper 层使用 Entity -@Service -public class UserServiceImpl extends ServiceImpl implements UserService { - @Override - public User getUserById(Long userId) { - return this.getById(userId); // 直接返回 Entity - } -} - -// Controller 层负责 Entity → VO 转换 -@GetMapping("/{id}") -public Result getUser(@PathVariable Long id) { - User user = userService.getUserById(id); // Service 返回 Entity - UserInfoVO vo = convertToVO(user); // Controller 转换为 VO - return Result.success(vo); -} -``` - ---- - -### 三、Service 层继承规范 - -**所有 Service 接口必须继承 `IService`,实现类必须继承 `ServiceImpl`** - -```java -// Service 接口 -public interface UserService extends IService { - User createUser(UserCreateRequest request); - Page pageUsers(Integer page, Integer size, String keyword); -} - -// Service 实现类 -@Service -public class UserServiceImpl extends ServiceImpl implements UserService { - // 自动拥有:save(), remove(), update(), getById(), list(), page(), count() 等方法 -} -``` - -**继承 IService 的好处:** -- 减少样板代码:基础 CRUD 方法无需手动编写 -- 统一接口规范:所有 Service 层接口一致 -- 类型安全:泛型确保类型正确 -- 链式调用:支持 `lambdaQuery()` 等链式操作 -- 批量操作:内置 `saveBatch()`, `removeBatch()` 等方法 - ---- - -### 四、查询分页规范 - -**所有返回列表的查询接口,默认必须分页处理** - -```java -// ❌ 错误:不分页返回所有数据 -@GetMapping("/list") -public Result> listUsers() { - List users = userService.list(); // 可能返回成千上万条 - return Result.success(users); -} - -// ✅ 正确:分页返回 -@GetMapping("/page") -public Result> pageUsers( - @RequestParam(defaultValue = "1") Integer page, - @RequestParam(defaultValue = "10") Integer size, - @RequestParam(required = false) String keyword) { - - Page userPage = this.page( - new Page<>(page, size), - Wrappers.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 使用规范 - -```java -// 使用 Lambda 表达式构建类型安全的查询条件 -LambdaQueryWrapper 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); -``` - ---- - -### 六、日志打印规范 - -**核心原则:所有日志打印内容必须使用中文。** - -**错误示例:** -```java -// ❌ 错误:使用英文日志 -log.info("User created successfully, id: {}", userId); -log.debug("Query user by id: {}", userId); -log.error("Failed to create user", e); -``` - -**正确示例:** -```java -// ✅ 正确:使用中文日志 -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 | - -**完整示例:** -```java -@Slf4j -@Service -public class UserServiceImpl extends ServiceImpl 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`) | 静态方法调用 | - -#### 错误示例 - -```java -// ❌ 错误:工具方法不应该写在 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() { /* ... */ } // 重复代码 -} -``` - -#### 正确示例 - -**多处调用的工具函数 → 统一工具类** - -```java -// ✅ 正确:统一工具类 -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 层内部方法** - -```java -// ✅ 正确:仅在本 Service 内使用的工具方法写在 Service 层 -@Service -public class LessonServiceImpl extends ServiceImpl implements LessonService { - - @Override - public void finishLesson(Long lessonId, List 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"); - } - } -} -``` - -#### 工具类设计原则 - -1. **私有构造**:防止实例化 -2. **静态方法**:所有方法都是 `static` 的 -3. **无状态**:工具类不应持有状态 -4. **线程安全**:工具方法必须是线程安全的 -5. **充分注释**:每个方法都要有 JavaDoc 注释 - -#### 代码审查要点 - -- [ ] 工具函数是否抽取到工具类或 Service 内部 -- [ ] 是否存在 Controller 层直接编写工具方法的情况 -- [ ] 多处使用的工具是否统一到工具类中 -- [ ] 工具类是否有私有构造防止实例化 -- [ ] 工具方法是否为静态方法 - ---- - -### 八、Swagger/OpenAPI 注解规范 - -**所有 Entity、DTO、VO 都必须添加 `@Schema` 注解** - -```java -// 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` -- [ ] Service 实现类是否继承 `ServiceImpl` -- [ ] Service ↔ Mapper 之间是否只使用 Entity(无 DTO 转换) -- [ ] 查询列表接口是否进行了分页处理 -- [ ] 简单查询是否优先使用 QueryWrapper + 通用方法 -- [ ] 复杂联表查询是否使用自定义 SQL - -#### Controller 层 - -- [ ] 是否使用 `@RestController` 注解 -- [ ] 返回类型是否为 `Result` 或 `Result>` -- [ ] 是否使用 `@Operation` 描述接口 -- [ ] 是否使用 `@Parameter` 描述参数 -- [ ] 是否使用 `@Valid` 校验请求参数 - -#### Entity/DTO/VO - -- [ ] Entity 类是否添加 `@Schema(description = "...")` -- [ ] Entity 字段是否添加 `@Schema` 注解 -- [ ] DTO/VO 类是否添加 `@Schema(description = "...")` -- [ ] DTO/VO 字段是否添加 `@Schema` 注解 -- [ ] 必填字段是否标注 `requiredMode = REQUIRED` -- [ ] 只读字段(ID、时间戳)是否标注 `requiredMode = READ_ONLY` \ No newline at end of file diff --git a/dev.db b/dev.db deleted file mode 100644 index 0414b48cc2ea69edd806d6c839e964e7a59a1235..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458752 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