支持cloudtentacles覆盖规则

This commit is contained in:
yml
2026-05-27 13:25:15 +08:00
parent c7e2a225bc
commit c1f3a48634
18 changed files with 997 additions and 31 deletions
@@ -7,6 +7,7 @@ import {
deleteAdminCloudtentaclesSource, deleteAdminCloudtentaclesSource,
fetchAdminCloudtentaclesVirtualNumberCode, fetchAdminCloudtentaclesVirtualNumberCode,
generateAdminCloudtentaclesLoginCode, generateAdminCloudtentaclesLoginCode,
getAdminCloudtentaclesOverrideRules,
getAdminCloudtentaclesAsset, getAdminCloudtentaclesAsset,
getAdminCloudtentaclesBindUrl, getAdminCloudtentaclesBindUrl,
getAdminCloudtentaclesCategories, getAdminCloudtentaclesCategories,
@@ -17,6 +18,7 @@ import {
runAdminCloudtentaclesFullFlow, runAdminCloudtentaclesFullFlow,
sendAdminCloudtentaclesSmsCode, sendAdminCloudtentaclesSmsCode,
testAdminCloudtentaclesLogin, testAdminCloudtentaclesLogin,
updateAdminCloudtentaclesOverrideRules,
updateAdminCloudtentaclesSourceConfig, updateAdminCloudtentaclesSourceConfig,
useAdminCloudtentaclesSku, useAdminCloudtentaclesSku,
validateAdminCloudtentaclesSession, validateAdminCloudtentaclesSession,
@@ -25,6 +27,7 @@ import {
import type { import type {
AdminCloudtentaclesCatalogQueryRouteBody, AdminCloudtentaclesCatalogQueryRouteBody,
AdminCloudtentaclesFullFlowRouteBody, AdminCloudtentaclesFullFlowRouteBody,
AdminCloudtentaclesOverrideRuleConfigRouteBody,
AdminCloudtentaclesSendSmsCodeRouteBody, AdminCloudtentaclesSendSmsCodeRouteBody,
AdminCloudtentaclesSkuBuyRouteBody, AdminCloudtentaclesSkuBuyRouteBody,
AdminCloudtentaclesSkuUseRouteBody, AdminCloudtentaclesSkuUseRouteBody,
@@ -96,6 +99,43 @@ router.post(
) )
); );
router.get(
"/cloudtentacles/override-rules",
createJsonHandler(() => getAdminCloudtentaclesOverrideRules(), {
successMessage: "ok",
errorMessage: "读取 cloudtentacles 覆盖规则失败",
scope: "[admin/platform-config/cloudtentacles/override-rules]",
})
);
router.post(
"/cloudtentacles/override-rules",
createJsonHandler(
(req) =>
updateAdminCloudtentaclesOverrideRules(
req.body as AdminCloudtentaclesOverrideRuleConfigRouteBody
),
{
successMessage: "cloudtentacles 覆盖规则已保存",
errorMessage: "保存 cloudtentacles 覆盖规则失败",
scope: "[admin/platform-config/cloudtentacles/override-rules]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_cloudtentacles_override_rules_updated",
targetType: "platform_config",
targetId: "cloudtentacles_override_rules",
data: {
filePath: String(result.filePath || "").trim(),
enabled: result.enabled !== false,
ruleCount: Array.isArray(result.rules) ? result.rules.length : 0,
},
};
},
}
)
);
router.post( router.post(
"/cloudtentacles/send-sms-code", "/cloudtentacles/send-sms-code",
createJsonHandler( createJsonHandler(
@@ -39,6 +39,11 @@ import {
verifyCloudtentaclesLoginCode, verifyCloudtentaclesLoginCode,
} from "../../../platforms/cloudtentacles/virtual-number-service.js"; } from "../../../platforms/cloudtentacles/virtual-number-service.js";
import { runCloudtentaclesFullDebugFlow } from "../../../platforms/cloudtentacles/debug-flow-service.js"; import { runCloudtentaclesFullDebugFlow } from "../../../platforms/cloudtentacles/debug-flow-service.js";
import {
getCloudtentaclesOverrideRuleConfig,
getCloudtentaclesOverrideRuleFilePath,
saveCloudtentaclesOverrideRuleConfig,
} from "../../../order/cloudtentacles-override-rule-service.js";
import { import {
hasCloudtentaclesCredentialContextChanged, hasCloudtentaclesCredentialContextChanged,
pickFirstNonEmpty, pickFirstNonEmpty,
@@ -62,6 +67,7 @@ import type {
AdminCloudtentaclesCatalogQueryInput, AdminCloudtentaclesCatalogQueryInput,
AdminCloudtentaclesDeliveryRecordQueryInput, AdminCloudtentaclesDeliveryRecordQueryInput,
AdminCloudtentaclesFullFlowInput, AdminCloudtentaclesFullFlowInput,
AdminCloudtentaclesOverrideRuleConfigInput,
AdminCloudtentaclesSendSmsCodeInput, AdminCloudtentaclesSendSmsCodeInput,
AdminCloudtentaclesSkuBuyInput, AdminCloudtentaclesSkuBuyInput,
AdminCloudtentaclesSkuUseInput, AdminCloudtentaclesSkuUseInput,
@@ -115,6 +121,22 @@ export function listAdminCloudtentaclesRecordSources() {
}; };
} }
export function getAdminCloudtentaclesOverrideRules() {
return {
filePath: getCloudtentaclesOverrideRuleFilePath(),
...getCloudtentaclesOverrideRuleConfig(),
};
}
export function updateAdminCloudtentaclesOverrideRules(
payload: AdminCloudtentaclesOverrideRuleConfigInput = {}
) {
return {
filePath: getCloudtentaclesOverrideRuleFilePath(),
...saveCloudtentaclesOverrideRuleConfig(payload),
};
}
export function updateAdminCloudtentaclesSourceConfig( export function updateAdminCloudtentaclesSourceConfig(
payload: AdminCloudtentaclesSourceConfigInput = {} payload: AdminCloudtentaclesSourceConfigInput = {}
) { ) {
@@ -0,0 +1,9 @@
export function normalizeCloudtentaclesMatchName(value: unknown) {
return String(value || '')
.toLowerCase()
.replace(/[【】\[\]()()]/g, ' ')
.replace(/(自动发货|秒发|极速发货|官方直充|官方充值)/gi, ' ')
.replace(/[^\p{L}\p{N}]+/gu, ' ')
.replace(/\s+/g, ' ')
.trim()
}
@@ -57,3 +57,91 @@ test('resolveCloudtentaclesSkuByProductName matches SKU name with logged-in sour
assert.deepEqual(result?.cloudSourceKeys, ['account-a']) assert.deepEqual(result?.cloudSourceKeys, ['account-a'])
assert.equal(result?.matchMode, 'cloudtentacles_name') assert.equal(result?.matchMode, 'cloudtentacles_name')
}) })
test('resolveCloudtentaclesSkuByProductName prefers override rule with multiple delivery items', async () => {
const result = await resolveCloudtentaclesSkuByProductName('套装1', {
listCloudtentaclesSources: () => ({
enabled: true,
sources: [
{
key: 'account-a',
label: '账号 A',
enabled: true,
baseUrl: 'https://cloud.example.com',
deviceId: '-',
deviceType: 0,
},
],
}),
getCloudtentaclesSessionStateByKey: () => ({
token: 'token-a',
baseUrl: 'https://cloud.example.com',
username: 'user-a',
phone: '',
loggedInAt: '2026-05-27T12:00:00.000Z',
deviceId: '-',
deviceType: 0,
}),
listCloudtentaclesSku: async () => ({
baseUrl: 'https://cloud.example.com',
itemCount: 3,
rawItems: [],
items: [
{
id: 28,
name: '商品A',
inventory: 10,
price: 360,
},
{
id: 29,
name: '商品B',
inventory: 20,
price: 888,
},
{
id: 30,
name: '套装1',
inventory: 1,
price: 999,
},
],
}),
resolveCloudtentaclesOverrideRule: () => ({
id: 'combo-1',
enabled: true,
productName: '套装1',
normalizedProductName: normalizeCloudtentaclesMatchName('套装1'),
sourceKey: '',
deliveryItems: [
{
cloudSkuId: 28,
cloudSkuName: '商品A',
quantity: 1,
},
{
cloudSkuId: 0,
cloudSkuName: '商品B',
quantity: 20,
},
],
notes: '',
}),
})
assert.equal(result?.matchMode, 'cloudtentacles_override')
assert.equal(result?.cloudSkuId, 28)
assert.equal(result?.cloudSkuName, '商品A')
assert.deepEqual(result?.deliveryItems, [
{
cloudSkuId: 28,
cloudSkuName: '商品A',
quantity: 1,
},
{
cloudSkuId: 29,
cloudSkuName: '商品B',
quantity: 20,
},
])
})
@@ -7,6 +7,11 @@ import {
import { import {
listCloudtentaclesSku, listCloudtentaclesSku,
} from '../platforms/cloudtentacles/catalog-service.js' } from '../platforms/cloudtentacles/catalog-service.js'
import {
resolveCloudtentaclesOverrideRule,
type CloudtentaclesOverrideRule,
} from './cloudtentacles-override-rule-service.js'
import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js'
type JsonObject = Record<string, any> type JsonObject = Record<string, any>
@@ -38,10 +43,17 @@ type CloudtentaclesNameMatchDeps = {
listCloudtentaclesSources?: typeof listCloudtentaclesSources listCloudtentaclesSources?: typeof listCloudtentaclesSources
getCloudtentaclesSessionStateByKey?: typeof getCloudtentaclesSessionStateByKey getCloudtentaclesSessionStateByKey?: typeof getCloudtentaclesSessionStateByKey
listCloudtentaclesSku?: typeof listCloudtentaclesSku listCloudtentaclesSku?: typeof listCloudtentaclesSku
resolveCloudtentaclesOverrideRule?: typeof resolveCloudtentaclesOverrideRule
}
export type CloudtentaclesResolvedDeliveryItem = {
cloudSkuId: number
cloudSkuName: string
quantity: number
} }
export type CloudtentaclesNameMatchResult = { export type CloudtentaclesNameMatchResult = {
matchMode: 'cloudtentacles_name' matchMode: 'cloudtentacles_name' | 'cloudtentacles_override'
productName: string productName: string
normalizedProductName: string normalizedProductName: string
cloudSkuId: number cloudSkuId: number
@@ -50,6 +62,7 @@ export type CloudtentaclesNameMatchResult = {
cloudSkuInventory: number cloudSkuInventory: number
cloudSourceKeys: string[] cloudSourceKeys: string[]
resolvedSourceKey: string resolvedSourceKey: string
deliveryItems: CloudtentaclesResolvedDeliveryItem[]
skuSnapshot: JsonObject skuSnapshot: JsonObject
} }
@@ -65,6 +78,7 @@ export async function resolveCloudtentaclesSkuByProductName(
const listSources = deps.listCloudtentaclesSources || listCloudtentaclesSources const listSources = deps.listCloudtentaclesSources || listCloudtentaclesSources
const getSessionByKey = deps.getCloudtentaclesSessionStateByKey || getCloudtentaclesSessionStateByKey const getSessionByKey = deps.getCloudtentaclesSessionStateByKey || getCloudtentaclesSessionStateByKey
const listSku = deps.listCloudtentaclesSku || listCloudtentaclesSku const listSku = deps.listCloudtentaclesSku || listCloudtentaclesSku
const resolveOverrideRule = deps.resolveCloudtentaclesOverrideRule || resolveCloudtentaclesOverrideRule
const sourcesConfig = listSources() const sourcesConfig = listSources()
if (sourcesConfig.enabled === false) { if (sourcesConfig.enabled === false) {
@@ -79,6 +93,14 @@ export async function resolveCloudtentaclesSkuByProductName(
return null return null
} }
const overrideRule = resolveOverrideRule(productName)
const overrideMatch = overrideRule
? await resolveOverrideMatch(productName, normalizedProductName, overrideRule, sourceContexts, listSku)
: null
if (overrideMatch) {
return overrideMatch
}
const cloudSourceKeys = sourceContexts.map((context) => context.sourceKey) const cloudSourceKeys = sourceContexts.map((context) => context.sourceKey)
for (const context of sourceContexts) { for (const context of sourceContexts) {
@@ -109,6 +131,13 @@ export async function resolveCloudtentaclesSkuByProductName(
cloudSkuInventory: Number(matchedSku.inventory || 0) || 0, cloudSkuInventory: Number(matchedSku.inventory || 0) || 0,
cloudSourceKeys, cloudSourceKeys,
resolvedSourceKey: context.sourceKey, resolvedSourceKey: context.sourceKey,
deliveryItems: [
{
cloudSkuId,
cloudSkuName,
quantity: 1,
},
],
skuSnapshot: { ...matchedSku }, skuSnapshot: { ...matchedSku },
} }
} catch { } catch {
@@ -119,16 +148,6 @@ export async function resolveCloudtentaclesSkuByProductName(
return null return null
} }
export function normalizeCloudtentaclesMatchName(value: unknown) {
return String(value || '')
.toLowerCase()
.replace(/[【】\[\]()()]/g, ' ')
.replace(/(自动发货|秒发|极速发货|官方直充|官方充值)/gi, ' ')
.replace(/[^\p{L}\p{N}]+/gu, ' ')
.replace(/\s+/g, ' ')
.trim()
}
function buildCloudtentaclesSourceContext( function buildCloudtentaclesSourceContext(
source: CloudtentaclesSource, source: CloudtentaclesSource,
getSessionByKey: typeof getCloudtentaclesSessionStateByKey, getSessionByKey: typeof getCloudtentaclesSessionStateByKey,
@@ -163,3 +182,103 @@ function findCloudtentaclesSkuByName(items: CloudtentaclesSku[], productName: un
null null
) )
} }
async function resolveOverrideMatch(
productName: unknown,
normalizedProductName: string,
rule: CloudtentaclesOverrideRule,
sourceContexts: Array<NonNullable<ReturnType<typeof buildCloudtentaclesSourceContext>>>,
listSku: typeof listCloudtentaclesSku,
): Promise<CloudtentaclesNameMatchResult | null> {
const candidates = rule.sourceKey
? sourceContexts.filter((context) => context.sourceKey === rule.sourceKey)
: sourceContexts
if (candidates.length === 0) {
return null
}
for (const context of candidates) {
try {
const skuList = await listSku(context)
const items = Array.isArray(skuList.items) ? skuList.items : []
const deliveryItems = resolveOverrideDeliveryItems(rule, items)
if (deliveryItems.length !== rule.deliveryItems.length || !deliveryItems[0]) {
continue
}
const firstDeliveryItem = deliveryItems[0]
const firstSku = items.find((item) => Number(item.id || 0) === firstDeliveryItem.cloudSkuId) || {}
const cloudSourceKeys = rule.sourceKey
? [context.sourceKey]
: sourceContexts.map((sourceContext) => sourceContext.sourceKey)
return {
matchMode: 'cloudtentacles_override',
productName: String(productName || '').trim(),
normalizedProductName,
cloudSkuId: firstDeliveryItem.cloudSkuId,
cloudSkuName: firstDeliveryItem.cloudSkuName,
cloudSkuPrice: Number(firstSku.price || 0) || 0,
cloudSkuInventory: Number(firstSku.inventory || 0) || 0,
cloudSourceKeys,
resolvedSourceKey: context.sourceKey,
deliveryItems,
skuSnapshot: {
overrideRuleId: rule.id,
overrideRuleProductName: rule.productName,
deliveryItems,
firstSku,
},
}
} catch {
// 当前账号不可用时继续尝试下一个已登录账号。
}
}
return null
}
function resolveOverrideDeliveryItems(rule: CloudtentaclesOverrideRule, skuItems: CloudtentaclesSku[]) {
const resolvedItems: CloudtentaclesResolvedDeliveryItem[] = []
for (const ruleItem of rule.deliveryItems) {
const matchedSku = ruleItem.cloudSkuId > 0
? skuItems.find((item) => Number(item.id || 0) === ruleItem.cloudSkuId)
: findCloudtentaclesSkuByName(skuItems, ruleItem.cloudSkuName)
const cloudSkuId = Number(matchedSku?.id || 0)
const cloudSkuName = String(matchedSku?.name || '').trim()
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0 || !cloudSkuName) {
return []
}
resolvedItems.push({
cloudSkuId,
cloudSkuName,
quantity: Math.max(1, Number(ruleItem.quantity || 1)),
})
}
return mergeResolvedDeliveryItems(resolvedItems)
}
function mergeResolvedDeliveryItems(items: CloudtentaclesResolvedDeliveryItem[]) {
const merged = new Map<number, CloudtentaclesResolvedDeliveryItem>()
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())
}
export { normalizeCloudtentaclesMatchName }
@@ -0,0 +1,70 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
normalizeCloudtentaclesOverrideRuleConfig,
} from './cloudtentacles-override-rule-service.js'
test('normalizeCloudtentaclesOverrideRuleConfig merges duplicated delivery items', () => {
const config = normalizeCloudtentaclesOverrideRuleConfig({
enabled: true,
rules: [
{
productName: '套装1',
deliveryItems: [
{
cloudSkuId: 28,
cloudSkuName: '商品A',
quantity: 1,
},
{
cloudSkuId: 28,
cloudSkuName: '商品A',
quantity: 2,
},
{
cloudSkuName: '商品B',
quantity: 20,
},
],
},
],
})
assert.equal(config.enabled, true)
assert.equal(config.rules.length, 1)
assert.deepEqual(config.rules[0]?.deliveryItems, [
{
cloudSkuId: 28,
cloudSkuName: '商品A',
quantity: 3,
},
{
cloudSkuId: 0,
cloudSkuName: '商品B',
quantity: 20,
},
])
})
test('normalizeCloudtentaclesOverrideRuleConfig drops invalid rules', () => {
const config = normalizeCloudtentaclesOverrideRuleConfig({
rules: [
{
productName: '',
deliveryItems: [
{
cloudSkuId: 28,
quantity: 1,
},
],
},
{
productName: '有效商品',
deliveryItems: [],
},
],
})
assert.deepEqual(config.rules, [])
})
@@ -0,0 +1,173 @@
import path from 'node:path'
import { PROJECT_ROOT } from '../../config/runtime.js'
import { readJsonFile, writeJsonFile } from '../../utils/json-file-store.js'
import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js'
const CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH = path.join(
PROJECT_ROOT,
'data',
'cloudtentacles-override-rules.json',
)
type JsonObject = Record<string, any>
export type CloudtentaclesOverrideDeliveryItem = {
cloudSkuId: number
cloudSkuName: string
quantity: number
}
export type CloudtentaclesOverrideRule = {
id: string
enabled: boolean
productName: string
normalizedProductName: string
sourceKey: string
deliveryItems: CloudtentaclesOverrideDeliveryItem[]
notes: string
}
export type CloudtentaclesOverrideRuleConfig = {
enabled: boolean
rules: CloudtentaclesOverrideRule[]
}
export function getCloudtentaclesOverrideRuleFilePath() {
return CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH
}
export function getCloudtentaclesOverrideRuleConfig(): CloudtentaclesOverrideRuleConfig {
return readJsonFile(
CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH,
createDefaultCloudtentaclesOverrideRuleConfig,
normalizeCloudtentaclesOverrideRuleConfig,
)
}
export function saveCloudtentaclesOverrideRuleConfig(rawValue: unknown): CloudtentaclesOverrideRuleConfig {
return writeJsonFile(
CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH,
rawValue,
normalizeCloudtentaclesOverrideRuleConfig,
)
}
export function resolveCloudtentaclesOverrideRule(productName: unknown) {
const config = getCloudtentaclesOverrideRuleConfig()
if (config.enabled === false) {
return null
}
const normalizedProductName = normalizeCloudtentaclesMatchName(productName)
if (!normalizedProductName) {
return null
}
return config.rules.find((rule) =>
rule.enabled !== false &&
rule.normalizedProductName === normalizedProductName &&
rule.deliveryItems.length > 0
) || null
}
export function normalizeCloudtentaclesOverrideRuleConfig(rawValue: unknown): CloudtentaclesOverrideRuleConfig {
const source = isPlainObject(rawValue) ? rawValue : {}
const rules = Array.isArray(source.rules) ? source.rules : []
return {
enabled: source.enabled !== false,
rules: rules
.map((item: unknown) => normalizeCloudtentaclesOverrideRule(item))
.filter((item): item is CloudtentaclesOverrideRule => Boolean(item)),
}
}
function normalizeCloudtentaclesOverrideRule(rawValue: unknown): CloudtentaclesOverrideRule | null {
if (!isPlainObject(rawValue)) {
return null
}
const productName = String(rawValue.productName || rawValue.name || '').trim()
const normalizedProductName = normalizeCloudtentaclesMatchName(productName)
const deliveryItems = normalizeDeliveryItems(rawValue.deliveryItems)
if (!productName || !normalizedProductName || deliveryItems.length === 0) {
return null
}
return {
id: String(rawValue.id || normalizedProductName).trim() || normalizedProductName,
enabled: rawValue.enabled !== false,
productName,
normalizedProductName,
sourceKey: String(rawValue.sourceKey || '').trim(),
deliveryItems,
notes: String(rawValue.notes || '').trim(),
}
}
function normalizeDeliveryItems(value: unknown) {
const rawItems = Array.isArray(value) ? value : []
const items = rawItems
.map((item) => normalizeDeliveryItem(item))
.filter((item): item is CloudtentaclesOverrideDeliveryItem => Boolean(item))
return mergeDeliveryItems(items)
}
function normalizeDeliveryItem(value: unknown): CloudtentaclesOverrideDeliveryItem | null {
const source = isPlainObject(value) ? value : {}
const cloudSkuId = normalizePositiveInteger(source.cloudSkuId || source.skuId, 0)
const cloudSkuName = String(source.cloudSkuName || source.skuName || '').trim()
const quantity = normalizePositiveInteger(source.quantity, 1)
if (!cloudSkuId && !cloudSkuName) {
return null
}
return {
cloudSkuId,
cloudSkuName,
quantity,
}
}
function mergeDeliveryItems(items: CloudtentaclesOverrideDeliveryItem[]) {
const merged = new Map<string, CloudtentaclesOverrideDeliveryItem>()
for (const item of items) {
const key = item.cloudSkuId > 0 ? `id:${item.cloudSkuId}` : `name:${normalizeCloudtentaclesMatchName(item.cloudSkuName)}`
const existing = merged.get(key)
if (existing) {
existing.quantity += item.quantity
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
existing.cloudSkuId = existing.cloudSkuId || item.cloudSkuId
continue
}
merged.set(key, { ...item })
}
return Array.from(merged.values())
}
function normalizePositiveInteger(value: unknown, fallback: number) {
const parsed = Number(value)
if (!Number.isInteger(parsed) || parsed <= 0) {
return fallback
}
return parsed
}
function createDefaultCloudtentaclesOverrideRuleConfig(): CloudtentaclesOverrideRuleConfig {
return {
enabled: true,
rules: [],
}
}
function isPlainObject(value: unknown): value is JsonObject {
return Object.prototype.toString.call(value) === '[object Object]'
}
@@ -194,3 +194,92 @@ test('syncDeliveryTasksForOrderWithDeps creates kuaishou cloud task from cloudte
assert.equal(result[0]?.claim_token, 'claim-token') assert.equal(result[0]?.claim_token, 'claim-token')
assert.equal(updates[0]?.patch.claim_token, 'claim-token') assert.equal(updates[0]?.patch.claim_token, 'claim-token')
}) })
test('syncDeliveryTasksForOrderWithDeps keeps override delivery items and quantities', async () => {
const createdTasks = []
const dynamicOrderItems = [
{
id: 22,
order_id: 10,
sku_code: '套装1',
sku_name: '套装1',
quantity: 1,
spec_json: '{}',
item_snapshot_json: JSON.stringify({
matchMode: 'cloudtentacles_override',
cloudtentacles: {
matchMode: 'cloudtentacles_override',
normalizedProductName: '套装1',
cloudSkuId: 28,
cloudSkuName: '商品A',
cloudSourceKeys: ['account-a'],
resolvedSourceKey: 'account-a',
deliveryItems: [
{
cloudSkuId: 28,
cloudSkuName: '商品A',
quantity: 1,
},
{
cloudSkuId: 29,
cloudSkuName: '商品B',
quantity: 20,
},
],
},
}),
},
]
await syncDeliveryTasksForOrderWithDeps(paidOrder, dynamicOrderItems, {
listTasksByOrderId: async () => [],
getFulfillmentProfileByKey: async () => ({
id: 2,
profile_key: 'kuaishou_ct_assisted',
name: '快手 cloud 履约',
executor_key: 'kuaishou_ct_assisted',
requires_claim: false,
auto_dispatch: false,
config_json: '{}',
}),
createTask: async (input) => {
createdTasks.push(input)
return {
id: 32,
order_id: input.orderId,
order_item_id: input.orderItemId,
task_status: input.taskStatus,
executor_key: input.executorKey,
requires_claim: input.requiresClaim,
claim_token: '',
primary_claim_token_id: null,
last_error: '',
context_json: input.contextJson,
}
},
createTaskClaimToken: async () => ({
token: 'claim-token',
expired_at: '2026-04-15T12:00:00.000Z',
}),
updateTask: async (taskId, patch) => ({ id: taskId, ...patch }),
nowIso: () => '2026-04-14T12:04:00.000Z',
randomId: () => 'DT-OVERRIDE',
})
const context = JSON.parse(createdTasks[0].contextJson)
assert.equal(context.kuaishouCloudFulfillment.binding.skuId, 28)
assert.equal(context.kuaishouCloudFulfillment.binding.resolvedSourceKey, 'account-a')
assert.deepEqual(context.kuaishouCloudFulfillment.deliveryItems, [
{
cloudSkuId: 28,
cloudSkuName: '商品A',
quantity: 1,
},
{
cloudSkuId: 29,
cloudSkuName: '商品B',
quantity: 20,
},
])
})
@@ -180,6 +180,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
binding: { binding: {
prepareStatus: 'pending', prepareStatus: 'pending',
cloudSourceKeys: normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys), cloudSourceKeys: normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys),
resolvedSourceKey: String(cloudtentaclesConfig.resolvedSourceKey || '').trim(),
skuId: primaryDeliveryItem.cloudSkuId, skuId: primaryDeliveryItem.cloudSkuId,
skuName: primaryDeliveryItem.cloudSkuName, skuName: primaryDeliveryItem.cloudSkuName,
vnKey: '1', vnKey: '1',
@@ -384,11 +385,19 @@ async function resolveDynamicCloudtentaclesProfile(
): Promise<FulfillmentBindingLike | null> { ): Promise<FulfillmentBindingLike | null> {
const snapshot = parseJsonObject(item.item_snapshot_json) const snapshot = parseJsonObject(item.item_snapshot_json)
const cloudtentacles = parseJsonObject(snapshot.cloudtentacles) const cloudtentacles = parseJsonObject(snapshot.cloudtentacles)
const cloudSkuId = Number(cloudtentacles.cloudSkuId || 0) || 0
const cloudSkuName = String(cloudtentacles.cloudSkuName || item.sku_name || '').trim()
const cloudSourceKeys = normalizeStringArray(cloudtentacles.cloudSourceKeys) const cloudSourceKeys = normalizeStringArray(cloudtentacles.cloudSourceKeys)
const deliveryItems = normalizeCloudDeliveryItems({
deliveryItems: cloudtentacles.deliveryItems,
skuId: cloudtentacles.cloudSkuId,
skuName: cloudtentacles.cloudSkuName || item.sku_name,
})
const primaryDeliveryItem = deliveryItems[0] || {
cloudSkuId: 0,
cloudSkuName: '',
quantity: 1,
}
if (!cloudSkuId || !cloudSkuName || cloudSourceKeys.length === 0) { if (!primaryDeliveryItem.cloudSkuId || !primaryDeliveryItem.cloudSkuName || cloudSourceKeys.length === 0) {
return null return null
} }
@@ -407,18 +416,13 @@ async function resolveDynamicCloudtentaclesProfile(
auto_dispatch: false, auto_dispatch: false,
config_json: { config_json: {
flowType: 'kuaishou_cloud_fulfillment', flowType: 'kuaishou_cloud_fulfillment',
configId: `cloudtentacles_name:${cloudSkuId}`, configId: `${String(cloudtentacles.matchMode || 'cloudtentacles_name').trim()}:${String(cloudtentacles.normalizedProductName || primaryDeliveryItem.cloudSkuId).trim()}`,
cloudtentacles: { cloudtentacles: {
cloudSourceKeys, cloudSourceKeys,
skuId: cloudSkuId, skuId: primaryDeliveryItem.cloudSkuId,
skuName: cloudSkuName, skuName: primaryDeliveryItem.cloudSkuName,
deliveryItems: [ resolvedSourceKey: String(cloudtentacles.resolvedSourceKey || '').trim(),
{ deliveryItems,
cloudSkuId,
cloudSkuName,
quantity: 1,
},
],
vnKey: '1', vnKey: '1',
autoBuyEnabled: true, autoBuyEnabled: true,
minAssetReserve: 0, minAssetReserve: 0,
@@ -429,7 +433,9 @@ async function resolveDynamicCloudtentaclesProfile(
shopName: '', shopName: '',
autoConsumeAfterDispatch: false, autoConsumeAfterDispatch: false,
}, },
notes: '91卡券商品名自动匹配 cloudtentacles 商品', notes: cloudtentacles.matchMode === 'cloudtentacles_override'
? '91卡券商品名命中 cloudtentacles 覆盖规则'
: '91卡券商品名自动匹配 cloudtentacles 商品',
}, },
} }
} }
@@ -96,6 +96,7 @@ export async function resolveOrderItemForFulfillment({
cloudSkuInventory: cloudtentaclesNameMatch.cloudSkuInventory, cloudSkuInventory: cloudtentaclesNameMatch.cloudSkuInventory,
cloudSourceKeys: cloudtentaclesNameMatch.cloudSourceKeys, cloudSourceKeys: cloudtentaclesNameMatch.cloudSourceKeys,
resolvedSourceKey: cloudtentaclesNameMatch.resolvedSourceKey, resolvedSourceKey: cloudtentaclesNameMatch.resolvedSourceKey,
deliveryItems: cloudtentaclesNameMatch.deliveryItems,
skuSnapshot: cloudtentaclesNameMatch.skuSnapshot, skuSnapshot: cloudtentaclesNameMatch.skuSnapshot,
} }
: null, : null,
@@ -7,6 +7,7 @@ import type {
AdminCloudtentaclesCatalogQueryInput, AdminCloudtentaclesCatalogQueryInput,
AdminCloudtentaclesDeliveryRecordQueryInput, AdminCloudtentaclesDeliveryRecordQueryInput,
AdminCloudtentaclesFullFlowInput, AdminCloudtentaclesFullFlowInput,
AdminCloudtentaclesOverrideRuleConfigInput,
AdminCloudtentaclesSendSmsCodeInput, AdminCloudtentaclesSendSmsCodeInput,
AdminCloudtentaclesSkuBuyInput, AdminCloudtentaclesSkuBuyInput,
AdminCloudtentaclesSkuUseInput, AdminCloudtentaclesSkuUseInput,
@@ -37,6 +38,7 @@ export type AdminKuaishouEticketDetailQueryRouteBody = AdminKuaishouEticketDetai
export type AdminKuaishouEticketConsumeRouteBody = AdminKuaishouEticketConsumeInput export type AdminKuaishouEticketConsumeRouteBody = AdminKuaishouEticketConsumeInput
export type AdminKuaishouEticketShopInfoRouteBody = AdminKuaishouEticketShopInfoInput export type AdminKuaishouEticketShopInfoRouteBody = AdminKuaishouEticketShopInfoInput
export type AdminCloudtentaclesSourceConfigRouteBody = AdminCloudtentaclesSourceConfigInput export type AdminCloudtentaclesSourceConfigRouteBody = AdminCloudtentaclesSourceConfigInput
export type AdminCloudtentaclesOverrideRuleConfigRouteBody = AdminCloudtentaclesOverrideRuleConfigInput
export type AdminCloudtentaclesSendSmsCodeRouteBody = AdminCloudtentaclesSendSmsCodeInput export type AdminCloudtentaclesSendSmsCodeRouteBody = AdminCloudtentaclesSendSmsCodeInput
export type AdminCloudtentaclesTestLoginRouteBody = AdminCloudtentaclesTestLoginInput export type AdminCloudtentaclesTestLoginRouteBody = AdminCloudtentaclesTestLoginInput
export type AdminCloudtentaclesValidateSessionRouteBody = AdminCloudtentaclesValidateSessionInput export type AdminCloudtentaclesValidateSessionRouteBody = AdminCloudtentaclesValidateSessionInput
@@ -108,6 +108,26 @@ export type AdminCloudtentaclesSourceConfigInput = {
sources?: AdminCloudtentaclesSourceConfigInput[] sources?: AdminCloudtentaclesSourceConfigInput[]
} }
export type AdminCloudtentaclesOverrideDeliveryItemInput = {
cloudSkuId?: number | string
cloudSkuName?: string
quantity?: number | string
}
export type AdminCloudtentaclesOverrideRuleInput = {
id?: string
enabled?: boolean
productName?: string
sourceKey?: string
deliveryItems?: AdminCloudtentaclesOverrideDeliveryItemInput[]
notes?: string
}
export type AdminCloudtentaclesOverrideRuleConfigInput = {
enabled?: boolean
rules?: AdminCloudtentaclesOverrideRuleInput[]
}
export type AdminCloudtentaclesSendSmsCodeInput = { export type AdminCloudtentaclesSendSmsCodeInput = {
sourceKey?: string sourceKey?: string
baseUrl?: string baseUrl?: string
@@ -2,6 +2,7 @@ import { apiDelete, apiGet, apiPost } from '@/lib/http'
import type { import type {
AdminCloudtentaclesLoginTestResult, AdminCloudtentaclesLoginTestResult,
AdminCloudtentaclesDeliveryRecordListResult, AdminCloudtentaclesDeliveryRecordListResult,
AdminCloudtentaclesOverrideRuleConfig,
AdminCloudtentaclesSkuListResult, AdminCloudtentaclesSkuListResult,
AdminCloudtentaclesSendSmsResult, AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSourceConfigResponse, AdminCloudtentaclesSourceConfigResponse,
@@ -33,6 +34,35 @@ export function deleteAdminCloudtentaclesSource(sourceKey: string) {
) )
} }
export function fetchAdminCloudtentaclesOverrideRules() {
return apiGet<AdminCloudtentaclesOverrideRuleConfig>(
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
)
}
export function saveAdminCloudtentaclesOverrideRules(
payload: {
enabled: boolean
rules: Array<{
id?: string
enabled?: boolean
productName?: string
sourceKey?: string
deliveryItems?: Array<{
cloudSkuId?: number
cloudSkuName?: string
quantity?: number
}>
notes?: string
}>
},
) {
return apiPost<AdminCloudtentaclesOverrideRuleConfig>(
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
payload,
)
}
export function sendAdminCloudtentaclesSmsCode(payload: { export function sendAdminCloudtentaclesSmsCode(payload: {
baseUrl?: string baseUrl?: string
username?: string username?: string
+3
View File
@@ -54,6 +54,9 @@ export type {
AdminCloudtentaclesSourceItem, AdminCloudtentaclesSourceItem,
AdminCloudtentaclesSourcesConfig, AdminCloudtentaclesSourcesConfig,
AdminCloudtentaclesSourceConfigResponse, AdminCloudtentaclesSourceConfigResponse,
AdminCloudtentaclesOverrideDeliveryItem,
AdminCloudtentaclesOverrideRule,
AdminCloudtentaclesOverrideRuleConfig,
AdminCloudtentaclesSessionsMap, AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSourceConfig, AdminCloudtentaclesSourceConfig,
AdminCloudtentaclesPersistedSession, AdminCloudtentaclesPersistedSession,
@@ -41,6 +41,28 @@ export interface AdminCloudtentaclesSourceConfigResponse {
sessions: AdminCloudtentaclesSessionsMap sessions: AdminCloudtentaclesSessionsMap
} }
export interface AdminCloudtentaclesOverrideDeliveryItem {
cloudSkuId: number
cloudSkuName: string
quantity: number
}
export interface AdminCloudtentaclesOverrideRule {
id: string
enabled: boolean
productName: string
normalizedProductName: string
sourceKey: string
deliveryItems: AdminCloudtentaclesOverrideDeliveryItem[]
notes: string
}
export interface AdminCloudtentaclesOverrideRuleConfig {
filePath: string
enabled: boolean
rules: AdminCloudtentaclesOverrideRule[]
}
export interface AdminCloudtentaclesSendSmsResult { export interface AdminCloudtentaclesSendSmsResult {
baseUrl: string baseUrl: string
username: string username: string
@@ -34,6 +34,9 @@ export type {
AdminCloudtentaclesSourceItem, AdminCloudtentaclesSourceItem,
AdminCloudtentaclesSourcesConfig, AdminCloudtentaclesSourcesConfig,
AdminCloudtentaclesSourceConfigResponse, AdminCloudtentaclesSourceConfigResponse,
AdminCloudtentaclesOverrideDeliveryItem,
AdminCloudtentaclesOverrideRule,
AdminCloudtentaclesOverrideRuleConfig,
AdminCloudtentaclesSessionsMap, AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSourceConfig, AdminCloudtentaclesSourceConfig,
AdminCloudtentaclesPersistedSession, AdminCloudtentaclesPersistedSession,
@@ -9,7 +9,7 @@
.fulfillment-overview { .fulfillment-overview {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(120px, auto); grid-template-columns: repeat(4, minmax(0, 1fr)) minmax(120px, auto);
gap: var(--space-3); gap: var(--space-3);
align-items: stretch; align-items: stretch;
} }
@@ -182,15 +182,67 @@
word-break: break-all; word-break: break-all;
} }
.override-panel {
display: grid;
gap: var(--space-4);
min-width: 0;
padding-top: var(--space-2);
}
.override-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
justify-content: flex-end;
align-items: center;
}
.override-rule-list {
display: grid;
gap: var(--space-3);
}
.override-rule-item {
display: grid;
gap: var(--space-3);
min-width: 0;
padding: var(--space-3);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-surface);
}
.override-rule-head {
display: grid;
grid-template-columns: auto minmax(220px, 1fr) minmax(160px, 220px) auto;
gap: var(--space-2);
align-items: center;
}
.delivery-item-list {
display: grid;
gap: var(--space-2);
}
.delivery-item-row {
display: grid;
grid-template-columns: minmax(220px, 1.1fr) minmax(180px, 1fr) 120px auto;
gap: var(--space-2);
align-items: center;
}
@media (max-width: 960px) { @media (max-width: 960px) {
.fulfillment-overview, .fulfillment-overview,
.fulfillment-layout, .fulfillment-layout,
.match-checker { .match-checker,
.override-rule-head,
.delivery-item-row {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.overview-actions, .overview-actions,
.catalog-toolbar { .catalog-toolbar,
.override-actions {
justify-content: stretch; justify-content: stretch;
} }
@@ -2,10 +2,14 @@
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { import {
fetchAdminCloudtentaclesOverrideRules,
fetchAdminCloudtentaclesSkuList, fetchAdminCloudtentaclesSkuList,
fetchAdminCloudtentaclesSourceConfig, fetchAdminCloudtentaclesSourceConfig,
saveAdminCloudtentaclesOverrideRules,
} from '@/services/admin' } from '@/services/admin'
import type { import type {
AdminCloudtentaclesOverrideDeliveryItem,
AdminCloudtentaclesOverrideRule,
AdminCloudtentaclesSessionItem, AdminCloudtentaclesSessionItem,
AdminCloudtentaclesSkuItem, AdminCloudtentaclesSkuItem,
AdminCloudtentaclesSourceItem, AdminCloudtentaclesSourceItem,
@@ -17,14 +21,23 @@ type MatchState = {
input: string input: string
} }
type EditableOverrideRule = Omit<AdminCloudtentaclesOverrideRule, 'normalizedProductName'> & {
normalizedProductName?: string
}
const loading = ref(true) const loading = ref(true)
const skuLoading = ref(false) const skuLoading = ref(false)
const overrideLoading = ref(false)
const overrideSaving = ref(false)
const errorMessage = ref('') const errorMessage = ref('')
const skuErrorMessage = ref('') const skuErrorMessage = ref('')
const overrideErrorMessage = ref('')
const selectedSourceKey = ref('') const selectedSourceKey = ref('')
const sources = ref<AdminCloudtentaclesSourceItem[]>([]) const sources = ref<AdminCloudtentaclesSourceItem[]>([])
const sessions = ref<AdminCloudtentaclesSessionsMap>({}) const sessions = ref<AdminCloudtentaclesSessionsMap>({})
const skuItems = ref<AdminCloudtentaclesSkuItem[]>([]) const skuItems = ref<AdminCloudtentaclesSkuItem[]>([])
const overrideEnabled = ref(true)
const overrideRules = ref<EditableOverrideRule[]>([])
const matchState = reactive<MatchState>({ const matchState = reactive<MatchState>({
input: '', input: '',
}) })
@@ -65,6 +78,8 @@ const matchedSku = computed(() => {
) )
}) })
const overrideRuleCount = computed(() => overrideRules.value.length)
watch(selectedSourceKey, (sourceKey) => { watch(selectedSourceKey, (sourceKey) => {
if (sourceKey) { if (sourceKey) {
void loadSkuList(sourceKey) void loadSkuList(sourceKey)
@@ -85,9 +100,16 @@ async function loadPage() {
errorMessage.value = '' errorMessage.value = ''
try { try {
const response = await fetchAdminCloudtentaclesSourceConfig() const [sourceResponse, overrideResponse] = await Promise.all([
sources.value = Array.isArray(response.data.sources) ? response.data.sources : [] fetchAdminCloudtentaclesSourceConfig(),
sessions.value = response.data.sessions || {} fetchAdminCloudtentaclesOverrideRules(),
])
sources.value = Array.isArray(sourceResponse.data.sources) ? sourceResponse.data.sources : []
sessions.value = sourceResponse.data.sessions || {}
overrideEnabled.value = overrideResponse.data.enabled !== false
overrideRules.value = Array.isArray(overrideResponse.data.rules)
? overrideResponse.data.rules.map((rule) => normalizeEditableRule(rule))
: []
selectedSourceKey.value = resolveDefaultSourceKey() selectedSourceKey.value = resolveDefaultSourceKey()
if (!selectedSourceKey.value) { if (!selectedSourceKey.value) {
@@ -100,6 +122,33 @@ async function loadPage() {
} }
} }
async function saveOverrideRules() {
overrideSaving.value = true
overrideErrorMessage.value = ''
try {
const response = await saveAdminCloudtentaclesOverrideRules({
enabled: overrideEnabled.value,
rules: overrideRules.value.map((rule) => ({
id: rule.id,
enabled: rule.enabled,
productName: rule.productName,
sourceKey: rule.sourceKey,
deliveryItems: rule.deliveryItems,
notes: rule.notes,
})),
})
overrideEnabled.value = response.data.enabled !== false
overrideRules.value = Array.isArray(response.data.rules)
? response.data.rules.map((rule) => normalizeEditableRule(rule))
: []
} catch (error) {
overrideErrorMessage.value = error instanceof Error ? error.message : '保存 cloudtentacles 覆盖规则失败'
} finally {
overrideSaving.value = false
}
}
async function loadSkuList(sourceKey = selectedSourceKey.value) { async function loadSkuList(sourceKey = selectedSourceKey.value) {
if (!sourceKey) { if (!sourceKey) {
skuItems.value = [] skuItems.value = []
@@ -124,6 +173,45 @@ function refreshAll() {
void loadPage() void loadPage()
} }
function addOverrideRule() {
overrideRules.value.unshift({
id: createRuleId(),
enabled: true,
productName: matchState.input.trim(),
normalizedProductName: normalizeMatchName(matchState.input),
sourceKey: selectedSourceKey.value,
deliveryItems: [createDeliveryItem()],
notes: '',
})
}
function removeOverrideRule(index: number) {
overrideRules.value.splice(index, 1)
}
function addDeliveryItem(rule: EditableOverrideRule) {
rule.deliveryItems.push(createDeliveryItem())
}
function removeDeliveryItem(rule: EditableOverrideRule, index: number) {
rule.deliveryItems.splice(index, 1)
if (rule.deliveryItems.length === 0) {
rule.deliveryItems.push(createDeliveryItem())
}
}
function handleRuleSourceChange(sourceKey: string) {
if (sourceKey) {
selectedSourceKey.value = sourceKey
}
}
function handleDeliverySkuChange(item: AdminCloudtentaclesOverrideDeliveryItem, skuId: number) {
const sku = skuItems.value.find((current) => Number(current.id || 0) === Number(skuId || 0))
item.cloudSkuId = Number(skuId || 0) || 0
item.cloudSkuName = String(sku?.name || item.cloudSkuName || '').trim()
}
function resolveDefaultSourceKey() { function resolveDefaultSourceKey() {
const current = sourceRows.value.find((source) => source.key === selectedSourceKey.value) const current = sourceRows.value.find((source) => source.key === selectedSourceKey.value)
if (current?.ready) { if (current?.ready) {
@@ -156,6 +244,10 @@ function formatCloudPrice(value: unknown) {
return Number.isFinite(price) ? String(price) : '-' return Number.isFinite(price) ? String(price) : '-'
} }
function formatSkuOption(sku: AdminCloudtentaclesSkuItem) {
return `#${sku.id} ${sku.name}`
}
function getSourceTagType(source: { ready: boolean; enabled?: boolean }) { function getSourceTagType(source: { ready: boolean; enabled?: boolean }) {
if (source.ready) return 'success' if (source.ready) return 'success'
if (source.enabled === false) return 'info' if (source.enabled === false) return 'info'
@@ -168,6 +260,37 @@ function getSourceStatusText(source: { ready: boolean; enabled?: boolean; sessio
if (!source.session?.hasToken) return '未登录' if (!source.session?.hasToken) return '未登录'
return '待检查' return '待检查'
} }
function normalizeEditableRule(rule: AdminCloudtentaclesOverrideRule): EditableOverrideRule {
return {
id: String(rule.id || createRuleId()).trim(),
enabled: rule.enabled !== false,
productName: String(rule.productName || '').trim(),
normalizedProductName: String(rule.normalizedProductName || '').trim(),
sourceKey: String(rule.sourceKey || '').trim(),
deliveryItems: Array.isArray(rule.deliveryItems) && rule.deliveryItems.length > 0
? rule.deliveryItems.map((item) => ({
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
cloudSkuName: String(item.cloudSkuName || '').trim(),
quantity: Math.max(1, Number(item.quantity || 1) || 1),
}))
: [createDeliveryItem()],
notes: String(rule.notes || '').trim(),
}
}
function createDeliveryItem(): AdminCloudtentaclesOverrideDeliveryItem {
const firstSku = skuItems.value[0]
return {
cloudSkuId: Number(firstSku?.id || 0) || 0,
cloudSkuName: String(firstSku?.name || '').trim(),
quantity: 1,
}
}
function createRuleId() {
return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
}
</script> </script>
<template> <template>
@@ -199,6 +322,10 @@ function getSourceStatusText(source: { ready: boolean; enabled?: boolean; sessio
<span>总库存</span> <span>总库存</span>
<strong>{{ metrics.inventoryTotal }}</strong> <strong>{{ metrics.inventoryTotal }}</strong>
</div> </div>
<div class="metric-card">
<span>覆盖规则</span>
<strong>{{ overrideRuleCount }}</strong>
</div>
<div class="overview-actions"> <div class="overview-actions">
<el-button :loading="loading || skuLoading" @click="refreshAll">刷新</el-button> <el-button :loading="loading || skuLoading" @click="refreshAll">刷新</el-button>
</div> </div>
@@ -305,6 +432,96 @@ function getSourceStatusText(source: { ready: boolean; enabled?: boolean; sessio
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<section class="override-panel">
<div class="catalog-toolbar">
<div class="section-head">
<h3>商品覆盖规则</h3>
<p>套装多数量商品会优先按这里发货没有规则时继续同名匹配</p>
</div>
<div class="override-actions">
<el-switch v-model="overrideEnabled" active-text="启用" inactive-text="停用" />
<el-button @click="addOverrideRule">新增规则</el-button>
<el-button
type="primary"
:loading="overrideLoading || overrideSaving"
@click="saveOverrideRules"
>
保存规则
</el-button>
</div>
</div>
<el-alert
v-if="overrideErrorMessage"
:title="overrideErrorMessage"
type="error"
show-icon
:closable="false"
/>
<el-empty v-if="overrideRules.length === 0" description="暂无覆盖规则" />
<div v-else class="override-rule-list">
<article
v-for="(rule, ruleIndex) in overrideRules"
:key="rule.id"
class="override-rule-item"
>
<div class="override-rule-head">
<el-switch v-model="rule.enabled" active-text="启用" inactive-text="停用" />
<el-input v-model="rule.productName" placeholder="91 商品名 / productNo" clearable />
<el-select
v-model="rule.sourceKey"
clearable
placeholder="全部账号"
@change="handleRuleSourceChange"
>
<el-option label="全部账号" value="" />
<el-option
v-for="source in sourceRows"
:key="source.key"
:label="formatSourceLabel(source)"
:value="source.key"
/>
</el-select>
<el-button type="danger" plain @click="removeOverrideRule(ruleIndex)">
删除
</el-button>
</div>
<div class="delivery-item-list">
<div
v-for="(item, itemIndex) in rule.deliveryItems"
:key="`${rule.id}-${itemIndex}`"
class="delivery-item-row"
>
<el-select
v-model="item.cloudSkuId"
filterable
placeholder="选择 cloudtentacles 商品"
@change="(value: number) => handleDeliverySkuChange(item, value)"
>
<el-option
v-for="sku in skuItems"
:key="sku.id"
:label="formatSkuOption(sku)"
:value="sku.id"
/>
</el-select>
<el-input v-model="item.cloudSkuName" placeholder="cloudtentacles 商品名" clearable />
<el-input-number v-model="item.quantity" :min="1" :max="999" controls-position="right" />
<el-button plain @click="removeDeliveryItem(rule, itemIndex)">
删除
</el-button>
</div>
<el-button plain @click="addDeliveryItem(rule)">增加发货商品</el-button>
</div>
<el-input v-model="rule.notes" placeholder="备注" clearable />
</article>
</div>
</section>
</main> </main>
</section> </section>
</template> </template>