diff --git a/docs/抖音-码速达/05-最小接入方案分析.md b/docs/抖音-码速达/05-最小接入方案分析.md new file mode 100644 index 00000000..4e30563a --- /dev/null +++ b/docs/抖音-码速达/05-最小接入方案分析.md @@ -0,0 +1,389 @@ +# 抖音码速达 · 最小接入方案分析 + +> 目标:**用最小代价跑通一条真实闭环**(收单 → 发券 → 用户兑换 → 回执核销),先不做对账轮询、售后、商品映射后台、充值回执。 +> 协议依据:[README.md](README.md)、[02-接口调用规范与签名.md](02-接口调用规范与签名.md)、[03-消息推送规范与签名.md](03-消息推送规范与签名.md)、[接口API/](接口API)。 +> 本仓库依据:[04-结合本项目的深度分析.md](04-结合本项目的深度分析.md)(下称「深度分析」)。 +> 日期:2026-10-08 | 分支:`dev` + +--- + +## 0. 结论速览 + +**最小闭环成立**,且比标准方案少掉约 2/3 的工作量:不需要 `topup.result`、不需要商品映射后台、不需要对账轮询、不需要售后回执、不需要新建履约执行器(**人工兜底即可**)。 + +四步闭环: + +```text +① 收单 码速达推 trade_TradePaid → 我们调 order.orderDetail 补全商品/买家/金额 → 建单 +② 发券 本地生成 DYC 券码 → 调 coupons.syncV2 把(券码 + 固定兑换链接)交给平台 +③ 兑换 买家打开【固定链接】→ 输入订单号 → 校验状态 → 兑换(发货/展示卡密) +④ 回执 调 coupons.verifyV3 上报核销 → 调 order.message 把「去使用链接」发给买家 +``` + +**必须先解决/确认的 3 件事**(否则最小方案会翻车): + +| # | 问题 | 影响 | 处理 | +| --- | --- | --- | --- | +| 1 | `order.message` 文档写明「**测试接口,仅支持特定商户**」 | 第④步发不了链接 | **最高优先级找平台方确认**;退路是 `topup.result` 的 `url`(去使用链接)字段 | +| 2 | 固定链接 + 订单号 = **知道订单号就能兑换** | 被盗领 | 6 层防护(§3.2)+ 一次性核销;业务若不接受残余风险,加「手机号后 4 位」 | +| 3 | 退款/取消后券码还活着 | 资损 + 客诉 | **最小集里必须带上** `refund_RefundSuccess` → 券码作废(成本极低,见 §6) | + +> ⚠️ 术语纠正:平台**只有** `coupons.syncV2`(卡券同步)和 `coupons.verifyV3`(卡券核销),**没有 syncV3**。 +> 需求里说的「卡券同步 / 卡券同步v3」应理解为「同步用 V2、核销用 V3」。 + +--- + +## 1. 最小闭环时序(每步的接口与关键参数) + +```text +抖店买家付款 + │ + │ ① trade_TradePaid(topic + pub_time + sign 走 URL,data 走 POST body) + ▼ +码速达 ──────────────────────────► 我们 /api/v1/open/douyin-mashida/message + │ 验签 → 幂等去重 → 解析 + │ 只有 p_id / s_ids / pay_amount,没有商品 + ▼ + order.orderDetail(shop_order_id = p_id) + │ 拿到 sku_order_list[](子订单号/商品名/编码/数量) + │ + post_tel / post_receiver / buyer_words + ▼ + 建单(orders + order_items,复用 upsertOrderFromSource) + 建履约任务(人工兜底:manual_dispatch) + 生成 DYC 券码(表:douyin_mashida_certificates) + │ + │ ② coupons.syncV2(order_id = 子订单号, + │ cert_list = [{cert_no: "DYC…", + │ cert_link: 固定兑换链接, + │ grant_time}]) + ▼ + 平台把卡券展示给买家 + │ + │ ③ 买家点卡券 → 打开【固定兑换链接】 + │ https://<域名>/#/redeem + ▼ + 输入订单号 → 查状态 → 兑换(发货/展示卡密) + │ + │ ④ coupons.verifyV3(cert_no) ← 上报核销(幂等) + │ order.message(order_id, content=去使用链接, message_type=2) + ▼ + 平台把「去使用链接」推给买家 +``` + +**每一步的对外接口**(全部走同一个网关 `POST /api/openplatform/gateway`,公共参数 `uid/method/timestamp/sign`): + +| 步骤 | method | 关键参数 | 备注 | +| --- | --- | --- | --- | +| ① | `order.orderDetail` | `shop_order_id`(父订单号) | 高级功能(2 金豆/次) | +| ② | `coupons.syncV2` | `order_id`(**子订单ID**)、`cert_list`(Json 数组) | `cert_link` ≤ 255 字符 | +| ④ | `coupons.verifyV3` | `cert_no` | 只认券码,**不带订单号** → 券码必须能反查订单 | +| ④ | `order.message` | `order_id`、`content`、`message_type`(1/2/4/5) | **测试接口,需确认可用性** | +| 兜底 | `coupons.cancelVerify` | `cert_no`、`batch_no`(幂等号)、`cancel_verify_count` | 发错/误核销时冲正 | + +--- + +## 2. 四步拆解 + +### 2.1 收单:消息 + 订单详情 + +**事实**:`trade_TradePaid` 的 data 只有 `pay_amount / s_ids / order_status / order_type / pay_time / p_id / shop_id / pay_type / biz` —— **没有商品、没有买家账号**。 +所以必须再调一次 `order.orderDetail`(深度分析 §2.2 已论证这是与 91 卡券最大的差异)。 + +最小字段映射: + +| 来源 | 字段 | 落到哪 | +| --- | --- | --- | +| 消息 | `p_id` | `orders.platform_order_id`(**按父单建 order**) | +| 消息 | `pay_amount`(分) | `orders.total_amount` | +| 消息 | `pay_time`(秒) | `orders.paid_at` | +| 消息 | `order_status=2` | `orderStatus=paid` / `payStatus=paid`(深度分析 §3.5 映射表) | +| 详情 | `sku_order_list[].order_id` | `order_items` 的**子订单号**(②④都要用,务必存下来) | +| 详情 | `sku_order_list[].product_name` / `code` / `out_sku_id` / `item_num` | `order_items.sku_name` / `sku_code` / 数量 | +| 详情 | `post_tel` / `buyer_words` | **只进 orders / 原始报文**(深度分析 §8.1 红线:不进打手工单素材) | + +**关键动作**:把**子订单号 `s_id`** 持久化到 `order_items`(或 `orders.raw_payload_json`,但强烈建议落列/落 snapshot,因为后面 syncV2 与 order.message 都要它)。 + +> 若一次回调里 `s_ids` 有多个(多商品订单):最小版建议**每个子单一条 order_item**,每个子单**各发一张券**(`cert_list` 支持数组)。 + +### 2.2 发券:DYC 券码 + `coupons.syncV2` + +**券码规则**(照抄现有快手券码生成器:`repositories/kuaishou-industry-voucher-repo.ts:26/27/49-52` 用 10 字节随机 → Crockford Base32,`:546 encodeCrockfordBase32`,前缀 `KSV` 改 `DYC`): + +```text +DYC + 例:DYC7K3M9QF2XR5TB +``` + +- 全局唯一:`cert_no` 建 UNIQUE(现有快手表同样做法:`voucher_code UNIQUE`)。 +- 券码本身不带订单信息(`verifyV3` 只回传 `cert_no`,所以必须靠本地表反查)。 + +**同步参数**: + +```json +{ + "order_id": "<子订单号 s_id>", + "cert_list": "[{\"cert_no\":\"DYC…\",\"cert_link\":\"https://<域名>/#/redeem\",\"grant_time\":\"2026-10-08 15:30:00\"}]" +} +``` + +- `cert_list` 要**以 JSON 字符串**作为单个参数值提交(与文档示例一致)。 +- `cert_link` 就是**我们的固定兑换链接**,长度上限 255(固定常量,天然满足)。 +- `grant_time` 用发券当时的本地时间(`YYYY-MM-DD HH:MM:SS`)。 + +**幂等**:以「子订单号」为键,一个子订单只同步一次券(表上加 `UNIQUE(order_id, cert_no)` 或状态标记 `sync_status`),失败可重试、成功不重发。 + +### 2.3 固定兑换链接 + 订单号闸门 ★ 本次核心变化 + +**需求**:一个固定链接,用户输入订单号,按订单状态决定下一步。 + +**与旧 token 链接的对比**: + +| 维度 | 旧:`/claim/:token`(现状) | 新:固定链接 + 订单号 | +| --- | --- | --- | +| 链接 | 每单一个(token 24 位随机) | **全平台一个常量** | +| 凭据 | token 即凭证(`claim_tokens.status/expired_at/max_use_count`) | **订单号 + 服务端状态校验** | +| 发链接成本 | 建单时生成 token、写 `claim_tokens`、随 91 查询接口下发 | 建单即可发(甚至可写死在卡券 `cert_link` 里) | +| 领错人风险 | 低(token 不可猜) | **中**(订单号可枚举,见 §3.2) | +| 复用 | `buildClaimUrl` / `resolveClaimTokenExpiration`(`claim-service.ts:11-52`) | 复用 **`/collect` 的「输入订单号 → 查状态 → 下一步」模式** | + +**可复用的现成资产(重要)**:仓库里已经有一个**无 token、按订单号查询的公开页**: + +| 资产 | 位置 | 说明 | +| --- | --- | --- | +| 公开路由 + 按订单号限流 | `routes/collect.ts:11-27` | `POST /api/v1/collect/lookup`,`key: getBodyFieldRateLimitKey('orderNo')`,60 次/分钟 | +| 查询服务(含状态门禁) | `services/worker-platform/worker-collect-service.ts:22` | 只查「待完善」状态的工单,查不到就 404 | +| 对外最小投影 | `work-order-display-mappers.ts:514 mapWorkOrderPublic` | **只暴露 5 个字段**(工单号/订单号/商品名/状态)—— 隐私边界范本 | +| 前端两步式页面 | `pages/collect/CollectPage.tsx`(186 行) | 「输入订单号 → 查询 → 按结果渲染下一步」,可直接照抄骨架 | +| 短链(可选) | `routes/short-links.ts`、`buildShortLinkUrl` | 若想把兑换链接再缩短,可复用(注意该路由注释里写的是「历史短链只读」) | + +**建议的固定链接**:`https:///#/redeem`(与 `/collect` 同级的前端路由,无需 token)。 + +**建议的接口**(与 collect 完全同型): + +```text +POST /api/v1/redeem/lookup { orderNo } → 订单摘要 + 下一步动作 +POST /api/v1/redeem/confirm { orderNo } → 执行兑换 + 上报核销 + 返回结果 +``` + +**为什么分两步**:第一步只返回**脱敏摘要**,让用户确认「这是我的订单」;确认后才执行兑换(防误领、也防脚本直接打 confirm)。 + +### 2.4 回执:`coupons.verifyV3` + `order.message` + +**核销时机(需要拍板)**:建议 **「我们的履约动作确认成功之后」** 才调 `verifyV3`—— + +- 若兑换页直接返回卡密/链接 → 展示成功即核销; +- 若兑换后还要人工处理(人工兜底)→ 兑换页只置「处理中」,等后台确认发货后再核销。 + +**理由**:`verifyV3` 上报后平台侧显示「已核销」,如果这时我们还没真正发货,用户会拿着「已核销」来投诉。发错了还有 `coupons.cancelVerify`(支持 `batch_no` 幂等号)可以冲正。 + +**发链接**: + +```json +{ "order_id": "<子订单号>", "content": "您的订单已核销,请点击链接使用:https://…", "message_type": 2 } +``` + +- `message_type`:1-订单状态,2-订单发货,4-图片,5-视频 → 建议 `2`(发货语义最贴)。 +- ⚠️ **该接口文档标注「测试接口,仅支持特定商户」**,必须确认;若不可用 → 退路是 `topup.result` 的 `url` / `url_type` 字段(去使用链接)承载同样的链接。 + +--- + +## 3. 固定兑换链接专项分析 + +### 3.1 状态分流表(输入订单号之后「下一步」怎么判) + +| 订单号查询结果 | 页面提示 | 下一步 | 是否调 verifyV3 | +| --- | --- | --- | --- | +| 查不到(未同步/输错) | 「订单号有误,或订单尚未同步,请 1 分钟后再试」 | 停留输入页 | 否 | +| 未支付 / 已取消 | 「该订单尚未支付」/「订单已取消」 | 不可兑换 | 否 | +| 已退款 | 「订单已退款,无法兑换」 | 不可兑换 + **券码作废** | 否 | +| 已付款、未发券 | 「正在为该订单发券,请稍后再试」 | 重试(后台补偿发券) | 否 | +| 已付款、已发券、未核销 | 展示**脱敏摘要**(商品名/金额/下单时间) | 「确认兑换」按钮 | 确认后调 | +| 已核销 | 展示上次兑换结果(卡密/链接)或「已兑换」 | 不能重复兑换 | 否(幂等) | +| 已发货未核销(人工兜底场景) | 「处理中,请稍后查看」 | 轮询 | 否 | + +### 3.2 安全分析:订单号可枚举,必须分层防护 + +**残余风险(必须写清)**:知道**别人的订单号**即可兑换 → 盗领。抖店订单号是 18~19 位数字,虽然不可穷举,但订单号会出现在**买家聊天记录、截图、客服工单**里,泄露面不小。 + +6 层防护(按成本从低到高,建议前 5 层在最小版就做): + +| 层 | 措施 | 落点 | +| --- | --- | --- | +| 1 | **接口限流**:按 `orderNo` + IP 双维度(复用 `getBodyFieldRateLimitKey('orderNo')`) | `middleware/rate-limit.ts:31`、`routes/collect.ts:11-17` | +| 2 | **状态门禁**:只允许「已付款 + 已发券 + 未核销」三种前置状态进入兑换 | 查询服务(同 `worker-collect-service.ts:22` 的写法) | +| 3 | **两步确认**:lookup 只给脱敏摘要,confirm 才执行(脚本无法一步打穿) | `POST /redeem/lookup` / `/confirm` | +| 4 | **一次性核销**:DB 唯一约束 + 状态机(`ISSUED → VERIFIED`),重复请求幂等返回原结果 | 券表 `cert_no UNIQUE` + 状态字段 | +| 5 | **访问留痕**:记录 IP/UA/订单号到 `webhook_events` 或独立表;同 IP 高频查不同订单号 → `admin_notifications` 告警 | `repositories/admin-notification-repo.ts:25` + `publishRealtimeEvent` | +| 6 | **二次身份校验**(可选):订单号 + 手机号后 4 位 / 下单日期 | 需确认 `order.orderDetail.post_tel` 在虚拟商品场景是否**真的有值**(很可能被充值账号占用,见待确认 Q3) | + +**如果业务不接受残余风险**:第 6 层必须做,但会牺牲「只输订单号」的体验 → 与需求冲突,需要业务拍板。 + +### 3.3 页面交互(3 屏) + +```text +屏1 输入订单号 [订单号 __________] [查询] +屏2 确认 商品:XX会员月卡 金额:¥19.90 下单:2026-10-08 15:20 + [确认兑换] [不是我的订单] +屏3 结果 ✅ 兑换成功 → 卡密:XXXX-XXXX / 去使用链接 + 或 ⏳ 处理中,请稍后刷新 +``` + +- 屏 2 的摘要要**脱敏**:商品名可全显(买家自己知道),**不要**显示买家手机号/地址/open_id。 +- 屏 3 的卡密按深度分析 §8.1 第 6 条做**掩码 + 复制按钮**,不要明文铺在页面上被截图外传。 + +--- + +## 4. 最小数据模型 + +只新增 **1 张表**(其余全部复用): + +```sql +-- 094_douyin_mashida_certificates.sql +CREATE TABLE IF NOT EXISTS douyin_mashida_certificates ( + id BIGSERIAL PRIMARY KEY, + cert_no TEXT NOT NULL UNIQUE, -- DYC + Base32 + order_id TEXT NOT NULL, -- 抖店子订单号 s_id(syncV2 的 order_id) + parent_order_id TEXT NOT NULL, -- 抖店父订单号 p_id + order_item_id BIGINT, -- 关联 order_items.id(可空,最小版可只存 order_id) + cert_link TEXT NOT NULL, -- 固定兑换链接 + status TEXT NOT NULL DEFAULT 'issued', -- issued / synced / verified / invalid + sync_status TEXT NOT NULL DEFAULT 'pending', -- pending / success / failed + sync_error TEXT, + grant_time TIMESTAMPTZ, + synced_at TIMESTAMPTZ, + verified_at TIMESTAMPTZ, + raw_payload_json JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_dm_cert_order ON douyin_mashida_certificates(order_id, cert_no); +``` + +- **幂等 / 核销**:`cert_no UNIQUE` + `status` 状态机;`order_id` 唯一到子订单(一个子单一张券)。 +- **复用**:`orders` / `order_items` / `fulfillment_tasks` / `webhook_events`(消息幂等)/ `app_config_entries`(凭据配置)。 +- 数量多的商品(`item_num > 1`):最小版建议**只发 1 张券**(整单核销),避免券码数量与兑换次数对不上;要按件发券则二期再说。 + +--- + +## 5. 代码落点(严格最小集) + +> 命名统一按深度分析 §5 D8:**`douyin_mashida`**。 + +**新增(后端)** + +| 文件 | 作用 | 参照 | +| --- | --- | --- | +| `services/platforms/douyin-mashida/sign.ts` | 两套 MD5 小写签名(出网 + 推送验签) | 独立实现,**不能复用** `open-91/crypto.ts`(大写且带 `=`) | +| `services/platforms/douyin-mashida/http-client.ts` | 网关 POST(超时 + 报文日志) | `kuaishou-industry/http.ts:4` | +| `services/platforms/douyin-mashida/gateway-client.ts` | `orderDetail` / `syncV2` / `verifyV3` / `orderMessage` 四个方法 | `kuaishou-industry/openapi-client.ts:33` | +| `services/platforms/douyin-mashida/source-config-service.ts` | `uid` / `secret` / `certLink` / `enabled` 配置 | `affiliate-dash/source-config-service.ts` | +| `services/platforms/douyin-mashida/notify-service.ts` | 推送验签 + 幂等 + 分派(只处理 `trade_TradePaid`、`refund_RefundSuccess`) | `affiliate-dash/notify-service.ts:21-120` | +| `services/platforms/douyin-mashida/order-mapper.ts` | 消息 + 详情 → `SourceOrderEvent` | `pdd/pdd-order-mapper.ts:135` | +| `services/platforms/douyin-mashida/certificate-service.ts` | 生成 DYC 券码 / 同步 / 核销 / 作废 | `kuaishou-industry/voucher-service.ts` | +| `services/redeem/redeem-service.ts` | 订单号查询 + 状态分流 + 兑换执行 | `worker-collect-service.ts:22` | +| `repositories/douyin-mashida-certificate-repo.ts` | 券表读写 | `kuaishou-industry-voucher-repo.ts` | +| `routes/douyin-mashida.ts` | 推送入口 | `routes/affiliate-dash.ts:16` | +| `routes/redeem.ts` | `/lookup` `/confirm` | `routes/collect.ts` | +| `db/migrations/094_douyin_mashida_certificates.sql` | 券表 | 迁移规范:三位序号 + `IF NOT EXISTS` + `COMMENT ON` | +| `services/admin/platform-config/douyin-mashida-service.ts` + `routes/admin/platform-config/douyin-mashida.ts` | 后台配置(**带脱敏**) | `kuaishou-industry-service.ts:17/216/233` | + +**修改(后端,按依赖顺序)** + +| 文件 | 改动 | +| --- | --- | +| `config/app-config-keys.ts:1-15` | 加 `douyinMashida: 'douyin_mashida'` | +| `app.ts:102-111` | 挂 `/api/v1/open/douyin-mashida` + `/api/v1/redeem` | +| `services/admin/platform-config/...` | 按现有模式注册(admin 守卫之后) | +| `services/fulfillment/product-resolution-service.ts:165/346` | ⚠️ **必须加分支**:否则订单 `isConfigured=false` 永远不建任务(深度分析 G3)。最小版直接给「人工兜底」候选即可 | + +**新增(前端)** + +| 文件 | 作用 | +| --- | --- | +| `pages/redeem/RedeemPage.tsx` | 固定兑换页(照抄 `CollectPage.tsx` 骨架:输入 → 查询 → 结果) | +| `services/redeem.ts` | `lookupRedeemOrder` / `confirmRedeemOrder` | +| `App.tsx` | 加 `} />` | +| `services/admin/platform-config/douyin-mashida.ts` + `types/...` + `AdminPlatformShopsPage.tsx` Tab | 后台配置面板 | + +**明确不做(最小版边界)**:`topup.result`、`order.searchList` 对账轮询、`afterSale.*` 售后、`product.*/sku.list` 商品管理、`coupons.list` 券码查询、`coupons.cancelVerify`(先留接口不接后台)、`message.config`(手动在平台后台配推送地址即可)、商品映射后台(先按商品名/编码人工对一次)、定时任务、多商品拆分。 + +--- + +## 6. 最小版必须带上的一件事:退款作废 + +虽然最小版「不做售后」,但**必须**订一条消息:`refund_RefundSuccess`(退款成功)。 + +- 收到后:订单置 `refunded`、**券码置 `invalid`**、兑换页显示「订单已退款,无法兑换」。 +- 成本:一个分支 + 一次 UPDATE(**不到 0.5 人日**)。 +- 不做的话:买家退款之后**仍然能拿券兑换** → 直接资损,而且这类单一定会有。 + +另外建议顺带订上(同样便宜):`trade_TradeCanceled`(订单取消)→ 同上作废。其余 11 条消息**先不订**(每条都扣金豆)。 + +--- + +## 7. 与「后续对接抖音官方」的兼容设计 + +需求里提到「后面需要对接抖音官方的」,最小版要留好接口,避免返工: + +| 设计点 | 现在的做法(兼容官方) | +| --- | --- | +| 命名 | `provider = 'douyin_mashida'`(**通道**)、`platform = 'doudian'`(**业务平台**)。以后直连官方只需改 provider,业务语义不动 | +| 出网层 | `gateway-client.ts` 只暴露 `orderDetail / syncV2 / verifyV3 / orderMessage` 四个**业务方法**,URL/签名在内部;以后换成官方网关只改这一个文件 | +| 券码 | `DYC` 前缀 + 独立券表;官方接入后券码规则不变 | +| 订单来源 | 走标准 `upsertOrderFromSource`,官方推送/轮询都只是换一个 mapper | +| 配置 | `app_config_entries` 存 `uid/secret/baseUrl`;官方通道只需再加一个配置键 | + +--- + +## 8. 风险与待确认 + +**待确认(阻塞)** + +| # | 问题 | 问谁 | +| --- | --- | --- | +| Q1 | `order.message`(订单消息测试)我们的商户号**能不能用**?`message_type` 语义(2-发货消息会不会触发抖店发货状态?) | 平台方 | +| Q2 | `coupons.syncV2` 的 `order_id` 确认是**子订单号**(`s_id`)吗?多个子单能否共用一个 `cert_list`? | 平台方 | +| Q3 | `order.orderDetail.post_tel` 在虚拟/卡券商品场景**是否有值**(决定能不能做「订单号 + 手机号后4位」二次校验) | 平台方 | +| Q4 | `coupons.verifyV3` 的核销是否**不可逆**(除 `cancelVerify`)?有无核销时效要求 | 平台方 | +| Q5 | 券码格式有无限制(长度/字符集/是否必须数字)? | 平台方 | +| Q6 | 业务能否接受「**知道订单号即可兑换**」的残余风险(§3.2)? | 业务方 | +| Q7 | 兑换成功的判定:页面展示卡密就算成功,还是必须等人工发货完成(决定 verifyV3 时机,§2.4) | 业务方 | + +**技术风险** + +| 级别 | 风险 | 缓解 | +| --- | --- | --- | +| 🔴 高 | 固定链接被盗刷/枚举 | §3.2 前 5 层 + 告警;业务不接受则加二次校验 | +| 🟠 中 | `order.message` 不可用 | 退路:`topup.result.url` | +| 🟠 中 | 发券失败(syncV2 报错)导致用户看不到卡券 | `sync_status` + 后台手动重发按钮;错误写 `admin_notifications` | +| 🟠 中 | 消息丢失导致漏单 | 最小版**先接受**(人工按订单号补单);二期加 `order.searchList` 对账 | +| 🟡 低 | 券码枚举(DYC+16 位 Base32 = 32^16,实际不可枚举) | 唯一约束 + 查询限流 | + +--- + +## 9. 联调与验收步骤 + +1. **本地 mock 平台**:用 [03-消息推送规范与签名.md](03-消息推送规范与签名.md) 的算法造 `trade_TradePaid`,验证验签 + 幂等(同一单推 3 次只处理 1 次)。 +2. **真单验证收单**:拿一笔真实小额订单,核对 `order.orderDetail` 落库字段(子订单号、商品名、金额)与抖店后台一致。 +3. **验证发券**:调 `coupons.syncV2` 后在抖店订单里确认能看到卡券 + `cert_link` 跳转正常(**注意:这步要确认卡券是否真展示给买家**)。 +4. **验证兑换**:打开固定链接 → 输订单号 → 确认 → 拿到结果;重复兑换 → 返回原结果不重复发放。 +5. **验证回执**:`verifyV3` 后平台侧显示已核销;`order.message` 后买家能收到消息。 +6. **验证作废**:对一笔已发券订单发起退款 → 券变 `invalid` → 兑换页正确拒绝。 +7. **验收口径**:一笔真实订单从付款到「买家收到链接」全程 ≤ 1 分钟(人工兜底场景除外)。 + +--- + +## 10. 工作量估算(最小版) + +| 模块 | 规模 | +| --- | --- | +| 签名 + 出网客户端(4 个方法) | M(1.5~2 人日) | +| 推送入口 + 验签 + 幂等 + mapper | M(1.5 人日) | +| 券码生成 + syncV2 + 券表 | M(1 人日) | +| 兑换页 + `/redeem` 接口 + 状态分流 | M(1.5 人日) | +| verifyV3 + order.message + 幂等 | S(0.5 人日) | +| 退款作废 | S(0.5 人日) | +| 后台配置(脱敏)+ 测试推送 | M(1 人日) | +| 联调与修 bug | M(1~2 人日) | +| **合计** | **约 8~10 人日** | + +> 相比深度分析里「进单+回执 6~10 人日 + 卡券/售后/面板 8~12 人日」的标准方案,最小版**砍掉了约一半**:省掉 `topup.result`、商品映射后台、对账任务、售后回执、`coupons.list/cancelVerify` 的后台接入、多商品拆券。 diff --git a/docs/抖音-码速达/README.md b/docs/抖音-码速达/README.md index 48dd0b15..2e0898de 100644 --- a/docs/抖音-码速达/README.md +++ b/docs/抖音-码速达/README.md @@ -30,6 +30,7 @@ | [02-接口调用规范与签名.md](02-接口调用规范与签名.md) | 网关、公共参数、签名算法、返回结构、JAVA/.NET/PHP 请求示例 | | [03-消息推送规范与签名.md](03-消息推送规范与签名.md) | 推送参数、data 提交方式、签名与验签、消息设置、示例代码 | | [04-结合本项目的深度分析.md](04-结合本项目的深度分析.md) | **接入本仓库(order_site)的深度分析**:能力映射、差距清单、待拍板决策、分期实施、风险与合规红线 | +| [05-最小接入方案分析.md](05-最小接入方案分析.md) | **最小接入(跑通优先)方案**:只做 收单→发券→固定链接兑换→核销发链接,含固定兑换链接的安全性分析 | | `接口API/` | 16 个接口逐条文档(请求参数、返回参数与示例、签名原串示例) | | `消息推送/` | 14 条消息逐条文档(topic、data 字段、示例 JSON、签名原串示例) | | `data/raw_data.json` | 页面内嵌原始数据(api / message / fee / menu 四块,完整未删减) |