WEB开发小技巧

前言

Maven能很好的解决引入jar包问题,
Tomcat热部署可以让你少点几次鼠标,
一个好的工具类(虽然我也写的不咋地)能够在各个类似项目里复用,
掌握Manager工具可以让你对服务器的运行情况有更多了解,

如果想要更加便捷地开发,可以了解一下这些框架
Mybatis(不用考虑DAO层的实现)
Mabatis-plus(在Mybatis的基础上,不用自己写简单的SQL),
Bootstrap(不用自己写简单的HTML),
Log-4j(不用自己sout打印日志),
SSM(Spring,Spring MVC,Mybatis,一般的项目用这三个就够了)
SpringBoot(SSM的简化版,比SSM写项目方便一点)

Maven构建工具

这个工具可以让你不用再去自己复制jar包再放入自己的项目,而是可以通过网络进行下载jar包。
在最初使用web框架时,你会发现每当你创建一个新的项目时,为了连接数据库,你都要把数据库驱动jar包放在WEB-INF的lib文件夹下,比较麻烦。
在这里插入图片描述
所以你可以将项目变成Maven工具构建的项目,以test2项目为例,
你只需要在这个test目录下新建一个pom.xml文件
在这里插入图片描述
(这里我的web根目录就叫web而不是webapp,这个是可以通过Fact或者工件设置那里自己改的,不要在意)
在这里插入图片描述
新建后pom.xml文件,点击进去,右键添加为Maven项目
在这里插入图片描述
然后右边会出现这个,Unknown就是刚刚添加为Maven项目的那一个。
在这里插入图片描述

复制这段进去,记得改把工件名和组名改成自己的

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--  组名,加个企业名或者啥的,反正就是自己的包名   -->
    <groupId>com.budiu</groupId>
    <!-- 工件名    -->
    <artifactId>test2</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
    	<!-- 这个JSTL之后应该会学的 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
            <scope>compile</scope>
        </dependency>

        <!--这里的provided是指服务器自己本身就已经有这个jar包了,不需要打包进入部署单元(如war文件)-->
        <!-- 引入这个纯粹是为了在写Servlet类是不用自己手动在模块里再去Tomcat安装目录下再去手动引入这个包了,不引入IDEA会提示无法解析该类,这是IDEA的问题,不是Tomcat的问题      -->
        <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/jsp-api -->
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
            <scope>provided</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <!-- 这个可能要根据自己的数据库进行版本变动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>

重新加载项目
在这里插入图片描述

自己就把名字变好了,然后就可以把lib文件夹删除掉了
在这里插入图片描述
之后就可以在这个pom.xml文件填写依赖坐标,而不是自己去网上下载到本地再去引用。
Maven这个工具以后要专门去学的,不过全部内容也就两个小时,常用的内容十几分钟就看完了。

Tomcat服务器热部署与热加载

每次进行代码改动后,每次都得进行更新类和资源,然后再去浏览器刷新网页,这也是个麻烦的事。
虽然热部署的范围比热加载大,但是开发环境下,大多更喜欢用热加载,因为范围更小,对性能消耗也会变小。
在生产环境下,热部署更常见。
我的建议是直接热部署得了,我们写的那些作业体量根本还不需要考虑服务器负载性能那种程度。
在这里插入图片描述

热加载与热部署的区别

此处参考博文
腾讯云社区 热加载与热部署有什么区别?
腾讯云社区 热部署热加载原理解析
优快云 Tomcat热加载/热部署
一篇文章彻底搞懂Tomcat热部署与热加载,这篇思路最清晰,但是不太好读懂

热加载(Hot Swap):

定义:热加载是指在Tomcat服务器不停止运行情况下,重新加载Java类文件和资源文件
执行主体:热加载的执行主体是Context,它表示单个Web应用程序。

热部署(Hot Deployment):

定义:热部署是指在Tomcat服务器在不停止运行情况下,重新部署整个Web应用程序
执行主体:热部署的执行主体是Host,它表示Tomcat服务器上的主机环境,负责管理多个Web应用程序。

热加载
开启热加载后会有一个后台线程周期性(每隔10秒)监测WEB-INF/classes和WEB-INF/lib里面的文件内容是否变动,如果内容变动,Tomcat会清空类加载缓存,重新进行类加载和资源加载。

