/** * 系统数据完整性检查和自动修复模块 * * 设计目标: * 1. 客户重新安装时自动初始化必要数据 * 2. 系统更新时自动补全新增的内置数据 * 3. 不影响客户已有的自定义数据 * 4. 提供健康检查API供监控使用 * * @module system-integrity * @created 2025-12-13 */ import { prisma } from '@/lib/prisma' import { BUILTIN_TEMPLATE_KEYS as BUILTIN_TEMPLATE_KEYS_SOURCE, createBuiltinTemplate, } from './upload-template-presets' // ============================================================================ // 内置模板配置(单一数据源) // ============================================================================ export const BUILTIN_TEMPLATE_KEYS = BUILTIN_TEMPLATE_KEYS_SOURCE export type BuiltinTemplateKey = (typeof BUILTIN_TEMPLATE_KEYS)[number] export 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 }> } > = Object.fromEntries( BUILTIN_TEMPLATE_KEYS.map((key) => { const template = createBuiltinTemplate(key) return [ key, { name: template.name, description: template.description, materialType: template.materialType, materialKind: template.materialKind, fields: template.fields.map((field) => ({ key: field.key, label: field.label, type: field.type, required: field.required, layoutWidth: field.layoutWidth || 'full', visibleOnHomeFilter: field.visibleOnHomeFilter, isSearchable: field.isSearchable, addToCategory: field.addToCategory, })), }, ] }) ) as 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 }> } > // ============================================================================ // 数据完整性检查结果类型 // ============================================================================ export interface IntegrityCheckResult { healthy: boolean timestamp: string checks: { builtinTemplates: { healthy: boolean total: number existing: number missing: string[] } orphanedMaterials: { healthy: boolean count: number templateIds: string[] } } recommendations: string[] } export interface IntegrityRepairResult { success: boolean timestamp: string repairs: { templatesCreated: string[] templatesFailed: string[] orphanedMaterialsFixed: number } errors: string[] } // ============================================================================ // 核心功能函数 // ============================================================================ /** * 检查系统数据完整性 * * 检查项目: * 1. 内置模板是否完整 * 2. 是否有素材引用了不存在的模板(孤儿素材) */ export async function checkSystemIntegrity(): Promise { const timestamp = new Date().toISOString() const recommendations: string[] = [] // 1. 检查内置模板 const expectedTemplateIds = BUILTIN_TEMPLATE_KEYS.map((key) => `builtin-${key}`) const existingTemplates = await prisma.uploadTemplate.findMany({ where: { id: { in: expectedTemplateIds } }, select: { id: true }, }) const existingIds = new Set(existingTemplates.map((t) => t.id)) const missingTemplates = expectedTemplateIds.filter((id) => !existingIds.has(id)) if (missingTemplates.length > 0) { recommendations.push(`缺少 ${missingTemplates.length} 个内置模板,建议运行修复`) } // 2. 检查孤儿素材(引用了不存在模板的素材) const materialsWithTemplate = await prisma.material.findMany({ where: { templateId: { not: null }, deletedAt: null, }, select: { templateId: true }, distinct: ['templateId'], }) const referencedTemplateIds = materialsWithTemplate .map((m) => m.templateId) .filter((id): id is string => id !== null) // 检查这些模板是否存在 const existingReferencedTemplates = await prisma.uploadTemplate.findMany({ where: { id: { in: referencedTemplateIds } }, select: { id: true }, }) const existingReferencedIds = new Set(existingReferencedTemplates.map((t) => t.id)) const orphanedTemplateIds = referencedTemplateIds.filter((id) => !existingReferencedIds.has(id)) // 统计孤儿素材数量 let orphanedMaterialCount = 0 if (orphanedTemplateIds.length > 0) { const count = await prisma.material.count({ where: { templateId: { in: orphanedTemplateIds }, deletedAt: null, }, }) orphanedMaterialCount = count recommendations.push( `有 ${orphanedMaterialCount} 个素材引用了不存在的模板,建议运行修复` ) } const builtinHealthy = missingTemplates.length === 0 const orphanedHealthy = orphanedTemplateIds.length === 0 return { healthy: builtinHealthy && orphanedHealthy, timestamp, checks: { builtinTemplates: { healthy: builtinHealthy, total: expectedTemplateIds.length, existing: existingIds.size, missing: missingTemplates, }, orphanedMaterials: { healthy: orphanedHealthy, count: orphanedMaterialCount, templateIds: orphanedTemplateIds, }, }, recommendations, } } /** * 修复系统数据完整性问题 * * 修复策略: * 1. 创建缺失的内置模板(使用 upsert 避免重复) * 2. 为孤儿素材创建占位模板或清除无效引用 */ export async function repairSystemIntegrity( options: { createMissingBuiltinTemplates?: boolean fixOrphanedMaterials?: boolean } = {} ): Promise { const { createMissingBuiltinTemplates = true, fixOrphanedMaterials = true, } = options const timestamp = new Date().toISOString() const templatesCreated: string[] = [] const templatesFailed: string[] = [] const errors: string[] = [] let orphanedMaterialsFixed = 0 // 1. 创建缺失的内置模板 if (createMissingBuiltinTemplates) { for (const key of BUILTIN_TEMPLATE_KEYS) { const templateId = `builtin-${key}` const config = BUILTIN_TEMPLATE_CONFIG[key] try { // 使用 upsert 确保幂等性 await prisma.$transaction(async (tx) => { // 检查模板是否存在 const existing = await tx.uploadTemplate.findUnique({ where: { id: templateId }, }) if (existing) { // 模板已存在,跳过 return } // 创建模板 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, }, }) } templatesCreated.push(templateId) }) } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) errors.push(`创建模板 ${templateId} 失败: ${errorMsg}`) templatesFailed.push(templateId) } } } // 2. 修复孤儿素材 if (fixOrphanedMaterials) { try { // 获取所有被素材引用但不存在的模板ID const materialsWithTemplate = await prisma.material.findMany({ where: { templateId: { not: null }, deletedAt: null, }, select: { templateId: true }, distinct: ['templateId'], }) const referencedTemplateIds = materialsWithTemplate .map((m) => m.templateId) .filter((id): id is string => id !== null) const existingTemplates = await prisma.uploadTemplate.findMany({ where: { id: { in: referencedTemplateIds } }, select: { id: true }, }) const existingIds = new Set(existingTemplates.map((t) => t.id)) const orphanedTemplateIds = referencedTemplateIds.filter((id) => !existingIds.has(id)) // 对于孤儿素材,根据模板ID类型处理 for (const orphanedId of orphanedTemplateIds) { if (orphanedId.startsWith('builtin-')) { // 内置模板应该已经在上一步创建了,这里不需要额外处理 continue } // 自定义模板不存在,清除素材的模板引用 // 这样素材仍然可以正常显示,只是没有关联模板 const result = await prisma.material.updateMany({ where: { templateId: orphanedId, deletedAt: null, }, data: { templateId: null, }, }) orphanedMaterialsFixed += result.count } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) errors.push(`修复孤儿素材失败: ${errorMsg}`) } } return { success: errors.length === 0, timestamp, repairs: { templatesCreated, templatesFailed, orphanedMaterialsFixed, }, errors, } } /** * 确保内置模板存在(用于应用启动时调用) * * 这是一个轻量级的检查,只在缺少模板时才执行创建 * 设计为可以安全地多次调用(幂等性) */ export async function ensureBuiltinTemplates(): Promise<{ checked: boolean created: string[] errors: string[] }> { const created: string[] = [] const errors: string[] = [] try { // 快速检查:获取现有的内置模板ID const expectedIds = BUILTIN_TEMPLATE_KEYS.map((key) => `builtin-${key}`) const existing = await prisma.uploadTemplate.findMany({ where: { id: { in: expectedIds } }, select: { id: true }, }) const existingIds = new Set(existing.map((t) => t.id)) // 只创建缺失的模板 for (const key of BUILTIN_TEMPLATE_KEYS) { const templateId = `builtin-${key}` if (existingIds.has(templateId)) continue const config = BUILTIN_TEMPLATE_CONFIG[key] try { await prisma.$transaction(async (tx) => { // 再次检查(防止并发) const check = await tx.uploadTemplate.findUnique({ where: { id: templateId }, }) if (check) return 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, }, }) } created.push(templateId) }) } catch (error) { // 忽略唯一约束冲突(可能是并发创建) const errorMsg = error instanceof Error ? error.message : String(error) if (!errorMsg.includes('Unique constraint')) { errors.push(`${templateId}: ${errorMsg}`) } } } if (created.length > 0) { console.log(`[SystemIntegrity] 创建了 ${created.length} 个内置模板:`, created) } return { checked: true, created, errors } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) console.error('[SystemIntegrity] 检查内置模板失败:', errorMsg) return { checked: false, created, errors: [errorMsg] } } }