统一前后端代码格式化配置
This commit is contained in:
@@ -50,10 +50,7 @@ function buildTask(overrides: Partial<TaskRow> = {}): TaskRow {
|
||||
test('buildAffiliateDashClientOrderNo uses the platform order number for a single task', () => {
|
||||
const task = buildTask()
|
||||
|
||||
assert.equal(
|
||||
buildAffiliateDashClientOrderNo(task, [task]),
|
||||
'2622300001260431',
|
||||
)
|
||||
assert.equal(buildAffiliateDashClientOrderNo(task, [task]), '2622300001260431')
|
||||
})
|
||||
|
||||
test('buildAffiliateDashClientOrderNo appends a stable position for split affiliate-dash tasks', () => {
|
||||
|
||||
@@ -52,10 +52,13 @@ export async function prepareAffiliateDashTask(task: TaskRow) {
|
||||
}
|
||||
|
||||
if (!flow.sku) {
|
||||
throw createHttpError('affiliate-dash 商品 sku 缺失,请配置 91 商品 → affiliate_dash sku 映射', {
|
||||
statusCode: 409,
|
||||
errorCode: 'affiliate_dash_sku_missing',
|
||||
})
|
||||
throw createHttpError(
|
||||
'affiliate-dash 商品 sku 缺失,请配置 91 商品 → affiliate_dash sku 映射',
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'affiliate_dash_sku_missing',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const siblingTasks = await listTasksByOrderId(task.order_id)
|
||||
@@ -97,13 +100,18 @@ export async function prepareAffiliateDashTask(task: TaskRow) {
|
||||
)
|
||||
}
|
||||
if (!consumeResult.ok) {
|
||||
logIntegration('[affiliate-dash]', '建单成功但电子凭证核销失败,delivered 回调将兜底重试', {
|
||||
taskId: task.id,
|
||||
orderNo: order.orderNo,
|
||||
voucherCount: consumeResult.vouchers.length,
|
||||
failedCount: consumeResult.failed.length,
|
||||
errorMessage: consumeResult.failed[0]?.errorMessage || '',
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[affiliate-dash]',
|
||||
'建单成功但电子凭证核销失败,delivered 回调将兜底重试',
|
||||
{
|
||||
taskId: task.id,
|
||||
orderNo: order.orderNo,
|
||||
voucherCount: consumeResult.vouchers.length,
|
||||
failedCount: consumeResult.failed.length,
|
||||
errorMessage: consumeResult.failed[0]?.errorMessage || '',
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
nextFlow.consumeStatus = 'not_required'
|
||||
@@ -231,11 +239,16 @@ export async function syncAffiliateDashTaskStatus(
|
||||
resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理'
|
||||
lastError = resultMessage
|
||||
nextFlow.consumeStatus = 'failed'
|
||||
logIntegration('[affiliate-dash]', 'affiliate-dash 履约完成但核销失败', {
|
||||
taskId: task.id,
|
||||
orderNo: flow.orderNo,
|
||||
errorMessage: resultMessage,
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[affiliate-dash]',
|
||||
'affiliate-dash 履约完成但核销失败',
|
||||
{
|
||||
taskId: task.id,
|
||||
orderNo: flow.orderNo,
|
||||
errorMessage: resultMessage,
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -256,7 +269,12 @@ export async function syncAffiliateDashTaskStatus(
|
||||
}
|
||||
|
||||
const isAlreadyTerminal = (
|
||||
[TASK_STATUS.REDEEMED, TASK_STATUS.MANUAL_REVIEW, TASK_STATUS.CLOSED, TASK_STATUS.FAILED] as string[]
|
||||
[
|
||||
TASK_STATUS.REDEEMED,
|
||||
TASK_STATUS.MANUAL_REVIEW,
|
||||
TASK_STATUS.CLOSED,
|
||||
TASK_STATUS.FAILED,
|
||||
] as string[]
|
||||
).includes(task.task_status)
|
||||
// 终态保护:已收敛成功的任务,非 delivered 状态不允许降级(如轮询时平台详情短暂返回
|
||||
// delivering/paid 会把 REDEEMED 打回 REDEEMING,导致结果页闪烁/倒退)。
|
||||
@@ -316,9 +334,8 @@ export type AffiliateDashFlow = {
|
||||
}
|
||||
|
||||
export function normalizeAffiliateDashFlow(value: unknown): AffiliateDashFlow {
|
||||
const source = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
|
||||
return {
|
||||
flowType: 'affiliate_dash',
|
||||
@@ -397,10 +414,15 @@ async function preflightAffiliateDashWallet(sku: string) {
|
||||
if ((error as { errorCode?: string })?.errorCode === 'affiliate_dash_wallet_not_enough') {
|
||||
throw error
|
||||
}
|
||||
logIntegration('[affiliate-dash]', '余额预检失败(忽略,继续下单)', {
|
||||
sku,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[affiliate-dash]',
|
||||
'余额预检失败(忽略,继续下单)',
|
||||
{
|
||||
sku,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ import { resolveTaskDeliveryLink } from './delivery-link-service.js'
|
||||
import type { TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
primary_claim_token: 'cloud-token',
|
||||
primary_claim_expires_at: '2026-07-09T00:00:00.000Z',
|
||||
}))
|
||||
const result = await resolveTaskDeliveryLink(
|
||||
createTask({
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
primary_claim_token: 'cloud-token',
|
||||
primary_claim_expires_at: '2026-07-09T00:00:00.000Z',
|
||||
}),
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: buildClaimUrl('cloud-token'),
|
||||
@@ -19,11 +21,13 @@ test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async (
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou-industry',
|
||||
claim_token: 'industry-token',
|
||||
claim_expires_at: '2026-07-09T08:00:00.000Z',
|
||||
}))
|
||||
const result = await resolveTaskDeliveryLink(
|
||||
createTask({
|
||||
executor_key: 'kuaishou-industry',
|
||||
claim_token: 'industry-token',
|
||||
claim_expires_at: '2026-07-09T08:00:00.000Z',
|
||||
}),
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: buildClaimUrl('industry-token'),
|
||||
@@ -32,18 +36,20 @@ test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', asyn
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 为 feifei 任务返回本站领取链接', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou_feifei',
|
||||
primary_claim_token: 'feifei-token',
|
||||
primary_claim_expires_at: '2026-07-09T12:00:00.000Z',
|
||||
context_json: {
|
||||
kuaishouFeifei: {
|
||||
h5: {
|
||||
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
|
||||
const result = await resolveTaskDeliveryLink(
|
||||
createTask({
|
||||
executor_key: 'kuaishou_feifei',
|
||||
primary_claim_token: 'feifei-token',
|
||||
primary_claim_expires_at: '2026-07-09T12:00:00.000Z',
|
||||
context_json: {
|
||||
kuaishouFeifei: {
|
||||
h5: {
|
||||
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: buildClaimUrl('feifei-token'),
|
||||
@@ -52,9 +58,11 @@ test('resolveTaskDeliveryLink 为 feifei 任务返回本站领取链接', async
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 对人工履约任务返回 null', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'manual_dispatch',
|
||||
}))
|
||||
const result = await resolveTaskDeliveryLink(
|
||||
createTask({
|
||||
executor_key: 'manual_dispatch',
|
||||
}),
|
||||
)
|
||||
|
||||
assert.equal(result, null)
|
||||
})
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
export type TaskDeliveryLink = FulfillmentDeliveryLink
|
||||
|
||||
export async function resolveTaskDeliveryLink(
|
||||
task: TaskRow,
|
||||
): Promise<TaskDeliveryLink | null> {
|
||||
export async function resolveTaskDeliveryLink(task: TaskRow): Promise<TaskDeliveryLink | null> {
|
||||
return resolveFulfillmentDeliveryLink(task)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { buildClaimUrl } from '../../claim/claim-service.js'
|
||||
import {
|
||||
shouldEnsureKuaishouCloudClaimLink,
|
||||
TASK_STATUS,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { shouldEnsureKuaishouCloudClaimLink, TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
confirmKuaishouCloudTaskRole,
|
||||
dispatchKuaishouCloudFulfillmentTask,
|
||||
@@ -95,9 +92,7 @@ async function prepareBinding(
|
||||
options: FulfillmentActionOptions = {},
|
||||
): Promise<FulfillmentActionResult> {
|
||||
// 后台/显式 force:旧号可被 CT 回收,忽略退号失败并取新号+新绑链
|
||||
const force =
|
||||
options.force === true ||
|
||||
String(options.source || '').includes('admin_')
|
||||
const force = options.force === true || String(options.source || '').includes('admin_')
|
||||
const result = await prepareKuaishouCloudFulfillmentTask(task, {
|
||||
source: options.source || 'executor_prepare_binding',
|
||||
actor: options.actor,
|
||||
|
||||
@@ -78,51 +78,30 @@ async function runExecutorAction(
|
||||
return handler(task, options)
|
||||
}
|
||||
|
||||
export function prepareFulfillmentBinding(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function prepareFulfillmentBinding(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'prepareBinding', options)
|
||||
}
|
||||
|
||||
export function rebindFulfillmentRole(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function rebindFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'rebindRole', options)
|
||||
}
|
||||
|
||||
export function refreshFulfillmentRole(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function refreshFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'refreshRole', options)
|
||||
}
|
||||
|
||||
export function confirmFulfillmentRole(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function confirmFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'confirmRole', options)
|
||||
}
|
||||
|
||||
export function redeemFulfillmentTask(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function redeemFulfillmentTask(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'redeemTask', options)
|
||||
}
|
||||
|
||||
export function dispatchFulfillmentTask(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function dispatchFulfillmentTask(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'dispatchTask', options)
|
||||
}
|
||||
|
||||
export function returnFulfillmentNumber(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function returnFulfillmentNumber(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'returnNumber', options)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ export const FULFILLMENT_EXECUTOR_KEYS = {
|
||||
} as const
|
||||
|
||||
export type FulfillmentExecutorKey =
|
||||
(typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS] | (string & {})
|
||||
| (typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS]
|
||||
| (string & {})
|
||||
|
||||
export type FulfillmentDeliveryLink = {
|
||||
claimUrl: string
|
||||
@@ -53,10 +54,7 @@ export type FulfillmentActionResult = {
|
||||
|
||||
export type FulfillmentExecutor = {
|
||||
key: FulfillmentExecutorKey
|
||||
preparePaidTask?: (
|
||||
task: TaskRow,
|
||||
deps: FulfillmentPrepareDeps,
|
||||
) => Promise<TaskRow | null>
|
||||
preparePaidTask?: (task: TaskRow, deps: FulfillmentPrepareDeps) => Promise<TaskRow | null>
|
||||
resolveDeliveryLink?: (task: TaskRow) => Promise<FulfillmentDeliveryLink | null>
|
||||
/** lewan:准备绑定资源(虚拟号 / bindUrl) */
|
||||
prepareBinding?: (
|
||||
@@ -104,8 +102,10 @@ export function normalizeExecutorKey(value: unknown): FulfillmentExecutorKey {
|
||||
|
||||
export function isKuaishouCloudExecutor(value: unknown): boolean {
|
||||
const executorKey = normalizeExecutorKey(value)
|
||||
return executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD ||
|
||||
return (
|
||||
executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD ||
|
||||
executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY
|
||||
)
|
||||
}
|
||||
|
||||
export function isKuaishouFeifeiExecutor(value: unknown): boolean {
|
||||
|
||||
@@ -65,7 +65,8 @@ test('selectCloudtentaclesSourceForFulfillment 无号码时忽略残留固定账
|
||||
},
|
||||
},
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
resolveContextBySourceKeys: (sourceKeys) =>
|
||||
contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
listSourceLoadStats: async () => [
|
||||
{ sourceKey: 'account-a', activeCount: 20, redeemingCount: 0 },
|
||||
{ sourceKey: 'account-b', activeCount: 1, redeemingCount: 0 },
|
||||
@@ -86,7 +87,8 @@ test('selectCloudtentaclesSourceForFulfillment 跳过真实占用已达20的账
|
||||
const selection = await selectCloudtentaclesSourceForFulfillment(
|
||||
{ binding: { cloudSourceKeys: ['account-a', 'account-b'] } },
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
resolveContextBySourceKeys: (sourceKeys) =>
|
||||
contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
listSourceLoadStats: async () => [],
|
||||
listVirtualNumbers: async (payload) => {
|
||||
const count = payload.sourceKey === 'account-a' ? 20 : 3
|
||||
@@ -115,12 +117,14 @@ test('selectCloudtentaclesSourceForFulfillment 不把 status=0 的空闲号码
|
||||
const selection = await selectCloudtentaclesSourceForFulfillment(
|
||||
{ binding: { cloudSourceKeys: ['account-a', 'account-b'] } },
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
resolveContextBySourceKeys: (sourceKeys) =>
|
||||
contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
listSourceLoadStats: async () => [],
|
||||
listVirtualNumbers: async (payload) => {
|
||||
const items = payload.sourceKey === 'account-a'
|
||||
? Array.from({ length: 20 }, (_, id) => ({ id: id + 1, status: 0 }))
|
||||
: Array.from({ length: 3 }, (_, id) => ({ id: id + 1, status: 1 }))
|
||||
const items =
|
||||
payload.sourceKey === 'account-a'
|
||||
? Array.from({ length: 20 }, (_, id) => ({ id: id + 1, status: 0 }))
|
||||
: Array.from({ length: 3 }, (_, id) => ({ id: id + 1, status: 1 }))
|
||||
return {
|
||||
baseUrl: 'https://cloud.example.com',
|
||||
key: '1',
|
||||
@@ -145,7 +149,8 @@ test('selectCloudtentaclesSourceForFulfillment 真实占用优先于数据库历
|
||||
const selection = await selectCloudtentaclesSourceForFulfillment(
|
||||
{ binding: { cloudSourceKeys: ['account-a', 'account-b'] } },
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
resolveContextBySourceKeys: (sourceKeys) =>
|
||||
contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
listSourceLoadStats: async () => [
|
||||
{ sourceKey: 'account-a', activeCount: 99, redeemingCount: 10 },
|
||||
{ sourceKey: 'account-b', activeCount: 0, redeemingCount: 0 },
|
||||
|
||||
@@ -10,14 +10,8 @@ import {
|
||||
} from './domain.js'
|
||||
|
||||
test('isKuaishouCloudDispatchSucceeded 识别 dispatch.success', () => {
|
||||
assert.equal(
|
||||
isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'success' } }),
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'pending' } }),
|
||||
false,
|
||||
)
|
||||
assert.equal(isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'success' } }), true)
|
||||
assert.equal(isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'pending' } }), false)
|
||||
})
|
||||
|
||||
test('isKuaishouCloudBindingMutationFrozen:dispatch.success 即使 status=waiting_binding 也冻结', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHttpError } from "../../../utils/http.js";
|
||||
import { normalizeProductName } from "../product-resolution-service.js";
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { normalizeProductName } from '../product-resolution-service.js'
|
||||
import {
|
||||
appointCloudtentaclesVirtualNumber,
|
||||
backCloudtentaclesVirtualNumber,
|
||||
@@ -7,107 +7,95 @@ import {
|
||||
generateCloudtentaclesLoginCode,
|
||||
getCloudtentaclesBindUrl,
|
||||
verifyCloudtentaclesLoginCode,
|
||||
} from "../../platforms/cloudtentacles/virtual-number-service.js";
|
||||
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from "./domain.js";
|
||||
import { logInfo } from "../../../utils/logger.js";
|
||||
} from '../../platforms/cloudtentacles/virtual-number-service.js'
|
||||
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from './domain.js'
|
||||
import { logInfo } from '../../../utils/logger.js'
|
||||
|
||||
/** 账号号码配额占满后冷却:避免领取页轮询 / open-91 交付每轮都狂打上游 */
|
||||
const APPOINT_QUOTA_COOLDOWN_MS = 60_000;
|
||||
const appointQuotaCooldowns = new Map<string, number>();
|
||||
const APPOINT_QUOTA_COOLDOWN_MS = 60_000
|
||||
const appointQuotaCooldowns = new Map<string, number>()
|
||||
|
||||
export function resolveKuaishouCloudBindingResources(
|
||||
flow: JsonObject,
|
||||
{ skuItems = [], knapsackItems = [] }: { skuItems?: unknown[], knapsackItems?: unknown[] } = {}
|
||||
{ skuItems = [], knapsackItems = [] }: { skuItems?: unknown[]; knapsackItems?: unknown[] } = {},
|
||||
) {
|
||||
const normalizedSkuItems = Array.isArray(skuItems)
|
||||
? skuItems.filter(isCloudSkuLikeItem)
|
||||
: [];
|
||||
const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : []
|
||||
const normalizedKnapsackItems = Array.isArray(knapsackItems)
|
||||
? knapsackItems.filter(isCloudSkuLikeItem)
|
||||
: [];
|
||||
const currentSkuId = Number(flow?.binding?.skuId || 0) || 0;
|
||||
const currentSkuName = String(flow?.binding?.skuName || "").trim();
|
||||
const nameCandidates = collectKuaishouCloudNameCandidates(flow);
|
||||
: []
|
||||
const currentSkuId = Number(flow?.binding?.skuId || 0) || 0
|
||||
const currentSkuName = String(flow?.binding?.skuName || '').trim()
|
||||
const nameCandidates = collectKuaishouCloudNameCandidates(flow)
|
||||
|
||||
const skuItemById =
|
||||
currentSkuId > 0
|
||||
? normalizedSkuItems.find(
|
||||
(item) => Number(item.id || 0) === currentSkuId
|
||||
) || null
|
||||
: null;
|
||||
? normalizedSkuItems.find((item) => Number(item.id || 0) === currentSkuId) || null
|
||||
: null
|
||||
const knapsackItemById =
|
||||
currentSkuId > 0
|
||||
? normalizedKnapsackItems.find(
|
||||
(item) => Number(item.id || 0) === currentSkuId
|
||||
) || null
|
||||
: null;
|
||||
? normalizedKnapsackItems.find((item) => Number(item.id || 0) === currentSkuId) || null
|
||||
: null
|
||||
|
||||
if (skuItemById || knapsackItemById) {
|
||||
const matchedItem = skuItemById || knapsackItemById;
|
||||
const matchedItem = skuItemById || knapsackItemById
|
||||
return {
|
||||
skuId: Number(matchedItem?.id || 0) || 0,
|
||||
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
|
||||
skuName: String(currentSkuName || matchedItem?.name || '').trim(),
|
||||
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
skuItem: skuItemById,
|
||||
knapsackItem: knapsackItemById,
|
||||
resolvedByName: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const matchedSkuItem = findCloudItemByNames(
|
||||
normalizedSkuItems,
|
||||
nameCandidates
|
||||
);
|
||||
const matchedSkuItem = findCloudItemByNames(normalizedSkuItems, nameCandidates)
|
||||
const matchedKnapsackItem = findCloudItemByNames(
|
||||
normalizedKnapsackItems,
|
||||
nameCandidates,
|
||||
matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0
|
||||
);
|
||||
const matchedItem = matchedSkuItem || matchedKnapsackItem;
|
||||
matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0,
|
||||
)
|
||||
const matchedItem = matchedSkuItem || matchedKnapsackItem
|
||||
|
||||
return {
|
||||
skuId: Number(matchedItem?.id || 0) || 0,
|
||||
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
|
||||
skuName: String(currentSkuName || matchedItem?.name || '').trim(),
|
||||
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
skuItem: matchedSkuItem,
|
||||
knapsackItem: matchedKnapsackItem,
|
||||
resolvedByName: Boolean(matchedItem),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveKuaishouCloudVnKeyCandidates(_input: JsonObject = {}) {
|
||||
return [KUAISHOU_CLOUD_FIXED_VN_KEY];
|
||||
return [KUAISHOU_CLOUD_FIXED_VN_KEY]
|
||||
}
|
||||
|
||||
export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonObject = {}) {
|
||||
const { cloudContext = {}, vnKeyCandidates = [] } = input;
|
||||
const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : [];
|
||||
const sourceKey = String(cloudContext.resolvedSourceKey || "").trim();
|
||||
let lastError = null;
|
||||
const { cloudContext = {}, vnKeyCandidates = [] } = input
|
||||
const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : []
|
||||
const sourceKey = String(cloudContext.resolvedSourceKey || '').trim()
|
||||
let lastError = null
|
||||
|
||||
for (const vnKey of candidates) {
|
||||
let vnId = 0;
|
||||
let vnPhone = "";
|
||||
const cooldownKey = `${sourceKey}|${vnKey}`;
|
||||
let vnId = 0
|
||||
let vnPhone = ''
|
||||
const cooldownKey = `${sourceKey}|${vnKey}`
|
||||
|
||||
const cooldownUntil = appointQuotaCooldowns.get(cooldownKey) || 0;
|
||||
const cooldownUntil = appointQuotaCooldowns.get(cooldownKey) || 0
|
||||
if (cooldownUntil > Date.now()) {
|
||||
throw createHttpError(
|
||||
"账号虚拟号配额已满,请先退回已占用号码或稍后重试",
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: "cloudtentacles_vn_quota_cooldown",
|
||||
}
|
||||
);
|
||||
throw createHttpError('账号虚拟号配额已满,请先退回已占用号码或稍后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'cloudtentacles_vn_quota_cooldown',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const appointed = await appointCloudtentaclesVirtualNumber({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
});
|
||||
vnId = Number(appointed.item?.id || 0);
|
||||
vnPhone = String(appointed.item?.phone || "").trim();
|
||||
})
|
||||
vnId = Number(appointed.item?.id || 0)
|
||||
vnPhone = String(appointed.item?.phone || '').trim()
|
||||
|
||||
logInfo('[kuaishou-cloud/binding]', '虚拟号申请成功', {
|
||||
sourceKey,
|
||||
@@ -115,51 +103,51 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonOb
|
||||
purpose: String(input.purpose || 'prepare_binding'),
|
||||
vnKey,
|
||||
vnId,
|
||||
});
|
||||
})
|
||||
|
||||
if (!vnId || !vnPhone) {
|
||||
throw createHttpError("申请虚拟号成功但返回数据不完整", {
|
||||
throw createHttpError('申请虚拟号成功但返回数据不完整', {
|
||||
statusCode: 502,
|
||||
errorCode: "kuaishou_cloud_invalid_vn",
|
||||
});
|
||||
errorCode: 'kuaishou_cloud_invalid_vn',
|
||||
})
|
||||
}
|
||||
|
||||
await generateCloudtentaclesLoginCode({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
});
|
||||
})
|
||||
|
||||
const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
phone: vnPhone,
|
||||
});
|
||||
})
|
||||
|
||||
await verifyCloudtentaclesLoginCode({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
code: fetchedCode.code,
|
||||
});
|
||||
})
|
||||
|
||||
const bindUrlResult = await getCloudtentaclesBindUrl({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
});
|
||||
})
|
||||
|
||||
return {
|
||||
vnKey,
|
||||
vnId,
|
||||
vnPhone,
|
||||
bindUrl: String(bindUrlResult.bindUrl || "").trim(),
|
||||
};
|
||||
bindUrl: String(bindUrlResult.bindUrl || '').trim(),
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
lastError = error
|
||||
|
||||
if (isAppointQuotaExhaustedError(error)) {
|
||||
appointQuotaCooldowns.set(cooldownKey, Date.now() + APPOINT_QUOTA_COOLDOWN_MS);
|
||||
appointQuotaCooldowns.set(cooldownKey, Date.now() + APPOINT_QUOTA_COOLDOWN_MS)
|
||||
}
|
||||
|
||||
if (vnId > 0) {
|
||||
@@ -168,112 +156,108 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonOb
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
});
|
||||
})
|
||||
} catch {
|
||||
// 退号失败保留主错误
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRecoverableKuaishouCloudVnKeyError(error)) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw (
|
||||
lastError ||
|
||||
createHttpError("没有找到可用的 VN Key", {
|
||||
createHttpError('没有找到可用的 VN Key', {
|
||||
statusCode: 409,
|
||||
errorCode: "kuaishou_cloud_missing_binding_config",
|
||||
errorCode: 'kuaishou_cloud_missing_binding_config',
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function collectKuaishouCloudNameCandidates(flow: JsonObject) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
[
|
||||
String(flow?.binding?.skuName || "").trim(),
|
||||
String(flow?.internalSkuName || "").trim(),
|
||||
String(flow?.internalSkuCode || "").trim(),
|
||||
].filter(Boolean)
|
||||
)
|
||||
);
|
||||
String(flow?.binding?.skuName || '').trim(),
|
||||
String(flow?.internalSkuName || '').trim(),
|
||||
String(flow?.internalSkuCode || '').trim(),
|
||||
].filter(Boolean),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function findCloudItemByNames(items: JsonObject[], nameCandidates: string[], preferredId = 0) {
|
||||
const normalizedItems = Array.isArray(items) ? items : [];
|
||||
const normalizedItems = Array.isArray(items) ? items : []
|
||||
const normalizedNames = nameCandidates
|
||||
.map((item: string) => ({
|
||||
raw: String(item || "").trim(),
|
||||
raw: String(item || '').trim(),
|
||||
normalized: normalizeProductName(item),
|
||||
}))
|
||||
.filter((item) => item.raw && item.normalized);
|
||||
.filter((item) => item.raw && item.normalized)
|
||||
|
||||
if (normalizedNames.length === 0 || normalizedItems.length === 0) {
|
||||
return preferredId > 0
|
||||
? normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) ||
|
||||
null
|
||||
: null;
|
||||
? normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || null
|
||||
: null
|
||||
}
|
||||
|
||||
if (preferredId > 0) {
|
||||
const preferred =
|
||||
normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) ||
|
||||
null;
|
||||
normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || null
|
||||
if (preferred) {
|
||||
return preferred;
|
||||
return preferred
|
||||
}
|
||||
}
|
||||
|
||||
const exactMatches = normalizedItems.filter((item: JsonObject) => {
|
||||
const itemName = normalizeProductName(item.name);
|
||||
const itemName = normalizeProductName(item.name)
|
||||
return normalizedNames.some(
|
||||
(candidate: { normalized: string }) => candidate.normalized === itemName
|
||||
);
|
||||
});
|
||||
(candidate: { normalized: string }) => candidate.normalized === itemName,
|
||||
)
|
||||
})
|
||||
if (exactMatches.length > 0) {
|
||||
return exactMatches[0];
|
||||
return exactMatches[0]
|
||||
}
|
||||
|
||||
const partialMatches = normalizedItems.filter((item: JsonObject) => {
|
||||
const itemName = normalizeProductName(item.name);
|
||||
const itemName = normalizeProductName(item.name)
|
||||
return normalizedNames.some(
|
||||
(candidate: { normalized: string }) =>
|
||||
itemName.includes(candidate.normalized) ||
|
||||
candidate.normalized.includes(itemName)
|
||||
);
|
||||
});
|
||||
itemName.includes(candidate.normalized) || candidate.normalized.includes(itemName),
|
||||
)
|
||||
})
|
||||
if (partialMatches.length > 0) {
|
||||
return partialMatches.sort(
|
||||
(left, right) =>
|
||||
String(left.name || "").length - String(right.name || "").length
|
||||
)[0];
|
||||
(left, right) => String(left.name || '').length - String(right.name || '').length,
|
||||
)[0]
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
function isCloudSkuLikeItem(item: unknown): item is JsonObject {
|
||||
const current = item && typeof item === "object" ? item as JsonObject : {};
|
||||
return Number(current.id || 0) > 0;
|
||||
const current = item && typeof item === 'object' ? (item as JsonObject) : {}
|
||||
return Number(current.id || 0) > 0
|
||||
}
|
||||
|
||||
function isAppointQuotaExhaustedError(error: unknown) {
|
||||
const current = error && typeof error === "object" ? error as JsonObject : {};
|
||||
const current = error && typeof error === 'object' ? (error as JsonObject) : {}
|
||||
return (
|
||||
String(current.errorCode || current.code || "").trim() ===
|
||||
"cloudtentacles_vn_appoint_failed" &&
|
||||
String(current.message || "").trim().includes("最多同时占用")
|
||||
);
|
||||
String(current.errorCode || current.code || '').trim() === 'cloudtentacles_vn_appoint_failed' &&
|
||||
String(current.message || '')
|
||||
.trim()
|
||||
.includes('最多同时占用')
|
||||
)
|
||||
}
|
||||
|
||||
function isRecoverableKuaishouCloudVnKeyError(error: unknown) {
|
||||
const current = error && typeof error === "object" ? error as JsonObject : {};
|
||||
const errorCode = String(current.errorCode || current.code || "").trim();
|
||||
const errorMessage = String(current.message || "").trim();
|
||||
const current = error && typeof error === 'object' ? (error as JsonObject) : {}
|
||||
const errorCode = String(current.errorCode || current.code || '').trim()
|
||||
const errorMessage = String(current.message || '').trim()
|
||||
return (
|
||||
errorCode === "cloudtentacles_vn_bind_url_failed" &&
|
||||
errorMessage.includes("不支持的游戏类型")
|
||||
);
|
||||
errorCode === 'cloudtentacles_vn_bind_url_failed' && errorMessage.includes('不支持的游戏类型')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createHttpError } from "../../../utils/http.js";
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import {
|
||||
normalizeCloudtentaclesDeviceId,
|
||||
normalizeCloudtentaclesDeviceType,
|
||||
} from "../../platforms/cloudtentacles/defaults.js";
|
||||
import { getCloudtentaclesSourceByKey } from "../../platforms/cloudtentacles/source-config-service.js";
|
||||
import { getCloudtentaclesSessionStateByKey } from "../../platforms/cloudtentacles/session-state-service.js";
|
||||
import { normalizeStringArray } from "./domain.js";
|
||||
} from '../../platforms/cloudtentacles/defaults.js'
|
||||
import { getCloudtentaclesSourceByKey } from '../../platforms/cloudtentacles/source-config-service.js'
|
||||
import { getCloudtentaclesSessionStateByKey } from '../../platforms/cloudtentacles/session-state-service.js'
|
||||
import { normalizeStringArray } from './domain.js'
|
||||
|
||||
/**
|
||||
* 严格模式:只解析列表中第一个账号(调用方均把任务实际取号账号 resolvedSourceKey 放首位),
|
||||
@@ -14,21 +14,19 @@ import { normalizeStringArray } from "./domain.js";
|
||||
* 退号、取链、发货等「操作任务已有号码」的场景必须使用此函数。
|
||||
* 将实际使用的 resolvedSourceKey 也返回,确保后续操作使用同一个 sourceKey。
|
||||
*/
|
||||
export function resolvePersistedCloudtentaclesContextBySourceKeys(
|
||||
sourceKeys: unknown[] = []
|
||||
) {
|
||||
const candidates = [...new Set(normalizeStringArray(sourceKeys))];
|
||||
export function resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys: unknown[] = []) {
|
||||
const candidates = [...new Set(normalizeStringArray(sourceKeys))]
|
||||
|
||||
if (candidates.length === 0) {
|
||||
throw createHttpError(
|
||||
"cloudtentacles 没有可用账号,请先到平台配置完成账号配置",
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_no_source_keys" }
|
||||
);
|
||||
throw createHttpError('cloudtentacles 没有可用账号,请先到平台配置完成账号配置', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_no_source_keys',
|
||||
})
|
||||
}
|
||||
|
||||
const sourceKey = String(candidates[0] || "").trim();
|
||||
const sourceKey = String(candidates[0] || '').trim()
|
||||
|
||||
return resolveSingleCloudtentaclesAccountContext(sourceKey);
|
||||
return resolveSingleCloudtentaclesAccountContext(sourceKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,77 +34,69 @@ export function resolvePersistedCloudtentaclesContextBySourceKeys(
|
||||
* 仅用于「取新号」场景(新号码归属被选中的账号,不会产生跨账号孤儿),
|
||||
* 以及 account-selector 单候选解析。严禁用于退号/取链/发货等操作已有号码的场景。
|
||||
*/
|
||||
export function resolvePersistedCloudtentaclesContextWithFallback(
|
||||
sourceKeys: unknown[] = []
|
||||
) {
|
||||
const candidates = [...new Set(normalizeStringArray(sourceKeys))];
|
||||
export function resolvePersistedCloudtentaclesContextWithFallback(sourceKeys: unknown[] = []) {
|
||||
const candidates = [...new Set(normalizeStringArray(sourceKeys))]
|
||||
|
||||
let lastError: unknown = null;
|
||||
let lastError: unknown = null
|
||||
|
||||
for (const sourceKey of candidates) {
|
||||
try {
|
||||
return resolveSingleCloudtentaclesAccountContext(sourceKey);
|
||||
return resolveSingleCloudtentaclesAccountContext(sourceKey)
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
|
||||
throw (
|
||||
lastError ||
|
||||
createHttpError(
|
||||
"所有 cloudtentacles 账号均不可用,请先到平台配置完成登录校验",
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_all_source_keys_exhausted" }
|
||||
)
|
||||
);
|
||||
createHttpError('所有 cloudtentacles 账号均不可用,请先到平台配置完成登录校验', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_all_source_keys_exhausted',
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function resolveSingleCloudtentaclesAccountContext(sourceKey: string) {
|
||||
const source = getCloudtentaclesSourceByKey(sourceKey);
|
||||
const session = getCloudtentaclesSessionStateByKey(sourceKey);
|
||||
const source = getCloudtentaclesSourceByKey(sourceKey)
|
||||
const session = getCloudtentaclesSessionStateByKey(sourceKey)
|
||||
|
||||
if (!source) {
|
||||
throw createHttpError(
|
||||
`cloudtentacles 账号 ${sourceKey} 不存在(取号账号已变更或被删除,无法继续操作其虚拟号)`,
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_source_missing" }
|
||||
);
|
||||
{ statusCode: 409, errorCode: 'kuaishou_cloud_source_missing' },
|
||||
)
|
||||
}
|
||||
|
||||
if (source.enabled === false) {
|
||||
throw createHttpError(
|
||||
`cloudtentacles 账号 ${sourceKey} 已停用`,
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_source_disabled" }
|
||||
);
|
||||
throw createHttpError(`cloudtentacles 账号 ${sourceKey} 已停用`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_source_disabled',
|
||||
})
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
throw createHttpError(
|
||||
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
|
||||
);
|
||||
throw createHttpError(`cloudtentacles 账号 ${sourceKey} 没有可用 token`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_missing_cloud_token',
|
||||
})
|
||||
}
|
||||
|
||||
const token = String(session.token || "").trim();
|
||||
const token = String(session.token || '').trim()
|
||||
if (!token) {
|
||||
throw createHttpError(
|
||||
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
|
||||
);
|
||||
throw createHttpError(`cloudtentacles 账号 ${sourceKey} 没有可用 token`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_missing_cloud_token',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl:
|
||||
String(session.baseUrl || source.baseUrl || "").trim() ||
|
||||
"https://123.207.217.176",
|
||||
baseUrl: String(session.baseUrl || source.baseUrl || '').trim() || 'https://123.207.217.176',
|
||||
token,
|
||||
deviceId: normalizeCloudtentaclesDeviceId(
|
||||
session.deviceId || source.deviceId
|
||||
),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(
|
||||
session.deviceType ?? source.deviceType
|
||||
),
|
||||
deviceId: normalizeCloudtentaclesDeviceId(session.deviceId || source.deviceId),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(session.deviceType ?? source.deviceType),
|
||||
// 透传给 http-client / 错误包装,便于定位跨账号问题
|
||||
sourceKey,
|
||||
resolvedSourceKey: sourceKey,
|
||||
accountLabel: String(source.label || sourceKey).trim() || sourceKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ export async function confirmKuaishouCloudTaskRole(
|
||||
}
|
||||
|
||||
const expectedUid = assertClaimExpectedUidReady(task)
|
||||
const mockMode = isKuaishouCloudMockTask(task) || isKuaishouCloudMockContext(parseTaskContext(task))
|
||||
const mockMode =
|
||||
isKuaishouCloudMockTask(task) || isKuaishouCloudMockContext(parseTaskContext(task))
|
||||
|
||||
const refreshed = mockMode
|
||||
? { task }
|
||||
@@ -58,9 +59,7 @@ export async function confirmKuaishouCloudTaskRole(
|
||||
forceProbe: options.forceProbe !== false,
|
||||
})
|
||||
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(refreshed.task).kuaishouCloudFulfillment,
|
||||
)
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
|
||||
|
||||
if (!flow.binding.vnPhone || !flow.binding.roleName || !flow.binding.roleId) {
|
||||
throw createHttpError('角色信息还未刷新到系统,请完成绑定后稍等片刻再试', {
|
||||
|
||||
@@ -68,8 +68,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
||||
const industryVoucherCode = String(
|
||||
voucherContext.voucherCode || voucherContext.eticketId || '',
|
||||
).trim()
|
||||
const hasIndustryVoucherForDispatch =
|
||||
Boolean(industryVoucherCode) || isIndustryEVoucherTask(task)
|
||||
const hasIndustryVoucherForDispatch = Boolean(industryVoucherCode) || isIndustryEVoucherTask(task)
|
||||
const resolvedTicketCode = persistedTicketCode || industryVoucherCode
|
||||
|
||||
if (!resolvedTicketCode && !hasIndustryVoucherForDispatch) {
|
||||
@@ -190,9 +189,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
||||
purchaseTriggered: stockResult.purchaseTriggered,
|
||||
assetBefore: stockResult.assetBefore,
|
||||
assetAfter: stockResult.assetAfter,
|
||||
purchaseAt: stockResult.purchaseTriggered
|
||||
? now
|
||||
: syncedFlow.purchase.purchaseAt,
|
||||
purchaseAt: stockResult.purchaseTriggered ? now : syncedFlow.purchase.purchaseAt,
|
||||
items: stockResult.items,
|
||||
},
|
||||
},
|
||||
@@ -255,9 +252,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
||||
|
||||
return {
|
||||
task: updatedTask,
|
||||
flow: normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(updatedTask).kuaishouCloudFulfillment,
|
||||
),
|
||||
flow: normalizeKuaishouCloudFlow(parseTaskContext(updatedTask).kuaishouCloudFulfillment),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +1,65 @@
|
||||
import { createTaskEvent } from "../../../repositories/task-event-repo.js";
|
||||
import { updateTask } from "../../../repositories/task-repo.js";
|
||||
import type { TaskRow } from "../../../types/repository/rows.js";
|
||||
import { createHttpError } from "../../../utils/http.js";
|
||||
import { getCloudtentaclesBindInfo } from "../../platforms/cloudtentacles/virtual-number-service.js";
|
||||
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
||||
import { updateTask } from '../../../repositories/task-repo.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtual-number-service.js'
|
||||
import {
|
||||
assertLewanAutoFulfillmentUidReady,
|
||||
isClaimUidMatched,
|
||||
normalizeClaimUid,
|
||||
} from "../../claim/claim-identity.js";
|
||||
import { asJsonObject } from "../../../types/json.js";
|
||||
} from '../../claim/claim-identity.js'
|
||||
import { asJsonObject } from '../../../types/json.js'
|
||||
import {
|
||||
normalizeKuaishouCloudFlow,
|
||||
normalizeKuaishouCloudRoleInfo,
|
||||
type JsonObject,
|
||||
} from "./domain.js";
|
||||
} from './domain.js'
|
||||
|
||||
export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
task: TaskRow,
|
||||
input: JsonObject = {}
|
||||
input: JsonObject = {},
|
||||
) {
|
||||
const now = String(input.now || "").trim() || new Date().toISOString();
|
||||
const source =
|
||||
String(input.source || "system_before_dispatch").trim() ||
|
||||
"system_before_dispatch";
|
||||
const now = String(input.now || '').trim() || new Date().toISOString()
|
||||
const source = String(input.source || 'system_before_dispatch').trim() || 'system_before_dispatch'
|
||||
const errorCodePrefix =
|
||||
String(input.errorCodePrefix || "kuaishou_cloud").trim() ||
|
||||
"kuaishou_cloud";
|
||||
const cloudContext =
|
||||
asJsonObject(input.cloudContext);
|
||||
const taskContext =
|
||||
asJsonObject(input.taskContext);
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
input.flow || taskContext.kuaishouCloudFulfillment
|
||||
);
|
||||
String(input.errorCodePrefix || 'kuaishou_cloud').trim() || 'kuaishou_cloud'
|
||||
const cloudContext = asJsonObject(input.cloudContext)
|
||||
const taskContext = asJsonObject(input.taskContext)
|
||||
const flow = normalizeKuaishouCloudFlow(input.flow || taskContext.kuaishouCloudFulfillment)
|
||||
|
||||
// 新策略:lewan 自动发货必须有 expectedUid,旧单无 UID 不可静默回落
|
||||
const expectedUid = assertLewanAutoFulfillmentUidReady(task, {
|
||||
allowMockSkip: true,
|
||||
errorCodePrefix,
|
||||
});
|
||||
})
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
throw createHttpError("当前任务缺少可同步角色的虚拟号信息,暂时不能发货", {
|
||||
throw createHttpError('当前任务缺少可同步角色的虚拟号信息,暂时不能发货', {
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_missing_bind_info_context`,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const bindInfoResult = await getCloudtentaclesBindInfo({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
id: flow.binding.vnId,
|
||||
});
|
||||
const roleInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo);
|
||||
})
|
||||
const roleInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo)
|
||||
|
||||
if (!roleInfo.name || !roleInfo.rid) {
|
||||
throw createHttpError(
|
||||
"cloudtentacles 还没有同步到客户绑定角色,请稍后刷新角色信息后再发货",
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_role_not_ready`,
|
||||
}
|
||||
);
|
||||
throw createHttpError('cloudtentacles 还没有同步到客户绑定角色,请稍后刷新角色信息后再发货', {
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_role_not_ready`,
|
||||
})
|
||||
}
|
||||
|
||||
const liveBoundUid = normalizeClaimUid(roleInfo.rid);
|
||||
const liveBoundUid = normalizeClaimUid(roleInfo.rid)
|
||||
|
||||
if (expectedUid && !isClaimUidMatched(expectedUid, liveBoundUid)) {
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
"kuaishou_cloud_dispatch_uid_mismatch",
|
||||
'kuaishou_cloud_dispatch_uid_mismatch',
|
||||
{
|
||||
source,
|
||||
vnId: flow.binding.vnId,
|
||||
@@ -78,16 +68,16 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
cloudtentaclesRoleId: roleInfo.rid,
|
||||
actor: input.actor || null,
|
||||
},
|
||||
now
|
||||
);
|
||||
now,
|
||||
)
|
||||
|
||||
throw createHttpError(
|
||||
`cloudtentacles 当前绑定角色 ID(${roleInfo.rid})与用户填写 UID(${expectedUid})不一致,不能发货`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_uid_mismatch`,
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const nextFlow = normalizeKuaishouCloudFlow({
|
||||
@@ -99,36 +89,36 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
},
|
||||
role: {
|
||||
...flow.role,
|
||||
status: "ready",
|
||||
status: 'ready',
|
||||
name: roleInfo.name,
|
||||
rid: roleInfo.rid,
|
||||
refreshedAt: now,
|
||||
errorMessage: "",
|
||||
errorMessage: '',
|
||||
rawInfo: roleInfo.rawInfo,
|
||||
},
|
||||
});
|
||||
})
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: nextFlow,
|
||||
};
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
role_id: roleInfo.rid,
|
||||
role_name: roleInfo.name,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
});
|
||||
})
|
||||
|
||||
if (!updatedTask) {
|
||||
throw createHttpError("发货前角色信息同步失败", {
|
||||
throw createHttpError('发货前角色信息同步失败', {
|
||||
statusCode: 500,
|
||||
errorCode: `${errorCodePrefix}_dispatch_role_update_failed`,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
"kuaishou_cloud_role_info_synced_before_dispatch",
|
||||
'kuaishou_cloud_role_info_synced_before_dispatch',
|
||||
{
|
||||
source,
|
||||
roleName: roleInfo.name,
|
||||
@@ -136,15 +126,13 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
vnId: flow.binding.vnId,
|
||||
actor: input.actor || null,
|
||||
},
|
||||
now
|
||||
);
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: updatedTask,
|
||||
taskContext: nextContext,
|
||||
flow: nextFlow,
|
||||
roleInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -51,9 +51,8 @@ export function resolveDispatchDeliveryItems(flow: JsonObject): DispatchDelivery
|
||||
return items
|
||||
}
|
||||
|
||||
const binding = flow.binding && typeof flow.binding === 'object'
|
||||
? (flow.binding as JsonObject)
|
||||
: {}
|
||||
const binding =
|
||||
flow.binding && typeof flow.binding === 'object' ? (flow.binding as JsonObject) : {}
|
||||
const fallbackSkuId = Number(binding.skuId || 0) || 0
|
||||
if (!fallbackSkuId) {
|
||||
return []
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
|
||||
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.js'
|
||||
import {
|
||||
isTaskFinalStatus,
|
||||
normalizeTaskStatus,
|
||||
TASK_STATUS,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { isTaskFinalStatus, normalizeTaskStatus, TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import { asJsonObject, type JsonObject } from '../../../types/json.js'
|
||||
|
||||
export type { JsonObject }
|
||||
@@ -138,8 +134,7 @@ export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow {
|
||||
const role = asJsonObject(source.role)
|
||||
const purchase = asJsonObject(source.purchase)
|
||||
const dispatch = asJsonObject(source.dispatch)
|
||||
const returnNumber =
|
||||
asJsonObject(source.returnNumber)
|
||||
const returnNumber = asJsonObject(source.returnNumber)
|
||||
const consume = asJsonObject(source.consume)
|
||||
const ticket = asJsonObject(source.ticket)
|
||||
const rebind = asJsonObject(source.rebind)
|
||||
@@ -280,7 +275,8 @@ export function normalizeKuaishouCloudShippedSnapshot(
|
||||
roleName: String(source.roleName || source.name || '').trim(),
|
||||
vnId,
|
||||
vnPhone: String(source.vnPhone || '').trim(),
|
||||
vnKey: String(source.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
vnKey:
|
||||
String(source.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
dispatchedAt,
|
||||
}
|
||||
}
|
||||
@@ -331,8 +327,7 @@ export function buildKuaishouCloudShippedSnapshot(input: {
|
||||
roleName: String(input.roleName || '').trim(),
|
||||
vnId: Number(input.vnId || 0) || 0,
|
||||
vnPhone: String(input.vnPhone || '').trim(),
|
||||
vnKey:
|
||||
String(input.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
vnKey: String(input.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
dispatchedAt: input.dispatchedAt ? String(input.dispatchedAt) : null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,7 @@ import {
|
||||
} from './account-selector.js'
|
||||
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
|
||||
import { wrapCloudtentaclesOperationError } from './cloudtentacles-errors.js'
|
||||
import {
|
||||
getTaskClaimExpiresAt,
|
||||
normalizeActor,
|
||||
parseTaskContext,
|
||||
} from './task-context.js'
|
||||
import { getTaskClaimExpiresAt, normalizeActor, parseTaskContext } from './task-context.js'
|
||||
import { resolveKuaishouCloudDeliveryPlan } from './delivery-plan.js'
|
||||
import { ensureTaskClaimLink } from './ensure-claim-link.js'
|
||||
import {
|
||||
@@ -332,11 +328,14 @@ export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options
|
||||
}
|
||||
|
||||
function isRetryableCloudtentaclesSourceError(error: unknown) {
|
||||
const current = error && typeof error === 'object' ? error as JsonObject : {}
|
||||
const current = error && typeof error === 'object' ? (error as JsonObject) : {}
|
||||
const code = String(current.errorCode || current.code || '').trim()
|
||||
return (
|
||||
code === 'cloudtentacles_vn_quota_cooldown' ||
|
||||
(code === 'cloudtentacles_vn_appoint_failed' && String(current.message || '').trim().includes('最多同时占用')) ||
|
||||
(code === 'cloudtentacles_vn_appoint_failed' &&
|
||||
String(current.message || '')
|
||||
.trim()
|
||||
.includes('最多同时占用')) ||
|
||||
code === 'cloudtentacles_vn_list_failed' ||
|
||||
code === 'cloudtentacles_sku_list_failed' ||
|
||||
code === 'cloudtentacles_knapsack_failed' ||
|
||||
@@ -484,8 +483,9 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
|
||||
binding: {
|
||||
...flow.binding,
|
||||
prepareStatus: 'ready',
|
||||
resolvedSourceKey:
|
||||
String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(),
|
||||
resolvedSourceKey: String(
|
||||
cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
|
||||
).trim(),
|
||||
bindUrl,
|
||||
bindPreparedAt: now,
|
||||
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
|
||||
@@ -667,8 +667,9 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
|
||||
binding: {
|
||||
...flow.binding,
|
||||
prepareStatus: 'ready',
|
||||
resolvedSourceKey:
|
||||
String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(),
|
||||
resolvedSourceKey: String(
|
||||
cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
|
||||
).trim(),
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId: preparedBinding.vnId,
|
||||
vnPhone: preparedBinding.vnPhone,
|
||||
@@ -739,7 +740,6 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function markKuaishouCloudBindUrlRefreshFailed(
|
||||
task: TaskRow,
|
||||
{
|
||||
@@ -855,7 +855,7 @@ async function clearKuaishouCloudStaleBinding(
|
||||
error: unknown
|
||||
claimUrl: string
|
||||
token: string
|
||||
}
|
||||
},
|
||||
) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error || '绑定已失效')
|
||||
const oldVnId = flow.binding.vnId
|
||||
@@ -912,7 +912,7 @@ async function clearKuaishouCloudStaleBinding(
|
||||
errorMessage,
|
||||
actor,
|
||||
},
|
||||
now
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -51,9 +51,7 @@ export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: Json
|
||||
|
||||
const probeIntervalMs =
|
||||
Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) * 1000
|
||||
const lastProbeAt = flow.binding.bindProbeAt
|
||||
? Date.parse(String(flow.binding.bindProbeAt))
|
||||
: NaN
|
||||
const lastProbeAt = flow.binding.bindProbeAt ? Date.parse(String(flow.binding.bindProbeAt)) : NaN
|
||||
if (
|
||||
!options.force &&
|
||||
Number.isFinite(lastProbeAt) &&
|
||||
@@ -187,4 +185,3 @@ async function markKuaishouCloudBindUrlRefreshFailed(
|
||||
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,7 @@ import {
|
||||
isCloudtentaclesPermissionLikeError,
|
||||
wrapCloudtentaclesOperationError,
|
||||
} from './cloudtentacles-errors.js'
|
||||
import {
|
||||
getTaskClaimExpiresAt,
|
||||
normalizeActor,
|
||||
parseTaskContext,
|
||||
} from './task-context.js'
|
||||
import { getTaskClaimExpiresAt, normalizeActor, parseTaskContext } from './task-context.js'
|
||||
import { ensureTaskClaimLink } from './ensure-claim-link.js'
|
||||
import {
|
||||
buildPendingRoleWithDefaultSnapshot,
|
||||
@@ -115,7 +111,8 @@ export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonOb
|
||||
bindExpiresAt: flow.binding.bindExpiresAt,
|
||||
roleName: flow.binding.roleName || flow.role.name || task.role_name || '',
|
||||
roleId: flow.binding.roleId || flow.role.rid || task.role_id || '',
|
||||
resolvedSourceKey: flow.binding.resolvedSourceKey || String(cloudContext.resolvedSourceKey || ''),
|
||||
resolvedSourceKey:
|
||||
flow.binding.resolvedSourceKey || String(cloudContext.resolvedSourceKey || ''),
|
||||
}
|
||||
const previousRebind: JsonObject =
|
||||
flow.rebind && typeof flow.rebind === 'object' ? (flow.rebind as JsonObject) : {}
|
||||
@@ -339,8 +336,9 @@ export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonOb
|
||||
binding: {
|
||||
...flow.binding,
|
||||
prepareStatus: 'ready',
|
||||
resolvedSourceKey:
|
||||
String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(),
|
||||
resolvedSourceKey: String(
|
||||
cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
|
||||
).trim(),
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId: preparedBinding.vnId,
|
||||
vnPhone: preparedBinding.vnPhone,
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
canRedeemKuaishouCloudClaimStatus,
|
||||
isKuaishouCloudRedeemSettledStatus,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { assertBoundUidMatchesExpected, assertClaimExpectedUidReady } from '../../claim/claim-identity.js'
|
||||
import {
|
||||
assertBoundUidMatchesExpected,
|
||||
assertClaimExpectedUidReady,
|
||||
} from '../../claim/claim-identity.js'
|
||||
|
||||
/**
|
||||
* redeem 用例前置条件单测(不打 DB / CT)。
|
||||
@@ -73,10 +76,7 @@ test('redeem 前置:uid 不一致拒绝', () => {
|
||||
})
|
||||
|
||||
test('redeem 闸门函数与 claim-identity 一致', () => {
|
||||
assert.throws(
|
||||
() => assertClaimExpectedUidReady({ context_json: '{}' }),
|
||||
/填写游戏 UID/,
|
||||
)
|
||||
assert.throws(() => assertClaimExpectedUidReady({ context_json: '{}' }), /填写游戏 UID/)
|
||||
assert.throws(
|
||||
() =>
|
||||
assertBoundUidMatchesExpected(
|
||||
|
||||
@@ -20,10 +20,7 @@ import {
|
||||
} from '../../claim/claim-identity.js'
|
||||
import { isKuaishouCloudTask, normalizeKuaishouCloudFlow, type JsonObject } from './domain.js'
|
||||
import { dispatchKuaishouCloudFulfillmentTask } from './dispatch-fulfillment.js'
|
||||
import {
|
||||
completeMockKuaishouCloudTask,
|
||||
isKuaishouCloudMockTask,
|
||||
} from './mock-helpers.js'
|
||||
import { completeMockKuaishouCloudTask, isKuaishouCloudMockTask } from './mock-helpers.js'
|
||||
import { normalizeActor, parseTaskContext } from './task-context.js'
|
||||
|
||||
export type RedeemKuaishouCloudTaskResult = {
|
||||
@@ -74,9 +71,7 @@ export async function redeemKuaishouCloudTask(
|
||||
|
||||
// WAITING_BINDING + UID 匹配:自动升为 ROLE_CONFIRMED,实现一键兑换
|
||||
if (currentStatus === TASK_STATUS.WAITING_BINDING) {
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(workingTask).kuaishouCloudFulfillment,
|
||||
)
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(workingTask).kuaishouCloudFulfillment)
|
||||
if (!isKuaishouCloudMockTask(workingTask)) {
|
||||
assertBoundUidMatchesExpected(workingTask, flow, {
|
||||
errorCodePrefix,
|
||||
@@ -122,16 +117,12 @@ export async function redeemKuaishouCloudTask(
|
||||
assertClaimExpectedUidReady(workingTask)
|
||||
}
|
||||
|
||||
const lockedTask = await updateTaskStatusIfCurrent(
|
||||
workingTask.id,
|
||||
TASK_STATUS.ROLE_CONFIRMED,
|
||||
{
|
||||
task_status: TASK_STATUS.REDEEMING,
|
||||
user_action_status: 'not_required',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
},
|
||||
)
|
||||
const lockedTask = await updateTaskStatusIfCurrent(workingTask.id, TASK_STATUS.ROLE_CONFIRMED, {
|
||||
task_status: TASK_STATUS.REDEEMING,
|
||||
user_action_status: 'not_required',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
if (!lockedTask) {
|
||||
const latest = (await getTaskById(workingTask.id)) || workingTask
|
||||
@@ -174,8 +165,7 @@ export async function redeemKuaishouCloudTask(
|
||||
throw error
|
||||
}
|
||||
|
||||
const message =
|
||||
error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理'
|
||||
const message = error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理'
|
||||
await updateTask(lockedTask.id, {
|
||||
task_status: TASK_STATUS.MANUAL_REVIEW,
|
||||
user_action_status: 'not_required',
|
||||
|
||||
@@ -23,10 +23,7 @@ import { normalizeActor, parseTaskContext } from './task-context.js'
|
||||
* - 默认(发货后 autoFinalize):退号 + 尝试行业电子凭证核销收尾
|
||||
* - `consumeIndustryVoucher: false`(admin 清理):只退虚拟号,避免占号;与核销无关
|
||||
*/
|
||||
export async function returnKuaishouCloudFulfillmentTask(
|
||||
task: TaskRow,
|
||||
options: JsonObject = {},
|
||||
) {
|
||||
export async function returnKuaishouCloudFulfillmentTask(task: TaskRow, options: JsonObject = {}) {
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
|
||||
statusCode: 409,
|
||||
@@ -79,7 +76,7 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
errorMessage: error instanceof Error ? error.message : String(error || ''),
|
||||
actor,
|
||||
},
|
||||
now
|
||||
now,
|
||||
)
|
||||
} else {
|
||||
throw error
|
||||
@@ -113,8 +110,7 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
} else if (shouldConsumeIndustryVoucher) {
|
||||
const industryResult = hasIndustryVoucher
|
||||
? await consumeKuaishouIndustryVouchersForTask(task, {
|
||||
source:
|
||||
String(options.source || 'system_auto_finalize').trim() || 'system_auto_finalize',
|
||||
source: String(options.source || 'system_auto_finalize').trim() || 'system_auto_finalize',
|
||||
token: String(
|
||||
isPlainObject(taskContext.kuaishouIndustryVoucher)
|
||||
? taskContext.kuaishouIndustryVoucher.token || ''
|
||||
@@ -122,7 +118,11 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
).trim(),
|
||||
consumeTime: Date.now(),
|
||||
})
|
||||
: { ok: true, consumed: [] as Array<Record<string, unknown>>, failed: [] as Array<{ errorMessage?: string }> }
|
||||
: {
|
||||
ok: true,
|
||||
consumed: [] as Array<Record<string, unknown>>,
|
||||
failed: [] as Array<{ errorMessage?: string }>,
|
||||
}
|
||||
|
||||
if (industryResult.ok) {
|
||||
consumeStatus = 'success'
|
||||
@@ -158,8 +158,7 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
if (shouldConsumeIndustryVoucher) {
|
||||
nextTaskStatus = TASK_STATUS.MANUAL_REVIEW
|
||||
nextResultCode = 'kuaishou_cloud_consume_failed'
|
||||
nextResultMessage =
|
||||
consumeErrorMessage || '号码已退还,但电子凭证核销未完成,请人工处理'
|
||||
nextResultMessage = consumeErrorMessage || '号码已退还,但电子凭证核销未完成,请人工处理'
|
||||
} else {
|
||||
nextResultCode = allowConsumeIndustryVoucher
|
||||
? 'kuaishou_cloud_completed_without_eticket_consume'
|
||||
@@ -256,12 +255,15 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
}
|
||||
}
|
||||
|
||||
function hasKuaishouIndustryVoucherContext(value: JsonObject): boolean { const voucher = isPlainObject(value.kuaishouIndustryVoucher)
|
||||
? value.kuaishouIndustryVoucher
|
||||
: {}
|
||||
function hasKuaishouIndustryVoucherContext(value: JsonObject): boolean {
|
||||
const voucher = isPlainObject(value.kuaishouIndustryVoucher) ? value.kuaishouIndustryVoucher : {}
|
||||
const voucherCode = String(voucher.voucherCode || voucher.eticketId || '').trim()
|
||||
const status = String(voucher.status || 'UNUSED').trim().toUpperCase()
|
||||
const sendCallbackStatus = String(voucher.sendCallbackStatus || 'success').trim().toLowerCase()
|
||||
const status = String(voucher.status || 'UNUSED')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
const sendCallbackStatus = String(voucher.sendCallbackStatus || 'success')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return Boolean(voucherCode && status !== 'DESTROYED' && sendCallbackStatus === 'success')
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { parseTaskContext as parseTaskContextValue } from "../../../utils/task-json.js";
|
||||
import type { TaskRow } from "../../../types/repository/rows.js";
|
||||
import { parseTaskContext as parseTaskContextValue } from '../../../utils/task-json.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
import type { JsonObject } from '../../../types/json.js'
|
||||
|
||||
export function normalizeActor(actor: unknown) {
|
||||
if (!actor || typeof actor !== "object") {
|
||||
return null;
|
||||
if (!actor || typeof actor !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const current = actor as JsonObject;
|
||||
const source = String(current.source || "").trim();
|
||||
const userId = Number(current.userId || 0) || 0;
|
||||
const username = String(current.username || "").trim();
|
||||
const role = String(current.role || "").trim();
|
||||
const current = actor as JsonObject
|
||||
const source = String(current.source || '').trim()
|
||||
const userId = Number(current.userId || 0) || 0
|
||||
const username = String(current.username || '').trim()
|
||||
const role = String(current.role || '').trim()
|
||||
|
||||
if (!source && !userId && !username && !role) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -22,22 +22,24 @@ export function normalizeActor(actor: unknown) {
|
||||
userId,
|
||||
username,
|
||||
role,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTaskContext(task: Partial<TaskRow> | null | undefined) {
|
||||
return parseTaskContextValue(task);
|
||||
return parseTaskContextValue(task)
|
||||
}
|
||||
|
||||
export function getTaskClaimExpiresAt(task: Partial<TaskRow> | null | undefined) {
|
||||
return task?.claim_expires_at || task?.primary_claim_expires_at || null;
|
||||
return task?.claim_expires_at || task?.primary_claim_expires_at || null
|
||||
}
|
||||
|
||||
export function isClaimExpired(expiredAt: unknown) {
|
||||
if (!expiredAt) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
const timestamp = new Date(expiredAt instanceof Date ? expiredAt : String(expiredAt || "")).getTime();
|
||||
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
||||
const timestamp = new Date(
|
||||
expiredAt instanceof Date ? expiredAt : String(expiredAt || ''),
|
||||
).getTime()
|
||||
return Number.isFinite(timestamp) && timestamp <= Date.now()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { buildDispatchStockItems } from "./task-finalization.js";
|
||||
import { buildDispatchStockItems } from './task-finalization.js'
|
||||
|
||||
test("buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口", () => {
|
||||
test('buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口', () => {
|
||||
const result = buildDispatchStockItems(
|
||||
[
|
||||
{
|
||||
cloudSkuId: 101,
|
||||
cloudSkuName: "",
|
||||
cloudSkuName: '',
|
||||
quantity: 3,
|
||||
},
|
||||
],
|
||||
@@ -16,38 +16,38 @@ test("buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口",
|
||||
skuItems: [
|
||||
{
|
||||
id: 101,
|
||||
name: "套装-暗影哥特",
|
||||
name: '套装-暗影哥特',
|
||||
price: 88,
|
||||
},
|
||||
],
|
||||
knapsackItems: [
|
||||
{
|
||||
id: 101,
|
||||
name: "套装-暗影哥特",
|
||||
name: '套装-暗影哥特',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{
|
||||
cloudSkuId: 101,
|
||||
cloudSkuName: "套装-暗影哥特",
|
||||
cloudSkuName: '套装-暗影哥特',
|
||||
quantity: 3,
|
||||
requiredCount: 3,
|
||||
knapsackCount: 1,
|
||||
purchasedCount: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
])
|
||||
})
|
||||
|
||||
test("buildDispatchStockItems 背包足够时不需要补买", () => {
|
||||
test('buildDispatchStockItems 背包足够时不需要补买', () => {
|
||||
const result = buildDispatchStockItems(
|
||||
[
|
||||
{
|
||||
cloudSkuId: 202,
|
||||
cloudSkuName: "套装-浪漫天命",
|
||||
cloudSkuName: '套装-浪漫天命',
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
@@ -55,13 +55,13 @@ test("buildDispatchStockItems 背包足够时不需要补买", () => {
|
||||
knapsackItems: [
|
||||
{
|
||||
id: 202,
|
||||
name: "套装-浪漫天命",
|
||||
name: '套装-浪漫天命',
|
||||
count: 5,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(result[0]?.knapsackCount, 5);
|
||||
assert.equal(result[0]?.purchasedCount, 0);
|
||||
});
|
||||
assert.equal(result[0]?.knapsackCount, 5)
|
||||
assert.equal(result[0]?.purchasedCount, 0)
|
||||
})
|
||||
|
||||
@@ -64,43 +64,56 @@ test('resolveKuaishouFeifeiH5UrlWithUid appends uid query', () => {
|
||||
|
||||
test('buildFeifeiPlatformOrderNo prefers source order number', () => {
|
||||
assert.equal(
|
||||
buildFeifeiPlatformOrderNo(createTask({
|
||||
platform_order_id: 'KS202607080001',
|
||||
task_no: 'DT600a8bd23b8',
|
||||
})),
|
||||
buildFeifeiPlatformOrderNo(
|
||||
createTask({
|
||||
platform_order_id: 'KS202607080001',
|
||||
task_no: 'DT600a8bd23b8',
|
||||
}),
|
||||
),
|
||||
'KS202607080001',
|
||||
)
|
||||
})
|
||||
|
||||
test('buildFeifeiPlatformOrderNo falls back to task number', () => {
|
||||
assert.equal(
|
||||
buildFeifeiPlatformOrderNo(createTask({
|
||||
platform_order_id: '',
|
||||
task_no: 'DT600a8bd23b8',
|
||||
})),
|
||||
buildFeifeiPlatformOrderNo(
|
||||
createTask({
|
||||
platform_order_id: '',
|
||||
task_no: 'DT600a8bd23b8',
|
||||
}),
|
||||
),
|
||||
'DT600a8bd23b8',
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveKuaishouFeifeiPlatformAmount converts order fen amount to yuan', () => {
|
||||
assert.equal(resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 3800,
|
||||
quantity: 1,
|
||||
}), 38)
|
||||
assert.equal(
|
||||
resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 3800,
|
||||
quantity: 1,
|
||||
}),
|
||||
38,
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveKuaishouFeifeiPlatformAmount splits amount by item quantity', () => {
|
||||
assert.equal(resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 7600,
|
||||
quantity: 2,
|
||||
}), 38)
|
||||
assert.equal(
|
||||
resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 7600,
|
||||
quantity: 2,
|
||||
}),
|
||||
38,
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveKuaishouFeifeiPlatformAmount skips empty amount', () => {
|
||||
assert.equal(resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 0,
|
||||
quantity: 1,
|
||||
}), 0)
|
||||
assert.equal(
|
||||
resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 0,
|
||||
quantity: 1,
|
||||
}),
|
||||
0,
|
||||
)
|
||||
})
|
||||
|
||||
function createTask(patch: Partial<TaskRow> = {}): TaskRow {
|
||||
|
||||
@@ -173,19 +173,25 @@ export async function syncKuaishouFeifeiTaskStatus(task: TaskRow) {
|
||||
resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理'
|
||||
lastError = resultMessage
|
||||
nextFlow.consumeStatus = 'failed'
|
||||
logIntegration('[kuaishou-feifei]', '飞飞履约完成,但行业电子凭证核销失败', {
|
||||
taskId: task.id,
|
||||
platformOrderNo: nextFlow.platformOrderNo,
|
||||
orderNo: nextFlow.orderNo,
|
||||
voucherCount: consumeResult.vouchers.length,
|
||||
failedCount: consumeResult.failed.length,
|
||||
errorMessage: resultMessage,
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[kuaishou-feifei]',
|
||||
'飞飞履约完成,但行业电子凭证核销失败',
|
||||
{
|
||||
taskId: task.id,
|
||||
platformOrderNo: nextFlow.platformOrderNo,
|
||||
orderNo: nextFlow.orderNo,
|
||||
voucherCount: consumeResult.vouchers.length,
|
||||
failedCount: consumeResult.failed.length,
|
||||
errorMessage: resultMessage,
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
} else if ([40, 50, 60].includes(order.rechargeStatus)) {
|
||||
nextTaskStatus = order.rechargeStatus === 60 ? TASK_STATUS.CLOSED : TASK_STATUS.MANUAL_REVIEW
|
||||
resultCode = `kuaishou_feifei_status_${order.rechargeStatus}`
|
||||
resultMessage = order.rechargeResultMessage || order.rechargeStatusLabel || 'kuaishou-feifei 履约异常'
|
||||
resultMessage =
|
||||
order.rechargeResultMessage || order.rechargeStatusLabel || 'kuaishou-feifei 履约异常'
|
||||
lastError = resultMessage
|
||||
}
|
||||
|
||||
@@ -229,10 +235,9 @@ export type KuaishouFeifeiFlow = {
|
||||
}
|
||||
|
||||
export function normalizeKuaishouFeifeiFlow(value: unknown): KuaishouFeifeiFlow {
|
||||
const source = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
const h5 = source.h5 && typeof source.h5 === 'object' ? source.h5 as JsonObject : {}
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
const h5 = source.h5 && typeof source.h5 === 'object' ? (source.h5 as JsonObject) : {}
|
||||
|
||||
return {
|
||||
flowType: 'kuaishou_feifei',
|
||||
@@ -263,10 +268,7 @@ export function resolveKuaishouFeifeiClaimUrl(value: unknown) {
|
||||
}
|
||||
|
||||
/** 在已有 expectedUid 时返回拼好 uid 的 H5 链接。 */
|
||||
export function resolveKuaishouFeifeiH5UrlWithUid(
|
||||
value: unknown,
|
||||
expectedUid?: unknown,
|
||||
) {
|
||||
export function resolveKuaishouFeifeiH5UrlWithUid(value: unknown, expectedUid?: unknown) {
|
||||
const directUrl = resolveKuaishouFeifeiClaimUrl(value)
|
||||
if (!directUrl) {
|
||||
return ''
|
||||
@@ -319,9 +321,7 @@ function mergeKuaishouFeifeiOrder(
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKuaishouFeifeiDirectClaimUrl(
|
||||
flow: KuaishouFeifeiFlow,
|
||||
) {
|
||||
function resolveKuaishouFeifeiDirectClaimUrl(flow: KuaishouFeifeiFlow) {
|
||||
const h5ClaimUrl = flow.h5.rechargeUrl || flow.h5.entryUrl
|
||||
if (h5ClaimUrl) {
|
||||
return h5ClaimUrl
|
||||
@@ -335,7 +335,9 @@ function resolveKuaishouFeifeiDirectClaimUrl(
|
||||
return isLocalClaimUrl(flow.claimUrl) ? '' : flow.claimUrl
|
||||
}
|
||||
|
||||
function resolveKuaishouFeifeiOrderClaimUrl(order: Awaited<ReturnType<typeof createKuaishouFeifeiOrder>>) {
|
||||
function resolveKuaishouFeifeiOrderClaimUrl(
|
||||
order: Awaited<ReturnType<typeof createKuaishouFeifeiOrder>>,
|
||||
) {
|
||||
return String(order.h5.rechargeUrl || order.h5.entryUrl || '').trim()
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,9 @@ test('resolveOrderFulfillmentReadiness 电子凭证发码未确认时阻止履
|
||||
})
|
||||
})
|
||||
|
||||
function createVoucher(patch: Partial<KuaishouIndustryVoucherRow> = {}): KuaishouIndustryVoucherRow {
|
||||
function createVoucher(
|
||||
patch: Partial<KuaishouIndustryVoucherRow> = {},
|
||||
): KuaishouIndustryVoucherRow {
|
||||
return {
|
||||
id: 1,
|
||||
voucher_code: 'ETICKET-1',
|
||||
|
||||
@@ -39,7 +39,9 @@ export async function resolveOrderFulfillmentReadiness(
|
||||
}
|
||||
}
|
||||
|
||||
const blockedVoucher = vouchers.find((voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher))
|
||||
const blockedVoucher = vouchers.find(
|
||||
(voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher),
|
||||
)
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
|
||||
@@ -54,9 +54,9 @@ export async function planFulfillmentTaskForOrderItem({
|
||||
const profileId = Number(profile.profile_id || profile.id || 0)
|
||||
const profileKey = String(profile.profile_key || '').trim()
|
||||
const profileName = String(profile.profile_name || profile.name || '').trim()
|
||||
const executorKey = String(profile.executor_key || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH)
|
||||
.trim()
|
||||
|| FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
const executorKey =
|
||||
String(profile.executor_key || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH).trim() ||
|
||||
FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
||||
const context = buildFulfillmentTaskContext({
|
||||
order,
|
||||
@@ -229,9 +229,7 @@ function buildFulfillmentTaskContext({
|
||||
? {
|
||||
flowType: 'affiliate_dash',
|
||||
sku: String(affiliateDashConfig.sku || '').trim(),
|
||||
productName: String(
|
||||
affiliateDashConfig.productName || item.sku_name || '',
|
||||
).trim(),
|
||||
productName: String(affiliateDashConfig.productName || item.sku_name || '').trim(),
|
||||
orderNo: '',
|
||||
clientOrderNo: '',
|
||||
orderStatus: '',
|
||||
@@ -279,9 +277,9 @@ async function resolveDynamicFulfillmentProfile(
|
||||
}
|
||||
|
||||
return (
|
||||
await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) ||
|
||||
await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey) ||
|
||||
await resolveDynamicAffiliateDashProfile(item, getProfileByKey)
|
||||
(await resolveDynamicCloudtentaclesProfile(item, getProfileByKey)) ||
|
||||
(await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey)) ||
|
||||
(await resolveDynamicAffiliateDashProfile(item, getProfileByKey))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,23 +8,23 @@ import {
|
||||
} from './product-resolution-service.js'
|
||||
|
||||
test('matchAffiliateDashSku 原样精确命中', () => {
|
||||
const mapping = { '幸运币90个': 'lucky_coin_x90' }
|
||||
const mapping = { 幸运币90个: 'lucky_coin_x90' }
|
||||
assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90')
|
||||
})
|
||||
|
||||
test('matchAffiliateDashSku 全角数字/空格归一命中', () => {
|
||||
const mapping = { '幸运币90个': 'lucky_coin_x90' }
|
||||
const mapping = { 幸运币90个: 'lucky_coin_x90' }
|
||||
assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90')
|
||||
assert.equal(matchAffiliateDashSku(mapping, ' 幸运币 90 个 '), 'lucky_coin_x90')
|
||||
})
|
||||
|
||||
test('matchAffiliateDashSku 大小写归一命中', () => {
|
||||
const mapping = { '星际漫游服装礼包': 'pack_star_roam_outfit' }
|
||||
const mapping = { 星际漫游服装礼包: 'pack_star_roam_outfit' }
|
||||
assert.equal(matchAffiliateDashSku(mapping, '星际漫游服装礼包'), 'pack_star_roam_outfit')
|
||||
})
|
||||
|
||||
test('matchAffiliateDashSku 未命中返回空串', () => {
|
||||
const mapping = { '幸运币90个': 'lucky_coin_x90' }
|
||||
const mapping = { 幸运币90个: 'lucky_coin_x90' }
|
||||
assert.equal(matchAffiliateDashSku(mapping, '神秘新商品'), '')
|
||||
assert.equal(matchAffiliateDashSku(mapping, ''), '')
|
||||
})
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
resolveKuaishouFeifeiProductByName,
|
||||
type KuaishouFeifeiProductMatch,
|
||||
} from '../platforms/kuaishou-feifei/product-rule-service.js'
|
||||
import {
|
||||
resolveFulfillmentRoute,
|
||||
type FulfillmentRouteResult,
|
||||
} from './routing-config-service.js'
|
||||
import { resolveFulfillmentRoute, type FulfillmentRouteResult } from './routing-config-service.js'
|
||||
import { getAffiliateDashConfig } from '../platforms/affiliate-dash/config.js'
|
||||
import { listAllAffiliateDashProducts } from '../platforms/affiliate-dash/product-service.js'
|
||||
import type { AffiliateDashSkuMapping } from '../../types/runtime-config.js'
|
||||
@@ -97,9 +94,7 @@ export async function resolveOrderItemForFulfillment({
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI
|
||||
? kuaishouFeifeiMatch?.productCode
|
||||
: '',
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH
|
||||
? affiliateDashMatch?.sku
|
||||
: '',
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH ? affiliateDashMatch?.sku : '',
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
@@ -110,9 +105,7 @@ export async function resolveOrderItemForFulfillment({
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI
|
||||
? kuaishouFeifeiMatch?.skuName
|
||||
: '',
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH
|
||||
? affiliateDashMatch?.sku
|
||||
: '',
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH ? affiliateDashMatch?.sku : '',
|
||||
item.skuName,
|
||||
externalSkuName,
|
||||
resolvedSkuCode,
|
||||
@@ -187,11 +180,13 @@ export async function hasConfiguredOrderItems({
|
||||
items = [],
|
||||
}: HasConfiguredOrderItemsInput): Promise<boolean> {
|
||||
const candidates = await Promise.all(
|
||||
(Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
item,
|
||||
})),
|
||||
(Array.isArray(items) ? items : []).map((item) =>
|
||||
resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
item,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return candidates.some((item) => item.isConfigured)
|
||||
@@ -217,20 +212,9 @@ async function resolveConfiguredItemCandidate({
|
||||
platform = '',
|
||||
item = {},
|
||||
}: ResolveOrderItemForFulfillmentInput): Promise<FulfillmentItemCandidate> {
|
||||
const externalItemId = pickFirstNonEmpty([
|
||||
item.externalItemId,
|
||||
item.itemId,
|
||||
])
|
||||
const externalSkuCode = pickFirstNonEmpty([
|
||||
item.externalSkuCode,
|
||||
item.skuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const externalSkuName = pickFirstNonEmpty([
|
||||
item.externalSkuName,
|
||||
item.skuName,
|
||||
externalSkuCode,
|
||||
])
|
||||
const externalItemId = pickFirstNonEmpty([item.externalItemId, item.itemId])
|
||||
const externalSkuCode = pickFirstNonEmpty([item.externalSkuCode, item.skuCode, externalItemId])
|
||||
const externalSkuName = pickFirstNonEmpty([item.externalSkuName, item.skuName, externalSkuCode])
|
||||
const externalSkuNameNormalized = normalizeProductName(externalSkuName)
|
||||
|
||||
if (isOpen91KuaishouOrder(provider, platform)) {
|
||||
@@ -293,10 +277,7 @@ async function resolveConfiguredItemCandidate({
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const resolvedSkuCode = pickFirstNonEmpty([externalSkuCode, externalItemId])
|
||||
|
||||
return {
|
||||
externalItemId,
|
||||
@@ -370,22 +351,32 @@ let affiliateDashSkuCache: { skus: Set<string>; fetchedAt: number } | null = nul
|
||||
|
||||
async function getAffiliateDashSkuSet(): Promise<Set<string>> {
|
||||
const now = Date.now()
|
||||
if (affiliateDashSkuCache && now - affiliateDashSkuCache.fetchedAt < AFFILIATE_DASH_SKU_CACHE_TTL_MS) {
|
||||
if (
|
||||
affiliateDashSkuCache &&
|
||||
now - affiliateDashSkuCache.fetchedAt < AFFILIATE_DASH_SKU_CACHE_TTL_MS
|
||||
) {
|
||||
return affiliateDashSkuCache.skus
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await listAllAffiliateDashProducts()
|
||||
const skus = new Set<string>(result.list.map((product) => String(product.sku || '').trim()).filter(Boolean))
|
||||
const skus = new Set<string>(
|
||||
result.list.map((product) => String(product.sku || '').trim()).filter(Boolean),
|
||||
)
|
||||
affiliateDashSkuCache = { skus, fetchedAt: now }
|
||||
return skus
|
||||
} catch (error) {
|
||||
// 拉取失败:返回上次缓存(即使过期)或空集合,匹配 miss 走其他通道,不阻塞下单;
|
||||
// warn 日志便于线上排查(密钥未配/平台不可用都会导致透传降级为 miss)。
|
||||
logIntegration('[affiliate-dash]', 'affiliate-dash 商品列表拉取失败,透传降级为未命中', {
|
||||
cachedSkuCount: affiliateDashSkuCache?.skus.size || 0,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[affiliate-dash]',
|
||||
'affiliate-dash 商品列表拉取失败,透传降级为未命中',
|
||||
{
|
||||
cachedSkuCount: affiliateDashSkuCache?.skus.size || 0,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
return affiliateDashSkuCache?.skus || new Set<string>()
|
||||
}
|
||||
}
|
||||
@@ -394,10 +385,7 @@ async function getAffiliateDashSkuSet(): Promise<Set<string>> {
|
||||
* 91 商品编码为中文名(如「幸运币90个」),映射键查询做归一化:
|
||||
* NFKC(全角→半角)→ 去所有空白 → 小写。先原样精确,再归一化遍历。
|
||||
*/
|
||||
export function matchAffiliateDashSku(
|
||||
mapping: AffiliateDashSkuMapping,
|
||||
productNo: string,
|
||||
): string {
|
||||
export function matchAffiliateDashSku(mapping: AffiliateDashSkuMapping, productNo: string): string {
|
||||
const raw = String(productNo || '').trim()
|
||||
if (!raw) {
|
||||
return ''
|
||||
@@ -425,8 +413,9 @@ export function normalizeAffiliateDashMappingKey(value: unknown): string {
|
||||
}
|
||||
|
||||
function isOpen91KuaishouOrder(provider: unknown, platform: unknown) {
|
||||
return String(provider || '').trim() === '91kaquan' &&
|
||||
String(platform || '').trim() === 'kuaishou'
|
||||
return (
|
||||
String(provider || '').trim() === '91kaquan' && String(platform || '').trim() === 'kuaishou'
|
||||
)
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
@@ -2,10 +2,7 @@ import path from 'node:path'
|
||||
|
||||
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||
import {
|
||||
readAppConfigEntry,
|
||||
saveAppConfigEntry,
|
||||
} from '../config/app-config-store.js'
|
||||
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
|
||||
import { normalizeCloudtentaclesMatchName } from '../order/cloudtentacles-match-utils.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js'
|
||||
@@ -113,11 +110,13 @@ export function normalizeFulfillmentRoutingConfig(rawValue: unknown): Fulfillmen
|
||||
defaultExecutorPriority:
|
||||
defaultExecutorPriority.length > 0 ? defaultExecutorPriority : [...DEFAULT_EXECUTOR_PRIORITY],
|
||||
unmatchedExecutorKey:
|
||||
unmatchedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH ? unmatchedExecutorKey : '',
|
||||
unmatchedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
? unmatchedExecutorKey
|
||||
: '',
|
||||
executors: ROUTABLE_EXECUTOR_KEYS.reduce<Record<string, FulfillmentRoutingExecutorConfig>>(
|
||||
(result, executorKey) => {
|
||||
const executorConfig = isPlainObject(sourceExecutors[executorKey])
|
||||
? sourceExecutors[executorKey] as JsonObject
|
||||
? (sourceExecutors[executorKey] as JsonObject)
|
||||
: {}
|
||||
result[executorKey] = {
|
||||
enabled: executorConfig.enabled !== false,
|
||||
@@ -383,17 +382,19 @@ function findMatchedRoutingRule(productName: unknown, rules: FulfillmentRoutingR
|
||||
return null
|
||||
}
|
||||
|
||||
return rules.find((rule) => {
|
||||
if (rule.enabled === false || !rule.normalizedProductName) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
rules.find((rule) => {
|
||||
if (rule.enabled === false || !rule.normalizedProductName) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (rule.matchType === 'contains') {
|
||||
return normalizedProductName.includes(rule.normalizedProductName)
|
||||
}
|
||||
if (rule.matchType === 'contains') {
|
||||
return normalizedProductName.includes(rule.normalizedProductName)
|
||||
}
|
||||
|
||||
return normalizedProductName === rule.normalizedProductName
|
||||
}) || null
|
||||
return normalizedProductName === rule.normalizedProductName
|
||||
}) || null
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeFulfillmentRoutingRule(rawValue: unknown): FulfillmentRoutingRule | null {
|
||||
@@ -409,7 +410,8 @@ function normalizeFulfillmentRoutingRule(rawValue: unknown): FulfillmentRoutingR
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(source.id || `${normalizedProductName}:${executorKey}`).trim() ||
|
||||
id:
|
||||
String(source.id || `${normalizedProductName}:${executorKey}`).trim() ||
|
||||
`${normalizedProductName}:${executorKey}`,
|
||||
enabled: source.enabled !== false,
|
||||
productName,
|
||||
@@ -446,7 +448,11 @@ function normalizeExecutorPriority(value: unknown) {
|
||||
|
||||
for (const item of rawItems) {
|
||||
const executorKey = normalizeExecutorKey(item)
|
||||
if (!executorKey || seen.has(executorKey) || executorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH) {
|
||||
if (
|
||||
!executorKey ||
|
||||
seen.has(executorKey) ||
|
||||
executorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
) {
|
||||
continue
|
||||
}
|
||||
seen.add(executorKey)
|
||||
|
||||
Reference in New Issue
Block a user