# 要店帮-红包商城 · 腾讯云商用上线准备方案

> **文档版本**：v1.0
> **编制日期**：2026-11-04
> **适用系统**：芋道商城多租户底座 + 店员积分返现 / 晒单核销 / 红包提现
> **目标读者**：业务负责人（决策与资质）、技术负责人（实施）、运维（部署与值守）、财务/运营（打款台账与对账）
> **当前生产形态**：腾讯云 CVM 尚未采购，当前环境为内网 Tailscale 直连的 h02（主机名 `l16`）测试环境

---

## 0. 阅读指引

### 0.1 三类标记

本文全篇使用以下三种标记，含义固定：

| 标记 | 含义 | 处理方式 |
|---|---|---|
| 🔴 **必须** | 不做则无法上线，或上线即违规/资金安全事故 | 必须在上线检查表中勾选完成 |
| 🟡 **可选** | 提升体验或降低运维成本，可延后到上线后迭代 | 需业务方明确"暂缓"并记录 |
| ⚪ **需业务方确认** | 文中无法从现有事实判定，必须由业务方拍板 | 上线决策会前必须有书面结论 |

### 0.2 责任人代号

| 代号 | 角色 | 职责范围 |
|---|---|---|
| **BIZ** | 业务方（深圳市瑞康大药房连锁有限公司 / 要店帮） | 资质、账号、备案、主体、对公账户、线下打款、运营 SOP |
| **TECH** | 技术负责人（AI 负责代码与配置全部实现） | 代码、配置、部署、联调、证书与密钥落地、数据迁移 |
| **OPS** | 运维 | 云资源申请、安全组、备份、监控、值守 |
| **FIN** | 财务/运营 | 红包打款审批、付款、对账、凭证归档 |

> 说明：本方案按客户已拍板的商业决策编写——**主体混合模式**（小程序与支付主体归「要店帮」＝深圳市瑞康大药房连锁有限公司，但每客户配一套独立商户号）、**腾讯云 CVM + 域名 + HTTPS 全套且需 ICP 备案**、**本轮红包打款走人工台账不接微信自动转账**、**代码与配置由 AI 全部实现，资质与账号由业务方办理**。

---

## 1. 一句话结论与总成本估算

### 1.1 一句话结论

> **系统业务功能已具备商用骨架（多租户、积分返现、晒单核销、红包提现审批链路齐全，文件存储已用腾讯云 COS），但"支付"与"打款"两条资金链路目前是空壳——`pay_app`/`pay_order`/`pay_transfer` 全为 0 行，红包打款是恒返回 `true` 的 MOCK；同时全站纯 HTTP、无域名、短信不可用、OCR 空实现。因此本轮上线的最小可行集合（MVP上线集）是：完成 ICP 备案 + 域名 + HTTPS + 至少 1 个真实微信支付商户号跑通一筆真实支付 + 红包改人工台账打款 + 短信与 OCR 降级为人工兜底 + 生产数据清理与口令加固 —— 预计在 ICP 备案启动后 6–8 周可达到可对客户开放的状态。**

### 1.2 上线阻塞项一览（Top 阻塞）

| # | 阻塞项 | 为什么阻塞 | 谁解决 | 能否与开发并行 |
|---|---|---|---|---|
| B1 | ICP 备案 | 腾讯云 CVM 在大陆境内**未备案域名无法通过接入**，小程序 request 合法域名也要求已备案域名 | BIZ + OPS | ✅ 可并行（最长前置项） |
| B2 | 微信支付商户号（企业主体 + 对公账户） | 无商户号 = 零收入；`pay_app` 为空说明从未真实联调 | BIZ | ✅ 可并行 |
| B3 | 小程序注册 + 认证（300元/年，企业主体） | 未认证的小程序无法上架、无法接入微信支付相关能力 | BIZ | ✅ 可并行 |
| B4 | 每客户独立商户号的密钥落地 | 决定"一客一号"商业模型能否成立 | BIZ 提供 + TECH 落地 | ✅ 可并行 |
| B5 | 生产数据清理与口令加固 | 弱口令 MySQL root + 演示租户数据 = 上线即事故 | TECH | ✅ 可并行 |
| B6 | 红包人工台账 SOP 与财务闭环 | 若无人工流程，`mockTransfer()` 恒 true 会造成"提现已到账"的假象与资金纠纷 | BIZ + FIN | ✅ 可并行 |

### 1.3 总成本估算表

> ⚠️ 以下所有金额均为**参考区间，以实际采购为准**（含 2026 年 11 月的腾讯云官网/市场价与市场价格，均未含企业协议价）。已购账号续费、员工时间成本未计入。

#### 1.3.1 一次性支出（首年投入）

| 项目 | 明细 | 一次性区间（元） | 必需/可选 | 备注 |
|---|---|---|---|---|
| 域名注册 | `.cn` 约 35–60 / `.com` 约 70–130（首年） | 35–130 | 🔴 必须 | 建议 `.cn` + 保护性 `.com`，一次性成本可忽略 |
| ICP 备案 | 工信部备案免费；云厂商接入费 0 | 0 | 🔴 必须 | 但周期是**最长前置项**，见 §12 |
| 小程序认证 | 企业主体 300 元/年 | 0（已计入年费） | 🔴 必须 | **省钱点**：已有认证企业公众号可走"快速注册并认证小程序"复用主体资质，**免 300 元** |
| 腾讯云账号 | 企业实名认证免费；需对公打款 | 0 | 🔴 必须 | 主体须与小程序主体一致 |
| SSL 证书 | Let's Encrypt（certbot 自动续期） | 0 | 🔴 必须 | 付费 DV 证书 300–1500/年 属 🟡 可选 |
| 服务器首购 | CVM 或轻量按年包（见 §4） | 2,000–4,500 | 🔴 必须 | 4核8G 档位参考价 |
| 云数据库 MySQL | 高可用版按年包 | 3,000–9,000 | 🔴 必须 | 单节点版可降至约 1,200–2,500 |
| 云 Redis | 1G 标准版按年包 | 600–1,500 | 🟡 可选 | 初期可继续自建，见 §4.4 |
| TCR 镜像仓库 | 企业版按年包 | 300–3,000 | 🔴 必须 | 替代当前内网 HTTP Registry |
| COS | 桶 + 存储 + 外网下行 | 0（按量） | 🔴 必须 | **master 已在用 COS**，配置可直接复用 |
| 短信（预充值） | 腾讯云短信，首充按需 | 1,000–3,000 | 🟡 可选 | 见 §8.3，MVP 可先关 |
| OCR（预充值） | 通用/票据识别按量 | 500–2,000 | 🟡 可选 | MVP 可先用人工审核兜底 |
| 内容安全（天御） | 文本/图片审核按量 | 200–1,000 | 🔴 必须（UGC） | 小程序 UGC 强制要求 |
| 打款启动资金 | 红包池首期备付（按业务量定） | 视业务量 | 🟡 可选 | 属运营资金非技术成本 |
| **一次性小计** | | **约 7,600 – 24,100** | | 不含运营资金；为上表各项加总（含全部 🟡 可选项） |

#### 1.3.2 年费支出（稳定期）

| 项目 | 明细 | 年费区间（元） | 必需/可选 | 备注 |
|---|---|---|---|---|
| 域名续费 | `.cn` 约 35–60 / `.com` 约 70–130 | 35–130 | 🔴 必须 | |
| 小程序认证 | 企业主体 300 元/年 | 300 | 🔴 必须 | 可用公众号快速注册免首年 |
| 服务器 | CVM/轻量 + 云硬盘 + 快照 | 2,000–4,500 | 🔴 必须 | |
| 云数据库 MySQL | 高可用版 | 3,000–9,000 | 🔴 必须 | 稳定期可优化规格 |
| 云 Redis | 1–2G | 600–1,500 | 🟡 可选 | |
| TCR | 企业版 | 300–3,000 | 🔴 必须 | |
| COS | 存储 + 流量 + CDN（若接） | 100–3,000 | 🔴 必须 | 图片类晒单流量为主要变量 |
| 短信 | 腾讯云短信约 0.04–0.045 元/条 | 1,000–3,000 | 🟡 可选 | 验证码 + 通知 |
| OCR | 按次计费，约 0.1–1.2 元/次（按接口不同） | 500–3,000 | 🟡 可选 | |
| 内容安全 | 按次计费/包量 | 200–2,000 | 🔴 必须 | |
| CLB 负载均衡 | 标准型 | 700–2,000 | 🟡 可选 | 见 §4.2 |
| CDN | 静态资源 + 图片 | 500–5,000 | 🟡 可选 | 门店多、素材大时收益明显 |
| 监控与日志 | 云监控 CLS 日志服务 | 0–1,500 | 🟡 可选 | 生产建议至少开启 |
| **年费小计（不含交易成本）** | | **约 9,200 – 37,900** | | 为上表各项加总（含全部 🟡 可选项） |

#### 1.3.3 交易相关成本（非固定，随业务量变动）

| 项目 | 费率/单价 | 说明 |
|---|---|---|
| 微信支付手续费 | **0.2% – 0.6%** | 常见档位；零售/药类常落在 0.6%，部分行业有优惠费率。**以商户后台实际费率为准** |
| 红包打款 | 0 手续费（人工线下转账） | 本轮不接微信自动转账，无通道费 |
| 微信"商家转账"（未来） | 官方按产品规则，运营账户出资 | 本轮不发生 |
| 人工打款人工工时 | 按台账审核量估算 | 财务工时成本，非现金支出 |

#### 1.4 MVP 上线集（最小可行上线集合）

> 定义：**只做这些，即可正式对客户开放并开始真实收款**；其余全部可在上线后迭代。

| # | MVP 必做项 | 验收标准 |
|---|---|---|
| M1 | ICP 备案通过 + 域名解析生效 | `https://mall.example.cn` 可访问，工信部备案号在页脚展示 |
| M2 | 全站 HTTPS + 安全响应头 + HSTS | 微信开发者工具无 SSL 报错；`curl -I` 返回 `Strict-Transport-Security` |
| M3 | ≥1 个真实微信支付商户号（`pay_app` + `pay_channel` 有行） | 用 1 元订单跑通 **下单 → 支付 → 回调 → 订单已支付**，且 `pay_order` 落库 |
| M4 | 支付回调按租户路由正确 | 租户 A 的订单回调不会写到租户 B |
| M5 | 红包打款改人工台账，`mockTransfer()` 停用 | 提现单生成待打款记录；打款动作必须有人工凭证才可置为已打款 |
| M6 | MySQL root 口令改强口令 + Redis 加密码 | 口令不在任何脚本/仓库明文出现 |
| M7 | 生产演示数据清理（租户/用户/流水/商品/黑名单） | 见 §9 清理清单逐项打勾 |
| M8 | 镜像源改为 TCR 公网/腾讯云仓库 | compose 中无 `100.66.41.15:5000` |
| M9 | 小程序注册 + 认证 + 类目审核通过 | 微信后台可提交审核 |
| M10 | 内容安全 API 接入（文本 + 图片） | UGC 提交触发服务端校验并落审核记录 |
| M11 | 隐私保护指引配置 + `wx.getPrivacySetting` 弹窗接入 | 弹窗可触发，未默认勾选 |
| M12 | 备份与回滚点就绪 | 有一份可恢复的全量备份 + 回滚演练记录 |

> 🟡 **可延后**：OCR 真实接入（先用人工审核）、短信（注册/找回密码临时走人工核验或微信授权）、CDN、CLB、云 Redis、云数据库高可用版（先单节点）、微信商家转账自动打款。

**MVP 口径下的最低技术支出（只算 🔴 必须项，可核验）**

| 项目 | 取值 | 金额（元） |
|---|---|---|
| 域名注册（.cn 首年） | 35–60 | 35–60 |
| 小程序认证 | 300 | 300 |
| CVM 4核8G 按年包 + 硬盘 + 快照 | 2,000–4,500 | 2,000–4,500 |
| 云数据库 MySQL（**单节点版**，MVP 折中） | 1,200–2,500 | 1,200–2,500 |
| TCR 企业版 | 300–3,000 | 300–3,000 |
| 内容安全（按量，起步量） | 200–1,000 | 200–1,000 |
| SSL 证书（Let's Encrypt） | 0 | 0 |
| ICP 备案 / 腾讯云实名 | 0 | 0 |
| **MVP 首年最低合计** | | **约 4,000 – 11,400** |

> 说明：MVP 折中路径 = **MySQL 用单节点自建或单节点云数据库、Redis 自建、短信与 OCR 走人工兜底、暂不接 CDN/CLB/云 Redis**。差额部分即 §1.3.1/§1.3.2 中被标为 🟡 可选项的额度。**均为参考区间，以实际采购为准。**

---

## 2. 现状体检报告

### 2.1 就绪度评分总表

评分口径：**5 = 完全就绪**；**4 = 基本就绪，仅需配置**；**3 = 需开发**；**2 = 需外部资质/账号**；**1 = 空实现或 MOCK**；**0 = 缺失**。

| # | 模块 | 现状（已核实） | 商用就绪 | 差距（需补齐） | 责任人 | 阻塞上线 |
|---|---|---|---|---|---|---|
| 1 | 🔴 **红包打款** | `IncentiveWithdrawServiceImpl.mockTransfer()` **恒返回 true**，`transferNo` 写死 `"MOCK"+时间戳` | **1** | 改人工打款台账；打款状态机；凭证必填；对账表 | TECH + FIN | **是** |
| 2 | 🔴 **微信支付接入** | `pay_app` 0 行、`pay_order` 0 行、`pay_transfer` 0 行 —— **从未真实接过微信支付** | **1** | 申请商户号；配 `pay_app`/`pay_channel`；APIv3 密钥 + 证书；跑通真实小额支付 | BIZ + TECH | **是** |
| 3 | 🔴 **域名与 HTTPS** | 无公网 IP、无域名、无 SSL，**全站纯 HTTP**；仅 Tailscale `100.127.133.77` 可达 | **0** | 域名注册 + ICP 备案 + CVM + Let's Encrypt | BIZ + OPS | **是** |
| 4 | 🔴 **数据库安全** | MySQL root 口令 `123456` **弱口令**；`ydb_hb_mall` | **1** | 改强口令、删 demo 账号、限制来源 IP、安全组 | TECH + OPS | **是** |
| 5 | 🔴 **镜像来源** | 全部来自内网 Registry `100.66.41.15:5000/ydb-hb-*`（h04 中转仓，HTTP + insecure-registries） | **2** | 迁 TCR，去掉 insecure-registries | TECH | **是** |
| 6 | 🔴 **短信** | `system_sms_channel` 3 行但 **status=0**（阿里云/腾讯云/钉钉 mock 占位）；**注册与找回密码实际不可用** | **1** | 腾讯云短信签名 + 模板报备 + 启用 channel | BIZ + TECH | 否（可人工兜底） |
| 7 | 🔴 **OCR 识别** | 数据模型已就位（`clerk_task_submission.ocr_status/ocr_text`），风控 `RiskControlServiceImpl` 已有 OCR 置信度<70 扣分规则，但**无任何真实 OCR 服务商调用** | **2** | 接腾讯云 OCR；置信度回写；失败降级人工审核 | TECH | 否（可人工兜底） |
| 8 | 🔴 **UGC 内容安全** | 未接入 `msgSecCheck`/`imgSecCheck` | **0** | 小程序 UGC 强制要求；前端 + 服务端双审 | TECH | **是** |
| 9 | 🔴 **小程序** | uni-app 源码 `yudao-mall-uniapp`，H5 版在跑 `/clerk/`；`manifest.json` appid `wxae7a0c156da9383b` 是**芋道模板上游的、不是我们自己的**；无 mp-weixin 构建产物；package.json 只有 `dev:h5`/`build:h5` | **2** | 注册 + 认证 + 改 appid + 补 mp-weixin 构建脚本 + 类目审核 | BIZ + TECH | **是** |
| 10 | 🟡 **多商户号能力** | 芋道原生支持：`pay_app`/`pay_channel` 带 tenant_id（`PayChannelDO extends TenantBaseDO`）；`PayClientFactoryImpl` 按 channelId 缓存客户端，**支持多商户号并行** | **4** | 配置流程 + 密钥安全落地（不能明文入库/进 git） | TECH | 否 |
| 11 | 🟡 **转账链路** | 芋道已有完整 `pay_transfer` 链路（`PayTransferService` / `PayTransferSyncJob` / `PayTransferApi`） | **4** | 本轮不用；保留扩展点即可 | TECH | 否 |
| 12 | 🟡 **对象存储** | **master 已是腾讯云 COS**（`infra_file_config` id=22，storage=20 S3 类型，endpoint `cos.ap-shanghai...`） | **5** | 仅需核对各租户 file_config 是否同样为 COS | TECH | 否 |
| 13 | 🟡 **生产数据** | 4 个租户（含 iocoder 官方测试租户 121/122）、22 个系统用户、105 个 clerk_member、24 个 product_spu、25 条 mall_blacklist、18 条 clerk_withdraw | **1** | 按 §9 清理清单执行 | TECH | **是** |
| 14 | 🟡 **演示账号** | admin/`«后台»`（平台端）、demo123/`«演示»`（示范大药房租户） | **1** | 全部删除或改强口令 + 首登强制改密 | TECH | **是** |
| 15 | 🟡 **访问通道** | 仅 Tailscale 内网可达 | **3** | 运维走 Tailscale / 堡垒机；对外只暴露 443 | OPS | 否 |
| 16 | 🟡 **备份与监控** | 未在已核实事实中体现（**需业务方/运维确认**） | **2** | 定时备份 + 快照 + 告警 | OPS | **是**（生产必备） |
| 17 | 🟡 **H5 商城 / 后台 / 店员端** | 已跑通（8081 商城 H5、8082 后台+店员端+医药前台） | **4** | 换域名与 HTTPS；前端资源路径与 CDN 适配 | TECH | 否 |
| 18 | 🟡 **前端构建管线** | 已踩坑验证：需 `find -mindepth 1 -delete`（禁 `rm -rf dist`）、`@swc/core` 锁 `1.15.47`、`vite-plugin-top-level-await@1.6.0`、构建 `nice -n 15` | **4** | 固化到 CI 脚本 | TECH | 否 |
| 19 | 🟡 **动态路由刷新** | 芋道动态路由登录后注入内存，整页刷新会白屏 | **4** | 生产引导用菜单切换；埋点/文案告知 | TECH | 否 |

