Java之MinIO存储桶和对象API使用

本文介绍了如何在Maven项目中集成Minio服务,包括引入依赖、初始化Minio客户端以及执行存储桶的基本操作,如检查、创建、查询和删除。此外,还详细讲解了对象的上传、下载、删除等操作,提供了相关代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

环境搭建,创建一个 maven项目,引入依赖:

    <!-- minio依赖-->
    <dependency>
      <groupId>io.minio</groupId>
      <artifactId>minio</artifactId>
      <version>8.3.3</version>
    </dependency>
    <!-- 官方 miniodemo需要的依赖-->
    <dependency>
      <groupId>me.tongfei</groupId>
      <artifactId>progressbar</artifactId>
      <version>0.7.4</version>
    </dependency>
    
    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>4.9.2</version>
    </dependency>

注意:这里我把 官方demo 放到了我的项目中,所以必须引入 progressbar依赖。否则忽略它。

初始化Minio客户端

注意:
新版 MinIO 采用 Builder构建者模式来构造 MinioClient对象。
API端口和 访问控制台端口不要搞混了

    // 初始化Minio客户端
    MinioClient minioClient = MinioClient.builder()
            .endpoint("http://xxx.xxx.xxx.xxx:9000/")
            .credentials("admin", "xxx")
            .build();
    System.out.println(minioClient);

配置MinIO的yml

server:
  port: 8089

spring:
  servlet:
    multipart:
      max-request-size: 200MB
      max-file-size: 200MB

minio:
  url: http://127.0.0.1:19000 #换成自己的minio服务端地址
  accessKey: admin# 用户名
  secretKey: 12345678# 密码
  bucketName: otest # bucketName指的就是之前创建的MinIO桶Bucket

配置类Config

@Data
@Configuration
public class MinioConfig {
 
    /**
     * 访问地址
     */
    @Value("${minio.url}")
    private String endpoint;
 
    /**
     * accessKey类似于用户ID,用于唯一标识你的账户
     */
    @Value("${minio.accessKey}")
    private String accessKey;
 
    /**
     * secretKey是你账户的密码
     */
    @Value("${minio.secretKey}")
    private String secretKey;
 
    /**
     * 默认存储桶
     */
    @Value("${minio.bucketName}")
    private String bucketName;
 
    @Bean
    public MinioClient minioClient() {
        MinioClient minioClient = MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
        return minioClient;
    }
}

MinIO工具类

