Chat2Excel 文件服务模块上传文件功能开发

一、创建子模块 file-service

二、接入依赖

复制代码
<dependencies>
        <!--1. 通用服务依赖-->
        <dependency>
            <groupId>com.xxxxx</groupId>
            <artifactId>common-service</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!--2. web依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--3. lombok依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--4. validation依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!--5. 数据库驱动依赖-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <!--6. mybatis-plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>

        <!--7 poi依赖-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
        </dependency>

        <!--8. 阿里云oss依赖-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
        </dependency>

        <!--9. Nacos配置中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!--10. Nacos服务注册-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--11. bootstrap依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <!--12 easyExcel依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.3.4</version>
        </dependency>

    </dependencies>

三、表结构设计

三张表分别管三件事:

files:记「这是谁的文件」。谁上传的、原文件名、OSS 上的位置、上传状态。列表、下载、鉴权、删文件都靠它。

file_table_mappings:记「这个文件生成了哪几张 MySQL 表」。一个 Excel 可能有多个 Sheet,每个 Sheet 会建成一张独立表。预览切 Sheet、复原、删除、AI 选表,都先通过 fileId 查这张映射。

field_mappings:记「Excel 表头怎么对应数据库列」。例如「姓名」→ col_xingming。预览要显示中文表头,AI 要把用户说的列名翻成 SQL 字段,都靠这张表。

一句话:files 管文件本身,file_table_mappings 管文件到表,field_mappings 管表头到字段。

四、上传文件功能的实现

接口定义

定义 Controller 接口

java 复制代码
/**
 * 文件服务控制器
 */
@RestController
@RequestMapping("/files")
public class FilesController {

    /**
     * 单个文件上传接口
     */
    @PostMapping("/upload/single")
    @LogOperation("文件上传")
    public Result<FileUploadResponse> uploadFile(
            @RequestParam("file")MultipartFile file,
            @RequestHeader("Authorization")String authorization,
            @Valid FileUploadRequest request
            ) {
       
    }
}

补充 dto 的 FileUploadResponse 类 与 FileUploadRequest 类

java 复制代码
/**
 * 文件上传的响应结果
 */
@Data
@Builder
public class FileUploadResponse {

    /**
     * 文件ID
     */
    private Long fileId;

    /**
     * 原始文件名
     */
    private String fileName;

    /**
     * 文件大小(字节)
     */
    private Long fileSize;

    /**
     * 文件访问路径
     */
    private String fileUrl;

    /**
     * oss中的key
     */
    private String ossKey;

    /**
     * 上传的状态
     */
    private Integer uploadStatus;
}
java 复制代码
/**
 * 文件上传请求参数
 */
@Data
@Builder
public class FileUploadRequest {

    /**
     * 文件目录
     */
    private String category;
}

定义操作文件的接口

java 复制代码
/**
 * 操作文件的接口
 */
public interface FilesService {


    FileUploadResponse upload(MultipartFile file, FileUploadRequest request, Long userId);

}

定义操作文件的实现类

java 复制代码
/**
 * 操作文件接口的实现类
 */
@Service
@Slf4j
public class FileServiceImpl implements FilesService {

    @Override
    public FileUploadResponse upload(MultipartFile file, FileUploadRequest request, Long userId) {
        // 1. 校验文件(文件类型、文件大小)
     

        // 2. 上传文件到阿里云OSS
       

        // 3. 创建文件记录
       

        // 4. 把excel转化成mysql的表
       
        // 5. 构建响应
        
        return null;
    }

   
}

实现文件校验工具类(写在 common-service 模块下)

java 复制代码
/**
 * 文件校验的工具类
 */
@Component
public class FileValidationUtil {

    // 文件扩展名支持列表
    private static  final List<String> SUPPORT_FILE_EXTENSIONS = Arrays.asList(
            ".xls", ".xlsx"
    );

    // 文件类型的支持列表
    private static  final List<String> SUPPORT_FILE_TYPE = Arrays.asList(
            "application/vnd.ms-excel", // .xls
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" // .xlsx
    );


    public static String getContentType(String fileName) {
        String extension = fileName.substring(fileName.lastIndexOf("."));
        switch (extension) {
            case ".xls":
                return "application/vnd.ms-excel";
            case ".xlsx":
                return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            default:
                return "";
        }
    }

    // 获取文件扩展名
    private static String getFileExtension(String fileName) {
        if (fileName == null || !fileName.contains(".")) {
            return "";
        }
        return fileName.substring(fileName.lastIndexOf("."));
    }

    // 校验文件格式是否支持
    public static boolean validateFileFormat(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            return false;
        }
        // 检查文件扩展名
        String extension = getFileExtension(file.getOriginalFilename());
        if (!SUPPORT_FILE_EXTENSIONS.contains(extension)) {
            return false;
        }

        // 检查文件类型
        String contentType = file.getContentType();
        if (!SUPPORT_FILE_TYPE.contains(contentType)) {
            return false;
        }
        return true;
    }

    /**
     * 判断文件大小
     * @param file 文件
     * @return 是否超过50MB
     */
    public static boolean ifOutOfLarge(MultipartFile file) {
        return file.getSize() <= 50 * 1024 * 1024;
    }
}

完善 FileServiceImpl 校验文件功能

java 复制代码
        // 1. 校验文件(文件类型、文件大小)
        if (!FileValidationUtil.validateFileFormat(file)) {
            throw new IllegalArgumentException("只能处理excel文件");
        }

        if (!FileValidationUtil.ifOutOfLarge(file)) {
            throw new IllegalArgumentException("文件大小不能超过50MB");
        }

接入 OSS

注册、登录阿里云

https://www.aliyun.com/

创建 AccessKey

开通 oss 服务

开通阿里云OSS免费试用

创建 bucket

创建 OssService 与 OssServiceImpl

java 复制代码
/**
 * 阿里云oss服务接口
 */
public interface OssService {

    /**
     * 上传文件到指定路径
     * @param file 文件本身
     * @param filePath 文件路径
     * @return 可以访问的URL
     */
    String uploadFile(MultipartFile file, String filePath);

  
}
java 复制代码
/**
 * oss服务的实现类
 */
@Service
@Slf4j
@ConditionalOnBean(OssConfig.class)
public class OssServiceImpl implements OssService {


