增加忽略商品

This commit is contained in:
yml
2026-04-12 16:10:21 +08:00
parent eb5041ea60
commit 4c98b78267
18 changed files with 328 additions and 124 deletions
@@ -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,