Jbuilder 9.0 打造“完美”FTP

一、概述

  大家知道,文件传输系统是由服务器端服务程序和客户端应用程序两部分组成。一个FTP服务器进程可同时处理多个客户进程的请求服务。FTP服务器进程由两大部分组成:一个主进程,负责接受新的请求;另外有若干个从进程,负责处理单一请求。象我们在windows 2000/2003 Server 中IIS所提供的FTP服务器程序就是服务器端服务程序的典型代表。我们这里所说设计的一个实现上传和下载文件功能程序主要是指ftp客户端应用程序。

  FTP客户端设计的原理,FTP使用两条TCP连接来完成文件传输:控制连接与数据连接。控制连接用于传输控制,数据连接用于数据输送。在服务器启动后,服务器就会在端口21等待客户的连接请求,有用户需要传输文件时,客户与服务器的端口21建立一个控制连接,用来传送客户的命令和服务器的应答,该连接一直保持到客户与服务器通信结束为止。当客户发出数据传输命令时,服务器会主动与客户建立数据连接,并与其进行数据交换。

  客户端用户并不直接处理控制连接上的FTP命令和FTP响应,而是由两个协议解释器进行处理。用户接口为客户端用户提供一定形式的输入界面,接收用户的命令,将其转换成标准的FTP命令,最终将控制连接上的FTP响应转换成用户可理解的方式。在客户与服务器的整个连接期间,控制连接必须保持,而数据连接却可以根据请求动态地建立和关闭。在最常用的流模式中,文件结束是用数据连接的关闭来表示的,换句话说,每传输一个文件或文件列表,系统就会再建立一个新的数据连接。 

  二、Jbuilder9中FTP库简介

  在Jbuilder9使用的java语言中,提供了一类网络类库sun.net.ftp.FtpClient.,该类库主要提供了用于建立FTP连接的类。利用这些类的方法,编程人员可以远程登录到FTP服务器,列举该服务器上的目录,设置传输协议,以及传送文件。FtpClient类涵盖了几乎所有FTP的功能,FtpClient的实例变量保存了有关建立"代理"的各种信息。下面给出了这些实例变量。

  public static boolean useFtpProxy

  这个变量用于表明FTP传输过程中是否使用了一个代理,因此,它实际上是一个标记,此标记若为TRUE,表明使用了一个代理主机。

  public static String ftpProxyHost

  此变量只有在变量useFtpProxy为TRUE时才有效,用于保存代理主机名。

  public static int ftpProxyPort

  此变量只有在变量useFtpProxy为TRUE时才有效,用于保存代理主机的端口地址。

  FtpClient有三种不同形式的构造函数,如下所示:

  1、public FtpClient(String hostname,int port)

   此构造函数利用给出的主机名和端口号建立一条FTP连接。

  2、public FtpClient(String hostname)

  此构造函数利用给出的主机名建立一条FTP连接,使用默认端口号。

  3、FtpClient()

  此构造函数将创建一FtpClient类,但不建立FTP连接。这时,FTP连接可以用openServer方法建立。

  一旦建立了类FtpClient,就可以用这个类的方法来打开与FTP服务器的连接。类ftpClient提供了如下两个可用于打开与FTP服务器之间的连接的方法。

  public void openServer(String hostname)

  这个方法用于建立一条与指定主机上的FTP服务器的连接,使用默认端口号。

  public void openServer(String host,int port)

  这个方法用于建立一条与指定主机、指定端口上的FTP服务器的连接。

  打开连接之后,接下来的工作是注册到FTP服务器。这时需要利用下面的方法。

  public void login(String username,String password)

  此方法利用参数username和password登录到FTP服务器。使用过Intemet的用户应该知道,匿名FTP服务器的登录用户名为anonymous,密码一般用自己的电子邮件地址。

  下面是FtpClient类所提供的一些控制命令。

  public void cd(String remoteDirectory)

  该命令用于把远程系统上的目录切换到参数remoteDirectory所指定的目录。

  public void cdUp():该命令用于把远程系统上的目录切换到上一级目录。

  public String pwd():该命令可显示远程系统上的目录状态。

  public void binary():该命令可把传输格式设置为二进制格式。

  public void ascii():该命令可把传输协议设置为ASCII码格式。

  public void rename(String string,String string1)

  该命令可对远程系统上的目录或者文件进行重命名操作。

  除了上述方法外,类FtpClient还提供了可用于传递并检索目录清单和文件的若干方法。这些方法返回的是可供读或写的输入、输出流。下面是其中一些主要的方法。

  public TelnetInputStream list()

  返回与远程机器上当前目录相对应的输入流。

  public TelnetInputStream get(String filename)

  获取远程机器上的文件filename,借助TelnetInputStream把该文件传送到本地。

  public TelnetOutputStream put(String filename)

  以写方式打开一输出流,通过这一输出流把文件filename传送到远程计算机。

  三、设计一个FTP的客户端程序,实现文件上传下载功能

  FTP的客户端程序设计主要有四个部分:客户登录程序设计、FTP服务器目录浏览程序设计、FTP服务器目录更新程序设计、上传和下载文件程序设计。而相应的操作流程为: 首先客户端程序先连接服务器,然后浏览或更新FTP服务器目录,选择文件进行下载或上传,进行文件数据传输,最后断开连接。 我们要注意,在 下载文件和上传文件时,其数据流向是不同的;而连接的断开也是由服务器程序执行的。

  FTP客户端系统的系统结构(也包含在服务器端的部分处理过程),整个系统由图形界面、登录程序、浏览程序、上传下载程序四大模块组成。图形界面又由登录界面和文件处理界面组成,它主要给用户提供所见即所得的交互手段。登录程序通过用户提供的登录信息(包括FTP服务器IP,服务器应用程序端口号、用户名(ID)和密码)连接到服务器,并把信息反馈于登录界面。浏览程序实现服务器和客户端文件目录的浏览。

  上传下载程序主要完成文件的传输,是系统的核心模块。Jbuilder9提供的FTP控件已经对其进行了类封装,因而对用户而言它是透明的,不必再去编程解释响应消息等,这就是控件的好处。这样使用JBuilder9,就节约了大量的开发时间。即使对FTP不是很了解,都可以很快建立起一个FTP客户端程序来。由于本程序只是FTP客户端,下面将分别对整个系统的图形界面、登录程序、浏览程序和上传下载程序四大模块的程序实现细节进行详细介绍。

  1、系统的图形界面设计

  系统的图形界面设计是关于连接FTP服务器界面和文件处理界面这二个界面的设计,它主要是为用户提供所见即所得的交互接口。

  ①主界面的设计

  用户可以使用JBuilder 9的Project Wizard来创建工程。工程文件扩展名为.jpx。工程文件包含了工程中其他文件所在目录信息。使用Project Wizard的具体步骤如下:选择File->New Proiect菜单项;在Name文本框中输入FTPClient;单击Finish按钮。然后使用ApplicationWizard向导创建应用程序,均选择默认值。如图1所示:

