Files
order_site/apps/backend/src/routes/douyin-mashida.ts
T
yml2213 72606796b3 feat(douyin-mashida): 抖音码速达最小接入后端(消息建单/券码同步/兑换核销/退款作废)
- 签名:出网 md5(secret+Σ(名+值)+secret) 与推送 md5(secret+data+pub_time+topic+secret)
  两套独立实现,含文档 golden 向量单测(与 91 大写 / affiliate_dash HMAC 都不通用)
- 进单:trade_TradePaid 推送 → 验签 → webhook_events 幂等 → order.orderDetail
  → upsertOrderFromSource(provider=douyin_mashida / platform=doudian)
- 精度:19 位订单号超出 JS 安全整数,JSON.parse 会把 p_id 抹成 ...000;
  故解析后从原始 JSON 文本用正则抓回精确数字(单测同时证明"不修会错")
- 券码:迁移 094 redeem_certificates((provider,order_id) 唯一做发券幂等)
  + DYC 券码生成 + coupons.syncV2 同步(子单号优先、父单兜底)
- 兑换:POST /api/v1/redeem/lookup|confirm,按 orderNo+IP 限流;
  核销成功后 order.message 通知买家(best-effort,失败只记错误日志)
- 退款/取消:作废未核销券;券已核销时打 WARN 资损告警(已消费不可回滚)
- 后台:GET/POST /api/v1/admin/platform-config/douyin-mashida,
  secret 出参置空 + 掩码,掩码回写不覆盖原值
- 消息路由挂在全局 body parser 之前(app.ts),验签必须用 body 原始字符串
2026-10-09 12:00:50 +08:00

68 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import express, { Router } from 'express'
import { createRateLimitMiddleware } from '../middleware/rate-limit.js'
import { handleDouyinMashidaMessage } from '../services/platforms/douyin-mashida/notify-service.js'
import { sendRouteError } from '../utils/http.js'
import { createRequestId, logIntegration } from '../utils/logger.js'
/**
* 抖音码速达消息推送入口。
*
* ⚠️ 这个路由必须在 `app.ts` 的 `express.json()` / `express.urlencoded()` **之前**挂载:
* 平台签名用的是 `data` 的**原始 JSON 字符串**,任何全局 body parser 都会破坏它。
* 这里用 `express.text({ type: () => true })` 自己收原文(与快手/91 的入站不同,那两个不需要 rawBody)。
*
* 推送形态:`POST /api/v1/open/douyin-mashida/message?topic=&pub_time=&sign=`,body 为 data 的 JSON 字符串。
*/
const router = Router()
const messageRateLimit = createRateLimitMiddleware({
scope: 'douyinMashidaMessage',
windowMs: 60_000,
max: 300,
})
const captureRawBody = express.text({ type: () => true, limit: '2mb' })
router.all('/message', messageRateLimit, captureRawBody, async (req, res) => {
const requestId = createRequestId('dym')
const startedAt = Date.now()
const topic = String(req.query.topic || '').trim()
const pubTime = String(req.query.pub_time || req.query.pubTime || '').trim()
const sign = String(req.query.sign || '').trim()
const rawBody = typeof req.body === 'string' ? req.body : ''
logIntegration('[douyin-mashida/message]', '收到码速达消息推送', {
requestId,
method: req.method,
originalUrl: req.originalUrl,
ip: req.ip,
topic,
pubTime,
bodyLength: rawBody.length,
})
try {
const result = await handleDouyinMashidaMessage({ topic, pubTime, sign, rawBody })
logIntegration('[douyin-mashida/message]', '消息推送处理完成', {
requestId,
durationMs: Date.now() - startedAt,
...result,
})
// 平台侧只要求「处理成功」;具体期望的响应体在联调时确认(失败时用 4xx/5xx 触发平台重推)
res.status(200).json({ code: 0, message: 'success', data: { action: result.action } })
} catch (error) {
logIntegration(
'[douyin-mashida/message]',
'消息推送处理失败',
{ requestId, durationMs: Date.now() - startedAt, topic, error },
{ level: 'error' },
)
sendRouteError(res, error, '抖音码速达消息处理失败', '[douyin-mashida/message]')
}
})
export default router