/**
 * MinIO工具类
 *
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class MinioUtils {
 
    private final MinioClient minioClient;
 
    /******************************  Operate Bucket Start  ******************************/
 
    /**
     * 启动SpringBoot容器的时候初始化Bucket
     * 如果没有Bucket则创建
     *
     * @param bucketName
     */
    @SneakyThrows(Exception.class)
    private void createBucket(String bucketName) {
        if (!bucketExists(bucketName)) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }
 
    /**
     * 判断Bucket是否存在,true:存在,false:不存在
     *
     * @param bucketName
     * @return
     */
    @SneakyThrows(Exception.class)
    public boolean bucketExists(String bucketName) {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }
 
    /**
     * 获得Bucket的策略
     *
     * @param bucketName
     * @return
     */
    @SneakyThrows(Exception.class)
    public String getBucketPolicy(String bucketName) {
        return minioClient.getBucketPolicy(GetBucketPolicyArgs
                .builder()
                .bucket(bucketName)
                .build());
    }
 
    /**
     * 获得所有Bucket列表
     *
     * @return
     */
    @SneakyThrows(Exception.class)
    public List<Bucket> getAllBuckets() {
        return minioClient.listBuckets();
    }
 
    /**
     * 根据bucketName获取其相关信息
     *
     * @param bucketName
     * @return
     */
    @SneakyThrows(Exception.class)
    public Optional<Bucket> getBucket(String bucketName) {
        return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
    }
 
    /**
     * 根据bucketName删除Bucket,true:删除成功; false:删除失败,文件或已不存在
     *
     * @param bucketName
     * @throws Exception
     */
    @SneakyThrows(Exception.class)
    public void removeBucket(String bucketName) {
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }
 
    /******************************  Operate Bucket End  ******************************/
 
 
    /******************************  Operate Files Start  ******************************/
 
    /**
     * 判断文件是否存在
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    public boolean isObjectExist(String bucketName, String objectName) {
        boolean exist = true;
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            log.error("[Minio工具类]>>>> 判断文件是否存在, 异常:", e);
            exist = false;
        }
        return exist;
    }
 
    /**
     * 判断文件夹是否存在
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    public boolean isFolderExist(String bucketName, String objectName) {
        boolean exist = false;
        try {
            Iterable<Result<Item>> results = minioClient.listObjects(
                    ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());
            for (Result<Item> result : results) {
                Item item = result.get();
                if (item.isDir() && objectName.equals(item.objectName())) {
                    exist = true;
                }
            }
        } catch (Exception e) {
            log.error("[Minio工具类]>>>> 判断文件夹是否存在,异常:", e);
            exist = false;
        }
        return exist;
    }
 
    /**
     * 根据文件前置查询文件
     *
     * @param bucketName 存储桶
     * @param prefix     前缀
     * @param recursive  是否使用递归查询
     * @return MinioItem 列表
     */
    @SneakyThrows(Exception.class)
    public List<Item> getAllObjectsByPrefix(String bucketName,
                                            String prefix,
                                            boolean recursive) {
        List<Item> list = new ArrayList<>();
        Iterable<Result<Item>> objectsIterator = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());
        if (objectsIterator != null) {
            for (Result<Item> o : objectsIterator) {
                Item item = o.get();
                list.add(item);
            }
        }
        return list;
    }
 
    /**
     * 获取文件流
     *
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @return 二进制流
     */
    @SneakyThrows(Exception.class)
    public InputStream getObject(String bucketName, String objectName) {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
    }
 
    /**
     * 断点下载
     *
     * @param bucketName 存储桶
     * @param objectName 文件名称
     * @param offset     起始字节的位置
     * @param length     要读取的长度
     * @return 二进制流
     */
    @SneakyThrows(Exception.class)
    public InputStream getObject(String bucketName, String objectName, long offset, long length) {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .offset(offset)
                        .length(length)
                        .build());
    }
 
    /**
     * 获取路径下文件列表
     *
     * @param bucketName 存储桶
     * @param prefix     文件名称
     * @param recursive  是否递归查找,false:模拟文件夹结构查找
     * @return 二进制流
     */
    public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) {
        return minioClient.listObjects(
                ListObjectsArgs.builder()
                        .bucket(bucketName)
                        .prefix(prefix)
                        .recursive(recursive)
                        .build());
    }
 
    /**
     * 使用MultipartFile进行文件上传
     *
     * @param bucketName  存储桶
     * @param file        文件名
     * @param objectName  对象名
     * @param contentType 类型
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) {
        InputStream inputStream = file.getInputStream();
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .contentType(contentType)
                        .stream(inputStream, inputStream.available(), -1)
                        .build());
    }
 
    /**
     * 图片上传
     * @param bucketName
     * @param imageBase64
     * @param imageName
     * @return
     */
    public ObjectWriteResponse uploadImage(String bucketName, String imageBase64, String imageName) {
        if (!StringUtils.isEmpty(imageBase64)) {
            InputStream in = base64ToInputStream(imageBase64);
            String newName = System.currentTimeMillis() + "_" + imageName + ".jpg";
            String year = String.valueOf(new Date().getYear());
            String month = String.valueOf(new Date().getMonth());
            return uploadFile(bucketName, year + "/" + month + "/" + newName, in);
 
        }
        return null;
    }