按此在新窗口浏览图片
图1  

  ②连接FTP服务器界面的设计

  连接FTP服务器界面的设计是在jPanel1的基础止设计实现的,增加了一些其他控件。如图2所示:
 

按此在新窗口浏览图片
图2 

  ③文件处理界面的设计

  文件处理界面的设计是在jPanel2的基础上实现的,增加了一些其他控件。如图3所示:

按此在新窗口浏览图片

图3 

  ④界面设计的程序实现

  Java是一个面向对象的程序语言,它使用了很多对象,在JBuilder9中使用的是JDK1.4库,它提供了很多的界面对象。在本实例中,使用如下的对象和库。

package ftpclient;
//库声明
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import com.borland.jbcl.layout.*;
import java.beans.*;
import java.io.*;
import sun.net.TelnetInputStream;
import sun.net.ftp.*;
import java.lang.Object;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.JTextField;
import javax.swing.JCheckBox;
import javax.swing.tree.TreePath;
import sun.net.TelnetOutputStream;
import java.util.Properties;
/**
* <p>Title: this is study</p>
* <p>Description:</p>
* <p>Copyright: Copyright (c) 2004</p>
* ** ghq ***
* @version 1.0
*/
public class Frame1 extends JFrame {
 //对象说明
 private JPanel contentPane;
 private JTextField jTextField1 = new JTextField();
 private JTextField jTextField2 = new JTextField();
 private JTextField jTextField3 = new JTextField();
 private JCheckBox jCheckbox1 = new JCheckBox();
 FtpClient ftp=null;
 JTabbedPane jTabbedPane1 = new JTabbedPane();
 JPanel jPanel1 = new JPanel();
 JPanel jPanel2 = new JPanel();
 PaneLayout paneLayout1 = new PaneLayout();
 XYLayout xYLayout1 = new XYLayout();
 JLabel jLabel1 = new JLabel();
 JLabel jLabel2 = new JLabel();
 JLabel jLabel3 = new JLabel();
 JTextField jTextField4 = new JTextField();
 JLabel jLabel4 = new JLabel();
 BorderLayout borderLayout1 = new BorderLayout();
 Box box1;
 JPanel jPanel3 = new JPanel();
 JPanel jPanel4 = new JPanel();
 XYLayout xYLayout2 = new XYLayout();
 BorderLayout borderLayout2 = new BorderLayout();
 jscrollPane jscrollPane1 = new jscrollPane();
 JButton jButton3 = new JButton();
 JButton jButton4 = new JButton();
 jscrollPane jscrollPane2 = new jscrollPane();
 //定义树节点,模型和树视图
 DefaultMutableTreeNode root1 = new DefaultMutableTreeNode("目录中没有文件");
 DefaultTreeModel model1 = new DefaultTreeModel(root1);
 JTree jTree1 = new JTree(model1);
 JButton jButton5 = new JButton();
 JLabel statusLabel = new JLabel();
 List list1 = new List();
 jscrollPane jscrollPane3 = new jscrollPane();
 JTextArea jTextArea1 = new JTextArea();
 JButton jButton1 = new JButton();
 JButton jButton2 = new JButton();
 JLabel jLabel5 = new JLabel();
 JLabel jLabel6 = new JLabel();
}  



  2、登录程序

  下面我们来说明一下利用FtpClient控件实现FTP客户端应用程序的登录功能。要登录FTP服务器,一定要设置下面的参数:

  hostname:服务器的IP地址或主机名称,为字符串类型;
  port: 服务器的通信端口,为int类型(一般而言,FTP的通信端口默认为21);
  username:用户的登录账号;
  password:用户的登录密码。

  一般的使用办法是,直接使用构造方法创建一个FtpClient类对象(如果在构造FtpClient类对象时没有设置参数,则可以使用openServer方法来打开服务器端口),然后使用login方法来登录,并进行用户认证。以下给出示例程序的源代码:


