From 064267ad86bb70d45d6701da1dfd9dccccb5a9f8 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 9 Oct 2026 12:43:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(douyin-mashida):=20pay=5Ftime=20=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E5=B9=B3=E5=8F=B0=E5=AE=9E=E9=99=85=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=9A=84=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真实联调发现:`order.orderDetail` 返回的 `pay_time` 是格式化字符串 `"2026-10-09 12:28:02"`(北京时间),而文档写的是秒级 Integer, 消息推送里的 `pay_time` 又确实是 Integer —— 两种形态并存。 原实现只走 `parseEpochSecondsIso`,对字符串 `Number("2026-10-09 12:28:02")` = NaN, 会把真实支付时间丢掉、退化成"收到时间"。 改为 `parseDouyinMashidaTimeIso`:纯数字按秒级时间戳,`YYYY-MM-DD HH:mm:ss`(含 T) 按 +08:00 解析;平台用 `1970-01-01 08:00:00`(epoch 0)表示"无",返回空串。 用真实 orderDetail 响应反例补了单测(含 19 位订单号 + 子单号等于父单号)。 --- .../douyin-mashida/order-mapper.test.ts | 70 +++++++++++++++++++ .../platforms/douyin-mashida/order-mapper.ts | 38 +++++++++- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/services/platforms/douyin-mashida/order-mapper.test.ts b/apps/backend/src/services/platforms/douyin-mashida/order-mapper.test.ts index 607e63fc..fe266290 100644 --- a/apps/backend/src/services/platforms/douyin-mashida/order-mapper.test.ts +++ b/apps/backend/src/services/platforms/douyin-mashida/order-mapper.test.ts @@ -7,6 +7,7 @@ import { buildDouyinMashidaSourceEvent, extractDouyinMashidaSubOrderId, mapDouyinMashidaOrderState, + parseDouyinMashidaTimeIso, parseEpochSecondsIso, resolveDouyinMashidaParentOrderId, resolveDouyinMashidaReceiptOrderId, @@ -153,3 +154,72 @@ test('秒级时间戳转 ISO:非法值返回空串', () => { assert.equal(parseEpochSecondsIso('abc'), '') assert.equal(parseEpochSecondsIso(null), '') }) + +test('平台时间两种形态都要认:秒级时间戳 与 "YYYY-MM-DD HH:mm:ss"(真实联调发现)', () => { + // 消息推送里是秒级 Integer + assert.equal(parseDouyinMashidaTimeIso(1630373504), new Date(1630373504 * 1000).toISOString()) + + // order.orderDetail 返回的是北京时间字符串(文档没写,实测才发现) + assert.equal(parseDouyinMashidaTimeIso('2026-10-09 12:28:02'), '2026-10-09T04:28:02.000Z') + assert.equal(parseDouyinMashidaTimeIso('2026-10-09T12:28:02'), '2026-10-09T04:28:02.000Z') + + // 平台用 epoch 0 表示"无" + assert.equal(parseDouyinMashidaTimeIso('1970-01-01 08:00:00'), '') + assert.equal(parseDouyinMashidaTimeIso('0'), '') + assert.equal(parseDouyinMashidaTimeIso(''), '') + assert.equal(parseDouyinMashidaTimeIso('not-a-time'), '') +}) + +test('真实 order.orderDetail 形态能正确映射(pay_time 为字符串 + 子单号等于父单号)', () => { + const realDetail = { + order_id: '6930310175251856483', + order_status: 3, + order_type: 5, + biz: 11, + shop_id: 305760014, + shop_name: '向日葵游戏交易店', + order_amount: 100, + pay_amount: 100, + pay_time: '2026-10-09 12:28:02', + create_time: '2026-10-09 12:28:08', + finish_time: '1970-01-01 08:00:00', + doudian_open_id: '1@#c6oZFIm15Q1cgcYCu+xGhx+Shv/+JlJlOzcmtRHhaPZi1aYjK7rUOza7AU4lr2i5xOuT', + post_receiver: '', + post_tel: '', + buyer_words: '', + seller_words: '', + sku_order_list: [ + { + code: '幸运币2个', + goods_price: 100, + item_num: 1, + order_amount: 100, + order_id: '6930310175251856483', + product_id: 3847255376114352600, + product_name: '【测试专用】和平精英皮肤小店入口出售返场吃鸡皮肤浪漫天命专用', + sku_id: 3383719439799589, + }, + ], + } + + const event = buildDouyinMashidaSourceEvent({ + // p_id 用字符串:写成数字字面量在测试里就会先被 JS 截断(正是生产里那个精度坑) + message: { p_id: '6930310175251856483', order_status: 2, pay_amount: 100 }, + detail: realDetail, + }) + + assert.equal(event.platformOrderId, '6930310175251856483') + assert.equal(event.shopId, '305760014') + assert.equal(event.shopName, '向日葵游戏交易店') + assert.equal(event.totalAmount, 100) + assert.equal(event.orderStatus, 'paid') + assert.equal(event.payStatus, 'paid') + // 关键是这个:字符串 pay_time 也要拿到真实支付时间,而不是退化成 receivedAt + assert.equal(event.paidAt, '2026-10-09T04:28:02.000Z') + assert.equal(event.items.length, 1) + + const [item] = event.items as Array> + assert.equal(item.itemId, '6930310175251856483') + assert.equal(item.skuCode, '幸运币2个') + assert.equal(item.quantity, 1) +}) diff --git a/apps/backend/src/services/platforms/douyin-mashida/order-mapper.ts b/apps/backend/src/services/platforms/douyin-mashida/order-mapper.ts index 395eeb16..a500f0e5 100644 --- a/apps/backend/src/services/platforms/douyin-mashida/order-mapper.ts +++ b/apps/backend/src/services/platforms/douyin-mashida/order-mapper.ts @@ -95,7 +95,7 @@ export function buildDouyinMashidaSourceEvent(input: { const detail = input.detail || {} const parentOrderId = resolveDouyinMashidaParentOrderId(message, detail) const state = mapDouyinMashidaOrderState(detail.order_status ?? message.order_status) - const paidAt = parseEpochSecondsIso(detail.pay_time ?? message.pay_time) + const paidAt = parseDouyinMashidaTimeIso(detail.pay_time ?? message.pay_time) const receivedAt = input.receivedAt || nowIso() return { @@ -198,6 +198,42 @@ export function parseEpochSecondsIso(value: unknown): string { return new Date(seconds * 1000).toISOString() } +/** + * 平台时间字段 → ISO。 + * + * ⚠️ 真实联调发现:`order.orderDetail` 返回的 `pay_time` 是**格式化字符串** + * `"2026-10-09 12:28:02"`(北京时间),而**消息推送**里 `pay_time` 是秒级 Integer。 + * 文档只写了 Integer,照文档实现会把真实支付时间解析成空、退化成"收到时间"。 + * 所以两种形态都要认: + * - 纯数字 → 按秒级时间戳 + * - `YYYY-MM-DD HH:mm:ss`(含 T 分隔)→ 按 **+08:00** 解析 + * `"1970-01-01 08:00:00"`(平台表示"无")与非法值一律返回空串。 + */ +export function parseDouyinMashidaTimeIso(value: unknown): string { + const text = String(value ?? '').trim() + if (!text || text === '0') { + return '' + } + + if (/^\d+$/.test(text)) { + return parseEpochSecondsIso(text) + } + + const matched = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/.exec(text) + if (!matched) { + return '' + } + + const [, year, month, day, hour, minute, second] = matched + const date = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}+08:00`) + const timestamp = date.getTime() + // 平台用 "1970-01-01 08:00:00"(即 epoch 0)表示"无" + if (!Number.isFinite(timestamp) || timestamp <= 0) { + return '' + } + return date.toISOString() +} + function normalizeFen(value: unknown): number { const amount = Number(value) return Number.isFinite(amount) && amount > 0 ? Math.round(amount) : 0