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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user