149 lines
6.1 KiB
TypeScript
149 lines
6.1 KiB
TypeScript
const CONFIRM_TOKEN = 'update-local-model-urls'
|
||
|
||
function assertLocalModelUrlUpdateAllowed() {
|
||
if (!process.argv.includes(`--confirm=${CONFIRM_TOKEN}`)) {
|
||
throw new Error(`update-model-urls 只允许显式本地维护:请追加 --confirm=${CONFIRM_TOKEN}。`)
|
||
}
|
||
if (process.env.NODE_ENV === 'production') {
|
||
throw new Error('update-model-urls 禁止在 NODE_ENV=production 下执行。')
|
||
}
|
||
const databaseUrl = process.env.DATABASE_URL || ''
|
||
if (!databaseUrl) {
|
||
throw new Error('update-model-urls 必须先配置 DATABASE_URL。')
|
||
}
|
||
const host = new URL(databaseUrl).hostname
|
||
if (!['localhost', '127.0.0.1', '::1'].includes(host)) {
|
||
throw new Error('update-model-urls 只允许连接本机数据库。')
|
||
}
|
||
}
|
||
|
||
// Three.js 官方示例模型 URL 列表
|
||
const THREE_JS_MODELS = {
|
||
// 高质量 PBR 模型
|
||
DamagedHelmet: 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf',
|
||
FlightHelmet: 'https://threejs.org/examples/models/gltf/FlightHelmet/glTF/FlightHelmet.gltf',
|
||
|
||
// 动物模型
|
||
Horse: 'https://threejs.org/examples/models/gltf/Horse.glb',
|
||
Flamingo: 'https://threejs.org/examples/models/gltf/Flamingo.glb',
|
||
Parrot: 'https://threejs.org/examples/models/gltf/Parrot.glb',
|
||
Stork: 'https://threejs.org/examples/models/gltf/Stork.glb',
|
||
|
||
// 其他模型
|
||
Duck: 'https://threejs.org/examples/models/gltf/Duck/glTF/Duck.gltf',
|
||
BoomBox: 'https://threejs.org/examples/models/gltf/BoomBox/glTF/BoomBox.gltf',
|
||
Avocado: 'https://threejs.org/examples/models/gltf/Avocado/glTF/Avocado.gltf',
|
||
Fox: 'https://threejs.org/examples/models/gltf/Fox/glTF/Fox.gltf',
|
||
|
||
// 角色模型(带动画)
|
||
RobotExpressive: 'https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb',
|
||
Soldier: 'https://threejs.org/examples/models/gltf/Soldier.glb',
|
||
|
||
// 建筑/家具相关
|
||
SheenChair: 'https://threejs.org/examples/models/gltf/SheenChair.glb',
|
||
MaterialsVariantsShoe: 'https://threejs.org/examples/models/gltf/MaterialsVariantsShoe/glTF/MaterialsVariantsShoe.gltf',
|
||
|
||
// 车辆
|
||
Ferrari: 'https://threejs.org/examples/models/gltf/ferrari.glb',
|
||
}
|
||
|
||
// 模型预览图 URL(来自 Three.js 官网截图)
|
||
const THUMBNAILS = {
|
||
DamagedHelmet: 'https://threejs.org/examples/screenshots/webgl_loader_gltf.jpg',
|
||
FlightHelmet: 'https://threejs.org/examples/screenshots/webgl_loader_gltf_compressed.jpg',
|
||
Horse: 'https://threejs.org/examples/screenshots/webgl_morphtargets_horse.jpg',
|
||
Flamingo: 'https://threejs.org/examples/screenshots/webgl_loader_gltf.jpg',
|
||
Duck: 'https://threejs.org/examples/screenshots/webgl_loader_gltf.jpg',
|
||
Fox: 'https://threejs.org/examples/screenshots/webgl_animation_skinning_blending.jpg',
|
||
RobotExpressive: 'https://threejs.org/examples/screenshots/webgl_animation_skinning_morph.jpg',
|
||
Ferrari: 'https://threejs.org/examples/screenshots/webgl_materials_car.jpg',
|
||
SheenChair: 'https://threejs.org/examples/screenshots/webgl_loader_gltf_sheen.jpg',
|
||
}
|
||
|
||
async function main() {
|
||
assertLocalModelUrlUpdateAllowed()
|
||
const { prisma } = await import('../src/lib/prisma')
|
||
console.log('开始更新产品模型 URL...\n')
|
||
|
||
try {
|
||
// 获取所有产品
|
||
const products = await prisma.product.findMany({
|
||
orderBy: { modelNumber: 'asc' }
|
||
})
|
||
|
||
console.log(`找到 ${products.length} 个产品\n`)
|
||
|
||
// 模型分配方案(模拟不同产品使用不同模型)
|
||
const modelAssignments: Record<string, { model: keyof typeof THREE_JS_MODELS; thumbnail?: keyof typeof THUMBNAILS }> = {
|
||
'SF-001': { model: 'SheenChair', thumbnail: 'SheenChair' }, // 沙发 -> 椅子模型
|
||
'TB-001': { model: 'Duck', thumbnail: 'Duck' }, // 餐桌 -> 鸭子(测试)
|
||
'CH-001': { model: 'SheenChair', thumbnail: 'SheenChair' }, // 椅子 -> 椅子模型
|
||
'BD-001': { model: 'Fox', thumbnail: 'Fox' }, // 床 -> 狐狸(测试)
|
||
'CB-001': { model: 'BoomBox' }, // 柜子 -> 音箱(测试)
|
||
'WD-001': { model: 'Avocado' }, // 衣柜 -> 牛油果(测试)
|
||
'DK-001': { model: 'RobotExpressive', thumbnail: 'RobotExpressive' }, // 书桌 -> 机器人
|
||
'SH-001': { model: 'MaterialsVariantsShoe' }, // 鞋柜 -> 鞋子模型
|
||
'TV-001': { model: 'FlightHelmet', thumbnail: 'FlightHelmet' }, // 电视柜 -> 头盔
|
||
'CF-001': { model: 'Horse', thumbnail: 'Horse' }, // 茶几 -> 马(测试)
|
||
}
|
||
|
||
// 默认模型(对于没有特定分配的产品)
|
||
const defaultModels = [
|
||
{ model: 'DamagedHelmet' as const, thumbnail: 'DamagedHelmet' as const },
|
||
{ model: 'Flamingo' as const },
|
||
{ model: 'Parrot' as const },
|
||
{ model: 'Stork' as const },
|
||
{ model: 'Soldier' as const },
|
||
{ model: 'Ferrari' as const, thumbnail: 'Ferrari' as const },
|
||
]
|
||
|
||
let updatedCount = 0
|
||
|
||
for (let i = 0; i < products.length; i++) {
|
||
const product = products[i]
|
||
|
||
// 查找分配的模型
|
||
let assignment = modelAssignments[product.modelNumber]
|
||
|
||
// 如果没有特定分配,使用默认模型轮换
|
||
if (!assignment) {
|
||
assignment = defaultModels[i % defaultModels.length]
|
||
}
|
||
|
||
const modelUrl = THREE_JS_MODELS[assignment.model]
|
||
const thumbnailUrl = assignment.thumbnail ? THUMBNAILS[assignment.thumbnail] : null
|
||
|
||
// 更新产品
|
||
await prisma.product.update({
|
||
where: { id: product.id },
|
||
data: {
|
||
modelUrl,
|
||
...(thumbnailUrl && { thumbnailUrl })
|
||
}
|
||
})
|
||
|
||
console.log(`✓ ${product.modelNumber} (${product.name})`)
|
||
console.log(` 模型: ${assignment.model}`)
|
||
if (thumbnailUrl) {
|
||
console.log(` 预览图: ${assignment.thumbnail}`)
|
||
}
|
||
console.log()
|
||
|
||
updatedCount++
|
||
}
|
||
|
||
console.log(`\n完成!已更新 ${updatedCount} 个产品的模型 URL`)
|
||
|
||
// 显示模型 URL 汇总
|
||
console.log('\n=== 可用的 Three.js 示例模型 ===')
|
||
for (const [name, url] of Object.entries(THREE_JS_MODELS)) {
|
||
console.log(`${name}: ${url}`)
|
||
}
|
||
} finally {
|
||
await prisma.$disconnect()
|
||
}
|
||
}
|
||
|
||
main()
|
||
.catch(console.error)
|