热部署
开启热部署后,host会监控自己的war包是否被删除,有没有新的war包进入,然后调用内部API进行部署。

热加载配置方法

这是最简单的热加载,IDEA里就能配置,只需要在切出IDE时,把它变为更新类和资源。
在这里插入图片描述
日志出现这种,就说明热加载成功了。
在这里插入图片描述

热部署配置方法

热部署有三种主流方式,第一种是Jrebel插件,可以在IDEA插件里去下载,不过是付费插件,但是网上也有破解之法,第二种也需要用到Maven工具,是一个Maven插件,第三种就是更改Tomcat本身的配置。

这里只说最简单的一种:

  1. 首先先把上面实现热加载的IDEA设置弄好,也就是切换出IDEA时选择更新类文件于资源操作。

其实这里原理没弄懂,个人猜测是IDEA的项目并不是直接部署在Tomcat里的,必须将项目构建后,才会放入Tomcat的Manager进行部署。如果更改代码,不重新构建,Tomcat是无法监测到文件发生变化的,所以需要这个设置去重新构建

在这里我试过假如不执行第一步操作的话,那就需要点一这个构建工件,才能达到相同效果

在这里插入图片描述

  1. 找到Tomcat的context.xml文件: 这个文件通常位于$CATALINA_BASE/conf目录下(CATALINA_BASE是Tomcat的安装根目录)。
  2. 编辑context.xml文件: 打开context.xml文件,找到标签,在Context加上这个属性
<Context reloadable="true">

reloadable="true"属性指示Tomcat在类文件更改时重新加载Web应用程序,然后重启一下Tomcat就好。如果失败了,可以重启一下电脑。
在这里插入图片描述

这里看起来和热加载好像没什么变化,但你可以试试改变Servlet的路径,如果只是热加载,你网址栏输入更改后的路径将会得到一个404报错,但是如果是热部署,那就会返回这个Servlet处理后的结果。

数据库连接工具

基础工具类实现

老师提供的数据库连接的模板文件已经很好了,但是我偷了个懒,写得简单点(更简单的代价肯定就是不够全面,无法应对所有情况,但是应付作业肯定还是轻轻松松的)。

我最初写的MYSQL工具类,就只有两个静态方法,一个获取连接,一个关闭连接。

package com.budiu.util;

import java.sql.*;

/**
 * 连接MySQL数据库的工具类
 *
 * @author 罗不丢
 * @since 2024/11/10
 */

public class MySQLUtil {
    static String driverClass="com.mysql.cj.jdbc.Driver";
    static String userName="*******";
    static String password="*******";
    static String url="jdbc:mysql://sh-cynosdbmysql-grp-jmg83f6i.sql.tencentcdb.com:22552/test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC";
    private static  Connection connection=null;