// 登录程序: void jButton1_actionPerformed(ActionEvent e)
// 作用:登录连接到FTP服务器。
void jButton1_actionPerformed(ActionEvent e) {
 int ch;
 String hostname=jTextField1.getText();
 //如果已经打开了FTP服务器,则先关闭FTP文件服务器
 try {
  if (ftp!=null) ftp.closeServer();
 }
 catch (IOException ex) {
  ex.printStackTrace();
 }
 //连接到服务器
 try {
  statusLabel.setText("正在连接,请等待.....");
  ftp= new FtpClient(hostname);
  //登录Ftp服务器
  ftp.login(jTextField2.getText(),jTextField3.getText());
  //使用二进制协议
  ftp.binary();
 }
 catch(FtpLoginException ex){
  //没有主机的登录权限
  statusLabel.setText("无权限与主机:"+hostname+"连接!");
 }
 catch (IOException ex){
  //连接主机失败
  statusLabel.setText("连接主机:"+hostname+"失败!");
 }
 catch(SecurityException ex)
 {
  //用户或者密码可能不对
  statusLabel.setText("用户或者密码可能不对,无权限与主机:"+hostname+"连接!");
 }
 //连接成功后的显示
 statusLabel.setText("连接主机:"+hostname+"成功!");
 //列表框需要重新刷新
 ReloadList();
}


  3、浏览程序

  我们利用FtpClient控件可以实现FTP客户端应用程序的浏览功能,浏览功能的实现程序由浏览服务器端文件和浏览本地文件两部分组成。

  ①浏览服务器端的程序

  当连接登录成功之后,可使用FtpClient控件的List()方法执行FTP服务器端上的目录浏览(List)功能,以取得FTP服务器默认的目录内容。一般List方法会列出FTP端目录与文件的详细内容,包括创建文件日期,文件大小,目录与文件的名称等相关信息。当然,FTP服务器端必须开放目录读取的权限,以允许连接登录及浏览目录,否则,Connect方法会执行失败。而这个方法执行后的结果是TelnetInputStream数据流,本系统要把它显示在一个列表框中,还需要进行分解操作。下面是对列表框进行操作的源程序代码:


