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,
|
buildDouyinMashidaSourceEvent,
|
||||||
extractDouyinMashidaSubOrderId,
|
extractDouyinMashidaSubOrderId,
|
||||||
mapDouyinMashidaOrderState,
|
mapDouyinMashidaOrderState,
|
||||||
|
parseDouyinMashidaTimeIso,
|
||||||
parseEpochSecondsIso,
|
parseEpochSecondsIso,
|
||||||
resolveDouyinMashidaParentOrderId,
|
resolveDouyinMashidaParentOrderId,
|
||||||
resolveDouyinMashidaReceiptOrderId,
|
resolveDouyinMashidaReceiptOrderId,
|
||||||
@@ -153,3 +154,72 @@ test('秒级时间戳转 ISO:非法值返回空串', () => {
|
|||||||
assert.equal(parseEpochSecondsIso('abc'), '')
|
assert.equal(parseEpochSecondsIso('abc'), '')
|
||||||
assert.equal(parseEpochSecondsIso(null), '')
|
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 detail = input.detail || {}
|
||||||
const parentOrderId = resolveDouyinMashidaParentOrderId(message, detail)
|
const parentOrderId = resolveDouyinMashidaParentOrderId(message, detail)
|
||||||
const state = mapDouyinMashidaOrderState(detail.order_status ?? message.order_status)
|
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()
|
const receivedAt = input.receivedAt || nowIso()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -198,6 +198,42 @@ export function parseEpochSecondsIso(value: unknown): string {
|
|||||||
return new Date(seconds * 1000).toISOString()
|
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 {
|
function normalizeFen(value: unknown): number {
|
||||||
const amount = Number(value)
|
const amount = Number(value)
|
||||||
return Number.isFinite(amount) && amount > 0 ? Math.round(amount) : 0
|
return Number.isFinite(amount) && amount > 0 ? Math.round(amount) : 0
|
||||||
|
|||||||
Reference in New Issue
Block a user