detailDesign.md 59 KB

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

1. 技术栈

技术 版本 用途
Vue 3 ^3.5.13 前端框架(Composition API + <script setup>)
Vite ^6.3.5 构建工具(HMR、代理配置)
TypeScript ~5.8.3 类型安全(strict: true, noUnusedLocals: true)
Element Plus ^2.13.6 UI 组件库(主题色 #e34889)
Pinia ^3.0.4 状态管理(Composition API 风格)
Pinia Persistedstate ^4.7.1 Store 自动持久化到 localStorage
Vue Router ^4.6.4 路由管理(嵌套路由 + 导航守卫)
Axios ^1.15.2 HTTP 请求(withCredentials: true, 响应拦截)
Orval 7.0 OpenAPI → TypeScript API 代码生成
Prettier ^3.8.3 代码格式化

2. 项目结构

ui/src/
├── main.ts                        # 入口文件(createApp, use pinia/router/element-plus)
├── App.vue                        # 根组件(仅 <router-view/>)
├── style.css                      # 全局样式(CSS 变量、主题色定义)
├── vite-env.d.ts                  # Vite 环境类型声明
│
├── type/
│   └── index.ts                   # 自定义类型定义
│       - User { id, name, avatar?, role }
│       - LoginUser { isLogin, token?, user? }
│       - Pet { id, name, breed, age, weight, gender, notes? }
│       - Package { id, type, name, price, description?, durationDays?, isActive }
│       - BoardingRoom { id, name, capacity, pricePerNight, description? }
│       - RoomStatus { date, status: 'AVAILABLE'|'OCCUPIED' }
│       - Appointment { id, userId, petId, packageId, roomId?, type, startDate?,
│           endDate?, appointmentTime?, status, deposit, depositPaid,
│           totalPrice, cancelReason?, rejectReason?, createdAt }
│       - Schedule { id, designerName, workDate, timeSlot, isAvailable }
│
├── util/
│   ├── index.ts                   # 工具函数(当前为空)
│   └── axios-instance.ts          # Axios 实例配置
│       - baseURL: '/api'(Vite 代理 /api → localhost:8080)
│       - withCredentials: true(自动携带 Cookie/JSESSIONID)
│       - 响应拦截器:校验 code !== 0/200 → reject;401 → window.location.href='/login'
│       - customAxiosInstance<T>(config): Promise<T>(供 Orval 使用)
│
├── api/                           # Orval 生成的 API 控制器(按标签拆分)
│   ├── auth-controller.ts         # getAuthController() → { login, register, logout }
│   ├── user-controller.ts         # getUserController() → { queryByPage, add, edit, delete, ... }
│   ├── pet-controller.ts          # getPetController() → { list, add, getById, update, delete, ... }
│   ├── appointment-controller.ts  # getAppointmentController() → { create, list, getById, cancel, payDeposit }
│   ├── admin-appointment-controller.ts  # getAdminAppointmentController() → { listAll, review, handleCancel }
│   ├── boarding-room-controller.ts      # getBoardingRoomController() → { listRooms, getStatus }
│   ├── admin-boarding-room-controller.ts # getAdminBoardingRoomController() → { updateStatus, ... }
│   ├── package-controller.ts            # getPackageController() → { list, getById }
│   ├── admin-package-controller.ts      # getAdminPackageController() → { create, update, delete }
│   ├── admin-schedule-controller.ts     # getAdminScheduleController() → { list, create, delete }
│   ├── admin-user-controller.ts         # getAdminUserController() → { list, getDetail }
│   ├── schedule-controller.ts           # getScheduleController()(预留)
│   └── models/                    # Orval 生成的 TypeScript 类型
│       ├── index.ts               # 统一导出
│       ├── response.ts            # Response<T> { code, message, data }
│       ├── responseUserVo.ts, responseBoolean.ts, ...
│       ├── userVo.ts, userDto.ts, loginDto.ts, registerDto.ts
│       ├── updateUserPasswordDto.ts, updateUserAvatarDto.ts, updateUserStatusDto.ts
│       ├── deleteByIdParams.ts, queryByPageParams.ts, pageVoListUserVo.ts
│       ├── appointmentDto.ts, reviewDto.ts, handleCancelDto.ts
│       └── ...
│
├── store/                         # Pinia 状态管理
│   ├── index.ts                   # 统一导出 { useLoginUserStore, useMetaStore, useAppointmentStore, usePetStore }
│   ├── user.ts                    # useLoginUserStore — 用户登录状态
│   ├── meta.ts                    # useMetaStore — 站点元数据
│   ├── appointment.ts             # useAppointmentStore — 预约流程临时状态
│   └── pet.ts                     # usePetStore — 宠物列表缓存
│
├── router/
│   └── index.ts                   # Vue Router 配置 + 导航守卫
│       - 公共路由: /login
│       - 用户侧: UserLayout + children(预约/订单/宠物/个人中心)
│       - 管理侧: AdminLayout + children(订单审核/房态/排班/套餐/用户)
│       - beforeEach 守卫: requiresAuth → 登录校验, requiresAdmin → 角色校验
│
├── layout/
│   ├── UserLayout.vue             # 用户侧布局(水平 TopNav + 内容区)
│   └── AdminLayout.vue            # 商家后台侧布局(可折叠侧边栏 + 顶栏 + 内容区)
│
├── view/
│   ├── HomeView.vue               # 首页(品牌展示,快速入口卡片)
│   ├── LoginView.vue              # 登录/注册(Tab 切换,毛玻璃卡片,渐变背景)
│   ├── AboutView.vue              # 关于页面(占位)
│   ├── UserView.vue               # 用户管理(后台,搜索/表格/CRUD/分页/状态切换)
│   │
│   ├── booking/                   # 服务预约模块
│   │   ├── BookingView.vue               # 预约首页(选择寄养/洗护)
│   │   ├── BoardingBookingView.vue       # 寄养预约(4 步 Stepper:套餐→房型日期→宠物→提交)
│   │   └── GroomingBookingView.vue       # 洗护预约(4 步 Stepper:套餐→时段→宠物→提交)
│   │
│   ├── order/                     # 订单管理模块
│   │   ├── OrderListView.vue             # 我的订单列表(Tab 筛选,状态标签)
│   │   └── OrderDetailView.vue           # 订单详情(信息展示,支付/取消按钮)
│   │
│   ├── pet/                       # 宠物档案模块
│   │   ├── PetListView.vue               # 宠物列表(卡片展示,增删查改)
│   │   ├── PetCreateView.vue             # 新增宠物表单
│   │   ├── PetDetailView.vue             # 宠物详情(基本信息+疫苗记录+驱虫记录)
│   │   └── PetEditView.vue               # 编辑宠物表单
│   │
│   ├── profile/                   # 个人中心模块
│   │   ├── ProfileView.vue               # 个人资料展示
│   │   ├── ProfileEditView.vue           # 修改个人资料
│   │   └── PasswordEditView.vue          # 修改密码
│   │
│   └── admin/                     # 商家后台模块
│       ├── AdminOrderListView.vue        # 订单审核列表(表格+筛选+审核/取消处理弹窗)
│       ├── AdminOrderDetailView.vue      # 订单审核详情
│       ├── AdminRoomCalendarView.vue     # 宠物房间管理(房型选择+月份选择+日历)
│       ├── AdminScheduleListView.vue     # 排班管理(表格+新增弹窗+删除)
│       ├── AdminPackageListView.vue      # 套餐配置列表
│       ├── AdminPackageCreateView.vue    # 新增套餐表单
│       ├── AdminPackageEditView.vue      # 编辑套餐表单
│       └── AdminUserDetailView.vue       # 用户详情(含宠物档案列表)
│
└── components/                    # 公共可复用组件
    ├── RoomCalendar.vue           # 宠物房间日历(props: roomId/month/statusList, events: date-click)
    ├── PaymentDialog.vue          # 模拟支付弹窗(props: visible/amount/orderId, events: success/close)
    └── PetSelector.vue            # 宠物选择器(props: petList/selected, events: update:selected)

3. 路由设计

3.1 路由总览

flowchart TD
    A["/login<br/>(LoginView)"] --- B[""/""]

    B --> C["UserLayout<br/>用户侧布局"]
    C --> C1["/booking<br/>服务预约"]
    C --> C2["/booking/boarding<br/>寄养预约*"]
    C --> C3["/booking/grooming<br/>洗护预约*"]
    C --> C4["/orders<br/>我的订单*"]
    C --> C5["/orders/:id<br/>订单详情*"]
    C --> C6["/pets<br/>宠物档案*"]
    C --> C7["/pets/new<br/>新增宠物*"]
    C --> C8["/pets/:id<br/>宠物详情*"]
    C --> C9["/pets/:id/edit<br/>编辑宠物*"]
    C --> C10["/profile<br/>个人中心*"]
    C --> C11["/profile/edit<br/>修改资料*"]
    C --> C12["/profile/password<br/>修改密码*"]

    B --> D["AdminLayout<br/>商家后台布局*"]
    D --> D1["/admin/orders<br/>订单审核"]
    D --> D2["/admin/orders/:id<br/>审核详情"]
    D --> D3["/admin/rooms<br/>宠物房间管理"]
    D --> D4["/admin/schedules<br/>排班管理"]
    D --> D5["/admin/packages<br/>套餐配置"]
    D --> D6["/admin/packages/new<br/>新增套餐"]
    D --> D7["/admin/packages/:id/edit<br/>编辑套餐"]
    D --> D8["/admin/users<br/>用户管理"]
    D --> D9["/admin/users/:id<br/>用户详情"]

    style D fill:#fce7f3,stroke:#db2777
    style C fill:#e8f5e9,stroke:#2e7d32
    style A fill:#fff3e0,stroke:#f57c00

* 标记的路由需要 requiresAuth: true(登录校验)

/admin 前缀下的所有路由需要 requiresAdmin: true(管理员角色校验)

3.2 路由守卫

flowchart TD
    A["Router Navigation"] --> B{"to.matched<br/>有 requiresAuth<br/>或 requiresAdmin?"}
    B -- 否 --> C["next()<br/>放行"]
    B -- 是 --> D{"loginUserStore<br/>.isLogin?"}
    D -- 否 --> E["ElMessage.warning('请先登录')"]
    E --> F["next({ path: '/login',<br/>  query: { redirect } })"]
    D -- 是 --> G{"requiresAdmin &&<br/>role !== 'admin'?"}
    G -- 是 --> H["ElMessage.error('无权访问')"]
    H --> I["next('/booking')"]
    G -- 否 --> C

守卫逻辑摘要:

  • requiresAuth 标记在需要登录的路由 meta 上(预约、订单、宠物、个人中心等)
  • requiresAdmin 标记在 /admin 布局路由的 meta 上,子路由自动继承
  • redirect 参数在登录后导航回原始目标页面
  • 登录页本身无需守卫,已登录用户可自由访问

3.3 用户侧路由表

路径 名称 组件 meta.title meta.icon requiresAuth
/login login LoginView - - ❌
/ - UserLayout (布局) - - ❌
/booking booking BookingView 服务预约 Calendar ❌
/booking/boarding boardingBooking BoardingBookingView 寄养预约 - ✅
/booking/grooming groomingBooking GroomingBookingView 洗护预约 - ✅
/orders orderList OrderListView 我的订单 Tickets ✅
/orders/:id orderDetail OrderDetailView 订单详情 - ✅
/pets petList PetListView 宠物档案 Avatar ✅
/pets/new petCreate PetCreateView 新增宠物 - ✅
/pets/:id petDetail PetDetailView 宠物详情 - ✅
/pets/:id/edit petEdit PetEditView 编辑宠物 - ✅
/profile profile ProfileView 个人中心 User ✅
/profile/edit profileEdit ProfileEditView 修改资料 - ✅
/profile/password passwordEdit PasswordEditView 修改密码 - ✅

3.4 商家后台侧路由表

路径 名称 组件 meta.title meta.icon requiresAdmin
/admin adminLayout AdminLayout (布局) - - ✅
/admin/orders adminOrderList AdminOrderListView 订单审核 List ✅
/admin/orders/:id adminOrderDetail AdminOrderDetailView 订单审核详情 - ✅
/admin/rooms adminRooms AdminRoomCalendarView 房态管理 Calendar ✅
/admin/schedules adminSchedules AdminScheduleListView 排班管理 EditPen ✅
/admin/packages adminPackageList AdminPackageListView 套餐配置 Coin ✅
/admin/packages/new adminPackageCreate AdminPackageCreateView 新增套餐 - ✅
/admin/packages/:id/edit adminPackageEdit AdminPackageEditView 编辑套餐 - ✅
/admin/users adminUsers UserView 用户管理 User ✅
/admin/users/:id adminUserDetail AdminUserDetailView 用户详情 - ✅

4. 布局组件

4.1 UserLayout

┌──────────────────────────────────────────────────────────────────┐
│ [Logo] 宠物服务管理系统   [服务预约] [我的订单] [宠物档案] [个人中心]  │ ← el-header
│                            [后台管理]         [用户名 ▼] [登录/注册] │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│                         <router-view />                          │ ← el-main
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

导航菜单渲染:

  • 从 userRoutes[0].children 中筛选出有 meta.icon 的菜单项(过滤掉无图标的详情页)
  • 当前激活项根据 route.path 高亮
  • 管理员用户额外显示"后台管理"入口,跳转 /admin

用户状态区域:

  • 未登录:显示「登录」「注册」两个按钮
  • 已登录:显示用户头像 + 用户名下拉菜单(个人信息、我的订单、宠物档案、退出登录)

退出逻辑:

const logoutHandler = async () => {
  await getAuthController().logout()  // 服务端销毁 Session
  loginUserStore.logoutUser()        // 清除 Pinia 状态
  router.push('/login')
}

4.2 AdminLayout

┌──────────────────────────────────────────────────────────────────┐
│ [Logo] 宠物服务管理系统         后台管理           [用户名 ▼]     │ ← el-header
├────────────┬─────────────────────────────────────────────────────┤
│  [展开/收起]│                                                     │
│            │                <router-view />                       │ ← el-main
│  订单审核   │                                                     │
│  房态管理   │                                                     │
│  排班管理   │                                                     │
│  套餐配置   │                                                     │
│  用户管理   │                                                     │
└────────────┴─────────────────────────────────────────────────────┘

可折叠侧边栏:

  • el-aside 宽度默认 200px,收起时 64px
  • el-radio-group 展开/收起切换(isCollapse ref)
  • 侧边栏菜单从 adminRoutes[0].children 中筛选有 meta.icon 的项

顶部栏右侧用户下拉:

  • 用户名显示 + 下拉菜单(个人信息、返回前台、退出)

5. 公共组件

5.1 RoomCalendar.vue — 宠物房间日历

用途: 在寄养预约和宠物房间管理页面中展示指定房型某月的每日状态。

┌──────────────────────────────┐
│   <   2026年7月   >          │
├──────────────────────────────┤
│ 日  一  二  三  四  五  六   │
│              1   2   3   4  │
│  5   6   7   8   9  10  11  │
│ ...                        │
└──────────────────────────────┘
  绿色=AVAILABLE  红色=OCCUPIED

Props:

prop 类型 必填 说明
roomId string ✅ 房型 ID,用于标识当前展示的房型
month string ✅ 格式 YYYY-MM,如 2026-07
statusList { date: string; status: 'AVAILABLE' \| 'OCCUPIED' }[] ✅ 当月每日状态列表

Emits:

event 参数 说明
date-click (date: string, status: 'AVAILABLE' \| 'OCCUPIED') 点击某天时触发
month-change (month: string) 月份切换时触发(TODO: 当前组件中 prevMonth/nextMonth 为占位实现)

渲染逻辑:

  • 根据 month 计算当月天数、第一天是星期几
  • 构造 6×7 的日历网格((DayInfo | null)[][])
  • 空白天数用 null 填充
  • 每一天根据 statusMap 查找状态,应用 CSS 类:available(绿色 #e8f5e9)、occupied(红色 #ffebee)

5.2 PaymentDialog.vue — 模拟支付弹窗

用途: 预约提交成功后弹出,模拟支付定金流程。

Props:

prop 类型 必填 说明
visible boolean ✅ 弹窗显隐(v-model:visible)
amount number ✅ 定金金额
orderId string ✅ 订单 ID,用于调用支付接口

Emits:

event 参数 说明
update:visible (value: boolean) 弹窗显隐变化
success 无 支付成功后触发
close 无 弹窗关闭时触发

插槽: 无

交互流程:

  1. 显示定金金额 ¥{{ amount }}
  2. 支付方式选择:微信支付 / 支付宝(el-radio-group)
  3. 点击「确认支付」→ POST /api/appointments/{orderId}/pay-deposit
  4. 成功 → ElMessage.success('支付成功') → emit('success') → 关闭弹窗
  5. 失败 → ElMessage.error 显示错误信息

5.3 PetSelector.vue — 宠物选择器

用途: 在预约流程中选择宠物。

Props:

prop 类型 必填 说明
petList Pet[] ✅ 用户的所有宠物列表
selected Pet \| null ✅ 当前选中的宠物

Emits:

event 参数 说明
update:selected (pet: Pet) 选择宠物时触发

插槽: 无

渲染逻辑:

  • 宠物列表为空 → 显示 el-empty +「新增宠物」按钮(跳转 /pets/new)
  • 有宠物 → el-card 列表,每张卡片显示宠物首字母头像、名称、品种、年龄
  • 选中卡片高亮(border-color: var(--el-color-primary))+ 勾选图标

6. 页面详细设计

6.1 登录/注册页 (LoginView)

布局:

┌──────────────────────────────────────┐
│  ← 返回主页                           │
│                                      │
│          欢  迎                       │
│     请登录或注册您的账号               │
│                                      │
│    ┌────────────┬────────────┐       │
│    │    登录     │    注册    │       │  ← el-tabs
│    └────────────┴────────────┘       │
│                                      │
│    ┌────────────────────────┐        │
│    │  👤 请输入账号          │        │
│    ├────────────────────────┤        │
│    │  🔒 请输入密码          │        │
│    ├────────────────────────┤        │
│    │     [    登录    ]      │        │  ← 渐变粉色按钮
│    └────────────────────────┘        │
└──────────────────────────────────────┘

数据流:

sequenceDiagram
    actor U as User
    participant V as LoginView
    participant S as useLoginUserStore
    participant A as auth-controller
    participant B as Backend

    U->>V: 填写账号密码
    U->>V: 点击登录
    V->>V: formRef.validate()
    alt 校验失败
        V->>U: 显示错误提示
    else 校验通过
        V->>A: authApi.login({ account, password })
        A->>B: POST /api/auth/login
        B->>B: 校验账号密码
        B->>B: 存入 HttpSession
        B-->>A: Response<UserVo>
        A-->>V: res.data
        V->>S: loginSuccess({ id, name, role })
        V->>V: router.push(redirect || '/booking')
        V->>U: ElMessage.success('登录成功')
    end

表单校验规则:

表单 字段 规则
登录 account required
登录 password required, min:4
注册 account required, min:4, max:20
注册 username required, min:2, max:32
注册 password required, min:4, max:20
注册 confirmPassword required, 自定义校验器:必须等于 password

API 调用:

  • getAuthController().login(LoginDto) → POST /api/auth/login → 服务端存入 Session → 返回 UserVo
  • getAuthController().register(RegisterDto) → POST /api/auth/register → 返回成功 → 切换到登录 tab

状态管理:

  • activeTab: ref('login') — 当前 tab
  • loginForm: reactive({ account, password })
  • registerForm: reactive({ account, username, password, confirmPassword })
  • loginLoading, registerLoading: ref(false)

6.2 首页 (HomeView)

布局:

┌──────────────────────────────────────┐
│                                      │
│    宠物服务管理系统                     │
│    专业宠物寄养与洗护服务平台            │
│                                      │
│  ┌──────────────┐ ┌──────────────┐  │
│  │  🏠 寄养服务  │ │  ✂️ 洗护服务  │  │
│  │  专业宠物寄养  │ │  专业宠物洗护  │  │
│  │  安心托管     │ │  焕然一新     │  │
│  │  [查看详情]   │ │  [查看详情]   │  │
│  └──────────────┘ └──────────────┘  │
│                                      │
│  功能介绍 / 平台优势展示               │
│                                      │
└──────────────────────────────────────┘

交互逻辑:

  • 点击「寄养服务」卡片 → router.push('/booking/boarding')
  • 点击「洗护服务」卡片 → router.push('/booking/grooming')
  • 未登录用户点击预约需登录(由路由守卫 requiresAuth 拦截)

数据依赖: 无(纯展示页面)

6.3 预约模块

6.3.1 服务预约首页 (BookingView)

┌──────────────────────────────────────┐
│        选择服务类型                     │
│                                      │
│  ┌────────────────┐ ┌────────────────┐│
│  │  寄养服务       │ │  洗护服务       ││
│  │  专业宠物寄养   │ │  专业宠物洗护   ││
│  │  安心托管       │ │  焕然一新       ││
│  │                │ │                ││
│  │ [查看详情]      │ │ [查看详情]      ││
│  └────────────────┘ └────────────────┘│
└──────────────────────────────────────┘

数据流: 无 API 调用,纯导航跳转

6.3.2 寄养预约页 (BoardingBookingView)

多步表单流程(el-steps 4 步):

sequenceDiagram
    actor U as User
    participant V as BoardingBookingView
    participant A as API Layer
    participant B as Backend

    Note over U,V: Step 1 — 选择套餐
    V->>A: getPackageController().list4({ type: 'BOARDING' })
    A->>B: GET /api/packages?type=BOARDING
    B-->>A: PackageVo[]
    A-->>V: packages[]
    U->>V: 选中一个套餐

    Note over U,V: Step 2 — 选择房型与日期
    V->>A: getBoardingRoomController().listRooms()
    A->>B: GET /api/boarding-rooms
    B-->>A: BoardingRoomVo[]
    A-->>V: rooms[]
    U->>V: 选中房型
    V->>A: getBoardingRoomController().getStatus(roomId, { month })
    A->>B: GET /api/boarding-rooms/{id}/status?month=2026-07
    B-->>A: RoomStatusVo[]
    A-->>V: roomStatusList[]
    V->>V: RoomCalendar 渲染日历
    U->>V: 选择入住/离店日期
    V->>V: calcTotal() — 计算总价(天数 × pricePerNight)

    Note over U,V: Step 3 — 选择宠物
    V->>A: getPetController().list()
    A->>B: GET /api/pets
    B-->>A: PetVo[]
    A-->>V: myPets[]
    U->>V: PetSelector 选中宠物

    Note over U,V: Step 4 — 确认提交
    V->>V: 展示订单摘要
    U->>V: 点击提交预约
    V->>A: getAppointmentController().create(AppointmentDto)
    A->>B: POST /api/appointments { packageId, roomId, petId, startDate, endDate }
    B-->>A: AppointmentVo { id, deposit, ... }
    A-->>V: createdOrderId, depositAmount
    V->>V: PaymentDialog 弹窗
    U->>V: 支付定金
    V->>A: getAppointmentController().payDeposit(orderId)
    A->>B: POST /api/appointments/{id}/pay-deposit
    B-->>A: success
    V->>V: router.push('/orders')

组件树:

flowchart TD
    V["BoardingBookingView"]
    V --> S["el-steps (4步)"]
    V --> Step1["Step1: 套餐选择"]
    Step1 --> PkgList["el-radio-group > el-card<br/>(API: GET packages)"]
    V --> Step2["Step2: 房型与日期"]
    Step2 --> RoomSel["el-select 房型列表<br/>(API: GET rooms)"]
    Step2 --> DatePicker["el-date-picker daterange"]
    Step2 --> Calendar["RoomCalendar 宠物房间日历<br/>(API: GET room status)"]
    Step2 --> Price["总价显示 ¥xxx"]
    V --> Step3["Step3: 宠物选择"]
    Step3 --> PetSel["PetSelector<br/>(API: GET pets)"]
    V --> Step4["Step4: 确认提交"]
    Step4 --> Summary["el-descriptions 订单摘要"]
    Step4 --> Btn["提交预约"]
    V --> Payment["PaymentDialog<br/>支付弹窗 (API: payDeposit)"]

价格计算逻辑:

const calcTotal = () => {
  if (dateRange.value && selectedRoom.value) {
    const days = Math.ceil(
      (dateRange.value[1].getTime() - dateRange.value[0].getTime()) / 86400000
    )
    totalPrice.value = days * selectedRoom.value.pricePerNight
  }
}
// 定金 = totalPrice × 30%

API 调用列表:

  • GET /api/packages?type=BOARDING — 获取寄养套餐
  • GET /api/boarding-rooms — 获取所有启用房型
  • GET /api/boarding-rooms/{id}/status?month=YYYY-MM — 获取房态
  • GET /api/pets — 获取用户宠物
  • POST /api/appointments — 提交预约
  • POST /api/appointments/{id}/pay-deposit — 支付定金

6.3.3 洗护预约页 (GroomingBookingView)

sequenceDiagram
    actor U as User
    participant V as GroomingBookingView
    participant A as API Layer

    Note over U,V: Step 1 — 选择套餐
    V->>A: getPackageController().list4({ type: 'GROOMING' })
    A-->>V: packages[]
    U->>V: 选中套餐

    Note over U,V: Step 2 — 选择时间
    U->>V: 选择日期 + 时段(上午/下午)

    Note over U,V: Step 3 — 选择宠物
    V->>A: getPetController().list()
    A-->>V: myPets[]
    U->>V: PetSelector 选中宠物

    Note over U,V: Step 4 — 确认提交
    V->>A: getAppointmentController().create(AppointmentDto)
    A-->>V: createdOrderId
    V->>V: PaymentDialog 弹窗
    U->>V: 支付定金

组件树:

flowchart TD
    V["GroomingBookingView"]
    V --> S["el-steps (4步)"]
    V --> Step1["Step1: 套餐选择"]
    Step1 --> PkgList["el-radio-group > el-card<br/>(API: GET packages?type=GROOMING)"]
    V --> Step2["Step2: 时段选择"]
    Step2 --> Date["el-date-picker"]
    Step2 --> Slot["el-radio-group MORNING/AFTERNOON"]
    V --> Step3["Step3: 宠物选择"]
    Step3 --> PetSel["PetSelector<br/>(API: GET pets)"]
    V --> Step4["Step4: 确认提交"]
    Step4 --> Summary["el-descriptions 订单摘要"]
    Step4 --> Btn["提交预约"]
    V --> Payment["PaymentDialog<br/>(API: payDeposit)"]

API 调用列表:

  • GET /api/packages?type=GROOMING — 获取洗护套餐
  • GET /api/pets — 获取宠物列表
  • POST /api/appointments — 提交预约(type=GROOMING)
  • POST /api/appointments/{id}/pay-deposit — 支付定金

6.4 订单模块

6.4.1 订单列表 (OrderListView)

布局:

┌──────────────────────────────────────┐
│  我的预约                              │
│  [全部] [待确认] [已确认] [已完成] [已取消]│ ← el-tabs
├──────────────────────────────────────┤
│  ┌──────────────────────────────────┐│
│  │ 订单 ID: 1       [PENDING] 待审核 ││
│  │                  [查看详情]       ││
│  └──────────────────────────────────┘│
│  ┌──────────────────────────────────┐│
│  │ 订单 ID: 2      [CONFIRMED] 已确认││
│  │                  [查看详情]       ││
│  └──────────────────────────────────┘│
└──────────────────────────────────────┘

组件树:

flowchart TD
    V["OrderListView"]
    V --> Tabs["el-tabs 状态筛选"]
    V --> Cards["el-card 列表"]
    Cards --> Card["el-card × N"]
    Card --> Tag["el-tag 状态标签"]
    Card --> Btn["查看详情 → router.push /orders/:id"]

状态标签颜色映射:

状态 el-tag type 中文
PENDING warning 待审核
CONFIRMED success 已确认
CANCELLED info 已取消
COMPLETED primary 已完成
CANCEL_REQUESTED danger 取消申请

API 调用:

  • GET /api/appointments → 返回所有用户预约列表 → 前端按 activeStatus 客户端过滤

6.4.2 订单详情 (OrderDetailView)

布局:

┌──────────────────────────────────────┐
│  ← 返回                               │
├──────────────────────────────────────┤
│  订单详情                              │
│  ┌──────────────────────────────────┐│
│  │ 订单编号: 1    状态: [PENDING]    ││
│  │ 服务类型: BOARDING                ││
│  │ 总价: ¥450                       ││
│  │ 定金: ¥135   定金支付: 未支付      ││
│  │ 创建时间: 2026-07-01 10:00       ││
│  │                                  ││
│  │   [支付定金]  [申请取消]           ││
│  └──────────────────────────────────┘│
└──────────────────────────────────────┘

条件按钮逻辑:

  • showPayDeposit: depositPaid === false && status === 'PENDING'
  • showCancel: status === 'PENDING' || status === 'CONFIRMED'

API 调用:

  • GET /api/appointments/{id} — 获取订单详情
  • POST /api/appointments/{id}/cancel — 申请取消(ElMessageBox.confirm + 输入原因)
  • POST /api/appointments/{id}/pay-deposit — 支付定金

6.5 宠物档案模块

6.5.1 宠物列表 (PetListView)

┌──────────────────────────────────────┐
│  宠物档案              [+ 新增宠物]   │
├──────────────────────────────────────┤
│  ┌──────────────────────────────────┐│
│  │ [W] 旺财 - 金毛                  ││
│  │ 2岁 | 公                         ││
│  │ [查看详情] [编辑] [删除]          ││
│  └──────────────────────────────────┘│
│  ┌──────────────────────────────────┐│
│  │  ...                             ││
│  └──────────────────────────────────┘│
└──────────────────────────────────────┘

API 调用:

  • GET /api/pets — 获取所有宠物
  • DELETE /api/pets/{id} — 删除宠物(ElMessageBox.confirm 确认)

6.5.2 宠物详情 (PetDetailView)

┌──────────────────────────────────────┐
│  ← 返回                  [编辑] [删除]│
├──────────────────────────────────────┤
│  基本信息                             │
│  名称: 旺财 | 品种: 金毛              │
│  年龄: 2岁 | 体重: 25kg              │
│  性别: 公   | 备注: 活泼好动          │
├──────────────────────────────────────┤
│  疫苗记录                 [+ 新增]   │
│  ┌──────────────────────────────────┐│
│  │ 狂犬疫苗 | 2026-01-01 | 安心医院  ││
│  └──────────────────────────────────┘│
├──────────────────────────────────────┤
│  驱虫记录                 [+ 新增]   │
│  ┌──────────────────────────────────┐│
│  │ 体外驱虫 | 2026-03-15 | 福来恩   ││
│  └──────────────────────────────────┘│
└──────────────────────────────────────┘

API 调用:

  • GET /api/pets/{id} — 获取宠物详情(含疫苗/驱虫记录)
  • DELETE /api/pets/{id} — 删除宠物
  • POST /api/pets/{id}/vaccinations — 新增疫苗记录
  • POST /api/pets/{id}/deworming — 新增驱虫记录

新增记录弹窗表单字段:

  • 疫苗:疫苗名称、接种日期、医院、医生、备注
  • 驱虫:驱虫名称、驱虫日期、医院、医生、备注

6.5.3 新增/编辑宠物 (PetCreateView / PetEditView)

┌──────────────────────────────────────┐
│  ← 返回                               │
│  新增宠物 / 编辑宠物                    │
│                                      │
│  宠物名: [__________]                 │
│  品种:   [__________]                 │
│  年龄:   [0]                         │
│  体重:   [0.0]                       │
│  性别:   ○ 公  ○ 母                  │
│  备注:   [__________]                 │
│                                      │
│         [保存]  [取消]                │
└──────────────────────────────────────┘

API 调用(PetCreateView):

  • POST /api/pets — 新增宠物

API 调用(PetEditView):

  • GET /api/pets/{id} — 加载宠物数据填充表单
  • PUT /api/pets/{id} — 更新宠物信息

6.6 个人中心 (ProfileView / ProfileEditView / PasswordEditView)

ProfileView 布局:

┌──────────────────────────────────────┐
│  个人中心                              │
│  ┌──────────────────────────────────┐│
│  │         [大头像]                  ││
│  │         用户名                    ││
│  │        [admin/user]              ││
│  ├──────────────────────────────────┤│
│  │ ID: 1                            ││
│  │ 昵称: 张三            [编辑]      ││
│  ├──────────────────────────────────┤│
│  │         [修改密码]                ││
│  └──────────────────────────────────┘│
└──────────────────────────────────────┘

ProfileEditView:

  • 表单:昵称输入
  • PUT /api/user — 更新用户信息 → 更新 Pinia store

PasswordEditView:

  • 表单:原密码、新密码、确认密码
  • PUT /api/user/updatePassword — 修改密码

6.7 商家后台

6.7.1 订单审核 (AdminOrderListView + AdminOrderDetailView)

sequenceDiagram
    actor A as Admin
    participant V as AdminOrderListView
    participant API as API Layer
    participant B as Backend

    Note over V: 页面加载
    V->>API: getAdminAppointmentController().listAll({ pageNum, pageSize, status? })
    API->>B: GET /api/admin/appointments?pageNum=1&pageSize=10
    B-->>API: PageVo<AppointmentVo>
    API-->>V: orders[]

    Note over A,V: 审核操作
    A->>V: 点击"审核"
    V->>V: 打开审核弹窗
    A->>V: 选择通过/拒绝 + 填写原因
    V->>API: review(appointmentId, { action, rejectReason? })
    API->>B: PUT /api/admin/appointments/{id}/review
    B-->>API: success
    API-->>V: ElMessage.success
    V->>V: 刷新列表

    Note over A,V: 处理取消申请
    A->>V: 点击"处理取消"
    V->>V: 打开取消处理弹窗
    A->>V: 选择同意/拒绝
    V->>API: handleCancel(appointmentId, { action })
    API->>B: PUT /api/admin/appointments/{id}/handle-cancel
    B-->>API: success
    V->>V: 刷新列表

AdminOrderListView 组件树:

flowchart TD
    V["AdminOrderListView"]
    V --> Filter["el-select 状态筛选"]
    V --> Table["el-table 订单列表"]
    Table --> Cols["ID | 用户 | 类型 | 总价 | 状态标签 | 操作"]
    Table --> Pagination["el-pagination"]
    V --> ReviewDlg["审核弹窗"]
    ReviewDlg --> Radio["通过/拒绝 radio"]
    ReviewDlg --> Reason["原因 textarea"]
    V --> CancelDlg["取消处理弹窗"]
    CancelDlg --> CancelRadio["同意/拒绝 radio"]
    CancelDlg --> Opinion["处理意见 textarea"]

AdminOrderDetailView: 与 AdminOrderListView 共享审核/取消弹窗逻辑,以详情页形式展示完整订单信息。

6.7.2 宠物房间管理 (AdminRoomCalendarView)

flowchart TD
    A["商家进入宠物房间管理"]
    A --> B["选择房型<br/>el-select 切换"]
    A --> C["选择月份<br/>el-date-picker type=month"]
    B --> D["RoomCalendar 宠物房间日历"]
    C --> D
    D --> E["点击某天"]
    E --> F{"当前状态?"}
    F -->|"AVAILABLE"| G["切换为 OCCUPIED<br/>PUT /api/admin/rooms/{id}/status"]
    F -->|"OCCUPIED"| H["切换为 AVAILABLE<br/>PUT /api/admin/rooms/{id}/status"]
    G --> I["刷新日历 + ElMessage.success"]
    H --> I

布局:

┌──────────────────────────────────────────────┐
│  宠物房间管理                                  │
│  [房型选择 ▼]     [2026年07月 ▼]              │
├──────────────────────────────────────────────┤
│  ┌──────────────────────────────────────────┐│
│  │           宠物房间日历                     ││
│  │  日   一   二   三   四   五   六          ││
│  │             1    2    3    4              ││
│  │  5    6    7    8    9   10   11          ││
│  │  12  13   14   15   16   17   18          ││
│  │  ...                                     ││
│  │  绿色=空闲  红色=占用                     ││
│  └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘

CRITICAL: 使用"宠物房间管理" / "宠物房间日历" 术语:

  • 页面标题为「宠物房间管理」
  • 日历组件使用「宠物房间日历」称谓
  • 菜单项 meta.title 为「房态管理」(但页面内统一使用"宠物房间"术语)

API 调用:

  • GET /api/boarding-rooms — 获取所有房型
  • GET /api/boarding-rooms/{id}/status?month=YYYY-MM — 获取房态
  • PUT /api/admin/boarding-rooms/{roomId}/status — 修改单日状态(切换 AVAILABLE/OCCUPIED)
  • PUT /api/admin/boarding-rooms/{roomId}/status/batch — 批量修改(预留,当前 UI 暂未实现多选)

状态切换逻辑(handleDateClick):

const handleDateClick = async (date: string, status: 'AVAILABLE' | 'OCCUPIED') => {
  const newStatus = status === 'AVAILABLE' ? 'OCCUPIED' : 'AVAILABLE'
  // PUT /api/admin/boarding-rooms/{roomId}/status { date, status: newStatus }
  // 刷新日历
}

6.7.3 排班管理 (AdminScheduleListView)

┌──────────────────────────────────────────────┐
│  排班管理                      [+ 新增]       │
│  [2026年07月 ▼]                               │
├──────────────────────────────────────────────┤
│  ┌──────────────────────────────────────────┐│
│  │ 设计师 | 日期 | 时段 | 状态 | 操作       ││
│  │ 张三   | 07-01 | 上午 | 可用 | [删除]    ││
│  │ 李四   | 07-01 | 下午 | 可用 | [删除]    ││
│  │ ...                                      ││
│  └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘

新增弹窗:

┌──────────────────────────┐
│    新增排班               │
│  设计师: [__________]     │
│  日期:   [date ▼]        │
│  时段:   [上午 ▼]        │
│  是否可用: [🟢]          │
│                          │
│    [取消]    [保存]      │
└──────────────────────────┘

API 调用:

  • GET /api/admin/schedules?month=YYYY-MM — 获取排班列表
  • POST /api/admin/schedules — 新增排班
  • DELETE /api/admin/schedules/{id} — 删除排班

6.7.4 套餐配置 (AdminPackageListView + Create + Edit)

┌──────────────────────────────────────────────┐
│  套餐配置                       [+ 新增套餐]  │
├──────────────────────────────────────────────┤
│  ┌──────────────────────────────────────────┐│
│  │ 名称 | 类型 | 价格 | 天数 | 状态 | 操作  ││
│  │ 标准寄养 | 寄养 | ¥100 | 7天 | 启用 | [编辑][删]││
│  │ 基础洗护 | 洗护 | ¥80  | -   | 启用 | [编辑][删]││
│  └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘

新增/编辑表单:

┌──────────────────────────────────┐
│  新增套餐 / 编辑套餐               │
│  套餐类型: ○ 寄养  ○ 洗护         │
│  套餐名称: [______________]       │
│  价格:     [0.00]                │
│  描述:     [______________]       │
│  寄养天数: [1]   (仅寄养显示)     │
│  是否启用: [🟢]                   │
│                                  │
│     [取消]        [保存]         │
└──────────────────────────────────┘

API 调用:

  • GET /api/packages — 获取套餐列表
  • GET /api/packages/{id} — 获取单个套餐
  • POST /api/admin/packages — 新增套餐
  • PUT /api/admin/packages/{id} — 更新套餐
  • DELETE /api/admin/packages/{id} — 删除套餐(逻辑删除)

6.7.5 用户管理 (AdminUserListView + AdminUserDetailView)

AdminUserListView 复用 UserView.vue:

  • 搜索区域:用户名、账号、角色、状态
  • el-table:ID、用户名、账号、角色标签、启用/禁用 switch、头像、操作(编辑/删除)
  • 新增/编辑弹窗:用户名、账号、密码、角色
  • 分页、批量删除、单条删除

AdminUserDetailView:

┌──────────────────────────────────────────────┐
│  ← 用户管理                                   │
├──────────────────────────────────────────────┤
│  用户信息                                     │
│  ┌──────────────────────────────────────────┐│
│  │ 用户名: 张三 | 账号: zhangsan            ││
│  │ 角色: user   | 邮箱: z@x.com             ││
│  │ 手机: 138xxxx |                           ││
│  └──────────────────────────────────────────┘│
├──────────────────────────────────────────────┤
│  该用户的宠物档案                             │
│  ┌──────────────────────────────────────────┐│
│  │ 名称 | 品种 | 性别 | 体重                ││
│  │ 旺财 | 金毛 | 公   | 25kg               ││
│  └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘

API 调用:

  • GET /api/admin/users/{id} — 获取用户详情(含用户信息 + 宠物列表)
  • UserView 使用的 GET /api/user 分页查询

7. 状态管理 (Pinia Store)

7.1 useLoginUserStore (store/user.ts)

分类 名称 类型 说明
State loginUser Reactive<LoginUser> { isLogin: false, user: { id, name, role } }
Getter userId ComputedRef<string> loginUser.user?.id
Action updateUser(user: User) void 替换 user 对象
Action loginSuccess(user: User) void 设置 isLogin=true + 更新 user
Action logoutUser() void 重置为未登录状态

持久化: persist: true — 通过 pinia-plugin-persistedstate 自动同步到 localStorage

7.2 useMetaStore (store/meta.ts)

分类 名称 类型 说明
State title Ref<string> 网站标题(默认"标题")
State logo Ref<string> Logo 路径(默认 /logo.svg)
State baseUrl Ref<string> 后端地址(默认 http://localhost:8080/)

持久化: persist: true

7.3 usePetStore (store/pet.ts)

分类 名称 类型 说明
State pets Ref<Pet[]> 宠物列表缓存
Action setPets(list: Pet[]) void 覆盖设置宠物列表
Action addPet(pet: Pet) void 追加一只宠物
Action updatePet(updated: Pet) void 按 id 更新宠物
Action removePet(id: string) void 按 id 删除宠物

7.4 useAppointmentStore (store/appointment.ts)

分类 名称 类型 说明
State currentPackage Ref<Package \| null> 当前选中套餐
State selectedPet Ref<Pet \| null> 当前选中宠物
State selectedRoom Ref<BoardingRoom \| null> 当前选中房型
State dateRange Ref<[Date, Date] \| null> 入住/离店日期范围
Action reset() void 重置所有状态为 null

注意:目前 BoardingBookingView 使用局部 ref 而非此 store 管理状态。

8. API 层设计

8.1 Axios 实例配置

// util/axios-instance.ts
const axiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',  // Vite 代理前缀
  timeout: 30000,
  withCredentials: true,        // 自动携带 JSESSIONID Cookie
  headers: { 'Content-Type': 'application/json' }
})

// 响应拦截器
axiosInstance.interceptors.response.use(
  (response) => {
    if (response.data.code !== 0 && response.data.code !== 200) {
      return Promise.reject(new Error(response.data.message || '请求失败'))
    }
    return response.data    // 解包,直接返回 { code, message, data }
  },
  (error) => {
    if (error.response?.status === 401) {
      window.location.href = '/login'  // Session 过期 → 跳转登录
    }
    return Promise.reject(error)
  }
)

// Orval 自定义实例
export const customAxiosInstance = <T>(config: CustomInstanceConfig): Promise<T> => {
  return axiosInstance(config)
}

8.2 Orval 生成的控制器

文件 工厂函数 主要方法 对应 API 端点
auth-controller.ts getAuthController() login, register, logout POST /api/auth/login, POST /api/auth/register, ...
user-controller.ts getUserController() queryByPageUsers, add, edit, deleteById, updateStatus, updatePassword, updateAvatar, queryById GET/POST/PUT/DELETE /api/user/**
pet-controller.ts getPetController() list, add1, getById, update, _delete, addVaccination, addDeworming GET/POST/PUT/DELETE /api/pets/**
appointment-controller.ts getAppointmentController() create, list1, getById2, cancel, payDeposit POST/GET /api/appointments/**
admin-appointment-controller.ts getAdminAppointmentController() listAll, review, handleCancel GET/PUT /api/admin/appointments/**
boarding-room-controller.ts getBoardingRoomController() listRooms, getStatus GET /api/boarding-rooms/**
admin-boarding-room-controller.ts getAdminBoardingRoomController() updateStatus1, batchUpdateStatus PUT /api/admin/boarding-rooms/**
package-controller.ts getPackageController() list4, getById1 GET /api/packages/**
admin-package-controller.ts getAdminPackageController() create2, update1, delete1 POST/PUT/DELETE /api/admin/packages/**
admin-schedule-controller.ts getAdminScheduleController() list2, create1, delete2 GET/POST/DELETE /api/admin/schedules/**
admin-user-controller.ts getAdminUserController() listAllUsers, getDetail GET /api/admin/users/**
schedule-controller.ts getScheduleController() (预留) -

9. 关键数据流

9.1 登录数据流

sequenceDiagram
    actor U as User
    participant V as LoginView
    participant S as useLoginUserStore
    participant A as Axios Instance
    participant B as Backend

    U->>V: 输入账号密码
    U->>V: 点击登录
    V->>V: formRef.validate()
    alt 校验失败
        V->>U: 显示表单错误
    else 校验通过
        V->>A: authApi.login({ account, password })
        A->>B: POST /api/auth/login<br/>withCredentials: true
        B->>B: MD5 校验密码
        B->>B: request.getSession().setAttribute("user", userInfo)
        B-->>A: Set-Cookie: JSESSIONID=xxx<br/>Response<UserVo>
        A-->>V: res.data (UserVo)
        V->>S: loginSuccess({ id, name, role })
        S->>S: loginUser.isLogin = true
        S->>S: persist → localStorage
        V->>V: router.push(redirect || '/booking')
        V->>U: ElMessage.success('登录成功')
    end

    Note over U,B: 后续请求
    U->>V: 访问需登录页面
    V->>A: 任意 API 请求
    A->>B: Cookie: JSESSIONID=xxx
    B->>B: session.getAttribute("user")
    B-->>A: 正常响应

9.2 预约数据流

sequenceDiagram
    actor U as User
    participant V as BoardingBookingView
    participant S as useAppointmentStore
    participant A as API Layer
    participant B as Backend

    Note over V: Step 1 — 选择套餐
    V->>A: GET /api/packages?type=BOARDING
    A->>B: 查询套餐表
    B-->>A: PackageVo[] (is_active=true)
    A-->>V: 卡片列表渲染
    U->>V: 点击选择套餐
    V->>S: currentPackage = selected

    Note over V: Step 2 — 选择房型与日期
    V->>A: GET /api/boarding-rooms
    A-->>V: BoardingRoomVo[]
    U->>V: 选择房型
    V->>A: GET /api/boarding-rooms/{id}/status?month=2026-07
    A-->>V: RoomStatusVo[]
    V->>V: 渲染 RoomCalendar 宠物房间日历
    U->>V: 选择入住/离店日期
    V->>V: 计算天数、总价
    V->>S: selectedRoom, dateRange

    Note over V: Step 3 — 选择宠物
    V->>A: GET /api/pets
    A-->>V: PetVo[]
    V->>V: 渲染 PetSelector
    U->>V: 选择宠物
    V->>S: selectedPet

    Note over V: Step 4 — 提交 + 支付
    U->>V: 点击提交预约
    V->>A: POST /api/appointments<br/>{ packageId, roomId, petId, startDate, endDate }
    A->>B: 创建预约记录 (status=PENDING)
    B-->>A: AppointmentVo { id, deposit, totalPrice }
    A-->>V: createdOrderId, depositAmount
    V->>V: 打开 PaymentDialog
    U->>V: 选择支付方式
    U->>V: 点击确认支付
    V->>A: POST /api/appointments/{id}/pay-deposit
    A->>B: 更新 depositPaid=true
    B-->>A: success
    V->>U: ElMessage.success('支付成功')
    V->>V: router.push('/orders')

9.3 订单取消数据流

sequenceDiagram
    actor U as User
    participant V as OrderDetailView
    participant A as API Layer
    participant B as Backend

    alt PENDING 状态取消
        U->>V: 点击"申请取消"
        V->>V: ElMessageBox.confirm
        U->>V: 确认 + 填写原因
        V->>A: POST /api/appointments/{id}/cancel<br/>{ cancelReason }
        A->>B: 校验状态为 PENDING
        B->>B: status → CANCELLED
        alt 已支付定金
            B->>B: depositRefunded = true
        end
        B->>B: 释放房间状态 (若有)
        B-->>A: success
        A-->>V: ElMessage.success('取消成功')
        V->>V: 刷新订单详情

    else CONFIRMED 状态申请取消
        U->>V: 点击"申请取消"
        V->>A: POST /api/appointments/{id}/cancel
        A->>B: 校验状态为 CONFIRMED
        B->>B: status → CANCEL_REQUESTED
        B-->>A: success
        A-->>V: ElMessage.success('取消申请已提交')
    end

9.4 后台审核数据流

sequenceDiagram
    actor A as Admin
    participant V as AdminOrderListView
    participant API as API Layer
    participant B as Backend

    Note over V: 页面加载
    V->>API: GET /api/admin/appointments?pageNum=1&pageSize=10
    API->>B: 分页查询所有预约
    B-->>API: PageVo<AppointmentVo>
    API-->>V: 表格渲染

    Note over A,V: 审核通过
    A->>V: 点击 PENDING 订单的"审核"
    V->>V: 打开审核弹窗
    A->>V: 选择"通过"
    V->>API: PUT /api/admin/appointments/{id}/review<br/>{ action: 'CONFIRMED' }
    API->>B: 校验订单
    B->>B: status → CONFIRMED
    B->>B: 批量插入 boarding_room_status (OCCUPIED)
    B->>B: 关联 appointment_id
    B-->>API: success
    API-->>V: ElMessage.success
    V->>V: 刷新列表

    Note over A,V: 审核拒绝
    A->>V: 选择"拒绝" + 填写原因
    V->>API: PUT /api/admin/appointments/{id}/review<br/>{ action: 'CANCELLED', rejectReason }
    API->>B: status → CANCELLED
    B->>B: 保存 rejectReason
    B-->>API: success

    Note over A,V: 处理取消申请
    A->>V: 点击 CANCEL_REQUESTED 的"处理取消"
    V->>V: 打开处理弹窗
    A->>V: 同意/拒绝
    V->>API: PUT /api/admin/appointments/{id}/handle-cancel<br/>{ action: 'CANCELLED'|'CONFIRMED' }
    API->>B: 更新 status + 释放房间
    B-->>API: success
    V->>V: 刷新列表