diff --git a/apps/backend/data/kuaishou-cloud-fulfillment.example.json b/apps/backend/data/kuaishou-cloud-fulfillment.example.json index 41e470e5..29ccc46e 100644 --- a/apps/backend/data/kuaishou-cloud-fulfillment.example.json +++ b/apps/backend/data/kuaishou-cloud-fulfillment.example.json @@ -14,8 +14,7 @@ "externalItemId": "", "externalSkuName": "示例外部商品", "resolvedSkuName": "示例快手云商品", - "cloudSourceKey": "default", - "cloudSourceKeyFallbacks": [], + "cloudSourceKeys": ["account_example"], "cloudSkuId": 1, "cloudSkuName": "示例云商品", "autoBuyEnabled": true, diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.ts b/apps/backend/src/services/admin/admin-read-shared-helpers.ts index 1307acc2..5e419522 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.ts +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.ts @@ -84,9 +84,8 @@ export function mapKuaishouCloudFulfillmentContext(value: unknown): JsonRecord | }, binding: { prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending', - cloudSourceKey: String(binding.cloudSourceKey || '').trim(), - cloudSourceKeyFallbacks: Array.isArray(binding.cloudSourceKeyFallbacks) - ? binding.cloudSourceKeyFallbacks.map((value: unknown) => String(value || '').trim()).filter(Boolean) + cloudSourceKeys: Array.isArray(binding.cloudSourceKeys) + ? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean) : [], resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(), skuId: Number(binding.skuId || 0) || 0, diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.test.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.test.ts index 64aa9747..3bb77f9d 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.test.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.test.ts @@ -79,7 +79,7 @@ test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', () id: ' item-1 ', provider: '', platform: '', - cloudSourceKey: '', + cloudSourceKeys: [' account_a ', 'account_b'], cloudSkuId: '0', autoConsumeAfterDispatch: true, kuaishouConsumeShopId: ' ks1 ', @@ -103,8 +103,7 @@ test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', () externalItemId: '', externalSkuName: '', resolvedSkuName: '', - cloudSourceKey: 'default', - cloudSourceKeyFallbacks: [], + cloudSourceKeys: ['account_a', 'account_b'], cloudSkuId: 0, cloudSkuName: '', vnKey: '', diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts index 587d5430..557cf93f 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts @@ -75,10 +75,8 @@ export function mapAdminKuaishouCloudFulfillmentItem(item: JsonObject = {}) { externalItemId: String(item.externalItemId || "").trim(), externalSkuName: String(item.externalSkuName || "").trim(), resolvedSkuName: String(item.resolvedSkuName || "").trim(), - cloudSourceKey: - String(item.cloudSourceKey || "default").trim() || "default", - cloudSourceKeyFallbacks: Array.isArray(item.cloudSourceKeyFallbacks) - ? item.cloudSourceKeyFallbacks + cloudSourceKeys: Array.isArray(item.cloudSourceKeys) + ? item.cloudSourceKeys .map((value) => String(value || "").trim()) .filter(Boolean) : [], diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts index 50047e51..9b8a22a0 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts @@ -75,7 +75,7 @@ export async function prepareAdminTaskKuaishouCloudFulfillment( const taskContext = parseTaskContext(task) const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment) - const cloudContext = resolvePersistedCloudtentaclesContext() + const cloudContext = resolvePersistedCloudtentaclesContext(flow.binding.cloudSourceKeys) const [knapsack, skuList] = await Promise.all([ getCloudtentaclesKnapsack(cloudContext), listCloudtentaclesSku(cloudContext), @@ -164,6 +164,7 @@ export async function prepareAdminTaskKuaishouCloudFulfillment( ...flowWithResolvedBinding, binding: { ...flowWithResolvedBinding.binding, + resolvedSourceKey: cloudContext.resolvedSourceKey, vnKey: preparedBinding.vnKey, prepareStatus: 'ready', vnId, @@ -250,7 +251,10 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment( const taskContext = parseTaskContext(task) const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment) - const cloudContext = resolvePersistedCloudtentaclesContext() + const cloudContext = resolvePersistedCloudtentaclesContext([ + flow.binding.resolvedSourceKey, + ...flow.binding.cloudSourceKeys, + ]) if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) { throw createHttpError('当前任务还没有准备好绑定资源,请先准备绑定资源', { @@ -364,7 +368,10 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo( }) } - const cloudContext = resolvePersistedCloudtentaclesContext() + const cloudContext = resolvePersistedCloudtentaclesContext([ + flow.binding.resolvedSourceKey, + ...flow.binding.cloudSourceKeys, + ]) const bindInfoResult = await getCloudtentaclesBindInfo({ ...cloudContext, key: flow.binding.vnKey, @@ -441,7 +448,10 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment( const taskContext = parseTaskContext(task) const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment) - const cloudContext = resolvePersistedCloudtentaclesContext() + const cloudContext = resolvePersistedCloudtentaclesContext([ + flow.binding.resolvedSourceKey, + ...flow.binding.cloudSourceKeys, + ]) if (!flow.binding.vnId || !flow.binding.vnKey) { throw createHttpError('当前任务缺少可退还的虚拟号信息', { diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts b/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts index 1400f423..3358e966 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts @@ -1,7 +1,7 @@ import { createHttpError } from '../../../utils/http.js' import type { HttpErrorLike } from '../../../utils/http.js' import { - resolvePersistedCloudtentaclesContext as resolveFulfillmentPersistedCloudtentaclesContext, + resolvePersistedCloudtentaclesContextBySourceKeys, } from '../../fulfillment/kuaishou-cloud/cloudtentacles-context.js' export { @@ -23,6 +23,7 @@ export type CloudtentaclesContext = { token: string deviceId: string deviceType: number + resolvedSourceKey: string } export type CloudSkuLikeItem = { @@ -62,9 +63,9 @@ export type PreparedKuaishouCloudBindResource = { bindUrl: string } -export function resolvePersistedCloudtentaclesContext(): CloudtentaclesContext { +export function resolvePersistedCloudtentaclesContext(sourceKeys: unknown[]): CloudtentaclesContext { try { - return resolveFulfillmentPersistedCloudtentaclesContext() + return resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys) } catch (error) { if (isMissingCloudtentaclesTokenError(error)) { throw createHttpError('当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验', { diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts index cde8804f..a77dd140 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts @@ -172,7 +172,10 @@ export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow) }, binding: { prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending', - cloudSourceKey: String(binding.cloudSourceKey || '').trim(), + cloudSourceKeys: Array.isArray(binding.cloudSourceKeys) + ? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean) + : [], + resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(), skuId: Number(binding.skuId || 0) || 0, skuName: String(binding.skuName || '').trim(), vnKey: String(binding.vnKey || '').trim(), diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts index 9cfc8d37..ea13b759 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts @@ -1,56 +1,18 @@ import { createHttpError } from "../../../utils/http.js"; -import { - getCloudtentaclesSourceConfig, - getCloudtentaclesSourceByKey, -} from "../../platforms/cloudtentacles/source-config-service.js"; -import { - getCloudtentaclesSessionState, - getCloudtentaclesSessionStateByKey, -} from "../../platforms/cloudtentacles/session-state-service.js"; +import { getCloudtentaclesSourceByKey } from "../../platforms/cloudtentacles/source-config-service.js"; +import { getCloudtentaclesSessionStateByKey } from "../../platforms/cloudtentacles/session-state-service.js"; import { normalizeStringArray } from "./domain.js"; -export function resolvePersistedCloudtentaclesContext(sourceKey = "default") { - const source = - getCloudtentaclesSourceByKey(sourceKey) || getCloudtentaclesSourceConfig(); - const session = - getCloudtentaclesSessionStateByKey(sourceKey) || - getCloudtentaclesSessionState(); - const token = String(session.token || "").trim(); - - if (!token) { - throw createHttpError( - "当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验", - { - statusCode: 409, - errorCode: "kuaishou_cloud_missing_cloud_token", - } - ); - } - - return { - baseUrl: - String(session.baseUrl || source.baseUrl || "").trim() || - "https://123.207.217.176", - token, - deviceId: String(session.deviceId || source.deviceId || "-").trim() || "-", - deviceType: Number(session.deviceType ?? source.deviceType ?? 0), - }; -} - /** - * 按优先级依次尝试 cloudSourceKey 和 fallbacks 列表中的账号, + * 按优先级依次尝试 cloudSourceKeys 列表中的账号, * 找到第一个有可用 token 的账号返回其 context。 * 将实际使用的 resolvedSourceKey 也返回,确保后续操作(退号、发货等) * 使用同一个 sourceKey,防止跨账号操作导致数据不一致。 */ -export function resolvePersistedCloudtentaclesContextWithFallback( - primarySourceKey = "default", - fallbacks: unknown[] = [] +export function resolvePersistedCloudtentaclesContextBySourceKeys( + sourceKeys: unknown[] = [] ) { - const candidates = [ - String(primarySourceKey || "default").trim() || "default", - ...normalizeStringArray(fallbacks), - ]; + const candidates = normalizeStringArray(sourceKeys); const uniqueCandidates = [...new Set(candidates)]; @@ -101,7 +63,7 @@ export function resolvePersistedCloudtentaclesContextWithFallback( throw ( lastError || createHttpError( - "所有 cloudtentacles 备选账号均不可用,请先到平台配置完成登录校验", + "所有 cloudtentacles 账号均不可用,请先到平台配置完成登录校验", { statusCode: 409, errorCode: "kuaishou_cloud_all_source_keys_exhausted" } ) ); diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts index deecbc22..afcf4753 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts @@ -62,11 +62,7 @@ export function normalizeKuaishouCloudFlow(value: unknown) { binding: { prepareStatus: String(binding.prepareStatus || "pending").trim() || "pending", - cloudSourceKey: - String(binding.cloudSourceKey || "default").trim() || "default", - cloudSourceKeyFallbacks: normalizeStringArray( - binding.cloudSourceKeyFallbacks - ), + cloudSourceKeys: normalizeStringArray(binding.cloudSourceKeys), resolvedSourceKey: String(binding.resolvedSourceKey || "").trim(), skuId: Number(binding.skuId || 0) || 0, skuName: String(binding.skuName || "").trim(), diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/index.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/index.ts index b35a8ae4..de687416 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/index.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/index.ts @@ -35,7 +35,7 @@ import { resolveKuaishouCloudVnKeyCandidates, } from "./binding-resources.js"; import { - resolvePersistedCloudtentaclesContextWithFallback, + resolvePersistedCloudtentaclesContextBySourceKeys, } from "./cloudtentacles-context.js"; import { getTaskClaimExpiresAt, @@ -55,8 +55,7 @@ export { resolveKuaishouCloudBindUrlExpiresAt, } from "./domain.js"; export { - resolvePersistedCloudtentaclesContext, - resolvePersistedCloudtentaclesContextWithFallback, + resolvePersistedCloudtentaclesContextBySourceKeys, } from "./cloudtentacles-context.js"; export async function ensureTaskClaimLink(task: TaskRow) { @@ -135,9 +134,8 @@ export async function prepareKuaishouCloudFulfillmentTask( }; } - const cloudContext = resolvePersistedCloudtentaclesContextWithFallback( - flow.binding.cloudSourceKey || "default", - flow.binding.cloudSourceKeyFallbacks || [] + const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys( + flow.binding.cloudSourceKeys ); const [knapsack, skuList] = await Promise.all([ getCloudtentaclesKnapsack(cloudContext), @@ -335,12 +333,10 @@ export async function refreshKuaishouCloudTaskBindUrl( }); } - const effectiveSourceKey = - flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default"; - const cloudContext = resolvePersistedCloudtentaclesContextWithFallback( - effectiveSourceKey, - flow.binding.cloudSourceKeyFallbacks || [] - ); + const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([ + flow.binding.resolvedSourceKey, + ...flow.binding.cloudSourceKeys, + ]); const oldVnKey = flow.binding.vnKey; const oldVnId = flow.binding.vnId; const oldVnPhone = flow.binding.vnPhone; @@ -707,12 +703,10 @@ export async function refreshKuaishouCloudTaskRoleInfo( } } - const effectiveSourceKey = - flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default"; - const cloudContext = resolvePersistedCloudtentaclesContextWithFallback( - effectiveSourceKey, - flow.binding.cloudSourceKeyFallbacks || [] - ); + const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([ + flow.binding.resolvedSourceKey, + ...flow.binding.cloudSourceKeys, + ]); const bindInfoResult = await getCloudtentaclesBindInfo({ ...cloudContext, key: flow.binding.vnKey, diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts index f069c1b5..b52a132f 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts @@ -18,7 +18,7 @@ import { normalizeKuaishouCloudFlow, type JsonObject, } from "./domain.js"; -import { resolvePersistedCloudtentaclesContextWithFallback } from "./cloudtentacles-context.js"; +import { resolvePersistedCloudtentaclesContextBySourceKeys } from "./cloudtentacles-context.js"; import { normalizeActor, parseTaskContext } from "./task-context.js"; import type { TaskRow } from "../../../types/repository/rows.js"; @@ -37,12 +37,10 @@ export async function dispatchKuaishouCloudFulfillmentTask( const now = nowIso(); const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); - const effectiveSourceKey = - flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default"; - const cloudContext = resolvePersistedCloudtentaclesContextWithFallback( - effectiveSourceKey, - flow.binding.cloudSourceKeyFallbacks || [] - ); + const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([ + flow.binding.resolvedSourceKey, + ...flow.binding.cloudSourceKeys, + ]); if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) { throw createHttpError( @@ -178,12 +176,10 @@ export async function returnKuaishouCloudFulfillmentTask( const now = nowIso(); const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); - const effectiveSourceKey = - flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default"; - const cloudContext = resolvePersistedCloudtentaclesContextWithFallback( - effectiveSourceKey, - flow.binding.cloudSourceKeyFallbacks || [] - ); + const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([ + flow.binding.resolvedSourceKey, + ...flow.binding.cloudSourceKeys, + ]); if (!flow.binding.vnId || !flow.binding.vnKey) { throw createHttpError("当前任务缺少可退还的虚拟号信息", { diff --git a/apps/backend/src/services/order/delivery-task-service.ts b/apps/backend/src/services/order/delivery-task-service.ts index 7a8953b9..02f37978 100644 --- a/apps/backend/src/services/order/delivery-task-service.ts +++ b/apps/backend/src/services/order/delivery-task-service.ts @@ -185,7 +185,7 @@ export async function syncDeliveryTasksForOrderWithDeps( }, binding: { prepareStatus: 'pending', - cloudSourceKey: String(cloudtentaclesConfig.cloudSourceKey || 'default').trim() || 'default', + cloudSourceKeys: normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys), skuId: Number(cloudtentaclesConfig.skuId || 0) || 0, skuName: String(cloudtentaclesConfig.skuName || '').trim(), vnKey: '1', @@ -384,6 +384,16 @@ function parseJsonObject(value: unknown): JsonObject { } } +function normalizeStringArray(value: unknown): string[] { + if (!Array.isArray(value)) { + return [] + } + + return Array.from( + new Set(value.map((item) => String(item || '').trim()).filter(Boolean)), + ) +} + function isTaskRow(task: TaskRow | DeliveryTaskRow | null | undefined): task is TaskRow { return Boolean(task && Number(task.id || 0) > 0) } diff --git a/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.test.ts b/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.test.ts index 080333a6..8503975b 100644 --- a/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.test.ts +++ b/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.test.ts @@ -17,6 +17,7 @@ test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => { externalSkuCode: '952', externalItemId: '952', externalSkuName: '测试2', + cloudSourceKeys: ['account_a', 'account_b'], cloudSkuId: 28, }, ], @@ -35,8 +36,7 @@ test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => { flowType: 'kuaishou_cloud_fulfillment', configId: '', cloudtentacles: { - cloudSourceKey: 'default', - cloudSourceKeyFallbacks: [], + cloudSourceKeys: ['account_a', 'account_b'], skuId: 28, skuName: '', vnKey: '1', diff --git a/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.ts b/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.ts index 44f02040..79e40ae8 100644 --- a/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.ts +++ b/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.ts @@ -46,11 +46,7 @@ export function mapKuaishouCloudFulfillmentItemsToBindings(config: JsonObject = flowType: "kuaishou_cloud_fulfillment", configId: String(item.id || "").trim(), cloudtentacles: { - cloudSourceKey: - String(item.cloudSourceKey || "default").trim() || "default", - cloudSourceKeyFallbacks: Array.isArray(item.cloudSourceKeyFallbacks) - ? item.cloudSourceKeyFallbacks - : [], + cloudSourceKeys: normalizeStringArray(item.cloudSourceKeys), skuId: normalizePositiveInteger(item.cloudSkuId), skuName: String(item.cloudSkuName || "").trim(), vnKey: "1", @@ -113,6 +109,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue: unknown) { const internalSkuCode = String(rawValue.internalSkuCode || "").trim(); const cloudSkuId = normalizePositiveInteger(rawValue.cloudSkuId); + const cloudSourceKeys = normalizeStringArray(rawValue.cloudSourceKeys); const externalSkuCode = String(rawValue.externalSkuCode || "").trim(); const externalItemId = String(rawValue.externalItemId || "").trim(); const externalSkuName = String(rawValue.externalSkuName || "").trim(); @@ -135,6 +132,10 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue: unknown) { return null; } + if (cloudSourceKeys.length === 0) { + return null; + } + if (!externalSkuCode && !externalItemId && !externalSkuName) { return null; } @@ -155,11 +156,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue: unknown) { resolvedSkuName: String( rawValue.resolvedSkuName || rawValue.internalSkuName || "" ).trim(), - cloudSourceKey: - String(rawValue.cloudSourceKey || "default").trim() || "default", - cloudSourceKeyFallbacks: normalizeStringArray( - rawValue.cloudSourceKeyFallbacks - ), + cloudSourceKeys, cloudSkuId, cloudSkuName: String(rawValue.cloudSkuName || "").trim(), vnKey: "1", @@ -204,12 +201,5 @@ function normalizeStringArray(value: unknown) { if (Array.isArray(value)) { return value.map((v: unknown) => String(v || "").trim()).filter(Boolean); } - // 兼容旧格式:字符串用逗号分隔 - if (typeof value === "string") { - return value - .split(",") - .map((v: string) => v.trim()) - .filter(Boolean); - } return []; } diff --git a/apps/backend/src/types/admin/write-inputs.ts b/apps/backend/src/types/admin/write-inputs.ts index 0a7350d5..8c5a2785 100644 --- a/apps/backend/src/types/admin/write-inputs.ts +++ b/apps/backend/src/types/admin/write-inputs.ts @@ -11,8 +11,7 @@ export type AdminKuaishouCloudFulfillmentItemInput = { externalItemId?: string externalSkuName?: string resolvedSkuName?: string - cloudSourceKey?: string - cloudSourceKeyFallbacks?: string[] + cloudSourceKeys?: string[] cloudSkuId?: number | string cloudSkuName?: string vnKey?: string diff --git a/apps/frontend/src/types/admin/platform-config/kuaishou-cloud-fulfillment.ts b/apps/frontend/src/types/admin/platform-config/kuaishou-cloud-fulfillment.ts index c08140b0..6cce132d 100644 --- a/apps/frontend/src/types/admin/platform-config/kuaishou-cloud-fulfillment.ts +++ b/apps/frontend/src/types/admin/platform-config/kuaishou-cloud-fulfillment.ts @@ -11,7 +11,7 @@ export interface AdminKuaishouCloudFulfillmentItem { externalItemId: string externalSkuName: string resolvedSkuName: string - cloudSourceKey: string + cloudSourceKeys: string[] cloudSkuId: number cloudSkuName: string vnKey: string @@ -22,7 +22,6 @@ export interface AdminKuaishouCloudFulfillmentItem { kuaishouConsumeShopId: string kuaishouConsumeShopName: string notes: string - cloudSourceKeyFallbacks?: string[] } export interface AdminKuaishouCloudFulfillmentConfig { diff --git a/apps/frontend/src/types/admin/tasks.ts b/apps/frontend/src/types/admin/tasks.ts index f791a628..4994f88d 100644 --- a/apps/frontend/src/types/admin/tasks.ts +++ b/apps/frontend/src/types/admin/tasks.ts @@ -126,8 +126,7 @@ export interface AdminTaskDetail { } binding: { prepareStatus: string - cloudSourceKey: string - cloudSourceKeyFallbacks: string[] + cloudSourceKeys: string[] resolvedSourceKey: string skuId: number skuName: string diff --git a/apps/frontend/src/types/claim.ts b/apps/frontend/src/types/claim.ts index f05d76d1..7ef213c7 100644 --- a/apps/frontend/src/types/claim.ts +++ b/apps/frontend/src/types/claim.ts @@ -74,7 +74,8 @@ export interface ClaimKuaishouCloudFlowInfo { } binding: { prepareStatus: string - cloudSourceKey: string + cloudSourceKeys: string[] + resolvedSourceKey: string skuId: number skuName: string vnKey: string diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/AdminKuaishouCloudFulfillmentView.vue b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/AdminKuaishouCloudFulfillmentView.vue index a3a1595d..f6f36555 100644 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/AdminKuaishouCloudFulfillmentView.vue +++ b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/AdminKuaishouCloudFulfillmentView.vue @@ -58,6 +58,7 @@ onMounted(config.loadConfigs) :get-card-summary="config.getCardSummary" :get-rule-state="config.getRuleState" :get-external-match-summary="config.getExternalMatchSummary" + :get-cloud-source-summary="config.getCloudSourceSummary" :get-cloud-sku-summary="config.getCloudSkuSummary" :get-consume-shop-summary="config.getConsumeShopSummary" :get-cloud-sku-options="cloudSku.getCloudSkuOptions" @@ -78,6 +79,7 @@ onMounted(config.loadConfigs) (item, visible) => cloudSku.handleCloudSkuDropdownVisible(item, visible) " @handle-kuaishou-consume-shop-selected="config.handleKuaishouConsumeShopSelected($event)" + @handle-cloud-source-keys-changed="config.handleCloudSourceKeysChanged($event)" @remove-item="config.removeItem($event)" /> diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudRuleCard.vue b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudRuleCard.vue index 36f74afa..d5dc1caf 100644 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudRuleCard.vue +++ b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudRuleCard.vue @@ -16,6 +16,7 @@ defineProps<{ getCardSummary: (item: EditableItem) => string getRuleState: (item: EditableItem) => { label: string; tone: string } getExternalMatchSummary: (item: EditableItem) => string + getCloudSourceSummary: (item: EditableItem) => string getCloudSkuSummary: (item: EditableItem) => string getConsumeShopSummary: (item: EditableItem) => string getCloudSkuOptions: (item: EditableItem) => AdminCloudtentaclesSkuItem[] @@ -29,6 +30,7 @@ const emit = defineEmits<{ handleCloudSkuSelected: [value: number | string | undefined] handleCloudSkuDropdownVisible: [visible: boolean] handleKuaishouConsumeShopSelected: [] + handleCloudSourceKeysChanged: [] remove: [] }>() @@ -66,6 +68,9 @@ const emit = defineEmits<{ {{ item.internalSkuCode || '-' }} + {{ + getCloudSourceSummary(item) + }} {{ getCloudSkuSummary(item) }} {{ getConsumeShopSummary(item) @@ -178,11 +183,15 @@ const emit = defineEmits<{ placeholder="展示用,可选但建议填" /> - + - - - - - + 执行时会从左到右尝试,SKU 列表使用第一个账号加载。 string getRuleState: (item: EditableItem) => { label: string; tone: string } getExternalMatchSummary: (item: EditableItem) => string + getCloudSourceSummary: (item: EditableItem) => string getCloudSkuSummary: (item: EditableItem) => string getConsumeShopSummary: (item: EditableItem) => string getCloudSkuOptions: (item: EditableItem) => AdminCloudtentaclesSkuItem[] @@ -42,6 +43,7 @@ const emit = defineEmits<{ handleCloudSkuSelected: [item: EditableItem, value: number | string | undefined] handleCloudSkuDropdownVisible: [item: EditableItem, visible: boolean] handleKuaishouConsumeShopSelected: [item: EditableItem] + handleCloudSourceKeysChanged: [item: EditableItem] removeItem: [localId: string] }>() @@ -125,6 +127,7 @@ const emit = defineEmits<{ :get-card-summary="getCardSummary" :get-rule-state="getRuleState" :get-external-match-summary="getExternalMatchSummary" + :get-cloud-source-summary="getCloudSourceSummary" :get-cloud-sku-summary="getCloudSkuSummary" :get-consume-shop-summary="getConsumeShopSummary" :get-cloud-sku-options="getCloudSkuOptions" @@ -135,6 +138,7 @@ const emit = defineEmits<{ @handle-cloud-sku-selected="emit('handleCloudSkuSelected', item, $event)" @handle-cloud-sku-dropdown-visible="emit('handleCloudSkuDropdownVisible', item, $event)" @handle-kuaishou-consume-shop-selected="emit('handleKuaishouConsumeShopSelected', item)" + @handle-cloud-source-keys-changed="emit('handleCloudSourceKeysChanged', item)" @remove="emit('removeItem', item.localId)" /> diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/types.ts b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/types.ts index a65bc7ed..39989986 100644 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/types.ts +++ b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/types.ts @@ -2,6 +2,7 @@ import type { AdminKuaishouCloudFulfillmentItem } from '@/types/admin' export type EditableItem = AdminKuaishouCloudFulfillmentItem & { localId: string + cloudSourceKeys: string[] } export type ValidationState = { diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudConfig.ts b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudConfig.ts index 0301aee8..383bdd9b 100644 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudConfig.ts +++ b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudConfig.ts @@ -61,6 +61,7 @@ export function useKuaishouCloudConfig() { function createEmptyItem(): EditableItem { const defaultShop = getDefaultKuaishouConsumeShop() + const sourceKeys = getDefaultCloudSourceKeys() return { localId: crypto.randomUUID(), id: '', @@ -75,7 +76,7 @@ export function useKuaishouCloudConfig() { externalItemId: '', externalSkuName: '', resolvedSkuName: '', - cloudSourceKey: 'default', + cloudSourceKeys: sourceKeys, cloudSkuId: 0, cloudSkuName: '', vnKey: '1', @@ -86,7 +87,6 @@ export function useKuaishouCloudConfig() { kuaishouConsumeShopId: defaultShop?.shopId || '', kuaishouConsumeShopName: defaultShop?.kshopName || '', notes: '', - cloudSourceKeyFallbacks: [], } } @@ -95,6 +95,7 @@ export function useKuaishouCloudConfig() { item.kuaishouConsumeShopId, item.kuaishouConsumeShopName, ) + const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys) return { localId: crypto.randomUUID(), id: item.id || crypto.randomUUID(), @@ -109,7 +110,7 @@ export function useKuaishouCloudConfig() { externalItemId: item.externalItemId || '', externalSkuName: item.externalSkuName || '', resolvedSkuName: item.resolvedSkuName || '', - cloudSourceKey: item.cloudSourceKey || 'default', + cloudSourceKeys: sourceKeys, cloudSkuId: Number(item.cloudSkuId || 0) || 0, cloudSkuName: item.cloudSkuName || '', vnKey: item.vnKey || '', @@ -120,9 +121,6 @@ export function useKuaishouCloudConfig() { kuaishouConsumeShopId: matchedShop?.shopId || item.kuaishouConsumeShopId || '', kuaishouConsumeShopName: matchedShop?.kshopName || item.kuaishouConsumeShopName || '', notes: item.notes || '', - cloudSourceKeyFallbacks: Array.isArray(item.cloudSourceKeyFallbacks) - ? item.cloudSourceKeyFallbacks - : [], } } @@ -131,6 +129,7 @@ export function useKuaishouCloudConfig() { item.kuaishouConsumeShopId, item.kuaishouConsumeShopName, ) + const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys) return { id: item.id.trim() || item.internalSkuCode.trim() || item.localId, enabled: item.enabled, @@ -144,7 +143,7 @@ export function useKuaishouCloudConfig() { externalItemId: item.externalItemId.trim(), externalSkuName: item.externalSkuName.trim(), resolvedSkuName: item.resolvedSkuName.trim(), - cloudSourceKey: item.cloudSourceKey.trim() || 'default', + cloudSourceKeys: sourceKeys, cloudSkuId: Number(item.cloudSkuId || 0) || 0, cloudSkuName: item.cloudSkuName.trim(), vnKey: item.vnKey.trim(), @@ -157,12 +156,20 @@ export function useKuaishouCloudConfig() { matchedShop?.kshopName || item.kuaishouConsumeShopName, ).trim(), notes: item.notes.trim(), - cloudSourceKeyFallbacks: Array.isArray(item.cloudSourceKeyFallbacks) - ? item.cloudSourceKeyFallbacks.filter(Boolean) - : [], } } + function normalizeCloudSourceKeys(values: unknown[] = []) { + const keys = values + .map((value) => String(value || '').trim()) + .filter(Boolean) + return Array.from(new Set(keys)) + } + + function getDefaultCloudSourceKeys() { + return normalizeCloudSourceKeys([cloudtentaclesSourceOptions.value[0]?.key]) + } + // ── validation helpers ──────────────────────────────── function hasExternalMatch( @@ -181,7 +188,7 @@ export function useKuaishouCloudConfig() { item.externalItemId.trim() || item.externalSkuName.trim() || item.resolvedSkuName.trim() || - item.cloudSourceKey.trim() !== 'default' || + item.cloudSourceKeys.length > 0 || Number(item.cloudSkuId || 0) > 0 || item.cloudSkuName.trim() || item.vnKey.trim() || @@ -203,6 +210,7 @@ export function useKuaishouCloudConfig() { function isItemComplete(item: EditableItem) { return Boolean( item.internalSkuCode.trim() && + item.cloudSourceKeys.length > 0 && Number(item.cloudSkuId || 0) > 0 && (!item.autoConsumeAfterDispatch || Boolean(item.kuaishouConsumeShopId.trim())) && hasExternalMatch(item), @@ -220,6 +228,10 @@ export function useKuaishouCloudConfig() { return { localId: item.localId, message: `${title} 缺少内部 SKU 编码` } } + if (item.cloudSourceKeys.length === 0) { + return { localId: item.localId, message: `${title} 需要选择至少一个 cloud 账号` } + } + if (Number(item.cloudSkuId || 0) <= 0) { return { localId: item.localId, message: `${title} 需要填写 cloud SKU ID` } } @@ -340,6 +352,20 @@ export function useKuaishouCloudConfig() { return name || id || '未绑定核销店铺' } + function getCloudSourceSummary(item: EditableItem) { + const keys = normalizeCloudSourceKeys(item.cloudSourceKeys) + if (keys.length === 0) { + return '未选择 cloud 账号' + } + + return keys + .map((key, index) => { + const option = cloudtentaclesSourceOptions.value.find((source) => source.key === key) + return `${index + 1}. ${option?.label || key}` + }) + .join(' / ') + } + function getCloudSkuSummary(item: EditableItem) { if (item.cloudSkuId > 0 && item.cloudSkuName.trim()) { return `${item.cloudSkuName.trim()} · #${item.cloudSkuId}` @@ -441,6 +467,13 @@ export function useKuaishouCloudConfig() { setCollapsed(localId, false) } + function handleCloudSourceKeysChanged(item: EditableItem) { + const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys) + item.cloudSourceKeys = sourceKeys + item.cloudSkuId = 0 + item.cloudSkuName = '' + } + async function focusValidationTarget(localId: string) { if (!localId) { return @@ -524,6 +557,7 @@ export function useKuaishouCloudConfig() { getRuleState, getExternalMatchSummary, getCloudSkuSummary, + getCloudSourceSummary, getConsumeShopSummary, // consume shops getKuaishouConsumeShopOptions, @@ -531,6 +565,7 @@ export function useKuaishouCloudConfig() { getDefaultKuaishouConsumeShop, formatKuaishouConsumeShopOption, handleKuaishouConsumeShopSelected, + handleCloudSourceKeysChanged, // collapse isCollapsed, setCollapsed, diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudNinetyone.ts b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudNinetyone.ts index af95e43d..6140e96d 100644 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudNinetyone.ts +++ b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudNinetyone.ts @@ -42,6 +42,9 @@ export function useKuaishouCloudNinetyone( const defaultShop = config.getDefaultKuaishouConsumeShop() const shopOpts = config.getKuaishouConsumeShopOptions() const shop = defaultShop || (shopOpts.length > 0 ? shopOpts[0] : null) + const sourceKeys = config.cloudtentaclesSourceOptions.value[0]?.key + ? [config.cloudtentaclesSourceOptions.value[0].key] + : [] return { localId: crypto.randomUUID(), id: '', @@ -56,7 +59,7 @@ export function useKuaishouCloudNinetyone( externalItemId: productNo, externalSkuName: productName, resolvedSkuName: productName, - cloudSourceKey: 'default', + cloudSourceKeys: sourceKeys, cloudSkuId: 0, cloudSkuName: '', vnKey: '1', diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudSku.ts b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudSku.ts index 978bdf1b..ac4d8404 100644 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudSku.ts +++ b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudSku.ts @@ -12,7 +12,7 @@ export function useKuaishouCloudSku(cloudtentaclesSourceOptions?: { value: { key const cloudSkuCatalogBySourceKey = ref>({}) function getSourceKey(item: EditableItem) { - return String(item.cloudSourceKey || 'default').trim() + return String(item.cloudSourceKeys[0] || '').trim() } async function ensureCloudSkuCatalogLoaded(item: EditableItem, force = false) { @@ -149,19 +149,13 @@ export function useKuaishouCloudSku(cloudtentaclesSourceOptions?: { value: { key const count = cloudSkuCatalogBySourceKey.value[sourceKey]?.length ?? 0 showSuccess(`cloud SKU 列表(${sourceKey})已刷新,共 ${count} 条`) } else { - // No item provided — refresh all configured sourceKeys, or at least 'default' const cachedKeys = Object.keys(cloudSkuCatalogBySourceKey.value) const configuredKeys = cloudtentaclesSourceOptions?.value?.map((s) => s.key) || [] - const keysToRefresh = - cachedKeys.length > 0 - ? cachedKeys - : configuredKeys.length > 0 - ? configuredKeys - : ['default'] + const keysToRefresh = cachedKeys.length > 0 ? cachedKeys : configuredKeys for (const key of keysToRefresh) { await ensureCloudSkuCatalogLoaded( - { cloudSourceKey: key, localId: '' } as EditableItem, + { cloudSourceKeys: [key], localId: '' } as EditableItem, true, ) } @@ -177,8 +171,6 @@ export function useKuaishouCloudSku(cloudtentaclesSourceOptions?: { value: { key } const cloudSkuCatalogLoading = ref(false) - // Maintain backward-compat: derive a combined loading state - // The template uses cloudSkuCatalogLoading for the refresh button spinner function syncCloudSkuCatalogLoading() { cloudSkuCatalogLoading.value = Object.values(cloudSkuCatalogLoadingBySourceKey.value).some( Boolean, diff --git a/apps/frontend/src/views/admin/tasks/components/AdminTaskKuaishouCloudPanel.vue b/apps/frontend/src/views/admin/tasks/components/AdminTaskKuaishouCloudPanel.vue index c232f722..5fe10ed4 100644 --- a/apps/frontend/src/views/admin/tasks/components/AdminTaskKuaishouCloudPanel.vue +++ b/apps/frontend/src/views/admin/tasks/components/AdminTaskKuaishouCloudPanel.vue @@ -221,19 +221,14 @@ const emit = defineEmits<{ - 规则主账号{{ flow.binding.cloudSourceKey || '-' }} + 账号优先级 + {{ + flow.binding.cloudSourceKeys?.length ? flow.binding.cloudSourceKeys.join(' / ') : '-' + }} 实际使用账号 - {{ flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || '-' }} - - - 备选账号 - {{ - flow.binding.cloudSourceKeyFallbacks?.length - ? flow.binding.cloudSourceKeyFallbacks.join(' / ') - : '-' - }} + {{ flow.binding.resolvedSourceKey || '-' }} cloud SKU