fix(product-family): auto-merge SKUs after manual tag edits; fix session cookie path
问题2(SKU 无法自动合并,GBIU017 案例)四处叠加根因修复: - 人工标签旧词「热转印」被封闭词表静默踢出合并矩阵 → CRAFT_TAG_ALIASES 别名归一为「烫画」(矩阵归因与 CUSTOM 标签同路径生效) - updateTags 缺任一定价维度组即整链掉出矩阵且人工接管后永不恢复 → 缺啥补啥(按链接名派生补齐,人工勾选值不动,响应带 filledDimensionTags) - autoGroup 只建新族从不并入已有族(产生 USIU005-2 类碎片)→ 并入已有 autoManaged 同款族优先,无匹配才新建;同款仅人工锁定族则跳过并报告 - 名称回退分组键含物流备注,包邮/不包邮永不同组 → 新增族语义键 familyNameKey(国家+品名+SKU),api/admin 两侧同构,合并默认勾选随之修复 附加: - updateTags 后未归族链接自动并入匹配族(attachToMatchingFamily,响应带 attachedFamilyId) - 整理新增碎片族合并 consolidateFragments:纯碎片族并入带商品族并删除 (保公开 goodId=族ID 稳定),带商品/覆盖价/人工锁定进人工复审报告 - admin:保存标签提示补齐明细,整理完成消息含并入/碎片合并/待人工数 问题1(后台频繁 Unauthorized 掉线): - refresh cookie path '/auth' 与代理前缀(/api、/v2-api)不匹配导致浏览器 永远带不上 refresh cookie → 两 cookie path 统一为 '/' - v2 构建基址带尾斜杠 + 手工拼接产生 /v2-api//auth/refresh 双斜杠 404 → request.ts 规范拼接 测试:api jest 187/187、admin vitest 22/22、双侧 tsc 0 错误; public.service 夹具改为自包含(不依赖共享库既有数据)。
This commit is contained in:
@@ -7,6 +7,8 @@ import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
describe('OriginGoodsService', () => {
|
||||
let service: OriginGoodsService;
|
||||
let organize: OrganizeService;
|
||||
let families: ProductFamiliesService;
|
||||
let prisma: PrismaService;
|
||||
const stamp = Date.now();
|
||||
const createdSds: string[] = [];
|
||||
@@ -16,6 +18,8 @@ describe('OriginGoodsService', () => {
|
||||
providers: [OriginGoodsService, FamilyRecomputeService, ProductFamiliesService, OrganizeService, PrismaService],
|
||||
}).compile();
|
||||
service = moduleRef.get(OriginGoodsService);
|
||||
organize = moduleRef.get(OrganizeService);
|
||||
families = moduleRef.get(ProductFamiliesService);
|
||||
prisma = moduleRef.get(PrismaService);
|
||||
await prisma.onModuleInit();
|
||||
|
||||
@@ -161,4 +165,96 @@ describe('OriginGoodsService', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTags:缺啥补啥 + 自动归族', () => {
|
||||
const tagIdByNames = async (names: string[]) => {
|
||||
const rows = await prisma.tag.findMany({ where: { tagName: { in: names } } });
|
||||
return rows.map((r) => r.id);
|
||||
};
|
||||
const tagNamesOf = async (ogId: bigint) => {
|
||||
const rows = await prisma.originGoodTag.findMany({
|
||||
where: { originGoodId: ogId },
|
||||
include: { tag: true },
|
||||
});
|
||||
return rows.map((r) => r.tag.tagName).sort();
|
||||
};
|
||||
let ogFill: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
ogFill = await prisma.originGood.create({
|
||||
data: { sdsGoodId: `ogt-fill-${stamp}`, goodName: '美国(包邮)测试K-OG1-单面印花' },
|
||||
});
|
||||
createdSds.push(ogFill.sdsGoodId);
|
||||
// 派生一次,确保三个维度组的标签字典存在
|
||||
await organize.deriveTagsForOg(ogFill.id);
|
||||
});
|
||||
|
||||
it('缺维度自动补齐:只勾「不包邮」→ 补 单面印花+烫画,全部人工行', async () => {
|
||||
const [nfs] = await tagIdByNames(['不包邮']);
|
||||
const res = await service.updateTags(ogFill.id, [Number(nfs)]);
|
||||
expect(res.tagsManual).toBe(true);
|
||||
expect(res.tags.map((t: any) => t.tagName).sort()).toEqual(
|
||||
['不包邮', '单面印花', '烫画'].sort(),
|
||||
);
|
||||
expect((res as any).filledDimensionTags?.sort()).toEqual(['单面印花', '烫画'].sort());
|
||||
const rows = await prisma.originGoodTag.findMany({ where: { originGoodId: ogFill.id } });
|
||||
expect(rows.every((r) => r.manual)).toBe(true);
|
||||
});
|
||||
|
||||
it('人工勾齐不补;旧词「热转印」经别名视为工艺维度已齐', async () => {
|
||||
const full = await tagIdByNames(['包邮', '单面印花', '烫画']);
|
||||
const res1 = await service.updateTags(ogFill.id, full.map(Number));
|
||||
expect((res1 as any).filledDimensionTags ?? []).toEqual([]);
|
||||
// 热转印(旧词)满足工艺维度,不再补 烫画
|
||||
const alias = await tagIdByNames(['包邮', '单面印花', '热转印']);
|
||||
// 旧词可能不存在于字典:ensureDerivedTagMap 只建封闭词;此处兜底创建
|
||||
if (alias.length === 3) {
|
||||
const res2 = await service.updateTags(ogFill.id, alias.map(Number));
|
||||
expect((res2 as any).filledDimensionTags ?? []).toEqual([]);
|
||||
} else {
|
||||
const g = await prisma.tagGroup.upsert({
|
||||
where: { groupName: '印刷工艺' },
|
||||
create: { groupName: '印刷工艺' },
|
||||
update: {},
|
||||
});
|
||||
const t = await prisma.tag.upsert({
|
||||
where: { tagName: '热转印' },
|
||||
create: { tagName: '热转印', tagGroupId: g.id },
|
||||
update: {},
|
||||
});
|
||||
const [baoyou, danmian] = await tagIdByNames(['包邮', '单面印花']);
|
||||
const res2 = await service.updateTags(ogFill.id, [Number(baoyou), Number(danmian), Number(t.id)]);
|
||||
expect((res2 as any).filledDimensionTags ?? []).toEqual([]);
|
||||
}
|
||||
expect(await tagNamesOf(ogFill.id)).toEqual(['包邮', '单面印花', '热转印'].sort());
|
||||
});
|
||||
|
||||
it('无族链接保存标签后自动并入匹配族(同分类优先),响应带 attachedFamilyId', async () => {
|
||||
const cat = `cat-ogt-${stamp}`;
|
||||
const a = await prisma.originGood.create({
|
||||
data: { sdsGoodId: `ogt-fam-${stamp}-a`, goodName: '美国(包邮)测试K-OG2-双面印花', sdsCategoryId: cat },
|
||||
});
|
||||
const b = await prisma.originGood.create({
|
||||
data: { sdsGoodId: `ogt-fam-${stamp}-b`, goodName: '美国(不包邮)测试K-OG2-双面印花', sdsCategoryId: cat },
|
||||
});
|
||||
createdSds.push(a.sdsGoodId, b.sdsGoodId);
|
||||
const family = await families.create({
|
||||
familyName: `测试K族${stamp}`,
|
||||
originGoodIds: [a.id.toString()],
|
||||
primaryOriginGoodId: a.id.toString(),
|
||||
});
|
||||
try {
|
||||
await organize.deriveTagsForOg(b.id);
|
||||
const tagIds = (await prisma.originGoodTag.findMany({ where: { originGoodId: b.id } })).map((r) => Number(r.tagId));
|
||||
const res = await service.updateTags(b.id, tagIds);
|
||||
expect((res as any).attachedFamilyId).toBe(String(family.id));
|
||||
const after = await prisma.originGood.findUniqueOrThrow({ where: { id: b.id } });
|
||||
expect(after.familyId).toBe(BigInt(family.id));
|
||||
} finally {
|
||||
await prisma.originGoodTag.deleteMany({ where: { originGoodId: { in: [a.id, b.id] } } }).catch(() => undefined);
|
||||
await prisma.originGood.deleteMany({ where: { id: { in: [a.id, b.id] } } }).catch(() => undefined);
|
||||
await prisma.productFamily.deleteMany({ where: { id: BigInt(family.id) } }).catch(() => undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { FamilyRecomputeService } from '../product-families/family-recompute.service';
|
||||
import { OrganizeService } from '../product-families/organize.service';
|
||||
import { ProductFamiliesService } from '../product-families/product-families.service';
|
||||
import { QueryOriginGoodDto } from './dto/query-origin-good.dto';
|
||||
|
||||
/** 链接标签(origin_good_tags 行,含人工/派生标记) */
|
||||
@@ -21,6 +22,10 @@ export interface OriginGoodTagItem {
|
||||
export interface OriginGoodTagsResult {
|
||||
tagsManual: boolean;
|
||||
tags: OriginGoodTagItem[];
|
||||
/** 人工勾选缺定价维度时按链接名自动补齐的标签名(缺啥补啥) */
|
||||
filledDimensionTags?: string[];
|
||||
/** 保存时链接无族且自动并入成功的目标族 id */
|
||||
attachedFamilyId?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedOriginGoods {
|
||||
@@ -91,6 +96,7 @@ export class OriginGoodsService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly familyRecompute: FamilyRecomputeService,
|
||||
private readonly organize: OrganizeService,
|
||||
private readonly families: ProductFamiliesService,
|
||||
) {}
|
||||
|
||||
/** 链接当前标签(含 manual 标记) */
|
||||
@@ -120,16 +126,17 @@ export class OriginGoodsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工接管链接标签:全量替换为 manual 行(自动同步永不覆盖),
|
||||
* 并把有效标签镜像到该链接名下的商品。
|
||||
* 人工接管链接标签:人工勾选全量保存为 manual 行(自动同步永不覆盖);
|
||||
* 三个定价维度组(印花数量/工艺/物流)缺哪组按链接名自动补哪组(缺啥补啥),
|
||||
* 保证链接 SKU 不掉出族价格矩阵;未归族链接随后自动并入匹配族。
|
||||
*/
|
||||
async updateTags(id: bigint, tagIds: number[]): Promise<OriginGoodTagsResult> {
|
||||
const og = await this.prisma.originGood.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
select: { id: true, goodName: true, source: true },
|
||||
});
|
||||
if (!og) throw new NotFoundException(`Origin good ${id} not found`);
|
||||
const uniqueIds = [...new Set(tagIds.map((v) => BigInt(v)))].sort((a, b) =>
|
||||
let uniqueIds = [...new Set(tagIds.map((v) => BigInt(v)))].sort((a, b) =>
|
||||
Number(a - b),
|
||||
);
|
||||
if (uniqueIds.length) {
|
||||
@@ -138,6 +145,16 @@ export class OriginGoodsService {
|
||||
throw new BadRequestException('存在无效标签');
|
||||
}
|
||||
}
|
||||
// CUSTOM 成员的归因走管理员显式 craftLabel/logisticsLabel,名称不解析、不补齐
|
||||
const fill =
|
||||
og.source === 'SDS'
|
||||
? await this.organize.fillMissingDimTags(og.goodName, uniqueIds)
|
||||
: { tagIds: [] as bigint[], names: [] as string[] };
|
||||
if (fill.tagIds.length) {
|
||||
uniqueIds = [...new Set([...uniqueIds, ...fill.tagIds])].sort((a, b) =>
|
||||
Number(a - b),
|
||||
);
|
||||
}
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.originGoodTag.deleteMany({ where: { originGoodId: id } }),
|
||||
...(!uniqueIds.length
|
||||
@@ -159,8 +176,20 @@ export class OriginGoodsService {
|
||||
where: { id },
|
||||
select: { familyId: true },
|
||||
});
|
||||
if (ogFull?.familyId) await this.familyRecompute.recomputeFamily(ogFull.familyId);
|
||||
return this.getTags(id);
|
||||
let attachedFamilyId: string | undefined;
|
||||
if (ogFull?.familyId) {
|
||||
await this.familyRecompute.recomputeFamily(ogFull.familyId);
|
||||
} else {
|
||||
// 无族链接:自动并入匹配的同款族(同 SDS 分类优先,回退族语义名称键)
|
||||
const fid = await this.families.attachToMatchingFamily(id);
|
||||
if (fid) attachedFamilyId = fid.toString();
|
||||
}
|
||||
const base = await this.getTags(id);
|
||||
return {
|
||||
...base,
|
||||
filledDimensionTags: fill.names,
|
||||
...(attachedFamilyId ? { attachedFamilyId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** 恢复自动:清掉全部标签行(含人工行),按链接名称重新派生(显式人工动作) */
|
||||
|
||||
Reference in New Issue
Block a user