    public static Connection getConnection()  {
        try {
        Class.forName(driverClass);
            return DriverManager.getConnection(url,userName,password);
        } catch (SQLException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
    public static void closeConnection() {
       if (connection!=null){
           try {
               connection.close();
           } catch (SQLException e) {
               throw new RuntimeException(e);
           }
       }

    }

}

不知道你们能在里面发现几个问题

首先是我用static来修饰Connection,而且我的方法没有进行同步操作,那么说明在多人使用的情况下,这个Connection很有可能会出现异常。

然后是我的硬编码(也就是我的数据库驱动名称,用户名等都写在类里,正常情况下是需要写在yml或者properties配置文件里的,不过这里我也懒得去额外再去写配置文件了,毕竟是个懒人)。

还有closeConnection方法中虽然尝试关闭连接,但是没有提供关闭ResultSet和Statement的方法,这还是可能会导致内存泄漏。

工具类优化

但是吧,如果我不用静态变量connection的话,每个人进行一次读取数据的访问我就得创建一个数据库连接对象,其实很多时候一些连接对象是闲置的,还可以反复利用,所以就有了数据库连接池这个概念来进行性能优化。

java里比较常见的数据库连接池技术有c3po和Druid

这里只展示德鲁伊数据库连接池的基本使用。
先进行Maven引用,把这个依赖放到pom.xml文件里,不会Maven那就只有先下载jar到本地再引用了

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>

德鲁伊数据库连接池工具类,那个sqlLog方法是为了打印填写好参数的SQL

public class DruidMySQLUtil {
    private static DruidDataSource dataSource;

    static {
        dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://sh-cynosdbmysql-grp-jmg83f6i.sql.tencentcdb.com:22552/test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC");
        dataSource.setUsername("*******");
        dataSource.setPassword("*******");

        // 配置初始化大小、最小、最大
        dataSource.setInitialSize(1);
        dataSource.setMinIdle(1);
        dataSource.setMaxActive(20);

        // 配置获取连接等待超时的时间
        dataSource.setMaxWait(60000);

        // 配置间隔多久进行一次检测,检测需要关闭的空闲连接
        dataSource.setTimeBetweenEvictionRunsMillis(60000);

        // 配置一个连接在池中最小生存的时间
        dataSource.setMinEvictableIdleTimeMillis(300000);

        //用于检查数据库连接是否仍然有效
        dataSource.setValidationQuery("SELECT 'x'");
        //连接池会定期检查空闲的连接是否仍然有效。如果验证失败,连接会被从池中移除并重新创建。
        dataSource.setTestWhileIdle(true);
        //表示在从连接池借用连接时不会进行验证。如果设置为true,每次从连接池获取连接时都会执行validationQuery来验证连接的有效性。
        dataSource.setTestOnBorrow(false);
        //属性设置为false时,表示在将连接返回到连接池时不会进行验证。
        dataSource.setTestOnReturn(false);

        // 打开PSCache,并且指定每个连接上PSCache的大小
        dataSource.setPoolPreparedStatements(true);
        dataSource.setMaxOpenPreparedStatements(20);
        try {
            dataSource.init();
        } catch (SQLException e) {
            throw new RuntimeException("初始化数据连接池失败",e);
        }
    }

    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    public static void close(ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                throw new RuntimeException("结果集资源关闭失败", e);
            }
        }
    }

    public  static void close(Connection conn) {
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                throw new RuntimeException("连接资源关闭失败", e);

            }
        }
    }

    public  static void close(PreparedStatement pstmt) {
        if (pstmt != null) {
            try {
                pstmt.close();
            } catch (SQLException e) {
                throw new RuntimeException("预处理语句资源关闭失败", e);
            }
        }
    }
    public static  void  close(Statement statement){
        if(statement!=null){
            try{
                statement.close();
            }catch (SQLException e){
                throw new RuntimeException("statement资源关闭失败", e);
            }
        }
    }
    // 关闭所有资源
    public static void closeAll(Connection conn, PreparedStatement pstmt, ResultSet rs) {
        close(rs);
        close(pstmt);
        close(conn);
    }
    public static void closeAll(Connection conn, PreparedStatement pstmt, ResultSet rs,Statement statement) {
        close(rs);
        close(pstmt);
        close(conn);
        close(statement);
    }
    public static void closeAll(Connection conn,  ResultSet rs,Statement statement){
        close(rs);
        close(conn);
        close(statement);
    }
    public static void closeAll(Connection conn,  PreparedStatement prestatement){
        close(conn);
        close(prestatement);
    }

    /**
     * @Description 日志记录
     * @Params
     * @param preparedStatement 已经填充好参数的预处理语句
     * @Return void
     * @Author 罗不丢
     * @Date 2024/11/15
     */
    public static void sqlLog(PreparedStatement preparedStatement){
        if (preparedStatement!=null) {
            String executedSql = preparedStatement.toString(); // 包含实际的参数值
            System.out.println("执行的SQL语句: " + executedSql); // 打印填充参数后的SQL语句
        }else{
            System.out.println("预处理语句为空");
        }
    }
    public static void sqlLog(Statement statement){
        if (statement!=null) {
            String executedSql = statement.toString();
            System.out.println("执行的SQL语句: " + executedSql);
        }else{
            System.out.println("语句为空");
        }
    }
    // Tomcat较高版本会有内存泄漏警告,目前还没找到较好的解决办法
  	public static void closeDataSource() {
        try {
            while(DriverManager.getDrivers().hasMoreElements()) {
            	DriverManager.deregisterDriver(DriverManager.getDrivers().nextElement());
            }
        } catch (SQLException e) {
            throw new RuntimeException("JDBC驱动取消注册过程异常",e);
        }
        // 尝试停止MySQL的AbandonedConnectionCleanupThread
        System.out.println("尝试关闭报错线程");
        AbandonedConnectionCleanupThread.checkedShutdown();
        if (dataSource != null) {
            dataSource.close();
            dataSource = null;
        }
    }
}

