导入历史跟踪机制实战指南

导入历史跟踪机制实战指南

一、概述

在后台管理系统中,Excel 批量导入是高频操作。用户提交导入后,通常需要知道:这次导入了多少条、成功多少、失败多少、失败文件在哪里下载。如果只在接口响应中返回结果,用户关闭页面后就无法再次查看。

导入历史跟踪机制解决了这个问题:每次导入操作都记录到一张"导入管理表"中,用户可以在"导入历史"页面随时查看所有历史导入记录和结果。


二、架构设计

核心思路:在导入流程的关键节点(开始、完成、失败),通过远程服务将导入状态和结果持久化到导入管理表中。

流程节点

节点 动作 status
导入开始 创建导入记录 1(进行中)
导入成功 更新记录(成功数、失败数、失败文件) 2(成功)
导入失败 更新记录(异常信息) 3(失败)

数据流向

Controller 接收文件 ↓ 解析 Excel → 生成 taskId → 创建导入记录(status=1) ↓ Service 逐条处理 ↓ 处理完成 → 更新导入记录(status=2,含结果统计) ↓(异常时) 捕获异常 → 更新导入记录(status=3)→ 抛出异常


三、核心数据模型

3.1 导入记录表

复制代码
CREATE TABLE import_manage (
    id             BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    member_id      INT          NULL COMMENT '所属用户/租户ID',
    function_code  VARCHAR(100) NOT NULL COMMENT '功能标识',
    function_name  VARCHAR(100) NOT NULL COMMENT '功能名称',
    task_id        VARCHAR(50)  NOT NULL COMMENT '任务唯一ID',
    file_name      VARCHAR(200) NULL COMMENT '导入文件名',
    file_path      VARCHAR(500) NULL COMMENT '失败文件下载地址',
    qty            INT          NULL COMMENT '导入总条数',
    success_qty    INT          NULL COMMENT '成功条数',
    error_qty      INT          NULL COMMENT '失败条数',
    status         TINYINT      NOT NULL COMMENT '1-进行中 2-成功 3-失败',
    operator_code  VARCHAR(50)  NULL COMMENT '操作人工号',
    operator_name  VARCHAR(50)  NULL COMMENT '操作人姓名',
    create_time    DATETIME     NOT NULL COMMENT '创建时间',
    update_time    DATETIME     NOT NULL COMMENT '更新时间'
) COMMENT '导入管理表';

3.2 导入记录 DTO

java 复制代码
@Data
public class ImportManageDto implements Serializable {
    private Integer memberId;       // 所属用户ID
    private String functionCode;    // 功能标识(唯一区分不同业务)
    private String functionName;    // 功能名称(前端展示)
    private String taskId;          // 任务唯一ID(UUID)
    private String fileName;        // 导入文件名
    private String filePath;        // 失败文件OSS地址
    private Integer qty;            // 导入总条数
    private Integer successQty;     // 成功条数
    private Integer errorQty;       // 失败条数
    private Integer status;         // 1-进行中 2-成功 3-失败
    private Integer userId;         // 操作人ID
    private String operatorCode;    // 操作人工号
    private String operatorName;    // 操作人姓名
}

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

四、完整示例

4.1 远程服务接口(Feign)

java 复制代码
@FeignClient(name = "base-service")
public interface BaseFeign {

    /**
     * 创建导入记录.
     */
    @PostMapping("/inner/import/import-manage/save-import-manage")
    RestControllerResult<Boolean> saveImportManage(@RequestBody ImportManageDto paramsDto);

    /**
     * 更新导入记录.
     */
    @PostMapping("/inner/import/import-manage/update-import-manage")
    RestControllerResult<Boolean> updateImportManage(@RequestBody ImportManageDto paramsDto);
}

4.2 Controller 实现

java 复制代码
@Slf4j
@RestController
@RequestMapping("/api/page/product")
public class ProductImportController {

    @Resource
    private ProductImportService productImportService;

    @Resource
    private BaseFeign baseFeign;

