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

42 KiB
Raw Blame History

CLAUDE.md

本文档为 Claude Code (claude.ai/code) 在本项目中工作时提供指导。

项目概述

这是一个少儿智慧阅读平台Kindergarten Course Management System采用 Spring Boot 后端 + Vue 3 前端架构。系统管理幼儿园的课程、课时、任务和学生成长记录。

技术架构

后端 (reading-platform-java)

  • 框架: Spring Boot 3.2.3 + Java 17
  • 持久层: MyBatis-Plus 3.5.5
  • 安全: Spring Security + JWT
  • API 文档: Knife4j (Swagger OpenAPI 3)
  • 数据库: MySQL 8.0
  • 数据库迁移: Flyway

前端 (reading-platform-frontend)

  • 框架: Vue 3 + TypeScript + Vite
  • UI 组件库: Ant Design Vue
  • 状态管理: Pinia
  • API: Axios + Orval 自动生成的 TypeScript 客户端

多租户架构

系统支持多个幼儿园(租户):

  • admin 角色:超级管理员(无租户,管理全系统课程)
  • school 角色:学校管理员(管理本校的教师、学生、班级)
  • teacher 角色:教师(管理本校的课时和任务)
  • parent 角色:家长(查看孩子的进度和任务)

admin_users 外,每个实体都有 tenant_id 字段。系统课程的 tenant_id = NULL

项目结构

kindergarten_java/
├── reading-platform-java/           # Spring Boot 后端
│   ├── src/main/java/.../controller/
│   │   ├── admin/                   # 超级管理员端点 (/api/v1/admin/*)
│   │   ├── school/                  # 学校管理员端点 (/api/v1/school/*)
│   │   ├── teacher/                 # 教师端点 (/api/v1/teacher/*)
│   │   └── parent/                  # 家长端点 (/api/v1/parent/*)
│   ├── entity/                      # 数据库实体27张表
│   ├── mapper/                      # MyBatis-Plus 映射器
│   ├── service/                     # 服务层接口 + 实现
│   ├── common/
│   │   ├── annotation/RequireRole   # 基于角色的访问控制
│   │   ├── security/                # JWT 认证
│   │   ├── enums/                   # UserRole, CourseStatus 等枚举
│   │   ├── response/                # Result<T>, PageResult<T>
│   │   └── config/                  # Security, MyBatis, OpenAPI 配置
│   └── resources/
│       ├── db/migration/            # Flyway 迁移脚本
│       └── mapper/                  # MyBatis XML 文件
│
├── reading-platform-frontend/       # Vue 3 前端
│   ├── src/views/
│   │   ├── admin/                   # 超级管理员页面
│   │   ├── school/                  # 学校管理员页面
│   │   ├── teacher/                 # 教师页面
│   │   └── parent/                  # 家长页面
│   ├── api/generated/               # 自动生成的 API 客户端
│   ├── api-spec.yml                 # OpenAPI 规范
│   └── router/index.ts              # Vue Router 配置
│
├── docker-compose.yml               # 后端 + 前端服务
└── docs/开发协作指南.md              # 开发指南(中文)

关键模式

1. 基于角色的访问控制

在 Controller/Service 上使用 @RequireRole 注解:

@RequireRole(UserRole.SCHOOL)  // 只有学校管理员可以访问

2. 租户隔离

在学校/教师/家长端点中使用 SecurityUtils.getCurrentTenantId() 按当前租户过滤数据。

3. 统一响应格式

Result<T> success(T data)   // { code: 200, message: "success", data: ... }
Result<T> error(code, msg)  // { code: xxx, message: "...", data: null }

4. OpenAPI 驱动开发

  • 后端:在 Controller 上使用 @Operation@Parameter@Schema 注解
  • 前端:运行 npm run api:updateapi-spec.yml 重新生成 TypeScript 客户端

5. 前后端接口规范

后端:以 Controller 为"唯一真源"