### 2.2 关键风险点速览（详情见 §13）

| 编号 | 风险点 | 现状证据 | 若不处理的后果 |
|---|---|---|---|
| R-A | 资金链路是 MOCK | `mockTransfer()` 恒 true | 用户提现显示"已到账"但实际没付 → 投诉/赔付/平台处罚 |
| R-B | 支付零真实联调 | `pay_order` 0 行 | 首次真实交易大概率踩坑（证书、签名、回调验签、金额校验） |
| R-C | 全站 HTTP | 无 SSL | 微信小程序强制 HTTPS；密码/令牌明文传输；不予上架 |
| R-D | 弱口令 DB root | `123456` | 数据全量泄露风险 |
| R-E | 内网 HTTP 镜像仓 | `100.66.41.15:5000` | 生产镜像被中间人替换（供应链投毒） |
| R-F | 备案周期不可控 | 需数周 | 直接决定上线日期 |
| R-G | 短信不可用 | status=0 | 用户无法注册、无法找回密码 |
| R-H | UGC 无审核 | 未接内容安全 | 小程序审核驳回 + 内容违规下架 |

### 2.3 体检结论

| 结论项 | 判断 |
|---|---|
| 业务功能骨架是否具备 | ✅ 具备。积分返现、晒单核销、提现审批、多租户隔离、店员端/医药前台链路已在跑 |
| 是否具备"商用"条件 | ❌ 不具备。**资金链路（支付 + 打款）两条都没真正打通**，且缺域名/HTTPS |
| 阻塞上线的问题数 | 6 项 🔴（B1–B6，见 §1.2 与 §11） |
| 最长前置项 | **ICP 备案（数周）**，必须 W1 第一天启动 |
| 可并行度 | 高。6 项阻塞全部可与备案并行推进 |
| 建议上线目标 | ICP 备案受理通过后 **+6 周**（保守），资料齐备无驳回可压缩至 **+4 周** |

---

## 3. 资质与账号办理清单

### 3.1 办理总表（业务方办理 vs 我能代办）

| # | 事项 | 我能代办（TECH） | 业务方办理（BIZ） | 阻塞上线 |
|---|---|---|---|---|
| A1 | 域名注册 + 实名 | ⭕ 代注册（需营业执照+法人身份证+实名） | ✅ 提供主体材料 | 是 |
| A2 | ICP 备案 | ⭕ 备案系统代填（需备案授权码/管理员账号） | ✅ 主体资料 + 域名证书 + 备案授权 | **是（最长前置）** |
| A3 | 小程序注册 | ⭕ 主体信息表 | ✅ 注册 + 认证（300元/年） | 是 |
| A4 | 微信支付商户号 | ⭕ 配置 `pay_app`/`pay_channel`、APIv3 密钥与证书落地 | ✅ 申请（企业主体+对公账户） | 是 |
| A5 | 腾讯云账号 + CVM | ✅ 全程代办 | ⭕ 提供主体 + 对公打款 | 是 |
| A6 | 短信签名与模板 | ✅ 代提交 | ✅ 提供签名来源证明 | 否（可兜底） |
| A7 | OCR 开通 | ✅ 全程代办 | ⭕ 无需 | 否（可兜底） |
| A8 | 内容安全 | ✅ 全程代办 | ⭕ 无需 | 是 |
| A9 | 隐私保护指引 | ✅ 代填模板 | ✅ 审阅并确认 | 是 |
| A10 | 小程序服务类目资质 | ⭕ 提示所需材料 | ✅ 提供许可证 | 是 |

### 3.2 逐项办理明细

#### A1 域名注册与实名

| 项 | 内容 |
|---|---|
| 需要什么材料 | 营业执照（深圳市瑞康大药房连锁有限公司）、法人身份证正反面、对公打款账户、联系人手机与邮箱 |
| 在哪办 | 腾讯云 DNSPod / 阿里云 / 西部数码（DNSPod 略优，与 CVM 同账号便于解析与备案） |
| 大概多久 | 注册即时生效；实名审核 **1–2 个工作日**（需上传证件，AI 审核快，人工驳回则重提） |
| 卡点在哪 | ① 域名后缀选择（`.cn` 需实名一致）；② 部分后缀要求域名持有满一定期限才能备案（须业务方确认所选后缀的备案限制）；③ 企业主体名称必须与营业执照**完全一致**（含括号、全称） |
| 阻塞上线 | **是** —— 备案与小程序 request 合法域名都依赖已备案域名 |
| 建议选型 | 至少两个：主域 `ydb-<品牌>.cn`、备用 `ydb-<品牌>.com`；保护性再购 2–3 个短后缀防抢注 |
| 验收标准 | `whois` 显示Registrant 主体与营业执照一致；`dig` 解析到目标 IP；DNSPod 控制台显示"已实名" |

**域名挑选建议（可核验）**

| 维度 | 建议 | 理由 |
|---|---|---|
| 长度 | 主域 ≤ 12 字符，尽量 2 级 | 便于门店物料印刷、微信搜索、客服口述 |
| 后缀 | `.cn` 优先，`.com` 兜底 | `.cn` 价格低（35–60/年）、中文用户认知强、对搜索引擎与微信搜索友好 |
| 是否含数字/连字符 | 避免 | 客服口述与手输错误率高 |
| 是否含 "红包/提现" 等词 | 避免 | 涉及资金类词在微信生态内有额外审核风险 |
| 子域规划 | `mall.`（商城）、`admin.`（后台）、`api.`（接口）、`m.`（H5） | 微信后台需分别填入合法域名清单，见 §5.4 |

#### A2 ICP 备案 🔴 关键路径

| 项 | 内容 |
|---|---|
| 需要什么材料 | ① 营业执照；② 法人身份证正反面；③ 域名证书（持有人与企业一致）；④ 主体负责人 / 网站负责人身份证；⑤ 网站负责人手机 + 邮箱；⑥ 按云厂商要求上传真实性核验单/面核；⑦ 前置审批文件（如涉及医药类内容，需确认是否需要前置/后置审批 ⚪ **需业务方确认**） |
| 在哪办 | 腾讯云备案控制台（若用腾讯云 CVM）或工信部 ICP/IP 地址/域名信息备案管理系统 |
| 大概多久 | **通常数周**。首次备案（新增网站）常见 **7–20 个工作日**；各省管局差异大，资料被驳回会重新计时 |
| 卡点在哪 | ① **主体名称必须与营业执照完全一致**；② 网站负责人需配合腾讯云人脸核验（APP 内完成，5–10 分钟）；③ 备案期间域名不可对外提供服务（云厂商通常封禁 80/443 接入）；④ 若服务器不在腾讯云且未做腾讯云接入备案，需"接入备案"额外 1–2 周 |
| 阻塞上线 | **是，且是最长前置项** |
| 策略 | 备案域名指向腾讯云 CVM，与 §4 资源采购同批下单；备案期间可先用 Tailscale 环境做联调 |
| 验收标准 | 收到工信部/管局下发的**ICP 备案号**；腾讯云控制台"备案信息"状态为"备案成功"；页脚可展示备案号 |

**时间预估口径**：以"提交 → 管局初审（1–3 工作日）→ 腾讯云核验（1–2 工作日）→ 管局审核（3–10 工作日）→ 备案号下发"估算，总计 **7–20 个工作日**；预留驳回重提缓冲，**按 4 周规划**。

#### A3 小程序注册与认证

| 项 | 内容 |
|---|---|
| 需要什么材料 | 营业执照、法人身份证、管理员身份证 + 本人手机号（用于扫码确认）、企业邮箱、公共平台联系方式（可用的企业邮箱/客服电话） |
| 在哪办 | 微信公众平台（mp.weixin.qq.com）→ 注册小程序 → 认证 |
| 大概多久 | 注册即时；**认证审核 1–3 个工作日**；认证通过后可提交类目审核（1–3 个工作日） |
| 认证费用 | **300 元/年（企业主体）**；个体工商户 30 元/年。腾讯官方收取，微信已关闭免费认证通道 |
| **省钱点** | 若已有**认证过的企业公众号**，可走"快速注册并认证小程序"复用主体资质，**免 300 元认证费**。⚪ **需业务方确认是否已有认证企业公众号** |
| 主体要求 | 小程序主体 = 深圳市瑞康大药房连锁有限公司，须与①微信支付商户号主体、②ICP 备案主体 **三者一致**（不一致会被打回） |
| appid 处理 | 现有 `manifest.json` 中 `wxae7a0c156da9383b` 是**芋道模板上游的、不是我们自己的**，必须替换为新 appid |
| 验收标准 | 公众平台"小程序"→"开发管理"可见新 appid；认证状态为"已认证"；`dev:h5` 外的 mp-weixin 构建可产出包 |
| 阻塞上线 | 是 |

#### A4 微信支付商户号（每客户独立一套）🔴

| 项 | 内容 |
|---|---|
| 需要什么材料 | 营业执照、法人身份证、结算银行账户**（企业主体必须是对公账户）**、经营范围、门店/网站经营场景说明、联系人手机与邮箱 |
| 在哪办 | 微信支付商户平台（pay.weixin.qq.com）→ 注册商户号 → 申请接入；主体一般为"要店帮（深圳市瑞康大药房连锁有限公司）" |
| 大概多久 | 商户号申请 **1–2 个工作日**审核；签约费率确认；**API 权限（APIv3 密钥、商户证书/公钥）需单独申请**：APIv3 密钥设置即时生效，**商户证书申请约 1–3 个工作日**，审核通过后下载 `apiclient_key.pem` 与证书序列号 |
| 卡点在哪 | ① **企业主体必须对公账户**（个体户可用经营者个人银行卡，本方案主体为企业 → 必须对公）；② 商户号主体必须与小程序主体一致，否则小程序无法绑定；③ 结算账户变更需重新验证；④ 涉及"医药"经营类目可能触发补充资料 |
| 费率 | 常见 **0.2% – 0.6%**，以商户后台实际费率为准 |
| 本方案范围 | 至少 **1 套**跑通上线；随后按"每客户一套"逐个申请（见 §6） |
| 验收标准 | 商户平台可发起"接口V2/V3 支付"接口测试；APIv3 密钥与商户证书（apiclient_key.pem + 证书序列号）或微信支付公钥（公钥ID + 公钥文件）齐备 |
| 阻塞上线 | 是 |

> **关于"每客户独立商户号"的现实约束**：每个商户号都需独立的对公结算账户与独立申请、审核。这是商业模型的**落地成本项而非技术项**，商户号数量增长会线性增加 BIZ 的办理工作量。建议与业务方确认首期客户数与商户号申请批次计划（见 §13 R-J）。

#### A5 腾讯云账号与 CVM

| 项 | 内容 |
|---|---|
| 需要什么材料 | 企业营业执照、法人身份证、对公账户、企业授权书（若由他人代管） |
| 在哪办 | 腾讯云控制台 → 实名认证（企业）→ 资源购买 |
| 大概多久 | 企业实名 **1 个工作日内**（腾讯云一般实时审核，复杂主体可能转人工 1–3 工作日）；CVM 购买即时交付 |
| 卡点在哪 | ① **主体必须与小程序/支付一致**（用于 ICP 备案）；② 企业实名无法自助，需对公打款或法人云端认证 |
| 验收标准 | 控制台可下单、可开票；完成 ICP 接入备案 |
| 阻塞上线 | 是 |

#### A6 短信签名与模板

| 项 | 内容 |
|---|---|
| 需要什么材料 | 营业执照、签名来源证明（企业官网/APP/公众号 截图或工单）、模板文案、使用场景说明 |
| 在哪办 | 腾讯云控制台 → 短信 → 国内短信 → 签名/模板报备 |
| 大概多久 | 签名 **1–2 个工作日**；模板 **1–2 个工作日**（需匹配对应签名；验证码类模板需单独报备） |
| 卡点在哪 | ① **签名必须能证明与主体关系**（如"深圳市瑞康大药房连锁有限公司"需有官网/公众号/APP 佐证）——这是最常见驳回原因；② 模板需符合"验证码/通知/推广"分类，推广类需单独资质；③ 营销类短信与验证码类分开签名 |
| 现状修正 | 现有 `system_sms_channel` 3 行全部 **status=0**（阿里云/腾讯云/钉钉 mock 占位），需将腾讯云渠道的 status 置 1 并填入真实的 SignName/TemplateCode/AccessKey |
| 需要哪些模板 | 见 §8.3 模板清单（注册验证码、找回密码验证码、提现结果通知、到账通知等） |
| 验收标准 | 控制台模板状态"审核通过"；`system_sms_channel` 对应记录 status=1；真实手机号可收到验证码（60 秒内） |
| 阻塞上线 | 否 —— 但**注册与找回密码在启用前不可用**，MVP 可用"管理员建号 + 微信授权登录"兜底 |

#### A7 OCR 开通

| 项 | 内容 |
|---|---|
| 需要什么材料 | 腾讯云账号即可（部分接口需开通"通用文字识别/票据识别"权限） |
| 在哪办 | 腾讯云控制台 → OCR → 开通服务 |
| 大概多久 | 即时开通 |
| 卡点在哪 | 需确认实际场景是否落入"票据/小票"类目；若自定义模板识别需单独申请 |
| 验收标准 | 用一张真实门店小票图片调用成功，返回结构化字段与置信度 |
| 阻塞上线 | 否 —— 现有 `clerk_task_submission.ocr_status/ocr_text` 与风控规则已就位，可先走人工审核 |

#### A8 内容安全（UGC 强制）

| 项 | 内容 |
|---|---|
| 需要什么材料 | 腾讯云账号；小程序 appid + 微信 access_token |
| 在哪办 | 腾讯云"天御-内容安全"开通；小程序端调用微信 `msgSecCheck` / `imgSecCheck`（以小程序服务端接口为准） |
| 大概多久 | 即时开通 |
| 卡点在哪 | 需在服务端持有小程序 access_token 并维护刷新；**必须服务端二次校验**，不能只靠前端 |
| 验收标准 | 提交含违规词的晒单文本/图片被拦截并落审核记录；正常内容通过 |
| 阻塞上线 | 是（UGC 小程序强制） |

#### A9 隐私保护指引与隐私弹窗

| 项 | 内容 |
|---|---|
| 需要什么材料 | 收集的用户信息清单（手机号、头像昵称、位置、订单/小票图片）、用途、存储期限、第三方 SDK 清单 |
| 在哪办 | 微信公众平台 → 设置 → 用户隐私保护指引；代码侧接 `wx.getPrivacySetting` / `wx.requirePrivacyAuthorize` |
| 大概多久 | 指引填写即时生效（后台自助配置）；代码接入 0.5–1 人日 |
| 卡点在哪 | ① **禁止默认勾选**；② 指引内容须与实际收集行为一致，否则审核驳回；③ 小票图片属敏感个人信息，需明确告知 |
| 验收标准 | 首次授权弹窗出现且未勾选；用户拒绝后走降级路径（不阻断核心流程但不得违规采集） |
| 阻塞上线 | 是 |

#### A10 小程序服务类目与资质 🔴 医药重点监管

| 项 | 内容 |
|---|---|
| 需要什么材料 | 医药/医疗健康类目对应资质：**《医疗机构执业许可证》/《药品经营许可证》/《互联网药品信息服务资格证书》**（按实际经营范围与所选细分类目确定具体清单）⚪ **需业务方确认** |
| 在哪办 | 微信公众平台 → 设置 → 基本设置 → 服务类目 |
| 大概多久 | 类目审核 1–3 个工作日（需资质审核时更长） |
| 卡点在哪 | **类目选错是驳回高频原因**。医药/医疗健康属重点监管行业，若类目与实际提供的服务（药店店员积分、晒单、小票 OCR）不匹配，审核直接驳回；资质过期同样驳回 |
| 建议策略 | ① 主体已有《药品经营许可证》的，类目选择"医药"下对应细类；② 若提供的是"药店营销工具"而非"在线售药"，类目选择应与实际功能一致，**不得选错**以免被判定为无资质经营；③ 提交前由 BIZ 对照资质逐条核对 |
| 验收标准 | 服务类目审核通过，状态"已通过"；资质未过期 |
| 阻塞上线 | 是 |

### 3.3 业务方办理进度跟踪表（请业务方填写）

| # | 事项 | 负责人 | 计划启动 | 预计完成 | 实际完成 | 状态 | 备注 |
|---|---|---|---|---|---|---|---|
| A1 | 域名注册 | | | | | ☐ | |
| A2 | ICP 备案 | | | | | ☐ | W1 第一天启动 |
| A3 | 小程序注册 + 认证 | | | | | ☐ | 确认是否有认证公众号可复用 |
| A4 | 微信支付商户号（首个） | | | | | ☐ | 企业 + 对公账户 |
| A5 | 腾讯云实名 | | | | | ☐ | |
| A6 | 短信签名 + 模板 | | | | | ☐ | |
| A10 | 小程序类目与资质 | | | | | ☐ | 需核对许可证有效期 |

---

## 4. 腾讯云资源选型建议

### 4.1 形态选型：CVM vs 轻量应用服务器

| 维度 | CVM（标准型） | 轻量应用服务器（Lighthouse） | 建议 |
|---|---|---|---|
| 部署方式 | 独立 CVM 实例，可挂载 CBS 云硬盘 | 整机打包的轻量实例 | 两者均可跑本项目 |
| 扩容弹性 | **强**：CPU/内存/云硬盘独立升降配、跨可用区迁移、快照镜像 | 弱：升级需换套餐，重装系统会重置 | **选 CVM**（后续要扩容要加盘要快照） |
| 网络 | 独立 VPC/子网/弹性网卡/CLB 对接完善 | 有独立 IP 与防火墙，但 CLB 对接较弱 | 需 CDN/CLB → **选 CVM** |
| 数据库托管对接 | 与 TDSQL-C / 云数据库 MySQL 同 VPC 低延迟直连 | 同 VPC 也可，但网络与运维体验略逊 | 有计划上云数据库 → **选 CVM** |
| 负载均衡 | CLB 直接绑定 CVM | CLB 只能绑 CVM（部分规则限制） | **选 CVM** |
| 镜像与快照 | 制作自定义镜像、跨地域复制 | 快照功能较弱 | 需要"一键回滚环境" → **选 CVM** |
| 价格（同 4核8G 档） | 参考 2,000–4,500 元/年 | 参考 1,200–2,500 元/年 | CVM 略贵 |
| 运维自由度 | 需自行做安全组、监控、备份编排 | 自带防火墙与一键快照 | 团队规模小时轻量更省心 |
| **本项目结论** | ✅ **推荐 CVM**（4核8G 起） | 🟡 作为预算紧张时的备选 | 见 §4.2 |

