fix(fulfillment): 修复自动发货任务上下文并发覆盖

This commit is contained in:
yml2213
2026-11-03 13:14:07 +08:00
parent b3fb330ad9
commit b842d00fc3
4 changed files with 132 additions and 19 deletions
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { JsonObject } from '../../../types/json.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js'
test('电子凭证绑定只原子合并自己的 context key,不覆盖 affiliateDash', async () => {
const task = {
id: 120509,
context_json: {
affiliateDash: {
orderNo: 'FO2026110313080188725b91e99',
clientOrderNo: '2630702009068141',
},
},
} as unknown as TaskRow
const voucher = {
oid: '2630702009068141',
voucher_code: 'KSV4YQWP6SBEQFRBCBD',
token: 'token',
status: 'CONSUMED',
unit_index: 1,
send_callback_status: 'success',
send_callback_attempt_count: 1,
} as unknown as KuaishouIndustryVoucherRow
let capturedTaskId = 0
let capturedPatch: JsonObject | null = null
let capturedUpdatedAt = ''
const merged = await attachKuaishouIndustryVoucherToTask(
task,
voucher,
{ now: '2026-11-03T13:08:01.383Z' },
{
mergeTaskContext: async (taskId, patch, updatedAt) => {
capturedTaskId = Number(taskId)
capturedPatch = patch
capturedUpdatedAt = String(updatedAt || '')
return task
},
},
)
assert.equal(merged, task)
assert.equal(capturedTaskId, 120509)
assert.equal(capturedUpdatedAt, '2026-11-03T13:08:01.383Z')
assert.deepEqual(Object.keys(capturedPatch || {}), ['kuaishouIndustryVoucher'])
assert.equal(
(capturedPatch?.kuaishouIndustryVoucher as Record<string, unknown> | undefined)?.oid,
'2630702009068141',
)
assert.equal('affiliateDash' in (capturedPatch || {}), false)
})
@@ -1,11 +1,10 @@
import { updateTask } from '../../../repositories/task-repo.js'
import { mergeTaskContext } from '../../../repositories/task-repo.js'
import type { JsonObject } from '../../../types/json.js'
import {
listKuaishouIndustryVouchersByOid,
updateKuaishouIndustryVoucherByCode,
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
import { normalizeTimestampIso } from '../../../utils/time.js'
import { parseTaskContext } from '../../../utils/task-json.js'
import {
isKuaishouIndustryVoucherSendCallbackSuccess,
normalizeKuaishouIndustrySendCallbackStatus,
@@ -17,6 +16,10 @@ import type {
TaskRow,
} from '../../../types/repository/rows.js'
type VoucherBindingDeps = {
mergeTaskContext?: typeof mergeTaskContext
}
export async function bindKuaishouIndustryVouchersToOrderTasks(
order: OrderRow | null | undefined,
tasks: TaskRow[] = [],
@@ -62,27 +65,17 @@ export async function attachKuaishouIndustryVoucherToTask(
source?: string
now?: string
} = {},
deps: VoucherBindingDeps = {},
) {
const now = normalizeTimestampIso(options.now || new Date().toISOString())
const context = parseTaskContext(task)
const nextVoucherContext = buildKuaishouIndustryVoucherContext(
voucher,
context.kuaishouIndustryVoucher,
const nextVoucherContext = buildKuaishouIndustryVoucherContext(voucher, null, now)
const mergeContext = deps.mergeTaskContext || mergeTaskContext
// 只原子替换本模块负责的顶层 key,禁止用旧 TaskRow 整段覆盖 affiliateDash。
return mergeContext(
task.id,
{ kuaishouIndustryVoucher: nextVoucherContext } satisfies JsonObject,
now,
)
const nextContext: JsonObject = {
...context,
kuaishouIndustryVoucher: nextVoucherContext,
}
if (JSON.stringify(context) === JSON.stringify(nextContext)) {
return task
}
return updateTask(task.id, {
context_json: JSON.stringify(nextContext),
updated_at: now,
})
}
function resolveTaskForVoucher(voucher: KuaishouIndustryVoucherRow, tasks: TaskRow[]) {