后端迁移商品匹配服务
This commit is contained in:
+66
-49
@@ -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 补直接单测后再迁移,避免只依赖订单链路间接覆盖
|
||||||
|
|
||||||
## 执行原则
|
## 执行原则
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user