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-idaccessKeyId),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);
    }
相关推荐
Tangyuewei1 小时前
Java 写 Agent:模型只是组件
java·开发语言
彧azz2 小时前
Java学习语法篇:变量
java·学习
信誓旦旦的程序猿3 小时前
【量化系统从零构建 #04】存储设计:选型·建库·交易日历
java·人工智能·python·股票数据api·股票数据·股票数据api接口·股票api数据接口
yurenpai(27届找实习中)3 小时前
Java 10 的 var 为什么不能随便用?从订单汇总看懂类型推断与泛型陷阱
java·开发语言·java18
Flynt4 小时前
把公司项目迁到 Spring Boot 4.0:编译通过只是开始
java·spring boot·后端
Tangyuewei4 小时前
给老 Spring 项目装个 AI Agent
java·人工智能·spring
她的男孩5 小时前
多租户和数据权限怎么共存?扒完拦截器注册链路,我找到 4 个隐蔽的坑
java·后端·架构
小溪学编程5 小时前
Java BufferedReader 详解:从基础用法到性能优化
java·python·性能优化
HZero.chen5 小时前
Java 匿名类简介
java·匿名类