fix(douyin-mashida): pay_time 兼容平台实际返回的字符串格式

真实联调发现:`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 位订单号 + 子单号等于父单号)。
This commit is contained in:
yml2213
2026-10-09 12:43:24 +08:00
parent d9848aaef3
commit 064267ad86
2 changed files with 107 additions and 1 deletions
@@ -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<Record<string, unknown>>
assert.equal(item.itemId, '6930310175251856483')
assert.equal(item.skuCode, '幸运币2个')
assert.equal(item.quantity, 1)
})
@@ -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