> ⚠️ 当前 h02 实测为 **8核15G**，且已知构建期 load 飙 38。生产环境**不在 CVM 上做前端构建**（见 §4.6 流水线），运行规格 4核8G 即可；但若要保留"生产机兼构建机"的临时方案，需按 8核16G 采购。

### 4.2 推荐规格与配置

| 资源 | 推荐规格 | 参考价（元/年） | 必需/可选 | 理由 |
|---|---|---|---|---|
| CVM | **S6/SA5 系列 4核8G**（生产首选） | 2,000–4,000 | 🔴 必须 | 跑 server + web + worker；构建迁出后 4核8G 足够 |
| CVM（构建机，可选） | 8核16G，按需计费，用完即停 | 按需 ≈ 0.6–1.2 元/小时 | 🟡 可选 | 规避"构建期 load 38 整机重启"铁律 |
| 系统盘 | 高性能云硬盘 SSD 100G | 200–400 | 🔴 必须 | 镜像 + 日志 + 数据库本地缓存 |
| 数据盘 | 高性能云硬盘 SSD 100–200G（独立挂载到 MySQL 数据目录） | 300–800 | 🟡 可选 | 便于扩容与快照，强烈建议 |
| 公网带宽 | **按固定带宽 5–10 Mbps**（初期） | 带宽费 ≈ 900–1,800 元/年 | 🔴 必须 | 静态资源走 CDN 后，动态流量很小 |
| CLB 负载均衡 | 标准型（公网）+ 监听 443 | 700–2,000 | 🟡 可选 | 初期单机直出即可；多实例/高可用时再上 |
| CDN | 静态资源 + 商品/晒单图片 | 500–5,000 | 🟡 可选 | 图片流量大时收益显著；COS+CDN 需先配回源与鉴权 |
| 云数据库 MySQL 8.0 | **TDSQL-C MySQL 版 通用型 2核4G（高可用）** 或 云数据库 MySQL 8.0 高可用版 | 3,000–9,000 | 🔴 必须 | 免自建备份/主从切换；与现 MySQL 8 兼容 |
| 云 Redis | 标准架构 1G 主从 | 600–1,500 | 🟡 可选 | 见 §4.4 |
| COS | 桶（**沿用现有 `cos.ap-shanghai...` endpoint 与配置**） | 按量 | 🔴 必须 | master 已是 COS，避免迁移 |
| TCR | 企业版（需支持镜像扫描与多地域） | 300–3,000 | 🔴 必须 | 替代内网 HTTP Registry |
| 日志服务 CLS | 索引 + 告警 | 0–1,500 | 🟡 可选 | 生产必须至少开 nginx/access + 错误日志采集 |
| 云监控 | 自定义告警（CPU/内存/磁盘/连接数） | 免费额度内 | 🔴 必须 | |
| 快照 | 每日自动快照，保留 7–14 天 | 200–600 | 🔴 必须 | 回滚点 |

### 4.3 网络拓扑与端口规划

```
                        ┌──────────────────────────────┐
   微信小程序 ──HTTPS──▶│  安全组 (仅 443/80 对公)      │
   门店/客户 H5 ───────▶│  CVM: ydbsrv-prod-01         │
   运维 Tailscale ─────▶│  ┌────────┬────────┬──────┐ │
                        │  │web    │server  │worker│ │
   本机数据库 ─────────▶│  │nginx  │48080   │异步  │ │
                        │  │ 80/443│ 127.0.0.1│     │ │
                        │  └────────┴────────┴──────┘ │
                        │  MySQL(云数据库)  Redis(云Redis)│
                        └──────────────────────────────┘
```

**安全组端口清单（唯一权威表）**

| 端口 | 协议 | 来源 | 用途 | 必需/可选 |
|---|---|---|---|---|
| **443** | TCP | `0.0.0.0/0` | HTTPS 对外（商城 H5 / 后台 / 店员端 / 医药前台 / API） | 🔴 必须 |
| 80 | TCP | `0.0.0.0/0` | 仅用于跳转 HTTPS + 证书验证（签发后建议 80 只做 301） | 🔴 必须 |
| 22 | TCP | 办公 IP 白名单 / Tailscale 网段 `100.64.0.0/10` | SSH 运维 | 🔴 必须 |
| 3306 | TCP | 仅 CVM 私网 IP / Tailscale 网段 | MySQL 直连运维 | 🟡 可选（用云数据库则可不开） |
| 6379 | TCP | 仅 CVM 私网 IP / Tailscale 网段 | Redis 直连运维 | 🟡 可选 |
| 48080 | TCP | **不开放** | 后端直连端口，仅绑 `127.0.0.1` | 🔴 必须（保持关闭） |
| 3010 / 3001 | TCP | Tailscale 网段 | Gitea 源码仓 | 🟡 可选（生产建议迁到内网 Gitea 或关闭） |
| Tailscale | UDP 41641 / 派生端口 | `0.0.0.0/0`（仅 Tailscale 走 UDP 打洞） | 组网 | 🟡 可选 |

> **铁律复述（不可违背）**：后端 48080 **必须只绑 `127.0.0.1`**，nginx 反代到 `127.0.0.1:48080`。任何"图省事"把 48080 直接对公暴露的做法都视为安全事故。

### 4.4 MySQL 与 Redis 的取舍

| 决策点 | 方案 A（自建） | 方案 B（上云） | 建议 |
|---|---|---|---|
| MySQL 部署 | CVM 内 `ydb-hb-mysql`（MySQL 8） | TDSQL-C MySQL 版 / 云数据库 MySQL 8.0 | 🟡 **建议 B**。理由：备份、主从切换、慢查询分析、监控告警都是长期工作量；芋道代码兼容 MySQL 8 协议，改造成本低（仅连接串与字符集）。但**必须先做一次读写分离与连接池压测**（芋道多租户 + 报表查询易打满） |
| MySQL 成本 | 0（占用 CVM 资源） | 3,000–9,000 元/年 | B 约等于多 1–2 台机器的等价成本，换来可用性 |
| Redis 部署 | CVM 内 `ydb-hb-redis`（6-alpine） | 云 Redis 1G 主从 | 🟡 初期可自建（省 600–1,500/年）；当出现"重启丢缓存导致登录态丢失/限流失效"或需要多实例共享会话时必须上云 |
| Redis 安全 | 必须 `requirepass` + `bind 127.0.0.1` | 云 Redis 需设密码 + 白名单 | 两者都必须设密码 |
| 迁移复杂度 | 无 | 需数据迁移 + 停机窗口 + 回滚方案 | 见 §9.5 |
| **MVP 建议** | ✅ **MVP 用自建 MySQL/Redis 但必须改强口令 + 加备份** | | 上云数据库作为上线后 2–4 周的迭代项 |

> 折中路径（推荐）：**MVP 阶段 CVM 自建 MySQL + 每日全量备份到 COS**；上线稳定后 2–4 周迁至云数据库。这样把"迁移风险"推迟到"系统稳定、且有备份可回滚"的窗口。

### 4.5 对象存储 COS

| 项 | 现状 / 建议 |
|---|---|
| 现状 | ✅ **master 已是腾讯云 COS**：`infra_file_config` id=22，storage=20（S3 类型），endpoint `cos.ap-shanghai...` → 这块可直接用 |
| 动作 | ① 核对其他租户的 `infra_file_config` 是否同样指向 COS（不要让某租户还指向本地磁盘）；② 生产桶开启**版本控制**与**生命周期**（晒单小票图片按业务约定保留期自动归档）；③ 开启**回源鉴权**并配置 CDN 时防止盗刷流量 |
| 晒单图片敏感性 | 小票图片可能含顾客姓名/手机/金额 → 桶应为**私有读**，通过后端签名 URL 下发；CDN 需开启 URL 鉴权 |

### 4.6 镜像仓库 TCR 与 CI/CD

| 现状问题 | 目标 |
|---|---|
| 镜像全部来自内网 Registry `100.66.41.15:5000/ydb-hb-*`（h04 中转仓，**HTTP + insecure-registries**） | 改为腾讯云 TCR（`*.tencentcloudcr.com`），HTTPS 拉取，删除 `insecure-registries` 配置 |

| 步骤 | 动作 | 负责人 | 产出物 | 验收标准 |
|---|---|---|---|---|
| 1 | 开通 TCR 企业版，建命名空间（如 `ydb-hb`），配置**镜像扫描**与**多地域同步** | OPS | TCR 实例 + 命名空间 | 控制台可推送/拉取 |
| 2 | 配置 TCR 专属**免密拉取**凭证（配置到 CVM `/root/.docker/config.json` 或 K8s secret） | OPS | 凭证 | CVM 可 `docker pull` 成功且**不需要明文密码入库** |
| 3 | CI 中 `docker push` 到 TCR，tag 用 `git-commit-sha` | TECH | CI 配置 | 每个 commit 可追溯到唯一镜像 |
| 4 | 生产 compose 引用 TCR 地址，删除 `insecure-registries` | TECH | 新 compose | `docker compose config` 无 insecure 项 |
| 5 | 迁移 h04 中转仓存量镜像 | TECH | 镜像清单 | 生产所需 4 个镜像全部在 TCR 中可拉取 |

**构建注意事项（源自已踩坑事实，必须固化到流水线）**

1. **构建机与运行机分离**：构建在 CI/独立构建机进行，生产 CVM **只拉镜像不构建**。原因：h02 构建期 load 飙 38 会整机重启。
2. **构建限流**：本地/CI 构建命令前缀 `nice -n 15`。
3. **前端产物发布禁用 `rm -rf dist`**：会删掉 bind mount 挂载点 inode，导致 nginx `try_files` 循环 500。必须使用 `find ./dist -mindepth 1 -delete`。
4. **依赖锁死**：`@swc/core` 必须锁 `1.15.47`（升 `1.16.2` 与 `vite-plugin-top-level-await@1.6.0` 冲突报 `missing field 'type'`；删插件则产物结构错乱导致刷新白屏 `Cannot read properties of null (reading 'nextSibling')`）。
5. **动态路由不可整页刷新**：芋道动态路由表登录后注入内存，生产引导文案必须要求用户用菜单切换，不可 F5。

### 4.7 资源采购一次性清单（可直接下单）

| 序号 | 云资源 | 规格 | 数量 | 交付方式 |
|---|---|---|---|---|
| 1 | VPC | 1 个默认 VPC + 1 子网 | 1 | 控制台 |
| 2 | CVM | S6 4核8G + 100G SSD 系统盘 + 100G SSD 数据盘 + 10Mbps 固定带宽 | 1 | 控制台 |
| 3 | 安全组 | 按 §4.3 端口表 | 1 | 控制台 |
| 4 | 快照计划 | 每日 02:00，保留 14 天 | 1 | 控制台 |
| 5 | TCR | 企业版 | 1 | 控制台 |
| 6 | COS | 沿用现有桶；另建 `ydb-hb-backup` 桶存备份 | 2 | 控制台 |
| 7 | 云监控告警 | CPU>80%、内存>85%、磁盘>80%、MySQL 连接数、CVM 不可用 | 5 条 | 控制台 |
| 8 | 免费/付费 SSL | Let's Encrypt（certbot） | — | 自动化 |
| 9 | 云数据库 MySQL | （二期）TDSQL-C 2核4G 高可用 | 1 | MVP 后 |
| 10 | 云 Redis | （二期）1G 主从 | 1 | MVP 后 |
| 11 | CLB | （二期）标准型 | 1 | 需高可用时 |

## 5. 域名与 HTTPS 实施

### 5.1 实施总流程

| 步骤 | 动作 | 负责人 | 前置 | 产出物 | 验收标准 |
|---|---|---|---|---|---|
| 1 | 注册域名并完成企业实名 | BIZ | — | 域名证书 | whois 主体 = 营业执照全称 |
| 2 | 解析 A 记录指向 CVM 公网 IP（先指向待上线 IP，用于备案核验） | OPS | 1 + CVM 已购 | DNS 记录 | `dig` 返回目标 IP |
| 3 | 腾讯云提交 ICP 备案 | BIZ + TECH | 1 + 2 | 备案号 | 备案状态"备案成功" |
| 4 | 域名接入备案到腾讯云 | OPS | 3 | 接入备案通过 | 控制台可访问 |
| 5 | 安装 certbot + 申请 Let's Encrypt 证书（webroot 模式） | TECH | 3 + CVM | 证书链 | `certbot certificates` 显示有效期 90 天 |
| 6 | 配置 certbot 自动续期（timer/cron + `--dry-run` 验证） | TECH | 5 | 续期定时器 | dry-run 成功 |
| 7 | nginx 配置 443 + 反代 + 安全响应头 | TECH | 5 | nginx 配置 | `curl -I` 校验通过 |
| 8 | 微信公众平台配置合法域名清单 | TECH | 3 | 合法域名配置 | 开发者工具无"不在以下 request 合法域名列表中"报错 |
| 9 | 全链路回归（H5 / 后台 / 店员端 / 医药前台 / API） | TECH | 6,7,8 | 回归报告 | 全部功能正常 |

### 5.2 nginx 反代配置要点

> 当前 h02 拓扑需在生产上等价复刻：**web（nginx 1.27-alpine）对外提供 8081（商城 H5）与 8082（后台 + 店员端 + 医药前台），server 只绑 127.0.0.1:48080**。生产改为 nginx 直接监听 443。

| 要点 | 具体做法 | 踩坑关联 |
|---|---|---|
| **上游绑定** | `proxy_pass http://127.0.0.1:48080;` | 后端 48080 必须绑 `127.0.0.1`，nginx 同容器/同机反代 |
| **静态资源路由** | `/mall-h5/` → `./frontend/mall-h5`；`/admin/` → `./frontend/admin`；`/clerk/` → `./frontend/clerk`；`/pharma/` → `./frontend/pharma`；均绑定 mount 到 `/usr/share/nginx/html/` | 四个前端目录是 bind mount，**发布时不得 `rm -rf dist`**，必须 `find -mindepth 1 -delete` |
| **try_files 兜底** | `try_files $uri $uri/ /index.html;` | 若挂载点 inode 被删会陷入 try_files 循环 → 500 |
| **H5 history 模式** | H5 端如用 history 路由，必须有 `location / { try_files ... /index.html; }` 且 `location /api/` 前置 | |
| **上传体积** | `client_max_body_size 20m;`（晒单小票图片必配） | 否则上传 413 |
| **上传超时** | `proxy_read_timeout 120s; proxy_send_timeout 120s;` | OCR 同步识别会耗时 |
| **真实 IP** | `proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;` | 微信回调 IP 白名单、风控 IP 判定都依赖 |
| **HTTPS 跳转** | 80 端口只做 `return 301 https://$host$request_uri;` | |
| **证书路径** | `/etc/letsencrypt/live/<domain>/fullchain.pem` + `privkey.pem` | |

**反代 server 块骨架（参数化，按实际域名替换）**

```nginx
server {
    listen 80;
    server_name mall.example.cn api.example.cn;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.cn;

    ssl_certificate     /etc/letsencrypt/live/api.example.cn/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.cn/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options    "nosniff" always;
    add_header X-Frame-Options           "SAMEORIGIN" always;
    add_header Referrer-Policy           "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy        "camera=(), microphone=(), geolocation=()" always;
    server_tokens off;

    client_max_body_size 20m;
    proxy_read_timeout 120s;
    proxy_send_timeout 120s;

    location / {
        proxy_pass http://127.0.0.1:48080;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 443 ssl;
    http2 on;
    server_name mall.example.cn;

    ssl_certificate     /etc/letsencrypt/live/mall.example.cn/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mall.example.cn/privkey.pem;
    # ...（与上面一致的 ssl/security 配置）

    root /usr/share/nginx/html;

    location / { try_files $uri $uri/ /index.html; }
    location /admin/  { try_files $uri $uri/ /admin/index.html; }
    location /clerk/  { try_files $uri $uri/ /clerk/index.html; }
    location /pharma/ { try_files $uri $uri/ /pharma/index.html; }
}
```

> ⚠️ `Permissions-Policy` 中 `geolocation=()` 会禁掉定位。若业务使用门店定位，需改为 `geolocation=(self)` 并在小程序隐私指引中声明。

### 5.3 HSTS 与安全响应头

| 响应头 | 生产建议值 | 说明 |
|---|---|---|
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | 上线前确认所有子域均已 HTTPS，否则**先不加 `includeSubDomains`** 避免子域被锁死 |
| `X-Content-Type-Options` | `nosniff` | 防 MIME 嗅探 |
| `X-Frame-Options` | `SAMEORIGIN` | 防点击劫持（后台尤其需要） |
| `Referrer-Policy` | `strict-origin-when-cross-origin` | 减少 referrer 泄露 |
| `Content-Security-Policy` | 需按前端实际资源域名定制 | 🟡 可选。芋道前端含内联脚本，严格 CSP 改造量大，先不做，但需记录为技术债 |
| `server_tokens` | `off` | 不暴露 nginx 版本 |
| `Permissions-Policy` | 按业务最小授权 | |

**证书选型**

| 方案 | 成本 | 续期 | 适用 |
|---|---|---|---|
| **Let's Encrypt（certbot）** 🔴 | 0 | 90 天，需自动续期 | **推荐**：多子域可用 SAN 证书 |
| 腾讯云免费 DV 证书 | 0 | 需手动/云托管续期 | 可作为灾备，但不支持多 SAN 便利 |
| 付费 DV（ DigiCert / GlobalSign 等） | 300–1,500 元/年 | 1 年 | 🟡 可选：想要更高保险额度/企业级支持时 |

**Let's Encrypt 自动续期落地要点**

