From 1ffcbec7712a0de8da9d5d46fe091a5738900681 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 8 Oct 2026 14:14:09 +0800 Subject: [PATCH] =?UTF-8?q?docs(=E6=8A=96=E9=9F=B3-=E7=A0=81=E9=80=9F?= =?UTF-8?q?=E8=BE=BE):=20=E7=BB=93=E5=90=88=20order=5Fsite=20=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=B7=B1=E5=BA=A6=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 docs/抖音-码速达/04-结合本项目的深度分析.md(358 行),把新平台协议 数据与本仓库现有架构对齐,回答「能不能接、怎么接、代价与风险」: - 现状摸底(全部带代码位置):进单主链路 upsertOrderFromSource / 履约执行器 契约与注册表 / 履约路由白名单 / 商品解析分支 / webhook 幂等闸门 / 配置体系 / 审计与 RBAC / 调度 / 可观测性;并指出目前只有 91 卡券一条「推送式进单 → 自动履约」完整通路,仓库内无任何码速达实现代码。 - 能力映射:14 条消息 → 本系统动作(7 条可直接复用订单状态机,5 条售后/仲裁 本系统无对应模型);16 个接口 → 本系统能力(topup.result 回执、coupons.* 券码链路、afterSale.* 售后为全新);字段级映射与状态映射建议表。 - 关键差异:trade_TradePaid 不含商品与买家账号,必须再调 order.orderDetail 补全;码速达推送没有 event_id,给出 4 种幂等键方案(推荐 topic+pub_time+ p_id:s_id,body 哈希兜底);协议无 nonce,防重放只能靠时间窗 + 幂等表。 - 差距清单 11 项(G1~G11):签名/出网、消息入口、商品解析分支、状态映射、 topup.result 回执、卡券、售后、配置面板、对账补偿、成本与可观测、env 白名单 硬约束;每项给出影响、落点文件与规模估算。 - 7 个待拍板设计决策 + 分 5 期实施计划(含每期落点文件与验收标准)+ 测试方案 + 风险清单 + 10 条合规红线(密钥只读不回显、买家隐私不进工单素材等) + 待确认问题清单(给平台方 6 条 / 给业务方 3 条)。 结论:主干可复用约 70%,协议层/状态映射/回执/卡券需新写;打通进单+回执约 6~10 人日,卡券+售后+面板再加 8~12 人日。已同步更新目录索引 README.md。 --- docs/抖音-码速达/04-结合本项目的深度分析.md | 358 ++++++++++++++++++++ docs/抖音-码速达/README.md | 1 + 2 files changed, 359 insertions(+) create mode 100644 docs/抖音-码速达/04-结合本项目的深度分析.md diff --git a/docs/抖音-码速达/04-结合本项目的深度分析.md b/docs/抖音-码速达/04-结合本项目的深度分析.md new file mode 100644 index 00000000..62262331 --- /dev/null +++ b/docs/抖音-码速达/04-结合本项目的深度分析.md @@ -0,0 +1,358 @@ +# 抖音码速达 × order_site 深度分析 + +> 分析对象:新平台「抖音码速达 - 开放平台」(下文简称 **码速达**),协议数据见同目录 +> [README.md](README.md) / [02-接口调用规范与签名.md](02-接口调用规范与签名.md) / [03-消息推送规范与签名.md](03-消息推送规范与签名.md)。 +> 分析主体:本仓库 `order_site`(快手自动发货与履约管理系统)。 +> 写作日期:2026-10-08 | 分支:`dev` | 结论均标注「事实 / 推断」,「事实」都带代码位置。 + +--- + +## 0. 一句话结论 + +**码速达不是「又发一个链接的平台」,而是「抖店订单的进单口 + 履约结果的回执口 + 卡券的券码源」。** +它跟本系统现有能力是**互补**关系:本系统已有的「进单 → 建单 → 商品匹配 → 履约路由 → 履约执行 → 结果落库」主干**可以整体复用**(复用度约 70%), +但**协议层(两套 MD5 签名)、状态映射层(抖店 8 个状态 → 内部 4 个状态)、回执层(`topup.result` 出网)、卡券层(`coupons.*`)必须新写**。 + +仓库现状(事实):`apps/backend/src`、`apps/frontend/src` 中 **没有任何** 抖店 / 码速达 相关业务代码 +(`grep -riE "码速达|doudian|抖店|jinritemai"` 仅命中本次新增的 `docs/抖音-码速达/`),属于全新接入,不存在「改造旧实现」的迁移风险。 + +初步规模判断(推断):协议层 + 进单 + 回执打通 **约 6~10 人日**;卡券全链路 + 售后 + 后台面板 **再加 8~12 人日**; +必须在联调前确认的**外部阻塞项 5 个**(见 §9),其中「抖店商户号 uid / 用户秘钥 secret 由谁分配」「我们扮演商家还是服务商」是**最高优先级**。 + +--- + +## 1. 本项目现状(可复用的主干) + +### 1.1 主链路(事实) + +```text +外部订单 + → 归一化成 SourceOrderEvent(services/order/order-service.ts:41-57,内存 DTO,无独立表) + → upsertOrderFromSource()(order-service.ts:98) + ├─ advisory lock:hashtextextended(provider|platform|platformOrderId)(order-service.ts:124-127) + ├─ orders(唯一键 provider+platform+shop_id+platform_order_id)/ order_items + └─ mergeSourceOrderState()(order-service.ts:445,状态只升不降,见 :432-444 优先级表) + → 商品/履约解析 resolveOrderItemForFulfillment(fulfillment/product-resolution-service.ts:165) + → 履约任务 syncDeliveryTasksForOrder(order/delivery-task-service.ts:48) + → 内部工单 syncWorkerOrdersForSourceOrder(worker-platform/sync-worker-orders-service.ts:37,工单号 WO+…随机) + → 执行器 preparePaidFulfillmentTask(fulfillment/executors/registry.ts:26) + → 结果落库 + 事件留痕(task_events / work_order_events) +``` + +入口挂载点:`apps/backend/src/app.ts:102-111`(`/api/v1/open/<平台>`、`/api/v1/claim`、`/api/v1/admin`); +`rawBody` 已在 `app.ts:37-45` 统一采集,**消息推送验签可以直接复用**。 + +### 1.2 现有「来源平台」对比(事实 + 推断) + +| 平台 | provider / platform | 接入方式 | 入口 | 我方面向外部的能力 | +| --- | --- | --- | --- | --- | +| 91 卡券 | `91kaquan` / `kuaishou` | **推送**(对方调我方 HTTP) | `routes/open-91.ts:27` `POST /api/v1/open/91/orders/create` | 返回 `orderStatus 10/20/30` + AES 加密 cards + `claimUrl` | +| 拼多多 | `pdd` | **轮询**(我方定时拉单) | `services/scheduler/pdd-order-sync-job.ts:11` → `pdd-sync-service.ts` | 无对外接口;本地 `pdd_raw_orders` 暂存后转订单 | +| 快手行业电子凭证 | 复用 `91kaquan`/`kuaishou` | 推送(补全/兜底源) | `routes/kuaishou-industry.ts:24/62/100/138` | 出网调快手 OpenAPI 回执(发码/核销/销毁/冲正) | +| affiliate_dash | — | **不是来源,是履约通道** | `routes/affiliate-dash.ts:16`(只同步任务状态) | 无 | + +> 关键事实:**目前只有 91 卡券一条「推送式进单 → 建单 → 自动履约」的完整通路**。 +> 码速达与它形态最像,是最值得照抄的模板;差别在于码速达**同时要求我们出网回执结果**(topup.result),这点更像快手行业的回执 worker。 + +### 1.3 扩展点地图(事实) + +| 层 | 扩展点 | 位置 | 新增平台要做什么 | +| --- | --- | --- | --- | +| 路由 | 平台入口挂载 | `app.ts:102-111` | 加一行 `app.use('/api/v1/open/douyin-mashida', …)` | +| 进单 | `upsertOrderFromSource` | `order/order-service.ts:98` | 无需改(复用);**要改的是下面「商品解析」** | +| 商品/履约解析 | `resolveConfiguredItemCandidate` | `fulfillment/product-resolution-service.ts:165` | **必须加分支**:目前只有 `isOpen91KuaishouOrder`(:175,判定 :346)一个入口,其他 provider 一律 `isConfigured=false`(:214-225)→ 不 auto 履约 | +| 执行器契约 | `FulfillmentExecutor` | `fulfillment/executors/types.ts:53-57` | 实现 `preparePaidTask` / `resolveDeliveryLink` 并注册进 `registry.ts:15-20` | +| 执行器白名单 | `ROUTABLE_EXECUTOR_KEYS` | `fulfillment/routing-config-service.ts:64-69` | **必须加**,否则 `normalizeExecutorKey`(:470)会把新 key 清空,后台路由规则选不中 | +| 履约档案 | `CORE_PROFILES` | `bootstrap/fulfillment-bootstrap-service.ts:22-39` | 加一个 profile(key/executor_key/requires_claim) | +| 入站幂等 | `insertWebhookEventOnce` | `repositories/webhook-event-repo.ts:9-43`,表 `webhook_events(provider,event_id)` 唯一(迁移 `013_*.sql:12`);**全仓只有 affiliate_dash 一个调用点**(`notify-service.ts:70`) | 直接用,但**码速达推送没有 `event_id` 字段**,需自行派生(见 §3.5) | +| 可观测 | `logIntegration` / `logExternalHttpPacket` / `task_events` / `admin_notifications`(dedupe_key) / `publishRealtimeEvent`(SSE) | `utils/logger.ts:96/122`、`repositories/task-event-repo.ts:16`、`repositories/admin-notification-repo.ts:25`、`services/realtime/realtime-event-service.ts:92` | 消息流水 + 回执失败 + 仲裁告警都要往这里写 | +| 数据保留 | `data-retention-job.ts:21/39-48` | 定期清理 `webhook_events`/`task_events`/原始报文 | 新平台的报文留痕自动纳入保留策略,无需额外处理 | +| 配置 | `app_config_entries(config_key, config_json JSONB)` | 表 `001_init.sql:45-62`;`config/app-config-keys.ts:1-15`、`services/config/app-config-store.ts:20/43`、`repositories/app-config-repo.ts:20/32` | 加配置键 + `platforms` 块;**凭据脱敏照抄快手**(见 G8) | +| 环境变量 | `config/env-overrides.ts:61-101` | DB 配置 + env 覆盖双通道 | 加 `DOUYIN_MASHIDA_*` | +| 审计 | `admin_audit_logs`(`001_init.sql:23-33`)+ `createJsonHandler({ audit })` | `routes/admin/session.ts:30/138` | action `platform_douyin_mashida_*`、targetType `platform_config` | +| 定时任务 | `scheduler/scheduler-service.ts:204`(job type 分发) | 现有:工单超时、押金解冻、PDD 同步、数据保留 | 兜底对账/补偿任务挂这里 | +| 后台配置 | `routes/admin/platform-config.ts:13-19` + 前端 `pages/admin/platform/` | RBAC `requireAdminRoles` | 加平台配置子路由 + 前端面板 | +| 数据库 | `db/migrations/0XX_*.sql` | 当前最新 `093_clear_internal_consignment_last_worker.sql` | 新表从 `094` 起编号 | + +--- + +## 2. 码速达在业务链路里的定位(推断 + 文档依据) + +码速达的每条接口文档都带 `dop_path` / `dop_url` 指向 `op.jinritemai.com`(抖店开放平台),说明它是一层**代理/中转网关**: +它把抖店开放平台的「订单消息推送 + 商家 API」封装成一套统一签名(用户秘钥 + 商户号 uid + MD5)供我们调用。 +因此 —— **我们在这条链路里的角色是「抖店商家 / 履约方」**,而不是 91 卡券场景里的「发链接方」。 + +```text +抖店买家下单付款 + → 码速达推送 trade_TradePaid(或 trade_TradeCreate) + → order_site 建单 / 匹配商品规则 / 建履约任务(打手或自动通道) + → 履约完成 + → order_site 回执:topup.result(充值类)或 coupons.syncV2 + coupons.verifyV3(卡券类) + → 平台侧完成发货/发码 +售后:refund_* 消息 → order_site 处理 → afterSale.operate 回执审核结果 +``` + +对照现有链路,**「买家自己打开领取页 claimUrl」这一步在码速达场景里是可选的**: +充值类订单的履约对象是买家在 `buyer_words` / 订单详情里给的游戏账号(现有 PDD 映射已有同样注释:`pdd-order-mapper.ts:216`「买家留言通常为充值游戏账号」), +链路是「我们替买家充值 → 回报平台」,买家无需打开我们的页面。**是否仍然生成 claimUrl 作为兜底/留痕,需要业务确认(见 §9 Q3)。** + +--- + +## 3. 能力映射 + +### 3.1 14 条消息 → 本系统动作 + +| 消息 (topic) | 分级 | 本系统应做什么(建议) | 现有可复用 | +| --- | --- | --- | --- | +| `trade_TradePaid` 买家已付款 | 基础 | **主进单事件**:`order.orderDetail` 拉详情 → 建单(`paid/paid`)→ 建履约任务 | 91 的 `POST /orders/create` 全流程 | +| `trade_TradeCreate` 订单创建 | 基础 | 只落「待付款」影子单或忽略(推荐**先落单不派单**) | `upsertOrderFromSource`(`payStatus=unpaid`) | +| `trade_TradeSellerShip` 卖家发货 | 基础 | 发货后**停止/关闭**履约任务(避免重复发货) | `syncAffiliateDashTaskStatus` 式状态合并 | +| `trade_TradePartlySellerShip` 卖家部分发货 | 基础 | 同上,按子订单粒度关闭 | 同上 | +| `trade_TradeMemoModify` 卖家备注变更 | 基础 | 同步备注(→ `order.addOrderRemark` 反向/本地留痕) | — | +| `trade_TradeSuccess` 订单完成 | 基础 | 订单置 `completed`,结束超时计时 | `mergeSourceOrderState` | +| `trade_TradeCanceled` 订单取消 | 基础 | 订单置 `closed`,**终止未完成履约任务** | `mergeSourceOrderState`(优先级表) | +| `refund_RefundCreated` 买家发起售后 | 基础 | 建售后跟踪(**内部 `worker_after_sales_cases` 是打手履约售后,不是这套**) | 需新写电商售后模块(或先只落事件) | +| `refund_RefundAgreed` 同意退款 | 基础 | 同上 + 提示客服 | — | +| `refund_RefundRefused` 拒绝退款 | 高级 | 同上 | — | +| `refund_RefundSuccess` 退款成功 | 高级 | 订单置 `refunded`,**回收/中止履约** | `mergeSourceOrderState`(refunded 优先级最高) | +| `refund_RefundClosed` 售后关闭 | 高级 | 售后跟踪关闭 | — | +| `refund_ArbitrateApplied` 发起仲裁 | 高级 | 高危告警(客服必须介入) | `notifyInternalSafely`(`services/notification/domain-notifications.ts:58`) | +| `refund_ArbitrateCancelled` 取消仲裁 | 高级 | 关闭告警 | 同上 | + +**覆盖度判断**:14 条里 **7 条(订单生命周期)** 可完全复用现有订单状态机;**5 条(售后/仲裁)** 本系统没有对应模型,需要新增或先 degrade 成「事件留痕 + 后台通知」。 + +### 3.2 16 个接口 → 本系统能力 + +| 接口 (method) | 方向 | 分级 | 本系统用途 | 现状 | +| --- | --- | --- | --- | --- | +| `topup.result` 充值结果提交 | 我方→平台 | 基础 | **履约完成回执**(充值类核心出口) | ❌ 全新;需出网 + 重试 | +| `order.orderDetail` 订单详情 | 我方→平台 | 高级 | 收到消息后补全订单(商品/金额/买家账号) | ❌ 全新(可对标 `affiliate-dash/http-client.ts`) | +| `order.searchList` 订单列表 | 我方→平台 | 未标注 | 定时兜底对账 / 补漏单(近 90 天) | ❌ 全新 + 挂调度 | +| `order.addOrderRemark` 卖家备注 | 我方→平台 | 基础 | 把内部处理进度写回抖店备注 | ❌ 全新 | +| `coupons.syncV2` 卡券同步 | 我方→平台 | 基础 | 生成券码后同步给平台(`order_id` + `cert_list[{cert_no,cert_link,grant_time}]`) | ⚠️ 券码生成可复用 `KSV`+CrockfordBase32(`kuaishou-industry-voucher-repo.ts:49`) | +| `coupons.verifyV3` 卡券核销 | 我方→平台 | 高级 | 上报核销(`cert_no`) | ⚠️ 状态机可复用 `UNUSED/CONSUMED/DESTROYED`(`voucher-service.ts:500`) | +| `coupons.list` 券码查询 | 我方→平台 | 基础 | 核销前查平台侧状态,防重复核销 | ❌ 全新 | +| `coupons.cancelVerify` 卡券取消核销 | 我方→平台 | 基础 | 误核销冲正 | ❌ 全新 | +| `coupons.CertSyncFailedCancelOrder` 商家主动取消订单 | 我方→平台 | 高级 | 券同步失败时取消抖店订单 | ❌ 全新 | +| `afterSale.operate` 售后审核 | 我方→平台 | 高级 | 回执同意/拒绝售后 | ❌ 全新 | +| `afterSale.rejectReasonCodeList` 拒绝原因列表 | 我方→平台 | 免费 | 拒绝售后的原因下拉数据 | ❌ 全新(可缓存) | +| `message.config` 消息设置 | 我方→平台 | 免费 | 配置推送地址与订阅主题 | ❌ 全新(上线时调一次) | +| `product.detail` / `product.list` / `sku.list` | 我方→平台 | 基础/高级/高级 | 拉商品与 SKU,用于建立码速达商品 ↔ 内部接单模板的映射 | ⚠️ 可对标 `affiliate-dash/product-service.ts`、`sku-mapping.ts` | +| `order.message` 订单消息测试 | 我方→平台 | 高级 | 联调期模拟消息(**仅支持特定商户**) | ❌ 全新(联调工具) | + +### 3.3 字段级映射(进单:抖店 → `SourceOrderEvent`) + +| `SourceOrderEvent` | 来源 | 说明 | +| --- | --- | --- | +| `provider` | 常量 `'douyin_mashida'` | 建议值,避免与 `91kaquan`/`pdd` 混淆 | +| `platform` | 常量 `'doudian'` | 抖店 | +| `shopId` / `shopName` | `shop_id` / `order.orderDetail.shop_name` | 消息里只有 `shop_id`,店名需 `orderDetail` 补 | +| `platformOrderId` | `p_id`(父订单) | ⚠️ 卡片字段叫 `p_id`;`s_ids` 是子订单数组,**必须决定按父单还是子单建单**(见 §5 D2) | +| `orderStatus` / `payStatus` | 映射见下 | | +| `buyerId` | `doudian_open_id` | 加密串,注意脱敏与「不暴露给打手」的既有约定 | +| `buyerName` / `receiverContact` | `buyer_words`(充值账号常在此)/ `post_tel`+`post_receiver` | **充值账号的取值位置需与业务确认**(`buyer_words` 还是商品字段) | +| `totalAmount` | `pay_amount`(分) | 与内部金额单位一致(分) | +| `paidAt` | `pay_time`(秒) | 统一转 ISO | +| `items[]` | `order.orderDetail.sku_order_list[]`(消息本身**不含商品明细**) | `product_name` / `sku_id` / `out_sku_id` / `code`(商家编码)/ `item_num` | +| `skuCode` | `code`(商家后台商品编码)优先,其次 `out_sku_id` | 用于匹配 `work_product_rules` | +| `rawPayload` | 原始 body / 接口响应 | 落 `orders.raw_payload_json` | + +**重要事实**:`trade_TradePaid` 消息本身**不含商品与买家账号**(只有 `pay_amount/s_ids/order_status/order_type/pay_time/p_id/shop_id/pay_type/biz`)。 +→ **必须再调 `order.orderDetail` 补全**,否则无法建履约任务。这是与 91 卡券(报文里直接带商品号)最大的工程差异。 + +### 3.4 幂等键怎么定(码速达没有 `event_id`) + +现有幂等闸门 `insertWebhookEventOnce({provider, eventId, …})` 要求一个稳定 `event_id`(表唯一键 `(provider,event_id)`), +但码速达推送只有 `topic` + `pub_time` + `sign`,**没有消息 ID**。可选方案: + +| 方案 | `event_id` | 评价 | +| --- | --- | --- | +| A | `sign` | **不可用**:`sign` 由 secret + 参数派生,secret 轮换后同一消息的 sign 变化,且理论上不同消息 sign 必不同但不可读 | +| B | `topic:pub_time` | 简单;同一秒同一 topic 的两条不同消息会**误判重复**(低频场景可接受) | +| C | `topic:pub_time:p_id:s_id` | 推荐;订单维度天然唯一,且与业务键对齐 | +| D | `sha256(原始 body)` | 最严格(内容级去重),但要保证 body 字节稳定(不能重新序列化) | + +**建议 C 为主、D 为兜底**;`event_type` 写 topic。注意:秒级 `pub_time` + topic 的误判风险在「同一订单同一秒两次推送」时真实存在, +所以**不要只用 B**。文档另需标注:抖音协议**没有 nonce**(对比 PDD 插件有 nonce 防重放), +防重放只能靠 `pub_time` 时间窗(参考 `open-91/payload.ts:67 assertOpen91Timestamp` 的写法)+ 幂等表。 + +### 3.5 状态映射建议(事实:内部只有 4 态) + +内部优先级表(`order-service.ts:432-444`):`created(0) < paid(1) < closed(2) < refunded(3)`;`unpaid(0) < paid(1) < refunded(2)`。 + +| 抖店 `order_status` | 含义 | 建议内部 `orderStatus/payStatus` | +| --- | --- | --- | +| `1` | 待确认/待支付 | `created` / `unpaid` | +| `105` | 已支付 | `paid` / `paid` | +| `2` | 备货中 | `paid` / `paid` | +| `101` | 部分发货 | `paid` / `paid`(+ 关闭对应子单履约) | +| `3` | 已发货(全部发货) | `paid` / `paid`(+ 关闭履约任务) | +| `4` | 已取消 | `closed` / `unpaid` | +| `5` | 已完成 | `completed` / `paid` ⚠️ **`completed`/`shipped` 不在优先级表内,按 -1 处理**(与 PDD 同样的问题,见 §5 D3) | +| `after_sale_status` | 13/28/51/53 等 | 建议 `refunded` / `refunded`(仅在退款成功/售后成立时) | + +--- + +## 4. 差距分析(Gap) + +> 规模:S ≈ 0.5~1 人日,M ≈ 2~4 人日,L ≥ 5 人日(推断,按现有同类实现复杂度类比)。 + +| # | 缺口 | 现状(事实) | 影响 | 落点(建议文件) | 规模 | +| --- | --- | --- | --- | --- | --- | +| **G1** | 出网客户端 + 签名(两套 MD5) | 现有三套签名互不兼容:91 = `MD5(secret + k=v&… + secret)` **大写**(`open-91/crypto.ts:8-30`);affiliate_dash = HMAC-SHA256(`sign.ts:22`);快手 = MD5/HMAC(`crypto.ts:28`)。码速达 = `MD5(secret + Σ(name+value) + secret)` **小写**(无 `=`/`&` 分隔) | 无此层则一个接口都调不通 | `services/platforms/douyin-mashida/sign.ts`、`http.ts`、`openapi-client.ts` | M | +| **G2** | 消息接收端点 + 验签 + 幂等 | 现有最完整模板:`routes/affiliate-dash.ts:16` + `notify-service.ts:21-120`(验签 → 找本地任务 → `webhook_events` 去重 → 任务事件 → 状态合并);`rawBody` 已在 `app.ts:37-45` 采集 | 不能收单 | `routes/douyin-mashida.ts`、`services/platforms/douyin-mashida/notify-service.ts`;复用 `webhook-event-repo.ts:9` | M | +| **G3** | 进单商品解析分支 | **硬编码**:`product-resolution-service.ts:175/346` 只认 `91kaquan`+`kuaishou`,其他来源 `isConfigured=false`(:214-225)→ 不自动履约 | 码速达订单会「建单但永远不派单」 | 改 `product-resolution-service.ts`(加 `isDouyinMashidaOrder` 分支 + 候选来源) | S~M | +| **G4** | 状态映射层 | 内部只有 4 个订单态;抖店 8 个状态无映射;且 `completed`/`shipped` 不在优先级表(PDD 已有同样坑) | 状态乱跳/不更新 | `order-service.ts:432-444` 扩表 或 在 mapper 里降级(推荐后者,改动面小) | S | +| **G5** | 履约完成 → 回执(`topup.result`) | 现有回执参考:快手发码回调 worker(`send-code-service.ts:166/400/456`,退避 `<3s,10s,30s,60s,120s,300s>`、≤8 次、15s 扫描);affiliate_dash 回调靠对方重试 | 平台不知道我们充值完了 → 业务不闭环 | `services/platforms/douyin-mashida/topup-service.ts` + 任务完成钩子 + 重试 worker(挂 `startup/bootstrap.ts:47-49`) | M~L | +| **G6** | 卡券能力(syncV2/verifyV3/list/cancelVerify) | 现有券模型:`kuaishou_industry_vouchers`(`001_init.sql:272-297`,`(oid,unit_index)` 唯一)+ 状态机 `UNUSED/CONSUMED/DESTROYED`(`voucher-service.ts:500`),但语义是「快手电子凭证」,字段带 `oid/token/seller_id` | 卡券类订单无法交付 | 新表 `douyin_mashida_certificates`(语义不同,**不建议复用**快手表,见 §5 D4) | L | +| **G7** | 电商售后链路 | 内部售后 `worker_after_sales_cases`(`052_*.sql:3`)是**打手履约售后**(`awaiting_worker_response/under_review/recovery_pending/closed`),与抖店售后无关 | 售后消息只能留痕,无法回执审核 | 先做「事件留痕 + 后台通知」,二期再做 `afterSale.operate` 回执 | M~L | +| **G8** | 配置与后台面板 | 配置体系完备:`app_config_entries(config_key, config_json JSONB)`(`001_init.sql:45-62`)为**唯一配置中心**,`app-config-store.ts:20/43` 读写 + 旧 JSON 文件回退,`APP_CONFIG_KEYS`(`app-config-keys.ts:1-15`)登记键,env 覆盖(`env-overrides.ts:61-101`),后台子路由(`routes/admin/platform-config.ts:13-19`),前端面板走 `/admin/platform-shops` 的 `?tab=`(`AdminPlatformShopsPage.tsx:72/85/164-203`,**新增 Tab 不用改路由与菜单**)。**凭据脱敏只有快手一条链路实现了**:`services/admin/platform-config/kuaishou-industry-service.ts:17 SECRET_FIELDS` + `:216 isMaskedSecretValue` + `:233 resolveSecretPatch`,配 `utils/masking.ts:10 maskSecret`(首6尾6);affiliate-dash 与 PDD 都是**明文回传前端**(`affiliate-dash-service.ts:17`、`routes/admin/platform-config/pdd.ts:92`) | 无配置 = 无法联调;照抄明文模式会把 secret 暴露给前端与审计日志 | `services/platforms/douyin-mashida/source-config-service.ts`、`routes/admin/platform-config/douyin-mashida.ts`、`services/admin/platform-config/douyin-mashida-service.ts`、前端 `services/admin/platform-config/douyin-mashida.ts` + `types/admin/platform-config/douyin-mashida.ts` + `AdminPlatformShopsPage.tsx` 加 Tab | M | +| **G9** | 对账/补偿(订单可能丢消息) | 现有兜底:PDD 轮询(`scheduler/pdd-order-sync-job.ts:11`,默认 20s);快手有券—工单绑定补全(`sync-work-orders-from-send-code.ts`) | 消息丢了就漏单 | `services/scheduler/douyin-mashida-order-sync-job.ts`(调 `order.searchList` 按时间窗对账) | M | +| **G10** | 成本与可观测性 | 平台按功能分级扣**金豆**:免费/基础 1/高级 2/增值 4(0.001~0.004 元/次)。现有设施:`logIntegration` / `logExternalHttpPacket`(`utils/logger.ts:96/122`,自带密钥脱敏正则 `:65-70`)、`task_events`(`task-event-repo.ts:16`)、后台待办 `admin_notifications`(`027_*.sql:2`,`dedupe_key` 唯一)+ SSE `publishRealtimeEvent`(`realtime-event-service.ts:92`)、`data-retention-job.ts:39-48` 定期清理报文 | 高频轮询烧钱;出问题无法定位;异常没人跟 | 对账窗口设宽 + 增量拉;低频接口本地缓存(`afterSale.rejectReasonCodeList`/`product.*`);**异常分流**:未匹配订单/密钥失效/回执连续失败 → `admin_notifications` + `publishRealtimeEvent`;出网日志复用 `logExternalHttpPacket` | S | +| **G11** | 环境变量登记(容易被忽略的硬约束) | `config/env-documentation.test.ts:63` 断言 `.env.server.example` / `.env.mac-docker.example` / compose 中出现的变量名**必须**在白名单 `KNOWN_ENV_NAMES`(`:57`)内 | 加了 env 不登记 → **测试直接红**,CI/hook 拦截推送 | 若走 env:`types/runtime-config.ts` + `config/defaults.ts:74` + `config/env-overrides.ts:24` + 两个 `.env*.example` + 白名单。**若走后台可改型(推荐)则完全绕开这个约束** | S | + +--- + +## 5. 必须拍板的设计决策 + +| # | 问题 | 选项 | 建议 | +| --- | --- | --- | --- | +| **D1** | 履约完成以什么为准? | ①打手提交资料即回执 ②验收通过才回执 ③发货动作完成即回执 | **②验收通过**(有留痕、可追责);充值类需要「充值平台返回值」作为凭证,建议引入「履约凭证」字段 | +| **D2** | 主键粒度:父订单 `p_id` 还是子订单 `s_ids`? | ①父单建 `orders`,子单建 `order_items` ②每个子单独立建单 | **①**,与现有 `orders`/`order_items` 结构一致;但 `topup.result` / `coupons.syncV2` 需要**子订单号**,回执时按 item → `s_id` 映射 | +| **D3** | 抖店 `completed`/`shipped` 状态怎么落? | ①扩 `ORDER_STATUS_PRIORITY` ②mapper 降级为 `paid` | **②先降级**(S 改动),长期 ① 统一修(PDD 也有同样坑) | +| **D4** | 卡券:复用快手券表还是新建表? | ①复用 `kuaishou_industry_vouchers` ②新建 `douyin_mashida_certificates` | **②新建**:语义(抖店券 vs 快手电子凭证)、字段(无 `oid/token`)、唯一键(`order_id+cert_no`)都不同;复用会污染既有状态机与重试逻辑 | +| **D5** | 消息订阅范围 | 14 条全订 vs 只订关键 7 条 | **先订 5 条**:`trade_TradePaid`、`trade_TradeCanceled`、`refund_RefundCreated`、`refund_RefundSuccess`、`trade_TradeSellerShip`;其余留观(**每条消息按分级扣金豆**) | +| **D6** | 买家隐私边界 | 抖店会给我们 `post_tel`/`post_receiver`/`doudian_open_id` | **必须遵守既有约定**:`doudian_open_id` 只留内部,不写进工单 `material`、不下发打手;充值账号单独用专用字段承载(现有项目已有「退库后不再带出上一单买家资料」等约束,见 commit `250c456a`/`eae78aae`) | +| **D7** | 密钥存储 | 现有 affiliate_dash 的 `appSecret`/`callbackSecret` 是**明文存在 `app_config_entries.config_json`**(`source-config-service.ts:16-24`,事实);**只有快手链路做了脱敏 + 只写不读**(`kuaishou-industry-service.ts:17/216/233` + `utils/masking.ts:10`) | **照抄快手模式**(出参 `secret:''` + `secretMasked` + `hasSecret`,掩码回写不覆盖真值),不要照抄 affiliate-dash 的明文回传;全库加密改造另立优化项 | +| **D8** | 命名约定(避免各写各的) | `provider` / config key / 目录名 / 路由前缀 / 前端 Tab key 需统一 | 建议:`provider = 'douyin_mashida'`、`APP_CONFIG_KEYS.douyinMashida = 'douyin_mashida'`、后端目录 `services/platforms/douyin-mashida/`、路由 `/api/v1/open/douyin-mashida`、前端 Tab key `douyinMashida`(**统一用 `douyin_mashida`,不要混用 `codespeed`/`masuda`/`doudian`**) | +| **D9** | 入站响应格式 | 现有三种并存:affiliate-dash `{code:0}`、91 `{message,code,data}`、快手行业 `result` 风格;内部管理端是 `buildSuccessPayload`(`utils/http.ts:21`,`{code,msg,time,data}`) | **按码速达规范返回**(文档只要求「返回成功即可」,实际以联调为准);**绝不要**套用内部 `buildSuccessPayload`;建议统一返回 HTTP 200 + `{code:0}` 之类的简单体,且失败时按 §5 D2 的策略决定是否 4xx 触发重推 | + +--- + +## 6. 分期实施建议 + +### Phase 0 · 联调准备(0.5~1 人日) +- 向平台方确认 §9 的 5 个阻塞项;拿到 `uid` + `secret` + 测试店铺。 +- 本地起一个 mock 推送服务,用 [03-消息推送规范与签名.md](03-消息推送规范与签名.md) 的签名算法造请求,验证我们的验签实现。 +- 交付物:`docs/抖音-码速达/05-接入开发设计.md`(对齐 `docs/91卡券/6.开发设计.md` 的写法)+ 联调清单。 + +### Phase 1 · 只读接入(2~3 人日) +- 新增 `routes/douyin-mashida.ts`(`POST /api/v1/open/douyin-mashida/message`,query + body 混合取值可参照 `routes/kuaishou-industry.ts:176 mergeRequestParams`)+ `notify-service.ts`:验签(**用 `req.rawBody` 原始串**,`app.ts:41`)+ `insertWebhookEventOnce` 去重(§3.5 的 event_id 方案)+ 原始报文落库。 +- 新增出网层 `services/platforms/douyin-mashida/{sign,http,openapi-client}.ts`(`AbortSignal.timeout` + `logExternalHttpPacket` + `createHttpError`,**不要**改 `affiliate-dash/http-client.ts`),实现 `order.orderDetail`。 +- `trade_TradePaid` → 拉详情 → `upsertOrderFromSource`(**先不派单**,观察数据质量)。 +- 后台配置:`app-config-keys.ts` 加 key → `routes/admin/platform-config/douyin-mashida.ts`(挂在 `routes/admin/platform-config.ts:14` 的 admin 守卫**之后**)+ 出参脱敏(照抄 `kuaishou-industry-service.ts`)+ `audit`(action `platform_douyin_mashida_config_updated`、targetType `platform_config`)+ 前端 `AdminPlatformShopsPage.tsx` 加 Tab(`:72 PlatformTab`、`:85 platformTabs`、`:164-203 Tabs items`、`:1541 normalizePlatformTab`)。 +- 验收:真实/模拟消息能在后台看到完整订单,字段与抖店后台一致;重复推送只处理一次。 + +### Phase 2 · 履约闭环(3~5 人日) +- 打通 G3(商品解析分支)+ G4(状态映射)+ 执行器注册,让订单能自动派单/人工兜底。 +- 建「码速达商品 ↔ 接单模板」的映射(可复用 `work_product_rules` 体系;`product.list`/`sku.list` 拉取、`code` 作 `skuCode`)。 +- 实现 `topup.result` 回执 + 重试 worker(照抄快手发码回调 worker 的退避策略与扫描机制)。 +- 验收:一笔真实小额充值订单全链路闭环(下单 → 派单 → 充值 → 回执 → 平台侧显示成功)。 + +### Phase 3 · 卡券能力(3~4 人日) +- 新表 + `coupons.syncV2`(券码生成与同步)、`coupons.verifyV3`(核销上报)、`coupons.list`(查)、`coupons.cancelVerify`(冲正)。 +- 与 91 卡券的差异对照测试:同一批券码在两条链路的幂等与状态一致性。 + +### Phase 4 · 售后与运维(3~5 人日) +- `order.searchList` 兜底对账任务(建议 5~15 分钟窗口、按 `update_time` 增量):新增 job 要改**三处** —— `services/scheduler/config-service.ts`(normalize + 默认任务 + job type 分派 `:103-121`)、`services/scheduler/scheduler-service.ts:194 dispatchJob`(新分支)、新建 `services/scheduler/douyin-mashida-order-sync-job.ts`(参照 `pdd-order-sync-job.ts:11`:分页上限、熔断跳过、店铺间隔 500ms、日志降噪)。手动触发可直接复用 `POST /api/v1/admin/platform-config/scheduled-jobs/:id/run`(`routes/admin/platform-config/notifications.ts:118`)。 +- 售后:`afterSale.operate` 回执 + `rejectReasonCodeList` 本地缓存 + 仲裁(`refund_ArbitrateApplied`)走 `admin_notifications` + `publishRealtimeEvent` 告警。 +- 后台:消息流水、回执失败重发(参照快手「手动重发」`resendKuaishouIndustryVoucherSendCallback:186` 与 PDD「暂存单重放」`/pdd/raw-orders/replay` 两种范式)、金豆消耗统计。 + +--- + +## 7. 测试方案 + +| 层 | 用例 | +| --- | --- | +| 签名单测 | 用文档里的占位秘钥/时间戳算出 `e4a08602d21966e99d2fa0921d7456b0`(订单详情)与 `1f4e1e6fc7e2ebad3368abfc2e07f2d1`(TradePaid 推送),断言字节级一致 — 这两条 golden vector 已写进 [接口API/01-订单详情.md](接口API/01-订单详情.md)、[消息推送/01-买家已付款.md](消息推送/01-买家已付款.md) | +| 消息入口 | 重复推送同一 `event_id`(§3.4 方案)→ 只处理一次;坏签名 → 4xx 且不落库;`data` 非法 JSON → 不 500;`pub_time` 超出时间窗 → 拒绝 | +| 状态映射 | 表驱动测 8 个 `order_status` × 是否售后 → 内部态;验证 `mergeSourceOrderState` 不回退 | +| 回执 | `topup.result` 失败重试退避与上限;重复回执幂等;平台返回 `code≠10000` 的处理 | +| 商品映射 | `code` / `out_sku_id` / `product_name` 三种键的匹配优先级与歧义处理 | +| 配置与合规 | 后台读配置不返回 secret(`hasSecret` + `Masked`);掩码回写不覆盖真值(照抄 `kuaishou-industry-service.test.ts:7/17`);审计 payload 无密钥;买家隐私不出现在 `work_orders.material_json` | +| 端到端 | 内网 mock 平台 + 沙箱店铺;覆盖「付款→履约→回执」与「付款→取消→终止履约」两条主线 | + +--- + +## 8. 风险清单 + +| 级别 | 风险 | 说明 | 缓解 | +| --- | --- | --- | --- | +| 🔴 高 | **业务角色未确认** | 我们到底是「抖店商家」还是「服务商代运营」?决定 uid/secret 归谁、订单归属谁 | Phase 0 必答(§9 Q1) | +| 🔴 高 | **消息不含商品明细** | 只靠 `trade_TradePaid` 无法建单,必须回调 `order.orderDetail`(高级功能,扣 2 金豆/次) | 进单即调一次详情并缓存;对账不重复调 | +| 🟠 中 | 出网回执丢失 | 平台不重试我们的回执(文档未说明) | 自建重试 worker + 后台可手动重发(照抄快手券回调 worker) | +| 🟠 中 | 状态语义错配 | 抖店 8 态 vs 内部 4 态;售后台与打手售后混淆 | mapper 层统一降级 + 表驱动单测;**绝不**把抖店售后写进 `worker_after_sales_cases` | +| 🟠 中 | 金豆成本 | 轮询 `order.searchList` + 每条消息都扣费 | 只订关键 5 条消息;对账窗口尽量宽、按 `update_time` 增量 | +| 🟡 低 | 密钥明文 | 沿用了现有 affiliates 的明文配置模式 | 中期加密/脱敏改造(独立优化项) | +| 🟡 低 | 命名冲突 | `provider` 若取 `douyin` 之类易与未来抖音原生开放平台混淆 | 见 §5 D8(统一 `douyin_mashida`) | +| 🟡 低 | 环境变量白名单测试 | `env-documentation.test.ts:63` + `KNOWN_ENV_NAMES:57`:**新增 env 不登记会让测试失败**,pre-push hook 直接拦 | 走「后台可改型」配置即可绕开;若必须走 env,四处一起改(见 G11) | +| 🟡 低 | 文档自身瑕疵 | `coupons.verifyV3` 的 `dop_path` 写成 `/coupons/verifyV2`(抓取原文如此);个别数值返回字段无示例值 | 已在本目录文档中标注,联调时以平台方为准 | + +### 8.1 必须遵守的既有约定(对接第三方时的「红线」) + +这些是仓库里已经成型、且在代码里有对应实现的约束,新平台接入**必须**沿用: + +| # | 约定 | 代码位置 | +| --- | --- | --- | +| 1 | 密钥只读不回显:出参 `secret: ''` + `secretMasked` + `hasSecret`,掩码回写**不得覆盖**真实值 | `kuaishou-industry-service.ts:113-136/216/233`、`utils/masking.ts:10` | +| 2 | 审计只记布尔摘要(`hasSecret`/`enabled`),**不落密钥** | `routes/admin/platform-config/kuaishou-industry.ts:46-53` | +| 3 | 日志强制脱敏(sign/token/cookie/authorization/card…) | `utils/logger.ts:65-70`、`:362`、`:407` | +| 4 | **买家隐私不进打手工单素材**:`work_orders.material_json.source` 里只有 orderId/sku/金额/店铺,没有 buyer/receiver | `sync-worker-orders-service.ts:206-220`(对照 `orders` 表可存 `buyer_name`/`receiver_contact`:`sync-work-orders-from-send-code.ts:285-286`) | +| 5 | 买家信息仅在「匹配成立」后对卖方/客服可见 | `consignment-worker-service.ts:469/509`、`consignment-admin-service.ts:218` | +| 6 | 卡密/核销码在后台出参掩码 | `kuaishou-industry-admin-service.ts:495-496/518`、`write/task-actions.ts:395 deliveredCredentialMasked` | +| 7 | 入站接口一律限流 | `routes/affiliate-dash.ts:10`、`routes/open-91.ts:10`、`routes/kuaishou-industry.ts:15` 等 | +| 8 | 幂等优先于「必成功」:重复回调 200 跳过、未匹配 404 让上游重推 | `affiliate-dash/notify-service.ts:70-88`、`:48-57` | +| 9 | 报文与事件有保留期,到期由 `data-retention-job` 清理 | `scheduler/data-retention-job.ts:21/39-48` + `*_RETENTION_DAYS` env | +| 10 | 回调返回体按**对方**规范,不要套内部 `buildSuccessPayload` | `utils/http.ts:21`(内部格式)对比 `routes/affiliate-dash.ts:45`(`{code:0}`) | + +> 对码速达的直接含义:`doudian_open_id`、`post_tel`、`post_receiver`、`buyer_words`(常含充值账号) +> 只能进 `orders`/原始报文,**不得**下发到打手工单素材;券码(`cert_no`)在后台展示要掩码。 + +--- + +## 9. 待确认清单 + +**给平台方(码速达)** +1. `uid`(抖店商户号)与用户秘钥如何分配?我们属于**商家**还是**服务商**?一个 uid 能否绑定多店铺? +2. 消息推送**失败重试策略**是什么?是否要求我们在 N 秒内返回 200?超时/失败会重推几次? +3. `order_status=5`(已完成)与售后状态,平台期望我们在什么时机做最终回执? +4. 充值类订单的**买家账号**标准取值位置(`buyer_words`?商品 SKU 字段?还是 `order.orderDetail` 的某字段)? +5. `topup.result` 的 `topup_biz` 取值枚举有哪些(示例只给了 `MOBILE_TOPUP`)? +6. 测试环境/沙箱店铺是否可用?`order.message` 测试接口是否对我们这类商户开放(文档写「仅支持特定商户」)? + +**给业务方** +7. 抖店这条线是**现有业务的延伸**(用同一批打手/同一套接单模板)还是**独立品类**?决定商品映射与履约通道。 +8. 是否仍要给买家生成 `claimUrl`(去使用链接)?`topup.result` 的 `url` 字段要不要填我们的领取页? +9. 售后(拒绝/同意退款)由**客服在后台点**还是自动按规则回执? + +--- + +## 10. 附录:本次分析的关键代码索引 + +| 主题 | 文件(相对仓库根) | 关键行 | +| --- | --- | --- | +| 进单归一化 | `apps/backend/src/services/order/order-service.ts` | 41-57(DTO)、98(upsert)、124-127(advisory lock)、432-476(状态合并) | +| 入站 webhook 模板 | `routes/affiliate-dash.ts:16`、`services/platforms/affiliate-dash/notify-service.ts:21-120` | 验签→查任务→去重→事件→同步 | +| 幂等闸门 | `repositories/webhook-event-repo.ts:9-43`(表 `webhook_events`,迁移 `013_*`) | `(provider,event_id)` 唯一 | +| 执行器契约/注册 | `services/fulfillment/executors/types.ts:53-57`、`registry.ts:15-55` | | +| 履约解析硬编码 | `services/fulfillment/product-resolution-service.ts:165/175/214-225/346` | 仅 91 来源 | +| 路由白名单 | `services/fulfillment/routing-config-service.ts:64-69`、`:132`、`:470` | | +| 券码状态机/重试 | `services/platforms/kuaishou-industry/voucher-service.ts:500`、`send-code-service.ts:166/400/456` | 退避 ≤8 次 | +| 出网 HTTP | `services/platforms/affiliate-dash/http-client.ts:14`、`kuaishou-industry/http.ts:4` | 超时/错误码归一 | +| 配置体系 | `services/config/app-config-store.ts:20/43`、`config/app-config-keys.ts:1-15`、`config/env-overrides.ts:61-101`、表 `app_config_entries`(`001_init.sql:45-62`) | DB + env 双通道;**明文 JSONB** | +| 凭据脱敏(唯一正确范本) | `services/admin/platform-config/kuaishou-industry-service.ts:17/113-136/216/233`、`utils/masking.ts:10` | 置空 + Masked + hasSecret,掩码回写不覆盖 | +| 审计 | `routes/admin/session.ts:30/138`、`repositories/admin-audit-log-repo.ts:42`、表 `admin_audit_logs`(`001_init.sql:23-33`) | action `platform__`、targetType `platform_config` | +| 后台面板(前端) | `pages/admin/platform/AdminPlatformShopsPage.tsx:72/85/164-203/1541`、`services/admin/platform-config/*`、`types/admin/platform-config/*` | `?tab=` 内部 Tab,不用改路由/菜单 | +| 调度 | `services/scheduler/scheduler-service.ts:13/51/194`、`config-service.ts:103-121`、`pdd-order-sync-job.ts:11` | job type 三处登记;手动触发路由 `notifications.ts:118` | +| 可观测性 | `utils/logger.ts:96/122`、`repositories/task-event-repo.ts:16`、`repositories/admin-notification-repo.ts:25`、`services/realtime/realtime-event-service.ts:92` | 日志 / 任务事件 / 后台待办 + SSE | +| 环境变量白名单(硬约束) | `config/env-documentation.test.ts:57/63`、`.env.server.example`、`.env.mac-docker.example` | 新增 env 必须登记,否则测试红 | +| 隐私红线 | `services/worker-platform/sync-worker-orders-service.ts:206-220` | 打手工单素材不含买家信息 | +| 平台接入设计范本 | `docs/91卡券/6.开发设计.md`(来源建模/配置/签名/代码落点/测试范围)、`docs/履约配置/多发货平台与电子凭证整体链路.md` | 本项目的「新平台接入」方法论 | + +--- + +## 11. 下一步(建议按序执行) + +1. **确认 §9 的 Q1/Q2/Q4**(阻塞项,拿到答案才能定协议细节与数据模型)。 +2. 写 `docs/抖音-码速达/05-接入开发设计.md`(对齐 `docs/91卡券/6.开发设计.md`:来源建模 → 路由 → 配置 → 签名 → 下单 → 回执 → 商品匹配 → 代码落点 → 测试范围)。 +3. 从 **G1 + G2**(签名 + 消息入口)开工 —— 这两块不依赖任何业务口径,且是后续一切联调的前提。 diff --git a/docs/抖音-码速达/README.md b/docs/抖音-码速达/README.md index af72d88b..48dd0b15 100644 --- a/docs/抖音-码速达/README.md +++ b/docs/抖音-码速达/README.md @@ -29,6 +29,7 @@ | [01-计费规则.md](01-计费规则.md) | 免费/基础/高级/增值 四档功能单价与各接口分级 | | [02-接口调用规范与签名.md](02-接口调用规范与签名.md) | 网关、公共参数、签名算法、返回结构、JAVA/.NET/PHP 请求示例 | | [03-消息推送规范与签名.md](03-消息推送规范与签名.md) | 推送参数、data 提交方式、签名与验签、消息设置、示例代码 | +| [04-结合本项目的深度分析.md](04-结合本项目的深度分析.md) | **接入本仓库(order_site)的深度分析**:能力映射、差距清单、待拍板决策、分期实施、风险与合规红线 | | `接口API/` | 16 个接口逐条文档(请求参数、返回参数与示例、签名原串示例) | | `消息推送/` | 14 条消息逐条文档(topic、data 字段、示例 JSON、签名原串示例) | | `data/raw_data.json` | 页面内嵌原始数据(api / message / fee / menu 四块,完整未删减) |