java 网络编程五 (仿QQ聊天程序)

学完了socket通讯后,在老师的要求下,写了一个仿qq的聊天程序:

最终调试程序结果如下图:   有bug希望提出来,我们一起解决。


设计思路:

在服务器端 用一个HashMap<userName,socket> 维护所有用户相关的信息,从而能够保证和所有的用户进行通讯。

客户端的动作:
(1)连接(登录):发送userName    服务器的对应动作:1)界面显示,2)通知其他用户关于你登录的信息, 3)把其他在线用户的userName通知当前用户 4)开启一个线程专门为当前线程服务
(2)退出(注销):

(3)发送消息


※※发送通讯内容之后,对方如何知道是干什么,通过消息协议来实现:

客户端向服务器发的消息格式设计:

命令关键字 @# 接收方 @# 消息内容 @# 发送方
1)连接:userName      ----握手的线程serverSocket专门接收该消息,其它的由服务器新开的与客户进行通讯的socket来接收
2)退出:exit @# 全部 @# null@# userName
3)发送: on @# JList.getSelectedValue() @# tfdMsg.getText() @# tfdUserName.getText()

服务器向客户端发的消息格式设计:

命令关键字 @# 发送方 @# 消息内容
登录:
   1) msg @# server @# 用户[userName]登录了  (给客户端显示用的)
   2) cmdAdd @# server @# userName (给客户端维护在线用户列表用的)
退出:
   1) msg @# server @# 用户[userName]退出了  (给客户端显示用的)
   2) cmdRed@#server @# userName (给客户端维护在线用户列表用的)
发送:

   msg @# 消息发送者 @# 消息内容

代码实现:

客户端:

package cn.hncu.net.sina;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.border.TitledBorder;

public class ClientFrom extends JFrame implements ActionListener{
	private static String ip="127.0.0.1";
	private static int port=8080;
	
	private JTextField tfdUserName=new JTextField(10);	//用户标识
	private JTextArea allMsg=new JTextArea();	//聊天信息显示
	private JTextField tfdMsg=new JTextField(10);//发送消息消息框
	private JButton btnSend;	//发送消息按钮
	private JButton btnCon;
	
	//在线用户列表
	private DefaultListModel<String> dataModel=new DefaultListModel<String>();
	private JList<String> list=new JList<String>(dataModel);
	
	public ClientFrom() {
		setBounds(300,300,400,300);
		
		addMenuBar();	//添加菜单
		上方面板/
		JPanel northPanel=new JPanel();
		northPanel.add(new JLabel("用户名称"));
		tfdUserName.setText("");
		northPanel.add(tfdUserName);
		
		btnCon=new JButton("连接");
		btnCon.setActionCommand("c");
		JButton btnExit=new JButton("退出");
		btnExit.setActionCommand("exit");
		northPanel.add(btnCon);
		northPanel.add(btnExit);
		
		getContentPane().add(northPanel,BorderLayout.NORTH);	//放在上方
		//中间面板
		JPanel centerPanel=new JPanel(new BorderLayout());
		//中
		allMsg=new JTextArea();
		allMsg.setEditable(false);
		allMsg.setForeground(Color.blue);
		allMsg.setFont(new Font("幼圆", Font.BOLD, 14));
		centerPanel.add(new JScrollPane(allMsg));
		//东
		dataModel.addElement("全部");
		list.setSelectedIndex(0);	//设置默认选择位置
		list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);	//设置只能单选
		list.setVisibleRowCount(5);		//设置显示的行数
		list.setFont(new Font("幼圆", Font.BOLD, 12));
		
		JScrollPane scroll=new JScrollPane(list);		//为list添加滚动条
		scroll.setBorder(new TitledBorder("在线"));	//Border的实现类TitileBorder
		scroll.setPreferredSize(new Dimension(70, allMsg.getHeight()));	//设置滚动条的首选大小
		centerPanel.add(scroll,BorderLayout.EAST);
		//南
		JPanel southPanel=new JPanel();
		southPanel.add(new JLabel("消息"));
		southPanel.add(tfdMsg);
		
		btnSend=new JButton("发送");
		btnSend.setActionCommand("send");
		btnSend.setEnabled(false);
		southPanel.add(btnSend);
		
		centerPanel.add(southPanel,BorderLayout.SOUTH);
		
		//把中间面板加到框架中
		getContentPane().add(centerPanel);
		