// BASE64Decoder在jdk8以上的版本移除了,报错最简单解决换成jdk8就行了
    public static InputStream base64ToInputStream(String base64) {
        ByteArrayInputStream stream = null;
        try {
            byte[] bytes = new BASE64Decoder().decodeBuffer(base64.trim());
            stream = new ByteArrayInputStream(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return stream;
    }
 
 
    /**
     * 上传本地文件
     *
     * @param bucketName 存储桶
     * @param objectName 对象名称
     * @param fileName   本地文件路径
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, String objectName, String fileName) {
        return minioClient.uploadObject(
                UploadObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .filename(fileName)
                        .build());
    }
 
    /**
     * 通过流上传文件
     *
     * @param bucketName  存储桶
     * @param objectName  文件对象
     * @param inputStream 文件流
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) {
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(inputStream, inputStream.available(), -1)
                        .build());
    }
 
    /**
     * 创建文件夹或目录
     *
     * @param bucketName 存储桶
     * @param objectName 目录路径
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse createDir(String bucketName, String objectName) {
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(new ByteArrayInputStream(new byte[]{}), 0, -1)
                        .build());
    }
 
    /**
     * 获取文件信息, 如果抛出异常则说明文件不存在
     *
     * @param bucketName 存储桶
     * @param objectName 文件名称
     * @return
     */
    @SneakyThrows(Exception.class)
    public String getFileStatusInfo(String bucketName, String objectName) {
        return minioClient.statObject(
                StatObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build()).toString();
    }
 
    /**
     * 拷贝文件
     *
     * @param bucketName    存储桶
     * @param objectName    文件名
     * @param srcBucketName 目标存储桶
     * @param srcObjectName 目标文件名
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName) {
        return minioClient.copyObject(
                CopyObjectArgs.builder()
                        .source(CopySource.builder().bucket(bucketName).object(objectName).build())
                        .bucket(srcBucketName)
                        .object(srcObjectName)
                        .build());
    }
 
    /**
     * 删除文件
     *
     * @param bucketName 存储桶
     * @param objectName 文件名称
     */
    @SneakyThrows(Exception.class)
    public void removeFile(String bucketName, String objectName) {
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
    }
 
    /**
     * 批量删除文件
     *
     * @param bucketName 存储桶
     * @param keys       需要删除的文件列表
     * @return
     */
    public void removeFiles(String bucketName, List<String> keys) {
        List<DeleteObject> objects = new LinkedList<>();
        keys.forEach(s -> {
            objects.add(new DeleteObject(s));
            try {
                removeFile(bucketName, s);
            } catch (Exception e) {
                log.error("[Minio工具类]>>>> 批量删除文件,异常:", e);
            }
        });
    }
 
    /**
     * 获取文件外链
     *
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @param expires    过期时间 <=7 秒 (外链有效时间(单位:秒))
     * @return url
     */
    @SneakyThrows(Exception.class)
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build();
        return minioClient.getPresignedObjectUrl(args);
    }
 
    /**
     * 获得文件外链
     *
     * @param bucketName
     * @param objectName
     * @return url
     */
    @SneakyThrows(Exception.class)
    public String getPresignedObjectUrl(String bucketName, String objectName) {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .method(Method.GET).build();
        return minioClient.getPresignedObjectUrl(args);
    }
 
    /**
     * 将URLDecoder编码转成UTF8
     *
     * @param str
     * @return
     * @throws UnsupportedEncodingException
     */
    public String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
        String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
        return URLDecoder.decode(url, "UTF-8");
    }
}

创建Controller

@Slf4j
@RestController
@RequestMapping("/oss")
public class OSSController {
 
    @Autowired
    private MinioUtils minioUtils;
 
    @Autowired
    private MinioConfig minioConfig;
 
    /**
     * 文件上传
     *
     * @param file
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) {
        try {
            //文件名
            String fileName = file.getOriginalFilename();
            String newFileName = System.currentTimeMillis() + "." + StringUtils.substringAfterLast(fileName, ".");
            //类型
            String contentType = file.getContentType();
            minioUtils.uploadFile(minioConfig.getBucketName(), file, newFileName, contentType);
            return "上传成功";
        } catch (Exception e) {
            log.error("上传失败");
            return "上传失败";
        }
    }
 
    /**
     * 删除
     *
     * @param fileName
     */
    @DeleteMapping("/")
    public void delete(@RequestParam("fileName") String fileName) {
        minioUtils.removeFile(minioConfig.getBucketName(), fileName);
    }
 