额外说明

说一下最后的closeDataSource(),这个方法是为了解决这个日志警告。
Tomcat 高版本能够检测到内存泄漏情况,但我还没弄懂这个线程没有正常关闭的原因。
在这里插入图片描述

 注册了JDBC驱动程序 [com.alibaba.druid.proxy.DruidDriver],
 但在Web应用程序停止时无法注销它。 为防止内存泄漏,JDBC驱动程序已被强制取消注册。
16-Nov-2024 10:59:37.848 警告 [RMI TCP Connection(4)-127.0.0.1] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesJdbc Web应用程序 [test3_war_exploded] 注册了JDBC驱动程序 [com.mysql.cj.jdbc.Driver],
但在Web应用程序停止时无法注销它。 为防止内存泄漏,JDBC驱动程序已被强制取消注册。
16-Nov-2024 10:59:38.284 信息 [mysql-cj-abandoned-connection-cleanup] org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading 非法访问:此Web应用程序实例已停止。无法加载[]。为了调试以及终止导致非法访问的线程,将抛出以下堆栈跟踪。
	java.lang.IllegalStateException: 非法访问:
	此Web应用程序实例已停止。无法加载[]。
	为了调试以及终止导致非法访问的线程,将抛出以下堆栈跟踪。
		at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading(WebappClassLoaderBase.java:1432)
		at org.apache.catalina.loader.WebappClassLoaderBase.getResource(WebappClassLoaderBase.java:1057)
		at com.mysql.cj.jdbc.AbandonedConnectionCleanupThread.checkThreadContextClassLoader(AbandonedConnectionCleanupThread.java:123)
		at com.mysql.cj.jdbc.AbandonedConnectionCleanupThread.run(AbandonedConnectionCleanupThread.java:90)
		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
		at java.lang.Thread.run(Thread.java:748)

需要再创建一个监听器
在这里插入图片描述
监听到web程序快关闭时,手动去取消注册驱动和终止报错线程。

import com.budiu.util.DruidMySQLUtil;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;


@WebListener
public class DataSourceServletContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
         //销毁数据源
        System.out.println("正在销毁数据库连接池资源");
        DruidMySQLUtil.closeDataSource();
    }
}

工具类用法

具体用法类似这样

 @Override
    public boolean insertOne(Emp emp) {
        String sql="insert into emp(empno,ename,hiredate,job,sal,comm) values (?,?,?,?,?,?)";
        PreparedStatement preparedStatement=null;
        Connection connection=null;
        try {
        	//获取连接
            connection=DruidMySQLUtil.getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1,emp.getNo());
            preparedStatement.setString(2, emp.getName());
            preparedStatement.setDate(3, (Date) emp.getHireDate());
            preparedStatement.setString(4, emp.getJob());
            preparedStatement.setDouble(5,emp.getSalary());
            preparedStatement.setString(6,emp.getComm());
            //打印SQL语句
            DruidMySQLUtil.sqlLog(preparedStatement);
            //进行sql语句执行
            return preparedStatement.execute();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
        	//关闭资源
            DruidMySQLUtil.closeAll(connection,preparedStatement);
        }
    }

Tomcat Manager

Tomcat其实还提供了一个管理器,本人觉得一般般,但是后期远程部署啥的应该会有点用吧。
当你启动Tomcat管理器后,可以通过访问

localhost:8080/manager/html

在这里插入图片描述

这里还需要进行用户登录,Tomcat默认是不会允许你登录的,你得先去配置文件改一下
在conf目录的tomcat-user.xml里去掉注释,然后随便改个密码
在这里插入图片描述

登录后
在这里插入图片描述
除了主页比较有用外,它还能给你粗略看一下服务器的运行情况,可以监测自己的服务有没有正常运行
在这里插入图片描述

自制Servlet了解web程序情况

Tomcat的Manager并只能理解web程序的整体情况,所以需要自己写几个Servlet去了解web程序的具体情况,可以直接复制粘贴到自己的Servlet目录就能用。

