691 lines
24 KiB
TypeScript
691 lines
24 KiB
TypeScript
import { Prisma } from '@prisma/client';
|
|
import {
|
|
BadRequestException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { CreateGoodDto } from './dto/create-good.dto';
|
|
import { UpdateGoodDto } from './dto/update-good.dto';
|
|
import { QueryGoodDto } from './dto/query-good.dto';
|
|
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,
|
|
CustomGoodDetailDto,
|
|
CustomGoodVariantDto,
|
|
UpdateCustomGoodContentDto,
|
|
} from './dto/custom-good.dto';
|
|
|
|
const GOOD_INCLUDE = {
|
|
country: true,
|
|
category: true,
|
|
tag: true,
|
|
position: true,
|
|
originGood: {
|
|
include: {
|
|
detail: true,
|
|
variants: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] },
|
|
_count: { select: { variants: true } },
|
|
},
|
|
},
|
|
goodTags: { include: { tag: true } },
|
|
mergedOriginGoods: {
|
|
orderBy: { createdAt: 'asc' },
|
|
include: {
|
|
originGood: {
|
|
include: {
|
|
detail: true,
|
|
variants: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] },
|
|
_count: { select: { variants: true } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} satisfies Prisma.GoodInclude;
|
|
|
|
@Injectable()
|
|
export class GoodsService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly syncService: SyncService,
|
|
private readonly familyRecompute: FamilyRecomputeService,
|
|
) {}
|
|
|
|
async findAll(query: QueryGoodDto): Promise<PaginatedGoods> {
|
|
const { page, pageSize, countryId, categoryId, tagId, positionId, keyword } = query;
|
|
const where: Prisma.GoodWhereInput = {};
|
|
if (countryId !== undefined) where.countryId = BigInt(countryId);
|
|
if (tagId !== undefined) where.goodTags = { some: { tagId: BigInt(tagId) } };
|
|
if (positionId !== undefined) where.positionId = BigInt(positionId);
|
|
if (keyword) {
|
|
where.goodName = { contains: keyword, mode: 'insensitive' };
|
|
}
|
|
if (categoryId !== undefined) {
|
|
const ids = await this.collectCategoryDescendants(BigInt(categoryId));
|
|
where.categoryId = { in: ids };
|
|
}
|
|
|
|
const [total, rows] = await this.prisma.$transaction([
|
|
this.prisma.good.count({ where }),
|
|
this.prisma.good.findMany({
|
|
where,
|
|
include: GOOD_INCLUDE,
|
|
orderBy: [{ goodPriority: 'desc' }, { createdAt: 'desc' }],
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
}),
|
|
]);
|
|
|
|
return {
|
|
items: rows.map((g) => GoodDto.from(g, {
|
|
country: g.country,
|
|
category: g.category,
|
|
tag: g.tag,
|
|
position: g.position,
|
|
originGood: g.originGood,
|
|
goodTags: g.goodTags,
|
|
mergedOriginGoods: g.mergedOriginGoods,
|
|
})),
|
|
total,
|
|
page,
|
|
pageSize,
|
|
};
|
|
}
|
|
|
|
async findOne(id: bigint): Promise<GoodDetailDto> {
|
|
const good = await this.prisma.good.findUnique({
|
|
where: { id },
|
|
include: GOOD_INCLUDE,
|
|
});
|
|
if (!good) throw new NotFoundException(`Good ${id} not found`);
|
|
return GoodDetailDto.fromGood(good, {
|
|
country: good.country,
|
|
category: good.category,
|
|
tag: good.tag,
|
|
position: good.position,
|
|
originGood: good.originGood,
|
|
goodTags: good.goodTags,
|
|
mergedOriginGoods: good.mergedOriginGoods,
|
|
});
|
|
}
|
|
|
|
async create(dto: CreateGoodDto): Promise<GoodDto> {
|
|
await this.ensureReferences(dto);
|
|
const mergedIds = this.dedupeMergedIds(
|
|
BigInt(dto.originGoodId),
|
|
dto.mergedOriginGoodIds,
|
|
);
|
|
await this.ensureMergedOriginGoods(mergedIds);
|
|
const result = await this.prisma.$transaction(async (tx) => {
|
|
// Good 的族是派生数据:主链接所属族
|
|
const primary = await tx.originGood.findUnique({
|
|
where: { id: BigInt(dto.originGoodId) },
|
|
select: { familyId: true },
|
|
});
|
|
const created = await tx.good.create({
|
|
data: {
|
|
goodName: dto.goodName,
|
|
goodImage: dto.goodImage,
|
|
originGoodId: BigInt(dto.originGoodId),
|
|
familyId: primary?.familyId ?? null,
|
|
countryId: BigInt(dto.countryId),
|
|
categoryId: BigInt(dto.categoryId),
|
|
positionId: dto.positionId === undefined ? null : BigInt(dto.positionId),
|
|
goodPriority: dto.goodPriority ?? 0,
|
|
},
|
|
});
|
|
if (dto.tagIds && dto.tagIds.length > 0) {
|
|
await tx.goodTag.createMany({
|
|
data: dto.tagIds.map((tagId) => ({
|
|
goodId: created.id,
|
|
tagId: BigInt(tagId),
|
|
})),
|
|
});
|
|
}
|
|
if (mergedIds.length > 0) {
|
|
await tx.goodOriginGood.createMany({
|
|
data: mergedIds.map((originGoodId) => ({
|
|
goodId: created.id,
|
|
originGoodId,
|
|
})),
|
|
});
|
|
}
|
|
const result = await tx.good.findUniqueOrThrow({
|
|
where: { id: created.id },
|
|
include: GOOD_INCLUDE,
|
|
});
|
|
return GoodDto.from(result, {
|
|
country: result.country,
|
|
category: result.category,
|
|
tag: result.tag,
|
|
position: result.position,
|
|
originGood: result.originGood,
|
|
goodTags: result.goodTags,
|
|
mergedOriginGoods: result.mergedOriginGoods,
|
|
});
|
|
});
|
|
if (
|
|
result.originGood?.source === 'SDS' &&
|
|
result.originGood.sdsGoodId &&
|
|
!result.originGood.hasDetail
|
|
) {
|
|
this.syncService.queueProductDetailSync(result.originGood.sdsGoodId);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async createCustom(dto: CreateCustomGoodDto): Promise<GoodDetailDto> {
|
|
await this.ensureCountry(dto.countryId);
|
|
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({
|
|
data: {
|
|
source: 'CUSTOM',
|
|
sdsGoodId: `custom-${randomUUID()}`,
|
|
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 ?? {},
|
|
) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput,
|
|
},
|
|
},
|
|
});
|
|
if (dto.variants?.length) {
|
|
await this.replaceCustomVariants(tx, originGood.id, dto.variants);
|
|
}
|
|
const good = await tx.good.create({
|
|
data: {
|
|
originGoodId: originGood.id,
|
|
familyId: family?.id ?? null,
|
|
countryId: BigInt(dto.countryId),
|
|
categoryId: BigInt(dto.categoryId),
|
|
positionId:
|
|
dto.positionId === undefined ? null : BigInt(dto.positionId),
|
|
goodName: dto.goodName,
|
|
goodImage: dto.goodImage ?? null,
|
|
goodPriority: dto.goodPriority ?? 0,
|
|
},
|
|
});
|
|
if (dto.tagIds?.length) {
|
|
await tx.goodTag.createMany({
|
|
data: dto.tagIds.map((tagId) => ({
|
|
goodId: good.id,
|
|
tagId: BigInt(tagId),
|
|
})),
|
|
});
|
|
}
|
|
return good.id;
|
|
});
|
|
if (family) this.familyRecompute.enqueue(family.id);
|
|
return this.findOne(goodId);
|
|
}
|
|
|
|
async updateCustomContent(
|
|
id: bigint,
|
|
dto: UpdateCustomGoodContentDto,
|
|
): Promise<GoodDetailDto> {
|
|
const existing = await this.prisma.good.findUnique({
|
|
where: { id },
|
|
include: { originGood: true },
|
|
});
|
|
if (!existing) throw new NotFoundException(`Good ${id} not found`);
|
|
if (existing.originGood.source !== 'CUSTOM') {
|
|
throw new BadRequestException('SDS 映射商品的上游信息不可修改');
|
|
}
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
await tx.originGood.update({
|
|
where: { id: existing.originGoodId },
|
|
data: {
|
|
goodName: dto.goodName,
|
|
goodImage: dto.goodImage,
|
|
goodPrice:
|
|
dto.goodPrice === undefined ? undefined : this.decimal(dto.goodPrice),
|
|
},
|
|
});
|
|
if (dto.detail !== undefined) {
|
|
await tx.originGoodDetail.upsert({
|
|
where: { originGoodId: existing.originGoodId },
|
|
create: {
|
|
originGoodId: existing.originGoodId,
|
|
...(this.customDetailData(
|
|
dto.detail,
|
|
) as Prisma.OriginGoodDetailUncheckedCreateWithoutOriginGoodInput),
|
|
},
|
|
update: this.customDetailData(dto.detail, true),
|
|
});
|
|
}
|
|
if (dto.variants !== undefined) {
|
|
await this.replaceCustomVariants(
|
|
tx,
|
|
existing.originGoodId,
|
|
dto.variants,
|
|
);
|
|
}
|
|
const goodData: Prisma.GoodUpdateInput = {};
|
|
if (dto.goodName !== undefined) goodData.goodName = dto.goodName;
|
|
if (dto.goodImage !== undefined) goodData.goodImage = dto.goodImage;
|
|
if (Object.keys(goodData).length) {
|
|
await tx.good.update({ where: { id }, data: goodData });
|
|
}
|
|
});
|
|
return this.findOne(id);
|
|
}
|
|
|
|
async update(id: bigint, dto: UpdateGoodDto): Promise<GoodDto> {
|
|
await this.findOne(id);
|
|
let mergedIds: bigint[] | undefined;
|
|
if (dto.mergedOriginGoodIds !== undefined || dto.originGoodId !== undefined) {
|
|
const current = await this.prisma.good.findUniqueOrThrow({
|
|
where: { id },
|
|
select: { originGoodId: true },
|
|
});
|
|
const primaryId =
|
|
dto.originGoodId !== undefined ? BigInt(dto.originGoodId) : current.originGoodId;
|
|
mergedIds = this.dedupeMergedIds(primaryId, dto.mergedOriginGoodIds);
|
|
await this.ensureMergedOriginGoods(mergedIds);
|
|
}
|
|
const data: Prisma.GoodUpdateInput = {};
|
|
if (dto.goodName !== undefined) data.goodName = dto.goodName;
|
|
if (dto.originGoodId !== undefined) {
|
|
await this.ensureOriginGood(dto.originGoodId);
|
|
data.originGood = { connect: { id: BigInt(dto.originGoodId) } };
|
|
}
|
|
if (dto.countryId !== undefined) {
|
|
await this.ensureCountry(dto.countryId);
|
|
data.country = { connect: { id: BigInt(dto.countryId) } };
|
|
}
|
|
if (dto.categoryId !== undefined) {
|
|
await this.ensureCategory(dto.categoryId);
|
|
data.category = { connect: { id: BigInt(dto.categoryId) } };
|
|
}
|
|
if (dto.positionId !== undefined) {
|
|
data.position =
|
|
dto.positionId === null
|
|
? { disconnect: true }
|
|
: { connect: { id: BigInt(dto.positionId) } };
|
|
}
|
|
if (dto.goodPriority !== undefined) data.goodPriority = dto.goodPriority;
|
|
if (dto.goodImage !== undefined) data.goodImage = dto.goodImage;
|
|
if (dto.tagIds !== undefined) {
|
|
for (const tagId of dto.tagIds) {
|
|
await this.ensureTag(tagId);
|
|
}
|
|
}
|
|
|
|
const result = await this.prisma.$transaction(async (tx) => {
|
|
if (dto.tagIds !== undefined) {
|
|
await tx.goodTag.deleteMany({ where: { goodId: id } });
|
|
if (dto.tagIds.length > 0) {
|
|
await tx.goodTag.createMany({
|
|
data: dto.tagIds.map((tagId) => ({
|
|
goodId: id,
|
|
tagId: BigInt(tagId),
|
|
})),
|
|
});
|
|
}
|
|
}
|
|
if (mergedIds !== undefined) {
|
|
await tx.goodOriginGood.deleteMany({ where: { goodId: id } });
|
|
if (mergedIds.length > 0) {
|
|
await tx.goodOriginGood.createMany({
|
|
data: mergedIds.map((originGoodId) => ({
|
|
goodId: id,
|
|
originGoodId,
|
|
})),
|
|
});
|
|
}
|
|
}
|
|
const updated = await tx.good.update({
|
|
where: { id },
|
|
data,
|
|
include: GOOD_INCLUDE,
|
|
});
|
|
return GoodDto.from(updated, {
|
|
country: updated.country,
|
|
category: updated.category,
|
|
tag: updated.tag,
|
|
position: updated.position,
|
|
originGood: updated.originGood,
|
|
goodTags: updated.goodTags,
|
|
mergedOriginGoods: updated.mergedOriginGoods,
|
|
});
|
|
});
|
|
if (
|
|
result.originGood?.source === 'SDS' &&
|
|
result.originGood.sdsGoodId &&
|
|
!result.originGood.hasDetail
|
|
) {
|
|
this.syncService.queueProductDetailSync(result.originGood.sdsGoodId);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async remove(id: bigint): Promise<{ id: string }> {
|
|
const good = await this.prisma.good.findUnique({
|
|
where: { id },
|
|
include: { originGood: true },
|
|
});
|
|
if (!good) throw new NotFoundException(`Good ${id} not found`);
|
|
await this.prisma.$transaction(async (tx) => {
|
|
await tx.good.delete({ where: { id } });
|
|
if (good.originGood.source === 'CUSTOM') {
|
|
const remaining = await tx.good.count({
|
|
where: { originGoodId: good.originGoodId },
|
|
});
|
|
if (remaining === 0) {
|
|
await tx.originGood.delete({ where: { id: good.originGoodId } });
|
|
}
|
|
}
|
|
});
|
|
return { id: id.toString() };
|
|
}
|
|
|
|
/**
|
|
* Updates priorities in a single transaction; either all rows update
|
|
* or none do.
|
|
*/
|
|
async batchUpdatePriority(dto: BatchPriorityDto): Promise<{ count: number }> {
|
|
const result = await this.prisma.$transaction(async (tx) => {
|
|
for (const item of dto.items) {
|
|
await tx.good.update({
|
|
where: { id: BigInt(item.id) },
|
|
data: { goodPriority: item.priority },
|
|
});
|
|
}
|
|
return { count: dto.items.length };
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Creates multiple goods atomically, sharing countryId/categoryId/tagIds/positionId
|
|
* and a default priority that may be overridden per item.
|
|
*/
|
|
async batchCreate(dto: BatchCreateGoodDto): Promise<GoodDto[]> {
|
|
const defaultPriority = dto.defaultPriority ?? 0;
|
|
if (dto.tagIds && dto.tagIds.length > 0) {
|
|
for (const tagId of dto.tagIds) {
|
|
await this.ensureTag(tagId);
|
|
}
|
|
}
|
|
const result = await this.prisma.$transaction(async (tx) => {
|
|
const created: GoodDto[] = [];
|
|
for (const item of dto.items) {
|
|
const og = await tx.originGood.findUnique({
|
|
where: { id: BigInt(item.originGoodId) },
|
|
});
|
|
if (!og) {
|
|
throw new BadRequestException(
|
|
`Origin good ${item.originGoodId} not found`,
|
|
);
|
|
}
|
|
const row = await tx.good.create({
|
|
data: {
|
|
goodName: og.goodName ?? `Origin Good ${og.sdsGoodId}`,
|
|
goodImage: og.goodImage,
|
|
originGoodId: og.id,
|
|
familyId: og.familyId,
|
|
countryId: BigInt(dto.countryId),
|
|
categoryId: BigInt(dto.categoryId),
|
|
positionId: dto.positionId === undefined ? null : BigInt(dto.positionId),
|
|
goodPriority: item.priority ?? defaultPriority,
|
|
},
|
|
});
|
|
if (dto.tagIds && dto.tagIds.length > 0) {
|
|
await tx.goodTag.createMany({
|
|
data: dto.tagIds.map((tagId) => ({
|
|
goodId: row.id,
|
|
tagId: BigInt(tagId),
|
|
})),
|
|
});
|
|
}
|
|
const itemMerged = this.dedupeMergedIds(og.id, item.mergedOriginGoodIds);
|
|
if (itemMerged.length > 0) {
|
|
const existRows = await tx.originGood.findMany({
|
|
where: { id: { in: itemMerged } },
|
|
select: { id: true },
|
|
});
|
|
if (existRows.length !== itemMerged.length) {
|
|
const found = new Set(existRows.map((r) => r.id.toString()));
|
|
const missing = itemMerged.find((mid) => !found.has(mid.toString()));
|
|
throw new BadRequestException(`Origin good ${missing} not found`);
|
|
}
|
|
await tx.goodOriginGood.createMany({
|
|
data: itemMerged.map((originGoodId) => ({
|
|
goodId: row.id,
|
|
originGoodId,
|
|
})),
|
|
});
|
|
}
|
|
const result = await tx.good.findUniqueOrThrow({
|
|
where: { id: row.id },
|
|
include: GOOD_INCLUDE,
|
|
});
|
|
created.push(GoodDto.from(result, {
|
|
country: result.country,
|
|
category: result.category,
|
|
tag: result.tag,
|
|
position: result.position,
|
|
originGood: result.originGood,
|
|
goodTags: result.goodTags,
|
|
mergedOriginGoods: result.mergedOriginGoods,
|
|
}));
|
|
}
|
|
return created;
|
|
});
|
|
for (const goodId of new Set(
|
|
result
|
|
.filter(
|
|
(item) =>
|
|
item.originGood?.source === 'SDS' &&
|
|
!item.originGood.hasDetail,
|
|
)
|
|
.map((item) => item.originGood?.sdsGoodId)
|
|
.filter((id): id is string => Boolean(id)),
|
|
)) {
|
|
this.syncService.queueProductDetailSync(goodId);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Walk the category tree and return the requested id + all of its
|
|
* descendants. We use a level-by-level BFS to keep the queries small
|
|
* for the typical tree sizes we expect.
|
|
*/
|
|
private async collectCategoryDescendants(rootId: bigint): Promise<bigint[]> {
|
|
const ids: bigint[] = [rootId];
|
|
let frontier: bigint[] = [rootId];
|
|
while (frontier.length > 0) {
|
|
const children = await this.prisma.category.findMany({
|
|
where: { parentCategoryId: { in: frontier } },
|
|
select: { id: true },
|
|
});
|
|
if (children.length === 0) break;
|
|
const childIds = children.map((c) => c.id);
|
|
ids.push(...childIds);
|
|
frontier = childIds;
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
private async ensureOriginGood(id: number) {
|
|
const og = await this.prisma.originGood.findUnique({
|
|
where: { id: BigInt(id) },
|
|
});
|
|
if (!og) throw new BadRequestException(`Origin good ${id} not found`);
|
|
}
|
|
|
|
/** Dedupe merged ids and reject any that equals the primary source. */
|
|
private dedupeMergedIds(primaryId: bigint, ids?: number[]): bigint[] {
|
|
if (!ids || ids.length === 0) return [];
|
|
const unique = [...new Set(ids.map((id) => BigInt(id)))];
|
|
if (unique.includes(primaryId)) {
|
|
throw new BadRequestException(
|
|
'mergedOriginGoodIds 不能包含主源 originGoodId',
|
|
);
|
|
}
|
|
return unique;
|
|
}
|
|
|
|
private async ensureMergedOriginGoods(ids: bigint[]) {
|
|
if (ids.length === 0) return;
|
|
const rows = await this.prisma.originGood.findMany({
|
|
where: { id: { in: ids } },
|
|
select: { id: true },
|
|
});
|
|
if (rows.length !== ids.length) {
|
|
const found = new Set(rows.map((r) => r.id.toString()));
|
|
const missing = ids.find((id) => !found.has(id.toString()));
|
|
throw new BadRequestException(`Origin good ${missing} not found`);
|
|
}
|
|
}
|
|
|
|
private async ensureCountry(id: number) {
|
|
const c = await this.prisma.country.findUnique({ where: { id: BigInt(id) } });
|
|
if (!c) throw new BadRequestException(`Country ${id} not found`);
|
|
}
|
|
|
|
private async ensureCategory(id: number) {
|
|
const c = await this.prisma.category.findUnique({ where: { id: BigInt(id) } });
|
|
if (!c) throw new BadRequestException(`Category ${id} not found`);
|
|
}
|
|
|
|
private async ensureTag(id: number) {
|
|
const t = await this.prisma.tag.findUnique({ where: { id: BigInt(id) } });
|
|
if (!t) throw new BadRequestException(`Tag ${id} not found`);
|
|
}
|
|
|
|
private async ensureReferences(dto: CreateGoodDto) {
|
|
await this.ensureOriginGood(dto.originGoodId);
|
|
await this.ensureCountry(dto.countryId);
|
|
await this.ensureCategory(dto.categoryId);
|
|
if (dto.tagIds && dto.tagIds.length > 0) {
|
|
for (const tagId of dto.tagIds) {
|
|
await this.ensureTag(tagId);
|
|
}
|
|
}
|
|
if (dto.positionId !== undefined) {
|
|
const p = await this.prisma.position.findUnique({ where: { id: BigInt(dto.positionId) } });
|
|
if (!p) throw new BadRequestException(`Position ${dto.positionId} not found`);
|
|
}
|
|
}
|
|
|
|
private async ensurePosition(id: number) {
|
|
const position = await this.prisma.position.findUnique({
|
|
where: { id: BigInt(id) },
|
|
});
|
|
if (!position) throw new BadRequestException(`Position ${id} not found`);
|
|
}
|
|
|
|
private decimal(value: string | null | undefined): Prisma.Decimal | null {
|
|
return value === undefined || value === null || value === ''
|
|
? null
|
|
: new Prisma.Decimal(value);
|
|
}
|
|
|
|
private customDetailData(
|
|
detail: CustomGoodDetailDto,
|
|
preserveMissing = false,
|
|
): Prisma.OriginGoodDetailUncheckedUpdateInput {
|
|
const nullable = <T>(value: T | null | undefined): T | null | undefined =>
|
|
preserveMissing && value === undefined ? undefined : value ?? null;
|
|
const decimal = (value: string | null | undefined) =>
|
|
preserveMissing && value === undefined ? undefined : this.decimal(value);
|
|
const json = (
|
|
value: Record<string, unknown> | null | undefined,
|
|
): Prisma.InputJsonValue | Prisma.NullTypes.DbNull | undefined =>
|
|
preserveMissing && value === undefined
|
|
? undefined
|
|
: value === null || value === undefined
|
|
? Prisma.DbNull
|
|
: (value as Prisma.InputJsonValue);
|
|
return {
|
|
productCode: nullable(detail.productCode),
|
|
englishName: nullable(detail.englishName),
|
|
blankDesignUrl: nullable(detail.blankDesignUrl),
|
|
detailsPageVideoUrl: nullable(detail.detailsPageVideoUrl),
|
|
textureName: nullable(detail.textureName),
|
|
productionCycleHours: nullable(detail.productionCycleHours),
|
|
minWeightG: decimal(detail.minWeightG),
|
|
reminder: nullable(detail.reminder),
|
|
productionProcess: nullable(detail.productionProcess),
|
|
materialDescription: nullable(detail.materialDescription),
|
|
productPerformance: nullable(detail.productPerformance),
|
|
applicableScenarios: nullable(detail.applicableScenarios),
|
|
washingInstructions: nullable(detail.washingInstructions),
|
|
specialDescription: nullable(detail.specialDescription),
|
|
designExplanation: nullable(detail.designExplanation),
|
|
designArea: nullable(detail.designArea),
|
|
pictureRequest: nullable(detail.pictureRequest),
|
|
sizeChart: json(detail.sizeChart),
|
|
packageSpecs: json(detail.packageSpecs),
|
|
options: json(detail.options),
|
|
media: json(detail.media),
|
|
};
|
|
}
|
|
|
|
private async replaceCustomVariants(
|
|
tx: Prisma.TransactionClient,
|
|
originGoodId: bigint,
|
|
variants: CustomGoodVariantDto[],
|
|
): Promise<void> {
|
|
await tx.originGoodVariant.deleteMany({ where: { originGoodId } });
|
|
for (const variant of variants) {
|
|
await tx.originGoodVariant.create({
|
|
data: {
|
|
originGoodId,
|
|
sdsVariantId: `custom-${randomUUID()}`,
|
|
sku: variant.sku,
|
|
sizeId: variant.sizeId ?? null,
|
|
sizeName: variant.sizeName ?? null,
|
|
colorId: variant.colorId ?? null,
|
|
colorName: variant.colorName ?? null,
|
|
colorHex: variant.colorHex ?? null,
|
|
imageUrl: variant.imageUrl ?? null,
|
|
price: this.decimal(variant.price),
|
|
originalPrice: this.decimal(variant.originalPrice),
|
|
weightG: this.decimal(variant.weightG),
|
|
boxLengthCm: this.decimal(variant.boxLengthCm),
|
|
boxWidthCm: this.decimal(variant.boxWidthCm),
|
|
boxHeightCm: this.decimal(variant.boxHeightCm),
|
|
enabled: variant.enabled ?? true,
|
|
sortOrder: variant.sortOrder ?? 0,
|
|
designData:
|
|
variant.designData === null || variant.designData === undefined
|
|
? Prisma.DbNull
|
|
: (variant.designData as Prisma.InputJsonValue),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|