From a3d58fa8f238d40e0d214df22d9cda71196b76ac Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sun, 18 Oct 2026 15:47:29 +0800 Subject: [PATCH] feat: add monthly card periods and apparatus lesson feedback --- docs/lesson-apparatus-feedback.md | 24 ++++ docs/membership-periods.md | 51 +++++++++ .../app/src/components/ApparatusNoteList.vue | 16 +++ .../src/components/BookingConfirmPopup.vue | 5 +- .../app/src/components/CoachNoteEditor.vue | 99 ++++++++++++++++ .../app/src/components/MemberProgress.vue | 37 +++--- .../app/src/pages/admin/member-arrange.vue | 7 +- .../app/src/pages/admin/member-detail.vue | 3 +- packages/app/src/pages/booking/index.vue | 9 +- packages/app/src/pages/card/detail.vue | 101 ++++++++++++----- packages/app/src/pages/profile/membership.vue | 64 +++++++++-- packages/app/src/stores/user.ts | 4 +- .../migration.sql | 2 + .../migration.sql | 1 + packages/server/prisma/schema.prisma | 2 + .../src/auth/__tests__/auth.service.spec.ts | 5 +- packages/server/src/auth/auth.service.ts | 3 +- .../booking/__tests__/booking.service.spec.ts | 50 +++++++++ .../server/src/booking/booking.service.ts | 23 ++-- .../__tests__/membership-renewal.spec.ts | 56 ++++++++- .../__tests__/membership.service.spec.ts | 56 ++++----- .../__tests__/monthly-periods.spec.ts | 106 ++++++++++++++++++ .../period-activation.controller.spec.ts | 25 +++++ .../src/membership/dto/activate-period.dto.ts | 6 + .../server/src/membership/membership-query.ts | 7 ++ .../src/membership/membership.controller.ts | 7 ++ .../src/membership/membership.service.ts | 68 +++++++++-- .../payment/__tests__/payment.service.spec.ts | 14 +++ .../__tests__/slot-generator.service.spec.ts | 11 +- .../src/time-slot/slot-generator.service.ts | 30 +++-- .../user/__tests__/lesson-feedback.spec.ts | 72 ++++++++++++ .../src/user/__tests__/user.service.spec.ts | 29 +++-- .../src/user/dto/member-progress.dto.ts | 11 +- .../src/user/member-progress.service.ts | 9 +- packages/server/src/user/user.service.ts | 31 ++--- packages/shared/src/enums.ts | 1 + packages/shared/src/index.ts | 5 + packages/shared/src/lesson-feedback.ts | 20 ++++ packages/shared/src/membership-renewal.ts | 67 ++++++++++- packages/shared/src/types/member-care.ts | 1 + 40 files changed, 968 insertions(+), 170 deletions(-) create mode 100644 docs/lesson-apparatus-feedback.md create mode 100644 docs/membership-periods.md create mode 100644 packages/app/src/components/ApparatusNoteList.vue create mode 100644 packages/app/src/components/CoachNoteEditor.vue create mode 100644 packages/server/prisma/migrations/20261018090000_scheduled_membership_periods/migration.sql create mode 100644 packages/server/prisma/migrations/20261018100000_lesson_apparatus_notes/migration.sql create mode 100644 packages/server/src/membership/__tests__/monthly-periods.spec.ts create mode 100644 packages/server/src/membership/__tests__/period-activation.controller.spec.ts create mode 100644 packages/server/src/membership/dto/activate-period.dto.ts create mode 100644 packages/server/src/membership/membership-query.ts create mode 100644 packages/server/src/user/__tests__/lesson-feedback.spec.ts create mode 100644 packages/shared/src/lesson-feedback.ts diff --git a/docs/lesson-apparatus-feedback.md b/docs/lesson-apparatus-feedback.md new file mode 100644 index 0000000..5867bdf --- /dev/null +++ b/docs/lesson-apparatus-feedback.md @@ -0,0 +1,24 @@ +# 按器械填写课后评价 + +教练从已完成课程详情的「为这次课写评语」进入,评价继续关联该预约与学员。成长档案的「寄语 → 写评价」也可以记录不关联课程的教练笔记。 + +- 保留整体评价,新增 M、R、TT、Chair、ARC、SC、LB 七项自由文本,每项最多 1000 字。 +- 所有字段均选填;整体评价或任意一项器械有非空白内容即可保存。空白器械不入库、不展示。 +- 按器械展开填写,收起保留摘要;保存失败保留当前输入,可重试。主动关闭时会提醒放弃未保存内容。 +- 继续沿用「分享给学员」开关,默认关闭。学员只可读取已共享笔记。历史笔记无需转换。 + +## 部署 + +新增可空 MySQL JSON 字段 `member_notes.apparatus_notes`,迁移为 `20261018100000_lesson_apparatus_notes`。先在目标环境执行 `pnpm --filter @mp-pilates/server exec prisma migrate deploy`,重新生成 Prisma Client 并部署后端,然后发布小程序。此命令会部署目标库中所有尚未执行的迁移,应按现有发布流程核对。 + +本次仅添加迁移文件,未对任何数据库执行迁移。 + +## 验收 + +1. 从一节已完成课程进入,仅填写 M、仅填写整体评价、填写多种器械,分别保存;检查关联课程、换行及各器械内容。 +2. 所有输入为空或只有空格时不可保存。填满 1000 字,检查计数和输入上限。 +3. 展开、收起、切换器械,检查内容摘要与填写数量;滚动至 LB,检查软键盘、输入框和底部按钮在微信 iOS / Android 的可见性。 +4. 保存时连续点击不重复提交;断网保存后内容保留,恢复网络可重试;关闭未保存表单可选择继续填写。 +5. 默认私密记录仅馆主可见,开启共享后学员端按器械分段展示;未填写的器械不显示,历史整体评价仍显示。 + +本地已验证共享包构建、前端类型检查、小程序构建、后端构建,以及完整 Jest 测试。本地浏览器组件预览用于验证展开、切换、填写计数、仅器械保存及分段展示;不替代微信真机验收。 diff --git a/docs/membership-periods.md b/docs/membership-periods.md new file mode 100644 index 0000000..0353682 --- /dev/null +++ b/docs/membership-periods.md @@ -0,0 +1,51 @@ +# 月卡周期与续费规则 + +## 已确认规则 + +- 月卡(DURATION)每次购买生成独立会员卡周期,不向旧周期累加课时,不覆盖旧卡、订单和预约关联。 +- 旧周期尚未到期时,新周期从同款卡已购周期的最晚到期时间开始。连续购买按支付确认顺序排期,每期额度独立。 +- 旧周期已结束时,新周期从支付确认时间开始。旧月卡即使账面还有 6 次,也不可继续使用,不会与新购 10 次合成 16 次。 +- 次卡(TIMES)保留现有规则:有效期内续费累加次数、延长有效期;过期余次不结转。体验卡仍为独立新卡。 +- 原有月卡沿用已经约定的有效期,不按开卡日倒推缩短,不批量改写历史余额。 + +## 状态与展示 + +新增 `SCHEDULED`(待生效)状态,数据库迁移只在原枚举末尾追加值,不改写现有记录。 + +读取会员权益时,依据服务端时间实时判断生效、到期和可用次数,不依赖凌晨任务。到期的限次月卡返回 0 次可用权益;原始账面余额留在旧记录中用于历史核对。待生效额度不会计入当前余额。 + +会员页分为「正在使用」「下期待生效」「历史记录」。续费页显示本期剩余与下期起止时间/独立额度,提前续费提示「支付后排期待生效」。页面打开期间在生效和到期边界刷新展示。 + +## 提前启用 + +接口:`POST /membership/:id/activate`,要求登录及 `{ "confirmed": true }`,仅操作登录用户自己的周期。 + +只能启用同款卡最早的待生效周期。本期必须已经用完次数;不限次月卡或仍有余次的周期不允许提前结束。 + +启用前先检查本期未完成预约;如仍有未来待确认/已确认课程,需上完或取消后再启用。新周期从启用时刻开始,保留完整已购时长,后续待生效周期连续重新衔接。已经预约到新周期或后续周期的课程必须仍在调整后的范围内,否则拒绝操作并提示调整预约,不自动取消课程。 + +购卡排期、提前启用及预约写入使用同一会员的数据库行锁串行处理。启用事务只修改周期日期与状态,不改变订单和预约所关联的周期 ID。 + +## 预约与扣次 + +月卡可提前预约已付款周期内的课程,但课程起止时间必须完全落在所选周期内。上课时间按场馆中国时区计算。学员预约、馆主代约及确认预约均验证周期,不能借用另一期额度。 + +仍由教练确认预约时扣次;未来周期被确认预约后保留待生效状态。取消后退回原周期,不转入其他新卡;已过期月卡的退回次数不会变为可用权益。 + +## 发布与兼容 + +1. 备份生产数据库和当前 server/shared 构建及 Prisma Client。 +2. 应用 `20261018090000_scheduled_membership_periods`,生成对应 Prisma Client,再发布 shared/server。 +3. 发布小程序新版本,才能显示独立周期、正确续卡预览及提前启用入口。旧版内置的续卡计算不会随服务端更新,应配套发布小程序。 + +没有新增环境变量,不需要清空或重建已有会员数据。新周期与历史记录共用原有 memberships、orders、bookings 关系,既有购买不会重新发卡。 + +验证包括月卡 6 + 新购 10 分期保存、过期与临界时刻、连续购买排期、提前启用确认与归属、已有预约保护、课程周期边界、支付确认发放新周期、次卡原逻辑,以及共享/服务端/小程序构建。 + +## 本次验证(2026-10-18) + +- 当前工作区完整服务端测试通过:34 个套件、545 项测试。 +- shared/server 构建、小程序类型检查和微信小程序生产构建通过。 +- `pnpm lint` 无法执行:现有环境未安装脚本引用的 `eslint`;未为此调整依赖或锁文件。 +- 使用真实 Vue 页面与模拟数据检查了 390px 宽度下的未过期续卡、已过期续卡、待生效周期列表、提前启用确认及成功后的日期/额度更新;未发起真实支付或修改真实会员权益。 +- 本次月卡迁移及服务端改动尚未部署生产,新版小程序尚未上传或发布。 diff --git a/packages/app/src/components/ApparatusNoteList.vue b/packages/app/src/components/ApparatusNoteList.vue new file mode 100644 index 0000000..7d7342c --- /dev/null +++ b/packages/app/src/components/ApparatusNoteList.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/app/src/components/BookingConfirmPopup.vue b/packages/app/src/components/BookingConfirmPopup.vue index a38a6cb..953a252 100644 --- a/packages/app/src/components/BookingConfirmPopup.vue +++ b/packages/app/src/components/BookingConfirmPopup.vue @@ -44,6 +44,7 @@ {{ memberships[0].cardType.name }} + 周期 {{ formatChinaDate(memberships[0].startDate) }} 至 {{ formatChinaDate(memberships[0].expireDate) }} 剩余 {{ memberships[0].remainingTimes }} 次 @@ -68,6 +69,7 @@ > {{ m.cardType.name }} + 周期 {{ formatChinaDate(m.startDate) }} 至 {{ formatChinaDate(m.expireDate) }} 剩余 {{ m.remainingTimes }} 次 @@ -92,7 +94,7 @@ {{ selectedMembership.remainingTimes === null ? `「${selectedMembership.cardType.name}」有效期内不限次` - : `确认后将从「${selectedMembership.cardType.name}」扣除 1 次课时` }} + : `教练确认后将从「${selectedMembership.cardType.name}」对应周期扣除 1 次课时` }} @@ -124,6 +126,7 @@ import { ref, computed, watch } from 'vue' import type { TimeSlotWithBookingStatus, MembershipWithCardType } from '@mp-pilates/shared' import { requestBookingCreatedSubscriptionMessage } from '../utils/wechat-subscription' +import { formatChinaDate } from '../utils/format' const props = defineProps<{ visible: boolean diff --git a/packages/app/src/components/CoachNoteEditor.vue b/packages/app/src/components/CoachNoteEditor.vue new file mode 100644 index 0000000..01d1ea2 --- /dev/null +++ b/packages/app/src/components/CoachNoteEditor.vue @@ -0,0 +1,99 @@ +