Spring Boot 中实现动态列导入讲解和案例示范

news2024/9/29 23:21:03

在业务系统中,数据导入是非常常见的需求,尤其是在大规模数据处理场景中。一个灵活且高效的数据导入功能可以显著提高业务处理效率。在本文中,我们将详细讲解如何在 Spring Boot 中实现动态列导入功能,以电商交易系统为案例,探讨如何设计具备扩展性且高效的导入功能。

1. 动态列导入的需求分析

在电商系统中,不同的业务场景可能会涉及到不同的数据导入需求。例如,商品信息导入、订单信息导入、用户数据导入等,涉及到的数据结构可能不同,字段顺序也可能不一致。这时候,要求系统具备根据导入文件的动态变化进行数据解析和处理的能力。动态列导入的核心是:

  • 导入文件的列数和顺序可以动态变化。
  • 系统需要灵活匹配导入的列,并将其映射到相应的数据库表字段。
  • 数据导入过程要支持数据校验、异常处理、事务管理和性能优化。

2. 技术选型与依赖

在本案例中,我们选择以下技术栈来实现动态列导入功能:

  • Spring Boot:用于构建核心业务逻辑和接口。
  • MySQL:作为数据库管理系统,存储电商系统的商品、订单、用户等相关数据。
  • EasyExcel:用于解析 Excel 文件,支持读取 Excel 文件中的动态列数据。
  • MyBatis:用于数据库访问层,简化 SQL 操作。
Maven 依赖

pom.xml 文件中,我们需要添加以下依赖:

<dependencies>
    <!-- Spring Boot Web 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- MyBatis 依赖 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- EasyExcel 依赖 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>3.0.5</version>
    </dependency>

    <!-- Lombok 依赖,用于简化代码 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

3. 数据库表结构设计

在导入商品信息的场景中,我们首先设计商品信息的表结构。在实际应用中,表结构应能够动态匹配不同的导入数据,并允许根据业务需求进行扩展。以下是一个典型的商品表设计:

CREATE TABLE `product` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `product_code` VARCHAR(100) NOT NULL COMMENT '商品编码',
  `product_name` VARCHAR(255) NOT NULL COMMENT '商品名称',
  `category` VARCHAR(100) COMMENT '商品分类',
  `price` DECIMAL(10, 2) COMMENT '商品价格',
  `stock` INT COMMENT '库存数量',
  `description` TEXT COMMENT '商品描述',
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';

此表包含了常见的商品信息字段,比如 product_code(商品编码)、product_name(商品名称)、price(价格)、stock(库存)等字段。根据业务需求,动态列导入时,可以有选择性地导入其中的部分字段,或者根据导入的文件动态扩展数据。


4. Spring Boot 中实现动态列导入的详细步骤

4.1 定义数据模型

首先,我们定义导入文件对应的 DTO(数据传输对象),即与 Excel 列对应的模型类。为了实现动态列导入,我们可以使用 Map 作为数据容器,以支持不确定的列。

@Data
public class ProductImportDTO {
    private Map<String, Object> dynamicFields = new HashMap<>();
}
4.2 使用 EasyExcel 解析 Excel 文件

通过 EasyExcel,我们可以灵活地读取 Excel 文件中的列,并将其映射到 ProductImportDTO 对象中。以下是一个示例,展示如何使用 EasyExcel 读取动态列数据。

public class DynamicProductImportListener extends AnalysisEventListener<Map<Integer, String>> {

    private final List<ProductImportDTO> productList = new ArrayList<>();
    private List<String> excelHeaders;

    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        ProductImportDTO product = new ProductImportDTO();
        // 遍历每一列的数据,按列索引动态存储
        for (Map.Entry<Integer, String> entry : data.entrySet()) {
            String columnValue = entry.getValue();
            String columnName = excelHeaders.get(entry.getKey());
            product.getDynamicFields().put(columnName, columnValue);
        }
        productList.add(product);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 数据处理完后的逻辑
        saveProducts(productList);
    }

    // 设置 Excel 表头
    public void setExcelHeaders(List<String> headers) {
        this.excelHeaders = headers;
    }
}

在这个示例中,DynamicProductImportListener 类实现了 AnalysisEventListener,并负责将 Excel 文件中的每一行数据解析为 ProductImportDTO 对象。Map 用于动态获取每一列的数据,并将其存储到 dynamicFields 中。

4.3 Controller 处理导入请求

在控制层,我们定义一个接口,接受上传的 Excel 文件并处理导入逻辑。

@RestController
@RequestMapping("/api/products")
public class ProductImportController {

    @PostMapping("/import")
    public ResponseEntity<String> importProducts(@RequestParam("file") MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("文件不能为空");
        }
        
        // 读取表头
        List<String> excelHeaders = readExcelHeaders(file);
        
        // 动态解析 Excel 数据
        DynamicProductImportListener listener = new DynamicProductImportListener();
        listener.setExcelHeaders(excelHeaders);

