fix(categories): 分类名去括号——同步入库整段删除括号段
左侧分类树/官网分类名的括号段(型号限定词(JSA002)与内容性括号 (黑+卡其)/(短裤 & 长裤))整段删除。分类名每小时被 SDS 同步覆盖, 清洗落在 syncCategories 入库路径(cleanCategoryDisplayName,款号 token 保留,剥空守卫),另附幂等存量脚本 fix:category-names。 已执行:V1/V2 各 33 条分类名去括号,复跑幂等 0。注意 api 容器重建 部署前旧镜像的下一轮同步会把名称写回原文,部署后自动恢复干净。 Tests: api 23 suites/206 passed(含新增 category-name.spec 7 例),tsc clean
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
"import:product-detail": "ts-node prisma/import-product-detail.ts",
|
||||
"backfill:product-families": "ts-node prisma/backfill-product-families.ts",
|
||||
"fix:good-names": "ts-node prisma/fix-pure-sku-good-names.ts",
|
||||
"fix:category-names": "ts-node prisma/fix-category-parens.ts",
|
||||
"organize": "ts-node prisma/backfill-product-families.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 存量修复脚本(幂等):分类名去括号 —— 删除所有括号段(型号限定词/内容性括号,
|
||||
* 整段删除)。分类名每小时被 SDS 同步覆盖,清洗已落在 syncCategories 入库路径;
|
||||
* 本脚本用于部署后立即生效(不等下一轮同步)或修人工建的历史分类。
|
||||
*
|
||||
* 运行:
|
||||
* pnpm --filter @inkreach/api fix:category-names # dry-run,只打印清单
|
||||
* pnpm --filter @inkreach/api fix:category-names -- --apply # 实际写库
|
||||
*/
|
||||
import { PrismaService } from '../src/prisma/prisma.service';
|
||||
import { cleanCategoryDisplayName } from '../src/sync/sync.service';
|
||||
|
||||
async function main() {
|
||||
const apply = process.argv.includes('--apply');
|
||||
const prisma = new PrismaService();
|
||||
await prisma.onModuleInit();
|
||||
|
||||
const rows = await prisma.category.findMany({
|
||||
select: { id: true, categoryName: true },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
const renames = rows
|
||||
.map((row) => ({
|
||||
id: row.id,
|
||||
from: row.categoryName,
|
||||
to: cleanCategoryDisplayName(row.categoryName) ?? row.categoryName,
|
||||
}))
|
||||
.filter((r) => r.from !== r.to);
|
||||
|
||||
if (apply && renames.length > 0) {
|
||||
await prisma.$transaction(
|
||||
renames.map((r) =>
|
||||
prisma.category.update({
|
||||
where: { id: r.id },
|
||||
data: { categoryName: r.to },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
const mode = apply ? 'APPLIED' : 'DRY-RUN(未写库,加 --apply 执行)';
|
||||
console.log(`[fix:category-names] ${mode} renamed=${renames.length}`);
|
||||
for (const r of renames) {
|
||||
console.log(` #${r.id} ${r.from} -> ${r.to}`);
|
||||
}
|
||||
await prisma.onModuleDestroy();
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { cleanCategoryDisplayName } from './sync.service';
|
||||
|
||||
describe('cleanCategoryDisplayName(分类名去括号,同步入库清洗)', () => {
|
||||
it('删除型号限定词括号段(含名称后带空格的形态)', () => {
|
||||
expect(cleanCategoryDisplayName('DG001 180G纯棉T恤 (JSA002)')).toBe(
|
||||
'DG001 180G纯棉T恤',
|
||||
);
|
||||
expect(cleanCategoryDisplayName('DG004 230G水洗T恤(JSA003)')).toBe(
|
||||
'DG004 230G水洗T恤',
|
||||
);
|
||||
});
|
||||
|
||||
it('删除内容性括号段(整段删除)', () => {
|
||||
expect(cleanCategoryDisplayName('YSM02 全棉斜纹拼色帽子(黑+卡其)')).toBe(
|
||||
'YSM02 全棉斜纹拼色帽子',
|
||||
);
|
||||
expect(cleanCategoryDisplayName('下装(短裤 & 长裤)')).toBe('下装');
|
||||
});
|
||||
|
||||
it('多个括号段一并删除并折叠空白', () => {
|
||||
expect(cleanCategoryDisplayName('T恤(短袖)(男款)成人')).toBe('T恤 成人');
|
||||
});
|
||||
|
||||
it('半角括号同样处理', () => {
|
||||
expect(cleanCategoryDisplayName('DG001 180G纯棉T恤 (JSA002)')).toBe(
|
||||
'DG001 180G纯棉T恤',
|
||||
);
|
||||
});
|
||||
|
||||
it('无括号名称原样返回', () => {
|
||||
expect(cleanCategoryDisplayName('PLTK016 童装纯色插肩短袖T恤')).toBe(
|
||||
'PLTK016 童装纯色插肩短袖T恤',
|
||||
);
|
||||
});
|
||||
|
||||
it('剥空守卫:名称仅剩括号段时原样返回', () => {
|
||||
expect(cleanCategoryDisplayName('(JSA002)')).toBe('(JSA002)');
|
||||
expect(cleanCategoryDisplayName('(短裤 & 长裤)')).toBe('(短裤 & 长裤)');
|
||||
});
|
||||
|
||||
it('空/null 输入容错', () => {
|
||||
expect(cleanCategoryDisplayName('')).toBe('');
|
||||
expect(cleanCategoryDisplayName(null)).toBe(null);
|
||||
expect(cleanCategoryDisplayName(undefined)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,24 @@ export interface CategorySyncResult {
|
||||
deletedStale: number;
|
||||
}
|
||||
|
||||
/** 全角/半角括号段(非嵌套),分类名清洗用 */
|
||||
const PAREN_GROUP = /([^()()]*)|\([^()()]*\)/g;
|
||||
|
||||
/**
|
||||
* 分类展示名清洗:删除所有括号段(型号限定词如 `(JSA002)`、内容性括号如
|
||||
* `(黑+卡其)`/`(短裤 & 长裤)`,整段删除),折叠多余空白。剥空(名称仅剩
|
||||
* 括号段)时原样返回。分类名每小时被 SDS 同步覆盖,清洗必须在此入库路径生效
|
||||
* 才能持久;款号 token(如 `DG001`)不受影响。
|
||||
*/
|
||||
export function cleanCategoryDisplayName(name: string | null | undefined): string | null | undefined {
|
||||
if (!name) return name;
|
||||
let cleaned = name.replace(PAREN_GROUP, ' ');
|
||||
// 嵌套/残缺括号剥除后可能留下空括号对
|
||||
cleaned = cleaned.replace(/(\s*)|\(\s*\)/g, ' ');
|
||||
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
||||
return cleaned || name;
|
||||
}
|
||||
|
||||
export interface ProductSyncResult {
|
||||
inserted: number;
|
||||
updated: number;
|
||||
@@ -185,7 +203,7 @@ export class SyncService {
|
||||
await tx.category.create({
|
||||
data: {
|
||||
sdsCategoryId: node.sdsId,
|
||||
categoryName: node.name,
|
||||
categoryName: cleanCategoryDisplayName(node.name) ?? node.name,
|
||||
categoryIcon: node.icon ?? null,
|
||||
},
|
||||
});
|
||||
@@ -194,7 +212,7 @@ export class SyncService {
|
||||
await tx.category.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
categoryName: node.name,
|
||||
categoryName: cleanCategoryDisplayName(node.name) ?? node.name,
|
||||
categoryIcon: node.icon ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user