IO流复习

IO流

  • 程序到文件,文件到程序
  • 分为输出流和输入流
  • 流分为字节流,字符流
    字节流可以操作所有类型文件,字符流只能操作文本文件(可以用windows记事本打开并且能正常读懂的文件)
  • 俩都有.read()``.write()``.read(byte[] bytes)等方法,空参的返回值是文件字节,有参返回的是指针移动了多少位,一般受到数组长短的影响,有参无参有个相同点就是指针走到末尾了,就会返回-1
    在这里插入图片描述
    流的结构图:只有下面接口的实现类才可以创建对象,如FileOutStream(文件字节输出流)
    在这里插入图片描述

字节流: 一般不用于读入写出中文

从下图更清晰的知道,再上面接口名字的基础上添加前缀就可以形成新的流(这里还有一些流没有展示,但是命名规则都是按照上面这句话来)
在这里插入图片描述

创建一个文件字节输出流:

package test;

import java.io.FileOutputStream;
import java.io.IOException;

public class Test01 {
    public static void main(String[] args) throws IOException {
        FileOutputStream outputStream=new FileOutputStream("a.txt");
        //下面的数字走的是阿斯克码值
        outputStream.write(97);
        outputStream.close();
    }
}

关于创建上面这个流的细节值得念叨一下:

细节1:和上面的创建有点不一样欧,但是上面创建方式的底层其实就下面这货

FileOutputStream outputStream=new FileOutputStream(new File("a.txt"));

细节2:注意是父类,不是根目录。在这里可以通俗点看成路径名中的倒数第二位,如c/b/a.txtb是父类,而c是根目录位置
细节3:就是会覆盖掉之前的内容

在这里插入图片描述

字节输出流的三种方式:
在这里插入图片描述
第一种:直接放
第二种:

byte[] bytes={97,98,99};
        //走的数组
        outputStream.write(bytes);

第三种:

byte[] bytes={96,97,98,99,100};
        //参数一:数组,参数二:从哪个下标开始,
        //参数三:显示多少个元素
        outputStream.write(bytes,3,2);

输出字符串: 将String类型数据装进数组中,进行输出到文件中
换行: 换行操作,将String类型\r\n装进数组

package test;

import java.io.FileOutputStream;
import java.io.IOException;

public class Test04 {
    public static void main(String[] args) throws IOException {
        //将转义字符装进数组中
        String br="\r\n";
        byte[] bytes1=br.getBytes();
        //将String类型装进数组中
        String arg="daliwang";
        byte[] bytes=arg.getBytes();
        //打开字节流
        FileOutputStream outputStream=new FileOutputStream("d.txt");
        outputStream.write(bytes1);
        outputStream.write(bytes);
        outputStream.close();
    }
}

续写: 接着文件中已经有的内容进行续写,就是不删除原来的内容,给创建字节流的时候添加参数

 		//第二个参数添加true,表示续写
        FileOutputStream outputStream=new FileOutputStream("d.txt",true);
        outputStream.write(bytes1);
        outputStream.write(bytes);
        outputStream.close();

文件输入字节流:

将数据读取出来,.read()的返回值是int类型的对应文件中的阿斯克码值,一次只会读一个字节(对于其他类型的文件也生效,如.mp4),读一次指针移动一次,数据读完了,想要再读,就会返回-1(别的情况就正常返回应当的数),想循环读取就加个循环呗:

package test;

import java.io.FileInputStream;
import java.io.IOException;

public class Test05 {
    public static void main(String[] args) throws IOException {
        FileInputStream inputStream=new FileInputStream("d.txt");
        int b;
        while((b=inputStream.read())!=-1){
            System.out.println(b);
        }
    }
}

文件拷贝:
将文件读取一下,再将其写出(下面的是.mp3文件,可知读出时返回的值不是简单的int类型数字,而是当前文件的每一个字节对应的数字,后面再读入将这些字节转换成回文件本来的类型)

package test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test06 {
    public static void main(String[] args) throws IOException {
        FileInputStream inputStream=new FileInputStream("C:\\Users\\李家院刘俊辉\\Desktop\\老歌共享\\多年以后我还能不能活着.mp3");
        FileOutputStream outputStream=new FileOutputStream("e.mp3");
        int b;
        while((b=inputStream.read())!=-1){
            outputStream.write(b);
        }
        outputStream.close();
        inputStream.close();
    }
}

当文件比较大时,会减缓速度,于是加个数组(读到字节数据会放入数组),从文件中读取了一定的量(和数组长度有关),返回值和指针移动有关,再进行输出到新文件中:看注释,答案都在里面

