detailDesign.md 65 KB

宠物服务管理系统 — 服务端详细设计文档

1. 技术栈

技术 版本 用途
Java 17 开发语言
Spring Boot 3.5.15 应用框架
Spring Boot Starter Web 3.5.15 Web MVC + 嵌入式 Tomcat
MyBatis Spring Boot Starter 3.0.5 ORM 框架
MySQL Connector/J (spring-boot-starter-parent 管理) MySQL JDBC 驱动
SpringDoc OpenAPI 2.8.16 API 文档 (Swagger UI)
Maven (wrapper 3.9+) 构建工具
MySQL 8.0 数据库

构建命令:./mvnw spring-boot:run — 启动开发服务器(端口 8080, context-path /)

2. 项目结构

server/
├── pom.xml                                    # Maven 项目描述文件
├── sql/
│   └── init.sql                               # 数据库初始化脚本(建库建表+种子数据)
├── src/main/java/space/anyi/petServiceManagementSystem/server/
│   ├── ServerApplication.java                 # Spring Boot 启动入口
│   │
│   ├── config/                                # 配置类
│   │   ├── CorsConfig.java                    # 跨域 CORS 配置,允许 credentials + 任意 origin pattern
│   │   ├── WebMvcConfig.java                  # 注册 AuthInterceptor 和 AdminInterceptor 拦截器
│   │   ├── PasswordConfig.java                # MD5 + SALT 密码加密组件(@Component)
│   │   └── GlobalExceptionHandler.java        # @RestControllerAdvice 全局异常处理
│   │
│   ├── interceptor/                           # 拦截器
│   │   ├── AuthInterceptor.java               # HttpSession 登录认证(排除 /auth/**, /swagger-ui/**)
│   │   └── AdminInterceptor.java              # admin 角色校验(匹配 /admin/**)
│   │
│   ├── common/                                # 通用基础设施
│   │   ├── R.java                             # 统一响应 R<T> {code, message, data}
│   │   ├── ResultCode.java                    # 状态码常量 (200/400/401/403/404/409/500)
│   │   ├── BusinessException.java             # 业务异常 (code=400)
│   │   ├── UnauthorizedException.java         # 未授权异常 (code=401)
│   │   ├── ForbiddenException.java            # 无权限异常 (code=403)
│   │   ├── ResourceNotFoundException.java     # 资源不存在异常 (code=404)
│   │   └── PageVo.java                        # 分页结果封装 {data, total, pageNum, pageSize}
│   │
│   ├── entity/                                # 数据实体(对应数据库表)
│   │   ├── User.java                          # 用户 user 表
│   │   ├── Pet.java                           # 宠物 pet 表
│   │   ├── VaccinationRecord.java             # 疫苗记录 vaccination_record 表
│   │   ├── DewormingRecord.java               # 驱虫记录 deworming_record 表
│   │   ├── ServicePackage.java                # 套餐 package 表(避 java.lang.Package 冲突)
│   │   ├── BoardingRoom.java                  # 寄养房型 boarding_room 表
│   │   ├── BoardingRoomStatus.java            # 房态 boarding_room_status 表
│   │   ├── Appointment.java                   # 预约订单 appointment 表
│   │   └── Schedule.java                      # 排班 schedule 表
│   │
│   ├── dto/                                   # 请求参数 DTO
│   │   ├── auth/
│   │   │   ├── RegisterDto.java               # 注册 (account, password, username) + @Valid 校验
│   │   │   └── LoginDto.java                  # 登录 (account, password)
│   │   ├── user/
│   │   │   ├── UserDto.java                   # 新增/编辑用户
│   │   │   ├── UpdateUserPasswordDto.java     # 修改密码 (id, oldPassword, newPassword)
│   │   │   ├── UpdateUserAvatarDto.java       # 修改头像 (id, avatar)
│   │   │   └── UpdateUserStatusDto.java       # 启用/禁用 (id, enable)
│   │   ├── pet/
│   │   │   ├── PetDto.java                    # 新增/编辑宠物
│   │   │   ├── VaccinationDto.java            # 新增疫苗记录
│   │   │   └── DewormingDto.java              # 新增驱虫记录
│   │   ├── appointment/
│   │   │   ├── AppointmentDto.java            # 提交预约
│   │   │   ├── ReviewDto.java                 # 审核 (action + rejectReason)
│   │   │   └── HandleCancelDto.java           # 处理取消 (action)
│   │   ├── room/
│   │   │   └── RoomStatusDto.java             # 修改房态 (date, status)
│   │   ├── servicePackage/
│   │   │   └── PackageDto.java                # 新增/编辑套餐
│   │   └── schedule/
│   │       └── ScheduleDto.java               # 新增排班
│   │
│   ├── vo/                                    # 返回结果 VO
│   │   ├── user/
│   │   │   ├── UserVo.java                    # 用户基本信息
│   │   │   └── AdminUserDetailVo.java         # 管理员查看用户详情(含宠物列表)
│   │   ├── pet/
│   │   │   ├── PetVo.java                     # 宠物信息(含疫苗/驱虫记录列表)
│   │   │   ├── VaccinationVo.java             # 疫苗记录
│   │   │   └── DewormingVo.java               # 驱虫记录
│   │   ├── appointment/
│   │   │   └── AppointmentVo.java             # 预约订单详情
│   │   ├── room/
│   │   │   ├── BoardingRoomVo.java            # 寄养房型信息
│   │   │   └── RoomStatusVo.java              # 房态(含 appointmentId)
│   │   ├── servicePackage/
│   │   │   └── PackageVo.java                 # 套餐信息
│   │   └── schedule/
│   │       └── ScheduleVo.java                # 排班信息
│   │
│   ├── mapper/                                # MyBatis Mapper 接口
│   │   ├── UserMapper.java
│   │   ├── PetMapper.java
│   │   ├── VaccinationRecordMapper.java
│   │   ├── DewormingRecordMapper.java
│   │   ├── PackageMapper.java
│   │   ├── BoardingRoomMapper.java
│   │   ├── BoardingRoomStatusMapper.java
│   │   ├── AppointmentMapper.java
│   │   └── ScheduleMapper.java
│   │
│   ├── service/                               # 服务接口
│   │   ├── AuthService.java
│   │   ├── UserService.java
│   │   ├── PetService.java
│   │   ├── AppointmentService.java
│   │   ├── BoardingRoomService.java
│   │   ├── PackageService.java
│   │   ├── ScheduleService.java
│   │   └── impl/                              # 服务实现
│   │       ├── AuthServiceImpl.java
│   │       ├── UserServiceImpl.java
│   │       ├── PetServiceImpl.java
│   │       ├── AppointmentServiceImpl.java
│   │       ├── BoardingRoomServiceImpl.java
│   │       ├── PackageServiceImpl.java
│   │       └── ScheduleServiceImpl.java
│   │
│   └── util/
│       └── PageUtil.java                      # 分页工具类(利用 Supplier 封装列表+总数查询)
│
└── src/main/resources/
    ├── application.yaml                       # Spring Boot 配置文件
    └── mapper/                                # MyBatis XML 映射文件
        ├── UserMapper.xml
        ├── PetMapper.xml
        ├── VaccinationRecordMapper.xml
        ├── DewormingRecordMapper.xml
        ├── PackageMapper.xml
        ├── BoardingRoomMapper.xml
        ├── BoardingRoomStatusMapper.xml
        ├── AppointmentMapper.xml
        └── ScheduleMapper.xml

3. 数据模型

3.1 实体关系图 (E-R)

