删除旧履约配置逻辑
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS product_match_rules;
|
||||
DROP TABLE IF EXISTS sku_fulfillment_bindings;
|
||||
@@ -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<string, unknown>
|
||||
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<string, unknown>
|
||||
}
|
||||
|
||||
type FulfillmentProfileUpsertInput = {
|
||||
profileKey: string
|
||||
name: string
|
||||
@@ -69,26 +47,6 @@ export type FulfillmentProfileRequirementInput = {
|
||||
configJson?: string | Record<string, unknown>
|
||||
}
|
||||
|
||||
type SkuFulfillmentBindingUpsertInput = {
|
||||
skuCode: string
|
||||
provider?: string
|
||||
platform?: string
|
||||
shopId?: string
|
||||
profileId: number | string
|
||||
enabled?: boolean
|
||||
priority?: number | string
|
||||
configJson?: string | Record<string, unknown>
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
type FulfillmentBindingResolveInput = {
|
||||
skuCode: string
|
||||
provider?: string
|
||||
platform?: string
|
||||
shopId?: string
|
||||
}
|
||||
|
||||
export async function getFulfillmentProfileByKey(profileKey: string): Promise<FulfillmentProfileRow | null> {
|
||||
const result = await query<FulfillmentProfileRow>(
|
||||
'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<SkuFulfillmentBindingRow | null> {
|
||||
const existing = await query<SkuFulfillmentBindingRow>(
|
||||
`
|
||||
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<SkuFulfillmentBindingRow>(
|
||||
`
|
||||
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<SkuFulfillmentBindingRow>(
|
||||
`
|
||||
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<SkuFulfillmentBindingRow | null> {
|
||||
const result = await query<SkuFulfillmentBindingRow>(
|
||||
`
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<OrderRow | null> {
|
||||
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<OrderRow>(
|
||||
`
|
||||
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<OrderRow | null> {
|
||||
const result = await query<OrderRow>(
|
||||
`
|
||||
|
||||
@@ -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<string, unknown>
|
||||
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<string, unknown>
|
||||
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<ProductMatchRuleRow | null> {
|
||||
const result = await query<ProductMatchRuleRow>(
|
||||
`
|
||||
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<ProductMatchRuleRow | null> {
|
||||
const result = await query<ProductMatchRuleRow>(
|
||||
`
|
||||
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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<string, any>
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<FulfillmentBindingLike | null>
|
||||
getFulfillmentProfileByKey?: (profileKey: string) => Promise<FulfillmentBindingLike | null>
|
||||
createTaskClaimToken?: (taskId: number | string) => Promise<ClaimTokenLike>
|
||||
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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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<string, any>;
|
||||
|
||||
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<number, DeliveryItem>();
|
||||
|
||||
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 [];
|
||||
}
|
||||
@@ -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<UpsertOrderResult> {
|
||||
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<SourceOrderEvent, 'shopId' | 'shopIdAliases'>): 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,
|
||||
|
||||
@@ -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({}), [''])
|
||||
})
|
||||
@@ -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<ReturnType<typeof resolveProductMatchRule>>
|
||||
resolvedSkuCode: string
|
||||
binding: Awaited<ReturnType<typeof resolveFulfillmentBinding>>
|
||||
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<ResolvedFulfillmentItem> {
|
||||
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<boolean> {
|
||||
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<FulfillmentItemCandidate> {
|
||||
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<string, unknown> {
|
||||
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<string, unknown> {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'] },
|
||||
|
||||
@@ -3,4 +3,3 @@ export * from './scheduled-jobs'
|
||||
export * from './ninetyone'
|
||||
export * from './kuaishou-eticket'
|
||||
export * from './cloudtentacles'
|
||||
export * from './kuaishou-cloud-fulfillment'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -66,7 +66,4 @@ export type {
|
||||
AdminCloudtentaclesSkuListResult,
|
||||
AdminCloudtentaclesDeliveryRecordItem,
|
||||
AdminCloudtentaclesDeliveryRecordListResult,
|
||||
AdminKuaishouCloudDeliveryItem,
|
||||
AdminKuaishouCloudFulfillmentItem,
|
||||
AdminKuaishouCloudFulfillmentConfig,
|
||||
} from './platform-config'
|
||||
|
||||
@@ -47,9 +47,3 @@ export type {
|
||||
AdminCloudtentaclesDeliveryRecordItem,
|
||||
AdminCloudtentaclesDeliveryRecordListResult,
|
||||
} from './cloudtentacles'
|
||||
|
||||
export type {
|
||||
AdminKuaishouCloudDeliveryItem,
|
||||
AdminKuaishouCloudFulfillmentItem,
|
||||
AdminKuaishouCloudFulfillmentConfig,
|
||||
} from './kuaishou-cloud-fulfillment'
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
-165
@@ -1,165 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminNinetyoneOrderItem } from '@/types/admin'
|
||||
|
||||
defineProps<{
|
||||
ninetyoneLookupLoading: boolean
|
||||
ninetyoneLookupErrorMessage: string
|
||||
ninetyoneLookupResults: AdminNinetyoneOrderItem[]
|
||||
ninetyoneLookupMetrics: {
|
||||
total: number
|
||||
importedCount: number
|
||||
availableCount: number
|
||||
pendingCount: number
|
||||
}
|
||||
ninetyoneLookupForm: {
|
||||
status: 'pending_config' | 'all' | 'manual_failed'
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
isImported: (item: AdminNinetyoneOrderItem) => boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
lookup: []
|
||||
importProduct: [item: AdminNinetyoneOrderItem]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header card-header--split">
|
||||
<div>
|
||||
<span class="card-title">91卡券订单取样导入</span>
|
||||
<span class="card-desc"
|
||||
>查询已接收的 91卡券订单,把 productNo 导入为规则草稿,再补齐内部 SKU 和 cloud
|
||||
资源。</span
|
||||
>
|
||||
</div>
|
||||
<el-space wrap>
|
||||
<el-tag effect="plain">结果 {{ ninetyoneLookupMetrics.total }}</el-tag>
|
||||
<el-tag type="warning" effect="plain"
|
||||
>待补 {{ ninetyoneLookupMetrics.pendingCount }}</el-tag
|
||||
>
|
||||
<el-tag type="success" effect="plain"
|
||||
>待导入 {{ ninetyoneLookupMetrics.availableCount }}</el-tag
|
||||
>
|
||||
</el-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="lookup-panel">
|
||||
<el-form label-position="top" class="lookup-toolbar">
|
||||
<el-form-item label="订单状态" class="lookup-field lookup-field--status">
|
||||
<el-select v-model="ninetyoneLookupForm.status" class="text-input">
|
||||
<el-option label="待补全" value="pending_config" />
|
||||
<el-option label="已失败" value="manual_failed" />
|
||||
<el-option label="全部" value="all" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="页码" class="lookup-field lookup-field--number">
|
||||
<el-input-number
|
||||
v-model="ninetyoneLookupForm.page"
|
||||
class="text-input"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="每页条数" class="lookup-field lookup-field--number">
|
||||
<el-input-number
|
||||
v-model="ninetyoneLookupForm.pageSize"
|
||||
class="text-input"
|
||||
:max="100"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作" class="lookup-field lookup-field--action">
|
||||
<el-button
|
||||
:loading="ninetyoneLookupLoading"
|
||||
type="primary"
|
||||
class="lookup-action"
|
||||
@click="emit('lookup')"
|
||||
>
|
||||
查询 91卡券订单
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-alert
|
||||
title="导入后会创建 provider=91kaquan、platform=kuaishou、外部 SKU=productNo 的规则草稿;保存规则后,回到“平台配置 -> 91卡券接入”重试订单。"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="lookup-note"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="ninetyoneLookupErrorMessage"
|
||||
:title="ninetyoneLookupErrorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
<el-skeleton v-else-if="ninetyoneLookupLoading" :rows="4" animated class="mt-4" />
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
:data="ninetyoneLookupResults"
|
||||
class="data-table element-data-table"
|
||||
empty-text="还没有 91卡券查询结果。"
|
||||
>
|
||||
<el-table-column label="订单" min-width="180">
|
||||
<template #default="{ row: item }">
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.orderNo || '-' }}</strong>
|
||||
<span class="cell-subtle">{{ item.outTradeNo || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源" min-width="180">
|
||||
<template #default="{ row: item }">
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.shopName || '91卡券' }}</strong>
|
||||
<span class="cell-subtle">provider: 91kaquan</span>
|
||||
<span class="cell-subtle">shopId: {{ item.shopId || '91kaquan' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品" min-width="240">
|
||||
<template #default="{ row: item }">
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.productName || item.productNo || '-' }}</strong>
|
||||
<span class="cell-subtle">productNo: {{ item.productNo || '-' }}</span>
|
||||
<span class="cell-subtle">数量:{{ item.buyNum || 0 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="120">
|
||||
<template #default="{ row: item }">
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.orderStatus || '-' }}</strong>
|
||||
<span class="cell-subtle">任务 {{ item.taskCount || 0 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="130">
|
||||
<template #default="{ row: item }">
|
||||
<el-button
|
||||
v-if="!isImported(item)"
|
||||
link
|
||||
type="primary"
|
||||
@click="emit('importProduct', item)"
|
||||
>
|
||||
导入到规则
|
||||
</el-button>
|
||||
<span v-else class="cell-subtle">已导入草稿</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped src="../AdminKuaishouCloudFulfillment.css"></style>
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
filePath: string
|
||||
metrics: {
|
||||
total: number
|
||||
readyCount: number
|
||||
draftCount: number
|
||||
enabledCount: number
|
||||
disabledCount: number
|
||||
}
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never" class="section-card overview-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">快手 Cloud 新履约概览</span>
|
||||
<span class="card-desc">维护 91卡券商品到内部 SKU、cloud 资源和核销店铺的映射。</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="1" border class="overview-file">
|
||||
<el-descriptions-item label="配置文件">
|
||||
<code>{{ filePath || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="overview-stats">
|
||||
<el-statistic class="overview-stat" title="规则总数" :value="metrics.total" />
|
||||
<el-statistic class="overview-stat" title="可投产" :value="metrics.readyCount" />
|
||||
<el-statistic class="overview-stat" title="待完善" :value="metrics.draftCount" />
|
||||
<el-statistic class="overview-stat" title="已启用" :value="metrics.enabledCount" />
|
||||
<el-statistic class="overview-stat" title="已停用" :value="metrics.disabledCount" />
|
||||
</div>
|
||||
|
||||
<div class="overview-notes">
|
||||
<el-tag effect="plain">91卡券订单来自已接收的待补全队列</el-tag>
|
||||
<el-tag effect="plain">内部 SKU 决定任务与云资源匹配</el-tag>
|
||||
<el-tag effect="plain">cloud SKU 决定自动购买、发货与退号资源</el-tag>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped src="../AdminKuaishouCloudFulfillment.css"></style>
|
||||
-405
@@ -1,405 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminCloudtentaclesSkuItem, AdminKuaishouEticketShopConfigItem } from '@/types/admin'
|
||||
|
||||
import type { EditableItem, ValidationState } from '../composables/types'
|
||||
|
||||
defineProps<{
|
||||
item: EditableItem
|
||||
index: number
|
||||
validationState: ValidationState
|
||||
cloudtentaclesSourceOptions: { key: string; label: string }[]
|
||||
cloudSkuCatalogErrorMessage: string
|
||||
cloudSkuCatalogLoading: boolean
|
||||
isCollapsed: boolean
|
||||
isItemComplete: (item: EditableItem) => boolean
|
||||
getCardTitle: (item: EditableItem, index: number) => string
|
||||
getCardSummary: (item: EditableItem) => string
|
||||
getRuleState: (item: EditableItem) => { label: string; tone: string }
|
||||
getExternalMatchSummary: (item: EditableItem) => string
|
||||
getCloudSourceSummary: (item: EditableItem) => string
|
||||
getCloudSkuSummary: (item: EditableItem) => string
|
||||
getConsumeShopSummary: (item: EditableItem) => string
|
||||
getDeliveryPlanSummary: (item: EditableItem) => string
|
||||
getCloudSkuOptions: (item: EditableItem) => AdminCloudtentaclesSkuItem[]
|
||||
formatCloudSkuOptionLabel: (sku: AdminCloudtentaclesSkuItem) => string
|
||||
getKuaishouConsumeShopOptions: () => AdminKuaishouEticketShopConfigItem[]
|
||||
formatKuaishouConsumeShopOption: (shop: AdminKuaishouEticketShopConfigItem) => string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleCollapsed: []
|
||||
handleCloudSkuSelected: [value: number | string | undefined]
|
||||
handleDeliveryItemCloudSkuSelected: [index: number, value: number | string | undefined]
|
||||
handleCloudSkuDropdownVisible: [visible: boolean]
|
||||
handleKuaishouConsumeShopSelected: []
|
||||
handleCloudSourceKeysChanged: []
|
||||
addDeliveryItem: []
|
||||
removeDeliveryItem: [index: number]
|
||||
remove: []
|
||||
}>()
|
||||
|
||||
function isAdvancedDeliveryConfigured(item: EditableItem) {
|
||||
return (
|
||||
item.deliveryItems.length > 1 ||
|
||||
item.deliveryItems.some((deliveryItem) => Number(deliveryItem.quantity || 1) > 1)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card
|
||||
:data-local-id="item.localId"
|
||||
:class="[
|
||||
'binding-card',
|
||||
{ 'binding-card--invalid': validationState?.localId === item.localId },
|
||||
]"
|
||||
shadow="never"
|
||||
>
|
||||
<template #header>
|
||||
<div class="binding-header">
|
||||
<div class="binding-summary">
|
||||
<div class="binding-title-row">
|
||||
<strong>{{ getCardTitle(item, index) }}</strong>
|
||||
<el-tag :type="isItemComplete(item) ? 'success' : 'warning'" effect="plain">
|
||||
{{ getRuleState(item).label }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span class="binding-subtle">{{ getCardSummary(item) }}</span>
|
||||
</div>
|
||||
<el-button link type="primary" @click="emit('toggleCollapsed')">
|
||||
{{ isCollapsed ? '展开' : '折叠' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions v-if="isCollapsed" :column="4" border class="binding-collapsed-preview">
|
||||
<el-descriptions-item label="外部商品">{{
|
||||
getExternalMatchSummary(item)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="内部 SKU">{{
|
||||
item.internalSkuCode || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="cloud 账号">{{
|
||||
getCloudSourceSummary(item)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="cloud 资源">{{ getCloudSkuSummary(item) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="核销店铺">{{
|
||||
getConsumeShopSummary(item)
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-else class="rule-editor">
|
||||
<div class="rule-editor-main">
|
||||
<section class="rule-panel rule-panel--match">
|
||||
<div class="rule-panel-head">
|
||||
<div class="rule-panel-title">
|
||||
<span class="rule-step">01</span>
|
||||
<strong>商品匹配</strong>
|
||||
</div>
|
||||
<span>订单进来后先命中这里</span>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top" class="mapping-grid mapping-grid--core">
|
||||
<el-form-item label="外部 SKU / productNo" class="field-wide">
|
||||
<el-input
|
||||
v-model="item.externalSkuCode"
|
||||
class="text-input"
|
||||
maxlength="120"
|
||||
placeholder="优先填写 91 卡券 productNo,例如 183999074512936"
|
||||
/>
|
||||
<small class="field-help">最推荐的命中条件。没有特殊情况时,只填这一项即可。</small>
|
||||
</el-form-item>
|
||||
<el-form-item label="内部 SKU">
|
||||
<el-input
|
||||
v-model="item.internalSkuCode"
|
||||
class="text-input"
|
||||
maxlength="80"
|
||||
placeholder="必填,例如 romantic-destiny-pack"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="内部商品名">
|
||||
<el-input
|
||||
v-model="item.internalSkuName"
|
||||
class="text-input"
|
||||
maxlength="120"
|
||||
placeholder="后台识别用,建议填清楚"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="rule-panel rule-panel--delivery">
|
||||
<div class="rule-panel-head">
|
||||
<div class="rule-panel-title">
|
||||
<span class="rule-step">02</span>
|
||||
<strong>履约资源</strong>
|
||||
</div>
|
||||
<span>账号、cloud SKU 和核销店铺</span>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top" class="mapping-grid mapping-grid--core">
|
||||
<el-form-item label="cloud 账号优先级" class="field-wide">
|
||||
<el-select
|
||||
v-model="item.cloudSourceKeys"
|
||||
class="text-input"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="按顺序选择可用于履约的 cloud 账号"
|
||||
@change="emit('handleCloudSourceKeysChanged')"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in cloudtentaclesSourceOptions"
|
||||
:key="opt.key"
|
||||
:label="opt.label || opt.key"
|
||||
:value="opt.key"
|
||||
/>
|
||||
</el-select>
|
||||
<small class="field-help">执行时从左到右尝试,SKU 列表使用第一个账号加载。</small>
|
||||
</el-form-item>
|
||||
<el-form-item label="cloud SKU">
|
||||
<el-select
|
||||
v-model="item.cloudSkuId"
|
||||
class="text-input"
|
||||
placeholder="请选择 cloud SKU"
|
||||
:disabled="isAdvancedDeliveryConfigured(item)"
|
||||
@change="emit('handleCloudSkuSelected', item.cloudSkuId)"
|
||||
@visible-change="emit('handleCloudSkuDropdownVisible', $event)"
|
||||
>
|
||||
<el-option label="请选择 cloud SKU" :value="0" />
|
||||
<el-option
|
||||
v-for="sku in getCloudSkuOptions(item)"
|
||||
:key="sku.id"
|
||||
:label="formatCloudSkuOptionLabel(sku)"
|
||||
:value="sku.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="cloud SKU 名称">
|
||||
<el-input
|
||||
:model-value="item.cloudSkuName"
|
||||
class="text-input"
|
||||
placeholder="选择后自动带出"
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="快手核销店铺" class="field-wide">
|
||||
<el-select
|
||||
v-model="item.kuaishouConsumeShopId"
|
||||
class="text-input"
|
||||
placeholder="请选择已配置 Cookie 的快手小店"
|
||||
@change="emit('handleKuaishouConsumeShopSelected')"
|
||||
>
|
||||
<el-option label="请选择已配置 Cookie 的快手小店" value="" />
|
||||
<el-option
|
||||
v-for="shop in getKuaishouConsumeShopOptions()"
|
||||
:key="shop.shopId"
|
||||
:label="formatKuaishouConsumeShopOption(shop)"
|
||||
:value="shop.shopId"
|
||||
/>
|
||||
</el-select>
|
||||
<small class="field-help">来自“平台店铺 -> 快手小店核销”配置。</small>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="rule-panel rule-panel--strategy">
|
||||
<div class="rule-panel-head">
|
||||
<div class="rule-panel-title">
|
||||
<span class="rule-step">03</span>
|
||||
<strong>执行策略</strong>
|
||||
</div>
|
||||
<span>一般保持默认即可</span>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top" class="strategy-grid">
|
||||
<el-form-item label="优先级">
|
||||
<el-input-number
|
||||
v-model="item.priority"
|
||||
class="text-input"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="最低保留余额">
|
||||
<el-input-number
|
||||
v-model="item.minAssetReserve"
|
||||
class="text-input"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="自动动作" class="field-wide">
|
||||
<div class="strategy-checks">
|
||||
<el-checkbox v-model="item.enabled">启用规则</el-checkbox>
|
||||
<el-checkbox v-model="item.autoBuyEnabled">自动购买</el-checkbox>
|
||||
<el-checkbox v-model="item.autoReturnNumberAfterDispatch">发货后退号</el-checkbox>
|
||||
<el-checkbox v-model="item.autoConsumeAfterDispatch">发货后核销</el-checkbox>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<details class="advanced-fields">
|
||||
<summary>
|
||||
<span>高级履约配置</span>
|
||||
<small>{{ getDeliveryPlanSummary(item) }}</small>
|
||||
</summary>
|
||||
<div class="delivery-items-editor">
|
||||
<div class="delivery-items-head">
|
||||
<span>发货物品明细</span>
|
||||
<el-button size="small" @click="emit('addDeliveryItem')">添加物品</el-button>
|
||||
</div>
|
||||
<div
|
||||
v-for="(deliveryItem, deliveryIndex) in item.deliveryItems"
|
||||
:key="`${item.localId}-${deliveryIndex}`"
|
||||
class="delivery-item-row"
|
||||
>
|
||||
<el-select
|
||||
v-model="deliveryItem.cloudSkuId"
|
||||
class="delivery-item-sku"
|
||||
placeholder="请选择 cloud SKU"
|
||||
@change="emit('handleDeliveryItemCloudSkuSelected', deliveryIndex, deliveryItem.cloudSkuId)"
|
||||
@visible-change="emit('handleCloudSkuDropdownVisible', $event)"
|
||||
>
|
||||
<el-option label="请选择 cloud SKU" :value="0" />
|
||||
<el-option
|
||||
v-for="sku in getCloudSkuOptions(item)"
|
||||
:key="sku.id"
|
||||
:label="formatCloudSkuOptionLabel(sku)"
|
||||
:value="sku.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
:model-value="deliveryItem.cloudSkuName || '-'"
|
||||
class="delivery-item-name"
|
||||
readonly
|
||||
/>
|
||||
<el-input-number
|
||||
v-model="deliveryItem.quantity"
|
||||
class="delivery-item-count"
|
||||
:min="1"
|
||||
:max="999"
|
||||
controls-position="right"
|
||||
/>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="item.deliveryItems.length <= 1"
|
||||
@click="emit('removeDeliveryItem', deliveryIndex)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
<small class="field-help">同一个用户只绑定一次角色,系统会按这里的物品和数量依次发货。</small>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="advanced-fields">
|
||||
<summary>
|
||||
<span>高级匹配与备注</span>
|
||||
<small>多店铺、多平台或人工排查时再展开</small>
|
||||
</summary>
|
||||
<el-form label-position="top" class="mapping-grid advanced-grid">
|
||||
<el-form-item label="来源 provider">
|
||||
<el-input
|
||||
v-model="item.provider"
|
||||
class="text-input"
|
||||
maxlength="32"
|
||||
placeholder="默认 91kaquan"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="来源 platform">
|
||||
<el-input
|
||||
v-model="item.platform"
|
||||
class="text-input"
|
||||
maxlength="32"
|
||||
placeholder="默认 kuaishou"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="店铺 ID">
|
||||
<el-input
|
||||
v-model="item.shopId"
|
||||
class="text-input"
|
||||
maxlength="80"
|
||||
placeholder="留空表示跨店铺共用"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="外部商品 ID">
|
||||
<el-input
|
||||
v-model="item.externalItemId"
|
||||
class="text-input"
|
||||
maxlength="120"
|
||||
placeholder="平台商品 ID,可选"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="外部商品名" class="field-wide">
|
||||
<el-input
|
||||
v-model="item.externalSkuName"
|
||||
class="text-input"
|
||||
maxlength="200"
|
||||
placeholder="只有需要按名称匹配时再填"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="解析商品名">
|
||||
<el-input
|
||||
v-model="item.resolvedSkuName"
|
||||
class="text-input"
|
||||
maxlength="120"
|
||||
placeholder="商品名兜底,可选"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="虚拟号 VN Key">
|
||||
<el-input class="text-input" model-value="1" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="核销店铺 ID">
|
||||
<el-input
|
||||
:model-value="item.kuaishouConsumeShopId || '-'"
|
||||
class="text-input"
|
||||
readonly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="核销店铺名">
|
||||
<el-input
|
||||
:model-value="item.kuaishouConsumeShopName || '-'"
|
||||
class="text-input"
|
||||
readonly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="field-wide">
|
||||
<el-input
|
||||
v-model="item.notes"
|
||||
class="text-input textarea-input"
|
||||
maxlength="400"
|
||||
placeholder="绑定场景、客服注意事项等"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="validationState?.localId === item.localId"
|
||||
:title="validationState?.message"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="cloudSkuCatalogErrorMessage"
|
||||
:title="cloudSkuCatalogErrorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<div v-if="!isCollapsed" class="binding-actions">
|
||||
<el-button link type="danger" @click="emit('remove')">删除</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped src="../AdminKuaishouCloudFulfillment.css"></style>
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminCloudtentaclesSkuItem, AdminKuaishouEticketShopConfigItem } from '@/types/admin'
|
||||
|
||||
import type { EditableItem, RuleFilter, ValidationState } from '../composables/types'
|
||||
|
||||
import AdminKuaishouCloudRuleCard from './AdminKuaishouCloudRuleCard.vue'
|
||||
|
||||
defineProps<{
|
||||
enabled: boolean
|
||||
saving: boolean
|
||||
cloudtentaclesSourceOptions: { key: string; label: string }[]
|
||||
cloudSkuCatalogLoading: boolean
|
||||
validationState: ValidationState
|
||||
items: EditableItem[]
|
||||
filteredItems: EditableItem[]
|
||||
ruleFilter: RuleFilter
|
||||
ruleFilterOptions: Array<{ value: RuleFilter; label: string; count: number }>
|
||||
isCollapsed: (localId: string) => boolean
|
||||
isItemComplete: (item: EditableItem) => boolean
|
||||
getCardTitle: (item: EditableItem, index: number) => string
|
||||
getCardSummary: (item: EditableItem) => string
|
||||
getRuleState: (item: EditableItem) => { label: string; tone: string }
|
||||
getExternalMatchSummary: (item: EditableItem) => string
|
||||
getCloudSourceSummary: (item: EditableItem) => string
|
||||
getCloudSkuSummary: (item: EditableItem) => string
|
||||
getConsumeShopSummary: (item: EditableItem) => string
|
||||
getDeliveryPlanSummary: (item: EditableItem) => string
|
||||
getCloudSkuOptions: (item: EditableItem) => AdminCloudtentaclesSkuItem[]
|
||||
formatCloudSkuOptionLabel: (sku: AdminCloudtentaclesSkuItem) => string
|
||||
getKuaishouConsumeShopOptions: () => AdminKuaishouEticketShopConfigItem[]
|
||||
formatKuaishouConsumeShopOption: (shop: AdminKuaishouEticketShopConfigItem) => string
|
||||
cloudSkuCatalogErrorMessage: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:enabled': [value: boolean]
|
||||
'update:ruleFilter': [value: RuleFilter]
|
||||
expandAll: []
|
||||
collapseReady: []
|
||||
refreshCloudSku: []
|
||||
addItem: []
|
||||
saveConfigs: []
|
||||
toggleCollapsed: [localId: string]
|
||||
handleCloudSkuSelected: [item: EditableItem, value: number | string | undefined]
|
||||
handleDeliveryItemCloudSkuSelected: [item: EditableItem, index: number, value: number | string | undefined]
|
||||
handleCloudSkuDropdownVisible: [item: EditableItem, visible: boolean]
|
||||
handleKuaishouConsumeShopSelected: [item: EditableItem]
|
||||
handleCloudSourceKeysChanged: [item: EditableItem]
|
||||
addDeliveryItem: [item: EditableItem]
|
||||
removeDeliveryItem: [item: EditableItem, index: number]
|
||||
removeItem: [localId: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header card-header--split">
|
||||
<div>
|
||||
<span class="card-title">规则编辑</span>
|
||||
<span class="card-desc"
|
||||
>每一条规则描述“快手外部商品”如何映射到“内部 SKU + cloud 资源”。</span
|
||||
>
|
||||
</div>
|
||||
<el-space wrap>
|
||||
<el-button
|
||||
v-for="option in ruleFilterOptions"
|
||||
:key="option.value"
|
||||
size="small"
|
||||
:type="ruleFilter === option.value ? 'primary' : 'default'"
|
||||
@click="emit('update:ruleFilter', option.value)"
|
||||
>
|
||||
{{ option.label }} {{ option.count }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="rule-action-row">
|
||||
<el-space wrap>
|
||||
<el-button @click="emit('expandAll')">全部展开</el-button>
|
||||
<el-button @click="emit('collapseReady')">收起已完成</el-button>
|
||||
<el-button :loading="cloudSkuCatalogLoading" @click="emit('refreshCloudSku')">
|
||||
刷新 cloud SKU
|
||||
</el-button>
|
||||
<el-button @click="emit('addItem')">新增规则</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="emit('saveConfigs')">
|
||||
保存全部
|
||||
</el-button>
|
||||
</el-space>
|
||||
<div class="rule-enabled-control">
|
||||
<el-switch
|
||||
:model-value="enabled"
|
||||
active-text="启用整条配置"
|
||||
@update:model-value="emit('update:enabled', Boolean($event))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="validationState?.message"
|
||||
:title="validationState.message"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
<el-empty
|
||||
v-if="items.length === 0"
|
||||
description="当前还没有新履约规则,先新增一条。"
|
||||
:image-size="60"
|
||||
/>
|
||||
<el-empty
|
||||
v-else-if="filteredItems.length === 0"
|
||||
description="当前筛选下没有规则。"
|
||||
:image-size="60"
|
||||
/>
|
||||
|
||||
<AdminKuaishouCloudRuleCard
|
||||
v-for="(item, index) in filteredItems"
|
||||
:key="item.localId"
|
||||
:item="item"
|
||||
:index="index"
|
||||
:validation-state="validationState"
|
||||
:cloudtentacles-source-options="cloudtentaclesSourceOptions"
|
||||
:cloud-sku-catalog-error-message="cloudSkuCatalogErrorMessage"
|
||||
:cloud-sku-catalog-loading="cloudSkuCatalogLoading"
|
||||
:is-collapsed="isCollapsed(item.localId)"
|
||||
:is-item-complete="isItemComplete"
|
||||
:get-card-title="getCardTitle"
|
||||
:get-card-summary="getCardSummary"
|
||||
:get-rule-state="getRuleState"
|
||||
:get-external-match-summary="getExternalMatchSummary"
|
||||
:get-cloud-source-summary="getCloudSourceSummary"
|
||||
:get-cloud-sku-summary="getCloudSkuSummary"
|
||||
:get-consume-shop-summary="getConsumeShopSummary"
|
||||
:get-delivery-plan-summary="getDeliveryPlanSummary"
|
||||
:get-cloud-sku-options="getCloudSkuOptions"
|
||||
:format-cloud-sku-option-label="formatCloudSkuOptionLabel"
|
||||
:get-kuaishou-consume-shop-options="getKuaishouConsumeShopOptions"
|
||||
:format-kuaishou-consume-shop-option="formatKuaishouConsumeShopOption"
|
||||
@toggle-collapsed="emit('toggleCollapsed', item.localId)"
|
||||
@handle-cloud-sku-selected="emit('handleCloudSkuSelected', item, $event)"
|
||||
@handle-delivery-item-cloud-sku-selected="
|
||||
(index, value) => emit('handleDeliveryItemCloudSkuSelected', item, index, value)
|
||||
"
|
||||
@handle-cloud-sku-dropdown-visible="emit('handleCloudSkuDropdownVisible', item, $event)"
|
||||
@handle-kuaishou-consume-shop-selected="emit('handleKuaishouConsumeShopSelected', item)"
|
||||
@handle-cloud-source-keys-changed="emit('handleCloudSourceKeysChanged', item)"
|
||||
@add-delivery-item="emit('addDeliveryItem', item)"
|
||||
@remove-delivery-item="emit('removeDeliveryItem', item, $event)"
|
||||
@remove="emit('removeItem', item.localId)"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped src="../AdminKuaishouCloudFulfillment.css"></style>
|
||||
@@ -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'
|
||||
-735
@@ -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<ValidationState>(null)
|
||||
const filePath = ref('')
|
||||
const enabled = ref(true)
|
||||
const items = ref<EditableItem[]>([])
|
||||
const collapsedIds = ref<string[]>([])
|
||||
const ruleFilter = ref<RuleFilter>('all')
|
||||
const kuaishouConsumeShops = ref<AdminKuaishouEticketShopConfigItem[]>([])
|
||||
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<Array<{ value: RuleFilter; label: string; count: number }>>(
|
||||
() => [
|
||||
{ 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<EditableItem, 'deliveryItems' | 'cloudSkuId' | 'cloudSkuName'> | 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<AdminKuaishouCloudDeliveryItem>) : {}
|
||||
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<number, AdminKuaishouCloudDeliveryItem>()
|
||||
|
||||
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<EditableItem, 'externalSkuCode' | 'externalItemId' | 'externalSkuName'>,
|
||||
) {
|
||||
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<HTMLElement>(`[data-local-id="${localId}"]`)
|
||||
if (!card) {
|
||||
return
|
||||
}
|
||||
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
|
||||
// ── save ──────────────────────────────────────────────
|
||||
|
||||
async function saveConfigs() {
|
||||
const invalid = items.value.reduce<ValidationState>(
|
||||
(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,
|
||||
}
|
||||
}
|
||||
-166
@@ -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<typeof useKuaishouCloudConfig>,
|
||||
) {
|
||||
const ninetyoneLookupLoading = ref(false)
|
||||
const ninetyoneLookupErrorMessage = ref('')
|
||||
const ninetyoneLookupResults = ref<AdminNinetyoneOrderItem[]>([])
|
||||
const importedNinetyoneOrderKeys = ref<string[]>([])
|
||||
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,
|
||||
}
|
||||
}
|
||||
-205
@@ -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<Record<string, boolean>>({})
|
||||
const cloudSkuCatalogErrorMessage = ref('')
|
||||
const cloudSkuCatalogBySourceKey = ref<Record<string, AdminCloudtentaclesSkuItem[]>>({})
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user