package test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test07 {
    public static void main(String[] args) throws IOException {
        FileInputStream inputStream=new FileInputStream("d.txt");
        FileOutputStream outputStream=new FileOutputStream("c.txt");
        byte[] bytes=new byte[2];
        int  b;
        //数组长度为多少,一次可以就读多少,
        //返回值为本次指针移动了多少,因为剩下未读的
        //字节数不一定满足
        //设返回值为b,为当前指针移动了多少,直到文件的末尾时
        //就会返回-1
        while ((b=inputStream.read(bytes))!=-1){
            //看下当前的返回值指的是什么:2为多少个字节数据,-1为到末尾了
            System.out.println(b);
            outputStream.write(bytes);
        }
        outputStream.close();
        inputStream.close();
    }
}

有个问题:这里的数组大小是2,于是每一次只读2个,如果文件的字节数为奇数个,则指针走到最后一个时,就会用最后一个字节替换掉上一个成对字节中的第一个字节(数据结构):
在这里插入图片描述
输出的文件为:
在这里插入图片描述
关键在于每次读的大小都是固定的,所以就会这样

解决办法:用.read(三个参数)指定每次都写出的多少为读入时指针移动的个数即可:

package test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test08 {
    public static void main(String[] args) throws IOException {
        FileInputStream inputStream=new FileInputStream("d.txt");
        FileOutputStream outputStream=new FileOutputStream("c.txt");
        byte[] bytes=new byte[2];
        int  b;
        //数组长度为多少,一次就读多少,返回值为本次读到了多少
        //返回值为b,为当前返回了多少字节数据,知道文件的末尾时
        //就会返回-1
        while ((b=inputStream.read(bytes))!=-1){
            //看下当前的返回值指的是什么:2为多少个字节数据,-1为到末尾了
            System.out.println(b);
            //设置写出的数据量为读入时的指针移动数
            outputStream.write(bytes,0,b);
        }
        outputStream.close();
        inputStream.close();
    }
}

结果就是两文件一模一样了

乱码:

字符数据转成字节格式,就是编码
将字节格式的数据转换成字符类型的数据,就是解码
只有编码解码用的用一种方式才可以正确的得到想要的数据,idea默认UTF-8eclipse默认为GBK
在这里插入图片描述

具体的,这里简单展示idea的代码:指定编码解码格式

package test;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;

public class Test09 {
    public static void main(String[] args) throws UnsupportedEncodingException {
        String name="张舟荥";
        //将其转换成字节格式,方式叫做GBK编码
        byte[] bytes=name.getBytes("GBK");
        System.out.println(Arrays.toString(bytes));
        //转换字符串,方式叫做GBK解码
        String st=new String(bytes,"GBK");
        System.out.println(st);
    }
}

字符流: 可以用来读入写出中文

适用于对文本文件进行操作,并且输入流的read()空参方法遇到英文时就像上面的字节流一样,一次读一个字节,但是遇到中文时,一次读多个字节,然后都要再将其转换成十进制返回
在这里插入图片描述

创建对象,然后在里面写入文件的路径有两种方式,和前面字节流一样,然后调用的方法也一样,.read()这种不含有参数的返回数据,含参数的返回指针移动的位数

.read()不含参数: 具体的细节看后面的流程原理

package test;

import java.io.FileReader;
import java.io.IOException;

public class characterTest01{
    public static void main(String[] args) throws IOException {
        FileReader reader=new FileReader("z.txt");
        int b;
        while((b=reader.read())!=-1){
        	//直接将其转换为char型的数据
            System.out.print((char) b);
        }
    }
}

.read(char[] chars)含参数: 和字节流的一样,先创建一个char型的数组,再放在.read(参数)

具体的细节看后面的流程原理

package test;

import java.io.FileReader;
import java.io.IOException;

public class characterTest01{
    public static void main(String[] args) throws IOException {
        FileReader reader=new FileReader("z.txt");
        int b;
        char[] chars=new char[2];
        //有参的.read()方法,将数据放入数组中
        while((b=reader.read(chars)) !=-1){
            System.out.print(new String(chars,0,b));
        }
        reader.close();
    }
}

字符输入流含参空参流程原理:
空参:读到的数据进行解码,然后将其进行转换成十进制
含参:多了一个将十进制的数据强转char[]类型数据的操作

输入流底层原理:
在这里插入图片描述

字符输出流
方法要比字节输出流多一点:
在这里插入图片描述
在这里插入图片描述
代码实现:

package test;

import java.io.FileWriter;
import java.io.IOException;

