Go_实现聊天室

聊天通讯系统

需求分析

  1. 用户注册
  2. 用户登陆
  3. 显示在线用户列表
  4. 群聊

界面设计

rooms/client/main.go实现,这同样也是main.go文件的全部内容
根据接收用户的选择,是登录、注册,还是退出,登录和注册都需要按要求给出相应的用户信息

var userID int
var userPwd string
var userName string
func main() {

	//接收选择
	var key int

	for true {
		fmt.Println("---------欢迎登陆---------")
		fmt.Println("\t 1 登陆")
		fmt.Println("\t 2 注册")
		fmt.Println("\t 3 退出")
		fmt.Println("\t 请选择")

		fmt.Scanf("%d\n", &key)
		switch key {
		case 1:
			fmt.Println("登陆聊天室")
			fmt.Println("请输入用户id")
			fmt.Scanf("%d\n", &userID)
			fmt.Println("请输入用户密码")
			fmt.Scanf("%s\n", &userPwd)
			//完成登陆
			//创建实例
			up := &process.UserProcess{}
			up.Login(userID,userPwd)
		case 2:
			fmt.Println("注册用户")
			fmt.Println("请输入用户id")
			fmt.Scanf("%d\n",&userID)
			fmt.Println("请输入用户密码")
			fmt.Scanf("%s\n",&userPwd)
			fmt.Println("请输入用户名")
			fmt.Scanf("%s\n",&userName)
			up:=&process.UserProcess{}
			up.Register(userID,userPwd,userName)
		case 3:
			fmt.Println("退出系统")
		default:
			fmt.Println("输入有误,请重新输入")
		}
	}

userProcess.go完成登录和注册的定义
在userProcess.go,可以处理用户的登录,首先与服务器连接,将传来的用户名和密码进行赋值,将数据进行序列化操作,因为数据是要发送的消息,防止丢包,还要限制序列的长度,当登录的状态码=200时,登录则成功。在登陆中,还会对于在线用户进行显示,也就是遍历目前的用户列表,除去自己,都输入。然后登陆成功之后又显示一个登陆后的界面

func(this *UserProcess) Login(userId int,userPwd string) (err error) {
	/*fmt.Printf("userID=%d, userPwd=%s",userID,userPwd)
	return nil*/
	conn,err:=net.Dial("tcp","localhost:8889")
	if err!=nil{
		fmt.Println("Dial err",err)
		return
	}
	//延时关闭
	defer conn.Close()
	//通过conn发送消息给服务
	var mes message.Message
	mes.Type=message.LoginResMesType
	// 创建结构体
	var loginMes message.LoginMes
	loginMes.UserID=userId
	loginMes.UserPwd=userPwd

	//loginMes 序列化
	data,err:=json.Marshal(loginMes)
	if err!=nil{
		fmt.Println("json err",err)
		return
	}
	//把data给mes.data字段
	mes.Data=string(data)
	//将Mse序列化
	data,err=json.Marshal(mes)
	if err!=nil{
		fmt.Println("json err",err)
		return
	}
	//data 就是要发送的消息
	//先发送data的长度 防止丢包
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4],pkgLen)
	n,err :=conn.Write(buf[:4])
	if n!=4 || err!=nil{
		fmt.Println("读取(buf)失败 err",err)
		return
	}
	//fmt.Printf("客户端发送消息长度=%d,内容是%s",len(data),data)

	_,err =conn.Write(data)
	if  err!=nil{
		fmt.Println("读取(data)失败 err",err)
		return
	}
	//返回内容

	//创建Transfer实例
	tf:=&utils.Transfer{
		Conn:conn,
	}
	mes,err= tf.ReadPkg()
	if err!=nil{
		fmt.Println("readPkg err",err)
		return
	}
	//将mes data部分反序列化成loginResMes
	var loginResMes message.LoginResMes
	err = json.Unmarshal([]byte(mes.Data),&loginResMes)
	if loginResMes.Code==200{
		//fmt.Println("登陆成功")
		//显示登陆成功菜单
		//再起协程保持和服务器端的通讯
		//接收并显示在客户端的终端

		//初始化
		CurUser.Conn = conn
		CurUser.UserId=userId
		CurUser.UserStatus=message.UserOnline//显示在线

		//显示当前用户列表
		fmt.Println("当前在线用户列表如下:")
		for _,v:=range loginResMes.UsersId {
			if v==userId{
				continue
			}
			fmt.Printf("用户id:%d\t",v)
			//完成初始化
			user:=&message.User{
				UserId:v,
				UserStatus: message.UserOnline,
			}
			onlineUsers[v]=user
		}



		go serverProcessMes(conn)



		for {
			ShowMenu()
		}
	}else {
		fmt.Println(loginResMes.Error)
	}
	return

}