        EasyExcel.read(file.getInputStream(), new HashMap<Integer, String>().getClass(), listener).sheet().doRead();

        return ResponseEntity.ok("导入成功");
    }

    // 获取表头
    private List<String> readExcelHeaders(MultipartFile file) throws IOException {
        // 读取 Excel 第一行表头
        ExcelReader excelReader = EasyExcel.read(file.getInputStream()).build();
        ReadSheet readSheet = EasyExcel.readSheet(0).build();
        List<String> headers = new ArrayList<>();
        
        excelReader.read(readSheet, new AnalysisEventListener<Map<Integer, String>>() {
            @Override
            public void invoke(Map<Integer, String> data, AnalysisContext context) {
                headers.addAll(data.values());
                context.interrupt();
            }
            @Override
            public void doAfterAllAnalysed(AnalysisContext context) {
            }
        });

        excelReader.finish();
        return headers;
    }
}
  • 逻辑说明:在上传 Excel 文件时,首先读取文件的表头信息,并将其传递给 DynamicProductImportListener 进行数据解析。在解析过程中,系统能够根据文件的动态变化,将每列的数据存储到 ProductImportDTO 对象中。

5. 数据校验与异常处理

数据导入过程中,校验是保证数据质量和系统稳定性的重要环节。对于电商系统中的数据导入,需要对文件中的数据进行多种类型的校验,例如必填校验、长度校验、日期格式校验、金额格式校验以及业务逻辑校验等。为了保证代码的简洁性和通用性,我们需要将这些校验逻辑进行组件化设计,支持不同业务模块的校验规则,并且能够灵活扩展。

5.1 校验框架的设计思路

为了实现灵活、可扩展的校验机制,我们可以将数据校验逻辑设计为以下几个组件:

  1. 校验接口 (ValidationRule):定义通用校验接口,不同类型的校验逻辑通过实现该接口来完成。
  2. 校验责任链 (ValidationChain):将不同的校验规则串联成责任链,按顺序对数据进行校验。
  3. 通用校验工厂 (ValidationFactory):根据业务需要,动态生成不同的校验规则集合,确保系统可以灵活扩展。
  4. 校验异常处理:为每种校验异常提供具体的处理机制,例如记录日志、返回给用户详细的错误信息等。
校验接口设计

首先,定义一个通用的校验接口,校验接口可以应用于不同类型的校验规则:

public interface ValidationRule<T> {
    /**
     * 校验方法
     * @param value 被校验的字段值
     * @return 校验是否通过
     */
    boolean validate(T value);

    /**
     * 返回校验失败的错误信息
     * @return 错误信息
     */
    String getErrorMessage();
}
校验规则实现

根据具体需求实现不同的校验规则,例如:必填校验、长度校验、日期格式校验、金额校验等。

1. 必填校验
public class NotNullValidationRule implements ValidationRule<String> {

    private final String fieldName;

    public NotNullValidationRule(String fieldName) {
        this.fieldName = fieldName;
    }

    @Override
    public boolean validate(String value) {
        return value != null && !value.trim().isEmpty();
    }

    @Override
    public String getErrorMessage() {
        return fieldName + " 不能为空";
    }
}
2. 长度校验
public class LengthValidationRule implements ValidationRule<String> {

    private final String fieldName;
    private final int maxLength;

    public LengthValidationRule(String fieldName, int maxLength) {
        this.fieldName = fieldName;
        this.maxLength = maxLength;
    }

    @Override
    public boolean validate(String value) {
        return value != null && value.length() <= maxLength;
    }

    @Override
    public String getErrorMessage() {
        return fieldName + " 长度不能超过 " + maxLength + " 字符";
    }
}
3. 日期格式校验
public class DateValidationRule implements ValidationRule<String> {

    private final String fieldName;
    private final String dateFormat;

    public DateValidationRule(String fieldName, String dateFormat) {
        this.fieldName = fieldName;
        this.dateFormat = dateFormat;
    }

    @Override
    public boolean validate(String value) {
        if (value == null || value.isEmpty()) return true;  // 空值不校验
        try {
            SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
            sdf.setLenient(false);
            sdf.parse(value);
            return true;
        } catch (ParseException e) {
            return false;
        }
    }

    @Override
    public String getErrorMessage() {
        return fieldName + " 日期格式错误,应为 " + dateFormat;
    }
}

4. 金额校验
public class MoneyValidationRule implements ValidationRule<String> {

    private final String fieldName;

    public MoneyValidationRule(String fieldName) {
        this.fieldName = fieldName;
    }