    @Override
    public String uploadFile(MultipartFile file, String filePath) {
        // 1.获取完整的文件流
        try {
            InputStream inputStream = file.getInputStream();
            String objectKey = createKey(filePath, file.getOriginalFilename());
            PutObjectRequest putObjectRequest = new PutObjectRequest(
                   
            );

            // 2. 调用oss的客户端直接上传代码

            // 3. 返回上传后的url地址
           
            return "";
    }
}

PutObjectRequest 需要 有 BuckName 配置相关的东西,所以这里创建一个 OssConfig 来记录阿里云配置

java 复制代码
/**
 * 阿里云配置
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "aliyun.oss")
@ConditionalOnProperty(prefix = "aliyun.oss", name = "access-key-id")
public class OssConfig {

    /**
     * 访问断点
     */
    private String endPoint;

    /**
     * 秘钥ID
     */
    private String accessKeyId;

    /**
     * 秘钥
     */
    private String accessKeySecret;

    /**
     * 存储空间
     */
    private String bucketName;

    /**
     * 最大文件限制(字节)
     */
    private Long maxFileSize;

    @Bean
    public OSS ossClient() {
        OSS oSSClient = new OSSClientBuilder().build(
                this.endPoint,
                this.accessKeyId,
                this.accessKeySecret
        );
        return oSSClient;
    }
}

@ConfigurationProperties(prefix = "aliyun.oss"):将配置文件中以 aliyun.oss 为前缀的属性,自动绑定到当前类的字段上。如 Spring 会自动把 aliyun.oss.access-key-id 的值赋给 accessKeyId 字段,aliyun.oss.endpoint 赋给 endpoint,以此类推。注意 :字段名要符合驼峰命名 规则(access-key-id → accessKeyId),Spring 会自动转换。

@ConditionalOnProperty(prefix = "aliyun.oss", name = "access-key-id"):条件注解,只有当配置文件中存在 aliyun.oss.access-key-id 这个属性时,这个 Bean 才会被创建。告诉 Spring ------"如果配置文件里有 OSS 的 AccessKey,才加载这个配置类;如果没有,就不加载。"

完善OssServiceImpl

java 复制代码
/**
 * oss服务的实现类
 */
@Service
@Slf4j
@ConditionalOnBean(OssConfig.class)
public class OssServiceImpl implements OssService {

    @Autowired
    private OssConfig ossConfig;

    @Autowired
    private OSS ossClient;

    @Override
    public String uploadFile(MultipartFile file, String filePath) {
        // 1.获取完整的文件流
        try {
            InputStream inputStream = file.getInputStream();
            String objectKey = createKey(filePath, file.getOriginalFilename());
            PutObjectRequest putObjectRequest = new PutObjectRequest(
                   ossConfig.getBucketName(), 
                   objectKey,
                   inputStream
            );

            // 2. 调用oss的客户端直接上传代码
            PutObjectResult putObjectResult = ossClient.putObject(putObjectRequest);

            // 3. 返回上传后的url地址
            return getUrl(objectKey);
        } catch (IOException e) {
            log.error("上传oss失败{}", file.getOriginalFilename());
            throw new RuntimeException(e);
        }
    }


    private String createKey(String filePath, String fileName) {
        if (filePath == null || filePath.trim().isEmpty()) {
            return "uploads/" +fileName;
        } else {
            return filePath + fileName;
        }
    }

    private String getUrl(String objectKey) {
        return String.format("https://%s.%s/%s",
                ossConfig.getBucketName(),
                ossConfig.getEndPoint(),
                objectKey
                );
    }
    
}

PutObjectRequest 构造方法

java 复制代码
PutObjectRequest putObjectRequest = new PutObjectRequest(
    ossConfig.getBucketName(),  // 参数1:bucket名称
    objectKey,                   // 参数2:对象键(文件在OSS中的完整路径)
    inputStream                  // 参数3:文件输入流
);

完善 FileServiceImpl 步骤二上传 文件到阿里云OSS

java 复制代码
// 2. 上传文件到阿里云OSS
String fileUrl = ossService.uploadFile(file, request.getCategory());

Excel 转换成 mysql 的表

建表

创建 ExcelToTableService 与 ExcelToTableServiceImpl

java 复制代码
/**
 * 用于做excel到mysql表转换的服务接口
 */
public interface ExcelToTableService {

    /**
     * 把已有的excel文件转换成mysql表
     */
    List<String> convertExcelToTable(MultipartFile file, Long fileId);

}
java 复制代码
/**
 * excel转换mysql表的实现类
 */
@Slf4j
@Service
public class ExcelToTableServiceImpl implements ExcelToTableService {
     @Override
    public List<String> convertExcelToTable(MultipartFile file, Long fileId) {
        // 1. 先获取excel的数据
        

        // 2. 获取sheet的名称
       


        // 3. 根据文件信息做实际table的转换

        return "";
    }
}

获取 sheet 个数方法,判断是否有数据(在ExcelToTableServiceImpl 中实现)

