【javaWeb技术】·Thymeleaf项目

🌈 个人主页:十二月的猫-CSDN博客
🔥 系列专栏: 🏀系统学javaWeb开发_十二月的猫的博客-CSDN博客

💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 

目录

1. 前言

2. 项目配置和MVC中的JDBC部分

2.1 创建数据库和表

2.2 部署环境 

2.3 pojo实体:数据库表的实体类

2.4 resources配置:编写配置文件

2.5 util根据:创建JDBC工具类

2.6 编写baseDAO层

2.7 编写fruitDAO

2.8 包和类的结构

3. MVC中的Thymeleaf部分(前端渲染)

3.1 配置上下文参数

3.2 编写servlet层

3.2.1 servlet基类

3.2.2 编写servlet

3.3 编写页面

4. 项目结果展示 ​编辑


 

1. 前言

在前面的学习中,我们已经了解到Thymeleaf技术能够用于页内渲染。

页面渲染包括:页面加载页面内容加载两部分

页面加载:浏览器发送URL访问页面请求时,后端的servlet能够通过thymeleaf技术渲染出对应的页面,然后由Tomcat反馈给浏览器

页面内容加载:后端数据库中的数据是不确定数目(动态变化的),因此我们的html语句不能是死语句。而是类似循环结构的语句,从数据库中获取数据并动态展示在前端页面中。

本篇文章将通过写项目的形式带大家深入了解Thymeleaf机制与应用

2. 项目配置和MVC中的JDBC部分

一个项目可以分为两个部分:

1、由客户端(前端)向服务器端(后端)发送请求

2、由服务器端(后端)向客户端(前端)响应请求

这两部分中间需要经过MVC层,我们需要编写的就是MVC层

MVC层包括:Thymeleaf技术(前端渲染)、JDBC技术(后端数据获取)

2.1 创建数据库和表

为了与前面JDBC等文章内容相对应,这边使用的数据库、Util等都是相同的

如果前面的文章都是一一实现的,这边几乎不需要改动。仅仅需要增加一个html、修改web.xml,增加servlet三个操作即可

【JavaWeb技术】·一篇搞定JDBC-CSDN博客

【javaWeb技术】·javaWeb项目结构(web开发必看)-CSDN博客

【javaWeb技术】·一篇搞定Servlet-CSDN博客

【javaWeb技术】·一篇搞定thymeleaf-CSDN博客

create table t_fruit
(
    f_id     int auto_increment comment '水果编号' primary key,
    f_name   varchar(100)       not null comment '水果名字',
    price    double(6,2)        not null comment '水果价格',
    f_count  int                not null comment '水果库存',
    remark   VARCHAR(100)       comment '水果备注'
);
 
insert into t_fruit (f_name,price,f_count,remark)
values ('apple',100.00,500,'China'),
			 ('banana',56.00,600,'China'),
			 ('orange',121.00,300,'China'),
			 ('watermelon',23.00,900,'China')

由于个人测试时对数据库表内容有所修改,最终数据如下:

2.2 部署环境 

1、首先需要有前面四篇文章中所配置的所有环境

2、在此基础上,增加Thymeleaf所需要的jar包,如图:

关键点:

1、在于这个lib包放哪个位置???

一开始我在思考放在javaWeb项目下的lib包中和放在web下的WEB-INF(如上图)中会存在区别吗?

在查阅资料后,我们可以发现:lib对模块是共享的(一个模块可以共享模块内的lib)

这里我的项目只有一个模块(项目自身),所以我这个项目中的lib都是共享的

2、lib包需要配置给模块

虽然lib包是模块共享的,但是仍然是需要配置的

这里,我自己的演示项目选择将Thymeleaf所需要的所有包都存放在一个项目lib中,如下:

2.3 pojo实体:数据库表的实体类

package com.javaWebEx.fruitPro.fruit.pojo;

public class Fruit {
    //数据库中列名用下划线,这里用驼峰
    private Integer fId;//f_id
    private String fName;//f_name
    private Double price;//price
    private Integer fCount;//f_count
    private String remark;//remark

    public Fruit(Integer fId, String fName, Double price, Integer fCount, String remark) {
        this.fId = fId;
        this.fName = fName;
        this.price = price;
        this.fCount = fCount;
        this.remark = remark;
    }
    public Fruit() {this(0,"",0.0,0,"");}

    public Integer getfId() {
        return fId;
    }

