【Springboot实例】WebSocket即时聊天室设计与实现

一、基本介绍

1.1 什么是WebSocket?

WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。它是客户端与服务器端的交互更加简便,在客户端与服务器端建立连接后,两者可以创建持久的连接,服务器端可主动发送数据给客户端,并进行双向通信。

1.2 与传统Ajax轮查的区别?

传统的Ajax技术是在设立一定的时间间隔后(比如1s)对服务器端发送HTTP请求进行查询,这种轮查是有很明显的问题,那就是浪费带宽资源,与传统Ajax轮查对比下,WebSocket技术对资源的使用就更加合理了。

对比图

二、WebSocket功能介绍

2.1 HTML

2.1.1 HTML中WebSocket方法属性

方法处理程序描述
opensocket.onopen与服务器连接时触发
closesocket.onclose与服务器断开时触发
messagesocket.onmessage收到服务器发送消息时触发
errorsocket.onerror通信错误时触发

2.1.2 HTML中WebSocket基本方法

方法描述
socket.send()发送数据
socket.close()关闭连接

2.1.2 HTML中WebSocket使用方法

  1. 实例化一个WebSocket对象
  2. 指定要连接的端口
  3. 注意端口连接名称(ws:http请求;wss:https请求),注意证书名

2.2 Springboot

2.2.1 pom.xml文件添加WebSocket方法

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2.2.2 Springboot中WebSocket属性

注解方法描述
@ServerEndpoint声明接口的注解
@PostConstruct初始化调用的方法
@OnOpen连接建立成功调用的方法
@OnClose连接关闭调用的方法
@OnMessage收到客户端消息后调用的方法
@OnError出现错误调用的方法

三、即时聊天室设计实例代码

3.1后端代码

3.1.1 WebSocketServer

package cn.chairc.platform.utils;

import cn.chairc.platform.config.WebSocketConfig;
import cn.chairc.platform.entity.Chat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @auther chairc
 * @date 2021/2/8 15:34
 */
@ServerEndpoint(value = "/chatOnline/websocket", encoders = {WebSocketServerEncoder.class}, configurator = WebSocketConfig.class)
@Component
public class WebSocketServer {

    private static Logger log = LoggerFactory.getLogger(WebSocketServer.class); //slf4j

    private static final AtomicInteger onlineCount = new AtomicInteger(0);
    //当前在线数
    private static int cnt;
    // concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
    private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();

    /**
     * 初始化
     */

    @PostConstruct
    public void init() {
        log.info("websocket 已加载加载!!!");
    }

    /**
     * 连接建立成功调用的方法
     *
     * @param session 加入连接的session
     * @throws IOException IO异常
     */