    /**
     * 获取文件信息
     *
     * @param fileName
     * @return
     */
    @GetMapping("/info")
    public String getFileStatusInfo(@RequestParam("fileName") String fileName) {
        return minioUtils.getFileStatusInfo(minioConfig.getBucketName(), fileName);
    }
 
    /**
     * 获取文件外链
     *
     * @param fileName
     * @return
     */
    @GetMapping("/url")
    public String getPresignedObjectUrl(@RequestParam("fileName") String fileName) {
        return minioUtils.getPresignedObjectUrl(minioConfig.getBucketName(), fileName);
    }
 
    /**
     * 文件下载
     *
     * @param fileName
     * @param response
     */
    @GetMapping("/download")
    public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
        try {
            InputStream fileInputStream = minioUtils.getObject(minioConfig.getBucketName(), fileName);
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            response.setContentType("application/force-download");
            response.setCharacterEncoding("UTF-8");
            IOUtils.copy(fileInputStream, response.getOutputStream());
        } catch (Exception e) {
            log.error("下载失败");
        }
    }
 
}

获取到 MinioClient对象,就可以进行 MinIO的 API操作使用了。

存储桶基本使用

1、检查存储桶是否存在
boolean bucketExists(BucketExistsArgs args):检查存储桶是否存在。

BucketExistsArgs btest2 = BucketExistsArgs.builder().bucket("btest2").build();
boolean existFlag = minioClient.bucketExists(btest2);

2、创建存储桶
public void makeBucket(MakeBucketArgs args):创建一个启用给定区域和对象锁定功能的存储桶。
示例1:存储桶不存在则创建

String bucketName = "java.minio.demo";
// 存储桶不存在则创建
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
System.out.printf("%s,创建成功", bucketName);
}else{
System.out.printf("%s,已存在", bucketName);
}

3、查询存储桶
3.1 查询所有桶的列表信息
public List listBuckets():列出所有桶的桶信息。
示例:

List<Bucket> bucketList = minioClient.listBuckets();
bucketList.forEach(bucket -> {
System.out.printf("存储桶名:%s,创建时间:%s ", bucket.name(), bucket.creationDate());
});

注意:桶的创建时间默认是美国时间,创建桶时我们可以指定桶的时区或者设置 MinIO服务器时区。

3.2查询所有桶的列表信息

Iterable<Result<Item>> otatest = minioClient.listObjects(
                ListObjectsArgs.builder().bucket("otatest").build());
        otatest.forEach(v->{
            try {
                //文件名
                System.out.println(v.get().objectName());
                System.out.println(v.get().userMetadata());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

4、删除存储桶
public void removeBucket(RemoveBucketArgs args):删除一个空桶。
示例:

String bucketName2 = "btest3";
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName2).build());

对象基本使用

1、上传对象
1.1 PutObject方法
public ObjectWriteResponse putObject(PutObjectArgs args):将给定的流上传为存储桶中的对象。
示例1,InputStream上传:

        String bucketName = "java.minio.demo";
        // 创建InputStream上传
        File file = new File("D:\Files\demo.jpg");
        InputStream inputStream = new FileInputStream(file);

        long start = System.currentTimeMillis();
        // 上传流
        minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object("2023/02/25/" + file.getName()).stream(inputStream, inputStream.available(), -1)
                        .build());
        inputStream.close();
        System.out.println("uploaded successfully 耗时:" + (System.currentTimeMillis() - start));

注意:
添加的Object大小不能超过 5GB。
默认情况下,如果已存在同名Object且对该Object有访问权限,则新添加的Object将覆盖原有的Object,并返回 200 OK。
OSS没有文件夹的概念,所有资源都是以文件来存储,但您可以通过创建一个以正斜线(/)结尾,大小为 0的Object来创建模拟文件夹(指定 /后,默认会自动创建)

1.2 uploadObject方法
public void uploadObject(UploadObjectArgs args):将文件中的内容作为存储桶中的对象上传。
不太常用,一般适合上传磁盘文件(mc cp命令更方便)。
示例:

        String bucketName = "java.minio.demo";
        
        long start = System.currentTimeMillis();
        // 上传文件
        minioClient.uploadObject(
                UploadObjectArgs.builder()
                        .bucket(bucketName)
                        .object("2023/02/25/demo2.jpg")
                        .filename("D:\Files\demo.jpg")
                        .build());
        System.out.println("uploaded successfully 耗时:" + (System.currentTimeMillis() - start));