		//事件监听
		btnCon.addActionListener(this);
		btnExit.addActionListener(this);
		btnSend.addActionListener(this);
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				if(tfdUserName.getText()==null || tfdUserName.getText().trim().length()==0){
					int result = JOptionPane.showConfirmDialog(ClientFrom.this, "你还没登录,是否退出");
					if(result==JOptionPane.YES_OPTION){
						System.exit(0);
					}else{
						return;
					}
				}
				System.out.println(tfdUserName.getText()+"退出");
				sendExitMsg();
				System.exit(0);
			}
		});
		
		setVisible(true);
	}
	
	private void addMenuBar() {
		JMenuBar menuBar=new JMenuBar();
		setJMenuBar(menuBar);
		
		JMenu menu=new JMenu("选项");
		menuBar.add(menu);
		
		JMenuItem itemSet=new JMenuItem("设置");
		JMenuItem itemHelp=new JMenuItem("帮助");
		
		itemSet.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				final JDialog setDlg=new JDialog(ClientFrom.this);
				setDlg.setBounds(ClientFrom.this.getX(), ClientFrom.this.getY(), 250, 100);
				setDlg.setLayout(new FlowLayout());
				setDlg.add(new JLabel("服务器:"));
				final JTextField tfdIP=new JTextField(10);
				tfdIP.setText(ip);
				setDlg.add(tfdIP);
				setDlg.add(new JLabel("端口:"));
				final JTextField tfdPort=new JTextField(10);
				tfdPort.setText(port+"");
				setDlg.add(tfdPort);
				
				JButton btnSet=new JButton("设置");
				btnSet.setActionCommand("set");
				JButton btnCanel=new JButton("取消");
				btnCanel.setActionCommand("canel");
				setDlg.add(btnSet);
				setDlg.add(btnCanel);
				
				btnSet.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						if("set".equals(e.getActionCommand())){
							if(tfdIP.getText()!=null && tfdIP.getText().trim().length()>0){
								ClientFrom.this.ip=tfdIP.getText();
							}
							if(tfdPort.getText()!=null && tfdPort.getText().trim().length()>0){
								try {
									ClientFrom.this.port=Integer.parseInt(tfdPort.getText());
								} catch (NumberFormatException e1) {
									JOptionPane.showMessageDialog(setDlg, "端口号格式输入错误,请输入数字");
								}
							}
							btnCon.setEnabled(true);
							tfdUserName.setEditable(true);
							if(client!=null){
								//如果前面已经登录着用户,就把用户退出
								String msg="exit@#全部@#null@#"+tfdUserName.getText();
								pw.println(msg);
								dataModel.removeElement(tfdUserName.getText());
								list.validate();
								tfdUserName.setText("");
							}
							
							setDlg.dispose();
						}else if("canel".equals(e.getActionCommand())){
							return;
						}
					}
				});
				setDlg.setVisible(true);
			}
		});
		
		itemHelp.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				JDialog helpDlg = new JDialog(ClientFrom.this);
				helpDlg.setBounds(ClientFrom.this.getX()+10, ClientFrom.this.getY(), 300, 100);
				JLabel str = new JLabel("版权所有@dragon_Dai.QQ:794530831");
				helpDlg.add(str);
				helpDlg.setVisible(true);
			}
		});
		
		menu.add(itemSet);
		menu.add(itemHelp);
		
	}
	
	@Override
	public void actionPerformed(ActionEvent e) {
		if("c".equals(e.getActionCommand())){
			System.out.println(tfdUserName.getText());
			
			if(tfdUserName.getText()==null || tfdUserName.getText().trim().length()==0){
				JOptionPane.showMessageDialog(this, "用户名不能为空");
				return;
			}
			System.out.println(tfdUserName.getText()+":连接ing...");
			connecting();
		}else if("exit".equals(e.getActionCommand())){
			if(tfdUserName.getText()==null || tfdUserName.getText().trim().length()==0){
				int result = JOptionPane.showConfirmDialog(this, "你还没登录,是否退出");
				if(result==JOptionPane.YES_OPTION){
					System.exit(0);
				}else{
					return;
				}
			}
			System.out.println(tfdUserName.getText()+"退出");
			sendExitMsg();
		}else if("send".equals(e.getActionCommand())){
			if(tfdMsg.getText()==null){
				JOptionPane.showMessageDialog(this, "发送消息不能为空");
				return;
			}
			
			String msg="on@#"+list.getSelectedValue()+"@#"+tfdMsg.getText()+"@#"+tfdUserName.getText();
			pw.println(msg);
		}
	}
	private Socket client;
	private PrintWriter pw;
	private void connecting() {
		//与服务器建立连接,把userName传给服务器
		try {
			client=new Socket(ip,port);
			//发送用户名给服务器
			btnCon.setEnabled(false);	//连接成功后关掉连接按钮
			String userName=tfdUserName.getText().trim();
			pw=new PrintWriter(client.getOutputStream(),true);
			pw.println(userName);
			//连接之后,设置标题为userName在线
			setTitle(userName+"在线");
			
			btnSend.setEnabled(true);		//打开发送按钮
			tfdUserName.setEditable(false);		//用户名不能再修改
			
			//开一个线程单独用于跟服务器通信
			new ClientThread(client).start();
			
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
	
	private void sendExitMsg() {
		//与服务器建立连接,把userName传给服务器
		try {
			client=new Socket(ip, port);
			String msg="exit@#全部@#null@#"+tfdUserName.getText();
			pw.println(msg);
			
			System.exit(0);
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
	
	class ClientThread extends Thread{
		private Socket client;
		public ClientThread(Socket client) {
			this.client=client;
		}

		@Override
		public void run() {
			//接收服务器返回的信息
			try {
				Scanner sc=new Scanner(client.getInputStream());
				
				while(sc.hasNext()){
					String msg=sc.nextLine();
					String msgs[]=msg.split("@#");
					if(msgs==null || msgs.length!=3){
						System.out.println("通讯异常");
						return;
					}
					
					if("msg".equals(msgs[0])){
						//表示该信息是用来显示用的
						if("server".equals(msgs[1])){
							//表示该信息是系统信息
							msg="系统信息:"+msgs[2];
							allMsg.append(msg+"\r\n");
						}else{
							//表示该信息聊天信息
							msg=msgs[1]+msgs[2];
							allMsg.append(msg+"\r\n");
						}
					}else if("cmdAdd".equals(msgs[0])){
						//表示该消息是用来更新用户在线列表的,添加用户
						dataModel.addElement(msgs[2]);
					}else if("cmdRed".equals(msgs[0])){
						//表示该消息是用来更新用户在线列表的,移除用户
						dataModel.removeElement(msgs[2]);
					}
					list.validate();	//需要刷新list,不然可能出现list更新失败的bug
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) {
		JFrame.setDefaultLookAndFeelDecorated(true);
		new ClientFrom();
	}
}
服务器:
package cn.hncu.net.sina;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;

import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.border.TitledBorder;


public class ServerFrom extends JFrame{
	
	private JTextArea area;//在线的用户信息显示
	private DefaultListModel<String> dataModel;	//在线的用户列表显示
	
	//注册的用户名不能相同
	//用于存储所有的用户,这里采用注册的"用户名"做key值,通信的socket做value值
	private Map<String, Socket> userMap=new HashMap<String, Socket>();
	
	public ServerFrom() {
		setTitle("聊天服务器");
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		Toolkit toolkit=Toolkit.getDefaultToolkit();
		Dimension dim=toolkit.getScreenSize();
		int runWidth=500;
		int runHeight=400;
		int width=(int) dim.getWidth();
		int height=(int) dim.getHeight();
		//设置界面居中显示
		setBounds(width/2-runWidth/2, height/2-runHeight/2, runWidth, runHeight);
		
		area=new JTextArea();
		area.setEditable(false);
		getContentPane().add(new JScrollPane(area),BorderLayout.CENTER);
		
		//列表显示
		dataModel=new DefaultListModel<String>();
		JList<String> list=new JList<String>(dataModel);
		JScrollPane scroll=new JScrollPane(list);
		scroll.setBorder(new TitledBorder("在线"));
		scroll.setPreferredSize(new Dimension(100, this.getHeight()));
		getContentPane().add(scroll,BorderLayout.EAST);
		
		//菜单
		JMenuBar menuBar=new JMenuBar();
		setJMenuBar(menuBar);
		
		JMenu menu=new JMenu("控制(C)");
		menu.setMnemonic('C');		//设置快捷键为 Alt+C
		menuBar.add(menu);
		//开启
		final JMenuItem itemRun=new JMenuItem("开启");
		//快捷键 Ctrl+R
		itemRun.setAccelerator(KeyStroke.getKeyStroke('R', KeyEvent.CTRL_MASK));
		itemRun.setActionCommand("run");
		menu.add(itemRun);
		//退出
		JMenuItem itemExit=new JMenuItem("退出");
		itemExit.setAccelerator(KeyStroke.getKeyStroke('E', KeyEvent.CTRL_MASK));
		itemExit.setActionCommand("exit");
		menu.add(itemExit);
		
		itemRun.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if("run".equals(e.getActionCommand())){
					startServer();
					itemRun.setEnabled(false);
				}
			}
		});
		
		
		setVisible(true);
	}
	
	private void startServer() {
		try {
			System.out.println("服务器启动");
			ServerSocket server=new ServerSocket(8080);
			area.append("启动服务器:"+server);
			
			//单独开启一个线程用于与客户端握手
			new ServerThread(server).start();
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
	
	class ServerThread extends Thread{
		
		private ServerSocket server;
		public ServerThread(ServerSocket server) {
			this.server=server;
		}

		@Override
		public void run() {
			try {
				while(true){
					Socket s=server.accept();
					//读取客户端第一次向服务器请求的信息
//					BufferedReader br=new BufferedReader(new InputStreamReader(s.getInputStream()));
//					if(br.readLine()!=null){
//						String userName=br.readLine();
//					}
					Scanner sc=new Scanner(s.getInputStream());
					if(sc.hasNext()){
						String userName=sc.next();
						area.append("\r\n"+userName+"上线了。"+s);
						dataModel.addElement(userName);
//						userMap.put(userName, s);	//在后面在把这个用户加入到集合中好一点,那样发送上线信息给所有用户时,就不用判断不发发给自己了。
						
						//登录成功
						//在专门开一个线程用于跟针对某一个客户端通讯
						//根据接收客户端发来的协议判断,客户端进行的是什么样的请求
						new ClientThread(s).start();
						
						//告诉其他用户有人上线了
						sendMsgToAll(userName);
						//把消息其他在线的用户的信息传给登录的这个客户端
						sendMsgToSelf(s);
						
						userMap.put(userName, s);
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	public void sendMsgToAll(String userName) throws IOException{		//这里的异常可以抛,因为调用这个方法的位置抓了IOException
		//遍历map中所有除了该用户之外的客户--此时登录的用户还没有加入到容器中,所有可以直接遍历所有用户
		Iterator<Socket> it = userMap.values().iterator();
		while(it.hasNext()){
			Socket s=it.next();
			PrintWriter pw=new PrintWriter(s.getOutputStream(),true);
			//服务器向客户端发的消息格式设计:
			//命令关键字@#发送方@#消息内容
			String msg="msg@#server@#"+userName+"登录了";	//用于显示用的.
			pw.println(msg);
			msg="cmdAdd@#server@#"+userName;	//用于给客户端维护在线用户列表用的
			pw.println(msg);
			
//			pw.close();
//			s.close();
		}
	}
	
	public void sendMsgToSelf(Socket s) throws IOException{
		
		PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
		Iterator<String> it = userMap.keySet().iterator();
		while(it.hasNext()){
			String userName=it.next();
			System.out.println("map:"+userMap);
			//告诉用户当前在线用户信息,不需要发送显示信息,只需要发送给客户端更新在线列表的信息
			String msg="cmdAdd@#server@#"+userName;
			pw.println(msg);
		}
		
//		pw.close();
	}

	//专门用于跟某一个用户通讯的线程
	class ClientThread extends Thread{
		private Socket s;
		public ClientThread(Socket s) {
			this.s=s;
		}
		@Override
		public void run() {
			try {
				//根据接收客户端发来的协议判断,客户端进行的是什么样的请求
				Scanner sc=new Scanner(s.getInputStream());
				while(sc.hasNextLine()){
					String msg=sc.nextLine();
					String msgs[]=msg.split("@#");
					//简单防黑。
					if(msgs==null || msgs.length!=4){
						System.out.println("通讯异常:"+msg);
						return;
					}
					
					if("on".equals(msgs[0])){//表示客户端的请求是:向别人发送信息
						sendMsgToSb(msgs);
						
					}else if("exit".equals(msgs[0])){//表示客户端发送的请求是:退出(下线)
						area.append("\r\n"+msgs[3]+"下线了"+s);
						dataModel.removeElement(msgs[3]);
						userMap.remove(msgs[3]);
						
						//通知其他所有在线的用户,***退出了
						sendSbExitMsgToAll(msgs);
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	//命令关键字@#接收方@#消息内容@#发送方
	public void sendMsgToSb(String[] msgs) throws IOException {
		//可能是发给所有人,也可能是发给某一个人
		if("全部".equals(msgs[1])){
			//发给所有人(群聊)
			Iterator<String> it = userMap.keySet().iterator();
			while(it.hasNext()){
				String userName=it.next();
				String msg=null;
				if(userName.equals(msgs[3])){
					msg="msg@#"+"我"+"@#说:"+msgs[2];
				}else{
					msg="msg@#"+msgs[3]+"@#说:"+msgs[2];
				}
				Socket s=userMap.get(userName);
				//msg@#消息发送者@#消息内容
				PrintWriter pw=new PrintWriter(s.getOutputStream(), true);
				pw.println(msg);
			}
		}else{
			//发送给某一个人
			String userName=msgs[1];
			Socket s=userMap.get(userName);
			//msg@#消息发送者@#消息内容
			String msg="msg@#"+msgs[3]+"@#悄悄对你说:"+msgs[2];
			PrintWriter pw=new PrintWriter(s.getOutputStream(), true);
			pw.println(msg);
			
			//在发给自己
			Socket s2 = userMap.get(msgs[3]);
			PrintWriter pw2 = new PrintWriter(s2.getOutputStream(), true);
			String str2 = "msg@#"+"我"+"@#对 "+userName+"说:"+msgs[2];
			pw2.println(str2);
		}
	}

	//通知其他所有在线的用户,***退出了
	//1) msg @# server @# 用户[userName]退出了  (给客户端显示用的)
	//2) cmdRed@#server @# userName (给客户端维护在线用户列表用的)
	public void sendSbExitMsgToAll(String[] msgs) throws IOException {
		Iterator<String> it=userMap.keySet().iterator();
		while(it.hasNext()){
			String userName=it.next();
			Socket s=userMap.get(userName);
			PrintWriter pw=new PrintWriter(s.getOutputStream(), true);
			String msg="msg@#server@#用户["+msgs[3]+"]退出了";
			pw.println(msg);
			msg="cmdRed@#server@#"+msgs[3];
			pw.println(msg);
		}
	}

	public static void main(String[] args) {
		JFrame.setDefaultLookAndFeelDecorated(true);
		new ServerFrom();
	}

}



一共包括10个以上的聊天程序版本!绝对物有所值! 为感谢大家长期的支持,我将下载所需的资源分下调为2。网络聊天程序设计(可选)  实验要求 1、分析典型网络聊天应用软件(如QQ、MSN等)的实现原理,模拟设计一套网络聊天应用程序,必须实现以下功能: ①按照C/S结构分别设计服务端程序和客户端程序; ②服务端通过形用户界面实现对服务器的控制,负责维护用户帐户和用户群,并维护用户信息、维持客户端之间的端对端通信和群聊通信、适时维护用户在线信息,并能够发送广播消息。 2、增加尽可能多的功能,用户界面友好,操作简便,代码设计遵从程序设计规范,易读性强,对关键过程和代码进行标注说明。 3、程序设计过程遵从软件工程规范,有需求分析、系统设计和详细设计过程,有相应的规范化说明文档。  实验提示 1、客户端之间的通信是通过服务器进行转发的,对于两个客户端,服务器需要创建两个套接字分别维持与客户端之间的连接。当客户端需要向另一个客户发送消息时,它首先将消息发送到服务器,由服务器根据目标用户帐户转发到目标主机。 2、群聊是采用多播技术实现的,也可以采用单播技术实现,但是服务器开销会增加。具体说来,若采用组播技术,当服务端收到来自一个客户端的消息后,向预先分配的该组组播地址转发该消息。若采用单播技术,服务端需要向该组内的所有客户端一一转发该消息。 3、广播消息通过广播方式发送由服务端创建的消息。 4、服务端根据客户的连接和断开情况,实时向其它客户端发送用户在线信息。 实验题目二:自选网络通信程序设计(可选)  实验要求 可以自选与网络通信相关的设计题目,要求如下: 1、在确定实验题目、设计内容以及设计功能指标要求后,向实验指导教师提交书面申请,由实验指导教师根据所选实验题目的难度和工作量确定立题后方能开始实验。 2、选择的实验题目必须具有一定综合性,并能够利用网络通信原理加以解决,同时需要具备一定的工作量。 3、设计的结果要求用户界面友好,操作简便,代码设计遵从程序设计规范,易读性强,对关键过程和代码进行标注说明。 4、程序设计过程遵从软件工程规范,有需求分析、系统设计和详细设计过程,有相应的规范化说明文档。 5、严禁抄袭别人成果,但可以部分借鉴。
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值