    @Override
    public boolean validate(String value) {
        if (value == null || value.isEmpty()) return true;  // 空值不校验
        try {
            new BigDecimal(value);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    @Override
    public String getErrorMessage() {
        return fieldName + " 金额格式错误";
    }
}

5. 业务逻辑校验(示例)

比如订单的状态校验,状态必须是某些特定值。

public class StatusValidationRule implements ValidationRule<String> {

    private final String fieldName;
    private final List<String> validStatuses;

    public StatusValidationRule(String fieldName, List<String> validStatuses) {
        this.fieldName = fieldName;
        this.validStatuses = validStatuses;
    }

    @Override
    public boolean validate(String value) {
        return validStatuses.contains(value);
    }

    @Override
    public String getErrorMessage() {
        return fieldName + " 状态无效,必须为 " + validStatuses.toString();
    }
}

5.2 责任链模式校验器

为了支持多种校验规则组合,采用责任链模式,将所有校验规则串联起来逐一执行。责任链模式可以确保每个校验规则独立,同时按需扩展。

public class ValidationChain<T> {

    private final List<ValidationRule<T>> rules = new ArrayList<>();

    public void addRule(ValidationRule<T> rule) {
        rules.add(rule);
    }

    public List<String> validate(T value) {
        List<String> errors = new ArrayList<>();
        for (ValidationRule<T> rule : rules) {
            if (!rule.validate(value)) {
                errors.add(rule.getErrorMessage());
            }
        }
        return errors;
    }
}

5.3 校验工厂

为了增强校验逻辑的灵活性和扩展性,可以使用工厂模式创建校验规则集合。工厂根据不同的业务场景生成特定的校验责任链,例如针对商品、订单等模块。

public class ValidationFactory {

    public ValidationChain<String> createProductValidationChain() {
        ValidationChain<String> chain = new ValidationChain<>();
        chain.addRule(new NotNullValidationRule("商品名称"));
        chain.addRule(new LengthValidationRule("商品名称", 100));
        chain.addRule(new MoneyValidationRule("商品价格"));
        chain.addRule(new DateValidationRule("创建时间", "yyyy-MM-dd"));
        return chain;
    }

    public ValidationChain<String> createOrderValidationChain() {
        ValidationChain<String> chain = new ValidationChain<>();
        chain.addRule(new NotNullValidationRule("订单编号"));
        chain.addRule(new StatusValidationRule("订单状态", Arrays.asList("待支付", "已支付", "已发货")));
        return chain;
    }
}

在业务逻辑中调用校验工厂来获取对应的校验规则,并进行校验:

ValidationFactory factory = new ValidationFactory();
ValidationChain<String> productValidationChain = factory.createProductValidationChain();

String productName = "exampleProduct";
List<String> errors = productValidationChain.validate(productName);
if (!errors.isEmpty()) {
    errors.forEach(System.out::println);
}

5.4 统一异常处理

在进行数据校验时,如果发生错误,可以通过统一的异常处理机制捕获并处理异常。通过自定义异常类来捕获校验失败,并提供有意义的错误提示。

public class ValidationException extends RuntimeException {
    private final List<String> errorMessages;

    public ValidationException(List<String> errorMessages) {
        this.errorMessages = errorMessages;
    }

    public List<String> getErrorMessages() {
        return errorMessages;
    }
}

在业务代码中,捕获校验异常并返回错误信息:

public void importProduct(ProductImportDTO product) {
    ValidationChain<String> chain = validationFactory.createProductValidationChain();
    List<String> errors = chain.validate(product.getProductName());
    if (!errors.isEmpty()) {
        throw new ValidationException(errors);
    }
    // 数据保存逻辑
}

在控制层处理异常并返回友好的提示信息:

@ExceptionHandler(ValidationException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(ValidationException e) {
    Map<String, Object> response = new HashMap<>();
    response.put("status", "error");
    response.put("errors", e.getErrorMessages());
    return ResponseEntity.badRequest().body(response);
}

5.5 校验系统的扩展性和通用性

通过以上设计,校验系统具备以下扩展性和通用性:

  1. 灵活扩展:可以根据业务需求增加新的校验规则,如手机号校验、电子邮件校验等。
  2. 可复用性:校验规则和责任链可跨模块使用,减少重复代码,提高代码复用率。
  3. 易维护性:通过责任链模式,将不同校验规则分开,实现单一职责,便于维护。
  4. 业务无关性:校验规则与具体业务逻辑解

6. 性能优化

在大规模数据导入时,性能是需要特别关注的问题。以下是一些性能优化的建议:

  • 批量导入:避免逐条插入数据库,建议使用批量插入的方式,将导入的数据分批插入数据库。
  • 异步处理:对于耗时较长的导入操作,可以考虑使用异步处理,避免阻塞主线程。
  • 分页导入:当文件数据量过大时,可以分页读取文件中的数据,分批导入,减少内存压力。

7. 扩展性设计

为了确保数据导入系统能够满足未来不断变化的需求,我们需要设计具有高度扩展性和灵活性的导入方案。以下是几个关键扩展性设计方向:

7.1 支持多种文件格式

电商系统中的数据导入不仅仅局限于 Excel 文件。在实际应用中,用户可能会要求系统支持其他格式的数据文件,如 CSV、JSON 等。为了实现这种扩展性,我们可以设计通用的数据导入接口,不同的文件格式实现相应的处理逻辑。

设计思路:
  1. 定义统一的导入接口:我们可以为不同的文件格式设计统一的导入接口 DataImportHandler,不同格式的数据导入通过实现该接口来处理相应逻辑。
  2. 策略模式应用:通过策略模式选择具体的导入处理器,实现对不同文件格式的导入逻辑隔离,方便后期扩展。
public interface DataImportHandler {
    void handleFile(MultipartFile file) throws IOException;
}

实现示例:

对于 Excel 文件,我们可以实现如下导入处理器:

public class ExcelImportHandler implements DataImportHandler {

    @Override
    public void handleFile(MultipartFile file) throws IOException {
        // 使用 EasyExcel 处理 Excel 文件
        ExcelReader excelReader = EasyExcel.read(file.getInputStream()).build();
        // ... 具体 Excel 导入处理逻辑
    }
}

对于 CSV 文件,我们可以使用 OpenCSV 处理:

public class CsvImportHandler implements DataImportHandler {

    @Override
    public void handleFile(MultipartFile file) throws IOException {
        // 使用 OpenCSV 解析 CSV 文件
        try (CSVReader reader = new CSVReader(new InputStreamReader(file.getInputStream()))) {
            String[] nextLine;
            while ((nextLine = reader.readNext()) != null) {
                // 处理 CSV 数据
            }
        }
    }
}

策略选择:

我们可以通过工厂模式或依赖注入来动态选择导入处理器:

@Service
public class ImportHandlerFactory {

    @Autowired
    private ExcelImportHandler excelImportHandler;

    @Autowired
    private CsvImportHandler csvImportHandler;

    public DataImportHandler getImportHandler(String fileType) {
        if ("excel".equalsIgnoreCase(fileType)) {
            return excelImportHandler;
        } else if ("csv".equalsIgnoreCase(fileType)) {
            return csvImportHandler;
        }
        throw new UnsupportedOperationException("不支持的文件类型");
    }
}

在接口调用时,可以根据文件扩展名或用户选择,动态调用对应的导入处理器:

@PostMapping("/import")
public ResponseEntity<String> importData(@RequestParam("file") MultipartFile file) {
    String fileType = determineFileType(file);
    DataImportHandler handler = importHandlerFactory.getImportHandler(fileType);
    handler.handleFile(file);
    return ResponseEntity.ok("导入成功");
}

7.2 灵活配置映射关系

在导入不同类型的文件时,文件中的列名和数据库表中的字段名往往不一致。为了解决这个问题,我们可以实现动态的列映射机制。通过配置文件或数据库定义列与字段的对应关系,在导入时自动进行映射。这种设计可以大大增强系统的灵活性,减少硬编码。

设计思路:
  1. 列与字段的映射配置:我们可以将列名与数据库字段名的映射关系存储在数据库或配置文件中,并在导入时动态加载。
  2. 动态映射工具:实现一个工具类,在导入过程中根据配置进行字段映射。
数据库表设计:

我们可以为列映射关系创建一张配置表,存储 Excel 列名与数据库字段名的对应关系:

sql复制代码CREATE TABLE `column_mapping` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `business_type` VARCHAR(100) NOT NULL COMMENT '业务类型,例如商品、订单',
    `excel_column_name` VARCHAR(100) NOT NULL COMMENT 'Excel 列名',
    `db_column_name` VARCHAR(100) NOT NULL COMMENT '数据库字段名',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='列与字段映射表';

实现动态映射工具:
@Service
public class ColumnMappingService {

    @Autowired
    private ColumnMappingRepository columnMappingRepository;

    public Map<String, String> getColumnMapping(String businessType) {
        List<ColumnMapping> mappings = columnMappingRepository.findByBusinessType(businessType);
        return mappings.stream()
                .collect(Collectors.toMap(ColumnMapping::getExcelColumnName, ColumnMapping::getDbColumnName));
    }
}

在导入时,使用该服务获取列与字段的映射关系,并在数据导入时根据映射关系存储数据:

@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
    // 获取动态映射关系
    Map<String, String> columnMapping = columnMappingService.getColumnMapping("product");
    
    ProductImportDTO product = new ProductImportDTO();
    for (Map.Entry<Integer, String> entry : data.entrySet()) {
        String excelColumn = excelHeaders.get(entry.getKey());
        String dbColumn = columnMapping.get(excelColumn);
        product.getDynamicFields().put(dbColumn, entry.getValue());
    }
    productList.add(product);
}

7.3 插件式扩展导入逻辑

在电商系统中,数据导入不仅限于商品、订单等基础数据,不同业务场景可能涉及不同的导入逻辑。例如,商品导入时可能需要进行分类检查,订单导入时可能需要进行支付信息校验。为了满足这种业务需求,我们可以设计插件式的导入逻辑,通过合理的接口设计支持各类导入逻辑扩展。

设计思路:
  1. 定义导入逻辑接口:设计一个通用的导入逻辑接口,不同业务场景的具体逻辑通过实现该接口进行扩展。
  2. 引入责任链模式:可以采用责任链模式,在导入过程中将多个逻辑按顺序执行,确保业务规则的灵活应用。
导入逻辑接口:
public interface ImportPlugin {
    void process(ProductImportDTO product);
}

商品导入插件实现:
@Component
public class ProductCategoryValidationPlugin implements ImportPlugin {

    @Override
    public void process(ProductImportDTO product) {
        String category = (String) product.getDynamicFields().get("category");
        if (!isValidCategory(category)) {
            throw new RuntimeException("无效的商品分类");
        }
    }

    private boolean isValidCategory(String category) {
        // 校验分类逻辑
        return true;
    }
}

责任链模式处理器:
@Service
public class ImportProcessor {

    @Autowired
    private List<ImportPlugin> importPlugins;

    public void processProductImport(ProductImportDTO product) {
        for (ImportPlugin plugin : importPlugins) {
            plugin.process(product);
        }
    }
}

在数据导入时,我们可以通过 ImportProcessor 动态执行一系列业务逻辑插件:

@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
    ProductImportDTO product = new ProductImportDTO();
    // 动态列映射...
    importProcessor.processProductImport(product);
    productList.add(product);
}

通过插件式设计,我们可以轻松为不同的业务场景添加导入逻辑,而无需修改核心代码,大大增强了系统的可扩展性。

7.4 指定模板导入示例

为了满足灵活配置映射关系和插件式扩展导入逻辑的需求,我们可以设计一个模板化的导入机制,使不同业务模块(如订单、商品、用户等)可以灵活配置导入文件中的列与数据库字段的映射关系。同时,通过插件化的设计,系统可以轻松地扩展不同的业务导入逻辑,而无需对主框架进行大幅改动。

7.4.1 模板配置设计

为了实现灵活的列映射配置,可以将每种业务对应的导入模板列和数据库字段映射关系存储在数据库或配置文件中。以下是一个简单的表设计,用于保存列与字段的映射关系:

CREATE TABLE import_template_config (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    business_type VARCHAR(50) NOT NULL, -- 业务类型,例如 "订单", "商品"
    column_name VARCHAR(100) NOT NULL,  -- 文件中的列名
    field_name VARCHAR(100) NOT NULL,   -- 数据库中的字段名
    is_required BOOLEAN NOT NULL DEFAULT FALSE, -- 是否为必填项
    validation_rule VARCHAR(100),       -- 校验规则,例如 "notNull", "length:100"
    UNIQUE(business_type, column_name)
);

配置数据示例:

假设有一个订单导入的模板,包含以下字段:

列名字段名必填校验规则
订单编号order_idnotNull
订单状态statusenum:待支付,已支付,已发货
下单日期order_datedate
订单金额amountmoney

对应的配置可以存储在数据库中:

INSERT INTO import_template_config (business_type, column_name, field_name, is_required, validation_rule)
VALUES ('订单', '订单编号', 'order_id', TRUE, 'notNull'),
       ('订单', '订单状态', 'status', TRUE, 'enum:待支付,已支付,已发货'),
       ('订单', '下单日期', 'order_date', FALSE, 'date:yyyy-MM-dd'),
       ('订单', '订单金额', 'amount', TRUE, 'money');

7.4.2 动态列映射导入逻辑

为了实现根据模板动态导入数据,我们可以设计一个通用的导入服务,通过业务类型和模板配置进行列与字段的映射。导入过程中根据配置动态解析列,并进行相应的数据处理和校验。

导入服务逻辑设计
public class ImportService {