    @OnOpen
    public void onOpen(Session session) throws IOException {
        Chat chat = new Chat();
        SessionSet.add(session);    //在数据集中添加新打开的session
        cnt = onlineCount.incrementAndGet();    //当前在线数加1
        log.info("有连接加入,当前连接数为:{}", cnt);
        chat.setChatPrivateId("SystemIn");
        String username = (String) session.getUserProperties().get("username");
        String sid = (String) session.getUserProperties().get("sid");
        chat.setChatText("当前" + username + "(" + sid + ")进入聊天室");
        BroadCastInfo(chat);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param session 加入连接的session
     * @throws IOException IO异常
     */

    @OnClose
    public void onClose(Session session) throws IOException {
        Chat chat = new Chat();
        SessionSet.remove(session);     //在数据集中移除关闭调用的session
        cnt = onlineCount.decrementAndGet();    //当前在线数减1
        chat.setChatPrivateId("SystemOut");
        log.info("有连接关闭,当前连接数为:{}", cnt);
        String username = (String) session.getUserProperties().get("username");
        String sid = (String) session.getUserProperties().get("sid");
        chat.setChatText("当前" + username + "(" + sid + ")离开聊天室");
        BroadCastInfo(chat);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     * @param session 加入连接的session
     */

    @OnMessage
    public void onMessage(String message, Session session) {
        Chat chat = new Chat();
        //chat.setChatText("收到消息,消息内容:" + message);
        //log.info("来自客户端的消息:{}", message);
        SendMessage(session, chat);
    }

    /**
     * 出现错误
     *
     * @param session 加入连接的session
     * @param error   错误
     */

    @OnError
    public void onError(Session session, Throwable error) {
        //log.error("发生错误:{},Session ID: {}", error.getMessage(), session.getId());
        error.printStackTrace();
    }

    /**
     * 发送消息
     *
     * @param session 加入连接的session
     * @param chat    聊天
     */

    private static void SendMessage(Session session, Chat chat) {
        try {
            //session.getBasicRemote().sendText(String.format("%s (来自服务器,Session ID=%s)", chat.getChat_text(), session.getId()));
            //ObjectMapper objectMapper = new ObjectMapper();
            //session.getBasicRemote().sendText(objectMapper.writeValueAsString(chat));
            chat.setChatroomPeople(cnt);
            session.getBasicRemote().sendObject(chat);//需要解码器
        } catch (IOException | EncodeException e) {
            log.error("发送消息出错:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 群发消息(单对多聊天)
     *
     * @param chat 聊天
     * @throws IOException IO异常
     */

    public static void BroadCastInfo(Chat chat) throws IOException {
        for (Session session : SessionSet) {
            if (session.isOpen()) {
                SendMessage(session, chat);
            }
        }
    }

    /**
     * 指定Session发送消息(单对单聊天)
     *
     * @param sessionId 加入连接的session
     * @param chat      聊天
     * @throws IOException IO异常
     */

    public static void SendMessage(Chat chat, String sessionId) throws IOException {
        Session session = null;
        for (Session s : SessionSet) {
            if (s.getId().equals(sessionId)) {
                session = s;
                break;
            }
        }
        if (session != null) {
            SendMessage(session, chat);
        } else {
            log.warn("没有找到你指定ID的会话:{}", sessionId);
        }
    }

}

3.1.2 ChatController

package cn.chairc.platform.controller;

import cn.chairc.platform.entity.Chat;
import cn.chairc.platform.entity.ResultSet;
import cn.chairc.platform.service.UserService;
import cn.chairc.platform.service.ChatService;
import cn.chairc.platform.utils.CommonUtil;
import cn.chairc.platform.utils.TimeUtil;
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.text.ParseException;

/**
 * @auther chairc
 * @date 2021/2/8 15:46
 */
@Controller
@RequestMapping("/api/websocket")
public class ChatController {

    private static Logger log = LoggerFactory.getLogger(ChatController.class); //slf4j

    private UserService userService;

    private ChatService chatService;

    @Autowired
    public ChatController(UserService userService, ChatService chatService) {
        this.userService = userService;
        this.chatService = chatService;
    }

    @Value("${upload-file.head-file-path}")
    private String UPLOAD_HEAD_PATH;

    @Value("${head-image.user-head-image-path}")
    private String USER_HEAD_IMAGE_PATH;

    /**
     * 群发消息
     *
     * @param message 消息
     * @return ResultSet结果
     */

    @RequestMapping("/sendAll")
    @ResponseBody
    public ResultSet sendAllMessage(@RequestParam(required = true, value = "chatText") String message,
                                    HttpServletRequest request) throws ParseException {
        String headUrl;
        Chat chat = new Chat();
        chat.setChatPrivateId(CommonUtil.createRandomPrivateId("chat"));
        chat.setChatUserPrivateId(CommonUtil.sessionValidate("privateId"));
        chat.setChatUsername(CommonUtil.sessionValidate("username"));
        chat.setChatText(StringEscapeUtils.escapeHtml3(message));
        chat.setChatTime(TimeUtil.exchangeTimeTypeDateToString(TimeUtil.getServerTime()));
        chat.setChatIp(CommonUtil.getUserIp(request));
        chat.setChatBrowser(CommonUtil.getBrowserVersion(request));
        chat.setChatSystem(CommonUtil.getSystemVersion(request));
        File file = new File(UPLOAD_HEAD_PATH + CommonUtil.sessionValidate("privateId") + "thumbnail.jpg");
        if (file.exists()) {
            headUrl = USER_HEAD_IMAGE_PATH + CommonUtil.sessionValidate("privateId") + "thumbnail.jpg?r=" + (int) (Math.random() * 10000);
        } else {
            headUrl = USER_HEAD_IMAGE_PATH + "default-head-image.svg?=" + (int) (Math.random() * 10000);
        }
        chat.setHeaderUrl(headUrl);
        return chatService.sendChatAll(message, chat);
    }
}

3.1.3 ChatService

package cn.chairc.platform.service;

import cn.chairc.platform.entity.Chat;
import cn.chairc.platform.entity.ResultSet;


/**
 * @auther chairc
 * @date 2021/2/8 15:38
 */
public interface ChatService {

    /**
     * 群发消息
     *
     * @param message 消息
     * @param chat chat
     * @return ResultSet结果集
     */

    ResultSet sendChatAll(String message, Chat chat);
}

3.1.4 ChatServiceImpl

package cn.chairc.platform.service.impl;

import cn.chairc.platform.entity.Chat;
import cn.chairc.platform.entity.ResultSet;
import cn.chairc.platform.service.ChatService;
import cn.chairc.platform.utils.CommonUtil;
import cn.chairc.platform.utils.TimeUtil;
import cn.chairc.platform.utils.WebSocketServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;

/**
 * @auther chairc
 * @date 2021/2/8 15:38
 */

@Service
public class ChatServiceImpl implements ChatService {

    private static Logger log = LoggerFactory.getLogger(ChatServiceImpl.class); //slf4j

    /**
     * 群发消息
     *
     * @param message 消息
     * @param chat    chat
     * @return ResultSet结果集
     */

    @Override
    public ResultSet sendChatAll(String message, Chat chat) {
        ResultSet resultSet = new ResultSet();
        if (CommonUtil.isUserOnline()) {
            try {
                WebSocketServer.BroadCastInfo(chat);
                resultSet.ok("ok");
                log.info("用户{}发送消息({})成功,ip{},浏览器{},系统{}", chat.getChatUsername(), chat.getChatText(),
                        chat.getChatIp(), chat.getChatBrowser(), chat.getChatSystem());
            } catch (IOException e) {
                log.error(e.toString());
            }
        } else {
            //未登录
            log.info("发送消息失败,用户未登录");
            resultSet.fail("用户未登录");
        }
        return resultSet;
    }
}

3.1.5 Chat

package cn.chairc.platform.entity;

/**
 * @auther chairc
 * @date 2021/2/8 15:36
 */
public class Chat {
    private String chatPrivateId;           //聊天信息私有ID
    private String chatUserPrivateId;       //聊天用户私有ID
    private String chatUsername = "System"; //聊天用户名
    private String chatText;                //聊天文本
    private String chatTime;                //时间
    private String chatIp;                  //IP
    private String chatBrowser;             //浏览器
    private String chatSystem;              //系统
    private int chatroomPeople;             //聊天室用户数
    private String headerUrl;               //头像

    public String getChatPrivateId() {
        return chatPrivateId;
    }

    public void setChatPrivateId(String chatPrivateId) {
        this.chatPrivateId = chatPrivateId;
    }

    public String getChatUserPrivateId() {
        return chatUserPrivateId;
    }

    public void setChatUserPrivateId(String chatUserPrivateId) {
        this.chatUserPrivateId = chatUserPrivateId;
    }

    public String getChatUsername() {
        return chatUsername;
    }

    public void setChatUsername(String chatUsername) {
        this.chatUsername = chatUsername;
    }

    public String getChatText() {
        return chatText;
    }

    public void setChatText(String chatText) {
        this.chatText = chatText;
    }

    public String getChatTime() {
        return chatTime;
    }

    public void setChatTime(String chatTime) {
        this.chatTime = chatTime;
    }

    public String getChatIp() {
        return chatIp;
    }

    public void setChatIp(String chatIp) {
        this.chatIp = chatIp;
    }

    public String getChatBrowser() {
        return chatBrowser;
    }

    public void setChatBrowser(String chatBrowser) {
        this.chatBrowser = chatBrowser;
    }

    public String getChatSystem() {
        return chatSystem;
    }

    public void setChatSystem(String chatSystem) {
        this.chatSystem = chatSystem;
    }

    public int getChatroomPeople() {
        return chatroomPeople;
    }

    public void setChatroomPeople(int chatroomPeople) {
        this.chatroomPeople = chatroomPeople;
    }

    public String getHeaderUrl() {
        return headerUrl;
    }

    public void setHeaderUrl(String headerUrl) {
        this.headerUrl = headerUrl;
    }

    @Override
    public String toString() {
        return "Chat{" +
                "chatPrivateId='" + chatPrivateId + '\'' +
                ", chatUserPrivateId='" + chatUserPrivateId + '\'' +
                ", chatUsername='" + chatUsername + '\'' +
                ", chatText='" + chatText + '\'' +
                ", chatTime='" + chatTime + '\'' +
                ", chatIp='" + chatIp + '\'' +
                ", chatBrowser='" + chatBrowser + '\'' +
                ", chatSystem='" + chatSystem + '\'' +
                ", chatroomPeople=" + chatroomPeople +
                ", headerUrl='" + headerUrl + '\'' +
                '}';
    }
}

3.1.6 ResultSet

package cn.chairc.platform.entity;

/**
 * @auther chairc
 * @date 2021/1/14 20:44
 */
//返回前端的验证结果集
public class ResultSet {

    private String code;            //返回码
    private String msg;             //返回信息
    private Object data = "";     //返回数据,默认设为空,需要返回数据时,使用setData()方法

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "ResultSet{" +
                "code='" + code + '\'' +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }

    /**
     * 自定义成功返回文本
     * @param msg 自定义文本
     */

    public void ok(String msg){
        this.code = "200";
        this.msg = msg;
    }

    /**
     * Bad Request 请求存在错误或参数错误
     * @param msg 自定义文本
     */

    public void fail(String msg){
        this.code = "400";
        this.msg = msg;
    }

    /**
     * OK 返回成功
     */

    public void ok() {
        this.code = "200";
        this.msg = "ok";
    }

    /**
     * Unauthorized 请求需要有HTTP认证或者认证失败
     */

    public void unauthorized() {
        this.code = "401";
        this.msg = "用户未登录,需要身份认证";
    }

    /**
     * 请求资源的访问被服务器拒绝
     */

    public void forbidden() {
        this.code = "403";
        this.msg = "服务器拒绝请求";
    }

    /**
     * 请求资源服务器未找到
     */

    public void notFound() {
        this.code = "404";
        this.msg = "请求资源不存在";
    }

    /**
     * 服务器执行请求出错
     */

    public void interServerError() {
        this.code = "500";
        this.msg = "服务器内部错误";
    }
}

3.2 前端代码

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>聊天室</title>
    <link rel="icon" th:href="@{/static/images/ico/favicon.ico}">
    <link rel="stylesheet" th:href="@{/static/css/bootstrap.min.css}">
    <link rel="stylesheet" th:href="@{/static/css/font-awesome.min.css}">
    <link rel="stylesheet" th:href="@{/static/css/animate.min.css}">
    <link rel="stylesheet" th:href="@{/static/css/main.css}">
    <link rel="stylesheet" th:href="@{/static/css/responsive.css}">
</head>
<body>
<div class="platform" id="user-val" th:value="${userPrivateId}">
    <header th:replace="header.html"></header>
    <main class="main-container">
        <div class="main-nav"></div>
        <div class="main-left main-left-normal">
            <div class="main-left-container fadeInDown animated animated-setting">
                <div class="main-box shadow chatroom-container">
                    <h2 class="main-title">聊天室</h2>
                    <div class="main-context" id="chat-data">

                    </div>
                    <div class="main-context">
                        <div style="width: 80%;height: 80px;float: left;box-sizing: border-box;padding: 10px 5%;">
                            <textarea id="chat-text"></textarea>
                        </div>
                        <div style="width: 20%;height: 70px;float: left;box-sizing: border-box;margin: auto;line-height: 70px;">
                            <button class="btn btn-info" onclick="sendChat()" style="width: 80%">发送</button>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="main-right main-right-normal">
            <div class="main-left-container fadeInDown animated animated-setting">
                <div class="main-box shadow">
                    <h2 class="main-title">聊天室公告</h2>
                    <div class="main-context">
                        <div class="form-group">
                            <div class="main-context">
                                <b>聊天室要求</b>
                                <p>聊天时:文明用语,文明交流。</p>
                                <p>同学间:互相尊重,相互帮助。</p>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="main-box shadow">
                    <h2 class="main-title">聊天室状态</h2>
                    <div class="main-context">
                        <div class="form-group">
                            <div class="main-context">
                                <p id="chatroom-people"></p>
                                <p id="chatroom-status"></p>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="main-box shadow">
                    <footer th:replace="footer.html"></footer>
                </div>
            </div>
        </div>
    </main>
</div>
<div class="message-box-warp">
    <div id="message-box" class="message-box">
        <p id="message-box-text"></p>
    </div>
</div>
</body>
<script type="text/javascript" th:src="@{/static/js/jquery-min.js}"></script>
<script type="text/javascript" th:src="@{/static/js/bootstrap.min.js}"></script>
<script type="text/javascript" th:src="@{/static/js/main.js}"></script>
<script>
    var socket;
    if (typeof (WebSocket) == "undefined") {
        console.log("遗憾:您的浏览器不支持WebSocket");
    } else {
        console.log("恭喜:您的浏览器支持WebSocket");
        //实现化WebSocket对象
        //指定要连接的服务器地址与端口建立连接
        //注意ws、wss使用不同的端口。我使用自签名的证书测试,
        //无法使用wss,浏览器打开WebSocket时报错
        //ws对应http、wss对应https。
        url = "ws://" + window.location.host + "/chatOnline/websocket";
        //console.log(url);
        socket = new WebSocket(url);
        //连接打开事件
        socket.onopen = function () {
            console.log("Socket 已打开");
            //socket.send("消息发送测试(来自客户端)");
        };
        //收到消息事件
        socket.onmessage = function (data) {
            console.log(data.data);
            var currentUser = $("#user-val").attr("value");
            var map = eval("(" + data.data + ")");
            var html;
            if(currentUser !== map["chatUserPrivateId"] && "SystemIn" !== map["chatPrivateId"] && "SystemOut" !== map["chatPrivateId"]){
                html = "<div class=\"chat-box\">\n" +
                    "           <div class=\"chat-title\">\n" +
                    "               <div class=\"chat-head-image chat-left\">\n" +
                    "               <a href=\"/user/"+ map["chatUserPrivateId"] +"\">\n" +
                    "                   <img src=\""+ map["headerUrl"] +"\" width=\"100%\" height=\"100%\" style=\"border-radius: 50%;\">\n" +
                    "               </a>" +
                    "               </div>" +
                    "               <div class=\"chat-info chat-left\">" +
                    "                   <span><b>"+ map["chatUsername"] +"</b></span>\n" +
                    "                   <span>"+ map["chatTime"] +"</span>" +
                    "               </div>" +
                    "           </div>" +
                    "           <div class=\"chat-context\">" +
                    "               <div class=\"chat-context-style\">" +
                    "                   <p>"+ map["chatText"] +" </p>" +
                    "               </div>\n" +
                    "           </div>\n" +
                    "       </div>";
            }else if("SystemIn" === map["chatPrivateId"] || "SystemOut" === map["chatPrivateId"]){
                html = "<p>" + map["chatText"]+ "来了</p>"
            }else if(currentUser === map["chatUserPrivateId"]){
                html = "<div class=\"chat-box\">\n" +
                    "           <div class=\"chat-title\">\n" +
                    "               <div class=\"chat-head-image chat-right\">\n" +
                    "               <a href=\"/user/"+ map["chatUserPrivateId"] +"\">\n" +
                    "                   <img src=\""+ map["headerUrl"] +"\" width=\"100%\" height=\"100%\" style=\"border-radius: 50%;\">\n" +
                    "               </a>" +
                    "               </div>" +
                    "               <div class=\"chat-info chat-right\">" +
                    "                   <span>"+ map["chatTime"] +"</span>" +
                    "                   <span><b>"+ map["chatUsername"] +"</b></span>\n" +
                    "               </div>" +
                    "           </div>" +
                    "           <div class=\"chat-context\">" +
                    "               <div class=\"chat-context-style\">" +
                    "                   <p>"+ map["chatText"] +" </p>" +
                    "               </div>\n" +
                    "           </div>\n" +
                    "       </div>";
            }
            $("#chat-data").append(html);
            $("#chatroom-people").html("<p id='chatroom-people'>当前在线数:"+ map["chatroomPeople"] +"</p>")
            //原生DOM
            var divscll = document.getElementById("chat-data");
            divscll.scrollTop = divscll.scrollHeight;
        };
        //连接关闭事件
        socket.onclose = function () {
            console.log("Socket已关闭");
        };
        //发生了错误事件
        socket.onerror = function () {
            alert("Socket发生了错误");
        };
        //窗口关闭时,关闭连接
        window.unload = function () {
            socket.close();
        };
    }

    $(document).keypress(function (e) {
        // 回车键事件
        if (e.which === 13) {
            sendChat();
        }
    });

    function sendChat() {
        var chatText = $("#chat-text").val();
        $.ajax({
            url: "/api/websocket/sendAll",
            dataType: "JSON",
            data: {
                "chatText": chatText
            },
            contentType: "application/json; charset=utf-8",
            success: function (data) {
                if (data.code === "200") {
                    //提交成功
                    $("#chat-text").val("");
                    $("#chat-text").focus();
                } else {
                    messageBoxFailure(data);
                    messageBoxSetTimeout();
                }
            }
        })
    }
</script>
</html>

3.3 实例图

3.3.1 登录系统

登录系统

3.3.2 进入聊天室

进入聊天室

3.3.3 进行聊天

进行聊天

3.3.4 离开聊天室

离开聊天室

四、最后

  1. 目前只放出关于WebSocket即时聊天室源码,后期整个项目会上传到GitHub
  2. 对文章有疑问的地方可以留言给我,我会定时看消息
  3. 我的github
  4. 我的个人网站
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ChairC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值