2026-03-06 12:23:10 +08:00
|
|
|
|
# CLAUDE.md
|
|
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
本文档为 Claude Code (claude.ai/code) 在本项目中工作时提供指导。
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
## 项目概述
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
这是一个**少儿智慧阅读平台**(Kindergarten Course Management System),采用 Spring Boot 后端 + Vue 3 前端架构。系统管理幼儿园的课程、课时、任务和学生成长记录。
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
## 技术架构
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
### 后端 (`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
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
### 前端 (`reading-platform-frontend`)
|
|
|
|
|
|
- **框架**: Vue 3 + TypeScript + Vite
|
|
|
|
|
|
- **UI 组件库**: Ant Design Vue
|
|
|
|
|
|
- **状态管理**: Pinia
|
|
|
|
|
|
- **API**: Axios + Orval 自动生成的 TypeScript 客户端
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
## 多租户架构
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
系统支持多个幼儿园(租户):
|
|
|
|
|
|
- `admin` 角色:超级管理员(无租户,管理全系统课程)
|
|
|
|
|
|
- `school` 角色:学校管理员(管理本校的教师、学生、班级)
|
|
|
|
|
|
- `teacher` 角色:教师(管理本校的课时和任务)
|
|
|
|
|
|
- `parent` 角色:家长(查看孩子的进度和任务)
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
除 `admin_users` 外,每个实体都有 `tenant_id` 字段。系统课程的 `tenant_id = NULL`。
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
## 项目结构
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
kindergarten_java/
|
2026-03-10 01:06:03 +08:00
|
|
|
|
├── reading-platform-java/ # Spring Boot 后端
|
2026-03-06 12:23:10 +08:00
|
|
|
|
│ ├── src/main/java/.../controller/
|
2026-03-10 01:06:03 +08:00
|
|
|
|
│ │ ├── admin/ # 超级管理员端点 (/api/v1/admin/*)
|
|
|
|
|
|
│ │ ├── school/ # 学校管理员端点 (/api/v1/school/*)
|
|
|
|
|
|
│ │ ├── teacher/ # 教师端点 (/api/v1/teacher/*)
|
|
|
|
|
|
│ │ └── parent/ # 家长端点 (/api/v1/parent/*)
|
|
|
|
|
|
│ ├── entity/ # 数据库实体(27张表)
|
|
|
|
|
|
│ ├── mapper/ # MyBatis-Plus 映射器
|
|
|
|
|
|
│ ├── service/ # 服务层接口 + 实现
|
2026-03-06 12:23:10 +08:00
|
|
|
|
│ ├── common/
|
2026-03-10 01:06:03 +08:00
|
|
|
|
│ │ ├── annotation/RequireRole # 基于角色的访问控制
|
|
|
|
|
|
│ │ ├── security/ # JWT 认证
|
|
|
|
|
|
│ │ ├── enums/ # UserRole, CourseStatus 等枚举
|
2026-03-06 12:23:10 +08:00
|
|
|
|
│ │ ├── response/ # Result<T>, PageResult<T>
|
2026-03-10 01:06:03 +08:00
|
|
|
|
│ │ └── config/ # Security, MyBatis, OpenAPI 配置
|
2026-03-06 12:23:10 +08:00
|
|
|
|
│ └── resources/
|
2026-03-10 01:06:03 +08:00
|
|
|
|
│ ├── db/migration/ # Flyway 迁移脚本
|
|
|
|
|
|
│ └── mapper/ # MyBatis XML 文件
|
2026-03-06 12:23:10 +08:00
|
|
|
|
│
|
2026-03-10 01:06:03 +08:00
|
|
|
|
├── reading-platform-frontend/ # Vue 3 前端
|
2026-03-06 12:23:10 +08:00
|
|
|
|
│ ├── src/views/
|
2026-03-10 01:06:03 +08:00
|
|
|
|
│ │ ├── admin/ # 超级管理员页面
|
|
|
|
|
|
│ │ ├── school/ # 学校管理员页面
|
|
|
|
|
|
│ │ ├── teacher/ # 教师页面
|
|
|
|
|
|
│ │ └── parent/ # 家长页面
|
|
|
|
|
|
│ ├── api/generated/ # 自动生成的 API 客户端
|
|
|
|
|
|
│ ├── api-spec.yml # OpenAPI 规范
|
|
|
|
|
|
│ └── router/index.ts # Vue Router 配置
|
2026-03-06 12:23:10 +08:00
|
|
|
|
│
|
2026-03-10 01:06:03 +08:00
|
|
|
|
├── docker-compose.yml # 后端 + 前端服务
|
|
|
|
|
|
└── docs/开发协作指南.md # 开发指南(中文)
|
2026-03-06 12:23:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
## 关键模式
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
### 1. 基于角色的访问控制
|
|
|
|
|
|
在 Controller/Service 上使用 `@RequireRole` 注解:
|
2026-03-06 12:23:10 +08:00
|
|
|
|
```java
|
2026-03-10 01:06:03 +08:00
|
|
|
|
@RequireRole(UserRole.SCHOOL) // 只有学校管理员可以访问
|
2026-03-06 12:23:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
### 2. 租户隔离
|
|
|
|
|
|
在学校/教师/家长端点中使用 `SecurityUtils.getCurrentTenantId()` 按当前租户过滤数据。
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
### 3. 统一响应格式
|
2026-03-06 12:23:10 +08:00
|
|
|
|
```java
|
|
|
|
|
|
Result<T> success(T data) // { code: 200, message: "success", data: ... }
|
|
|
|
|
|
Result<T> error(code, msg) // { code: xxx, message: "...", data: null }
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
### 4. OpenAPI 驱动开发
|
2026-03-10 23:50:53 +08:00
|
|
|
|
|
|
|
|
|
|
- **后端**:在 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 中可添加类型检查步骤,类型不通过则构建失败。
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
## 开发命令
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
### 后端
|
2026-03-11 16:21:22 +08:00
|
|
|
|
|
|
|
|
|
|
#### 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 路径配置。
|
|
|
|
|
|
|
2026-03-06 12:23:10 +08:00
|
|
|
|
```bash
|
2026-03-10 01:06:03 +08:00
|
|
|
|
# 使用 Docker Compose 运行(推荐)
|
2026-03-06 12:23:10 +08:00
|
|
|
|
docker compose up --build
|
|
|
|
|
|
|
2026-03-11 16:21:22 +08:00
|
|
|
|
# 本地运行(需要 MySQL 已启动,且确保使用 Java 17)
|
2026-03-06 12:23:10 +08:00
|
|
|
|
cd reading-platform-java
|
2026-03-11 16:21:22 +08:00
|
|
|
|
.\mvnw.cmd spring-boot:run
|
2026-03-06 12:23:10 +08:00
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
# 构建
|
2026-03-11 16:21:22 +08:00
|
|
|
|
.\mvnw.cmd clean package -DskipTests
|
2026-03-06 12:23:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
### 前端
|
2026-03-06 12:23:10 +08:00
|
|
|
|
```bash
|
|
|
|
|
|
cd reading-platform-frontend
|
|
|
|
|
|
npm install
|
|
|
|
|
|
npm run dev
|
|
|
|
|
|
npm run build
|
|
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
# 从后端规范更新 API 客户端
|
2026-03-06 12:23:10 +08:00
|
|
|
|
npm run api:update
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-10 01:06:03 +08:00
|
|
|
|
### 数据库迁移
|
|
|
|
|
|
- 将新的迁移脚本添加到 `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 23:50:53 +08:00
|
|
|
|
- 访问地址: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` - 提交任务家长反馈
|
|
|
|
|
|
|
|
|
|
|
|
### 数据库迁移
|
|
|
|
|
|
|
|
|
|
|
|
- 迁移脚本:``
|
2026-03-11 16:21:22 +08:00
|
|
|
|
- 包含上述所有实体类新增字段的 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`
|