第7章:Spring Boot的Web开发(2)

7.3 Web相关配置

7.3.1 Spring Boot提供的自动配置

通过查看WebMvcAutoConfiguration及WebMvcProperties的源码,可以发现Spring Boot为我们提供了如下的自动配置.

1.自动配置的ViewResolver
(1)ContentNegotiatingViewResolver
这是Spring MVC提供的一个特殊的ViewResolver,ContentNegotiatingViewResolver不是自己处理View,而是代理给不同的ViewResolver来处理不同的View,所以它有最高的优先级.

(2)BeanNameViewResolver
在控制器(@Controller)中的一个方法的返回值的字符串(视图名)会根据BeanNameViewResolver去查找Bean的名称为返回字符串的View来渲染视图.是不是不好理解,下面举个小例子

定义BeanNameViewResolver的Bean:

@Bean
public BeanNameViewResolver beanNameViewResolver(){
    BeanNameViewResolver resolver = new BeanNameViewResolver();
    return resolver;
}

定义一个View的Bean,名称为jsonView:

@Bean
public MappingJackson2JsonView jsonView(){
MappingJackson2JsonView jsonView = new MappingJacksonJsonView();
return jsonView;
}

在控制器中,返回字符串jsonView,它会找Bean的名称为jsonView的视图来渲染:

@RequestMapping(value="/json",produces={MediaType.APPLICATION_JSON_VALUE})
public String json(Model model){
    Person single = new Person("aa",11);
    model.addAttribute("single",single);
    return "jsonView";
}

(3) InternalResourceViewResolver
这个是一个极为常用的ViewResolver,主要通过前缀、后缀,以及控制器中方法来返回视图的字符串,已得到实际页面,Spring Boot的源码如下:

@Bean
@ConditionalOnMissingBean(InternalResourceViewResolver.class)
public InternalResourceViewResolver defaultViewResolver(){
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix(this.prefix);
    resolver.setSuffix(this.suffix);
    return resolver;
}

2.自动配置的静态资源
在自动配置类的addResourceHandlers方法中定义了以下静态资源的自动配置.
(1)类路径的/static、/public、/resource和/META-INF/resources文件夹下的静态文件直接映射为/,可以通过http://localhost:8080/来访问.
(2)webjar
何谓webjar,webjar就是将我们常用的脚本框架封装在jar包中的jar包,更多关于webjar的内容请访问http://www.webjars.org网站.

把webjar的/META-INF/resources/webjars/下的静态文件映射为/webjar/,可以通过http://localhost:8080/webjar/来访问.

3.自动配置的Formatter和Converter
关于自动配置的Formatter和Converter,我们可以看一下WebMvcAutoConfiguration类中的定义:

7.3.2 接管Spring Boot的Web配置

如果Spring Boot 提供的Spring MVC不符合要求,则可以通过一个配置类(注解有@Configuration的类)加上@EnableWebMvc注解来实现完全自己控制的MVC配置.

当然,通常情况下,Spring Boot的自动配置是符合我们大多数需求的.在你既需要保留Spring Boot提供的便利,又需要增加自己的额外配置的时候,可以定义一个配置类来继承WebMvcConfigurerAdapter,无需使用@EnableWebMvc注解,然后按照第4章讲解的Spring MVC的配置方法来添加Spring Boot为我们所做的其他配置,例如:

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter{
    @Override
    public void addViewControllers(ViewControllerRegistry registry){
        registry.addViewController("/xx").setViewName("/xx");
    }
}

值得指出的是,在这里重写的addViewControllers方法,并不会覆盖WebMvcAutoConfiguration中的addViewControllers(在此方法中,Spring Boot将”/”映射至index.html),这也意味着我们自己的配置和Spring Boot的自动配置同时有效,这也是我们推荐添加自己的MVC的配置方式.

7.3.3 注册Servlet、Filter、Listener

当使用嵌入式的Servlet容器(Tomcat、Jetty等)时,我们通过将Servlet、Filter和Listener声明为Spring Bean而达到注册的效果,或者注册ServletRegistrationBean、FilterRegistrationBean和ServletListenerRegistrationBean的Bean.

(1)直接注册Bean的示例,代码如下:

@Bean
public XxServlet xxServlet(){
    return new XxServlet();
}

@Bean
public ZzListener zzListener(){
    return new ZzListener();
}

(2)通过RegistrationBean示例:

@Bean
public ServletRegistrationBean servletRegistrationBean(){
    return new ServletRegistrationBean(new XxServlet(),"/xx/x");
}

@Bean
pulic FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new YyFilter());
registrationBean.setOrder(2);
return regidtrationBean;
}

@Bean
public ServletListenerRegistrationBean<ZzListener> zzListenerServletRegistrationBean(){
return new ServletListenerRegistrationBean<ZzListener>(new ZzListener());
}

7.4 Tomcat配置

本节虽然Tomcat配置,但其实指的是servlet容器的配置,因为Spring Boot默认内嵌的Tomcat为servlet容器,所以本节只讲对Tomcat配置,其实本节的配置对Tomcat、Jetty和Undertow都是通用的.

7.4.1 配置tomcat

关于tomcat的所有属性都在org.springframework.boot.autoconfigure.web.ServerProperties配置类中做了定义,我们只需要application.properties配置属性做配置即可.通用Servlet容器配置都以”server”作为前缀,而Tomcat特有配置都以”server.tomcat”作为前缀,下面举一些常用的例子

server.port=#配置程序端口,默认为8080#
server.session-timeout=#用户会话session过期时间,以秒为单位#
server.context-path=#用户访问路径,默认为/#

配置Tomcat:

server.tomcat.uri-encoding=#配置tomcat编码,默认为UTF-8#
server.tomcat.compression=#Tomcat是否开启压缩,默认关闭off#

更为详细的Servlet容器配置以及Tomcat配置,请查阅官网

7.4.2 代码配置tomcat

如果你需要通过代码的方式配置servlet容器,则可以注册一个实现EmbeddedServeletContainerCustomizer接口的Bean;若想直接配置Tomcat、Jetty、Undertow则可以直接定义TomcatEmbeddedServletContainerFactory、JettyEmbeddedServletContainerFactory、UndertowEmbeddedServletContainerFactory.

1.通用配置
(1)新建类的配置(2.0版本)

public class CustomServletContainer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {
        factory.setPort(9000);
        factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,"/404.html"));
    }
}

(2) 当前配置文件内配置.若要在当前已有的配置文件内添加类的Bean的话,则在Spring配置中,注意当前类要声明为static:(写在启动类中)

@Component
  public static class CustomServletContainer  implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>{

      @Override
      public void customize(ConfigurableServletWebServerFactory factory) {
          factory.setPort(9000);
          factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,"/404.html"));
      }
  }

2.特定配置
下面以Tomcat为例(Jetty使用JettyEmbeddedServletContainerFactory,Undertow使用UndertowEmbeddedServletContainerFactory):

@Bean
public ServletWebServerFactory servletContainer() {
    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
    tomcat.addAdditionalTomcatConnectors(createSslConnector()); // 添加http
    return tomcat;
}

// 配置https
private Connector createSslConnector() {
    Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
    Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
    try {
        File keystore = new ClassPathResource("sample.jks").getFile();
        /*File truststore = new ClassPathResource("sample.jks").getFile();*/
        connector.setScheme("https");
        connector.setSecure(true);
        connector.setPort(port);
        protocol.setSSLEnabled(true);
        protocol.setKeystoreFile(keystore.getAbsolutePath());
        protocol.setKeystorePass(key_store_password);
        protocol.setKeyPass(key_password);
        return connector;
    }
    catch (IOException ex) {
        throw new IllegalStateException("can't access keystore: [" + "keystore"
                + "] or truststore: [" + "keystore" + "]", ex);
    }
}

7.4.3 替换tomcat

Spring Boot默认使用Tomcat作为内置Servlet作为内嵌Servlet容器,查看spring-boot-starter-web依赖,如果想要使用Jetty或者Undertow为servlet容器,只需要修改spring-boot-starter-web的依赖即可

1.替换为Jetty
在pom.xml中,将spring-boot-strater-web的依赖由spring-boot-starter-tomcat替换为spring-boot-starter-Jetty