了解web根目录下的内容

可以根据这个Servlet看自己的jar是否正常导入

import java.io.File;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/listFiles")
public class ListFilesServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        File webRoot = new File(getServletContext().getRealPath("/"));
        response.setContentType("text/html");
        java.io.PrintWriter out = response.getWriter();
        out.println("<html><head>");
        out.println("<style>");
        out.println(".folder { cursor: pointer; }");
        out.println(".hidden { display: none; }");
        out.println("</style>");
        out.println("<script>");
        out.println("function toggleFolder(folderId) {");
        out.println("  var folder = document.getElementById(folderId);");
        out.println("  if (folder.classList.contains('hidden')) {");
        out.println("    folder.classList.remove('hidden');");
        out.println("  } else {");
        out.println("    folder.classList.add('hidden');");
        out.println("  }");
        out.println("}");
        out.println("</script>");
        out.println("</head><body>");
        out.println("<h1>Files in Web Root</h1>");
        out.println("<ul>");
        listFilesAndDirs(webRoot, out, false);
        out.println("</ul>");
        out.println("</body></html>");
    }

    private void listFilesAndDirs(File dir, java.io.PrintWriter out, boolean isWebInf) {
        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    String folderId = "folder" + file.getName().hashCode();
                    // 默认展开Web-INF目录
                    if (file.getName().equals("WEB-INF") || isWebInf) {
                        out.println("<li><strong class='folder' οnclick='toggleFolder(\"" + folderId + "\")'>" + file.getName() + "</strong> (directory)<ul id='" + folderId + "'"+">");
                        listFilesAndDirs(file, out, true); // Recursive call with isWebInf set to true
                        out.println("</ul></li>");
                    } else {
                        // Only show the directory name without expanding it
                        out.println("<li>" + file.getName() + " (directory)</li>");
                    }
                } else {
                    // Only list files if we're inside WEB-INF
                    if (isWebInf) {
                        out.println("<li>" + file.getName() + "</li>");
                    }
                }
            }
        }
    }
}

访问这个Sevlet返回的结果像这样,这些树形结构是可以点击展开的,但我们一般只关注WEB-INF的目录下有没有什么少的部分
在这里插入图片描述

查看Servelt名称和路由信息

/**
 * 遍历web程序里的Servlet和它的路由信息
 *
 * @author 罗不丢
 * @since 2024/11/15
 */
@WebServlet("/listServlets")
public class ListServletsServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取ServletContext对象
        ServletContext context = getServletContext();

        // 获取应用的上下文路径
        String contextPath = context.getContextPath();

        // 获取所有已注册的Servlet
        Map<String, ? extends ServletRegistration> servlets = context.getServletRegistrations();

        // 设置响应类型为HTML
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();

        // 输出HTML页面的头部
        out.println("<html><head><title>Servlet List</title>");
        out.println("<style>body { font-size: 28px; }</style>"); // 设置字体大小为18px
        out.println("</head><body>");
        out.println("<h2>注意部分链接可能需要表单提交参数才能访问成功,这里仅作信息展示</h2>");
        // 遍历所有Servlet并生成相应的URL
        out.println("<ul>");
        for (Map.Entry<String, ? extends ServletRegistration> entry : servlets.entrySet()) {
            String servletName = entry.getKey();
            ServletRegistration servlet = entry.getValue();
            for (String urlPattern : servlet.getMappings()) {
                // 构建完整的URL,包括上下文路径
                String fullUrl = contextPath + urlPattern;
                out.println("<li><a href=\"" + fullUrl + "\">" +"Servlet名称: "+servletName + "    -  路由信息: " + fullUrl + "</a></li>");
            }
        }
        out.println("</ul>");
        // 输出HTML页面的尾部
        out.println("</body></html>");
    }
}

在这里插入图片描述

返回的结果类似这样
在这里插入图片描述

总结

以后的框架可能会将这些基础代码封装再封装,我们只需要调用某个方法就能完成一堆现在繁琐的操作。所以也不用担心以后的代码有多难写,代码肯定是越写越简单的,这也是面向对象编程的意义所在。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

罗不丢

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

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

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

打赏作者

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

抵扣说明:

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

余额充值