以后端 Spring Boot Controller 为接口定义的唯一真源,通过 SpringDoc/Knife4j 导出OpenAPI 规范,所有接口必须符合统一响应模型。

统一响应模型:

// 普通接口
Result<T> success(T data)   // { code: 200, message: "success", data: ... }
Result<T> error(code, msg)  // { code: xxx, message: "...", data: null }

// 分页接口
Result<PageResult<T>>       // { code: 200, message: "success", data: { items, total, page, pageSize } }

响应结构说明:

接口类型 返回类型 分页字段命名
普通接口 Result<T> -
分页接口 Result<PageResult<T>> page, pageSize, total, items
错误响应 Result<Void> 参考 ErrorCode 枚举

请求参数规范:

参数类型 注解 说明
路径变量 @PathVariable 与 OpenAPI path 模板保持一致
查询参数 @RequestParam 分页:page, pageSize;过滤:keyword, category
请求体 @RequestBody DTO 使用 *Request 类,不直接暴露实体

前端src/apis + fetch.ts 调用模式

围绕 src/apis + fetch.ts 的调用模式将真实接口规范路径、method、参数、响应结构维护到 apis.ts

工作流程:

后端 Controller (带 @Schema 注解)
       ↓
  Knife4j/Swagger → /v3/api-docs
       ↓
  api-spec.yml (OpenAPI 规范)
       ↓
  orval (npm run api:gen)
       ↓
  生成的 TypeScript 类型 + API 客户端
       ↓
  Vue 组件使用 (强制类型校验)

实施步骤

一、梳理并固化接口响应规范

  1. 确认统一响应模型

    • 普通接口:Result<业务 DTO>,字段 code/message/data
    • 分页接口:Result<PageResult<业务 DTO>>,分页字段命名(page, pageSize, total 等)
    • 错误响应:统一使用 Result<Void> 或类似 ResultVoid schema
  2. 从后端提炼规范说明

    • 位置:reading-platform-java/common/responsecommon/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 实体类,不要使用 MapHashMapJSONObject 返回数据
  • VO 类应放在 com.reading.platform.dto.response 包下
  • 使用 @Schema 注解描述字段含义,便于生成 API 文档
  • 使用 @Data@Builder 注解简化代码

示例:

@Data
@Builder
@Schema(description = "用户信息响应")
public class UserInfoResponse {
    @Schema(description = "用户 ID")
    private Long id;

    @Schema(description = "用户名")
    private String username;

    @Schema(description = "昵称")
    private String nickname;

    @Schema(description = "角色")
    private String role;
}

请求对象Request/DTO

  • 复杂请求参数应创建 DTO 类,放在 com.reading.platform.dto.request 包下
  • 使用 @Valid 和校验注解确保参数合法性

为什么不用 Map

  • VO 实体类类型安全、IDE 智能提示、API 文档自动生成、易于维护
  • Map类型不安全、无文档、易出错、难以重构

7. Swagger/OpenAPI 注解规范

强制要求

所有实体类Entity、DTO、VO 都必须添加 @Schema 注解,确保 API 文档完整性和前端类型生成准确性。

注解使用规范

1. 类级别注解

// Entity 类
@Data
@TableName("users")
@Schema(description = "用户信息")  // 必须添加
public class User { ... }

// DTO/VO 类
@Data
@Schema(description = "用户创建请求")  // 必须添加
public class UserCreateRequest { ... }

@Data
@Schema(description = "用户信息响应")  // 必须添加
public class UserResponse { ... }

2. 字段级别注解

@Schema(description = "用户 ID", example = "1", requiredMode = Schema.RequiredMode.READ_ONLY)
private Long id;

@Schema(description = "用户名", example = "zhangsan", requiredMode = Schema.RequiredMode.REQUIRED)
private String username;

@Schema(description = "年龄", example = "18", minimum = "1", maximum = "150")
private Integer age;

