1. 排行榜悬浮窗关闭后小按钮消失问题 - 修复面板居中模式关闭时按钮位置错误 - 保存并恢复按钮的居中状态 2. 首页tag按钮按素材数量排序 - 添加按素材数量降序排序逻辑 3. 上传弹窗输入框浅色模式文字不可见 - 添加 text-slate-900 确保浅色模式下文字可见 - 添加 placeholder 样式确保占位符正确显示
289 lines
10 KiB
TypeScript
289 lines
10 KiB
TypeScript
/**
|
|
* Prisma 数据库种子脚本
|
|
*
|
|
* 用于初始化数据库的基础数据,包括:
|
|
* 1. 内置模板
|
|
* 2. 默认管理员账户(可选)
|
|
* 3. 默认部门、店铺、场景
|
|
*
|
|
* 使用方法:
|
|
* npx prisma db seed
|
|
*
|
|
* 或在 package.json 中配置后自动执行:
|
|
* "prisma": {
|
|
* "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
|
* }
|
|
*/
|
|
|
|
import { PrismaClient } from '@prisma/client'
|
|
import bcrypt from 'bcryptjs'
|
|
|
|
const prisma = new PrismaClient()
|
|
|
|
// ============================================================================
|
|
// 内置模板配置(与 lib/system-integrity.ts 保持一致)
|
|
// ============================================================================
|
|
|
|
const BUILTIN_TEMPLATE_KEYS = [
|
|
'3dmax-material',
|
|
'psd-material',
|
|
'audio-material',
|
|
'video-material',
|
|
'model-material',
|
|
'other-material',
|
|
] as const
|
|
|
|
type BuiltinTemplateKey = (typeof BUILTIN_TEMPLATE_KEYS)[number]
|
|
|
|
const BUILTIN_TEMPLATE_CONFIG: Record<
|
|
BuiltinTemplateKey,
|
|
{
|
|
name: string
|
|
description: string
|
|
materialType: string
|
|
materialKind: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'
|
|
fields: Array<{
|
|
key: string
|
|
label: string
|
|
type: string
|
|
required: boolean
|
|
layoutWidth: string
|
|
visibleOnHomeFilter: boolean
|
|
isSearchable: boolean
|
|
addToCategory?: boolean
|
|
}>
|
|
}
|
|
> = {
|
|
'3dmax-material': {
|
|
name: '3DMAX 素材模板',
|
|
description: '适用于 3D 模型类素材',
|
|
materialType: '3DMAX',
|
|
materialKind: 'IMAGE',
|
|
fields: [
|
|
{ key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true },
|
|
{ key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
{ key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
{ key: 'renderer', label: '渲染器', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true, addToCategory: true },
|
|
],
|
|
},
|
|
'psd-material': {
|
|
name: 'PSD 素材模板',
|
|
description: '针对平面设计 PSD 素材',
|
|
materialType: 'PSD',
|
|
materialKind: 'IMAGE',
|
|
fields: [
|
|
{ key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true },
|
|
{ key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
{ key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
],
|
|
},
|
|
'audio-material': {
|
|
name: '音频素材模板',
|
|
description: '适用于音频文件素材,支持 MP3、WAV、FLAC、AAC 等格式',
|
|
materialType: '音频',
|
|
materialKind: 'AUDIO',
|
|
fields: [
|
|
{ key: 'productModel', label: '音频名称', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true },
|
|
{ key: 'audioCategory', label: '音频分类', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
{ key: 'audioScene', label: '使用场景', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
{ key: 'authorName', label: '作者', type: 'text', required: false, layoutWidth: 'half', visibleOnHomeFilter: false, isSearchable: true },
|
|
],
|
|
},
|
|
'video-material': {
|
|
name: '视频素材模板',
|
|
description: '适用于视频文件素材',
|
|
materialType: '视频',
|
|
materialKind: 'VIDEO',
|
|
fields: [
|
|
{ key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true },
|
|
{ key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
{ key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
],
|
|
},
|
|
'model-material': {
|
|
name: '模特素材模板',
|
|
description: '用于模特/人像素材',
|
|
materialType: '模特',
|
|
materialKind: 'IMAGE',
|
|
fields: [
|
|
{ key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true },
|
|
{ key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
{ key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
],
|
|
},
|
|
'other-material': {
|
|
name: '其他素材模板',
|
|
description: '适用于其他类型的素材',
|
|
materialType: '其他',
|
|
materialKind: 'OTHER',
|
|
fields: [
|
|
{ key: 'productModel', label: 'SPU型号', type: 'text', required: true, layoutWidth: 'full', visibleOnHomeFilter: false, isSearchable: true },
|
|
{ key: 'categoryName', label: '使用店铺', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
{ key: 'sceneName', label: '场景类别', type: 'select', required: true, layoutWidth: 'full', visibleOnHomeFilter: true, isSearchable: true, addToCategory: true },
|
|
],
|
|
},
|
|
}
|
|
|
|
// ============================================================================
|
|
// 种子函数
|
|
// ============================================================================
|
|
|
|
async function seedBuiltinTemplates() {
|
|
console.log('正在创建内置模板...')
|
|
let created = 0
|
|
let skipped = 0
|
|
|
|
for (const key of BUILTIN_TEMPLATE_KEYS) {
|
|
const templateId = `builtin-${key}`
|
|
const config = BUILTIN_TEMPLATE_CONFIG[key]
|
|
|
|
// 检查是否已存在
|
|
const existing = await prisma.uploadTemplate.findUnique({
|
|
where: { id: templateId },
|
|
})
|
|
|
|
if (existing) {
|
|
console.log(` 跳过: ${templateId} (已存在)`)
|
|
skipped++
|
|
continue
|
|
}
|
|
|
|
// 创建模板和字段
|
|
await prisma.$transaction(async (tx) => {
|
|
await tx.uploadTemplate.create({
|
|
data: {
|
|
id: templateId,
|
|
name: config.name,
|
|
description: config.description,
|
|
status: 'active',
|
|
materialType: config.materialType,
|
|
materialKind: config.materialKind,
|
|
version: 1,
|
|
},
|
|
})
|
|
|
|
for (let i = 0; i < config.fields.length; i++) {
|
|
const field = config.fields[i]
|
|
await tx.uploadField.create({
|
|
data: {
|
|
templateId,
|
|
key: field.key,
|
|
label: field.label,
|
|
type: field.type,
|
|
required: field.required,
|
|
orderIndex: i,
|
|
layoutWidth: field.layoutWidth,
|
|
visibleOnHomeFilter: field.visibleOnHomeFilter,
|
|
isSearchable: field.isSearchable,
|
|
addToCategory: field.addToCategory || false,
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|
|
console.log(` 创建: ${templateId}`)
|
|
created++
|
|
}
|
|
|
|
console.log(`内置模板: 创建 ${created} 个, 跳过 ${skipped} 个`)
|
|
}
|
|
|
|
async function seedDefaultAdmin() {
|
|
console.log('正在检查默认管理员...')
|
|
|
|
const adminEmail = process.env.INIT_ADMIN_EMAIL || 'admin@flnxi.com'
|
|
const adminPassword = process.env.INIT_ADMIN_PASSWORD || '88888888'
|
|
|
|
// 检查是否已有管理员
|
|
const existingAdmin = await prisma.user.findFirst({
|
|
where: { role: 'ADMIN' },
|
|
})
|
|
|
|
if (existingAdmin) {
|
|
console.log(` 跳过: 已存在管理员 (${existingAdmin.email})`)
|
|
return
|
|
}
|
|
|
|
// 创建默认管理员
|
|
const hashedPassword = await bcrypt.hash(adminPassword, 10)
|
|
const admin = await prisma.user.create({
|
|
data: {
|
|
email: adminEmail,
|
|
username: adminEmail.split('@')[0],
|
|
password: hashedPassword,
|
|
role: 'ADMIN',
|
|
isSuperAdmin: true,
|
|
isActive: true,
|
|
canDownload: true,
|
|
department: '系统管理',
|
|
},
|
|
})
|
|
|
|
console.log(` 创建: 管理员 ${admin.email}`)
|
|
}
|
|
|
|
async function seedDefaultData() {
|
|
console.log('正在创建默认数据...')
|
|
|
|
// 默认部门
|
|
const dept = await prisma.department.findFirst({ where: { name: '默认部门' } })
|
|
if (!dept) {
|
|
await prisma.department.create({ data: { name: '默认部门' } })
|
|
console.log(' 创建: 默认部门')
|
|
} else {
|
|
console.log(' 跳过: 默认部门 (已存在)')
|
|
}
|
|
|
|
// 默认店铺
|
|
const category = await prisma.category.findFirst({ where: { name: '默认店铺' } })
|
|
if (!category) {
|
|
await prisma.category.create({ data: { name: '默认店铺' } })
|
|
console.log(' 创建: 默认店铺')
|
|
} else {
|
|
console.log(' 跳过: 默认店铺 (已存在)')
|
|
}
|
|
|
|
// 默认场景
|
|
const scene = await prisma.scene.findFirst({ where: { name: '默认场景' } })
|
|
if (!scene) {
|
|
await prisma.scene.create({ data: { name: '默认场景' } })
|
|
console.log(' 创建: 默认场景')
|
|
} else {
|
|
console.log(' 跳过: 默认场景 (已存在)')
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 主函数
|
|
// ============================================================================
|
|
|
|
async function main() {
|
|
console.log('========================================')
|
|
console.log('开始数据库种子初始化')
|
|
console.log('========================================\n')
|
|
|
|
try {
|
|
await seedBuiltinTemplates()
|
|
console.log('')
|
|
await seedDefaultAdmin()
|
|
console.log('')
|
|
await seedDefaultData()
|
|
console.log('')
|
|
console.log('========================================')
|
|
console.log('数据库种子初始化完成')
|
|
console.log('========================================')
|
|
} catch (error) {
|
|
console.error('种子初始化失败:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error(e)
|
|
process.exit(1)
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect()
|
|
})
|