    @Autowired
    private ImportTemplateConfigRepository templateConfigRepository;

    @Autowired
    private ValidationFactory validationFactory;

    public void importData(String businessType, List<Map<String, String>> importedRows) {
        // 获取该业务类型对应的导入模板配置
        List<ImportTemplateConfig> templateConfigs = templateConfigRepository.findByBusinessType(businessType);

        // 动态生成校验规则
        Map<String, ValidationChain<String>> validationChains = createValidationChains(templateConfigs);

        // 逐行处理导入的数据
        for (Map<String, String> row : importedRows) {
            Map<String, Object> entityData = new HashMap<>();
            List<String> errors = new ArrayList<>();

            for (ImportTemplateConfig config : templateConfigs) {
                String columnName = config.getColumnName();
                String fieldName = config.getFieldName();
                String value = row.get(columnName);

                // 进行校验
                if (validationChains.containsKey(fieldName)) {
                    List<String> validationErrors = validationChains.get(fieldName).validate(value);
                    errors.addAll(validationErrors);
                }

                // 如果校验通过,存入实体数据
                entityData.put(fieldName, value);
            }

            // 如果存在校验错误,抛出异常或记录错误
            if (!errors.isEmpty()) {
                throw new ValidationException(errors);
            }

            // 保存实体数据到数据库
            saveEntityData(businessType, entityData);
        }
    }

