# 普拉提私教约课小程序 — 设计文档 ## 概述 为单人普拉提工作室开发的微信小程序约课系统。核心功能:学员在线预约私教课、购买会员卡、查看训练记录;教练管理可约时段和会员。 ### 约束 - 单一工作室、单一教练,不涉及多场馆/多教练切换 - 仅私教课,不涉及小班课 - 微信小程序为唯一客户端 --- ## 技术架构 ### 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) - 多场馆切换 - 小班课 - 多教练筛选 - 积分系统 - 拼团功能 - 美团/抖音验券 - 体测记录 - 签到功能 - 代金券