springboot+bee+sotoken+redis+webSocket 即时通讯,发送给指定用户

1、在pom.xml中引入相关依赖

<!-- 集成websocket依赖  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- 集成redis依赖  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.创建获取上下文(getBean)的工具类

package com.bjprd.config;


import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
 
/**
 * @author Jiang
 * @create 2022-06-15 14:17
 */
@Component
public class SpringUtils implements ApplicationContextAware {
    private static ApplicationContext context;
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
 
    public static void set(ApplicationContext applicationContext) {
        context = applicationContext;
    }
 
    /**
     * 通过字节码获取
     * @param beanClass
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
 
    /**
     * 通过BeanName获取
     * @param beanName
     * @param <T>
     * @return
     */
    public static <T> T getBean(String beanName) {
        return (T) context.getBean(beanName);
    }
 
    /**
     * 通过beanName和字节码获取
     * @param name
     * @param beanClass
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name, Class<T> beanClass) {
        return context.getBean(name, beanClass);
    }
}

3.创建Redis工具类,主要调用publish()方法。用于redis发布消息

package com.bjprd.config.redis;

import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

@Component
public class RedisUtil {
    @Autowired
    private RedisTemplate redisTemplate;
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 写入缓存
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
    /**
     * 写入缓存设置时效时间
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value, Long expireTime ,TimeUnit timeUnit) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, timeUnit);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
    /**
     * 批量删除对应的value
     * @param keys
     */
    public void remove(final String... keys) {
        for (String key : keys) {
            remove(key);
        }
    }
    /**
     * 批量删除key
     * @param pattern
     */
    public void removePattern(final String pattern) {
        Set<Serializable> keys = redisTemplate.keys(pattern);
        if (keys.size() > 0){
            redisTemplate.delete(keys);
        }
    }
    /**
     * 删除对应的value
     * @param key
     */
    public void remove(final String key) {
        if (exists(key)) {
            redisTemplate.delete(key);
        }
    }
    /**
     * 判断缓存中是否有对应的value
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }
    /**
     * 读取缓存
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }
    /**
     * 哈希 添加
     * @param key
     * @param hashKey
     * @param value
     */
    public void hmSet(String key, Object hashKey, Object value){
        HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
        hash.put(key,hashKey,value);
    }
    /**
     * 哈希获取数据
     * @param key
     * @param hashKey
     * @return
     */
    public Object hmGet(String key, Object hashKey){
        HashOperations<String, Object, Object>  hash = redisTemplate.opsForHash();
        return hash.get(key,hashKey);
    }
    /**
     * 列表添加
     * @param k
     * @param v
     */
    public void lPush(String k,Object v){
        ListOperations<String, Object> list = redisTemplate.opsForList();
        list.rightPush(k,v);
    }
    /**
     * 列表获取
     * @param k
     * @param l
     * @param l1
     * @return
     */
    public List<Object> lRange(String k, long l, long l1){
        ListOperations<String, Object> list = redisTemplate.opsForList();
        return list.range(k,l,l1);
    }
    /**
     * 集合添加
     * @param key
     * @param value
     */
    public void add(String key,Object value){
        SetOperations<String, Object> set = redisTemplate.opsForSet();
        set.add(key,value);
    }
    /**
     * 集合获取
     * @param key
     * @return
     */
    public Set<Object> setMembers(String key){
        SetOperations<String, Object> set = redisTemplate.opsForSet();
        return set.members(key);
    }
    /**
     * 有序集合添加
     * @param key
     * @param value
     * @param scoure
     */
    public void zAdd(String key,Object value,double scoure){
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        zset.add(key,value,scoure);
    }
    /**
     * 有序集合获取
     * @param key
     * @param scoure
     * @param scoure1
     * @return
     */
    public Set<Object> rangeByScore(String key,double scoure,double scoure1){
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        return zset.rangeByScore(key, scoure, scoure1);
    }
    
    /**
     * 发布
     *
     * @param key
     */
    public void publish(String channel, Object message) {
    	stringRedisTemplate.convertAndSend(channel, message);
    }
}

4.创建redis配置文件

package com.bjprd.config.redis;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import cn.dev33.satoken.stp.StpUtil;

@Configuration //相当于xml中的beans
public class RedisConfig {
	
	
	@Bean("container")
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,
    		MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        // 添加订阅者监听类,数量不限.PatternTopic定义监听主题,这里监听test-topic主题
        container.addMessageListener(listenerAdapter, new PatternTopic("topic_all"));//建立topic_all通道
        container.addMessageListener(listenerAdapter, new PatternTopic("topic_byid"));
        return container;
    }
	
	@Bean
    MessageListenerAdapter listenerAdapter(RedisMessageListener receiver) {
        //设置监听
		//利用反射机制,调用RedisMessageListener类中onMessage()方法
        return new MessageListenerAdapter(receiver,"onMessage");
    }

    @Bean
    StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
        //StringRedisTemplate继承了RedisTemplate,是专门用于字符串操作
        return new StringRedisTemplate(connectionFactory);
    }
}

