2024.4.8 Web开发技术 读代码 实验一、二

这个笔记用于我本人的复习,所以引用了很多东西。如果被侵权了请联系我,我会删掉,谢谢。:-)

写这个笔记,根本目的是希望我能养成做有效笔记的习惯,帮助我提高学习的效率。除此之外,我还希望能借由这个渠道来锻炼我书面表达的能力(因为发现上了大学之后越来越不会写文章了,偏偏又还有一堆文章要写 :-(  )以及提取信息的能力。

说实话,不知道我能坚持多久,但是我还是希望我能一直坚持下去。

今天来看看之前做的一个Web实验,希望在我还有印象之前总结一下。

实验要求

 

新建项目并添加postgresql依赖

首先在IDEA新建项目,选择Jakarta EE项目,模板选择Web应用程序,构建系统选择Maven,服务器选择TomCat,如下图。

然后前往postgresql驱动下载官网,Copy Maven,复制到pom.xml的dependency之下:

再把目录设置成如下样式即可:

数据库操作类

首先我们先做一个数据库连接的基本操作类,有多基本呢,这个类中包括了连接的创建、获取和删除。我们将这个类放在util目录下,命名为Dbutil。

以下是一些基本的jdbc语言,域名和端口号这些都是固定的,当然后面那个hgd是你自己在postgresql中命名的。

//这是用于实现sql语句的工具类,与具体的表无关
    private static Connection con;
    //jdbc连接数据库
    private static final String URL = "jdbc:postgresql://localhost:5432/hgd";
    private static final String USERNAME = "postgres";
    //下面可得写自己设置的密码
    private static final String PASSWORD = "***";

回到代码,创建和获取连接是这么写的:

private static Connection createConnection(){
        //使用反射将数据库驱动类加载到JVM虚拟机中
        //导入库时,可在“项目结构-库-按加号进行导入-选择正确jar文件即可”
        try {
            Class.forName("org.postgresql.Driver");
            con = DriverManager.getConnection(URL,USERNAME,PASSWORD);
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        //以上是经典的错误处理环节,最好都写成e.printStackTrace,这样可以打印出异常信息在程序中的位置与出错原因
        return con;
    }
    public static Connection GetConnection(){
        //con为空或者已经关闭时
        try {
            if(con == null || con.isClosed()){
                con = createConnection();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return con;
    }

有创建自然就有关闭,注意这里我们把close方法给重载了一遍,这是因为后面可能有些地方需要用到ResultSet来参与数据表单的遍历。 

public static void close(PreparedStatement ps, Connection con){
        try {
            if(!ps.isClosed()){
                ps.close();
            }
            if(!con.isClosed()){
                con.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }


    //方法的重载,因为之后需要用到ResultSet
    public static void close(ResultSet rs,PreparedStatement ps, Connection con){
        try {
            if(!rs.isClosed())
            {
                rs.close();
            }
            if(!ps.isClosed()){
                ps.close();
            }
            if(!con.isClosed()){
                con.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

 Dbutil类的编辑到此结束。

实体类

我们要操作的实体很明显,就是学生。所以我们建一个Student类来存储学生信息。

private String sid;
    private String name;
    private int age;
    private String birthday;
    private String gender;

然后右键-生成,把构造函数、setter和getter全部让IDE帮我们自动生成。

Postgresql新建表

右键注册一个服务器,设置名字和密码,端口域名那些默认即可。

在“架构”中找到表,右键-属性,按如下方式创建:

增删改查操作类

根据实验要求,增删改查操作类全部放在Dao目录下,而且我们分别定义了接口和实现类。

我们先谈关于学生信息的Dao操作类,这是接口:

public interface StudentDao {
    void Add(Student student);
    //添加
    void Modify(Student student);
    //修改
    void Remove(String sid);
    //删除
    List<Student> GetAll(String sql);
    //读取所有的学生信息,相当于遍历数据库
    Student GetStu(String sid);
    //通过学号来获取学生信息
    List<Student> GetByName(String name);
    //用名字来模糊查询
}

以下是接口的实现类,我们先看那Add方法的实现类:

@Override
    public void Add(Student student) {
        String sid = student.getSid();
        String name = student.getName();
        int age = student.getAge();
        String birthday = student.getBirthday();
        String gender = student.getGender();
        //先读取所有的变量信息
        String sql = "insert into student values(?,?,?,?,?)";
        //这里的student是我们创建的表的名字
        //调用工具类创建连接
        Connection con = DbUtil.GetConnection();
        PreparedStatement ps = null;
        try {
            //把数据写入
            ps = con.prepareStatement(sql);
            ps.setString(1,sid);
            ps.setString(2,name);
            ps.setInt(3,age);
            //sql语句的Date类型允许由String类型转型,使用ValueOf方法
            ps.setDate(4, Date.valueOf(birthday));
            ps.setString(5,gender);
            //ps更新
            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        finally {
            //结束了就关闭
            DbUtil.close(ps,con);
        }
    }

 con用于读取连接,ps用于读取sql语句。

再看一个需要遍历的方法是如何实现的,以GetAll方法为例:

 @Override
    public List<Student> GetAll(String sql) {
        String sid;
        String name;
        int age;
        String birthday;
        String gender;
        Connection con = DbUtil.GetConnection();
        PreparedStatement ps = null;
        ResultSet rs = null;
        List<Student> List = new ArrayList<>();
        try {
            //ps读取sql语句
            ps = con.prepareStatement(sql);
            //以下是循环的写法
            rs = ps.executeQuery();
            //ResultSet对象可用于执行遍历操作
            while(rs.next()){
                sid = rs.getString("sid");
                name = rs.getString("name");
                age = rs.getInt("age");
                birthday = rs.getString("birthday");
                gender = rs.getString("gender");
                List.add(new Student(sid,name,age,birthday,gender));
            }
            return List;
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
        finally {
            DbUtil.close(rs,ps,con);
        }
    }

显然地,我们这里引入ResultSet是为了循环的进行,这时我们的重载的close方法就派上用场了。

还有更新信息的写法:

@Override
    public void Modify(Student student) {
        String sid = student.getSid();
        String name = student.getName();
        int age = student.getAge();
        String birthday = student.getBirthday();
        String gender = student.getGender();
        String sql = "update student set name = ?,age = ?,birthday=? ,gender=? where sid = ?";
        Connection con = DbUtil.GetConnection();
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(sql);
            ps.setString(5,sid);
            ps.setString(4,gender);
            ps.setString(1,name);
            ps.setInt(2,age);
            ps.setDate(3, Date.valueOf(birthday));
            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        finally {
            DbUtil.close(ps,con);
        }
    }

 删除的方法:

@Override
    public void Remove(String sid) {
        String sql = "delete from student where sid = ?";
        //都是先建立连接然后读取sql语句
        Connection con = DbUtil.GetConnection();
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(sql);
            ps.setString(1,sid);
            //如果内容有更新就加上以下语句
            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        finally {
            DbUtil.close(ps,con);
        }
    }

按照学号以及按照姓名来查找学生:

 @Override
    public Student GetStu(String sid) {
        String name;
        int age;
        String birthday;
        String gender;
        String sql = "select * from student where sid = ?";
        Connection con = DbUtil.GetConnection();
        PreparedStatement ps = null;
        ResultSet rs = null;
        Student student = null;
        try {
            ps = con.prepareStatement(sql);
            //寻找特定sid的学生
            ps.setString(1,sid);
            rs = ps.executeQuery();
            if(rs.next()) {
                sid = rs.getString("sid");
                name = rs.getString("name");
                age = rs.getInt("age");
                birthday = rs.getString("birthday");
                gender = rs.getString("gender");
                student = new Student(sid, name, age, birthday,gender);
            }
            return student;
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
        finally {
            DbUtil.close(rs,ps,con);
        }
    }

    @Override
    public List<Student> GetByName(String name) {
        String sid;
        int age;
        String birthday;
        String gender;
        //模糊查询?
        String sql = "select * from student where name like '%" + name + "%'";
        Connection con = DbUtil.GetConnection();
        PreparedStatement ps = null;
        ResultSet rs = null;
        List<Student> List = new ArrayList<>();
        try {
            ps = con.prepareStatement(sql);
            rs = ps.executeQuery();
            while(rs.next()){
                sid = rs.getString("sid");
                name = rs.getString("name");
                age = rs.getInt("age");
                birthday = rs.getString("birthday");
                gender = rs.getString("gender");
                List.add(new Student(sid,name,age,birthday,gender));
            }
            return List;
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
        finally {
            DbUtil.close(rs,ps,con);
        }
    }

index.jsp

首先我们需要把所有用到的类全部引入

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="cn.edu.hit.dao.StudentDao,cn.edu.hit.dao.impl.StudentDaoImpl" %>
<%@ page import="cn.edu.hit.entity.Student, java.util.List, java.util.ArrayList" %>

index页面就是你在登陆成功后所展示给你的界面,就像下面一样:

首先我们确保index.jsp不能被直接访问(不然那个登录界面就没有意义了),我们用如下的代码来实现。注意java的代码需要用<%  %>来括起来。

<%
  String usename = (String)session.getAttribute("usename");
  String password = (String)session.getAttribute("password");
  if(usename == null){
    out.println("非法用户,请<a href='login.html'>返回登录界面</a>");
    return;
  }
  StudentDao dao = new StudentDaoImpl();
  String sid,name,birthday,gender;
  int age;
  List<Student> list = dao.GetAll("select * from student order by sid ");
%>

下面那个表格的写法如下:

<a href="add.html">增加学生</a>
<table border="1">
  <tr>
    <th>学号</th><th>姓名</th><th>性别</th><th>年龄</th><th>生日</th><th>修改</th><th>删除</th>
  </tr>
<%
  for(Student student : list){
    sid = student.getSid();
    name = student.getName();
    age = student.getAge();
    birthday = student.getBirthday();
    gender = student.getGender();
%>
  <tr>
    <td><%=sid%></td><td><%=name%><td><%=gender.equals("m")?"男":"女"%></td></td><td><%=age%></td><td><%=birthday%></td>
    <td><a href="modify.jsp?sid=<%=sid%>">修改</a></td>
    <td><a onclick="return confirm('确定要删除<%=name%>的数据吗?')" href="student-servlet?action=remove&sid=<%=sid%>">删除</a></td>
  </tr>
  <%
  }
%>

Add.html(表单提交与ajax提交)

在建立Add.html的时候,我们需要知道:Add.html和Modify.jsp中的的数据先要传到Servlet,然后在Servlet中调用Dao层的方法对数据库进行操作。

又因为它们的数据都是传到同一个Servlet下(student-servlet)下,所以需要放置一个隐形的判断变量,也就是名为action的变量,来告诉student-servlet该执行添加学生操作还是修改学生操作。

<form action="student-servlet" method="post">
    <input type="hidden" name="action" value="add"/>
    <table border="1">
        <tr>
            <td>学号</td>
            <td><input type="text" name="sid"/></td>
        </tr>
        <tr>
            <td>姓名</td>
            <td><input type="text" name="name"/></td>
        </tr>
        <tr>
            <td>性别</td>
            <td>
                <input type="radio" name="gender" value="m" checked/>男
                <input type="radio" name="gender" value="f"/>女
            </td>
        </tr>
        <tr>
            <td>年龄</td>
            <td><input type="number" name="age"/></td>
        </tr>
        <tr>
            <td>生日</td>
            <td><input type="date" name="birthday"/></td>
        </tr>
        <tr>
            <td></td>
            <td><button type="submit">提交</button></td>
        </tr>
    </table>
</form>

 可以看到的是,这里我们使用的是表单的提交形式。每次提交都会进行页面的刷新。而在之前的实验一中我们用到了ajax的形式,当登录信息错误时不刷新界面,直接给出错误信息。

先来看看实验一的提交页面和servlet,这会对我们下面构建servlet提供灵感。

登录界面代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width ,initial-scale=1.0">
        <title>登录界面</title>
        <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
        <!--引入Jquery-->
        <script src="js/jquery-3.7.1.min.js"></script>
        <style>
            .img{
                float: left;
                display: inline;
            }
            .TakeIn{
                float: left;
                margin-left: 10px;
                margin-top: 50px;
            }
        </style>
        <script>
            function login(){
                //读取文本框的值
                var usename = $("#usename").val();
                var password = $("#password").val();
                if(usename=="" || password==""){
                    alert("必须输入,不能为空!");
                    return false;
                }
                $.ajax(
                    {
                        url:"http://localhost:8080/demo_war_exploded/login-servlet",
                        type:'post',
                        dataType:'json',
                        data:{
                            usename:usename,
                            password:password
                        },
                        success:function(res){
                            if(res.code=="200"){
                                sessionStorage.setItem("usename",usename)
                                window.location = "index.html"
                            }{
                                $("#msg").text(res.msg)
                            }
                            },
                        error:function(){
                            console.log("Error!");
                        }
                    }
                    )
                }

        </script>
    </head>
    <body>
        <div class="img">
            <img src="/img/OIP-C.jpg" alt="logo" class="img-fluid visible-lg-block" width="300" height="150">
        </div>
        <div class="TakeIn">
            <input type="text" id= "usename" placeholder="请输入用户名" ><br>
            <input type="password" id="password" placeholder="请输入密码"> <br>
            <button type="button" onclick="login()"> 登录 </button><br>
            <span id="msg" style="color: red;"></span>
        </div>
    </body>
</html>

 这个页面的内容相当丰富。

首先是引入特定的库,Bootstrap的作用是让页面大小发生变化时呈现不同的结果,利于Web项目适配移动端。他的作用代码其实只有img类div中的"img-fluid visible-lg-block",用于页面缩小时隐藏图片。

我们这里的引入的Jquery是从本地引入的,引入它是为了更方便地使用ajax,Jquery的标志就是那个很显眼的美元符号($)。

ajax可以不刷新页面返回结果。在输入错误的时候不传给servlet,只有正确的时候才传过去。

<style>里面的是css,是为了修改下面div的样式。

code的设定,我们可以在servlet里面看到。

你大概已经看出来了程序的逻辑:

首先,当我点下“登录”界面的时候,login函数立刻开始执行。函数首先读取usename和password判断是不是空的,然后进入ajax执行。如果执行成功再进行进一步判断。

if中的第二个大括号中的语句(msg)不受if的约束,也就是说不管登录成不成功都执行。

来看看servlet吧。

@WebServlet(name = "loginServlet", value = "/login-servlet")
public class loginServlet extends HttpServlet {
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //添加响应头,允许跨域
        response.addHeader("Access-Control-Allow-Origin","*");
        response.addHeader("Access-Control-Allow-Method","POST,GET");
        response.setContentType("application/json");
        String usename = request.getParameter("usename");
        String password = request.getParameter("password");
        PrintWriter out = response.getWriter();
        if(usename.equals("admin") && password.equals("123")){
            out.println("{\"code\":200,\"msg\":\"ok\"}");
        }else {
            out.println("{\"code\":600,\"msg\":\"用户名或密码错误\"}");
        }
    }
    public void destroy() {
    }
}

首先需要确保方法是post的(get就全看到了),然后你可以看到,这里有对code的定义,那个\“表示转义字符。 

Servlet

收拾行囊,我们回到实验二。首先不要忘了引入相应的元素和把servlet的value设定好,这样前端才能对应到它。

import cn.edu.hit.dao.StudentDao;
import cn.edu.hit.dao.impl.StudentDaoImpl;
import cn.edu.hit.entity.Student;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "StudentServlet", value = "/student-servlet")

接下来看看正文怎么写:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException{
        doPost(request,response);
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");
        //为了判断是谁提交的操作(add.html还是modify.jsp都提交到student-servlet)
        //所以我们需要设置一个隐藏的变量,用于判断
        String action = request.getParameter("action");
        StudentDao dao = new StudentDaoImpl();
        if(action.equals("add")) {
            //以下的sid指的就是我表单里的name
            String sid = request.getParameter("sid");
            String name = request.getParameter("name");
            //这里要做数据类型的强制转换,因为getParameter方法默认读取的是String类型
            int age = Integer.parseInt(request.getParameter("age"));
            String birthday = request.getParameter("birthday");
            String gender = request.getParameter("gender");
            Student student = new Student(sid, name, age, birthday,gender);
            dao.Add(student);
        }
        else if (action.equals("modify")){
            String sid = request.getParameter("sid");
            String name = request.getParameter("name");
            //这里要做数据类型的强制转换,因为getParameter方法默认读取的是String类型
            int age = Integer.parseInt(request.getParameter("age"));
            String birthday = request.getParameter("birthday");
            String gender = request.getParameter("gender");
            Student student = new Student(sid,name,age,birthday,gender);
            dao.Modify(student);
        } else if (action.equals("remove")) {
            String sid = request.getParameter("sid");
            dao.Remove(sid);
        }
        //进行重定向操作,表明无论进行什么操作,最后都能回到最开始的页面
        response.sendRedirect("index.jsp");
    }

 你可以发现两者是有共同点的(request.getParameter方法),而且student-servlet看来还要更简单了。

注意我们之前所说的,student-servlet需要判断到底应该执行什么操作,所以这里写了一个if条件判断语句。

Modify.jsp

<%--
  Created by IntelliJ IDEA.
  User: Lenovo
  Date: 2024/4/4
  Time: 11:24
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="cn.edu.hit.dao.StudentDao,cn.edu.hit.dao.impl.StudentDaoImpl" %>
<%@ page import="cn.edu.hit.entity.Student" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%
    //“sid”是href中所提及的sid,并不是java语言中的sid
    //顺带一提,在jsp文件中,request与response被称为内置对象,是不用再额外声明的
    String sid = request.getParameter("sid");
    StudentDao dao  = new StudentDaoImpl();
    Student student = dao.GetStu(sid);
    String name = student.getName();
    int age = student.getAge();
    String gender = student.getGender();
    String birthday = student.getBirthday();
%>
<form action="student-servlet" method="post">
    <input type="hidden" name="action" value="modify"/>
    <table border="1">
        <tr>
            <td>学号</td>
            <td><input type="text" name="sid" value="<%=sid%>" readonly="true"/></td>
        </tr>
        <tr>
            <td>姓名</td>
            <td><input type="text" name="name" value="<%=name%>"/></td>
        </tr>
        <tr>
            <td>性别</td>
            <td>
                <!--是男的就默认先选男,是女的就默认先选女,然后两个radio的name要取得一样,这可以让二者的选择存在一种互斥关系-->
                <input type="radio" name="gender" value="m" <%if(gender.equals("m")) out.print("checked");%>/>男
                <input type="radio" name="gender" value="f" <%if(gender.equals("f")) out.print("checked");%>/>女
            </td>
        </tr>
        <tr>
            <td>年龄</td>
            <td><input type="number" name="age" value="<%=age%>"/></td>
        </tr>
        <tr>
            <td>生日</td>
            <td><input type="date" name="birthday" value="<%=birthday%>"/></td>
        </tr>
        <tr>
            <td></td>
            <td><button type="submit">提交</button></td>
        </tr>
    </table>
</form>
</body>
</html>

不要忘记jsp中引入操作要怎么写 —— <%@ page import  a,b...%>

我们这里使用到了循环来将已知的消息填入空格作为缺省值,而且学号是不能改的readonly=="true"

login.html和login-servlet

在login.html中,表单登录的编写你应该已经很熟悉了:

<form action="login-servlet" method="post">
    <table border="1">
    <!--value=<%=>的方法是java方法,js的写法见下文-->
    <tr>
        <td>学号</td>
        <td><input type="text" id="usename" name="usename" placeholder="请输入用户名" /></td>
    </tr>
    <tr>
        <td>学号</td>
        <td><input type="text" id="password" name="password" placeholder="请输入密码"/></td>
    </tr>
        <td></td>
        <td><button type="submit">登录</button>
        <!--这里需要选择checkbox-->
       <input type="checkbox" name="rememberMe" value="1"/>记住我</td>

    </table>
</form>

login-servlet的代码如下 :

public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");
        String usename = request.getParameter("usename");
        String password = request.getParameter("password");
        UserDao userDao = new UserDaoImpl();
        if(userDao.login(usename,password)){
            String remenberMe = request.getParameter("rememberMe");
            if(remenberMe!=null){
                Cookie cookie1 = new Cookie("usename",usename);
                Cookie cookie2 = new Cookie("password",password);
                //给cookie设置有效期,这下面的单位是秒
                cookie1.setMaxAge(60*60*24*7);
                cookie2.setMaxAge(60*60*24*7);
                response.addCookie(cookie1);
                response.addCookie(cookie2);
            }
            //判断正确的话,我可以获取session对象
            HttpSession session = request.getSession();
            session.setAttribute("usename",usename);
            //这里的session是传给index.jsp的
            response.sendRedirect("index.jsp");
        }
        else{
            response.sendRedirect("login.html");
        }

我们先忽略其中的Cookie操作,我们发现这些语句我们是相当熟悉的。 

制作User数据表单,记录所有合法的登录

这是表单的样子:

 

根据前面的已有的操作,我们还可以尝试实现一个“注册”功能。

但是我懒得尝试了。

使用Cookie来“记住我”

重新回来看看login-servlet中的那个cookie操作:

            String remenberMe = request.getParameter("rememberMe");
            if(remenberMe!=null){
                Cookie cookie1 = new Cookie("usename",usename);
                Cookie cookie2 = new Cookie("password",password);
                //给cookie设置有效期,这下面的单位是秒
                cookie1.setMaxAge(60*60*24*7);
                cookie2.setMaxAge(60*60*24*7);
                response.addCookie(cookie1);
                response.addCookie(cookie2);
            }

你可以认为Cookie是存在本地的数据记录(这并不是正式的说法) ,在浏览器按下F12打开“应用程序”,你可以看到存在浏览器里的Cookie。

 setMaxAge方法其实就是设置Cookie的保存时间,你不设定的话一关浏览器就没了。它的单位是秒,我们这里设置了一周。

最后如果想在login.html把Cookie中存的数据填入登录栏,作为缺省值,我们怎么做呢?

<script>
    var index = document.cookie.indexOf(";");
    //alert(document.cookie);
    var first = document.cookie.substring(0,index);
    var second = document.cookie.substring(index+1);
    var usename = first.substring(first.indexOf("=")+1);
    var password = second.substring(second.indexOf("=")+1);
    //html上的属性或者文本(例如h1这种)才用innerText,如果是向input元素中输入内容,就使用value
    document.getElementById("usename").value = usename;
    document.getElementById("password").value = password;
</script>

Cookie其实就是一串字符串,你可以试着用Alert来打开它看看。

按照上面的方法慢慢切分即可。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值