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