支持cloudtentacles覆盖规则
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+55
-3
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+220
-3
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user