PSI进销存软件数据导入导出功能设计
导入导出概述
数据导入导出是进销存软件的核心功能之一,支持用户批量处理商品、客户、供应商等基础数据,也可以将业务数据导出进行二次分析。本文介绍 PSI 进销存软件中数据导入导出功能的完整设计与实现。
功能架构设计
| 模块 | 导入 | 导出 | 格式 |
|---|---|---|---|
| 商品管理 | 商品批量添加、规格更新 | 商品列表、库存报表 | Excel、CSV |
| 客户管理 | 客户批量导入 | 客户列表、销售统计 | Excel、CSV |
| 供应商管理 | 供应商批量导入 | 供应商列表、采购统计 | Excel、CSV |
| 库存管理 | 库存盘点导入 | 库存台账、盘点报表 | Excel |
| 销售订单 | 订单批量导入 | 订单明细、销售报表 | Excel |
Excel 导入实现
完整的 Excel 数据导入服务:
// Excel 导入服务
const XLSX = require('xlsx');
class ExcelImportService {
constructor(validationService, dataService) {
this.Validation = validationService;
this.Data = dataService;
}
// 导入 Excel 文件
async importExcel(fileBuffer, config) {
const {
sheetName = 0,
headerRow = 1,
mappingRules = {},
validate = true,
skipInvalidRows = false,
batchSize = 100
} = config;
// 解析 Excel
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const sheet = workbook.Sheets[workbook.SheetNames[sheetName]];
const rawData = XLSX.utils.sheet_to_json(sheet, { header: headerRow });
if (rawData.length === 0) {
throw new Error('Excel 文件为空');
}
// 获取表头映射
const headers = Object.keys(rawData[0]);
const columnMapping = this.buildColumnMapping(headers, mappingRules);
// 验证必填列
const requiredColumns = Object.values(mappingRules)
.filter(rule => rule.required)
.map(rule => rule.targetField);
const missingColumns = requiredColumns.filter(col => !Object.values(columnMapping).includes(col));
if (missingColumns.length > 0) {
throw new Error(`缺少必填列:${missingColumns.join(', ')}`);
}
// 转换数据
const transformedData = rawData.map((row, index) => {
const item = {};
for (const [sourceCol, targetField] of Object.entries(columnMapping)) {
if (sourceCol in row) {
item[targetField] = this.transformValue(row[sourceCol], mappingRules[sourceCol]);
}
}
item._rowNumber = index + headerRow + 1; # 原始行号
return item;
});
// 数据验证
let validData = [];
let invalidRows = [];
if (validate) {
for (const item of transformedData) {
const errors = await this.Validation.validate(item, config.entityType);
if (errors.length > 0) {
if (skipInvalidRows) {
invalidRows.push({ row: item._rowNumber, errors });
} else {
throw new Error(`第 ${item._rowNumber} 行数据验证失败:${errors.join('; ')}`);
}
} else {
validData.push(item);
}
}
} else {
validData = transformedData;
}
// 批量插入数据
const importResult = await this.batchInsert(validData, batchSize);
return {
success: true,
totalRows: rawData.length,
validRows: validData.length,
invalidRows: invalidRows.length,
importCount: importResult.insertedCount,
updateCount: importResult.updatedCount,
failedRows: invalidRows,
errors: importResult.errors
};
}
// 构建列映射
buildColumnMapping(headers, mappingRules) {
const mapping = {};
for (const header of headers) {
// 精确匹配
for (const [sourceCol, rule] of Object.entries(mappingRules)) {
if (header === sourceCol || header === rule.alias) {
mapping[header] = rule.targetField;
break;
}
}
// 模糊匹配(别名)
if (!mapping[header]) {
for (const [sourceCol, rule] of Object.entries(mappingRules)) {
if (rule.aliases && rule.aliases.includes(header)) {
mapping[header] = rule.targetField;
break;
}
}
}
}
return mapping;
}
// 数据类型转换
transformValue(value, rule) {
if (value === null || value === undefined || value === '') {
return rule.required ? undefined : null;
}
switch (rule.type) {
case 'string':
return String(value).trim();
case 'number':
const num = Number(value);
return isNaN(num) ? undefined : num;
case 'integer':
return parseInt(value, 10);
case 'boolean':
if (typeof value === 'boolean') return value;
return ['true', '1', 'yes', '是'].includes(String(value).toLowerCase());
case 'date':
if (value instanceof Date) return value;
return new Date(value);
case 'enum':
return rule.options.includes(value) ? value : undefined;
case 'currency':
return parseFloat(String(value).replace(/[^0-9.-]/g, ''));
default:
return value;
}
}
// 批量插入
async batchInsert(data, batchSize) {
const result = {
insertedCount: 0,
updatedCount: 0,
errors: []
};
for (let i = 0; i < data.length; i += batchSize) {
const batch = data.slice(i, i + batchSize);
try {
const insertResult = await this.Data.batchUpsert(batch);
result.insertedCount += insertResult.insertedCount;
result.updatedCount += insertResult.updatedCount;
} catch (error) {
result.errors.push({
batch: Math.floor(i / batchSize) + 1,
error: error.message
});
}
}
return result;
}
}
// 导入配置示例
const importConfig = {
entityType: 'product',
sheetName: 0,
headerRow: 1,
mappingRules: {
'商品编码': {
targetField: 'productCode',
type: 'string',
required: true,
alias: 'SKU'
},
'商品名称': {
targetField: 'productName',
type: 'string',
required: true
},
'商品分类': {
targetField: 'category',
type: 'string',
alias: ['类别', '分类']
},
'单位': {
targetField: 'unit',
type: 'string',
default: '个'
},
'进价': {
targetField: 'costPrice',
type: 'currency'
},
'售价': {
targetField: 'salePrice',
type: 'currency'
},
'库存': {
targetField: 'stock',
type: 'integer',
default: 0
},
'状态': {
targetField: 'status',
type: 'enum',
options: ['在售', '停售', '待进货'],
default: '在售'
}
},
validate: true,
skipInvalidRows: true,
batchSize: 100
};
Excel 导出实现
支持大数据量导出的优化实现:
// Excel 导出服务
const XLSX = require('xlsx');
class ExcelExportService {
constructor(queryService, formatter) {
this.Query = queryService;
this.Formatter = formatter;
}
// 导出数据
async exportData(config) {
const {
entityType,
query = {},
columns = [],
sheetName = '数据',
title,
formatOptions = {},
maxRowsPerSheet = 100000
} = config;
// 获取导出数据
const data = await this.Query.find(entityType, query);
if (data.length === 0) {
throw new Error('没有可导出的数据');
}
// 创建工作簿
const workbook = XLSX.utils.book_new();
// 分页处理大数据量
const sheets = this.splitDataIntoSheets(data, maxRowsPerSheet);
for (let i = 0; i < sheets.length; i++) {
const sheetData = sheets[i];
const currentSheetName = sheets.length > 1 ? `${sheetName}_${i + 1}` : sheetName;
// 构建表头
const headerRow = this.buildHeaderRow(columns);
// 转换数据行
const dataRows = sheetData.map(item => this.transformRow(item, columns, formatOptions));
// 合并表头和数据
const sheetDataArray = [headerRow, ...dataRows];
// 创建工作表
const worksheet = XLSX.utils.aoa_to_sheet(sheetDataArray);
// 设置列宽
worksheet['!cols'] = columns.map(col => ({
wch: col.width || 15
}));
// 添加标题行
if (title) {
XLSX.utils.sheet_add_aoa(worksheet, [[title]], { origin: 'A1' });
worksheet['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: columns.length - 1 } }];
}
XLSX.utils.book_append_sheet(workbook, worksheet, currentSheetName);
}
// 生成 buffer
const buffer = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'buffer'
});
return buffer;
}
// 构建表头行
buildHeaderRow(columns) {
return columns.map(col => col.label || col.field);
}
// 转换数据行
transformRow(item, columns, formatOptions) {
return columns.map(col => {
let value = item[col.field];
// 格式化处理
if (col.formatter) {
value = col.formatter(value, item);
} else if (col.formatType) {
value = this.formatValue(value, col.formatType, formatOptions);
}
return value;
});
}
// 值格式化
formatValue(value, formatType, options) {
if (value === null || value === undefined) {
return '';
}
switch (formatType) {
case 'currency':
return this.Formatter.formatCurrency(value, options.currency || 'CNY');
case 'number':
return this.Formatter.formatNumber(value, {
minimumFractionDigits: options.decimalPlaces || 2,
maximumFractionDigits: options.decimalPlaces || 2
});
case 'percent':
return this.Formatter.formatPercent(value, options.decimalPlaces || 2);
case 'date':
return this.Formatter.formatDate(value, options.dateFormat || 'short');
case 'datetime':
return this.Formatter.formatDate(value, 'medium') + ' ' +
this.Formatter.formatTime(value);
case 'boolean':
return value ? '是' : '否';
case 'enum':
return options.enumLabels?.[value] || value;
default:
return value;
}
}
// 分页处理大数据量
splitDataIntoSheets(data, maxRowsPerSheet) {
const sheets = [];
for (let i = 0; i < data.length; i += maxRowsPerSheet) {
sheets.push(data.slice(i, i + maxRowsPerSheet));
}
return sheets;
}
}
// 导出配置示例
const exportConfig = {
entityType: 'product',
query: {
status: '在售',
category: { $in: ['电子产品', '办公用品'] }
},
sheetName: '商品数据',
title: 'PSI商品数据导出',
columns: [
{ field: 'productCode', label: '商品编码', width: 15 },
{ field: 'productName', label: '商品名称', width: 25 },
{ field: 'category', label: '分类', width: 12 },
{ field: 'unit', label: '单位', width: 8 },
{ field: 'costPrice', label: '进价', width: 12, formatType: 'currency' },
{ field: 'salePrice', label: '售价', width: 12, formatType: 'currency' },
{ field: 'stock', label: '库存', width: 10, formatType: 'number' },
{ field: 'status', label: '状态', width: 10 },
{
field: 'createdAt',
label: '创建时间',
width: 18,
formatType: 'datetime',
formatter: (value) => value ? new Date(value).toLocaleString('zh-CN') : ''
}
],
formatOptions: {
currency: 'CNY',
decimalPlaces: 2,
dateFormat: 'short'
}
};
导入模板管理
标准化导入模板生成:
// 导入模板服务
class ImportTemplateService {
constructor(excelService) {
this.Excel = excelService;
}
// 生成导入模板
async generateTemplate(entityType) {
const templateConfig = this.getTemplateConfig(entityType);
const headers = templateConfig.columns.map(col => ({
header: col.label,
key: col.field,
width: col.width || 15
}));
// 添加示例数据
const sampleData = this.generateSampleData(templateConfig);
// 创建模板文件
const buffer = await this.Excel.exportData({
entityType,
columns: templateConfig.columns,
sheetName: '导入模板',
title: `${templateConfig.title}导入模板`,
data: sampleData
});
return {
buffer,
filename: `${entityType}_import_template.xlsx`,
description: templateConfig.description,
instructions: templateConfig.instructions
};
}
// 获取模板配置
getTemplateConfig(entityType) {
const templates = {
product: {
title: '商品',
description: '商品批量导入模板',
columns: [
{ field: 'productCode', label: '商品编码*', width: 15, required: true },
{ field: 'productName', label: '商品名称*', width: 25, required: true },
{ field: 'barcode', label: '商品条码', width: 18 },
{ field: 'category', label: '商品分类', width: 15 },
{ field: 'brand', label: '品牌', width: 15 },
{ field: 'unit', label: '单位', width: 10, default: '个' },
{ field: 'specification', label: '规格型号', width: 15 },
{ field: 'costPrice', label: '进价', width: 12 },
{ field: 'salePrice', label: '售价', width: 12 },
{ field: 'stock', label: '初始库存', width: 10 },
{ field: 'minStock', label: '最低库存', width: 10 },
{ field: 'supplierCode', label: '供应商编码', width: 15 },
{ field: 'status', label: '状态', width: 10, default: '在售' }
],
instructions: [
'带 * 号的字段为必填项',
'商品编码建议使用有规律的编码,便于管理',
'商品分类需与系统中已存在的分类一致',
'供应商编码需与系统中已存在的供应商编码一致',
'状态可选值:在售、停售、待进货'
]
},
customer: {
title: '客户',
description: '客户批量导入模板',
columns: [
{ field: 'customerCode', label: '客户编码*', width: 15, required: true },
{ field: 'customerName', label: '客户名称*', width: 25, required: true },
{ field: 'contact', label: '联系人', width: 12 },
{ field: 'phone', label: '联系电话', width: 15 },
{ field: 'mobile', label: '手机', width: 15 },
{ field: 'address', label: '地址', width: 30 },
{ field: 'email', label: '邮箱', width: 20 },
{ field: 'taxNo', label: '税号', width: 20 },
{ field: 'bankName', label: '开户行', width: 15 },
{ field: 'bankAccount', label: '银行账号', width: 20 },
{ field: 'creditLimit', label: '信用额度', width: 12 },
{ field: 'paymentDays', label: '账期天数', width: 10 },
{ field: 'status', label: '状态', width: 10, default: '正常' }
],
instructions: [
'带 * 号的字段为必填项',
'客户编码建议使用会员卡号或自定义编码',
'联系电话和手机至少填写一项',
'信用额度单位为元,默认为50000',
'账期天数默认0天(现结)'
]
},
supplier: {
title: '供应商',
description: '供应商批量导入模板',
columns: [
{ field: 'supplierCode', label: '供应商编码*', width: 15, required: true },
{ field: 'supplierName', label: '供应商名称*', width: 25, required: true },
{ field: 'contact', label: '联系人', width: 12 },
{ field: 'phone', label: '联系电话', width: 15 },
{ field: 'address', label: '地址', width: 30 },
{ field: 'email', label: '邮箱', width: 20 },
{ field: 'taxNo', label: '税号', width: 20 },
{ field: 'paymentDays', label: '账期天数', width: 10 },
{ field: 'status', label: '状态', width: 10, default: '正常' }
],
instructions: [
'带 * 号的字段为必填项',
'供应商编码用于关联商品和采购单',
'请确保供应商信息真实有效'
]
}
};
return templates[entityType];
}
// 生成示例数据
generateSampleData(config) {
const samples = [];
for (let i = 0; i < 3; i++) {
const row = {};
for (const col of config.columns) {
if (col.required) {
row[col.field] = col.default || `[示例${i + 1}]`;
} else if (col.default) {
row[col.field] = col.default;
}
}
samples.push(row);
}
return samples;
}
}
总结
数据导入导出功能是进销存软件提高工作效率的重要工具,核心价值包括:
- 批量处理:快速批量导入导出大量数据
- 数据校验:严格的数据验证保证数据质量
- 模板管理标准化模板简化用户操作
- 格式兼容:支持 Excel 和 CSV 多种格式
通过完善的导入导出功能,PSI 进销存软件可以大幅提升用户的数据处理效率。