ERP 资源大批量导入实践:PostgreSQL Staging 临时表 + 异步任务

ERP 资源大批量导入实践:PostgreSQL Staging 临时表 + 异步任务

摘要

本文基于自研 ERP中资源(Resource)模块的真实生产实现,完整讲解:为何引入 Staging 缓冲表、表结构如何设计、Java + MyBatis 如何实现主表 upsert + SKU 批量替换 ,以及异步接口 如何彻底解决大文件 HTTP 超时问题。

方案全程使用 PostgreSQL 原生语法,性能比传统逐条导入提升 10~100 倍。

关键词:Excel 导入、PostgreSQL、ON CONFLICT、DISTINCT ON、Staging、Spring Async、EasyExcel、MyBatis、ERP、高并发导入


一、背景与痛点

传统资源导入在 Java 应用层逐条 调用 createResource、写入 SKU,存在三大致命问题:

  1. 数据库往返爆炸:IO 次数与 Excel 行数线性增长,大文件极慢
  2. 超时不可控:HTTP / 网关超时,前端无感知,数据半写半不写
  3. 业务逻辑难处理:同一供应商货号(product_number)对应多行 SKU,主表字段取值、旧 SKU 清理逻辑复杂

本方案采用企业级标准架构
Excel 解析 → 批量写入 Staging 缓冲表 → 数据库级集合 SQL 合并正式表 → 异步任务 + 导入历史


二、为何用「Staging 持久表」(不是临时表)

很多人直接用 TEMP TABLE,生产异步场景下必坑 。本系统使用持久表 erp_resource_import_staging

对比项 逐条 Java 写库 Staging + 集合 SQL
数据库往返 行数级,极高 仅几次大 SQL
数据计算 循环查改,低效 PostgreSQL 原生 DISTINCT ON / JOIN / ON CONFLICT
批次隔离 无法隔离 batch_id 唯一标记一趟导入
故障排查 无中间数据 可查询 staging 定位错误
异步支持 不支持 完美支持异步线程、多节点部署

核心结论

Staging = 导入中间缓冲区,合并完成后按 batch_id 删除即可复用。


三、数据库设计

3.1 Staging 表结构 + 索引