public class characterTest02 {
    public static void main(String[] args) throws IOException {
        FileWriter writer=new FileWriter("z.txt");
        //将下面的十进制先转成字节数据,
        // 再将其转化成汉字放在指定的文件里面
        writer.write(25105);
        //将下面的汉字写入,转换成字节数据,
        //再将其转成汉字放入指定的文件中
        writer.write("将来进行时");
        //将数组写入分别转换成字节数组,
        //再将其装换成汉字放入文件中
        char[] chars={'s','我'};
        writer.write(chars);
        //关闭流
        writer.close();
    }
}

输出流的底层原理:
先将输出的数据放入一个大小为8192的缓冲区中,当缓冲区满了之后,才真正的将其放入文件中,所以没装满缓冲区的时候想要放入文件中,就要刷新writer.flush()
还有一点,流关闭之前会将缓冲区中的数据全部放入文件中,再关闭流

拷贝文件夹

将一个文件夹(a)中的子文件夹和文件全部拷贝到指定的文件夹下(b)
在这里插入图片描述
先获得文件夹得来去路径,用File类的方法得到循环问遇到文件夹还是文件,遇到a中文件就直接执行字节流进行读入和写出,遇到a中文件夹就直接递归,再次调用本方法,就是回到前面那句话 用File类的 的位置再来一遍就可以得出a文件夹中的文件夹中的文件,如此就全部拷贝了:

package test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ExampleTest01 {
    public static void main(String[] args) throws IOException {

        File src=new File("z:\\a");
        File dest=new File("z:\\b");
        copydir(src,dest);

    }
    private static void copydir(File src, File dest) throws IOException{
    //为防止b文件夹没有创建,就自己创建一下,如果已经
    //创建了,就失败呗,不会有影响
        dest.mkdirs();
        int a=0;
        //将当前传进来的文件的这条路径写成数组
        File[] files=src.listFiles();
        //判断这条路径指的东西是文件还是文件夹?
        for (File file:files) {
        //为文件,就正常拷贝
            if(file.isFile()){
                System.out.println("文件"+a);
                FileInputStream fis=new FileInputStream(file);
                FileOutputStream fos=new FileOutputStream(new File(dest,file.getName()));
                byte[] bytes=new byte[1024];
                int b;
                while ((b=fis.read(bytes))!=-1){
                    fos.write(bytes,0,b);
                }
                System.out.println("文件执行");
                fos.close();
                fis.close();
            }
            //为文件夹就再调用本方法,
            //这样这个文件夹的中的文件就可以被拷贝了
            else {
                copydir(file,new File(dest,file.getName()));
            }
        }
    }
}

缓冲流 :基本流的包装款#

字节缓冲流:BufferedInputStreamBufferedOutputStream,这两的底层还是字节流,所以用他们的时候,还是把他们当成字节流来用就行,他们的意义在于与磁盘进行传输数据的总体花费时间变短,先把数据存入长度为8192的缓冲区中,缓冲区满了,再和磁盘进行传输数据操作,这样就会提高速度

简单的看一下创建,写入的时候记得刷新一下.flush(),当然关闭流就会自动将缓冲区中的所有数据写在指定文件中
关闭流:关闭缓冲流的,就会自动先关闭非缓冲流

package test;

import java.io.*;

public class bufferStreamTest01 {
    public static void main(String[] args) throws IOException {
        BufferedInputStream inputStream=new BufferedInputStream(new FileInputStream("a.txt"));
        BufferedOutputStream outputStream=new BufferedOutputStream(new FileOutputStream("b.txt"));

        int b;
        byte[] bytes=new byte[2];
        while((b=inputStream.read(bytes))!=-1){
            outputStream.write(bytes,0,b);
        }
        outputStream.close();
        inputStream.close();
    }
}

原理:输出和输入的缓冲区不是同一个,而这个有参方法自己也有字节型的缓冲区,先存如本身的缓冲中,满了之后,再给外面的缓冲区传
在这里插入图片描述

字符缓冲流:BufferedReaderBufferedWriter,有大小为8192的缓冲区,别忘了字符流本身也有一个缓冲
关闭流:关闭缓冲流的,就会自动先关闭非缓冲流

他俩各有一个独特的方法

在这里插入图片描述

输入流特有方法:

package test;

import java.io.*;

public class bufferStreamest02 {
    public static void main(String[] args) throws IOException {
        BufferedReader reader=new BufferedReader(new FileReader("c.txt"));
        BufferedWriter writer=new BufferedWriter(new FileWriter("d.txt"));
        String str=reader.readLine();
        System.out.println(str);
    }
}

输出流特有方法:先输出,再调用.newLine()进行换行:既然是输出流自然还先存在缓冲中

