在开发管理系统 ,因为系统要求 要同时支持 本地、ftp、sftp、阿里云 oss、腾讯云 cos、minio、 amazon s3 这几种文件存储的上传方式 ,如果一一开发 肯定开发到花都谢了。
 经过搜索 发现了一个好用的插件
 x-file-storage
 官方地址:https://x-file-storage.xuyanwu.cn/#/
废话不多说 还是直接看代码
 spring 版本 啥的 就不说了
-  因为我们的文件上传的 基础参数配置是在 nacos 和数据库里 所以 采用的是动态 切换 存储方式 
 没有把配置参数 定义在 bootstrap.yml 配置文件中
-  官方模式的使用方式 是读取 配置文件的信息 来知道你用的哪种文件存储 
 我把官方的配置文件 复制过来 大家参考 一下
 这些配置 我们是读取的数据库 所以代码里 没有直接从这里取
 如果 配置了 这个 就不用使用动态切换了 他会默认找
 default-platform: local-plus-1 #默认使用的存储平台
dromara:
  x-file-storage: #文件存储配置,不使用的情况下可以不写
    default-platform: local-plus-1 #默认使用的存储平台
    thumbnail-suffix: ".min.jpg" #缩略图后缀,例如【.min.jpg】【.png】
    local: # 本地存储(不推荐使用)
      - platform: local-1 # 存储平台标识
        enable-storage: true  #启用存储
        enable-access: true #启用访问(线上请使用 nginx 配置,效率更高)
        domain: "" # 访问域名,例如:“http://127.0.0.1:8030/test/file/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名
        base-path: d:/temp/test/ # 存储地址
        path-patterns: /test/file/** # 访问路径,开启 enable-access 后,通过此路径可以访问到上传的文件
    local-plus: # 本地存储升级版
      - platform: local-plus-1 # 存储平台标识
        enable-storage: true  #启用存储
        enable-access: true #启用访问(线上请使用 nginx 配置,效率更高)
        domain: http://127.0.0.1:8030/file/ # 访问域名,访问域名,例如:“http://127.0.0.1:8030/file/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名
        base-path: local-plus/ # 基础路径
        path-patterns: /file/** # 访问路径
        storage-path: d:/temp/ # 存储路径
    huawei-obs: # 华为云 obs ,不使用的情况下可以不写
      - platform: huawei-obs-1 # 存储平台标识
        enable-storage: false  # 启用存储
        access-key: ??
        secret-key: ??
        end-point: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:http://abc.obs.com/
        base-path: hy/ # 基础路径
    aliyun-oss: # 阿里云 oss ,不使用的情况下可以不写
      - platform: aliyun-oss-1 # 存储平台标识
        enable-storage: false  # 启用存储
        access-key: ??
        secret-key: ??
        end-point: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/
        base-path: hy/ # 基础路径
    qiniu-kodo: # 七牛云 kodo ,不使用的情况下可以不写
      - platform: qiniu-kodo-1 # 存储平台标识
        enable-storage: false  # 启用存储
        access-key: ??
        secret-key: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:http://abc.hn-bkt.clouddn.com/
        base-path: base/ # 基础路径
    tencent-cos: # 腾讯云 cos
      - platform: tencent-cos-1 # 存储平台标识
        enable-storage: true  # 启用存储
        secret-id: ??
        secret-key: ??
        region: ?? #存仓库所在地域
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.cos.ap-nanjing.myqcloud.com/
        base-path: hy/ # 基础路径
    baidu-bos: # 百度云 bos
      - platform: baidu-bos-1 # 存储平台标识
        enable-storage: true  # 启用存储
        access-key: ??
        secret-key: ??
        end-point: ?? # 例如 abc.fsh.bcebos.com
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.fsh.bcebos.com/abc/
        base-path: hy/ # 基础路径
    upyun-uss: # 又拍云 uss
      - platform: upyun-uss-1 # 存储平台标识
        enable-storage: true  # 启用存储
        username: ??
        password: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:http://abc.test.upcdn.net/
        base-path: hy/ # 基础路径
    minio: # minio,由于 minio sdk 支持 amazon s3,其它兼容 amazon s3 协议的存储平台也都可配置在这里
      - platform: minio-1 # 存储平台标识
        enable-storage: true  # 启用存储
        access-key: ??
        secret-key: ??
        end-point: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:http://minio.abc.com/abc/
        base-path: hy/ # 基础路径
    amazon-s3: # amazon s3,其它兼容 amazon s3 协议的存储平台也都可配置在这里
      - platform: amazon-s3-1 # 存储平台标识
        enable-storage: true  # 启用存储
        access-key: ??
        secret-key: ??
        region: ?? # 与 end-point 参数至少填一个
        end-point: ?? # 与 region 参数至少填一个
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.hn-bkt.clouddn.com/
        base-path: s3/ # 基础路径
    ftp: # ftp
      - platform: ftp-1 # 存储平台标识
        enable-storage: true  # 启用存储
        host: ?? # 主机,例如:192.168.1.105
        port: 21 # 端口,默认21
        user: anonymous # 用户名,默认 anonymous(匿名)
        password: "" # 密码,默认空
        domain: ?? # 访问域名,注意“/”结尾,例如:ftp://192.168.1.105/
        base-path: ftp/ # 基础路径
        storage-path: / # 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
    sftp: # sftp
      - platform: sftp-1 # 存储平台标识
        enable-storage: true  # 启用存储
        host: ?? # 主机,例如:192.168.1.105
        port: 22 # 端口,默认22
        user: root # 用户名
        password: ?? # 密码或私钥密码
        private-key-path: ?? # 私钥路径,兼容spring的classpath路径、文件路径、http路径等,例如:classpath:id_rsa_2048
        domain: ?? # 访问域名,注意“/”结尾,例如:https://file.abc.com/
        base-path: sftp/ # 基础路径
        storage-path: /www/wwwroot/file.abc.com/ # 存储路径,注意“/”结尾
    webdav: # webdav
      - platform: webdav-1 # 存储平台标识
        enable-storage: true  # 启用存储
        server: ?? # 服务器地址,例如:http://192.168.1.105:8405/
        user: ?? # 用户名
        password: ?? # 密码
        domain: ?? # 访问域名,注意“/”结尾,例如:https://file.abc.com/
        base-path: webdav/ # 基础路径
        storage-path: / # 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
    google-cloud-storage: # 谷歌云存储
      - platform: google-1 # 存储平台标识
        enable-storage: true  # 启用存储
        project-id: ?? # 项目 id
        bucket-name: ??
        credentials-path: file:/deploy/example-key.json # 授权 key json 路径,兼容spring的classpath路径、文件路径、http路径等
        domain: ?? # 访问域名,注意“/”结尾,例如:https://storage.googleapis.com/test-bucket/
        base-path: hy/ # 基础路径
    fastdfs:
      - platform: fastdfs-1 # 存储平台标识
        enable-storage: true  # 启用存储
        run-mod: cover #运行模式
        tracker-server: # tracker server 配置
          server-addr: ?? # tracker server 地址(ip:port),多个用英文逗号隔开
          http-port: 80 # 默认:80
        extra: # 额外扩展配置
          group-name: group2 # 组名,可以为空
          http-secret-key: fastdfs1234567890 # 安全密钥,默认:fastdfs1234567890
        domain: ?? # 访问域名,注意“/”结尾,例如:https://file.abc.com/
        base-path: hy/ # 基础路径
    azure-blob:
      - platform: azure-blob-1 # 存储平台标识
        enable-storage: true  # 启用存储
        connection-string: ?? # 连接字符串,azureblob控制台-安全性和网络-访问秘钥-连接字符串
        end-point: ?? # 终结点 azureblob控制台-设置-终结点-主终结点-blob服务
        container-name: ?? # 容器名称,类似于 s3 的 bucketname,azureblob控制台-数据存储-容器
        domain: ?? # 访问域名,注意“/”结尾,与 end-point 保持一致
        base-path: hy/ # 基础路径
这个 大家按需 配置即可
我直接分享我的动态切换方式 大家按需
第一步:
引入依赖
    <!--        x spring file storage 开始-->
        <dependency>
            <groupid>org.dromara.x-file-storage</groupid>
            <artifactid>x-file-storage-spring</artifactid>
            <version>2.1.0</version>
        </dependency>
        <!--        阿里云-->
        <dependency>
            <groupid>com.aliyun.oss</groupid>
            <artifactid>aliyun-sdk-oss</artifactid>
            <version>3.16.1</version>
        </dependency>
        <!--        腾讯云-->
        <dependency>
            <groupid>com.qcloud</groupid>
            <artifactid>cos_api</artifactid>
            <version>5.6.137</version>
        </dependency>
         <!--        minio  发现用这个依赖请求minio存储 会报错 有可能是版本依赖的问题 -->
<!--        <dependency>-->
<!--            <groupid>io.minio</groupid>-->
<!--            <artifactid>minio</artifactid>-->
<!--            <version>8.5.2</version>-->
<!--        </dependency>-->
        <!--        amazon s3 其它兼容 amazon s3 协议  这个 可以兼容minio 可以用这个依赖请求 -->
        <dependency>
            <groupid>com.amazonaws</groupid>
            <artifactid>aws-java-sdk-s3</artifactid>
            <version>1.12.429</version>
        </dependency>
        <!--        ftp 开始-->
        <dependency>
            <groupid>commons-net</groupid>
            <artifactid>commons-net</artifactid>
            <version>3.9.0</version>
        </dependency>
        <!--糊涂工具类扩展-->
        <dependency>
            <groupid>cn.hutool</groupid>
            <artifactid>hutool-extra</artifactid>
            <version>5.8.22</version>
        </dependency>
           <!--  apache 的对象池   redis 也需要依赖 这个  所以这里不需要了
        如果以前引入的 两个依赖使用的版本不一致 需要调整  -->
<!--        <dependency>-->
<!--            <groupid>org.apache.commons</groupid>-->
<!--            <artifactid>commons-pool2</artifactid>-->
<!--            <version>2.11.1</version>-->
<!--        </dependency>-->
        <!--        ftp 结束-->
        <!--        x spring file storage 结束-->
第二步: 初始化 配置参数
package com.xx.init.filestorage;
import com.xx.init.utils.basedatautil;
import lombok.data;
import org.dromara.x.file.storage.core.filestorageproperties;
import org.dromara.x.file.storage.core.filestorageservice;
import org.dromara.x.file.storage.core.filestorageservicebuilder;
import org.dromara.x.file.storage.core.platform.filestorage;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.component;
import org.springframework.util.stringutils;
import java.util.collections;
import java.util.concurrent.copyonwritearraylist;
/**
 * user:json
 * date: 2024/4/12
 **/
