增加忽略商品
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { createTask, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js'
|
||||
import {
|
||||
getFulfillmentProfileByKey,
|
||||
listFulfillmentProfileRequirements,
|
||||
resolveFulfillmentBinding,
|
||||
} from '../../repositories/fulfillment-profile-repo.js'
|
||||
@@ -34,10 +33,11 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id,
|
||||
})
|
||||
const profile = binding || await getFulfillmentProfileByKey('manual_review')
|
||||
if (!profile) {
|
||||
|
||||
if (!binding) {
|
||||
continue
|
||||
}
|
||||
const profile = binding
|
||||
const requirements = await listFulfillmentProfileRequirements(profile.profile_id || profile.id)
|
||||
const primaryRequirement = requirements.find((requirement) => requirement.is_required !== false) || requirements[0] || null
|
||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
||||
|
||||
@@ -26,6 +26,37 @@ export async function upsertOrderFromWebhook(event) {
|
||||
existingOrderId: existing?.id || null,
|
||||
})
|
||||
|
||||
const resolvedItems = await Promise.all(
|
||||
event.items.map((item) => resolveOrderItemForFulfillment({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
item,
|
||||
})),
|
||||
)
|
||||
const configuredItems = resolvedItems.filter((item) => item.isConfigured)
|
||||
|
||||
if (configuredItems.length === 0) {
|
||||
logWebhook('[order-service]', 'Webhook 订单已忽略:未命中任何已配置履约商品', {
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
shopName: event.shopName,
|
||||
platformOrderId: event.platformOrderId,
|
||||
existingOrderId: existing?.id || null,
|
||||
rawItemCount: event.items.length,
|
||||
})
|
||||
|
||||
return {
|
||||
ignored: true,
|
||||
ignoreReason: 'unconfigured_items',
|
||||
order: null,
|
||||
orderItems: [],
|
||||
tasks: [],
|
||||
messageDeliveries: [],
|
||||
}
|
||||
}
|
||||
|
||||
const basePayload = {
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
@@ -54,18 +85,9 @@ export async function upsertOrderFromWebhook(event) {
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
const resolvedItems = await Promise.all(
|
||||
event.items.map((item) => resolveOrderItemForFulfillment({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
item,
|
||||
})),
|
||||
)
|
||||
|
||||
const orderItems = await replaceOrderItems(
|
||||
order.id,
|
||||
resolvedItems.map((item) => ({
|
||||
configuredItems.map((item) => ({
|
||||
skuCode: item.skuCode,
|
||||
skuName: item.skuName,
|
||||
quantity: item.quantity,
|
||||
@@ -88,7 +110,7 @@ export async function upsertOrderFromWebhook(event) {
|
||||
platformOrderId: order.platform_order_id,
|
||||
orderItemCount: orderItems.length,
|
||||
taskCount: tasks.length,
|
||||
resolvedSkuCodes: resolvedItems.map((item) => item.skuCode),
|
||||
resolvedSkuCodes: configuredItems.map((item) => item.skuCode),
|
||||
})
|
||||
|
||||
for (const task of tasks) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveFulfillmentBinding } from '../../repositories/fulfillment-profile-repo.js'
|
||||
import { resolveProductMatchRule } from '../../repositories/product-match-rule-repo.js'
|
||||
|
||||
export async function resolveOrderItemForFulfillment({
|
||||
@@ -5,6 +6,105 @@ export async function resolveOrderItemForFulfillment({
|
||||
platform = '',
|
||||
shopId = '',
|
||||
item = {},
|
||||
}) {
|
||||
const candidate = await resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
shopId,
|
||||
item,
|
||||
})
|
||||
const {
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
matchedRule,
|
||||
binding,
|
||||
} = candidate
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
matchedRule?.resolved_sku_code,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const resolvedSkuName = pickFirstNonEmpty([
|
||||
readConfigValue(matchedRule?.config_json, 'resolvedSkuName'),
|
||||
readConfigValue(matchedRule?.config_json, 'internalProductName'),
|
||||
item.skuName,
|
||||
externalSkuName,
|
||||
resolvedSkuCode,
|
||||
])
|
||||
const snapshot = {
|
||||
...(isPlainObject(item.snapshot) ? item.snapshot : {}),
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
resolvedSkuCode,
|
||||
matchedProductRuleId: matchedRule ? Number(matchedRule.id) : null,
|
||||
matchedProductRuleBy: String(matchedRule?.matched_by || '').trim(),
|
||||
matchedFulfillmentBindingId: binding ? Number(binding.id) : null,
|
||||
matchedFulfillmentProfileKey: String(binding?.profile_key || '').trim(),
|
||||
isConfigured: candidate.isConfigured,
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
itemId: externalItemId,
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
skuCode: resolvedSkuCode,
|
||||
skuName: resolvedSkuName,
|
||||
snapshot,
|
||||
isConfigured: candidate.isConfigured,
|
||||
}
|
||||
}
|
||||
|
||||
export async function hasConfiguredOrderItems({
|
||||
provider = '',
|
||||
platform = '',
|
||||
shopId = '',
|
||||
items = [],
|
||||
}) {
|
||||
const candidates = await Promise.all(
|
||||
(Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
shopId,
|
||||
item,
|
||||
})),
|
||||
)
|
||||
|
||||
return candidates.some((item) => item.isConfigured)
|
||||
}
|
||||
|
||||
export function normalizeProductName(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[【】\[\]()()]/g, ' ')
|
||||
.replace(/(自动发货|秒发|极速发货|官方直充|官方充值)/gi, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values) {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
async function resolveConfiguredItemCandidate({
|
||||
provider = '',
|
||||
platform = '',
|
||||
shopId = '',
|
||||
item = {},
|
||||
}) {
|
||||
const externalItemId = pickFirstNonEmpty([
|
||||
item.externalItemId,
|
||||
@@ -30,61 +130,30 @@ export async function resolveOrderItemForFulfillment({
|
||||
externalSkuCode,
|
||||
externalSkuNameNormalized,
|
||||
})
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
matchedRule?.resolved_sku_code,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const resolvedSkuName = pickFirstNonEmpty([
|
||||
readConfigValue(matchedRule?.config_json, 'resolvedSkuName'),
|
||||
readConfigValue(matchedRule?.config_json, 'internalProductName'),
|
||||
item.skuName,
|
||||
externalSkuName,
|
||||
resolvedSkuCode,
|
||||
])
|
||||
const snapshot = {
|
||||
...(isPlainObject(item.snapshot) ? item.snapshot : {}),
|
||||
const binding = resolvedSkuCode
|
||||
? await resolveFulfillmentBinding({
|
||||
skuCode: resolvedSkuCode,
|
||||
provider,
|
||||
platform,
|
||||
shopId,
|
||||
})
|
||||
: null
|
||||
|
||||
return {
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
matchedRule,
|
||||
resolvedSkuCode,
|
||||
matchedProductRuleId: matchedRule ? Number(matchedRule.id) : null,
|
||||
matchedProductRuleBy: String(matchedRule?.matched_by || '').trim(),
|
||||
binding,
|
||||
isConfigured: Boolean(binding),
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
itemId: externalItemId,
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
skuCode: resolvedSkuCode,
|
||||
skuName: resolvedSkuName,
|
||||
snapshot,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProductName(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[【】\[\]()()]/g, ' ')
|
||||
.replace(/(自动发货|秒发|极速发货|官方直充|官方充值)/gi, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values) {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function readConfigValue(rawValue, key) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { createWebhookEvent, updateWebhookEvent } from '../../repositories/webhook-event-repo.js'
|
||||
import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js'
|
||||
import { upsertOrderFromWebhook } from './order-service.js'
|
||||
import { hasConfiguredOrderItems } from './product-match-service.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { parseJsonObject } from '../../utils/json.js'
|
||||
import { logWebhook } from '../../utils/logger.js'
|
||||
@@ -89,6 +90,52 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
|
||||
})
|
||||
}
|
||||
|
||||
const shouldProcess = await hasConfiguredOrderItems({
|
||||
provider: parsed.provider,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
items: parsed.items,
|
||||
})
|
||||
|
||||
if (!shouldProcess) {
|
||||
await updateWebhookEvent(webhookEventId, {
|
||||
processed: true,
|
||||
process_error: 'ignored_unconfigured_product',
|
||||
related_order_id: null,
|
||||
})
|
||||
|
||||
logWebhook('[webhook-service/agiso]', 'Webhook 已忽略:未命中任何已配置履约商品', {
|
||||
requestId,
|
||||
webhookEventId,
|
||||
provider: parsed.provider,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
shopName: parsed.shopName,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
rawItemCount: Array.isArray(parsed.items) ? parsed.items.length : 0,
|
||||
})
|
||||
|
||||
return {
|
||||
accepted: true,
|
||||
ignored: true,
|
||||
ignoreReason: 'unconfigured_product',
|
||||
provider: parsed.provider,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
shopName: parsed.shopName,
|
||||
platformRaw: parsed.platformRaw,
|
||||
eventType: parsed.eventType,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
orderId: null,
|
||||
totalAmountFen: Number(parsed.totalAmount || 0),
|
||||
enriched: false,
|
||||
enrichReason: 'skipped_unconfigured_product',
|
||||
taskCount: 0,
|
||||
messageDeliveries: [],
|
||||
tasks: [],
|
||||
}
|
||||
}
|
||||
|
||||
const enriched = await enrichTradeBeforeUpsert(parsed, {
|
||||
requestId,
|
||||
webhookEventId,
|
||||
@@ -96,8 +143,8 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
|
||||
const result = await upsertOrderFromWebhook(enriched.parsed)
|
||||
await updateWebhookEvent(webhookEventId, {
|
||||
processed: true,
|
||||
process_error: '',
|
||||
related_order_id: result.order.id,
|
||||
process_error: result.ignored ? 'ignored_unconfigured_product' : '',
|
||||
related_order_id: result.order?.id || null,
|
||||
})
|
||||
|
||||
logWebhook('[webhook-service/agiso]', 'Webhook 业务处理成功', {
|
||||
@@ -110,7 +157,9 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
|
||||
eventKey: parsed.eventKey,
|
||||
eventType: parsed.eventType,
|
||||
platformOrderId: enriched.parsed.platformOrderId,
|
||||
orderId: result.order.id,
|
||||
orderId: result.order?.id || null,
|
||||
ignored: Boolean(result.ignored),
|
||||
ignoreReason: String(result.ignoreReason || ''),
|
||||
enriched: enriched.enriched,
|
||||
enrichReason: enriched.reason || '',
|
||||
totalAmountFen: Number(enriched.parsed.totalAmount || 0),
|
||||
@@ -127,8 +176,10 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
|
||||
platformRaw: enriched.parsed.platformRaw,
|
||||
eventType: enriched.parsed.eventType,
|
||||
platformOrderId: enriched.parsed.platformOrderId,
|
||||
orderId: result.order.id,
|
||||
orderId: result.order?.id || null,
|
||||
totalAmountFen: Number(enriched.parsed.totalAmount || 0),
|
||||
ignored: Boolean(result.ignored),
|
||||
ignoreReason: String(result.ignoreReason || ''),
|
||||
enriched: enriched.enriched,
|
||||
enrichReason: enriched.reason || '',
|
||||
taskCount: result.tasks.length,
|
||||
|
||||
Reference in New Issue
Block a user