Files

12 KiB
Raw Permalink Blame History

抖音码速达 · 公网联调(ngrok)

目标:让真实码速达平台把消息推到你的机器,同时买家能打开你的 /redeem。 离线模拟(网关桩 / 推送模拟器)已按要求删除 —— 只走真实平台。 实现细节见 07-最小接入实现说明.md。


0. 为什么必须公网

抖音码速达是双向的:

方向 谁发起 localhost
出站 order.orderDetail / syncV2 / verifyV3 / order.message 我们 ✅ 行(baseUrl 指真平台且能出网即可)
进站 trade_TradePaid / 退款推送 平台 ❌ 平台必须能主动访问你
买家打开 cert_link(/redeem) 买家手机 ❌ 手机打不开你的 localhost

隧道必须转发到 Caddy 的 80 端口(deploy/caddy/Caddyfile.dev 是 :80 {},任意 Host 都匹配,不用改 Caddy):

ngrok → Caddy:80 ─┬─ /api/*   → backend:3000   ← 平台推消息打这里
                  ├─ /health* → backend:3000
                  ├─ /s/*     → backend:3000
                  └─ 其余      → frontend:5173  ← 买家打开 /redeem 打这里

1. 起 ngrok

ngrok http 80
# 输出里的 https://xxxx.ngrok-free.app 就是公网地址

已验证可用(ngrok v3.39.10,authtoken 已配置)。

强烈建议固定域名与指定亚太区域(低延迟)

免费版每次重启域名都会变,一变更就要重配平台 message_url,而已发给买家的直充链接不可撤回。

# 1) 在 ngrok 后台 Domains 里保留一个免费静态域名(免费额度 1 个)
# 2) 用固定域名与亚太区域启动(国内联调延迟更低)
ngrok http 80 --domain=nonsecretly-posttracheal-amado.ngrok-free.dev --region=ap

⚠️ 免费版的浏览器警示页(影响买家)

实测:平台推送不受影响(服务端 POST,正常进我们的路由); 但浏览器访问会被插一页全英文 ngrok 警示页:

模拟 iPhone 浏览器访问 /redeem →
  HTTP 200, 2781 字节, <html ... ngrok assets ...>   ← 不是我们的页面
同一地址用 curl(Accept: */*)→ 200, 561 字节, 正常 vite 页面

也就是说:买家点开兑换链接会先看到 "You are about to visit ..." 并要求点一下,这对真实用户体验是硬伤。

对策(按推荐度):

  1. 联调期间接受:自己和测试同学点一下即可;平台回调完全不受影响。
  2. 换付费/带自定义域名的 ngrok:警示页会消失(生产环境本来也不该用隧道,应该用正式域名)。
  3. 我们的脚本自检时会带 ngrok-skip-browser-warning: 1 头,不受影响。

备注:如果哪天回头用 Cloudflare Tunnel,它没有这个警示页;当时连不上的原因是本机代理拦了 QUIC (拨到 fake-IP 198.18.x.x 一直 timeout),配置里加 protocol: http2 即可解决。


2. 必须做:订阅推送(只填 uid/secret 是不够的)★

真实踩坑:填好真实 uid/secret、隧道也通,但下了单本地什么都收不到。 根因是——平台推送是订阅制的:凭据只保证我们能调它,它不会自动往我们这推。 必须在「消息设置」接口(message.config)里配置 message_url + topics + status=1, 平台才会推消息过来。

cd apps/backend
# 订阅到当前公网地址(脚本自己从后台配置读真实 uid/secret)
npm run douyin-mashida:subscribe -- --public-url https://xxxx.ngrok-free.app

# 联调结束/隧道要停:关掉订阅,避免平台一直往失效地址推
npm run douyin-mashida:subscribe -- --disable

实测返回:

[订阅] 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. 一条命令切换地址 + 自检

cd apps/backend
npm run douyin-mashida:set-public-url -- --public-url https://xxxx.ngrok-free.app

它会把 certLink / afterVerifyLink 改成 <公网地址>/redeem, 打印平台侧要配的三项,并自检隧道(假签名打公网地址,期望 401)。


4. 真实联调前置自检

npm run douyin-mashida:preflight -- --public-url https://xxxx.ngrok-free.app
# 有真实密钥时(这一步才能验签名):
npm run douyin-mashida:preflight -- --public-url https://xxxx.ngrok-free.app --secret <真实用户秘钥>

四步:① 后台配置就绪度 ② 真实网关连通性 ③ 公网回调可达性 ④ certLink 与公网域名是否一致。

实测输出(配置未填完时):

[1/4] 后台配置
   enabled    ✅ 开启
   uid        ❌ 未填
   certLink   ✅ https://xxxx.ngrok-free.app/redeem
   baseUrl    https://dy-openplatform-api.wanmahui.com
   ready      ❌ false

[2/4] 真实网关连通性
   POST https://dy-openplatform-api.wanmahui.com/api/openplatform/gateway
   HTTP 200
   平台返回 code=10014 message=uid格式异常
   ✅ 网关连通、域名/路径/form 编码/参数形态都被接受

[3/4] 公网回调自检
   HTTP 401 errorCode=douyin_mashida_message_invalid_signature
   ✅ 平台能推到你

[4/4] 域名一致性
   ✅ 一致

②的解读:平台先校 uid 再校签名。不带 --secret 时只能证明"网络+路径+参数形态"没问题; 带上 --secret 后如果平台报的不是签名错误(而是"订单不存在"之类业务错),就说明签名/密钥也对了 —— 这一步能在正式推送前把密钥问题排掉,最值钱。

平台文档只给了 code=10000 表示成功、"其余均表示错误",没有完整错误码表; 脚本不翻译码值,只原样打印,并对实测观测到的码加说明(如 10014)。