1. certbot 使用 `webroot` 插件（nginx `location /.well-known/acme-challenge/` 指向固定目录），**避免用 `--standalone` 停 nginx**。
2. 续期钩子执行 `nginx -s reload`（或 `systemctl reload nginx`）。
3. 通过 systemd timer 或 cron 每日执行 `certbot renew --quiet`；**必须先跑 `certbot renew --dry-run` 验证通过**再上线。
4. 验收：手动 `certbot renew --force-renewal --dry-run` 成功，且 `systemctl list-timers | grep certbot` 有活动项。

### 5.4 微信要求的合法域名配置清单

> 微信公众平台 → 开发管理 → 开发设置 → 服务器域名。**每类域名上限 200 个，每个域名必须已 ICP 备案、必须 HTTPS、必须带端口则端口需为 443。**

| 类别 | 微信字段 | 我方需填 | 备注 |
|---|---|---|---|
| request 合法域名 | request 合法域名 | `https://api.example.cn` | 接口调用域名，**最多填 200 个**；小程序端所有 `request`/`uploadFile`/`downloadFile` 的 host 必须在此列表 |
| 业务域名 | 业务域名 | `https://mall.example.cn` | H5 页面嵌入 web-view 时必需；不嵌 web-view 也建议配 |
| **uploadFile 合法域名** | uploadFile 合法域名 | `https://api.example.cn` | 🔴 **晒单小票图片上传必配**，且必须与 request 合法域名一致 |
| **downloadFile 合法域名** | downloadFile 合法域名 | `https://api.example.cn`、`https://<bucket>.cos.ap-shanghai.myqcloud.com` | 若前端直接下载 COS 原图需单独填 COS 域名；**推荐改用后端签名 URL** 以免暴露桶地址 |
| **socket 合法域名（websocket）** | socket 合法域名 | `wss://api.example.cn` | 若有实时通知（提现状态推送、订单状态）必配；注意协议必须 `wss://` |
| 后台 request 合法域名 | — | 不适用 | |

**额外校验点**

| 检查项 | 要求 |
|---|---|
| 域名备案 | 每个域名必须与小程序主体一致地完成 ICP 备案 |
| 证书 | 必须是受信任 CA 签发、有效期 ≥ 3 个月、**支持 TLS 1.2+**、证书链完整（Let's Encrypt 链完整，配置时用 `fullchain.pem`） |
| 端口 | 不支持自定义端口，https 默认 443 |
| 域名数量 | request/uploadFile/downloadFile 各 ≤ 200 |
| 重定向 | 合法域名校验会跟随 301/302，**确保 API 不做跨域重定向** |

### 5.5 域名与 HTTPS 验收清单

| # | 验收项 | 方法 | 通过标准 |
|---|---|---|---|
| 1 | 全站 HTTPS | `curl -I https://mall.example.cn` | 200/301，无证书告警 |
| 2 | HTTP 自动跳转 | `curl -I http://mall.example.cn` | 301 → https |
| 3 | HSTS 生效 | `curl -I ... \| grep -i strict-transport` | 存在且 max-age ≥ 31536000 |
| 4 | 安全头齐全 | `curl -I ...` | 至少 4 个安全响应头存在 |
| 5 | 证书链完整 | `openssl s_client -connect api.example.cn:443` 无 verify error | 校验通过 |
| 6 | 证书自动续期 | `certbot renew --dry-run` | 成功 |
| 7 | TLS 版本 | `nmap --script ssl-enum-ciphers` 或 `testssl.sh` | 仅 TLSv1.2/1.3 |
| 8 | 上传可用 | 小程序端上传 5MB 小票图 | 成功，不报 413 |
| 9 | 微信合法域名 | 开发者工具真机调试 | 无合法域名报错 |
| 10 | 商城/后台/店员端/医药前台 | 逐个访问并登录 | 全部可用，无 500 |

---

## 6. 每客户独立商户号的落地设计

### 6.1 设计依据（已核实的芋道原生能力）

| 芋道现有能力 | 事实 | 对本方案的意义 |
|---|---|---|
| `pay_app` 表结构 | 带 `tenant_id` | 每个租户可挂独立支付应用 |
| `pay_channel` 表结构 | `PayChannelDO extends TenantBaseDO`，带 `tenant_id` | 每个租户可配独立商户号渠道 |
| `WxPayClientConfig` | 支持 appId / mchId / apiVersion=v3 / privateKeyContent / apiV3Key / certSerialNo / publicKeyContent / publicKeyId | **一套完整商户号所需的配置项芋道原生齐备** |
| `PayClientFactoryImpl` | 按 channelId 缓存客户端，支持多商户号并行 | **无需改造核心支付框架** |
| 微信支付客户端实现 | WxPub(公众号) / WxLite(小程序) / WxApp / WxBar(付款码) / WxNative(扫码) / WxWap | 小程序端用 **WxLite** |
| `pay_transfer` 链路 | `PayTransferService` / `PayTransferSyncJob` / `PayTransferApi` 完整 | 本轮不用；未来接"商家转账"直接复用 |

**结论**：本方案的最大技术增量不是"多商户号"（芋道已原生支持），而是 **密钥安全管理** 与 **支付回调按租户路由的验证**。

### 6.2 数据模型映射

| 芋道表 | 字段 | 本方案用途 | 每客户一行？ |
|---|---|---|---|
| `pay_app` | `tenant_id` | 区分租户 | ✅ 是 |
| `pay_app` | `app_id` | 小程序 appid（**统一为「要店帮」主体 appid**，非每客户不同） | 否（共用主体 appid） |
| `pay_app` | `name` | 应用名，如"要店帮-示范大药房" | ✅ |
| `pay_channel` | `tenant_id` | 区分租户 | ✅ 是 |
| `pay_channel` | `code` | `wx_pub` / `wx_lite` / `wx_app` / `wx_bar` / `wx_native` / `wx_wap` | ✅ |
| `pay_channel` | `config`（JSON） | `WxPayClientConfig` 全量：appId / mchId / apiVersion=v3 / privateKeyContent / apiV3Key / certSerialNo / publicKeyContent / publicKeyId | ✅ |
| `pay_channel` | `status` | 1=启用 | ✅ |
| `pay_order` | `tenant_id` | 订单隔离 | ✅ |
| `pay_transfer` | `tenant_id` | 转账隔离（本轮人工台账仍写入以留痕） | ✅ |

**配置示例（`pay_channel.config` JSON 骨架，密钥字段用占位符）**

```json
{
  "appId": "wx<replace-with-ydb-miniapp-appid>",
  "mchId": "16<replace-with-tenant-mchid>",
  "apiVersion": "v3",
  "privateKeyContent": "<由密钥管理系统注入，不入库>",
  "apiV3Key": "<由密钥管理系统注入，不入库>",
  "certSerialNo": "<证书序列号>",
  "publicKeyContent": "<微信支付公钥文件内容，或使用公钥模式>",
  "publicKeyId": "<微信支付公钥ID>"
}
```

### 6.3 密钥安全管理设计 🔴

> **红线**：`privateKeyContent` / `apiV3Key` / `publicKeyContent` **绝对不能明文存进 `pay_channel.config`，也绝对不能进 git**。芋道的 `pay_channel.config` 是数据库 TEXT 字段，直接后台填写会明文落库。

**三级方案（按落地难度递进）**

| 方案 | 做法 | 落地难度 | 安全等级 | 建议 |
|---|---|---|---|---|
| **S1 环境变量/密文注入**（推荐 MVP） | 数据库中 `config` 只存**占位符**（如 `${YDB_WXPAY_KEY_TENANT_121}`），应用启动时从**外部密文来源**（见 S2）或环境变量文件注入并替换；注入后不写回 DB | 中 | ★★★☆ | ✅ MVP 采用 |
| **S2 腾讯云 KMS / 云密钥管理服务** | 商户号私钥、APIv3 密钥存 KMS，应用侧通过 CAM 角色 + KMS 解密接口获取明文（带缓存与轮换） | 高 | ★★★★☆ | ✅ 生产目标态（上线后 2–4 周） |
| **S3 密钥托管服务（阿里云 KMS 等）** | 同上，跨云 | 高 | ★★★★☆ | 🟡 备选 |

**S1 落地细节（MVP）**

| 项 | 做法 |
|---|---|
| 密文文件位置 | `/etc/ydb-hb/secrets/tenant_<tenantId>.env`，权限 `0600`，属主 `root`，**不纳入 docker bind mount 的目录**（或挂载为只读且容器内非 root 可读） |
| 注入时机 | 容器 entrypoint 脚本读取 env 文件 → 生成运行时 `pay_channel.config` 的内存副本（**不落盘到 DB**）；或由启动时同步任务写入带密文占位符的 DB，实际调用支付时解密 |
| `.gitignore` | 显式加入 `.env`、`*.pem`、`secrets/`、`/deploy/secrets` |
| 仓库检查 | CI 中加密钥扫描（如 `gitleaks`/`trufflehog`），发现密钥直接 fail build |
| 轮换 | APIv3 密钥修改需与商户平台同步，轮换窗口选低峰；商户证书年度更新需提前 30 天排期 |
| 日志脱敏 | 支付日志中 `apiV3Key`/`privateKey`/`mchId` 需掩码（`mchId` 可保留前 6 后 4） |
| 应急 | 若密钥疑似泄露：立即在商户平台重置 APIv3 密钥 / 重新签发证书 → 更新 KMS/env → 重启服务 → 核查 `pay_order` 异常 |

### 6.4 配置流程（每客户一套）

| 步骤 | 动作 | 负责人 | 产出物 | 验收标准 |
|---|---|---|---|---|
| 1 | 客户签约，确认是否需要独立商户号 | BIZ | 客户确认单（⚪ 需业务方确认：是否所有客户独立，还是仅部分大客户独立） | 书面确认 |
| 2 | 申请微信支付商户号（主体：要店帮；结算：对公账户） | BIZ | 商户号 mchId | 商户平台可登录、状态正常 |
| 3 | 配置 APIv3 密钥、下载商户证书 `apiclient_key.pem`、获取证书序列号（或申请微信支付公钥 + 公钥ID） | BIZ + TECH | 证书与序列号 | 平台侧可发起接口测试 |
| 4 | 在 `pay_app` 建应用（tenant_id + 名称 + 共用 appid） | TECH | `pay_app` 行 | 记录 id |
| 5 | 在 `pay_channel` 建 `wx_lite` 渠道，`config` 写**占位符**，status=1（先 0，联调后置 1） | TECH | `pay_channel` 行 | 记录 id |
| 6 | 密钥写入 `/etc/ydb-hb/secrets/tenant_<id>.env`（0600） | TECH | env 文件 | `stat -c %a` = 600 |
| 7 | 重启/热加载服务，验证 `PayClientFactoryImpl` 能为该 channelId 创建客户端 | TECH | 日志 | 日志出现对应 mchId 客户端初始化成功（脱敏） |
| 8 | 跑 1 元真实支付（小程序端） | TECH + BIZ | 测试订单 | `pay_order` 落库、回调验签通过、订单状态 = 已支付 |
| 9 | 渠道 status 置 1，纳入灰度 | TECH | 配置变更记录 | 商户平台交易可见 |
| 10 | 登记到《商户号台账》（含 mchId、appId、密钥版本、到期日、责任人） | TECH + FIN | 台账表 | 台账完整、无明文密钥 |

### 6.5 支付回调按租户路由

| 关键点 | 设计 | 验收 |
|---|---|---|
| 回调 URL 形态 | 建议 `https://api.example.cn/notify/pay/{tenantId}` 或用芋道原生回调并在服务端按订单反查 tenant_id | 两租户各下一单，交错回调，各自订单状态正确 |
| **租户上下文来源** | 🔴 **以订单/交易记录中的 tenant_id 为准**，绝不能信任回调 URL 参数或请求体中的租户字段（可被伪造） | 构造跨租户伪造回调 → 被拒并告警 |
| 回调验签 | 使用该商户号自己的**平台证书 / 微信支付公钥**验签（`mchId` 维度） | 篡改报文的回调 → 验签失败，拒绝 |
| 金额与商户号校验 | 回调金额必须与 `pay_order` 金额一致；`mchid` 必须等于该订单所属租户的渠道 mchId | 不一致 → 拒绝 + 告警 + 记录 |
| 幂等 | 回调处理按 `out_trade_no` + 通知 ID 唯一约束，重复回调只处理一次 | 重复投递同一回调 → 订单状态不二次变更 |
| 回调来源 IP | 记录 `X-Forwarded-For` 首个公网 IP 并落日志；配合微信官方 IP 段校验 | 伪造 IP 的回调 → 拒绝 |
| 响应体 | 成功必须返回微信要求的成功报文（否则微信会持续重试 15 次左右） | 重试场景下最终状态收敛 |
| 重试与补偿 | 回调丢失 → 由主动查单（`PayTransferSyncJob` 同思路的订单查询任务）兜底 | 模拟丢回调 → 定时任务能补单 |
| 日志 | 回调原文（脱敏）落库/落日志，便于对账 | 可查最近 7 天回调记录 |

### 6.6 切换与灰度

| 阶段 | 范围 | 动作 | 回滚 |
|---|---|---|---|
| G0 内网联调 | Tailscale 环境 | 首个商户号跑通 1 元支付 + 回调 | — |
| G1 灰度（1 租户） | 1 个示范大药房租户 | 真实商户号上线，小范围真实用户 | 渠道 status 置 0 → 立即停止新支付；已支付订单照常回调 |
| G2 灰度（3 租户） | 3 个租户 | 观察支付成功率、回调延迟、对账差异 | 同上 |
| G3 全量 | 全部客户 | 逐步开放 | 同上 |
| 应急停止 | 全局 | 一键把全部 `pay_channel.status` 置 0（保留历史可查） | 恢复置 1 |

**灰度期监控指标**

| 指标 | 阈值 | 处置 |
|---|---|---|
| 支付发起成功率 | < 98% 持续 10 分钟 | 停止放量，排查 |
| 回调验签失败次数 | > 0 | 立即告警 + 停止放量 |
| 支付成功但订单未更新数 | > 0 | 触发主动查单补偿 |
| 对账差异（渠道流水 vs `pay_order`） | 金额差 ≠ 0 | 冻结该租户打款，人工核查 |

### 6.7 异常处理

| 异常 | 现象 | 处理 | 责任人 |
|---|---|---|---|
| APIv3 密钥错误 | 签名失败 `SIGN_ERROR` | 从 env/KMS 重新注入并重启 | TECH |
| 证书过期 | `NO_AUTH` / 证书校验失败 | 提前 30 天换证（商户平台年度证书更新） | TECH + BIZ |
| 回调验签失败 | 日志 `verify failed` | 拒绝处理 + 告警；核对是否误用其他商户号公钥 | TECH |
| 金额不一致 | `AMOUNT_MISMATCH` | 拒绝 + 落异常表 + 人工核查 | TECH + FIN |
| 重复回调 | 订单被重复更新 | 幂等键拦截（唯一索引） | TECH |
| 商户号被冻结/风控 | 下单返回 `USER_ACCOUNT_ABNORMAL` | BIZ 联系微信支付商户平台处理 | BIZ |
| 结算账户异常 | 提现到账失败 | 人工打款走对公账户，异常需 FIN 处理 | FIN |
| 一客户一商户号导致费率差异 | 各商户号费率不同 | 财务按商户号维护费率表（§7.6） | FIN |

---

## 7. 红包发放：人工打款台账方案

### 7.1 为什么本轮不接微信自动转账

| 维度 | 微信商家转账（自动） | 人工打款台账（本轮选定） | 判定 |
|---|---|---|---|
| 准入门槛 | 2026-05-13 起新开通商户**不再直接申请"商家红包/现金红包"**，统一走"商家转账"；需提交**转账场景证明资料**；需开通**运营账户**（次日 0:00 生效且**不可关闭**，所有运营业务强制从运营账户出资） | 无平台准入要求 | 自动转账**准入不确定、材料需业务方提供**，是本轮最大不确定性 |
| 技术要求 | 需 APIv3 密钥 + 商户证书（或微信支付公钥）、**请求来源 IP 必配**才能调 API | 无 API 依赖 | 自动转账有额外 IP/证书配置项 |
| 资金成本 | 运营账户需预先充值，余额不足则打款失败 | 财务按批次打款，额度可控 | 自动需运营账户资金垫付 |
| 产品能力边界 | 红包样式单笔 **≤ 200 元**；**暂不支持小微商户**（个体工商户部分场景支持） | 无金额上限（走银行转账） | 200 元上限可能不够覆盖大额提现 |
| 失败处理 | 失败需重试/对账，依赖幂等与状态机 | 人工重试 | 人工可控 |
| 实施工作量 | 需实现商家转账 API + 场景证明 + 运营账户配置 + 对账 | 需实现台账 + 审批 + 凭证 | 人工显著更小 |
| 结论 | 保留为后续扩展点（芋道 `pay_transfer` 链路已具备） | **本轮采用** | ✅ 客户已拍板 |

### 7.2 台账数据模型（新增表，建议）

> 芋道已有 `pay_transfer`（转账单）链路，可在 `clerk_withdraw` 与 `pay_transfer` 之间建立映射；下表为**业务台账表**的最小字段集。

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | bigint | 主键 |
| `tenant_id` | bigint | 租户隔离（与芋道 `TenantBaseDO` 对齐） |
| `withdraw_id` | bigint | 对应 `clerk_withdraw.id` |
| `apply_no` | varchar(64) | 申请单号（对客唯一，建议 `WD+yyyyMMdd+seq`） |
| `clerk_member_id` | bigint | 申请店员 |
| `payee_name` | varchar(64) | 收款人姓名 |
| `payee_account` | varchar(128) | 收款账号（银行卡/微信零钱） |
| `payee_account_masked` | varchar(64) | 掩码展示用（如 `6222****1234`） |
| `amount` | decimal(12,2) | 打款金额 |
| `fee` | decimal(12,2) | 手续费（默认 0） |
| `status` | tinyint | 状态机（见 §7.3） |
| `audit_user_id` | bigint | 审核人 |
| `audit_time` | datetime | 审核时间 |
| `audit_remark` | varchar(255) | 审核意见 |
| `pay_channel_type` | tinyint | 打款方式：1=对公转账 2=支付宝 3=其他人工渠道 |
| `pay_voucher_url` | varchar(512) | 凭证图片/文件（COS 私有桶 + 签名 URL） |
| `pay_batch_no` | varchar(64) | 打款批次号（一次批量转账共享） |
| `pay_time` | datetime | 实际打款时间 |
| `pay_operator` | varchar(64) | 打款操作人 |
| `reconcile_status` | tinyint | 对账状态：0=未对账 1=已对账 2=有差异 |
| `reconcile_remark` | varchar(255) | 对账备注 |
| `remark` | varchar(255) | 备注 |
| `tenant_id`/`creator`/`create_time`/`updater`/`update_time`/`deleted` | — | 对齐芋道 `BaseDO` 规范 |

