feat(api): category-based family grouping, tree family info and custom goods family attribution

This commit is contained in:
yeuimu
2026-08-28 12:43:58 +08:00
parent d63f1a6f35
commit c8531bfe08
10 changed files with 190 additions and 36 deletions
+25
View File
@@ -211,6 +211,31 @@ export class CreateCustomGoodDto extends OmitType(CreateGoodDto, [
@IsNumberString()
goodPrice?: string | null;
@ApiProperty({ required: false, description: '物流归因(入族后参与价格矩阵)', example: '包邮' })
@IsOptional()
@IsString()
logisticsLabel?: string;
@ApiProperty({ required: false, description: '工艺/印花数量归因', example: '双面印花' })
@IsOptional()
@IsString()
craftLabel?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
skuCode?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
warehouseLabel?: string;
@ApiProperty({ required: false, description: '挂入的产品族 id(缺省为独立商品)', example: '12' })
@IsOptional()
@IsNumberString()
familyId?: string;
@ApiProperty({ required: false, type: CustomGoodDetailDto })
@IsOptional()
@ValidateNested()
+6 -5
View File
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';
import { SyncModule } from '../sync/sync.module';
@Module({
imports: [SyncModule],
import { GoodsService } from './goods.service';
import { SyncModule } from '../sync/sync.module';
import { ProductFamiliesModule } from '../product-families/product-families.module';
@Module({
imports: [SyncModule, ProductFamiliesModule],
controllers: [GoodsController],
providers: [GoodsService],
exports: [GoodsService],
+5
View File
@@ -6,6 +6,7 @@ import {
import { GoodsService } from './goods.service';
import { PrismaService } from '../prisma/prisma.service';
import { SyncService } from '../sync/sync.service';
import { FamilyRecomputeService } from '../product-families/family-recompute.service';
describe('GoodsService', () => {
let service: GoodsService;
@@ -30,6 +31,10 @@ describe('GoodsService', () => {
provide: SyncService,
useValue: { queueProductDetailSync: jest.fn() },
},
{
provide: FamilyRecomputeService,
useValue: { enqueue: jest.fn() },
},
],
}).compile();
service = moduleRef.get(GoodsService);
+20
View File
@@ -12,6 +12,7 @@ import { BatchCreateGoodDto } from './dto/batch-create-good.dto';
import { BatchPriorityDto } from './dto/batch-priority.dto';
import { GoodDetailDto, GoodDto, PaginatedGoods } from './dto/good.dto';
import { SyncService } from '../sync/sync.service';
import { FamilyRecomputeService } from '../product-families/family-recompute.service';
import { randomUUID } from 'crypto';
import {
CreateCustomGoodDto,
@@ -52,6 +53,7 @@ export class GoodsService {
constructor(
private readonly prisma: PrismaService,
private readonly syncService: SyncService,
private readonly familyRecompute: FamilyRecomputeService,
) {}
async findAll(query: QueryGoodDto): Promise<PaginatedGoods> {
@@ -176,6 +178,14 @@ export class GoodsService {
await this.ensureCategory(dto.categoryId);
if (dto.positionId !== undefined) await this.ensurePosition(dto.positionId);
for (const tagId of dto.tagIds ?? []) await this.ensureTag(tagId);
let family: { id: bigint } | null = null;
if (dto.familyId !== undefined) {
family = await this.prisma.productFamily.findUnique({
where: { id: BigInt(dto.familyId) },
select: { id: true },
});
if (!family) throw new NotFoundException(`product family ${dto.familyId} not found`);
}
const goodId = await this.prisma.$transaction(async (tx) => {
const originGood = await tx.originGood.create({
@@ -185,6 +195,15 @@ export class GoodsService {
goodName: dto.goodName,
goodImage: dto.goodImage ?? null,
goodPrice: this.decimal(dto.goodPrice),
...(family ? { familyId: family.id } : {}),
...(dto.logisticsLabel !== undefined || dto.craftLabel !== undefined
? {
logisticsLabel: dto.logisticsLabel ?? null,
craftLabel: dto.craftLabel ?? null,
skuCode: dto.skuCode ?? null,
warehouseLabel: dto.warehouseLabel ?? null,
}
: {}),
detail: {
create: this.customDetailData(
dto.detail ?? {},
@@ -217,6 +236,7 @@ export class GoodsService {
}
return good.id;
});
if (family) this.familyRecompute.enqueue(family.id);
return this.findOne(goodId);
}