Files
mp-pilates/docs/superpowers/specs/2026-04-02-pilates-booking-miniprogram-design.md
richarjiang 05337944d8 docs: add design spec for pilates booking mini-program
Initial design document covering architecture (monorepo with pnpm),
page design, data models, business flows, and API endpoints for
the private Pilates lesson booking WeChat mini-program.
2026-04-02 10:15:25 +08:00

420 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 普拉提私教约课小程序 — 设计文档
## 概述
为单人普拉提工作室开发的微信小程序约课系统。核心功能:学员在线预约私教课、购买会员卡、查看训练记录;教练管理可约时段和会员。
### 约束
- 单一工作室、单一教练,不涉及多场馆/多教练切换
- 仅私教课,不涉及小班课
- 微信小程序为唯一客户端
---
## 技术架构
### Monorepo 结构
```
mp-pilates/
├── packages/
│ ├── app/ # uni-app 微信小程序前端
│ ├── server/ # NestJS 后端
│ └── shared/ # 前后端共享类型和常量
├── package.json # 根 workspace 配置
├── pnpm-workspace.yaml
└── tsconfig.base.json
```
### 技术选型
| 层 | 选型 |
|---|---|
| 包管理 | pnpm workspace |
| 前端 | uni-app + Vue 3 + TypeScript + Pinia + uni-ui |
| 后端 | NestJS + TypeScript + Prisma ORM |
| 数据库 | PostgreSQL |
| 共享层 | `@mp-pilates/shared`(类型、常量、枚举) |
### 角色体系
| 角色 | 说明 |
|------|------|
| MEMBER | 学员(默认),约课、查看记录、购卡 |
| ADMIN | 管理员/教练,排课、管理会员、查看订单 |
管理员通过数据库标记,小程序内根据角色展示不同菜单入口。
---
## 页面设计
### 底部 Tab 导航3 个)
| Tab | 路径 | 说明 |
|-----|------|------|
| 首页 | /pages/home/index | 品牌展示 + 快捷入口 |
| 课程预约 | /pages/booking/index | 私教时段 + 预约 |
| 我的 | /pages/profile/index | 个人中心 |
### 1. 首页
**顶部**:品牌 bannerLogo + 名称 + 背景图)
**场馆信息**
- 场馆照片横滑轮播
- 地址(点击跳转地图导航)+ 电话(点击拨打)
**快捷入口卡片**(根据用户状态智能展示):
- 新用户 → 「预约体验课」醒目入口
- 已有私教卡/时间卡 → 「一键约课」跳转课程预约页
- 卡次即将用完 → 「续卡」提醒入口
- 无卡用户 → 「购买会员卡」入口
**今日/近期课程**:已预约课程卡片,快速查看。
**会员卡商城**:卡种列表(私教次卡、月卡/季卡/年卡、体验课包),点击进入购买详情。
### 2. 课程预约页
**顶部日期选择器**:横向滚动,展示未来 7 天,标注星期几。
**筛选栏**(简化版):
- 时段筛选(全部 / 上午 / 下午 / 晚上)
- 单教练,无需老师筛选
**时段列表**
- 时段卡片:时间段、剩余名额、预约按钮
- 已约满 → 灰色置灰
- 已预约 → 显示「已预约」+「取消」
**预约流程**
1. 点击「预约」→ 检查是否有有效卡 → 无卡则引导购卡
2. 有卡 → 确认预约弹窗(时段信息 + 扣卡信息)
3. 确认 → 扣减卡次 / 验证时间卡有效期 → 预约成功
**取消规则**
- 开课前 N 小时可免费取消N 在 StudioConfig 中配置,默认 2 小时)
- 超时取消仍扣卡次
### 3. 个人中心
**顶部用户卡片**:头像 + 昵称 + 手机号 + 累计训练天数 / 训练次数
**本月训练统计**:训练次数 / 训练天数 / 训练时长
**功能入口**
- 我的会员卡 → 持有卡列表、有效期、剩余次数
- 我的预约 → 预约记录(即将上课 / 历史)
- 个人信息 → 编辑昵称、手机号
### 4. 管理功能ADMIN 角色可见)
个人中心底部显示「管理中心」入口:
- **周模板设置** → 7 天网格,每天添加多个时间段,开关启用/停用
- **临时调整** → 日历视图,点某天关闭/新增时段
- **会员管理** → 所有会员列表、卡状态、预约记录
- **订单管理** → 购卡订单列表、支付状态
- **卡种管理** → 设置卡种、价格、有效期
- **工作室设置** → 名称、Logo、地址、电话、照片
### 子页面清单
| 页面 | 路径 | 说明 |
|------|------|------|
| 卡种详情/购买 | /pages/card/detail | 卡种信息 + 购买按钮 |
| 预约确认 | 弹窗组件 | 确认预约信息 |
| 我的会员卡 | /pages/profile/membership | 持有卡列表 |
| 我的预约 | /pages/profile/bookings | 预约记录 |
| 个人信息 | /pages/profile/info | 编辑个人信息 |
| 管理中心 | /pages/admin/index | 管理功能入口 |
| 周模板管理 | /pages/admin/week-template | 设置周课表 |
| 临时时段调整 | /pages/admin/slot-adjust | 按日期调整时段 |
| 会员管理 | /pages/admin/members | 会员列表与详情 |
| 订单管理 | /pages/admin/orders | 订单列表 |
| 卡种管理 | /pages/admin/card-types | 管理卡种 |
| 工作室设置 | /pages/admin/studio | 编辑工作室信息 |
---
## 数据模型
### User用户
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键 |
| openid | String | 微信 openid唯一 |
| unionid | String? | 微信 unionid |
| phone | String? | 手机号(微信授权获取) |
| nickname | String | 昵称 |
| avatarUrl | String? | 头像 |
| role | Enum(MEMBER, ADMIN) | 角色,默认 MEMBER |
| createdAt | DateTime | 注册时间 |
| updatedAt | DateTime | 更新时间 |
### CardType卡种定义
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键 |
| name | String | 卡名,如「私教 10 次卡」 |
| type | Enum(TIMES, DURATION, TRIAL) | 次卡 / 时间卡 / 体验课 |
| totalTimes | Int? | 总次数TIMES / TRIAL 用) |
| durationDays | Int | 有效天数 |
| price | Decimal | 售价(分) |
| originalPrice | Decimal? | 原价(划线价,分) |
| description | String? | 描述 |
| isActive | Boolean | 是否上架,默认 true |
| sortOrder | Int | 排序权重,默认 0 |
| createdAt | DateTime | 创建时间 |
| updatedAt | DateTime | 更新时间 |
### Membership用户持有的卡
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键 |
| userId | UUID | 关联用户 |
| cardTypeId | UUID | 关联卡种 |
| remainingTimes | Int? | 剩余次数(次卡用) |
| startDate | DateTime | 生效日期 |
| expireDate | DateTime | 到期日期 |
| status | Enum(ACTIVE, EXPIRED, USED_UP) | 状态 |
| createdAt | DateTime | 创建时间 |
| updatedAt | DateTime | 更新时间 |
### WeekTemplate周模板时段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键 |
| dayOfWeek | Int | 星期几1=周一 ~ 7=周日) |
| startTime | String | 开始时间,如 "09:00" |
| endTime | String | 结束时间,如 "10:00" |
| capacity | Int | 容量,默认 1 |
| isActive | Boolean | 是否启用,默认 true |
| createdAt | DateTime | 创建时间 |
| updatedAt | DateTime | 更新时间 |
### TimeSlot实际可约时段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键 |
| date | Date | 具体日期 |
| startTime | String | 开始时间 |
| endTime | String | 结束时间 |
| capacity | Int | 容量 |
| bookedCount | Int | 已预约数,默认 0 |
| status | Enum(OPEN, FULL, CLOSED) | 状态 |
| source | Enum(TEMPLATE, MANUAL) | 来源:模板生成 / 手动添加 |
| templateId | UUID? | 关联周模板(模板生成时) |
| createdAt | DateTime | 创建时间 |
| updatedAt | DateTime | 更新时间 |
**生成逻辑**:定时任务每天按周模板自动生成未来 7-14 天的 TimeSlot。教练可手动添加/关闭某天的特定时段。
### Booking预约记录
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键 |
| userId | UUID | 关联用户 |
| timeSlotId | UUID | 关联时段 |
| membershipId | UUID | 扣的哪张卡 |
| status | Enum(CONFIRMED, CANCELLED, COMPLETED, NO_SHOW) | 状态 |
| cancelledAt | DateTime? | 取消时间 |
| createdAt | DateTime | 预约时间 |
| updatedAt | DateTime | 更新时间 |
### Order购卡订单
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键 |
| userId | UUID | 关联用户 |
| cardTypeId | UUID | 购买的卡种 |
| orderNo | String | 订单号,唯一 |
| amount | Decimal | 支付金额(分) |
| status | Enum(PENDING, PAID, REFUNDED) | 支付状态 |
| wxTransactionId | String? | 微信支付单号 |
| paidAt | DateTime? | 支付时间 |
| createdAt | DateTime | 下单时间 |
| updatedAt | DateTime | 更新时间 |
### StudioConfig工作室配置
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键(仅一条记录) |
| name | String | 工作室名称 |
| logo | String? | Logo URL |
| bannerUrl | String? | 背景图 URL |
| address | String | 地址 |
| phone | String | 联系电话 |
| latitude | Decimal? | 纬度 |
| longitude | Decimal? | 经度 |
| cancelHoursLimit | Int | 免费取消截止小时数,默认 2 |
| photos | Json | 场馆照片 URL 列表 |
| updatedAt | DateTime | 更新时间 |
---
## 业务流程
### 用户登录
```
小程序 wx.login() 获取 code
→ 服务端用 code 换取 openid
→ 查找/创建用户 → 签发 JWT
→ 需要手机号时,前端 getPhoneNumber 按钮授权
→ 服务端解密手机号 → 更新用户信息
```
### 预约
```
学员点击「预约」
→ 检查登录(未登录引导登录)
→ 检查有效会员卡
→ 无卡 → 引导购卡
→ 有卡 → 确认弹窗(时段 + 扣卡信息)
→ 服务端校验(时段可约 + 卡有效 + 无重复预约)
→ 成功 → 扣减卡次 → 预约成功
→ 失败 → 提示原因
```
### 取消预约
```
学员点击「取消」
→ 检查距开课时间
→ >= cancelHoursLimit → 免费取消,退还卡次
→ < cancelHoursLimit → 提示超时取消不退卡次,二次确认
→ 更新 Booking 状态 → 释放 TimeSlot 名额
```
### 购卡支付
```
选择卡种 → 创建 OrderPENDING
→ 调用微信支付统一下单 → 返回支付参数
→ 前端 wx.requestPayment
→ 支付成功 → 微信回调服务端 → 验签
→ 更新 Order 为 PAID → 创建 Membership
→ 支付失败 → 保持 PENDING可重新支付
```
### 时段自动生成
```
定时任务(每天凌晨执行)
→ 读取所有 isActive 的 WeekTemplate
→ 生成未来 7-14 天的 TimeSlot跳过已存在的日期
→ 检查过期 Membership标记 EXPIRED
→ 检查已过期 TimeSlot标记 CLOSED
```
---
## API 端点
### 认证 `/auth`
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /auth/login | 微信 code 登录,返回 JWT |
| POST | /auth/phone | 绑定手机号 |
### 用户 `/user`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /user/profile | 获取个人信息 |
| PUT | /user/profile | 更新个人信息 |
| GET | /user/stats | 训练统计(本月/累计) |
### 时段 `/time-slot`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /time-slot/available | 查询可约时段(按日期范围) |
| GET | /time-slot/:id | 时段详情 |
### 预约 `/booking`
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /booking | 创建预约 |
| PUT | /booking/:id/cancel | 取消预约 |
| GET | /booking/my | 我的预约列表 |
| GET | /booking/my/upcoming | 即将上课的预约 |
### 会员卡 `/membership`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /membership/card-types | 可购买的卡种列表 |
| GET | /membership/my | 我持有的卡列表 |
### 支付 `/payment`
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /payment/create-order | 创建购卡订单 + 发起微信支付 |
| POST | /payment/wx-notify | 微信支付回调 |
| GET | /payment/orders | 我的订单列表 |
### 管理 `/admin`ADMIN 角色)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /admin/week-template | 获取周模板 |
| PUT | /admin/week-template | 更新周模板(全量替换) |
| POST | /admin/time-slot/manual | 手动添加时段 |
| PUT | /admin/time-slot/:id/close | 关闭某个时段 |
| GET | /admin/members | 会员列表 |
| GET | /admin/members/:id | 会员详情 |
| GET | /admin/bookings | 所有预约记录 |
| GET | /admin/card-types | 卡种列表 |
| POST | /admin/card-types | 创建卡种 |
| PUT | /admin/card-types/:id | 更新卡种 |
| DELETE | /admin/card-types/:id | 删除卡种 |
| POST | /admin/generate-slots | 手动触发生成时段 |
### 工作室 `/studio`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /studio/info | 获取工作室信息 |
| PUT | /admin/studio/info | 更新工作室信息 |
---
## 关键业务规则
1. **预约扣卡**:次卡 → remainingTimes - 1时间卡 → 仅检查 expireDate 有效性;体验课 → 同次卡逻辑
2. **取消退还**:距开课 >= cancelHoursLimit 小时 → 次卡退还次数;否则不退
3. **时段状态**bookedCount >= capacity → 自动标记 FULL
4. **卡过期检测**:定时任务每天检查,过期卡标记 EXPIRED次数用完标记 USED_UP
5. **重复预约**:同一用户不可重复预约同一时段
6. **金额单位**:所有金额以「分」存储,避免浮点精度问题
---
## 不做的事项YAGNI
- 多场馆切换
- 小班课
- 多教练筛选
- 积分系统
- 拼团功能
- 美团/抖音验券
- 体测记录
- 签到功能
- 代金券