5.创建WebSocketConfig

package com.bjprd.config.webSocket;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * 这个配置类的作用是要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。
 * 如果是使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
 */
@Component
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

6.创建redis发送消息类

package com.bjprd.config.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Component;

import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
/**
 * 获取在线用户 id、token
 * @author jiang
 *
 * 2022年6月15日上午8:35:55
 */
@Component
public class FindAllOnelineUsers {
	
	public Map<String, List<String>> findAllOnelineUsers() {
	    // 获取 StpUtil 登录体系下的实际逻辑对象(单独把 StpLogic 提取出来是为了方便后期修改 StpUtil 的名字
	    StpLogic stpLogic = SaManager.getStpLogic(StpUtil.getLoginType());
	    // 获取所有 token ,如果数量庞大可以分页
	    List<String> tokens = stpLogic.searchTokenValue("", -1, -1);
	    // 保存所有用户id及对应的不同平台下的临时有效时长
	    Map<String, List<String>> idTokensMap = new HashMap<>();
	    for (String sourceToken: tokens) {
	        // 截取真实 token 值
	        String token = sourceToken.substring(sourceToken.lastIndexOf(":")+1);
	        // 获取临时有效期
	        long activityTimeout = stpLogic.getTokenActivityTimeoutByToken(token);
	        if (activityTimeout == -2) {
	            // 当前 token 所代表的会话已经临时过期了, 直接跳过
	            continue;
	        }
	        // 尝试根据 token 获取 loginId
	        Object loginIdByToken = stpLogic.getLoginIdByToken(token);
	        if (loginIdByToken != null) {
	            // 转换为原始类型, 这里根据登录时的参数类型动态修改
	            String loginId = (String)loginIdByToken;
	            // 每个用户id可以多次登录, 也可以在不同平台登录
//	            String loginTokenAndActivityTimeout = token + "|"+activityTimeout;
	            String loginTokenAndActivityTimeout = token;
	            // 同一个用户如果多端登录需要同时记录不同平台的时限
	            if (idTokensMap.containsKey(loginId)) {
	                // 如果有这个 loginId 直接获取并添加
	                idTokensMap.get(loginId).add(loginTokenAndActivityTimeout);
	            } else {
	                // 如果没有则新建并绑定 loginId
	                List<String> infoArr = new ArrayList<>();
	                infoArr.add(loginTokenAndActivityTimeout);
	                idTokensMap.put(loginId, infoArr);
	            }
	        }
	    }
	    return idTokensMap;
	}
	
	public Integer getUserId(){
		Object loginId = StpUtil.getLoginId();
		Integer creatorId = (Integer)loginId;
		return creatorId;
	}
}
package com.bjprd.config.redis;




import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.websocket.Session;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

import com.bjprd.config.SpringUtils;
import com.bjprd.config.db.SuidRichUtils;
import com.bjprd.config.util.FindAllOnelineUsers;
import com.bjprd.config.webSocket.WebsocketEndpoint;
import com.bjprd.entity.MessageTable;

import cn.dev33.satoken.stp.StpUtil;

@Component
public class RedisMessageListener implements MessageListener {
	
	@Autowired
	FindAllOnelineUsers findAUsers;
	
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	WebsocketEndpoint websocketEndpoint = (WebsocketEndpoint)SpringUtils.getBean("websocketEndpoint");

	//用户的session
    private Session session;

    //用户的ID
    private String userId;

 

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public Session getSession() {
        return session;
    }

    public void setSession(Session session) {
        this.session = session;
    }

