整理开发文档

This commit is contained in:
2026-08-19 16:20:33 +08:00
parent 233ed0af3b
commit 62353a6800
21 changed files with 0 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
# V3 验收清单
> 业务事实源:[杜康好客-v3编码手册.md](./杜康好客-v3编码手册.md)
> 自动化:`node scripts/smoke-v3.mjs`Mock 环境)
> 历史回归:`node scripts/smoke-prev1.mjs`preV1 单链路)
## 必过主链路
| # | 验收项 | Owner | API / 页面 | 自动化 |
|---|--------|-------|------------|--------|
| 1 | C 端手机号登录 | jacy | `POST /auth/login/sms` · h5-user `/login` | smoke-v3 |
| 2 | 首页郑州 4 商品 | jacy | `GET /catalog/products?cityCode=` · h5-user `/` | smoke-v3 |
| 3 | 同城 1 瓶失败 | jacy | `POST /trade/orders/preview` | smoke-v3 |
| 4 | 同城 2 瓶成功 | jacy | `POST /trade/orders` | smoke-v3 |
| 5 | 跨城 5/6 瓶边界 | jacy | preview + create | smoke-v3 |
| 6 | 支付后发权益 | jacy | pay + benefit | smoke-v3 |
| 7 | 直接核销(无 couponId | jacy + 刘京尧 | `POST /redeem/tokens` | smoke-v3 |
| 8 | 单据核销 cap | jacy + 刘京尧 | `POST /redeem/tokens` + couponId | smoke-v3 |
| 9 | 门店扫码确认 | 刘京尧 | `POST /shop/redeem/confirm` · h5-shop | smoke-v3 |
| 10 | store_payout 生成 | jacy | settlement | smoke-v3 |
| 11 | 关闭门店不可核销 | 刘京尧 | store status + redeem | smoke-v3 |
| 12 | 录店审核后 C 端可见 | 刘京尧 + jacy | admin stores audit · h5-partner | 手动 |
| 13 | 配送推进完成 | jacy | jobs / delivery callback | smoke-v3 |
| 14 | 退款工单一致 | jacy | tickets + benefit void | smoke-v3 |
| 15 | 门店 T+1 打款确认 | jacy | admin store-payouts | smoke-v3 |
| 16 | 合伙人 T+30 账单 | jacy | admin partner-bills | smoke-v3 |
## 必过后台链路
| # | 验收项 | 页面 |
|---|--------|------|
| 1 | WebAdmin 登录 | admin-web `/login` |
| 2 | 商品 CRUD | `/products` |
| 3 | 审核门店 | `/stores` |
| 4 | 订单与配送 | `/orders` · `/deliveries` |
| 5 | 权益/核销/打款 | `/benefit/*` · `/redeem-records` · `/store-payouts` |
| 6 | 退款/补发工单 | `/tickets` |
| 7 | 第三方日志 | `/third-party-logs` |
| 8 | 财务导出 | `/partner-bills` export |
## 环境矩阵
| 环境 | MOCK_SMS | MOCK_PAY | MOCK_DELIVERY_AUTO | 说明 |
|------|----------|----------|-------------------|------|
| 本地开发 | true | true | true | 固定验证码 123456 |
| 测试沙箱 | false | false | false | 微信/配送沙箱 |
| 生产 | false | false | false | 真实第三方 |
## 合伙人端交接(刘京尧)
Jacy 侧交付:`/partner/*` 后端 API、shared-types DTO、smoke 断言。
刘京尧负责:`apps/h5-partner` 页面接入与验收。
+30
View File
@@ -0,0 +1,30 @@
# 杜康好客 · V2 编码手册(已归档)
> **勿作需求依据**。交付与验收以 [`杜康好客-v3-PRD.md`](./杜康好客-v3-PRD.md) + [`杜康好客-v3编码手册.md`](./杜康好客-v3编码手册.md) 为准。
> 完整 V2 原文(§一~§九、DB v3.1、API v3.1、任务卡)保留在 **git 历史**2026-08-06 压缩前)。
## 仍可能引用的 V2 片段(与 V3 冲突时以 V3 为准)
| 主题 | V2 口径 | V3 替代 |
|------|---------|---------|
| 核销上限 | 单次 ¥500 | 直接核销 ≤ 总余额;单据核销 ≤ 单据余额 |
| 订单 Tab | 5 Tab | 待付款 / 已付款 / 已完成 |
| 核销码 TTL | 5 分钟 | **3 分钟** |
| 总部端 | 小程序 `pages/hq/` | **`apps/admin-web` WebAdmin** |
| C 端 | 微信小程序 | `mini-user` / `h5-user` 过渡 |
## 索引(归档)
| § | 内容 |
|---|------|
| §一 | 四端目标、郑州 4 SKU |
| §二 | PRD + 原型 |
| §三 | 页面流 |
| §四 | Nest 模块边界、OWNER |
| §五 | MySQL 28 表 v3.1 |
| §六 | `/api/v1` 路由清单 |
| §七 | M0~M6 任务卡 |
| §八 | 购酒/核销/结算链路 |
| §九 | 微信/小飞侠/短信集成 |
**联调裁剪**[`杜康好客-preV1编码手册.md`](./杜康好客-preV1编码手册.md)
+41
View File
@@ -0,0 +1,41 @@
# 杜康好客 · preV1 编码手册(Mock 联调)
> **非 V3 验收标准**。核销等规则以 [`杜康好客-v3编码手册.md`](./杜康好客-v3编码手册.md) 为准。
> 原则:**同 V2 库表与 API 路径**,用 Mock/Flag/Seed 跳过外部依赖。
## 六条裁剪
| # | 裁剪 | 替代 |
|---|------|------|
| 1 | 无 HQ 前端 | `/admin/*` 脚本或后续 admin-web |
| 2 | 三端均 H5 | `h5-user` / `h5-shop` / `h5-partner` |
| 3 | 登录 | 手机号 + 固定码 `999888` |
| 4 | 支付 | Mock 即时成功 |
| 5 | 配送 | Mock 状态推进 |
| 6 | 短信 | Mock |
## Feature Flag`.env`
| Flag | 作用 |
|------|------|
| `MOCK_SMS` | 验证码 999888 |
| `MOCK_PAY` | 支付同步成功 |
| `MOCK_DELIVERY` | 配送 Mock 推进 |
## 启动验证
```bash
pnpm dev:api && pnpm dev:user && pnpm dev:shop && pnpm dev:partner
node scripts/smoke-prev1.mjs
```
## 与 V2 差异速查
| 项 | preV1 | V2/V3 |
|----|-------|-------|
| 客户端 | 三 H5 | 小程序 + H5 |
| 总部 | 无 UI | admin-web |
| 微信 | 无 | JSAPI/OAuth |
| 表结构 | v3.1 同左 | 同左 |
升级:逐项关 Mock → 接真实集成 → 补 admin-web / mini-user。
+3
View File
@@ -0,0 +1,3 @@
# 杜康好客 · v2.1 编码手册
> **已合并**。内容与 [`杜康好客-v3编码手册.md`](./杜康好客-v3编码手册.md) 重复,请以 **v3 编码手册** 为准。本文件保留作历史链接占位。
+165
View File
@@ -0,0 +1,165 @@
# 杜康好客 · V3.0 PRD
> **v3.0**2026-07-10)· 产品事实源 · 冲突时 **V3 > V2**
> 实现:[`v3编码手册`](./杜康好客-v3编码手册.md) · 审计:[`v3-现状对照`](./杜康好客-v3-现状对照.md)
## 0. 说明
| 项 | 内容 |
|----|------|
| 四端 | C 小程序 · 门店/合伙人 H5 · 总部 **WebAdmin**(工程口径,非 H5 |
| 试点 | 郑州 · 同城小飞侠 · 跨城总部物流到付 |
| 工程差异 | C 端现 `mini-user`/h5-user;核销 TTL **3min**;订单 Tab **三态** |
### 版本变更索引
| 版 | 日期 | 要点 | 开发文档 |
|----|------|------|----------|
| 3.4.10 | 08-03 | 门店套餐 | [`门店套餐`](./杜康好客-门店套餐功能开发文档-v3.4.10.md) |
| 3.4.11 | 08-04 | 开发计划 + 企微机器人/消息推送 | [`开发计划`](./杜康好客-开发计划功能开发文档-v3.4.11.md) |
| 3.4.12 | 08-04 | 退款回滚/财务/批量任务·工单/套餐 imageUrl | [`v3.4.12`](./杜康好客-v3.4.12-工单迭代开发文档.md) |
| 3.4.13 | 08-05 | metrics/核销详情/版本联动 PUBLISHED/mini 体验/H5 OAuth | [`v3.4.13`](./杜康好客-v3.4.13-体验优化开发文档.md) |
| 3.4.14 | 08-06 | mini-user 门头/套餐详情;小程序可配置;**统一测试白名单(不计账+限测可见+Mock旁路)** | [`v3.4.14`](./杜康好客-v3.4.14-mini-user门店体验开发文档.md) |
| 3.4.15 | 08-07 | mini-user 门店列表卡片;HQ 门店照片替换/删除;套餐多图上限 20 | [`v3.4.15`](./杜康好客-v3.4.15-mini-user门店列表优化.md) |
| 3.4.16 | 08-09 | 联系电话分离;取消自动 ST;套餐 UI;合伙人列表/入驻客服门槛;HQ 门店列表表格 | [`v3.4.16`](./杜康好客-v3.4.16-门店联系电话与体验优化.md) |
---
## 1. 背景与目标
**模型**:总部供酒 → 合伙人拓店 → 门店核销 1:1 好客权益(酒+餐)。
| 锚点 | 值 |
|------|-----|
| 酒水成本 | ~售价 3 折 |
| 门店结算 | 核销额 × **60%** |
| 合伙人佣金池 | 订单+核销 ≤ 订单额 **5%**(默认 **0%+3%** |
| 权益 | 实付 **1:1**,永久;口径 `benefit_amount ?? price` |
**G1~G5**:四端闭环 · 购酒履约 · 权益核销结算 · 合伙人拓店对账 · 售后/发票/代下单/推广/评价。
**门禁 S1~S4**:下单+权益(W1) · 核销(W1) · 拓店(W1) · 门店提现(W2)。
---
## 2. 角色与场景
| 角色 | 端 | 诉求 |
|------|-----|------|
| C 用户 | 小程序 | 买酒、权益、核销、售后 |
| 门店/店员 | H5 | 核销、营业、提现(W2) |
| 合伙人 | H5 | 拓店、订单、账单 |
| 总部 | WebAdmin | 审核、结算、运营 |
**SC-01~09**:同城购酒 · 现场提货 · 跨城 · 核销 · 拓店 · 售后 · 代下单 · 问卷评价 · 推广归因(见 PRD 原文路径摘要)。
**权限**:合伙人平级隔离;推广员仅拓店+看自己店;手机号脱敏(门店/合伙人中间4位)。
---
## 3. 核心规则
### 3.1 五条闭环
购酒履约 · 权益核销(60%+核销佣金) · 拓店入驻 · 售后工单 · 结算提现(T+1 出账+未出账可提)。
### 3.2 订单
`待付款 → 已付款 → 已完成`;30min 未付取消。同城自动推小飞侠(有仓+API);**≥10箱(60瓶)** 大单待 HQ 确认。跨城总部物流到付。现场提货支付即完成。
### 3.3 佣金与结算
| 类型 | 默认 | 释放 |
|------|------|------|
| 订单佣金 | 配置(0%) | 支付快照 |
| 核销佣金 | 配置(3%) | 核销时按门店归属合伙人 |
| 门店结算 | 60% | T+1;未出账可提现 |
**多合伙人**:全城/区域管辖;同城订单佣金按收货区县解析;核销佣金归拓店合伙人。
**FIN-001~003**:白名单未出账提现 · 单店日限 ¥5000 · T+0 审完预警。
合伙人 **T+30** 独立月账/打款。
### 3.4~3.8 Wave 能力
- **3.4** 门店一号多店、主账号、店员子账号(W2)
- **3.5** 拓店三步 + SOP + 试核销100
- **3.6** 工单四类型:仅退款/破损补发/破损退货/退货退款
- **3.7** 多仓、承运商、物流月结对账(小飞侠:2瓶6元 +2/瓶,6瓶箱14元)
- **3.8** 弱网5次拍照兜底(W3)
### 3.9~3.11 增量(详开发文档)
| § | 主题 | 文档 |
|---|------|------|
| 3.9 | 门店套餐 ≤10 条、独立审核 | v3.4.10 |
| 3.10 | 开发计划/任务/版本/技术支持联动 | v3.4.11 |
| 3.11 | 企微智能机器人 + 消息推送 Webhook | v3.4.11 |
| 3.12 | **登录手机号 ≠ 对外联系电话**;体验优化 | v3.4.16 |
### 3.12 门店登录号与体验增量(v3.4.16)
| 字段 | 含义 | 用途 |
|------|------|------|
| `phone` / 门店登录手机号 | 老板主账号 | 门店端短信登录;`StoreAccount.phone` |
| `contactPhone` / 联系电话 | 店长等对外号码 | C 端门店详情拨号与展示 |
- 合伙人拓店入驻、门店资料修改均须采集 **联系电话**(可与登录号不同)。
- 未填联系电话时,对外展示回退登录手机号(兼容旧数据)。
- **不**再因客户端 `validation_error` 自动创建技术支持工单。
- 合伙人入驻提交前须展示 HQ 可配企微客服二维码,并确认已添加客服。
- HQ 门店列表:长店名截断、操作列右固定、表过宽可横向滚动。
- 详 [`v3.4.16`](./杜康好客-v3.4.16-门店联系电话与体验优化.md)。
---
## 4. REQ 索引
> 完整 REQ`.cursor/skills/dukang-v3/reference-req-index.md`
| 端 | ID 范围 | 模块要点 |
|----|---------|----------|
| 用户 | U-001~027 | 四Tab/支付/权益3min/门店/套餐/工单/发票/推广/评价 |
| 门店 | S-001~021 | 核销双通道/记录×60%/提现/子账号/套餐提审 |
| 合伙人 | P-001~027 | 子账号/拓店+套餐/订单账单/代下单(W3) |
| 总部 | H-001~028 | 开城/审核/结算/推广/套餐/开发计划 |
**SKU 锚价**128/168/298/498(瓶)· 768/1008/1788/2988(箱6瓶)。
---
## 5. 非功能 NFR-001~010
7天免登 · 微信单支付 · 验证码3min · 脱敏鉴权 · 幂等 · 弱网/兜底 · 兼容 · 审计 · 24h履约 · 推广归因落库。
---
## 6. 数据与集成
**实体**:用户/商品/订单(佣金快照)/权益/门店/套餐/核销/工单/结算/合伙人/推广/仓(W3)/待处理核销(W3)。
| TECH | 系统 |
|------|------|
| 011~012 | 微信支付/短信 |
| 013 | 小飞侠 |
| 014~016 | 微信能力/分享/腾讯位置 |
**金额**:权益=实付1:1 · 门店结算=核销×60% · 试核销=**100元** · 码TTL=**3min**。
---
## 7~8. UI 与 ACC
主色杜康红 `#8B1E1E` · 权益金 `#C4A35A` · 空态人话+下一步。
ACC 全量:`.cursor/skills/dukang-v3/reference-acc.md`
---
## 9. 三波交付
| 波 | 日期 | 门禁 | 含 | 不含 |
|----|------|------|-----|------|
| W1 | 7.10 | S1~S4 | 登录商品支付核销拓店子账号 | 提现推广现场跨城工单发票 |
| W2 | 7.15 | S1~S5 | 提现FIN推广现场门店子账号多合伙 | 代下单弱网多仓跨城发票问卷 |
| W3 | 7.22 | 全量 | 代下单弱网多仓跨城工单发票看板 | — |
DLV 任务卡:`.cursor/skills/dukang-task-card/`
+38
View File
@@ -0,0 +1,38 @@
# 杜康好客 · V3 埋点规范
> 存储:C→`log_user_analytics` · 门店→`log_store_analytics` · 合伙人→`log_partner_analytics`
> 契约:`packages/shared-types/src/*-log.ts` · HQ`/logs/users|stores|partners`
## 原则
1. 端侧 page_view/点击 → `track()`;支付/核销结果 → 后端 `AnalyticsService.track*Safe`
2. 携带 `sessionId``dukang_session_id`
3. `extraJson` 无密码/令牌;手机号脱敏
4. eventName 先登记 taxonomy 再 emit
## API
| 端 | 路由 | 鉴权 |
|----|------|------|
| C | `POST /analytics/events` | OptionalJwt |
| 门店 | `POST /analytics/store-events` | Jwt+STORE |
| 合伙人 | `POST /analytics/partner-events` | Jwt+PARTNER |
| 推广 | `POST /promo/touch` | OptionalJwt(双写 touch |
## eventName
完整清单 → `user-log.ts` / `store-log.ts` / `partner-log.ts`
## extraJson 要点
| 端 | 常用字段 |
|----|----------|
| C | sessionId, cityCode, productId, storeId, orderId, amount, pagePath, failReason |
| 门店 | redeemChannel, amount, durationMs, failReason |
| 合伙人 | storeId, orderId, billId, cityCode |
## 漏斗(摘要)
- **C**session→home→detail→confirm→submit→pay→redeem
- **门店**home→scan→preview→confirm
- **合伙人**home→store_create→audit / orders→ship / bills→confirm
@@ -0,0 +1,37 @@
# 杜康好客 · 城市 / 仓库 / 日志架构
> **2026-07-12** P1 多合伙 + P2 仓库 ✅ · 合伙人管仓日志 ⏳ Wave 3
## 数据模型(摘要)
```
CommonCity → PartnerAccount(主) → Store(partnerAccountId, settlementRate)
→ CityWarehouse(HQ|PARTNER 管仓)
→ CatalogProduct
PartnerAccount → 子账号(parentAccountId, permissions JSON)
```
迁移:城市绑定/购酒分佣合并至 `partner_account`;订单快照 `partnerAccountIdAtPay` + `orderCommissionRateAtPay`
## 日志职责
| 主体 | 表 | 入口 |
|------|-----|------|
| HQ 写操作 | `common_event` HQ_OPERATION | `@HqOperation``/logs/hq` |
| 合伙人 | `log_partner_analytics` | `/logs/partners` |
| 门店 | `log_store_analytics` | `/logs/stores` |
| C 端 | `log_user_analytics` | `/logs/users` |
| 权益/订单 | `common_event` BENEFIT_LEDGER / ORDER_STATUS | 领域查询 |
## HQ action(新业务)
`CITY_*` · `WAREHOUSE_*` · `PARTNER_*` · `PARTNER_ACCOUNT_*` · `HQ_ACCOUNT_*` · `HQ_PERMISSION_*`
常量:`hq-operation.constants.ts` · 筛选项:`apps/admin-web/src/lib/hq-log.ts`
## 合伙人子账号 eventName
`partner_staff_create|update|permission_update|delete` ✅ · `partner_warehouse_*`
## DoD
HQ 写接口有 `@HqOperation`;合伙人写走 `trackPartnerOneSafe`;extraJson 脱敏;不跨模块直写他人日志表。
+70
View File
@@ -0,0 +1,70 @@
# 杜康好客 · V3.0 现状对照
> 基准:仅 [`杜康好客-v3-PRD.md`](./杜康好客-v3-PRD.md) · 总部 = **`admin-web`**(非 H5
> 图例:✅ 完成 · 🔶 部分 · ❌ 未做 · ⚠️ 曾冲突已修
## 0. 总览(2026-08-06
| 维度 | 结论 |
|------|------|
| 主链路 | 登录→下单支付→权益→扫码核销→payout→HQ 打款 **可跑通** |
| C 端 | `mini-user` 小程序 + h5-userv3.4.13 版本门控/门店/物流已上 |
| 近期版本 | … · v3.4.15 · **v3.4.16 联系电话/套餐 UI/合伙人列表与入驻客服门槛/HQ 门店列表表格** |
| 冒烟 | `scripts/smoke-v3.mjs` 窄路径 ≠ 全量 ACC |
| REQ 明细 | PRD §4 + `.cursor/skills/dukang-v3/reference-req-index.md` |
## 1. P0 冲突(2026-07-12 ✅)
| # | 项 | 状态 |
|---|-----|------|
| C2~C3 | 核销/短信 TTL **3min** | ✅ |
| C4~C5 | 订单 Tab/列表 **三态** | ✅ |
| C6~C7 | 4 SKU + 佣金 **0%+3%** | ✅ |
| C13 | 多合伙人主账号模型 | ✅ |
| C14 | 文档旧口径清理 | ✅ |
| C1 | C 端小程序 | 🔶 mini-user 进行中 |
| C8~C12 | 一号多店/自动推单/提现/四类型工单/现场提货 | 🔶~❌ 见 PRD Wave |
## 2. 按域快照
| 域 | ✅ 已有 | 🔶/❌ 主要缺口 |
|----|---------|----------------|
| **mini-user** | 购酒/权益/核销/门店/物流/版本门控 | 规则弹窗、发票、四类型工单、问卷 |
| **h5-shop** | 扫码核销、记录、营业、iOS 扫码 OAuth | 手机号核销、提现、子账号、弱网兜底 |
| **h5-partner** | 子账号、拓店、订单/账单、套餐 | 试核销100、负责人复核、代下单 |
| **admin-web** | 商品/开城/门店/订单/权益/核销/结算/推广码 metrics/开发计划/技术支持 | 完整 SOP 审核 UI、发票、热力图 |
| **后端** | 主模块、支付、权益、核销、payout、Courier 适配 | 30min 取消 job、部分 Wave3 |
## 3. 场景 SC-01~09
| 场景 | 状态 |
|------|------|
| SC-01 同城 | 🔶 支付权益✅;自动推单/24h 完成 部分 |
| SC-02 现场提货 | 🔶 |
| SC-03 跨城 | 🔶 |
| SC-04 核销 | 🔶 扫码✅;手机号通道❌ |
| SC-05 拓店 | 🔶 三步✅;试核销/SOP 部分 |
| SC-06 售后 | 🔶 REFUND 有;四类型 部分 |
| SC-07~09 | 代下单❌ · 问卷🔶 · 推广🔶 |
## 4. 版本交付索引
| 版本 | 文档 | 状态 |
|------|------|------|
| 3.4.10 | [`门店套餐`](./杜康好客-门店套餐功能开发文档-v3.4.10.md) | ✅ |
| 3.4.11 | [`开发计划`](./杜康好客-开发计划功能开发文档-v3.4.11.md) | ✅ |
| 3.4.12 | [`工单迭代`](./杜康好客-v3.4.12-工单迭代开发文档.md) | ✅ |
| 3.4.13 | [`体验优化`](./杜康好客-v3.4.13-体验优化开发文档.md) | ✅ 生产 |
| 3.4.14 | [`mini-user 门店体验 + 小程序可配置 + 测试白名单`](./杜康好客-v3.4.14-mini-user门店体验开发文档.md) | ✅ 生产 |
| 3.4.15 | [`mini-user 门店列表 + HQ 照片/套餐多图`](./杜康好客-v3.4.15-mini-user门店列表优化.md) | ✅ 生产 |
| 2026-08-08 | v3.4.14 / v3.4.15 已发生产,更新状态
| 日期 | 说明 |
|------|------|
| 2026-08-07 | v3.4.14 增补统一测试白名单(不计账 / 限测可见 / Mock 旁路) |
| 2026-08-07 | v3.4.15 增补:HQ 门店照片替换/删除、套餐多图上限 20 |
| 2026-08-05 | v3.4.13 |
| 2026-08-04 | v3.4.11 / v3.4.12 |
| 2026-07-11 | 首版对照表 |
@@ -0,0 +1,20 @@
# 杜康好客 · v3.4.12 工单迭代
> **2026-08-04** · 已上线 · PRD §0.4
| 域 | 交付 |
|----|------|
| P0 售后 | `initiateRefund` 微信失败回滚订单状态 |
| mini-user | 门头 preview、环境图双列 |
| 财务 | 酒厂 T+3;门店可多笔 pending 提现 |
| 开发计划 | 审批可选企微派发;`POST .../tasks/batch-update` |
| 技术支持 | PATCH 编辑/附件;`batch-update-status` |
| 套餐 | `StorePackage.imageUrl` 四端 |
## API
`POST /admin/dev-plan/tasks/batch-update` · `PATCH /admin/support-tickets/:id` · `POST .../batch-update-status` · review 扩展 `dispatchToWecom`
## ACC
退款 Mock 成功 · 门头大图 · T+3/多笔提现 · 企微派发 · 批量改任务/工单状态 · 套餐 imageUrl
@@ -0,0 +1,58 @@
# 杜康好客 · v3.4.13 体验优化
> **2026-08-05** · 20 ST · 已发生产 `0181af0` · PRD §0.5 · 现状对照 §10
## ST 交付一览
| ST | 交付要点 |
|----|----------|
| ST1785925037781309 | 推广码 attributionCount + `log_promo_event` + metrics/timeline/events + HQ ECharts |
| ST1785924286682833 | 门店电话 maskPhone + `store_phone_call` 埋点 |
| ST1785921693982470 | 工单 `priority` + HQ 筛选/编辑 |
| ST1785921585900725 | 门店 H5OAuth 单例回调 + iOS JSSDK 预热/重试 + 授权后续扫 |
| ST1785907536648201 | 我的页 `v3.4.x``minClientVersion` 过低 → UpdateManager 或 `exitMiniProgram` |
| ST1785906800359657 | 合伙人暂停:发码前 `phone/check`,微信/SMS 均拒 |
| ST1785906592340255 / ST1785901711627824 | partner/shop LoginPage 空字段前端拦截 |
| ST1785906390321653 | 现场提货提交 `showModal` 确认 |
| ST1785905773501871 | 核销列表 userNo/nickname/phone;详情含门店/券分摊/结算/评价 |
| ST1785904849234806 | 工单中心(已有) |
| ST1785904076632841 | 门店列表「开城合伙人」= `partnerOptionLabel`company/name/phone |
| ST1785902173093977 / ST1785902141731113 | 商品去分享;首图 preview |
| ST1785901870913349 | 门店列表展示两段营业时间 |
| ST1785901838948572 | SHOP 未绑定门店(已有) |
| ST1785901775231811 | 门店套餐页签横滑切换 |
| ST1785901314893145 | 门头 aspectFill 铺满 + preview |
| ST1785939375449985 | 物流:签收照/拨号/时间线/ETA;小飞侠回调签收→`COMPLETED` |
| — | 图片 >10MB`@dukang/shared-ui/compressImage`(各端 upload + mini 头像) |
| — | 权益券详情抽屉 980px;核销详情对齐权益券内抽屉 |
| — | 版本 `RELEASED` → 关联任务 `RELEASED`;关联工单 `PUBLISHED` + `releasedVersionNo` |
## 契约摘要
**API**
| 路径 | 说明 |
|------|------|
| `GET /common/client-config` | `minClientVersion``MINI_USER_MIN_VERSION` |
| `GET /trade/orders/:id/track` | nodes、signPhotoUrls、estimatedArrivalCourier 100102/108/301 |
| `POST /callbacks/courier/xfx/track` | 小飞侠路由回调;生产 `api.dukanghaoke.com`,测试 `api-test.dukanghaoke.com` |
| `GET /admin/promo-codes/:id/metrics/{timeline,events}` | 推广码四指标时序 + 事件分页 |
**表/枚举**
- `log_promo_event`SCAN / ATTRIBUTION / REGISTER / ORDER(含 IP/地点;仅统计上线后事件)
- `common_support_ticket``priority``status``PUBLISHED``released_version_no``published_at`
- 小飞侠回调:status **5** 或 statusName 含「签收」→ 订单 **COMPLETED**(幂等);7 取消仅日志
**HQ 开发计划**
创建版本 `v3.4.13` 并关联 ST 任务;标记 **已发布** 时自动联动任务/工单(见上表最后一行)。
## 验收抽样
- [ ] 推广码四指标趋势 + 事件日志;重复 touch 不计 SCAN
- [ ] 核销/权益券详情抽屉字段完整、980px 无横滚
- [ ] 版本 RELEASED 后工单「已发布」带版本号;门店列表合伙人列非空
- [ ] mini-user:版本门控、门店/商品/物流/压缩项
- [ ] 门店 H5 iPhone 微信:OAuth 后续扫可用
- [ ] 小飞侠回调 status=5 → 订单 COMPLETED
@@ -0,0 +1,87 @@
# 杜康好客 · v3.4.14 mini-user 门店体验 + 小程序可配置 + 测试白名单
> **2026-08-06** · **开发中** · PRD §0.6 · mini-user `3.4.14` · **未发版**
## 范围
| 项 | 交付 |
|----|------|
| 门头照 | **固定 4:3 区域**`aspectFit` 缩放完整显示(不裁剪);标题信息卡固定接在门头下方 |
| 门店详情·套餐 | 仅完整标题纵向列表;点击进入详情 |
| 套餐详情页 | 实底导航让出胶囊区;内容区顶/左右留白;有图在门店名下;无图直接菜品 |
| **系统设置·微信小程序** | Logo / 资质图 / 客服电话 / C 端 H5 / Mock 验证码 **可配置**,经 `client-config` 下发 |
| **测试白名单** | 全局手机号名单;账号/门店/订单/核销打标;不计四条结算;合并商品/门店可见性手机号;HQ 独立管理模块 |
## 页面路由
| 路径 | 参数 |
|------|------|
| `pages/store-detail/index` | `id` 门店 ID |
| `pages/store-package-detail/index` | `storeId` · `index` 套餐序号(0-based |
数据:复用 `GET /stores/:id` 内嵌 `packages[]`,详情页按 index 取项。
## 系统设置(微信小程序配置)
HQ → 系统设置 → **微信小程序配置**。启动时空缺键用 shared-types 默认值补种。
| 配置键 | 说明 |
|--------|------|
| `USER_H5_URL` | C 端 H5 落地页(推广码等) |
| `BRAND_LOGO_OSS_BASE` | Logo OSS 根路径(说明用) |
| `BRAND_LOGO_URL` / `_WIDE_` / `_MARK_` | 方形 / 长方形 / 图标 Logo |
| `MINI_USER_STATIC_OSS_BASE` | 小程序静态资源根路径 |
| `QUALIFICATION_DISCLOSURE_URL` | 资质公示长图 |
| `CUSTOMER_SERVICE_PHONE` | 总部客服电话 |
| `MOCK_SMS_FIXED_CODE` | Mock 短信固定验证码(仅 MOCK_SMS 开启) |
**下发**`GET /common/client-config` 增加 `userH5Url``brandLogoUrl``brandLogoWideUrl``brandLogoMarkUrl``qualificationDisclosureUrl``customerServicePhone`
`MOCK_SMS_FIXED_CODE` 仅服务端 Mock 短信读取,不下发客户端。
## 测试白名单(统一)
### 规则
| 规则 | 说明 |
|------|------|
| 源真相 | HQ「白名单管理」维护 `common_test_whitelist_phone`;名单内手机号 = 测试账号 |
| 可见性 | 商品/门店 `visibilityWhitelistEnabled` 开启后,C 端仅当观众手机号 ∈ 全局名单可见/可购(不再用分实体 `*_visibility_phone` |
| 打标 | `User` / `StoreAccount` / `PartnerAccount` / `Store` / `Order` / `RedeemRecord``isTest` |
| 不计账 | 测试核销不产生 `StorePayout`;酒厂/物流/合伙人账单排除 `isTest` 流水 |
| 验证旁路 | 页顶 Checkbox ↔ `MOCK_SMS` / `MOCK_WECHAT` / `MOCK_PAY`(勾选 = 不做真实验证) |
### API
| 路径 | 说明 |
|------|------|
| `GET/POST /admin/test-whitelist/phones` | 名单列表 / 新增 |
| `PATCH/DELETE /admin/test-whitelist/phones/:id` | 改备注 / 删除 |
| `GET /admin/test-whitelist/accounts` | 测试账号记录(type=user\|store_account\|partner\|store\|order |
| `GET /admin/test-whitelist/phones/:id/linked` | 单号关联实体 |
| `POST /admin/test-whitelist/migrate-visibility` | 旧可见性手机号导入 |
业务列表查询参数:`excludeTest=true` 排除测试数据。
### HQ
- 路由 `/test-whitelist`:验证旁路 + 手机号名单 + 测试账号记录
- 用户/订单/门店/门店账号/合伙人/核销:「过滤测试账号」复选框;列表「测试」Tag
- 商品/门店:保留「仅白名单可见」开关,去掉分实体手机号编辑
## ACC
- [ ] 门头区高度固定(4:3);图片 aspectFit 完整缩放;标题 section 位置不随图高变化
- [ ] 门店详情套餐区仅标题列表,标题完整展示、可换行
- [ ] 点击套餐进入详情页,字段完整;返回回到门店详情
- [ ] 无套餐时不展示区块;index 非法时友好提示
- [ ] HQ 微信小程序配置可改 Logo/电话/落地页/Mock 码;保存后 client-config 立即生效(无需重启)
- [ ] mini-user 登录/我的/客服/分享图读取配置;未配置时回退代码默认常量
- [ ] HQ「白名单管理」可增删手机号;可查看测试账号记录
- [ ] 页顶三 Checkbox 控制短信/微信/支付跳过真实验证,与 `MOCK_*` 同源立即生效
- [ ] 业务列表「过滤测试账号」勾选后不含测试数据
- [ ] 旧可见性手机号已导入;限测商品/门店仅全局名单可见
- [ ] 测试流水不进四条账单与门店打款
## HQ 开发计划
创建版本 `v3.4.14``IN_PROGRESS`),关联本迭代任务。发版前再合并发布。
@@ -0,0 +1,52 @@
# 杜康好客 · v3.4.15 mini-user 门店列表 + HQ 门店照片 / 套餐多图
> **2026-08-07** · mini-user `3.4.15`
## 更新内容
### A. mini-user 门店列表卡片
- 门店列表卡片去掉「去核销」按钮,整卡最右侧改为小箭头(进入详情)
- 第 1 行:门店名单行截断(不显示省略号),宽度顶到文案区最右
- 第 2 行:地址最多两行;右侧显示距离
- 第 3 行:营业时间同行展示(多段时段空格拼接)
- 店铺封面右上角叠加斜角「营业中」标签图
### B. HQ 门店照片支持替换与删除
- 门店详情「审核材料」Tab:门头照 / 环境照 / 签约合同可上传替换、删除
- 环境照最多 **20** 张;保存后写回 `CommonResource`(软删旧图再写入)
- `PUT /admin/stores/:id` 支持 `coverUrl` / `envPhotoUrls` / `contractUrl`(空值=删除)
### C. 门店套餐图片最多 20 张
- 单条套餐支持多图:`imageUrls`JSON+ `imageUrl` 同步为首图(兼容旧数据)
- 上限 `STORE_PACKAGE_IMAGE_MAX_COUNT = 20`
- HQ / 合伙人 H5 / 门店 H5 均可增删换图;C 端套餐详情左右滑动浏览并可点击预览
### D. 门店排序字段
- `store_store.sort_order``Store.sortOrder`,默认 0,越小越靠前)
- HQ 门店列表 / 新建 / 编辑可配置;C 端门店列表优先按 `sortOrder`,同序再按距离(有定位)或创建时间
## 范围
| 项 | 交付 |
|----|------|
| 门店列表卡片 | 布局与交互如上 |
| HQ 门店照片 | 审核材料可替换/删除 |
| 套餐多图 | 最多 20 张 / 条;C 端左右滑动 |
| 门店排序 | `sortOrder` 字段 + HQ 配置 + C 端排序 |
| 版本号 | `mini-user` `3.4.15` |
## ACC
- [ ] 列表项无「去核销」按钮;右侧有 › 箭头;点击整卡进详情
- [ ] 长店名单行截断且无 `…`;标题行无距离
- [ ] 地址 ≤2 行,距离在第二行右侧
- [ ] 营业时间单行;双时段同行显示
- [ ] 封面右上角可见「营业中」斜角标签
- [ ] HQ 门店详情可替换/删除门头照、环境照、合同并保存生效
- [ ] 环境照无法超过 20 张
- [ ] 套餐单条可上传至多 20 张图;C 端详情可左右滑动浏览全部
- [ ] HQ 可设置门店排序;C 端列表按排序优先展示
@@ -0,0 +1,83 @@
# 杜康好客 · v3.4.16 门店联系电话与体验优化
> **2026-08-09** · PRD §3.12 · mini-user / h5-partner / admin-web / API
## 范围
| 项 | 交付 |
|----|------|
| A. 登录号 / 联系电话分离 | `Store.contactPhone`;合伙人入驻/编辑;HQC 端 `publicDial` |
| B. 取消自动技术支持工单 | `validation_error` 不再自动建 ST;上报与人工 ST 保留 |
| C. 套餐详情排版 | 套餐名 + 价格:一行能放下则同行,否则名称换行、价格次行右对齐 |
| D. 门店详情套餐列表 | 套餐名称后展示价格 |
| E. 合伙人门店列表 | 右上角状态;开关营业/临时闭店;永久闭店按钮;下拉筛选 |
| F. 入驻企微客服门槛 | HQ 可配二维码;勾选「已添加客服」后才可提交 |
| G. HQ 门店列表表格 | 店名过长截断;操作列右侧固定;横向滚动 |
## A. 登录号 ≠ 对外联系电话
| 字段 | 含义 | 用途 |
|------|------|------|
| `phone` | 老板主账号 | 门店端登录;`StoreAccount.phone` |
| `contactPhone` | 店长/对外 | C 端拨号与展示 |
- C 端 `GET /stores``GET /stores/:id``phone` = `contactPhone ?? phone`(兼容旧小程序)
- 未填联系电话时对外回退登录号
## B. 取消 validation_error 自动 ST
- 原路径:`POST /common/client-errors` + `category=validation_error``createFromClientValidation`
- **删除**自动建单;保留 client-error 落库 / WeCom 告警;HQ 手动建单与企微「创建」不变
## C / D. mini-user 套餐 UI
- 套餐详情标题与门店详情「门店套餐」列表一致:名称在左可换行,价格始终居右;放不下时价格落在次行右侧
## E. 合伙人门店列表
| UI | 行为 |
|----|------|
| 右上角徽标 | 待审核 / 审核驳回 / 营业中 / 临时闭店 / 永久闭店 |
| 开关 | `OPEN``PAUSED`CLOSED / PENDING 禁用 |
| 永久闭店 | 独立按钮 + 确认 → `CLOSED` |
| 筛选 | 下拉:全部 / 营业中 / 临时闭店 / 待审核 / 已驳回 / 永久闭店 |
API`PUT /partner/stores/:id/status`(不变)
## F. 入驻企微客服二维码门槛
| 配置键 | 说明 |
|--------|------|
| `PARTNER_ONBOARD_CS_QR_URL` | 企微客服二维码图片 URL(OSS) |
| `PARTNER_ONBOARD_CS_HINT` | 提示文案(默认见下) |
默认提示:`使用问题、提现问题等随时可联系【杜康好客】客服`
- `GET /common/client-config` 对 PARTNER 下发 `partnerOnboardCsQrUrl` / `partnerOnboardCsHint`
- HQ **系统设置 → 微信小程序配置**(与「总部客服电话」同组):可上传二维码、编辑提示文案
- 入驻信息填完后展示二维码;须勾选「我已添加【杜康好客】客服」才可提交/跳过
- **未配置二维码**:禁止提交,提示联系总部
## G. HQ 门店列表页表格体验
[`apps/admin-web` 门店列表](apps/admin-web/src/pages/StoresPage.tsx)
| 点 | 行为 |
|----|------|
| 门店名 | 过长省略号截断,悬停可看全名;「测试」标签保留 |
| 操作列 | `fixed: 'right'`,横向滚动时详情/评价始终可见 |
| 整表过宽 | `scroll.x` 开启横向滚动条(列宽合计约 1720) |
合伙人名、介绍、分类等长文本列同样启用 ellipsis,避免撑破布局。
## ACC
- [ ] 旧门店对外拨号仍可用(回退登录号或已回填 contactPhone
- [ ] 合伙人可分别维护登录号与联系电话
- [ ] API 400 不再产生「客户端验证错误自动上报」ST
- [ ] 套餐详情短/长标题排版符合定案
- [ ] 门店详情套餐列表可见价格
- [ ] 合伙人列表状态/开关/永久闭店/下拉筛选可用
- [ ] 无二维码不可提交;有二维码须勾选后提交
- [ ] HQ 可上传/更换入驻客服二维码
- [ ] HQ 门店列表:长店名截断、操作栏右固定、可横向滚动
@@ -0,0 +1,87 @@
# 杜康好客 · v3.4.17 门店套餐审核快捷入口
> **2026-08-16** · PRD §套餐变更审核 · admin-web / API(ops)
> 目标:把「套餐变更审核」能力前置到**门店详情-套餐页签**与**门店列表操作栏**,让总部在不离开列表/详情流的情况下即可发现待审套餐并一键进入审核(含线上 vs 待审对照)。
## 范围
| 项 | 交付 |
|----|------|
| A. 门店详情·套餐页签 待审提醒 | 存在 `status=PENDING` 的套餐变更时,页签顶部黄色 Alert + 「审核 / 对比」按钮 |
| B. 审核 / 对比 跳转 | 复用现有 `StorePackageAuditsPage`,跳转 `/store-package-audits?requestId=xxx` 自动打开审核抽屉(抽屉内已含线上 vs 待审并排对照) |
| C. 门店列表 操作栏快捷入口 | 有待审套餐时显示「审核套餐」「对比」两个按钮,跳转同一地址(与详情页审核功能一致) |
| D. 列表接口补 `pendingPackageAuditId` | `GET /admin/stores` 返回每家店当前待审套餐变更的 `requestId`(一次批量查询,无 schema 变更) |
| E. 审核后提醒自动刷新 | 审核完成 dispatch `admin:package-audit-changed`,套餐页签监听后刷新提醒 |
## A / B. 门店详情·套餐页签
组件:`apps/admin-web/src/components/AdminStorePackagesSection.tsx`
- 现有 `GET /admin/stores/:storeId/packages` 已返回 `pendingRequest`(含 `id`/`status`/`packages`),此前被组件丢弃;本次**捕获 `pendingRequest`** 存入 state。
-`pendingRequest.status === 'PENDING'`
- 页签顶部渲染 `<Alert type="warning">`:「该门店有待审核套餐」。
- 提醒内放「审核」「对比」两个按钮,均 `navigate('/store-package-audits?requestId=' + pendingRequest.id)`
- 空套餐门店同样展示该提醒(不依赖 `live` 是否有数据)。
- 跳转后由 `StorePackageAuditsPage` 读取 `?requestId=` 自动打开抽屉:抽屉内左侧「线上已审核套餐」、右侧「待审核套餐」并排 `diffPackages` 对照;抽屉右上「通过 / 驳回」即审核动作。两套按钮语义:审核=处置,对比=看差异,落点同一页面。
## C. 门店列表·操作栏
组件:`apps/admin-web/src/pages/StoresPage.tsx`
- `StoreRow` 增加 `pendingPackageAuditId?: string | null`
- 操作列(原「详情 / 评价」)在 `pendingPackageAuditId` 存在时追加「审核套餐」「对比」按钮,均 `navigate('/store-package-audits?requestId=' + row.pendingPackageAuditId)`
- 操作列宽度 140 → 220,允许换行,避免按钮被挤压。
## D. 列表接口补 `pendingPackageAuditId`
后端:`server/dukang-api/src/modules/ops/admin-stores.service.ts``listStores`
- 取回 `items` 后,用一次查询批量取出待审变更:
`storePackageChangeRequest.findMany({ where: { storeId: { in: storeIds }, status: 'PENDING' }, select: { id, storeId } })`
-`storeId -> requestId` 映射,逐店附加 `pendingPackageAuditId`(无则 `null`)。
- `mapStoreCompat``{ ...store }` 展开透传,新字段不会被丢弃;**无 Prisma schema 变更**,发布可 `--skip-db`
## E. 审核后提醒自动刷新
- `StorePackageAuditsPage` 审核成功时调用 `notifyPackageAuditChanged()`dispatch `admin:package-audit-changed`)。
- `AdminStorePackagesSection` 监听该事件,重新拉取 `pendingRequest` 并刷新提醒(仅更新 `pendingRequest`,不触碰正在编辑的 `items`,避免覆盖未保存套餐)。
## 关键接口 / 文件
| 位置 | 说明 |
|------|------|
| `GET /admin/stores` | 列表,新增 `pendingPackageAuditId`ops/admin/* 透传,无 schema 变更) |
| `GET /admin/stores/:storeId/packages` | 返回 `pendingRequest`store-package.service.ts,未改,仅前端消费) |
| `GET /admin/store-package-audits/:requestId` | 审核详情(含 `livePackages` 对照) |
| `PUT /admin/store-package-audits/:requestId/audit` | 通过 / 驳回 |
| `apps/admin-web/src/components/AdminStorePackagesSection.tsx` | 套餐页签提醒 + 按钮 |
| `apps/admin-web/src/pages/StorePackageAuditsPage.tsx` | `?requestId=` 自动开抽屉 + 变更明细(含 `fieldChanges` / `diffText` / `TextDiff` |
| `apps/admin-web/src/pages/StoresPage.tsx` | 列表操作栏快捷入口 |
## F. 套餐变更对比·逐字段 / 逐字明细
组件:`apps/admin-web/src/pages/StorePackageAuditsPage.tsx`
> 需求:审核抽屉原本只给每条套餐「新增 / 删除 / 变更 / 未变」粗粒度标记,看不出具体哪里变了。本次在「待审核套餐」卡片底部新增**变更明细**块,逐字段列出差异;文本类字段进一步做**逐字(LCS)差异定位**,精确高亮哪些字被增 / 删。
- `fieldChanges(live, proposed)`:逐字段比对,产出 `{ label, old, now, kind }` 明细:
- `kind: 'value'`(整体替换展示 `旧 → 新`):**价格**、**图片(N 张 → M 张)**。
- `kind: 'text'`(走逐字差异):**套餐名称、菜品内容、可用时间、其他说明**。
- `diffText(a, b)`LCS 动态规划(`dp[i][j]`+ 回溯,产出 `equal / delete / insert` 段落并合并相邻同类型;时间/空间 `O(|a|·|b|)`,套餐字段长度可控,无性能风险。
- `TextDiff({ oldText, newText })`:渲染两行——
- 「原:」行用**红色删除线**标出被删的字(`delete` 段),其余正常;
- 「新:」行用**绿色**标出新增的字(`insert` 段),其余正常。
- 一眼定位到具体改动的字,而非整段替换。
- 抽屉顶部汇总条:**新增 X · 删除 Y · 变更 Z · 未变 W**(由 `diffPackages``changes` 统计)。
### 已知局限
- 套餐按**名称**(空则按位置)配对;若某条套餐**改名**,会误判为「删除旧名 + 新增新名」而非「变更」,改名本身不会进入逐字明细。如需把改名也识别为「变更」,需升级 `packageKey` 配对策略(名称变了但其余字段相近 → 视为变更)。
## 验收
- [ ] 某门店有待审套餐变更时:门店详情-套餐页签顶部出现黄色「有待审核套餐」提醒,且「审核 / 对比」可点。
- [ ] 点「审核」或「对比」均跳到套餐审核页并自动打开该门店变更抽屉,可见线上 vs 待审对照与通过/驳回。
- [ ] 门店列表操作栏对该门店出现「审核套餐」「对比」按钮,点击同样跳转并自动打开抽屉。
- [ ] 在审核页完成审核后,返回门店详情-套餐页签,提醒消失(或被事件即时刷新)。
- [ ] 无待审套餐的门店:列表与详情页签均不出现上述入口。
- [ ] 抽屉中「变更」套餐的「待审核」卡片底部出现「变更明细」:价格/图片以 `旧 → 新` 展示;菜品内容/可用时间/其他说明/套餐名称以**逐字差异**展示(原行红删、新行绿增),能精确定位到具体改动的字。
@@ -0,0 +1,128 @@
# 杜康好客 · v3.4.18 门店体验与登录态优化
> **2026-08-17** · mini-user `3.4.18` / h5-partner / h5-shop / API
> 目标:用户小程序端门店电话与套餐图体验优化;城市合伙人「暂停」账号禁止登录;门店端结算页留白对齐与「休息中能否开张核销」交互。
## 范围
| 项 | 交付 |
|----|------|
| A. mini-user 门店座机电话脱敏 | `maskPhone` 统一「中间四位隐藏」;门店详情已调用 |
| B. mini-user 套餐详情图 1/5 + 自动轮播 | `ProductCarousel` detail 变体 autoplay + 右下角 `1/5` 计数 |
| C. 合伙人账号暂停禁止登录(双拦截) | 账号 `status=DISABLED` 或主账号 `bindingStatus=PAUSED` → 登录/发码/微信均拒,提示「该账号已暂停使用」或「该合伙人合作已暂停」 |
| D. 门店端 申请提现 / 筛选栏留白对齐 | `WithdrawPage` 左右内边距统一 `--space-page` |
| E. 门店端 门店休息中 → 是否开启营业 | 门店 PAUSED 时点击首页「扫码核销」即弹「是否开启营业?」,不进入扫码流程 |
| F. mini-user 门店套餐图片自适应完整显示 | `ProductCarousel` 新增 `imageFit="adaptive"`(widthFix + 动态高度),门店套餐详情不再裁剪 |
## A. mini-user 门店座机电话脱敏(中间四位隐藏)
门店对外电话 `store.phone`= `contactPhone ?? loginPhone`,见 `server/.../common/compat/v31-compat.ts``resolveStoreContactPhone`)在门店详情以「电话: {maskPhone(...)}」展示(`apps/mini-user/src/pages/store-detail/index.tsx` ~L393-402)。
- 脱敏规则(座机):`区号 + 本地号前 2 位 + **** + 本地号后 2 位`,隐藏本地号中间四位。
- `0379-12345678``0379-12****78`
- `010-87654321``010-87****21`
- 无分机同理;手机号仍按 `138****8000` 不变。
- 实现点:`apps/mini-user/src/lib/phone.ts``maskPhone` 座机分支(当前保留末 2~4 位)→ 改为保留首 2 + 末 2、中间以 `****` 替代。
- 拨号仍走 `toDialablePhone`(明文),不受脱敏影响。
- 无 Prisma / API 变更;仅前端 `maskPhone` 规则调整。
## B. mini-user 套餐详情图 1/5 计数 + 自动轮播
入口:`apps/mini-user/src/pages/store-package-detail/index.tsx` 渲染
`<ProductCarousel images={imageUrls} variant="detail" previewable />`,图片来自 `GET /stores/:id``packages[index].imageUrls`(最多 20 张,由 `normalizeStorePackageImageUrls` 归一化)。
- 组件:`apps/mini-user/src/components/ProductCarousel.tsx`
- `variant='detail'` 时给 `<Swiper>` 增加 `autoplay` + `interval`(建议 3500ms),仅 `slides.length > 1` 时生效(现有 `circular` 已满足)。
- 右下角叠加分页计数:`${activeIndex + 1}/${slides.length}`(白底圆角胶囊,绝对定位于 `.detail-carousel-wrap` 右下角);保留原居中圆点(dots)或改为以右下计数为主。
- 单图(`slides.length === 1`)不展示计数、不开轮播。
- 图片容器 `.store-package-detail-gallery` 预留右下角定位锚点(如需)。
- 无 API / schema 变更。
## C. 合伙人账号暂停禁止登录(双拦截:账号停用 + 合伙人绑定暂停)
> 两个状态字段、两个枚举,**不要混**:
> | 维度 | 字段 | 枚举 | 取值 |
> |------|------|------|------|
> | 登录账号启用状态 | `partner_account.status` | `AccountStatus` | `ACTIVE` / `DISABLED` |
> | 城市合伙人绑定状态 | `partner_account.bindingStatus` | `CityPartnerStatus` | `ACTIVE` / `PAUSED` |
> 列表(`admin/partners`)里能看到的是 `bindingStatus`;主账号自身 `status` 不在列表返回体(仅子账号 `children[].status` 有)。
- 拦截规则(**主账号、子账号都拦**):
1. 登录账号自身 `status !== 'ACTIVE'``DISABLED`,主账号 / 子账号均适用)→ 抛 `该账号已暂停使用,请联系客服人员`
2. 所属**主账号** `bindingStatus !== 'ACTIVE'``PAUSED`,城市合伙人绑定暂停)→ 抛 `该合伙人合作已暂停,请联系客服人员`
- 子账号的绑定状态以其**父主账号**为准(`resolvePrimaryAccount`)。
- 后端统一闸门:新增 `assertPartnerAccountActive(account)``auth.service.ts` ~L307 后),一次性校验以上两条;由以下入口复用:
- `assertPartnerAccountByPhone`~L320):被 `checkPartnerPhone``POST /partner/auth/phone/check`)、发码预检 `assertSmsSendAllowed`PARTNER_LOGIN / PARTNER_PROXY_ORDER 场景)调用 → 暂停账号在「手机号校验」阶段即被拦截,无法获取短信验证码。
- `loginPartner``POST /partner/auth/login/sms`~L1063 后)。
- `loginPartnerWechat``POST /partner/auth/login/wechat`~L1726 后)。
- 前端 `apps/h5-partner/src/pages/LoginPage.tsx`
- `formatPartnerError`~L69/ `formatWechatError`(~L84):命中「已暂停 / 已停用」分支映射 `该账号已暂停使用,请联系客服人员`;其余未匹配错误(含 `该合伙人合作已暂停…`)原样透出 `return text`
- 枚举现状:`AccountStatus``schema.prisma` ~L244)与 `CityPartnerStatus`(~L203)均为既有,本次**无任何 Prisma 迁移**。
- 统一文案:
- 账号停用:`该账号已暂停使用,请联系客服人员`
- 合伙人绑定暂停:`该合伙人合作已暂停,请联系客服人员`
## D. 门店端 申请提现 / 筛选栏左右留白对齐
页面:`apps/h5-shop/src/pages/WithdrawPage.tsx`(门店管理 / 结算提现)。
- 申请提现按钮 `.shop-withdraw-btn`~L135):当前 `width:100%; margin-top:12px`,置于 `.shop-records-main`(无左右 padding)内,贴边满宽。
- 下方筛选栏 `.shop-records-filters`~L147`position: sticky; top:0`)与 `.shop-records-status-chips`:当前左右 `padding:0`chips 贴屏幕边缘。
- 同页 `.shop-records-summary` / `.shop-records-list` / `.shop-records-list-head` 均使用页面级 token `var(--space-page)` 左右内缩。
- 改动:`.shop-records-filters` 增加 `padding: 0 var(--space-page)`(sticky 背景保留);申请提现按钮区域同样左右内缩 `var(--space-page)`(或其父容器加 `padding: 0 var(--space-page)`),使其与上下组件留白一致。仅样式调整,无逻辑 / API 变更。
## E. 门店端 休息中核销 → 是否开启营业
现状:`PhoneRedeemPage.tsx` / `RedeemConfirmPage.tsx` 拉取 `GET /shop/store``status !== 'OPEN'``storeClosed=true`,核销前拦截并报「门店未营业,无法核销」(服务端 `server/.../modules/redeem/redeem.service.ts``loadOpenStoreAccount` ~L117 亦硬校验「门店未营业」)。
- **前置拦截(主路径 · 门店端首页 `HomePage`**:门店状态非 `OPEN`(PAUSED 临时闭店 / 休息中)时,用户点击首页「扫码核销」按钮**立即**弹确认框「门店目前休息中无法核销,是否开启营业?」,**不进入扫码流程**:
- 确认 → 调用 `PUT /shop/store/status { status: 'OPEN' }`,成功后关闭弹窗并 `loadDashboard()` 刷新门店状态为营业中,用户可再次点击扫码。
- 取消 → 关闭弹窗,维持拦截。
- **确认页兜底(次路径 · `RedeemConfirmPage`)**:若直接带核销码进入确认页且门店仍非 `OPEN`,点击「确认核销」时同样弹「是否开启营业?」,开张后重新拉取预览并继续核销(`doConfirm`)。
- 边界:门店 `auditStatus !== 'APPROVED'``updateShopStatus` 会拒绝开张(抛「门店尚在总部审核中 / 审核未通过」),前端需捕获并提示该错误,不进入误开启。
- 适用页:`HomePage`(门店端首页扫码入口,PAUSED 时点击即弹窗)、`RedeemConfirmPage`(扫码核销确认页兜底)、`PhoneRedeemPage`(手机号核销)。
- 后端无需改动(开张接口与硬校验已存在)。
## 关键接口 / 文件
| 位置 | 说明 |
|------|------|
| `apps/mini-user/src/lib/phone.ts` `maskPhone` | 座机脱敏规则改为「中间四位隐藏」 |
| `apps/mini-user/src/pages/store-detail/index.tsx` | 门店详情电话展示(已用 maskPhone) |
| `apps/mini-user/src/components/ProductCarousel.tsx` | detail 变体 autoplay + 右下 `1/5` 计数 |
| `apps/mini-user/src/pages/store-package-detail/index.tsx` | 套餐详情图渲染 |
| `GET /stores/:id` | 门店 / 套餐数据(无变更) |
| `server/.../modules/iam/auth.service.ts` `loginPartner` / `assertPartnerAccountByPhone` / `assertSmsSendAllowed` | 合伙人登录 / 发码非 ACTIVE 抛「该账号已暂停使用,请联系客服人员」 |
| `apps/h5-partner/src/pages/LoginPage.tsx` `formatPartnerError` / `formatWechatError` | 暂停文案 |
| `apps/h5-shop/src/pages/WithdrawPage.tsx` + `styles.css` `.shop-records-filters` / `.shop-records-status-chips` / `.shop-withdraw-btn` | 左右留白对齐 `--space-page` |
| `apps/h5-shop/src/pages/PhoneRedeemPage.tsx` / `RedeemConfirmPage.tsx` | 休息中弹「是否开启营业?」 |
| `apps/h5-shop/src/pages/StatusPage.tsx` `requestToggle` / `confirmToggle` | 复用开张调用 |
| `PUT /shop/store/status` `GET /shop/store` | 切换营业 / 读取门店(已存在) |
## F. mini-user 门店套餐图片自适应完整显示(不裁剪)
门店套餐详情(`apps/mini-user/src/pages/store-package-detail/index.tsx`)的 `ProductCarousel` 此前用 `variant="detail"` 默认 `imageFit="cover"`= `aspectFill`),配合 `.detail-carousel-wrap` 的固定 `aspect-ratio: 1``overflow:hidden`,非正方形图片被裁切 → 用户反馈「图片没显示全」。
- 新增 `imageFit="adaptive"` 模式(仅作用于门店套餐详情):
- `Image` 改用 `mode="widthFix"`,按图片真实比例缩放、完整显示、不裁剪。
- 组件挂载时实测容器宽度(`Taro.createSelectorQuery().select('.detail-carousel-wrap--adaptive').boundingClientRect`),图片 `onLoad` 拿到自然宽高后按 `容器宽 × (自然高/自然宽)` 计算每张幻灯片渲染高度,赋给 `<Swiper>` 内联 `height`,实现轮播高度自适应(多图比例不一也能逐张适配,带 0.2s 过渡)。
- 未加载前兜底高度为 `容器宽 × 0.75`(约 3:4)。
- 样式:`product-detail.css` 增加 `.detail-carousel-wrap--adaptive`,覆盖基类固定 `aspect-ratio:1``overflow:hidden``aspect-ratio:auto; overflow:visible`),并令 `.detail-carousel-image` 高度为 `auto`
- `autoplay`3.5s/ `1/5` 计数(feature B)在 adaptive 下保持不变。
- 仅门店套餐详情传 `imageFit="adaptive"`;门店头图(`variant="store"``contain`)、商品详情(`cover`)不受影响。
## 验收
- [ ] 门店详情座机显示形如 `0379-12****78`(中间四位隐藏),手机号仍 `138****8000`;拨打为明文。
- [ ] 套餐详情图多张时右下角显示 `1/5` 分页计数,并自动轮播(约 3.5s 切换);单图不计数、不轮播;点击仍可预览。
- [ ] 合伙人**账号** `status=DISABLED`(主账号或子账号)时:短信登录与微信登录均被拦截,提示「该账号已暂停使用,请联系客服人员」,无法进入。
- [ ] 合伙人**绑定** `bindingStatus=PAUSED`(主账号)时:无论用主账号还是其任一子账号登录,均被拦截,提示「该合伙人合作已暂停,请联系客服人员」。
- [ ] 以上拦截在「手机号校验(`phone/check`)」阶段即生效,暂停账号拿不到短信验证码。
- [ ] 门店管理(结算提现)页:申请提现按钮与筛选栏左右留白与其他区块一致(统一 `--space-page`),不再贴边。
- [ ] 门店休息中(PAUSED)时:点击门店端首页「扫码核销」按钮**立即**弹「门店目前休息中无法核销,是否开启营业?」,**不进入扫码流程**;确认开启后门店状态刷新、可再次扫码;取消则维持拦截;未过审门店开张被拒时给出对应提示。
- [ ] 门店套餐详情图片按真实比例完整显示、不再被裁切(轮播高度随图自适应);自动轮播与 `1/5` 计数仍正常。
- [ ] A 仅 `maskPhone` 规则、C/D/E 仅前端样式 / 交互;均无需 Prisma 迁移(C 复用 `DISABLED`)。
## HQ 开发计划
创建版本 `v3.4.18` 并关联本迭代任务;发版前再合并发布(mini-user 升 `3.4.18`)。
@@ -0,0 +1,663 @@
# 杜康好客 v3.5.1 开发方案
> 9 个修改点 · 涉及 4 端(admin-web / h5-shop / mini-user / h5-partner+ 1 后端
> **落地对照(已实现)**:[`杜康好客-v3.5.1-开发文档.md`](./杜康好客-v3.5.1-开发文档.md)
> 本文为开工方案与探索记录;与实现不一致时以落地对照为准。
> 协作:模块边界仍按 AGENTS.md`store` 模块 OWNER 为 B+D**当前由 jacy-dukang 全权**,可直接写 `store_*`/`redeem` 表
---
## 0. 总体一览
| 编号 | 模块 | 改动范围 | 估时 |
|------|------|----------|------|
| #1 | 大屏(订单轮播+脱敏) | 后端新增 + admin-web 新页 + OrdersPage 加按钮 | 0.5d |
| #2 | 门店端核销后即时刷新 | h5-shop RedeemSuccess/Mine/Withdraw 三处 | 0.3d |
| #3 | 用户端"发票抬头"列表 | 后端新表新接口 + mini-user 新 2 页 + 我的页入口 | 1d |
| #4 | 用户端订单"申请发票" | 后端新接口 + mini-user 订单列表/详情按钮 | 0.5d |
| #5 | 合伙人端门店编辑改"提交变更" | 后端新表新接口 + h5-partner StoreDetailPage | 1d |
| #6 | "套餐审核" 改 "审核通知" | admin-web Layout + StorePackageAuditsPage 改造 | 0.3d |
| #7 | 门店列表 提现/对比 按钮(信息变更) | admin-web StoresPage + 后端 admin list 返回字段 | 0.3d |
| #8 | 订单 30 分钟未支付自动取消 | 后端新增定时任务(复用 `payExpireAt` + 现有取消写法) | 0.5d |
| #9 | 城市合伙人/门店接口强校验 status+bindstatus,否则强制退登录 | 后端 `JwtAuthGuard` 注入状态校验 + `HttpExceptionFilter` 透传 + 两端前端跳登录 | 0.5d |
跨模块串联点:**#5 + #6 + #7** 共享同一个 `StoreInfoChangeRequest` 模型与 `AUDIT_NOTICE_CHANGED_EVENT`**#9** 串联后端守卫 + 前端两端 `api.ts` 的退出逻辑。
> **#8 / #9 补充说明**
> - #8:下单时 `trade.service.ts` 已写 `payExpireAt = now + 30min`line 206/1590/1935),支付/查询也已校验过期(line 2056/2145),但**缺少把 DB 状态翻成 `CANCELLED` 的定时任务**,本次补上。
> - #9:需求里的"门店 status + bindstatus"在模型上对应 **StoreAccount.status(门店账号启停,须 `ACTIVE`+ Store.status(门店启停,须 `OPEN`**"合伙人 status + bindstatus"对应 **PartnerAccount.status(须 `ACTIVE`+ PartnerAccount.bindingStatus(须 `ACTIVE`)**。两端不共用同一模型,校验需分别查。详见 §10。
---
## 1. 关键决策(已与用户对齐)
| 决策点 | 选择 |
|--------|------|
| 审核中心合并策略 | 合并套餐审核 + 信息变更 → 统一进「审核通知」 |
| 大屏实时性 | 3 秒 polling(无 SSE/WS |
| 审核通过生效方式 | 直接覆盖 `store` 表 + 审核中隐藏(前端展示 liveSnapshot |
| 申请发票范围 | 仅 `COMPLETED` 订单 |
---
## 2. 数据模型新增(共 2 张表)
### 2.1 `UserInvoiceTitle`(发票抬头)
```prisma
model UserInvoiceTitle {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @map("user_id") @db.UnsignedBigInt
titleType InvoiceTitleType @map("title_type") // PERSONAL | ENTERPRISE
titleName String @map("title_name") @db.VarChar(128)
taxNo String? @map("tax_no") @db.VarChar(32)
email String? @db.VarChar(128)
phone String? @db.VarChar(20)
addressPhone String? @map("address_phone") @db.VarChar(256)
bankAccount String? @map("bank_account") @db.VarChar(256)
isDefault Boolean @default(false) @map("is_default")
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
invoices UserInvoice[] // 反向关联,已有
@@index([userId, isDefault])
@@map("user_invoice_title")
}
```
> 同时在 `model User { ... }` 增加 `invoiceTitles UserInvoiceTitle[]`
### 2.2 `StoreInfoChangeRequest`(门店信息变更请求)
```prisma
enum StoreInfoChangeStatus { PENDING APPROVED REJECTED }
enum StoreInfoChangeSubmitterType { PARTNER SHOP HQ_DIRECT_ADMIN }
model StoreInfoChangeRequest {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
storeId BigInt @map("store_id") @db.UnsignedBigInt
status StoreInfoChangeStatus @default(PENDING)
/// liveSnapshot = 变更前 Store 全量快照;proposedSnapshot = 提单时希望变更的字段集合
liveSnapshot Json @map("live_snapshot")
proposedSnapshot Json @map("proposed_snapshot")
/// 可变字段白名单:name/contactPhone/address/intro/benefitUsageRule/coverUrl/envPhotoUrls/openTime/closeTime/openTime2/closeTime2/avgPrice
changedFields Json @map("changed_fields")
submitterType StoreInfoChangeSubmitterType @map("submitter_type")
submitterId BigInt @map("submitter_id") @db.UnsignedBigInt
rejectReason String? @map("reject_reason") @db.VarChar(512)
reviewedAt DateTime? @map("reviewed_at") @db.DateTime(3)
reviewerId BigInt? @map("reviewer_id") @db.UnsignedBigInt
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
@@index([storeId, status])
@@index([status, createdAt])
@@map("store_info_change_request")
}
```
> 同时在 `model Store { ... }` 增加 `infoChangeRequests StoreInfoChangeRequest[]`
**Prisma 迁移策略**
- 修改 `server/dukang-api/prisma/schema.prisma`
- 执行 `pnpm db:generate``cd server/dukang-api && npx prisma db push`
- 重启 `pnpm dev:api`
---
## 3. 后端新增/修改清单
### 3.1 新建 `modules/store/store-info-change.{controller,service}.ts`
| 路由 | 方法 | 角色 | 说明 |
|------|------|------|------|
| `POST /partner/stores/:storeId/info-change-request` | body: `{ fields: Partial<StoreInput> }` | PARTNER | 提交基本资料变更 |
| `GET /partner/stores/:storeId/info-change-requests` | — | PARTNER | 看本门店历史变更 |
| `POST /shop/store/info-change-request` | 同上 | SHOP | 门店端提交变更 |
| `GET /admin/store-info-change-requests?status=&page=` | — | HQ | 列表 |
| `GET /admin/store-info-change-requests/summary` | — | HQ | 返回 `pendingCount`(与套餐审核汇总汇总求和) |
| `GET /admin/store-info-change-requests/:id` | — | HQ | 详情(live + proposed diff |
| `PUT /admin/store-info-change-requests/:id/audit` | `{ action, rejectReason? }` | HQ | 通过/驳回 |
服务函数(`StoreInfoChangeService`):
```ts
submit({ storeId, actorId, actorType, fields }) {
// 1) 校验 store 归属
// 2) 拉 live = prisma.store.findUniquediff 出 changedFields
// 3) prisma.storeInfoChangeRequest.createlive/proposed JSON
// 4) 同 store 已有 PENDING 时直接替换(最新优先)
// 5) 触发 AUDIT_NOTICE_CHANGED_EVENT
}
audit({ id, action, reason, reviewerId }) {
if (action === APPROVED) {
// 把 proposedSnapshot 字段写到 store(白名单)
// store 通过 storeService.updateByHq()(如无此方法,需新建;不能直接 prisma 改 store
// store 信息变更后,partner 端 Strip Pending
}
// 写状态/驳回原因
// 触发 AUDIT_NOTICE_CHANGED_EVENT
}
```
---
### 3.2 新建 `modules/trade/invoice-title.{controller,service}.ts`
| 路由 | 方法 | 说明 |
|------|------|------|
| `GET /trade/invoice-titles` | — | 当前用户的抬头列表 |
| `POST /trade/invoice-titles` | body | 新建 |
| `PUT /trade/invoice-titles/:id` | body | 编辑;可同时 `isDefault=true` 触发其他置 false |
| `DELETE /trade/invoice-titles/:id` | — | 删除(已被 UserInvoice 引用的禁止删除,返回 409) |
| `GET /trade/orders/:orderId/invoice-titles` | — | 订单所有可用的抬头(用户的 + 默认) |
> 利用已有 `UserInvoice` 表;`UserInvoice.titleId` 可选填空(也可保留 `titleName/taxNo` 冗余快照——**建议保留**:每张发票保存当时的抬头快照,避免抬头被删后历史记录走样)
---
### 3.3 修改 `modules/trade/trade.controller.ts` — 新增(订单可用发票接口)
| 路由 | 方法 | 说明 |
|------|------|------|
| `GET /trade/orders/:orderId/invoice-status` | — | 返回当前订单是否已开票(`EXISTS \| NONE` |
| `GET /trade/invoices` | — | 我的发票申请列表 |
服务函数基于 `UserInvoice` 现有查询扩展。
---
### 3.4 修改 `modules/trade/admin-big-screen.controller.ts`**新建**
| 路由 | 方法 | 说明 |
|------|------|------|
| `GET /admin/orders/big-screen?limit=20` | — | 返回最近 N 条订单:订单号、金额、商品名/规格/数量、下单人 phone(**强制脱敏**)、下单时间 |
关键实现:
```ts
const orders = await prisma.order.findMany({
orderBy: { createdAt: 'desc' },
take: Math.min(Number(limit) || 20, 50),
include: { items: true, user: { select: { phone: true } } }
});
return orders.map(o => ({
id: o.id, orderNo: o.orderNo, payAmount: o.payAmount,
createdAt: o.createdAt,
userPhoneMasked: o.user?.phone ? maskPhone(o.user.phone) : null,
items: o.items.map(it => `${it.productName} × ${it.quantity}`).join(''),
}));
```
> 复用于 `packages/domain` 中已存在的 `maskPhone` 函数。新建 admin-big-screen.controller.ts 即可,不动 trade.controller.ts。
---
### 3.5 修改 `modules/store/store.controller.ts` 或 trade ops 模块
- 现有 `/partner/stores/:storeId/basic` (PUT) 是**直接生效**的,需保留为 backward-compat**新增** `/partner/stores/:storeId/info-change-request`(见 3.1
- 现有门店端 `/shop/store` (GET) 不变;本次不修改
---
### 3.6 shared-types 包
`packages/shared-types/src/invoice-title.ts` 新增:
```ts
export interface UserInvoiceTitleDto {
id: string; titleType: InvoiceTitleType; titleName: string;
taxNo?: string; email?: string; phone?: string;
addressPhone?: string; bankAccount?: string; isDefault: boolean;
}
export const INVOICE_CHANGEABLE_FIELDS = [
'titleName', 'taxNo', 'email', 'phone', 'addressPhone', 'bankAccount', 'isDefault'
] as const;
```
`packages/shared-types/src/store-info-change.ts` 新增:
```ts
export type StoreInfoChangeStatus = 'PENDING' | 'APPROVED' | 'REJECTED';
export type StoreInfoChangeSubmitterType = 'PARTNER' | 'SHOP' | 'HQ_DIRECT_ADMIN';
export interface StoreInfoChangeRequestDto { ... }
export interface StoreInfoChangeSummaryDto { pendingCount: number; }
export const STORE_INFO_CHANGEABLE_FIELDS = [
'name', 'contactPhone', 'address', 'intro', 'benefitUsageRule',
'coverUrl', 'envPhotoUrls', 'openTime', 'closeTime',
'openTime2', 'closeTime2', 'avgPrice'
] as const;
```
---
### 3.7 shared-events 全局事件
新增 `packages/shared-events/src/admin-events.ts` 或在 admin-web 的 `lib/admin-events.ts` 增加:
```ts
export const AUDIT_NOTICE_CHANGED_EVENT = 'dukang:audit-notice-changed';
export const STORE_REDEEM_SUCCESS_EVENT = 'dukang:shop-redeem-success';
```
---
### 3.8 新增 `jobs/order-expiry.scheduler.ts`#8 自动取消过期订单)
> 关键事实:下单时 `trade.service.ts` 已写 `payExpireAt = now + 30min`,支付/查询也已校验过期,但**没有定时任务**真正把 `status` 翻成 `CANCELLED`。后端 `src/jobs/` 已用 `@Cron``@nestjs/schedule`)范式(`settlement.scheduler.ts`),`ScheduleModule` 已在 `jobs.module.ts` 注册。
**新建 `server/dukang-api/src/jobs/order-expiry.scheduler.ts`**
```ts
@Injectable()
export class OrderExpiryScheduler {
private readonly logger = new Logger(OrderExpiryScheduler.name);
constructor(private readonly trade: TradeService) {}
// 每分钟扫描一次;订单量不大,1 分钟粒度足够"30 分钟"语义
@Cron('*/1 * * * *', { timeZone: 'Asia/Shanghai' })
async handleExpiredPendingOrders() {
try {
const n = await this.trade.cancelExpiredPendingOrders(200);
if (n > 0) this.logger.log(`Auto-cancelled ${n} expired pending orders`);
} catch (e) {
this.logger.error('Order expiry job failed', e instanceof Error ? e.stack : e);
}
}
}
```
**`TradeService.cancelExpiredPendingOrders(limit = 200)`(新增方法)**
```ts
async cancelExpiredPendingOrders(limit = 200): Promise<number> {
const expired = await this.prisma.order.findMany({
where: {
status: 'PENDING_PAY',
payStatus: 'UNPAID',
payExpireAt: { lt: new Date() },
isTest: false, // 跳过测试订单,避免污染数据
},
take: limit,
select: { id: true },
});
for (const o of expired) {
await this.prisma.$transaction(async (tx) => {
await tx.order.update({
where: { id: o.id },
data: { status: 'CANCELLED', cancelledAt: new Date() },
});
await tx.commonEvent.create({
data: buildOrderStatusEvent({ // 复用现有导入(见 line 1810
orderId: o.id, fromStatus: 'PENDING_PAY', toStatus: 'CANCELLED',
operator: 'SYSTEM_AUTO_EXPIRE', remark: '30 分钟未支付自动取消',
}),
});
});
}
return expired.length;
}
```
**注册**`server/dukang-api/src/jobs/jobs.module.ts``providers` 增加 `OrderExpiryScheduler``TradeModule` 已 import,无需改 imports)。
> **待确认**:取消时是否需回滚已占用的 `benefitCoupon`?现有 `cancelPartnerProxyOrder`line 1791)仅翻状态、未回滚权益券。若业务要求自动取消也释放权益,此处需补 `benefitCoupon.update({ status: 'ACTIVE' })`。
---
### 3.9 修改 `JwtAuthGuard` + `HttpExceptionFilter`#9 强校验,详见 §10
> 完整探查见 §10。要点:
- **`server/dukang-api/src/common/guards/jwt-auth.guard.ts`**
- `canActivate` 改为 `async`;注入 `PrismaService`
-`req.user` 组装完成后,按 `actorType` 分支(**仅 STORE / PARTNERUSER 跳过**):
- `STORE`:查 `storeAccount(actorId)``status``ACTIVE`;若 `req.user.storeId` 存在,查 `Store``status``OPEN`
- `PARTNER`:查 `partnerAccount(actorId)``status === 'ACTIVE'``bindingStatus === 'ACTIVE'`
- 不通过 → `throw new ForbiddenException({ reason: 'ACCOUNT_DISABLED', message: '账号已停用或解绑,请重新登录' })`
- 覆盖率 100%`/shop/*``/partner/*` 全部经过 `JwtAuthGuard`(含仅用 `JwtAuthGuard` 的合伙人路由);登录/refresh 走 `OptionalJwtAuthGuard`,不受影响。
- **`server/dukang-api/src/common/filters/http-exception.filter.ts`**line 58-60 的 `response.status(status).json({...})`):
- 增加透传 `reason` 字段:`reason: (res && typeof res === 'object' && (res as any).reason) ?? null`,使前端能精确识别"账号停用"。
---
## 4. 前端新增/修改清单
### 4.1 admin-web(管理后台)
| # | 文件 | 改动 |
|---|------|------|
| 1 | `src/pages/OrdersPage.tsx` | 头部加 `<Button>` "大屏" → `window.open('/orders/big-screen?token=xxx','_blank')` |
| 2 | `src/pages/BigScreenPage.tsx`(**新建**) | 暗色科技风,"杜康好客 · 发布仪式现场" + LIVE 标识 + 大时钟 + 订单表格轮播;3s polling |
| 3 | `src/App.tsx` | 加路由 `<Route path="/orders/big-screen" element={<BigScreenPage />} />` |
| 4 | `src/layouts/AdminLayout.tsx` | 菜单项 label `'套餐审核'``'审核通知'``attachPackageAuditBadge` 改名为 `attachAuditNoticeBadge`,订阅 `AUDIT_NOTICE_CHANGED_EVENT` + 调 `summary` 端点(含合并计数) |
| 5 | `src/pages/StorePackageAuditsPage.tsx` | 标题 `套餐变更审核``审核通知`;加 `<Tabs>`:套餐变更 / 门店信息变更;下方按 `kind` 调不同接口;详情抽屉兼容两类 |
| 6 | `src/pages/StoresPage.tsx` | 列表中加 `pendingInfoChangeId` 字段;操作列在已有 `pendingPackageAuditId` 旁补 `审核信息 / 对比`;提现按钮(指向 `/finance/store-bills?storeId=`)已存在,本次仅调整文案 |
> BigScreenPage 不依赖 AdminLayout(避免侧边栏干扰),可在路由外层做独立壳层。
---
### 4.2 h5-shop(门店端)
| # | 文件 | 改动 |
|---|------|------|
| 1 | `src/pages/RedeemSuccessPage.tsx` | 提交成功 onMounted 派发 `window.dispatchEvent(new CustomEvent(STORE_REDEEM_SUCCESS_EVENT, { detail: { storeId, amount }}))`;在 navigate(-1) 之前 |
| 2 | `src/pages/MinePage.tsx` | 监听 `STORE_REDEEM_SUCCESS_EVENT` → 重跑 `loadMine()`(已存在) |
| 3 | `src/pages/WithdrawPage.tsx` | 同上监听 → 重跑 `load()` |
| 4 | `src/lib/useRedeemSuccessBus.ts`**新建** | 简单 hook`useRedeemSuccessListener(handler)` 复用 |
---
### 4.3 mini-user(用户小程序)
| # | 文件 | 改动 |
|---|------|------|
| 1 | `src/pages/mine/index.tsx` | SERVICES 数组加一项 `{ icon: iconInvoice, label: '发票管理', url: '/pages/invoice-titles/index' }` |
| 2 | `src/pages/invoice-titles/index.tsx`(**新建**) | 列表(默认抬头星标 / 编辑 / 删除 / 设为默认)+ 进入"申请发票"快捷入口 |
| 3 | `src/pages/invoice-title-edit/index.tsx`(**新建**) | 编辑/新建抬头表单 |
| 4 | `src/pages/orders/index.tsx` | 仅 COMPLETED 订单下方加 `<View className="order-invoice-btn">申请发票</View>` → 跳发票申请页 |
| 5 | `src/pages/order-detail/index.tsx` | 订单状态 COMPLETED 时,详情页 actionbar 增加"申请发票"按钮 |
| 6 | `src/pages/invoice-apply/index.tsx`**新建** | 复用 SubPageHeader 组件;展示该订单明细 + 已开票则提示已存在 + 抬头选择器 + 提交 |
| 7 | `src/app.config.ts` | pages 数组增加 3 个新页(`invoice-titles/index``invoice-title-edit/index``invoice-apply/index` |
| 8 | `src/lib/api.ts` | 增加 `requestInvoiceTitles()``submitInvoiceApplication()` 等 helper |
---
### 4.4 h5-partner(合伙人端)
| # | 文件 | 改动 |
|---|------|------|
| 1 | `src/pages/StoreDetailPage.tsx` | 底部保存按钮 `保存修改``提交变更`;点击后调 `PUT /partner/stores/:id/info-change-request` 而不是 `/basic` |
| 2 | 同上 | 顶部加一个 `auditBanner`:当 `store.infoChangePending === true` 时提示"有变更待总部审核"(调 list 接口得到 PENDING |
| 3 | `src/lib/api.ts` | 加 `submitStoreInfoChangeRequest``listStoreInfoChangeRequests` |
> **兼容老路径**:保留 `/partner/stores/:id/basic`PUT)以兼容历史脚本;新按钮走新接口。
---
### 4.5 h5-shop / h5-partner 强制退出登录(#9,详 §10)
> 后端返回 `{ code: 403, reason: 'ACCOUNT_DISABLED', message }` 后,前端需 `clearAuth()` + 跳 `/login`。合伙人端现仅 401 跳登录、门店端缺此逻辑,两端都要补。
| # | 文件 | 改动 |
|---|------|------|
| 1 | `apps/h5-shop/src/lib/api.ts` | `rawRequest`~191-227)抛错时附加 `err.reason = json.reason ?? null``requestWithAuthRetry` 的不可恢复分支(~262 前)与 `ensureSession` catch~305)增加 `if (err.reason === 'ACCOUNT_DISABLED') { clearAuth(); window.location.href = '/login'; }` |
| 2 | `apps/h5-partner/src/lib/api.ts` | `rawRequest` 同样附加 `err.reason`;在现有 401 重定向分支旁增加 `reason === 'ACCOUNT_DISABLED'``clearAuth() + window.location.href = toAppPath('/login')`(复用既有登录路由) |
> 注意:`ACCOUNT_DISABLED` 用专属 `reason` 字段而非复用 403,避免与未来其它 403(权限不足)混淆;`code` 仍为 403 不破坏现有 `err.status` 判断。
---
## 5. 大屏页面设计要点(#1
```tsx
// BigScreenPage.tsx 骨架(admin-web 不依赖 AntD
export default function BigScreenPage() {
const [items, setItems] = useState<BigScreenOrder[]>([]);
useEffect(() => {
const fetchData = () => request<{ items: BigScreenOrder[] }>(
'/admin/orders/big-screen?limit=20'
).then(d => setItems(d.items));
fetchData();
const id = setInterval(fetchData, 3000);
return () => clearInterval(id);
}, []);
return (
<div className="big-screen-page"> {/* 全屏暗色 */}
<header>
<span className="title"></span>
<span className="subtitle"></span>
<span className="live"> LIVE</span>
</header>
<Clock /> {/* setInterval(每秒) */}
<table>
<thead>
<tr><th></th><th></th><th></th><th></th></tr>
</thead>
<tbody>
{items.map(o => (
<tr key={o.id}>
<td>¥{o.payAmount.toLocaleString()}</td>
<td>{o.items}</td>
<td>{formatHM(o.createdAt)}</td>
<td>{o.userPhoneMasked}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
```
CSS 暗色 + 蓝色辉光 + 全屏(`h1 字号 96px、background #001529`),仿参考图。
---
## 6. 关键交互流程图
### 6.1 #1 大屏实时轮播
```
[admin BigScreenPage mounted]
request GET /admin/orders/big-screen?limit=20
[Backend] findMany Order desc createdAt, take 20, mask phone
setInterval(3000) → 重复
```
### 6.2 #2 门店端核销即时刷新
```
[RedeemSuccessPage] onMount → window.dispatchEvent(STORE_REDEEM_SUCCESS_EVENT)
[MinePage useEffect] 收到 → setLoading → request /shop/store
[WithdrawPage useEffect] 收到 → request /shop/withdraw/summary
```
### 6.3 #5/#6/#7 审核通知闭环
```
[Partner StoreDetailPage] 点击"提交变更"
PUT /partner/stores/:id/info-change-request {fields}
[Backend] 写入 StoreInfoChangeRequest + 触发 AUDIT_NOTICE_CHANGED_EVENT
[StoreDetailPage] 显示"有变更待总部审核"banner
[admin StorePackageAuditsPage] 进入 → 切到「门店信息变更」Tab → 列表
[admin 点"通过"] → PUT .../audit {action:APPROVE}
[Backend] proposedSnapshot 写入 store → 触发 AUDIT_NOTICE_CHANGED_EVENT
[StoreDetailPage] banner 消失
```
---
## 7. 验证 DoD
- [ ] `pnpm db:generate && db:validate && npx prisma db push` 无报错
- [ ] 现有 admin/stores 接口能取到 `pendingInfoChangeId` 字段;旧调用方不报错
- [ ] 订单「申请发票」按钮仅在 `COMPLETED` 时出现;提交后调用 `/trade/orders/:id/invoices`
- [ ] 门店端在 A 设备核销 → B 设备 MinePage(如多 Tab)≤3s 看到变化(A 设备本身立即)
- [ ] 大屏 3s 内出现新单;手机号中间 4 位为 `****`
- [ ] 合伙人提交门店变更后,门店 C 端/合伙人端展示 liveSnapshot(不立即生效),admin 端能审核通过/驳回
- [ ] 审核通知 badge = 套餐变更 PENDING + 信息变更 PENDING
- [ ] #8:构造 `payExpireAt` 已过期的待支付订单,≤1 分钟内状态自动变 `CANCELLED``cancelledAt` 写入、状态日志 `SYSTEM_AUTO_EXPIRE` 存在
- [ ] #9:将某门店 `StoreAccount.status` 置非 `ACTIVE`(或 `Store.status``OPEN`),其门店端任意接口返回 `reason: ACCOUNT_DISABLED`,前端立即 `clearAuth()` 并跳 `/login`
- [ ] #9:将某合伙人 `bindingStatus` 置非 `ACTIVE`,合伙人端任意接口触发强退登录;`mini-user`USER actor)不受影响
- [ ] `pnpm lint && pnpm test` 无新增错误
---
## 8. 任务拆分(建议 DLV 命名)
```
DLV-W1-M1 #1 大屏(后端 + admin-web 新页)
DLV-W1-M2 #2 门店端核销事件总线
DLV-W1-M3 #6 审核通知菜单改 + Tabs 改造(先做,让 #5/#7 共享 UI)
DLV-W1-M4 #5 合伙人端门店"提交变更"StoreInfoChangeRequest 全套)
DLV-W1-M5 #7 门店列表 提现/对比 按钮(信息变更)
DLV-W1-M6 #3 发票抬头 mini-user 列表 + 编辑
DLV-W1-M7 #4 订单申请发票(依赖 M6)
DLV-W1-M8 #8 订单 30 分钟未支付自动取消(后端定时任务,独立)
DLV-W1-M9 #9 城市合伙人/门店接口强校验 + 前端强制退登录
```
依赖关系:M1 独立;M2 独立;M3 无依赖;M4 / M5 依赖 M3M6 无依赖;M7 依赖 M6;M8 独立;M9 独立(后端守卫 + 前端两端,互不阻塞)。
---
## 9. 待确认的设计细节(如需开工前敲定)
1. 发票抬头列表页是否需要"设为默认"按钮?
2. 门店信息变更驳回后,原表单是否需要提示驳回原因?
3. 大屏是否需要支持"暂停轮播"按钮?
4. 用户端 `My → 发票管理` 是否需要再放"申请记录"二级入口?
5. **#8 权益回滚**:自动取消订单时是否需要把已占用的 `benefitCoupon` 置回 `ACTIVE`?(现有手动取消 `cancelPartnerProxyOrder` 未回滚,建议保持一致或明确要回滚)
6. **#8 扫描粒度**:每分钟一次(`*/1 * * * *`)是否可接受?还是希望 2~5 分钟一次?
7. **#9 校验范围**:需求只提"城市合伙人 + 门店"。是否也要覆盖 `h5-shop` 的**城市合伙人代运营账号**`proxyPartnerAccountId` 场景)?目前按"仅 STORE + PARTNER actor"实现。
8. **#9 提示文案**:强退登录时前端是否需要 toast 提示"账号已停用/解绑,请重新登录"再跳登录页,还是静默跳回?
9. **#9 测试订单**`isTest` 门店/合伙人账号是否要跳过强校验(避免测试环境自锁)?
---
## 10. 状态校验能力探索报告(城市合伙人 / 门店 接口强制退出到登录页)
> 本需求:所有 `/shop/*` 与 `/partner/*` 接口需校验 status / bindstatus 是否为开启,否则强制前端退到登录页。以下是按代码实际探查的结果与落地建议。
### 10.1 模型字段(server/dukang-api/prisma/schema.prisma
- **Store**line 1245):只有 `status StoreStatus @default(PAUSED)`line 1268),**没有 bindstatus 字段**。
- `enum StoreStatus`line 282):`OPEN / PAUSED / CLOSED`。"开启" = `OPEN`
- **PartnerAccount**line 1030):`status AccountStatus @default(ACTIVE)`line 1042+ `bindingStatus CityPartnerStatus? @default(ACTIVE) @map("binding_status")`line 1049)。
- `enum AccountStatus`line 244):`ACTIVE / DISABLED`
- `enum CityPartnerStatus`line 203):`ACTIVE / PAUSED`。"已绑定/开启" = `ACTIVE`
- **StoreAccount**(门店登录账号,line 1365):`status AccountStatus @default(ACTIVE)`(line 1378)——这是门店端真正的"停用开关",登录时已被校验(`auth.service.ts:1050` `if (account.status !== 'ACTIVE') throw new BadRequestException('门店账号已停用')`)。
- 结论:需求中的"门店 status + bindstatus"在模型上对应 **StoreAccount.status(账号启停)+ Store.status(门店启停 OPEN**"合伙人 status + bindstatus"对应 **PartnerAccount.status + PartnerAccount.bindingStatus**。两端不共用同一模型。
### 10.2 后端鉴权守卫(server/dukang-api/src/common/guards/
- 全局错误格式 `http-exception.filter.ts:58-62``{ code: status, message, data: null }`;成功 `response.interceptor.ts:13-17``{ code:0, message:'ok', data }`。注册于 `main.ts:87-88`
- **JwtAuthGuard**jwt-auth.guard.ts:21-56):校验 Bearer + `x-client-app` + actorType,写入 `req.user: AuthUser{actorType, actorId, clientApp, storeId?}`。**无 DB 查询、无 status 校验**。
- 路由守卫分布:
- `/shop/*` 统一 `@UseGuards(JwtAuthGuard, ShopStoreGuard)`(如 store.controller.ts:186-187、settlement.controller.ts:48-49、redeem.controller.ts:54-55)。
- `/partner/*` 部分 `@UseGuards(JwtAuthGuard, PartnerPrimaryGuard/PartnerPermissionGuard)`,也有**仅** `@UseGuards(JwtAuthGuard)` 的(settlement.controller.ts:573 `partner/me`、store.controller.ts:75/86、store-package.controller.ts:8)。
- 现有守卫**均未校验 status/bindstatus**(运行时):
- `ShopStoreGuard`shop-store.guard.ts:1-15):仅校验 `user.storeId` 存在。
- `PartnerPrimaryGuard`partner-primary.guard.ts:11-32):查 `partnerAccount` + 校验 `isPrimary==1`
- `PartnerPermissionGuard`partner-permission.guard.ts:14-45):查 `partnerAccount` + 校验权限。
- `ShopPrimaryGuard`shop-primary.guard.ts):查 `storeAccount.isPrimary`
- **关键缺口**:登录后账号被停用/解绑,运行时接口不拦截(仅登录时校验过 StoreAccount.status)。
### 10.3 前端登录态与退出
- **h5-shop/src/lib/api.ts**token 存 localStorageACCESS_TOKEN/REFRESH_TOKEN),profile 存 STORE_PROFILE`clearAuth`136-145)清 token。
- `rawRequest`191-227):`json.code !== 0` 抛 Error`err.status = (>=500?res.status:json.code)`401→401403→403)。
- `requestWithAuthRetry`249-270):仅 `err.status===401` 且非豁免路径时尝试 refresh;失败则 `clearAuth()` 并 throw。**shop 端 request 没有跳登录页逻辑**。
- 登录路由 `/login`AuthGate.tsx:5、多处 `navigate('/login')`)。
- **h5-partner/src/lib/api.ts**:结构同构;`request`217-242)在 `err.status===401` 且本地有 token 时 `clearAuth()` + `showPartnerToast` + `window.location.href = toAppPath('/login')`(不在 /login 时)。**合伙人端已具备"401 强制跳登录"**。
- 登录路由 `toAppPath('/login')`
- 结论:partner 端天然可接 401 强退;shop 端缺这一步。
### 10.4 请求封装
- 两端共用 fetch 封装(rawRequest / requestWithAuthRetry / request),无 axios。业务 code 与 HTTP 码复用同一 `code` 字段(filter 把 HTTP status 当 code)。
- `requestWithAuthRetry` 只对 401 重试,**对 403/自定义 code 直接 throw → 不会触发 refresh 死循环**(这是设计专属 code 的基础)。
### 10.5 落地建议
**后端**:在 `JwtAuthGuard.canActivate` 内(或新建 `AccountActiveGuard` 全局)按 actorType 分支做 DB 校验:
- `STORE`:查 `StoreAccount(actorId)``status` 必须 `ACTIVE`;若 `user.storeId` 存在,查 `Store``status` 必须 `OPEN`
- `PARTNER`:查 `PartnerAccount(actorId)``status===ACTIVE``bindingStatus===ACTIVE`
-**`ForbiddenException` 携带专属业务 code(如 `ACCOUNT_DISABLED = 499`)** 而非 401,避免误触前端 refresh 重试。需扩展 `HttpExceptionFilter` 透传该 bizCode。
- 选 JwtAuthGuard 内部做可保证 100% 覆盖(所有 /shop、/partner 都过它,含仅用 JwtAuthGuard 的 partner/me);`USER` actorType 跳过,不影响 C 端/小程序。
**前端**:当 `json.code === ACCOUNT_DISABLED` 时,h5-partner 与 h5-shop 均 `clearAuth()` + 跳 `/login`partner 已在 401 分支,shop 需补 redirect)。若不想改 filter,可复用 403:但 partner 现只对 401 跳、对 403 不跳,故**专属 code 最稳**。
### 10.6 风险点
- **性能**:每个 /shop、/partner 请求多 1 次 DB 查询;用 `select` 仅取 status 字段即可,流量低可接受。
- **登录流程不受影响**`/shop/auth/login``/shop/auth/token/refresh` 等用 OptionalJwtAuthGuard 或无 guard,不会被拦;但 `/shop/auth/me``select-store` 用 JwtAuthGuard,停用账号访问会被拦→前端跳登录(符合预期)。
- **范围**HQ/admin 走 HqAuthGuard、mini-user 为 USER actor,均不受影响(按需求只做 shop + partner)。
- **门店校验建议同时覆盖 StoreAccount.status 与 Store.status**,否则只停门店不停账号仍可操作。
---
## 11. 订单 30 分钟未支付自动取消 — 代码探索报告
> 本报告为 v3.5.1 功能「订单 30 分钟未支付自动改为取消」的实现前探索,供开发方案设计参考。
## 1. 订单状态枚举
- Prisma 定义 `server/dukang-api/prisma/schema.prisma:321-331`
```prisma
enum OrderStatus {
PENDING_PAY // 待付款
PENDING_SHIP
OUT_WAREHOUSE
SHIPPING
PENDING_RECEIVE
COMPLETED
CANCELLED // 已取消
REFUNDING
REFUNDED
}
```
- TS 同步定义 `packages/shared-types/src/enums.ts:71-81`,字符串值完全一致:
`PENDING_PAY = 'PENDING_PAY'``CANCELLED = 'CANCELLED'`。
- 待付款 = `PENDING_PAY`;已取消 = `CANCELLED`(无 CLOSED / UNPAID 形态,PayStatus 才是 UNPAID)。
## 2. 订单创建流程
- `TradeService.createOrder` `src/modules/trade/trade.service.ts:153`;初始 `status: 'PENDING_PAY'`:232)、`payStatus: 'UNPAID'`:233)。
- 已写入支付截止时间:`const payExpireAt = new Date(Date.now() + 30 * 60 * 1000)`:206),并落库 `payExpireAt`:264)。
- 三条建单路径均已设置该字段:`createOrder`:206)、`createPartnerProxyOrder`:1590)、`createHqProxyOrder`:1935)。
- 下单时间 `createdAt``schema.prisma:1400/1473`);支付时间 `paidAt`:1455);取消时间 `cancelledAt`(:1458,已存在可复用,无需新增字段)。
## 3. 现有定时任务机制
- 已启用 `@nestjs/schedule``src/jobs/jobs.module.ts:13` `ScheduleModule.forRoot()`,并注册 BullMQ `DELIVERY_QUEUE``jobs.constants.ts`,用于 `delivery.processor.ts`)。
- `@Cron` 完整示例 `src/jobs/settlement.scheduler.ts`
```ts
@Cron('0 8 * * *', { timeZone: 'Asia/Shanghai' })
async handleDailyBills() {
try { /* ... */ }
catch (e) { this.alert.notify({ level: 'P0', category: 'job', title: '...', dedupeKey: '...' }); }
}
```
- **关键缺口**`src/jobs/monitor.scheduler.ts:54` `scanStuckOrders()` 已每 5 分钟(`*/5 * * * *`)扫描超时未付款订单,但**只 alert 不取消**(:61-81)。这正是自动取消要补的环节。
- `payExpireAt` 字段已存在,**无需新增迁移**;但 Order 表无 `(status, payExpireAt)` 复合索引(现有索引见 `schema.prisma:1488-1497`)。
## 4. 取消订单的业务影响
- 现有取消入口仅 `cancelPartnerProxyOrder`:1791,限 PROXY 单):事务内 `update status: 'CANCELLED', cancelledAt: new Date()`:1807+ 写 `commonEvent` 状态日志(:1809)。
- 通用状态机 `applyStatusTransition`:1324):翻 `order.status` + 写 `commonEvent``buildOrderStatusEvent`),但**不处理 `cancelledAt` / `payStatus`**。
- grep 确认建单与取消均**无库存占用/权益发放**(无 stock/inventory/decrement/release 调用,仅 `benefitCoupon` 出现在查询 include)。→ 自动取消只需翻状态+写日志,**无需回滚库存/释放权益**。
- 建议:新增 `TradeService.autoCancelExpiredOrders()`,复用 `applyStatusTransition` 但补 `cancelledAt` 写入(或在内联事务中同时置 `status/ cancelledAt/ payStatus`)。
## 5. 查询未支付订单
- 现成写法(`monitor.scheduler.ts:61`):
```ts
this.prisma.order.findMany({
where: { status: 'PENDING_PAY', payExpireAt: { lt: now } },
select: { orderNo: true }, take: sample, orderBy: { payExpireAt: 'asc' },
});
```
- 建议加 `payStatus: 'UNPAID'` 过滤更稳妥;字段即 `payExpireAt``DateTime?`)。扫描前建议新增 `@@index([status, payExpireAt])`。
## 6. 实现建议
- 在 `MonitorScheduler` 新增 `@Cron`(如每 1 分钟 `*/1 * * * *`)或扩展 `scanStuckOrders`,调用 `TradeService.autoCancelExpiredOrders()`:分页取 `status:PENDING_PAY && payExpireAt<now`,对每个订单用**条件更新** `updateMany({ where: { id, status: 'PENDING_PAY' }, data: { status: 'CANCELLED', cancelledAt: now } })`,并写 `commonEvent`operator=`'SYSTEM_AUTO_CANCEL'`)。
- 状态枚举引用:service 层当前直接用字符串 `'PENDING_PAY'`/`'CANCELLED'`(也可 `import { OrderStatus } from 'shared-types'`,二者值一致)。
- 失败处理沿用现有 `alert.notify` 模式(level P0/P1 + dedupeKey)。
## 7. 风险点
1. **分布式锁/幂等**:多实例下 `scanStuckOrders` 当前无锁(`monitor.scheduler` 已注入 `RedisService`,可加 Redis 分布式锁防重复执行);`where:{status:'PENDING_PAY'}` 条件更新保证幂等。
2. **无重复任务冲突**:当前仅 monitor 扫描告警,尚无真正取消任务,新增即补缺口,不会与既有任务重复。支付路径已在校验 `payExpireAt<now` 拒绝支付(:2053、:2142),与自动取消逻辑自洽。
3. **批量规模**:用 `take` 分页 + `orderBy payExpireAt asc`,勿一次性全量 update,避免长事务。
4. **索引缺失**:扫描前先加 `@@index([status, payExpireAt])`,否则全表扫描。
## 8. 关键文件清单
- `server/dukang-api/prisma/schema.prisma`OrderStatus:321Order 模型:1412-1499;索引:1488-1497
- `packages/shared-types/src/enums.ts`OrderStatus:71
- `server/dukang-api/src/modules/trade/trade.service.ts`createOrder:153payExpireAt:206/1590/1935applyStatusTransition:1324cancelPartnerProxyOrder:1791;支付过期校验:2053/2142
- `server/dukang-api/src/modules/trade/trade.controller.ts`cancel-pay:261
- `server/dukang-api/src/jobs/monitor.scheduler.ts`scanStuckOrders:54
- `server/dukang-api/src/jobs/settlement.scheduler.ts`@Cron 范例)
- `server/dukang-api/src/jobs/jobs.module.ts`、`jobs.constants.ts`、`delivery.processor.ts`
+171
View File
@@ -0,0 +1,171 @@
# 杜康好客 · v3.5.1 版本更新
> **2026-08-19** · admin-web / h5-shop / mini-user / h5-partner / API
> 工单方案(探索与拆分):[`杜康好客-v3.5.1-工单迭代开发文档.md`](./杜康好客-v3.5.1-工单迭代开发文档.md)
> 目标:发布会大屏、C 端发票抬头与申请、门店信息变更审核、待支付自动取消、门店/合伙人运行时强退登录。
## 范围
| # | 项 | 交付 |
|---|----|------|
| 1 | 发布会订单大屏 | `GET /admin/orders/big-screen`;独立全屏页 `/orders/big-screen`;订单页「大屏」入口;超管「测试」连播假单 |
| 2 | 门店端核销即时刷新 | `shop:redeem-success` 事件总线;Mine / 提现页监听后立刻重拉 |
| 3 | C 端发票抬头 | 表 `user_invoice_title` + CRUD;我的 → 发票管理;图标操作(编辑 / 默认 / 删除) |
| 4 | C 端申请发票 | 仅已完成订单;列表/详情「申请发票」与「开票中」互斥 |
| 5 | 合伙人门店「提交变更」 | 表 `store_info_change_request`;审核通过才覆盖线上门店 |
| 6 | 审核通知 | 原「套餐审核」菜单改为「审核通知」;套餐变更 + 信息变更双 Tabbadge 合计 PENDING |
| 7 | 门店列表快捷入口 | `pendingInfoChangeId`;「审核信息 / 对比」;「提现」进结算页 |
| 8 | 待支付 30 分钟自动取消 | Cron 每分钟;`payExpireAt` 已过期且非测试单 → `CANCELLED` |
| 9 | 门店 / 合伙人运行时强退 | `JwtAuthGuard` 校验启停;`reason: ACCOUNT_DISABLED`;两端清登录并跳登录页 |
**Prisma 迁移(发版不可 `--skip-db`**`user_invoice_title``store_info_change_request``user_order``@@index([status, payExpireAt])`
---
## 1. 发布会订单大屏
- 路由:`/orders/big-screen`**不走** AdminLayout(无侧栏)。
- 数据:`GET /admin/orders/big-screen?limit=`(上限 2000)。过滤 `isTest=false``payAmount >= 100`;手机号 `maskContactPhone` 中间四位 `****`
- 展示:一屏锁死、订单上滚;新成交按金额档位动效(首次进入不撒花)。
- 入口:订单监控「大屏」;超管另有「测试」,经 `BroadcastChannel` / `localStorage` 连播三级假单。
| 位置 | 说明 |
|------|------|
| `GET /admin/orders/big-screen` | `admin-orders.controller.ts`(须写在 `:id` 之前) |
| `apps/admin-web/src/pages/BigScreenPage.tsx` | 大屏页 |
| `apps/admin-web/src/lib/admin-events.ts` | 测试成交 payload |
| `apps/admin-web/src/pages/OrdersPage.tsx` | 「大屏」「测试」按钮 |
---
## 2. 门店端核销即时刷新
核销成功页 `onMounted` 派发 `shop:redeem-success`;「我的」与「申请提现」监听后重拉接口,同 Tab 立即看到余额变化。
| 位置 | 说明 |
|------|------|
| `apps/h5-shop/src/lib/useRedeemSuccessBus.ts` | `notifyRedeemSuccess` / `useRedeemSuccessListener` |
| `RedeemSuccessPage` / `MinePage` / `WithdrawPage` | 广播与刷新 |
---
## 3 / 4. C 端发票抬头与申请发票
**仅 `COMPLETED` 且非售后补发单**可申请。已有 `PENDING` / `ISSUED` 发票则不可再申(驳回后可重申)。
### 抬头
- 我的 → **发票管理**`/pages/invoice-titles/index`
- 卡片操作:`编辑.png` / `默认.png` / `删除.png`(无文字按钮)
- 新增/编辑走底部弹层(无独立编辑页)
- 小程序标题只用原生 `navigationBarTitleText`,不再画二级 title(见 `.cursor/rules/mini-user-weapp-nav-title.mdc`
### 申请
- 入口:订单列表卡片、订单详情底栏
- **互斥**:无进行中/已开具发票 →「申请发票」;状态 `PENDING` →「开票中」(点进申请页看进度);`ISSUED` 两者都不显示
- 列表接口带 `invoiceStatus`(最新一条发票)
| 接口 | 说明 |
|------|------|
| `GET/POST /trade/invoice-titles` | 列表 / 新建 |
| `PUT/DELETE /trade/invoice-titles/:id` | 编辑 / 删除(已被发票引用则 409) |
| `GET /trade/orders/:orderId/invoice-titles` | 申请页可选抬头 |
| `GET /trade/orders/:orderId/invoice-status` | `{ exists, status }` |
| `POST /trade/orders/:id/invoices` | 提交申请(可传 `titleId` |
| `GET /trade/orders` | 列表项增加 `invoiceStatus` |
| 前端 | 说明 |
|------|------|
| `apps/mini-user/src/pages/invoice-titles/index.tsx` | 抬头列表 |
| `apps/mini-user/src/pages/invoice-apply/index.tsx` | 申请发票 |
| `apps/mini-user/src/pages/orders/index.tsx` | 申请 / 开票中互斥 |
| `apps/mini-user/src/pages/order-detail/index.tsx` | 同上 |
---
## 5 / 6 / 7. 门店信息变更与审核通知
合伙人改基本资料不再直接写 `store`,改为提交变更;审核通过后用 `proposedSnapshot` 覆盖线上。审核期间 C 端 / 合伙人端仍展示线上 live 数据。
可变字段白名单见 `STORE_INFO_CHANGEABLE_FIELDS``packages/shared-types/src/store-info-change.ts`)。
- 合伙人详情底栏文案:**提交变更**;有 PENDING 时黄条「基础信息变更审核中」
- 总部「套餐审核」改名 **审核通知**:Tab「套餐变更 | 门店信息变更」;菜单 badge = 两类 PENDING 之和
- 门店列表:`pendingInfoChangeId` →「审核信息 / 对比」;「提现」→ `/finance/store-bills?storeId=`
- 门店端提交接口已备(`POST /shop/store/info-change-request`),**本期无门店 H5 入口**
| 接口 | 角色 | 说明 |
|------|------|------|
| `POST /partner/stores/:storeId/info-change-request` | 合伙人 | 提交 |
| `GET /partner/stores/:storeId/info-change-requests` | 合伙人 | 历史(用于 PENDING banner |
| `POST /shop/store/info-change-request` | 门店 | 已实现,本期无 UI |
| `GET /admin/store-info-change-requests` | 总部 | 列表 |
| `GET /admin/store-info-change-requests/summary` | 总部 | `pendingCount` |
| `GET /admin/store-info-change-requests/:id` | 总部 | 详情 + 字段 diff |
| `PUT /admin/store-info-change-requests/:id/audit` | 总部 | `APPROVE` / `REJECT` |
| `GET /admin/stores` | 总部 | 增加 `pendingInfoChangeId` |
通过 / 驳回后派发既有 `admin:package-audit-changed` / `AUDIT_NOTICE_CHANGED_EVENT`,侧栏 badge 刷新。
---
## 8. 待支付 30 分钟自动取消
下单路径已写 `payExpireAt = now + 30min`。本次补定时翻状态(此前 monitor 只告警不取消)。
- Cron`*/1 * * * *` Asia/Shanghai`OrderExpiryScheduler`
- 条件:`PENDING_PAY` + `UNPAID` + `payExpireAt < now` + `isTest=false`
- 动作:条件更新 `CANCELLED` + `cancelledAt`;日志 `operator=SYSTEM_AUTO_EXPIRE`,备注「30 分钟未支付自动取消」
- **不回滚**权益券(与手动取消代下单一致;建单时未占用库存)
- 每批最多 200 条;`updateMany``status=PENDING_PAY` 防并发重复
| 位置 | 说明 |
|------|------|
| `server/dukang-api/src/jobs/order-expiry.scheduler.ts` | 定时任务 |
| `TradeService.cancelExpiredPendingOrders` | 业务取消 |
| `schema.prisma` Order | `@@index([status, payExpireAt])` |
---
## 9. 门店 / 合伙人接口强制退登录
登录后账号被停用 / 解绑时,后续任意鉴权接口拦截,前端清 token 并回登录页。
校验(仅 `STORE` / `PARTNER``USER` / `HQ` 跳过):
| 端 | 条件 |
|----|------|
| 门店 | `StoreAccount.status === ACTIVE`;已选店则 `Store.status === OPEN` |
| 合伙人 | `PartnerAccount.status === ACTIVE``bindingStatus === ACTIVE` |
`isTest=true` 的账号 / 门店**跳过**,避免测试环境自锁。
响应:HTTP 403`{ code, message, reason: 'ACCOUNT_DISABLED' }``HttpExceptionFilter` 透传 `reason`,避免走 401 refresh)。
| 位置 | 说明 |
|------|------|
| `jwt-auth.guard.ts` `assertAccountActive` | 运行时校验 |
| `http-exception.filter.ts` | 透传 `reason` |
| `apps/h5-shop/src/lib/api.ts` | `ACCOUNT_DISABLED``clearAuth` + `/login` |
| `apps/h5-partner/src/lib/api.ts` | 同上(`toAppPath('/login')` |
---
## 验收
- [ ] 大屏:非测试且实付 ≥100 的订单上屏,手机号中间四位 `****`;「测试」仅超管可见且能连播动效
- [ ] 门店端核销成功后,「我的 / 提现」无需下拉即可看到余额变化
- [ ] 发票管理可增删改、设默认;小程序无二级导航标题
- [ ] 仅已完成订单出现「申请发票」;提交后卡片改为「开票中」,不再出现申请按钮
- [ ] 合伙人提交门店资料后线上不立即变;总部审核通过后覆盖;驳回后可改再提
- [ ] 审核通知 badge = 套餐 PENDING + 信息变更 PENDING;门店列表有「审核信息 / 对比 / 提现」
- [ ] 构造已过期待支付正式单,约 1 分钟内变为已取消,且有 `SYSTEM_AUTO_EXPIRE` 日志;测试单不被取消
- [ ] 停用门店账号或关闭门店后,门店端接口 `reason=ACCOUNT_DISABLED` 并退登录;合伙人 `bindingStatus` 非 ACTIVE 同理;C 端不受影响
- [ ] `pnpm db:generate && npx prisma db push` 无报错;`pnpm lint` 无新增错误
## 发版注意
- 必须跑 Prisma(两张新表 + 订单索引),不可 `--skip-db`
- 发版时 mini-user `package.json` / `src/lib/client-version.ts`**`3.5.1`**(当前仓库仍为 `3.4.15`
- HQ 开发计划创建版本 `v3.5.1` 并关联本迭代任务
+57
View File
@@ -0,0 +1,57 @@
# 杜康好客 · V3 编码手册(交付业务版)
> **事实源**[`杜康好客-v3-PRD.md`](./杜康好客-v3-PRD.md) · **审计**[`杜康好客-v3-现状对照.md`](./杜康好客-v3-现状对照.md)
> V2/preV1 **非需求依据**。总部交付 = **`apps/admin-web`**(非 H5)。
## 1. 交付目标(六条)
C 端购酒核销 · 门店扫码核销+打款 · 合伙人拓店履约 · WebAdmin 运营 · 后端支付/配送/结算/审计 · 主链路冒烟+边界测试。
## 2. 分工
| 负责人 | 范围 |
|--------|------|
| jacy-dukang | 全部 apps、packages、server 模块、Prisma2026-07 起代管 B+D |
| ~~刘景尧~~ | ~~h5-shop/partner、store/redeem~~(暂停) |
**四端**C=`mini-user`/h5-user · 门店=h5-shop · 合伙人=h5-partner · 总部=**admin-web**`/admin/*``HQ_WEB`)。
**边界**apps 只 HTTP+shared-types;跨模块只 inject exported Service;枚举/DTO→shared-types;纯规则→domain。
**日志**:见 [`杜康好客-v3-城市仓库与日志架构.md`](./杜康好客-v3-城市仓库与日志架构.md)
## 3. 核销规则(V3
| 入口 | 入参 | 上限 |
|------|------|------|
| 直接核销 | `{ amount }` | ≤ 全部 ACTIVE 权益总余额(FIFO |
| 单据核销 | `{ couponId, amount }` | ≤ 该单据可用金额 |
- Redis 码 TTL **3 分钟**;确认时二次校验 + 券 version 乐观锁
- 成功写:`user_redeem_record` · `common_event(BENEFIT_LEDGER)` · `store_payout(PENDING)`
- 绑定门店则仅该店可确认;`OPEN` 门店;暂停/关闭不可核销
- UI 无「单次 ¥500」文案
## 4. 业务闭环(摘要)
| 链路 | 关键节点 |
|------|----------|
| C 购酒 | 登录→开城商品→起购(同城2/跨城6)→支付→权益1:1→出码/核销→评价 |
| 门店 | 登录→扫码确认→记录→store_payout T+1 |
| 合伙人 | 拓店三步→HQ审核→辖区订单/账单 |
| HQ | 开城/商品/审核/订单/权益/核销/结算/工单 |
## 5. 验收用例(必过)
**主链路 15 项**:登录、4 SKU、起购、支付+权益、双通道核销、payout、关店不可见、拓店审核、配送完成、退款、T+1/T+30…
**后台 8 项**:商品/门店/订单/权益/核销/工单/日志/财务。
## 6. 技术债(摘要)
P0:旧文档¥500 · lint 占位 · smoke 窄覆盖
P1DTO 不全 · 跨模块 prisma · 真实短信/配送
P2mini-hq vs admin-web 重叠
## 7. 版本与波次
规则变更先改 **v3-PRD**。Wave 1/2/3 见 PRD §9。
@@ -0,0 +1,40 @@
# 杜康好客 · 开发计划 v3.4.11
> PRD §3.10~3.11 · REQ-H-026~028 · 模块 `server/.../dev-plan/`
## 表
`dev_plan_task` · `dev_plan_version` · `dev_plan_version_task` · `dev_plan_settings`(审核助手) · `dev_plan_task_dispatch` · `wecom_message_push`
枚举/DTO`packages/shared-types` `dev-plan.ts` · `wecom-message-push.ts`
## Admin API(摘要)
| 路由 | 说明 |
|------|------|
| `/admin/dev-plan/tasks` | CRUD · dispatch · batch-updatev3.4.12+ |
| `/admin/dev-plan/versions` | CRUD · 关联任务 · **RELEASED 联动任务/工单(v3.4.13** |
| `/admin/dev-plan/settings` | 审核助手 LLM/知识库 |
| `/admin/wecom-message-pushes` | Webhook 多实例 + eventKey |
| `/admin/support-tickets/*` | review · batch-review · batch-update-status |
任务派发:HQ「消息推送」勾选 `dev_plan.task_dispatch`(非 settings 字段)。
## 技术支持联动
审批 `APPROVE` + `tasks[]`≥1 → 工单 DEVELOPING + 创建 `dev_plan_task`。批量 AI 预审 → confirm。
## 企微机器人(摘要)
四类角色(客服/财务/运营/技术支持)+ 模块化权限;`support_ticket.review` 白名单;审计 `log_wecom_bot`
消息推送 eventKey`alert.ops` · `support_ticket.created` · `dev_plan.task_dispatch` 等。
## ACC 抽样
- [ ] 开发计划三页 + 企微三菜单
- [ ] 工单审批建任务;任务评审派发 Webhook
- [ ] v3.4.13:版本 RELEASED → 任务 RELEASED + 工单 PUBLISHED
## 发版
`prisma db push``wecom_message_push`)· 迁移旧 env Webhook · tag `v3.4.11`
+161
View File
@@ -0,0 +1,161 @@
# 杜康好客 · 业务知识库
> **非需求文档**(不改规则请改 PRD)。新人/运营/客服速查。
> 事实源:[`v3-PRD`](./杜康好客-v3-PRD.md) · 验收:[`v3编码手册`](./杜康好客-v3编码手册.md)
## 1. 概述
**链路**:购酒 → 1:1 好客权益 → 门店核销(酒+餐)。
| 角色 | 端 | 职责 |
|------|-----|------|
| C 用户 | mini-user / h5-user | 买酒、权益、核销、售后 |
| 门店 | h5-shop | 扫码核销、营业、提现 |
| 合伙人 | h5-partner | 拓店、辖区订单/账单 |
| 总部 | admin-web | 开城、审核、结算、运营 |
**常量**:权益额=`benefit_amount??price` · 门店结算=核销×**60%** · 佣金池≤**5%**(默认0%+3%) · 核销码**3min** · 同城≥**2瓶** · 跨城≥**6瓶** · 签约主体:山西领势酒业。
---
## 2. C 端(mini-user
- 四 Tab:首页/权益/门店/我的;微信登录+7天会话
- 下单:选城→商品→地址→起购校验→微信支付→权益1:1
- 权益:直接核销(≤总余额) / 单据核销(≤单据);出码3分钟
- 门店:仅 OPEN;详情含套餐/电话(脱敏可拨打)/两段营业时间
- 订单 Tab:待付款/已付款/已完成;物流详情(签收照/拨号/ETA)
- 售后:客服入口;发票/四类型工单按 PRD Wave 进度
- 版本:`minClientVersion` 过低强制更新或退出
- 「我的」头像昵称:`chooseAvatar` + `input type=nickname`(见下「踩坑」)
### 踩坑 · 小程序 open-type 按钮点击无反应(必读,勿再回归)
**现象**:「我的」完善资料弹层里点「选择头像」无反应(`open-type=chooseAvatar`);同类还有 `getPhoneNumber` / `contact` / `share`
**根因**:弹层内容上写了 `onClick={(e) => e.stopPropagation()}`Taro 编译为微信 **`catchtap`**,父级拦截后子级 `Button` 的原生 open-type **静默失效**
**硬规则**
| 规则 | 说明 |
|------|------|
| 弹层结构 | 遮罩 backdrop 单独绑关闭;**sheet 上禁止** `stopPropagation` / `catchtap` |
| Button 内子节点 | `Image` 等加 `pointer-events: none`,勿抢触摸 |
| 自检 | 凡含 `openType=` 的 Button,向上检查祖先有无 catch 类事件 |
实现参照:`apps/mini-user/src/pages/mine/index.tsx`Cursor 规则:`.cursor/rules/mini-user-weapp-opentype.mdc`
## 3. 门店端(h5-shop
- 登录绑定门店;首页扫码核销(微信 JSSDK)
- 核销记录;今日汇总;到账金额×60%展示
- 营业状态开关;Mine 门店信息
- 套餐:列表编辑→提交 HQ 审核(v3.4.10)
- iOS 微信:OAuth 后自动续扫(v3.4.13);登录后须整页跳转(见下「踩坑」)
### 踩坑 · iOS 微信 H5 扫码(必读,勿再回归)
**现象**:手机号重新登录后点「扫码核销」提示「微信权限校验尚未完成…」;关掉 H5 再进就正常。
**根因(不是系统相机权限)**
1. iOS 微信 WebView 对 JSSDK 验签用的是**本次 document 加载的入场 URL**(含 query),不是 SPA `pushState` 之后的 `location.href`
2. 登录 / OAuth 回跳常落在 `/login?code=…`,再 `navigate('/')` 进首页 → 签名 URL 与微信内部入场 URL 不一致 → `permission value is offline verifying` / invalid signature。
3. 文案「等 1~2 秒再点」只覆盖「权限离线校验偏慢」的一小部分场景;**签名错了等多久都不行**,必须整页刷新或重新授权。
**硬规则(编码)**
| 规则 | 说明 |
|------|------|
| iOS 登录/选店后 | 用 `location.replace(path)``hardNavigateInWechat`),禁止仅 React Router navigate |
| iOS 签名 URL | `getJssdkSignUrl()` = 入场 URL**保留** OAuth `code/state`;后端 `jssdk-config` 勿剔除 |
| 已绑定微信 | 短信登录后**不要**再强制 OAuth(避免反复重置入场 URL) |
| 扫码仍失败 | 弹窗引导「刷新页面」/「重新授权微信」,勿只提示再点一次 |
实现:`packages/weixin-sdk/src/jssdk.ts` · `apps/h5-shop` 登录/选店/HomePage。
## 4. 合伙人端(h5-partner
- 管理员 vs 推广员菜单裁剪;子账号 CRUD
- 拓店:基本信息→照片→结算资质→(套餐);一号多店确认
- 门店列表/详情/套餐提审;辖区订单;账单 T+30
- 暂停账号:发码前即拦截登录(v3.4.13)
## 5. HQadmin-web
| 菜单域 | 要点 |
|--------|------|
| 开城 | 城市、合伙人(全城/区域+佣金)、仓库 |
| 商品 | SKU、上下架、详情模板 |
| 门店 | 审核、套餐 Tab 直存/审核、合伙人列 |
| 交易 | 订单、权益券、核销记录、推广码+metrics |
| 财务 | 门店/合伙人/酒厂/物流账单;打款确认 |
| 工单 | 售后四类型 + 技术支持(ST) + 开发计划 |
| 系统 | 账号权限、客户端配置、企微机器人/消息推送 |
| 日志 | HQ/用户/门店/合伙人/企微 |
## 6. 商品与模板
HQ 创建商品:名称/价格/权益额/箱规/香型/城市上架;详情模板(JSON 块);资源 OSS。
## 7. 活动 / 推广码
HQ 推广码:场景/合伙人绑定/上下线;touch 归因;metrics 四指标+事件日志(v3.4.13)。
## 8. 开城
创建城市 → 绑定主账号合伙人 → 配置佣金 → 上架商品 → 仓库(可选)。
## 9. 开店
合伙人三步录入 → (负责人复核) → HQ 审核 → (试核销100) → OPEN → C 端可见。
## 10. 订单
状态机三态;支付回调幂等;同城小飞侠/跨城 HQ 填单;现场提货即完成;大单≥10箱 HQ 确认。
## 11. 财务
- **门店**:核销→store_payout T+1;未出账可提现→HQ 审→打款
- **合伙人**:T+30 月账独立确认打款
- **酒厂**T+3 账单(v3.4.12)
- **物流**:按承运商月结(小飞侠计价见 PRD §3.7.1)
## 12. 发票
用户申请 → HQ 2 工作日处理 → 回传(Wave 3 完整)
## 13. 工单
**售后**(用户):仅退款/补发/退货等四类型 → HQ 审 → 仓/合伙人协同
**技术支持**(内部 ST):BUG/建议 → 审批 → 开发任务 → 版本发布 → 工单 PUBLISHED(v3.4.13)
## 14. 配送
小飞侠推单/回调;`POST /callbacks/courier/xfx/track`;签收→订单 COMPLETED。
## 15. 好客权益
支付成功发放;FIFO 扣减;流水 `common_event(BENEFIT_LEDGER)`;核销后评价。
## 16. 系统设置
HQ 账号/角色(`hq-permissions`) · 客户端 `minClientVersion` · 运营告警走企微消息推送(DB Webhook)。
## 17. 企业微信
| 能力 | 入口 |
|------|------|
| 智能机器人 | `/wecom/bots` 长连接指令 |
| 消息推送 | `/wecom/pushes` Webhook+eventKey |
| 日志 | `/logs/wecom-bots` |
eventKey`alert.ops` · `support_ticket.created` · `dev_plan.task_dispatch` · 支付/核销/结算告警。
---
## 附录
**端职责矩阵** → PRD §4.5
**文档索引**:PRD · 编码手册 · 现状对照 · v3.4.x 开发文档 · 埋点规范 · 城市日志架构
**变更**:随 v3.4.x 版本文档更新,不在此重复 ST 明细。
@@ -0,0 +1,38 @@
# 杜康好客 · 门店套餐 v3.4.10
> PRD §3.9 · REQ-P/S/H/U-027 · **已实现**(含 v3.4.12 `imageUrl`
## 要点
- 与酒水 SKU 独立;每店 ≤10 条;字段:名称/价格/菜品/使用时间/说明
- C 端仅展示**已审核生效**套餐;异议类型 `PACKAGE_DISPUTE`
- 合伙/门店:编辑 → **提交审核**HQ:门店详情 Tab **直存生效**
- 审核通过:事务替换 `store_package`;驳回保留上一版;同店仅 1 条 `PENDING`
## 表
| 表 | 用途 |
|----|------|
| `store_package` | 生效套餐(含 `image_url` |
| `store_package_change_request` | 提审快照 `packagesJson` |
## API(前缀 `/api/v1`
| 端 | 路径 |
|----|------|
| partner | `GET/PUT /partner/stores/:id/packages` · 变更历史 |
| shop | `GET/PUT /shop/store/packages` |
| admin | `GET/PUT /admin/stores/:id/packages` · `/admin/store-package-audits` 审 |
## 流程
```
partner/shop 编辑 → PENDING → HQ 通过/驳回 → 生效 → mini-user 展示
HQ 直存 ──────────────────────────────→ 生效
```
## ACC
- [ ] 四端展示 imageUrlHQ 可删至 0 条
- [ ] 提审中 C 端仍见上一版;通过后替换
- [ ] 套餐异议工单可创建