统一前后端代码格式化配置

This commit is contained in:
yml2213
2026-08-16 17:27:12 +08:00
parent 8705f6a6a1
commit 5c7c3e14e3
278 changed files with 6386 additions and 5500 deletions
@@ -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('isKuaishouCloudBindingMutationFrozendispatch.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)