    @PostMapping("/import-product")
    public RestControllerResult<ImportResultDto> importProduct(
            @RequestParam("file") MultipartFile file) {
        // 1. 文件校验
        if (file == null || file.isEmpty()) {
            throw new BusinessException("导入文件不能为空");
        }
        String fileName = file.getOriginalFilename();
        if (fileName == null || (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx"))) {
            throw new BusinessException("仅支持Excel格式文件");
        }

        // 2. 构建导入历史记录
        ImportManageDto importManage = new ImportManageDto();
        importManage.setMemberId(UserContext.getMemberId());
        importManage.setFunctionCode("productImport");
        importManage.setFunctionName("商品信息导入");
        importManage.setTaskId(UUID.randomUUID().toString().replaceAll("-", ""));
        importManage.setFileName(fileName);
        importManage.setStatus(1); // 进行中
        importManage.setUserId(UserContext.getUserId());
        importManage.setOperatorCode(UserContext.getUserCode());
        importManage.setOperatorName(UserContext.getUserName());

        try {
            // 3. 解析Excel
            List<ProductImportDto> dataList = ExcelUtil.parse(file, ProductImportDto.class);
            if (dataList == null || dataList.isEmpty()) {
                throw new BusinessException("导入文件中无有效数据");
            }

            // 4. 记录导入总数,创建导入记录
            importManage.setQty(dataList.size());
            baseFeign.saveImportManage(importManage);

            // 5. 执行导入
            ImportResultDto importResult = productImportService.importProduct(dataList);

            // 6. 导入成功,更新记录
            importManage.setSuccessQty(importResult.getSuccessCount());
            importManage.setErrorQty(importResult.getFailCount());
            importManage.setFilePath(importResult.getFailUrl());
            importManage.setStatus(2); // 成功
            baseFeign.updateImportManage(importManage);

            RestControllerResult<ImportResultDto> result = new RestControllerResult<>();
            result.setSuccess(true);
            result.setData(importResult);
            return result;

        } catch (BusinessException e) {
            // 7. 业务异常,记录失败
            importManage.setStatus(3);
            baseFeign.saveImportManage(importManage);
            throw e;
        } catch (Exception e) {
            // 8. 系统异常,记录失败
            log.error("导入异常", e);
            importManage.setStatus(3);
            baseFeign.saveImportManage(importManage);
            throw new BusinessException("导入失败,请检查文件格式");
        }
    }
}

4.3 前端导入历史页面

前端可以调用导入历史查询接口,展示:

导入时间 功能名称 文件名 总条数 成功 失败 状态 操作
2026-05-29 14:30 商品信息导入 product_20260529.xlsx 100 97 3 成功 下载失败文件
2026-05-29 10:15 商品信息导入 product_batch.xlsx 50 50 0 成功 -
2026-05-28 16:00 商品信息导入 invalid.doc 0 0 0 失败 -

五、关键设计点

5.1 taskId 的作用

  • 唯一标识:每次导入生成唯一 UUID,用于关联创建和更新操作
  • 幂等保证:同一个 taskId 不会重复创建记录
  • 异步场景:如果导入是异步的,前端可通过 taskId 轮询进度

5.2 functionCode 设计

java 复制代码
// 不同业务使用不同的 functionCode
"productImport"          // 商品导入
"customerImport"         // 客户导入
"stockCheckImport"       // 库存盘点导入
"warehousePriorityImport" // 仓库优先级导入

作用:

  • 前端按 functionCode 筛选展示对应模块的导入历史
  • 后端可以按 functionCode 做权限控制

5.3 status 状态流转

复制代码
创建时 → status=1(进行中)
          ↓
    ┌─────┴─────┐
    ↓           ↓
status=2      status=3
(成功)      (失败)

注意:状态只会从 1 流转到 2 或 3,不会回退。

5.4 filePath 的使用