package test;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class bufferStreamest03 {
    public static void main(String[] args) throws IOException {
        BufferedWriter writer=new BufferedWriter(new FileWriter("d.txt"));
        writer.write("小日子过得不错的日本选手  ");
        //换行
        writer.newLine();
        writer.write("哦,我的朋友,你在这里呀!!!");
        writer.close();
    }

}

缓冲区长度: 字节缓冲流和字符缓冲流的底层缓冲区的长度都为8192,但是需要注意,字节的缓冲区是长度为8192的字节数组,为8k,而字符的缓冲区为8192的字符数组,一个字符为两个字节,所以为16k

在这里插入图片描述

转换流:基本流的包装款

InputStreamReader(将字节流转换成字符流)和OutputStreamWriter(将字符流转换成字节流)

在这里插入图片描述

看下淘汰了方法:从GBK编码文件中获取中文,然后打印出来:InputStreamReader可以指定字符集为GBK

package test;

import java.io.*;

public class readGBKExample {
    public static void main(String[] args) throws IOException {
        InputStreamReader inputStreamReader=new InputStreamReader(new FileInputStream("C:\\Users\\李家院刘俊辉\\Desktop\\xinxi.txt"),"GBK");
        int b;
        //遇到中文就一次多读几个字节
        while((b=inputStreamReader.read())!=-1){
            //中文转换表中的数字
            System.out.print(b+"  ");
            //char可以将中文转换表中的数字转换成中文
            System.out.println((char)b);
        }
        inputStreamReader.close();
    }
}

JDK11之后就淘汰了上面(InputStreamReader)这种指定字符集的方法,现在创建FileReader就可以实例化的时候指定字符编码

public static void show2() throws IOException {
        FileReader inputStreamReader=new FileReader("C:\\Users\\李家院刘俊辉\\Desktop\\xinxi.txt",Charset.forName("GBK"));
        int b;
        //遇到中文就一次多读几个字节
        while((b=inputStreamReader.read())!=-1){
            //中文转换表中的数字
            System.out.print(b+"  ");
            //char可以将中文转换表中的数字转换成中文
            System.out.println((char)b);
        }
        inputStreamReader.close();
    }

InputStreamWriter和上面一个道理,JDK后也有新的解决办法FileWriter

再来个将GBK文件读入,再写入UTF-8文件:

public static void show2() throws IOException {
        InputStreamReader inputStreamReader=new InputStreamReader(new FileInputStream("C:\\Users\\李家院刘俊辉\\Desktop\\xinxi.txt"),"GBK");
        OutputStreamWriter outputStreamWriter=new OutputStreamWriter(new FileOutputStream("C:\\Users\\李家院刘俊辉\\Desktop\\xix.txt"),"UTF-8");
        int b;
        while ((b=inputStreamReader.read())!=-1){
            //字符输出本来就可以把中文当成参数放进去
            outputStreamWriter.write((char)b);
        }
        outputStreamWriter.close();
        inputStreamReader.close();
    }

当然JDK11之后就用上面所说的那种方式

题目:

利用字节读取GBK文件,每次都只读一行,并且不能出现乱码
在这里插入图片描述

先创建一个文件字节输入流,然后通过转换流将这个字节流转换成字符流,并且指定将GBK文件按照GBK进行解码,再将这个字符流转换成字符缓冲流,最后就可以调用它独有的方法进行只读一行了:

public static void show3() throws IOException {
        //什么格式编码的文件就要用什么方法进行解码,然后再正常操作
        BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(new FileInputStream("C:\\Users\\李家院刘俊辉\\Desktop\\xinxi.txt"),"GBK"));
        //读一行字符数据,会根据遇到中文自动的一次读多个字节
        String readLine=bufferedReader.readLine();
        System.out.println(readLine);
        bufferedReader.close();
    }

在这里插入图片描述

序列化流: 基本流的包装款#

将数据包装一下存起来,别人来看时,看不懂,只有自己再用反序列流搞一下,才能还原真的数据
首先数据必须得被标记类(没有抽象方法的接口)标记一下,由此表示当前类产生的数据,才可以被调用者进行序列化和反序列化操作:

package com.itjh.pojo;

import java.io.Serializable;

public class Student implements Serializable {
    private String name;
    private int age;
    set和get和toString各种方法
}

现在将数据序列化存起来,再反序列化拿出来打印:

package test;

import com.itjh.pojo.Student;
import java.io.*;