**约束（防重打的核心）**

| 约束 | 实现 |
|---|---|
| 一个提现单只能有一张有效台账 | `withdraw_id` + `deleted=0` 唯一索引 |
| 打款幂等 | `apply_no` 唯一索引；打款接口以 `apply_no` 为幂等键 |
| 状态机单向 | `status` 只允许按 §7.3 顺序流转，`WHERE status = 期望的上一状态` 条件更新（乐观更新），避免并发 |
| 金额不可篡改 | 打款时校验 `amount == withdraw.amount`，不一致拒绝 |

### 7.3 状态机

```
  申请提交
     │
     ▼
 [1 待审核] ──审核驳回──▶ [9 已驳回]（终态，notify 申请人）
     │ 审核通过
     ▼
 [2 待打款] ──打款登记（填批次号+凭证）──▶ [3 已打款] ──对账一致──▶ [4 已对账]（终态）
     │                        │
     │                        └──对账差异──▶ [5 对账差异] ──核实修正──▶ [4 已对账]
     │ 打款失败
     ▼
 [6 打款失败] ──重新发起──▶ [2 待打款]
     
 任意状态 ──作废──▶ [8 已作废]（终态，需审批记录）
```

| 状态 | 名称 | 允许的下一步 | 谁操作 |
|---|---|---|---|
| 1 | 待审核 | 2 / 9 | 平台运营 / 租户管理员 |
| 2 | 待打款 | 3 / 6 / 8 | FIN |
| 3 | 已打款 | 4 / 5 | FIN |
| 4 | 已对账 | 终态 | FIN |
| 5 | 对账差异 | 4 / 8 | FIN + TECH |
| 6 | 打款失败 | 2 / 8 | FIN |
| 7 | 打款中（处理中） | 3 / 6 | FIN（长时间处理中标记） |
| 8 | 已作废 | 终态 | 需二次审批 |
| 9 | 已驳回 | 终态 | 审核人 |

### 7.4 运营 SOP（逐步）

| 步 | 动作 | 负责人 | 系统动作 | 产出物 | 验收标准 |
|---|---|---|---|---|---|
| S1 | 店员在小程序/H5 提交提现申请 | 店员 | 写 `clerk_withdraw` + 台账（status=1） | 申请单 `apply_no` | 前端可见申请状态 |
| S2 | 系统校验（余额、实名、风控、黑名单 `mall_blacklist`） | 系统 | 校验失败则退回并提示 | — | 校验规则生效 |
| S3 | 运营在后台「提现审核」页审核 | 平台运营/租户管理员 | status 1 → 2（或 9） | 审核记录（操作人、时间、意见） | 状态与审计日志可查 |
| S4 | FIN 按批次线下打款（对公转账优先） | FIN | 在台账页登记：打款方式、批次号、金额、时间 | 台账 status=3 | 状态变更留操作人 |
| S5 | 上传打款凭证（银行回单截图） | FIN | 存 COS 私有桶，`pay_voucher_url` 落库 | 凭证文件 | 凭证可下载查看、不可公开访问 |
| S6 | 通知店员到账（短信/站内信） | FIN / 系统 | status 3 时发通知 | 通知记录 | 店员端显示"已打款" |
| S7 | 月度/批次对账 | FIN | 导出对账表，逐笔勾稽 | 对账表（§7.6） | status → 4，无未对账遗留 |
| S8 | 差异处理 | FIN + TECH | 标记 status=5，查因修正 | 差异说明 | 差异清零 |

### 7.5 幂等与防重打

| 风险 | 防护措施 |
|---|---|
| 重复点击"提交申请" | 前端按钮防抖 + 后端按 `clerk_member_id + 状态` 校验（同一店员同一时间仅一张待审单） |
| 同一提现单生成多张台账 | `withdraw_id + deleted=0` 唯一索引 |
| 批量打款重复导入 | 导入文件按 `apply_no` 去重，重复行跳过并记录 |
| 两人同时点"已打款" | 状态更新带 `WHERE status = 2` 条件（乐观锁），只有一人成功 |
| 银行实际转了但系统没记 | 每日与银行流水核对（§7.6），差异挂起 |
| 系统记了但银行没转 | 对账时以**银行流水为准**，以"已对账"反查打款记录，未实际到账的置回 status=2 |
| 回调/重复通知导致的自动打款 | 本轮无自动打款，无此风险；未来接自动转账时以 `pay_transfer` 的 `out_biz_no` 做幂等键（芋道链路已具备） |

### 7.6 对账表与导出

| 对账维度 | 口径 | 差异判定 |
|---|---|---|
| 台账 vs `clerk_withdraw` | 台账金额 = 提现申请金额 | 不等 → 差异 |
| 台账 vs 银行流水 | 按批次号 + 金额 + 收款账号匹配 | 银行有单无台账 / 台账有单银行无 → 差异 |
| 台账 vs 余额扣减 | 提现申请时已扣减积分/余额，核对扣减记录唯一 | 重复扣减或漏扣 → 差异 |
| 商户号维度 | 各商户号费率不同，核对渠道流水与 `pay_order` 的手续费 | 费率不符 → 差异（财务关注） |

**对账表字段（Excel/CSV 导出）**

| 列 | 说明 |
|---|---|
| apply_no | 申请单号 |
| tenant_id / 租户名 | 租户 |
| clerk_member_id / 店员姓名 | 申请人 |
| payee_name / payee_account_masked | 收款人（脱敏） |
| amount | 金额 |
| status | 状态 |
| audit_user / audit_time | 审核信息 |
| pay_batch_no | 打款批次 |
| pay_time | 打款时间 |
| pay_operator | 打款人 |
| voucher_url | 凭证链接 |
| reconcile_status | 对账状态 |
| remark | 备注 |

**导出能力**

| 项 | 规格 |
|---|---|
| 入口 | 后台「红包打款台账」页，按租户/状态/时间区间筛选后导出 |
| 格式 | CSV / XLSX（UTF-8 BOM，防 Excel 中文乱码） |
| 权限 | 按租户隔离；跨租户导出需平台角色 |
| 审计 | 导出动作写操作日志（谁、何时、导出多少条） |

### 7.7 凭证留存

| 项 | 要求 |
|---|---|
| 存储位置 | 腾讯云 COS **私有桶**，与晒单图片分桶隔离 |
| 访问方式 | 后端生成**临时签名 URL**（短时效，如 10 分钟）供有权限者查看 |
| 命名 | `withdraw/{tenant_id}/{yyyyMM}/{apply_no}.{ext}` |
| 保留期 | 建议 **≥ 3 年**（财务凭证要求，⚪ 需业务方与财务确认当地/公司留存年限） |
| 脱敏 | 收款账号在列表页掩码，仅详情页（有权限）显示完整 |
| 审计 | 查看/下载凭证写日志 |

### 7.8 为后续接微信商家转账预留的扩展点

| 扩展点 | 现有基础 | 未来接入方式 |
|---|---|---|
| 转账单 | 芋道 `pay_transfer` 表 + `PayTransferService` | 自动转账时，写 `pay_transfer` 而非纯台账，台账作为**对账镜像**保留 |
| 状态机 | 本方案 status 机与 `pay_transfer` 状态语义接近 | 增加 `transfer_mode` 字段（1=人工 2=微信商家转账），状态机不变 |
| 幂等 | `apply_no` / `out_biz_no` | 自动转账用 `out_biz_no = apply_no` 保证幂等 |
| 同步任务 | 芋道已有 `PayTransferSyncJob` | 自动转账后用其拉取官方状态回写台账 |
| API 出口 | `PayTransferApi` | 改为调用微信商家转账 API，需配请求来源 IP 白名单 |
| 运营账户 | 无 | 未来需开通运营账户（次日 0:0 生效、不可关闭、所有运营业务强制从运营账户出资）——**这会改变资金归集方式，需提前与财务确认** |
| 金额上限 | 无 | 自动转账"红包样式"单笔 ≤ 200 元；超限仍走人工 |
| 场景证明 | 无 | 需业务方准备转账场景证明资料（用户申请提现的截图/场景说明） |

**结论**：本轮方案**不改动芋道 `pay_transfer` 链路**，仅在其上叠加一层"台账"，未来切换为自动转账时台账继续作为对账与审计凭证，改动面可控。

---

## 8. OCR 与短信接入方案

### 8.1 OCR 接口选型

| 候选接口 | 适用场景 | 与本项目的匹配度 | 结论 |
|---|---|---|---|
| **通用文字识别（GeneralBasicOCR）** | 通用图片文字 | ★★★☆☆ 能识别小票文字，但**不返回结构化字段**（金额/日期需自己解析，解析规则易错） | 🟡 备选 |
| **通用表格识别** | 表格 | ★★☆☆☆ 药店小票多为非规则文本，表格识别不适用 | ❌ |
| **票据识别（票夹/营业执照等）** | 标准化票据 | ★★☆☆☆ 小票非标准票据模板 | ❌ |
| **手写体/竖版文本识别** | 竖排、特殊排版 | ★★★☆☆ 部分小票是热敏纸小票，字体模糊 | 🟡 辅助 |
| **多来源通用识别（MultiSourceGeneralOCR / 通用印刷体 + 版面分析）** | 复杂版面 | ★★★★☆ | ✅ **首选** |
| **小票/卡证类专用识别（若有对应品类）** | 小票 | ★★★★★ 若腾讯云 OCR 品类中存在"小票/零售小票"专用接口，字段直出，**最强** | ✅ **优先核查是否有专用接口** |
| **AI 文档解析 / 智能结构化（DocParser 类）** | 结构化抽取 | ★★★★☆ 可指定 JSON Schema 输出字段 | 🟡 备选（成本较高） |

**结论与动作**

| 项 | 内容 |
|---|---|
| 首选 | 优先在腾讯云 OCR 控制台核查是否有**小票/零售凭证类专用识别接口**；若有则直出"金额/日期/门店"字段 |
| 兜底 | 无专用接口 → 用**通用文字识别 + 版面分析**，服务端解析出金额、日期、门店名 |
| 输出结构 | 统一归一化为 `amount`（decimal）、`purchase_time`（date）、`store_name`（string）、`raw_text`（string）、`confidence`（decimal 0–100） |
| 置信度 | 复用芋道已有的风控口径：**置信度 < 70 扣分**（`RiskControlServiceImpl` 已实现该规则），只补"把识别置信度写回 `clerk_task_submission`"这一步 |
| 数据落库 | 写 `clerk_task_submission.ocr_status`（0=待处理 1=成功 2=失败）与 `ocr_text`（原始识别文本 + 结构化 JSON） |
| 成本 | 按次计费，量级参考 0.1–1.2 元/次（按接口不同，⚠️ 以腾讯云实际定价为准） |

**晒单小票识别的实现流程**

| 步 | 动作 | 降级点 |
|---|---|---|
| 1 | 小程序端 `uploadFile` 上传小票图 → COS 私有桶 | 上传失败 → 提示重试，不影响其他功能 |
| 2 | 后端提交 OCR 任务（同步或异步） | — |
| 3 | 解析结果 → 写 `ocr_status`/`ocr_text` | 解析失败 → `ocr_status=2` |
| 4 | **置信度 ≥ 70** → 自动预填晒单字段（金额/时间/门店），店员确认后提交 | 置信度 < 70 → **强制进入人工审核队列** |
| 5 | 店员提交 → 触发内容安全校验（见 §3.2 A8）→ 等待运营审核 | 内容安全不通过 → 拒绝并提示 |
| 6 | 运营审核通过 → 结算积分/返现 | — |

### 8.2 降级策略（OCR 不可用时）

| 故障场景 | 降级动作 | 对用户的影响 | 恢复动作 |
|---|---|---|---|
| OCR 服务超时 | 后端超时设 **8 秒**，超时即置 `ocr_status=2` | 店员需手工填写小票金额/日期 | 无需恢复动作，后台可重试识别 |
| OCR 服务报错/限流 | 同上 + 告警 | 同上 | 运维看监控告警 |
| OCR 返回置信度 < 70 | **强制人工审核** | 审核时长变长（目标 ≤ 24h） | — |
| OCR 接口整体下线 | 开关关闭（配置项 `ocr.enabled=false`），全量转人工 | 同上 | 重新开启后新单走 OCR，历史单人工处理 |
| COS 上传失败 | 提示重试；允许"仅提交文字说明+金额"的弱证据通道（🟡 是否允许需业务方确认风控策略） | 需填更多字段 | — |

> **结论**：OCR 属于**增强项**而非阻塞项。数据模型与风控规则已就位，即使不接 OCR 也能上线（人工审核晒单），因此本项排在 MVP 之外。

### 8.3 腾讯云短信接入

**签名与模板清单**

| 用途 | 签名 | 模板类型 | 模板内容（示例） | 现状 |
|---|---|---|---|---|
| 注册验证码 | 企业简称（需佐证） | 验证码 | 您的验证码是 {1}，{2} 分钟内有效。 | ❌ 未启用 |
| 找回密码验证码 | 同上 | 验证码 | 您正在重置密码，验证码 {1}，{2} 分钟内有效。 | ❌ 未启用 |
| 提现审核通过 | 同上 | 通知 | 您在{1}的提现申请已审核通过，金额 {2} 元，预计 1–2 个工作日到账。 | ❌ 未建 |
| 打款完成 | 同上 | 通知 | 您的红包 {1} 元已于 {2} 打款，请注意查收。 | ❌ 未建 |
| 积分/返现到账 | 同上 | 通知 | 您的订单返现 {1} 元已到账，当前余额 {2} 元。 | ❌ 未建 |
| 账号异常登录提醒 | 同上 | 通知 | 您的账号于 {1} 在 {2} 登录，如非本人操作请立即修改密码。 | 🟡 建议 |
| 推广/营销 | — | 推广 | 需单独资质与审核 | 🟡 可选 |

**技术接入要点**

| 项 | 说明 |
|---|---|
| SDK | 腾讯云 SMS SDK（v20210111 或更新），密钥从环境变量注入 |
| 密钥管理 | `SecretId`/`SecretKey` 存 KMS 或 `/etc/ydb-hb/secrets/sms.env`（0600），**不入 git**；使用 CAM 子账号最小权限（仅短信发送） |
| 芋道配置 | `system_sms_channel` 中把腾讯云渠道 `status` 置 1，填入真实 `SignName`、`TemplateCode`、`AccessKeyId`/`SecretKey`（或改用 SDK 密钥） |
| 现状修正 | 现有 3 行（阿里云/腾讯云/钉钉 mock）**全部 status=0**，注册与找回密码实际不可用；需至少启用腾讯云一行，其余两行删除或保持禁用 |
| 频率限制 | 注册/找回密码类模板做**图形验证码前置 + 同手机号 60s + 同手机号 5 次/天 + 同 IP 100 次/天** 限流，防刷费用与被攻击 |
| 发送记录 | 芋道 `system_sms_log`（若已存在）落发送记录；失败要落错误码 |
| 降级 | 短信发送失败**不阻断主流程**（注册/改密需专门策略，见下） |

**短信不可用时的主流程降级**

| 场景 | 短信正常 | 短信不可用降级 |
|---|---|---|
| 注册 | 短信验证码注册 | 🟡 管理员后台建号 + 首次登录强制改密（**MVP 推荐**）；或接入微信手机号快速验证组件（需额外开通） |
| 找回密码 | 短信验证码 | 🟡 走人工申诉（运营核验身份后重置）；或管理员重置 |
| 提现/返现通知 | 短信通知 | 站内信 + 小程序订阅消息（🟡 可选，订阅消息需用户授权） |

### 8.4 密钥安全管理（OCR / 短信）

| 项 | 措施 |
|---|---|
| 存储 | 腾讯云 KMS 加密后落 `/etc/ydb-hb/secrets/`（0600）；或 KMS 加密值落库 + 运行时解密 |
| 最小权限 | OCR 用只读调用子账号；短信用仅发送权限子账号 |
| 轮换 | 腾讯云控制台可自助生成新密钥，双密钥并存灰度切换（密钥可同时存在 2 个） |
| 日志 | 日志中不打印 SecretKey、SecretId（可打印前 6 位掩码） |
| 仓库 | `.gitignore` 排除 `*.env`/`secrets/`；CI 接密钥扫描，命中即 fail |
| 审计 | 谁在何时更新了哪个密钥写操作日志 |

### 8.5 验收标准

| # | 项 | 方法 | 通过标准 |
|---|---|---|---|
| 1 | OCR 可用 | 上传 10 张真实药店小票 | ≥8 张正确提取金额与日期；置信度字段写入 `clerk_task_submission` |
| 2 | 低置信度降级 | 构造模糊小票 | 置信度 <70 → 进入人工审核队列，不自动通过 |
| 3 | 短信可用 | 用测试手机号触发验证码 | 60 秒内收到；`system_sms_channel` 对应记录 status=1 |
| 4 | 频率限制 | 同号连续请求 | 60s 内第二次被拒；5 次/天封顶生效 |
| 5 | 密钥不入库明文 | 检查 `system_sms_channel` 表内容 | 不含真实 SecretKey（或已加密） |
| 6 | 降级可用 | 关闭短信开关 | 主流程不报错；注册走管理员建号路径 |

---

## 9. 生产数据迁移与演示数据清理

### 9.1 数据现状（已核实）

| 数据 | 现状数量 | 性质 |
|---|---|---|
| 租户 | **4 个**（含 iocoder 官方测试租户 **121 / 122**） | iocoder 121/122 为**上游框架测试租户，必须删除** |
| 系统用户 | **22 个** | 含演示账号 admin/`«后台»`（平台端）、demo123/`«演示»`（示范大药房租户） |
| clerk_member | **105 个** | 店员演示数据 |
| product_spu | **24 个** | 商品演示数据 |
| mall_blacklist | **25 条** | 黑名单演示数据 |
| clerk_withdraw | **18 条** | 提现演示数据 |
| pay_app / pay_order / pay_transfer | **各 0 行** | 干净，无需清理；但也说明**从未真实支付** |

