From 47016744d4772fd3ccf90990efb6dd6d40f8755d Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 25 Jun 2026 23:05:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(M08-A):=20=E5=A2=9E=E5=8A=A0=E6=9D=83?= =?UTF-8?q?=E7=9B=8A=E5=8F=AF=E8=A7=86=E5=8C=96=E9=80=89=E6=8B=A9=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- backend/src/orders/pricing-repository.ts | 8 +- .../2026-06-25-M08-A-recharge-wechat.md | 9 ++ docs/current-baseline.md | 6 +- docs/devlogs/2026-06-24-M08-A-顾客端.md | 23 ++++ docs/feature-status.md | 6 +- docs/module-status.md | 4 +- miniapp/pages/room/detail.js | 116 +++++++++++++++++- miniapp/pages/room/detail.wxml | 42 ++++++- miniapp/pages/room/detail.wxss | 45 +++++++ scripts/check-miniapp-m08-a.mjs | 5 + 11 files changed, 249 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ba5f10d..89d7e83 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ WSL 已验证:EMQX `5.8.9`、MQTTX CLI `1.13.0`、EMQX 服务 `active (running - 当前执行游标:`M08-A`(PARTIAL) - 最近工程提交:`dda2b3c`,完成顾客端优惠券、套餐和余额下单抵扣工程闭环。 -- 下一工程目标:继续 M08-A,补权益可视化选择器、真机合法域名、真实微信支付和实物开门验证。 +- 下一工程目标:继续 M08-A,补真机合法域名、真实微信支付和实物开门验证。 ## 版本递进 diff --git a/backend/src/orders/pricing-repository.ts b/backend/src/orders/pricing-repository.ts index 0bb091d..f2b437c 100644 --- a/backend/src/orders/pricing-repository.ts +++ b/backend/src/orders/pricing-repository.ts @@ -120,7 +120,8 @@ export class PricingRepository { quote = this.calculate(room, { ...input, adjustment: { - discountCents: benefitReservation.discountCents, + discountCents: benefitReservation.discountCents + + timeCouponDiscountCents(benefitReservation.minutes, quote.unitPriceCents), packageCreditCents: benefitReservation.packageCreditCents } }, isHoliday); @@ -373,6 +374,11 @@ function clampAdjustment(value: number | undefined, maximum: number) { return Math.min(Math.trunc(value), maximum); } +function timeCouponDiscountCents(minutes: number | undefined, unitPriceCents: number) { + if (!minutes || minutes <= 0) return 0; + return Math.ceil(minutes / 60) * unitPriceCents; +} + function localDateKey(date: Date, timezone: string) { return new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' diff --git a/docs/api-changelog/2026-06-25-M08-A-recharge-wechat.md b/docs/api-changelog/2026-06-25-M08-A-recharge-wechat.md index 3856eb3..5ac5841 100644 --- a/docs/api-changelog/2026-06-25-M08-A-recharge-wechat.md +++ b/docs/api-changelog/2026-06-25-M08-A-recharge-wechat.md @@ -24,3 +24,12 @@ - `POST /app-api/orders/:orderId/cancel` - 行为补充:订单取消释放房间预占和访问权限时,同时释放仍处于 `FROZEN` 的优惠券和套餐持有记录。 + +## M08-A 权益可视化选择器补充 + +- `GET /app-api/profile/benefits` + - 小程序房间页现在复用该接口作为下单权益选择器数据源。 + - 前端按当前 `storeId`、`roomId`、权益状态和剩余量过滤可选项。 + +- `POST /app-api/orders/reserve` + - 行为补充:`TIME` 类型优惠券不再只冻结记录,会按当前服务端锁定单价和券分钟数折算成订单折扣;最终折扣仍受服务端计价规则限制,不允许抵扣押金外的非法金额。 diff --git a/docs/current-baseline.md b/docs/current-baseline.md index 48b1246..9e11d22 100644 --- a/docs/current-baseline.md +++ b/docs/current-baseline.md @@ -2,7 +2,7 @@ > V5.0 首次核验日期:2026-06-16 > audited_commit: `4ae9296` -> next_engineering_target: 继续 M08-A 权益可视化选择器、真机合法域名、真实微信支付和实物开门验证 +> next_engineering_target: 继续 M08-A 真机合法域名、真实微信支付和实物开门验证 > 事实源:当前工作区、Git 历史、状态文档、Windows/WSL 检查脚本。 ## 总体结论 @@ -13,7 +13,7 @@ | Git 远端 | `origin=ssh://git@git.txyundm.cn:2222/panda/qipai.git`,分支 `main` | `git rev-list main...origin/main` 为 `0 0` | 本地与远端同步 | | 正式后端 | 已完成 Fastify、迁移兼容、outbox/worker、多应用租户、微信身份/会话,以及六角色 RBAC 和门店范围 | `backend/src/auth/**`、`database/migrations/2026061805_m02c_rbac.*.sql`、`backend/tests/**` | M01、M02-A/B/C DONE;MySQL 已验证 capability 和跨租户门店授权拒绝;下一阶段 M02-D | | 后台管理端 | 仅有 `admin/.gitkeep` | 当前文件扫描 | M09 未开始,不能标记 DONE | -| 微信小程序 | 已纳管微信原生模板,并完成顾客端订单、充值、权益明细和下单抵扣主体链路 | `miniapp/pages/index`、`miniapp/pages/store/detail`、`miniapp/pages/room/detail`、`miniapp/pages/orders/*`、`miniapp/pages/recharge/*`、`miniapp/pages/profile/*`、`miniapp/utils/api.js`、`scripts/check-miniapp-m08-a.mjs` | M08-A PARTIAL;已支持选店、门店/房间浏览、报价、预占、测试支付、余额支付、优惠券/套餐下单抵扣、订单微信预支付调起、充值微信支付调起、受控 Wi-Fi、订单列表/详情、状态历史、取消、订单权限开门、续费、换房、分享、个人中心和权益明细,仍不能标记 DONE | +| 微信小程序 | 已纳管微信原生模板,并完成顾客端订单、充值、权益明细和下单抵扣主体链路 | `miniapp/pages/index`、`miniapp/pages/store/detail`、`miniapp/pages/room/detail`、`miniapp/pages/orders/*`、`miniapp/pages/recharge/*`、`miniapp/pages/profile/*`、`miniapp/utils/api.js`、`scripts/check-miniapp-m08-a.mjs` | M08-A PARTIAL;已支持选店、门店/房间浏览、报价、预占、测试支付、余额支付、优惠券/套餐下单抵扣和可视化选择器、订单微信预支付调起、充值微信支付调起、受控 Wi-Fi、订单列表/详情、状态历史、取消、订单权限开门、续费、换房、分享、个人中心和权益明细,仍不能标记 DONE | | 数据库迁移 | 已新增 M01-B 核心 schema up/down/verify SQL、最小脱敏 seed、旧库结构 fixture、迁移计划/执行 CLI、验证结果数量门禁和 MySQL 8 集成测试 | `database/migrations/2026061601_m01b_core_schema.*.sql`、`database/seeds/2026061601_m01b_minimal_seed.sql`、`database/fixtures/2026061801_m01b_legacy_schema.sql`、`backend/src/db/migration-runner.ts`、`backend/tests/mysql-migration-roundtrip.test.mjs` | M01-B DONE;WSL MySQL 8.4.9 已验证 8 条旧库 fixture、11 条 up、3 条 verify、10 条 down,并验证租户隔离和旧金额整数分转换;不导入真实旧数据 | | M00 部署脚本 | 已有菜单、状态、HTTPS、Certbot、EMQX、备份检查模板 | `setup.sh`、`scripts/setup/`、`deploy/` | PARTIAL,生产未执行 | | 发布清单 dry-run | 可基于当前 HEAD 输出 `deployed=false` 的 release manifest,记录后端/后台/迁移尚未生成而跳过 | `scripts/setup/deploy-business.sh --dry-run .`、`scripts/dev/windows/check-release-manifest.ps1` | 可在 M00 验证结构;真实构建与生产发布待 M01/M09 后接入 | @@ -35,6 +35,6 @@ ## 下一步 -1. 继续 M08-A:在已完成选店、门店/房间浏览、报价、预占、测试支付、余额支付、优惠券/套餐下单抵扣、订单微信预支付调起、充值微信支付调起、受控 Wi-Fi、订单列表/详情、取消、订单权限开门、续费、换房、分享、个人中心、充值入口和权益明细基础上,补权益可视化选择器、真机合法域名、真实微信支付和实物开门验证。 +1. 继续 M08-A:在已完成选店、门店/房间浏览、报价、预占、测试支付、余额支付、优惠券/套餐下单抵扣和可视化选择器、订单微信预支付调起、充值微信支付调起、受控 Wi-Fi、订单列表/详情、取消、订单权限开门、续费、换房、分享、个人中心、充值入口和权益明细基础上,补真机合法域名、真实微信支付和实物开门验证。 2. 继续复用 M03 门店发现、M04 订单、M05 支付、M06 设备联动和 M07 会员营销后端能力,不重复初始化小程序。 3. M06-G 真实硬件联调仍等待生产 MQTT 账号/ACL、DeviceID 和现场实物配线。 diff --git a/docs/devlogs/2026-06-24-M08-A-顾客端.md b/docs/devlogs/2026-06-24-M08-A-顾客端.md index 1b0a3d4..b7c4941 100644 --- a/docs/devlogs/2026-06-24-M08-A-顾客端.md +++ b/docs/devlogs/2026-06-24-M08-A-顾客端.md @@ -211,3 +211,26 @@ M08-A 仍为 `PARTIAL`。本次完成顾客端第一段工程闭环,但订单 - 工程提交:`dda2b3c` - 远端校验:待本轮 push 后执行 `HEAD == origin/main`。 + +## 2026-06-25 续接:权益可视化选择器 + +- 小程序房间页进入时读取 `GET /app-api/profile/benefits`,按当前门店和房间过滤可用优惠券与套餐,展示为可点选卡片,替代手工输入权益 ID。 +- 优惠券选择支持再次点击取消;套餐选择后自动填入可用分钟和可用金额,仍允许顾客调整本次使用分钟与抵扣金额。 +- 小程序下单 payload 继续只提交权益 ID、套餐使用量和幂等请求号,不接受客户端自报价格。 +- 后端预占重算时补充时长券折价:`TIME` 优惠券按当前锁价单价和券分钟折算为订单折扣,满减券、套餐金额抵扣继续走服务端 clamp。 +- `scripts/check-miniapp-m08-a.mjs` 扩展检查权益列表、选择事件、`/profile/benefits` 和余额支付入口。 + +验收: + +- `npm run build`(`backend/`):PASS。 +- `node scripts/check-miniapp-m08-a.mjs`:PASS。 +- `npm test`(`backend/`):PASS。 + +状态: + +- M08-A 仍为 `PARTIAL`;真机合法域名、真实微信支付和实物开门验证待继续。 + +提交: + +- 工程提交:待本轮提交后更新。 +- 远端校验:待本轮 push 后执行 `HEAD == origin/main`。 diff --git a/docs/feature-status.md b/docs/feature-status.md index 2fe8dec..035863d 100644 --- a/docs/feature-status.md +++ b/docs/feature-status.md @@ -17,7 +17,7 @@ | OPS-003 | 整仓发布清单 | M00-E/M10 | PARTIAL | `59d92c0` | `deploy-business.sh --dry-run` 可在 Windows/WSL 验证当前 HEAD 的 release manifest 结构;`check-release-manifest.ps1` 已接入 `test-all.ps1`;正式构建待项目生成。 | 后端/后台尚未生成,生产菜单未执行。 | M01/M09 后接入真实构建结果,并在生产菜单 2 生成真实 release manifest。 | | OPS-004 | 菜单式更新与环境监测 | M00-E/M10 | PARTIAL | `292ab7f` | `setup.sh` 已接入初始化、更新、MQTT、HTTPS、状态、备份、恢复、回滚和诊断菜单;`--backup-status` 已在 WSL 验证,可检查备份工具、目录和模板。 | 未在生产 Ubuntu 执行;真实备份配置未启用。 | 生产执行后补部署记录、真实备份和恢复演练记录。 | | IOT-001 | MQTT Broker 生产部署 | M00/M06 | PARTIAL | `d01f771` | 菜单 3 已支持 Ubuntu 24.04 amd64 原生安装/更新与复检;正式 Topic ACL 已固定;后端实现 MQTT.js 3.1、QoS 1、持久会话、自动重连、订阅恢复、健康检查、消息上限和真实硬件烟测运行器;Windows 全量测试通过,WSL EMQX 5.8.9 服务级检查通过。 | 缺生产 EMQX 安装、正式账号、ACL/端口/备份验收;WSL 本地账号未配置,真实硬件烟测按预期 SKIP。 | 由管理员在生产菜单 3 执行并登记结果;提供生产账号、DeviceID 和实物后执行 `node dist/devices/hardware-smoke-runner.js`。 | -| SYS-001 | 微信原生小程序 | M00-C/M08 | PARTIAL | `dda2b3c` | 微信原生模板已接入顾客端登录态、选店跳转、门店详情、房间列表、报价、预占、测试支付、订单生产微信预支付调起、充值微信支付调起、余额支付、优惠券/套餐下单抵扣参数、受控 Wi-Fi、订单列表、订单详情、状态历史、取消入口、订单权限开门入口、续费、同门店换房、分享口令生成、个人中心余额/权益/账单汇总、充值计划/充值单入口和优惠券/套餐明细;`scripts/check-miniapp-m08-a.mjs`、相关会员/权益/充值测试通过。 | 多角色菜单、权益可视化选择器和真机联调尚未完成。 | 继续 M08-A,补权益选择器、真机合法域名、真实微信支付和实物开门验证。 | +| SYS-001 | 微信原生小程序 | M00-C/M08 | PARTIAL | `dda2b3c` | 微信原生模板已接入顾客端登录态、选店跳转、门店详情、房间列表、报价、预占、测试支付、订单生产微信预支付调起、充值微信支付调起、余额支付、优惠券/套餐下单抵扣参数与可视化选择器、受控 Wi-Fi、订单列表、订单详情、状态历史、取消入口、订单权限开门入口、续费、同门店换房、分享口令生成、个人中心余额/权益/账单汇总、充值计划/充值单入口和优惠券/套餐明细;`scripts/check-miniapp-m08-a.mjs`、相关会员/权益/充值测试通过。 | 多角色菜单和真机联调尚未完成。 | 继续 M08-A,补真机合法域名、真实微信支付和实物开门验证。 | | CFG-001 | 小程序品牌自定义 | M02-A/M03/M08 | PARTIAL | `8b8fb28` | 已建立 AppID/tenant 品牌配置表和 `/app-api/bootstrap`;同一 AppID 两租户隔离读取在 MySQL 8.4.9 实测通过。 | Logo/分享图片上传与小程序实际展示属于后续阶段。 | M03/M08 接入配置管理和小程序展示。 | | BKG-010 | 后台多小程序管理 | M02-A/M08/M10 | PARTIAL | `8b8fb28` | 已建立逻辑应用、租户绑定和租户配置模型;多租户 AppID 未指定租户会被拒绝。 | 尚无后台管理页面,多 AppID 真机配置未验证。 | M02-B/C 完成身份权限,M08 增加管理页面。 | | AUTH-001 | 多端统一小程序身份 | M02-B/M02-C/M08 | PARTIAL | `5a300a8` | 已实现微信 code 交换适配器、租户/AppID 隔离身份、JWT、服务端会话、当前用户和注销;禁用、角色/门店范围变化及管理员重置均使旧会话立即失效。 | 缺真实 AppSecret 真机联调和小程序动态角色界面。 | M08 完成小程序角色界面与真机登录。 | @@ -32,11 +32,11 @@ | MAP-001 | 地图选店和最近门店 | M03-C/M08 | PARTIAL | `4ac2c96` | 已实现 AppID/tenant 隔离门店发现、城市/营业筛选、按门店时区判断营业中、后端 Haversine 距离排序;小程序支持定位和手工城市降级,并可从门店卡片进入公开门店详情与房间列表。 | 地图 SDK 可视化和真机定位授权尚未验收。 | M08-A 继续补真机定位、地图展示和门店装修渲染。 | | QR-001 | 门店/房间场景码与 NFC | M03-D/M08 | PARTIAL | `d563078` | 已实现场景码生成、重建失效、撤销、扫码统计和二维码/NFC 统一页面解析;返回权限固定为空,旧码失效已在 MySQL 8.4.9 实测。 | 真实微信小程序码图片生成依赖可用 AppSecret 与真机环境。 | M08 接入微信小程序码接口并完成真机扫描验收。 | | WIFI-001 | 受控 Wi-Fi 凭据 | M03-D/M04/M08 | PARTIAL | `4ac2c96` | 正式订单与分享令牌均执行最小权限;M08-A 门店详情新增登录后受控 Wi-Fi 请求入口,不在未鉴权页面暴露密码。 | 真实到店权限、真机 Wi-Fi 连接和设备权限仍需现场联调。 | M08-A 补有效订单下的 Wi-Fi 真机连接验证。 | -| ORD-001 | 预约下单与多方式支付 | M04/M05/M08 | PARTIAL | `dda2b3c` | 已完成订单全生命周期、微信支付 v3 工程闭环和团购支付;M08-A 小程序已接入报价、预占、测试支付、余额支付、订单生产微信预支付调起、优惠券/套餐下单抵扣、订单列表、订单详情、状态历史、取消测算、取消订单、续费和同门店换房入口。 | 缺真实微信和团购平台授权联调;权益选择器和真机支付验收待补。 | M08-A 继续补权益可视化选择器、真机合法域名和真实微信支付验收。 | +| ORD-001 | 预约下单与多方式支付 | M04/M05/M08 | PARTIAL | `dda2b3c` | 已完成订单全生命周期、微信支付 v3 工程闭环和团购支付;M08-A 小程序已接入报价、预占、测试支付、余额支付、订单生产微信预支付调起、优惠券/套餐下单抵扣和可视化选择器、订单列表、订单详情、状态历史、取消测算、取消订单、续费和同门店换房入口。 | 缺真实微信和团购平台授权联调;真机支付验收待补。 | M08-A 继续补真机合法域名和真实微信支付验收。 | | WAL-001 | 充值优惠 | M07-B/M08 | PARTIAL | `53f5d67` | 已建立充值规则和充值订单表,支持有效期、适用门店、限购、启停、clientRequestId 幂等;M08-A 新增顾客端充值计划查询、充值单创建 API、小程序充值页、微信 JSAPI 预支付调起和 WeChat Pay v3 通知验签解密入账;重复支付回调不重复充值;Windows 全量回归通过。 | 真实微信商户凭据、生产合法域名和微信侧回调仍需联调。 | M08-A 继续补余额/优惠券/套餐下单抵扣集成。 | | WAL-002 | 门店独立会员余额 | M07-A | DONE | `63711ad` | 已建立租户/门店余额范围策略、现金/赠送双余额账户和不可变流水;充值、赠送、消费、退款、人工调整均预留 entry type;消费按赠送后现金扣款,业务号幂等,余额不足拒绝;Windows 全量后端回归通过。 | - | M07-D 聚合会员余额和权益状态。 | | WAL-003 | 余额账单 | M07-A/M08 | PARTIAL | `7446852` | 账单底表已包含业务类型、业务号、现金/赠送变动、变动后余额、操作人、trace 和 metadata;M07-B 充值成功后写入真实充值流水;M08-A 个人中心已展示最近钱包账单。 | 完整分页账单页面待继续;优惠券/套餐不直接写钱包流水。 | M08-A 增加分页账单或权益明细入口。 | -| WAL-004 | 优惠券和套餐权益 | M07-C/M08 | PARTIAL | `dda2b3c` | 已建立优惠券模板、持券、套餐计划、持有和权益核销流水;支持满减券、时长券、适用门店/房型/房间/星期/节假日、套餐剩余时长/金额,以及订单冻结、确认核销、退回和重复请求幂等;M08-A 个人中心已展示可用/冻结券和套餐汇总,并新增本人优惠券/套餐明细页;下单预占可冻结优惠券/套餐并重算订单金额,支付成功确认核销,取消订单释放冻结权益。 | 发放/购买入口、权益可视化选择器和真机支付验收待补。 | M08-A 继续补权益选择器;M09 接入后台发放/购买和管理入口。 | +| WAL-004 | 优惠券和套餐权益 | M07-C/M08 | PARTIAL | `dda2b3c` | 已建立优惠券模板、持券、套餐计划、持有和权益核销流水;支持满减券、时长券、适用门店/房型/房间/星期/节假日、套餐剩余时长/金额,以及订单冻结、确认核销、退回和重复请求幂等;M08-A 个人中心已展示可用/冻结券和套餐汇总,并新增本人优惠券/套餐明细页;下单预占可通过可视化选择器冻结优惠券/套餐并重算订单金额,时长券可按服务端单价折算抵扣,支付成功确认核销,取消订单释放冻结权益。 | 发放/购买入口和真机支付验收待补。 | M09 接入后台发放/购买和管理入口;M08-A 继续真机验收。 | | MEM-001 | 会员管理 | M07-D/M08 | PARTIAL | `7446852` | 已实现后台会员聚合查询服务和 `/admin-api/members`、`/admin-api/members/:id`;新增顾客端 `/app-api/profile` 和小程序个人中心,可查看注册/最近登录、订单数、消费额、余额、充值、优惠券、套餐和最近钱包流水,手机号脱敏,租户/门店范围及本人读取通过测试。 | 管理员赠券、余额人工调整、禁用、备注和后台前端页面需在 M09/M08 管理端接入并复用既有审计能力。 | M08-A 继续补顾客端权益明细;M09 接入后台会员管理页面。 | | GRP-001 | 团购券兑换 | M05-C | PARTIAL | `cda640b` | 已实现美团/点评、抖音、快手统一适配器,支持顾客粘贴/扫码值、管理员人工核销、Mock/API 核销、clientRequestId 幂等和券码 SHA-256 存储;重复券不能再次记账,WSL MySQL 8.4.9 实测通过。 | 缺各平台商家 API 授权,未执行真实厂商核销。 | 取得授权后配置 API endpoint/token 并执行真实验券。 | | GRP-002 | 美团直订 | M05-C | PARTIAL | `cda640b` | 已实现 HMAC 回调、事件幂等、外部门店/房间映射、未映射人工队列、顾客认领和团购支付订单生成;映射成功订单可进入 PAID。 | 缺美团开放平台授权和真实回调协议确认。 | 取得授权后按厂商协议实现专用签名适配并联调。 | diff --git a/docs/module-status.md b/docs/module-status.md index 341f158..37158f4 100644 --- a/docs/module-status.md +++ b/docs/module-status.md @@ -11,7 +11,7 @@ execution_cursor: last_push_verified: true base_branch: main blocked_reason: "M06-G 真实硬件联调缺生产 MQTT 账号/ACL、DeviceID、控制箱、门锁、插座和现场配线;已完成烟测运行器并按 BLOCKED_EXTERNAL 旁路。" - updated_at: 2026-06-25T23:53:00+08:00 + updated_at: 2026-06-25T23:59:00+08:00 ``` | 模块 | 状态 | 最近提交 | 最近开发日志 | 备注 | @@ -24,7 +24,7 @@ execution_cursor: | M05 会员、余额、套餐、优惠券 | PARTIAL | `1680d73` | docs/devlogs/2026-06-22-M05-D-分账与收款配置.md | M05-A/B/C/D 工程闭环已完成:统一支付、微信支付退款、团购直订、门店收款账户、分账接收方、比例策略、授权门禁、幂等分账和对账查询均通过 Windows 与 WSL MySQL 回归;真实微信/团购/分账权限未提供,模块保持 PARTIAL/BLOCKED_EXTERNAL。 | | M06 设备、MQTT 与真实硬件联动 | PARTIAL | `d01f771` | docs/devlogs/2026-06-24-M06-G-真实硬件联调.md | M06-A 至 F 已完成 MQTT、拓扑、协议幂等、控制箱、Sub-1G 门锁、智慧插座和订单自动联动任务;M06-G 已补真实硬件烟测运行器,缺生产 MQTT 账号/ACL、DeviceID 和实物配线,保持 BLOCKED_EXTERNAL。 | | M07 会员、余额、充值、优惠券和套餐营销 | DONE | `ed0d455` | docs/devlogs/2026-06-24-M07-D-会员管理.md | M07-A 已完成现金/赠送双余额账本;M07-B 已完成充值优惠;M07-C 已完成优惠券/套餐权益底表和冻结、确认、退回的可靠补偿核销服务;M07-D 已完成会员画像、订单、消费、余额、充值、优惠券和套餐聚合查询。 | -| M08 微信原生小程序完整业务 | PARTIAL | `dda2b3c` | docs/devlogs/2026-06-24-M08-A-顾客端.md | M08-A 已完成顾客端选店、门店/房间浏览、报价、预占、测试支付、订单生产微信预支付调起、充值微信支付调起与通知入账、受控 Wi-Fi、订单列表、订单详情、状态历史、取消测算、取消订单、订单权限开门、顾客续费、同门店换房、分享口令生成、个人中心余额/权益/账单汇总、充值计划/充值单入口、优惠券/套餐明细,以及优惠券/套餐/余额下单抵扣工程闭环;真机合法域名、真实微信支付、实物开门验证和权益可视化选择器继续留在 M08-A。 | +| M08 微信原生小程序完整业务 | PARTIAL | `dda2b3c` | docs/devlogs/2026-06-24-M08-A-顾客端.md | M08-A 已完成顾客端选店、门店/房间浏览、报价、预占、测试支付、订单生产微信预支付调起、充值微信支付调起与通知入账、受控 Wi-Fi、订单列表、订单详情、状态历史、取消测算、取消订单、订单权限开门、顾客续费、同门店换房、分享口令生成、个人中心余额/权益/账单汇总、充值计划/充值单入口、优惠券/套餐明细、优惠券/套餐/余额下单抵扣工程闭环和权益可视化选择器;真机合法域名、真实微信支付和实物开门验证继续留在 M08-A。 | | M09 后台管理端完整业务 | TODO | - | - | - | | M10 部署、域名、验收和运维闭环 | TODO | - | - | - | diff --git a/miniapp/pages/room/detail.js b/miniapp/pages/room/detail.js index 9902db0..4780fd9 100644 --- a/miniapp/pages/room/detail.js +++ b/miniapp/pages/room/detail.js @@ -32,15 +32,82 @@ Page({ packageHoldingId: '', packageMinutes: '', packageCreditYuan: '', + benefitLoading: false, + availableCoupons: [], + availablePackages: [], + selectedCouponName: '', + selectedPackageName: '', }, onLoad(options) { this.setData({ storeId: options.storeId || '', roomId: options.roomId || '', }) + this.loadBenefitOptions() }, - updateBenefitField(event) { + async loadBenefitOptions() { + this.setData({ benefitLoading: true }) + try { + await ensureLogin() + const response = await request('/profile/benefits') + this.setData({ + availableCoupons: (response.data.coupons || []) + .filter((item) => canUseCoupon(item, this.data.storeId, this.data.roomId)) + .map(formatCouponOption), + availablePackages: (response.data.packages || []) + .filter((item) => canUsePackage(item, this.data.storeId, this.data.roomId)) + .map(formatPackageOption), + }) + } catch (error) { + this.setData({ errorMessage: error.message || '权益加载失败' }) + } finally { + this.setData({ benefitLoading: false }) + } + }, + + selectCoupon(event) { + const couponGrantId = event.currentTarget.dataset.id || '' + const current = this.data.couponGrantId === couponGrantId + const selected = current + ? null + : this.data.availableCoupons.find((item) => item.couponGrantId === couponGrantId) + this.setData({ + couponGrantId: current ? '' : couponGrantId, + selectedCouponName: selected ? selected.name : '', + order: null, + payment: null, + successMessage: '', + errorMessage: '', + }) + }, + + selectPackage(event) { + const packageHoldingId = event.currentTarget.dataset.id || '' + const current = this.data.packageHoldingId === packageHoldingId + const selected = current + ? null + : this.data.availablePackages.find((item) => item.packageHoldingId === packageHoldingId) + this.setData({ + packageHoldingId: current ? '' : packageHoldingId, + selectedPackageName: selected ? selected.name : '', + packageMinutes: selected && selected.remainingMinutes > 0 + ? String(Math.min(selected.remainingMinutes, this.data.durationMinutes)) + : '', + packageCreditYuan: selected && selected.remainingAmountCents > 0 + ? yuanInput(Math.min( + selected.remainingAmountCents, + this.data.quote?.totalCents || selected.remainingAmountCents + )) + : '', + order: null, + payment: null, + successMessage: '', + errorMessage: '', + }) + }, + + updatePackageUsage(event) { const field = event.currentTarget.dataset.field if (!field) return this.setData({ @@ -95,7 +162,52 @@ Page({ if (!this.data.order || !this.data.order.orderId) { this.setData({ errorMessage: '请先预占订单' }) return - } +} + +function canUseCoupon(item, storeId, roomId) { + return item.status === 'AVAILABLE' + && Number(item.remainingUses || 0) > 0 + && (!item.storeId || String(item.storeId) === String(storeId)) + && (!item.roomId || String(item.roomId) === String(roomId)) +} + +function canUsePackage(item, storeId, roomId) { + return item.status === 'ACTIVE' + && (Number(item.remainingMinutes || 0) > 0 || Number(item.remainingAmountCents || 0) > 0) + && (!item.storeId || String(item.storeId) === String(storeId)) + && (!item.roomId || String(item.roomId) === String(roomId)) +} + +function formatCouponOption(item) { + const isTime = item.couponType === 'TIME' + return { + ...item, + valueText: isTime ? `${item.timeMinutes || 0}分钟` : cents(item.discountAmountCents), + thresholdText: Number(item.minOrderAmountCents || 0) > 0 + ? `满${cents(item.minOrderAmountCents)}可用` + : '无门槛', + scopeText: scopeText(item), + } +} + +function formatPackageOption(item) { + return { + ...item, + remainingAmountText: cents(item.remainingAmountCents), + scopeText: scopeText(item), + } +} + +function scopeText(item) { + if (item.roomId) return `房间 ${item.roomId}` + if (item.roomCategoryId) return `房型 ${item.roomCategoryId}` + if (item.storeId) return `门店 ${item.storeId}` + return item.holidayOnly ? '节假日' : '通用' +} + +function yuanInput(centsValue) { + return (Number(centsValue || 0) / 100).toFixed(2) +} await this.withRequest(async () => { const response = await request('/payments', { method: 'POST', diff --git a/miniapp/pages/room/detail.wxml b/miniapp/pages/room/detail.wxml index 90d0c0a..33a08c6 100644 --- a/miniapp/pages/room/detail.wxml +++ b/miniapp/pages/room/detail.wxml @@ -10,11 +10,41 @@ 权益抵扣 - - - - - + 权益加载中 + 暂无可用权益 + + + + {{item.name}} + {{item.valueText}} + + {{item.thresholdText}} · {{item.scopeText}} + + + + + + {{item.name}} + {{item.remainingAmountText}} + + 剩余 {{item.remainingMinutes}} 分钟 · {{item.scopeText}} + + + + + @@ -39,6 +69,8 @@ 订单 订单编号:{{order.orderId}} 预占编号:{{order.reservationId}} + 已选优惠券:{{selectedCouponName}} + 已选套餐:{{selectedPackageName}} 优惠抵扣:{{order.quote.discountCents}}分 套餐抵扣:{{order.quote.packageCreditCents}}分 diff --git a/miniapp/pages/room/detail.wxss b/miniapp/pages/room/detail.wxss index b411fd4..31883de 100644 --- a/miniapp/pages/room/detail.wxss +++ b/miniapp/pages/room/detail.wxss @@ -40,6 +40,46 @@ background: #ffffff; } +.benefit-list { + display: flex; + flex-direction: column; + gap: 16rpx; + margin-bottom: 20rpx; +} + +.benefit-option { + padding: 22rpx; + border: 2rpx solid #d9dde5; + border-radius: 12rpx; + background: #ffffff; +} + +.benefit-option.active { + border-color: #1f6feb; + background: #eef5ff; +} + +.option-head { + display: flex; + gap: 16rpx; + align-items: flex-start; + justify-content: space-between; +} + +.option-title { + flex: 1; + min-width: 0; + font-size: 30rpx; + font-weight: 600; + word-break: break-all; +} + +.option-value { + flex: 0 0 auto; + color: #1f6feb; + font-weight: 600; +} + .action-grid button { flex: 1 1 280rpx; } @@ -67,6 +107,11 @@ color: #17823b; } +.muted { + color: #777777; + line-height: 1.6; +} + .error { margin: 16rpx 0; color: #c73535; diff --git a/scripts/check-miniapp-m08-a.mjs b/scripts/check-miniapp-m08-a.mjs index ecf970c..91cd1c2 100644 --- a/scripts/check-miniapp-m08-a.mjs +++ b/scripts/check-miniapp-m08-a.mjs @@ -52,6 +52,11 @@ for (const pattern of [ 'couponGrantId', 'packageHoldingId', 'packageCreditCents', + '/profile/benefits', + 'selectCoupon', + 'selectPackage', + 'availableCoupons', + 'availablePackages', "provider: 'BALANCE'" ]) { assert.match(room, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));