PSI进销存软件

PSI - 专业进销存管理软件

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;
  }
}

总结

数据导入导出功能是进销存软件提高工作效率的重要工具,核心价值包括:

通过完善的导入导出功能,PSI 进销存软件可以大幅提升用户的数据处理效率。

← 下一篇:PSI进销存软件多语言国际化实现