数据的导入导出是企业信息系统的核心功能之一。本文将介绍 IMS 系统中 Excel/CSV 导入导出的完整设计方案。
核心功能需求
- 多格式支持 - 支持 Excel (.xlsx, .xls) 和 CSV 格式
- 模板下载 - 提供标准导入模板,用户按模板填写
- 数据校验 - 格式校验、业务规则校验、重复检测
- 大文件处理 - 支持万级数据导入,需要进度提示
- 错误报告 - 导入失败时给出详细错误位置和原因
- 分批导入 - 大数据量分批入库,防止事务超时
整体架构设计
导入导出系统分为四个模块:
- 解析层 - 解析 Excel/CSV 文件
- 校验层 - 数据格式和业务规则校验
- 处理层 - 数据转换和入库
- 导出层 - 生成导出文件
导入模板定义
首先定义导入模板的数据结构:
import-template.js
// 导入模板定义 const ImportTemplate = { customer: { name: '客户导入模板', fields: [ { key: 'customer_code', label: '客户编码', required: true, type: 'string', pattern: /^[A-Z0-9]{6,10}$/, errorMsg: '客户编码必须为6-10位大写字母或数字' }, { key: 'customer_name', label: '客户名称', required: true, type: 'string', maxLength: 100 }, { key: 'contact_person', label: '联系人', required: false, type: 'string', maxLength: 50 }, { key: 'phone', label: '联系电话', required: true, type: 'string', pattern: /^1[3-9]\d{9}$/ }, { key: 'status', label: '状态', required: false, type: 'enum', options: ['正常', '禁用'], default: '正常' } ] } };
文件解析模块
解析 Excel 和 CSV 文件:
file-parser.js
// 文件解析服务 const xlsx = require('xlsx'); class FileParser { // 解析 Excel 文件 parseExcel(buffer) { const workbook = xlsx.read(buffer, { type: 'buffer' }); const sheetName = workbook.SheetNames[0]; const sheet = workbook.Sheets[sheetName]; return xlsx.utils.sheet_to_json(sheet); } // 解析 CSV 文件 parseCSV(content) { const lines = content.split('\n').filter(l => l.trim()); if (lines.length < 2) return []; const headers = lines[0].split(',').map(h => h.trim()); const data = []; for (let i = 1; i < lines.length; i++) { const values = parseCSVLine(lines[i]); const row = {}; headers.forEach((h, idx) => { row[h] = values[idx] || ''; }); data.push(row); } return data; } // 自动检测文件类型并解析 parse(file) { const ext = file.originalname.split('.').pop().toLowerCase(); switch(ext) { case 'xlsx': case 'xls': return this.parseExcel(file.buffer); case 'csv': return this.parseCSV(file.buffer.toString('utf-8')); default: throw new Error('不支持的文件格式'); } } }
数据校验模块
实现多层次数据校验:
data-validator.js
// 数据校验服务 class DataValidator { constructor(template) { this.template = template; this.errors = []; } validate(data) { this.errors = []; data.forEach((row, index) => { const rowNum = index + 2; // 跳过表头 this.validateRow(row, rowNum); }); return { valid: this.errors.length === 0, errors: this.errors, successCount: data.length - this.errors.filter(e => e.level === 'error').length }; } validateRow(row, rowNum) { for (const field of this.template.fields) { const value = row[field.key]; // 必填校验 if (field.required && !value) { this.addError(rowNum, field.key, '必填字段不能为空'); continue; } if (!value) continue; // 类型校验 if (!this.validateType(value, field.type)) { this.addError(rowNum, field.key, `${field.label}格式错误`); } // 正则校验 if (field.pattern && !field.pattern.test(value)) { this.addError(rowNum, field.key, field.errorMsg || `${field.label}格式不正确`); } // 长度校验 if (field.maxLength && value.length > field.maxLength) { this.addError(rowNum, field.key, `${field.label}不能超过${field.maxLength}个字符`); } // 枚举校验 if (field.options && !field.options.includes(value)) { this.addError(rowNum, field.key, `${field.label}必须为:${field.options.join('/')}`); } } } validateType(value, type) { switch(type) { case 'number': return !isNaN(value); case 'date': return !isNaN(Date.parse(value)); default: return true; } } addError(row, field, message) { this.errors.push({ row, field, message, level: 'error' }); } }
分批导入处理
大文件采用分批导入策略:
batch-importer.js
// 分批导入服务 class BatchImporter { constructor(batchSize = 500) { this.batchSize = batchSize; } async import(validData, onProgress) { const total = validData.length; let successCount = 0; let errorCount = 0; for (let i = 0; i < total; i += this.batchSize) { const batch = validData.slice(i, i + this.batchSize); try { await this.processBatch(batch); successCount += batch.length; } catch (e) { errorCount += batch.length; console.error(`批次导入失败: ${e.message}`); } // 进度回调 if (onProgress) { const progress = Math.min((i + batch.length) / total * 100, 100); onProgress({ progress, successCount, errorCount }); } } return { successCount, errorCount }; } async processBatch(batch) { // 事务处理,确保批次原子性 const conn = await db.getConnection(); await conn.beginTransaction(); try { for (const item of batch) { await conn.execute( `INSERT INTO customer (customer_code, customer_name, phone, status) VALUES (?, ?, ?, ?)`, [item.customer_code, item.customer_name, item.phone, item.status || '正常'] ); } await conn.commit(); } catch (e) { await conn.rollback(); throw e; } finally { conn.release(); } } }
导出功能实现
支持自定义列导出:
exporter.js
// 数据导出服务 class DataExporter { async exportToExcel(data, options = {}) { const worksheet = xlsx.utils.json_to_sheet(data); // 设置列宽 worksheet['!cols'] = calculateColumnWidth(data); const workbook = xlsx.utils.book_new(); xlsx.utils.book_append_sheet(workbook, worksheet, '数据导出'); return xlsx.write(workbook, { type: 'buffer', bookType: 'xlsx' }); } async exportToCSV(data) { const headers = Object.keys(data[0] || {}); let csv = headers.join(',') + '\n'; for (const row of data) { const values = headers.map(h => escapeCSVValue(row[h])); csv += values.join(',') + '\n'; } return csv; } } function escapeCSVValue(value) { if (value === null || value === undefined) return ''; const str = String(value); if (str.includes(',') || str.includes('"') || str.includes('\n')) { return `"${str.replace(/"/g, "")}"`; } return str; }
模板下载功能
生成带示例的导入模板:
template-generator.js
// 生成导入模板 function generateTemplate(template) { // 表头行 const headers = template.fields.map(f => f.label); const exampleRow = template.fields.map(f => { if (f.options) return f.options[0]; if (f.pattern) return '示例值'; return ''; }); const data = [headers, exampleRow]; const worksheet = xlsx.utils.aoa_to_sheet(data); // 冻结首行 worksheet['!freeze'] = { topRow: 1 }; const workbook = xlsx.utils.book_new(); xlsx.utils.book_append_sheet(workbook, worksheet, '导入模板'); return xlsx.write(workbook, { type: 'buffer', bookType: 'xlsx' }); }
总结
数据导入导出功能需要重点关注:模板标准化、数据多层次校验、大文件分批处理、错误详细报告。良好的导入导出体验能大幅提升用户使用效率。