kindergarten_java/CLAUDE.md
En e501e17403 feat: 完善学校统计报告、资源服务及实体类字段
主要变更:
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 编译
2026-03-11 16:21:22 +08:00

1269 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` 注解:
```java
@RequireRole(UserRole.SCHOOL) // 只有学校管理员可以访问
```
### 2. 租户隔离
在学校/教师/家长端点中使用 `SecurityUtils.getCurrentTenantId()` 按当前租户过滤数据。
### 3. 统一响应格式
```java
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` 规范,所有接口必须符合统一响应模型。
**统一响应模型:**
```java
// 普通接口
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 组件使用 (强制类型校验)
```
#### 实施步骤
**一、梳理并固化接口响应规范**
1. **确认统一响应模型**
- 普通接口:`Result<业务 DTO>`,字段 `code/message/data`
- 分页接口:`Result<PageResult<业务 DTO>>`,分页字段命名(`page`, `pageSize`, `total` 等)
- 错误响应:统一使用 `Result<Void>` 或类似 `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<T>` 或`Result<PageResult<T>>`
- 为缺少注解的接口补全 `@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<UserResponse> createUser(@Valid @RequestBody UserCreateRequest request) {
return Result.success(userService.createUser(request));
}
```
**4. 参数注解**
```java
@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 类:**
```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<UserInfoResponse> 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<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 统一响应格式
```java
// 普通接口
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 出** |
#### 错误示例
```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<User> {
UserInfoDTO selectUserDTO(Long id); // 错误!应返回 User
}
```
#### 正确示例
```java
// ✅ 正确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>`**
```java
// 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()` 等方法
---
### 四、查询分页规范
**所有返回列表的查询接口,默认必须分页处理**
```java
// ❌ 错误:不分页返回所有数据
@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 使用规范
```java
// 使用 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);
```
---
### 六、日志打印规范
**核心原则:所有日志打印内容必须使用中文。**
**错误示例:**
```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<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` | 静态方法调用 |
#### 错误示例
```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<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");
}
}
}
```
#### 工具类设计原则
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<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`