@component
@data
public class filestorageinit {
    @autowired
    private filestorageservice filestorageservice;//注入实列
    private copyonwritearraylist<filestorage> list;
    private string filesystemtype;
    public filestorageservice init(string filesystemtype) {
        this.list = this.filestorageservice.getfilestoragelist();
        this.filesystemtype = filesystemtype;
        if (stringutils.isempty(this.filesystemtype)) {
            this.filesystemtype = "cos";
        }
        if ("cos".equals(this.filesystemtype)) {
            filestorageproperties.tencentcosconfig tencentcosconfig = new filestorageproperties.tencentcosconfig();
            tencentcosconfig.setplatform(this.filesystemtype);
            tencentcosconfig.setregion(basedatautil.getsystemconfignacos().getfilesystemqcloudregion());
            tencentcosconfig.setsecretid(basedatautil.getsystemconfignacos().getqcloudsecretid());
            tencentcosconfig.setsecretkey(basedatautil.getsystemconfignacos().getqcloudsecretkey());
            tencentcosconfig.setbucketname(basedatautil.getsystemconfignacos().getfilesystemqcloudbucket() + "-" + basedatautil.getsystemconfignacos().getqcloudappid());
            list.addall(filestorageservicebuilder.buildtencentcosfilestorage(collections.singletonlist(tencentcosconfig), null));
        } else if ("minio".equals(this.filesystemtype) || "s3".equals(this.filesystemtype)) {
            filestorageproperties.amazons3config amazons3config = new filestorageproperties.amazons3config();
            amazons3config.setplatform(this.filesystemtype);
//            amazons3config.setaccesskey("");
//            amazons3config.setsecretkey("");
//            amazons3config.setregion("");
//            amazons3config.setendpoint("");
//            amazons3config.setbucketname("");
            amazons3config.setaccesskey(basedatautil.getsystemconfignacos().getfilesystems3key());
            amazons3config.setsecretkey(basedatautil.getsystemconfignacos().getfilesystems3secret());
            amazons3config.setregion(basedatautil.getsystemconfignacos().getfilesystems3region());
            amazons3config.setendpoint(basedatautil.getsystemconfignacos().getfilesystems3endpoint());
            amazons3config.setbucketname(basedatautil.getsystemconfignacos().getfilesystems3bucket());
            list.addall(filestorageservicebuilder.buildamazons3filestorage(collections.singletonlist(amazons3config), null));
//
        } else if ("oss".equals(this.filesystemtype)) {
            filestorageproperties.aliyunossconfig ossconfig = new filestorageproperties.aliyunossconfig();
            ossconfig.setplatform(this.filesystemtype);
//            ossconfig.setaccesskey("");
//            ossconfig.setsecretkey("");
//            ossconfig.setbucketname("");
//            ossconfig.setendpoint("");
            ossconfig.setaccesskey(basedatautil.getsystemconfignacos().getfilesystemossaccessid());
            ossconfig.setsecretkey(basedatautil.getsystemconfignacos().getfilesystemossaccesssecret());
            ossconfig.setbucketname(basedatautil.getsystemconfignacos().getfilesystemossbucket());
            ossconfig.setendpoint(basedatautil.getsystemconfignacos().getfilesystemossendpoint());
            list.addall(filestorageservicebuilder.buildaliyunossfilestorage(collections.singletonlist(ossconfig), null));
        } else if ("ftp".equals(this.filesystemtype)) {
            filestorageproperties.ftpconfig ftpconfig = new filestorageproperties.ftpconfig();
            ftpconfig.setplatform(this.filesystemtype);
//            ftpconfig.sethost("192.168.237.221");
//            ftpconfig.setport(21);
//            ftpconfig.setuser("");
//            ftpconfig.setpassword("123456");
            ftpconfig.sethost(basedatautil.getsystemconfignacos().getfilesystemftphost());
            ftpconfig.setport(integer.parseint(basedatautil.getsystemconfignacos().getfilesystemftpport()));
            ftpconfig.setuser(basedatautil.getsystemconfignacos().getfilesystemftpusername());
            ftpconfig.setpassword(basedatautil.getsystemconfignacos().getfilesystemftppassword());
            //ftpconfig.setroot(basedatautil.getsystemconfignacos().getfilesystemftproot()); //这个也没有 用上了调试
            //ftpconfig.setssl(basedatautil.getsystemconfignacos().getfilesystemftpssl());  //这个也没有 用上了调试
            list.addall(filestorageservicebuilder.buildftpfilestorage(collections.singletonlist(ftpconfig), null));
        }
        return this.filestorageservice;
        //下面这个是 不放list 直接修改默认的  参考 脱离 springboot 单独使用 文档
//        filestorageproperties properties = new filestorageproperties();
//        filestorageproperties.tencentcosconfig tencentcosconfig1 = new filestorageproperties.tencentcosconfig();
//        tencentcosconfig1.setplatform(this.filesystemtype);
//        tencentcosconfig1.setregion(basedatautil.getsystemconfignacos().getfilesystemqcloudregion());
//        tencentcosconfig1.setsecretid(basedatautil.getsystemconfignacos().getqcloudsecretid());
//        tencentcosconfig1.setsecretkey(basedatautil.getsystemconfignacos().getqcloudsecretkey());
//        tencentcosconfig1.setbucketname(basedatautil.getsystemconfignacos().getfilesystemqcloudbucket()+"-"+basedatautil.getsystemconfignacos().getqcloudappid());
//        properties.settencentcos(collections.singletonlist(tencentcosconfig1));
//        properties.setdefaultplatform(this.filesystemtype);
//        return  filestorageservicebuilder.create(properties).usedefault().build();
    }
}
第三步: 编写文件上传下载工具类
package com.xx.init.utils;
import com.xx.init.utils.basedatautil;
import com.xx.init.utils.httputil;
import com.xx.api.exception.xxruntimeexception;
import com.xx.init.filestorage.filestorageinit;
import org.apache.tika.tika;
import org.dromara.x.file.storage.core.fileinfo;
import org.dromara.x.file.storage.core.filestorageservice;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.beans.factory.annotation.value;
import org.springframework.http.httpheaders;
import org.springframework.http.responseentity;
import org.springframework.stereotype.component;
import org.springframework.util.objectutils;
import org.springframework.util.stringutils;
import org.springframework.web.multipart.multipartfile;
import java.io.file;
import java.io.ioexception;
import java.nio.file.path;
import java.nio.file.paths;
import java.util.*;
/**
 * user:json
 * date: 2024/4/12
 **/