	@Override
    public void onMessage(Message message, byte[] bytes) {

        
        // 获主题名称
        String channel = new String(bytes);
        String msg = new String(message.getBody());     //消息体
        if (msg!= null) {
            synchronized (this) {
                System.out.println("redis中订阅消息");
                if(channel.equals("topic_byid")){
                	//根据自身的业务需要编写
                	//下面是通过用户id,调用socket给对应的用户发送信息
                	
                	String[] strarray=message.toString().split (",");
                	for(String id : strarray){
                		Map<String, List<String>> findAllUsers = findAUsers.findAllOnelineUsers();
                		writeMessage(id);
                		if(findAllUsers.containsKey(id)){
                			websocketEndpoint.sendMessageToUser("您收到一条审核信息!",id);
                		}
                	}
                	
                }else if(channel.equals("topic_all")){
//                	writeMessage(message.toString());
                	//根据自身的业务需要编写
                	//下面是通过用户id,调用socket给对应的用户发送信息
                     websocketEndpoint.sendMessageToUser(message.toString(),"userId");
                }
            }
        } 
    }
    /**
     * 自定义类,用于把消息保存进数据库
     *  逻辑可自定义
     * @param id
     * @return
     */
    private void writeMessage(String id) {
        if (id != null) {
        	Integer creatorId = findAUsers.getUserId();
        	 MessageTable messageTable = new MessageTable();
	         messageTable.setMessage("您收到一条审核信息!");
	         messageTable.setRecipientId(Integer.valueOf(id));//接受人id
	         messageTable.setCreatorId(creatorId);//创建人id
	         messageTable.setCreationTime(new Date());//创建时间
	         SuidRichUtils.suidRich.insert(messageTable);
        }
    }
}


7、创建WebsocketEndpoint用于处理websocket请求

package com.bjprd.config.webSocket;

import com.alibaba.fastjson.JSONObject;
import com.bjprd.config.SpringUtils;
import com.bjprd.config.redis.RedisMessageListener;
import com.bjprd.config.redis.RedisUtil;

import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.RestController;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * userId 用户id
 * topic redis中的主题
 */
@ServerEndpoint("/socket/controller/{userId}")
@RestController
@Slf4j
public class WebsocketEndpoint {
	
    private RedisUtil redisUtil = SpringUtils.getBean("redisUtil");

    /***
     * 用来记录当前连接数的变量
     */
    private static AtomicInteger onlineCount = new AtomicInteger(0);


    /***
     * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象
     */
//    private static CopyOnWriteArraySet<WebsocketEndpoint> webSocketSet = new CopyOnWriteArraySet<WebsocketEndpoint>();

    private static ConcurrentHashMap<String, WebsocketEndpoint> webSocketSet = new ConcurrentHashMap<>();

	//用户id
    private String tokenid;

    /**
     * 得到线程池,执行并发操作
     */
    private ThreadPoolTaskExecutor threadPoolTaskExecutor = SpringUtils.getBean(ThreadPoolTaskExecutor.class);

    /**
     * 与某个客户端的连接会话,需要通过它来与客户端进行数据收发
     */
    private Session session;

    private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketEndpoint.class);

    //用来引入刚才在RedisConfig注入的类
    private RedisMessageListenerContainer container = SpringUtils.getBean("container");

    // 自定义redis监听器
    private RedisMessageListener listener2;

    /***
     * socket打开的处理逻辑
     * @param session
     * @param tokenid
     * @throws Exception
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) throws Exception {
        LOGGER.info(String.format("用户:%s 打开了Socket链接", tokenid));
        this.session = session;
        this.session.isOpen();
        this.tokenid = tokenid;
        //webSocketSet中存当前用户对象
        webSocketSet.put(userId,this);
        //在线人数加一
        addOnlineCount();
        listener2 = new RedisMessageListener();
        // 放入session
        listener2.setSession(session);
        // 放入用户ID
//        listener2.setUserId(userId);
		 //初始化监听器
        container.addMessageListener(listener2, new PatternTopic(userId));
    }

    /**
     * socket关闭的处理逻辑
     */
    @OnClose
    public void onClose() {
        // 删除当前对象(this)
        webSocketSet.remove(this);
        subOnlineCount();
        getOnlineCount();
        container.removeMessageListener(listener2);
        LOGGER.info(String.format("%s关闭了Socket链接Close a html ", tokenid));
    }


    /**
     * socket收到消息的处理逻辑
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        getOnlineCount();
        LOGGER.info("收到一条数据消息----------" + message + "----------------------------------------");
        //可以自己根据业务处理
        try {

	         
            // socket心跳返回
            Map map = new HashMap();
            map.put("type", "0");
            map.put("data", "soeket连接已建立");
            map.put("message", message);
            JSONObject jsonObject = new JSONObject(map);
//            this.sendMessage(jsonObject.toJSONString());
            redisUtil.publish("topic_byid", message);
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }

    /**
     * 加一方法
     */
    public volatile int p = 0;

    public synchronized void addOne() {
        p++;
        System.out.println(Thread.currentThread().getName() + "------->" + "自增==>" + p);
    }

    /**
     * socket链接错误
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        LOGGER.error("socket链接错误", error);
    }

    /**
     * 发送消息
     *
     * @param message
     */
    public void sendMessage(String message) {
        if (session.isOpen()) {
//            getOnlineCount();
            try {
				session.getBasicRemote().sendText(message);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
        }
    }


    /**
     * 发送给指定用户
     *
     * @param message
     * @param userid
     */
    public void sendMessageToUser(String message, String userid) {

        if (userid != null) {
            webSocketSet.get(userid).sendMessage(message);
            LOGGER.info("消息發送成功");
            //System.out.println("消息發送成功");
        }

    }


    //AtomicInteger是线程安全的 不需要synchronized修饰
    public static AtomicInteger getOnlineCount() {
        System.out.println(new Date() + "在线人数为" + onlineCount);
        return onlineCount;
    }

    //AtomicInteger是线程安全的 内置自增与自减的方法getAndIncrement()
    public static void addOnlineCount() {
        WebsocketEndpoint.onlineCount.getAndIncrement();
    }

    //AtomicInteger是线程安全的 内置自增与自减的方法getAndDecrement()
    public static void subOnlineCount() {
        WebsocketEndpoint.onlineCount.getAndDecrement();
    }
}

