diff --git a/apps/backend/src/db/migrations/001_init.sql b/apps/backend/src/db/migrations/001_init.sql index c53ee619..7fadbe0c 100644 --- a/apps/backend/src/db/migrations/001_init.sql +++ b/apps/backend/src/db/migrations/001_init.sql @@ -52,54 +52,6 @@ CREATE TABLE IF NOT EXISTS fulfillment_profile_requirements ( UNIQUE(profile_id, role_key) ); -CREATE TABLE IF NOT EXISTS sku_fulfillment_bindings ( - id BIGSERIAL PRIMARY KEY, - sku_code TEXT NOT NULL, - provider TEXT NOT NULL DEFAULT '', - platform TEXT NOT NULL DEFAULT '', - shop_id TEXT NOT NULL DEFAULT '', - profile_id BIGINT NOT NULL REFERENCES fulfillment_profiles(id) ON DELETE CASCADE, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - priority INTEGER NOT NULL DEFAULT 100, - config_json JSONB NOT NULL DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_sku_fulfillment_bindings_lookup - ON sku_fulfillment_bindings(sku_code, provider, platform, shop_id, enabled, priority); - -CREATE TABLE IF NOT EXISTS product_match_rules ( - id BIGSERIAL PRIMARY KEY, - provider TEXT NOT NULL DEFAULT '', - platform TEXT NOT NULL DEFAULT '', - shop_id TEXT NOT NULL DEFAULT '', - external_item_id TEXT NOT NULL DEFAULT '', - external_sku_code TEXT NOT NULL DEFAULT '', - external_sku_name TEXT NOT NULL DEFAULT '', - external_sku_name_normalized TEXT NOT NULL DEFAULT '', - resolved_sku_code TEXT NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - priority INTEGER NOT NULL DEFAULT 100, - config_json JSONB NOT NULL DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_product_match_rules_unique - ON product_match_rules( - provider, - platform, - shop_id, - external_item_id, - external_sku_code, - external_sku_name_normalized, - resolved_sku_code - ); - -CREATE INDEX IF NOT EXISTS idx_product_match_rules_lookup - ON product_match_rules(provider, platform, shop_id, enabled, priority, id); - CREATE TABLE IF NOT EXISTS orders ( id BIGSERIAL PRIMARY KEY, provider TEXT NOT NULL, diff --git a/apps/backend/src/db/migrations/003_drop_legacy_fulfillment_rules.sql b/apps/backend/src/db/migrations/003_drop_legacy_fulfillment_rules.sql new file mode 100644 index 00000000..536091d1 --- /dev/null +++ b/apps/backend/src/db/migrations/003_drop_legacy_fulfillment_rules.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS product_match_rules; +DROP TABLE IF EXISTS sku_fulfillment_bindings; diff --git a/apps/backend/src/repositories/fulfillment-profile-repo.ts b/apps/backend/src/repositories/fulfillment-profile-repo.ts index 91abfe4f..901fc01f 100644 --- a/apps/backend/src/repositories/fulfillment-profile-repo.ts +++ b/apps/backend/src/repositories/fulfillment-profile-repo.ts @@ -27,28 +27,6 @@ type FulfillmentProfileRequirementRow = { updated_at: string } -type SkuFulfillmentBindingRow = { - id: number - sku_code: string - provider: string - platform: string - shop_id: string - profile_id: number - enabled: boolean - priority: number - config_json: string | Record - created_at: string - updated_at: string - profile_key?: string - name?: string - profile_name?: string - executor_key?: string - requires_claim?: boolean - auto_dispatch?: boolean - inventory_strategy?: string - profile_config_json?: string | Record -} - type FulfillmentProfileUpsertInput = { profileKey: string name: string @@ -69,26 +47,6 @@ export type FulfillmentProfileRequirementInput = { configJson?: string | Record } -type SkuFulfillmentBindingUpsertInput = { - skuCode: string - provider?: string - platform?: string - shopId?: string - profileId: number | string - enabled?: boolean - priority?: number | string - configJson?: string | Record - createdAt: string - updatedAt: string -} - -type FulfillmentBindingResolveInput = { - skuCode: string - provider?: string - platform?: string - shopId?: string -} - export async function getFulfillmentProfileByKey(profileKey: string): Promise { const result = await query( 'SELECT * FROM fulfillment_profiles WHERE profile_key = $1 LIMIT 1', @@ -195,113 +153,3 @@ export async function listFulfillmentProfileRequirements( return result.rows } - -export async function upsertSkuFulfillmentBinding( - input: SkuFulfillmentBindingUpsertInput, -): Promise { - const existing = await query( - ` - SELECT * - FROM sku_fulfillment_bindings - WHERE sku_code = $1 AND provider = $2 AND platform = $3 AND shop_id = $4 - LIMIT 1 - `, - [input.skuCode, input.provider || '', input.platform || '', input.shopId || ''], - ) - - if (!existing.rows[0]) { - const inserted = await query( - ` - INSERT INTO sku_fulfillment_bindings ( - sku_code, - provider, - platform, - shop_id, - profile_id, - enabled, - priority, - config_json, - created_at, - updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10) - RETURNING * - `, - [ - input.skuCode, - input.provider || '', - input.platform || '', - input.shopId || '', - Number(input.profileId), - input.enabled !== false, - Number(input.priority || 100), - input.configJson || '{}', - input.createdAt, - input.updatedAt, - ], - ) - - return inserted.rows[0] || null - } - - const updated = await query( - ` - UPDATE sku_fulfillment_bindings - SET - profile_id = $1, - enabled = $2, - priority = $3, - config_json = $4::jsonb, - updated_at = $5 - WHERE id = $6 - RETURNING * - `, - [ - Number(input.profileId), - input.enabled !== false, - Number(input.priority || 100), - input.configJson || '{}', - input.updatedAt, - Number(existing.rows[0].id), - ], - ) - - return updated.rows[0] || null -} - -export async function resolveFulfillmentBinding({ - skuCode, - provider = '', - platform = '', - shopId = '', -}: FulfillmentBindingResolveInput): Promise { - const result = await query( - ` - SELECT - sfb.*, - fp.profile_key, - fp.name AS profile_name, - fp.executor_key, - fp.requires_claim, - fp.auto_dispatch, - fp.inventory_strategy, - fp.config_json AS profile_config_json - FROM sku_fulfillment_bindings sfb - JOIN fulfillment_profiles fp ON fp.id = sfb.profile_id - WHERE sfb.enabled = TRUE - AND sfb.sku_code = $1 - AND (sfb.provider = '' OR sfb.provider = $2) - AND (sfb.platform = '' OR sfb.platform = $3) - AND (sfb.shop_id = '' OR sfb.shop_id = $4) - ORDER BY - CASE WHEN sfb.shop_id = '' THEN 1 ELSE 0 END, - CASE WHEN sfb.platform = '' THEN 1 ELSE 0 END, - CASE WHEN sfb.provider = '' THEN 1 ELSE 0 END, - sfb.priority ASC, - sfb.id ASC - LIMIT 1 - `, - [skuCode, provider, platform, shopId], - ) - - return result.rows[0] || null -} diff --git a/apps/backend/src/repositories/order-repo.ts b/apps/backend/src/repositories/order-repo.ts index f04291bf..93a6d120 100644 --- a/apps/backend/src/repositories/order-repo.ts +++ b/apps/backend/src/repositories/order-repo.ts @@ -17,13 +17,6 @@ type OrderPlatformLookupInput = { platformOrderId: string } -type OrderPlatformCandidateLookupInput = { - provider?: string - platform?: string - shopIds?: unknown[] - platformOrderId?: string -} - export async function findOrderByPlatformOrderId({ provider = '91kaquan', platform, @@ -43,34 +36,6 @@ export async function findOrderByPlatformOrderId({ return result.rows[0] || null } -export async function findOrderByPlatformOrderIdCandidates({ - provider = '91kaquan', - platform, - shopIds = [], - platformOrderId, -}: OrderPlatformCandidateLookupInput): Promise { - const normalizedShopIds = [...new Set((Array.isArray(shopIds) ? shopIds : []) - .map((item) => String(item || '').trim()) - .filter(Boolean))] - - if (!platform || !platformOrderId || normalizedShopIds.length === 0) { - return null - } - - const result = await query( - ` - SELECT * - FROM orders - WHERE provider = $1 AND platform = $2 AND shop_id = ANY($3::text[]) AND platform_order_id = $4 - ORDER BY id DESC - LIMIT 1 - `, - [provider, platform, normalizedShopIds, platformOrderId], - ) - - return result.rows[0] || null -} - export async function createOrder(input: OrderCreateInput): Promise { const result = await query( ` diff --git a/apps/backend/src/repositories/product-match-rule-repo.ts b/apps/backend/src/repositories/product-match-rule-repo.ts deleted file mode 100644 index 9c77b611..00000000 --- a/apps/backend/src/repositories/product-match-rule-repo.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { query } from '../db/client.js' - -type ProductMatchRuleRow = { - id: number - provider: string - platform: string - shop_id: string - external_item_id: string - external_sku_code: string - external_sku_name: string - external_sku_name_normalized: string - resolved_sku_code: string - enabled: boolean - priority: number - config_json: string | Record - created_at: string - updated_at: string - matched_by?: string - match_score?: number -} - -type ProductMatchRuleUpsertInput = { - provider?: string - platform?: string - shopId?: string - externalItemId?: string - externalSkuCode?: string - externalSkuName?: string - externalSkuNameNormalized?: string - resolvedSkuCode: string - enabled?: boolean - priority?: number | string - configJson?: string | Record - createdAt: string - updatedAt: string -} - -type ProductMatchRuleResolveInput = { - provider?: string - platform?: string - shopId?: string - externalItemId?: string - externalSkuCode?: string - externalSkuNameNormalized?: string -} - -export async function upsertProductMatchRule( - input: ProductMatchRuleUpsertInput, -): Promise { - const result = await query( - ` - INSERT INTO product_match_rules ( - provider, - platform, - shop_id, - external_item_id, - external_sku_code, - external_sku_name, - external_sku_name_normalized, - resolved_sku_code, - enabled, - priority, - config_json, - created_at, - updated_at - ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13 - ) - ON CONFLICT ( - provider, - platform, - shop_id, - external_item_id, - external_sku_code, - external_sku_name_normalized, - resolved_sku_code - ) DO UPDATE - SET - external_sku_name = EXCLUDED.external_sku_name, - enabled = EXCLUDED.enabled, - priority = EXCLUDED.priority, - config_json = EXCLUDED.config_json, - updated_at = EXCLUDED.updated_at - RETURNING * - `, - [ - input.provider || '', - input.platform || '', - input.shopId || '', - input.externalItemId || '', - input.externalSkuCode || '', - input.externalSkuName || '', - input.externalSkuNameNormalized || '', - input.resolvedSkuCode, - input.enabled !== false, - Number(input.priority || 100), - input.configJson || '{}', - input.createdAt, - input.updatedAt, - ], - ) - - return result.rows[0] || null -} - -export async function resolveProductMatchRule({ - provider = '', - platform = '', - shopId = '', - externalItemId = '', - externalSkuCode = '', - externalSkuNameNormalized = '', -}: ProductMatchRuleResolveInput): Promise { - const result = await query( - ` - SELECT - pmr.*, - CASE - WHEN $4 != '' AND pmr.external_sku_code = $4 THEN 'external_sku_code' - WHEN $5 != '' AND pmr.external_item_id = $5 THEN 'external_item_id' - WHEN $6 != '' AND pmr.external_sku_name_normalized = $6 THEN 'external_sku_name_exact' - WHEN $6 != '' AND pmr.external_sku_name_normalized != '' AND ( - position(pmr.external_sku_name_normalized in $6) > 0 - OR position($6 in pmr.external_sku_name_normalized) > 0 - ) THEN 'external_sku_name_contains' - ELSE '' - END AS matched_by, - CASE - WHEN $4 != '' AND pmr.external_sku_code = $4 THEN 400 - WHEN $5 != '' AND pmr.external_item_id = $5 THEN 300 - WHEN $6 != '' AND pmr.external_sku_name_normalized = $6 THEN 200 - WHEN $6 != '' AND pmr.external_sku_name_normalized != '' AND ( - position(pmr.external_sku_name_normalized in $6) > 0 - OR position($6 in pmr.external_sku_name_normalized) > 0 - ) THEN 100 - ELSE 0 - END AS match_score - FROM product_match_rules pmr - WHERE pmr.enabled = TRUE - AND (pmr.provider = '' OR pmr.provider = $1) - AND (pmr.platform = '' OR pmr.platform = $2) - AND (pmr.shop_id = '' OR pmr.shop_id = $3) - AND ( - ($4 != '' AND pmr.external_sku_code = $4) - OR ($5 != '' AND pmr.external_item_id = $5) - OR ( - $6 != '' - AND pmr.external_sku_name_normalized != '' - AND ( - pmr.external_sku_name_normalized = $6 - OR position(pmr.external_sku_name_normalized in $6) > 0 - OR position($6 in pmr.external_sku_name_normalized) > 0 - ) - ) - ) - ORDER BY - match_score DESC, - CASE WHEN pmr.shop_id = '' THEN 1 ELSE 0 END, - CASE WHEN pmr.platform = '' THEN 1 ELSE 0 END, - CASE WHEN pmr.provider = '' THEN 1 ELSE 0 END, - pmr.priority ASC, - pmr.id ASC - LIMIT 1 - `, - [provider, platform, shopId, externalSkuCode, externalItemId, externalSkuNameNormalized], - ) - - return result.rows[0] || null -} diff --git a/apps/backend/src/routes/admin/platform-config.ts b/apps/backend/src/routes/admin/platform-config.ts index dd531225..c8555125 100644 --- a/apps/backend/src/routes/admin/platform-config.ts +++ b/apps/backend/src/routes/admin/platform-config.ts @@ -2,7 +2,6 @@ import { Router } from "express"; import { requireAdminRoles } from "./session.js"; import cloudtentaclesRouter from "./platform-config/cloudtentacles.js"; -import kuaishouCloudFulfillmentRouter from "./platform-config/kuaishou-cloud-fulfillment.js"; import kuaishouEticketRouter from "./platform-config/kuaishou-eticket.js"; import ninetyoneRouter from "./platform-config/ninetyone.js"; import notificationsRouter from "./platform-config/notifications.js"; @@ -14,6 +13,5 @@ router.use("/platform-config", notificationsRouter); router.use("/platform-config", kuaishouEticketRouter); router.use("/platform-config", ninetyoneRouter); router.use("/platform-config", cloudtentaclesRouter); -router.use("/platform-config", kuaishouCloudFulfillmentRouter); export default router; diff --git a/apps/backend/src/routes/admin/platform-config/kuaishou-cloud-fulfillment.ts b/apps/backend/src/routes/admin/platform-config/kuaishou-cloud-fulfillment.ts deleted file mode 100644 index 1264db83..00000000 --- a/apps/backend/src/routes/admin/platform-config/kuaishou-cloud-fulfillment.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Router } from "express"; - -import { - getAdminKuaishouCloudFulfillmentConfig, - updateAdminKuaishouCloudFulfillmentConfig, -} from "../../../services/admin/platform-config/kuaishou-cloud-fulfillment-service.js"; -import type { AdminKuaishouCloudFulfillmentConfigRouteBody } from "../../../types/admin/route-inputs.js"; -import { createJsonHandler } from "../session.js"; -import type { JsonRecord } from "../../../types/json.js"; - -const router = Router(); - -router.get( - "/kuaishou-cloud-fulfillment", - createJsonHandler(() => getAdminKuaishouCloudFulfillmentConfig(), { - successMessage: "ok", - errorMessage: "读取新履约配置失败", - scope: "[admin/platform-config/kuaishou-cloud-fulfillment]", - }) -); - -router.post( - "/kuaishou-cloud-fulfillment", - createJsonHandler( - (req) => - updateAdminKuaishouCloudFulfillmentConfig( - req.body as AdminKuaishouCloudFulfillmentConfigRouteBody - ), - { - successMessage: "新履约配置已保存", - errorMessage: "保存新履约配置失败", - scope: "[admin/platform-config/kuaishou-cloud-fulfillment]", - audit: (_req, data) => { - const result = data as JsonRecord; - return { - action: "platform_kuaishou_cloud_fulfillment_updated", - targetType: "platform_config", - targetId: "kuaishou_cloud_fulfillment", - data: { - itemCount: Array.isArray(result.source?.items) - ? result.source.items.length - : 0, - filePath: String(result.filePath || "").trim(), - }, - }; - }, - } - ) -); - -export default router; 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 1d2780cd..eefbcc05 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 @@ -3,7 +3,6 @@ import assert from 'node:assert/strict' import { mapAdminCloudtentaclesSession, - mapAdminKuaishouCloudFulfillmentSource, mapAdminKuaishouEticketSourceConfig, maskPhone, maskSecret, @@ -69,54 +68,3 @@ test('mapAdminCloudtentaclesSession derives masked fields and token presence', ( }, ) }) - -test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', () => { - assert.deepEqual( - mapAdminKuaishouCloudFulfillmentSource({ - enabled: true, - items: [ - { - id: ' item-1 ', - provider: '', - platform: '', - cloudSourceKeys: [' account_a ', 'account_b'], - cloudSkuId: '0', - autoConsumeAfterDispatch: true, - kuaishouConsumeShopId: ' ks1 ', - notes: ' note ', - }, - ], - }), - { - enabled: true, - items: [ - { - id: 'item-1', - enabled: true, - priority: 100, - provider: '91kaquan', - platform: 'kuaishou', - shopId: '', - internalSkuCode: '', - internalSkuName: '', - externalSkuCode: '', - externalItemId: '', - externalSkuName: '', - resolvedSkuName: '', - cloudSourceKeys: ['account_a', 'account_b'], - cloudSkuId: 0, - cloudSkuName: '', - deliveryItems: [], - vnKey: '', - autoBuyEnabled: true, - minAssetReserve: 0, - autoReturnNumberAfterDispatch: false, - autoConsumeAfterDispatch: true, - kuaishouConsumeShopId: 'ks1', - kuaishouConsumeShopName: '', - notes: 'note', - }, - ], - }, - ) -}) 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 8ccd6f45..e52fc50d 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts @@ -60,85 +60,3 @@ export function mapAdminCloudtentaclesSession(session: JsonObject = {}) { hasToken: Boolean(String(session.token || "").trim()), }; } - -export function mapAdminKuaishouCloudFulfillmentItem(item: JsonObject = {}) { - const deliveryItems = normalizeDeliveryItems(item); - return { - id: String(item.id || "").trim(), - enabled: item.enabled !== false, - priority: Number(item.priority || 100) || 100, - provider: String(item.provider || "91kaquan").trim() || "91kaquan", - platform: String(item.platform || "kuaishou").trim() || "kuaishou", - shopId: String(item.shopId || "").trim(), - internalSkuCode: String(item.internalSkuCode || "").trim(), - internalSkuName: String(item.internalSkuName || "").trim(), - externalSkuCode: String(item.externalSkuCode || "").trim(), - externalItemId: String(item.externalItemId || "").trim(), - externalSkuName: String(item.externalSkuName || "").trim(), - resolvedSkuName: String(item.resolvedSkuName || "").trim(), - cloudSourceKeys: Array.isArray(item.cloudSourceKeys) - ? item.cloudSourceKeys - .map((value) => String(value || "").trim()) - .filter(Boolean) - : [], - cloudSkuId: Number(item.cloudSkuId || 0) || 0, - cloudSkuName: String(item.cloudSkuName || "").trim(), - deliveryItems, - vnKey: String(item.vnKey || "").trim(), - autoBuyEnabled: item.autoBuyEnabled !== false, - minAssetReserve: Number(item.minAssetReserve || 0) || 0, - autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true, - autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true, - kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || "").trim(), - kuaishouConsumeShopName: String(item.kuaishouConsumeShopName || "").trim(), - notes: String(item.notes || "").trim(), - }; -} - -export function mapAdminKuaishouCloudFulfillmentSource(config: JsonObject = {}) { - return { - enabled: config.enabled !== false, - items: (Array.isArray(config.items) ? config.items : []).map((item) => - mapAdminKuaishouCloudFulfillmentItem(item) - ), - }; -} - -function normalizeDeliveryItems(item: JsonObject = {}) { - const rawItems = Array.isArray(item.deliveryItems) ? item.deliveryItems : []; - const normalizedItems = rawItems - .map((value) => normalizeDeliveryItem(value)) - .filter(Boolean); - - if (normalizedItems.length > 0) { - return normalizedItems; - } - - const cloudSkuId = Number(item.cloudSkuId || 0) || 0; - if (cloudSkuId <= 0) { - return []; - } - - return [ - { - cloudSkuId, - cloudSkuName: String(item.cloudSkuName || "").trim(), - quantity: 1, - }, - ]; -} - -function normalizeDeliveryItem(value: unknown) { - const source = value && typeof value === "object" ? value as JsonObject : {}; - const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0; - if (cloudSkuId <= 0) { - return null; - } - - const quantity = Number(source.quantity || 1) || 1; - return { - cloudSkuId, - cloudSkuName: String(source.cloudSkuName || source.skuName || "").trim(), - quantity: Math.max(1, Math.round(quantity)), - }; -} diff --git a/apps/backend/src/services/admin/platform-config/kuaishou-cloud-fulfillment-service.ts b/apps/backend/src/services/admin/platform-config/kuaishou-cloud-fulfillment-service.ts deleted file mode 100644 index d12a5578..00000000 --- a/apps/backend/src/services/admin/platform-config/kuaishou-cloud-fulfillment-service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - getKuaishouCloudFulfillmentConfig, - getKuaishouCloudFulfillmentFilePath, - saveKuaishouCloudFulfillmentConfig, -} from '../../order/kuaishou-cloud-fulfillment-config-service.js' -import { mapAdminKuaishouCloudFulfillmentSource } from './cloudtentacles/mappers.js' - -type JsonObject = Record - -export function getAdminKuaishouCloudFulfillmentConfig() { - const config = getKuaishouCloudFulfillmentConfig() - - return { - filePath: getKuaishouCloudFulfillmentFilePath(), - source: mapAdminKuaishouCloudFulfillmentSource(config), - } -} - -/** @param {AdminKuaishouCloudFulfillmentConfigInput} [payload] */ -export async function updateAdminKuaishouCloudFulfillmentConfig( - payload: JsonObject = {}, -) { - const saved = saveKuaishouCloudFulfillmentConfig({ - enabled: payload.enabled !== false, - items: Array.isArray(payload.items) ? payload.items : [], - }) - - return { - filePath: getKuaishouCloudFulfillmentFilePath(), - source: mapAdminKuaishouCloudFulfillmentSource(saved), - } -} 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 c4e1e40c..9dd3d4ce 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts @@ -7,7 +7,6 @@ import { formatFenToAmount, normalizeFen } from '../../utils/money.js' import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js' import { nowIso } from '../../utils/time.js' import { buildClaimUrl } from './claim-service.js' -import { getKuaishouCloudFulfillmentConfig } from '../order/kuaishou-cloud-fulfillment-config-service.js' import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js' export const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed']) @@ -144,13 +143,9 @@ function resolveClaimOrderItemDisplaySkuName( const fulfillment = isPlainObject(kuaishouCloudFulfillment) ? kuaishouCloudFulfillment : {} const binding = isPlainObject(fulfillment.binding) ? fulfillment.binding : {} const ticket = isPlainObject(fulfillment.ticket) ? fulfillment.ticket : {} - const matchedConfigItem = resolveClaimFulfillmentConfigItem(orderItem, fulfillment, binding) const skuCode = String(orderItem.sku_code || '').trim() const rawSkuName = String(orderItem.sku_name || '').trim() const candidates = [ - matchedConfigItem?.internalSkuName, - matchedConfigItem?.cloudSkuName, - matchedConfigItem?.resolvedSkuName, fulfillment.internalSkuName, binding.skuName, ticket.goodsTitle, @@ -168,40 +163,6 @@ function resolveClaimOrderItemDisplaySkuName( return rawSkuName || skuCode } -function resolveClaimFulfillmentConfigItem( - orderItem: OrderItemRow, - fulfillment: JsonObject, - binding: JsonObject, -) { - const config = getKuaishouCloudFulfillmentConfig() - const rawItems: unknown[] = Array.isArray(config.items) ? config.items : [] - const items = rawItems.filter(isPlainObject) - const configId = String(fulfillment.configId || '').trim() - const skuCode = String(orderItem.sku_code || '').trim() - const skuName = String(orderItem.sku_name || '').trim() - const cloudSkuId = Number(binding.skuId || 0) || 0 - - return items.find((item) => { - if (configId && String(item.id || '').trim() === configId) { - return true - } - - if (cloudSkuId > 0 && Number(item.cloudSkuId || 0) === cloudSkuId) { - return true - } - - return [ - item.internalSkuCode, - item.externalSkuCode, - item.externalItemId, - item.externalSkuName, - ].some((value) => { - const normalized = String(value || '').trim() - return normalized && (normalized === skuCode || normalized === skuName) - }) - }) || null -} - export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow) { if (String(task?.executor_key || '').trim() !== 'kuaishou_ct_assisted') { return null diff --git a/apps/backend/src/services/order/delivery-task-service.test.ts b/apps/backend/src/services/order/delivery-task-service.test.ts index 42d805b8..1fcccd98 100644 --- a/apps/backend/src/services/order/delivery-task-service.test.ts +++ b/apps/backend/src/services/order/delivery-task-service.test.ts @@ -139,7 +139,6 @@ test('syncDeliveryTasksForOrderWithDeps creates kuaishou cloud task from cloudte const result = await syncDeliveryTasksForOrderWithDeps(paidOrder, dynamicOrderItems, { listTasksByOrderId: async () => [], - resolveFulfillmentBinding: async () => null, getFulfillmentProfileByKey: async () => ({ id: 2, profile_key: 'kuaishou_ct_assisted', diff --git a/apps/backend/src/services/order/delivery-task-service.ts b/apps/backend/src/services/order/delivery-task-service.ts index bcd76edf..fc57f6cf 100644 --- a/apps/backend/src/services/order/delivery-task-service.ts +++ b/apps/backend/src/services/order/delivery-task-service.ts @@ -1,8 +1,5 @@ import { createTask, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js' -import { - getFulfillmentProfileByKey, - resolveFulfillmentBinding, -} from '../../repositories/fulfillment-profile-repo.js' +import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js' import { createTaskClaimToken } from '../claim/claim-service.js' import { notifyTaskAutoManualReview } from '../notification/domain-notifications.js' import { nowIso } from '../../utils/time.js' @@ -39,12 +36,6 @@ type DeliveryTaskDeps = { createTask?: typeof createTask listTasksByOrderId?: typeof listTasksByOrderId updateTask?: typeof updateTask - resolveFulfillmentBinding?: (input: { - skuCode: string - provider?: string - platform?: string - shopId?: string - }) => Promise getFulfillmentProfileByKey?: (profileKey: string) => Promise createTaskClaimToken?: (taskId: number | string) => Promise notifyTaskAutoManualReview?: (payload: { @@ -84,7 +75,6 @@ export async function syncDeliveryTasksForOrderWithDeps( createTask: createDeliveryTask = createTask, listTasksByOrderId: listTasks = listTasksByOrderId, updateTask: updateDeliveryTask = updateTask, - resolveFulfillmentBinding: resolveBinding = resolveFulfillmentBinding, getFulfillmentProfileByKey: getProfileByKey = getFulfillmentProfileByKey, createTaskClaimToken: createClaimToken = createTaskClaimToken, notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview, @@ -118,14 +108,7 @@ export async function syncDeliveryTasksForOrderWithDeps( const tasks: DeliveryTaskRow[] = [] for (const item of orderItems) { - const binding = await resolveBinding({ - skuCode: item.sku_code, - provider: order.provider, - platform: order.platform, - shopId: order.shop_id, - }) - - const profile = binding || await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) + const profile = await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) if (!profile) { continue 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 deleted file mode 100644 index b0cfd8ba..00000000 --- a/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import test from 'node:test' -import assert from 'node:assert/strict' - -import { mapKuaishouCloudFulfillmentItemsToBindings } from './kuaishou-cloud-fulfillment-config-service.js' - -test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => { - assert.deepEqual( - mapKuaishouCloudFulfillmentItemsToBindings({ - items: [ - { - enabled: true, - provider: '91kaquan', - platform: 'kuaishou', - shopId: '91kaquan', - internalSkuCode: 'sku-1', - internalSkuName: '内部商品', - externalSkuCode: '952', - externalItemId: '952', - externalSkuName: '测试2', - cloudSourceKeys: ['account_a', 'account_b'], - cloudSkuId: 28, - }, - ], - }), - [ - { - provider: '91kaquan', - platform: 'kuaishou', - shopId: '91kaquan', - skuCode: 'sku-1', - skuName: '内部商品', - profileKey: 'kuaishou_ct_assisted', - enabled: true, - priority: 100, - deliveryItems: [ - { - cloudSkuId: 28, - cloudSkuName: '', - quantity: 1, - }, - ], - config: { - flowType: 'kuaishou_cloud_fulfillment', - configId: '', - cloudtentacles: { - cloudSourceKeys: ['account_a', 'account_b'], - skuId: 28, - skuName: '', - deliveryItems: [ - { - cloudSkuId: 28, - cloudSkuName: '', - quantity: 1, - }, - ], - vnKey: '1', - autoBuyEnabled: true, - minAssetReserve: 0, - autoReturnNumberAfterDispatch: false, - }, - kuaishouConsume: { - shopId: '', - shopName: '', - autoConsumeAfterDispatch: false, - }, - notes: '', - }, - match: { - externalSkuCode: '952', - externalItemId: '952', - externalSkuName: '测试2', - config: { - resolvedSkuName: '内部商品', - }, - }, - }, - ], - ) -}) - -test('mapKuaishouCloudFulfillmentItemsToBindings maps package delivery items', () => { - const [binding] = mapKuaishouCloudFulfillmentItemsToBindings({ - items: [ - { - enabled: true, - internalSkuCode: 'package-1', - internalSkuName: '套餐 1', - externalSkuCode: 'P001', - cloudSourceKeys: ['account_a'], - deliveryItems: [ - { cloudSkuId: 73, cloudSkuName: '物品 A', quantity: 1 }, - { cloudSkuId: 76, cloudSkuName: '物品 B', quantity: 50 }, - ], - }, - ], - }) - - assert.deepEqual(binding?.deliveryItems, [ - { cloudSkuId: 73, cloudSkuName: '物品 A', quantity: 1 }, - { cloudSkuId: 76, cloudSkuName: '物品 B', quantity: 50 }, - ]) - assert.deepEqual(binding?.config.cloudtentacles.deliveryItems, [ - { cloudSkuId: 73, cloudSkuName: '物品 A', quantity: 1 }, - { cloudSkuId: 76, cloudSkuName: '物品 B', quantity: 50 }, - ]) - assert.equal(binding?.config.cloudtentacles.skuId, 73) - assert.equal(binding?.config.cloudtentacles.skuName, '物品 A') -}) 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 deleted file mode 100644 index b9060bc9..00000000 --- a/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.ts +++ /dev/null @@ -1,279 +0,0 @@ -import path from "node:path"; - -import { PROJECT_ROOT } from "../../config/runtime.js"; -import { readJsonFile, writeJsonFile } from "../../utils/json-file-store.js"; -import { resolveKuaishouEticketShopConfig } from "../platforms/kuaishou-eticket/source-config-service.js"; - -const KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH = path.join( - PROJECT_ROOT, - "data", - "kuaishou-cloud-fulfillment.json" -); - -type JsonObject = Record; - -type DeliveryItem = { - cloudSkuId: number; - cloudSkuName: string; - quantity: number; -}; - -export function getKuaishouCloudFulfillmentFilePath() { - return KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH; -} - -export function getKuaishouCloudFulfillmentConfig() { - return loadKuaishouCloudFulfillmentConfigFromFile(); -} - -export function saveKuaishouCloudFulfillmentConfig(rawValue: unknown) { - return writeJsonFile( - KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH, - rawValue, - normalizeKuaishouCloudFulfillmentConfig - ); -} - -export function mapKuaishouCloudFulfillmentItemsToBindings(config: JsonObject = {}) { - const items = Array.isArray(config.items) ? config.items : []; - - return items - .filter((item) => item && item.enabled !== false) - .map((item) => ({ - provider: String(item.provider || "91kaquan").trim() || "91kaquan", - platform: String(item.platform || "kuaishou").trim() || "kuaishou", - shopId: String(item.shopId || "").trim(), - skuCode: String(item.internalSkuCode || "").trim(), - skuName: String(item.internalSkuName || "").trim(), - profileKey: "kuaishou_ct_assisted", - enabled: item.enabled !== false, - priority: normalizePriority(item.priority), - deliveryItems: normalizeDeliveryItems(item), - config: { - flowType: "kuaishou_cloud_fulfillment", - configId: String(item.id || "").trim(), - cloudtentacles: { - cloudSourceKeys: normalizeStringArray(item.cloudSourceKeys), - skuId: - normalizeDeliveryItems(item)[0]?.cloudSkuId || - normalizePositiveInteger(item.cloudSkuId), - skuName: - normalizeDeliveryItems(item)[0]?.cloudSkuName || - String(item.cloudSkuName || "").trim(), - deliveryItems: normalizeDeliveryItems(item), - vnKey: "1", - autoBuyEnabled: item.autoBuyEnabled !== false, - minAssetReserve: normalizeNonNegativeInteger(item.minAssetReserve, 0), - autoReturnNumberAfterDispatch: - item.autoReturnNumberAfterDispatch === true, - }, - kuaishouConsume: { - shopId: String(item.kuaishouConsumeShopId || "").trim(), - shopName: String(item.kuaishouConsumeShopName || "").trim(), - autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true, - }, - notes: String(item.notes || "").trim(), - }, - match: { - externalSkuCode: String(item.externalSkuCode || "").trim(), - externalItemId: String(item.externalItemId || "").trim(), - externalSkuName: String(item.externalSkuName || "").trim(), - config: { - resolvedSkuName: String( - item.resolvedSkuName || item.internalSkuName || "" - ).trim(), - }, - }, - })) - .filter( - (item) => - item.skuCode && - (item.match.externalSkuCode || - item.match.externalItemId || - item.match.externalSkuName) - ); -} - -function loadKuaishouCloudFulfillmentConfigFromFile() { - return readJsonFile( - KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH, - () => normalizeKuaishouCloudFulfillmentConfig({}), - normalizeKuaishouCloudFulfillmentConfig - ); -} - -function normalizeKuaishouCloudFulfillmentConfig(rawValue: unknown) { - const source = isPlainObject(rawValue) ? rawValue : {}; - return { - enabled: source.enabled !== false, - items: Array.isArray(source.items) - ? source.items - .map((item: unknown) => normalizeKuaishouCloudFulfillmentItem(item)) - .filter(Boolean) - : [], - }; -} - -function normalizeKuaishouCloudFulfillmentItem(rawValue: unknown) { - if (!isPlainObject(rawValue)) { - return null; - } - - const internalSkuCode = String(rawValue.internalSkuCode || "").trim(); - const cloudSkuId = normalizePositiveInteger(rawValue.cloudSkuId); - const deliveryItems = normalizeDeliveryItems(rawValue); - const cloudSourceKeys = normalizeStringArray(rawValue.cloudSourceKeys); - const externalSkuCode = String(rawValue.externalSkuCode || "").trim(); - const externalItemId = String(rawValue.externalItemId || "").trim(); - const externalSkuName = String(rawValue.externalSkuName || "").trim(); - const kuaishouConsumeShopId = String( - rawValue.kuaishouConsumeShopId || "" - ).trim(); - const kuaishouConsumeShopName = String( - rawValue.kuaishouConsumeShopName || "" - ).trim(); - const kuaishouShopConfig = resolveKuaishouEticketShopConfig({ - shopId: kuaishouConsumeShopId, - shopName: kuaishouConsumeShopName, - }); - - if (!internalSkuCode) { - return null; - } - - if (deliveryItems.length === 0) { - return null; - } - - if (cloudSourceKeys.length === 0) { - return null; - } - - if (!externalSkuCode && !externalItemId && !externalSkuName) { - return null; - } - - return { - id: String(rawValue.id || internalSkuCode).trim() || internalSkuCode, - enabled: rawValue.enabled !== false, - priority: normalizePriority(rawValue.priority), - provider: String(rawValue.provider || "91kaquan").trim() || "91kaquan", - platform: String(rawValue.platform || "kuaishou").trim() || "kuaishou", - shopId: String(rawValue.shopId || "").trim(), - internalSkuCode, - internalSkuName: - String(rawValue.internalSkuName || "").trim() || internalSkuCode, - externalSkuCode, - externalItemId, - externalSkuName, - resolvedSkuName: String( - rawValue.resolvedSkuName || rawValue.internalSkuName || "" - ).trim(), - cloudSourceKeys, - cloudSkuId: deliveryItems[0]?.cloudSkuId || cloudSkuId, - cloudSkuName: - deliveryItems[0]?.cloudSkuName || - String(rawValue.cloudSkuName || "").trim(), - deliveryItems, - vnKey: "1", - autoBuyEnabled: rawValue.autoBuyEnabled !== false, - minAssetReserve: normalizeNonNegativeInteger(rawValue.minAssetReserve, 0), - autoReturnNumberAfterDispatch: - rawValue.autoReturnNumberAfterDispatch === true, - autoConsumeAfterDispatch: rawValue.autoConsumeAfterDispatch === true, - kuaishouConsumeShopId: String( - kuaishouShopConfig?.shopId || kuaishouConsumeShopId - ).trim(), - kuaishouConsumeShopName: String( - kuaishouShopConfig?.kshopName || kuaishouConsumeShopName - ).trim(), - notes: String(rawValue.notes || "").trim(), - }; -} - -function normalizePositiveInteger(value: unknown) { - const parsed = Number(value); - return Number.isInteger(parsed) && parsed > 0 ? parsed : 0; -} - -function normalizeNonNegativeInteger(value: unknown, fallback: number) { - const parsed = Number(value); - return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback; -} - -function normalizeDeliveryItems(value: JsonObject): DeliveryItem[] { - const rawItems = Array.isArray(value.deliveryItems) ? value.deliveryItems : []; - const items = rawItems - .map((item: unknown) => normalizeDeliveryItem(item)) - .filter(Boolean) as DeliveryItem[]; - - if (items.length > 0) { - return mergeDeliveryItems(items); - } - - const cloudSkuId = normalizePositiveInteger(value.cloudSkuId); - if (!cloudSkuId) { - return []; - } - - return [ - { - cloudSkuId, - cloudSkuName: String(value.cloudSkuName || "").trim(), - quantity: 1, - }, - ]; -} - -function normalizeDeliveryItem(value: unknown): DeliveryItem | null { - if (!isPlainObject(value)) { - return null; - } - - const cloudSkuId = normalizePositiveInteger(value.cloudSkuId || value.skuId); - if (!cloudSkuId) { - return null; - } - - return { - cloudSkuId, - cloudSkuName: String(value.cloudSkuName || value.skuName || "").trim(), - quantity: normalizePositiveInteger(value.quantity) || 1, - }; -} - -function mergeDeliveryItems(items: DeliveryItem[]): DeliveryItem[] { - const merged = new Map(); - - for (const item of items) { - const existing = merged.get(item.cloudSkuId); - if (existing) { - existing.quantity += item.quantity; - existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName; - continue; - } - - merged.set(item.cloudSkuId, { ...item }); - } - - return Array.from(merged.values()); -} - -function normalizePriority(value: unknown) { - const parsed = Number(value); - if (!Number.isFinite(parsed)) { - return 100; - } - - return Math.max(1, Math.round(parsed)); -} -function isPlainObject(value: unknown): value is JsonObject { - return Object.prototype.toString.call(value) === "[object Object]"; -} - -function normalizeStringArray(value: unknown) { - if (Array.isArray(value)) { - return value.map((v: unknown) => String(v || "").trim()).filter(Boolean); - } - return []; -} diff --git a/apps/backend/src/services/order/order-service.ts b/apps/backend/src/services/order/order-service.ts index ede473d8..e82c6134 100644 --- a/apps/backend/src/services/order/order-service.ts +++ b/apps/backend/src/services/order/order-service.ts @@ -1,7 +1,6 @@ import { createOrder, findOrderByPlatformOrderId, - findOrderByPlatformOrderIdCandidates, updateOrder, } from '../../repositories/order-repo.js' import { replaceOrderItems } from '../../repositories/order-item-repo.js' @@ -32,7 +31,6 @@ type SourceOrderEvent = { provider: string platform: string shopId: string - shopIdAliases?: string[] shopName: string platformOrderId: string orderStatus: string @@ -89,18 +87,12 @@ export async function upsertOrderFromSource( { sourceLabel = 'source' }: UpsertOrderSourceOptions = {}, ): Promise { const now = nowIso() - const exactExisting = await findOrderByPlatformOrderId({ + const existing = await findOrderByPlatformOrderId({ provider: event.provider, platform: event.platform, shopId: event.shopId, platformOrderId: event.platformOrderId, }) - const existing = exactExisting || await findOrderByPlatformOrderIdCandidates({ - provider: event.provider, - platform: event.platform, - shopIds: resolveEventShopIdCandidates(event), - platformOrderId: event.platformOrderId, - }) logIntegration('[order-service]', `开始处理 ${sourceLabel} 订单 upsert`, { provider: event.provider, @@ -115,9 +107,7 @@ export async function upsertOrderFromSource( event.items.map((item) => resolveOrderItemForFulfillment({ provider: event.provider, platform: event.platform, - shopId: event.shopId, item, - ...(event.shopIdAliases !== undefined ? { shopIdAliases: event.shopIdAliases } : {}), })), ) const configuredItems = (resolvedItems as FulfillmentOrderItem[]).filter((item) => item.isConfigured) @@ -214,13 +204,6 @@ export async function upsertOrderFromSource( } } -function resolveEventShopIdCandidates(event: Pick): string[] { - return [...new Set([ - String(event?.shopId || '').trim(), - ...(Array.isArray(event?.shopIdAliases) ? event.shopIdAliases : []).map((item) => String(item || '').trim()), - ].filter(Boolean))] -} - const ORDER_STATUS_PRIORITY = { created: 0, paid: 1, diff --git a/apps/backend/src/services/order/product-match-service.test.ts b/apps/backend/src/services/order/product-match-service.test.ts deleted file mode 100644 index a98c0b7d..00000000 --- a/apps/backend/src/services/order/product-match-service.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import test from 'node:test' -import assert from 'node:assert/strict' - -import { resolveShopIdCandidates } from './product-match-service.js' - -test('resolveShopIdCandidates merges primary and alias shop ids', () => { - assert.deepEqual( - resolveShopIdCandidates({ - shopId: '10', - shopIdAliases: ['4269276762', '10', ''], - }), - ['10', '4269276762'], - ) -}) - -test('resolveShopIdCandidates preserves wildcard fallback when ids are empty', () => { - assert.deepEqual(resolveShopIdCandidates({}), ['']) -}) diff --git a/apps/backend/src/services/order/product-match-service.ts b/apps/backend/src/services/order/product-match-service.ts index 7a453db9..801e23a6 100644 --- a/apps/backend/src/services/order/product-match-service.ts +++ b/apps/backend/src/services/order/product-match-service.ts @@ -1,6 +1,5 @@ -import { resolveFulfillmentBinding } from '../../repositories/fulfillment-profile-repo.js' -import { resolveProductMatchRule } from '../../repositories/product-match-rule-repo.js' import { + normalizeCloudtentaclesMatchName, resolveCloudtentaclesSkuByProductName, type CloudtentaclesNameMatchResult, } from './cloudtentacles-name-match-service.js' @@ -21,8 +20,6 @@ export type FulfillmentItem = { type ResolveOrderItemForFulfillmentInput = { provider?: string platform?: string - shopId?: string - shopIdAliases?: string[] item?: FulfillmentItem } @@ -35,9 +32,7 @@ type FulfillmentItemCandidate = { externalSkuCode: string externalSkuName: string externalSkuNameNormalized: string - matchedRule: Awaited> resolvedSkuCode: string - binding: Awaited> cloudtentaclesNameMatch: CloudtentaclesNameMatchResult | null isConfigured: boolean } @@ -53,23 +48,14 @@ export type ResolvedFulfillmentItem = FulfillmentItem & { isConfigured: boolean } -type ShopIdCandidatesInput = { - shopId?: string - shopIdAliases?: string[] -} - export async function resolveOrderItemForFulfillment({ provider = '', platform = '', - shopId = '', - shopIdAliases = [], item = {}, }: ResolveOrderItemForFulfillmentInput): Promise { const candidate = await resolveConfiguredItemCandidate({ provider, platform, - shopId, - shopIdAliases, item, }) const { @@ -77,21 +63,16 @@ export async function resolveOrderItemForFulfillment({ externalSkuCode, externalSkuName, externalSkuNameNormalized, - matchedRule, - binding, cloudtentaclesNameMatch, } = candidate const resolvedSkuCode = pickFirstNonEmpty([ cloudtentaclesNameMatch?.cloudSkuName, - matchedRule?.resolved_sku_code, externalSkuCode, externalItemId, ]) const resolvedSkuName = pickFirstNonEmpty([ cloudtentaclesNameMatch?.cloudSkuName, - readConfigValue(matchedRule?.config_json, 'resolvedSkuName'), - readConfigValue(matchedRule?.config_json, 'internalProductName'), item.skuName, externalSkuName, resolvedSkuCode, @@ -103,11 +84,7 @@ export async function resolveOrderItemForFulfillment({ externalSkuName, externalSkuNameNormalized, resolvedSkuCode, - matchedProductRuleId: matchedRule ? Number(matchedRule.id) : null, - matchedProductRuleBy: String(matchedRule?.matched_by || '').trim(), - matchedFulfillmentBindingId: binding ? Number(binding.id) : null, - matchedFulfillmentProfileKey: String(binding?.profile_key || '').trim(), - matchMode: cloudtentaclesNameMatch?.matchMode || String(matchedRule?.matched_by || '').trim(), + matchMode: cloudtentaclesNameMatch?.matchMode || '', cloudtentacles: cloudtentaclesNameMatch ? { matchMode: cloudtentaclesNameMatch.matchMode, @@ -141,16 +118,12 @@ export async function resolveOrderItemForFulfillment({ export async function hasConfiguredOrderItems({ provider = '', platform = '', - shopId = '', - shopIdAliases = [], items = [], }: HasConfiguredOrderItemsInput): Promise { const candidates = await Promise.all( (Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({ provider, platform, - shopId, - shopIdAliases, item, })), ) @@ -159,13 +132,7 @@ export async function hasConfiguredOrderItems({ } export function normalizeProductName(value: unknown): string { - return String(value || '') - .toLowerCase() - .replace(/[【】\[\]()()]/g, ' ') - .replace(/(自动发货|秒发|极速发货|官方直充|官方充值)/gi, ' ') - .replace(/[^\p{L}\p{N}]+/gu, ' ') - .replace(/\s+/g, ' ') - .trim() + return normalizeCloudtentaclesMatchName(value) } function pickFirstNonEmpty(values: unknown[]): string { @@ -182,8 +149,6 @@ function pickFirstNonEmpty(values: unknown[]): string { async function resolveConfiguredItemCandidate({ provider = '', platform = '', - shopId = '', - shopIdAliases = [], item = {}, }: ResolveOrderItemForFulfillmentInput): Promise { const externalItemId = pickFirstNonEmpty([ @@ -201,9 +166,6 @@ async function resolveConfiguredItemCandidate({ externalSkuCode, ]) const externalSkuNameNormalized = normalizeProductName(externalSkuName) - const shopIdCandidates = resolveShopIdCandidates({ shopId, shopIdAliases }) - let matchedRule = null - let binding = null if (isOpen91KuaishouOrder(provider, platform)) { const cloudtentaclesNameMatch = await resolveCloudtentaclesSkuByProductName(externalSkuName) @@ -218,48 +180,13 @@ async function resolveConfiguredItemCandidate({ externalSkuCode, externalSkuName, externalSkuNameNormalized, - matchedRule: null, resolvedSkuCode, - binding: null, cloudtentaclesNameMatch, isConfigured: Boolean(cloudtentaclesNameMatch), } } - for (const candidateShopId of shopIdCandidates) { - const candidateRule = await resolveProductMatchRule({ - provider, - platform, - shopId: candidateShopId, - externalItemId, - externalSkuCode, - externalSkuNameNormalized, - }) - if (candidateRule) { - matchedRule = candidateRule - } - - const resolvedSkuCode = pickFirstNonEmpty([ - candidateRule?.resolved_sku_code, - externalSkuCode, - externalItemId, - ]) - binding = resolvedSkuCode - ? await resolveFulfillmentBinding({ - skuCode: resolvedSkuCode, - provider, - platform, - shopId: candidateShopId, - }) - : null - - if (binding) { - break - } - } - const resolvedSkuCode = pickFirstNonEmpty([ - matchedRule?.resolved_sku_code, externalSkuCode, externalItemId, ]) @@ -269,52 +196,17 @@ async function resolveConfiguredItemCandidate({ externalSkuCode, externalSkuName, externalSkuNameNormalized, - matchedRule, resolvedSkuCode, - binding, cloudtentaclesNameMatch: null, - isConfigured: Boolean(binding), + isConfigured: false, } } -export function resolveShopIdCandidates({ - shopId = '', - shopIdAliases = [], -}: ShopIdCandidatesInput = {}): string[] { - const candidates = [...new Set([ - String(shopId || '').trim(), - ...(Array.isArray(shopIdAliases) ? shopIdAliases : []).map((item) => String(item || '').trim()), - ].filter(Boolean))] - - return candidates.length > 0 ? candidates : [''] -} - -function readConfigValue(rawValue: unknown, key: string): string { - if (!rawValue) { - return '' - } - - const parsed = typeof rawValue === 'string' ? safeParseJson(rawValue) : rawValue - if (!isPlainObject(parsed)) { - return '' - } - - return String(parsed[key] || '').trim() -} - -function safeParseJson(rawValue: unknown): unknown { - try { - return JSON.parse(String(rawValue || '{}')) - } catch { - return null - } -} - -function isPlainObject(value: unknown): value is Record { - return Object.prototype.toString.call(value) === '[object Object]' -} - function isOpen91KuaishouOrder(provider: unknown, platform: unknown) { return String(provider || '').trim() === '91kaquan' && String(platform || '').trim() === 'kuaishou' } + +function isPlainObject(value: unknown): value is Record { + return Object.prototype.toString.call(value) === '[object Object]' +} diff --git a/apps/backend/src/types/admin/route-inputs.ts b/apps/backend/src/types/admin/route-inputs.ts index 22658f40..6d03f73e 100644 --- a/apps/backend/src/types/admin/route-inputs.ts +++ b/apps/backend/src/types/admin/route-inputs.ts @@ -14,7 +14,6 @@ import type { AdminCloudtentaclesTestLoginInput, AdminCloudtentaclesValidateSessionInput, AdminCloudtentaclesVirtualNumberInput, - AdminKuaishouCloudFulfillmentConfigInput, AdminKuaishouEticketConsumeInput, AdminKuaishouEticketDetailQueryInput, AdminKuaishouEticketShopInfoInput, @@ -38,7 +37,6 @@ export type AdminKuaishouEticketDetailQueryRouteBody = AdminKuaishouEticketDetai export type AdminKuaishouEticketConsumeRouteBody = AdminKuaishouEticketConsumeInput export type AdminKuaishouEticketShopInfoRouteBody = AdminKuaishouEticketShopInfoInput export type AdminCloudtentaclesSourceConfigRouteBody = AdminCloudtentaclesSourceConfigInput -export type AdminKuaishouCloudFulfillmentConfigRouteBody = AdminKuaishouCloudFulfillmentConfigInput export type AdminCloudtentaclesSendSmsCodeRouteBody = AdminCloudtentaclesSendSmsCodeInput export type AdminCloudtentaclesTestLoginRouteBody = AdminCloudtentaclesTestLoginInput export type AdminCloudtentaclesValidateSessionRouteBody = AdminCloudtentaclesValidateSessionInput diff --git a/apps/backend/src/types/admin/write-inputs.ts b/apps/backend/src/types/admin/write-inputs.ts index ca1905a5..5cdf143b 100644 --- a/apps/backend/src/types/admin/write-inputs.ts +++ b/apps/backend/src/types/admin/write-inputs.ts @@ -1,34 +1,3 @@ -export type AdminKuaishouCloudFulfillmentItemInput = { - id?: string - enabled?: boolean - priority?: number | string - provider?: string - platform?: string - shopId?: string - internalSkuCode?: string - internalSkuName?: string - externalSkuCode?: string - externalItemId?: string - externalSkuName?: string - resolvedSkuName?: string - cloudSourceKeys?: string[] - cloudSkuId?: number | string - cloudSkuName?: string - vnKey?: string - autoBuyEnabled?: boolean - minAssetReserve?: number | string - autoReturnNumberAfterDispatch?: boolean - autoConsumeAfterDispatch?: boolean - kuaishouConsumeShopId?: string - kuaishouConsumeShopName?: string - notes?: string -} - -export type AdminKuaishouCloudFulfillmentConfigInput = { - enabled?: boolean - items?: AdminKuaishouCloudFulfillmentItemInput[] -} - export type AdminKuaishouEticketShopConfigWriteItemInput = { shopId?: string kshopName?: string diff --git a/apps/frontend/src/router/index.ts b/apps/frontend/src/router/index.ts index bb7006a7..09f9c0f9 100644 --- a/apps/frontend/src/router/index.ts +++ b/apps/frontend/src/router/index.ts @@ -68,11 +68,6 @@ const router = createRouter({ meta: { allowedRoles: ['admin'] }, component: () => import('@/views/admin/fulfillment/AdminFulfillmentConfigHubView.vue'), }, - { - path: 'platform-kuaishou-cloud-fulfillment', - meta: { allowedRoles: ['admin'] }, - redirect: '/admin/platform-fulfillment?tab=kuaishou-cloud', - }, { path: 'audit-logs', meta: { allowedRoles: ['admin'] }, diff --git a/apps/frontend/src/services/admin/platform-config/index.ts b/apps/frontend/src/services/admin/platform-config/index.ts index 3a29bf1b..73bc8a95 100644 --- a/apps/frontend/src/services/admin/platform-config/index.ts +++ b/apps/frontend/src/services/admin/platform-config/index.ts @@ -3,4 +3,3 @@ export * from './scheduled-jobs' export * from './ninetyone' export * from './kuaishou-eticket' export * from './cloudtentacles' -export * from './kuaishou-cloud-fulfillment' diff --git a/apps/frontend/src/services/admin/platform-config/kuaishou-cloud-fulfillment.ts b/apps/frontend/src/services/admin/platform-config/kuaishou-cloud-fulfillment.ts deleted file mode 100644 index 0680176c..00000000 --- a/apps/frontend/src/services/admin/platform-config/kuaishou-cloud-fulfillment.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { apiGet, apiPost } from '@/lib/http' -import type { AdminKuaishouCloudFulfillmentConfig } from '@/types/admin' - -export function fetchAdminKuaishouCloudFulfillmentConfig() { - return apiGet<{ - filePath: string - source: AdminKuaishouCloudFulfillmentConfig - }>('/api/v1/admin/platform-config/kuaishou-cloud-fulfillment') -} - -export function saveAdminKuaishouCloudFulfillmentConfig( - payload: AdminKuaishouCloudFulfillmentConfig, -) { - return apiPost<{ - filePath: string - source: AdminKuaishouCloudFulfillmentConfig - }>('/api/v1/admin/platform-config/kuaishou-cloud-fulfillment', payload) -} diff --git a/apps/frontend/src/types/admin/index.ts b/apps/frontend/src/types/admin/index.ts index e95fe021..a9628de3 100644 --- a/apps/frontend/src/types/admin/index.ts +++ b/apps/frontend/src/types/admin/index.ts @@ -66,7 +66,4 @@ export type { AdminCloudtentaclesSkuListResult, AdminCloudtentaclesDeliveryRecordItem, AdminCloudtentaclesDeliveryRecordListResult, - AdminKuaishouCloudDeliveryItem, - AdminKuaishouCloudFulfillmentItem, - AdminKuaishouCloudFulfillmentConfig, } from './platform-config' diff --git a/apps/frontend/src/types/admin/platform-config/index.ts b/apps/frontend/src/types/admin/platform-config/index.ts index 9581062d..a09f09bb 100644 --- a/apps/frontend/src/types/admin/platform-config/index.ts +++ b/apps/frontend/src/types/admin/platform-config/index.ts @@ -47,9 +47,3 @@ export type { AdminCloudtentaclesDeliveryRecordItem, AdminCloudtentaclesDeliveryRecordListResult, } from './cloudtentacles' - -export type { - AdminKuaishouCloudDeliveryItem, - AdminKuaishouCloudFulfillmentItem, - AdminKuaishouCloudFulfillmentConfig, -} from './kuaishou-cloud-fulfillment' 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 deleted file mode 100644 index e9318652..00000000 --- a/apps/frontend/src/types/admin/platform-config/kuaishou-cloud-fulfillment.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface AdminKuaishouCloudDeliveryItem { - cloudSkuId: number - cloudSkuName: string - quantity: number -} - -export interface AdminKuaishouCloudFulfillmentItem { - id: string - enabled: boolean - priority: number - provider: string - platform: string - shopId: string - internalSkuCode: string - internalSkuName: string - externalSkuCode: string - externalItemId: string - externalSkuName: string - resolvedSkuName: string - cloudSourceKeys: string[] - cloudSkuId: number - cloudSkuName: string - deliveryItems: AdminKuaishouCloudDeliveryItem[] - vnKey: string - autoBuyEnabled: boolean - minAssetReserve: number - autoReturnNumberAfterDispatch: boolean - autoConsumeAfterDispatch: boolean - kuaishouConsumeShopId: string - kuaishouConsumeShopName: string - notes: string -} - -export interface AdminKuaishouCloudFulfillmentConfig { - enabled: boolean - items: AdminKuaishouCloudFulfillmentItem[] -} diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudNinetyoneSection.vue b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudNinetyoneSection.vue deleted file mode 100644 index caec8593..00000000 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudNinetyoneSection.vue +++ /dev/null @@ -1,165 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudOverviewSection.vue b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudOverviewSection.vue deleted file mode 100644 index ffef2d4d..00000000 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudOverviewSection.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - - - 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 deleted file mode 100644 index dbe124e4..00000000 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudRuleCard.vue +++ /dev/null @@ -1,405 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudRuleSection.vue b/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudRuleSection.vue deleted file mode 100644 index ac73cce8..00000000 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/components/AdminKuaishouCloudRuleSection.vue +++ /dev/null @@ -1,157 +0,0 @@ - - - - - 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 deleted file mode 100644 index 39989986..00000000 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AdminKuaishouCloudFulfillmentItem } from '@/types/admin' - -export type EditableItem = AdminKuaishouCloudFulfillmentItem & { - localId: string - cloudSourceKeys: string[] -} - -export type ValidationState = { - localId: string - message: string -} | null - -export type RuleFilter = 'all' | 'draft' | 'ready' | 'disabled' 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 deleted file mode 100644 index 9968ab3b..00000000 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudConfig.ts +++ /dev/null @@ -1,735 +0,0 @@ -import { computed, nextTick, ref } from 'vue' - -import { showError, showSuccess } from '@/lib/feedback' -import { - fetchAdminKuaishouCloudFulfillmentConfig, - fetchAdminKuaishouEticketSourceConfig, - fetchAdminCloudtentaclesSourceConfig, - saveAdminKuaishouCloudFulfillmentConfig, -} from '@/services/admin' -import type { - AdminKuaishouCloudDeliveryItem, - AdminKuaishouCloudFulfillmentConfig, - AdminKuaishouCloudFulfillmentItem, - AdminKuaishouEticketShopConfigItem, -} from '@/types/admin' -import { hasAdminRole } from '@/utils/admin-auth' - -import type { EditableItem, RuleFilter, ValidationState } from './types' - -export function useKuaishouCloudConfig() { - const loading = ref(true) - const saving = ref(false) - const errorMessage = ref('') - const validationState = ref(null) - const filePath = ref('') - const enabled = ref(true) - const items = ref([]) - const collapsedIds = ref([]) - const ruleFilter = ref('all') - const kuaishouConsumeShops = ref([]) - const cloudtentaclesSourceOptions = ref<{ key: string; label: string }[]>([]) - - // ── computed ────────────────────────────────────────── - - const metrics = computed(() => { - const readyCount = items.value.filter(isItemComplete).length - const enabledCount = items.value.filter((item) => item.enabled).length - const disabledCount = items.value.filter((item) => !item.enabled).length - return { - total: items.value.length, - readyCount, - enabledCount, - disabledCount, - draftCount: items.value.filter((item) => item.enabled && !isItemComplete(item)).length, - } - }) - - const filteredItems = computed(() => - items.value.filter((item) => matchesRuleFilter(item, ruleFilter.value)), - ) - - const ruleFilterOptions = computed>( - () => [ - { value: 'all', label: '全部', count: metrics.value.total }, - { value: 'draft', label: '待完善', count: metrics.value.draftCount }, - { value: 'ready', label: '可投产', count: metrics.value.readyCount }, - { value: 'disabled', label: '已停用', count: metrics.value.disabledCount }, - ], - ) - - // ── item factories ─────────────────────────────────── - - function createEmptyItem(): EditableItem { - const defaultShop = getDefaultKuaishouConsumeShop() - const sourceKeys = getDefaultCloudSourceKeys() - return { - localId: crypto.randomUUID(), - id: '', - enabled: true, - priority: 100, - provider: '91kaquan', - platform: 'kuaishou', - shopId: '', - internalSkuCode: '', - internalSkuName: '', - externalSkuCode: '', - externalItemId: '', - externalSkuName: '', - resolvedSkuName: '', - cloudSourceKeys: sourceKeys, - cloudSkuId: 0, - cloudSkuName: '', - deliveryItems: [], - vnKey: '1', - autoBuyEnabled: true, - minAssetReserve: 0, - autoReturnNumberAfterDispatch: true, - autoConsumeAfterDispatch: true, - kuaishouConsumeShopId: defaultShop?.shopId || '', - kuaishouConsumeShopName: defaultShop?.kshopName || '', - notes: '', - } - } - - function mapEditableItem(item: AdminKuaishouCloudFulfillmentItem): EditableItem { - const matchedShop = findKuaishouConsumeShop( - item.kuaishouConsumeShopId, - item.kuaishouConsumeShopName, - ) - const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys) - const deliveryItems = normalizeDeliveryItems(item) - const primaryDeliveryItem = deliveryItems[0] - return { - localId: crypto.randomUUID(), - id: item.id || crypto.randomUUID(), - enabled: item.enabled !== false, - priority: item.priority || 100, - provider: item.provider || '91kaquan', - platform: item.platform || 'kuaishou', - shopId: item.shopId || '', - internalSkuCode: item.internalSkuCode || '', - internalSkuName: item.internalSkuName || '', - externalSkuCode: item.externalSkuCode || '', - externalItemId: item.externalItemId || '', - externalSkuName: item.externalSkuName || '', - resolvedSkuName: item.resolvedSkuName || '', - cloudSourceKeys: sourceKeys, - cloudSkuId: primaryDeliveryItem?.cloudSkuId || Number(item.cloudSkuId || 0) || 0, - cloudSkuName: primaryDeliveryItem?.cloudSkuName || item.cloudSkuName || '', - deliveryItems, - vnKey: item.vnKey || '', - autoBuyEnabled: item.autoBuyEnabled !== false, - minAssetReserve: Number(item.minAssetReserve || 0) || 0, - autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true, - autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true, - kuaishouConsumeShopId: matchedShop?.shopId || item.kuaishouConsumeShopId || '', - kuaishouConsumeShopName: matchedShop?.kshopName || item.kuaishouConsumeShopName || '', - notes: item.notes || '', - } - } - - function mapSaveItem(item: EditableItem): AdminKuaishouCloudFulfillmentItem { - const matchedShop = findKuaishouConsumeShop( - item.kuaishouConsumeShopId, - item.kuaishouConsumeShopName, - ) - const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys) - const deliveryItems = normalizeDeliveryItems(item) - const primaryDeliveryItem = deliveryItems[0] - return { - id: item.id.trim() || item.internalSkuCode.trim() || item.localId, - enabled: item.enabled, - priority: Number(item.priority || 100) || 100, - provider: item.provider.trim() || '91kaquan', - platform: item.platform.trim() || 'kuaishou', - shopId: item.shopId.trim(), - internalSkuCode: item.internalSkuCode.trim(), - internalSkuName: item.internalSkuName.trim(), - externalSkuCode: item.externalSkuCode.trim(), - externalItemId: item.externalItemId.trim(), - externalSkuName: item.externalSkuName.trim(), - resolvedSkuName: item.resolvedSkuName.trim(), - cloudSourceKeys: sourceKeys, - cloudSkuId: primaryDeliveryItem?.cloudSkuId || Number(item.cloudSkuId || 0) || 0, - cloudSkuName: primaryDeliveryItem?.cloudSkuName || item.cloudSkuName.trim(), - deliveryItems, - vnKey: item.vnKey.trim(), - autoBuyEnabled: item.autoBuyEnabled, - minAssetReserve: Math.max(0, Number(item.minAssetReserve || 0) || 0), - autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch, - autoConsumeAfterDispatch: item.autoConsumeAfterDispatch, - kuaishouConsumeShopId: String(matchedShop?.shopId || item.kuaishouConsumeShopId).trim(), - kuaishouConsumeShopName: String( - matchedShop?.kshopName || item.kuaishouConsumeShopName, - ).trim(), - notes: item.notes.trim(), - } - } - - 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]) - } - - function normalizeDeliveryItems( - item: Pick | AdminKuaishouCloudFulfillmentItem, - ): AdminKuaishouCloudDeliveryItem[] { - const rawItems = Array.isArray(item.deliveryItems) ? item.deliveryItems : [] - const normalizedItems = rawItems - .map((value) => normalizeDeliveryItem(value)) - .filter((value): value is AdminKuaishouCloudDeliveryItem => Boolean(value)) - - if (normalizedItems.length > 0) { - return mergeDeliveryItems(normalizedItems) - } - - const cloudSkuId = Number(item.cloudSkuId || 0) || 0 - if (cloudSkuId <= 0) { - return [] - } - - return [ - { - cloudSkuId, - cloudSkuName: String(item.cloudSkuName || '').trim(), - quantity: 1, - }, - ] - } - - function normalizeDeliveryItem(value: unknown): AdminKuaishouCloudDeliveryItem | null { - const source = value && typeof value === 'object' ? (value as Partial) : {} - const cloudSkuId = Number(source.cloudSkuId || 0) || 0 - if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) { - return null - } - - const quantity = Number(source.quantity || 1) || 1 - return { - cloudSkuId, - cloudSkuName: String(source.cloudSkuName || '').trim(), - quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1, - } - } - - function mergeDeliveryItems(items: AdminKuaishouCloudDeliveryItem[]) { - const merged = new Map() - - for (const item of items) { - const existing = merged.get(item.cloudSkuId) - if (existing) { - existing.quantity += item.quantity - existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName - continue - } - - merged.set(item.cloudSkuId, { ...item }) - } - - return Array.from(merged.values()) - } - - // ── validation helpers ──────────────────────────────── - - function hasExternalMatch( - item: Pick, - ) { - return Boolean( - item.externalSkuCode.trim() || item.externalItemId.trim() || item.externalSkuName.trim(), - ) - } - - function hasMeaningfulContent(item: EditableItem) { - return Boolean( - item.internalSkuCode.trim() || - item.internalSkuName.trim() || - item.externalSkuCode.trim() || - item.externalItemId.trim() || - item.externalSkuName.trim() || - item.resolvedSkuName.trim() || - item.cloudSourceKeys.length > 0 || - Number(item.cloudSkuId || 0) > 0 || - item.cloudSkuName.trim() || - normalizeDeliveryItems(item).length > 0 || - item.vnKey.trim() || - item.shopId.trim() || - item.notes.trim() || - item.provider.trim() !== '91kaquan' || - item.platform.trim() !== 'kuaishou' || - item.priority !== 100 || - item.autoBuyEnabled !== true || - item.minAssetReserve !== 0 || - item.autoReturnNumberAfterDispatch || - item.autoConsumeAfterDispatch || - item.kuaishouConsumeShopId.trim() || - item.kuaishouConsumeShopName.trim() || - item.enabled !== true, - ) - } - - function isItemComplete(item: EditableItem) { - return Boolean( - item.internalSkuCode.trim() && - item.cloudSourceKeys.length > 0 && - normalizeDeliveryItems(item).length > 0 && - (!item.autoConsumeAfterDispatch || Boolean(item.kuaishouConsumeShopId.trim())) && - hasExternalMatch(item), - ) - } - - function resolveValidation(item: EditableItem, index: number): ValidationState { - if (!hasMeaningfulContent(item)) { - return null - } - - const title = `第 ${index + 1} 条规则` - - if (!item.internalSkuCode.trim()) { - return { localId: item.localId, message: `${title} 缺少内部 SKU 编码` } - } - - if (item.cloudSourceKeys.length === 0) { - return { localId: item.localId, message: `${title} 需要选择至少一个 cloud 账号` } - } - - const deliveryItems = normalizeDeliveryItems(item) - if (deliveryItems.length === 0) { - return { localId: item.localId, message: `${title} 需要至少配置 1 个 cloud 发货物品` } - } - - const invalidDeliveryItem = deliveryItems.find( - (deliveryItem) => Number(deliveryItem.quantity || 0) <= 0, - ) - if (invalidDeliveryItem) { - return { localId: item.localId, message: `${title} 的发货数量必须大于 0` } - } - - if (!hasExternalMatch(item)) { - return { localId: item.localId, message: `${title} 至少填写一种外部匹配条件` } - } - - if (item.autoConsumeAfterDispatch && !item.kuaishouConsumeShopId.trim()) { - return { localId: item.localId, message: `${title} 已开启发货后核销,需要选择快手核销店铺` } - } - - return null - } - - // ── display helpers ─────────────────────────────────── - - function getCardTitle(item: EditableItem, index: number) { - return ( - item.internalSkuName.trim() || - item.externalSkuName.trim() || - item.internalSkuCode.trim() || - `规则 ${index + 1}` - ) - } - - function getCardSummary(item: EditableItem) { - const parts = [ - item.provider.trim() || '91kaquan', - item.platform.trim() || 'kuaishou', - item.shopId.trim() || '跨店铺', - getDeliveryPlanSummary(item), - ] - return parts.join(' / ') - } - - function getRuleState(item: EditableItem) { - if (!item.enabled) { - return { label: '已停用', tone: 'muted' } - } - - if (isItemComplete(item)) { - return { label: '可投产', tone: 'success' } - } - - return { label: '草稿待完善', tone: 'warning' } - } - - function matchesRuleFilter(item: EditableItem, filter: RuleFilter) { - if (filter === 'ready') { - return item.enabled && isItemComplete(item) - } - - if (filter === 'draft') { - return item.enabled && !isItemComplete(item) - } - - if (filter === 'disabled') { - return !item.enabled - } - - return true - } - - function getExternalMatchSummary(item: EditableItem) { - const parts = [ - item.externalSkuCode.trim() ? `SKU ${item.externalSkuCode.trim()}` : '', - item.externalItemId.trim() ? `Item ${item.externalItemId.trim()}` : '', - item.externalSkuName.trim() ? item.externalSkuName.trim() : '', - ].filter(Boolean) - - return parts.length > 0 ? parts.join(' / ') : '未设置外部命中条件' - } - - // ── kuaishou consume shop helpers ───────────────────── - - function getKuaishouConsumeShopOptions() { - return kuaishouConsumeShops.value.filter((shop) => shop.enabled !== false && shop.hasCookie) - } - - function findKuaishouConsumeShop(shopId = '', shopName = '') { - const normalizedShopId = String(shopId || '').trim() - const normalizedShopName = String(shopName || '').trim() - return ( - getKuaishouConsumeShopOptions().find( - (shop) => - (normalizedShopId && shop.shopId === normalizedShopId) || - (normalizedShopName && shop.kshopName === normalizedShopName), - ) || null - ) - } - - function getDefaultKuaishouConsumeShop() { - return getKuaishouConsumeShopOptions()[0] || null - } - - function formatKuaishouConsumeShopOption(shop: AdminKuaishouEticketShopConfigItem) { - return `${shop.kshopName || '未命名快手小店'} · ${shop.shopId}` - } - - function handleKuaishouConsumeShopSelected(item: EditableItem) { - const matchedShop = findKuaishouConsumeShop(item.kuaishouConsumeShopId) - item.kuaishouConsumeShopName = matchedShop?.kshopName || '' - } - - function getConsumeShopSummary(item: EditableItem) { - const matchedShop = findKuaishouConsumeShop( - item.kuaishouConsumeShopId, - item.kuaishouConsumeShopName, - ) - const name = String(matchedShop?.kshopName || item.kuaishouConsumeShopName).trim() - const id = String(matchedShop?.shopId || item.kuaishouConsumeShopId).trim() - - if (name && id) { - return `${name} · ${id}` - } - - 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) { - const deliveryItems = normalizeDeliveryItems(item) - if (deliveryItems.length > 1) { - return `${deliveryItems.length} 个物品 / 共 ${getDeliveryTotalQuantity(item)} 次` - } - - const primaryItem = deliveryItems[0] - if (primaryItem?.cloudSkuId && primaryItem.cloudSkuName.trim()) { - const suffix = primaryItem.quantity > 1 ? ` × ${primaryItem.quantity}` : '' - return `${primaryItem.cloudSkuName.trim()} · #${primaryItem.cloudSkuId}${suffix}` - } - - if (primaryItem?.cloudSkuId) { - const suffix = primaryItem.quantity > 1 ? ` × ${primaryItem.quantity}` : '' - return `cloud SKU #${primaryItem.cloudSkuId}${suffix}` - } - - return '待选择 cloud SKU' - } - - function getDeliveryPlanSummary(item: EditableItem) { - const deliveryItems = normalizeDeliveryItems(item) - if (deliveryItems.length === 0) { - return '待填发货物品' - } - - if (deliveryItems.length === 1) { - const firstItem = deliveryItems[0] - return firstItem.quantity > 1 - ? `cloud#${firstItem.cloudSkuId} × ${firstItem.quantity}` - : `cloud#${firstItem.cloudSkuId}` - } - - return `${deliveryItems.length} 个物品 / ${getDeliveryTotalQuantity(item)} 次` - } - - function getDeliveryTotalQuantity(item: EditableItem) { - return normalizeDeliveryItems(item).reduce((sum, deliveryItem) => sum + deliveryItem.quantity, 0) - } - - // ── collapse state ──────────────────────────────────── - - function isCollapsed(localId: string) { - return collapsedIds.value.includes(localId) - } - - function setCollapsed(localId: string, collapsed: boolean) { - const next = new Set(collapsedIds.value) - if (collapsed) { - next.add(localId) - } else { - next.delete(localId) - } - collapsedIds.value = Array.from(next) - } - - function toggleCollapsed(localId: string) { - setCollapsed(localId, !isCollapsed(localId)) - } - - function rebuildCollapsedState() { - collapsedIds.value = items.value.filter(isItemComplete).map((item) => item.localId) - } - - function expandAllRules() { - collapsedIds.value = [] - } - - function collapseReadyRules() { - collapsedIds.value = items.value - .filter((item) => isItemComplete(item) || !item.enabled) - .map((item) => item.localId) - } - - // ── data loading ────────────────────────────────────── - - async function loadConfigs() { - if (!hasAdminRole('admin')) { - loading.value = false - return - } - - loading.value = true - errorMessage.value = '' - - try { - const [response, eticketResponse, cloudtentaclesSourceResponse] = await Promise.all([ - fetchAdminKuaishouCloudFulfillmentConfig(), - fetchAdminKuaishouEticketSourceConfig(), - fetchAdminCloudtentaclesSourceConfig(), - ]) - kuaishouConsumeShops.value = Array.isArray(eticketResponse.data.source.shops) - ? eticketResponse.data.source.shops - : [] - const sources = Array.isArray(cloudtentaclesSourceResponse.data.sources) - ? cloudtentaclesSourceResponse.data.sources - : [] - cloudtentaclesSourceOptions.value = sources.map((s) => ({ - key: s.key, - label: s.label || s.key, - })) - filePath.value = response.data.filePath - enabled.value = response.data.source.enabled !== false - items.value = (response.data.source.items || []).map(mapEditableItem) - rebuildCollapsedState() - } catch (error) { - errorMessage.value = error instanceof Error ? error.message : '读取新履约配置失败' - } finally { - loading.value = false - } - } - - // ── mutation ────────────────────────────────────────── - - function addItem() { - validationState.value = null - const next = createEmptyItem() - items.value.unshift(next) - setCollapsed(next.localId, false) - } - - function removeItem(localId: string) { - if (validationState.value?.localId === localId) { - validationState.value = null - } - items.value = items.value.filter((item) => item.localId !== localId) - setCollapsed(localId, false) - } - - function handleCloudSourceKeysChanged(item: EditableItem) { - const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys) - item.cloudSourceKeys = sourceKeys - item.cloudSkuId = 0 - item.cloudSkuName = '' - item.deliveryItems = [] - } - - function addDeliveryItem(item: EditableItem) { - item.deliveryItems.push({ - cloudSkuId: 0, - cloudSkuName: '', - quantity: 1, - }) - } - - function removeDeliveryItem(item: EditableItem, index: number) { - item.deliveryItems.splice(index, 1) - syncPrimaryCloudSkuFromDeliveryItems(item) - } - - function syncPrimaryCloudSkuFromDeliveryItems(item: EditableItem) { - const primaryItem = normalizeDeliveryItems(item)[0] - item.cloudSkuId = primaryItem?.cloudSkuId || 0 - item.cloudSkuName = primaryItem?.cloudSkuName || '' - } - - function syncDeliveryItemsFromPrimaryCloudSku(item: EditableItem) { - if (Number(item.cloudSkuId || 0) <= 0) { - item.deliveryItems = [] - return - } - - if (item.deliveryItems.length === 0) { - item.deliveryItems.push({ - cloudSkuId: item.cloudSkuId, - cloudSkuName: item.cloudSkuName, - quantity: 1, - }) - return - } - - item.deliveryItems[0] = { - ...item.deliveryItems[0], - cloudSkuId: item.cloudSkuId, - cloudSkuName: item.cloudSkuName, - quantity: Math.max(1, Number(item.deliveryItems[0].quantity || 1) || 1), - } - } - - async function focusValidationTarget(localId: string) { - if (!localId) { - return - } - - await nextTick() - const card = document.querySelector(`[data-local-id="${localId}"]`) - if (!card) { - return - } - - card.scrollIntoView({ behavior: 'smooth', block: 'center' }) - } - - // ── save ────────────────────────────────────────────── - - async function saveConfigs() { - const invalid = items.value.reduce( - (state, item, index) => state || resolveValidation(item, index), - null, - ) - if (invalid) { - validationState.value = invalid - setCollapsed(invalid.localId, false) - showError(invalid.message) - await focusValidationTarget(invalid.localId) - return - } - - const payloadItems = items.value.filter(hasMeaningfulContent).map(mapSaveItem) - const payload: AdminKuaishouCloudFulfillmentConfig = { - enabled: enabled.value, - items: payloadItems, - } - - saving.value = true - validationState.value = null - - try { - const response = await saveAdminKuaishouCloudFulfillmentConfig(payload) - filePath.value = response.data.filePath - enabled.value = response.data.source.enabled !== false - items.value = (response.data.source.items || []).map(mapEditableItem) - rebuildCollapsedState() - showSuccess('新履约配置已保存') - } catch (error) { - const message = error instanceof Error ? error.message : '保存新履约配置失败' - validationState.value = { localId: '', message } - showError(message) - } finally { - saving.value = false - } - } - - return { - // state - loading, - saving, - errorMessage, - validationState, - filePath, - enabled, - items, - collapsedIds, - ruleFilter, - kuaishouConsumeShops, - // computed - metrics, - filteredItems, - ruleFilterOptions, - // factories - createEmptyItem, - mapEditableItem, - // validation - isItemComplete, - resolveValidation, - hasMeaningfulContent, - // display - getCardTitle, - getCardSummary, - getRuleState, - getExternalMatchSummary, - getCloudSkuSummary, - getCloudSourceSummary, - getConsumeShopSummary, - getDeliveryPlanSummary, - // consume shops - getKuaishouConsumeShopOptions, - findKuaishouConsumeShop, - getDefaultKuaishouConsumeShop, - formatKuaishouConsumeShopOption, - handleKuaishouConsumeShopSelected, - handleCloudSourceKeysChanged, - addDeliveryItem, - removeDeliveryItem, - syncPrimaryCloudSkuFromDeliveryItems, - syncDeliveryItemsFromPrimaryCloudSku, - // collapse - isCollapsed, - setCollapsed, - toggleCollapsed, - expandAllRules, - collapseReadyRules, - // data - loadConfigs, - addItem, - removeItem, - saveConfigs, - focusValidationTarget, - // cloudtentacles sources - cloudtentaclesSourceOptions, - } -} 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 deleted file mode 100644 index f2c90065..00000000 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudNinetyone.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { computed, reactive, ref } from 'vue' - -import { showSuccess } from '@/lib/feedback' -import { fetchAdminNinetyoneOrders } from '@/services/admin' -import type { AdminNinetyoneOrderItem } from '@/types/admin' - -import type { EditableItem } from './types' - -import type { useKuaishouCloudConfig } from './useKuaishouCloudConfig' - -export function useKuaishouCloudNinetyone( - config: ReturnType, -) { - const ninetyoneLookupLoading = ref(false) - const ninetyoneLookupErrorMessage = ref('') - const ninetyoneLookupResults = ref([]) - const importedNinetyoneOrderKeys = ref([]) - const ninetyoneLookupForm = reactive({ - status: 'pending_config' as 'pending_config' | 'all' | 'manual_failed', - page: 1, - pageSize: 20, - }) - - const ninetyoneLookupMetrics = computed(() => { - const importedCount = ninetyoneLookupResults.value.filter((item) => - isNinetyoneProductImported(item), - ).length - const pendingCount = ninetyoneLookupResults.value.filter( - (item) => item.orderStatus === 'pending_config', - ).length - return { - total: ninetyoneLookupResults.value.length, - importedCount, - availableCount: Math.max(ninetyoneLookupResults.value.length - importedCount, 0), - pendingCount, - } - }) - - function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableItem { - const productNo = String(item.productNo || '').trim() - const productName = String(item.productName || productNo).trim() - 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: '', - enabled: true, - priority: 100, - provider: '91kaquan', - platform: 'kuaishou', - shopId: item.shopId || '91kaquan', - internalSkuCode: '', - internalSkuName: productName, - externalSkuCode: productNo, - externalItemId: productNo, - externalSkuName: productName, - resolvedSkuName: productName, - cloudSourceKeys: sourceKeys, - cloudSkuId: 0, - cloudSkuName: '', - deliveryItems: [], - vnKey: '1', - autoBuyEnabled: true, - minAssetReserve: 0, - autoReturnNumberAfterDispatch: false, - autoConsumeAfterDispatch: false, - kuaishouConsumeShopId: shop?.shopId || '', - kuaishouConsumeShopName: shop?.kshopName || '', - notes: `从 91卡券订单 ${item.orderNo} 导入`, - } - } - - function findExistingItemFromNinetyoneOrder(order: AdminNinetyoneOrderItem) { - const productNo = String(order.productNo || '').trim() - const productName = String(order.productName || '').trim() - const shopId = String(order.shopId || '91kaquan').trim() - - return ( - config.items.value.find((item) => { - const sameSource = item.provider.trim() === '91kaquan' && item.platform.trim() === 'kuaishou' - const sameShop = !shopId || item.shopId.trim() === shopId - const sameProductNo = - productNo && - (item.externalSkuCode.trim() === productNo || item.externalItemId.trim() === productNo) - const sameName = - productName && - [item.externalSkuName, item.internalSkuName, item.resolvedSkuName].some( - (value) => value.trim() === productName, - ) - return sameSource && sameShop && (sameProductNo || sameName) - }) || null - ) - } - - async function importNinetyoneProduct(item: AdminNinetyoneOrderItem) { - config.validationState.value = null - const importKey = getNinetyoneOrderImportKey(item) - const existing = findExistingItemFromNinetyoneOrder(item) - if (existing) { - config.setCollapsed(existing.localId, false) - if (!importedNinetyoneOrderKeys.value.includes(importKey)) { - importedNinetyoneOrderKeys.value = [...importedNinetyoneOrderKeys.value, importKey] - } - showSuccess('已定位到现有 91卡券规则草稿,直接继续完善即可') - await config.focusValidationTarget(existing.localId) - return - } - - const next = createItemFromNinetyoneOrder(item) - config.items.value.unshift(next) - config.setCollapsed(next.localId, false) - - if (!importedNinetyoneOrderKeys.value.includes(importKey)) { - importedNinetyoneOrderKeys.value = [...importedNinetyoneOrderKeys.value, importKey] - } - - await config.focusValidationTarget(next.localId) - } - - function getNinetyoneOrderImportKey(item: AdminNinetyoneOrderItem) { - return [item.orderNo, item.productNo, item.shopId || '91kaquan'] - .map((value) => String(value || '').trim()) - .join(':') - } - - function isNinetyoneProductImported(item: AdminNinetyoneOrderItem) { - return importedNinetyoneOrderKeys.value.includes(getNinetyoneOrderImportKey(item)) - } - - async function lookupNinetyoneProducts() { - ninetyoneLookupLoading.value = true - ninetyoneLookupErrorMessage.value = '' - - try { - const response = await fetchAdminNinetyoneOrders({ - page: Number(ninetyoneLookupForm.page || 1), - pageSize: Number(ninetyoneLookupForm.pageSize || 20), - status: ninetyoneLookupForm.status, - }) - ninetyoneLookupResults.value = response.data.items - importedNinetyoneOrderKeys.value = [] - } catch (error) { - ninetyoneLookupResults.value = [] - ninetyoneLookupErrorMessage.value = - error instanceof Error ? error.message : '91卡券订单查询失败' - } finally { - ninetyoneLookupLoading.value = false - } - } - - return { - ninetyoneLookupLoading, - ninetyoneLookupErrorMessage, - ninetyoneLookupResults, - importedNinetyoneOrderKeys, - ninetyoneLookupForm, - ninetyoneLookupMetrics, - importNinetyoneProduct, - isNinetyoneProductImported, - lookupNinetyoneProducts, - } -} 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 deleted file mode 100644 index 799e5b11..00000000 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-cloud/composables/useKuaishouCloudSku.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { ref } from 'vue' - -import { showError, showSuccess } from '@/lib/feedback' -import { fetchAdminCloudtentaclesSkuList } from '@/services/admin' -import type { AdminCloudtentaclesSkuItem, AdminKuaishouCloudDeliveryItem } from '@/types/admin' - -import type { EditableItem } from './types' - -export function useKuaishouCloudSku(cloudtentaclesSourceOptions?: { value: { key: string }[] }) { - const cloudSkuCatalogLoadingBySourceKey = ref>({}) - const cloudSkuCatalogErrorMessage = ref('') - const cloudSkuCatalogBySourceKey = ref>({}) - - function getSourceKey(item: EditableItem) { - return String(item.cloudSourceKeys[0] || '').trim() - } - - async function ensureCloudSkuCatalogLoaded(item: EditableItem, force = false) { - const sourceKey = getSourceKey(item) - if (!force && (cloudSkuCatalogBySourceKey.value[sourceKey]?.length ?? 0) > 0) { - return cloudSkuCatalogBySourceKey.value[sourceKey] - } - - cloudSkuCatalogLoadingBySourceKey.value[sourceKey] = true - cloudSkuCatalogErrorMessage.value = '' - - try { - const response = await fetchAdminCloudtentaclesSkuList({ sourceKey }) - cloudSkuCatalogBySourceKey.value[sourceKey] = Array.isArray(response.data.items) - ? response.data.items - : [] - return cloudSkuCatalogBySourceKey.value[sourceKey] - } catch (error) { - cloudSkuCatalogBySourceKey.value[sourceKey] = [] - cloudSkuCatalogErrorMessage.value = - error instanceof Error ? error.message : 'cloud SKU 列表查询失败' - throw error - } finally { - cloudSkuCatalogLoadingBySourceKey.value[sourceKey] = false - } - } - - function getCloudSkuKeyword(item: EditableItem) { - return ( - [item.internalSkuCode, item.internalSkuName, item.resolvedSkuName, item.externalSkuName] - .map((value) => String(value || '').trim()) - .find(Boolean) || '' - ) - } - - function normalizeSearchText(value: string) { - return String(value || '') - .trim() - .toLowerCase() - } - - function scoreCloudSkuMatch(item: AdminCloudtentaclesSkuItem, keyword: string) { - const normalizedKeyword = normalizeSearchText(keyword) - if (!normalizedKeyword) { - return 0 - } - - const name = normalizeSearchText(item.name) - const description = normalizeSearchText(item.description) - - if (name === normalizedKeyword) { - return 120 - } - - if (name.startsWith(normalizedKeyword)) { - return 100 - } - - if (name.includes(normalizedKeyword)) { - return 80 - } - - if (description.includes(normalizedKeyword)) { - return 40 - } - - return 0 - } - - function getCloudSkuOptions(item: EditableItem) { - const sourceKey = getSourceKey(item) - const catalog = cloudSkuCatalogBySourceKey.value[sourceKey] || [] - const keyword = getCloudSkuKeyword(item) - return catalog - .map((sku) => ({ sku, score: scoreCloudSkuMatch(sku, keyword) })) - .sort((left, right) => { - if (right.score !== left.score) { - return right.score - left.score - } - - return left.sku.name.localeCompare(right.sku.name, 'zh-CN') - }) - .map((entry) => entry.sku) - } - - function formatCloudSkuOptionLabel(item: AdminCloudtentaclesSkuItem) { - const price = Number(item.price || 0) - const inventory = Number(item.inventory || 0) - return `${item.name} · ID ${item.id} · 库存 ${inventory} · 价格 ${price}` - } - - function handleCloudSkuSelected(item: EditableItem, value: number | string | undefined) { - const skuId = Number(value || 0) - item.cloudSkuId = Number.isInteger(skuId) && skuId > 0 ? skuId : 0 - - if (!item.cloudSkuId) { - item.cloudSkuName = '' - return - } - - const sourceKey = getSourceKey(item) - const catalog = cloudSkuCatalogBySourceKey.value[sourceKey] || [] - const matched = catalog.find((sku) => sku.id === item.cloudSkuId) - item.cloudSkuName = matched?.name || item.cloudSkuName || '' - } - - function handleDeliveryItemCloudSkuSelected( - item: EditableItem, - deliveryItem: AdminKuaishouCloudDeliveryItem, - value: number | string | undefined, - ) { - const skuId = Number(value || 0) - deliveryItem.cloudSkuId = Number.isInteger(skuId) && skuId > 0 ? skuId : 0 - - if (!deliveryItem.cloudSkuId) { - deliveryItem.cloudSkuName = '' - return - } - - const sourceKey = getSourceKey(item) - const catalog = cloudSkuCatalogBySourceKey.value[sourceKey] || [] - const matched = catalog.find((sku) => sku.id === deliveryItem.cloudSkuId) - deliveryItem.cloudSkuName = matched?.name || deliveryItem.cloudSkuName || '' - } - - async function handleCloudSkuDropdownVisible(item: EditableItem, visible: boolean) { - if (!visible) { - return - } - - try { - await ensureCloudSkuCatalogLoaded(item) - if (item.cloudSkuId && !item.cloudSkuName) { - handleCloudSkuSelected(item, item.cloudSkuId) - } - } catch (error) { - showError(error instanceof Error ? error.message : 'cloud SKU 列表查询失败') - } - } - - async function refreshCloudSkuCatalog(item?: EditableItem) { - try { - if (item) { - await ensureCloudSkuCatalogLoaded(item, true) - const sourceKey = getSourceKey(item) - const count = cloudSkuCatalogBySourceKey.value[sourceKey]?.length ?? 0 - showSuccess(`cloud SKU 列表(${sourceKey})已刷新,共 ${count} 条`) - } else { - const cachedKeys = Object.keys(cloudSkuCatalogBySourceKey.value) - const configuredKeys = cloudtentaclesSourceOptions?.value?.map((s) => s.key) || [] - const keysToRefresh = cachedKeys.length > 0 ? cachedKeys : configuredKeys - - for (const key of keysToRefresh) { - await ensureCloudSkuCatalogLoaded( - { cloudSourceKeys: [key], localId: '' } as EditableItem, - true, - ) - } - const total = Object.values(cloudSkuCatalogBySourceKey.value).reduce( - (sum, list) => sum + list.length, - 0, - ) - showSuccess(`cloud SKU 列表已全部刷新,共 ${total} 条`) - } - } catch (error) { - showError(error instanceof Error ? error.message : 'cloud SKU 列表查询失败') - } - } - - const cloudSkuCatalogLoading = ref(false) - function syncCloudSkuCatalogLoading() { - cloudSkuCatalogLoading.value = Object.values(cloudSkuCatalogLoadingBySourceKey.value).some( - Boolean, - ) - } - - return { - cloudSkuCatalogLoading, - cloudSkuCatalogLoadingBySourceKey, - cloudSkuCatalogErrorMessage, - cloudSkuCatalogBySourceKey, - getCloudSkuOptions, - formatCloudSkuOptionLabel, - handleCloudSkuSelected, - handleDeliveryItemCloudSkuSelected, - handleCloudSkuDropdownVisible, - refreshCloudSkuCatalog, - syncCloudSkuCatalogLoading, - } -}