在同文件下还有同样一个处理注册的,几乎直接是把处理登陆相关的给复制过来,其中只是重新创建了一个注册的结构体,再以相同的方式进行序列化。

func (this *UserProcess) Register(userId int,
	userPwd string, userName string) (err error) {


	//1. 链接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	if err != nil {
		fmt.Println("net.Dial err=", err)
		return
	}
	//延时关闭
	defer conn.Close()

	//2. 准备通过conn发送消息给服务
	var mes message.Message
	mes.Type = message.RegisterMesType
	//3. 创建一个注册 结构体
	var registerMes message.RegisterMes
	registerMes.User.UserId = userId
	registerMes.User.UserPwd = userPwd
	registerMes.User.UserName = userName

	//4.将registerMes 序列化
	data, err := json.Marshal(registerMes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	// 5. 把data赋给 mes.Data字段
	mes.Data = string(data)

	// 6. 将 mes进行序列化化
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	//创建一个Transfer 实例
	tf := &utils.Transfer{
		Conn : conn,
	}

	//发送data给服务器端
	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("注册发送信息错误 err=", err)
	}

	mes, err = tf.ReadPkg() // mes 就是 RegisterResMes

	if err != nil {
		fmt.Println("readPkg(conn) err=", err)
		return
	}

	//将mes的Data部分反序列化成 RegisterResMes
	var registerResMes message.RegisterResMes
	err = json.Unmarshal([]byte(mes.Data), &registerResMes)
	if registerResMes.Code == 200 {
		fmt.Println("注册成功, 请重新登录")
		os.Exit(0)
	} else  {
		fmt.Println(registerResMes.Error)
		os.Exit(0)
	}
	return
}

userMgr.go文件用于输出当前在线用户的列表和更新状态。

var onlineUsers map[int]*message.User = make(map[int]*message.User,10)
var CurUser model.CurUser

func outputOnlineUser(){
	fmt.Println("当前在线用户列表")
	for id,_:=range onlineUsers{
		fmt.Println("用户Id\t",id)

	}
}

//处理返回NotifyUserStatusMes
func updatauserStatus(notifyUserStatusMes *message.NotifyUserStatusMes){

	user,ok:=onlineUsers[notifyUserStatusMes.UserId]
	if !ok{
		//没有 才去创建
		user=&message.User{
			UserId:notifyUserStatusMes.UserId,
			//UserStatus: notifyUserStatusMes.Status,
		}
	}
	user.UserStatus=notifyUserStatusMes.Status
	onlineUsers[notifyUserStatusMes.UserId]=user

	outputOnlineUser()
}

smsProcess.go这里定义函数用于发送群聊消息,将内容传入函数中,再定义相应的类型,并对发出消息的用户ID和用户状态记录下来,在对要发送的数据进行序列化。