    public void setfId(Integer fId) {
        this.fId = fId;
    }

    public String getfName() {
        return fName;
    }

    public void setfName(String fName) {
        this.fName = fName;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Integer getfCount() {
        return fCount;
    }

    public void setfCount(Integer fCount) {
        this.fCount = fCount;
    }

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Fruit fruit = (Fruit) o;
        return fId == fruit.fId && fCount == fruit.fCount && fName.equals(fruit.fName) && price == fruit.price && remark==fruit.remark;
    }

    @Override
    public String toString() {
        return "Fruit{" +
                "fId=" + fId +
                ", fName='" + fName + '\'' +
                ", price=" + price +
                ", fCount=" + fCount +
                ", remark=" + remark +
                '}';
    }
}

2.4 resources配置:编写配置文件

JDBC连接数据库这边采用的是druid的连接池

所以需要写db.properties文件,内容如下

driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql:///atguigu
username=root
password=1557304
initialSize=10
maxActive=20

2.5 util根据:创建JDBC工具类

package com.javaWebEx.util;
/*
 * JDBC工具类
 *   1、维护一个连接池对象,维护一个线程绑定变量的threadlocal对象
 *   2、对外提供在TreadLocal中获取连接的方法
 *   3、对外提供回收连接的方法,将要回收的连接从threadlocal中移除
 * */

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