@component
public class uploadhelper {
    @autowired
    filestorageinit filestorageinit;
    @value("${filestorage.${spring.profiles.active}}")
    private string filestoragehome;
    public filestorageservice init() {
        return filestorageinit.init(basedatautil.getsystemconfignacos().getfilesystemtype());
    }
    public string getfilesystemtype() {
        return filestorageinit.getfilesystemtype();
    }
    //上传文件
    public fileinfo uploadfile(multipartfile file, string filedir) {
        if (stringutils.isempty(filedir)) {
            filedir = "files";
        }
        if (file.isempty()) {
            throw new xxruntimeexception("请上传文件!");
        }
        // 获取文件的mime类型
        string mimetype = getmimetype(file);
        // 检查是否允许mime类型
        if (!isvalidmimetype(mimetype,true)) {
            throw new xxruntimeexception("文件类型不合法!");
        }
        return this.init()
                .of(file)
                .setplatform(getfilesystemtype())
                .setpath(generatefilepath(filedir))
                .setsavefilename(getfilename() + "." + getfileextensionwithdot(objects.requirenonnull(file.getoriginalfilename())))
                .upload();
    }
    //上传图片
    public fileinfo uploadimage(multipartfile file, string filedir) {
        if (stringutils.isempty(filedir)) {
            filedir = "images";
        }
        if (file.isempty()) {
            throw new xxruntimeexception("请上传图片!");
        }
        // 获取文件的mime类型
        string mimetype = getmimetype(file);
        // 检查是否允许mime类型
        if (!isvalidmimetype(mimetype,false)) {
            throw new xxruntimeexception("文件类型不合法!");
        }
        // string filename = generatefilename(filedir) + "." + getfileextensionwithdot(file.getname());
        return this.init()
                .of(file)
                .setplatform(getfilesystemtype())
                .setpath(generatefilepath(filedir))
                .setsavefilename(getfilename() + "." + getfileextensionwithdot(objects.requirenonnull(file.getoriginalfilename())))
                .upload();
    }
    //上传远程文件(服务应用内部调用,先下载再上传). 没测
    // fileurl 远程文件网址,folder 文件目录 ,extension 没有指定上传保存扩展名,通过链接获取
    public fileinfo uploadremotefile(string fileurl, string folder, string extension) {
        if (stringutils.isempty(folder)) {
            folder = "remote";
        }
        try {
            responseentity<string> responseentity = httputil.doget(fileurl, new httpheaders());
            if (responseentity.getstatuscodevalue() != 200) {
                throw new xxruntimeexception(string.format("文件下载失败(错误码%s)", responseentity.getstatuscodevalue()));
            }
            string filestr = responseentity.getbody();
            //没有指定上传保存扩展名,通过链接获取
            if (stringutils.isempty(extension)) {
                path path = paths.get(fileurl);
                string filename = path.getfilename().tostring();
                extension = getfileextensionwithdot(filename);
            }
            return this.init()
                    .of(filestr)
                    .setplatform(getfilesystemtype())
                    .setpath(generatefilepath(folder))
                    .setsavefilename(getfilename() + "." +extension)
                    .upload();
        } catch (exception e) {
            throw new xxruntimeexception("文件上传出错:" + e.getmessage());
        }
    }
    /**
     * 上传本地文件(服务应用内部调用).  没测
     * file /opt/www/runtime/doc/1640071827.docx
     * folder 文件目录
     */
    public fileinfo uploadlocalfile(string filepath, string folder, boolean unlink) {  //线上开启
        if (stringutils.isempty(folder)) {
            folder = "contract";
        }
        file file = new file(filepath);
        if (!file.exists()) {
            throw new xxruntimeexception("文件不存在");
        }
        path path = paths.get(filepath);
        string filename = path.getfilename().tostring();
        string extension = getfileextensionwithdot(filename);
        fileinfo upload = this.init()
                .of(file)
                .setplatform(getfilesystemtype())
                .setpath(generatefilepath(folder))
                .setsavefilename(getfilename() + "." +extension)
                .upload();
        if (!objectutils.isempty(upload) && unlink) {
            if (file.exists()) {
                if (file.delete()) {
                    // system.out.println("文件删除成功");
                } else {
                    throw new xxruntimeexception("文件删除失败!");
                }
            } else {
                //system.out.println("文件不存在,无需删除");
            }
        }
        return upload;
    }
    /**
     * 上传本地文件(服务应用内部调用).  没测
     * mixed $file doc/1640071827.docx
     * string $folder 文件目录
     */
    public fileinfo uploadlocalfilesystem(string filepath, string folder, boolean unlink) {  //线上开启
        if (stringutils.isempty(folder)) {
            folder = "contract";
        }
        file file = new file(filepath);
        if (!file.exists()) {
            throw new xxruntimeexception("文件不存在");
        }
        path path = paths.get(filepath);
        string filename = path.getfilename().tostring();
        string extension = getfileextensionwithdot(filename);
        fileinfo upload = this.init()
                .of(file)
                .setplatform(getfilesystemtype())
                .setpath(generatefilepath(folder))
                .setsavefilename(getfilename() + "." +extension)
                .upload();
        if (!objectutils.isempty(upload) && unlink) {
            if (file.exists()) {
                if (file.delete()) {
                    // system.out.println("文件删除成功");
                } else {
                    throw new xxruntimeexception("文件删除失败!");
                }
            } else {
                //system.out.println("文件不存在,无需删除");
            }
        }
        return upload;
    }
    /**
     * 下载云文件至本地(服务应用内部调用).
     */
    public fileinfo downloadfile(string file, string folder, string filename) {
        if (stringutils.isempty(folder)) {
            folder = "contract";
        }
        file = relativepath(file);
        if (stringutils.isempty(filename)) {
            path path = paths.get(file);
            string filename1 = path.getfilename().tostring();
            string extension = getfileextensionwithdot(filename1);
            filename=getfilename() + "." + extension;
        }
        string localfile = generatefilepath(folder) + filename;
        filestorageservice init = this.init();
        fileinfo fileinfobyurl = init.getfileinfobyurl(file);
        init.download(fileinfobyurl).file(filestoragehome+"/"+localfile);
        return fileinfobyurl;
    }
    //获取文件地址
    private string relativepath(string filepath) {
        list<string> domainsec = getdomainsec();
        string result="";
        if (filepath instanceof string) {
             result = filepath.replaceall(string.join("|", domainsec), "").replacefirst("^/", "");
        }
        return result;
    }
    private list<string>  relativepath(list<string> filepath) {
        list<string> domainsec = getdomainsec();
        if (filepath instanceof list<?> && domainsec.stream().allmatch(s -> s instanceof string)) {
            for (int i = 0; i < filepath.size(); i++) {
                filepath.set(i, filepath.get(i).replaceall(string.join("|", domainsec), "").replacefirst("^/", ""));
            }
       //     system.out.println(filepath);
        }
        return filepath;
    }
        private list<string> getdomainsec() {
        list<string> domainsec = new arraylist<>();
        if ("cos".equals(getfilesystemtype()) || stringutils.isempty(getfilesystemtype())) {
            domainsec.add("https://" + basedatautil.getsystemconfignacos().getfilesystemqcloudbucket() + "-" + basedatautil.getsystemconfignacos().getqcloudappid() + ".cos." + basedatautil.getsystemconfignacos().getfilesystemqcloudregion() + ".myqcloud.com");
            if (!stringutils.isempty(basedatautil.getsystemconfignacos().getfilesystemqclouddomain())) {
                domainsec.add("https://" + basedatautil.getsystemconfignacos().getfilesystemqclouddomain());
            }
        } else if ("minio".equals(getfilesystemtype()) || "s3".equals(getfilesystemtype())) {
            if (!stringutils.isempty(basedatautil.getsystemconfignacos().getfilesystems3endpoint())) {
                domainsec.add(basedatautil.getsystemconfignacos()
                        .getfilesystems3endpoint().trim().replace("/", "") + "/" +
                        basedatautil.getsystemconfignacos().getfilesystems3bucket());
            }
            if (!stringutils.isempty(basedatautil.getsystemconfignacos().getfilesystems3domain())) {
                domainsec.add(basedatautil.getsystemconfignacos().getfilesystems3domain().trim().replace("/", ""));
            }
        } else if ("oss".equals(getfilesystemtype())) {
            domainsec.add("https://" + basedatautil.getsystemconfignacos().getfilesystemossdomain());
        } else if ("ftp".equals(getfilesystemtype())) {
            domainsec.add(basedatautil.getsystemconfignacos().getfilesystemftpdomain().trim().replace("/", ""));
        }
        return domainsec;
    }
    private string getmimetype(multipartfile file) {
        try {
            tika tika = new tika();
            return tika.detect(file.getinputstream());
        } catch (ioexception e) {
            return "";
        }
    }
    //文件验证 isall true 全部验证  false 只验证图片
    private boolean isvalidmimetype(string mimetype,boolean isall) {
        if(isall){
            // 允许的mime类型列表
            string[] allowedmimetypes = {"image/png", "image/jpeg", "image/gif", "application/zip", "text/plain", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/pdf", "application/x-rar-compressed"};
            for (string allowedmimetype : allowedmimetypes) {
                if (allowedmimetype.equalsignorecase(mimetype)) {
                    return true;
                }
            }
            return false;
        }else{
            // 允许的mime类型列表
            string[] allowedmimetypes = {"image/png", "image/jpeg", "image/gif"};
            for (string allowedmimetype : allowedmimetypes) {
                if (allowedmimetype.equalsignorecase(mimetype)) {
                    return true;
                }
            }
            return false;
        }
    }
    //定义文件路径
    private string generatefilepath(string filedir) {
        // 'yyyymmdd'
        string currentdate = new java.text.simpledateformat("yyyymmdd").format(new date());
        //  file name
        string filename = "upload/" + filedir + "/" + currentdate + "/";
        return filename;
    }
    //随机文件名
    private string getfilename() {
        //  unique id
        string uniqueid = uuid.randomuuid().tostring();
        // 10000 and 99999
        int randomnum = (int) (math.random() * (99999 - 10000 + 1)) + 10000;
        return uniqueid + randomnum;
    }
    //获取文件后缀
    private string getfileextensionwithdot(string filename) {
        int dotindex = filename.lastindexof('.');
        if (dotindex > 0 && dotindex < filename.length() - 1) {
            return filename.substring(dotindex + 1);
        }
        return "";
    }
}
下面代码 是 这个插件 的 默认的增删改查 因为上传了 他需要保存 数据 下载的时候要取数据
 还包含 分片上传的表
 这两个表 按需可以修改 我这边就直接用官方提供的表
 对数据库的操作 就是 这里使用了 mybatis-plus 和 hutool 工具类
对应的官方这里
 
两个数据表
-- 这里使用的是 mysql
create table `file_detail`
(
    `id`                varchar(32)  not null comment '文件id',
    `url`               varchar(512) not null comment '文件访问地址',
    `size`              bigint(20)   default null comment '文件大小,单位字节',
    `filename`          varchar(256) default null comment '文件名称',
    `original_filename` varchar(256) default null comment '原始文件名',
    `base_path`         varchar(256) default null comment '基础存储路径',
    `path`              varchar(256) default null comment '存储路径',
    `ext`               varchar(32)  default null comment '文件扩展名',
    `content_type`      varchar(128) default null comment 'mime类型',
    `platform`          varchar(32)  default null comment '存储平台',
    `th_url`            varchar(512) default null comment '缩略图访问路径',
    `th_filename`       varchar(256) default null comment '缩略图名称',
    `th_size`           bigint(20)   default null comment '缩略图大小,单位字节',
    `th_content_type`   varchar(128) default null comment '缩略图mime类型',
    `object_id`         varchar(32)  default null comment '文件所属对象id',
    `object_type`       varchar(32)  default null comment '文件所属对象类型,例如用户头像,评价图片',
    `metadata`          text comment '文件元数据',
    `user_metadata`     text comment '文件用户元数据',
    `th_metadata`       text comment '缩略图元数据',
    `th_user_metadata`  text comment '缩略图用户元数据',
    `attr`              text comment '附加属性',
    `file_acl`          varchar(32)  default null comment '文件acl',
    `th_file_acl`       varchar(32)  default null comment '缩略图文件acl',
    `hash_info`         text comment '哈希信息',
    `upload_id`         varchar(128) default null comment '上传id,仅在手动分片上传时使用',
    `upload_status`     int(11)      default null comment '上传状态,仅在手动分片上传时使用,1:初始化完成,2:上传完成',
    `create_time`       datetime     default null comment '创建时间',
    primary key (`id`) using btree
) engine = innodb default charset = utf8 row_format = dynamic comment ='文件记录表';
create table `file_part_detail`
(
    `id`          varchar(32) not null comment '分片id',
    `platform`    varchar(32)  default null comment '存储平台',
    `upload_id`   varchar(128) default null comment '上传id,仅在手动分片上传时使用',
    `e_tag`       varchar(255) default null comment '分片 etag',
    `part_number` int(11)      default null comment '分片号。每一个上传的分片都有一个分片号,一般情况下取值范围是1~10000',
    `part_size`   bigint(20)   default null comment '文件大小,单位字节',
    `hash_info`   text character set utf8 comment '哈希信息',
    `create_time` datetime     default null comment '创建时间',
    primary key (`id`)
) engine = innodb default charset = utf8 comment ='文件分片信息表,仅在手动分片上传时使用';
建两个实体类
package com.xx.api.entities.files;
import com.baomidou.mybatisplus.annotation.idtype;
import com.baomidou.mybatisplus.annotation.tablefield;
import com.baomidou.mybatisplus.annotation.tableid;
import com.baomidou.mybatisplus.annotation.tablename;
import com.xx.api.entities.baseentity;
import io.swagger.annotations.apimodel;
import io.swagger.annotations.apimodelproperty;
import lombok.data;
import lombok.equalsandhashcode;
import lombok.experimental.accessors;
import java.time.localdatetime;
/**
 * <p>
 * 文件记录表
 * </p>
 *
 * @author json
 * @since 2024-04-15
 */
@data
@accessors(chain = true)
@tablename("file_detail")
@apimodel(value="filedetail对象", description="文件记录表")
public class filedetail{
    @tableid(value = "id", type = idtype.assign_id)
    private string id;
    @apimodelproperty(value = "文件访问地址")
    @tablefield("url")
    private string url;
    @apimodelproperty(value = "文件大小,单位字节")
    @tablefield("size")
    private long size;
    @apimodelproperty(value = "文件名称")
    @tablefield("filename")
    private string filename;
    @apimodelproperty(value = "原始文件名")
    @tablefield("original_filename")
    private string originalfilename;
    @apimodelproperty(value = "基础存储路径")
    @tablefield("base_path")
    private string basepath;
    @apimodelproperty(value = "存储路径")
    @tablefield("path")
    private string path;
    @apimodelproperty(value = "文件扩展名")
    @tablefield("ext")
    private string ext;
    @apimodelproperty(value = "mime类型")
    @tablefield("content_type")
    private string contenttype;
    @apimodelproperty(value = "存储平台")
    @tablefield("platform")
    private string platform;
    @apimodelproperty(value = "缩略图访问路径")
    @tablefield("th_url")
    private string thurl;
    @apimodelproperty(value = "缩略图名称")
    @tablefield("th_filename")
    private string thfilename;
    @apimodelproperty(value = "缩略图大小,单位字节")
    @tablefield("th_size")
    private long thsize;
    @apimodelproperty(value = "缩略图mime类型")
    @tablefield("th_content_type")
    private string thcontenttype;
    @apimodelproperty(value = "文件所属对象id")
    @tablefield("object_id")
    private string objectid;
    @apimodelproperty(value = "文件所属对象类型,例如用户头像,评价图片")
    @tablefield("object_type")
    private string objecttype;
    @apimodelproperty(value = "文件元数据")
    @tablefield("metadata")
    private string metadata;
    @apimodelproperty(value = "文件用户元数据")
    @tablefield("user_metadata")
    private string usermetadata;
    @apimodelproperty(value = "缩略图元数据")
    @tablefield("th_metadata")
    private string thmetadata;
    @apimodelproperty(value = "缩略图用户元数据")
    @tablefield("th_user_metadata")
    private string thusermetadata;
    @apimodelproperty(value = "附加属性")
    @tablefield("attr")
    private string attr;
    @apimodelproperty(value = "文件acl")
    @tablefield("file_acl")
    private string fileacl;
    @apimodelproperty(value = "缩略图文件acl")
    @tablefield("th_file_acl")
    private string thfileacl;
    @apimodelproperty(value = "哈希信息")
    @tablefield("hash_info")
    private string hashinfo;
    @apimodelproperty(value = "上传id,仅在手动分片上传时使用")
    @tablefield("upload_id")
    private string uploadid;
    @apimodelproperty(value = "上传状态,仅在手动分片上传时使用,1:初始化完成,2:上传完成")
    @tablefield("upload_status")
    private integer uploadstatus;
    @apimodelproperty(value = "创建时间")
    @tablefield("create_time")
    private localdatetime createtime;
    public static final string col_id = "id";
    public static final string col_url = "url";
    public static final string col_size = "size";
    public static final string col_filename = "filename";
    public static final string col_original_filename = "original_filename";
    public static final string col_base_path = "base_path";
    public static final string col_path = "path";
    public static final string col_ext = "ext";
    public static final string col_content_type = "content_type";
    public static final string col_platform = "platform";
    public static final string col_th_url = "th_url";
    public static final string col_th_filename = "th_filename";
    public static final string col_th_size = "th_size";
    public static final string col_th_content_type = "th_content_type";
    public static final string col_object_id = "object_id";
    public static final string col_object_type = "object_type";
    public static final string col_metadata = "metadata";
    public static final string col_user_metadata = "user_metadata";
    public static final string col_th_metadata = "th_metadata";
    public static final string col_th_user_metadata = "th_user_metadata";
    public static final string col_attr = "attr";
    public static final string col_hash_info = "hash_info";
    public static final string col_upload_id = "upload_id";
    public static final string col_upload_status = "upload_status";
    public static final string col_create_time = "create_time";
}
package com.xx.api.entities.files;
import com.baomidou.mybatisplus.annotation.idtype;
import com.baomidou.mybatisplus.annotation.tablefield;
import com.baomidou.mybatisplus.annotation.tableid;
import com.baomidou.mybatisplus.annotation.tablename;
import com.xx.api.entities.baseentity;
import io.swagger.annotations.apimodel;
import io.swagger.annotations.apimodelproperty;
import lombok.data;
import lombok.equalsandhashcode;
import lombok.experimental.accessors;
import java.time.localdatetime;
import java.util.date;
/**
 * <p>
 * 文件分片信息表,仅在手动分片上传时使用
 * </p>
 *
 * @author json
 * @since 2024-04-15
 */
@data
@accessors(chain = true)
@tablename("file_part_detail")
@apimodel(value="filepartdetail对象", description="文件分片信息表,仅在手动分片上传时使用")
public class filepartdetail  {
    @tableid(value = "id", type = idtype.assign_id)
    private string id;
    @apimodelproperty(value = "存储平台")
    @tablefield("platform")
    private string platform;
    @apimodelproperty(value = "上传id,仅在手动分片上传时使用")
    @tablefield("upload_id")
    private string uploadid;
    @apimodelproperty(value = "分片 etag")
    @tablefield("e_tag")
    private string etag;
    @apimodelproperty(value = "分片号。每一个上传的分片都有一个分片号,一般情况下取值范围是1~10000")
    @tablefield("part_number")
    private integer partnumber;
    @apimodelproperty(value = "文件大小,单位字节")
    @tablefield("part_size")
    private long partsize;
    @apimodelproperty(value = "哈希信息")
    @tablefield("hash_info")
    private string hashinfo;
    @apimodelproperty(value = "创建时间")
    @tablefield("create_time")
    private date createtime;
    public static final string col_id = "id";
    public static final string col_platform = "platform";
    public static final string col_upload_id = "upload_id";
    public static final string col_e_tag = "e_tag";
    public static final string col_part_number = "part_number";
    public static final string col_part_size = "part_size";
    public static final string col_hash_info = "hash_info";
    public static final string col_create_time = "create_time";
}
接口层 两个接口
public interface ifiledetailservice extends iservice<filedetail> {
}
public interface ifilepartdetailservice extends iservice<filepartdetail> {
}
mapper 层 也是两个接口
@mapper
public interface filedetailmapper extends basemapper<filedetail> {
}
@mapper
public interface filepartdetailmapper extends basemapper<filepartdetail> {
}
重点是 实现层的代码 当下载 和 上传后 会自动执行这里的代码
 因为 实现了 filerecorder 这个接口,把文件信息保存到数据库中。
 这个接口filerecorder
package com.xx.init.filestorage.impl;
import cn.hutool.core.bean.beanutil;
import cn.hutool.core.lang.dict;
import cn.hutool.core.util.strutil;
import com.baomidou.mybatisplus.core.conditions.query.querywrapper;
import com.baomidou.mybatisplus.extension.service.impl.serviceimpl;
import com.fasterxml.jackson.core.jsonprocessingexception;
import com.fasterxml.jackson.core.type.typereference;
import com.fasterxml.jackson.databind.objectmapper;
import java.util.map;
import com.xx.init.filestorage.impl.filepartdetailserviceimpl;
import com.xx.api.entities.files.filedetail;
import com.xx.api.exception.xxruntimeexception;
import com.xx.api.inteface.skeleton.ifiledetailservice;
import com.xx.init.filestorage.mapper.filedetailmapper;
import lombok.sneakythrows;
import org.dromara.x.file.storage.core.fileinfo;
import org.dromara.x.file.storage.core.hash.hashinfo;
import org.dromara.x.file.storage.core.recorder.filerecorder;
import org.dromara.x.file.storage.core.upload.filepartinfo;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.service;
import org.springframework.util.objectutils;
/**
 * 用来将文件上传记录保存到数据库,这里使用了 mybatis-plus 和 hutool 工具类
 */
@service
public class filedetailserviceimpl extends serviceimpl<filedetailmapper, filedetail> implements filerecorder, ifiledetailservice {
    private final objectmapper objectmapper = new objectmapper();
    @autowired
    private filepartdetailserviceimpl filepartdetailservice;
    /**
     * 保存文件信息到数据库
     */
    @sneakythrows
    @override
    public boolean save(fileinfo info) {
        filedetail detail = tofiledetail(info);
        boolean b = save(detail);
        if (b) {
            info.setid(detail.getid());
        }
        return b;
    }
    /**
     * 更新文件记录,可以根据文件 id 或 url 来更新文件记录,
     * 主要用在手动分片上传文件-完成上传,作用是更新文件信息
     */
    @sneakythrows
    @override
    public void update(fileinfo info) {
        filedetail detail = tofiledetail(info);
        querywrapper<filedetail> qw = new querywrapper<filedetail>()
                .eq(detail.geturl() != null, filedetail.col_url, detail.geturl())
                .eq(detail.getid() != null, filedetail.col_id, detail.getid());
        update(detail, qw);
    }
    /**
     * 根据 url 查询文件信息
     */
    @sneakythrows
    @override
    public fileinfo getbyurl(string url) {
        filedetail one = getone(new querywrapper<filedetail>().eq(filedetail.col_url, url));
        if(objectutils.isempty(one)){
            throw new xxruntimeexception("未查询到文件记录!下载失败!");
        }
        return tofileinfo(one);
    }
    /**
     * 根据 url 删除文件信息
     */
    @override
    public boolean delete(string url) {
        remove(new querywrapper<filedetail>().eq(filedetail.col_url, url));
        return true;
    }
    /**
     * 保存文件分片信息
     * @param filepartinfo 文件分片信息
     */
    @override
    public void savefilepart(filepartinfo filepartinfo) {
        filepartdetailservice.savefilepart(filepartinfo);
    }
    /**
     * 删除文件分片信息
     */
    @override
    public void deletefilepartbyuploadid(string uploadid) {
        filepartdetailservice.deletefilepartbyuploadid(uploadid);
    }
    /**
     * 将 fileinfo 转为 filedetail
     */
    public filedetail tofiledetail(fileinfo info) throws jsonprocessingexception {
        filedetail detail = beanutil.copyproperties(
                info, filedetail.class, "metadata", "usermetadata", "thmetadata", "thusermetadata", "attr", "hashinfo");
        // 这里手动获 元数据 并转成 json 字符串,方便存储在数据库中
        detail.setmetadata(valuetojson(info.getmetadata()));
        detail.setusermetadata(valuetojson(info.getusermetadata()));
        detail.setthmetadata(valuetojson(info.getthmetadata()));
        detail.setthusermetadata(valuetojson(info.getthusermetadata()));
        // 这里手动获 取附加属性字典 并转成 json 字符串,方便存储在数据库中
        detail.setattr(valuetojson(info.getattr()));
        // 这里手动获 哈希信息 并转成 json 字符串,方便存储在数据库中
        detail.sethashinfo(valuetojson(info.gethashinfo()));
        return detail;
    }
    /**
     * 将 filedetail 转为 fileinfo
     */
    public fileinfo tofileinfo(filedetail detail) throws jsonprocessingexception {
        fileinfo info = beanutil.copyproperties(
                detail, fileinfo.class, "metadata", "usermetadata", "thmetadata", "thusermetadata", "attr", "hashinfo");
        // 这里手动获取数据库中的 json 字符串 并转成 元数据,方便使用
        info.setmetadata(jsontometadata(detail.getmetadata()));
        info.setusermetadata(jsontometadata(detail.getusermetadata()));
        info.setthmetadata(jsontometadata(detail.getthmetadata()));
        info.setthusermetadata(jsontometadata(detail.getthusermetadata()));
        // 这里手动获取数据库中的 json 字符串 并转成 附加属性字典,方便使用
        info.setattr(jsontodict(detail.getattr()));
        // 这里手动获取数据库中的 json 字符串 并转成 哈希信息,方便使用
        info.sethashinfo(jsontohashinfo(detail.gethashinfo()));
        return info;
    }
    /**
     * 将指定值转换成 json 字符串
     */
    public string valuetojson(object value) throws jsonprocessingexception {
        if (value == null) return null;
        return objectmapper.writevalueasstring(value);
    }
    /**
     * 将 json 字符串转换成元数据对象
     */
    public map<string, string> jsontometadata(string json) throws jsonprocessingexception {
        if (strutil.isblank(json)) return null;
        return objectmapper.readvalue(json, new typereference<map<string, string>>() {});
    }
    /**
     * 将 json 字符串转换成字典对象
     */
    public dict jsontodict(string json) throws jsonprocessingexception {
        if (strutil.isblank(json)) return null;
        return objectmapper.readvalue(json, dict.class);
    }
    /**
     * 将 json 字符串转换成哈希信息对象
     */
    public hashinfo jsontohashinfo(string json) throws jsonprocessingexception {
        if (strutil.isblank(json)) return null;
        return objectmapper.readvalue(json, hashinfo.class);
    }
}
package com.xx.init.filestorage.impl;
import com.baomidou.mybatisplus.core.conditions.query.querywrapper;
import com.baomidou.mybatisplus.extension.service.impl.serviceimpl;
import com.fasterxml.jackson.core.jsonprocessingexception;
import com.fasterxml.jackson.databind.objectmapper;
import com.xx.api.entities.files.filepartdetail;
import com.xx.api.inteface.skeleton.ifilepartdetailservice;
import com.xx.init.filestorage.mapper.filepartdetailmapper;
import lombok.sneakythrows;
import org.dromara.x.file.storage.core.upload.filepartinfo;
import org.springframework.stereotype.service;
/**
 * 用来将文件分片上传记录保存到数据库,这里使用了 mybatis-plus 和 hutool 工具类
 * 目前仅手动分片分片上传时使用
 */
@service
public class filepartdetailserviceimpl extends serviceimpl<filepartdetailmapper, filepartdetail> implements ifilepartdetailservice {
    private final objectmapper objectmapper = new objectmapper();
    /**
     * 保存文件分片信息
     * @param info 文件分片信息
     */
    @sneakythrows
    public void savefilepart(filepartinfo info) {
        filepartdetail detail = tofilepartdetail(info);
        if (save(detail)) {
            info.setid(detail.getid());
        }
    }
    /**
     * 删除文件分片信息
     */
    public void deletefilepartbyuploadid(string uploadid) {
        remove(new querywrapper<filepartdetail>().eq(filepartdetail.col_upload_id, uploadid));
    }
    /**
     * 将 filepartinfo 转成 filepartdetail
     * @param info 文件分片信息
     */
    public filepartdetail tofilepartdetail(filepartinfo info) throws jsonprocessingexception {
        filepartdetail detail = new filepartdetail();
        detail.setplatform(info.getplatform());
        detail.setuploadid(info.getuploadid());
        detail.setetag(info.getetag());
        detail.setpartnumber(info.getpartnumber());
        detail.setpartsize(info.getpartsize());
        detail.sethashinfo(valuetojson(info.gethashinfo()));
        detail.setcreatetime(info.getcreatetime());
        return detail;
    }
    /**
     * 将指定值转换成 json 字符串
     */
    public string valuetojson(object value) throws jsonprocessingexception {
        if (value == null) return null;
        return objectmapper.writevalueasstring(value);
    }
}
测试:
  @postmapping("index12")
    @apioperation(value = "文件上传 测试")
    @passtoken
    public r index12(@requestbody multipartfile file){
        fileinfo fileinfo = uploadhelper.uploadfile(file, "jsontest");
        system.out.println(fileinfo);
        return r.success();
    }
    @postmapping("index13")
    @apioperation(value = "文件下载 测试")
    @passtoken
    public r index13(){
        fileinfo fileinfo = uploadhelper.downloadfile("/upload/jsontest/20240415/661d14915a772807e8dd1f89.xls", "jsontest","测试.xls");
        system.out.println(fileinfo);
        return r.success();
    }
最后启动类上 不要忘记打注解
@enablefilestorage
如果 附件的增删改查 扫不到包 需要使用 @mapperscan 注解 指定位置
补充:如果附件不想下载到本地 也可以读取 数据流 显示 比如 img 标签图片显示 这段代码是后补充的
 没有穿插上上面
 具体操作可以看官方文档
https://x-file-storage.xuyanwu.cn/#/%e5%9f%ba%e7%a1%80%e5%8a%9f%e8%83%bd?id=%e4%b8%8b%e8%bd%bd
  public downloader downloadfile(string file) {
        filestorageservice init = this.init();
        file = relativepath(file);
        fileinfo fileinfobyurl = init.getfileinfobyurl(file);
  
       return init.download(fileinfobyurl);
    }
    
 public void download(string fileurl, httpservletresponse response) {
        try {
            response.setcontenttype("application/force-download");// 设置强制下载不打开
            response.addheader("content-disposition", "attachment;filename=" + new string(fileurl.getbytes("utf-8"), "iso-8859-1"));
            downloader  downloader=downloadfile(fileurl);
            downloader  downloader=filesutil.downloadfile(fileurl);
            downloader.outputstream(response.getoutputstream());
            response.flushbuffer();
        } catch (exception e) {
            log.error("文件下载失败: " + e.getmessage());
        }
    }
完美收工!!!
 兄弟们
 配置文件 :
 filestorage:
 dev: d:\home
 test: /home
 prd: /home
大文件上传 分片上传 参考下面博客
 
             我要评论
我要评论![[ 云计算 | AWS 实践 ] Java 如何重命名 Amazon S3 中的文件和文件夹](https://images.3wcode.com/3wcode/20240802/s_0_202408020028483641.png) 
                                             
                                            
发表评论