func (this *SmsProcess) SendGroupMes(content string) (err error){
	//创建Mes
	var mes message.Message
	mes.Type=message.SmsMesType

	var smsMes message.SmsMes
	smsMes.Content=content
	smsMes.UserId=CurUser.UserId
	smsMes.UserStatus=CurUser.UserStatus

	//序列化
	data,err:=json.Marshal(smsMes)
	if err!=nil{
		fmt.Println("群聊消息序列化失败",err.Error())
		return
	}
	mes.Data=string(data)

	data,err = json.Marshal(mes)
	if err!=nil {
		fmt.Println("群聊消息序列化失败", err.Error())
		return
	}
	tf:=&utils.Transfer{
		Conn: CurUser.Conn,
	}
	err = tf.WritePkg(data)
	if err!=nil{
		fmt.Println("群发失败",err.Error())
		return
	}
	return

群聊消息发送了则需要一个函数去输出群聊的消息,这里定义的函数就是用于将接收到的数据序列化之后进行输出,并输出相应用户的id

func outputGroupMes(mes *message.Message){
	//smsMes类型
	var smsMes message.SmsMes
	err:=json.Unmarshal([]byte(mes.Data),&smsMes)
	if err!=nil{
		fmt.Println("反序列化失败 ",err)
		return
	}
	info:=fmt.Sprintf("用户id %d 发送消息 %s",smsMes.UserId,smsMes.Content)
	fmt.Println(info)
	fmt.Println()
}

上述代码为实现客户端部分

下面的代码是实现对于读取和写入数据进行函数封装,并为了防止丢包,限制了大小,再对于数据进行序列化和反序列化的操作。

type Transfer struct {
	Conn net.Conn
	Buf[8096]byte  //传输时 使用缓冲

}

func(this *Transfer) ReadPkg() (mes message.Message,err error) {
	//buf := make([]byte, 8096)
	fmt.Println("读取客户端发送的数据")
	_, err = this.Conn.Read(this.Buf[:4])
	if err != nil {
		//fmt.Println("读取错误 err", err)
		//err = errors.New("read pkg header error")
		return
	}
	var pkgLen uint32
	pkgLen=binary.BigEndian.Uint32(this.Buf[0:4])

	//从 conn读 扔到buf
	n, err := this.Conn.Read(this.Buf[:pkgLen])
	if n!=int(pkgLen)||err != nil {
		fmt.Println("read pkg body error", err)
		return
	}
	err = json.Unmarshal(this.Buf[:pkgLen], &mes)
	if err != nil {
		fmt.Println("反序列化出错",err)
		return
	}
	return

}
func (this *Transfer) WritePkg(data[]byte)(err error){

	//先发送长度
	var pkgLen uint32
	pkgLen = uint32(len(data))
	//var buf [4]byte
	binary.BigEndian.PutUint32(this.Buf[0:4],pkgLen)
	n,err :=this.Conn.Write(this.Buf[:4])
	if n!=4 || err!=nil{
		fmt.Println("读取(buf)失败 err",err)
		return
	}
	//发送数据本身
	n,err =this.Conn.Write(data)
	if n!=int(pkgLen) || err!=nil{
		fmt.Println("读取(buf)失败 err",err)
		return
	}
	return
}

初次之外,还定义了服务器端和客户机端都共用的,当然上述客户端也用到了其中的一些函数和方法。
定义的是message包下,用于区别不用的信息,比如说登录信息、注册信息,用户状态、发送的消息等,它们都是一个结构体,比如登录信息就要包括登录的ID、密码和登录名,而对应这个登录信息的也又另外一个结构体去实现它的状态码,并对于一些不正确状态码去返回错误信息。

const (
	LoginMseType= "LoginMes"
	LoginResMesType="LoginResMes"
	RegisterMesType = "RegisterMes"
	RegisterResMesType="RegisterResMes"
	NotifyUserStatusMesType = "NotifyUserStatusMes"
	SmsMesType = "SmsMes"
)
const(
	UserOnline = iota
	UserOfline
	UserBusyStatus
)

type User struct {
	UserId int `json:"userId"`
	UserPwd string `json:"userPwd"`
	UserName string `json:"userName"`
	UserStatus int `json:"userStatus"`

}
type Message struct {
	Type string `json:"type"`
	Data string `json:"data"`
}


type LoginMes struct {
	UserID int `json:"userId"`
	UserPwd string`json:"userPwd"`
	UserName string`json:"userName"`

}
type LoginResMes struct {
	Code int`json:"code"`//状态码 500 未注册   200登陆成功
	Error string`json:"error"` //返回错误
	UsersId []int //保存用户ID

}

type RegisterMes struct {
	User User`json:"user"`
}
type RegisterResMes struct {
	Code int `json:"code"`
	Error string `json:"error"`
}

//用户状态
type NotifyUserStatusMes struct {
	UserId int `json:"userId"`
	Status int `json:"status"`
}

//发送的消息
type SmsMes struct {
	Content string `json:"content"`  //内容
	User
}

下面开始服务器端的代码部分,服务器端main文件主要是处理与客户端通讯,实现与客户端等待客户端连接,连接成功就保持通讯状态,和对redis相连接,再定义redis的连接池,到时候需要时直接调用函数去取出相应的数据即可,并开启协程用于通讯。

func process(conn net.Conn){
	defer conn.Close()
	//调用总控函数

	processor:= &Processor{
		Conn:conn,
	}
	err:=processor.process2()
	if err!=nil{
		fmt.Println("客户端和服务器端通信协程错误",err)
		return
	}
	
func init() {
	initPool("localhost:6379",16,0,300*time.Second)
	initUserDao()
}

//编写函数完成 UserDao的初始化
func initUserDao(){
	model.MyUserDao=model.NewUserDao(pool)
}
func main() {
	//服务器启动时, 初始化连接池
	fmt.Println("服务器在端口监听")
	listen,err:=net.Listen("tcp","0.0.0.0:8889")
	defer listen.Close()
	if err!=nil{
		fmt.Println("监听出错 err",err)
		return
	}
	//监听成功则等待连接服务器
	for {
		fmt.Println("等待客户端连接")
		conn,err:=listen.Accept()
		if err!=nil{
			fmt.Println("连接出错 err",err)
		}
		//启动协程 和客户端保持通讯
		go process(conn)


	}
}

下面是定义连接池的初始化,对它一些属性进行定义。

var pool *redis.Pool

func initPool(addredss string,maxIdle,maxActive int,idleTimeout time.Duration){
	pool = &redis.Pool{
		MaxIdle: maxIdle,//最大空闲连接数
		MaxActive: maxActive, //和数据库最大连接数
		IdleTimeout: idleTimeout, //最大空闲时间
		Dial: func()(redis.Conn,error){
			return redis.Dial("tcp",addredss)
		},
	}
}

在服务器端,会根据传来消息类型来进行相应不同的处理,比如是登录消息类型,就实例化进行登录相关的处理,如果是注册那就执行注册。

//创建processor的结构体
type Processor struct {
	Conn  net.Conn
}

//根据客户端发送消息钟类不同,决定调用那个函数来处理
func (this *Processor)severProcessMes(mes *message.Message) (err error){

	switch mes.Type {
		case message.LoginResMesType:
			//处理登陆
			//创建实例
			up:=&process2.UserProcess{
				Conn:this.Conn,
}

			err =up.SeverProcessLogin(mes)
		case message.RegisterMesType:
			//处理注册
			up:=&process2.UserProcess{
				Conn:this.Conn,
			}
			err =up.SeverProcessRegister(mes)
		case message.SmsMesType:
			//完成转发消息
			smsProcess:=&process2.SmsProcess{}
			smsProcess.SendGroupMes(mes)

		default:
			fmt.Println("消息类型不存在,无法处理")

	}
	return
}
func (this *Processor) process2() (err error){
	//读取客户端发送
	for {
		tf:=&utils.Transfer{
			Conn :this.Conn,
		}
		mes,err:=tf.ReadPkg()
		if err!=nil{
			if err==io.EOF{
				fmt.Println("客户端退出,服务器端也退出")
				return err
			}else {
				fmt.Println("readPkg err", err)
				return err
			}
		}
		//fmt.Println("mes=",mes)
		err =this.severProcessMes(&mes)
		if err!=nil{
			return err
		}
	}

}

对于已经定义好的连接池,需要对登录注册和连接池同步,先创建实例,在去通过id去在连接池中查找这个人,如果没有提示请注册的错误信息,如果又的化则验证密码是否正确,注册的话也要从连接池中拿出看之前有没有注册过。

var (
	MyUserDao *UserDao
)
type UserDao struct {
	pool *redis.Pool
}
//创建UserDao实例
func NewUserDao(pool *redis.Pool)(userDao *UserDao){

	userDao=&UserDao{
		pool: pool,
	}
	return
}
func (this *UserDao) getUserById(conn redis.Conn,id int) (user *User,err error){
	//通过id去redis去查询用户
	res,err:=redis.String(conn.Do("HGet","users",id))
	if err!=nil{
		if err==redis.ErrNil{
			//表示在user 哈希中没有找到对应的ID
			err = ERROR_USER_NOTEXISTS
		}
		return
	}
	user = &User{}
	//需要把 res发序列化成User实例
	err = json.Unmarshal([]byte(res),user)
	if err!=nil{
		fmt.Println("反序列化错误 err",err)
		return
	}
	return
}

//完成登陆校验
func (this *UserDao)Login(userId int,usePwd string)(user *User,err error) {

	//先从UserDao 连接池中取出连接
	conn:=this.pool.Get()
	defer conn.Close()
	user,err= this.getUserById(conn,userId)
	if err!=nil{
		return
	}

	//获取到用户
	//密码不同则错误
	if user.UserPwd!=usePwd{
		err=ERROR_USER_PWD
		return
	}
	return

}

//注册
func (this *UserDao)Register(user *message.User)(err error) {

	//先从UserDao 连接池中取出连接
	conn:=this.pool.Get()
	defer conn.Close()
	_,err= this.getUserById(conn,user.UserId)
	if err!=nil{
		err = ERROR_USER_EXISTS
		return
	}
    //ID在redis还没有注册 则可以完成注册
	//序列化
	data,err:=json.Marshal(user)
	if err!=nil{
		return
	}
	//入库
	_,err=conn.Do("Hset","users",user.UserId,string(data))
	if err!=nil {
		fmt.Println("保存注册用户错误 err", err)
		return
	}
	return
}

对于错误信息都定义在这里。

var (
	ERROR_USER_NOTEXISTS = errors.New("用户不存在")
	ERROR_USER_EXISTS = errors.New("用户已存在")
	ERROR_USER_PWD = errors.New("密码不正确")
)

之后就是服务器端处理客户端发送来的登录和注册的命令。登录根据传来的处理信息类型,将数据如用户id和密码做处理,并对不同的错误类型赋予不同的状态码值,正确的才是200,对于成功登录的用于信息,作于相应的记录,也就是广播通知其他在线该用户已上线。

func (this *UserProcess) SeverProcessLogin(mes *message.Message) (err error){
	//从mes中取出data,并直接反序列化成LoginMes
	var loginMes message.LoginMes
	err = json.Unmarshal([]byte(mes.Data),&loginMes)
	if err!=nil{
		fmt.Println("反序列化失败 err",err)
		return
	}
	//先声明resMes
	var resMes message.Message
	resMes.Type = message.LoginResMesType

	//再声明LoginResMes
	var loginResMes message.LoginResMes


	user,err:=model.MyUserDao.Login(loginMes.UserID,loginMes.UserPwd)
	if err!=nil{
		if err==model.ERROR_USER_NOTEXISTS{
			loginResMes.Code=500 //错误
			loginResMes.Error=err.Error()
		}else if err==model.ERROR_USER_PWD{
			loginResMes.Code=403
			loginResMes.Error=err.Error()
		}else{
			loginResMes.Code=505
			loginResMes.Error="服务器内部错误"

		}
	}else{
		loginResMes.Code=200
		//登陆成功  将成功登入的放入
		this.UserId=loginMes.UserID
		userMgr.AddonlineUser(this)
		//通知其他用户 上线
		this.NotifyOthersOnlineUser(loginMes.UserID)
		//遍历在线用户
		for id,_:=range userMgr.onlineUsers{
			loginResMes.UsersId=append(loginResMes.UsersId,id)
		}
		fmt.Println(user,"登录成功")

	//将loginResMes序列化
	data,err:=json.Marshal(loginResMes)
	if err!=nil{
		fmt.Println("loginResMes 序列化失败",err)
		return
	}

	//将data赋值给resMes
	resMes.Data=string(data)

	//再对它序列化 因为是结构体
	data,err=json.Marshal(resMes)
	if err!=nil{
		fmt.Println("loginResMes 序列化失败",err)
		return
	}
	//fmt.Printf("客户端发送的消息 长度为%d 内容为%s",len(data),string(data))
	//发送data
	//先创建transfer实例,然后读取
	tf:=&utils.Transfer{
		Conn:this.Conn,
	}
	err=tf.WritePkg(data)
	return
}
	}

处理注册也与登录差不多,甚至还要简单一点,它只用序列化反序列化等,并提示用户出错的消息和出错类型。

func (this *UserProcess)SeverProcessRegister(mes *message.Message)(err error){
	var registerMes message.RegisterMes
	err=json.Unmarshal([]byte(mes.Data),&registerMes)
	if err!=nil{
		fmt.Println("序列化出错 err",err)
		return
	}
	var resMes message.Message
	resMes.Type=message.RegisterMesType
	var registerResMes message.RegisterResMes

	//到数据库完成注册
	err = model.MyUserDao.Register(&registerMes.User)
	if err != nil {
		if err == model.ERROR_USER_EXISTS {
			registerResMes.Code = 505
			registerResMes.Error = model.ERROR_USER_EXISTS.Error()
		} else {
			registerResMes.Code = 506
			registerResMes.Error = "注册发生未知错误..."
		}
	} else {
		registerResMes.Code = 200
	}

	data, err := json.Marshal(registerResMes)
	if err != nil {
		fmt.Println("json.Marshal fail", err)
		return
	}
   //将data 赋值给 resMes
	resMes.Data = string(data)

	//对resMes 进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fa  il", err)
		return
	}
	//6. 发送data, 我们将其封装到writePkg函数
	tf := &utils.Transfer{
		Conn : this.Conn,
	}
	err = tf.WritePkg(data)
	return
}

再有两个函数,一个是用于验证在线的用户,根据传来的信息类型,还是将数据做序列化反序列化等操作,第二是通知在线的用户,遍历当前在线用户就可以了

//通知在线用户
func (this *UserProcess)NotifyOthersOnlineUser(userId int){
	//遍历
	for id,up:=range userMgr.onlineUsers{
		if id==userId{
			continue//除去自己不通知
		}
		up.NotifyMeonline(userId)

	}
}

func (this *UserProcess)NotifyMeonline(userId int)  {
	var mes message.Message
	mes.Type=message.NotifyUserStatusMesType

	var notifyUserStatusMes  message.NotifyUserStatusMes
	notifyUserStatusMes.UserId=userId
	notifyUserStatusMes.Status=message.UserOnline

	data,err:=json.Marshal(notifyUserStatusMes)
	if err!=nil{
		fmt.Println("序列化错误 err",err)
		return
	}
	mes.Data=string(data)
	data,err=json.Marshal(mes)
	if err!=nil{
		fmt.Println("序列化错误 err",err)
		return
	}

	tf:=&utils.Transfer{
		Conn:this.Conn,
	}
	err = tf.WritePkg(data)
	if err!=nil{
		fmt.Println("NotifyMeOnline err",err)
		return
	}
}

转发消息也定义了两个函数去实现转发的效果。首先还是通过将消息序列化,再遍历,除去自己都进行发送操作。

func (this *SmsProcess) SendGroupMes(mes *message.Message) {
	//遍历服务器端map将消息转发
	var smsMes message.SmsMes
	err:=json.Unmarshal([]byte(mes.Data),&smsMes)
	if err!=nil{
		fmt.Println("反序列化失败 ",err)
	}
	data,err :=json.Marshal(mes)
	if err!=nil{
		fmt.Println("序列化出错",err)
		return
	}
	for id,up:=range userMgr.onlineUsers{
		//过滤自己 不给自己发送消息
		if id ==smsMes.UserId{
			continue
		}
		this.SendEachOnlineUser(data,up.Conn)
	}
}

func (this *SmsProcess)SendEachOnlineUser(data []byte,conn net.Conn)  {
	//创建TransFer
	tf:=&utils.Transfer{
		Conn: conn,
	}
	err:=tf.WritePkg(data)
	if err!=nil{
		fmt.Println("转发消息失败 ",err)
	}
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值