    private Map<String, ValidationChain<String>> createValidationChains(List<ImportTemplateConfig> templateConfigs) {
        Map<String, ValidationChain<String>> validationChains = new HashMap<>();
        for (ImportTemplateConfig config : templateConfigs) {
            ValidationChain<String> chain = validationFactory.createValidationChain(config);
            validationChains.put(config.getFieldName(), chain);
        }
        return validationChains;
    }

    private void saveEntityData(String businessType, Map<String, Object> entityData) {
        // 根据业务类型保存数据,例如调用订单服务、商品服务等
        // 此处为示例,可通过反射或策略模式动态调用具体的业务模块
        if ("订单".equals(businessType)) {
            Order order = new Order();
            order.setOrderId((String) entityData.get("order_id"));
            order.setStatus((String) entityData.get("status"));
            order.setOrderDate(LocalDate.parse((String) entityData.get("order_date")));
            order.setAmount(new BigDecimal((String) entityData.get("amount")));
            // 保存订单
            orderRepository.save(order);
        }
    }
}

动态列导入示例

假设导入的 Excel 文件数据如下:

订单编号订单状态下单日期订单金额
123456已支付2023-09-01100.50
789012待支付2023-09-02200.75

通过上面的导入服务和模板配置,系统会根据列名与字段名进行自动映射,并根据配置的校验规则对数据进行校验。如果所有校验通过,数据会被保存到数据库中。

7.4.3 插件式扩展导入逻辑

通过插件化的设计,不同业务模块可以轻松地扩展各自的导入逻辑。例如,如果我们需要为商品、用户等其他模块添加导入功能,只需为对应的业务模块配置新的模板,并在 saveEntityData 方法中增加对新业务的处理逻辑。

我们可以使用策略模式或反射机制来动态加载不同业务模块的导入处理逻辑。以下是策略模式的实现示例:

public interface ImportStrategy {
    void save(Map<String, Object> entityData);
}

public class OrderImportStrategy implements ImportStrategy {

