后端迁移商品匹配服务

This commit is contained in:
yml
2026-05-21 14:27:45 +08:00
parent ca6e8b43c9
commit c870414ad6
2 changed files with 75 additions and 49 deletions
@@ -1,37 +1,65 @@
import { resolveFulfillmentBinding } from '../../repositories/fulfillment-profile-repo.js' import { resolveFulfillmentBinding } from '../../repositories/fulfillment-profile-repo.js'
import { resolveProductMatchRule } from '../../repositories/product-match-rule-repo.js' import { resolveProductMatchRule } from '../../repositories/product-match-rule-repo.js'
/** export type FulfillmentItem = {
* @typedef {{ itemId?: string
* itemId?: string externalItemId?: string
* externalItemId?: string skuCode?: string
* skuCode?: string skuName?: string
* skuName?: string externalSkuCode?: string
* externalSkuCode?: string externalSkuName?: string
* externalSkuName?: string quantity?: number
* quantity?: number spec?: Record<string, unknown>
* spec?: Record<string, unknown> snapshot?: Record<string, unknown>
* snapshot?: Record<string, unknown> [key: string]: unknown
* [key: string]: unknown }
* }} FulfillmentItem
*/ type ResolveOrderItemForFulfillmentInput = {
provider?: string
platform?: string
shopId?: string
shopIdAliases?: string[]
item?: FulfillmentItem
}
type HasConfiguredOrderItemsInput = Omit<ResolveOrderItemForFulfillmentInput, 'item'> & {
items?: FulfillmentItem[]
}
type FulfillmentItemCandidate = {
externalItemId: string
externalSkuCode: string
externalSkuName: string
externalSkuNameNormalized: string
matchedRule: Awaited<ReturnType<typeof resolveProductMatchRule>>
resolvedSkuCode: string
binding: Awaited<ReturnType<typeof resolveFulfillmentBinding>>
isConfigured: boolean
}
export type ResolvedFulfillmentItem = FulfillmentItem & {
itemId: string
externalItemId: string
externalSkuCode: string
externalSkuName: string
skuCode: string
skuName: string
snapshot: Record<string, unknown>
isConfigured: boolean
}
type ShopIdCandidatesInput = {
shopId?: string
shopIdAliases?: string[]
}
/**
* @param {{
* provider?: string
* platform?: string
* shopId?: string
* shopIdAliases?: string[]
* item?: FulfillmentItem
* }} params
*/
export async function resolveOrderItemForFulfillment({ export async function resolveOrderItemForFulfillment({
provider = '', provider = '',
platform = '', platform = '',
shopId = '', shopId = '',
shopIdAliases = [], shopIdAliases = [],
item = /** @type {FulfillmentItem} */ ({}), item = {},
}) { }: ResolveOrderItemForFulfillmentInput): Promise<ResolvedFulfillmentItem> {
const candidate = await resolveConfiguredItemCandidate({ const candidate = await resolveConfiguredItemCandidate({
provider, provider,
platform, platform,
@@ -87,22 +115,13 @@ export async function resolveOrderItemForFulfillment({
} }
} }
/**
* @param {{
* provider?: string
* platform?: string
* shopId?: string
* shopIdAliases?: string[]
* items?: FulfillmentItem[]
* }} params
*/
export async function hasConfiguredOrderItems({ export async function hasConfiguredOrderItems({
provider = '', provider = '',
platform = '', platform = '',
shopId = '', shopId = '',
shopIdAliases = [], shopIdAliases = [],
items = [], items = [],
}) { }: HasConfiguredOrderItemsInput): Promise<boolean> {
const candidates = await Promise.all( const candidates = await Promise.all(
(Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({ (Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({
provider, provider,
@@ -116,7 +135,7 @@ export async function hasConfiguredOrderItems({
return candidates.some((item) => item.isConfigured) return candidates.some((item) => item.isConfigured)
} }
export function normalizeProductName(value) { export function normalizeProductName(value: unknown): string {
return String(value || '') return String(value || '')
.toLowerCase() .toLowerCase()
.replace(/[【】\[\]()()]/g, ' ') .replace(/[【】\[\]()()]/g, ' ')
@@ -126,7 +145,7 @@ export function normalizeProductName(value) {
.trim() .trim()
} }
function pickFirstNonEmpty(values) { function pickFirstNonEmpty(values: unknown[]): string {
for (const value of values) { for (const value of values) {
const normalized = String(value || '').trim() const normalized = String(value || '').trim()
if (normalized) { if (normalized) {
@@ -142,8 +161,8 @@ async function resolveConfiguredItemCandidate({
platform = '', platform = '',
shopId = '', shopId = '',
shopIdAliases = [], shopIdAliases = [],
item = /** @type {FulfillmentItem} */ ({}), item = {},
}) { }: ResolveOrderItemForFulfillmentInput): Promise<FulfillmentItemCandidate> {
const externalItemId = pickFirstNonEmpty([ const externalItemId = pickFirstNonEmpty([
item.externalItemId, item.externalItemId,
item.itemId, item.itemId,
@@ -213,7 +232,10 @@ async function resolveConfiguredItemCandidate({
} }
} }
export function resolveShopIdCandidates({ shopId = '', shopIdAliases = [] } = {}) { export function resolveShopIdCandidates({
shopId = '',
shopIdAliases = [],
}: ShopIdCandidatesInput = {}): string[] {
const candidates = [...new Set([ const candidates = [...new Set([
String(shopId || '').trim(), String(shopId || '').trim(),
...(Array.isArray(shopIdAliases) ? shopIdAliases : []).map((item) => String(item || '').trim()), ...(Array.isArray(shopIdAliases) ? shopIdAliases : []).map((item) => String(item || '').trim()),
@@ -222,7 +244,7 @@ export function resolveShopIdCandidates({ shopId = '', shopIdAliases = [] } = {}
return candidates.length > 0 ? candidates : [''] return candidates.length > 0 ? candidates : ['']
} }
function readConfigValue(rawValue, key) { function readConfigValue(rawValue: unknown, key: string): string {
if (!rawValue) { if (!rawValue) {
return '' return ''
} }
@@ -232,11 +254,10 @@ function readConfigValue(rawValue, key) {
return '' return ''
} }
const config = /** @type {Record<string, unknown>} */ (parsed) return String(parsed[key] || '').trim()
return String(config[key] || '').trim()
} }
function safeParseJson(rawValue) { function safeParseJson(rawValue: unknown): unknown {
try { try {
return JSON.parse(String(rawValue || '{}')) return JSON.parse(String(rawValue || '{}'))
} catch { } catch {
@@ -244,10 +265,6 @@ function safeParseJson(rawValue) {
} }
} }
/** function isPlainObject(value: unknown): value is Record<string, unknown> {
* @param {unknown} value
* @returns {value is Record<string, unknown>}
*/
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]' return Object.prototype.toString.call(value) === '[object Object]'
} }
@@ -280,6 +280,14 @@
- `npm run typecheck` - `npm run typecheck`
- `npm run build` - `npm run build`
- `npm test` 共 125 个用例通过 - `npm test` 共 125 个用例通过
34. 商品匹配服务迁移到 `.ts`
- `src/services/order/product-match-service.ts`
35. 外部商品 item、匹配候选、解析后的履约商品、店铺候选与配置读取已显式类型化
36. Docker 内验证通过:
- 商品匹配 / 订单 / webhook / bootstrap 相关 4 个测试文件共 13 个用例通过
- `npm run typecheck`
- `npm run build`
- `npm test` 共 125 个用例通过
## 下一步建议 ## 下一步建议
@@ -287,6 +295,7 @@
1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块 1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块
2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货、delivery task 2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货、delivery task
3. 为 delivery task 补直接单测后再迁移,避免只依赖订单链路间接覆盖
## 执行原则 ## 执行原则