Files
order_site/apps/backend/src/services/order/cloudtentacles-name-match-service.ts
T
2026-05-28 13:05:44 +08:00

289 lines
8.7 KiB
TypeScript

import {
listCloudtentaclesSources,
} from '../platforms/cloudtentacles/source-config-service.js'
import {
getCloudtentaclesSessionStateByKey,
} from '../platforms/cloudtentacles/session-state-service.js'
import {
normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType,
} from '../platforms/cloudtentacles/defaults.js'
import {
listCloudtentaclesSku,
} 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 CloudtentaclesSource = {
key?: string
label?: string
enabled?: boolean
baseUrl?: string
deviceId?: string
deviceType?: number
}
type CloudtentaclesSession = {
token?: string
baseUrl?: string
deviceId?: string
deviceType?: number
}
type CloudtentaclesSku = {
id?: number
name?: string
inventory?: number
price?: number
[key: string]: unknown
}
type CloudtentaclesNameMatchDeps = {
listCloudtentaclesSources?: typeof listCloudtentaclesSources
getCloudtentaclesSessionStateByKey?: typeof getCloudtentaclesSessionStateByKey
listCloudtentaclesSku?: typeof listCloudtentaclesSku
resolveCloudtentaclesOverrideRule?: typeof resolveCloudtentaclesOverrideRule
}
export type CloudtentaclesResolvedDeliveryItem = {
cloudSkuId: number
cloudSkuName: string
quantity: number
}
export type CloudtentaclesNameMatchResult = {
matchMode: 'cloudtentacles_name' | 'cloudtentacles_override'
productName: string
normalizedProductName: string
cloudSkuId: number
cloudSkuName: string
cloudSkuPrice: number
cloudSkuInventory: number
cloudSourceKeys: string[]
resolvedSourceKey: string
deliveryItems: CloudtentaclesResolvedDeliveryItem[]
skuSnapshot: JsonObject
}
export async function resolveCloudtentaclesSkuByProductName(
productName: unknown,
deps: CloudtentaclesNameMatchDeps = {},
): Promise<CloudtentaclesNameMatchResult | null> {
const normalizedProductName = normalizeCloudtentaclesMatchName(productName)
if (!normalizedProductName) {
return null
}
const listSources = deps.listCloudtentaclesSources || listCloudtentaclesSources
const getSessionByKey = deps.getCloudtentaclesSessionStateByKey || getCloudtentaclesSessionStateByKey
const listSku = deps.listCloudtentaclesSku || listCloudtentaclesSku
const resolveOverrideRule = deps.resolveCloudtentaclesOverrideRule || resolveCloudtentaclesOverrideRule
const sourcesConfig = listSources()
if (sourcesConfig.enabled === false) {
return null
}
const sourceContexts = (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : [])
.map((source: CloudtentaclesSource) => buildCloudtentaclesSourceContext(source, getSessionByKey))
.filter((context): context is NonNullable<ReturnType<typeof buildCloudtentaclesSourceContext>> => Boolean(context))
if (sourceContexts.length === 0) {
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)
for (const context of sourceContexts) {
try {
const skuList = await listSku(context)
const matchedSku = findCloudtentaclesSkuByName(
Array.isArray(skuList.items) ? skuList.items : [],
productName,
)
if (!matchedSku) {
continue
}
const cloudSkuId = Number(matchedSku.id || 0)
const cloudSkuName = String(matchedSku.name || '').trim()
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0 || !cloudSkuName) {
continue
}
return {
matchMode: 'cloudtentacles_name',
productName: String(productName || '').trim(),
normalizedProductName,
cloudSkuId,
cloudSkuName,
cloudSkuPrice: Number(matchedSku.price || 0) || 0,
cloudSkuInventory: Number(matchedSku.inventory || 0) || 0,
cloudSourceKeys,
resolvedSourceKey: context.sourceKey,
deliveryItems: [
{
cloudSkuId,
cloudSkuName,
quantity: 1,
},
],
skuSnapshot: { ...matchedSku },
}
} catch {
// 当前账号不可用时继续尝试下一个已登录账号。
}
}
return null
}
function buildCloudtentaclesSourceContext(
source: CloudtentaclesSource,
getSessionByKey: typeof getCloudtentaclesSessionStateByKey,
) {
const sourceKey = String(source.key || '').trim()
if (!sourceKey || source.enabled === false) {
return null
}
const session = getSessionByKey(sourceKey) as CloudtentaclesSession | null
const token = String(session?.token || '').trim()
if (!token) {
return null
}
return {
sourceKey,
baseUrl: String(session?.baseUrl || source.baseUrl || '').trim(),
token,
deviceId: normalizeCloudtentaclesDeviceId(session?.deviceId || source.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(session?.deviceType ?? source.deviceType),
}
}
function findCloudtentaclesSkuByName(items: CloudtentaclesSku[], productName: unknown) {
const rawName = String(productName || '').trim()
const normalizedName = normalizeCloudtentaclesMatchName(rawName)
return (
items.find((item) => String(item.name || '').trim() === rawName) ||
items.find((item) => normalizeCloudtentaclesMatchName(item.name) === normalizedName) ||
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 }