### 9.2 清理清单（必须逐条执行并留证）

| # | 对象 | 数量 | 动作 | SQL/操作要点 | 负责人 | 验收标准 |
|---|---|---|---|---|---|---|
| C1 | iocoder 测试租户 | 121、122 | **删除** | 芋道多租户删除需连带清理租户下所有业务表；**必须先全量导出备份** | TECH | 两租户及其所有关联数据不可查 |
| C2 | 演示系统用户 | 22（含 admin/`«后台»`、demo123/`«演示»`） | 删除或改强口令 | 平台 admin 建议保留 1 个但改强口令 + 首登强制改密 | TECH | 无默认口令账号可登录 |
| C3 | clerk_member | 105 | 清空或仅保留业务方指定样本 | `DELETE` 后重置自增；或改标记为 `deleted=1` | TECH | 表内仅剩业务方确认保留的样本 |
| C4 | product_spu | 24 | 清空 | 连带清理 spu 相关 sku/sku 属性/库存 | TECH | 商品列表为空 |
| C5 | mall_blacklist | 25 | 清空 | — | TECH | 黑名单为空 |
| C6 | clerk_withdraw | 18 | 清空 | 连带清理积分流水/余额 | TECH | 无历史提现残留，避免财务误以为有未结台账 |
| C7 | 芋道框架自带演示数据 | 未统计 | **逐表排查**（`system_dict_data`、`system_notice`、`system_sms_template`、`mall_*` 系列、`promotion_*`、`pay_*`） | 芋道初始化 SQL 会插入大量示例字典/配置 | TECH | 抽查 20 张核心表无明显 demo 数据 |
| C8 | `system_sms_channel` 里的阿里云/钉钉 mock 行 | 2 | 删除或禁用 | 只保留腾讯云一行并置 status=1（§8.3） | TECH | 渠道表仅 1 行启用 |
| C9 | 租户级 `infra_file_config` | 各租户 | 核对是否指向 COS | 非 COS 的改为 COS（参照 master 的 id=22 配置） | TECH | 所有租户文件存储均为 COS |
| C10 | 演示文件（COS 中） | 未统计 | 清理测试上传的图片 | COS 桶内按前缀删除 | TECH | 桶内无 demo 素材 |
| C11 | 应用配置里的演示开关 | — | 检查 `system_config` 中 iocoder demo 开关 | 芋道默认带 "芋道源码" 等演示开关 | TECH | 演示性配置关闭 |
| C12 | Gitea 仓库中的演示分支 | — | 归档 | 主仓 `http://100.127.133.77:3010/yaodianbang/h04-op`（private），备份仓 h04 `:3001` | TECH | 分支清晰，主干可发布 |

> ⚠️ **执行顺序铁律**：**先备份 → 再清理 → 再验证**。任何 DELETE 之前必须有一份可恢复的全量备份（见 §9.5 M1 与 §9.7 RP-1）。

### 9.3 保留清单（不得误删）

| # | 对象 | 保留原因 |
|---|---|---|
| K1 | 芋道框架表结构与基础字典（真实使用的字典类型） | 平台运行依赖；只删数据不删表结构 |
| K2 | `system_tenant` 中业务方确认的真实客户租户 | — |
| K3 | `infra_file_config` id=22（COS 配置） | 生产直接复用 |
| K4 | 芋道多租户框架表（tenant 隔离相关） | 核心框架 |
| K5 | `pay_app`/`pay_channel` 结构 | 新建商户号要用 |
| K6 | `pay_transfer` 结构 | 未来接自动转账要用 |
| K7 | 角色/权限（`system_role`/`system_menu`/`system_permission`） | 需按"要店帮"业务重塑菜单权限 |
| K8 | 字典类型（`system_dict_type`） | 业务依赖 |
| K9 | 定时任务配置（`infra_job`） | 同步任务要保留（如 `PayTransferSyncJob`） |

### 9.4 客户租户初始化

| # | 步骤 | 动作 | 负责人 | 产出物 | 验收标准 |
|---|---|---|---|---|---|
| T1 | 创建租户 | 在 `system_tenant` 建客户租户，指定套餐/状态/到期时间 | TECH | tenant 记录 | 租户可在后台列表看到 |
| T2 | 分配管理员 | 创建租户管理员账号并绑定角色 | TECH + BIZ | 账号 + 角色绑定 | 该管理员登录后只见本租户数据 |
| T3 | 复制菜单权限 | 按芋道多租户机制把菜单权限分配给租户角色 | TECH | 权限配置 | 租户管理员看不到平台端菜单 |
| T4 | 初始化业务基础数据 | 商品分类、门店、店员、积分规则、返现规则、晒单审核流 | TECH + BIZ | 基础数据 | 店员端与后台可见 |
| T5 | 配置 file_config | 复制 master 的 COS 配置 | TECH | `infra_file_config` 行 | 上传文件落到 COS |
| T6 | 配置短信渠道 | 复制/新建腾讯云短信渠道配置 | TECH | `system_sms_channel` 行 | 可发短信 |
| T7 | 配置支付渠道 | 按 §6.4 建 `pay_app` + `pay_channel` | TECH | 支付配置 | 1 元支付跑通 |
| T8 | 微信授权/小程序登录绑定 | 配小程序 appid 到芋道 `system_wx_*`/社交配置 | TECH | 配置项 | 小程序可登录 |
| T9 | 黑名单与风控基线 | 初始化空黑名单 + 合理的风控阈值 | TECH | 配置 | `RiskControlServiceImpl` 规则生效 |
| T10 | 验收 | 按租户走一遍店员全流程 | BIZ + TECH | 验收单 | 店员注册→下单/晒单→审核→积分→提现申请全通 |

### 9.5 迁移步骤（从 h02 测试环境 → 腾讯云生产）

| 步 | 动作 | 命令/方法 | 负责人 | 验收 |
|---|---|---|---|---|
| M1 | 快照 h02 现状 | `docker compose ps` + 库表全量导出 `mysqldump --single-transaction --routines --triggers ydb_hb_mall` | TECH | 导出文件 > 0 字节且可 `mysql <` 回放 |
| M2 | 迁移文件与前端产物 | `rsync` 传输 `deploy/` 目录与 `frontend/{admin,mall-h5,clerk,pharma}` | TECH | 文件数与大小一致（`du -sh` 比对） |
| M3 | 腾讯云环境初始化 | 装 Docker + Compose（对齐 h02 的 Docker 29.7.2 / Compose v5.5.0），拉取镜像 | OPS | `docker compose ps` 全 Up |
| M4 | 恢复数据库 | 导入 M1 的 dump | TECH | 表数量与行数一致 |
| M5 | 恢复文件存储 | COS 桶已有数据（master 已在 COS）→ 核对 `infra_file_config` | TECH | 图片可访问 |
| M6 | 前端发布 | 用 `find ./dist -mindepth 1 -delete` 替换内容，**禁止 `rm -rf dist`** | TECH | 页面正常加载，无 500 循环 |
| M7 | 配置注入 | 写 `/etc/ydb-hb/secrets/*.env`（0600） | TECH | 权限正确 |
| M8 | 强口令与配置加固 | 见 §10 | TECH | §10 全部通过 |
| M9 | 清理演示数据 | 执行 §9.2 清单 | TECH | 逐条打勾 |
| M10 | 双跑校验 | 见 §9.6 | TECH + BIZ | 校验通过 |
| M11 | 切流 | DNS 解析切到生产 IP | OPS | 全站指向生产 |
| M12 | 观察期 | 3 天重点监控（§11 第四段） | OPS | 无 P1 故障 |

### 9.6 双跑校验

| 校验项 | 方法 | 通过标准 |
|---|---|---|
| 表结构一致 | `mysqldump --no-data` 对比 h02 与生产 | 无差异（或差异已在迁移脚本中声明） |
| 关键表行数一致 | 对 `system_tenant`/`system_user`/`clerk_member`/`product_spu` 等计数 | 差值 = 预期清理条数 |
| 租户隔离生效 | 用租户 A 账号查租户 B 数据 | 查不到 |
| 核心接口连通 | 商城 H5、后台、店员端、医药前台各跑一遍关键接口 | 全部 200 |
| 支付链路 | 1 元真实支付 | `pay_order` 落库、回调成功 |
| 文件访问 | 上传 + 下载一张图片 | 落 COS，签名 URL 可访问 |
| 提现台账 | 提交一条测试提现并走完状态机 | 状态流转正确、凭证可上传 |
| 消息通知 | 触发一条短信 | 收到（若短信已启用） |

### 9.7 回滚点

| 回滚点 | 触发条件 | 回滚动作 | 恢复时长目标 |
|---|---|---|---|
| **RP-1：迁移前全量备份** | 迁移后校验不通过 | 从 dump 全量回灌，恢复原状态 | ≤ 30 分钟 |
| **RP-2：切流前生产快照** | 切流后出现 P1 故障 | DNS 回切到旧地址（若旧环境仍在）或从快照恢复 | ≤ 15 分钟（DNS 生效受 TTL 影响，TTL 建议 ≤ 300s） |
| **RP-3：代码版本回滚** | 新版本引发功能异常 | 部署上一版本镜像（tag 用 git sha，可精确回退） | ≤ 10 分钟 |
| **RP-4：数据库迁移脚本回滚** | 迁移脚本有误 | 每个迁移脚本配对应 down 脚本 | ≤ 20 分钟 |
| **RP-5：支付灰度回退** | 支付异常 | `pay_channel.status` 置 0 | ≤ 2 分钟 |
| **RP-6：打款停摆** | 对账差异未查清 | 台账状态冻结在"待对账"，停止打款 | 立即 |

> **保留期**：RP-1 的 dump 与 RP-2 的快照至少保留 **30 天**，覆盖上线后首个完整对账周期。

---

## 10. 安全加固清单

> 目标：从"内网测试环境"提升到"公网可访问的生产环境"。每项给出**现状问题 → 加固动作 → 验证方法**。

### 10.1 加固总表

| # | 项 | 现状问题 | 加固动作 | 验证方法 | 责任人 | 优先级 |
|---|---|---|---|---|---|---|
| S01 | MySQL root 口令 | **弱口令 `123456`** | 改 ≥16 位随机强口令；建独立应用账号 `ydb_app`（最小权限，仅业务库）；root 仅本地/运维使用 | `mysql -uroot -p` 用旧口令失败、新口令成功 | TECH | 🔴 P0 |
| S02 | MySQL 端口暴露 | `127.0.0.1:3306`（现状安全） | 生产若用云数据库则**彻底关闭 CVM 上的 3306 对外**；自建则仅安全组白名单 | 从公网 `nc -zv <ip> 3306` 不通 | OPS | 🔴 P0 |
| S03 | Redis 密码 | 现状未知，**按未设密码处理** | `requirepass <强口令>` + `bind 127.0.0.1`；如上云则用云 Redis 密码 + 白名单 | `redis-cli -a <pwd> ping` 成功、无密码失败 | TECH | 🔴 P0 |
| S04 | Redis 端口暴露 | `127.0.0.1:6379`（现状安全） | 保持仅内网；**禁止 `protected-mode no` 等冗余放开项** | 公网扫描 6379 关闭 | OPS | 🔴 P0 |
| S05 | 后端 48080 | 已绑 `127.0.0.1` ✅ | **保持**，并加启动自检脚本断言端口绑定 | `ss -lntp \| grep 48080` 显示 `127.0.0.1:48080` | TECH | 🔴 P0 |
| S06 | compose 中的 permit-all 冗余项 | 需排查 | 移除任何 `privileged: true`、`network_mode: host`、`cap_add: ALL`、`security_opt: --privileged` 等；删除 `ports: "0.0.0.0:xxxx:xxxx"` 形式的宽松映射 | `docker compose config` 人工审阅 + 安全组比对 | TECH | 🔴 P0 |
| S07 | 容器内 root 运行 | 需排查 | 非必需容器加 `user: <uid>:<gid>`；MySQL/Redis 保持官方镜像的内部用户 | `docker exec <c> id` 返回非 root（server/web） | TECH | 🟡 P1 |
| S08 | 镜像来源 | **内网 HTTP Registry `100.66.41.15:5000` + insecure-registries** | 迁 TCR；删除 `insecure-registries`；镜像加 digest 校验 | `docker info \| grep -i insecure` 无输出 | TECH | 🔴 P0 |
| S09 | 镜像扫描 | 未启用 | TCR 开启镜像漏洞扫描；基线镜像定期更新 | TCR 控制台有扫描报告 | OPS | 🟡 P1 |
| S10 | 支付密钥 | `pay_channel.config` 是 DB TEXT 字段，直接填会明文落库 | 占位符 + 运行时注入；目标态 KMS | `SELECT config FROM pay_channel` 不含真实私钥/APIv3 密钥 | TECH | 🔴 P0 |
| S11 | 短信/OCR 密钥 | `system_sms_channel` 同理 | 占位符 + 运行时注入 / KMS | `SELECT * FROM system_sms_channel` 无明文 SecretKey | TECH | 🔴 P0 |
| S12 | Git 密钥 | 未知 | `.gitignore` 排除 `.env`/`*.pem`/`secrets/`；CI 密钥扫描（gitleaks）命中即 fail | `gitleaks detect` 无告警 | TECH | 🔴 P0 |
| S13 | 默认口令账号 | admin/`«后台»`、demo123/`«演示»` | 删除或改强口令 + 首登强制改密 + 登录失败锁定 | 用旧口令登录失败 | TECH | 🔴 P0 |
| S14 | 生产演示数据 | 4 租户/22 用户/105 店员/24 商品/25 黑名单/18 提现 | 见 §9.2 | 抽查为空 | TECH | 🔴 P0 |
| S15 | HTTPS | **全站纯 HTTP** | 全站 443 + 80 强跳转 + HSTS | `curl -I` | TECH | 🔴 P0 |
| S16 | 安全组最小化 | 未知 | 按 §4.3 端口表，只开 80/443/22（白名单） | 安全组规则逐条核对 | OPS | 🔴 P0 |
| S17 | SSH | 需排查 | 禁用密码登录（仅密钥）、改非 22 端口、禁用 root 直接登录 | `sshd_config` 校验 | OPS | 🔴 P0 |
| S18 | 运维通道 | 仅 Tailscale | 生产保留 Tailscale，**不额外开放 22 给全网** | 公网 SSH 扫描关闭 | OPS | 🔴 P0 |
| S19 | 数据库备份 | 未知 | 每日全量 dump → 加密 → COS；保留 30 天；**每周做一次恢复演练** | 从 COS 拉备份恢复到临时库成功 | OPS | 🔴 P0 |
| S20 | CVM 快照 | 未知 | 每日自动快照，保留 14 天 | 控制台快照列表 | OPS | 🔴 P0 |
| S21 | 日志与审计 | 未知 | 采集 nginx access/error、server 应用日志、支付回调日志到 CLS；保留 ≥ 30 天 | 日志检索可查 | OPS | 🔴 P0 |
| S22 | 敏感日志脱敏 | 未知 | 密码/密钥/token/手机号（中间位）掩码 | grep 日志无明文密钥 | TECH | 🔴 P0 |
| S23 | COS 桶权限 | master 已用 COS | 桶**私有读**；开启版本控制与生命周期；开启流量防盗刷（Referer/签名） | 用无签名 URL 访问返回 403 | TECH | 🔴 P0 |
| S24 | 小票图片敏感性 | 含顾客个人信息 | 桶隔离 + 后端签名 URL + 访问日志 | 越权访问他人小票返回 403 | TECH | 🔴 P0 |
| S25 | 隐私指引 | 未配置 | 配《小程序用户隐私保护指引》+ `wx.getPrivacySetting` 弹窗，未默认勾选 | 首次启动弹窗出现 | TECH | 🔴 P0 |
| S26 | 内容安全 | 未接入 | 服务端 `msgSecCheck`/`imgSecCheck` + 前端预检 | 违规内容被拦 | TECH | 🔴 P0 |
| S27 | 动态路由刷新坑 | 芋道动态路由登录后注入内存，整页刷新白屏 | 生产引导用菜单切换；后台加提示 | 用户按提示操作无白屏 | TECH | 🟡 P1 |
| S28 | 构建进程资源 | 构建期 load 飙 38 会整机重启 | 构建移出生产机 + `nice -n 15` | 生产机无构建进程 | TECH | 🔴 P0 |
| S29 | 前端发布方式 | `rm -rf dist` 会删挂载点 inode → nginx 500 循环 | 固化 `find -mindepth 1 -delete` 到发布脚本 | 发布后页面正常 | TECH | 🔴 P0 |
| S30 | 依赖版本 | `@swc/core` 升 1.16.2 与 `vite-plugin-top-level-await@1.6.0` 冲突报 `missing field 'type'`；删插件则白屏 | 锁 `@swc/core@1.15.47` + 保留插件；CI 加依赖校验 | `pnpm-lock` 中版本固定；CI 构建通过 | TECH | 🔴 P0 |
| S31 | 管理后台暴露面 | 后台与店员端同域 8082 | 生产拆子域；后台加 IP 白名单或二次验证（🟡 可选） | 未授权 IP 访问后台被拒 | TECH | 🟡 P1 |
| S32 | 依赖漏洞 | 未知 | 定期 `npm audit` / 依赖扫描；芋道版本落后需评估升级 | 无高危 CVE | TECH | 🟡 P1 |
| S33 | 限流与防刷 | 未知 | 接口层面对登录/注册/短信/下单限流；`mall_blacklist` 黑名单生效 | 压测触发限流 | TECH | 🔴 P0 |
| S34 | 提现金额风控 | 未知 | 单笔上限、单日累计上限、新账号冷静期 | 超限申请被拒 | TECH + BIZ | 🔴 P0 |

### 10.2 加固执行顺序

| 阶段 | 时点 | 内容 |
|---|---|---|
| 第一次 | 环境初始化时（迁移前） | S01–S14、S16–S20、S23、S28–S30 |
| 第二次 | 切流前 24 小时 | S15、S24–S27、S31–S34 + 全量回归 |
| 持续 | 上线后 | S21/S22 日志巡检、S09 镜像扫描、S32 依赖扫描、S19 每周恢复演练 |

