|
|
|
@@ -1,10 +1,22 @@
|
|
|
|
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
|
|
|
import { Prisma } from '@prisma/client';
|
|
|
|
|
import { BOTTLES_PER_BOX } from '@dukang/domain';
|
|
|
|
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
|
|
|
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
|
|
|
|
import { groupResourcesByProductId, mapProductMedia } from '../catalog/catalog.mapper';
|
|
|
|
|
import {
|
|
|
|
|
buildSpecKey,
|
|
|
|
|
buildSpecText,
|
|
|
|
|
mapSkuDto,
|
|
|
|
|
mapSpecAttrsDto,
|
|
|
|
|
} from '../catalog/product-sku.util';
|
|
|
|
|
import type { AdminProductsQueryDto } from './dto/admin-query.dto';
|
|
|
|
|
import type { CreateProductDto, UpdateProductDto } from './dto/admin-mutate.dto';
|
|
|
|
|
import type {
|
|
|
|
|
CreateProductDto,
|
|
|
|
|
SaveProductSkusDto,
|
|
|
|
|
SaveProductSpecsDto,
|
|
|
|
|
UpdateProductDto,
|
|
|
|
|
} from './dto/admin-mutate.dto';
|
|
|
|
|
import { TestWhitelistService } from '../../common/test-whitelist/test-whitelist.service';
|
|
|
|
|
|
|
|
|
|
function normalizePhones(phones?: string[]): string[] {
|
|
|
|
@@ -27,6 +39,8 @@ function normalizePhones(phones?: string[]): string[] {
|
|
|
|
|
|
|
|
|
|
const SKU_AUTO_PREFIX = 'DK';
|
|
|
|
|
const SKU_AUTO_PAD = 6;
|
|
|
|
|
const MAX_SPEC_ATTRS = 3;
|
|
|
|
|
const MAX_SPEC_VALUES = 10;
|
|
|
|
|
|
|
|
|
|
/** 解析履约开关:无线上则强制不可跨城;须至少线上或现场之一 */
|
|
|
|
|
function resolveFulfillmentFlags(input: {
|
|
|
|
@@ -80,6 +94,8 @@ export class AdminProductsService {
|
|
|
|
|
include: {
|
|
|
|
|
coverResource: true,
|
|
|
|
|
visibilityPhones: { select: { phone: true }, orderBy: { phone: 'asc' } },
|
|
|
|
|
skus: { select: { id: true }, take: 2 },
|
|
|
|
|
specAttrs: { select: { id: true } },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
this.prisma.commonProductItem.count({ where }),
|
|
|
|
@@ -100,7 +116,14 @@ export class AdminProductsService {
|
|
|
|
|
const resourceMap = groupResourcesByProductId(resources);
|
|
|
|
|
|
|
|
|
|
return serializeBigInt({
|
|
|
|
|
items: items.map((p) => this.formatProduct(p, resourceMap.get(p.id.toString()) ?? [])),
|
|
|
|
|
items: items.map((p) => {
|
|
|
|
|
const { skus, specAttrs, ...rest } = p;
|
|
|
|
|
return {
|
|
|
|
|
...this.formatProduct(rest as never, resourceMap.get(p.id.toString()) ?? []),
|
|
|
|
|
specEnabled: specAttrs.length > 0 || skus.length > 1,
|
|
|
|
|
skuCount: skus.length,
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
total,
|
|
|
|
|
page,
|
|
|
|
|
pageSize,
|
|
|
|
@@ -113,6 +136,14 @@ export class AdminProductsService {
|
|
|
|
|
include: {
|
|
|
|
|
coverResource: true,
|
|
|
|
|
visibilityPhones: { select: { phone: true }, orderBy: { phone: 'asc' } },
|
|
|
|
|
skus: {
|
|
|
|
|
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
|
|
|
|
include: { skuSpecs: { select: { valueId: true } } },
|
|
|
|
|
},
|
|
|
|
|
specAttrs: {
|
|
|
|
|
orderBy: { sortOrder: 'asc' },
|
|
|
|
|
include: { values: { orderBy: { sortOrder: 'asc' } } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (!product) throw new NotFoundException('商品不存在');
|
|
|
|
@@ -127,11 +158,18 @@ export class AdminProductsService {
|
|
|
|
|
orderBy: { sortOrder: 'asc' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return serializeBigInt(this.formatProduct(product, resources));
|
|
|
|
|
const { skus, specAttrs, ...rest } = product;
|
|
|
|
|
return serializeBigInt({
|
|
|
|
|
...this.formatProduct(rest as never, resources),
|
|
|
|
|
specEnabled: specAttrs.length > 0 || skus.length > 1,
|
|
|
|
|
specAttrs: mapSpecAttrsDto(specAttrs),
|
|
|
|
|
skus: skus.map(mapSkuDto),
|
|
|
|
|
defaultSkuId: skus.find((s) => s.isDefault)?.id.toString() ?? skus[0]?.id.toString(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async create(dto: CreateProductDto) {
|
|
|
|
|
const barcodeExists = await this.prisma.commonProductItem.findFirst({
|
|
|
|
|
const barcodeExists = await this.prisma.commonProductSku.findFirst({
|
|
|
|
|
where: { barcode69: dto.barcode69 },
|
|
|
|
|
});
|
|
|
|
|
if (barcodeExists) throw new BadRequestException('69 码已存在');
|
|
|
|
@@ -147,7 +185,6 @@ export class AdminProductsService {
|
|
|
|
|
if (whitelistEnabled) {
|
|
|
|
|
await this.testWhitelist.assertGlobalWhitelistNotEmpty();
|
|
|
|
|
}
|
|
|
|
|
// 手机号统一在「白名单管理」维护;此处忽略分实体 phones(兼容旧客户端传参)
|
|
|
|
|
void phones;
|
|
|
|
|
|
|
|
|
|
const product = await this.createWithGeneratedSku({
|
|
|
|
@@ -169,6 +206,26 @@ export class AdminProductsService {
|
|
|
|
|
: {}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.prisma.commonProductSku.create({
|
|
|
|
|
data: {
|
|
|
|
|
productId: product.id,
|
|
|
|
|
skuCode: product.skuCode,
|
|
|
|
|
barcode69: product.barcode69,
|
|
|
|
|
specKey: '',
|
|
|
|
|
specText: product.spec,
|
|
|
|
|
price: product.price,
|
|
|
|
|
benefitAmount: product.benefitAmount,
|
|
|
|
|
status: product.status,
|
|
|
|
|
allowOnSitePickup: product.allowOnSitePickup,
|
|
|
|
|
allowOnlinePurchase: product.allowOnlinePurchase,
|
|
|
|
|
allowCrossCityDelivery: product.allowCrossCityDelivery,
|
|
|
|
|
saleUnit: 'BOTTLE',
|
|
|
|
|
bottlesPerUnit: 1,
|
|
|
|
|
isDefault: true,
|
|
|
|
|
sortOrder: 0,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (dto.coverUrl) {
|
|
|
|
|
await this.syncCover(product.id, dto.coverUrl);
|
|
|
|
|
}
|
|
|
|
@@ -230,7 +287,9 @@ export class AdminProductsService {
|
|
|
|
|
if (dto.visibilityWhitelistEnabled) {
|
|
|
|
|
await this.testWhitelist.assertGlobalWhitelistNotEmpty();
|
|
|
|
|
}
|
|
|
|
|
// 分实体手机号已废弃;忽略 dto.visibilityPhones
|
|
|
|
|
|
|
|
|
|
// 无规格 payload 时:同步默认 SKU(兼容旧 admin 表单)
|
|
|
|
|
await this.syncDefaultSkuFromProduct(id);
|
|
|
|
|
|
|
|
|
|
if (dto.coverUrl) {
|
|
|
|
|
await this.syncCover(id, dto.coverUrl);
|
|
|
|
@@ -243,6 +302,281 @@ export class AdminProductsService {
|
|
|
|
|
return this.detail(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async saveSpecs(productId: bigint, dto: SaveProductSpecsDto) {
|
|
|
|
|
const product = await this.prisma.commonProductItem.findUnique({ where: { id: productId } });
|
|
|
|
|
if (!product) throw new NotFoundException('商品不存在');
|
|
|
|
|
|
|
|
|
|
const attrs = dto.attrs ?? [];
|
|
|
|
|
if (attrs.length > MAX_SPEC_ATTRS) {
|
|
|
|
|
throw new BadRequestException(`规格轴最多 ${MAX_SPEC_ATTRS} 个`);
|
|
|
|
|
}
|
|
|
|
|
for (const attr of attrs) {
|
|
|
|
|
if ((attr.values?.length ?? 0) > MAX_SPEC_VALUES) {
|
|
|
|
|
throw new BadRequestException(`每个规格轴最多 ${MAX_SPEC_VALUES} 个值`);
|
|
|
|
|
}
|
|
|
|
|
if (!attr.values?.length) {
|
|
|
|
|
throw new BadRequestException(`规格「${attr.name}」至少需要一个值`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existingAttrs = await this.prisma.commonProductSpecAttr.findMany({
|
|
|
|
|
where: { productId },
|
|
|
|
|
include: { values: true },
|
|
|
|
|
});
|
|
|
|
|
const existingValueIds = existingAttrs.flatMap((a) => a.values.map((v) => v.id));
|
|
|
|
|
const keepValueIds = new Set(
|
|
|
|
|
attrs.flatMap((a) => (a.values ?? []).map((v) => v.id).filter(Boolean) as string[]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (const vid of existingValueIds) {
|
|
|
|
|
if (keepValueIds.has(vid.toString())) continue;
|
|
|
|
|
const used = await this.prisma.commonProductSkuSpec.count({ where: { valueId: vid } });
|
|
|
|
|
if (used > 0) {
|
|
|
|
|
const orderCount = await this.prisma.order.count({
|
|
|
|
|
where: { sku: { skuSpecs: { some: { valueId: vid } } } },
|
|
|
|
|
});
|
|
|
|
|
if (orderCount > 0) {
|
|
|
|
|
throw new BadRequestException('有订单关联的规格值不可删除');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
// 删除未保留的轴(级联值);先清 sku_spec 中将被删的 value
|
|
|
|
|
const keepAttrIds = new Set(attrs.map((a) => a.id).filter(Boolean) as string[]);
|
|
|
|
|
for (const old of existingAttrs) {
|
|
|
|
|
if (!keepAttrIds.has(old.id.toString())) {
|
|
|
|
|
await tx.commonProductSkuSpec.deleteMany({
|
|
|
|
|
where: { valueId: { in: old.values.map((v) => v.id) } },
|
|
|
|
|
});
|
|
|
|
|
await tx.commonProductSpecAttr.delete({ where: { id: old.id } });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let ai = 0; ai < attrs.length; ai++) {
|
|
|
|
|
const attr = attrs[ai];
|
|
|
|
|
let attrId: bigint;
|
|
|
|
|
if (attr.id) {
|
|
|
|
|
attrId = BigInt(attr.id);
|
|
|
|
|
await tx.commonProductSpecAttr.update({
|
|
|
|
|
where: { id: attrId },
|
|
|
|
|
data: { name: attr.name.trim(), sortOrder: attr.sortOrder ?? ai },
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const created = await tx.commonProductSpecAttr.create({
|
|
|
|
|
data: {
|
|
|
|
|
productId,
|
|
|
|
|
name: attr.name.trim(),
|
|
|
|
|
sortOrder: attr.sortOrder ?? ai,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
attrId = created.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const oldValues = await tx.commonProductSpecValue.findMany({ where: { attrId } });
|
|
|
|
|
const keepVids = new Set((attr.values ?? []).map((v) => v.id).filter(Boolean) as string[]);
|
|
|
|
|
for (const ov of oldValues) {
|
|
|
|
|
if (!keepVids.has(ov.id.toString())) {
|
|
|
|
|
await tx.commonProductSkuSpec.deleteMany({ where: { valueId: ov.id } });
|
|
|
|
|
await tx.commonProductSpecValue.delete({ where: { id: ov.id } });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let vi = 0; vi < (attr.values ?? []).length; vi++) {
|
|
|
|
|
const val = attr.values[vi];
|
|
|
|
|
if (val.id) {
|
|
|
|
|
await tx.commonProductSpecValue.update({
|
|
|
|
|
where: { id: BigInt(val.id) },
|
|
|
|
|
data: { name: val.name.trim(), sortOrder: val.sortOrder ?? vi },
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await tx.commonProductSpecValue.create({
|
|
|
|
|
data: {
|
|
|
|
|
attrId,
|
|
|
|
|
name: val.name.trim(),
|
|
|
|
|
sortOrder: val.sortOrder ?? vi,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return this.detail(productId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async saveSkus(productId: bigint, dto: SaveProductSkusDto) {
|
|
|
|
|
const product = await this.prisma.commonProductItem.findUnique({
|
|
|
|
|
where: { id: productId },
|
|
|
|
|
include: {
|
|
|
|
|
specAttrs: { include: { values: true }, orderBy: { sortOrder: 'asc' } },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (!product) throw new NotFoundException('商品不存在');
|
|
|
|
|
|
|
|
|
|
const rows = dto.skus ?? [];
|
|
|
|
|
if (!rows.length) throw new BadRequestException('至少保留一个 SKU');
|
|
|
|
|
|
|
|
|
|
const valueNameById = new Map<string, string>();
|
|
|
|
|
const attrValueSets = product.specAttrs.map((a) => {
|
|
|
|
|
const set = new Set(a.values.map((v) => v.id.toString()));
|
|
|
|
|
for (const v of a.values) valueNameById.set(v.id.toString(), v.name);
|
|
|
|
|
return set;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let defaultCount = 0;
|
|
|
|
|
const seenKeys = new Set<string>();
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
const flags = resolveFulfillmentFlags({
|
|
|
|
|
allowOnlinePurchase: row.allowOnlinePurchase,
|
|
|
|
|
allowCrossCityDelivery: row.allowCrossCityDelivery,
|
|
|
|
|
allowOnSitePickup: row.allowOnSitePickup,
|
|
|
|
|
defaults: {
|
|
|
|
|
allowOnlinePurchase: product.allowOnlinePurchase,
|
|
|
|
|
allowCrossCityDelivery: product.allowCrossCityDelivery,
|
|
|
|
|
allowOnSitePickup: product.allowOnSitePickup,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
void flags;
|
|
|
|
|
if (row.isDefault) defaultCount += 1;
|
|
|
|
|
const valueIds = (row.specValueIds ?? []).map((id) => BigInt(id));
|
|
|
|
|
if (attrValueSets.length) {
|
|
|
|
|
const idSet = new Set(valueIds.map((id) => id.toString()));
|
|
|
|
|
if (idSet.size !== valueIds.length) {
|
|
|
|
|
throw new BadRequestException('规格值不可重复');
|
|
|
|
|
}
|
|
|
|
|
for (const set of attrValueSets) {
|
|
|
|
|
const hits = [...set].filter((id) => idSet.has(id));
|
|
|
|
|
if (hits.length !== 1) {
|
|
|
|
|
throw new BadRequestException('每个 SKU 须选择每个规格轴的一个值');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (valueIds.length !== attrValueSets.length) {
|
|
|
|
|
throw new BadRequestException('每个 SKU 须选择每个规格轴的一个值');
|
|
|
|
|
}
|
|
|
|
|
} else if (valueIds.length) {
|
|
|
|
|
throw new BadRequestException('商品尚未配置规格轴');
|
|
|
|
|
}
|
|
|
|
|
const key = buildSpecKey(valueIds);
|
|
|
|
|
if (seenKeys.has(key)) throw new BadRequestException('存在重复规格组合');
|
|
|
|
|
seenKeys.add(key);
|
|
|
|
|
}
|
|
|
|
|
if (defaultCount !== 1) {
|
|
|
|
|
throw new BadRequestException('请且仅指定一个默认 SKU');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const barcodes = rows.map((r) => r.barcode69.trim());
|
|
|
|
|
if (new Set(barcodes).size !== barcodes.length) {
|
|
|
|
|
throw new BadRequestException('69 码不可重复');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
const existing = await tx.commonProductSku.findMany({ where: { productId } });
|
|
|
|
|
const keepIds = new Set(rows.map((r) => r.id).filter(Boolean) as string[]);
|
|
|
|
|
|
|
|
|
|
for (const old of existing) {
|
|
|
|
|
if (keepIds.has(old.id.toString())) continue;
|
|
|
|
|
const orderCount = await tx.order.count({ where: { skuId: old.id } });
|
|
|
|
|
if (orderCount > 0) {
|
|
|
|
|
throw new BadRequestException(`SKU ${old.skuCode} 已有订单,无法删除`);
|
|
|
|
|
}
|
|
|
|
|
await tx.commonProductSkuSpec.deleteMany({ where: { skuId: old.id } });
|
|
|
|
|
await tx.commonProductSku.delete({ where: { id: old.id } });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < rows.length; i++) {
|
|
|
|
|
const row = rows[i];
|
|
|
|
|
const valueIds = (row.specValueIds ?? []).map((id) => BigInt(id));
|
|
|
|
|
const specKey = buildSpecKey(valueIds);
|
|
|
|
|
const specText =
|
|
|
|
|
buildSpecText(valueIds, valueNameById) || product.spec || row.barcode69;
|
|
|
|
|
const flags = resolveFulfillmentFlags({
|
|
|
|
|
allowOnlinePurchase: row.allowOnlinePurchase,
|
|
|
|
|
allowCrossCityDelivery: row.allowCrossCityDelivery,
|
|
|
|
|
allowOnSitePickup: row.allowOnSitePickup,
|
|
|
|
|
defaults: {
|
|
|
|
|
allowOnlinePurchase: product.allowOnlinePurchase,
|
|
|
|
|
allowCrossCityDelivery: product.allowCrossCityDelivery,
|
|
|
|
|
allowOnSitePickup: product.allowOnSitePickup,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const saleUnit = row.saleUnit === 'BOX' ? 'BOX' : 'BOTTLE';
|
|
|
|
|
const bottlesPerUnit =
|
|
|
|
|
row.bottlesPerUnit && row.bottlesPerUnit > 0
|
|
|
|
|
? Math.floor(row.bottlesPerUnit)
|
|
|
|
|
: saleUnit === 'BOX'
|
|
|
|
|
? BOTTLES_PER_BOX
|
|
|
|
|
: 1;
|
|
|
|
|
const status = (row.status ?? 'DRAFT') as 'DRAFT' | 'ON_SALE' | 'OFF_SALE';
|
|
|
|
|
const skuCode = row.skuCode?.trim() || (await this.nextAutoSkuCodeTx(tx));
|
|
|
|
|
|
|
|
|
|
let skuId: bigint;
|
|
|
|
|
if (row.id) {
|
|
|
|
|
skuId = BigInt(row.id);
|
|
|
|
|
await tx.commonProductSku.update({
|
|
|
|
|
where: { id: skuId },
|
|
|
|
|
data: {
|
|
|
|
|
skuCode,
|
|
|
|
|
barcode69: row.barcode69.trim(),
|
|
|
|
|
specKey,
|
|
|
|
|
specText,
|
|
|
|
|
price: row.price,
|
|
|
|
|
benefitAmount: row.benefitAmount ?? row.price,
|
|
|
|
|
status,
|
|
|
|
|
...flags,
|
|
|
|
|
saleUnit,
|
|
|
|
|
bottlesPerUnit,
|
|
|
|
|
isDefault: !!row.isDefault,
|
|
|
|
|
sortOrder: row.sortOrder ?? i,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
await tx.commonProductSkuSpec.deleteMany({ where: { skuId } });
|
|
|
|
|
} else {
|
|
|
|
|
const created = await tx.commonProductSku.create({
|
|
|
|
|
data: {
|
|
|
|
|
productId,
|
|
|
|
|
skuCode,
|
|
|
|
|
barcode69: row.barcode69.trim(),
|
|
|
|
|
specKey,
|
|
|
|
|
specText,
|
|
|
|
|
price: row.price,
|
|
|
|
|
benefitAmount: row.benefitAmount ?? row.price,
|
|
|
|
|
status,
|
|
|
|
|
...flags,
|
|
|
|
|
saleUnit,
|
|
|
|
|
bottlesPerUnit,
|
|
|
|
|
isDefault: !!row.isDefault,
|
|
|
|
|
sortOrder: row.sortOrder ?? i,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
skuId = created.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (valueIds.length) {
|
|
|
|
|
await tx.commonProductSkuSpec.createMany({
|
|
|
|
|
data: valueIds.map((valueId) => ({ skuId, valueId })),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (row.isDefault) {
|
|
|
|
|
await tx.commonProductItem.update({
|
|
|
|
|
where: { id: productId },
|
|
|
|
|
data: {
|
|
|
|
|
skuCode,
|
|
|
|
|
barcode69: row.barcode69.trim(),
|
|
|
|
|
spec: specText,
|
|
|
|
|
price: row.price,
|
|
|
|
|
benefitAmount: row.benefitAmount ?? row.price,
|
|
|
|
|
...flags,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return this.detail(productId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async remove(id: bigint) {
|
|
|
|
|
const product = await this.prisma.commonProductItem.findUnique({ where: { id } });
|
|
|
|
|
if (!product) throw new NotFoundException('商品不存在');
|
|
|
|
@@ -260,15 +594,82 @@ export class AdminProductsService {
|
|
|
|
|
return { ok: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async syncDefaultSkuFromProduct(productId: bigint) {
|
|
|
|
|
const product = await this.prisma.commonProductItem.findUniqueOrThrow({ where: { id: productId } });
|
|
|
|
|
const defaultSku =
|
|
|
|
|
(await this.prisma.commonProductSku.findFirst({
|
|
|
|
|
where: { productId, isDefault: true },
|
|
|
|
|
})) ??
|
|
|
|
|
(await this.prisma.commonProductSku.findFirst({
|
|
|
|
|
where: { productId },
|
|
|
|
|
orderBy: { id: 'asc' },
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
if (!defaultSku) {
|
|
|
|
|
await this.prisma.commonProductSku.create({
|
|
|
|
|
data: {
|
|
|
|
|
productId,
|
|
|
|
|
skuCode: product.skuCode,
|
|
|
|
|
barcode69: product.barcode69,
|
|
|
|
|
specKey: '',
|
|
|
|
|
specText: product.spec,
|
|
|
|
|
price: product.price,
|
|
|
|
|
benefitAmount: product.benefitAmount,
|
|
|
|
|
status: product.status,
|
|
|
|
|
allowOnSitePickup: product.allowOnSitePickup,
|
|
|
|
|
allowOnlinePurchase: product.allowOnlinePurchase,
|
|
|
|
|
allowCrossCityDelivery: product.allowCrossCityDelivery,
|
|
|
|
|
saleUnit: 'BOTTLE',
|
|
|
|
|
bottlesPerUnit: 1,
|
|
|
|
|
isDefault: true,
|
|
|
|
|
sortOrder: 0,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 仅当该商品只有 1 个 SKU 时,旧表单字段同步到默认 SKU(避免误改多规格)
|
|
|
|
|
const skuCount = await this.prisma.commonProductSku.count({ where: { productId } });
|
|
|
|
|
if (skuCount > 1) return;
|
|
|
|
|
|
|
|
|
|
await this.prisma.commonProductSku.update({
|
|
|
|
|
where: { id: defaultSku.id },
|
|
|
|
|
data: {
|
|
|
|
|
skuCode: product.skuCode,
|
|
|
|
|
barcode69: product.barcode69,
|
|
|
|
|
specText: product.spec,
|
|
|
|
|
price: product.price,
|
|
|
|
|
benefitAmount: product.benefitAmount,
|
|
|
|
|
status: product.status,
|
|
|
|
|
allowOnSitePickup: product.allowOnSitePickup,
|
|
|
|
|
allowOnlinePurchase: product.allowOnlinePurchase,
|
|
|
|
|
allowCrossCityDelivery: product.allowCrossCityDelivery,
|
|
|
|
|
isDefault: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 生成 DK + 6 位自增 SKU,冲突重试 */
|
|
|
|
|
private async nextAutoSkuCode(): Promise<string> {
|
|
|
|
|
const rows = await this.prisma.commonProductItem.findMany({
|
|
|
|
|
where: { skuCode: { startsWith: SKU_AUTO_PREFIX } },
|
|
|
|
|
select: { skuCode: true },
|
|
|
|
|
});
|
|
|
|
|
return this.nextAutoSkuCodeTx(this.prisma);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async nextAutoSkuCodeTx(
|
|
|
|
|
db: Prisma.TransactionClient | PrismaService,
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const [fromItem, fromSku] = await Promise.all([
|
|
|
|
|
db.commonProductItem.findMany({
|
|
|
|
|
where: { skuCode: { startsWith: SKU_AUTO_PREFIX } },
|
|
|
|
|
select: { skuCode: true },
|
|
|
|
|
}),
|
|
|
|
|
db.commonProductSku.findMany({
|
|
|
|
|
where: { skuCode: { startsWith: SKU_AUTO_PREFIX } },
|
|
|
|
|
select: { skuCode: true },
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
let maxSeq = 0;
|
|
|
|
|
const re = new RegExp(`^${SKU_AUTO_PREFIX}(\\d+)$`);
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
for (const row of [...fromItem, ...fromSku]) {
|
|
|
|
|
const m = re.exec(row.skuCode);
|
|
|
|
|
if (!m) continue;
|
|
|
|
|
const n = Number(m[1]);
|
|
|
|
@@ -300,16 +701,6 @@ export class AdminProductsService {
|
|
|
|
|
throw new BadRequestException('SKU 生成失败,请重试');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async syncVisibilityPhones(productId: bigint, phones: string[]) {
|
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
await tx.commonProductVisibilityPhone.deleteMany({ where: { productId } });
|
|
|
|
|
if (!phones.length) return;
|
|
|
|
|
await tx.commonProductVisibilityPhone.createMany({
|
|
|
|
|
data: phones.map((phone) => ({ productId, phone })),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private formatProduct(
|
|
|
|
|
product: Prisma.CommonProductItemGetPayload<{
|
|
|
|
|
include: {
|
|
|
|
|