获取对象

2.1 getObject方法
public GetObjectResponse getObject(GetObjectArgs args):获取对象的数据。
示例:

		String bucketName = "java.minio.demo";

		// GetObjectResponse 继承了 InputStream类
		GetObjectResponse objectResponse = minioClient.getObject(GetObjectArgs.builder()
				.bucket(bucketName)
				.object("2023/02/25/demo2.jpg")
				.build());
		System.out.println(objectResponse.bucket());
		System.out.println(objectResponse.object());
		byte[] allBytes = objectResponse.readAllBytes();
		System.out.println(allBytes);
		
		// Close the input stream.
		objectResponse.close();

返回数据通过流的方式

@GetMapping("/multipart/test")
    public void test(HttpServletResponse response) {
GetObjectResponse objectResponse = minioClient.getObject(GetObjectArgs.builder()
                .bucket(bucketName)
                .object("订餐.xlsx")
                .build());
//        return new Result().ok(objectResponse);
//        return  new ResponseEntity<>(objectResponse, HttpStatus.OK);
        ServletOutputStream outputStream = response.getOutputStream();
//        PrintWriter writer = response.getWriter();
        byte[] buffer = new byte[1024];

        int len;
        //从输入流中读取一定数量的字节,并将其存储在缓冲区字节数组中,读到末尾返回-1
        while ((len = objectResponse.read(buffer)) > 0) {
            outputStream.write(buffer, 0, len);
//            writer.write(new String(buffer));
        }
        objectResponse.close();
        outputStream.close();

    }
 }

注意:
返回结果是 GetObjectResponse类,它继承了 InputStream类。
GetObjectResponse使用后返回必须关闭以释放网络资源。
此操作需要对此Object具有读权限

downloadObject方法

public void downloadObject(DownloadObjectArgs args):将对象的数据下载到磁盘

    String bucketName = "java.minio.demo";

    minioClient.downloadObject(DownloadObjectArgs.builder()
            .bucket(bucketName)
            .object("2023/02/25/demo2.jpg")
            .filename("D:\Files\demo.jpg") // 必须指定文件名
            .build());

getPresignedObjectUrl方法

public String getPresignedObjectUrl(GetPresignedObjectUrlArgs args):获取 HTTP 方法、到期时间和自定义请求参数的对象的预签名 URL。
示例:返回获取文件对象的URL GET请求,此 URL过期时间为一分钟。

        String bucketName = "java.minio.demo";

        String objectUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                .bucket(bucketName)
                .object("2023/02/25/demo2.jpg")
                .method(Method.GET)
                //.expiry(60) // 单位秒
                .expiry(30, TimeUnit.SECONDS)
                .build());
        System.out.println(objectUrl);

删除对象

4.1 removeObject方法
public void removeObject(RemoveObjectArgs args) :移除一个对象。

       String bucketName = "java.minio.demo";

        minioClient.removeObject(RemoveObjectArgs.builder()
                .bucket(bucketName)
                .object("2023/02/25/demo2.jpg")
                //.versionId("my-versionid") //还可以删除指定版本号的对象
                .build());

removeObjects方法

public Iterable removeObjects(RemoveObjectsArgs args):懒惰地删除多个对象。它需要迭代返回的 Iterable 以执行删除。
示例:

        String bucketName = "java.minio.demo";
        
        // 构建需要删除的对象
        List<DeleteObject> objects = new LinkedList<>();
        objects.add(new DeleteObject("2023/02/25/demo1.jpg"));
        objects.add(new DeleteObject("2023/02/25/demo2.jpg"));
        objects.add(new DeleteObject("2023/02/25/demo3.jpg"));
        // 删除
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder()
                .bucket(bucketName)
                .objects(objects)
                .build());
        for (Result<DeleteError> result : results) {
            // 删除文件不存在时,不会报错
            DeleteError error = result.get();
            System.out.println("Error in deleting object " + error.objectName() + "; " + error.message());
        }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值