java 复制代码
    private int getSheetCount(MultipartFile file) {
        byte[] fileBytes = getFileBytes(file);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(fileBytes);
        ExcelReader excelReader = EasyExcel.read(byteArrayInputStream).build();
        List<ReadSheet> sheets = excelReader.excelExecutor().sheetList();
        return sheets.size();
    }

    private byte[] getFileBytes(MultipartFile file) {
        try {
            return file.getBytes();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

完善 convertExcelToTable 方法

java 复制代码
        // 1. 先获取excel的数据
        int sheetCount = getSheetCount(file);

        if (sheetCount == 0) {
            throw new IllegalArgumentException("excel是个空文件");
        }

获取sheet名称的实现(在ExcelToTableServiceImpl 中实现)

由于 一个文件有多个 sheet, 对应多张表,sheet名是生成表名的一部分

java 复制代码
    public List<String> getSheetNames(MultipartFile file) {
        byte[] fileBytes = getFileBytes(file);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(fileBytes);
        ExcelReader excelReader = EasyExcel.read(byteArrayInputStream).build();
        List<ReadSheet> sheets = excelReader.excelExecutor().sheetList();
        List<String> names = new ArrayList<>();
        for (ReadSheet readSheet :sheets) {
            names.add(readSheet.getSheetName());
        }
        return names;
    }

完善 convertExcelTTable 方法

java 复制代码
// 2. 获取sheet的名称
List<String> sheetNames = getSheetNames(file);

实现 convertExcelToTable 方法

java 复制代码
    private List<String> convertExcelToTable(MultipartFile file, Long fileId, List<String> sheetNames) {

        // 1. 先生成表名
        

        // 2. 创建表
        
        // 3. 获取file的数据

        // 4. 把file的数据转换成插入的sql

        // 5. 执行sql
           
    }

写分布式锁(写在 common-service 里)

在生成随机表名的时候,需要考虑并发性。需要使用分布式锁保证表名的全局唯一

实现 DistributeLockService 与 DistributeLockServiceImpl

java 复制代码
/**
 * 分布式锁服务
 */
public interface DistributeLockService {

    boolean tryLock(String lockKey, String lockValue, long expireTime);

    boolean releaseLock(String lockKey, String lockValue);

}

expireTime 为了防止程序挂了无法释放锁

java 复制代码
/**
 * 分布式锁的实现类
 */
@Service
@Slf4j
public class DistributeLockServiceImpl implements DistributeLockService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String LOCK_PREFIX = "lock_prefix:";


    private final DefaultRedisScript<Long> redisScript;

    private static final String SCRIPT_TEXT = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "    return redis.call('del', KEYS[1]) " +
        "else " +
        "    return 0 " +
        "end";


    public DistributeLockServiceImpl() {
        redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(SCRIPT_TEXT);
        redisScript.setResultType(Long.class);
    }


    @Override
    public boolean tryLock(String lockKey, String lockValue, long expireTime) {
        // 1. 补全完整的key
        String fullLockKey = LOCK_PREFIX + lockKey;

        // 2. 把value写入redis
        Boolean result = redisTemplate.opsForValue().setIfAbsent(
                fullLockKey,
                lockValue,
                expireTime,
                TimeUnit.SECONDS
        );

        if (result) {
            log.info("成功获取分布式锁{}:{}", fullLockKey, lockValue);
        } else {
            log.error("获取分布式锁{}失败, 被别的请求站上了", lockKey);
        }

        return result;
    }

    @Override
    public boolean releaseLock(String lockKey, String lockValue) {
        // 1. 补全完整的key
        String fullLockKey = LOCK_PREFIX + lockKey;

        Long result = redisTemplate.execute(
                redisScript,
                Collections.singletonList(fullLockKey),
                lockValue
        );

        boolean released = result != null && result > 0;
        if (released) {
            log.info("成功释放分布式锁{}", lockKey);
        } else {
            log.error("释放分布式锁失败{}", lockKey);
        }

        return released;
    }
}

fullLockKey 标识「锁哪把资源」,lockValue 标识「谁持有这把锁」。

Lua 脚本:

释放锁必须同时满足:

  1. 校验:当前 Redis 里的 value 还是我的 lockValue
  2. 删除:校验通过才 DEL
  3. 原子性:两步必须一气呵成,中间不能被别的命令插入
java 复制代码
public DistributeLockServiceImpl() {
    redisScript = new DefaultRedisScript<>(); // 1. 创建脚本对象
    redisScript.setScriptText(SCRIPT_TEXT);   // 2. 设置 Lua 脚本内容
    redisScript.setResultType(Long.class);    // 3. 设置返回值类型
}

这是一个构造函数,在 DistributeLockServiceImpl 类被 Spring 创建 Bean 实例时执行,用于初始化 Lua 脚本对象。

java 复制代码
    Long result = redisTemplate.execute(
                redisScript,
                Collections.singletonList(fullLockKey),
                lockValue
        );

上述代码执行 Lua 脚本的核心调用,将 Lua 脚本发送给 Redis 执行并返回结果。

完善ExcelToTableServiceImpl -- 使用分布式锁生成表名

java 复制代码
    private String createTableName() {
        // 1. 设置一个重试次数
        int maxRetries = 5;
        int retryCount = 1;

        // 2. 设置lockValue
        String lockValue = UUID.randomUUID().toString();
        // 3. 获取了锁之后再去生成表名, 否则就重复
        while (retryCount <= maxRetries) {
            String baseName = "excel_table";
            String tableName = baseName + String.valueOf(System.currentTimeMillis()).substring(8);

            String lockKey = tableName;
            boolean lockAcquired = distributeLockService.tryLock(lockKey, lockValue, 60);
            if (lockAcquired) {
                // 获取分布式锁成功,对表名做个唯一判断
                if (tableExists(tableName)) {
                    distributeLockService.releaseLock(lockKey, lockValue);
                } else {
                    return tableName;
                }
            }
            retryCount++;

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
        }
        throw new RuntimeException("生成唯一表名失败");
    }

     private boolean tableExists(String tableName) {
        String sql = "select count(1) from information_schema.tables where table_schema = DATABASE() and table_name = ?";
        return jdbcTemplate.queryForObject(sql, Integer.class, tableName) > 0;
    }

实现创建表逻辑

java 复制代码
/**
     * 创建表
     * @param tableName 表名
     * @param file 文件
     * @param fileId 文件ID
     * @param sheetIndex sheet所在的索引位置
     */
    private void createTableFromExcel(String tableName, MultipartFile file, Long fileId, int sheetIndex) {
        // 1. 读取excel的表头
        
        // 2. 根据excel的表头生成table的字段
           
        // 3. 生成sql语句  create table
          
        // 4. 执行sql语句    

    }

实现读取 excel 表头方法

java 复制代码
    private List<String> readHeader(MultipartFile file, int sheetIndex) throws IOException {
        byte[] fileBytes = getFileBytes(file);
        List<String> headers = new ArrayList<>();

        try (ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes)) {
            AnalysisEventListener<Map<Integer, String>> headerListener = new AnalysisEventListener<>() {
                @Override
                public void invoke(Map<Integer, String> data, AnalysisContext context) {
                    int maxCol = data.keySet().stream().max(Integer::compareTo).orElse(-1);
                    for (int i = 0; i <= maxCol; i++) {
                        headers.add(data.getOrDefault(i, ""));
                    }
                    // 读取到表头后立即停止解析
                    throw new ExcelAnalysisStopException("Header row read completed");
                }

                @Override
                public void doAfterAllAnalysed(AnalysisContext context) {
                    // no-op
                }
            };

            try {
                EasyExcel.read(bais, headerListener)
                        .sheet(sheetIndex)
                        .headRowNumber(0) // 第一行就是表头
                        .doRead();
            } catch (ExcelAnalysisStopException ignore) {
                // 正常用于提前结束读取
            }
        }

        if (headers.isEmpty()) {
            throw new IllegalArgumentException("指定 Sheet(索引 " + sheetIndex + ") 没有表头或无法读取表头");
        }

        return headers;
    }

Map<Integer, String> data:这个data就是 EasyExcel 读取到的一行数据,用"列索引→值"的 Map