    @Autowired
    private OrderRepository orderRepository;

    @Override
    public void save(Map<String, Object> entityData) {
        Order order = new Order();
        order.setOrderId((String) entityData.get("order_id"));
        order.setStatus((String) entityData.get("status"));
        order.setOrderDate(LocalDate.parse((String) entityData.get("order_date")));
        order.setAmount(new BigDecimal((String) entityData.get("amount")));
        orderRepository.save(order);
    }
}

public class ImportStrategyFactory {

    private final Map<String, ImportStrategy> strategyMap = new HashMap<>();

    public ImportStrategyFactory() {
        strategyMap.put("订单", new OrderImportStrategy());
        // 可扩展更多业务策略
    }

    public ImportStrategy getStrategy(String businessType) {
        return strategyMap.get(businessType);
    }
}

ImportService 中使用策略工厂:

@Autowired
private ImportStrategyFactory strategyFactory;

private void saveEntityData(String businessType, Map<String, Object> entityData) {
    ImportStrategy strategy = strategyFactory.getStrategy(businessType);
    if (strategy != null) {
        strategy.save(entityData);
    } else {
        throw new IllegalArgumentException("未知的业务类型:" + businessType);
    }
}

通过这种设计,系统可以轻松地扩展到其他业务模块,而无需修改主导入逻辑,只需新增具体的 ImportStrategy 实现类。


7.5 生成导入模板示例

在某些场景中,用户可能需要一个标准化的 Excel 模板来进行数据导入操作。为了方便用户操作,我们可以为每个业务模块生成对应的导入模板,模板包含每个字段的名称、说明(例如字段的必填性、数据格式要求等),甚至可以提供一些示例数据。

7.5.1 生成模板逻辑

使用 EasyExcel 可以方便地生成一个 Excel 文件作为导入模板。

public class TemplateService {

    @Autowired
    private ImportTemplateConfigRepository templateConfigRepository;

    /**
     * 根据业务类型生成导入模板
     * @param businessType 业务类型
     * @param outputStream 输出流
     */
    public void generateTemplate(String businessType, OutputStream outputStream) {
        List<ImportTemplateConfig> templateConfigs = templateConfigRepository.findByBusinessType(businessType);
        
        // 创建头信息
        List<List<String>> head = createHead(templateConfigs);
        
        // 创建示例数据
        List<List<Object>> data = createSampleData(templateConfigs);

        // 使用 EasyExcel 生成模板
        EasyExcel.write(outputStream)
                .head(head)
                .sheet(businessType + "导入模板")
                .doWrite(data);
    }

    private List<List<String>> createHead(List<ImportTemplateConfig> templateConfigs) {
        List<List<String>> head = new ArrayList<>();
        for (ImportTemplateConfig config : templateConfigs) {
            List<String> headColumn = new ArrayList<>();
            headColumn.add(config.getColumnName());
            head.add(headColumn);
        }
        return head;
    }

    private List<List<Object>> createSampleData(List<ImportTemplateConfig> templateConfigs) {
        List<Object> row = new ArrayList<>();
        for (ImportTemplateConfig config : templateConfigs) {
            // 为每个字段生成示例数据
            String sampleValue = getSampleValue(config);
            row.add(sampleValue);
        }
        List<List<Object>> data = new ArrayList<>();
        data.add(row);
        return data;
    }

