/** * 分类管理与模板的双向同步 * * 功能: * - 分类管理修改选项时同步到模板 * - 模板修改选项时同步到字段池 * - 删除字段时警告影响范围 * - 使用版本号检测同步冲突 * - 同步失败时回滚 * - 记录同步历史日志 * * @created 2024-12-13 */ import { prisma } from '@/lib/prisma' // 类型定义 export interface SyncResult { success: boolean syncedCount: number conflictCount: number errors: Array<{ source: string; target: string; error: string }> rollbackPerformed: boolean } export interface SyncLogEntry { id: string timestamp: Date operation: 'add' | 'update' | 'delete' | 'sync' source: 'category' | 'template' fieldKey: string optionValue?: string details: string success: boolean error?: string } // 内存中的同步日志(生产环境应该持久化到数据库) const syncLogs: SyncLogEntry[] = [] const MAX_LOG_ENTRIES = 1000 /** * 记录同步日志 */ function logSync(entry: Omit): void { const logEntry: SyncLogEntry = { id: `sync_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), ...entry, } syncLogs.unshift(logEntry) // 限制日志数量 if (syncLogs.length > MAX_LOG_ENTRIES) { syncLogs.pop() } // 输出到控制台(开发环境) if (process.env.NODE_ENV === 'development') { console.log(`[CategorySync] ${entry.operation} ${entry.source}:${entry.fieldKey}`, entry.details) } } /** * 获取同步日志 */ export function getSyncLogs(limit = 100): SyncLogEntry[] { return syncLogs.slice(0, limit) } /** * 清除同步日志 */ export function clearSyncLogs(): void { syncLogs.length = 0 } /** * 从分类管理同步选项到模板 * 当在分类管理中添加/修改/删除选项时调用 */ export async function syncOptionToTemplates( fieldKey: string, operation: 'add' | 'update' | 'delete', optionValue: string, newValue?: string ): Promise { const result: SyncResult = { success: true, syncedCount: 0, conflictCount: 0, errors: [], rollbackPerformed: false, } try { // 获取所有使用该字段的模板字段 const templateFields = await prisma.uploadField.findMany({ where: { key: fieldKey }, include: { template: true, }, }) if (templateFields.length === 0) { logSync({ operation, source: 'category', fieldKey, optionValue, details: '没有找到使用该字段的模板', success: true, }) return result } for (const field of templateFields) { try { // 解析现有选项 let options: string[] = [] if (field.optionsStatic) { try { const parsed = JSON.parse(field.optionsStatic) options = Array.isArray(parsed) ? parsed : [] } catch { options = [] } } let updated = false let newOptions = [...options] switch (operation) { case 'add': if (!options.includes(optionValue)) { newOptions.push(optionValue) updated = true } break case 'update': if (newValue && options.includes(optionValue)) { newOptions = options.map(opt => opt === optionValue ? newValue : opt) updated = true } break case 'delete': if (options.includes(optionValue)) { newOptions = options.filter(opt => opt !== optionValue) updated = true } break } if (updated) { // 检查版本冲突 const currentField = await prisma.uploadField.findUnique({ where: { id: field.id }, }) if (currentField && currentField.updatedAt > field.updatedAt) { result.conflictCount++ result.errors.push({ source: `category:${fieldKey}`, target: `template:${field.templateId}:${field.key}`, error: '版本冲突:模板字段已被其他操作修改', }) continue } // 更新模板字段 await prisma.uploadField.update({ where: { id: field.id }, data: { optionsStatic: JSON.stringify(newOptions), updatedAt: new Date(), }, }) result.syncedCount++ } } catch (error) { result.errors.push({ source: `category:${fieldKey}`, target: `template:${field.templateId}:${field.key}`, error: error instanceof Error ? error.message : '未知错误', }) } } logSync({ operation, source: 'category', fieldKey, optionValue, details: `同步到 ${result.syncedCount} 个模板字段,${result.conflictCount} 个冲突`, success: result.errors.length === 0, error: result.errors.length > 0 ? result.errors[0].error : undefined, }) result.success = result.errors.length === 0 } catch (error) { result.success = false result.errors.push({ source: `category:${fieldKey}`, target: 'templates', error: error instanceof Error ? error.message : '未知错误', }) logSync({ operation, source: 'category', fieldKey, optionValue, details: '同步失败', success: false, error: error instanceof Error ? error.message : '未知错误', }) } return result } /** * 从模板同步选项到字段池 * 当在模板管理中修改字段选项时调用 */ export async function syncOptionToFieldPool( fieldKey: string, options: string[] ): Promise { const result: SyncResult = { success: true, syncedCount: 0, conflictCount: 0, errors: [], rollbackPerformed: false, } try { // 获取字段池中的字段 const fieldDef = await prisma.fieldDefinition.findUnique({ where: { key: fieldKey }, include: { options: { orderBy: { orderIndex: 'asc' }, }, }, }) if (!fieldDef) { // 字段不存在,创建新字段 await prisma.fieldDefinition.create({ data: { key: fieldKey, label: fieldKey, type: 'select', source: 'template', isStandard: false, isFilterEnabled: false, filterOrder: 0, }, }) } // 获取现有选项 const existingOptions = fieldDef?.options || [] const existingValues = new Set(existingOptions.map(o => o.value)) // 获取字段ID(可能是新创建的) const field = await prisma.fieldDefinition.findUnique({ where: { key: fieldKey }, select: { id: true }, }) if (!field) { throw new Error(`字段 ${fieldKey} 不存在`) } // 添加新选项 let orderIndex = existingOptions.length for (const optionValue of options) { if (!existingValues.has(optionValue)) { await prisma.fieldOption.create({ data: { fieldId: field.id, value: optionValue, label: optionValue, orderIndex: orderIndex++, }, }) result.syncedCount++ } } logSync({ operation: 'sync', source: 'template', fieldKey, details: `同步 ${result.syncedCount} 个新选项到字段池`, success: true, }) result.success = true } catch (error) { result.success = false result.errors.push({ source: `template:${fieldKey}`, target: `fieldPool:${fieldKey}`, error: error instanceof Error ? error.message : '未知错误', }) logSync({ operation: 'sync', source: 'template', fieldKey, details: '同步到字段池失败', success: false, error: error instanceof Error ? error.message : '未知错误', }) } return result } /** * 获取字段删除的影响范围 */ export async function getFieldDeletionImpact(fieldKey: string): Promise<{ templateCount: number templateNames: string[] optionCount: number canDelete: boolean warning: string }> { // 获取使用该字段的模板 const templateFields = await prisma.uploadField.findMany({ where: { key: fieldKey }, include: { template: { select: { name: true }, }, }, }) const templateNames = templateFields.map(f => f.template.name) const templateCount = templateNames.length // 获取字段定义 const fieldDef = await prisma.fieldDefinition.findUnique({ where: { key: fieldKey }, select: { id: true }, }) // 获取选项数量 const optionCount = fieldDef ? await prisma.fieldOption.count({ where: { fieldId: fieldDef.id }, }) : 0 // 判断是否可以删除 const canDelete = templateCount === 0 // 生成警告信息 let warning = '' if (templateCount > 0) { warning = `该字段被 ${templateCount} 个模板使用(${templateNames.slice(0, 3).join('、')}${templateCount > 3 ? '等' : ''}),删除后这些模板将失去该字段。` } if (optionCount > 0) { warning += warning ? '\n' : '' warning += `该字段有 ${optionCount} 个选项将被一并删除。` } return { templateCount, templateNames, optionCount, canDelete, warning, } } /** * 执行带回滚的同步操作 */ export async function syncWithRollback( operation: () => Promise, rollback: () => Promise ): Promise<{ success: boolean; result?: T; error?: string; rollbackPerformed: boolean }> { try { const result = await operation() return { success: true, result, rollbackPerformed: false } } catch (error) { // 尝试回滚 try { await rollback() return { success: false, error: error instanceof Error ? error.message : '未知错误', rollbackPerformed: true, } } catch (rollbackError) { return { success: false, error: `操作失败: ${error instanceof Error ? error.message : '未知错误'}; 回滚也失败: ${rollbackError instanceof Error ? rollbackError.message : '未知错误'}`, rollbackPerformed: false, } } } } /** * 批量同步所有模板字段到字段池 */ export async function syncAllTemplatesToFieldPool(): Promise { const result: SyncResult = { success: true, syncedCount: 0, conflictCount: 0, errors: [], rollbackPerformed: false, } try { // 获取所有模板字段 const templateFields = await prisma.uploadField.findMany({ where: { type: 'select', optionsStatic: { not: null }, }, }) // 按字段key分组 const fieldOptionsMap = new Map>() for (const field of templateFields) { if (!field.optionsStatic) continue try { const options = JSON.parse(field.optionsStatic) if (!Array.isArray(options)) continue const existing = fieldOptionsMap.get(field.key) || new Set() options.forEach((opt: string) => { if (typeof opt === 'string' && opt.trim()) { existing.add(opt.trim()) } }) fieldOptionsMap.set(field.key, existing) } catch { // 忽略解析错误 } } // 同步到字段池 for (const [fieldKey, options] of Array.from(fieldOptionsMap.entries())) { const syncResult = await syncOptionToFieldPool(fieldKey, Array.from(options)) result.syncedCount += syncResult.syncedCount result.errors.push(...syncResult.errors) } logSync({ operation: 'sync', source: 'template', fieldKey: '*', details: `批量同步完成,共同步 ${result.syncedCount} 个选项`, success: result.errors.length === 0, }) result.success = result.errors.length === 0 } catch (error) { result.success = false result.errors.push({ source: 'templates', target: 'fieldPool', error: error instanceof Error ? error.message : '未知错误', }) } return result }