docs(douyin-mashida): 回填真实联调实测结论与推送订阅步骤
- 08-公网联调:新增「必须做:订阅推送」一节 —— 只填 uid/secret 平台不会推消息, 必须调 message.config 配置 message_url/topics/status;含脚本用法、实测返回、 域名变更后需重订阅、平台不补推历史订单等注意事项;排错表补两行 - 07-最小接入实现说明:新增三条实测结论 §4.8 pay_time 有 Integer 与 "YYYY-MM-DD HH:mm:ss" 两种形态(及 epoch 0 表示"无") §4.9 文档示例值不能照抄(真实 shop_id / 子单号可能是字符串且等于父单号) §4.10 平台推送是订阅制、不补推历史订单 - 00-总览:补「真实平台实测结论」表,并把 subscribe 脚本纳入清单
This commit is contained in:
+19
-4
@@ -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 要求;不影响真实链路 |
|
||||
|
||||
## 十、自检结果(本次)
|
||||
|
||||
| 项 | 结果 |
|
||||
|
||||
@@ -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` 就作废券、没有就留痕等人工」处理 —— **不抛错**,避免平台无意义重推。
|
||||
|
||||
+52
-21
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user