---

## 11. 上线检查表（Checklist）

> 每项都可勾选，且**带验证方法**。§11.1–11.4 全部通过才可宣布上线。

### 11.1 部署前（环境准备）

| # | 检查项 | 验证方法 | 通过标准 | 负责人 | ☑ |
|---|---|---|---|---|---|
| D01 | CVM 已购买并安全组按 §4.3 配好 | 控制台安全组规则逐条比对 | 只开 80/443/22(白名单) | OPS | ☐ |
| D02 | 域名已注册并完成企业实名 | `whois` 查主体 | 与营业执照全称一致 | BIZ | ☐ |
| D03 | ICP 备案已通过 | 腾讯云控制台备案状态 | 状态"备案成功"且有备案号 | BIZ | ☐ |
| D04 | DNS A 记录已指向生产 CVM | `dig +short <domain>` | 返回生产公网 IP | OPS | ☐ |
| D05 | 微信支付商户号已申请通过 | 商户平台状态 | 状态正常，可发起 API 测试 | BIZ | ☐ |
| D06 | APIv3 密钥 + 商户证书（或微信支付公钥）已取得 | 商户平台 API 安全页 | 三要素齐备 | BIZ | ☐ |
| D07 | 小程序已注册并认证 | 公众平台 | 状态"已认证" | BIZ | ☐ |
| D08 | 小程序服务类目审核通过 | 公众平台 | 类目状态"已通过" | BIZ | ☐ |
| D09 | 腾讯云 COS 桶可用 | 控制台 | 桶存在、权限私有读 | OPS | ☐ |
| D10 | TCR 实例与命名空间就绪 | `docker pull` 测试 | 镜像可拉取（HTTPS） | OPS | ☐ |
| D11 | 备份目录与保留策略已配置 | 检查计划任务 | 每日全量 + 保留 30 天 | OPS | ☐ |
| D12 | 生产强口令已生成并存入密钥位置 | `stat -c %a /etc/ydb-hb/secrets/*` | 全部 600，属主 root | TECH | ☐ |
| D13 | 旧口令（MySQL `123456` 等）已全部作废 | 用旧口令尝试登录 | 全部失败 | TECH | ☐ |

### 11.2 部署中（技术实施）

| # | 检查项 | 验证方法 | 通过标准 | 负责人 | ☑ |
|---|---|---|---|---|---|
| I01 | 镜像源已切换到 TCR，`insecure-registries` 已删除 | `docker info` | 无 insecure 记录 | TECH | ☐ |
| I02 | compose 引用 TCR 镜像 tag（含 git sha） | `docker compose config` | tag 唯一且可追溯 | TECH | ☐ |
| I03 | 后端 48080 仅绑 127.0.0.1 | `ss -lntp \| grep 48080` | 显示 `127.0.0.1:48080` | TECH | ☐ |
| I04 | compose 无 permit-all 冗余项 | 人工审阅 `docker compose config` | 无 privileged/host 网络/多余端口 | TECH | ☐ |
| I05 | Redis 已设 `requirepass` 且 bind 内网 | `redis-cli ping` 无密码失败 | 有密码 PONG | TECH | ☐ |
| I06 | 数据库已从备份恢复且可连 | 应用启动日志 | 连接成功 | TECH | ☐ |
| I07 | 前端发布使用 `find -mindepth 1 -delete` | 检查发布脚本 | 脚本无 `rm -rf dist` | TECH | ☐ |
| I08 | 前端四端页面可打开（admin/mall-h5/clerk/pharma） | 逐个访问 | 无 500，无 try_files 循环 | TECH | ☐ |
| I09 | `@swc/core` 锁定 1.15.47 | 检查 lock 文件 | 版本一致，CI 构建通过 | TECH | ☐ |
| I10 | nginx 已配 443 + 80 跳转 + 安全响应头 | `curl -I` | 全部符合 §5.3 | TECH | ☐ |
| I11 | certbot 自动续期已配置并 dry-run 通过 | `certbot renew --dry-run` | 成功 | TECH | ☐ |
| I12 | TLS 仅 1.2/1.3 | `testssl.sh` | 通过 | TECH | ☐ |
| I13 | `client_max_body_size` 已调（≥20m） | 上传 5MB 图片 | 成功 | TECH | ☐ |
| I14 | 真实 IP 头已透传 | 查日志 | `X-Forwarded-For` 为真实客户端 IP | TECH | ☐ |
| I15 | 密钥未明文入库 | `SELECT config FROM pay_channel` | 只有占位符 | TECH | ☐ |
| I16 | 密钥未进 git | `gitleaks detect` | 无告警 | TECH | ☐ |
| I17 | 演示数据已清理 | 按 §9.2 逐条核对 | 全部为空/已删除 | TECH | ☐ |
| I18 | 演示账号已删除或改强口令 | 尝试登录旧口令 | 失败 | TECH | ☐ |
| I19 | 客户租户已初始化 | 后台租户列表 | 每个租户可登录 | TECH | ☐ |
| I20 | `pay_app`/`pay_channel` 已建（每个租户） | 查表 | 行数 = 租户数 | TECH | ☐ |
| I21 | 支付回调按租户路由已验证 | 两租户交错回调 | 各自订单正确更新 | TECH | ☐ |
| I22 | 1 元真实支付全链路通 | 小程序下单 | `pay_order` 落库、订单已支付 | TECH | ☐ |
| I23 | 回调幂等验证 | 重放同一回调 | 订单状态不二次变更 | TECH | ☐ |
| I24 | 提现台账功能可用 | 提交并流转一次 | 状态机正确、凭证可上传 | TECH | ☐ |
| I25 | `mockTransfer()` 已停用 | 代码检查 + 行为验证 | 提现不会自动变"已到账" | TECH | ☐ |
| I26 | 内容安全已接入并生效 | 提交违规词 | 被拦截并落记录 | TECH | ☐ |
| I27 | 隐私指引已配置 + 弹窗接入 | 首次启动小程序 | 弹窗出现且未默认勾选 | TECH | ☐ |
| I28 | 微信合法域名清单已配全 | 开发者工具真机调试 | request/uploadFile/downloadFile/socket 均无报错 | TECH | ☐ |
| I29 | `manifest.json` appid 已替换为自有 appid | 检查文件 | 非 `wxae7a0c156da9383b` | TECH | ☐ |
| I30 | mp-weixin 构建脚本已补齐并出包 | `pnpm build:mp-weixin` | 产出 `dist/build/mp-weixin` | TECH | ☐ |
| I31 | 监控告警已配置 | 云监控 | CPU/内存/磁盘/连接数/服务存活告警已开 | OPS | ☐ |
| I32 | 日志采集与保留策略已配 | CLS | nginx + 应用日志可查，保留 ≥30 天 | OPS | ☐ |
| I33 | 备份恢复演练完成一次 | 从 COS 恢复 dump 到临时库 | 恢复成功 | OPS | ☐ |

### 11.3 上线前（业务验收）

| # | 检查项 | 验证方法 | 通过标准 | 负责人 | ☑ |
|---|---|---|---|---|---|
| U01 | 店员全流程走通 | 真实店员账号跑一遍 | 注册→登录→商城下单/晒单→审核→积分到账→提现申请 全通 | BIZ + TECH | ☐ |
| U02 | 晒单审核流可用 | 提交 3 张小票 | 进入审核队列，审核后积分正确 | BIZ | ☐ |
| U03 | 提现审批流可用 | 提交 1 笔提现 | 审核 → 待打款 状态正确 | BIZ + FIN | ☐ |
| U04 | 人工打款 SOP 已培训 | FIN 演练一次（不真打款） | 知道在哪登记、凭证怎么传、怎么对账 | FIN | ☐ |
| U05 | 打款额度与到账时效承诺已确定 | 业务方确认 | 书面写明（⚪ 需业务方确认 SLA） | BIZ | ☐ |
| U06 | 对账表可导出 | 后台导出 | CSV/XLSX 正常，中文不乱码 | TECH + FIN | ☐ |
| U07 | 费率已确认 | 商户平台 | 各商户号实际费率登记入台账 | FIN | ☐ |
| U08 | 隐私政策对外可见 | 页面/小程序 | 用户可查阅《用户隐私保护指引》 | TECH | ☐ |
| U09 | 客服与投诉通道就绪 | — | 有明确受理人与时限（⚪ 需业务方确认） | BIZ | ☐ |
| U10 | 灰度范围与放量计划已定 | — | 书面确认（⚪ 需业务方确认） | BIZ | ☐ |

### 11.4 上线后（观察期）

| # | 检查项 | 验证方法 | 通过标准 | 频率 | 负责人 | ☑ |
|---|---|---|---|---|---|---|
| O01 | 服务健康 | 云监控 | 无 P0 告警 | 持续 | OPS | ☐ |
| O02 | 支付成功率 | 支付日志统计 | ≥ 98% | 每日 | TECH | ☐ |
| O03 | 回调验签失败数 | 日志 grep | = 0 | 每日 | TECH | ☐ |
| O04 | 订单/支付对账 | 渠道流水 vs `pay_order` | 差额 = 0 | 每日 | FIN | ☐ |
| O05 | 提现台账对账 | 台账 vs 银行流水 | 无未对账遗留 | 每周 | FIN | ☐ |
| O06 | 备份可用性 | 检查最近备份 + 恢复演练 | 备份存在且可恢复 | 每周 | OPS | ☐ |
| O07 | 证书有效期 | `certbot certificates` | > 30 天 | 每周 | TECH | ☐ |
| O08 | 商户证书有效期 | 商户平台 | > 30 天（年度更新提前排期） | 每月 | TECH | ☐ |
| O09 | 密钥轮换检查 | 密钥台账 | 无超期未轮换 | 每月 | TECH | ☐ |
| O10 | 演示数据复现检查 | 抽查 | 无新增脏数据 | 每月 | TECH | ☐ |
| O11 | 错误率与慢接口 | 日志统计 | 无突增 | 每周 | TECH | ☐ |
| O12 | 容量水位 | CPU/内存/磁盘/连接数 | < 70% | 每周 | OPS | ☐ |
| O13 | 演练回滚 | 演练回滚一次 | ≤ 30 分钟恢复 | 上线后 30 天内一次 | OPS | ☐ |

---

## 12. 时间倒排表

> 以 **W1 第一天** 为项目启动日（T0）。周次为相对周，实际日历日 ⚪ 需业务方结合节假日确认（2026 年 11 月中下旬启动会跨元旦与春节前备货期，**春节前是药店零售旺季但也是备案/审核高峰**）。

### 12.1 周计划总表

| 周 | 主题 | 关键任务 | 交付物 | 阻塞 |
|---|---|---|---|---|
| **W1** | 启动与资质发起 | ① BIZ 启动域名注册 + 企业实名<br/>② BIZ 提交腾讯云 ICP 备案（**最长前置项启动**）<br/>③ OPS 购买 CVM/COS/TCR/快照，开通云监控<br/>④ TECH 生成全部生产强口令并落位<br/>⑤ TECH 备份 h02 全量并做一次恢复演练 | 备案提交回执、CVM 交付、备份可用性证明 | 🔴 备案驳回会顺延全部后续 |
| **W2** | 备案等待期（并行建设） | ① BIZ 注册小程序并提交认证（300元/年）<br/>② BIZ 申请微信支付商户号 + 对公账户<br/>③ TECH 开发生产 compose（切 TCR）+ 加固脚本<br/>④ TECH 实现人工打款台账（表 + 状态机 + 审批页 + 导出）<br/>⑤ TECH 停用 `mockTransfer()`，改走台账 | 台账功能可用（测试环境） | |
| **W3** | 备案等待期（并行建设） | ① BIZ 小程序服务类目与资质提交<br/>② TECH 停用 `mockTransfer()` 改造完成 + 自测<br/>③ TECH 接入内容安全（msgSecCheck/imgSecCheck）<br/>④ TECH 接入隐私指引弹窗 + 改 appid + 补 mp-weixin 构建脚本<br/>⑤ OPS 配置安全组、日志、备份、快照 | 内容安全与隐私弹窗可验收 | 🟡 类目资质若被驳回需补材料 |
| **W4** | 备案 + 支付联调 | ① 备案通过（目标日）<br/>② BIZ 完成商户号 APIv3 密钥与证书配置<br/>③ TECH 配置 `pay_app`/`pay_channel` + 密钥注入<br/>④ TECH 跑通 1 元真实支付 + 回调 + 幂等<br/>⑤ OPS 申请并签发 Let's Encrypt 证书，配置 nginx 443 | ICP 备案号 + 支付链路打通 | 🔴 **备案未过则整条线顺延** |
| **W5** | 迁移与清理 | ① 迁移数据库/文件/前端到腾讯云<br/>② 执行 §9.2 演示数据清理清单<br/>③ 客户租户初始化（首期客户）<br/>④ 微信合法域名清单配置<br/>⑤ OCR / 短信接入（🟡 可并行） | 生产环境可用 + 清理留证 | 🟡 短信签名若被驳回 → 走管理员建号兜底 |
| **W6** | 灰度与验收 | ① G1 灰度（1 租户）真实用户<br/>② 业务全流程验收（U01–U10）<br/>③ 备份恢复演练 + 回滚演练<br/>④ 安全加固复核（§10 全部过一遍）<br/>⑤ 小程序提审 | 验收单 + 小程序审核通过 | 🟡 小程序审核被驳回需 1–2 轮修改 |
| **W7** | 放量 | ① G2（3 租户）→ G3 全量<br/>② 观察期监控每日巡检<br/>③ FIN 完成首轮提现对账 | 全量开放 + 首轮对账无差异 | |
| **W8+** | 稳态运营 | ① 云数据库/云 Redis 迁移（🟡）<br/>② KMS 密钥托管（🟡）<br/>③ CDN 接入（🟡）<br/>④ 第二批商户号申请（按客户签约节奏）<br/>⑤ 微信商家转账可行性评估（🟡，需场景证明资料） | 运营手册 + 交接文档 | |

### 12.2 关键路径与并行关系

```
W1 ─────────────────────────────────────────────►  ICP 备案 (W1启动 → 目标W4通过)
                                                        │ 最长前置项
                                                        ▼
W1 CVM下单 ──► W4 证书签发 ──► W5 迁移 ──► W6 灰度 ──► W7 全量
                                                        ▲
W2 商户号申请 ──► W4 支付联调 ─────────────────────────┘ (并行)
W2 小程序认证 ──► W3 类目审核 ──► W6 小程序提审 ────────┘ (并行)
W2-W3 台账开发 ─► W5 上线前自测 ─────────────────────┘ (并行)
```

| 项 | 说明 |
|---|---|
| **关键路径** | W1 备案启动 → W4 备案通过 → W4 证书 → W5 迁移 → W6 灰度验收 → W7 全量 |
| **最长前置项** | 🔴 **ICP 备案**（W1→W4，按 4 周规划；若管局超期或驳回重提则整体顺延） |
| **最长可并行项** | 支付链路（商户号申请 → 证书 → 联调）与打款台账开发，可在备案期间全部完成 |
| **可压缩点** | 若备案提前至 W3 通过，W4–W7 可整体前移 1 周；反之则顺延 |
| **不可压缩项** | 微信商户号审核（1–2 工作日）+ 商户证书（1–3 工作日）+ 小程序审核（1–3 工作日，驳回则重来） |

### 12.3 里程碑与决策点

| 里程碑 | 时点 | 判定标准 | 不通过时的动作 |
|---|---|---|---|
| **GATE-1 备案提交** | W1 末 | 备案已提交且初审通过 | 立刻排查驳回原因并重提 |
| **GATE-2 备案通过** | W4 | 拿到备案号 | 🔴 **关键决策点**：若 W5 末仍未通过，重新评估上线日期（不做"无备案先上"的冒险动作） |
| **GATE-3 支付联调通过** | W4 末 | 1 元真实支付 + 回调 + 幂等全通 | 排查证书/签名/租户路由；**不允许用 mock 上线** |
| **GATE-4 环境就绪** | W5 末 | 生产环境可用 + 演示数据清理完 + 安全加固全过 | 推迟灰度 |
| **GATE-5 上线批准** | W6 末 | U01–U10 全通过 + 业务方签字 | 不放量，保持 G1 灰度 |
| **GATE-6 全量** | W7 | 灰度期无 P1、对账无差异 | 退回 G2 |

### 12.4 关键时间点提醒

| 事项 | 提前量 | 责任 |
|---|---|---|
| 微信商户证书年度更新 | **提前 30 天** | TECH + BIZ |
| APIv3 密钥轮换 | 建议每 6–12 个月 | TECH |
| Let's Encrypt 续期 | 自动（90 天），但需人工确认 dry-run | TECH |
| 快照保留期复核 | 每季度 | OPS |
| ICP 备案信息变更（主体/域名变更） | 变更后 3 个工作日内提交 | BIZ |
| 春节前业务高峰与人力安排 | ⚪ 需业务方确认 | BIZ |

---

## 13. 风险登记册

