/** * 分类管理批量操作工具 * * 支持以下批量操作: * - 批量启用/禁用字段 * - 批量删除字段 * - 批量导入选项(CSV/JSON) * - 批量导出选项 * * @created 2024-12-13 */ import { prisma } from '@/lib/prisma' // 类型定义 export interface BatchOperationResult { success: boolean processed: number failed: number errors: Array<{ key: string; error: string }> } export interface ImportedOption { fieldKey: string value: string label?: string } export interface ExportedField { key: string label: string filterDisplayName: string | null filterOrder: number isFilterEnabled: boolean options: Array<{ value: string; label: string | null }> } /** * 批量启用/禁用字段 */ export async function batchToggleFields( fieldKeys: string[], enabled: boolean ): Promise { const result: BatchOperationResult = { success: true, processed: 0, failed: 0, errors: [], } for (const key of fieldKeys) { try { await prisma.fieldDefinition.update({ where: { key }, data: { isFilterEnabled: enabled, updatedAt: new Date(), }, }) result.processed++ } catch (error) { result.failed++ result.errors.push({ key, error: error instanceof Error ? error.message : '未知错误', }) } } result.success = result.failed === 0 return result } /** * 批量删除字段(仅删除字段池中的记录,不影响模板) */ export async function batchDeleteFields( fieldKeys: string[] ): Promise { const result: BatchOperationResult = { success: true, processed: 0, failed: 0, errors: [], } for (const key of fieldKeys) { try { // 先获取字段ID const field = await prisma.fieldDefinition.findUnique({ where: { key }, select: { id: true }, }) if (field) { // 先删除关联的选项(使用 fieldId) await prisma.fieldOption.deleteMany({ where: { fieldId: field.id }, }) // 再删除字段定义 await prisma.fieldDefinition.delete({ where: { key }, }) } result.processed++ } catch (error) { result.failed++ result.errors.push({ key, error: error instanceof Error ? error.message : '未知错误', }) } } result.success = result.failed === 0 return result } /** * 解析CSV格式的选项数据 * 格式:fieldKey,value,label(label可选) */ export function parseCSVOptions(csvContent: string): ImportedOption[] { const lines = csvContent.split('\n').filter(line => line.trim()) const options: ImportedOption[] = [] // 跳过标题行(如果有) const startIndex = lines[0]?.toLowerCase().includes('fieldkey') ? 1 : 0 for (let i = startIndex; i < lines.length; i++) { const line = lines[i].trim() if (!line) continue // 处理CSV中的引号 const parts = parseCSVLine(line) if (parts.length >= 2) { options.push({ fieldKey: parts[0].trim(), value: parts[1].trim(), label: parts[2]?.trim() || undefined, }) } } return options } /** * 解析单行CSV(处理引号) */ function parseCSVLine(line: string): string[] { const result: string[] = [] let current = '' let inQuotes = false for (let i = 0; i < line.length; i++) { const char = line[i] if (char === '"') { if (inQuotes && line[i + 1] === '"') { current += '"' i++ } else { inQuotes = !inQuotes } } else if (char === ',' && !inQuotes) { result.push(current) current = '' } else { current += char } } result.push(current) return result } /** * 解析JSON格式的选项数据 */ export function parseJSONOptions(jsonContent: string): ImportedOption[] { try { const data = JSON.parse(jsonContent) // 支持两种格式: // 1. 数组格式:[{ fieldKey, value, label? }, ...] // 2. 对象格式:{ fieldKey: [{ value, label? }, ...], ... } if (Array.isArray(data)) { return data.filter(item => item.fieldKey && item.value).map(item => ({ fieldKey: String(item.fieldKey), value: String(item.value), label: item.label ? String(item.label) : undefined, })) } if (typeof data === 'object' && data !== null) { const options: ImportedOption[] = [] for (const [fieldKey, values] of Object.entries(data)) { if (Array.isArray(values)) { for (const item of values) { if (typeof item === 'string') { options.push({ fieldKey, value: item }) } else if (item && typeof item === 'object' && item.value) { options.push({ fieldKey, value: String(item.value), label: item.label ? String(item.label) : undefined, }) } } } } return options } return [] } catch { return [] } } /** * 批量导入选项 */ export async function batchImportOptions( options: ImportedOption[], mode: 'merge' | 'replace' = 'merge' ): Promise { const result: BatchOperationResult = { success: true, processed: 0, failed: 0, errors: [], } // 按字段分组 const optionsByField = new Map() for (const option of options) { const existing = optionsByField.get(option.fieldKey) || [] existing.push(option) optionsByField.set(option.fieldKey, existing) } for (const [fieldKey, fieldOptions] of Array.from(optionsByField.entries())) { try { // 检查字段是否存在 const field = await prisma.fieldDefinition.findUnique({ where: { key: fieldKey }, }) if (!field) { result.failed += fieldOptions.length result.errors.push({ key: fieldKey, error: `字段 ${fieldKey} 不存在`, }) continue } if (mode === 'replace') { // 替换模式:先删除所有现有选项 await prisma.fieldOption.deleteMany({ where: { fieldId: field.id }, }) } // 获取现有选项(用于去重和确定orderIndex) const existingOptions = await prisma.fieldOption.findMany({ where: { fieldId: field.id }, orderBy: { orderIndex: 'desc' }, }) const existingValues = new Set(existingOptions.map(o => o.value)) let nextOrderIndex = (existingOptions[0]?.orderIndex ?? -1) + 1 // 添加新选项 for (const option of fieldOptions) { if (mode === 'merge' && existingValues.has(option.value)) { // 合并模式下跳过已存在的选项 continue } await prisma.fieldOption.create({ data: { fieldId: field.id, value: option.value, label: option.label || '', orderIndex: nextOrderIndex++, }, }) result.processed++ } } catch (error) { result.failed += fieldOptions.length result.errors.push({ key: fieldKey, error: error instanceof Error ? error.message : '未知错误', }) } } result.success = result.failed === 0 return result } /** * 导出所有字段和选项 */ export async function exportAllFields(): Promise { const fields = await prisma.fieldDefinition.findMany({ orderBy: [ { filterOrder: 'asc' }, { label: 'asc' }, ], include: { options: { orderBy: { orderIndex: 'asc' }, }, }, }) return fields.map(field => ({ key: field.key, label: field.label, filterDisplayName: field.filterDisplayName, filterOrder: field.filterOrder, isFilterEnabled: field.isFilterEnabled, options: field.options.map(opt => ({ value: opt.value, label: opt.label, })), })) } /** * 导出为CSV格式 */ export function exportToCSV(fields: ExportedField[]): string { const lines: string[] = ['fieldKey,value,label'] for (const field of fields) { for (const option of field.options) { const escapedValue = escapeCSVField(option.value) const escapedLabel = escapeCSVField(option.label || '') lines.push(`${field.key},${escapedValue},${escapedLabel}`) } } return lines.join('\n') } /** * 导出为JSON格式 */ export function exportToJSON(fields: ExportedField[]): string { return JSON.stringify(fields, null, 2) } /** * 转义CSV字段 */ function escapeCSVField(value: string): string { if (value.includes(',') || value.includes('"') || value.includes('\n')) { return `"${value.replace(/"/g, '""')}"` } return value } /** * 获取字段的影响范围(用于删除前警告) */ export async function getFieldImpactInfo(fieldKey: string): Promise<{ templateCount: number materialCount: number optionCount: number }> { // 获取使用该字段的模板数量 const templateCount = await prisma.uploadField.count({ where: { key: fieldKey }, }) // 获取字段定义 const field = await prisma.fieldDefinition.findUnique({ where: { key: fieldKey }, select: { id: true }, }) // 获取该字段的选项数量 const optionCount = field ? await prisma.fieldOption.count({ where: { fieldId: field.id }, }) : 0 // 获取使用该字段的素材数量 // 注意:这是一个近似值,只统计标准字段 let materialCount = 0 try { // 对于标准字段,直接查询 const standardFields = ['categoryName', 'sceneName', 'productModel', 'renderer', 'brandName', 'seriesName'] if (standardFields.includes(fieldKey)) { materialCount = await prisma.material.count({ where: { [fieldKey]: { not: '' }, }, }) } } catch { materialCount = 0 } return { templateCount, materialCount, optionCount, } }