- 新增 types/claim-identity,统一 ClaimIdentity / Payload / AdminSummary - 导出 KuaishouCloudFlow、KuaishouFeifeiFlow 作为 normalize 出口 - 生产路径 JsonObject 统一从 types/json 导入,去掉 rebind as any - 前后端 claimIdentity 类型字段对齐
487 lines
15 KiB
TypeScript
487 lines
15 KiB
TypeScript
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
|
|
import type { JsonObject } from '../../types/json.js'
|
|
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
|
import {
|
|
FULFILLMENT_EXECUTOR_KEYS,
|
|
isKuaishouCloudExecutor,
|
|
isKuaishouFeifeiExecutor,
|
|
} from './executors/types.js'
|
|
|
|
export type FulfillmentBindingLike = {
|
|
id: number
|
|
profile_id?: number
|
|
profile_key?: string
|
|
profile_name?: string
|
|
name?: string
|
|
executor_key?: string
|
|
requires_claim?: boolean
|
|
auto_dispatch?: boolean
|
|
config_json?: string | JsonObject
|
|
[key: string]: unknown
|
|
}
|
|
|
|
export type FulfillmentProfileResolver = (
|
|
profileKey: string,
|
|
) => Promise<FulfillmentBindingLike | null>
|
|
|
|
export type FulfillmentTaskPlan = {
|
|
profile: FulfillmentBindingLike
|
|
profileId: number
|
|
profileKey: string
|
|
profileName: string
|
|
executorKey: string
|
|
requiresClaim: boolean
|
|
autoDispatch: boolean
|
|
context: JsonObject
|
|
}
|
|
|
|
export async function planFulfillmentTaskForOrderItem({
|
|
order,
|
|
item,
|
|
getProfileByKey = getFulfillmentProfileByKey,
|
|
}: {
|
|
order: OrderRow
|
|
item: OrderItemRow
|
|
getProfileByKey?: FulfillmentProfileResolver
|
|
}): Promise<FulfillmentTaskPlan | null> {
|
|
const profile = await resolveDynamicFulfillmentProfile(item, getProfileByKey)
|
|
|
|
if (!profile) {
|
|
return null
|
|
}
|
|
|
|
const profileId = Number(profile.profile_id || profile.id || 0)
|
|
const profileKey = String(profile.profile_key || '').trim()
|
|
const profileName = String(profile.profile_name || profile.name || '').trim()
|
|
const executorKey = String(profile.executor_key || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH)
|
|
.trim()
|
|
|| FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
|
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
|
const context = buildFulfillmentTaskContext({
|
|
order,
|
|
item,
|
|
profile,
|
|
profileKey,
|
|
profileName,
|
|
executorKey,
|
|
fulfillmentConfig,
|
|
})
|
|
|
|
return {
|
|
profile,
|
|
profileId,
|
|
profileKey,
|
|
profileName,
|
|
executorKey,
|
|
requiresClaim: Boolean(profile.requires_claim),
|
|
autoDispatch: Boolean(profile.auto_dispatch),
|
|
context,
|
|
}
|
|
}
|
|
|
|
function buildFulfillmentTaskContext({
|
|
order,
|
|
item,
|
|
profileKey,
|
|
profileName,
|
|
executorKey,
|
|
fulfillmentConfig,
|
|
}: {
|
|
order: OrderRow
|
|
item: OrderItemRow
|
|
profile: FulfillmentBindingLike
|
|
profileKey: string
|
|
profileName: string
|
|
executorKey: string
|
|
fulfillmentConfig: JsonObject
|
|
}) {
|
|
const itemSnapshot = parseJsonObject(item.item_snapshot_json)
|
|
const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles)
|
|
const kuaishouFeifeiConfig = parseJsonObject(fulfillmentConfig.kuaishouFeifei)
|
|
const kuaishouConsumeConfig = parseJsonObject(fulfillmentConfig.kuaishouConsume)
|
|
const kuaishouShopConfig = parseJsonObject(fulfillmentConfig.kuaishouShop)
|
|
const cloudSourceKeys = normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys)
|
|
const resolvedCloudSourceKey =
|
|
cloudSourceKeys.length === 1
|
|
? String(cloudtentaclesConfig.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
|
|
: ''
|
|
const deliveryItems = normalizeCloudDeliveryItems(cloudtentaclesConfig)
|
|
const primaryDeliveryItem = deliveryItems[0] || {
|
|
cloudSkuId: Number(cloudtentaclesConfig.skuId || 0) || 0,
|
|
cloudSkuName: String(cloudtentaclesConfig.skuName || '').trim(),
|
|
quantity: 1,
|
|
}
|
|
|
|
return {
|
|
profileKey,
|
|
profileName,
|
|
skuCode: item.sku_code,
|
|
skuName: item.sku_name,
|
|
kuaishouCloudFulfillment: isKuaishouCloudExecutor(executorKey)
|
|
? {
|
|
flowType: 'kuaishou_cloud_fulfillment',
|
|
configId: String(fulfillmentConfig.configId || '').trim(),
|
|
internalSkuCode: item.sku_code,
|
|
internalSkuName: item.sku_name,
|
|
deliveryItems,
|
|
ticket: {
|
|
code: '',
|
|
status: 'pending',
|
|
capturedAt: null,
|
|
capturedBy: null,
|
|
verifiedAt: null,
|
|
oid: '',
|
|
formToken: '',
|
|
leftCount: 0,
|
|
goodsTitle: '',
|
|
},
|
|
binding: {
|
|
prepareStatus: 'pending',
|
|
cloudSourceKeys,
|
|
resolvedSourceKey: resolvedCloudSourceKey,
|
|
skuId: primaryDeliveryItem.cloudSkuId,
|
|
skuName: primaryDeliveryItem.cloudSkuName,
|
|
vnKey: '1',
|
|
vnId: 0,
|
|
vnPhone: '',
|
|
bindUrl: '',
|
|
bindPreparedAt: null,
|
|
bindExpiresAt: null,
|
|
bindProbeAt: null,
|
|
bindProbeStatus: '',
|
|
bindProbeMessage: '',
|
|
},
|
|
role: {
|
|
status: 'pending',
|
|
name: '',
|
|
rid: '',
|
|
refreshedAt: null,
|
|
errorMessage: '',
|
|
rawInfo: null,
|
|
},
|
|
purchase: {
|
|
autoBuyEnabled: cloudtentaclesConfig.autoBuyEnabled !== false,
|
|
minAssetReserve: Number(cloudtentaclesConfig.minAssetReserve || 0) || 0,
|
|
usedKnapsack: false,
|
|
purchaseTriggered: false,
|
|
assetBefore: 0,
|
|
assetAfter: 0,
|
|
purchaseAt: null,
|
|
},
|
|
dispatch: {
|
|
status: 'pending',
|
|
dispatchAt: null,
|
|
dispatchBy: null,
|
|
sendType: 0,
|
|
note: '',
|
|
},
|
|
returnNumber: {
|
|
status: 'pending',
|
|
returnedAt: null,
|
|
returnedBy: null,
|
|
autoReturnEnabled: cloudtentaclesConfig.autoReturnNumberAfterDispatch === true,
|
|
},
|
|
consume: {
|
|
status: 'pending',
|
|
shopId: String(
|
|
kuaishouConsumeConfig.shopId ||
|
|
kuaishouShopConfig.shopId ||
|
|
itemSnapshot.shopId ||
|
|
order.shop_id ||
|
|
'',
|
|
).trim(),
|
|
shopName: String(
|
|
kuaishouConsumeConfig.shopName ||
|
|
kuaishouShopConfig.kshopName ||
|
|
itemSnapshot.shopName ||
|
|
order.shop_name ||
|
|
'',
|
|
).trim(),
|
|
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
|
|
consumedAt: null,
|
|
errorMessage: '',
|
|
},
|
|
notes: String(fulfillmentConfig.notes || '').trim(),
|
|
}
|
|
: null,
|
|
kuaishouFeifei: isKuaishouFeifeiExecutor(executorKey)
|
|
? {
|
|
flowType: 'kuaishou_feifei',
|
|
productCode: String(kuaishouFeifeiConfig.productCode || '').trim(),
|
|
productName: String(kuaishouFeifeiConfig.productName || item.sku_name || '').trim(),
|
|
platformOrderNo: '',
|
|
orderNo: '',
|
|
rechargeStatus: 0,
|
|
rechargeStatusLabel: '',
|
|
rechargeResultMessage: '',
|
|
claimUrl: '',
|
|
consumeStatus: 'pending',
|
|
h5: {
|
|
entryUrl: '',
|
|
rechargeUrl: '',
|
|
},
|
|
lastSyncedAt: null,
|
|
}
|
|
: null,
|
|
}
|
|
}
|
|
|
|
async function resolveDynamicFulfillmentProfile(
|
|
item: OrderItemRow,
|
|
getProfileByKey: FulfillmentProfileResolver,
|
|
): Promise<FulfillmentBindingLike | null> {
|
|
const selectedExecutorKey = resolveSelectedFulfillmentExecutorKey(item)
|
|
if (selectedExecutorKey) {
|
|
if (selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD) {
|
|
return resolveDynamicCloudtentaclesProfile(item, getProfileByKey)
|
|
}
|
|
|
|
if (selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI) {
|
|
return resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey)
|
|
}
|
|
|
|
if (selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH) {
|
|
return resolveDynamicManualDispatchProfile(item, getProfileByKey)
|
|
}
|
|
}
|
|
|
|
return (
|
|
await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) ||
|
|
await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey)
|
|
)
|
|
}
|
|
|
|
async function resolveDynamicCloudtentaclesProfile(
|
|
item: OrderItemRow,
|
|
getProfileByKey: FulfillmentProfileResolver,
|
|
): Promise<FulfillmentBindingLike | null> {
|
|
const snapshot = parseJsonObject(item.item_snapshot_json)
|
|
const cloudtentacles = parseJsonObject(snapshot.cloudtentacles)
|
|
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 (
|
|
!primaryDeliveryItem.cloudSkuId ||
|
|
!primaryDeliveryItem.cloudSkuName ||
|
|
cloudSourceKeys.length === 0
|
|
) {
|
|
return null
|
|
}
|
|
|
|
const profile = await getProfileByKey(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD)
|
|
if (!profile) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
...profile,
|
|
profile_id: Number(profile.id || profile.profile_id || 0),
|
|
profile_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
|
|
profile_name: String(profile.profile_name || profile.name || 'kuaishou-lewan 履约').trim(),
|
|
executor_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
|
|
requires_claim: false,
|
|
auto_dispatch: false,
|
|
config_json: {
|
|
flowType: 'kuaishou_cloud_fulfillment',
|
|
configId: `${String(cloudtentacles.matchMode || 'cloudtentacles_name').trim()}:${String(cloudtentacles.normalizedProductName || primaryDeliveryItem.cloudSkuId).trim()}`,
|
|
cloudtentacles: {
|
|
cloudSourceKeys,
|
|
skuId: primaryDeliveryItem.cloudSkuId,
|
|
skuName: primaryDeliveryItem.cloudSkuName,
|
|
resolvedSourceKey:
|
|
cloudSourceKeys.length === 1
|
|
? String(cloudtentacles.resolvedSourceKey || cloudSourceKeys[0] || '').trim()
|
|
: '',
|
|
deliveryItems,
|
|
vnKey: '1',
|
|
autoBuyEnabled: true,
|
|
minAssetReserve: 0,
|
|
autoReturnNumberAfterDispatch: true,
|
|
},
|
|
kuaishouConsume: {
|
|
shopId: String(snapshot.shopId || '').trim(),
|
|
shopName: String(snapshot.shopName || '').trim(),
|
|
autoConsumeAfterDispatch: false,
|
|
},
|
|
notes:
|
|
cloudtentacles.matchMode === 'cloudtentacles_override'
|
|
? '91卡券商品名命中 cloudtentacles 覆盖规则'
|
|
: '91卡券商品名自动匹配 cloudtentacles 商品',
|
|
},
|
|
}
|
|
}
|
|
|
|
async function resolveDynamicKuaishouFeifeiProfile(
|
|
item: OrderItemRow,
|
|
getProfileByKey: FulfillmentProfileResolver,
|
|
): Promise<FulfillmentBindingLike | null> {
|
|
const snapshot = parseJsonObject(item.item_snapshot_json)
|
|
const feifei = parseJsonObject(snapshot.kuaishouFeifei)
|
|
const productCode = String(feifei.productCode || '').trim()
|
|
if (!productCode) {
|
|
return null
|
|
}
|
|
|
|
const profile = await getProfileByKey(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI)
|
|
if (!profile) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
...profile,
|
|
profile_id: Number(profile.id || profile.profile_id || 0),
|
|
profile_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
|
|
profile_name: String(profile.profile_name || profile.name || 'kuaishou-feifei 履约').trim(),
|
|
executor_key: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
|
|
requires_claim: true,
|
|
auto_dispatch: false,
|
|
config_json: {
|
|
flowType: 'kuaishou_feifei',
|
|
configId: `kuaishou_feifei:${productCode}`,
|
|
kuaishouFeifei: {
|
|
productCode,
|
|
productName: String(feifei.skuName || feifei.productName || item.sku_name || '').trim(),
|
|
matchMode: String(feifei.matchMode || 'kuaishou_feifei_name').trim(),
|
|
},
|
|
notes: '91卡券商品名自动匹配 kuaishou-feifei 商品映射',
|
|
},
|
|
}
|
|
}
|
|
|
|
async function resolveDynamicManualDispatchProfile(
|
|
item: OrderItemRow,
|
|
getProfileByKey: FulfillmentProfileResolver,
|
|
): Promise<FulfillmentBindingLike | null> {
|
|
const profile = await getProfileByKey('manual_review')
|
|
if (!profile) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
...profile,
|
|
profile_id: Number(profile.id || profile.profile_id || 0),
|
|
profile_key: 'manual_review',
|
|
profile_name: String(profile.profile_name || profile.name || '人工发货').trim(),
|
|
executor_key: FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH,
|
|
requires_claim: false,
|
|
auto_dispatch: false,
|
|
config_json: {
|
|
flowType: 'manual_dispatch',
|
|
configId: `manual_dispatch:${item.sku_code || item.id}`,
|
|
manualDispatch: {
|
|
source: 'fulfillment_routing',
|
|
skuCode: item.sku_code,
|
|
skuName: item.sku_name,
|
|
},
|
|
notes: '履约路由规则指定人工履约',
|
|
},
|
|
}
|
|
}
|
|
|
|
function resolveSelectedFulfillmentExecutorKey(item: OrderItemRow) {
|
|
const snapshot = parseJsonObject(item.item_snapshot_json)
|
|
const route = parseJsonObject(snapshot.fulfillmentRoute)
|
|
return String(route.selectedExecutorKey || '').trim()
|
|
}
|
|
|
|
function parseJsonObject(value: unknown): JsonObject {
|
|
if (!value) {
|
|
return {}
|
|
}
|
|
|
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
return value as JsonObject
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(String(value || '{}'))
|
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
? (parsed as JsonObject)
|
|
: {}
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
function normalizeStringArray(value: unknown): string[] {
|
|
if (!Array.isArray(value)) {
|
|
return []
|
|
}
|
|
|
|
return Array.from(new Set(value.map((item) => String(item || '').trim()).filter(Boolean)))
|
|
}
|
|
|
|
function normalizeCloudDeliveryItems(value: JsonObject): Array<{
|
|
cloudSkuId: number
|
|
cloudSkuName: string
|
|
quantity: number
|
|
}> {
|
|
const rawItems = Array.isArray(value.deliveryItems) ? value.deliveryItems : []
|
|
const items = rawItems
|
|
.map((item) => normalizeCloudDeliveryItem(item))
|
|
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
|
|
Boolean(item),
|
|
)
|
|
|
|
if (items.length > 0) {
|
|
return mergeCloudDeliveryItems(items)
|
|
}
|
|
|
|
const cloudSkuId = Number(value.skuId || 0) || 0
|
|
if (!cloudSkuId) {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
{
|
|
cloudSkuId,
|
|
cloudSkuName: String(value.skuName || '').trim(),
|
|
quantity: 1,
|
|
},
|
|
]
|
|
}
|
|
|
|
function normalizeCloudDeliveryItem(value: unknown) {
|
|
const source =
|
|
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
|
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
|
|
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
|
|
return null
|
|
}
|
|
|
|
const quantity = Number(source.quantity || 1) || 1
|
|
return {
|
|
cloudSkuId,
|
|
cloudSkuName: String(source.cloudSkuName || source.skuName || '').trim(),
|
|
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
|
|
}
|
|
}
|
|
|
|
function mergeCloudDeliveryItems(
|
|
items: Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>,
|
|
) {
|
|
const merged = new Map<number, { cloudSkuId: number; cloudSkuName: string; quantity: number }>()
|
|
|
|
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())
|
|
}
|