5. 联调 checklist

1. 隧道起来            ngrok http 80 --domain=<静态域名>
2. 切公网地址          npm run douyin-mashida:set-public-url -- --public-url https://<域名>
3. 填真实凭据          面板:uid = 抖店商户号,secret = 真实用户秘钥
                       (baseUrl 保持 https://dy-openplatform-api.wanmahui.com)
4. 皮肤领取页两项配置   面板:
                       claimLinkBase = https://<域名>/claim   皮肤订单领取页基址
                       messageTemplate = 您的订单已发货,请点击领取:{link}
                       ⚠️ claimLinkBase 必须是**路径式**(不要带 #):
                          #/claim 是打手领取页,配错会把买家带到打手流程
5. 前置自检            npm run douyin-mashida:preflight -- --public-url https://<域名> --secret <真密钥>
                       全 ✅ 才能开始
6. 订阅推送 ★          npm run douyin-mashida:subscribe -- --public-url https://<域名>
                       (不做这步平台一条消息都不会推,见 §2)
7. 平台侧下一笔小额真单 → 日志看:
                       POST /api/v1/open/douyin-mashida/message      收到推送
                       [douyin-mashida/certificate] 券码已同步         syncV2 成功
                       fulfillment_tasks / claim_tokens               已建任务与领取 token
8. 手机打开买家领取链接 https://<域名>/claim/<token>(首次要过 ngrok 警示页)
                       → 看到物品图片 → 填游戏账号 → 领取 → 本地看到 verifyV3
9. 对账               orders / order_items / redeem_certificates / fulfillment_tasks 四张表

必须实测的 7 个点

  1. message_type=2 发出去会不会改变抖店订单的发货状态(我们只想发链接)
  2. 平台期望的成功响应体(当前回 {"code":0,"message":"success"})
  3. 平台重推策略(我们失败时回 4xx,是否真会重推)
  4. coupons.syncV2 的 order_id 认子单号还是父单号(代码子单优先,日志里能看实际值)
  5. 同步成功后买家在哪里看到卡券(决定他能不能走到 /redeem)
  6. verifyV3 重复核销的返回、能否 cancelVerify 冲正
  7. order.orderDetail 的 p_id/order_id 实际是字符串还是数字(数字会触发 19 位精度问题)

6. 坑

6.1 签名覆盖原始 body,中间层必须字节透传 ★

推送签名 = md5(secret + "data" + data + "pub_time" + pubTime + "topic" + topic + secret), data 是 body 里的原始 JSON 字符串。任何重排/转义/压缩 body 的中间层都会让验签失败。

  • ngrok 是透传,没问题;
  • 中间若再加别的网关 / WAF / "统一 JSON 美化",就会 401。报 401 先怀疑中间层改过 body。
  • 我们也因此把消息路由挂在 app.ts 全局 body parser 之前(app.ts:42),用 express.text 收原文。

6.2 ngrok 免费版:域名会变 + 浏览器警示页

见 §1(域名会变 + 浏览器警示页)。域名一变,平台 message_url 和已发出的 cert_link 都会失效;浏览器访问会先过警示页。

6.3 Vite 的 allowedHosts 会拦隧道域名

隧道域名经 Caddy 转发到前端 dev server 时 Host 是隧道域名,不在白名单里 Vite 直接拒绝 (Blocked request. This host is not allowed.),表现为页面打不开、后端日志却完全正常。

已修:vite.config.ts 默认放行 .ngrok-free.app / .ngrok.app / .ngrok.io / .trycloudflare.com, 并真正接上了 VITE_ALLOWED_HOSTS(.env 里那个 221329.cc.cd 现在才真正生效 —— 之前白名单是硬编码的,变量从没被读过)。

6.4 配置是内存缓存,只有后台接口能立刻生效

readAppConfigEntry 读的是启动时从 DB 载入的内存缓存(app-config-store.ts:8)。 直接 UPDATE app_config_entries 不生效,必须走 POST /api/v1/admin/platform-config/douyin-mashida (面板和两个脚本都走接口),否则要重启后端。

6.5 占位密钥会让"已配置"骗过你

早期本地联调存进库里的 dev_douyin_mashida_secret 会让 hasSecret=true,看起来配好了, 实际联调必然签名失败。preflight 会识别这个占位值并直接报阻塞。

certLink 是发给买家的链接,必须指向当前公网地址。preflight 第 ④ 步专门比对 certLink 与 --public-url 的 host,不一致会给提醒。


7. 排错表

现象 原因 / 处理
自检第 ③ 步 404 隧道没转发到 Caddy 80(可能指到了静态站点)
自检第 ③ 步拿到 HTML ngrok 浏览器警示页(自检脚本已带 skip 头);或隧道指错服务
页面能开但白屏 + Blocked request Vite allowedHosts 未放行隧道域名(§6.3)
平台推送返回 401 验签失败 secret 与平台不一致;或中间层改了 body(§6.1)
平台推送后本地无日志 ①没做订阅(见 §2,最常见)②隧道挂了 ③订阅的是旧域名
历史订单一直没进本地 平台不补推订阅之前的历史消息,只能人工补
收到推送但券同步失败 真实 uid/secret 不对;或 baseUrl 没指向真平台
买家点链接看到英文警示页 ngrok 免费版特性(§6.2),点一下即可;正式环境用正式域名
域名突然全失效 ngrok 重启换了域名(§6.2)→ 重跑 set-public-url 并改平台配置
改了配置行为没变 没走后台接口保存(§6.4)
preflight 报"占位密钥" 把面板里的 secret 换成真实用户秘钥(§6.5)