@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* v3.5.4:为尚无 SKU 的商品回填默认 SKU(与 migrate-product-sku-v354.sql 一致)
|
||||
* 用法:cd server/dukang-api && npx ts-node prisma/backfill-product-skus.ts
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const products = await prisma.commonProductItem.findMany({
|
||||
include: { skus: { select: { id: true }, take: 1 } },
|
||||
});
|
||||
let created = 0;
|
||||
for (const p of products) {
|
||||
if (p.skus.length > 0) continue;
|
||||
await prisma.commonProductSku.create({
|
||||
data: {
|
||||
productId: p.id,
|
||||
skuCode: p.skuCode,
|
||||
barcode69: p.barcode69,
|
||||
specKey: '',
|
||||
specText: p.spec,
|
||||
price: p.price,
|
||||
benefitAmount: p.benefitAmount,
|
||||
status: p.status,
|
||||
allowOnSitePickup: p.allowOnSitePickup,
|
||||
allowOnlinePurchase: p.allowOnlinePurchase,
|
||||
allowCrossCityDelivery: p.allowCrossCityDelivery,
|
||||
saleUnit: 'BOTTLE',
|
||||
bottlesPerUnit: 1,
|
||||
isDefault: true,
|
||||
sortOrder: 0,
|
||||
},
|
||||
});
|
||||
created += 1;
|
||||
console.log(`created default sku for product ${p.id} ${p.skuCode}`);
|
||||
}
|
||||
console.log(`done, created=${created}, scanned=${products.length}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,97 @@
|
||||
-- v3.5.4 商品规格:SPU + SKU
|
||||
-- 1) 新建规格/SKU 表 2) 订单快照列 3) SPU 去掉 sku/barcode 唯一 4) 存量默认 SKU 回填
|
||||
|
||||
-- 销售单位枚举(Prisma 用字符串枚举映射;MySQL 用 VARCHAR,此处仅建表)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS common_product_spec_attr (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(32) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_spec_attr_product (product_id, sort_order),
|
||||
CONSTRAINT fk_spec_attr_product FOREIGN KEY (product_id) REFERENCES common_product_item (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS common_product_spec_value (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
attr_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_spec_value_attr (attr_id, sort_order),
|
||||
CONSTRAINT fk_spec_value_attr FOREIGN KEY (attr_id) REFERENCES common_product_spec_attr (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS common_product_sku (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
sku_code VARCHAR(32) NOT NULL,
|
||||
barcode_69 VARCHAR(32) NOT NULL,
|
||||
spec_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
spec_text VARCHAR(128) NOT NULL DEFAULT '',
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
benefit_amount DECIMAL(10, 2) NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'DRAFT',
|
||||
allow_on_site_pickup TINYINT(1) NOT NULL DEFAULT 0,
|
||||
allow_online_purchase TINYINT(1) NOT NULL DEFAULT 1,
|
||||
allow_cross_city_delivery TINYINT(1) NOT NULL DEFAULT 1,
|
||||
sale_unit VARCHAR(16) NOT NULL DEFAULT 'BOTTLE',
|
||||
bottles_per_unit INT NOT NULL DEFAULT 1,
|
||||
is_default TINYINT(1) NOT NULL DEFAULT 0,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_product_sku_code (sku_code),
|
||||
UNIQUE KEY uk_product_sku_barcode (barcode_69),
|
||||
UNIQUE KEY uk_product_sku_spec_key (product_id, spec_key),
|
||||
KEY idx_product_sku_status (product_id, status),
|
||||
KEY idx_product_sku_default (product_id, is_default),
|
||||
CONSTRAINT fk_product_sku_product FOREIGN KEY (product_id) REFERENCES common_product_item (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS common_product_sku_spec (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
sku_id BIGINT UNSIGNED NOT NULL,
|
||||
value_id BIGINT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_sku_spec (sku_id, value_id),
|
||||
KEY idx_sku_spec_value (value_id),
|
||||
CONSTRAINT fk_sku_spec_sku FOREIGN KEY (sku_id) REFERENCES common_product_sku (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_sku_spec_value FOREIGN KEY (value_id) REFERENCES common_product_spec_value (id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 订单快照列
|
||||
ALTER TABLE user_order
|
||||
ADD COLUMN sku_id BIGINT UNSIGNED NULL AFTER product_id,
|
||||
ADD COLUMN sale_unit VARCHAR(16) NOT NULL DEFAULT 'BOTTLE' AFTER quantity,
|
||||
ADD COLUMN bottles_per_unit INT NOT NULL DEFAULT 1 AFTER sale_unit;
|
||||
|
||||
ALTER TABLE user_order
|
||||
ADD KEY idx_order_sku_id (sku_id);
|
||||
|
||||
-- SPU:去掉 sku_code / barcode_69 唯一(保留普通索引);若索引名不同可手工调整
|
||||
ALTER TABLE common_product_item DROP INDEX sku_code;
|
||||
ALTER TABLE common_product_item DROP INDEX barcode_69;
|
||||
ALTER TABLE common_product_item ADD INDEX idx_product_item_sku_code (sku_code);
|
||||
ALTER TABLE common_product_item ADD INDEX idx_product_item_barcode_69 (barcode_69);
|
||||
|
||||
-- 存量商品回填默认 SKU(无规格,瓶装)
|
||||
INSERT INTO common_product_sku (
|
||||
product_id, sku_code, barcode_69, spec_key, spec_text, price, benefit_amount, status,
|
||||
allow_on_site_pickup, allow_online_purchase, allow_cross_city_delivery,
|
||||
sale_unit, bottles_per_unit, is_default, sort_order, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
p.id, p.sku_code, p.barcode_69, '', COALESCE(p.spec, ''), p.price, p.benefit_amount, p.status,
|
||||
p.allow_on_site_pickup, p.allow_online_purchase, p.allow_cross_city_delivery,
|
||||
'BOTTLE', 1, 1, 0, NOW(3), NOW(3)
|
||||
FROM common_product_item p
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM common_product_sku s WHERE s.product_id = p.id
|
||||
);
|
||||
@@ -164,6 +164,12 @@ enum ProductStatus {
|
||||
OFF_SALE
|
||||
}
|
||||
|
||||
/// SKU 销售单位:瓶 / 箱(起购与物流按瓶当量)
|
||||
enum ProductSaleUnit {
|
||||
BOTTLE
|
||||
BOX
|
||||
}
|
||||
|
||||
enum DetailTemplateStatus {
|
||||
ACTIVE
|
||||
DISABLED
|
||||
@@ -791,10 +797,13 @@ model DevPlanTaskDispatch {
|
||||
@@map("dev_plan_task_dispatch")
|
||||
}
|
||||
|
||||
/// 商品 SPU(详情页实体);价格/码/履约等列为默认 SKU 冗余,供旧客户端拍平读取
|
||||
model CommonProductItem {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
skuCode String @unique @map("sku_code") @db.VarChar(32)
|
||||
barcode69 String @unique @map("barcode_69") @db.VarChar(32)
|
||||
/// 默认 SKU 冗余;唯一约束已下放到 common_product_sku
|
||||
skuCode String @map("sku_code") @db.VarChar(32)
|
||||
/// 默认 SKU 冗余;唯一约束已下放到 common_product_sku
|
||||
barcode69 String @map("barcode_69") @db.VarChar(32)
|
||||
name String @db.VarChar(128)
|
||||
subtitle String? @db.VarChar(256)
|
||||
aromaType AromaType @map("aroma_type")
|
||||
@@ -818,11 +827,94 @@ model CommonProductItem {
|
||||
coverResource CommonResource? @relation("ProductCover", fields: [coverResourceId], references: [id], onDelete: SetNull)
|
||||
orders Order[]
|
||||
visibilityPhones CommonProductVisibilityPhone[]
|
||||
specAttrs CommonProductSpecAttr[]
|
||||
skus CommonProductSku[]
|
||||
|
||||
@@index([skuCode])
|
||||
@@index([barcode69])
|
||||
@@index([status, aromaType])
|
||||
@@map("common_product_item")
|
||||
}
|
||||
|
||||
/// 销售规格轴(如「包装」)
|
||||
model CommonProductSpecAttr {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
productId BigInt @map("product_id") @db.UnsignedBigInt
|
||||
name String @db.VarChar(32)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
product CommonProductItem @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
values CommonProductSpecValue[]
|
||||
|
||||
@@index([productId, sortOrder])
|
||||
@@map("common_product_spec_attr")
|
||||
}
|
||||
|
||||
/// 规格值(如「单瓶」「整箱6瓶」)
|
||||
model CommonProductSpecValue {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
attrId BigInt @map("attr_id") @db.UnsignedBigInt
|
||||
name String @db.VarChar(64)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
attr CommonProductSpecAttr @relation(fields: [attrId], references: [id], onDelete: Cascade)
|
||||
skuSpecs CommonProductSkuSpec[]
|
||||
|
||||
@@index([attrId, sortOrder])
|
||||
@@map("common_product_spec_value")
|
||||
}
|
||||
|
||||
/// 可售 SKU(规格组合)
|
||||
model CommonProductSku {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
productId BigInt @map("product_id") @db.UnsignedBigInt
|
||||
skuCode String @unique @map("sku_code") @db.VarChar(32)
|
||||
barcode69 String @unique @map("barcode_69") @db.VarChar(32)
|
||||
/// 规格值 id 按 attr.sortOrder 拼接,无规格为 ""
|
||||
specKey String @default("") @map("spec_key") @db.VarChar(128)
|
||||
specText String @default("") @map("spec_text") @db.VarChar(128)
|
||||
price Decimal @db.Decimal(10, 2)
|
||||
benefitAmount Decimal? @map("benefit_amount") @db.Decimal(10, 2)
|
||||
status ProductStatus @default(DRAFT)
|
||||
allowOnSitePickup Boolean @default(false) @map("allow_on_site_pickup")
|
||||
allowOnlinePurchase Boolean @default(true) @map("allow_online_purchase")
|
||||
allowCrossCityDelivery Boolean @default(true) @map("allow_cross_city_delivery")
|
||||
saleUnit ProductSaleUnit @default(BOTTLE) @map("sale_unit")
|
||||
/// 每销售单位对应瓶数(瓶=1,箱默认=6)
|
||||
bottlesPerUnit Int @default(1) @map("bottles_per_unit")
|
||||
isDefault Boolean @default(false) @map("is_default")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
product CommonProductItem @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
skuSpecs CommonProductSkuSpec[]
|
||||
orders Order[]
|
||||
|
||||
@@unique([productId, specKey])
|
||||
@@index([productId, status])
|
||||
@@index([productId, isDefault])
|
||||
@@map("common_product_sku")
|
||||
}
|
||||
|
||||
/// SKU ↔ 规格值
|
||||
model CommonProductSkuSpec {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
skuId BigInt @map("sku_id") @db.UnsignedBigInt
|
||||
valueId BigInt @map("value_id") @db.UnsignedBigInt
|
||||
|
||||
sku CommonProductSku @relation(fields: [skuId], references: [id], onDelete: Cascade)
|
||||
value CommonProductSpecValue @relation(fields: [valueId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@unique([skuId, valueId])
|
||||
@@index([valueId])
|
||||
@@map("common_product_sku_spec")
|
||||
}
|
||||
|
||||
/// Product visibility whitelist phones (match by bound phone)
|
||||
model CommonProductVisibilityPhone {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
@@ -1495,11 +1587,16 @@ model Order {
|
||||
promoCodeId BigInt? @map("promo_code_id") @db.UnsignedBigInt
|
||||
channelSource String? @map("channel_source") @db.VarChar(128)
|
||||
productId BigInt @map("product_id") @db.UnsignedBigInt
|
||||
/// v3.5.4 起可空;历史单无 sku 快照时仍用 product 冗余字段展示
|
||||
skuId BigInt? @map("sku_id") @db.UnsignedBigInt
|
||||
barcode69 String @map("barcode_69") @db.VarChar(32)
|
||||
productName String @map("product_name") @db.VarChar(128)
|
||||
productSpec String @map("product_spec") @db.VarChar(128)
|
||||
imageResourceId BigInt? @map("image_resource_id") @db.UnsignedBigInt
|
||||
/// 下单数量(销售单位:瓶或箱)
|
||||
quantity Int
|
||||
saleUnit ProductSaleUnit @default(BOTTLE) @map("sale_unit")
|
||||
bottlesPerUnit Int @default(1) @map("bottles_per_unit")
|
||||
listUnitPrice Decimal @map("list_unit_price") @db.Decimal(10, 2)
|
||||
listAmount Decimal @map("list_amount") @db.Decimal(10, 2)
|
||||
discountAmount Decimal @default(0) @map("discount_amount") @db.Decimal(10, 2)
|
||||
@@ -1553,6 +1650,7 @@ model Order {
|
||||
reshipments Order[] @relation("OrderReshipment")
|
||||
promoCode CommonPromoCode? @relation(fields: [promoCodeId], references: [id], onDelete: SetNull)
|
||||
product CommonProductItem @relation(fields: [productId], references: [id], onDelete: Restrict)
|
||||
sku CommonProductSku? @relation(fields: [skuId], references: [id], onDelete: Restrict)
|
||||
imageResource CommonResource? @relation("OrderProductImage", fields: [imageResourceId], references: [id], onDelete: SetNull)
|
||||
delivery OrderDelivery?
|
||||
benefitCoupon BenefitCoupon?
|
||||
@@ -1561,6 +1659,7 @@ model Order {
|
||||
@@index([userId, status])
|
||||
@@index([cityId, createdAt])
|
||||
@@index([productId])
|
||||
@@index([skuId])
|
||||
@@index([barcode69])
|
||||
@@index([payExternalNo])
|
||||
@@index([ipCity])
|
||||
|
||||
@@ -412,6 +412,26 @@ async function main() {
|
||||
|
||||
});
|
||||
|
||||
await prisma.commonProductSku.create({
|
||||
data: {
|
||||
productId: product.id,
|
||||
skuCode: def.skuCode,
|
||||
barcode69: `69000000000${i + 1}`,
|
||||
specKey: '',
|
||||
specText: '500ml | 53度',
|
||||
price: def.price,
|
||||
benefitAmount: def.price,
|
||||
status: 'ON_SALE',
|
||||
allowOnSitePickup: false,
|
||||
allowOnlinePurchase: true,
|
||||
allowCrossCityDelivery: true,
|
||||
saleUnit: 'BOTTLE',
|
||||
bottlesPerUnit: 1,
|
||||
isDefault: true,
|
||||
sortOrder: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const cover = await createMockResource(ResourceOwnerType.PRODUCT, product.id, ResourceBizType.COVER, def.img);
|
||||
|
||||
await prisma.commonProductItem.update({
|
||||
|
||||
Reference in New Issue
Block a user