<dependency>
 <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
   <exclusions>
       <exclusion>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-tomcat</artifactId>
       </exclusion>
   </exclusions>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

7.4.4 SSL配置

SSL的配置也是我们在实际应用中经常遇到的场景
SSL(Secure Sockets Layer 安全套阶层)是为网络通信提供安全及数据完整性的一种安全协议,SSL在网络传输层对网络进行加密.SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通信提供安全支持.SSL协议可分为两层:SSL记录协议,它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装,压缩,加密等基本功能的支持,SSL握手协议,它建立在SSL记录协议之上,用于实际数据传输开始前,通信双方进行身份认证,协商加密算法,交换机密秘钥等

而在基于B/S的Web应用中,是通过HTTPS来实现SSL的,HTTPS是以安全为目标的HTTP通道,简单讲HTTP的安全版,即在HTTP层下加SSL层,HTTPS的安全基础是SSL.

因为Spring Boot用的是内嵌的Tomcat,因而我们做SSL配置的时候需要做如下操作

1.生成证书
使用SSL受限需要一个证书,这个证书可以是自签名的,也可以是从SSL证书授权中心获得的

每一个JDK或者JRE里都有一个工具叫keytool,它是一个证书管理工具,可以用来生成自签名的证书
在配置了JAVA_HOME,并将JAVA_HOME的bin目录加入到Path后,即可在控制台调用该命令

2.spring Boot配置SSL以及http转向到https

7.5 Favicon配置

7.5.1 默认的Favicon

Spring Boot提供了一个默认的Favicon,每次访问应用的时候都能看到

7.5.2 关闭Favicon

我们可以在application.properties中设置关闭Favicon,默认为开启

spring.mvc.favicon.enabled=false

7.5.3 设置自己的Favicon

若需要设置自己的Favicon,则只需要将自己的favicon.ico(文件名不能改动)文件放置在类路径根目录、类路径META-INF/recources/下、类路径resources/下、类路径static/下或类路径public/下.这里将favicon.ico放置在src/main/resources/static下.

7.6 WebSocket

7.6.1 什么是WebSocket

WebSocket为浏览器和服务器提供了双工异步通信的功能,即浏览器可以向服务端发送消息,服务端也可以向浏览器发送消息.WebSocket需要浏览器的支持

WebSocket是通过一个socket来实现双工异步通信能力的.但是直接使用WebSocket(或者SockJS:WebSocket协议的模拟,增加了当浏览器不支持WebSocket的时候的兼容支持)协议开发程序闲的特别繁琐,我们会使用它的子协议STOMP,它是一个更高级别的协议,STOMP协议使用了一个基于帧(frame)的格式来定义消息,与HTTP的request和response类似(具有类似于@RequestMapping和@MessageMapping)

7.6.2 Spring Boot提供的自动配置

Spring Boot对内嵌的Tomcat(7或8)、Jetty9和Undertow使用WebSocket提供了支持.配置源码存于org.springframework.boot.autoconfigure.websocket下
Spring Boot为WebSocket提供了stater pom是spring-boot-starter-websocket.

1.广播式
广播式即服务端有消息时,会将消息给所有连接了当前endpoint(端点)的浏览器.
(1)配置WebSocket,需要在配置类上使用@EnableWebSocketMessageBroker开启WebSocket支持,并通过继承AbstractWeb

@Configuration
@EnableWebSocketMessageBroker//1
public class WebSocketConfig  extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry){//2
        registry.addEndpoint("/endpointWisely").withSockJS();//3
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry){//4
        registry.enableSimpleBroker("/topic");//5
    }
}

代码解释:
①:开启使用STOMP协议来传输基于代理(message broker)的消息,这是控制器支持使用@MessageMapping,就像使用@RequestMapping一样
②:注册STOMP协议的节点(endpoint),并映射到指定的URL
③:注册一个STOMP的endpoint,并指定使用SockJS协议
④:配置消息代理(Message Broker)
⑤:广播式应配置一个/topic消息代理

(2)浏览器向服务端发送消息用此类接受:

public class WiselyMessage {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

(3)服务端向浏览器发送的此类消息:

public class WiselyResponse {
    private String responseMessage;