// 浏览程序: private void ReloadList()
// 作用:清空目录列表,调用List()方法获取文件列表。
private void ReloadList(){
 StringBuffer buf=new StringBuffer();
 int ch;
 //清空目录列表
 list1.removeAll();
 try {
  //调用List()方法得到目录表
  TelnetInputStream t = ftp.list();
  t.setStickyCRLF(true);
  //分解TelnetInputStream数据流
  while ( (ch = t.read()) >= 0) {
   if (ch == '\n') {
    //向列表框添加分解得到的目录和文件
    list1.add(getDIR(buf.toString()));
    buf.setLength(0);
   }
   else {
    buf.append( (char) ch);
   }
  }
  //完成后关闭TelnetInputStream数据流
  t.close();
 }
 catch (IOException ex) {
  ex.printStackTrace();
 }
 //刷新列表框的内容
 list1.validate();
}
//Overridden so we can exit when window is closed
protected void processWindowEvent(WindowEvent e) {
 super.processWindowEvent(e);
 if (e.getID() == WindowEvent.WINDOW_CLOSING) {
  System.exit(0);
 }
}

// 浏览程序: public String getDIR(String path)
// 作用:分解字符串得到目录和文件名

public String getDIR(String path){
 String DIRName;
 int ch;
 //分解字符串得到目录和文件名
 int begin=55;
 DIRName=path.substring(begin).trim();
 return DIRName;
}



  通过对列表框的操作,可更新FTP服务器端的目录位置。当双击列表框控件时,首先判断是否为目录(包括 ".. "". "),若是,则根据相应的目录并使用FtpClient控件的cd方法来处理服务器端的当前目录,它将切换当前目录到指定的路径。当cd方法成功后,由于FTP服务器端的当前目录内容变了,因此还需要使用ReloadList方法再重新浏览FTP服务器端目录内容,源程序代码如下:


// 浏览程序:void list1_mouseClicked(MouseEvent e)
// 作用:FTP服务器文件目录改变
void list1_mouseClicked(MouseEvent e) {
 //单击列表框的操作
 StringBuffer buf=new StringBuffer();
 if(ftp==null) list1.removeAll();
 if(list1.getRows()>0){
  //选择列表框中单击选中的内容
  String dir=list1.getSelectedItem().trim();
  try {
   //如果是目录,则进入目录
   ftp.cd(dir);
   //列表框需要重新刷新
   ReloadList();
   jTextArea1.append("进入目录: ");
   jTextArea1.append(dir);
   jTextArea1.append("\n");
  }
  catch (IOException ex) {
   //否则,表明点击的内容是ftp上的一个文件
   jTextArea1.append(dir);
   jTextArea1.append(":是一个文件\n");
  }
 }
}


  ②本地浏览程序

  文件传输是一个双向的过程,它涉及到客户端和服务器之间数据流传输的问题,人们不仅需要把服务器上的文件下载到指定的文件下,币残枰馨芽突Ф吮镜氐奈募洗椒衿魃稀?BR>
  本地浏览程序通过jTree1,jButton5和JFileChooser三种控件实现。jTree1用于显示当前选择目录中的所有文件和目录,jButton5和JFileChooser组合在一起用于选择需要的目录。其实现的源程序代码如下:


/ 浏览程序: void jButton5_actionPerformed(ActionEvent e)
// 作用:选择需要的本地文件目录
void jButton5_actionPerformed(ActionEvent e) {
 JFileChooser JFileCh = new JFileChooser();//创建文件对话框
 JFileCh.setFileSelectionMode(JFileCh.DIRECTORIES_ONLY);//只选择目录
 int returnVal = JFileCh.showOpenDialog(this);//显示文件对话框
 if(returnVal == JFileChooser.APPROVE_OPTION){
  File root2 = JFileCh.getSelectedFile();//得到根目录文件
  if(root2.isFile()) root2=root2.getParentFile();
  //如果得到的不是目录,则使用他的目录
  DefaultMutableTreeNode rootTree2 =new DefaultMutableTreeNode(root2.getPath());
  setTree(root2.getPath(),rootTree2);//遍历目录树
  model1.setRoot(rootTree2);//设置模型的根节点
  model1.reload();//重新构造树视图
  }//end if
 else
  JOptionPane.showMessageDialog(this, "没有选择文件");//显示提示信息。
}