@Schema(description = "创建时间", example = "2024-01-01 12:00:00")
private LocalDateTime createdAt;

3. Controller 方法注解

@Operation(summary = "创建用户", description = "创建新用户并返回用户信息")
@ApiResponses({
    @ApiResponse(responseCode = "200", description = "创建成功"),
    @ApiResponse(responseCode = "400", description = "参数错误"),
    @ApiResponse(responseCode = "409", description = "用户名已存在")
})
@PostMapping
public Result<UserResponse> createUser(@Valid @RequestBody UserCreateRequest request) {
    return Result.success(userService.createUser(request));
}

4. 参数注解

@Operation(summary = "根据 ID 获取用户")
@GetMapping("/{id}")
public Result<UserResponse> getUser(
    @Parameter(description = "用户 ID", required = true)
    @PathVariable Long id,

    @Parameter(description = "是否包含详细信息")
    @RequestParam(defaultValue = "false")
    Boolean includeDetails
) {
    return Result.success(userService.getUser(id, includeDetails));
}

常用注解说明

注解 位置 说明
@Schema(description = "...") 类/字段 描述类或字段的含义
@Schema(example = "...") 字段 示例值
@Schema(requiredMode = REQUIRED) 字段 必填字段
@Schema(requiredMode = READ_ONLY) 字段 只读字段(如 ID、创建时间
@Schema(minimum = "1", maximum = "100") 字段 数值范围
@Schema(minLength = 1, maxLength = 50) 字段 字符串长度
@Schema(pattern = "^[a-zA-Z]+$") 字段 正则表达式
@Operation(summary = "...") 方法 接口摘要
@Parameter(description = "...") 参数 参数描述

完整示例

Entity 类:

package com.reading.platform.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("courses")
@Schema(description = "课程信息")
public class Course {

    @Schema(description = "课程 ID", requiredMode = Schema.RequiredMode.READ_ONLY)
    @TableId(type = IdType.AUTO)
    private Long id;

    @Schema(description = "租户 ID", requiredMode = Schema.RequiredMode.READ_ONLY)
    private Long tenantId;

    @Schema(description = "课程名称", example = "绘本阅读入门", requiredMode = Schema.RequiredMode.REQUIRED)
    private String name;

    @Schema(description = "课程编码", example = "READ001")
    private String code;

    @Schema(description = "课程描述")
    private String description;

    @Schema(description = "封面图片 URL")
    private String coverUrl;

    @Schema(description = "分类", example = "language")
    private String category;

    @Schema(description = "适用年龄段", example = "5-6 岁")
    private String ageRange;

    @Schema(description = "难度等级", example = "beginner")
    private String difficultyLevel;

    @Schema(description = "课程时长(分钟)", example = "30")
    private Integer durationMinutes;

    @Schema(description = "状态", example = "published")
    private String status;

    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.READ_ONLY)
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdAt;

    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.READ_ONLY)
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedAt;
}

DTO 类:

package com.reading.platform.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

@Data
@Schema(description = "课程创建请求")
public class CourseCreateRequest {

    @Schema(description = "课程名称", example = "绘本阅读入门", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotBlank(message = "课程名称不能为空")
    @Size(max = 100, message = "课程名称不能超过 100 个字符")
    private String name;

    @Schema(description = "课程描述", example = "适合大班幼儿的绘本阅读课程")
    private String description;

    @Schema(description = "分类", example = "language")
    private String category;

    @Schema(description = "适用年龄段", example = "5-6 岁")
    private String ageRange;
}

代码审查要点

  • Entity 类是否添加了 @Schema(description = "...")
  • Entity 字段是否添加了 @Schema 注解描述字段含义
  • DTO/VO 类是否添加了 @Schema(description = "...")
  • DTO/VO 字段是否添加了 @Schema 注解
  • Controller 方法是否添加了 @Operation 注解
  • Controller 参数是否添加了 @Parameter 注解
  • 必填字段是否标注了 requiredMode = REQUIRED
  • 只读字段ID、时间戳是否标注了 requiredMode = READ_ONLY
  • 是否有合适的 example 示例值

注意事项

