271 lines
8.5 KiB
TypeScript
271 lines
8.5 KiB
TypeScript
/**
|
||
* 数据验证脚本:验证模板系统数据完整性
|
||
*
|
||
* 功能:
|
||
* 1. 验证所有模板的必填字段
|
||
* 2. 验证素材的templateId引用
|
||
* 3. 验证字段的templateId引用
|
||
* 4. 检查孤立数据
|
||
*/
|
||
|
||
import { PrismaClient } from '@prisma/client'
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
interface ValidationResult {
|
||
category: string
|
||
passed: boolean
|
||
message: string
|
||
details?: any
|
||
}
|
||
|
||
async function verifyTemplateData(): Promise<boolean> {
|
||
console.log('开始数据验证...\n')
|
||
|
||
const results: ValidationResult[] = []
|
||
let allPassed = true
|
||
|
||
try {
|
||
// 1. 验证模板数据
|
||
console.log('验证 1: 检查模板数据...')
|
||
|
||
// 1.1 检查必填字段
|
||
const templatesWithoutName = await prisma.uploadTemplate.count({
|
||
where: { name: { equals: '' } }
|
||
})
|
||
results.push({
|
||
category: '模板',
|
||
passed: templatesWithoutName === 0,
|
||
message: `模板名称完整性`,
|
||
details: templatesWithoutName === 0 ? '✓ 所有模板都有名称' : `✗ ${templatesWithoutName} 个模板没有名称`
|
||
})
|
||
|
||
// 1.2 检查version字段
|
||
const templatesWithoutVersion = await prisma.uploadTemplate.count({
|
||
where: {
|
||
OR: [
|
||
{ version: { equals: 0 } },
|
||
{ version: { lt: 1 } }
|
||
]
|
||
}
|
||
})
|
||
results.push({
|
||
category: '模板',
|
||
passed: templatesWithoutVersion === 0,
|
||
message: `模板version字段`,
|
||
details: templatesWithoutVersion === 0 ? '✓ 所有模板都有version' : `✗ ${templatesWithoutVersion} 个模板没有version`
|
||
})
|
||
|
||
// 1.3 检查materialKind字段
|
||
const validKinds = ['IMAGE', 'VIDEO', 'AUDIO', 'OTHER']
|
||
const templatesWithInvalidKind = await prisma.uploadTemplate.count({
|
||
where: {
|
||
materialKind: {
|
||
notIn: validKinds
|
||
}
|
||
}
|
||
})
|
||
results.push({
|
||
category: '模板',
|
||
passed: templatesWithInvalidKind === 0,
|
||
message: `模板materialKind有效性`,
|
||
details: templatesWithInvalidKind === 0 ? '✓ 所有模板的materialKind都有效' : `✗ ${templatesWithInvalidKind} 个模板的materialKind无效`
|
||
})
|
||
|
||
// 2. 验证素材数据
|
||
console.log('验证 2: 检查素材数据...')
|
||
|
||
// 2.1 检查未删除素材的templateId
|
||
const materialsWithoutTemplate = await prisma.material.count({
|
||
where: {
|
||
templateId: null,
|
||
deletedAt: null
|
||
}
|
||
})
|
||
results.push({
|
||
category: '素材',
|
||
passed: materialsWithoutTemplate === 0,
|
||
message: `素材templateId完整性`,
|
||
details: materialsWithoutTemplate === 0 ? '✓ 所有素材都有templateId' : `✗ ${materialsWithoutTemplate} 个素材没有templateId`
|
||
})
|
||
|
||
// 2.2 检查templateId引用的有效性
|
||
const materialsWithInvalidTemplate = await prisma.material.findMany({
|
||
where: {
|
||
NOT: { templateId: null },
|
||
deletedAt: null,
|
||
template: null
|
||
},
|
||
select: { id: true, templateId: true }
|
||
})
|
||
results.push({
|
||
category: '素材',
|
||
passed: materialsWithInvalidTemplate.length === 0,
|
||
message: `素材templateId引用有效性`,
|
||
details: materialsWithInvalidTemplate.length === 0
|
||
? '✓ 所有素材的templateId引用都有效'
|
||
: `✗ ${materialsWithInvalidTemplate.length} 个素材的templateId引用无效`
|
||
})
|
||
|
||
// 3. 验证字段数据
|
||
console.log('验证 3: 检查字段数据...')
|
||
|
||
// 3.1 检查字段的templateId
|
||
const fieldsWithoutTemplate = await prisma.uploadField.count({
|
||
where: {
|
||
OR: [
|
||
{ templateId: { equals: '' } },
|
||
{ templateId: { equals: null as any } }
|
||
]
|
||
}
|
||
})
|
||
results.push({
|
||
category: '字段',
|
||
passed: fieldsWithoutTemplate === 0,
|
||
message: `字段templateId完整性`,
|
||
details: fieldsWithoutTemplate === 0 ? '✓ 所有字段都有templateId' : `✗ ${fieldsWithoutTemplate} 个字段没有templateId`
|
||
})
|
||
|
||
// 3.2 检查字段key的唯一性(同一模板内)
|
||
const duplicateFieldKeys = await prisma.$queryRaw<Array<{ templateId: string, key: string, count: number }>>`
|
||
SELECT "templateId", "key", COUNT(*) as count
|
||
FROM "UploadField"
|
||
GROUP BY "templateId", "key"
|
||
HAVING COUNT(*) > 1
|
||
`
|
||
results.push({
|
||
category: '字段',
|
||
passed: duplicateFieldKeys.length === 0,
|
||
message: `字段key唯一性`,
|
||
details: duplicateFieldKeys.length === 0
|
||
? '✓ 所有字段key在模板内唯一'
|
||
: `✗ ${duplicateFieldKeys.length} 个重复的字段key`
|
||
})
|
||
|
||
// 4. 验证字段池数据
|
||
console.log('验证 4: 检查字段池数据...')
|
||
|
||
// 4.1 检查字段池key的唯一性
|
||
const duplicateFieldDefKeys = await prisma.$queryRaw<Array<{ key: string, count: number }>>`
|
||
SELECT "key", COUNT(*) as count
|
||
FROM "FieldDefinition"
|
||
GROUP BY "key"
|
||
HAVING COUNT(*) > 1
|
||
`
|
||
results.push({
|
||
category: '字段池',
|
||
passed: duplicateFieldDefKeys.length === 0,
|
||
message: `字段池key唯一性`,
|
||
details: duplicateFieldDefKeys.length === 0
|
||
? '✓ 所有字段池key唯一'
|
||
: `✗ ${duplicateFieldDefKeys.length} 个重复的字段池key`
|
||
})
|
||
|
||
// 5. 检查孤立数据
|
||
console.log('验证 5: 检查孤立数据...')
|
||
|
||
// 5.1 检查没有字段的模板
|
||
const templatesWithoutFields = await prisma.uploadTemplate.findMany({
|
||
where: {
|
||
fields: { none: {} }
|
||
},
|
||
select: { id: true, status: true }
|
||
})
|
||
results.push({
|
||
category: '孤立数据',
|
||
passed: true, // 这不是错误,只是警告
|
||
message: `没有字段的模板`,
|
||
details: templatesWithoutFields.length === 0
|
||
? '✓ 所有模板都有字段'
|
||
: `⚠ ${templatesWithoutFields.length} 个模板没有字段(可能是草稿)`
|
||
})
|
||
|
||
// 5.2 检查没有素材的模板
|
||
const templatesWithoutMaterials = await prisma.uploadTemplate.findMany({
|
||
where: {
|
||
materials: { none: {} }
|
||
},
|
||
select: { id: true, status: true }
|
||
})
|
||
results.push({
|
||
category: '孤立数据',
|
||
passed: true, // 这不是错误,只是警告
|
||
message: `没有素材的模板`,
|
||
details: templatesWithoutMaterials.length === 0
|
||
? '✓ 所有模板都有素材'
|
||
: `⚠ ${templatesWithoutMaterials.length} 个模板没有素材(可能是新建的)`
|
||
})
|
||
|
||
// 6. 生成验证报告
|
||
console.log('\n' + '='.repeat(80))
|
||
console.log('验证报告')
|
||
console.log('='.repeat(80))
|
||
|
||
const groupedResults = results.reduce((acc, result) => {
|
||
if (!acc[result.category]) {
|
||
acc[result.category] = []
|
||
}
|
||
acc[result.category].push(result)
|
||
return acc
|
||
}, {} as Record<string, ValidationResult[]>)
|
||
|
||
for (const [category, categoryResults] of Object.entries(groupedResults)) {
|
||
console.log(`\n${category}:`)
|
||
for (const result of categoryResults) {
|
||
const icon = result.passed ? '✓' : '✗'
|
||
console.log(` ${icon} ${result.message}`)
|
||
console.log(` ${result.details}`)
|
||
if (!result.passed) {
|
||
allPassed = false
|
||
}
|
||
}
|
||
}
|
||
|
||
// 7. 统计信息
|
||
console.log('\n' + '='.repeat(80))
|
||
console.log('统计信息')
|
||
console.log('='.repeat(80))
|
||
|
||
const totalTemplates = await prisma.uploadTemplate.count()
|
||
const activeTemplates = await prisma.uploadTemplate.count({ where: { status: 'active' } })
|
||
const totalFields = await prisma.uploadField.count()
|
||
const totalMaterials = await prisma.material.count({ where: { deletedAt: null } })
|
||
const totalFieldDefs = await prisma.fieldDefinition.count()
|
||
const enabledFieldDefs = await prisma.fieldDefinition.count({ where: { isFilterEnabled: true } })
|
||
|
||
console.log(`模板总数: ${totalTemplates} (活跃: ${activeTemplates})`)
|
||
console.log(`字段总数: ${totalFields}`)
|
||
console.log(`素材总数: ${totalMaterials}`)
|
||
console.log(`字段池定义: ${totalFieldDefs} (启用筛选: ${enabledFieldDefs})`)
|
||
console.log('='.repeat(80))
|
||
|
||
if (allPassed) {
|
||
console.log('\n✓ 所有验证通过!')
|
||
} else {
|
||
console.warn('\n⚠ 部分验证失败,请检查上述错误')
|
||
}
|
||
|
||
return allPassed
|
||
|
||
} catch (error) {
|
||
console.error('\n✗ 验证过程出错:', error)
|
||
throw error
|
||
} finally {
|
||
await prisma.$disconnect()
|
||
}
|
||
}
|
||
|
||
// 执行验证
|
||
if (require.main === module) {
|
||
verifyTemplateData()
|
||
.then((success) => {
|
||
process.exit(success ? 0 : 1)
|
||
})
|
||
.catch((error) => {
|
||
console.error('验证脚本执行失败:', error)
|
||
process.exit(1)
|
||
})
|
||
}
|
||
|
||
export { verifyTemplateData }
|