/** * 字段迁移工具 * * 功能: * - 字段key修改的数据迁移 * - 字段类型转换验证 * - 批量更新素材字段结构 * - 字段恢复功能 */ import { parseMetadata, buildMetadata, isStandardField, FieldValue } from './field-data-storage' /** * 字段迁移操作类型 */ export type MigrationOperation = | { type: 'rename'; oldKey: string; newKey: string } | { type: 'delete'; key: string } | { type: 'restore'; key: string; defaultValue?: string } | { type: 'typeChange'; key: string; oldType: string; newType: string } /** * 迁移结果 */ export interface MigrationResult { success: boolean materialId: string operation: MigrationOperation oldValue?: unknown newValue?: unknown error?: string } /** * 批量迁移结果 */ export interface BatchMigrationResult { totalCount: number successCount: number failedCount: number skippedCount: number results: MigrationResult[] } /** * 字段类型兼容性检查 */ export function checkTypeCompatibility( oldType: string, newType: string, sampleValue?: unknown ): { compatible: boolean; warning?: string; canConvert: boolean } { // 相同类型总是兼容 if (oldType === newType) { return { compatible: true, canConvert: true } } // 定义类型转换规则 const conversionRules: Record> = { text: { textarea: { compatible: true }, number: { compatible: false, warning: '文本可能无法转换为数字' }, select: { compatible: true, warning: '文本值可能不在选项列表中' }, checkbox: { compatible: false, warning: '文本无法转换为布尔值' }, date: { compatible: false, warning: '文本格式可能不是有效日期' }, }, textarea: { text: { compatible: true, warning: '多行文本将被截断为单行' }, number: { compatible: false, warning: '文本可能无法转换为数字' }, select: { compatible: true, warning: '文本值可能不在选项列表中' }, }, number: { text: { compatible: true }, textarea: { compatible: true }, select: { compatible: true, warning: '数字值可能不在选项列表中' }, }, select: { text: { compatible: true }, textarea: { compatible: true }, number: { compatible: false, warning: '选项值可能无法转换为数字' }, }, checkbox: { text: { compatible: true }, select: { compatible: true, warning: '布尔值将转换为 "true" 或 "false"' }, }, date: { text: { compatible: true }, textarea: { compatible: true }, }, } const rules = conversionRules[oldType]?.[newType] if (rules) { return { ...rules, canConvert: rules.compatible } } // 默认不兼容 return { compatible: false, warning: `不支持从 ${oldType} 转换到 ${newType}`, canConvert: false } } /** * 转换字段值类型 */ export function convertFieldValue( value: unknown, fromType: string, toType: string ): { success: boolean; value: unknown; error?: string } { if (value === null || value === undefined) { return { success: true, value: null } } const stringValue = String(value) try { switch (toType) { case 'text': case 'textarea': return { success: true, value: stringValue } case 'number': const num = parseFloat(stringValue) if (isNaN(num)) { return { success: false, value: null, error: `无法将 "${stringValue}" 转换为数字` } } return { success: true, value: num } case 'checkbox': const boolValue = stringValue.toLowerCase() === 'true' || stringValue === '1' return { success: true, value: boolValue } case 'date': const date = new Date(stringValue) if (isNaN(date.getTime())) { return { success: false, value: null, error: `无法将 "${stringValue}" 转换为日期` } } return { success: true, value: date.toISOString().split('T')[0] } case 'select': // select类型保持原值,由调用方验证是否在选项列表中 return { success: true, value: stringValue } default: return { success: true, value: stringValue } } } catch (error) { return { success: false, value: null, error: `转换失败: ${error instanceof Error ? error.message : String(error)}` } } } /** * 重命名metadata中的字段key */ export function renameFieldInMetadata( metadata: string | null, oldKey: string, newKey: string ): { metadata: string | null; oldValue: unknown; migrated: boolean } { if (!metadata) { return { metadata: null, oldValue: undefined, migrated: false } } const parsed = parseMetadata(metadata) const templateFields = parsed.templateFields if (!(oldKey in templateFields)) { return { metadata, oldValue: undefined, migrated: false } } const oldValue = templateFields[oldKey] delete templateFields[oldKey] templateFields[newKey] = oldValue const newMetadata = buildMetadata( templateFields, parsed.templateId, parsed.previewUrls ) return { metadata: newMetadata, oldValue, migrated: true } } /** * 从metadata中删除字段 */ export function deleteFieldFromMetadata( metadata: string | null, key: string ): { metadata: string | null; oldValue: unknown; deleted: boolean } { if (!metadata) { return { metadata: null, oldValue: undefined, deleted: false } } const parsed = parseMetadata(metadata) const templateFields = parsed.templateFields if (!(key in templateFields)) { return { metadata, oldValue: undefined, deleted: false } } const oldValue = templateFields[key] delete templateFields[key] const newMetadata = buildMetadata( templateFields, parsed.templateId, parsed.previewUrls ) return { metadata: newMetadata, oldValue, deleted: true } } /** * 恢复metadata中的字段(从备份或默认值) */ export function restoreFieldInMetadata( metadata: string | null, key: string, value: FieldValue ): { metadata: string | null; restored: boolean } { const parsed = parseMetadata(metadata) const templateFields = parsed.templateFields // 如果字段已存在,不覆盖 if (key in templateFields) { return { metadata, restored: false } } templateFields[key] = value const newMetadata = buildMetadata( templateFields, parsed.templateId, parsed.previewUrls ) return { metadata: newMetadata, restored: true } } /** * 验证迁移操作 */ export function validateMigrationOperation( operation: MigrationOperation ): { valid: boolean; error?: string } { switch (operation.type) { case 'rename': if (!operation.oldKey || !operation.newKey) { return { valid: false, error: '重命名操作需要提供旧键名和新键名' } } if (operation.oldKey === operation.newKey) { return { valid: false, error: '新键名不能与旧键名相同' } } // 检查是否尝试重命名标准字段 if (isStandardField(operation.oldKey)) { return { valid: false, error: `不能重命名标准字段 "${operation.oldKey}"` } } break case 'delete': if (!operation.key) { return { valid: false, error: '删除操作需要提供键名' } } if (isStandardField(operation.key)) { return { valid: false, error: `不能删除标准字段 "${operation.key}"` } } break case 'restore': if (!operation.key) { return { valid: false, error: '恢复操作需要提供键名' } } break case 'typeChange': if (!operation.key || !operation.oldType || !operation.newType) { return { valid: false, error: '类型转换操作需要提供键名、旧类型和新类型' } } const compatibility = checkTypeCompatibility(operation.oldType, operation.newType) if (!compatibility.canConvert) { return { valid: false, error: compatibility.warning || '类型不兼容' } } break } return { valid: true } } /** * 生成迁移预览 */ export function generateMigrationPreview( materials: Array<{ id: string; metadata: string | null }>, operation: MigrationOperation ): { affectedCount: number sampleChanges: Array<{ materialId: string; before: unknown; after: unknown }> } { const affectedMaterials: Array<{ materialId: string; before: unknown; after: unknown }> = [] for (const material of materials) { const parsed = parseMetadata(material.metadata) const templateFields = parsed.templateFields switch (operation.type) { case 'rename': if (operation.oldKey in templateFields) { affectedMaterials.push({ materialId: material.id, before: { [operation.oldKey]: templateFields[operation.oldKey] }, after: { [operation.newKey]: templateFields[operation.oldKey] }, }) } break case 'delete': if (operation.key in templateFields) { affectedMaterials.push({ materialId: material.id, before: { [operation.key]: templateFields[operation.key] }, after: { [operation.key]: undefined }, }) } break case 'restore': if (!(operation.key in templateFields)) { affectedMaterials.push({ materialId: material.id, before: { [operation.key]: undefined }, after: { [operation.key]: operation.defaultValue }, }) } break case 'typeChange': if (operation.key in templateFields) { const oldValue = templateFields[operation.key] const converted = convertFieldValue(oldValue, operation.oldType, operation.newType) affectedMaterials.push({ materialId: material.id, before: { [operation.key]: oldValue, type: operation.oldType }, after: { [operation.key]: converted.value, type: operation.newType }, }) } break } } return { affectedCount: affectedMaterials.length, sampleChanges: affectedMaterials.slice(0, 5), // 只返回前5个示例 } } /** * 创建迁移回滚数据 */ export function createRollbackData( materialId: string, operation: MigrationOperation, originalMetadata: string | null ): { materialId: string operation: MigrationOperation originalMetadata: string | null timestamp: string } { return { materialId, operation, originalMetadata, timestamp: new Date().toISOString(), } }