  1. 导入正确的包
import io.swagger.v3.oas.annotations.media.Schema;  // Swagger 3.x
  1. Lombok 与 Schema 的配合
  • @Data 生成的 getter 方法会自动继承字段上的 @Schema 注解
  • 但建议字段和方法都加上注解以确保兼容性
  1. 必填校验
  • requiredMode = REQUIRED 仅用于文档说明
  • 实际校验需要配合 @NotNull@NotBlank 等校验注解
  1. 枚举类型
@Schema(description = "状态", example = "active", allowableValues = {"active", "inactive"})
private String status;

8. 前端接口校验规范

基于 OpenAPI + orval + TypeScript 的强制校验链路

本项目前端采用从接口文档到页面代码的强制类型校验链路,确保前后端接口一致性和类型安全。

技术栈:

  • OpenAPI 3.0:统一的 API 接口规范
  • orval:从 OpenAPI 规范生成 TypeScript 类型和 API 客户端
  • TypeScript:静态类型检查
  • ESLint:代码规范约束
  • axiosHTTP 请求(由 orval 自动生成)

工作流程:

后端 Controller (带 @Schema 注解)
       ↓
  Knife4j/Swagger
       ↓
  api-spec.yml (OpenAPI 规范)
       ↓
  orval (npm run api:gen)
       ↓
  生成的 TypeScript 类型 + API 客户端
       ↓
  Vue 组件使用 (强制类型校验)

配置说明

orval 配置 (orval.config.tsvite.config.ts)

export default {
  'api': {
    input: {
      target: './api-spec.yml',
      filters: {
        tags: ['default'], // 只生成指定标签的接口
      },
    },
    output: {
      mode: 'split', // 分离模式:每个接口一个文件
      target: './src/api/generated/client.ts',
      schemas: './src/api/generated/schemas',
      client: 'axios',
      mock: false,
      override: {
        fetch: {
          includeHttpRequestHeader: true,
        },
        useTypeOverInterfaces: true, // 优先使用 type
        useDate: true,
      },
    },
  },
};

使用规范

1. 后端必须添加 @Schema 注解

@Operation(summary = "获取用户信息")
@GetMapping("/{id}")
public Result<UserInfoResponse> getUser(@PathVariable Long id) {
    return Result.success(userService.getUser(id));
}

2. 前端必须使用生成的类型和客户端

// ✅ 正确:使用生成的类型和 API
import { getUser } from '@/api/generated/client';
import type { UserInfoResponse } from '@/api/generated/schemas';

const user: UserInfoResponse = await getUser(id);

// ❌ 错误:不要手写类型或手动调用 axios
interface ManualUser { id: number; name: string; }
const user = await axios.get(`/api/users/${id}`);

3. ESLint 约束规则

// .eslintrc.cjs 中添加
rules: {
  // 禁止手动调用 axios必须使用生成的 API 客户端
  'no-restricted-imports': [
    'error',
    {
      patterns: ['axios', '!@/api/generated/*'],
    },
  ],
  // 强制使用生成的类型定义
  '@typescript-eslint/no-explicit-any': 'error',
  // 禁止使用 any 类型(特殊情况需 eslint-disable 注释)
}

4. 运行时校验(可选) 使用 zodyup 进行运行时数据校验:

import { UserInfoSchema } from '@/api/generated/schemas';
import { z } from 'zod';

// 运行时校验响应数据
const parsedData = UserInfoSchema.parse(apiResponse);

开发命令

cd reading-platform-frontend

# 从后端更新 API 规范并生成客户端
npm run api:gen

# 或者完整流程(包含从后端导出 swagger
npm run api:update

代码审查要点