sql 复制代码
-- 文件:sql/postgresql/add/erp_resource_import_staging.sql
CREATE TABLE IF NOT EXISTS erp_resource_import_staging (
    id               int8         NOT NULL DEFAULT nextval('erp_resource_import_staging_seq'::regclass),
    batch_id         varchar(64)  NOT NULL,
    line_no          int4         NOT NULL,
    tenant_id        int8         NOT NULL DEFAULT 0,
    product_number   varchar(50)  NOT NULL,
    group_code       varchar(100) NOT NULL,
    name             varchar(100) NOT NULL,
    model_no         varchar(50)  NOT NULL,
    list_price       int4         NOT NULL,
    unit             varchar(50)  DEFAULT NULL,
    package_unit     varchar(50)  DEFAULT NULL,
    packing_spec     varchar(100) DEFAULT NULL,
    properties_json  varchar(1024) DEFAULT NULL,
    error_message    text         DEFAULT NULL,
    create_time      timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT pk_erp_resource_import_staging PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS idx_erp_res_imp_staging_batch ON erp_resource_import_staging (batch_id);
CREATE INDEX IF NOT EXISTS idx_erp_res_imp_staging_batch_tenant ON erp_resource_import_staging (batch_id, tenant_id);

核心字段说明

  • batch_id:一次导入一个 UUID,用于隔离批次、定位数据
  • line_no:行号,用于 DISTINCT ON第一行作为主表数据
  • properties_json:Java 提前序列化,避免数据库复杂解析
  • 一行 = 一个 SKU,主表字段在每行重复携带

3.2 正式表唯一索引(UPSERT 必须依赖)

PostgreSQL ON CONFLICT 必须与唯一索引严格一致

sql 复制代码
CREATE UNIQUE INDEX IF NOT EXISTS ux_erp_resource_tenant_pn_active
    ON erp_resource (tenant_id, product_number)
    WHERE deleted = false AND product_number IS NOT NULL AND btrim(product_number) <> '';

四、Mapper 接口:5 条固定顺序 SQL(核心管道)

java 复制代码
public interface ErpResourceImportStagingMapper {
    // 1. 批量插入 staging
    void insertBatch(@Param("list") List<ErpResourceImportStagingDO> list);
    // 2. 主表 UPSERT(去重+更新)
    void upsertResourcesFromStaging(...);
    // 3. 回填 spu_id = id
    void updateResourceSpuIdForBatch(...);
    // 4. 删除本次导入商品的旧 SKU
    void deleteSkusForBatchProducts(...);
    // 5. 批量插入新 SKU
    void insertSkusFromStaging(...);
    // 清理 staging
    void deleteStagingByBatch(...);
}

执行顺序(绝对不能乱)

  1. insertBatch → 写 Staging
  2. upsertResourcesFromStaging → 主表 Upsert
  3. updateResourceSpuIdForBatch → 回填 SPU ID
  4. deleteSkusForBatchProducts → 删除旧 SKU
  5. insertSkusFromStaging → 写入新 SKU
  6. deleteStagingByBatch → 清理 Staging

五、服务端核心实现

5.1 事务核心方法:importResourceListViaStaging

java 复制代码
@Override
@Transactional(rollbackFor = Exception.class)
public ResourceImportRespVO importResourceListViaStaging(...) {
    // 1. 生成批次 ID
    String batchId = UUID.randomUUID().toString().replace("-", "");
    List<ErpResourceImportStagingDO> stagingRows = new ArrayList<>();
    
    // 2. 组装 Staging 数据(一行一个 SKU)
    // ... 业务校验、Excel 转换 ...
    
    // 3. 分批插入 Staging(每 200 条,防止 SQL 过大)
    final int chunk = 200;
    for (int i = 0; i < stagingRows.size(); i += chunk) {
        int end = Math.min(i + chunk, stagingRows.size());
        resourceImportStagingMapper.insertBatch(stagingRows.subList(i, end));
    }

    // 4. 5 条核心 SQL 顺序执行
    resourceImportStagingMapper.upsertResourcesFromStaging(batchId, tenantId, creator);
    resourceImportStagingMapper.updateResourceSpuIdForBatch(batchId, tenantId, creator);
    resourceImportStagingMapper.deleteSkusForBatchProducts(batchId, tenantId, creator);
    resourceImportStagingMapper.insertSkusFromStaging(batchId, tenantId, creator);
    
    // 5. 清理
    resourceImportStagingMapper.deleteStagingByBatch(batchId);
}

5.2 同步接口(小文件/联调)

java 复制代码
@PostMapping("/import-via-staging")
public CommonResult<ResourceImportRespVO> importExcelViaStaging(...) {
    // EasyExcel 读取 → 分组 → 调用 staging 导入
}

5.3 异步接口(大文件生产必备)

java 复制代码
@PostMapping("/import-via-staging-async")
public CommonResult<ResourceImportAsyncSubmitRespVO> importExcelViaStagingAsync(...) {
    // 文件转存临时文件
    tempFile = Files.createTempFile("erp-resource-import-", ".xlsx");
    file.transferTo(tempFile.toFile());
    
    // 创建导入历史
    Long historyId = importHistoryService.createPendingImportHistory(...);
    
    // 异步执行
    resourceImportAsyncService.importResourceViaStagingFromFileAsync(historyId, tempFile, updateSupport, tenantId);
    
    // 立即返回,不阻塞前端
    return success(vo);
}

异步服务核心

java 复制代码
@Async
public void importResourceViaStagingFromFileAsync(...) {
    try {
        TenantUtils.execute(tenantId, () -> runStagingImport(historyId, tempFile, updateSupport));
    } finally {
        Files.deleteIfExists(tempFile);
    }
}

六、前端对接

  1. 导入按钮 → 弹出上传框
  2. 请求地址:/erp/resource/import-via-staging-async
  3. 上传成功 → 返回 importHistoryId
  4. 提示用户去导入历史查看进度与结果
javascript 复制代码
const importUrl = import.meta.env.VITE_BASE_URL + '/erp/resource/import-via-staging-async'

七、部署 Checklist(上线必看)

  1. 执行 PostgreSQL 脚本:创建 erp_resource_import_staging + 唯一索引
  2. 后端开启 @EnableAsync 异步配置
  3. 分配权限:erp:resource:import
  4. 大文件只使用异步接口
  5. 异常残留 Staging:可按 batch_id 手工 DELETE

八、注意与限制

  1. 强依赖 PostgreSQL :使用 DISTINCT ON / ON CONFLICT / 部分唯一索引,换库需重写
  2. Java 仅做校验 + 组装,真正高性能靠数据库批量 SQL
  3. 同步接口只适合小文件
  4. 异步异常不会丢失,会记录在导入历史

九、总结

这套Staging + 异步 + 数据库批量 SQL 是企业级 ERP 大批量导入的标准最优方案

  1. Java 负责校验、组装、分批写入 Staging
  2. PostgreSQL 用 5 条 SQL 完成:主表 Upsert → SPU 回填 → SKU 替换
  3. 异步任务 解决超时,导入历史 提供可观测性
  4. Staging 表 是整套架构的核心骨架,通过 batch_id/line_no 保证数据安全、可重试、可排查

相比传统逐条导入,性能提升10~100 倍,完全支撑万行级 Excel 稳定导入。


相关推荐
星辰_mya2 小时前
jvm之生老病死
jvm·数据库·面试·架构师
王二车8 小时前
交叉编译microcom ARM终端串口调试工具
数据库
xxxibolva10 小时前
SQL 学习
数据库·sql·学习
孪生质数-10 小时前
MySQL主从延迟根因诊断法
数据库·mysql
bLEd RING10 小时前
Redis 设置密码无效问题解决
数据库·redis·缓存
WiChP11 小时前
【V0.1B5】从零开始的2D游戏引擎开发之路
java·服务器·数据库
751158912 小时前
笔记:postgresql如何下载驱动并安装?
数据库·postgresql
荒川之神12 小时前
拉链表概念与基本设计
java·开发语言·数据库
Highcharts.js12 小时前
适合报表系统的可视化图表|Highcharts支持直接导出PNG和PDF
javascript·数据库·react.js·pdf