public class JDBCUtil {
    private static DataSource dataSource;
    private static ThreadLocal<Connection> threadLocal=new ThreadLocal<>();//connection设定为TheadLocal使得线程中connection是唯一的
    static {
        Properties properties=new Properties();//配置对象
        InputStream inputStream=JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");//加载db.properties到类中(以数据流形式)
        try {
            properties.load(inputStream);//设置配置对象
            dataSource= DruidDataSourceFactory.createDataSource(properties);//利用配置对象创建连接池
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    public static Connection getConnection(){
        try {
            //从threadlocal中获取连接
            Connection connection=threadLocal.get();//从TheadLocal中获取连接
            //threadlocal中没有连接(第一次获取),则从连接池中加载连接到TheadLocal中
            if(connection==null){
                connection= dataSource.getConnection();
                threadLocal.set(connection);
            }
            return connection;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    public static void release(){
        try {
            //归还连接对象是以线程为单位的(因为一个线程只有一个连接对象)
            Connection connection=threadLocal.get();
            //线程中
            if(connection!=null){
                //从threadLocal中移除当前已经存储的Connection对象
                threadLocal.remove();
                //归还connection后要修改回自动提交
                connection.setAutoCommit(true);
                //将Connection对象归还连接池
                connection.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

工具类的出现是因为:

  1. 每个JDBC的操作都要获取连接
  2. 每个JDBC的操作也都要释放连接
  3. 连接池通过druid来获取

2.6 编写baseDAO层

DAO层用来实现和数据库操作——增删改查

这些操作存在共性可以提取:

  1. 查询类的可以有一个baseDAO
  2. 更新类的可以有一个baseDAO
package com.javaWebEx.fruitPro.fruit.dao;

import com.javaWebEx.util.JDBCUtil;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class BaseDAO {
    /**
     * 通用的增删改方法
     * @param sql 请求者要执行的SQL语句
     * @param params SQL语句中的占位符要赋值的参数
     * @return 受影响的行数
     */
    public int executeUpdate(String sql,Object...params) throws SQLException {
        //1、获取驱动+获取连接
        Connection connection= JDBCUtil.getConnection();
        //2、预编译SQL语句
        PreparedStatement preparedStatement=connection.prepareStatement(sql);
        //3、确定SQL语句并处理
        if(params!=null&&params.length>0){
            for(int i=0;i<params.length;i++){
                preparedStatement.setObject(i+1,params[i]);
            }
        }
        //4、处理结果
        int row=preparedStatement.executeUpdate();
        //5、释放资源
        preparedStatement.close();
        //如果开启了自动提交,才自动释放连接;否则,手动提交情况下不释放连接
        if(connection.getAutoCommit()){
            JDBCUtil.release();
        }
        return row;
    }

    /**
     * 通用的查询方法
     * @param clazz 查询结果返回的对象
     * @param sql 查询的预编译sql语句
     * @param params 确定sql语句的占位符
     * @return 单行单列、单行多列、多行多列都有可能
     * @param <T> 与Class<T>合起来共同表示一个通用类
     * @throws SQLException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws NoSuchFieldException
     */
    /*
    通用查询结果:单行单列、单行多列、多行多列
        单行单列:一个int结果
        单行多列:一个对象(例如employee)
        多行多列:list(object)
    封装过程
        1、返回的类型:泛型。类型不确定,使用时将结果类型提供给baseDAO
        2、返回的结果:通用list。可以存储多个结果,也可以存储单个结果
        3、结果的封装:反射!!要求调用者告诉baseDAO要封装对象的类对象
     */
    //Class<T>:类的类型
    public <T> List<T> executeQuery(Class<T> clazz, String sql, Object...params) throws Exception{
        //1:获取连接
        Connection connection=JDBCUtil.getConnection();
        //2、获取预编译sql
        PreparedStatement preparedStatement=connection.prepareStatement(sql);
        //3、确定sql
        if(params!=null && params.length>0){
            for(int i=0;i<params.length;i++){
                preparedStatement.setObject(i+1,params[i]);
            }
        }
        //4、获取并处理结果(只有得到对象的属性,才能对对象的属性赋值)
        ResultSet resultSet=preparedStatement.executeQuery();
            //处理结果:获取数据库表的元数据(元数据中有:列数、列名)
        ResultSetMetaData resultSetMetaData=resultSet.getMetaData();
            //处理结果:根据数据库表的元数据得到数据库表的列的数量
        int columCount=resultSetMetaData.getColumnCount();
        List<T> list=new ArrayList<>();
            //处理结果(while循环一次处理完一个对象,也就是一条记录)
        while(resultSet.next()){
            //循环一次,代表有一个记录,创建一个对象
            T t=clazz.newInstance();
            for(int i=1;i<=columCount;i++){
                //通过getObject操作获得列的值
                Object value = resultSet.getObject(i);
                //获得到的列的value值,就是t对象中的一个属性值
                //获取t对象中的一个属性名
                String fieldName=resultSetMetaData.getColumnLabel(i);
                //通过对象的属性名来得到对象的属性
                Field field=clazz.getDeclaredField(fieldName);
                //为属性突破封装的private
                field.setAccessible(true);
                //为属性赋值
                field.set(t,value);
            }
            list.add(t);
        }
        //5、释放资源
        resultSet.close();
        preparedStatement.close();
        if(connection.getAutoCommit()){
            JDBCUtil.release();
        }
        return list;
    }

    /**
     * 通用的查询方法————简化仅用于单行数据的查询
     * @param clazz 查询结果返回的对象
     * @param sql 查询的预编译sql语句
     * @param params 确定sql语句的占位符
     * @return
     * @param <T>
     * @throws Exception
     */
    public <T> T executeQueryBean(Class<T> clazz,String sql,Object...params) throws Exception{
        List<T> list=this.executeQuery(clazz,sql,params);
        if(list==null || list.size()==0)
            return null;
        return list.get(0);
    }
}

 baseDAO更详细的理解见:

【JavaWeb技术】·一篇搞定JDBC-CSDN博客

2.7 编写fruitDAO

接口:

package com.javaWebEx.fruitPro.fruit.dao;

import com.javaWebEx.fruitPro.fruit.pojo.Fruit;

import java.sql.SQLException;
import java.util.List;

public interface FruitDAO {
    /**
     * 查询数据库中水果的所有数据
     * @return 表中所有的数据
     */
    List<Fruit> selectAll() throws Exception;
    /**
     * 根据水果id查询一个水果对象
     * @param fId
     * @return 一个水果对象
     */
    Fruit selectByFId(Integer fId) throws Exception;
    /**
     * 新增数据库对应的新增一条水果数据
     * @param fruit
     * @return 受影响行数
     */
    int insert(Fruit fruit) throws SQLException;

    /**
     * 数据库对应的修改一条水果数据
     * @param fruit 数据库中想要修改的水果对象
     * @return 受影响的行数
     */
    int update(Fruit fruit) throws SQLException;

    /**
     * 数据库对应的删除一条员工数据
     * @param fruit 数据库中想要删除的水果对象
     * @return 受影响的行数
     */
    int delete(Fruit fruit) throws SQLException;
}

实现:

package com.javaWebEx.fruitPro.fruit.dao;

import com.javaWebEx.fruitPro.fruit.pojo.Fruit;

import java.sql.SQLException;
import java.util.List;

public class FruitDaoImpl extends BaseDAO implements FruitDAO{

    @Override
    public List<Fruit> selectAll() throws Exception {
        String sql="select f_Id as fId,f_name as fName,price as price,f_count as fCount,remark as remark from t_fruit";
        return executeQuery(Fruit.class,sql,null);
    }

    @Override
    public Fruit selectByFId(Integer fId) throws Exception {
        String sql="select f_Id as fId,f_name as fName,price as price,f_count as fCount,remark as remark from t_fruit where f_id=?";
        return executeQueryBean(Fruit.class,sql,1);
    }

    @Override
    public int insert(Fruit fruit) throws SQLException {
        String sql="insert into t_fruit(f_name,price,f_count,remark) values(?,?,?,?)";
        return executeUpdate(sql,fruit.getfName(),fruit.getPrice(),fruit.getfCount(),fruit.getRemark());
    }

    @Override
    public int update(Fruit fruit) throws SQLException {
        String sql="UPDATE t_fruit set price=?,f_count=? ,remark=? WHERE f_name=?";
        return executeUpdate(sql,fruit.getPrice(),fruit.getfCount(),fruit.getRemark(),fruit.getfName());
    }

    @Override
    public int delete(Fruit fruit) throws SQLException {
        String sql="delete from t_fruit where f_id=?";
        return executeUpdate(sql,fruit.getfId());
    }
}

实现此时只要依靠baseDAO中的操作即可

baseDAO中的实现都是通用的

2.8 包和类的结构

3. MVC中的Thymeleaf部分(前端渲染)

3.1 配置上下文参数

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
<!--    <servlet>
        <servlet-name>AddServlet</servlet-name>
        <servlet-class>com.javaWebEx.servlets.AddServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AddServlet</servlet-name>
        <url-pattern>/add</url-pattern>
    </servlet-mapping>-->
<!--    <servlet>
        <servlet-name>Demo02Servlet</servlet-name>
        <servlet-class>com.javaWebEx.servlets.Demo02Servlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Demo02Servlet</servlet-name>
        <url-pattern>/demo02</url-pattern>
    </servlet-mapping>-->
<!--    <servlet>
        <servlet-name>TestServlet</servlet-name>
        <servlet-class>com.javaWebEx.servlets.TestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>TestServlet</servlet-name>
        <url-pattern>/demo03</url-pattern>
    </servlet-mapping>-->

      <!--  配置上下文参数  -->
    <context-param>
        <!--        配置前缀-->
        <param-name>view-prefix</param-name>
        <!--WEB根目录-->
        <param-value>/</param-value>
    </context-param>
    <context-param>
        <!--        配置后缀-->
        <param-name>view-suffix</param-name>
        <param-value>.html</param-value>
    </context-param>
</web-app>

3.2 编写servlet层

3.2.1 servlet基类

package com.javaWebEx.thymeleaf.servlet;

import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ViewBaseServlet extends HttpServlet {

    private TemplateEngine templateEngine;

    @Override
    public void init() throws ServletException {

        // 1.获取ServletContext对象
        ServletContext servletContext = this.getServletContext();

        // 2.创建Thymeleaf解析器对象
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);

        // 3.给解析器对象设置参数
        // ①HTML是默认模式,明确设置是为了代码更容易理解
        templateResolver.setTemplateMode(TemplateMode.HTML);

        // ②设置前缀
        String viewPrefix = servletContext.getInitParameter("view-prefix");

        templateResolver.setPrefix(viewPrefix);

        // ③设置后缀
        String viewSuffix = servletContext.getInitParameter("view-suffix");

        templateResolver.setSuffix(viewSuffix);

        // ④设置缓存过期时间(毫秒)
        templateResolver.setCacheTTLMs(60000L);

        // ⑤设置是否缓存
        templateResolver.setCacheable(true);

        // ⑥设置服务器端编码方式
        templateResolver.setCharacterEncoding("utf-8");

        // 4.创建模板引擎对象
        templateEngine = new TemplateEngine();

        // 5.给模板引擎对象设置模板解析器
        templateEngine.setTemplateResolver(templateResolver);

    }

    protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 1.设置响应体内容类型和字符集
        resp.setContentType("text/html;charset=UTF-8");

        // 2.创建WebContext对象
        WebContext webContext = new WebContext(req, resp, getServletContext());

        // 3.处理模板数据
        templateEngine.process(templateName, webContext, resp.getWriter());
    }
}

该基类提供processTemplate函数用来实现Thymeleaf技术的页面渲染

3.2.2 编写servlet

package com.javaWebEx.fruitPro.servlets;

import com.javaWebEx.fruitPro.fruit.dao.FruitDAO;
import com.javaWebEx.fruitPro.fruit.dao.FruitDaoImpl;
import com.javaWebEx.fruitPro.fruit.pojo.Fruit;
import com.javaWebEx.thymeleaf.servlet.ViewBaseServlet;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@WebServlet("/index")
public class IndexServlet extends ViewBaseServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
        FruitDAO fruitDAO=new FruitDaoImpl();
            //利用DAO从数据库中获取数据
        List<Fruit>fruitList= null;
        try {
            fruitList = fruitDAO.selectAll();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        //将水果放入请求的session作用域中
        HttpSession httpSession=req.getSession();
        httpSession.setAttribute("fruit",fruitList);
        //每个客户端都是带着自己的ID号发出申请的,所以通过req能够获取其ID号
        //利用thymeleaf技术渲染页面
        super.processTemplate("index",req,res);

    }

}

使用了几个之前没有用过的技术:

  1. @WebServlet()完成web.xml配置
  2. 调用Dao获取数据库数据存放在会话的保存域中
  3. 根据index去渲染index.html页面,从而将页面加载工作移交给servlet来完成

3.3 编写页面

html:

<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="css/index.css">
</head>
<body>
<div id="div_container">
  <div id="div_fruit_list">
    <p class="center f30">欢迎使用水果库存后台管理系统</p>
    <div style="border:0px solid red;width:60%;margin-left:20%;text-align:right;">
      <a th:href="@{/add.html}" style="border:0px solid blue;margin-bottom:4px;">添加新库存记录</a>
    </div>
    <table id="tbl_fruit">
      <tr>
        <th class="w20">名称</th>
        <th class="w20">单价</th>
        <th class="w20">库存</th>
        <th>操作</th>
      </tr>
      <!--            如果链表为空-->
      <tr th:if="${#lists.isEmpty(session.fruit)}">
        <td colspan="4">对不起,库存为空!</td>
      </tr>
      <!--            如果链表不为空,取出数据-->
      <tr th:unless="${#lists.isEmpty(session.fruit)}" th:each="fruit : ${session.fruit}">
        <td th:text="${fruit.fName}">苹果</td>
        <td th:text="${fruit.price}">4</td>
        <td th:text="${fruit.fCount}">20</td>
        <!-- <td><img src="imgs/del.jpg" class="delImg" th:onclick="'delFruit('+${fruit.fid}+')'"/></td> -->
        <td><img src="imgs/del.jpg" class="delImg"/></td>
      </tr>
    </table>
  </div>
</div>
</body>
</html>

css:

*{
	color: threeddarkshadow;
}
a{
	text-decoration: none;
}
body{
	margin:0;
	padding:0;
	background-color:#808080;
}
div{
	position:relative;
	float:left;
}

#div_container{
	width:80%;
	height:100%;
	border:0px solid blue;
	margin-left:10%;
	float:left;
	background-color: honeydew;
	border-radius:8px;
}
#div_fruit_list{
	width:100%;
	border:0px solid red;
}
#tbl_fruit{
	width:60%;
	line-height:28px;
	margin-top:16px;
	margin-left:20%;
}
#tbl_fruit , #tbl_fruit tr , #tbl_fruit th , #tbl_fruit td{
	border:1px solid gray;
	border-collapse:collapse;
	text-align:center;
	font-size:16px;
	font-family:"黑体";
	font-weight:lighter;
	
}
.w20{
	width:20%;
}
.delImg{
	width:24px;
	height:24px;
}
.btn{
	border:1px solid lightgray;
	width:80px;
	height:24px;
}

.center{
	text-align:center;
}
.f30{
	font-size: 30px;
}

 提供图片:

4. 项目结果展示 

可以看到页面已经把数据库中的所有信息体现在页面上

完整项目结构展示:

图片中存在一些其他的文件并不是用在该项目中的

该项目中的文件都已经在上面一一说明

 总结

如果觉得对你有帮助,辛苦友友点个赞,收个藏呀~~~

猫猫未来一定会产出更多优质文章的!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

十二月的猫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值