8.创建controller层方法,用于返回页面想要的数据

@ResponseBody
	@GetMapping("/index")
	public Map<String, Object> index(HttpServletRequest request,HttpServletResponse response) {
		Map<String, Object> map = new HashMap<String, Object>();
		try {
			Object loginId = StpUtil.getLoginId();
			ConditionImpl conditionImpl = new ConditionImpl();
			conditionImpl.op("recipient_id", Op.eq, loginId);
			conditionImpl.op("state", Op.eq, "01");
			List<MessageTable> select = SuidRichUtils.suidRich.select(new MessageTable(), conditionImpl);
			map.put("data", select);
			map.put("id", loginId);
		} catch (Exception e) {
			// TODO: handle exception
			e.getMessage();
		}
		return map;
	}

9.前端

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
    <script src="../js/jquery-1.8.3.min.js" charset="utf-8"></script>
</head>
<body>
	<div>
		<div>
			<input type="text" id="inputid"><button onclick="fs()">发送</button>
		</div>
		<div>
		<input type="text" id="price">
		<input type="text" id="hiddenid">
		</div>
	</div>

</body>
<script>
var searchURL = window.location.search;
	searchURL = searchURL.substring(1, searchURL.length);
var id = searchURL.split("&")[0].split("=")[1];//0-新增;1-修改

		var websocket;
		
	   $(function () {
		   // var id = searchURL.split("&")[1].split("=")[1];
                $.ajax({
                    url: '/test/index',
                    async: false,
//                    data: {id: 20},
                    success: function (data) {
                        console.log(data);
                        var datas = data.data;
                        var id = data.id;
                        $("#hiddenid").val(id);
                        console.log(websocket);
                        $.each(datas, function (index, item) {
                           alert(item.message);
                        });
                    }
                });
               if (window.WebSocket){
            	   var id = document.getElementById('hiddenid').value;
            	   websocket = new WebSocket("ws://127.0.0.1:8080/socket/controller/"+id+"");
            	   console.log(id);
//       			websocket = new WebSocket("ws://127.0.0.1:8080/socket/controller/"+id+"");
       			//连接成功建立的回调方法
       		        websocket.onopen = function () {
       		          //  websocket.send("客户端链接成功");
       		        }
       		 
       		        //接收到消息的回调方法
       		        websocket.onmessage = function (event) {
       		            setMessageInnerHTML(event.data);
       		        }
       		         
       		        //连接发生错误的回调方法
       		        websocket.onerror = function () {
       		            alert("WebSocket连接发生错误");
       		        };
       		 
       		       //连接关闭的回调方法
       		        websocket.onclose = function () {
       		            alert("WebSocket连接关闭");
       		        }
       		 
       		        //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
       		        window.onbeforeunload = function () {
       		            closeWebSocket();
       		        }
       		         
       		    }
       		    else {
       		        alert('当前浏览器 Not support websocket')
       		    }
		  });
		    //将消息显示在网页上
		    function setMessageInnerHTML(innerHTML) {
		    	console.log(innerHTML);
		      /*   var bitcoin = eval("("+innerHTML+")");
		        console.log(bitcoin); */
		        $("#price").val(innerHTML);
		       // document.getElementById('price').innerHTML = bitcoin.price;
		      //  document.getElementById('total').innerHTML = bitcoin.total;
		    }
		 
		    //关闭WebSocket连接
		    function closeWebSocket() {
		        websocket.close();
		    }

		    function fs(){
		    	var message = document.getElementById('inputid').value;
		        websocket.send(message);
			}
		    
		    
		    
</script>
</html>

个人业务逻辑,用于借鉴

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值