erDiagram
    user ||--o{ pet : "拥有"
    user ||--o{ appointment : "发起"
    pet ||--o{ vaccination_record : "疫苗记录"
    pet ||--o{ deworming_record : "驱虫记录"
    pet ||--o{ appointment : "被预约"
    appointment ||--|| package : "选择"
    appointment ||--o| boarding_room : "使用房型(寄养)"
    appointment ||--o{ boarding_room_status : "生成房态(审核通过后)"
    boarding_room ||--o{ boarding_room_status : "房态记录"

    user {
        BIGINT id PK
        VARCHAR account UK "登录账号"
        VARCHAR username
        VARCHAR password "MD5+SALT"
        VARCHAR email
        VARCHAR phone
        VARCHAR avatar
        VARCHAR role "user|admin"
        TINYINT enable "0禁用 1启用"
        DATETIME created_at
        DATETIME updated_at
    }

    pet {
        BIGINT id PK
        BIGINT user_id FK
        VARCHAR name
        VARCHAR species "DOG|CAT|OTHER"
        VARCHAR breed
        VARCHAR gender "MALE|FEMALE"
        DATE birth_date
        DECIMAL weight
        VARCHAR avatar
        TEXT remark
        DATETIME created_at
        DATETIME updated_at
    }

    vaccination_record {
        BIGINT id PK
        BIGINT pet_id FK
        VARCHAR vaccine_name
        DATE vaccine_date
        VARCHAR hospital
        VARCHAR doctor
        TEXT remark
        DATETIME created_at
    }

    deworming_record {
        BIGINT id PK
        BIGINT pet_id FK
        VARCHAR deworming_name
        DATE deworming_date
        VARCHAR hospital
        VARCHAR doctor
        TEXT remark
        DATETIME created_at
    }

    package {
        BIGINT id PK
        VARCHAR name
        VARCHAR type "BOARDING|GROOMING"
        DECIMAL price
        INT duration "寄养天数(仅寄养)"
        TEXT description
        TINYINT is_active "0禁用 1启用"
        DATETIME created_at
        DATETIME updated_at
    }

    boarding_room {
        BIGINT id PK
        VARCHAR room_name
        VARCHAR room_type "STANDARD|VIP|DELUXE"
        INT capacity "可容纳宠物数"
        DECIMAL price_per_day "每晚价格"
        TINYINT is_active "0禁用 1启用"
        TEXT description
        DATETIME created_at
        DATETIME updated_at
    }

    boarding_room_status {
        BIGINT id PK
        BIGINT room_id FK "关联房型"
        DATE date "日期"
        VARCHAR status "AVAILABLE|OCCUPIED|MAINTENANCE"
        BIGINT appointment_id "关联预约(系统写入)"
        DATETIME created_at
    }

    appointment {
        BIGINT id PK
        BIGINT user_id FK
        BIGINT pet_id FK
        BIGINT package_id FK
        BIGINT room_id FK "寄养房型(寄养必填)"
        VARCHAR type "BOARDING|GROOMING"
        VARCHAR status "PENDING|CONFIRMED|CANCELLED|CANCEL_REQUESTED"
        DATE start_date "寄养开始/洗护预约日"
        DATE end_date "寄养结束"
        DECIMAL total_price
        DECIMAL deposit "定金=总价*30%"
        TINYINT deposit_paid "0未支付 1已支付"
        TEXT cancel_reason
        VARCHAR reject_reason
        TEXT remark
        DATETIME created_at
        DATETIME updated_at
    }

    schedule {
        BIGINT id PK
        VARCHAR employee_name
        DATE work_date
        VARCHAR time_slot "MORNING|AFTERNOON|FULL_DAY"
        TEXT remark
        DATETIME created_at
    }

3.2 核心 SQL 脚本设计要点

UNIQUE 索引: boarding_room_status 表使用 UNIQUE KEY uk_room_date (room_id, date) 确保每个房型每天只有一条记录。通过 INSERT ... ON DUPLICATE KEY UPDATE 实现插入或更新(upsert):

INSERT INTO boarding_room_status (room_id, date, status, appointment_id, created_at)
VALUES (#{roomId}, #{date}, #{status}, #{appointmentId}, NOW())
ON DUPLICATE KEY UPDATE status = #{status}, appointment_id = #{appointmentId}

索引策略:

表 索引 用途
user uk_account (account) 登录/注册唯一性校验
pet idx_user_id (user_id) 快速查询某用户的所有宠物
vaccination_record idx_pet_id (pet_id) 查询某宠物疫苗记录
deworming_record idx_pet_id (pet_id) 查询某宠物驱虫记录
appointment idx_user_id (user_id) 查询某用户预约列表
appointment idx_status (status) 管理员按状态筛选订单
appointment idx_package_id (package_id) 统计套餐预约量
boarding_room_status uk_room_date (room_id, date) 唯一约束+日期范围查询
boarding_room_status idx_appointment_id (appointment_id) 取消订单时释放房态
schedule idx_work_date (work_date) 按月查询排班
package idx_type (type) 按类型筛选套餐

bagin 数据类型: price_per_day, total_price, deposit 等金额字段使用 DECIMAL(10,2) 避免浮点精度丢失。

4. 通用基础设施

4.1 统一响应 R<T>

public class R<T> {
    private int code;       // 状态码
    private String message; // 提示信息
    private T data;         // 响应数据

    public static <T> R<T> ok(T data)     // 成功返回 data (code=200, message="success")
    public static <T> R<T> ok()           // 成功无数据 (code=200)
    public static <T> R<T> error(int code, String message)  // 指定错误码
    public static <T> R<T> error(String message)            // 默认 400 错误
}

响应示例:

// 成功
{ "code": 200, "message": "success", "data": { "id": 1, "username": "张三" } }

// 分页
{ "code": 200, "message": "success", "data": { "data": [...], "total": 50, "pageNum": 1, "pageSize": 10 } }

// 业务错误
{ "code": 400, "message": "账号已存在", "data": null }

// 未登录
{ "code": 401, "message": "未登录或Session已过期", "data": null }

// 无权限
{ "code": 403, "message": "无权访问,需要管理员权限", "data": null }

4.2 业务异常 BusinessException

public class BusinessException extends RuntimeException {
    private int code;  // 默认 400

    public BusinessException(String message)           // code=400
    public BusinessException(int code, String message) // 自定义 code
}

继承体系:

RuntimeException
├── BusinessException          (400) — 业务校验失败
├── UnauthorizedException      (401) — 未登录/Session 过期
├── ForbiddenException         (403) — 无管理员权限
└── ResourceNotFoundException  (404) — 资源不存在

4.3 全局异常处理 GlobalExceptionHandler

拦截所有 Controller 抛出的异常,统一转换为 R 响应格式:

异常类型 HTTP Status code 处理逻辑
BusinessException 400 400 提取 message 返回业务提示
UnauthorizedException 401 401 返回 "未登录或Session已过期"
ForbiddenException 403 403 返回 "无权访问"
ResourceNotFoundException 404 404 返回 "资源不存在"
MethodArgumentNotValidException 400 400 提取 @Valid 校验失败信息
BindException 400 400 提取参数绑定失败信息
MissingServletRequestParameterException 400 400 返回 "缺少必填参数"
DuplicateKeyException 409 409 返回 "数据重复"
Exception (兜底) 500 500 打印堆栈后返回 "服务器内部错误"

4.4 分页实现 PageVo + PageUtil

public class PageVo<T> {
    private List<T> data;     // 当前页数据
    private long total;       // 总记录数
    private int pageNum;      // 当前页码(从1开始)
    private int pageSize;     // 每页大小
}

PageUtil 工具类封装分页查询范式,通过 Supplier 解耦列表查询和总数查询:

public static <T> PageVo<T> page(int pageNum, int pageSize,
        Supplier<List<T>> queryList, Supplier<Long> queryCount) {
    // 1. 执行列表查询: queryList.get()
    // 2. 执行总数查询: queryCount.get()
    // 3. 封装 PageVo 返回
}

算法步骤:

  1. Controller 接收 pageNum, pageSize 及筛选参数
  2. Service 计算 offset = (pageNum - 1) * pageSize
  3. MyBatis XML 中使用 LIMIT #{offset}, #{pageSize} 分页查询
  4. 同时执行 SELECT COUNT(*) 获取总记录数
  5. PageUtil 将两部分结果封装为 PageVo 返回

5. 认证与安全

5.1 HttpSession 认证机制

项目 说明
会话存储 服务端内存 Session,由 JSESSIONID Cookie 关联
Cookie 属性 HttpOnly=true, SameSite=Lax
登录时机 登录/注册成功后调用 request.getSession().setAttribute("user", userVo)
Session 内容 UserVo 对象(含 id, role, username 等)
注销方式 request.getSession().invalidate()
超时配置 server.servlet.session.timeout=1800 (30分钟)

5.2 AuthInterceptor 拦截器

配置路径: 拦截所有 /api/**,排除 /auth/**、/swagger-ui/**、/v3/api-docs/**

算法步骤:

  1. 调用 request.getSession(false) 获取 Session(不自动创建)
  2. 若 session == null → 抛出 UnauthorizedException("未登录")
  3. 从 session 获取 UserVo user = (UserVo) session.getAttribute("user")
  4. 若 user == null → 抛出 UnauthorizedException("未登录或Session已过期")
  5. 将 userId 和 role 设置到 request attribute,供 Controller 通过 request.getAttribute("userId") 获取

    sequenceDiagram
    participant Client as 客户端
    participant Interceptor as AuthInterceptor
    participant Session as HttpSession
    participant Controller as Controller
    
    Client->>Interceptor: HTTP 请求(携带 JSESSIONID Cookie)
    Interceptor->>Session: request.getSession(false)
    alt Session 为空
        Session-->>Interceptor: null
        Interceptor-->>Client: 401 Unauthorized
    else Session 存在
        Interceptor->>Session: getAttribute("user")
        alt user 为空
            Session-->>Interceptor: null
            Interceptor-->>Client: 401 Unauthorized
        else user 存在
            Interceptor->>Interceptor: request.setAttribute("userId", user.id)
            Interceptor->>Interceptor: request.setAttribute("role", user.role)
            Interceptor->>Controller: 放行
            Controller-->>Client: 正常响应
        end
    end
    

5.3 AdminInterceptor 拦截器

配置路径: 匹配 /admin/**(在 AuthInterceptor 之后执行,所以 request attribute 中已有 role)

算法步骤:

  1. 从 request attribute 获取 role
  2. 若 role != "admin" → 抛出 ForbiddenException("无权访问,需要管理员权限")
  3. 放行

5.4 密码加密方案

使用 MD5 + 固定盐值 加密:

SALT = "PetService2026!@#";

encrypt(password):
    1. toEncrypt = password + SALT
    2. MessageDigest.getInstance("MD5") 计算摘要
    3. 将 16 字节摘要转为 32 位小写十六进制字符串
    4. 返回密文

matches(rawPassword, encryptedPassword):
    1. 对 rawPassword 执行 encrypt
    2. 与 encryptedPassword 比较(equals)

5.5 跨域配置

registry.addMapping("/**")
    .allowedOriginPatterns("*")     // 允许所有来源(开发阶段)
    .allowCredentials(true)         // 允许携带 Cookie(必须)
    .allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
    .allowedHeaders("*");

注意:allowCredentials(true) 与 allowedOrigins("*") 不能同时使用,使用 allowedOriginPatterns("*") 替代。

6. 模块详细设计

6.1 认证模块 (AuthController)

路由映射: @RequestMapping("/auth")

Controller 定义

方法 路径 请求体 返回 校验 说明
POST /auth/register RegisterDto R<UserVo> @Valid 注册并自动登录
POST /auth/login LoginDto R<UserVo> @Valid 登录
POST /auth/logout - R<Void> - 登出(销毁 Session)

DTO / VO 定义

RegisterDto (src/dto/auth/RegisterDto.java)

字段 类型 约束 说明
account String @NotBlank @Size(min=4, max=20) 登录账号
password String @NotBlank @Size(min=4, max=20) 明文密码
username String @NotBlank @Size(min=2, max=32) 显示用户名

LoginDto (src/dto/auth/LoginDto.java)

字段 类型 约束 说明
account String @NotBlank 登录账号
password String @NotBlank 明文密码

UserVo (src/vo/user/UserVo.java)

字段 类型 说明
id Long 用户 ID
username String 用户名
account String 登录账号
email String 邮箱
phone String 联系方式
avatar String 头像 URL
role String 角色 (user/admin)
enable Integer 状态 (0禁用 1启用)

Service 算法

register(RegisterDto dto, HttpServletRequest request):

  1. 调用 userMapper.selectByAccount(dto.getAccount()) 检查账号是否已存在
  2. 若存在 → 抛出 BusinessException("账号已存在")
  3. 创建 User 对象,设置 password = passwordConfig.encrypt(dto.getPassword()), role = "user", enable = 1
  4. 调用 userMapper.insert(user) 插入数据库
  5. 调用 toUserVo(user) 转为 UserVo
  6. 调用 request.getSession().setAttribute("user", vo) 自动登录
  7. 返回 UserVo

login(LoginDto dto, HttpServletRequest request):

  1. 调用 userMapper.selectByAccount(dto.getAccount()) 查找用户
  2. 若查不到 → 抛出 BusinessException("账号或密码错误")
  3. 检查 user.getEnable() == 0 → 抛出 BusinessException("账号已被禁用")
  4. 调用 passwordConfig.matches(dto.getPassword(), user.getPassword()) 校验密码
  5. 密码不匹配 → 抛出 BusinessException("账号或密码错误")
  6. 转为 UserVo,存入 Session
  7. 返回 UserVo

时序图:登录流程

sequenceDiagram
    participant F as 前端
    participant C as AuthController
    participant S as AuthServiceImpl
    participant M as UserMapper
    participant DB as MySQL
    participant Session as HttpSession

    F->>+C: POST /auth/login { account, password }
    C->>+S: login(dto, request)
    S->>+M: selectByAccount(account)
    M->>+DB: SELECT * FROM user WHERE account=?
    DB-->>-M: User 实体
    M-->>-S: User or null

    alt 用户不存在
        S-->>F: BusinessException("账号或密码错误")
    else 用户被禁用
        S-->>F: BusinessException("账号已被禁用")
    else 密码错误
        S-->>F: BusinessException("账号或密码错误")
    else 登录成功
        S->>Session: setAttribute("user", userVo)
        Session-->>S: OK
        S-->>-C: UserVo
        C-->>-F: R<UserVo> { code:200, data: { id, username, role, ... } }
    end

6.2 用户模块 (UserController)

路由映射: @RequestMapping("/user")

Controller 定义

方法 路径 参数 返回 说明
GET /user query: pageNum, pageSize, username, account, role, enable R<PageVo<UserVo>> 分页查询用户列表
GET /user/{id} path: id R<UserVo> 按 ID 查询用户
POST /user UserDto R<Boolean> 新增用户
PUT /user UserDto R<Boolean> 修改用户
DELETE /user query: ids (逗号分隔) R<Boolean> 批量删除用户
PUT /user/updateStatus UpdateUserStatusDto R<Void> 启用/禁用用户
PUT /user/updatePassword UpdateUserPasswordDto R<Void> 修改密码
PUT /user/updateAvatar UpdateUserAvatarDto R<Void> 修改头像

DTO / VO 定义

UserDto (src/dto/user/UserDto.java)

字段 类型 必填 说明
id Long 编辑时必填 用户 ID
account String 新增时必填 登录账号
username String 是 用户名
password String 新增时必填 密码
email String 否 邮箱
phone String 否 电话
avatar String 否 头像 URL
role String 否 角色 (默认 user)
enable Integer 否 状态 (默认 1)

UpdateUserPasswordDto

字段 类型 约束 说明
id Long - 用户 ID
oldPassword String @NotBlank 旧密码
newPassword String @NotBlank 新密码

UpdateUserAvatarDto

字段 类型 约束 说明
id Long - 用户 ID
avatar String @NotBlank 头像 URL

UpdateUserStatusDto

字段 类型 约束 说明
id Long - 用户 ID
enable Integer @NotNull 0=禁用 1=启用

Service 算法

queryByPage(pageNum, pageSize, username, account, role, enable):

  1. 计算 offset = (pageNum - 1) * pageSize
  2. 调用 userMapper.selectByPage(offset, pageSize, username, account, role, enable) 查询当前页
  3. 调用 userMapper.countByCondition(username, account, role, enable) 查询总数
  4. 通过 PageUtil.page() 封装并返回

add(UserDto dto):

  1. 校验账号唯一性:userMapper.selectByAccount(dto.getAccount()) 若存在则抛异常
  2. 创建 User 对象,密码 MD5 加密
  3. userMapper.insert(user) 插入

edit(UserDto dto):

  1. 修改 User 对象(密码为空字符串时不修改密码)
  2. userMapper.update(user) 动态更新非空字段

updatePassword(UpdateUserPasswordDto dto):

  1. 查询用户:userMapper.selectById(dto.getId())
  2. 校验旧密码:passwordConfig.matches(dto.getOldPassword(), user.getPassword())
  3. 更新新密码:userMapper.updatePassword(id, encrypt(newPassword))

deleteByIds(List<Long> ids):

  1. 调用 userMapper.deleteByIds(ids) 批量物理删除

流程图:分页查询

flowchart TD
    A[前端请求 GET /api/user] --> B{接收参数}
    B --> C[pageNum=1, pageSize=10]
    B --> D[username=张三, role=user, enable=1]

    C --> E[Service 层]
    D --> E

    E --> F[计算 offset=(pageNum-1)*pageSize]
    F --> G[Mapper 层]
    G --> H[selectByPage: LIMIT offset, pageSize + WHERE 条件]
    G --> I[countByCondition: SELECT COUNT(*) + WHERE 条件]

    H --> J[List< User > 当前页数据]
    I --> K[long 总记录数]

    J --> L[PageUtil.page 封装]
    K --> L

    L --> M[PageVo<UserVo>]
    M --> N[返回前端: { data: [...], total: 50, pageNum: 1, pageSize: 10 }]

6.3 宠物模块 (PetController)

路由映射: @RequestMapping("/pets")

Controller 定义

方法 路径 参数 返回 说明
GET /pets - R<List<PetVo>> 获取当前用户宠物列表
POST /pets PetDto R<PetVo> 新增宠物
GET /pets/{id} path: id R<PetVo> 获取宠物详情
PUT /pets/{id} path: id + PetDto R<Boolean> 修改宠物信息
DELETE /pets/{id} path: id R<Boolean> 删除宠物(级联删除疫苗/驱虫)
GET /pets/{id}/vaccinations path: id R<List<VaccinationVo>> 获取疫苗记录
POST /pets/{id}/vaccinations path: id + VaccinationDto R<VaccinationVo> 新增疫苗记录
GET /pets/{id}/deworming path: id R<List<DewormingVo>> 获取驱虫记录
POST /pets/{id}/deworming path: id + DewormingDto R<DewormingVo> 新增驱虫记录

所有接口从 request attribute 获取当前登录用户 ID,校验宠物归属权。

DTO / VO 定义

PetDto (src/dto/pet/PetDto.java)

字段 类型 约束 说明
name String @NotBlank 宠物名称
species String @NotBlank 物种 (DOG/CAT/OTHER)
breed String 否 品种
gender String 否 MALE/FEMALE
birthDate LocalDate 否 出生日期
weight Double 否 体重(kg)
avatar String 否 头像 URL
remark String 否 备注

VaccinationDto (src/dto/pet/VaccinationDto.java)

字段 类型 约束 说明
vaccineName String @NotBlank 疫苗名称
vaccineDate LocalDate @NotNull 接种日期
hospital String 否 接种机构
doctor String 否 接种兽医
remark String 否 备注

DewormingDto (src/dto/pet/DewormingDto.java)

字段 类型 约束 说明
dewormingName String @NotBlank 驱虫名称
dewormingDate LocalDate @NotNull 驱虫日期
hospital String 否 驱虫机构
doctor String 否 驱虫兽医
remark String 否 备注

PetVo (src/vo/pet/PetVo.java)

字段 类型 说明
id Long 主键
userId Long 所属用户 ID
name String 宠物名称
species String 物种
breed String 品种
gender String 性别
birthDate LocalDate 出生日期
weight Double 体重
avatar String 头像
remark String 备注
createdAt LocalDateTime 创建时间
vaccinations List<VaccinationVo> 疫苗记录列表(仅详情接口返回)
dewormings List<DewormingVo> 驱虫记录列表(仅详情接口返回)

VaccinationVo / DewormingVo — 对应实体各字段 + createdAt。

Service 算法

add(PetDto dto, Long userId):

  1. 创建 Pet 实体,设置 userId,拷贝 dto 属性
  2. petMapper.insert(pet) — MyBatis 通过 useGeneratedKeys 回写 id
  3. 调用 toPetVo(pet) 转换后返回

delete(Long id, Long userId):

  1. 查询宠物并校验归属权(pet.getUserId().equals(userId))
  2. 级联删除:vaccinationRecordMapper.deleteByPetId(id)
  3. 级联删除:dewormingRecordMapper.deleteByPetId(id)
  4. petMapper.deleteById(id)

checkPetOwnership(petId, userId) — 归属校验工具方法:

  1. petMapper.selectById(petId) 查询宠物
  2. 若宠物不存在或 pet.getUserId() != userId → 抛出 BusinessException("宠物不存在或无权访问")

时序图:新增宠物并添加疫苗记录

sequenceDiagram
    participant F as 前端
    participant C as PetController
    participant S as PetServiceImpl
    participant M as PetMapper
    participant VRM as VaccinationRecordMapper
    participant DB as MySQL

    F->>+C: POST /api/pets { name, species, breed, gender }
    Note over F,C: Header 携带 JSESSIONID

    C->>C: request.getAttribute("userId")
    C->>+S: add(petDto, userId)

    S->>S: 创建 Pet 实体,设置 userId
    S->>+M: insert(pet)
    M->>+DB: INSERT INTO pet ...
    DB-->>-M: auto-increment id
    M-->>-S: 1 (影响行数)

    S-->>-C: PetVo { id, name, species, ... }
    C-->>-F: R<PetVo> { code:200, data: {...} }

    Note over F: 用户添加疫苗记录

    F->>+C: POST /api/pets/{petId}/vaccinations { vaccineName, vaccineDate }
    C->>C: request.getAttribute("userId")
    C->>+S: addVaccination(petId, dto, userId)

    S->>+M: selectById(petId)
    M-->>-S: Pet
    S->>S: checkPetOwnership(petId, userId)

    S->>S: 创建 VaccinationRecord 实体
    S->>+VRM: insert(record)
    VRM->>+DB: INSERT INTO vaccination_record ...
    DB-->>-VRM: auto-increment id
    VRM-->>-S: 1

    S-->>-C: VaccinationVo { id, vaccineName, vaccineDate, ... }
    C-->>-F: R<VaccinationVo> { code:200, data: {...} }

6.4 寄养房间模块

6.4.1 用户侧 (BoardingRoomController)

路由映射: @RequestMapping("/boarding-rooms")

方法 路径 参数 返回 说明
GET /boarding-rooms - R<List<BoardingRoomVo>> 获取所有启用的房型
GET /boarding-rooms/{id}/status query: month (yyyy-MM) R<List<RoomStatusVo>> 获取指定房型某月状态

请求示例: GET /api/boarding-rooms/1/status?month=2026-07

响应示例:

{
  "code": 200,
  "message": "success",
  "data": [
    { "id": null, "roomId": 1, "date": "2026-07-01", "status": "AVAILABLE", "appointmentId": null },
    { "id": 5, "roomId": 1, "date": "2026-07-02", "status": "OCCUPIED", "appointmentId": 3 },
    { "id": null, "roomId": 1, "date": "2026-07-03", "status": "AVAILABLE", "appointmentId": null }
  ]
}

BoardingRoomVo (src/vo/room/BoardingRoomVo.java)

字段 类型 说明
id Long 房型 ID
roomName String 房型名称
roomType String 分类 (STANDARD/VIP/DELUXE)
capacity Integer 可容纳宠物数
pricePerDay BigDecimal 每晚价格
description String 房型描述

RoomStatusVo (src/vo/room/RoomStatusVo.java)

字段 类型 说明
id Long 状态记录 ID (null 表示无记录)
roomId Long 房型 ID
date LocalDate 日期
status String AVAILABLE / OCCUPIED / MAINTENANCE
appointmentId Long 关联预约 ID (null 表示无)

6.4.2 管理员侧 (AdminBoardingRoomController)

路由映射: @RequestMapping("/admin/boarding-rooms")

方法 路径 参数 返回 说明
GET /admin/boarding-rooms - R<List<BoardingRoomVo>> 获取所有房型(含已禁用)
POST /admin/boarding-rooms BoardingRoomCreateDto R<BoardingRoomVo> 新增房型
PUT /admin/boarding-rooms/{id} BoardingRoomCreateDto R<Boolean> 修改房型信息
DELETE /admin/boarding-rooms/{id} path: id R<Boolean> 删除房型(逻辑删除)
GET /admin/boarding-rooms/{id}/status query: month R<List<RoomStatusVo>> 获取房型某月状态(管理员,含 appointmentId)
PUT /admin/boarding-rooms/{roomId}/status RoomStatusDto R<Boolean> 手动修改某日房态
PUT /admin/boarding-rooms/{roomId}/status/batch BatchRoomStatusDto R<Boolean> 批量修改日期范围房态
GET /admin/boarding-rooms/today-status - R<TodayStatusVo> 今日所有房型入住概览

DTO / VO 定义

RoomStatusDto (src/dto/room/RoomStatusDto.java)

字段 类型 约束 说明
date LocalDate @NotNull 日期
status String @NotBlank AVAILABLE / OCCUPIED / MAINTENANCE

BatchRoomStatusDto(新增 DTO)

字段 类型 约束 说明
startDate LocalDate @NotNull 开始日期
endDate LocalDate @NotNull 结束日期
status String @NotBlank 目标状态

BoardingRoomCreateDto(新增 DTO)

字段 类型 约束 说明
roomName String @NotBlank 房型名称
roomType String 否 房型分类
capacity Integer 否 容纳数
pricePerDay BigDecimal @NotNull 每晚价格
description String 否 描述

TodayStatusVo(新增 VO)

字段 类型 说明
totalRooms int 总房型数
availableCount int 空闲房间数
occupiedCount int 入住房间数
maintenanceCount int 维护房间数
todayGuests List<TodayGuestVo> 今日入住宠物列表

TodayGuestVo(新增 VO)

字段 类型 说明
roomName String 房型名称
petName String 宠物名称
ownerName String 主人姓名

Service 算法

listRooms():

  1. 调用 boardingRoomMapper.selectActive() 查询 is_active = 1 的所有房型
  2. 转为 BoardingRoomVo 列表返回

getStatus(Long roomId, String month):

  1. 解析 month = "2026-07" 为 YearMonth,计算 startDate = 当月1日, endDate = 当月最后一天
  2. 调用 boardingRoomStatusMapper.selectByRoomAndDateRange(roomId, startDate, endDate) 查询已有记录
  3. 将查询结果构建为 Map<LocalDate, BoardingRoomStatus> 用于快速查找
  4. 遍历当月每一天:
    • 若 statusMap 中存在该日期 → 使用数据库中的 status
    • 若不存在 → 默认返回 AVAILABLE
  5. 返回 List<RoomStatusVo>

updateStatus(Long roomId, RoomStatusDto dto):

  1. 创建 BoardingRoomStatus 实体:roomId, date, status
  2. 调用 boardingRoomStatusMapper.insertOrUpdate(status) — 使用 ON DUPLICATE KEY UPDATE 实现 upsert
  3. 若原记录有关联 appointment_id,手动修改时自动置为 NULL(由 SQL 中的 appointment_id = #{appointmentId} 控制,传入 null 时清空)

批量修改状态(batchUpdateStatus):

  1. 校验 startDate <= endDate,差值不超过 31 天
  2. 在事务中循环每个日期,调用 insertOrUpdate 或批量 insert
  3. 对于 OCCUPIED 状态的日期,appointment_id 置为 NULL(手动修改)

时序图:管理员修改房间状态

sequenceDiagram
    participant A as 管理员前端
    participant C as AdminBoardingRoomController
    participant S as BoardingRoomServiceImpl
    participant M as BoardingRoomStatusMapper
    participant DB as MySQL

    A->>+C: PUT /api/admin/boarding-rooms/1/status
    Note over A,C: { date: "2026-07-15", status: "MAINTENANCE" }

    C->>+S: updateStatus(roomId=1, dto)

    S->>S: 创建 BoardingRoomStatus 实体
    Note over S: roomId=1, date=2026-07-15, status=MAINTENANCE, appointmentId=null

    S->>+M: insertOrUpdate(status)
    M->>+DB: INSERT INTO boarding_room_status ...
    Note over DB: ON DUPLICATE KEY UPDATE<br/>status='MAINTENANCE',<br/>appointment_id=NULL
    DB-->>-M: 1 (inserted or updated)
    M-->>-S: 1

    S-->>-C: true
    C-->>-A: R<Boolean> { code:200, data: true }

流程图:房间状态变更逻辑

flowchart TD
    Start[商家选择修改房态] --> Input{输入参数}
    Input --> RoomId[房型 ID]
    Input --> Date[日期或日期范围]
    Input --> TargetStatus[目标状态: AVAILABLE/OCCUPIED/MAINTENANCE]

    Date --> CheckDate{是否过去的日期?}
    CheckDate -->|是| RejectPast[拒绝: 不可修改历史数据]
    CheckDate -->|否| CheckConflict{目标状态是 AVAILABLE?}

    CheckConflict -->|是| CheckOrder{该日期有关联订单?}
    CheckOrder -->|是| RejectOrder[拒绝: 存在关联订单,<br/>请先在订单管理中处理]
    CheckOrder -->|否| DoUpdate

    CheckConflict -->|否| DoUpdate[执行 UPDATE]

    DoUpdate --> Upsert[INSERT ... ON DUPLICATE KEY UPDATE]
    Upsert --> Result{执行结果}
    Result -->|成功| Success[返回 true]
    Result -->|失败| Error[抛出 BusinessException]

请求/响应示例:

新增房型:

// POST /api/admin/boarding-rooms
// Request:
{ "roomName": "豪华大间", "roomType": "VIP", "capacity": 2, "pricePerDay": 200.00, "description": "独立空调、24小时监控" }

// Response:
{ "code": 200, "message": "success", "data": { "id": 5, "roomName": "豪华大间", "roomType": "VIP", "capacity": 2, "pricePerDay": 200.00, "description": "独立空调、24小时监控" } }

批量修改:

// PUT /api/admin/boarding-rooms/1/status/batch
// Request:
{ "startDate": "2026-07-10", "endDate": "2026-07-15", "status": "MAINTENANCE" }

// Response:
{ "code": 200, "message": "success", "data": true }

今日概览:

// GET /api/admin/boarding-rooms/today-status
// Response:
{
  "code": 200,
  "message": "success",
  "data": {
    "totalRooms": 5,
    "availableCount": 2,
    "occupiedCount": 3,
    "maintenanceCount": 0,
    "todayGuests": [
      { "roomName": "标准单间", "petName": "旺财", "ownerName": "张三" },
      { "roomName": "豪华套房", "petName": "咪咪", "ownerName": "李四" }
    ]
  }
}

6.5 套餐模块

6.5.1 用户侧 (PackageController)

路由映射: @RequestMapping("/packages")

方法 路径 参数 返回 说明
GET /packages query: type (BOARDING/GROOMING) R<List<PackageVo>> 获取套餐列表,可按类型筛选
GET /packages/{id} path: id R<PackageVo> 获取套餐详情

请求/响应示例:

// GET /api/packages?type=BOARDING
// Response:
{
  "code": 200,
  "message": "success",
  "data": [
    { "id": 1, "name": "标准寄养", "type": "BOARDING", "price": 100.00, "duration": 1, "description": "基础寄养服务", "createdAt": "2026-06-01T10:00:00" },
    { "id": 2, "name": "豪华寄养", "type": "BOARDING", "price": 200.00, "duration": 1, "description": "豪华寄养服务", "createdAt": "2026-06-01T10:00:00" }
  ]
}

6.5.2 管理员侧 (AdminPackageController)

路由映射: @RequestMapping("/admin/packages")

方法 路径 参数 返回 说明
POST /admin/packages PackageDto R<PackageVo> 新增套餐
PUT /admin/packages/{id} PackageDto R<Boolean> 修改套餐
DELETE /admin/packages/{id} path: id R<Boolean> 删除套餐(逻辑删除)

DTO / VO 定义

PackageDto (src/dto/servicePackage/PackageDto.java)

字段 类型 约束 说明
name String @NotBlank 套餐名称
type String @NotBlank BOARDING / GROOMING
price BigDecimal @NotNull 价格
duration Integer 否 寄养天数(仅寄养套餐)
description String 否 描述
isActive Integer 否 是否启用 (默认 1)

PackageVo (src/vo/servicePackage/PackageVo.java)

字段 类型 说明
id Long 套餐 ID
name String 名称
type String 类型
price BigDecimal 价格
duration Integer 天数
description String 描述
createdAt LocalDateTime 创建时间

Service 算法

list(String type):

  1. 若 type 为空 → 查询所有套餐;若 type 非空 → 按 type 筛选
  2. packageMapper.selectByType(type) — SQL 中 WHERE type = #{type} AND is_active = 1
  3. 转为 PackageVo 列表返回

create(PackageDto dto):

  1. 若 isActive 为空,默认设为 1
  2. 创建 ServicePackage 实体,拷贝属性
  3. packageMapper.insert(pkg) — 返回自动生成的 id
  4. 转 PackageVo 返回

delete(Long id):

  1. 调用 packageMapper.deleteById(id) — SQL: UPDATE package SET is_active = 0 WHERE id = #{id}
  2. 逻辑删除,不物理删除

6.6 预约订单模块(最复杂模块)

6.6.1 用户侧 (AppointmentController)

路由映射: @RequestMapping("/appointments")

方法 路径 参数 返回 说明
POST /appointments AppointmentDto R<AppointmentVo> 提交预约
GET /appointments - R<List<AppointmentVo>> 获取我的预约列表
GET /appointments/{id} path: id R<AppointmentVo> 获取预约详情
POST /appointments/{id}/cancel body: { reason } R<Boolean> 申请取消
POST /appointments/{id}/pay-deposit - R<Boolean> 模拟支付定金

6.6.2 管理员侧 (AdminAppointmentController)

路由映射: @RequestMapping("/admin/appointments")

方法 路径 参数 返回 说明
GET /admin/appointments query: pageNum, pageSize, status, type R<PageVo<AppointmentVo>> 管理员分页查询全部预约
PUT /admin/appointments/{id}/review ReviewDto R<Boolean> 审核预约(通过/拒绝)
PUT /admin/appointments/{id}/handle-cancel HandleCancelDto R<Boolean> 处理取消申请

DTO / VO 定义

AppointmentDto (src/dto/appointment/AppointmentDto.java)

字段 类型 约束 说明
petId Long @NotNull 宠物 ID
packageId Long @NotNull 套餐 ID
roomId Long 寄养必填 寄养房型 ID
startDate LocalDate 寄养必填 开始日期
endDate LocalDate 寄养必填 结束日期
remark String 否 备注

注意:type 和 totalPrice 不由前端传入,由服务端根据套餐信息自动推导和计算。

ReviewDto (src/dto/appointment/ReviewDto.java)

字段 类型 约束 说明
action String @NotBlank APPROVE / REJECT
rejectReason String 拒绝时建议填写 拒绝原因

HandleCancelDto (src/dto/appointment/HandleCancelDto.java)

字段 类型 约束 说明
action String @NotBlank AGREE / REJECT

AppointmentVo (src/vo/appointment/AppointmentVo.java)

字段 类型 说明
id Long 预约 ID
userId Long 用户 ID
petId Long 宠物 ID
petName String 宠物名称(关联查询)
packageId Long 套餐 ID
packageName String 套餐名称(关联查询)
roomId Long 房型 ID
roomName String 房型名称(关联查询)
type String BOARDING / GROOMING
status String PENDING / CONFIRMED / CANCELLED / CANCEL_REQUESTED / COMPLETED
startDate LocalDate 开始日期
endDate LocalDate 结束日期
totalPrice BigDecimal 总价
deposit BigDecimal 定金金额
depositPaid Boolean 是否支付定金
cancelReason String 取消原因
rejectReason String 拒绝原因
remark String 备注
createdAt LocalDateTime 创建时间

Service 算法

a) create(AppointmentDto dto, Long userId) — 创建预约(@Transactional)

1. 套餐校验:
   package = packageMapper.selectById(dto.getPackageId())
   若 package == null || package.isActive == 0 → 抛异常 "套餐不存在或已下架"

2. 宠物校验:
   pet = petMapper.selectById(dto.getPetId())
   若 pet == null || pet.userId != userId → 抛异常 "宠物不存在或无权访问"

3. 创建 Appointment 实体:
   appointment.userId = userId
   appointment.petId = dto.petId
   appointment.packageId = dto.packageId
   appointment.type = package.type
   appointment.status = "PENDING"
   appointment.depositPaid = false
   appointment.remark = dto.remark

4. 若 type == BOARDING(寄养):
   a) 校验 roomId, startDate, endDate 不为空
   b) room = boardingRoomMapper.selectById(dto.roomId),校验 room != null && room.isActive == 1
   c) 设置 appointment.roomId, startDate, endDate
   d) days = ChronoUnit.DAYS.between(startDate, endDate) — 计算天数
   e) 若 days <= 0 → 抛异常 "结束日期必须晚于开始日期"
   f) 房间可用性校验:
      statusList = boardingRoomStatusMapper.selectByRoomAndDateRange(roomId, startDate, endDate-1)
      遍历 statusList,若有任意 status != "AVAILABLE" → 抛异常 "所选日期范围内房型不可用"
   g) totalPrice = room.pricePerDay * days(服务端计算,防止篡改)

5. 若 type == GROOMING(洗护):
   appointment.startDate = dto.startDate
   appointment.totalPrice = package.price

6. deposit = totalPrice * 0.3(定金 = 总价 × 30%)
   appointment.deposit = deposit
   appointment.totalPrice = totalPrice

7. appointmentMapper.insert(appointment) — INSERT 并回写 id

8. 返回 toAppointmentVo(appointment)

b) list(Long userId):

  1. appointmentMapper.selectByUserId(userId) — 按创建时间倒序
  2. 转为 AppointmentVo 列表返回

c) getById(Long id, Long userId):

  1. 查询并校验归属权
  2. 转为 AppointmentVo 返回

d) cancel(Long id, String reason, Long userId) — 取消订单:

1. 查询并校验归属权
2. 校验状态:仅 "PENDING" 和 "CONFIRMED" 允许取消
3. 若 status == "PENDING":
   - 直接取消:appointmentMapper.updateCancelReason(id, reason)
   - SQL:UPDATE appointment SET cancel_reason=?, status='CANCELLED' WHERE id=?
   - 若有 boarding_room_status 关联 → 释放房间(releaseRoomStatus)
4. 若 status == "CONFIRMED":
   - 进入取消待审:appointmentMapper.updateCancelReason(id, reason)
   - SQL:UPDATE appointment SET cancel_reason=?, status='CANCEL_REQUESTED' WHERE id=?
   - 等待商家审核处理

e) payDeposit(Long id, Long userId) — 支付定金:

1. 查询并校验归属权
2. 校验:depositPaid 必须为 false(防止重复支付)
3. 校验:status 必须为 "PENDING"
4. appointmentMapper.updateDepositPaid(id, true)
5. 返回 true

f) listAll(pageNum, pageSize, status, type) — 管理员查询:

1. 计算 offset = (pageNum - 1) * pageSize
2. appointmentMapper.selectAllByPage(offset, pageSize, status, type) — 动态 WHERE
3. appointmentMapper.countAll(status, type) — 总数
4. PageUtil.page() 封装返回

g) review(Long id, ReviewDto dto) — 审核预约(@Transactional):

1. 查询订单,校验 status == "PENDING"
2. 若 action == "APPROVE":
   a) appointmentMapper.updateStatus(id, "CONFIRMED")
   b) 若 type == BOARDING && roomId != null && 日期齐全:
      - 生成 [startDate, endDate) 之间的所有日期列表
      - 为每个日期创建 BoardingRoomStatus:
        roomId, date, status="OCCUPIED", appointmentId=id
      - boardingRoomStatusMapper.insertBatch(statusList)
      - 此处使用普通 INSERT(非 ON DUPLICATE),因为审核时已确认房间可用
   c) 返回 true

3. 若 action == "REJECT":
   a) 设置 appointment.status = "CANCELLED"
   b) 设置 appointment.rejectReason = dto.rejectReason
   c) appointmentMapper.update(appointment) — 动态更新
   d) 不修改 boarding_room_status(从未写入过)
   e) 返回 true

h) handleCancel(Long id, HandleCancelDto dto) — 处理取消申请(@Transactional):

1. 查询订单,校验 status == "CANCEL_REQUESTED"
2. 若 action == "AGREE":
   a) appointmentMapper.updateStatus(id, "CANCELLED")
   b) 若 type == BOARDING:
      - boardingRoomStatusMapper.deleteByAppointmentId(id)
      - SQL: DELETE FROM boarding_room_status WHERE appointment_id = #{id}
      - 释放所有关联的房间状态记录
   c) 返回 true

3. 若 action == "REJECT":
   a) appointmentMapper.updateStatus(id, "CONFIRMED") — 恢复原状态
   b) 不修改房间状态
   c) 返回 true

注意:现有实现采用物理删除而非 UPDATE status='AVAILABLE' + appointment_id=NULL,因为审核通过时已确认无冲突,取消时直接删除更简洁。

状态机:预约订单状态变迁

stateDiagram-v2
    [*] --> PENDING: 创建预约

    PENDING --> CONFIRMED: 管理员审核通过
    PENDING --> CANCELLED: 用户直接取消
    PENDING --> CANCELLED: 管理员拒绝审核

    CONFIRMED --> CANCEL_REQUESTED: 用户申请取消
    CONFIRMED --> COMPLETED: 服务完成

    CANCEL_REQUESTED --> CANCELLED: 商家同意取消
    CANCEL_REQUESTED --> CONFIRMED: 商家拒绝取消

    CANCELLED --> [*]: 终态
    COMPLETED --> [*]: 终态

状态转换规则:

当前状态 允许操作 目标状态 说明
PENDING 用户取消 CANCELLED 直接取消,房间未锁
PENDING 支付定金 PENDING depositPaid=true
PENDING 管理员审核通过 CONFIRMED 锁定房间
PENDING 管理员拒绝 CANCELLED 写入 rejectReason
CONFIRMED 用户申请取消 CANCEL_REQUESTED 等待商家处理
CONFIRMED 服务完成 COMPLETED 后台操作
CANCEL_REQUESTED 商家同意 CANCELLED 释放房间,退定金
CANCEL_REQUESTED 商家拒绝 CONFIRMED 恢复原状

时序图:寄养预约创建流程

sequenceDiagram
    participant U as 用户
    participant F as 前端
    participant C as AppointmentController
    participant S as AppointmentServiceImpl
    participant M as Mappers
    participant DB as MySQL

    U->>F: 选择寄养套餐/房型/日期/宠物
    F->>F: 前端连续性校验

    F->>+C: POST /api/appointments
    Note over F,C: { petId:2, packageId:1, roomId:3,<br/>startDate:2026-07-01, endDate:2026-07-04 }

    C->>+S: create(dto, userId)

    S->>+M: packageMapper.selectById(1)
    M->>DB: SELECT * FROM package WHERE id=1
    DB-->>M: ServicePackage { type:BOARDING, price:100, isActive:1 }
    M-->>-S: Package exists

    S->>+M: petMapper.selectById(2)
    M->>DB: SELECT * FROM pet WHERE id=2
    DB-->>M: Pet { userId: userId, name:旺财 }
    M-->>-S: Pet exists & owned

    S->>+M: boardingRoomMapper.selectById(3)
    M->>DB: SELECT * FROM boarding_room WHERE id=3
    DB-->>M: BoardingRoom { pricePerDay:100, isActive:1 }
    M-->>-S: Room exists & active

    S->>+M: boardingRoomStatusMapper.selectByRoomAndDateRange(3, 2026-07-01, 2026-07-03)
    M->>DB: SELECT * FROM boarding_room_status<br/>WHERE room_id=3 AND date BETWEEN '2026-07-01' AND '2026-07-03'
    DB-->>M: [status=AVAILABLE, status=AVAILABLE, ...]
    M-->>-S: All AVAILABLE → 可用

    S->>S: days = 3, totalPrice = 100*3 = 300, deposit = 300*0.3 = 90

    S->>+M: appointmentMapper.insert(appointment)
    M->>DB: INSERT INTO appointment ...
    DB-->>M: id generated
    M-->>-S: 1

    S-->>-C: AppointmentVo { id:10, status:PENDING, deposit:90, ... }
    C-->>-F: R<AppointmentVo> { code:200, data: {...} }

    F-->>U: 显示订单信息,引导支付

时序图:取消 + 处理取消流程

sequenceDiagram
    participant U as 用户
    participant C as AppointmentController
    participant AC as AdminAppointmentController
    participant S as AppointmentServiceImpl
    participant DB as MySQL

    %% PENDING 状态取消
    U->>C: POST /appointments/10/cancel { reason:"行程变更" }
    C->>S: cancel(10, "行程变更", userId)
    S->>DB: SELECT * FROM appointment WHERE id=10
    S->>S: status == "PENDING" → 直接取消
    S->>DB: UPDATE appointment SET status='CANCELLED', cancel_reason='行程变更' WHERE id=10
    S-->>C: true
    C-->>U: { code:200, data: true }

    %% CONFIRMED 状态申请取消
    U->>C: POST /appointments/11/cancel { reason:"宠物生病" }
    C->>S: cancel(11, "宠物生病", userId)
    S->>DB: SELECT * FROM appointment WHERE id=11
    S->>S: status == "CONFIRMED" → 转为等待审核
    S->>DB: UPDATE appointment SET status='CANCEL_REQUESTED', cancel_reason='宠物生病' WHERE id=11
    S-->>C: true
    C-->>U: { code:200, data: true }

    %% 商家处理取消
    商家->>AC: PUT /admin/appointments/11/handle-cancel { action:"AGREE" }
    AC->>S: handleCancel(11, dto)
    S->>DB: SELECT * FROM appointment WHERE id=11
    S->>S: status == "CANCEL_REQUESTED" → 处理
    S->>DB: UPDATE appointment SET status='CANCELLED' WHERE id=11
    S->>DB: DELETE FROM boarding_room_status WHERE appointment_id=11
    S-->>AC: true
    AC-->>商家: { code:200, data: true }

请求/响应示例:

创建预约:

// POST /api/appointments
// Request (寄养):
{ "petId": 2, "packageId": 1, "roomId": 3, "startDate": "2026-07-01", "endDate": "2026-07-04", "remark": "猫咪胆小,请多关照" }

// Request (洗护):
{ "petId": 3, "packageId": 4, "startDate": "2026-07-05", "remark": "下午2点到店" }

// Response:
{ "code": 200, "message": "success", "data": { "id": 10, "userId": 1, "petId": 2, "petName": "旺财", "packageId": 1, "packageName": "标准寄养", "roomId": 3, "roomName": "标准单间", "type": "BOARDING", "status": "PENDING", "startDate": "2026-07-01", "endDate": "2026-07-04", "totalPrice": 300.00, "deposit": 90.00, "depositPaid": false, "createdAt": "2026-07-01T10:30:00" } }

支付定金:

// POST /api/appointments/10/pay-deposit
// Response:
{ "code": 200, "message": "success", "data": true }

审核预约:

// PUT /api/admin/appointments/10/review
// Request (approved):
{ "action": "APPROVE" }

// Request (rejected):
{ "action": "REJECT", "rejectReason": "该日期房间已被其他订单占用" }

// Response:
{ "code": 200, "message": "success", "data": true }

用户取消:

// POST /api/appointments/10/cancel
// Request:
{ "reason": "行程变更" }

// Response:
{ "code": 200, "message": "success", "data": true }

处理取消申请:

// PUT /api/admin/appointments/10/handle-cancel
// Request (agree):
{ "action": "AGREE" }

// Request (reject):
{ "action": "REJECT" }

// Response:
{ "code": 200, "message": "success", "data": true }

6.7 排班模块 (AdminScheduleController)

路由映射: @RequestMapping("/admin/schedules")

方法 路径 参数 返回 说明
GET /admin/schedules query: month (yyyy-MM) R<List<ScheduleVo>> 按月获取排班列表
POST /admin/schedules ScheduleDto R<ScheduleVo> 新增排班
DELETE /admin/schedules/{id} path: id R<Boolean> 删除排班

DTO / VO 定义

ScheduleDto (src/dto/schedule/ScheduleDto.java)

字段 类型 约束 说明
employeeName String @NotBlank 员工姓名
workDate LocalDate @NotNull 排班日期
timeSlot String @NotBlank MORNING / AFTERNOON / FULL_DAY
remark String 否 备注

ScheduleVo (src/vo/schedule/ScheduleVo.java)

字段 类型 说明
id Long 排班 ID
employeeName String 员工姓名
workDate LocalDate 排班日期
timeSlot String 时段
remark String 备注
createdAt LocalDateTime 创建时间

Service 算法

list(String month):

  1. 若 month 非空 → 解析为 YearMonth,计算 startDate(当月1日)和 endDate(当月最后一天)
  2. 若 month 为空 → 查询全部(使用 LocalDate.MIN / MAX 作为日期范围)
  3. scheduleMapper.selectByMonth(startDate, endDate) — SQL: WHERE work_date BETWEEN ? AND ?
  4. 转为 ScheduleVo 列表返回

create(ScheduleDto dto):

  1. 冲突检测:scheduleMapper.countConflict(dto.getEmployeeName(), dto.getWorkDate(), dto.getTimeSlot())
  2. 若 count > 0 → 抛异常 "该员工在此时间段已有排班"
  3. 创建 Schedule 实体,插入数据库
  4. 返回 ScheduleVo

delete(Long id):

  1. scheduleMapper.deleteById(id) — 物理删除

请求/响应示例

// GET /api/admin/schedules?month=2026-07
// Response:
{ "code": 200, "message": "success", "data": [
  { "id": 1, "employeeName": "张设计师", "workDate": "2026-07-01", "timeSlot": "MORNING", "remark": null, "createdAt": "2026-06-28T10:00:00" },
  { "id": 2, "employeeName": "李美容师", "workDate": "2026-07-01", "timeSlot": "AFTERNOON", "remark": null, "createdAt": "2026-06-28T10:00:00" }
] }

// POST /api/admin/schedules
// Request:
{ "employeeName": "张设计师", "workDate": "2026-07-05", "timeSlot": "FULL_DAY" }
// Response:
{ "code": 200, "message": "success", "data": { "id": 3, "employeeName": "张设计师", "workDate": "2026-07-05", "timeSlot": "FULL_DAY", "createdAt": "2026-07-01T12:00:00" } }

6.8 管理员用户管理 (AdminUserController)

路由映射: @RequestMapping("/admin/users")

方法 路径 参数 返回 说明
GET /admin/users query: pageNum, pageSize, username, account, role, enable R<PageVo<UserVo>> 分页查询注册用户列表
GET /admin/users/{id} path: id R<AdminUserDetailVo> 获取用户详情及宠物档案

VO 定义

AdminUserDetailVo (src/vo/user/AdminUserDetailVo.java)

字段 类型 说明
user UserVo 用户基本信息
pets List<PetVo> 该用户的所有宠物(含疫苗/驱虫记录)

Service 算法

getAdminDetail(Long id):

  1. 查询用户:userMapper.selectById(id),不存在则抛异常
  2. 查询宠物列表:petMapper.selectByUserId(id)
  3. 为每个宠物查询疫苗记录和驱虫记录:
    • vaccinationRecordMapper.selectByPetId(pet.getId()) → List<VaccinationRecord>
    • dewormingRecordMapper.selectByPetId(pet.getId()) → List<DewormingRecord>
  4. 组装 AdminUserDetailVo 返回

queryByPage — 复用 UserServiceImpl.queryByPage 方法,参数同 UserController。

7. 配置文件 (application.yaml)

spring:
  application:
    name: PetServiceManagementSystem
  datasource:
    url: jdbc:mysql://localhost:3306/pet_service_managemen_system?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: space.anyi.petServiceManagementSystem.server.entity
  configuration:
    map-underscore-to-camel-case: true

springdoc:
  api-docs:
    path: /v3/api-docs
  swagger-ui:
    path: /swagger-ui.html

server:
  port: 8080
  servlet:
    context-path: /
    session:
      timeout: 1800                  # Session 超时时间(秒)= 30分钟
      cookie:
        http-only: true              # 禁止客户端 JS 读取 Cookie
        same-site: lax               # 防止 CSRF 攻击

配置要点说明:

配置项 说明
map-underscore-to-camel-case: true 数据库下划线命名自动映射为 Java 驼峰命名
type-aliases-package MyBatis XML 中可直接使用类名(如 resultType="User")
session.timeout=1800 HttpSession 30 分钟无操作自动过期
http-only: true 防止 XSS 攻击窃取 Session Cookie
same-site: lax 限制第三方网站跨站请求携带 Cookie

8. 模块协作规范

8.1 分层职责

Controller 层 (@RestController)
  职责:接收 HTTP 请求、参数校验(@Valid)、调用 Service、封装 R 响应
  规则:不写业务逻辑,只做参数校验和结果封装

Service 层 (@Service)
  职责:业务逻辑编排、事务管理(@Transactional)、校验规则、调用 Mapper
  规则:所有业务异常抛出 BusinessException

Mapper 层 (@Mapper + XML)
  职责:SQL 语句执行、ORM 映射
  规则:复杂查询在 XML 中编写,简单查询使用注解

Entity 层
  职责:数据库表映射 POJO
  规则:字段与表列一一对应,不包含业务逻辑

DTO / VO
  DTO:请求参数封装(可含 @Valid 注解)
  VO:响应数据封装(不暴露实体敏感字段)

8.2 事务管理

场景 事务注解 说明
注册 @Transactional(rollbackFor = Exception.class) 账号唯一性校验 + 插入为用户原子操作
创建预约 @Transactional(rollbackFor = Exception.class) 套餐校验 + 宠物校验 + 房间校验 + 插入订单
支付定金 @Transactional(rollbackFor = Exception.class) 校验 + 更新支付状态
审核预约 @Transactional(rollbackFor = Exception.class) 更新订单 + 批量生成房态
取消订单 @Transactional(rollbackFor = Exception.class) 更新订单 + 释放房态
手动改房态 @Transactional(rollbackFor = Exception.class) UPSERT 操作

8.3 API 规范

规范 说明
统一前缀 所有接口以 /api 为前缀(通过 nginx 或 context-path 配置)
统一响应 所有接口返回 R<T> 格式
分页参数 pageNum 从 1 开始,pageSize 默认 10
日期格式 请求 yyyy-MM-dd,响应 yyyy-MM-dd / yyyy-MM-dd'T'HH:mm:ss
认证方式 HttpSession (Cookie-based),前端需设置 withCredentials: true
权限控制 普通接口需登录(AuthInterceptor),/admin/** 接口需管理员(AdminInterceptor)
参数校验 使用 @Valid + @NotBlank/@NotNull/@Size 等 Jakarta Validation 注解

9. 测试策略

单元测试

测试目标 框架 覆盖内容
Service 实现类 JUnit 5 + Mockito 核心业务逻辑(创建预约校验、状态转换、房间可用性判断)
工具类 JUnit 5 PageUtil、PasswordConfig(MD5 加密/匹配)
异常路径 JUnit 5 + Mockito BusinessException 的各个触发场景

集成测试

测试目标 框架 覆盖内容
Controller 接口 Spring Boot Test + MockMvc 所有 API 的请求/响应验证
Mapper 映射 MyBatis Spring Boot Test SQL 语句正确性、结果映射
数据库交互 @MybatisTest + H2 内存数据库 CRUD 操作、唯一约束、事务回滚

关键测试场景

1. 认证模块
   - 注册成功并自动登录(Session 中有 user 属性)
   - 注册已存在的账号(抛 BusinessException)
   - 登录成功(Session 有 user)
   - 登录失败(密码错误/账号禁用)
   - 未登录访问受保护接口(返回 401)

2. 预约模块(核心)
   - 创建寄养预约成功(校验通过)
   - 创建预约时房间不可用(房间状态校验)
   - 创建预约时宠物冲突(时间重叠校验)
   - 支付定金成功(depositPaid=true)
   - 重复支付(抛 BusinessException)
   - 审核通过并自动锁定房间(生成 OCCUPIED 记录)
   - 审核拒绝(房间未锁定)
   - PENDING 状态用户直接取消(状态变为 CANCELLED)
   - CONFIRMED 状态用户申请取消(状态变为 CANCEL_REQUESTED)
   - 商家同意取消(状态变为 CANCELLED + 房间释放)
   - 商家拒绝取消(状态恢复为 CONFIRMED)

3. 宠物房间管理
   - 手动修改房态(UPSERT 正确)
   - 批量修改房态(事务回滚测试)
   - 查询某月房态(无记录的天默认 AVAILABLE)
   - 新增/修改/删除房型

4. 用户管理
   - 分页查询(正确计算 offset)
   - 新增用户(MD5 加密密码)
   - 修改密码(旧密码校验)