diff --git a/docs/抖音-码速达/00-总览.md b/docs/抖音-码速达/00-总览.md index 3bd032a9..55e8469c 100644 --- a/docs/抖音-码速达/00-总览.md +++ b/docs/抖音-码速达/00-总览.md @@ -1,6 +1,6 @@ # 抖音码速达 · 总览(进度 / 代码 / 配置 / 上线) -> 更新:2026-10-09 **状态:最小接入已实现完毕(含后台配置面板 + ngrok 公网联调工具链),两端自检全绿,已在真实公网与真实网关上验证,代码尚未提交(等确认)** +> 更新:2026-10-09 **状态:最小接入已实现完毕,且已在真实平台跑通**(真实网关签名验证 / 真实推送订阅 / 真实 orderDetail 建单 / 真实 coupons.syncV2 同步成功),两端自检全绿 > 本文是这套内容的**入口**,具体细节在 04~09。 --- @@ -15,7 +15,8 @@ 1. `ngrok http 80` 起隧道拿公网地址 2. `douyin-mashida:set-public-url` 一键把 `certLink` 切过去 + 打印平台侧待配项 + 自检隧道 -3. `douyin-mashida:preflight` 前置自检(配置就绪度 / **真实网关连通性** / 公网回调可达 / 域名一致性) +3. `douyin-mashida:subscribe` **订阅推送**(`message.config`)—— 不做这步平台一条消息都不会推 +4. `douyin-mashida:preflight` 前置自检(配置就绪度 / **真实网关连通性** / 公网回调可达 / 域名一致性) 详见 [08-公网联调.md](08-公网联调.md)。 @@ -53,7 +54,7 @@ | 兑换 | `routes/redeem.ts` | `redeem-service.ts` | | 后台配置 | `routes/admin/platform-config/douyin-mashida.ts` | `douyin-mashida-service.ts`(凭据脱敏)+ 前端面板 `DouyinMashidaPanel.tsx` | | 本地联调 | `scripts/douyin-mashida-gateway-stub.ts` / `douyin-mashida-push.ts` | 网关桩 + 推送模拟,见文档 08 | -| 公网联调 | `scripts/douyin-mashida-set-public-url.ts` + `douyin-mashida-preflight.ts` + ngrok | 一键切地址 / 自检隧道 / 真实网关连通性自检,见文档 08 | +| 公网联调 | `set-public-url` + `preflight` + `subscribe` 三个脚本 + ngrok | 一键切地址 / 自检隧道 / **推送订阅**,见文档 08 | | 前端页面 | `pages/redeem/RedeemPage.tsx` | `App.tsx` 的独立 `BrowserRouter` 分支 | --- @@ -84,6 +85,7 @@ | `scripts/douyin-mashida-push.ts` | 300 | **推送模拟器**:造签名消息打本地后端 | | `scripts/douyin-mashida-set-public-url.ts` | 290 | **公网联调切换**:改 `certLink` 为隧道地址 + 打印平台侧配置 + 自检隧道 | | `scripts/douyin-mashida-preflight.ts` | 330 | **前置自检**:配置就绪度 + 真实网关连通性(可选验签名)+ 公网回调 + 域名一致性 | +| `scripts/douyin-mashida-subscribe.ts` | 250 | **推送订阅**:调 `message.config` 配置推送地址与 topics + `messagetest` 自测 | ### 新增 · 前端 @@ -107,7 +109,7 @@ | `apps/backend/src/app.ts` | 挂载 `/api/v1/open/douyin-mashida`(**必须在 `express.json` 之前**)+ `/api/v1/redeem` | | `apps/backend/src/config/app-config-keys.ts` | 新增 key `douyin_mashida` | | `apps/backend/src/routes/admin/platform-config.ts` | 注册后台配置路由(自动继承 admin 守卫) | -| `apps/backend/package.json` | 新增 `douyin-mashida:set-public-url` / `douyin-mashida:preflight` | +| `apps/backend/package.json` | 新增 `douyin-mashida:set-public-url` / `douyin-mashida:preflight` / `douyin-mashida:subscribe` | | `apps/frontend/vite.config.ts` | dev server `allowedHosts` 放行常见隧道域名;**接上了此前从未被读取的 `VITE_ALLOWED_HOSTS`**(`.env`/compose 早就配了,白名单却是硬编码的) | | `apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx` | 新增「抖音码速达」Tab;本地 `SecretFieldControl` 改为共享组件 import | | `apps/frontend/src/App.tsx` | 新增 `isRedeemPath` 分支(早于 `isWorkerSite()`)+ `RedeemApplication` | @@ -239,6 +241,19 @@ git add apps/backend/src/app.ts \ --- +## 九点五、真实平台实测结论(2026-10-09) + +| 验证项 | 结果 | +| --- | --- | +| 出网签名(用真实 uid/secret 调 `order.orderDetail`) | ✅ `code=10000`,签名规则与文档一致 | +| 推送订阅 `message.config` | ✅ `code=10000 成功`(配置前平台一条都不推 —— 这是"下单没进 dev"的根因) | +| 进站推送(真密钥签名 → 公网地址 → 验签 → 建单) | ✅ `action=paid-order-upserted` | +| 19 位订单号精度 | ✅ `6930310175251856483` 精确保留 | +| `coupons.syncV2` 同步到真平台 | ✅ `sync_status=success` | +| 兑换页状态 | ✅ `redeemable`,展示真实商品名与真实支付时间 | +| `pay_time` 字符串形态 | ⚠️ 实测发现并已修复(见 07 §4.8) | +| `messagetest` 自测入口 | ⚠️ 始终 `20006 data参数内容解析异常`,未探明其 data 要求;不影响真实链路 | + ## 十、自检结果(本次) | 项 | 结果 | diff --git a/docs/抖音-码速达/07-最小接入实现说明.md b/docs/抖音-码速达/07-最小接入实现说明.md index 208cd005..1357f9d5 100644 --- a/docs/抖音-码速达/07-最小接入实现说明.md +++ b/docs/抖音-码速达/07-最小接入实现说明.md @@ -136,7 +136,28 @@ applyRawBigIntegerOverrides(parsed, rawText) // p_id / order_id / s_id / s_ids 它失败**不会**让兑换失败(用户那边已经核销成功、页面也显示了结果),只打 `level: 'error'` 的集成日志,便于事后补发。 这样处理的原因:通知渠道是「测试接口」,可用性未知;而核销一旦上报不可逆,不该被通知故障拖累。 -### 4.8 退款/取消消息文档没列字段 +### 4.8 `pay_time` 有**两种形态**(实测才发现)★ + +- **消息推送**里 `pay_time` 是**秒级 Integer**(文档如此); +- **`order.orderDetail` 返回**的却是**格式化字符串** `"2026-10-09 12:28:02"`(北京时间), + `create_time`/`finish_time` 同样是字符串,且"无"用 `"1970-01-01 08:00:00"`(epoch 0)表示。 + +只按文档实现会把真实支付时间解析成空、退化成"收到时间"。 +`order-mapper.ts:parseDouyinMashidaTimeIso()` 两种都认(纯数字按秒、字符串按 +08:00)。 + +### 4.9 文档里的示例值不能照抄 + +`order.orderDetail` 真实返回的 `shop_id` 是**商户自己的**(本例 `305760014`), +不是文档示例里的 `77977`;`sku_order_list[].order_id` 是**字符串**,本例与父单号相同。 +联调时以真实响应为准。 + +### 4.10 平台推送是**订阅制**(不配就永远收不到) + +填好 `uid`/`secret` 只代表我们能调平台,平台**不会**自动推消息过来: +必须调「消息设置」`message.config` 配置 `message_url` + `topics` + `status=1`。 +而且**不补推**订阅之前的历史订单。详见 [08-公网联调.md](08-公网联调.md) §2。 + +### 4.11 退款/取消消息文档没列字段 `refund_*`、`trade_TradeCanceled`、`trade_TradeSuccess` 这几条文档里 `props` 是空的(很可能只是文档缺失)。 代码按「有 `p_id` 就作废券、没有就留痕等人工」处理 —— **不抛错**,避免平台无意义重推。 diff --git a/docs/抖音-码速达/08-公网联调.md b/docs/抖音-码速达/08-公网联调.md index c4e0ca53..87313ee1 100644 --- a/docs/抖音-码速达/08-公网联调.md +++ b/docs/抖音-码速达/08-公网联调.md @@ -70,7 +70,37 @@ ngrok http 80 --domain=<你的静态域名>.ngrok-free.app --- -## 2. 一条命令切换地址 + 自检 +## 2. 必须做:订阅推送(只填 uid/secret 是不够的)★ + +**真实踩坑**:填好真实 `uid`/`secret`、隧道也通,但下了单**本地什么都收不到**。 +根因是——**平台推送是订阅制的**:凭据只保证我们能调它,它不会自动往我们这推。 +必须在「消息设置」接口(`message.config`)里配置 `message_url` + `topics` + `status=1`, +平台才会推消息过来。 + +```bash +cd apps/backend +# 订阅到当前公网地址(脚本自己从后台配置读真实 uid/secret) +npm run douyin-mashida:subscribe -- --public-url https://xxxx.ngrok-free.app + +# 联调结束/隧道要停:关掉订阅,避免平台一直往失效地址推 +npm run douyin-mashida:subscribe -- --disable +``` + +实测返回: + +```text +[订阅] method=message.config status=1 + message_url = https://75fb-166-0-19-25.ngrok-free.app/api/v1/open/douyin-mashida/message + topics = trade_TradePaid,trade_TradeCanceled,refund_RefundSuccess + ✅ 平台返回 code=10000 message=成功 +``` + +> ⚠️ 订阅保存的是**当时那个地址**:ngrok 换域名后必须重新订阅,否则平台会一直推到旧地址。 +> 另外平台**不会补推**订阅之前已支付的历史订单——`message.config` 之前下的单只能人工补。 + +--- + +## 3. 一条命令切换地址 + 自检 ```bash cd apps/backend @@ -82,7 +112,7 @@ npm run douyin-mashida:set-public-url -- --public-url https://xxxx.ngrok-free.ap --- -## 3. 真实联调前置自检 +## 4. 真实联调前置自检 ```bash npm run douyin-mashida:preflight -- --public-url https://xxxx.ngrok-free.app @@ -125,7 +155,7 @@ npm run douyin-mashida:preflight -- --public-url https://xxxx.ngrok-free.app --s --- -## 4. 联调 checklist +## 5. 联调 checklist ```text 1. 隧道起来 ngrok http 80 --domain=<静态域名> @@ -134,8 +164,8 @@ npm run douyin-mashida:preflight -- --public-url https://xxxx.ngrok-free.app --s (baseUrl 保持 https://dy-openplatform-api.wanmahui.com) 4. 前置自检 npm run douyin-mashida:preflight -- --public-url https://<域名> --secret <真密钥> 全 ✅ 才能开始 -5. 平台侧配推送 message_url = https://<域名>/api/v1/open/douyin-mashida/message - topics = trade_TradePaid(建议加 trade_TradeCanceled、refund_RefundSuccess) +5. 订阅推送 ★ npm run douyin-mashida:subscribe -- --public-url https://<域名> + (不做这步平台一条消息都不会推,见 §2) 6. 平台侧下一笔小额真单 → 日志看: POST /api/v1/open/douyin-mashida/message 收到推送 [douyin-mashida/certificate] 券码已同步 syncV2 成功 @@ -155,9 +185,9 @@ npm run douyin-mashida:preflight -- --public-url https://xxxx.ngrok-free.app --s --- -## 5. 坑 +## 6. 坑 -### 5.1 签名覆盖**原始 body**,中间层必须字节透传 ★ +### 6.1 签名覆盖**原始 body**,中间层必须字节透传 ★ 推送签名 = `md5(secret + "data" + data + "pub_time" + pubTime + "topic" + topic + secret)`, `data` 是 body 里的**原始 JSON 字符串**。任何重排/转义/压缩 body 的中间层都会让验签失败。 @@ -166,11 +196,11 @@ npm run douyin-mashida:preflight -- --public-url https://xxxx.ngrok-free.app --s - 中间若再加别的网关 / WAF / "统一 JSON 美化",就会 401。**报 401 先怀疑中间层改过 body。** - 我们也因此把消息路由挂在 `app.ts` 全局 body parser **之前**(`app.ts:42`),用 `express.text` 收原文。 -### 5.2 ngrok 免费版:域名会变 + 浏览器警示页 +### 6.2 ngrok 免费版:域名会变 + 浏览器警示页 -见 §1。域名一变,平台 message_url 和已发出的 `cert_link` 都会失效;浏览器访问会先过警示页。 +见 §1(域名会变 + 浏览器警示页)。域名一变,平台 message_url 和已发出的 `cert_link` 都会失效;浏览器访问会先过警示页。 -### 5.3 Vite 的 `allowedHosts` 会拦隧道域名 +### 6.3 Vite 的 `allowedHosts` 会拦隧道域名 隧道域名经 Caddy 转发到前端 dev server 时 Host 是隧道域名,不在白名单里 Vite 直接拒绝 (`Blocked request. This host is not allowed.`),**表现为页面打不开、后端日志却完全正常**。 @@ -178,35 +208,36 @@ npm run douyin-mashida:preflight -- --public-url https://xxxx.ngrok-free.app --s 已修:`vite.config.ts` 默认放行 `.ngrok-free.app` / `.ngrok.app` / `.ngrok.io` / `.trycloudflare.com`, 并真正接上了 `VITE_ALLOWED_HOSTS`(`.env` 里那个 `221329.cc.cd` 现在才真正生效 —— 之前白名单是硬编码的,变量从没被读过)。 -### 5.4 配置是内存缓存,只有后台接口能立刻生效 +### 6.4 配置是内存缓存,只有后台接口能立刻生效 `readAppConfigEntry` 读的是启动时从 DB 载入的**内存缓存**(`app-config-store.ts:8`)。 直接 `UPDATE app_config_entries` 不生效,必须走 `POST /api/v1/admin/platform-config/douyin-mashida` (面板和两个脚本都走接口),否则要重启后端。 -### 5.5 占位密钥会让"已配置"骗过你 +### 6.5 占位密钥会让"已配置"骗过你 早期本地联调存进库里的 `dev_douyin_mashida_secret` 会让 `hasSecret=true`,看起来配好了, 实际联调必然签名失败。`preflight` 会识别这个占位值并直接报阻塞。 -### 5.6 certLink 域名容易漏改 +### 6.6 certLink 域名容易漏改 `certLink` 是**发给买家**的链接,必须指向当前公网地址。`preflight` 第 ④ 步专门比对 `certLink` 与 `--public-url` 的 host,不一致会给提醒。 --- -## 6. 排错表 +## 7. 排错表 | 现象 | 原因 / 处理 | | --- | --- | | 自检第 ③ 步 404 | 隧道没转发到 Caddy 80(可能指到了静态站点) | | 自检第 ③ 步拿到 HTML | ngrok 浏览器警示页(自检脚本已带 skip 头);或隧道指错服务 | -| 页面能开但白屏 + `Blocked request` | Vite `allowedHosts` 未放行隧道域名(§5.3) | -| 平台推送返回 **401 验签失败** | `secret` 与平台不一致;或中间层改了 body(§5.1) | -| 平台推送后本地无日志 | 隧道挂了 / message_url 配错 / 平台没订阅 topic | +| 页面能开但白屏 + `Blocked request` | Vite `allowedHosts` 未放行隧道域名(§6.3) | +| 平台推送返回 **401 验签失败** | `secret` 与平台不一致;或中间层改了 body(§6.1) | +| 平台推送后本地无日志 | ①**没做订阅**(见 §2,最常见)②隧道挂了 ③订阅的是旧域名 | +| 历史订单一直没进本地 | 平台不补推订阅之前的历史消息,只能人工补 | | 收到推送但券同步失败 | 真实 `uid`/`secret` 不对;或 `baseUrl` 没指向真平台 | -| 买家点链接看到英文警示页 | ngrok 免费版特性(§5.2),点一下即可;正式环境用正式域名 | -| 域名突然全失效 | ngrok 重启换了域名(§5.2)→ 重跑 `set-public-url` 并改平台配置 | -| 改了配置行为没变 | 没走后台接口保存(§5.4) | -| `preflight` 报"占位密钥" | 把面板里的 secret 换成真实用户秘钥(§5.5) | +| 买家点链接看到英文警示页 | ngrok 免费版特性(§6.2),点一下即可;正式环境用正式域名 | +| 域名突然全失效 | ngrok 重启换了域名(§6.2)→ 重跑 `set-public-url` 并改平台配置 | +| 改了配置行为没变 | 没走后台接口保存(§6.4) | +| `preflight` 报"占位密钥" | 把面板里的 secret 换成真实用户秘钥(§6.5) |