public class xuliehualiuTest  {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        show1();
    }
    public static void show1() throws IOException, ClassNotFoundException {
        Student student=new Student("张三",19);
        //将数据序列化,再放入指定的文件中
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("b.txt"));
        //读取指定的序列化文件
        ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("b.txt"));
        //数据序列化写入
        outputStream.writeObject(student);
        //将序列化数据读出,并且打印
        System.out.println((inputStream.readObject()));
        inputStream.close();
        outputStream.close();
    }
}

版本号问题

当把数据序列化输出进文件之后,再改变实体类中的数据,然后再反序列化读入数据,这时就会报错
因为输出的时候产生的数据的实体类在后来改变了,(而程序在输入输出时本身会根据实体类生成一个版本号),所以需要给他一个版本号,这样表示改变前和改变后是同一个版本,就不会因为找不到想要的版本号而报错了
那么要让在实体类中创建:我这里是自动生成的,需要在setting中设置
private static final long serialVersionUID = 序列号;
注意: 在实体类中的数据都搞完后,再去生成,因为它是根据这个实体类中的所有数据来生成的版本号

在这里插入图片描述
实体类:

package com.itjh.pojo;

import java.io.Serializable;

public class Student implements Serializable {
	//自动生成的版本号
    private static final long serialVersionUID = 5527154188739101805L;
    private String name;
    private int age;
    
    
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                 '\'' +
                '}';
    }
}

这样即使再在实体类中新添加数据adress,反序列化也是成功的,只是新添加的数据显示为默认值罢了,并序列化数据的时候还没有这个新添加的数据呢

package com.itjh.pojo;

        import java.io.Serializable;

public class Student implements Serializable {

    private static final long serialVersionUID = 5527154188739101805L;
    private String name;
    private int age;
    private String adress;

    public String getAdress() {
        return adress;
    }

    public void setAdress(String adress) {
        this.adress = adress;
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", adress='" + adress + '\'' +
                '}';
    }
}

结果:

Student{name='张三', age=21, adress='null'}

Process finished with exit code 0

瞬态关键字:看注释

//transient:瞬态关键字
    //序列化的时候不会讲此数据也序列化
    private transient int id;

总结:
在这里插入图片描述
序列化可以一次搞多个数据进去,然后在指定文件中产生一个更长的序列化数据,但是反序列化时,不知道此文件中包含个几个数据,不然直接把.readObject()复制几行(几个数据就复制几行),就OK了,于是,可以在序列化之前,全部装入一个集合中,在将集合写入即可

文件:

�� sr java.util.ArrayListx����a� I sizexp   w   sr com.itjh.pojo.StudentL�esV�Dm I ageL namet Ljava/lang/String;xp   t 张三sq ~    t 李四sq ~    t 王五x

测试类:用哪个就把另外个注释掉

package test;

import com.itjh.pojo.Student;

import java.io.*;
import java.util.ArrayList;

public class xuliehuaTest02 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //序列化
          show1();
        //反序列化
          show2();
    }
    public static void show1() throws IOException {
        Student student1=new Student("张三",2);
        Student student2=new Student("李四",2);
        Student student3=new Student("王五",2);
        ArrayList<Student> list=new ArrayList<>();
        list.add(student1);
        list.add(student2);
        list.add(student3);
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("a.txt"));
        outputStream.writeObject(list);
        outputStream.close();
    }
    public static void show2() throws IOException, ClassNotFoundException {
        ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("a.txt"));
        System.out.println(inputStream.readObject());
    }
}

打印流

在这里插入图片描述
在这里插入图片描述

字节打印流:

构造方法:第一行,三个参数的底层其实都是创建一个字节输出流对象
在这里插入图片描述

方法
在这里插入图片描述
简单的代码实现:占位符有很多,各个字母代表着不同的含义,这里的%s指代替后面的字符

package test;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;

public class printStreamTest01 {
    public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
        //创建字节输出流对象,自动刷新,指定编码格式
        PrintStream stream=new PrintStream(new FileOutputStream("c.txt"),true,"UTF-8");
        //体现自动刷新
        stream.println("王二麻子");
        //体现占位符,会自动刷新
        stream.printf("%s小飞棍来喽!!%s","呦! 快看!!","芜湖");
        stream.close();
    }
}

生成的文件:

王二麻子
呦! 快看!!小飞棍来喽!!芜湖

字符打印流:

PrintWriter:和字节打印流一样
需要手动刷新,如在构造函数中加参数

在这里插入图片描述

Commons-io

工具包:将之前的从文件拷贝数据到另一个文件的操作等等的这种操作,全部用一个工具类,调用其中的一个方法就解决了,只用这个工具类提供的方法就可以全部解决,不用手动敲这么多代码

Hutool

和上面的工具包一样,但是这个是国货之光,国人自己写的,就是牛!,不懂得看它的文档就行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值