复制代码
data.keySet()                                    // 1. 获取所有列的索引
     .stream()                                   // 2. 转为流(便于操作)
     .max(Integer::compareTo)                    // 3. 找出最大的索引值
     .orElse(-1);                                // 4. 如果没有数据,返回 -1

// 写法1:方法引用(我的代码)

.max(Integer::compareTo)

// 写法2:Lambda 表达式

.max((a, b) -> a.compareTo(b))

// 写法3:完整匿名内部类(Java 8 之前)

.max(new Comparator<Integer>() {

@Override

public int compare(Integer a, Integer b) {

return a.compareTo(b);

}

})

补充 createTableFromExcel 方法中

java 复制代码
    /**
     * 创建表
     * @param tableName 表名
     * @param file 文件
     * @param fileId 文件ID
     * @param sheetIndex sheet所在的索引位置
     */
    private void createTableFromExcel(String tableName, MultipartFile file, Long fileId, int sheetIndex) {
        // 1. 读取excel的表头
        try {
            List<String> headers = readHeader(file, sheetIndex);
            if (headers.isEmpty()) {
                throw new IllegalArgumentException("sheet没有表头");
            }


            // 2. 根据excel的表头生成table的字段
            
            // 3. 生成sql语句  create table   

            // 4. 执行sql语句
        } catch(IOException e) {
            throw new RuntimeException(e);
        }

    }

实现 createColumNames 方法,生成表的列名

由于表头可能是中文,我发直接作为mysql 字段,需要 createColumNames 方法对字段做转义

创建 PinyinUtil

java 复制代码
/**
 * 中文转拼音工具类
 */
public class PinyinUtil {


    public static String chineseToPinYin(String chinese) {

        StringBuilder pinyin = new StringBuilder();
        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();

        // 设置格式
        format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        format.setVCharType(HanyuPinyinVCharType.WITH_V);

        char[] chars = chinese.toCharArray();

        for(int i =0; i< chars.length; i++) {
            char c = chars[i];

            if (Character.toString(c).matches("[\\u4E00-\\u9FA5]+")) {
                try {
                    String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
                    if (pinyinArray != null && pinyinArray.length > 0) {
                        pinyin.append(pinyinArray[0]);
                        if (i < chars.length -1) {
                            pinyin.append("_");
                        }
                    }
                } catch (BadHanyuPinyinOutputFormatCombination e) {
                    throw new RuntimeException(e);
                }
            } else if (Character.isLetterOrDigit(c)) {
                pinyin.append(Character.toLowerCase(c));
                if (i < chars.length - 1 && isSpecialChar(chars[i+1])) {
                    pinyin.append("_");
                }
            }
            else if (c == '_') {
                pinyin.append("_");
            }
            else {
                pinyin.append("_");
            }
        }
        return clean(pinyin.toString());
    }


    private static  boolean isSpecialChar(char c) {
        return !Character.isLetterOrDigit(c) && c != '_';
    }

    private static String clean(String str) {
        // 第1行:去掉开头和结尾的所有下划线
        str = str.replaceAll("^_+|_+$", "");

        // 第2行:将中间连续的多个下划线合并为单个
        str = str.replaceAll("_+", "_");        
        return str;
    }
}
java 复制代码
    private String createColumnName(String headerName) {
        // 1. 删掉首尾空格
        String cleanName = headerName.trim();

        // 2. 检查是否包含中文
        if (!cleanName.matches(".*[\\u4e00-\\u9fa5].*")) { //判断字符串中是否包含至少一个中文字符
            // 将所有非字母数字的字符替换为短横线 -
            cleanName = cleanName.replaceAll("[^a-zA-Z0-9]", "-");
            //判断字符串是否以字母(a-z 或 A-Z)开头
            if (!cleanName.matches("[^a-zA-Z].*")) {
                cleanName = "col_" +cleanName;
            }
        } else {
            cleanName = PinyinUtil.chineseToPinYin(cleanName);
            cleanName = "col_" + cleanName;
        }

        if (cleanName.length() > 64) {
            cleanName = cleanName.substring(0, 64);
            // 去掉字符串末尾的所有下划线 _
            cleanName = cleanName.replaceAll("_+$", "");
        }
        

        if (!cleanName.matches("^[a-zA-Z][a-zA-Z0-9]*$")) {
            cleanName = "filed_" + cleanName;
        }
        return cleanName.trim();
    }

补全 createTableFromExcel 创建表方法

