180 lines
5.8 KiB
TypeScript
180 lines
5.8 KiB
TypeScript
/**
|
||
* 为现有产品添加更多 SKU 数据
|
||
*
|
||
* 运行: npx tsx scripts/add-more-skus.ts
|
||
*/
|
||
|
||
const CONFIRM_TOKEN = 'add-local-demo-skus'
|
||
type PrismaSingleton = typeof import('../src/lib/prisma')['prisma']
|
||
|
||
let prisma: PrismaSingleton
|
||
|
||
function assertLocalScriptAllowed() {
|
||
if (!process.argv.includes(`--confirm=${CONFIRM_TOKEN}`)) {
|
||
throw new Error(`add-more-skus 只允许显式本地补数:请追加 --confirm=${CONFIRM_TOKEN}。`)
|
||
}
|
||
if (process.env.NODE_ENV === 'production') {
|
||
throw new Error('add-more-skus 禁止在 NODE_ENV=production 下执行。')
|
||
}
|
||
const databaseUrl = process.env.DATABASE_URL || ''
|
||
if (!databaseUrl) {
|
||
throw new Error('add-more-skus 必须先配置 DATABASE_URL。')
|
||
}
|
||
const host = new URL(databaseUrl).hostname
|
||
if (!['localhost', '127.0.0.1', '::1'].includes(host)) {
|
||
throw new Error('add-more-skus 只允许连接本机数据库。')
|
||
}
|
||
}
|
||
|
||
// 示例模型 URL(使用公开的 GLTF 模型)
|
||
const sampleModels = [
|
||
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/Box/glTF-Binary/Box.glb',
|
||
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/Duck/glTF-Binary/Duck.glb',
|
||
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/Avocado/glTF-Binary/Avocado.glb',
|
||
]
|
||
|
||
// 示例缩略图(使用占位图服务)
|
||
const sampleThumbnails = [
|
||
'https://via.placeholder.com/400x300/008D74/FFFFFF?text=SKU+Preview+1',
|
||
'https://via.placeholder.com/400x300/47C4AF/FFFFFF?text=SKU+Preview+2',
|
||
'https://via.placeholder.com/400x300/96D4C9/333333?text=SKU+Preview+3',
|
||
'https://via.placeholder.com/400x300/B9E2D5/333333?text=SKU+Preview+4',
|
||
'https://via.placeholder.com/400x300/CFEAE2/333333?text=SKU+Preview+5',
|
||
]
|
||
|
||
// 颜色选项
|
||
const colors = ['米白色', '浅灰色', '深灰色', '原木色', '胡桃色', '黑色', '白色']
|
||
|
||
// 尺寸选项
|
||
const sizes = ['小号', '中号', '大号', 'S', 'M', 'L', 'XL']
|
||
|
||
// 材质选项
|
||
const materials = ['实木', '板材', '金属', '布艺', '皮革', '玻璃']
|
||
|
||
async function addMoreSkus() {
|
||
assertLocalScriptAllowed()
|
||
;({ prisma } = await import('../src/lib/prisma'))
|
||
console.log('开始为产品添加更多 SKU...\n')
|
||
|
||
// 获取所有用户作为潜在负责人
|
||
const users = await prisma.user.findMany({
|
||
where: { status: 'ACTIVE' },
|
||
select: { id: true, name: true }
|
||
})
|
||
|
||
if (users.length === 0) {
|
||
console.log('错误: 没有找到活跃用户')
|
||
return
|
||
}
|
||
|
||
console.log(`找到 ${users.length} 个活跃用户`)
|
||
|
||
// 获取 SKU 数量较少的产品(0-2个)
|
||
const products = await prisma.product.findMany({
|
||
include: {
|
||
skus: true,
|
||
_count: { select: { skus: true } }
|
||
},
|
||
orderBy: { createdAt: 'asc' }
|
||
})
|
||
|
||
console.log(`找到 ${products.length} 个产品\n`)
|
||
|
||
let totalCreated = 0
|
||
|
||
for (const product of products) {
|
||
const existingSkuCount = product._count.skus
|
||
|
||
// 如果已有 3 个以上 SKU,跳过
|
||
if (existingSkuCount >= 3) {
|
||
console.log(`[跳过] ${product.modelNumber} 已有 ${existingSkuCount} 个 SKU`)
|
||
continue
|
||
}
|
||
|
||
// 为每个产品添加 3-5 个 SKU
|
||
const skusToAdd = Math.min(5 - existingSkuCount, 3 + Math.floor(Math.random() * 3))
|
||
|
||
console.log(`\n[处理] ${product.modelNumber} (${product.name}) - 添加 ${skusToAdd} 个 SKU`)
|
||
|
||
for (let i = 0; i < skusToAdd; i++) {
|
||
const skuIndex = existingSkuCount + i + 1
|
||
const color = colors[Math.floor(Math.random() * colors.length)]
|
||
const size = sizes[Math.floor(Math.random() * sizes.length)]
|
||
const material = materials[Math.floor(Math.random() * materials.length)]
|
||
const owner = users[Math.floor(Math.random() * users.length)]
|
||
|
||
const skuCode = `${product.modelNumber}-${color.substring(0, 2)}-${size}`.trim().toUpperCase()
|
||
const normalizedSkuCode = skuCode.trim().toUpperCase()
|
||
|
||
// 检查 SKU 编码是否已存在
|
||
const existing = await prisma.productSKU.findUnique({
|
||
where: { skuCode: normalizedSkuCode }
|
||
})
|
||
|
||
if (existing) {
|
||
console.log(` - 跳过: ${skuCode} 已存在`)
|
||
continue
|
||
}
|
||
|
||
const price = 500 + Math.floor(Math.random() * 5000)
|
||
const costPrice = Math.floor(price * (0.4 + Math.random() * 0.3))
|
||
|
||
const sku = await prisma.productSKU.create({
|
||
data: {
|
||
productId: product.id,
|
||
ownerId: owner.id,
|
||
skuCode,
|
||
price,
|
||
costPrice,
|
||
stock: Math.floor(Math.random() * 100),
|
||
isActive: Math.random() > 0.1, // 90% 概率上架
|
||
attributes: {
|
||
颜色: color,
|
||
尺寸: size,
|
||
材质: material,
|
||
},
|
||
images: [
|
||
sampleThumbnails[skuIndex % sampleThumbnails.length],
|
||
sampleThumbnails[(skuIndex + 1) % sampleThumbnails.length],
|
||
],
|
||
modelUrl: sampleModels[skuIndex % sampleModels.length],
|
||
}
|
||
})
|
||
|
||
console.log(` ✓ 创建 SKU: ${sku.skuCode} (${color}/${size}/${material}) - ¥${price}`)
|
||
totalCreated++
|
||
}
|
||
}
|
||
|
||
console.log(`\n========================================`)
|
||
console.log(`完成! 共创建 ${totalCreated} 个新 SKU`)
|
||
console.log(`========================================\n`)
|
||
|
||
// 显示统计
|
||
const stats = await prisma.product.findMany({
|
||
select: {
|
||
modelNumber: true,
|
||
name: true,
|
||
_count: { select: { skus: true } }
|
||
},
|
||
orderBy: { createdAt: 'asc' },
|
||
take: 20
|
||
})
|
||
|
||
console.log('产品 SKU 统计 (前20个):')
|
||
console.log('----------------------------------------')
|
||
stats.forEach(p => {
|
||
console.log(`${p.modelNumber.padEnd(12)} ${p.name.padEnd(20)} ${p._count.skus} 个 SKU`)
|
||
})
|
||
|
||
}
|
||
|
||
addMoreSkus()
|
||
.catch((error) => {
|
||
console.error(error)
|
||
process.exitCode = 1
|
||
})
|
||
.finally(async () => {
|
||
await prisma?.$disconnect()
|
||
})
|