| # | 风险 | 概率 | 影响 | 缓解措施 | 责任人 | 触发预警 |
|---|---|---|---|---|---|---|
| R-A | 红包打款是 MOCK，用户以为已到账 | **高** | **极高**（资金纠纷/赔付/平台处罚） | ① 停用 `mockTransfer()`；② 台账状态机强制"人工登记 + 凭证"才置已打款；③ 前端文案明确"审核通过后 1–2 个工作日打款"；④ 上线前 U01/U03 必测 | TECH + FIN | 代码中仍存在 `mockTransfer` 返回 true；或台账出现无凭证的"已打款"记录 |
| R-B | 首次真实支付踩坑（证书/签名/回调验签/金额校验） | **高** | 高 | ① W4 强制 1 元真实支付 Gate；② 参考芋道多商户号原生能力不改造核心；③ 回调按租户路由 + 幂等 + 金额校验三道校验；④ 保留 G0 内网联调环境 | TECH | 联调超过 2 个工作日未通；回调验签失败率 > 0 |
| R-C | ICP 备案超期或被驳回 | **中高** | **极高**（全线顺延） | ① W1 第一天提交；② 主体名称与营业执照完全一致；③ 预留 4 周并做 +2 周缓冲；④ GATE-2 决策点不冒险先上 | BIZ | W3 末仍未收到管局审核通过通知 |
| R-D | 微信支付商户号审核不通过（对公账户/主体不符/类目补充资料） | 中 | 高 | ① 提前确认对公账户可用；② 主体与小程序/备案三者一致；③ 准备补充资料（经营场景说明） | BIZ | 提交后 3 个工作日未出结果或收到补充材料通知 |
| R-E | 微信小程序认证/类目审核驳回 | 中 | 高 | ① 类目与实际功能严格对应（不虚选）；② 资质有效期核对；③ 保留 1–2 轮修改缓冲 | BIZ | 驳回通知到达 |
| R-F | 弱口令与演示数据导致数据泄露 | 中 | **极高** | ① S01/S03/S13/S14 上线前必须全过；② 安全组最小化；③ 每日巡检登录日志 | TECH | 公网扫描出现可登录的默认口令；异常登录告警 |
| R-G | 内网 HTTP 镜像仓被投毒 | 低 | **极高**（供应链） | ① 迁 TCR + 去掉 insecure-registries；② 镜像按 git sha 打标 + digest 校验；③ TCR 开启漏洞扫描 | TECH | compose 中仍出现 `100.66.41.15:5000` |
| R-H | 支付密钥明文入库或进 git | 中 | 高 | ① 占位符 + 运行时注入；② `.gitignore` + CI 密钥扫描；③ 日志脱敏 | TECH | `pay_channel.config` 中出现真实私钥；gitleaks 告警 |
| R-I | 短信签名/模板驳回 → 注册与找回密码不可用 | 中 | 中 | ① 备好签名佐证材料；② MVP 降级为"管理员建号 + 首登改密"；③ 人工申诉找回 | BIZ + TECH | 模板状态持续"审核中" > 2 个工作日 |
| R-J | "每客户独立商户号"办理量超预期 | 中 | 中 | ① 与业务方确认首期客户数与批次；② 优先大客户独立、其余共用一套（⚪ 需业务方确认）；③ 台账化批量申请流程 | BIZ | 单周需申请商户号 > 5 个 |
| R-K | 人工打款不及时引发投诉 | 中 | 高 | ① 明确 SLA 并写进用户协议（⚪ 需业务方确认）；② 每日待打款看板；③ 超 24h 未打款自动提醒 | FIN | 待打款单积压 > 20 笔或最长滞留 > 48h |
| R-L | 微信自动转账未来准入不确定 | 中 | 低（本轮不影响） | 本轮已选人工方案；保留 `pay_transfer` 扩展点；未来需场景证明材料 | BIZ | （仅影响二期） |
| R-M | 人工打款错账/重打 | 中 | 高 | ① `apply_no` 幂等 + 唯一索引；② 乐观锁状态流转；③ 凭证必填；④ 每周对账以银行流水为准 | FIN | 对账出现差异 |
| R-N | 云资源选型不足（容量/带宽） | 中 | 中 | ① 上线前压测 + 水位监控；② 带宽 5–10Mbps 起，图片走 COS/CDN；③ 可升配 | OPS | CPU/内存连续 3 天 > 70%；带宽峰值 > 80% |
| R-O | 备份不可恢复 | 低 | **极高** | ① 每周恢复演练；② 备份加密 + 异地（COS）留存；③ 快照保留 14 天 | OPS | 恢复演练失败 |
| R-P | 前端发布事故（删 dist 导致 500 循环） | 中 | 中 | ① 发布脚本固化 `find -mindepth 1 -delete`；② 发布后自动健康检查，失败即回滚上一版本 | TECH | 发布后健康检查失败 |
| R-Q | 构建打满生产机导致重启 | 中 | 中 | ① 构建移出生产机；② `nice -n 15`；③ 生产 CVM 只拉镜像 | TECH | 生产机出现构建进程；load > 20 |
| R-R | 依赖版本漂移导致构建失败/白屏 | 中 | 中 | ① 锁 `@swc/core@1.15.47`；② 保留 `vite-plugin-top-level-await@1.6.0`；③ CI 加依赖锁校验 | TECH | CI 构建失败；白屏 `Cannot read properties of null (reading 'nextSibling')` |
| R-S | 动态路由整页刷新导致白屏，用户投诉 | 中 | 低 | ① 引导用菜单切换；② 后台与店员端加提示 | TECH | 客服反馈刷新白屏 |
| R-T | UGC 违规内容未拦截导致小程序下架 | 中 | 高 | ① 服务端内容安全强校验；② 人工复审；③ 敏感词库 | TECH | 平台投诉/审核驳回 |
| R-U | 隐私指引不完整导致审核驳回 | 低 | 中 | ① 指引与实际采集逐项对齐；② 隐私弹窗未默认勾选 | TECH | 审核驳回通知 |
| R-V | 客户并发量突增击穿自建 MySQL | 中 | 高 | ① 上线前压测；② 读多写少改读缓存；③ 2–4 周内迁云数据库 | TECH | 慢查询 > 1s、连接数 > 70% |
| R-W | 第三方服务（OCR/短信/COS）故障或欠费 | 中 | 中 | ① 降级策略（§8.2/§8.3）；② COS 欠费告警；③ 余额告警 | OPS | 服务返回 5xx；余额不足告警 |
| R-X | 主体/备案/支付三者主体不一致被打回 | 中 | 高 | ① 全部统一为「要店帮（深圳市瑞康大药房连锁有限公司）」；② 提交前逐项核对 | BIZ | 任一平台提示主体不一致 |
| R-Y | 员工操作越权（跨租户查/改数据） | 低 | 高 | ① 芋道多租户隔离复核（U01 双跑校验）；② 平台角色最小化；③ 关键操作审计日志 | TECH | 出现跨租户可见数据 |
| R-Z | 上线期间业务中断（客户已有订单） | 中 | 中 | ① 灰度发布；② 切流窗口选低峰；③ 保留旧环境 7 天可回切 | OPS | 切流后错误率 > 1% |
| R-AA | 演示账号口令在 git 历史留痕 + BCrypt 弱 cost | 中 | 中 | ① **轮换 `demo123` 口令**（轮换即令历史中的旧口令失效，**优于改历史**：改历史要重写 15149 提交 + 重推 4 仓 88 tag）；② 重设时把 BCrypt cost 从 `$2a$04$` 升到 ≥10（`admin` 同为 cost4，`admin110`/`aoteman` 已是 cost10）；③ 对外分发只用 `ydb-hb-src:20261104-src`（无 `.git`）；④ 含历史的 `full2` 标注「仅限内网」 | TECH + BIZ | 登录日志出现 `demo123` 异地登录；`system_users` 出现 cost<10 的哈希 |
| R-AB | 孤儿租户数据（租户表无记录但有用户） | 中 | 低 | ① 上线前按 §12 演示数据清理范围处理：租户 118 / 119 / 120 各 1 个用户，租户主数据已不存在；② 清理后复跑租户一致性校验 | TECH | `system_users.tenant_id` 找不到对应 `system_tenant.id` |

### 13.1 风险等级汇总

| 等级 | 风险编号 | 数量 | 处理原则 |
|---|---|---|---|
| 🔴 极高 | R-A、R-C、R-F、R-G、R-O | 5 | **必须在 GATE-4（环境就绪）前关闭**，不可带病上线 |
| 🔴 高 | R-B、R-D、R-E、R-H、R-K、R-M、R-T、R-V、R-X | 9 | GATE-5 前必须有对应验证记录 |
| 🟡 中 | R-I、R-J、R-N、R-P、R-Q、R-R、R-S、R-U、R-W、R-Y、R-Z、**R-AA**、**R-AB** | 13 | 上线前处置或明确接受并记录 |
| 🟢 低 | R-L | 1 | 二期再评估 |
| **合计** | R-A ~ R-AB | **28** | 5 + 9 + 13 + 1 |

### 13.2 上线前必须关闭的"零容忍项"

| # | 项 | 对应风险 | 判定 |
|---|---|---|---|
| Z1 | 代码中不存在"恒返回 true 的自动打款"逻辑 | R-A | 代码审查 + 行为测试 |
| Z2 | `pay_app`/`pay_order` 有真实交易记录 | R-B | 查表 |
| Z3 | 全站 HTTPS，无 HTTP 明文页面 | — | `curl -I` |
| Z4 | MySQL/Redis 无弱口令 | R-F | 弱口令库扫描 |
| Z5 | 生产库无演示数据与默认账号 | R-F | 查表 + 登录测试 |
| Z6 | 镜像源为 TCR，无 insecure-registries | R-G | `docker info` |
| Z7 | 支付/短信密钥无明文入库、无明文入库 git | R-H | 查表 + gitleaks |
| Z8 | UGC 内容安全已生效 | R-T | 违规词测试 |
| Z9 | 备份已成功恢复过一次 | R-O | 恢复演练报告 |
| Z10 | 打款 SOP 已由 FIN 演练确认 | R-A、R-K | 演练记录 |
| Z11 | ICP 备案号已下发并展示 | R-C | 备案号截图 |
| Z12 | 提现"已打款"状态必须有凭证 | R-A、R-M | 状态机校验 |

---

## 附录 A：踩坑铁律（生产实施必须遵守）

> 以下每一条都来自已发生的真实故障，不是"最佳实践建议"。违反其中任何一条都会导致线上事故。

| # | 铁律 | 原因 | 正确做法 | 验证方式 |
|---|---|---|---|---|
| **F1** | 🔴 **前端部署不能 `rm -rf dist`** | 会删掉 bind mount 挂载点 inode → nginx `try_files` 循环 → **500** | `find ./dist -mindepth 1 -delete`（保留目录本身） | 发布后立即访问 4 个前端，无 500 循环 |
| **F2** | 🔴 **后端 48080 必须绑 `127.0.0.1`** | 一旦对外暴露，后端无鉴权接口直接裸奔 | compose 中 `ports: - "127.0.0.1:48080:48080"` | `ss -lntp \| grep 48080` 显示 `127.0.0.1` |
| **F3** | 🔴 **`@swc/core` 必须锁 `1.15.47`** | 升到 `1.16.2` 会与 `vite-plugin-top-level-await@1.6.0` 冲突，报 `missing field 'type'`；删插件则产物结构错乱 → 刷新白屏 `Cannot read properties of null (reading 'nextSibling')` | lock 文件锁版本；**插件不能删** | CI 构建通过 + 产物刷新无白屏 |
| **F4** | 🔴 **芋道动态路由不能整页刷新** | 路由表登录后注入内存，刷新即丢 | 用户用菜单切换；生产引导文案明示 | 刷新后走菜单可正常跳转 |
| **F5** | 🔴 **构建必须 `nice -n 15`** | 8G 机器构建期 load 飙 38 会整机重启 | `nice -n 15 <build cmd>`；且**构建移出生产 CVM** | 构建期间生产机 load < 5 |

**衍生铁律（由上述踩坑推出的实施纪律）**

| # | 纪律 |
|---|---|
| F6 | 生产发布脚本必须固化 F1 的 `find` 写法，并在发布后自动健康检查，失败自动回滚上一版本镜像 |
| F7 | CI 必须校验 `@swc/core` 版本与 `vite-plugin-top-level-await` 存在性，版本漂移直接 fail |
| F8 | 生产 CVM 上禁止任何前端/后端构建进程（只拉镜像、只跑容器） |
| F9 | 镜像 tag 必须带 git sha，回滚 = 换 tag，不重建 |

---

## 附录 B：外部规则引用（2026 年 11 月现行）

| # | 规则 | 影响 | 对本方案的约束 |
|---|---|---|---|
| E1 | 微信小程序认证：**300 元/年（企业主体）**，个体户 30 元/年，腾讯官方收取，已关闭免费认证通道 | 成本 | 计入年费；若已有认证企业公众号，走"快速注册并认证小程序"复用主体资质，**免 300 元** |
| E2 | 小程序 **ICP 备案**（工信部）：2023 年 9 月起新主体强制；主体须与小程序主体一致；审核周期通常数周；需已备案域名 | 关键路径 | 备案成为最长前置项；域名与主体三者（备案/小程序/支付）必须一致 |
| E3 | 服务类目：医药/医疗健康属重点监管行业，需对应资质（医疗机构执业许可证 / 药品经营许可证等）；**类目选错是驳回高频原因** | 审核 | A10 单列为一票阻塞项，提交前逐条核对资质 |
| E4 | 微信支付商户号：企业主体需**对公账户**；个体户可用经营者个人银行卡；费率常见 0.2%–0.6% | 办理 | 商户号申请材料含对公账户；费率登记入台账供财务对账 |
| E5 | **商家转账**（2025-01-15 起由"商家转账到零钱"升级）：**2026-05-13 起新开通商户不再直接申请"商家红包/现金红包"，统一走"商家转账"**；需开通**运营账户**（次日 0:00 生效、**不可关闭**、所有运营业务强制从运营账户出资）；需提交**转账场景证明资料**；**请求来源 IP 必配**才能调 API；支持"红包样式"单笔 **≤ 200 元**；**暂不支持小微商户**（个体工商户部分场景支持） | 决策 | 支撑"本轮人工台账"决策；§7.8 保留全部扩展点 |
| E6 | APIv3 密钥 + 商户证书（`apiclient_key.pem` + 证书序列号）或微信支付公钥（公钥ID + 公钥文件）为必需 | 密钥 | §6.3 密钥管理；证书提前 30 天续期 |
| E7 | UGC 小程序必须接入**内容安全 API**（`msgSecCheck`/`imgSecCheck`），前端 + 服务端双重审核 | 阻塞 | A8 + I26 + O 系列，零容忍项 Z8 |
| E8 | 必须配置《小程序用户隐私保护指引》，接隐私弹窗（`wx.getPrivacySetting`），**禁止默认勾选** | 阻塞 | A9 + I27 + Z-对应项 |
| E9 | 微信服务器域名：每类上限 200 个；必须已备案、必须 HTTPS、必须 443 端口、证书链完整、遵循 301/302 跳转 | 配置 | §5.4 清单，I28 验证 |

---

## 附录 C：待业务方确认事项汇总（⚪）

| # | 待确认事项 | 影响面 | 建议回复形式 | 期望回复时间 |
|---|---|---|---|---|
| C1 | 是否已有**认证过的企业公众号**（可免 300 元小程序认证费） | 成本 −300 | 是/否 | W1 内 |
| C2 | 首期上线客户数量与名单 | 商户号申请批次、T1 租户初始化 | 清单 | W1 内 |
| C3 | 是否**所有**客户都要独立商户号，还是仅大客户独立 | 商业模型与办理量 | 明确规则 | W1 内 |
| C4 | 提现打款的**到账时效 SLA**（建议 1–2 个工作日） | 用户预期与投诉口径 | 书面承诺 | W2 内 |
| C5 | 提现**单笔/单日限额**规则 | 风控配置 S34 | 数值 | W2 内 |
| C6 | 人工打款的对账周期（月结 / 批次结） | §7.6 SOP | 规则 | W2 内 |
| C7 | 打款凭证的**保留年限**（建议 ≥3 年） | 存储成本与合规 | 年限 | W2 内 |
| C8 | 医药类目所需的**具体资质清单**（以实际经营范围为准） | A10 审核 | 资质扫描件 | W2 内 |
| C9 | 药店经营是否需要**前置/后置审批**（如涉及互联网药品信息服务） | 备案材料 | 结论 | W1 内 |
| C10 | 短信签名用**企业全称还是简称**，佐证材料用哪个（官网/公众号/APP） | 短信审核 | 指定 | W2 内 |
| C11 | 客服与投诉受理人、受理时限 | 运营兜底 | 人员 + 时限 | W5 前 |
| C12 | 灰度放量的节奏与放量标准 | G1→G3 | 计划 | W5 前 |
| C13 | 弱证据晒单（无小票、仅文字+金额）是否允许 | 风控策略 | 允许/禁止 | W3 前 |
| C14 | 门店定位是否使用（影响 `Permissions-Policy` 与隐私指引） | 合规与配置 | 是/否 | W3 前 |
| C15 | 是否接受"先自建 MySQL/Redis、上线后 2–4 周迁云数据库"的折中路径 | 成本与风险 | 接受/不 | W2 内 |
| C16 | 上线后是否引入 CDN（图片流量成本 vs 体验） | 成本 | 是/否 | 上线后 |
| C17 | 春节前是否需要单独活动版本（影响排期） | 排期 | 是/否 | W1 内 |
| C18 | 是否需要为店员端/管理员提供独立的登录安全增强（二次验证/IP 白名单） | 安全 vs 体验 | 是/否 | W4 前 |

---

## 附录 D：一页纸执行摘要

| 项 | 内容 |
|---|---|
| **一句话结论** | 业务骨架已具备，**两条资金链路（支付 + 打款）是空壳**；补齐 ICP 备案 + 域名 + HTTPS + 真实商户号 + 人工打款台账 + 数据清理与加固后即可商用 |
| **最早可上线时间** | ICP 备案 W1 启动 → 目标 **W7 全量**（约 7 周）；备案提前则可压缩至 6 周 |
| **最长前置项** | 🔴 ICP 备案（数周），必须 W1 第一天启动 |
| **首年总投入** | 约 **1.7 万 – 6.2 万元**（一次性 0.76–2.41 万 + 年费 0.92–3.79 万，均含全部 🟡 可选项；不含交易手续费与运营资金），⚠️ 参考区间，以实际采购为准 |
| **MVP 上线集** | M1–M12（§1.4）—— 12 项，其余全部可延后 |
| **最大资金风险** | 🔴 `mockTransfer()` 恒 true —— 上线前必须关闭，否则"提现已到账"是假象 |
| **最省钱的两个动作** | ① 有认证企业公众号 → 免 300 元小程序认证；② Let's Encrypt + 自我管理证书 → 免 0–1500 元/年证书费 |
| **最容易被忽略的三件事** | ① 备案主体/小程序主体/支付主体三者必须一致；② 医药类目选错是驳回高频原因；③ 后端 48080 绝不能对公暴露 |
| **技术侧工作量的大头** | 人工打款台账（表 + 状态机 + 审批页 + 凭证 + 导出 + 对账）+ 支付租户路由与密钥安全管理 + 生产清理与加固 |
| **需要业务方做的一件事** | **W1 第一天启动 ICP 备案**，其余再慢都还有回旋余地；备案一慢，全线顺延 |

---

**文档结束**