而构建目录树则使用了一个专门的方法,它是一个递归调用方法,源程序代码如下:

[quote]
// 浏览程序: public void setTree(String Path,DefaultMutableTreeNode TreePath)
// 作用:构造jTree对象的目录树
public void setTree(String Path,DefaultMutableTreeNode TreePath){
 //递归遍历目录树
 try{
  File source = new File(Path);//得到源文件路径
  String[] fileName=source.list();//的到该目录下文件列表
  for(int i=0;i<fileName.length;i++){//显示选择了的文件列表
   File fileMem=new File(source.getPath(),fileName );
   if(fileMem.isDirectory()){//是目录则添加目录
    DefaultMutableTreeNode TreeMem =new DefaultMutableTreeNode(fileMem.getName());
    TreePath.add(TreeMem);//在树中添加目录
    setTree(fileMem.getPath(),TreeMem);//递归遍历目录树
   }//end if
   else{//是文件则添加节点
    DefaultMutableTreeNode TreeMem =new DefaultMutableTreeNode(fileMem.getName());
    TreePath.add(TreeMem);//添加节点
   }
  }//end for
 }//End try
 catch(Exception ei)
 {
  JOptionPane.showMessageDialog(this, "系统出错:"+ei);//显示提示信息。
 }
}
void JCheckbox1_propertyChange(PropertyChangeEvent e) {
 if(jCheckbox1.isSelected()){
  jTextField1.setText("anonymous");
  jTextField2.setText("a");
 }
}
[/quote]

  4、文件下载和上传程序

  通过浏览程序对FTP服务器和本地文件系统的浏览,解决了客户端和服务器端的文件和目录选择的问题,但是文件数据是需要传送和交换的,下面我们将介绍如何实现文件的下载和上传等操作过程。 

  ①文件下载程序

  当我们想下载文件,可以使用TelnetInputStream流,并使用FTPClient控件的get方法将流的源头绑定,再将TelnetInputStream流绑定到DataInputStream流中,然后再在本地新建一个文件,并绑定到RandomAccessFile流中,执行DataInputStream流的读并写入RandomAccessFile流中,就可以完成下载功能了。

  当然,程序首先需要判断本地浏览框中选中的是目录还是文件,或者根本就没有在本地浏览框中选择。为了可同时选择多个文件下载,必须先设置list1控件的MultipleMode属性为true,以便能够在远程浏览框中进行多重选择。

  接着在程序中利用list1控件对象的getSelectedItems方法获取所选择的文件和目录,并判断是否为目录;若是目录,则在本地创建一个目录(本实例没有使用递归的方法,所以不能下载目录中的文件和子目录),否则,将进行文件下载。一直重复至所有被选择文件或者目录都被完成操作为止。在这里,使用了一个isfile的boolean变量用于判断是否为文件。文件下载的源代码如下:

[quote]
// 下载程序: void jButton4_actionPerformed(ActionEvent e)
// 作用:下载ftp文件到本地目录
void jButton4_actionPerformed(ActionEvent e) {
 //下载ftp文件到本地目录
 if(jTree1.isSelectionEmpty()){
  //如果没有选择目录和文件提示用户
  jTextArea1.append("没有选择本地目录用于下载!!\n");
 }else{
  File file1=new File(conPath());
  if(file1.isFile()){
   //如果是文件也要提示用户选择的是文件
   jTextArea1.append("没有选择本地目录用于下     载"+jTree1.getSelectionPath().getLastPathComponent().toString()+"\n");
   jTextArea1.append("而选择的是文件:"+conPath()+"\n");
  }else{
   //下载文件到指定的目录里
   jTextArea1.append("选择的下载本地目录"+jTree1.getSelectionPath().getLastPathComponent().toString()+"\n");
   jTextArea1.append("目录:"+conPath()+"\n");
   //可以选择多个文件和目录
   String [] filenames=list1.getSelectedItems();
   for(int i=0;i<filenames.length;i++){
    String filename=filenames;
    boolean isfile=false;
    try {
     //判断是否为目录,如果是目录则在本地创建一个目录
     ftp.cd(filename);
     ftp.cdUp();
     File localDir = new File(conPath()+"\\"+filename) 
     localDir.mkdir();
    }
    catch (IOException ex) {
     isfile=true;
    }
    if(isfile){
     //对文件的处理,即下载文件到本地硬盘中
     int ch;
     try{
      File localFile = new File(conPath()+"\\"+filename) 
      RandomAccessFile getFile = new RandomAccessFile((conPath()+"\\"+filename),"rw");
      getFile.seek(0);
      TelnetInputStream ins = ftp.get(filename);
      DataInputStream Inputs = new DataInputStream(ins);
      //下载处理......
      while ((ch = Inputs.read()) >= 0) {
       getFile.write(ch);
      }
      //处理完成,关闭输入输出流
      ins.close();
      getFile.close();
     }
     catch (IOException ex) {
      ex.printStackTrace();
     }
    }
   }
  }
 }

[/quote]

  ②文件上传程序

  上传文件基本上与下载文件类似,不过使用的是TelnetOutputStream流,并使用FTPClient控件的put方法和流绑定,再将TelnetOutputStream流绑定到DataOutputStream流中,然后在本地新建一个文件,并绑定到RandomAccessFile流中,执行RandomAccessFile流的读并写入DataOutputStream流中,就可以完成上传功能了。源程序代码如下:


// 上载程序: void jButton3_actionPerformed(ActionEvent e)
// 作用:上载本地文件到ftp目录
void jButton3_actionPerformed(ActionEvent e) {
 //上载本地文件到ftp服务器中
 if(jTree1.isSelectionEmpty() ){
  jTextArea1.append("没有选择本地文件上载\n");
 }else{
  File file1=new File(conPath());
  //判断是否是文件,如果是目录则不与操作,以后的程序可能会考虑添加目录的操作
  if(file1.isFile()){
   String filename=jTree1.getSelectionPath().getLastPathComponent().toString();
   int ch;
   try {
    File localFile = new File(conPath() );
    RandomAccessFile sendFile = new RandomAccessFile( conPath() , "r");
    //上载文件到ftp服务器中
    sendFile.seek(0);
    TelnetOutputStream outs = ftp.put(filename);
    DataOutputStream outputs = new DataOutputStream(outs);
    //上载处理中......
    while (sendFile.getFilePointer() < sendFile.length()) {
     ch = sendFile.read();
     outputs.write(ch);
    }
    //上载完成,关闭输入输出流
    outs.close();
    sendFile.close();
   }
   catch (IOException ex) {
    ex.printStackTrace();
   }
   //列表框需要重新刷新
   ReloadList();
  }
 }
}


  当然,FTP服务器端必须开放目录写入的权限,以允许修改FTP服务器目录下的文件,否则,上传会执行失败。当上传方法执行成功之后,由于FTP服务器目录内容改变,因此需使用ReloadList方法,重新浏览FTP服务器端目录内容。

  当文件上传下载结束,要断开连接,采用FTPClient控件断开和服务器连接相对而言较简单,一般的关闭FTP服务器连接的方法为closeServer方法,源程序代码如下:


// 关闭程序: void jButton3_actionPerformed(ActionEvent e)
// 作用:关闭FTP服务器连接
void jButton2_actionPerformed(ActionEvent e) {
 try {
  //关闭FTP服务器连接
  if (ftp!=null) {
   ftp.closeServer();
   list1.removeAll();
  }
 }
 catch (IOException ex) {
  ex.printStackTrace();
 }
 statusLabel.setText("断开主机连接成功!");
}



  至此ftp客户端程序代码设计基本上完成,在Jbuilder9集成环境下编译运行,窗口出现如图4所示:
按此在新窗口浏览图片
 (本文已被浏览 715 次)

转载于:https://www.cnblogs.com/xnxqs/archive/2005/07/29/202856.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值