    private String getSampleValue(ImportTemplateConfig config) {
        // 根据字段类型或校验规则生成示例数据
        switch (config.getValidationRule()) {
            case "notNull":
                return "必填项";
            case "money":
                return "100.00";
            case "date:yyyy-MM-dd":
                return "2023-01-01";
            default:
                return "";
        }
    }
}

7.5.2 使用示例

在控制器中调用生成模板的服务:

@GetMapping("/template/{businessType}")
public void downloadTemplate(@PathVariable String businessType, HttpServletResponse response) throws IOException {
    response.setContentType("application/vnd.ms-excel");
    response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(businessType + "-导入模板.xlsx", "UTF-8"));

    try (OutputStream outputStream = response.getOutputStream()) {
        templateService.generateTemplate(businessType, outputStream);
    }
}

用户可以通过访问该接口,下载指定业务类型的 Excel 模板文件,模板中包含了每个字段的列名、示例数据,并且用户可以直接根据模板填写数据并上传进行导入。

8.前端示例

可以使用简单的 HTML 文件上传表单:

<form action="/api/products/import" method="post" enctype="multipart/form-data">
    <input type="file" name="file" />
    <button type="submit">上传并导入商品信息</button>
</form>


总结

在这篇文章中,我们详细讲解了如何在 Spring Boot 中实现具有扩展性和高效的动态列导入功能。通过使用 EasyExcel 解析 Excel 文件,结合 MyBatis 实现数据库访问,我们可以灵活处理 Excel 文件中的动态列,并支持大规模数据导入。为了保证系统的高效性和稳定性,我们还探讨了批量导入、异步处理等性能优化方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2178087.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

国产纯电SUV都在秀,只有Model Y在挨揍

文/王俣祺 导语&#xff1a;如果想知道纯电SUV应该怎么选&#xff0c;一定有人告诉你“无脑选Model Y”&#xff0c;虽说特斯拉确实粉丝多&#xff0c;但这也恰恰证明Model Y一度成为了纯电SUV的标杆。有标杆自然就有挑战者&#xff0c;随着阿维塔07、智己LS6以及乐道L60先后上…

云南省职业院校技能大赛赛项规程(软件测试)

赛项名称&#xff1a;软件测试 英文名称&#xff1a;Software Testing 赛项组别&#xff1a;高等职业教育 赛项编号&#xff1a;GZ034 目录 一、 赛项信息 二、竞赛目标 三、竞赛内容 1、本赛项考查的技术技能和涵盖的职业典型工作任务 2、专业核心能力与职业综合能力…

商标名称注册查询,到底是查询什么!

在商标注册前是需要商标名称注册查询&#xff0c;那这个到底是查询什么&#xff0c;普推知产商标老杨发现&#xff0c;近日国家知产局发布《商标代理委托合同示范文本》征求意见稿&#xff0c;虽然是参考使用不具有强制性&#xff0c;里面对商标名称注册查询描述是申请前商标检…

完成UI界面的绘制

绘制UI 接上文&#xff0c;在Order90Canvas下创建Image子物体&#xff0c;图片资源ui_fish_lv1&#xff0c;设置锚点&#xff08;CountdownPanelImg同理&#xff09;&#xff0c;命名为LvPanelImg,创建Text子物体&#xff0c;边框宽高各50&#xff0c; &#xff0c;重名为LvT…

阻焊层解析:PCB的“保护伞”是什么?

在电子制造行业中&#xff0c;尤其是PCBA贴片加工领域&#xff0c;阻焊层是一个重要的概念。以下是对阻焊层的详细讨论分析&#xff0c;包括其定义、作用以及类型。 阻焊层的定义 阻焊层&#xff0c;顾名思义&#xff0c;是一种用于阻止焊接的材料层。在PCB&#xff08;印刷电…

11.C++程序中的常用函数

我们将程序中反复执行的代码封装到一个代码块中&#xff0c;这个代码块就被称为函数&#xff0c;它类似于数学中的函数&#xff0c;在C程序中&#xff0c;有许多由编译器定义好的函数&#xff0c;供大家使用。下面就简单说一下&#xff0c;C中常用的函数。 1.sizeof sizeof函…

Perceptually Optimized Deep High-Dynamic-RangeImage Tone Mapping

Abstract 我们描述了一种深度高动态范围&#xff08;HDR&#xff09;图像色调映射算子&#xff0c;该算子计算效率高且感知优化。 我们首先将 HDR 图像分解为归一化拉普拉斯金字塔&#xff0c;并使用两个深度神经网络 (DNN) 根据归一化表示估计所需色调映射图像的拉普拉斯金字…

Mybatis缓存机制(图文并茂!)

