fix(goods): 商品展示名统一「品名 SKU」——剥 ASCII 型号限定词、纯款号补描述
左栏商品名显示成光款号(如 PLTK016)的根因是双层解析不一致:api 写路径按 首括号对解析产出「童装…(DTG180) PLTK016」,admin 展示解析按末括号取品名 误显示成 PLTK016。 - admin parseLinkName 对齐 api 首括号对解析 + 无 SKU 段中文品名守卫, cleanLinkName 剥离 ASCII 型号限定词(中文括号内容保留) - api 新增 cleanGoodDisplayName/describePureSkuGoodName,接入 create/update/ batchCreate(batchCreate 补上缺失的 normalize);纯款号按 SDS 分类名补描述 - 新增幂等存量修复脚本 fix:good-names(默认 dry-run,--apply 写库) - 已执行:V2 栈 34 条限定词名、V1 栈 213 条原始链接名规范化,复跑幂等 0 Tests: api 22 suites/199 passed, admin 27 passed, tsc clean
This commit is contained in:
@@ -80,6 +80,38 @@ describe('parseLinkName / cleanLinkName', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('已规范化的「品名(型号限定词) SKU」防误解析 + 限定词剥离', () => {
|
||||
it('品名内含(限定词)的已规范化名不再解析(否则展示会变成光款号)', () => {
|
||||
expect(parseLinkName('童装纯色插肩短袖T恤(DTG180) PLTK016')).toBeNull();
|
||||
expect(parseLinkName('牛奶丝T恤(女款) DG601')).toBeNull();
|
||||
expect(parseLinkName('180G纯棉T恤 (JSA002) DG001')).toBeNull();
|
||||
});
|
||||
|
||||
it('cleanLinkName 对已规范化名剥离 ASCII 型号限定词', () => {
|
||||
expect(cleanLinkName('童装纯色插肩短袖T恤(DTG180) PLTK016')).toBe(
|
||||
'童装纯色插肩短袖T恤 PLTK016',
|
||||
);
|
||||
expect(cleanLinkName('180G纯棉T恤 (JSA002) DG001')).toBe('180G纯棉T恤 DG001');
|
||||
});
|
||||
|
||||
it('原始链接名品名内含限定词时,解析后一并剥离', () => {
|
||||
expect(
|
||||
cleanLinkName('波兰(包邮)童装纯色插肩短袖T恤(DTG180)-PLTK016-双面印花'),
|
||||
).toBe('童装纯色插肩短袖T恤 PLTK016');
|
||||
});
|
||||
|
||||
it('中文括号内容(如(女款))不剥离', () => {
|
||||
expect(cleanLinkName('牛奶丝T恤(女款) DG601')).toBe('牛奶丝T恤(女款) DG601');
|
||||
});
|
||||
|
||||
it('无 SKU 段的单段链接名仍按首括号对解析(与 api 端一致)', () => {
|
||||
expect(parseLinkName('波兰(包邮)童装T恤')).toEqual({
|
||||
productName: '童装T恤',
|
||||
skuCode: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveLinkTagNames', () => {
|
||||
it('单面印花 + 包邮 + 无工艺关键字 → 单面印花/烫画/包邮', () => {
|
||||
expect(deriveLinkTagNames('美国(包邮)180g纯棉T恤成人款-DG001-单面印花')).toEqual([
|
||||
|
||||
@@ -27,27 +27,46 @@ export interface ParsedLinkName {
|
||||
|
||||
/**
|
||||
* 解析链接名称:`国家(物流备注)品名-SKU-工艺位置[-·仓库名]` → 品名 + 型号。
|
||||
* 与 api 端 origin-name.parser.ts 同构;解析失败(无「国家(备注)」前缀结构)返回 null,
|
||||
* 调用方应回退显示原始名称 —— 手动维护的品名不走该解析。
|
||||
* 与 api 端 origin-name.parser.ts 同构(首括号对解析,半角/全角混用先归一);
|
||||
* 解析失败(无「国家(备注)」前缀结构)返回 null,调用方应回退显示原始名称
|
||||
* —— 手动维护的品名不走该解析。
|
||||
*/
|
||||
export function parseLinkName(name: string | null | undefined): ParsedLinkName | null {
|
||||
if (!name) return null;
|
||||
const segs = name.split('-').map((s) => s.trim());
|
||||
const head = segs[0] ?? '';
|
||||
const closeIdx = Math.max(head.lastIndexOf(')'), head.lastIndexOf(')'));
|
||||
if (closeIdx <= 0 || closeIdx === head.length - 1) return null;
|
||||
const productName = head.slice(closeIdx + 1).trim();
|
||||
const normalized = head.replace(/\(/g, '(').replace(/\)/g, ')');
|
||||
const openAt = normalized.indexOf('(');
|
||||
const closeAt = openAt >= 0 ? normalized.indexOf(')', openAt) : -1;
|
||||
if (closeAt <= openAt || closeAt === normalized.length - 1) return null;
|
||||
const productName = normalized.slice(closeAt + 1).trim();
|
||||
if (!productName) return null;
|
||||
// 无 SKU 段时仅当括号后的品名含中文才视为链接名 —— 防把已规范化的
|
||||
// 「品名(型号限定词) SKU」(如「童装…(DTG180) PLTK016」)的尾段款号误当品名
|
||||
if (segs.length < 2 && !/[\u4e00-\u9fff]/.test(productName)) return null;
|
||||
const skuCode = segs[1] || null;
|
||||
return { productName, skuCode };
|
||||
}
|
||||
|
||||
/** 展示名:`品名 型号`(如「180g纯棉T恤成人款 DG001」);解析失败回退原名 */
|
||||
/** ASCII 型号限定词括号组(SDS 品名内的供应商/工艺型号,如(DTG180)(JSA002)) */
|
||||
const ASCII_PAREN_GROUP = /(?:(|\()[A-Za-z0-9]+(?:)|\))/g;
|
||||
|
||||
/** 展示名剥离 ASCII 型号限定词;剥空(名称仅剩限定词)时原样返回 */
|
||||
export function stripAsciiParenQualifiers(name: string): string {
|
||||
const stripped = name.replace(ASCII_PAREN_GROUP, ' ').replace(/\s+/g, ' ').trim();
|
||||
return stripped || name;
|
||||
}
|
||||
|
||||
/** 展示名:`品名 型号`(如「180g纯棉T恤成人款 DG001」);解析失败回退原名。品名内的 ASCII 型号限定词一并剥离 */
|
||||
export function cleanLinkName(name: string | null | undefined): string {
|
||||
if (!name) return '';
|
||||
const parsed = parseLinkName(name);
|
||||
if (!parsed) return name;
|
||||
return parsed.skuCode ? `${parsed.productName} ${parsed.skuCode}` : parsed.productName;
|
||||
const base = parsed
|
||||
? parsed.skuCode
|
||||
? `${parsed.productName} ${parsed.skuCode}`
|
||||
: parsed.productName
|
||||
: name;
|
||||
return stripAsciiParenQualifiers(base);
|
||||
}
|
||||
|
||||
const CRAFT_KEYWORD_MAP: Record<string, string> = {
|
||||
|
||||
Reference in New Issue
Block a user