  • 导入全部成功时:filePath 为空
  • 导入部分失败时:filePath 存放失败数据 Excel 的 OSS 下载地址
  • 前端根据 filePath 是否为空决定是否显示"下载失败文件"按钮

六、异常处理策略

6.1 何时创建 vs 何时更新

场景 调用方法 说明
文件解析成功,开始处理 saveImportManage(status=1) 此时已知总条数
处理完成 updateImportManage(status=2) 更新成功/失败数
文件解析失败 saveImportManage(status=3) 直接标记失败
处理过程中异常 updateImportManage(status=3) 更新为失败

6.2 Feign 调用失败的处理

java 复制代码
// 导入历史记录是辅助功能,不能影响主流程
try {
    baseFeign.saveImportManage(importManage);
} catch (Exception e) {
    log.warn("创建导入记录失败,不影响导入流程", e);
    // 不抛出异常,继续执行导入
}

6.3 异常时的记录策略

java 复制代码
try {
    // 正常流程...
} catch (BusinessException e) {
    // 业务异常(如文件为空):记录失败状态
    importManage.setStatus(3);
    baseFeign.saveImportManage(importManage);
    throw e; // 继续抛出给前端
} catch (Exception e) {
    // 系统异常:记录失败状态
    importManage.setStatus(3);
    baseFeign.saveImportManage(importManage);
    throw new BusinessException("导入失败");
}

七、同步导入 vs 异步导入的历史跟踪差异

维度 同步导入 异步导入(MQ)
创建记录时机 Controller 中解析文件后 Controller 中发送 MQ 前
更新记录时机 Service 返回后立即更新 MQ 消费者处理完成后更新
前端交互 等待接口返回结果 返回 taskId,轮询进度
失败处理 catch 中更新 status=3 消费者 catch 中更新
适用场景 数据量小(< 1000条) 数据量大(> 1000条)

异步导入示例

java 复制代码
// Controller:创建记录 + 发MQ
importManage.setStatus(1);
baseFeign.saveImportManage(importManage);
mqSender.send(dataList, importManage); // 发MQ
return taskId; // 返回taskId给前端

// MQ Consumer:处理完成后更新记录
try {
    ImportResultDto result = doImport(dataList);
    importManage.setSuccessQty(result.getSuccessCount());
    importManage.setStatus(2);
    baseFeign.updateImportManage(importManage);
} catch (Exception e) {
    importManage.setStatus(3);
    baseFeign.updateImportManage(importManage);
}

八、导入历史查询接口设计

java 复制代码
// 查询当前用户的导入历史
@GetMapping("/list-import-history")
public RestControllerResult<List<ImportHistoryDto>> listImportHistory(
        @RequestParam String functionCode,
        @RequestParam(required = false) Integer pageNum,
        @RequestParam(required = false) Integer pageSize) {
    // 按 memberId + functionCode 查询,按时间倒序
}

返回示例:

json 复制代码
{
  "success": true,
  "data": [
    {
      "taskId": "abc123",
      "functionName": "商品信息导入",
      "fileName": "product_20260529.xlsx",
      "qty": 100,
      "successQty": 97,
      "errorQty": 3,
      "status": 2,
      "filePath": "https://oss.example.com/fail_abc123.xlsx",
      "createTime": "2026-05-29 14:30:25",
      "operatorName": "admin 张三"
    }
  ]
}

九、最佳实践清单

  1. taskId 使用 UUID:保证全局唯一,便于关联创建和更新
  2. functionCode 按业务模块区分:方便前端按模块展示历史
  3. 记录创建时机在文件解析成功后:确保有 qty(总条数)信息
  4. 导入历史记录不影响主流程:Feign 调用失败时仅记录日志,不抛异常
  5. 状态只前进不回退:1→2 或 1→3
  6. 失败文件地址存 filePath:前端据此展示下载按钮
  7. catch 中必须更新状态为失败:避免记录一直处于"进行中"
  8. 设置操作人信息:方便管理员排查是谁做的导入
  9. 异步导入时前端轮询 taskId:通过 status 判断是否完成
  10. 导入历史定期清理:超过 30 天的记录可以归档或删除
相关推荐
日取其半万世不竭1 小时前
Uptime Kuma 应该放哪台机器?
java·docker·容器·https
肖爱Kun1 小时前
GB28181启动传参的设计
linux·服务器·数据库
消失的旧时光-19431 小时前
Kotlin 协程设计思想(四):launch、async、withContext 到底有什么区别?
java·kotlin·async·launch·withcontext·deferred
夜白宋1 小时前
【Redis深入】二、高性能
java·前端·redis
空圆小生1 小时前
Vue3 + Spring Boot 全栈实战:从零搭建在线彩票模拟系统
java·spring boot·后端
devpotato1 小时前
ArrayList 扩容机制:从源码细节到工程实践
java·list
剑神一笑1 小时前
Linux systemctl 服务管理命令:从 systemd 架构到实战技巧
linux·服务器·架构
运维瓦工1 小时前
DevOps 生态介绍(八):docker &dockerfile 命令介绍及构建项目的第一个镜像
java·docker·devops
艾莉丝努力练剑1 小时前
【Linux网络】传输层协议TCP(六)补充 - 面试题:HTTP 获取网页的完整过程
linux·运维·网络·tcp/ip·计算机网络·http·udp