优化多账号选择
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { query } from '../db/client.js'
|
||||
import { TASK_STATUS } from '../domain/task-status.js'
|
||||
import { maskCode } from '../utils/masking.js'
|
||||
import { parseTaskContext } from '../utils/task-json.js'
|
||||
import { normalizeTimestampIso } from '../utils/time.js'
|
||||
@@ -6,6 +7,12 @@ import { normalizeTimestampIso } from '../utils/time.js'
|
||||
type QueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>
|
||||
type JsonRecord = Record<string, any>
|
||||
|
||||
export type KuaishouCloudSourceLoadStat = {
|
||||
sourceKey: string
|
||||
activeCount: number
|
||||
redeemingCount: number
|
||||
}
|
||||
|
||||
export type KuaishouCloudTaskStateSyncInput = {
|
||||
taskId: number | string
|
||||
executorKey?: unknown
|
||||
@@ -86,6 +93,49 @@ export async function syncKuaishouCloudTaskStateForTask(
|
||||
)
|
||||
}
|
||||
|
||||
export async function listKuaishouCloudSourceLoadStats(
|
||||
sourceKeys: unknown[] = [],
|
||||
executor: QueryExecutor = query,
|
||||
): Promise<KuaishouCloudSourceLoadStat[]> {
|
||||
const candidates = normalizeStringArray(sourceKeys)
|
||||
if (candidates.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const activeStatuses = [
|
||||
TASK_STATUS.WAITING_BINDING,
|
||||
TASK_STATUS.ROLE_CONFIRMED,
|
||||
TASK_STATUS.REDEEMING,
|
||||
TASK_STATUS.DISPATCHED_PENDING_RETURN,
|
||||
]
|
||||
const result = await executor(
|
||||
`
|
||||
SELECT
|
||||
kcts.source_key,
|
||||
COUNT(*) FILTER (WHERE ft.task_status = ANY($2::text[])) AS active_count,
|
||||
COUNT(*) FILTER (WHERE ft.task_status = $3) AS redeeming_count
|
||||
FROM fulfillment_tasks ft
|
||||
JOIN kuaishou_cloud_task_states kcts ON kcts.task_id = ft.id
|
||||
WHERE ft.executor_key = 'kuaishou_ct_assisted'
|
||||
AND kcts.source_key = ANY($1::text[])
|
||||
AND ft.task_status = ANY($2::text[])
|
||||
GROUP BY kcts.source_key
|
||||
`,
|
||||
[candidates, activeStatuses, TASK_STATUS.REDEEMING],
|
||||
)
|
||||
|
||||
return result.rows
|
||||
.map((row) => {
|
||||
const source = asRecord(row)
|
||||
return {
|
||||
sourceKey: String(source.source_key || '').trim(),
|
||||
activeCount: Number(source.active_count || 0) || 0,
|
||||
redeemingCount: Number(source.redeeming_count || 0) || 0,
|
||||
}
|
||||
})
|
||||
.filter((row) => row.sourceKey)
|
||||
}
|
||||
|
||||
function resolveKuaishouCloudFlow(contextJson: unknown): JsonRecord | null {
|
||||
const context = parseTaskContext({ context_json: contextJson })
|
||||
const flow = asRecord(context.kuaishouCloudFulfillment)
|
||||
@@ -93,13 +143,32 @@ function resolveKuaishouCloudFlow(contextJson: unknown): JsonRecord | null {
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): JsonRecord {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : {}
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonRecord) : {}
|
||||
}
|
||||
|
||||
function normalizeStatus(value: unknown): string {
|
||||
return String(value || 'pending').trim() || 'pending'
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return Array.from(new Set(value.map((item) => String(item || '').trim()).filter(Boolean)))
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return Array.from(
|
||||
new Set(
|
||||
value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values: unknown[]): string {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { selectCloudtentaclesSourceForFulfillment } from './account-selector.js'
|
||||
|
||||
const contexts = {
|
||||
'account-a': {
|
||||
baseUrl: 'https://cloud.example.com',
|
||||
token: 'token-a',
|
||||
deviceId: '-',
|
||||
deviceType: 0,
|
||||
resolvedSourceKey: 'account-a',
|
||||
},
|
||||
'account-b': {
|
||||
baseUrl: 'https://cloud.example.com',
|
||||
token: 'token-b',
|
||||
deviceId: '-',
|
||||
deviceType: 0,
|
||||
resolvedSourceKey: 'account-b',
|
||||
},
|
||||
}
|
||||
|
||||
test('selectCloudtentaclesSourceForFulfillment 已固定账号时不按压力切换', async () => {
|
||||
const selection = await selectCloudtentaclesSourceForFulfillment(
|
||||
{
|
||||
binding: {
|
||||
resolvedSourceKey: 'account-a',
|
||||
cloudSourceKeys: ['account-a', 'account-b'],
|
||||
},
|
||||
},
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => {
|
||||
const key = String(sourceKeys[0] || '').trim()
|
||||
return contexts[key as keyof typeof contexts]
|
||||
},
|
||||
listSourceLoadStats: async () => [
|
||||
{ sourceKey: 'account-a', activeCount: 99, redeemingCount: 99 },
|
||||
{ sourceKey: 'account-b', activeCount: 0, redeemingCount: 0 },
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(selection.sourceKey, 'account-a')
|
||||
assert.equal(selection.fixed, true)
|
||||
})
|
||||
|
||||
test('selectCloudtentaclesSourceForFulfillment 多候选时选择压力最低账号', async () => {
|
||||
const selection = await selectCloudtentaclesSourceForFulfillment(
|
||||
{
|
||||
binding: {
|
||||
cloudSourceKeys: ['account-a', 'account-b'],
|
||||
},
|
||||
},
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => {
|
||||
const key = String(sourceKeys[0] || '').trim()
|
||||
return contexts[key as keyof typeof contexts]
|
||||
},
|
||||
listSourceLoadStats: async () => [
|
||||
{ sourceKey: 'account-a', activeCount: 3, redeemingCount: 1 },
|
||||
{ sourceKey: 'account-b', activeCount: 0, redeemingCount: 0 },
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
assert.equal(selection.sourceKey, 'account-b')
|
||||
assert.equal(selection.fixed, false)
|
||||
} finally {
|
||||
selection.release()
|
||||
}
|
||||
})
|
||||
|
||||
test('selectCloudtentaclesSourceForFulfillment 会跳过不可用账号', async () => {
|
||||
const selection = await selectCloudtentaclesSourceForFulfillment(
|
||||
{
|
||||
binding: {
|
||||
cloudSourceKeys: ['account-a', 'account-b'],
|
||||
},
|
||||
},
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => {
|
||||
const key = String(sourceKeys[0] || '').trim()
|
||||
if (key === 'account-a') {
|
||||
throw new Error('账号已停用')
|
||||
}
|
||||
return contexts[key as keyof typeof contexts]
|
||||
},
|
||||
listSourceLoadStats: async () => [],
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
assert.equal(selection.sourceKey, 'account-b')
|
||||
assert.equal(selection.candidateCount, 1)
|
||||
} finally {
|
||||
selection.release()
|
||||
}
|
||||
})
|
||||
|
||||
test('selectCloudtentaclesSourceForFulfillment 用进程内占位分散并发准备', async () => {
|
||||
const deps = {
|
||||
resolveContextBySourceKeys: (sourceKeys: unknown[] = []) => {
|
||||
const key = String(sourceKeys[0] || '').trim()
|
||||
return contexts[key as keyof typeof contexts]
|
||||
},
|
||||
listSourceLoadStats: async () => [],
|
||||
}
|
||||
const flow = {
|
||||
binding: {
|
||||
cloudSourceKeys: ['account-a', 'account-b'],
|
||||
},
|
||||
}
|
||||
const first = await selectCloudtentaclesSourceForFulfillment(flow, deps)
|
||||
const second = await selectCloudtentaclesSourceForFulfillment(flow, deps)
|
||||
|
||||
try {
|
||||
assert.equal(first.sourceKey, 'account-a')
|
||||
assert.equal(second.sourceKey, 'account-b')
|
||||
} finally {
|
||||
first.release()
|
||||
second.release()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,182 @@
|
||||
import { listKuaishouCloudSourceLoadStats } from '../../../repositories/kuaishou-cloud-task-state-repo.js'
|
||||
import { normalizeKuaishouCloudFlow, normalizeStringArray, type JsonObject } from './domain.js'
|
||||
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
|
||||
|
||||
type CloudtentaclesContext = ReturnType<typeof resolvePersistedCloudtentaclesContextBySourceKeys>
|
||||
|
||||
type CloudtentaclesSourceSelectionDeps = {
|
||||
resolveContextBySourceKeys?: typeof resolvePersistedCloudtentaclesContextBySourceKeys
|
||||
listSourceLoadStats?: typeof listKuaishouCloudSourceLoadStats
|
||||
}
|
||||
|
||||
export type CloudtentaclesSourceSelection = {
|
||||
context: CloudtentaclesContext
|
||||
sourceKey: string
|
||||
candidateCount: number
|
||||
activeCount: number
|
||||
redeemingCount: number
|
||||
inProcessCount: number
|
||||
score: number
|
||||
fixed: boolean
|
||||
release: () => void
|
||||
}
|
||||
|
||||
const inProcessSelections = new Map<string, number>()
|
||||
let selectionLockTail: Promise<void> = Promise.resolve()
|
||||
|
||||
export async function selectCloudtentaclesSourceForFulfillment(
|
||||
flowLike: unknown,
|
||||
deps: CloudtentaclesSourceSelectionDeps = {},
|
||||
): Promise<CloudtentaclesSourceSelection> {
|
||||
const flow = normalizeKuaishouCloudFlow(flowLike)
|
||||
const resolveContext =
|
||||
deps.resolveContextBySourceKeys || resolvePersistedCloudtentaclesContextBySourceKeys
|
||||
const listSourceLoadStats = deps.listSourceLoadStats || listKuaishouCloudSourceLoadStats
|
||||
const fixedSourceKey = String(flow.binding.resolvedSourceKey || '').trim()
|
||||
const candidates = normalizeCloudtentaclesSourceCandidates(
|
||||
fixedSourceKey
|
||||
? [fixedSourceKey, ...flow.binding.cloudSourceKeys]
|
||||
: flow.binding.cloudSourceKeys,
|
||||
)
|
||||
|
||||
if (fixedSourceKey) {
|
||||
const context = resolveContext([fixedSourceKey, ...flow.binding.cloudSourceKeys])
|
||||
return {
|
||||
context,
|
||||
sourceKey: context.resolvedSourceKey,
|
||||
candidateCount: candidates.length,
|
||||
activeCount: 0,
|
||||
redeemingCount: 0,
|
||||
inProcessCount: 0,
|
||||
score: 0,
|
||||
fixed: true,
|
||||
release: () => {},
|
||||
}
|
||||
}
|
||||
|
||||
return withCloudtentaclesSourceSelectionLock(async () => {
|
||||
const contexts = []
|
||||
let lastError: unknown = null
|
||||
|
||||
for (const sourceKey of candidates) {
|
||||
try {
|
||||
contexts.push(resolveContext([sourceKey]))
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
|
||||
if (contexts.length === 0) {
|
||||
if (lastError) {
|
||||
throw lastError
|
||||
}
|
||||
resolveContext(candidates)
|
||||
throw new Error('所有 cloudtentacles 账号均不可用')
|
||||
}
|
||||
|
||||
const stats = await listSourceLoadStats(contexts.map((context) => context.resolvedSourceKey))
|
||||
const statsBySourceKey = new Map(stats.map((stat) => [stat.sourceKey, stat]))
|
||||
const ranked = contexts
|
||||
.map((context, index) => {
|
||||
const stat = statsBySourceKey.get(context.resolvedSourceKey) || {
|
||||
sourceKey: context.resolvedSourceKey,
|
||||
activeCount: 0,
|
||||
redeemingCount: 0,
|
||||
}
|
||||
const activeCount = normalizeCount(stat.activeCount)
|
||||
const redeemingCount = normalizeCount(stat.redeemingCount)
|
||||
const inProcessCount = normalizeCount(inProcessSelections.get(context.resolvedSourceKey))
|
||||
const score = (activeCount + inProcessCount) * 10 + redeemingCount * 20
|
||||
|
||||
return {
|
||||
context,
|
||||
sourceKey: context.resolvedSourceKey,
|
||||
index,
|
||||
activeCount,
|
||||
redeemingCount,
|
||||
inProcessCount,
|
||||
score,
|
||||
}
|
||||
})
|
||||
.sort(
|
||||
(left, right) =>
|
||||
left.score - right.score ||
|
||||
left.activeCount - right.activeCount ||
|
||||
left.redeemingCount - right.redeemingCount ||
|
||||
left.index - right.index ||
|
||||
left.sourceKey.localeCompare(right.sourceKey),
|
||||
)
|
||||
|
||||
const selected = ranked[0]
|
||||
if (!selected) {
|
||||
throw new Error('所有 cloudtentacles 账号均不可用')
|
||||
}
|
||||
|
||||
reserveCloudtentaclesSource(selected.sourceKey)
|
||||
|
||||
return {
|
||||
context: selected.context,
|
||||
sourceKey: selected.sourceKey,
|
||||
candidateCount: contexts.length,
|
||||
activeCount: selected.activeCount,
|
||||
redeemingCount: selected.redeemingCount,
|
||||
inProcessCount: selected.inProcessCount,
|
||||
score: selected.score,
|
||||
fixed: false,
|
||||
release: () => releaseCloudtentaclesSource(selected.sourceKey),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function buildCloudtentaclesSourceSelectionEventDetail(
|
||||
selection: CloudtentaclesSourceSelection,
|
||||
): JsonObject {
|
||||
return {
|
||||
sourceKey: selection.sourceKey,
|
||||
candidateCount: selection.candidateCount,
|
||||
activeCount: selection.activeCount,
|
||||
redeemingCount: selection.redeemingCount,
|
||||
inProcessCount: selection.inProcessCount,
|
||||
score: selection.score,
|
||||
fixed: selection.fixed,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCloudtentaclesSourceCandidates(sourceKeys: unknown[] = []) {
|
||||
return Array.from(new Set(normalizeStringArray(sourceKeys)))
|
||||
}
|
||||
|
||||
async function withCloudtentaclesSourceSelectionLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
let releaseLock: () => void = () => {}
|
||||
const waitForTurn = selectionLockTail
|
||||
selectionLockTail = new Promise<void>((resolve) => {
|
||||
releaseLock = resolve
|
||||
})
|
||||
|
||||
await waitForTurn
|
||||
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
releaseLock()
|
||||
}
|
||||
}
|
||||
|
||||
function reserveCloudtentaclesSource(sourceKey: string) {
|
||||
inProcessSelections.set(sourceKey, normalizeCount(inProcessSelections.get(sourceKey)) + 1)
|
||||
}
|
||||
|
||||
function releaseCloudtentaclesSource(sourceKey: string) {
|
||||
const nextCount = normalizeCount(inProcessSelections.get(sourceKey)) - 1
|
||||
if (nextCount > 0) {
|
||||
inProcessSelections.set(sourceKey, nextCount)
|
||||
return
|
||||
}
|
||||
|
||||
inProcessSelections.delete(sourceKey)
|
||||
}
|
||||
|
||||
function normalizeCount(value: unknown) {
|
||||
const count = Number(value || 0) || 0
|
||||
return Number.isFinite(count) && count > 0 ? count : 0
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { syncDeliveryTasksForOrderWithDeps } from './delivery-task-service.js'
|
||||
|
||||
test('syncDeliveryTasksForOrder 多账号候选时不提前固定 cloudtentacles 账号', async () => {
|
||||
const createdInputs: any[] = []
|
||||
|
||||
await syncDeliveryTasksForOrderWithDeps(
|
||||
{
|
||||
id: 1,
|
||||
pay_status: 'pending_payment',
|
||||
provider: 'open_91',
|
||||
platform: 'kuaishou',
|
||||
shop_id: 'shop-1',
|
||||
shop_name: '测试店铺',
|
||||
platform_order_id: '2614900602069169',
|
||||
} as any,
|
||||
[
|
||||
{
|
||||
id: 10,
|
||||
order_id: 1,
|
||||
sku_code: 'SKU-1',
|
||||
sku_name: '荣耀勋章礼包(30个)',
|
||||
quantity: 1,
|
||||
item_snapshot_json: {
|
||||
cloudtentacles: {
|
||||
cloudSourceKeys: ['account-a', 'account-b'],
|
||||
resolvedSourceKey: 'account-a',
|
||||
cloudSkuId: 74,
|
||||
cloudSkuName: '荣耀勋章礼包(30个)',
|
||||
deliveryItems: [
|
||||
{
|
||||
cloudSkuId: 74,
|
||||
cloudSkuName: '荣耀勋章礼包(30个)',
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
] as any,
|
||||
{
|
||||
listTasksByOrderId: async () => [],
|
||||
getFulfillmentProfileByKey: async () => ({
|
||||
id: 9,
|
||||
profile_key: 'kuaishou_ct_assisted',
|
||||
profile_name: '快手 cloud 履约',
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
}),
|
||||
createTask: async (input: any) => {
|
||||
createdInputs.push(input)
|
||||
return {
|
||||
id: 100,
|
||||
order_item_id: input.orderItemId,
|
||||
context_json: input.contextJson,
|
||||
task_status: input.taskStatus,
|
||||
executor_key: input.executorKey,
|
||||
} as any
|
||||
},
|
||||
nowIso: () => '2026-05-29T09:05:46.658Z',
|
||||
randomId: () => 'DT-test',
|
||||
},
|
||||
)
|
||||
|
||||
const context = JSON.parse(createdInputs[0].contextJson)
|
||||
assert.deepEqual(context.kuaishouCloudFulfillment.binding.cloudSourceKeys, [
|
||||
'account-a',
|
||||
'account-b',
|
||||
])
|
||||
assert.equal(context.kuaishouCloudFulfillment.binding.resolvedSourceKey, '')
|
||||
})
|
||||
|
||||
test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles 账号', async () => {
|
||||
const createdInputs: any[] = []
|
||||
|
||||
await syncDeliveryTasksForOrderWithDeps(
|
||||
{
|
||||
id: 2,
|
||||
pay_status: 'pending_payment',
|
||||
provider: 'open_91',
|
||||
platform: 'kuaishou',
|
||||
shop_id: 'shop-1',
|
||||
shop_name: '测试店铺',
|
||||
platform_order_id: '2614900602069170',
|
||||
} as any,
|
||||
[
|
||||
{
|
||||
id: 11,
|
||||
order_id: 2,
|
||||
sku_code: 'SKU-1',
|
||||
sku_name: '荣耀勋章礼包(30个)',
|
||||
quantity: 1,
|
||||
item_snapshot_json: {
|
||||
cloudtentacles: {
|
||||
cloudSourceKeys: ['account-a'],
|
||||
cloudSkuId: 74,
|
||||
cloudSkuName: '荣耀勋章礼包(30个)',
|
||||
},
|
||||
},
|
||||
},
|
||||
] as any,
|
||||
{
|
||||
listTasksByOrderId: async () => [],
|
||||
getFulfillmentProfileByKey: async () => ({
|
||||
id: 9,
|
||||
profile_key: 'kuaishou_ct_assisted',
|
||||
profile_name: '快手 cloud 履约',
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
}),
|
||||
createTask: async (input: any) => {
|
||||
createdInputs.push(input)
|
||||
return {
|
||||
id: 101,
|
||||
order_item_id: input.orderItemId,
|
||||
context_json: input.contextJson,
|
||||
task_status: input.taskStatus,
|
||||
executor_key: input.executorKey,
|
||||
} as any
|
||||
},
|
||||
nowIso: () => '2026-05-29T09:05:46.658Z',
|
||||
randomId: () => 'DT-test',
|
||||
},
|
||||
)
|
||||
|
||||
const context = JSON.parse(createdInputs[0].contextJson)
|
||||
assert.deepEqual(context.kuaishouCloudFulfillment.binding.cloudSourceKeys, ['account-a'])
|
||||
assert.equal(context.kuaishouCloudFulfillment.binding.resolvedSourceKey, 'account-a')
|
||||
})
|
||||
@@ -53,13 +53,12 @@ type DeliveryTaskDeps = {
|
||||
randomId?: (prefix?: string) => string
|
||||
}
|
||||
|
||||
type RuntimeDeliveryTaskDeps = Required<Pick<
|
||||
DeliveryTaskDeps,
|
||||
'updateTask'
|
||||
| 'createTaskClaimToken'
|
||||
| 'notifyTaskAutoManualReview'
|
||||
| 'nowIso'
|
||||
>>
|
||||
type RuntimeDeliveryTaskDeps = Required<
|
||||
Pick<
|
||||
DeliveryTaskDeps,
|
||||
'updateTask' | 'createTaskClaimToken' | 'notifyTaskAutoManualReview' | 'nowIso'
|
||||
>
|
||||
>
|
||||
|
||||
type TaskContext = {
|
||||
[key: string]: unknown
|
||||
@@ -103,11 +102,18 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
}
|
||||
|
||||
const itemMap = new Map(orderItems.map((item) => [item.id, item]))
|
||||
const preparedTasks = await Promise.all(existingTasks.map((task) => preparePaidTask({
|
||||
...task,
|
||||
skuCode: itemMap.get(task.order_item_id)?.sku_code || '',
|
||||
skuName: itemMap.get(task.order_item_id)?.sku_name || '',
|
||||
}, runtimeDeps)))
|
||||
const preparedTasks = await Promise.all(
|
||||
existingTasks.map((task) =>
|
||||
preparePaidTask(
|
||||
{
|
||||
...task,
|
||||
skuCode: itemMap.get(task.order_item_id)?.sku_code || '',
|
||||
skuName: itemMap.get(task.order_item_id)?.sku_name || '',
|
||||
},
|
||||
runtimeDeps,
|
||||
),
|
||||
),
|
||||
)
|
||||
return preparedTasks.filter(isTaskRow)
|
||||
}
|
||||
|
||||
@@ -125,6 +131,11 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles)
|
||||
const kuaishouConsumeConfig = parseJsonObject(fulfillmentConfig.kuaishouConsume)
|
||||
const kuaishouShopConfig = parseJsonObject(fulfillmentConfig.kuaishouShop)
|
||||
const cloudSourceKeys = normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys)
|
||||
const resolvedCloudSourceKey =
|
||||
cloudSourceKeys.length === 1
|
||||
? String(cloudtentaclesConfig.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
|
||||
: ''
|
||||
const deliveryItems = normalizeCloudDeliveryItems(cloudtentaclesConfig)
|
||||
const primaryDeliveryItem = deliveryItems[0] || {
|
||||
cloudSkuId: Number(cloudtentaclesConfig.skuId || 0) || 0,
|
||||
@@ -134,9 +145,8 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
|
||||
for (let index = 0; index < quantity; index += 1) {
|
||||
const createdAt = getNowIso()
|
||||
const initialStatus = order.pay_status === 'paid'
|
||||
? resolvePaidTaskStatus(profile)
|
||||
: TASK_STATUS.PENDING_PAYMENT
|
||||
const initialStatus =
|
||||
order.pay_status === 'paid' ? resolvePaidTaskStatus(profile) : TASK_STATUS.PENDING_PAYMENT
|
||||
|
||||
const task = await createDeliveryTask({
|
||||
orderId: order.id,
|
||||
@@ -168,78 +178,90 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
skuName: item.sku_name,
|
||||
kuaishouCloudFulfillment: isKuaishouCloudExecutor(profile.executor_key)
|
||||
? {
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
configId: String(fulfillmentConfig.configId || '').trim(),
|
||||
internalSkuCode: item.sku_code,
|
||||
internalSkuName: item.sku_name,
|
||||
deliveryItems,
|
||||
ticket: {
|
||||
code: '',
|
||||
status: 'pending',
|
||||
capturedAt: null,
|
||||
capturedBy: null,
|
||||
verifiedAt: null,
|
||||
oid: '',
|
||||
formToken: '',
|
||||
leftCount: 0,
|
||||
goodsTitle: '',
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: 'pending',
|
||||
cloudSourceKeys: normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys),
|
||||
resolvedSourceKey: String(cloudtentaclesConfig.resolvedSourceKey || '').trim(),
|
||||
skuId: primaryDeliveryItem.cloudSkuId,
|
||||
skuName: primaryDeliveryItem.cloudSkuName,
|
||||
vnKey: '1',
|
||||
vnId: 0,
|
||||
vnPhone: '',
|
||||
bindUrl: '',
|
||||
bindPreparedAt: null,
|
||||
bindExpiresAt: null,
|
||||
bindProbeAt: null,
|
||||
bindProbeStatus: '',
|
||||
bindProbeMessage: '',
|
||||
},
|
||||
role: {
|
||||
status: 'pending',
|
||||
name: '',
|
||||
rid: '',
|
||||
refreshedAt: null,
|
||||
errorMessage: '',
|
||||
rawInfo: null,
|
||||
},
|
||||
purchase: {
|
||||
autoBuyEnabled: cloudtentaclesConfig.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(cloudtentaclesConfig.minAssetReserve || 0) || 0,
|
||||
usedKnapsack: false,
|
||||
purchaseTriggered: false,
|
||||
assetBefore: 0,
|
||||
assetAfter: 0,
|
||||
purchaseAt: null,
|
||||
},
|
||||
dispatch: {
|
||||
status: 'pending',
|
||||
dispatchAt: null,
|
||||
dispatchBy: null,
|
||||
sendType: 0,
|
||||
note: '',
|
||||
},
|
||||
returnNumber: {
|
||||
status: 'pending',
|
||||
returnedAt: null,
|
||||
returnedBy: null,
|
||||
autoReturnEnabled: cloudtentaclesConfig.autoReturnNumberAfterDispatch === true,
|
||||
},
|
||||
consume: {
|
||||
status: 'pending',
|
||||
shopId: String(kuaishouConsumeConfig.shopId || kuaishouShopConfig.shopId || itemSnapshot.shopId || order.shop_id || '').trim(),
|
||||
shopName: String(kuaishouConsumeConfig.shopName || kuaishouShopConfig.kshopName || itemSnapshot.shopName || order.shop_name || '').trim(),
|
||||
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
|
||||
consumedAt: null,
|
||||
errorMessage: '',
|
||||
},
|
||||
notes: String(fulfillmentConfig.notes || '').trim(),
|
||||
}
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
configId: String(fulfillmentConfig.configId || '').trim(),
|
||||
internalSkuCode: item.sku_code,
|
||||
internalSkuName: item.sku_name,
|
||||
deliveryItems,
|
||||
ticket: {
|
||||
code: '',
|
||||
status: 'pending',
|
||||
capturedAt: null,
|
||||
capturedBy: null,
|
||||
verifiedAt: null,
|
||||
oid: '',
|
||||
formToken: '',
|
||||
leftCount: 0,
|
||||
goodsTitle: '',
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: 'pending',
|
||||
cloudSourceKeys,
|
||||
resolvedSourceKey: resolvedCloudSourceKey,
|
||||
skuId: primaryDeliveryItem.cloudSkuId,
|
||||
skuName: primaryDeliveryItem.cloudSkuName,
|
||||
vnKey: '1',
|
||||
vnId: 0,
|
||||
vnPhone: '',
|
||||
bindUrl: '',
|
||||
bindPreparedAt: null,
|
||||
bindExpiresAt: null,
|
||||
bindProbeAt: null,
|
||||
bindProbeStatus: '',
|
||||
bindProbeMessage: '',
|
||||
},
|
||||
role: {
|
||||
status: 'pending',
|
||||
name: '',
|
||||
rid: '',
|
||||
refreshedAt: null,
|
||||
errorMessage: '',
|
||||
rawInfo: null,
|
||||
},
|
||||
purchase: {
|
||||
autoBuyEnabled: cloudtentaclesConfig.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(cloudtentaclesConfig.minAssetReserve || 0) || 0,
|
||||
usedKnapsack: false,
|
||||
purchaseTriggered: false,
|
||||
assetBefore: 0,
|
||||
assetAfter: 0,
|
||||
purchaseAt: null,
|
||||
},
|
||||
dispatch: {
|
||||
status: 'pending',
|
||||
dispatchAt: null,
|
||||
dispatchBy: null,
|
||||
sendType: 0,
|
||||
note: '',
|
||||
},
|
||||
returnNumber: {
|
||||
status: 'pending',
|
||||
returnedAt: null,
|
||||
returnedBy: null,
|
||||
autoReturnEnabled: cloudtentaclesConfig.autoReturnNumberAfterDispatch === true,
|
||||
},
|
||||
consume: {
|
||||
status: 'pending',
|
||||
shopId: String(
|
||||
kuaishouConsumeConfig.shopId ||
|
||||
kuaishouShopConfig.shopId ||
|
||||
itemSnapshot.shopId ||
|
||||
order.shop_id ||
|
||||
'',
|
||||
).trim(),
|
||||
shopName: String(
|
||||
kuaishouConsumeConfig.shopName ||
|
||||
kuaishouShopConfig.kshopName ||
|
||||
itemSnapshot.shopName ||
|
||||
order.shop_name ||
|
||||
'',
|
||||
).trim(),
|
||||
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
|
||||
consumedAt: null,
|
||||
errorMessage: '',
|
||||
},
|
||||
notes: String(fulfillmentConfig.notes || '').trim(),
|
||||
}
|
||||
: null,
|
||||
}),
|
||||
createdAt,
|
||||
@@ -344,7 +366,9 @@ function parseTaskContext(task: { context_json?: unknown } | null | undefined):
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as TaskContext : {}
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? (parsed as TaskContext)
|
||||
: {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
@@ -361,7 +385,9 @@ function parseJsonObject(value: unknown): JsonObject {
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as JsonObject : {}
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? (parsed as JsonObject)
|
||||
: {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
@@ -385,7 +411,11 @@ async function resolveDynamicCloudtentaclesProfile(
|
||||
quantity: 1,
|
||||
}
|
||||
|
||||
if (!primaryDeliveryItem.cloudSkuId || !primaryDeliveryItem.cloudSkuName || cloudSourceKeys.length === 0) {
|
||||
if (
|
||||
!primaryDeliveryItem.cloudSkuId ||
|
||||
!primaryDeliveryItem.cloudSkuName ||
|
||||
cloudSourceKeys.length === 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -409,7 +439,10 @@ async function resolveDynamicCloudtentaclesProfile(
|
||||
cloudSourceKeys,
|
||||
skuId: primaryDeliveryItem.cloudSkuId,
|
||||
skuName: primaryDeliveryItem.cloudSkuName,
|
||||
resolvedSourceKey: String(cloudtentacles.resolvedSourceKey || '').trim(),
|
||||
resolvedSourceKey:
|
||||
cloudSourceKeys.length === 1
|
||||
? String(cloudtentacles.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
|
||||
: '',
|
||||
deliveryItems,
|
||||
vnKey: '1',
|
||||
autoBuyEnabled: true,
|
||||
@@ -421,9 +454,10 @@ async function resolveDynamicCloudtentaclesProfile(
|
||||
shopName: String(snapshot.shopName || '').trim(),
|
||||
autoConsumeAfterDispatch: false,
|
||||
},
|
||||
notes: cloudtentacles.matchMode === 'cloudtentacles_override'
|
||||
? '91卡券商品名命中 cloudtentacles 覆盖规则'
|
||||
: '91卡券商品名自动匹配 cloudtentacles 商品',
|
||||
notes:
|
||||
cloudtentacles.matchMode === 'cloudtentacles_override'
|
||||
? '91卡券商品名命中 cloudtentacles 覆盖规则'
|
||||
: '91卡券商品名自动匹配 cloudtentacles 商品',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -433,9 +467,7 @@ function normalizeStringArray(value: unknown): string[] {
|
||||
return []
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(value.map((item) => String(item || '').trim()).filter(Boolean)),
|
||||
)
|
||||
return Array.from(new Set(value.map((item) => String(item || '').trim()).filter(Boolean)))
|
||||
}
|
||||
|
||||
function normalizeCloudDeliveryItems(value: JsonObject): Array<{
|
||||
@@ -446,7 +478,9 @@ function normalizeCloudDeliveryItems(value: JsonObject): Array<{
|
||||
const rawItems = Array.isArray(value.deliveryItems) ? value.deliveryItems : []
|
||||
const items = rawItems
|
||||
.map((item) => normalizeCloudDeliveryItem(item))
|
||||
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } => Boolean(item))
|
||||
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
|
||||
Boolean(item),
|
||||
)
|
||||
|
||||
if (items.length > 0) {
|
||||
return mergeCloudDeliveryItems(items)
|
||||
@@ -457,17 +491,18 @@ function normalizeCloudDeliveryItems(value: JsonObject): Array<{
|
||||
return []
|
||||
}
|
||||
|
||||
return [{
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(value.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
}]
|
||||
return [
|
||||
{
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(value.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeCloudDeliveryItem(value: unknown) {
|
||||
const source = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
|
||||
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user