目录 一级缓存 需求我们在一个测试中通过ID两次查询Monster表中的信息。 二级缓存 案例分许(和上述一样的需求) EhCache第三方缓存 在了解缓存机制之前&#xff0c;我们要先了解什么是缓存&#xff1a; ‌缓存是一种高速存储器&#xff0c;用于暂时存储访问频繁的数据&…

利用大模型改进知识图谱补全的研究

人工智能咨询培训老师叶梓 转载标明出处 尽管现有的基于描述的KGC方法已经利用预训练语言模型来学习实体和关系的文本表示&#xff0c;并取得了一定的成果&#xff0c;但这些方法的性能仍然受限于文本数据的质量和结构的不完整性。 为了克服这些限制&#xff0c;中国科学技术…

PG高可靠模拟

模拟延迟 主库故障&#xff0c;备库尝试切换为主库

9.29 LeetCode 3304、3300、3301

思路&#xff1a; ⭐进行无限次操作&#xff0c;但是 k 的取值小于 500 &#xff0c;所以当 word 的长度大于 500 时就可以停止操作进行取值了 如果字符为 ‘z’ &#xff0c;单独处理使其变为 ‘a’ 得到得到操作后的新字符串&#xff0c;和原字符串拼接 class Solution { …

MySQL - 运维篇

一、日志 1. 错误日志 2. 二进制日志 3. 查询日志 记录了所有的增删改查语句以及DDL语句 4. 慢查询日志 二、主从复制 1. 概述 2. 原理 3. 搭建 三、分库分表 1. 介绍 2. Mycat概述 3. Mycat入门 4. Mycat配置 5. Mycat分片 6. Mycat管理及监控 四、读写分离 1. 介绍 2. 一…

【ADC】使用运算放大器驱动 SAR 型 ADC 时的线性输入范围

概述 本文学习于TI 高精度实验室课程&#xff0c;总结使用运算放大器驱动 SAR ADC 时的注意事项。具体包括&#xff1a;了解运算放大器共模范围和输出摆幅限制如何影响 SAR ADC 性能&#xff0c;研究运算放大器设计技术以避免共模和输出摆幅限制&#xff0c;讨论轨到轨放大器与…

PCB敷铜敷不了相同网络的线怎么办?

图片上的情况就是今天需要讲的内容&#xff0c;可以看出出来的线头是GND,敷的铜也是GND但是相同网络就是不能连在一起。 解释&#xff1a; 这是因为我们敷铜的时候属性选的是连接相同的net,如图所示&#xff1a; 解决办法&#xff1a; 只需要设置改为相同的Object就可以了&…

[Linux#60][HTTPS] 加密 | 数字指纹 | 详解HTTPS工作方案 | CA认证

目录 一.预备知识 1. 什么是HTTPS&#xff1f; 2. HTTP与HTTPS的区别 3. 什么是加密&#xff1f; 4. 常见的加密方式 4.1 对称加密 4.2 非对称加密 4.3 数据摘要与数据指纹 4.4 数字签名 二. HTTPS的工作方案 1 方案一&#xff1a;对称加密 2 方案二&#xff1a;非…

图像增强论文精读笔记-Deep Retinex Decomposition for Low-Light Enhancement(Retinex-Net)

1. 论文基本信息 论文标题&#xff1a;Deep Retinex Decomposition for Low-Light Enhancement 作者&#xff1a;Chen Wei等 发表时间和期刊&#xff1a;2018&#xff1b;BMVC 论文链接&#xff1a;https://arxiv.org/abs/1808.04560 2. 研究背景和动机 低光照条件下拍摄的…

LLM工程师启航:生成式AI简明教程

编者按&#xff1a; 大模型发展了近两年&#xff0c;Baihai IDP公众号也分享了近百篇LLM各环节的技术洞察&#xff0c;有前沿探讨、有落地实践、有应用经验。但回头来看&#xff0c;我们似乎从来没有认真、从0开始探讨过LLM的基本原理。 最近&#xff0c;一些企业客户和伙伴来询…

【IP限流】⭐️通过切面实现无校验保护接口的防刷逻辑

目录 &#x1f378;前言 &#x1f37b;一、实现方法 &#x1f37a;二、伪代码实现 &#x1f379;三、章末 &#x1f378;前言 小伙伴们大家好&#xff0c;上次写了一篇文章记录了最近自己装台式电脑中遇到的问题&#xff0c;以及整体的安装步骤和本地的配置选择&#xff0c…

【JavaEE初阶】网络原理

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 ⽹络互连 IP地址 端口号 协议 协议分层 优势 TCP/IP 五层网络模型 数据在网络通信中的整体流程 封装和分用 封装 分用 ⽹络互连 随着时代的发展&#xff0c;越来越需…

【PyTorch】生成对抗网络

生成对抗网络是什么 Generative Adversarial Nets&#xff0c;简称GAN GAN&#xff1a;生成对抗网络 —— 一种可以生成特定分布数据的模型 《Recent Progress on Generative Adversarial Networks (GANs): A Survey》 《How Generative Adversarial Networks and Its Varian…