java 复制代码
/**
     * 创建表
     * @param tableName 表名
     * @param file 文件
     * @param fileId 文件ID
     * @param sheetIndex sheet所在的索引位置
     */
    private void createTableFromExcel(String tableName, MultipartFile file, Long fileId, int sheetIndex) {
        // 1. 读取excel的表头
        try {
            List<String> headers = readHeader(file, sheetIndex);
            if (headers.isEmpty()) {
                throw new IllegalArgumentException("sheet没有表头");
            }

            // 2. 根据excel的表头生成table的字段
            List<String> safeHeaders = new ArrayList<>();
            for (String header : headers) {
                String safeHeader = createColumnName(header);
                safeHeaders.add(safeHeader);
            }
            System.out.println("字段长度" + safeHeaders.size());

            // 3. 生成sql语句  create table
            StringBuilder sql = new StringBuilder();
            sql.append("create table `").append(tableName).append("` (");
            sql.append(" `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',");
            for (int i =0; i < safeHeaders.size(); i++) {
                String safeHeader = safeHeaders.get(i);
                String header = headers.get(i);
                sql.append("`").append(safeHeader).append("` TEXT COMMENT '").append(header).append("',");
            }
            sql.setLength(sql.length() - 1);
            sql.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Excel导入表'");
            log.info("生成的sql{}", sql);

            // 4. 执行sql语句
            jdbcTemplate.execute(sql.toString());
            
            // 记录 excel表头 和 mysql表 的映射关系
            

            
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

实现 excel表头 和 mysql表 的映射关系的记录

Excel 表头不能直接当 MySQL 列名用,建表时必须改成合法字段名,所以要单独存一份「原始表头 ↔ 数据库字段」的对应关系。若不记录映射,之后就无法知道「这个 DB 字段对应用户原来看到的哪一列」。

编写实体类 FileMappingEntity

java 复制代码
/**
 * 字段映射关系表的实体类
 */
@Data
@TableName("field_mappings")
public class FieldMappingEntity {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 文件ID
     */
    private Long fileId;

    /**
     * 表名
     */
    private String tableName;

    /**
     * 动态字段名
     */
    private String dbFieldName;

    /**
     * excel中的表头
     */
    private String originalHeader;

    /**
     * excel表头的顺序
     */
    private Integer fieldOrder;

}

创建 FieldMappingMapper

java 复制代码
/**
 * 字段映射关系表mapper
 */
@Mapper
public interface FieldMappingMapper extends BaseMapper<FieldMappingEntity> {


    @Insert("<script>" +
            "INSERT INTO field_mappings (file_id, table_name, db_field_name, original_header, field_order) " +
            "VALUES " +
            "<foreach collection='mappings' item='mapping' separator=','> " +
            "(#{mapping.fileId}, #{mapping.tableName}, #{mapping.dbFieldName}, #{mapping.originalHeader}, #{mapping.fieldOrder}) " +
            "</foreach>" +
            "</script>"
    )
    int batchInsert(@Param("mappings")List<FieldMappingEntity> mappings);

}

继承 BaseMapper<FieldMappingEntity> 后,自动拥有以下方法:

|----------------|------------|
| insert() | 插入单条记录 |
| deleteById() | 根据 ID 删除 |
| updateById() | 根据 ID 更新 |
| selectById() | 根据 ID 查询 |
| selectList() | 条件查询列表 |
| selectPage() | 分页查询 |
| 等等... | 基础 CRUD 操作 |

方法签名 int batchInsert(@Param("mappings") List<FieldMappingEntity> mappings);

部分 说明
int 返回值:成功插入的记录条数
@Param("mappings") 给参数起别名,在 SQL 中用 #{mappings} 引用
List<FieldMappingEntity> 传入的映射实体列表

<script>标签

java 复制代码
@Insert("<script>" +
        "INSERT INTO field_mappings (file_id, table_name, db_field_name, original_header, field_order) " +
        "VALUES " +
        "<foreach collection='mappings' item='mapping' separator=','> " +
        "(#{mapping.fileId}, #{mapping.tableName}, #{mapping.dbFieldName}, #{mapping.originalHeader}, #{mapping.fieldOrder}) " +
        "</foreach>" +
        "</script>"
)
标签 作用
<script> 告诉 MyBatis 这是一个动态 SQL,需要解析
<foreach> 循环遍历集合,生成多条 VALUES
collection='mappings' 遍历的参数名
item='mapping' 循环中的单个元素变量名
separator=',' 每个元素之间用逗号分隔

举例:输入数据

java 复制代码
List<FieldMappingEntity> mappings = Arrays.asList(
    FieldMappingEntity.builder()
        .fileId(10000001L)
        .tableName("sales_data")
        .dbFieldName("sale_date")
        .originalHeader("日期")
        .fieldOrder(1)
        .build(),
    FieldMappingEntity.builder()
        .fileId(10000001L)
        .tableName("sales_data")
        .dbFieldName("salesperson")
        .originalHeader("销售员")
        .fieldOrder(2)
        .build()
);

生成的 SQL:

java 复制代码
INSERT INTO field_mappings (file_id, table_name, db_field_name, original_header, field_order)
VALUES
(10000001, 'sales_data', 'sale_date', '日期', 1),
(10000001, 'sales_data', 'salesperson', '销售员', 2)

创建 FieldMappingService 与 FieldMappingServiceImpl

java 复制代码
/**
 * 保存字段关系的实现类
 */
@Service
@Slf4j
public class FieldMappingServiceImpl implements FieldMappingService {

    @Autowired
    private FieldMappingMapper fieldMappingMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int saveMappings(Long fileId, String tableName, List<String> originalHeader, List<String> dbFieldName) {

        List<FieldMappingEntity> mappingEntities = new ArrayList<>();
        for (int i = 0; i < originalHeader.size(); i++) {
            String excelHeader = originalHeader.get(i);
            String  dbName = dbFieldName.get(i);

            FieldMappingEntity fieldMappingEntity = new FieldMappingEntity();
            fieldMappingEntity.setFileId(fileId);
            fieldMappingEntity.setTableName(tableName);
            fieldMappingEntity.setDbFieldName(dbName);
            fieldMappingEntity.setOriginalHeader(excelHeader);
            fieldMappingEntity.setFieldOrder(i);
            mappingEntities.add(fieldMappingEntity);
        }
        return fieldMappingMapper.batchInsert(mappingEntities);
    }
}

补全 createTableFromExcel 中记录映射关系内容

java 复制代码
// 记录 excel表头 和 mysql表 的映射关系
fieldMappingService.saveMappings(fileId, tableName, headers, safeHeaders);

完善 convertExcelToTable 方法

java 复制代码
    private List<String> convertExcelToTable(MultipartFile file, Long fileId, List<String> sheetNames) {

        // 1. 先生成表名
        String baseTableName = createTableName();
        List<String> tableNames = new ArrayList<>();

        // 2. 创建表
        for (int i =0; i < sheetNames.size(); i++) {
            String tableName = baseTableName + "_sheet" + i;
            createTableFromExcel(tableName, file, fileId, i);
            // 3. 获取file的数据

            // 4. 把file的数据转换成插入的sql

            // 5. 执行sql
           
        }

        return tableNames;
    }
实现数据的插入

创建 insertData 方法实现数据的插入

insertData(tableName, file, sheetIndex)

│

├─① 元数据准备:表头、表结构、列名映射

│

├─② 按文件大小分流读数

│ ├─ > 5MB → EasyExcel 分批流式(每批 1000 行)

EasyExcel 逐行读取

↓

每 1000 行触发一次回调

↓

将 1000 行数据转为 SQL 参数

↓

执行批量插入(1000 条/批)

↓

清空缓冲区,继续读取

│ └─ ≤ 5MB → POI 一次性读入内存

│

└─③ executeBatchInsert:拼多值 INSERT,每批最多 1000 行执行

java 复制代码
    public void insertData(String tableName, MultipartFile file, int sheetIndex) {

        // 1. 实现2种读取数据的方式 以文件的大小作为判断的标准
        long fileSize = 5 * 1024 * 1024;
        try {
            // 获取中文表头
            List<String> originalHeaders = readHeader(file, sheetIndex);
            // 获取表的结构才能实现数据的插入
            List<Map<String, Object>> tableStructure = getTableStructure(tableName);
            List<String> columns = new ArrayList<>();

            for (Map<String, Object> column : tableStructure) {
                String columnName = (String) column.get("FieLd");
                if (!columnName.equals("id")) {
                    columns.add(columnName);
                }
            }

            Map<String, String> headerMapping = new HashMap<>();
            for (int i = 0; i< originalHeaders.size(); i++) {
                String originalHeader = originalHeaders.get(i);
                String safeColum = columns.get(i);
                headerMapping.put(originalHeader, safeColum);
            }


            // 2. 大于5MB,使用流式处理
            if (file.getSize() > fileSize) {
                // 初始化一个计数器
                final int[] streamInserted = {0};
                final Map<String, String> finalHeaderMapping = headerMapping;
                final List<String> finalSafeColumnNames = columns;
                final int finalSheetIndex = sheetIndex;

                // 流式地获取数据,逐个加入sql语句
                processExcelInBatches(file, finalSheetIndex, batchData -> {

                    List<Object[]> batchArgs = new ArrayList<>();

                    for (Map<String, Object> row : batchData) {
                        Object[] args = new Object[finalSafeColumnNames.size()];
                        for (int i = 0; i <finalSafeColumnNames.size(); i++) {
                            String safeColumnName = finalSafeColumnNames.get(i);

                            String originalHeader = null;
                            for (Map.Entry<String, String> entry : finalHeaderMapping.entrySet()) {
                                if (entry.getValue().equals(safeColumnName)) {
                                    originalHeader = entry.getKey();
                                    break;
                                }
                            }

                            // 根据原始列名从row中获取对应的数据值
                            if (originalHeader != null) {
                                args[i] = row.get(originalHeader);
                            } else {
                                args[i] = null;
                            }

                        }
                        batchArgs.add(args);
                    }
                    // 每次将当前批次的数据拼成多值 INSERT,单条 SQL 最多 500 行
                    executeBatchInsert(tableName, finalSafeColumnNames, batchArgs);
                    streamInserted[0] += batchArgs.size();
                });


            } else {
                // 3. 否则一次性读取数据
               

            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

获取表结构方法的实现

java 复制代码
    // 获取表结构
    private List<Map<String, Object>> getTableStructure(String tableName) {
        String sql = "describe `" +tableName + "`";
        return jdbcTemplate.queryForList(sql);
    }

分批处理逻辑

java 复制代码
processExcelInBatches(file, sheetIndex, batchData -> {
    List<Object[]> batchArgs = new ArrayList<>();
    for (Map<String, Object> row : batchData) {
        // 转换数据...
        batchArgs.add(args);
    }
    executeBatchInsert(tableName, columns, batchArgs);
});

这是一个 Lambda 表达式(匿名函数)-》 参数 -> { 方法体 }

等价于

java 复制代码
processExcelInBatches(file, sheetIndex, new Consumer<List<Map<String, Object>>>() {
    @Override
    public void accept(List<Map<String, Object>> batchData) {
        List<Object[]> batchArgs = new ArrayList<>();
        for (Map<String, Object> row : batchData) {
            // 转换数据...
            batchArgs.add(args);
        }
        executeBatchInsert(tableName, columns, batchArgs);
    }
});
复制代码
1. Lambda 被定义 → 还没执行(像一个"待办事项")
2. 传给 processExcelInBatches() 方法
3. 方法内部在合适时机调用 batchProcessor.accept()
4. Lambda 里的代码才真正被执行

DESCRIBE 表名 返回的是元数据结果集,每一行大致是:

Field Type Null Key Default Extra
id bigint NO PRI null auto_increment
col_xxx text YES null

queryForList 会把:

  • 每一行 → 一个 Map
  • 列名 → Map 的 key(String)
  • 单元格值 → Map 的 value(Object,因为类型不一:字符串、数字、null 都有)

所以整体是 List<Map<String, Object>>:一行一个 Map

实现流式数据,逐个加入 sql 语句

java 复制代码
     private void processExcelInBatches(MultipartFile file,
                                       int sheetIndex, Consumer<List<Map<String, Object>>> batchProcessor) {
        // 1. 直接把文件转换成字节数组
        byte[] fileBytes = getFileBytes(file);

        // 2. 使用字节数组输入流创建EasyExcel读取器
        // 3. 保存数据
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(fileBytes)) {

            EasyExcel.read(byteArrayInputStream, new EasyExcelBatchReadListener(batchProcessor, EXCEL_BATCH_SIZE))
                    .sheet(sheetIndex)
                    .headRowNumber(0)
                    .doRead();
        } catch (Exception e) {
            throw new RuntimeException("处理大文件失败:"+e.getMessage());
        }

    }
    
    private static class EasyExcelBatchReadListener extends AnalysisEventListener<Map<Integer, String>> {
        private final Consumer<List<Map<String, Object>>> batchProcessor;
        private final int batchSize;

        private final List<String> headers = new ArrayList<>();
        private final List<Map<String, Object>> currentBatch = new ArrayList<>();

        private EasyExcelBatchReadListener(Consumer<List<Map<String, Object>>> batchProcessor, int batchSize) {
            this.batchProcessor = batchProcessor;
            this.batchSize = batchSize;
        }

        @Override
        public void invoke(Map<Integer, String> integerStringMap, AnalysisContext analysisContext) {
            if (headers.isEmpty()) {
                int maxCol = integerStringMap.keySet().stream().max(Integer::compareTo).orElse(-1);
                for (int i = 0; i <= maxCol; i++) {
                    headers.add(integerStringMap.getOrDefault(i, ""));
                }
                return;
            }

            Map<String, Object> rowData = new LinkedHashMap<>();
            for (int i =0; i <headers.size(); i++) {
                String header = headers.get(i);
                String value = integerStringMap.getOrDefault(i, "");
                rowData.put(header, value);
            }
            currentBatch.add(rowData);

            if (currentBatch.size() >= batchSize) {
                batchProcessor.accept(new ArrayList<>(currentBatch));
                currentBatch.clear();
            }
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext analysisContext) {

        }
    }     

   

监听器的 invoke 逻辑:

解释 batchProcessor.accept(new ArrayList<>(currentBatch)):将当前批次的数据传递给外部处理器,外部处理器在 insertData 方法代码中

java 复制代码
// 流式地获取数据,逐个加入sql语句
                processExcelInBatches(file, finalSheetIndex, batchData -> {

                    List<Object[]> batchArgs = new ArrayList<>();

                    for (Map<String, Object> row : batchData) {
                        Object[] args = new Object[finalSafeColumnNames.size()];
                        for (int i = 0; i <finalSafeColumnNames.size(); i++) {
                            String safeColumnName = finalSafeColumnNames.get(i);

                            String originalHeader = null;
                            for (Map.Entry<String, String> entry : finalHeaderMapping.entrySet()) {
                                if (entry.getValue().equals(safeColumnName)) {
                                    originalHeader = entry.getKey();
                                    break;
                                }
                            }

                            // 根据原始列名从row中获取对应的数据值
                            if (originalHeader != null) {
                                args[i] = row.get(originalHeader);
                            } else {
                                args[i] = null;
                            }

                        }
                        batchArgs.add(args);
                    }
                    // 每次将当前批次的数据拼成多值 INSERT,单条 SQL 最多 500 行
                    executeBatchInsert(tableName, finalSafeColumnNames, batchArgs);
                    streamInserted[0] += batchArgs.size();
                });
java 复制代码
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);  // 接收一个参数,无返回值
}

实现批量插入执行器:executeBatchInsert

java 复制代码
    private void executeBatchInsert(String tableName, List<String> columns, List<Object[]> args) {
        if (args == null || args.isEmpty()) {
            return;
        }

        int columnCount = columns.size();

        for (int start = 0; start < args.size(); start += SQL_BATCH_SIZE) {
            int end = Math.min(start + SQL_BATCH_SIZE, args.size());

            StringBuilder sqlBuilder = new StringBuilder();
            sqlBuilder.append("insert into `").append(tableName).append("` (");
            for (String name : columns) {
                sqlBuilder.append("`").append(name).append("`,");
            }
            sqlBuilder.setLength(sqlBuilder.length() - 1);
            sqlBuilder.append(") values ");

            List<Object> paramList = new ArrayList<>();

            for (int i = start; i < end; i++) {
                Object[] rowValues = args.get(i);
                sqlBuilder.append("(");
                for (int j = 0; j < columnCount; j++) {
                    sqlBuilder.append("?,");
                    paramList.add(rowValues[j]);
                }
                sqlBuilder.setLength(sqlBuilder.length() - 1);
                sqlBuilder.append("),");
            }

            sqlBuilder.setLength(sqlBuilder.length() - 1); // 去掉最后一个逗号

            jdbcTemplate.update(sqlBuilder.toString(), paramList.toArray());
        }
    }

一次性处理逻辑:

java 复制代码
public void insertData(String tableName, MultipartFile file, int sheetIndex) {

        // 1. 实现2种读取数据的方式 以文件的大小作为判断的标准
        long fileSize = 5 * 1024 * 1024;
        try {
            List<String> originalHeaders = readHeader(file, sheetIndex);
            List<Map<String, Object>> tableStructure = getTableStructure(tableName);
            List<String> columns = new ArrayList<>();

            for (Map<String, Object> column : tableStructure) {
                String columnName = (String) column.get("FieLd");
                if (!columnName.equals("id")) {
                    columns.add(columnName);
                }
            }

            Map<String, String> headerMapping = new HashMap<>();
            for (int i = 0; i< originalHeaders.size(); i++) {
                String originalHeader = originalHeaders.get(i);
                String safeColum = columns.get(i);
                headerMapping.put(originalHeader, safeColum);
            }


            // 2. 大于5MB,使用流式处理
            if (file.getSize() > fileSize) {
                
            } else {
                // 3. 否则一次性读取数据
                byte[] fileBytes = getFileBytes(file);
                List<Object[]> allArgs = new ArrayList<>();
                try(Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(fileBytes)))  {
                    Sheet sheet = workbook.getSheetAt(sheetIndex);
                    int totalRows = sheet.getLastRowNum();
                    
                    // 遍历每一行
                    for (int i = 1; i<= totalRows; i++) {
                        Row row = sheet.getRow(i);
                        if (row == null) {
                            continue;
                        }
                        
                        // 读取每行数据到 map
                        Map<String, Object> rowData = new LinkedHashMap<>();
                        for (int j = 0; j <originalHeaders.size(); j++) {
                            Cell cell = row.getCell(j);
                            rowData.put(originalHeaders.get(j), getCellValue(cell));
                        }

                        Object[] args = new Object[columns.size()];
                        for (int k = 0; k < columns.size(); k++) {
                            String safeColumName = columns.get(k);
                            String originHeader = null;

                            for (Map.Entry<String, String> entry : headerMapping.entrySet()) {
                                if (entry.getValue().equals(safeColumName)) {
                                    originHeader = entry.getKey();
                                    break;
                                }
                            }
                            if (originHeader != null) {
                                args[k] = rowData.get(originHeader);
                            } else {
                                args[k] = null;
                            }
                        }
                        allArgs.add(args);
                    }
                }
                executeBatchInsert(tableName, columns, allArgs);


            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

提取单元格数据代码

java 复制代码
// 提取excel单元格值
    private String getCellValue(Cell cell) {
        return switch (cell.getCellType()) {
            case STRING -> cell.getStringCellValue().trim();
            case NUMERIC -> {
                if (DateUtil.isCellDateFormatted(cell)) {
                    Date date = cell.getDateCellValue();
                    SimpleDateFormat sdf = new SimpleDateFormat("YY-MM-dd HH:mm:ss");
                    yield sdf.format(date);
                } else {
                    double number = cell.getNumericCellValue();
                    yield String.valueOf(number);
                }
            }
            case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
            case FORMULA -> String.valueOf(cell.getCellFormula());
            default -> "";
        };
    }

处理过程:

最终 allArgs 收集的是

补全 convertExcelToTable 转换单个sheet方法

java 复制代码
    private List<String> convertExcelToTable(MultipartFile file, Long fileId, List<String> sheetNames) {

        // 1. 先生成表名
        String baseTableName = createTableName();
        List<String> tableNames = new ArrayList<>();

        // 2. 创建表
        for (int i =0; i < sheetNames.size(); i++) {
            String tableName = baseTableName + "_sheet" + i;
            createTableFromExcel(tableName, file, fileId, i);
            // 3. 获取file的数据

            // 4. 把file的数据转换成插入的sql

            // 5. 执行sql
            insertData(tableName, file, i);
            tableNames.add(tableName);
        }

        return tableNames;
    }

补全 convertExcelToTable 转换一个文件,多个 sheet 方法

java 复制代码
    @Override
    public List<String> convertExcelToTable(MultipartFile file, Long fileId) {
        // 1. 先获取excel的数据
        int sheetCount = getSheetCount(file);

        if (sheetCount == 0) {
            throw new IllegalArgumentException("excel是个空文件");
        }

        // 2. 获取sheet的名称
        List<String> sheetNames = getSheetNames(file);


        // 3. 根据文件信息做实际table的转换

        return convertExcelToTable(file, fileId, sheetNames);
    }

根据已实现的方法,完善 FileServiceImpl 里的 upload 方法

java 复制代码
    /**
 * 操作文件接口的实现类
 */
@Service
@Slf4j
public class FileServiceImpl implements FilesService {

    @Autowired
    private OssService ossService;

    @Autowired
    private ExcelToTableService excelToTableService;

    @Autowired
    private FilesMapper filesMapper;

    @Autowired
    private FileTableMappingService fileTableMappingService;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private FieldMappingService fieldMappingService;


    @Override
    public FileUploadResponse upload(MultipartFile file, FileUploadRequest request, Long userId) {
        // 1. 校验文件(文件类型、文件大小)
        if (!FileValidationUtil.validateFileFormat(file)) {
            throw new IllegalArgumentException("只能处理excel文件");
        }

        if (!FileValidationUtil.ifOutOfLarge(file)) {
            throw new IllegalArgumentException("文件带下不能超过50MB");
        }

        // 2. 上传文件到阿里云OSS
        String fileUrl = ossService.uploadFile(file, request.getCategory());

        // 3. 创建文件记录
        


        // 4. 把excel转化成mysql的表
        List<String> tableNames = excelToTableService.convertExcelToTable(file, filesEntity.getId());

        fileTableMappingService.saveMappings(filesEntity.getId(), tableNames, null);

        // 5. 构建响应
        FileUploadResponse response = FileUploadResponse.builder()
                .fileId(filesEntity.getId())
                .fileName(filesEntity.getFileName())
                .fileSize(filesEntity.getFileSize())
                .fileUrl(fileUrl)
                .uploadStatus(1)
                .ossKey(filesEntity.getOssKey())
                .build();


        return response;
    }

实现 记录文件原信息 的功能

创建 FilesEntity 实体类

java 复制代码
/**
 * 文件元信息
 */
@Data
@Slf4j
@Builder
@TableName("files")
public class FilesEntity {

    /**
     * 文件ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 用户ID
     */
    @TableField("user_id")
    private Long userId;

    /**
     * 文件名
     */
    @TableField("file_name")
    private String fileName;

    /**
     * 文件上传路径
     */
    @TableField("file_path")
    private String filePath;

    /**
     * 文件大小
     */
    @TableField("file_size")
    private Long fileSize;

    /**
     * 保存在oss中的标识
     */
    @TableField("oss_key")
    private String ossKey;

    /**
     * 文件上传状态
     */
    @TableField("upload_status")
    private Integer uploadStatus;
}

在 upload 方法中创造 FilesEntity 实例

java 复制代码
        // 3. 创建文件记录
        FilesEntity filesEntity = FilesEntity.builder()
                .userId(userId)
                .fileName(file.getOriginalFilename())
                .filePath(request.getCategory() == null ? "upload" : request.getCategory())
                .fileSize(file.getSize())
                .ossKey(getOssKey(fileUrl))
                .build();
        filesMapper.insert(filesEntity);
        

getOssKey(fileUrl) 方法的实现

java 复制代码
    // 根据文件地址获取key
    private String getOssKey(String fileUrl) {
        String[] parts = fileUrl.split("/");
        if (parts.length >= 4) {
            StringBuilder key = new StringBuilder();
            for (int i = 3; i <parts.length; i++) {
                if (i >3) {
                    key.append("/");
                }
                key.append(parts[i]);
            }
            return key.toString();
        }
        return "";
    }

实现 FilesMapper 把 filesEntity 插入数据库

java 复制代码
/**
 * 文件访问数据库的 mapper
 */
@Mapper
public interface FilesMapper extends BaseMapper<FilesEntity> {

    
}

FileTableMappingService

java 复制代码
/**
 * 文件映射表的服务类
 */
public interface FileTableMappingService {


    void saveMappings(Long fileId, List<String> tableNames, List<String> sheetNames);
}

FileTableMappingEntity

java 复制代码
/**
 * 文件sheet和mysql表的映射关系
 */
@Data
@TableName("file_table_mappings")
public class FileTableMappingEntity {

    @TableId(type = IdType.AUTO)
    private Long id;

    private Long fileId;

    private String tableName;

    private Integer sheetIndex;

    private String sheetName;

}

FileTableMappingMapper

java 复制代码
/**
 * 文件与表映射mapper
 */
@Mapper
public interface FileTableMappingMapper extends BaseMapper<FileTableMappingEntity> {
}

FileTableMappingServiceImpl

java 复制代码
/**
 * 文件映射表的服务实现类
 */
@Service
public class FileTableMappingServiceImpl implements FileTableMappingService {

    @Autowired
    private FileTableMappingMapper fileTableMappingMapper;


    @Override
    public void saveMappings(Long fileId, List<String> tableNames, List<String> sheetNames) {
        for (int i = 0; i <tableNames.size(); i++) {
            FileTableMappingEntity fileTableMappingEntity = new FileTableMappingEntity();
            fileTableMappingEntity.setFileId(fileId);
            fileTableMappingEntity.setTableName(tableNames.get(i));
            if (sheetNames != null && sheetNames.get(i) != null) {
                fileTableMappingEntity.setSheetName(sheetNames.get(i));
            }
            fileTableMappingEntity.setSheetIndex(i);
            fileTableMappingMapper.insert(fileTableMappingEntity);
        }
    }
}

完善 Controller 接口

java 复制代码
/**
 * 文件服务控制器
 */
@RestController
@RequestMapping("/files")
public class FilesController {


    @Autowired
    private FilesService filesService;

    @Autowired
    private JwtUtil jwtUtil;


    /**
     * 单个文件上传接口
     */
    @PostMapping("/upload/single")
    @LogOperation("文件上传")
    public Result<FileUploadResponse> uploadFile(
            @RequestParam("file")MultipartFile file,
            @RequestHeader("Authorization")String authorization,
            @Valid FileUploadRequest request
            ) {
        Long userId = jwtUtil.getUserIdByAuthorization(authorization);
        if (userId == null) {
            return Result.badRequest("无效的令牌");
        }

        FileUploadResponse response = filesService.upload(file, request, userId);
        return  Result.success("文件上传成功", response);
    }
相关推荐
xcl09251 天前
北京24小时自助健身房系统软件开发实战:从架构到部署全流程指南
java·spring boot·架构
一 乐1 天前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长1 天前
w214基于jsp知道特产网
java·intellij-idea
步行cgn1 天前
Spring 注解使用详解
java·spring
一条小小yu1 天前
Spring IoC的理解
java·后端·spring
落魄实习生1 天前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈1 天前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
自强的小白1 天前
核心功能(Service接口)
java·mybatis
乌暮1 天前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习