|
@@ -0,0 +1,1767 @@
|
|
|
|
|
+# 宠物服务管理系统 — 服务端详细设计文档
|
|
|
|
|
+
|
|
|
|
|
+## 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)
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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):
|
|
|
|
|
+
|
|
|
|
|
+```sql
|
|
|
|
|
+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\>
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+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 错误
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**响应示例:**
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+// 成功
|
|
|
|
|
+{ "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
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+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
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public class PageVo<T> {
|
|
|
|
|
+ private List<T> data; // 当前页数据
|
|
|
|
|
+ private long total; // 总记录数
|
|
|
|
|
+ private int pageNum; // 当前页码(从1开始)
|
|
|
|
|
+ private int pageSize; // 每页大小
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`PageUtil` 工具类封装分页查询范式,通过 `Supplier` 解耦列表查询和总数查询:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+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")` 获取
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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 + 固定盐值** 加密:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+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 跨域配置
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+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
|
|
|
|
|
+
|
|
|
|
|
+#### 时序图:登录流程
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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)` 批量物理删除
|
|
|
|
|
+
|
|
|
|
|
+#### 流程图:分页查询
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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("宠物不存在或无权访问")`
|
|
|
|
|
+
|
|
|
|
|
+#### 时序图:新增宠物并添加疫苗记录
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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`
|
|
|
|
|
+
|
|
|
|
|
+**响应示例:**
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "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(手动修改)
|
|
|
|
|
+
|
|
|
|
|
+#### 时序图:管理员修改房间状态
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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 }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 流程图:房间状态变更逻辑
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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]
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**请求/响应示例:**
|
|
|
|
|
+
|
|
|
|
|
+**新增房型:**
|
|
|
|
|
+```json
|
|
|
|
|
+// 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小时监控" } }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**批量修改:**
|
|
|
|
|
+```json
|
|
|
|
|
+// 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 }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**今日概览:**
|
|
|
|
|
+```json
|
|
|
|
|
+// 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\> | 获取套餐详情 |
|
|
|
|
|
+
|
|
|
|
|
+**请求/响应示例:**
|
|
|
|
|
+```json
|
|
|
|
|
+// 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,因为审核通过时已确认无冲突,取消时直接删除更简洁。
|
|
|
|
|
+
|
|
|
|
|
+#### 状态机:预约订单状态变迁
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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 | 恢复原状 |
|
|
|
|
|
+
|
|
|
|
|
+#### 时序图:寄养预约创建流程
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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: 显示订单信息,引导支付
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 时序图:取消 + 处理取消流程
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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 }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**请求/响应示例:**
|
|
|
|
|
+
|
|
|
|
|
+**创建预约:**
|
|
|
|
|
+```json
|
|
|
|
|
+// 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" } }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**支付定金:**
|
|
|
|
|
+```json
|
|
|
|
|
+// POST /api/appointments/10/pay-deposit
|
|
|
|
|
+// Response:
|
|
|
|
|
+{ "code": 200, "message": "success", "data": true }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**审核预约:**
|
|
|
|
|
+```json
|
|
|
|
|
+// PUT /api/admin/appointments/10/review
|
|
|
|
|
+// Request (approved):
|
|
|
|
|
+{ "action": "APPROVE" }
|
|
|
|
|
+
|
|
|
|
|
+// Request (rejected):
|
|
|
|
|
+{ "action": "REJECT", "rejectReason": "该日期房间已被其他订单占用" }
|
|
|
|
|
+
|
|
|
|
|
+// Response:
|
|
|
|
|
+{ "code": 200, "message": "success", "data": true }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**用户取消:**
|
|
|
|
|
+```json
|
|
|
|
|
+// POST /api/appointments/10/cancel
|
|
|
|
|
+// Request:
|
|
|
|
|
+{ "reason": "行程变更" }
|
|
|
|
|
+
|
|
|
|
|
+// Response:
|
|
|
|
|
+{ "code": 200, "message": "success", "data": true }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**处理取消申请:**
|
|
|
|
|
+```json
|
|
|
|
|
+// 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)` — 物理删除
|
|
|
|
|
+
|
|
|
|
|
+#### 请求/响应示例
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+// 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)
|
|
|
|
|
+
|
|
|
|
|
+```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 加密密码)
|
|
|
|
|
+ - 修改密码(旧密码校验)
|
|
|
|
|
+```
|