  • 前端是否使用了 orval 生成的类型,而非手写 interface
  • 是否使用生成的 API 客户端,而非手动 axios 调用
  • TypeScript 编译是否通过(无类型错误)
  • 后端 Controller 是否正确添加 @Schema 注解
  • API 变更后是否重新运行了 npm run api:gen

常见问题

Q: 为什么要强制使用生成的类型? A: 手写类型容易与后端实际返回不一致,导致运行时错误。生成类型确保前后端一致性。

Q: 如何添加自定义逻辑? A: 在生成的客户端外封装业务逻辑层,不要修改生成的文件。

Q: API 变更后忘记更新怎么办? A: CI/CD 中可添加类型检查步骤,类型不通过则构建失败。

开发命令

后端

Java 环境配置

项目要求 Java 17Spring 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

# Windows 命令行(在项目目录下)
.\mvnw.cmd clean install -DskipTests

# 或者使用编译脚本
.\compile.bat

⚠️ 重要: 不要直接使用 mvn 命令,因为它会使用系统 JAVA_HOME 环境变量(可能是 Java 8。必须使用 .\mvnw.cmd,它内置了 Java 17 路径配置。

# 使用 Docker Compose 运行(推荐)
docker compose up --build

# 本地运行(需要 MySQL 已启动,且确保使用 Java 17
cd reading-platform-java
.\mvnw.cmd spring-boot:run

# 构建
.\mvnw.cmd clean package -DskipTests

前端

cd reading-platform-frontend
npm install
npm run dev
npm run build

# 从后端规范更新 API 客户端
npm run api:update

数据库迁移

  • 将新的迁移脚本添加到 reading-platform-java/src/main/resources/db/migration/V{n}__description.sql
  • Flyway 会在后端启动时自动运行(仅开发模式)

数据库表结构27张表

  • 租户: tenants, tenant_courses
  • 用户: admin_users, teachers, students, parents, parent_students
  • 班级: classes, class_teachers, student_class_history
  • 课程: courses, course_versions, course_resources, course_scripts, course_script_pages, course_activities
  • 课时: lessons, lesson_feedbacks, student_records
  • 任务: tasks, task_targets, task_completions, task_templates
  • 成长: growth_records
  • 资源: resource_libraries, resource_items
  • 日程: schedule_plans, schedule_templates
  • 系统: system_settings, notifications, operation_logs, tags

测试账号

角色 用户名 密码
管理员 admin admin123
学校 school 123456
教师 teacher1 123456
家长 parent1 123456

API 文档

近期补充的接口和字段2026-03-10

实体类新增字段

StudentRecord - 学生评价记录

  • focus - 专注力评分 (1-5)
  • participation - 参与度评分 (1-5)
  • interest - 兴趣评分 (1-5)
  • understanding - 理解度评分 (1-5)

LessonFeedback - 课时反馈

  • design_quality - 设计质量评分 (1-5)
  • participation - 参与度评分 (1-5)
  • goal_achievement - 目标达成度评分 (1-5)
  • step_feedbacks - 环节反馈 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 统一响应格式

// 普通接口
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 出

错误示例

// ❌ 错误:不要在 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
}

正确示例

// ✅ 正确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>

// 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() 等方法

四、查询分页规范

所有返回列表的查询接口,默认必须分页处理

// ❌ 错误:不分页返回所有数据
@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 使用规范

// 使用 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);

六、日志打印规范

核心原则:所有日志打印内容必须使用中文。

错误示例:

// ❌ 错误:使用英文日志
log.info("User created successfully, id: {}", userId);
log.debug("Query user by id: {}", userId);
log.error("Failed to create user", e);

正确示例:

// ✅ 正确:使用中文日志
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

完整示例:

@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 静态方法调用

错误示例

// ❌ 错误:工具方法不应该写在 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() { /* ... */ } // 重复代码
}

正确示例

多处调用的工具函数 → 统一工具类

// ✅ 正确:统一工具类
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 层内部方法

// ✅ 正确:仅在本 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 注解

// 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