commit 05337944d87e409bbb86af03f8803f2268d2e14d Author: richarjiang Date: Thu Apr 2 10:15:25 2026 +0800 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. diff --git a/docs/superpowers/specs/2026-04-02-pilates-booking-miniprogram-design.md b/docs/superpowers/specs/2026-04-02-pilates-booking-miniprogram-design.md new file mode 100644 index 0000000..b0d0a86 --- /dev/null +++ b/docs/superpowers/specs/2026-04-02-pilates-booking-miniprogram-design.md @@ -0,0 +1,419 @@ +# 普拉提私教约课小程序 — 设计文档 + +## 概述 + +为单人普拉提工作室开发的微信小程序约课系统。核心功能:学员在线预约私教课、购买会员卡、查看训练记录;教练管理可约时段和会员。 + +### 约束 + +- 单一工作室、单一教练,不涉及多场馆/多教练切换 +- 仅私教课,不涉及小班课 +- 微信小程序为唯一客户端 + +--- + +## 技术架构 + +### 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. 首页 + +**顶部**:品牌 banner(Logo + 名称 + 背景图) + +**场馆信息**: +- 场馆照片横滑轮播 +- 地址(点击跳转地图导航)+ 电话(点击拨打) + +**快捷入口卡片**(根据用户状态智能展示): +- 新用户 → 「预约体验课」醒目入口 +- 已有私教卡/时间卡 → 「一键约课」跳转课程预约页 +- 卡次即将用完 → 「续卡」提醒入口 +- 无卡用户 → 「购买会员卡」入口 + +**今日/近期课程**:已预约课程卡片,快速查看。 + +**会员卡商城**:卡种列表(私教次卡、月卡/季卡/年卡、体验课包),点击进入购买详情。 + +### 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 名额 +``` + +### 购卡支付 + +``` +选择卡种 → 创建 Order(PENDING) + → 调用微信支付统一下单 → 返回支付参数 + → 前端 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) + +- 多场馆切换 +- 小班课 +- 多教练筛选 +- 积分系统 +- 拼团功能 +- 美团/抖音验券 +- 体测记录 +- 签到功能 +- 代金券