    public String getResponseMessage() {
        return responseMessage;
    }

    public WiselyResponse(String responseMessage) {
        this.responseMessage = responseMessage;
    }
}

(4)演示控制器,代码如下:

@Controller
public class WsController {

    @MessageMapping("/welcome")
    @SendTo("/topic/getResponse")
    public WiselyResponse say(WiselyMessage message) throws Exception{
        Thread.sleep(3000);
        return new WiselyResponse("Welcome, "+message.getName()+"!");
    }
}

代码解释
①:当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似于@RequestMapping
②:当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息

(5)添加脚本.将stomp.min.js(STOMP协议的客户端脚本)、sockjs.min.js(SockJS的客户端脚本)以及jQuery放置在src/mainmresources/static下

(6)演示页面.在src/main/resources/templates下新建ws.html,代码如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Spring Boot+WebSocket+广播式</title>

</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">貌似你的浏览器不支持websocket</h2></noscript>
<div>
    <div>
        <button id="connect" onclick="connect();">连接</button>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
    </div>
    <div id="conversationDiv">
        <label>输入你的名字</label><input type="text" id="name" />
        <button id="sendName" onclick="sendName();">发送</button>
        <p id="response"></p>
    </div>
</div>
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
<script type="text/javascript">
    var stompClient = null;

    function setConnected(connected) {
        document.getElementById('connect').disabled = connected;
        document.getElementById('disconnect').disabled = !connected;
        document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
        $('#response').html();
    }

    function connect() {
        var socket = new SockJS('/endpointWisely'); //1
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function(frame) {
            setConnected(true);
            console.log('Connected: ' + frame);
            stompClient.subscribe('/topic/getResponse', function(respnose){ //2
                showResponse(JSON.parse(respnose.body).responseMessage);
            });
        });
    }


    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log("Disconnected");
    }

    function sendName() {
        var name = $('#name').val();
        //3
        stompClient.send("/welcome", {}, JSON.stringify({ 'name': name }));
    }

    function showResponse(message) {
          var response = $("#response");
          response.html(message);
    }
</script>
</body>
</html>

代码解释:
①:连接SockJS的endpoint名称为”/endpointWisely”
②:使用STOMP子协议的WebSocket客户端
③:连接WebSocket服务端
④:通过stompClient.subscribe订阅/topic/getResponse目标(destination)发送的消息,这个是在控制器的@SendTo中定义的
⑤:通过stompClient.send向/welcome目标(destination)发送消息,这个是在控制器的@MessageMapping中定义的

(7)配置viewController,为ws.html提供便捷的映射路径:

public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry){
        registry.addViewController("/ws").setViewName("/ws");
    }
}

(8)运行,我们的预期效果是:当一个浏览器发送一个消息到服务端时,其他浏览器也能接收到从服务端发送来的这个消息.

开启三个浏览器窗口,并访问http://localhost:8080/ws 分别连接服务器.然后一个浏览器发送一条消息

2.点对点式
广播式有自己的应用场景,但是广播式不能解决我们一个常见的场景,即消息由谁发送、由谁接收的问题.
本例中演示一个简单的聊天程序.例子中有两个用户,互相发送消息给彼此,因需要用户相关的内容,所以先在这里引入最简单的Spring Security相关内容.

(1)添加Spring Security的starter pom

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

(2) Spring Security的简单配置.这里不对Spring Security做过多的解释,值解释对本项目由帮助的部分:

public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }
}

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/login").permitAll()//1
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")//2
                .defaultSuccessUrl("/chat")//3
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }

    //4
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder())
                .withUser("wyf").password("wyf").roles("USER")
                .and()
                .withUser("wisely").password("wisely").roles("USER");
    }

    @Override
    public void configure(WebSecurity web){
        web.ignoring().antMatchers("/resources/static/**");
    }
}

代码解释:
①:设置Spring Security 对/和/login路径不拦截
②:设置Spring Security的登陆页面访问的路径为/login
③:登陆成功后转向/chat路径
④:在内存中分别配置两个用户wyf和wisely,密码和用户名一致,角色是USER.
⑤:/resource/static/目录下的静态资源,Spring Security不拦截

(3)配置WebSocket:

@Configuration
@EnableWebSocketMessageBroker//1
public class WebSocketConfig  extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry){
        registry.addEndpoint("/endpointWisely").withSockJS();
        registry.addEndpoint("/endpointChat").withSockJS();//1
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry){
        registry.enableSimpleBroker("/queue","/topic");//2
    }
}

代码解释:
①:注册一个名为/endpointChat的endpoint.
②:点对点应增加一个/queue消息代理

(4)控制器.在WWScontroller内添加如下代码

@Controller
public class WsController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;//1

    @MessageMapping("/welcome")
    @SendTo("/topic/getResponse")
    public WiselyResponse say(WiselyMessage message) throws Exception{
        Thread.sleep(3000);
        return new WiselyResponse("Welcome, "+message.getName()+"!");
    }

    @MessageMapping("/chat")
    public void handleChat(Principal principal,String msg){//2

        if(principal.getName().equals("wsf")){//3
            messagingTemplate.convertAndSendToUser("wisely","/queue/notifications",principal.getName()+"-send:" +msg);//4
        }else{
            messagingTemplate.convertAndSendToUser("wsy","/queue/notifications",principal.getName()+"-send:" +msg);
        }
    }
}

代码解释:
①:通过SimpMessagingTemplate 向浏览器发送消息
②:在Spring MVC中,可以直接在参数中获得principal,principle中包含当前用户的信息.
③:这里是一段硬编码,如果发送人是wyf,则发送给wisely;如果发送人是wisely,则发送给wyf,读者可以根据项目实际需要改此处代码.
④:通过messagingTemplate.convertAndSendToUser向用户发送消息,第一个参数是接收消息的用户,第二个是浏览器订阅的地址,第三个是消息本身.

(5)登录页面.在src/main/resources/templates下新建login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta charset="UTF-8" />
<head>
    <title>登陆页面</title>
</head>
<body>
<div th:if="${param.error}">
    无效的账号和密码
</div>
<div th:if="${param.logout}">
    你已注销
</div>
<form th:action="@{/login}" method="post">
    <div><label> 账号 : <input type="text" name="username"/> </label></div>
    <div><label> 密码: <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="登陆"/></div>
</form>
</body>
</html>

(6) 聊天页面.在src/main/resources/templates下新建chat.html,代码如下:

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<head>
    <title>Home</title>
    <script th:src="@{sockjs.min.js}"></script>
    <script th:src="@{stomp.min.js}"></script>
    <script th:src="@{jquery.js}"></script>
</head>
<body>
<p>
    聊天室
</p>

<form id="wiselyForm">
    <textarea rows="4" cols="60" name="text"></textarea>
    <input type="submit"/>
</form>

<script th:inline="javascript">
    $('#wiselyForm').submit(function(e){
        e.preventDefault();
        var text = $('#wiselyForm').find('textarea[name="text"]').val();
        sendSpittle(text);
    });

    var sock = new SockJS("/endpointChat"); //1
    var stomp = Stomp.over(sock);
    stomp.connect('guest', 'guest', function(frame) {
        stomp.subscribe("/user/queue/notifications", handleNotification);//2
    });



    function handleNotification(message) {
        $('#output').append("<b>Received: " + message.body + "</b><br/>")
    }

    function sendSpittle(text) {
        stomp.send("/chat", {}, text);//3
    }
    $('#stop').click(function() {sock.close()});
</script>

<div id="output"></div>
</body>
</html>

代码解释
①:连接endpoint名称为”/endpointChat”的端点
②:订阅/user/queue/notifications发送消息,这里与在控制器的messagingTemplate.convertAndSendToUser中定义的订阅地址保持一致.这里多了一个/user,并且这个/user是必须的,使用了/user才会发送消息给指定的用户.

(7)增加页面的viewController

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry){
        registry.addViewController("/ws").setViewName("/ws");
        registry.addViewController("/login").setViewName("/login");
        registry.addViewController("/chat").setViewName("/chat");
    }
}

(8)运行.我们预期的效果是:两个用户登录系统,可以互发消息.但是在一个浏览器的用户会话session是共享的,我们可以在谷歌浏览器设置两个独立的用户,从而实现用户会话session隔离.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值