kiddkid的javaweb后端笔记

Tomcat

启停服务

  • 打开tomcat压缩包,在bin里面,双击startup.bat,然后弹出CMD窗口,不要关闭

  • 打开浏览器,输入localhost:8080,能看到tomcat的默认主页

将web应用放置到服务器

  1. 直接把web应用文件夹复制到webapps里面去,文件夹名就是访问路径

在这里插入图片描述

  1. conf/server.xml里的host标签内配一个Context标签
    在这里插入图片描述

  2. conf/Catalina/localhost文件夹下 添加一个xml文件

    在这里插入图片描述

  3. 将真实web应用文件夹打包为war包,然后将war包放置到webapps里面

  • 当前项目文件夹下输入cmd回车

  • 使用jar命令
    在这里插入图片描述

  • 进行压缩

jar -cvf FF.war *

FF.war要压缩的文件名 *所有文件进行压缩

  • 移动.war位置,从原项目下剪切到webapps

在这里插入图片描述

  • 启动tomcat,进行访问

在这里插入图片描述

  1. tomcat的后台管理里的应用管理,远程上传一个war文件(远程上传,停止卸载方便)
  • 输入要配置的用户名,密码等内容

在这里插入图片描述

  • 压缩war包
    在这里插入图片描述

  • 点击Manager App
    在这里插入图片描述

  • 输入配置内容
    在这里插入图片描述
    在这里插入图片描述

  • 部署
    在这里插入图片描述

  • 根据war包名称访问
    在这里插入图片描述

关于ROOT

浏览器访问页面

在这里插入图片描述

增加主机

  • 增加Host标签

  • 写入本地文件
    在这里插入图片描述

  • 新建文件夹
    在这里插入图片描述

  • 访问: www.kiddkid.com:8080/FFFF/index.html

IDEA中创建web应用和使用tomcat

  • tomcat可以在IDEA eclipse等中使用
  • web应用应该包含静态资源动态资源以及一些配置文件 , 也就是说 一个web应用 应该有良好的文件目录结构,来管理不同类型的文件
  • web应用结构在eclipse 和 IDEA 中 有些许不同,使用了maven 等项目构建和管理工具之后,结构也会发生变化.

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
F3:浏览器上运行时的项目名,可更改
在这里插入图片描述
在这里插入图片描述

Servlet

介绍

  • servlet是服务器端java写的小程序,是被web服务器(容器)来运行管理的,用来处理客户端的请求

  • Servlet三大域: Request Session Application

WEB-INF:安全路径
在这里插入图片描述
在这里插入图片描述

关于"/"

  • 在页面上 路径开头部分的 / 代表的是虚拟主机 也就是 localhost:8080
<h2><a href="/servlet0321/first">点击访问第一个Servlet程序1</a></h2>
<!-- 超链接默认是get请求 -->
  • web.xml / 注解 里 书写路径时 开头的/ 代表本项目
<welcome-file-list>
    <welcome-file>/index.html</welcome-file>
</welcome-file-list>

servlet配置

web.xml(tomcat)

在这里插入图片描述

<!-- 配置servlet组件 javaweb三大组件: servlet filter listener -->
<servlet>
    <!-- web.xml 中可以配置多个servlet,这里的first仅仅是一个昵称/符号 -->
    <servlet-name>first</servlet-name>
    <servlet-class>com.ls.servlets.FirstServlet</servlet-class><!-- 反射 -->
    <!-- 对内找这个类 -->

    <!-- 可以配置servlet 跟随服务器的启动(web应用的加载)而创建和初始化 -->
    <!-- 数值越小,创建和初始化时间越早,数值是一个正整数 -->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>first</servlet-name>
    <!-- 对外first对应的servlet访问路径 -->
    <!-- 访问路径 可以配置多个,可以使用通配符 -->
    <url-pattern>/first</url-pattern>
    <url-pattern>/first.action</url-pattern>
    <url-pattern>/first.do</url-pattern>
    <url-pattern>*.f30</url-pattern>
    <url-pattern>/a/b/c/*</url-pattern>
    <!-- 当精确的和有通配符的同时满足,先以精确的为准,再以通配符为准 -->
</servlet-mapping>
  • ServletConfig
<!-- servlet的配置 -->
<servlet>
    <servlet-name>t3</servlet-name>
    <servlet-class>com.ls.servlets.ThirdServlet</servlet-class>
    <!-- servlet的初始化参数 是配置在指定的servlet内的,所以ServletConfig对象 是 每一个servlet都有的 -->
    <!-- servlet 之间不共享! -->
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>tea</param-name>
        <param-value>jasmin tea</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>t3</servlet-name>
    <url-pattern>/third</url-pattern>
</servlet-mapping>
//获取当前servlet在 web.xml 或 使用注解 做的配置
ServletConfig servletConfig = this.getServletConfig();
//获取配置中所有初始化参数名字 的迭代器(老版本)
Enumeration<String> initParameterNames = servletConfig.getInitParameterNames();
while (initParameterNames.hasMoreElements()){
    String name = initParameterNames.nextElement();
    System.out.println(name + "=" +servletConfig.getInitParameter(name));
}
System.out.println(servletConfig.getServletName());
  • ServletContext
<!-- ServletContext对象,一个web应用 对应 一个servlet上下文对象 -->
<!-- 作用:用于servlet跟web应用服务器(容器)进行交流的.是web应用内所有servlet共享的 -->
<!-- ServletContext对象 就代表了 整个web应用 -->
<!-- ServletContext对象 也被成为 application 应用域 , 跟随服务器启动(web应用加载)而创建了,是最大的域对象 -->
<!-- 域对象:有4个,域:有一定的作用范围 -->
<context-param>
    <param-name>key</param-name>
    <param-value>正月里来是新春!</param-value>
</context-param>
//获取web应用对象/上下文对象
ServletContext application = this.getServletContext();
String key = application.getInitParameter("key");
System.out.println(key);

resp.getWriter().println("OK 4!");
注解

servlet注解里 urlPatterns属性里 路径开始的**/** 也是代表当前的项目

/* /:当前项目路径 */
@WebServlet(urlPatterns = {"/s4","/ssss"},loadOnStartup = 1,
initParams = {@WebInitParam(name="keyS4",value = "values4"),@WebInitParam(name="teas",value = "花茶s")})
public class FourServlet extends HttpServlet {
	....
}

在这里插入图片描述
在这里插入图片描述

请求与响应

在这里插入图片描述

请求
  • 请求转发 和 请求包含 都是 服务器上应用内部的行为.对客户端不可见.
    在这里插入图片描述

  • 获取请求的配置等

@WebServlet(urlPatterns = {"/FiveServlet"})
public class FiveServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //请求对象可以获取 客户端请求里的方式,请求的头信息,请求的参数信息,其他信息
        System.out.println("本次请求的请求方式: " + req.getMethod());

        //获取本次请求的所有请求头的名字 组成的 迭代
        Enumeration<String> headerNames = req.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String header = headerNames.nextElement();
            System.out.println("请求头: " + header +"\t" +req.getHeader(header));
        }

        //获取请求参数(查询参数) 如果get方式 url?后面的键值对
        //如果是post方式,就是表单里的组件的name 和 组件的值 的 键值对
        //获取指定请求参数名的 请求参数值
        System.out.println("p1 = "+req.getParameter("p1"));

        System.out.println("本次请求的URL: "+req.getRequestURL());//带协议
        System.out.println("本次请求的URI: "+req.getRequestURI());//项目下的资源
        System.out.println("本次请求的主机: "+req.getLocalAddr());
        System.out.println("本次请求的 请求参数的字符串: "+req.getQueryString());//查询字符串
        System.out.println("本次请求的web应用上下文路径: "+req.getContextPath());

        //响应对象 打印回一句话
        resp.getWriter().println("okkkkk!");
    }
}
  • 获取请求参数
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //设置本次请求中的字符编码形式 仅对post方式起效果
    //get方式下 不设置字符编码格式 就默认为UTF-8,原因是在 tomcat 的安装包里 server.xml 配置里.
    req.setCharacterEncoding("UTF-8");



    //获取表单里的请求数据
    //1.获取指定请求参数名字的 值
    System.out.println(req.getParameter("username"));
    System.out.println(req.getParameter("pass"));
    System.out.println(req.getParameter("nickname"));//无此请求参数
    //2.获取请求参数的值是数组形式
    String[] hobbies = req.getParameterValues("hobby");
    Stream.of(hobbies).forEach(System.out::println);
    //3.获取本次请求中的 所有请求参数名字 作为键的map
    Map<String, String[]> parameterMap = req.getParameterMap();
    for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
        System.out.println("请求参数名: "+entry.getKey());
        System.out.println("请求参数值: "+ Arrays.toString(entry.getValue()));
    }
    //响应对象里设置 响应回的数据的字符编码形式 还可以设置响应头
    resp.setHeader("Content-Type","text/html;charset=UTF-8");
    resp.getWriter().write("一百昏!");
}
请求转发
@WebServlet(urlPatterns = {"/EightServlet"})
public class EightServlet extends HttpServlet {

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doPost(req, resp);
}

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //请求里的参数(来自客户端,来自用户)
    String p1 = req.getParameter("p1");
    System.out.println("8号servlet里面,获得的请求参数p1 = " + p1);

    //请求转发 的 是同一个请求,也就在一个请求域内. 任何的域对象 都可以向作用域中设置属性(来自服务器内部)
    req.setAttribute("attr1","属性值1");

    //经过某些逻辑,需要将请求转发给9号
    req.getRequestDispatcher("/NineServlet").forward(req,resp);
}

}
@WebServlet(urlPatterns = {"/NineServlet"})
public class NineServlet extends HttpServlet {

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doPost(req, resp);
}

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //可以再次获取 由8号 转发过来的请求里的 请求参数(原本来自客户端)
    String p1 = req.getParameter("p1");
    System.out.println("9号 获取请求参数p1 = "+p1);

    //获取同一个请求域中 刚才 8号 给我传递过来的属性
    String attr1 = (String) req.getAttribute("attr1");
    System.out.println("请求域里的属性值 attr1 = "+attr1);

    resp.setContentType("text/html;charset=UTF-8");
    resp.getWriter().write("咱们9号为你完成了处理!");
}

}
请求包含

在这里插入图片描述

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    System.out.println(req.getParameter("p10"));
    req.setAttribute("attr10","attr10");
    //请求包含
    req.getRequestDispatcher("/ElevenServlet").include(req,resp);
    resp.getWriter().write("10!");
}
请求重定向

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

响应
  • 响应对象里设置 响应回的数据的字符编码形式 还可以设置响应头
//设置本次响应回的内容类型是 页面类型 编码是UTF-8
resp.setHeader("Content-Type","text/html;charset=UTF-8");
resp.getWriter().write("<h2 style='color:red;'>GET 好的</h2>");
  • 读文件(图片)
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //使用上下文(应用域)对象 获得web目录下的1.png文件的数据
    ServletContext application = req.getServletContext();
    //获取web目录下/imgs/1.png在tomcat软件 所在主机上的 真实路径
    String realPath = application.getRealPath("/imgs/1.png");
    FileInputStream fis = new FileInputStream(realPath);
    //使用 响应流 输出
    //设置 响应的内容类型是图片
    resp.setContentType("image/png");
    IOUtils.copy(fis,resp.getOutputStream());
}

JSP java serve page

介绍

在这里插入图片描述
在这里插入图片描述

<%@ page contentType="text/html;charset=UTF-8" language="java" errorPage="error.jsp" %>
<html>
<head>
    <title>JSP已经过时!</title>
</head>
<body>
<p>
    1.JSP java serve page 使用java语言生成HTML的技术.<br/>
    2.原理: .jsp文件 在HTML的基础上 嵌入java语法的部分,交给JSP容器(web应用服务器)来处理.<br/>
    3.JSP容器 会把.jsp文件 生成.java文件.
</p>
<p>
    JSP可以由7部分组成.
    html
    jsp编译指令 第一行就是页面编译指令,通常是当前JSP页面的一些属性或导包等.
    jsp动作指令
    jsp注释
    jsp声明 jsp文件生成的java文件 里的 成员变量 或 成员方法 声明使用的
    jsp脚本
    jsp表达式
</p>
<!-- html注释 看浏览器页面源码,可以看到 -->
<%-- jsp注释 在java生成html的时候,会被跳过,看不到 --%>

<%-- 声明 --%>
<%! private int a = 10; %>
<%! public int add2(int a, int b) {
    return a + b;
}%>

<%-- 脚本 --%>
<%
    int a = 10, b = 20,c=0;
    if (a + b >= 25) {
%>
    <p>a和b的总和多于25!</p>
<%
    }
%>

<%-- 表达式 --%>
<%=a+b%>
<%=a/c%>
<%="hello word!"%>
</body>
</html>

错误页面

<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<html>
<head>
    <title>错误页面</title>
</head>
<body>
<%-- 500是服务器内部 异常导致的 --%>
<p>服务器内部出错了,稍后重试,请联系:网络管理员:QQ:123132</p>
<p>JSP内 默认有个内置对象 excpetion 可以在错误页面内 使用出来,获取异常的信息 </p>
<p>错误信息:<%=exception.getMessage()%></p>
<img src="/servlet0321/imgs/1.png" width="400px"/>
</body>
</html>
  • 单页面配置
<%@ page contentType="text/html;charset=UTF-8" language="java" errorPage="error.jsp" %>
  • 全局错误页面配置(web.xml)
<error-page>
    <error-code>500</error-code>
    <location>/error.jsp</location>
</error-page>
<error-page>
    <error-code>404</error-code>
    <location>/error404.jsp</location>
</error-page>

页面包含

在这里插入图片描述
在这里插入图片描述

jsp内置对象

在这里插入图片描述

  • 9个内置对象里 有4个是 作用域对象

  • 大到小顺序: application > session > request > pageContext(页面域)

  • 作用域都有:setAttribute getAttribute removeAttribute 方法

  • pageContext里 有个 方法 findAttribute(“键”) 自动从小到大 查找域中的属性

  • pageContext内置对象 可以通过getXXX 获取其他8个对象

    • 在这里插入图片描述
<a href="/servlet0321/XYServlet">访问XYServlet</a>

<%
    pageContext.setAttribute("name","page");
    request.setAttribute("name","req");
    session.setAttribute("name","session");
    application.setAttribute("name","application");
%>

<%=pageContext.getAttribute("name")%><br/>
<%=request.getAttribute("name")%><br/>
<%=session.getAttribute("name")%><br/>
<%=application.getAttribute("name")%><br/>

在这里插入图片描述

EL&JSTL

介绍

jsp中,html和java混在一起,不利于观看和维护,不符合面对象的思想

java提供了JSTL 和 EL 替代 JSP的脚本,表达式等,使得jsp更像是页面

EL expressionLanguage
  • EL expressionLanguage 是表达式语言. 格式$ {},大括号内可以完成简单的运算
  • ${对象.某属性} 实际去调用 对象.get某属性方法()
  • 更重要的是 获取某对象里的值,还有11个内置对象
  • 当值是空的时候,EL会自动转为空串
    • 在这里插入图片描述
<%--<%=request.getAttribute("key100")%>--%>

<%-- el表达式里  requestScope 表示 请求域中 所有 属性键值对 组成的集合 --%>
<%-- el表达式里  sessionScope 表示 会话域中 所有 属性键值对 组成的集合 --%>
<%-- el表达式里  applicationScope 表示 应用域中 所有 属性键值对 组成的集合 --%>
<p>
   请求域里的数据: ${requestScope.reqKey}
</p>
<p>
    会话域里的数据: ${sessionScope.sessionKey.name} - ${sessionScope.sessionKey.age}
</p>
<p>
    应用域里的数据: ${applicationScope.appKey}
</p>

<%-- 原理是 pageContext.findAttribute(键) --%>

<p>
    最好用的从小到大的 在 四个作用域中找数据: ${reqKey} - ${sessionKey} - ${appKey}
</p>

<p>
    学生的年龄大于25: ${sessionScope.sessionKey.age >= 25}
</p>
<p>
    域中的属性 some : ${some}  <%-- 使用EL去取值的好处,当值是空的时候,EL会自动转为空串 --%>
</p>
<p>
    域中的属性 some : <%=pageContext.findAttribute("some")%>
</p>
<p>
    域中的属性 some : ${some==null}
</p>
JSTL jsp standard tag lib
  • JSTL: jsp standard tag lib jsp标准标签库,是一种标签形式的java代码
  • 主要能够完成页面里 判断 循环 遍历 设置 等功能
  • 需要导入.jar
core标签库

遍历

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%-- taglib指令用来导入 标签库信息 prefix是标签的前缀 --%>
<html>
<head>
    <title>Title</title>
</head>
<body>
       
<h2>JSTL结合EL来遍历域中的各种数据</h2>
<%-- items是要遍历的域中的集合或数组  var是集合或数组里的单个数据 --%>
<%-- varStatus遍历时 集合中单个数据的状态 --%>
<%-- step 对应 普通循环里的 i++ --%>
<%-- begin 对应 普通循环里的 i的初始值  end对应 普通循环里i的结束值 --%>
<c:forEach items="${requestScope.names}" var="nick" varStatus="state">
    <p>
            ${nick} - 遍历时当前数据的个数:${state.count} - 遍历时当前数据的索引: ${state.index}
    </p>
</c:forEach>

<c:forEach begin="1" end="5" step="1" varStatus="sta">
    <p>你好吗? - ${sta.index}</p>
</c:forEach>

<hr/>
学生名字:${student.name} - 学生年龄:${requestScope.student.age}
<hr/>

<h2>遍历List</h2>
<c:forEach items="${requestScope.stus}" var="stu">
    <p>学生名字:${stu.name} - 学生年龄:${stu.age}</p>
</c:forEach>

<h2>遍历Map</h2>
<c:forEach items="${requestScope.maps}" var="entry">
    <p style="background-color: cornflowerblue">键:${entry.key}</p>
    <p>学生名字:${entry.value.name} - 学生年龄:${entry.value.age}</p>
</c:forEach>

<h2>遍历Map2</h2>
<c:forEach items="${requestScope.datas}" var="item" varStatus="state">
    <p style="background-color: gold">键:${item.key}</p>
    <c:forEach items="${item.value}" var="s">
        <p>学生名字:${s.name} - 学生年龄:${s.age}</p>
    </c:forEach>
</c:forEach>

</body>
</html>

注: 当step==2

在这里插入图片描述

补充: jstl表达式判空:
在这里插入图片描述

判断标签

<c:if test="${requestScope.student.age<18}">
    <p>${requestScope.student.name}还未成年!</p>
</c:if>
<c:if test="${requestScope.student.age>=18}">
    <p>${requestScope.student.name}已经成年!</p>
</c:if>


<%-- 向某个域中设置属性值 --%>
<c:set scope="session" var="nation" value="俄罗斯"/>
<%-- 多选switch --%>
<c:choose>
    <c:when test="${nation==null}">没有值</c:when>
    <c:when test="${nation=='中国'}">是中国</c:when>
    <c:when test="${nation=='俄罗斯'}">是俄罗斯</c:when>
    <c:otherwise>是其他</c:otherwise>
</c:choose>
fmt标签库
国际化
  • 配置资源文件
    在这里插入图片描述
    在这里插入图片描述

  • 监听器中在应用域里设置大语言
    在这里插入图片描述

  • 在要修改的部分设置资源文件
    在这里插入图片描述

  • 与后端相连的语言切换按钮
    在这里插入图片描述

  • 后端程序
    在这里插入图片描述

Maven

介绍

在这里插入图片描述

maven的web项目结构

在这里插入图片描述

MVC

在这里插入图片描述

Lombok

在这里插入图片描述

Filter

介绍

servlet listener filter 都是由容器进行管理运行的
在这里插入图片描述

过滤器常见功能

  1. 字符编码过滤
  2. 权限验证
  3. 敏感词汇替换
  4. … 很多

示例

过滤post请求普通请求的组件
在这里插入图片描述

listener

  • 介绍
    在这里插入图片描述

  • 配置

    • @webListener 里面没有要配置的数据,只是在事件发生时要运行的代码(对内,不对用户)
  • 示例

@WebListener
public class AppListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        //sce是 事件对象,可以从中获取一些发生事件时的信息(数据)
        ServletContext application = sce.getServletContext();
        System.out.println("application 创建了!");
        //查询所有的角色信息,设置到应用域中
        RoleService roleService = RoleService.getInstance();
        List<Role> rolelist = roleService.rolelist();
        application.setAttribute("rolelist",rolelist);
        //遍历测试
        rolelist.stream().forEach(System.out::println);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("application 销毁了!");
    }
}

注: 用的多 不常变

Cookie

介绍

在这里插入图片描述

示例

  • 十天免登录

用户页面

<div class="form-group form-check">
    <input type="checkbox" class="form-check-input" id="Check1" value="1" name="remember">
    <label class="form-check-label text-muted" for="Check1">记住用户名和密码</label>
</div>

后端

//判断是否勾选了记住用户名和密码
String remember = req.getParameter("remember");
if ("1".equals(remember)) {
    //设置 用户名cookie 密码cookie 到客户端
    Cookie yhm = new Cookie("yhm",login.getUsername());
    yhm.setMaxAge(10*24*60*60);
    Cookie mm = new Cookie("mm",login.getPassword());
    yhm.setMaxAge(10*24*60*60);
    resp.addCookie(yhm);
    resp.addCookie(mm);
}

用户页面读入 : 使用EL表达式

<div class="form-group">
    <label for="username" class="text-muted">用户名:</label>
    <input type="text" class="form-control" name="username" value="${cookie.yhm.value}" id="username" aria-describedby="usernameHelp">
    <small id="usernameHelp" class="form-text text-muted">用户名或邮箱名</small>
</div>
<div class="form-group">
    <label for="password" class="text-muted">密码:</label>
    <input type="password" class="form-control" name="password" value="${cookie.mm.value}" id="password" aria-describedby="usernameHelp">
    <small id="passwordHelp" class="form-text text-muted">这里可以写一些提示的小文字</small>
</div>

AJAX

介绍

AJAX : Asynchronous JavaScript and XML
在这里插入图片描述

应用方法

在这里插入图片描述

应用实例

  • 大分类级联小分类

每次点击<a>,总会重新请求后端程序,重新遍历,然后返回新的数据,从而做到及时刷新

展示页面:

<div class="container-fluid">
    <h2 class="mt-2 pb-2">分类信息</h2>
    <div class="row">
        <div class="col-2"></div>

        <div class="col-4">
            <label for="big">大分类</label>
            <select class="custom-select custom-select-lg" id="big" >
                <option value="0">请选择</option>
                <c:forEach items="${requestScope.bigs}" var="big">
                    <option value="${big.bigid}">${big.bigname}</option>
                </c:forEach>
            </select>
        </div>

        <div class="col-4">
                <label for="small">小分类</label>
                <select class="custom-select custom-select-lg" id="small" >
                    <option value="0">请选择</option>
                </select>
        </div>

        <div class="col-2"></div>
    </div>
</div>

<script>
    //大分类:
    //上面经categorylist.action servlet 转发过来 bigs,在页面遍历出来
    //还可以在categorylist.jsp页面 加载完毕之后 发一个ajax请求,得到 所有大分类的List<Big> 对应的json格式的字符串
    //----------------------  window.onload = function(){} ---- $(function{}) --> $(document).ready()

    //小分类:
    //必须使用ajax 根据大分类的不同 去 请求 对应的小分类数据

    $("#big").change(function () {

        $("#small").empty();

        //当前选择的大分类选项
        let bigid = $("#big option:selected").val();
        if (bigid == "0"){
            let $op = $("<option value='0'>请选择小分类</option>");
            $("#small").append($op);
            return;
        }
        //发 ajax 请求
        let datas = {"choose":"small","bigid":bigid};
        $.ajax({
            url:"${pageContext.request.contextPath}/categorylist.action",
            data:datas,
            method:"post",
            dataType:"json",
            success:function (smalls) {//接收到返回数据后要做的事
                for (let i = 0; i < smalls.length; i++) {
                    let $opi = $("<option></option>");
                    $opi.val(smalls[i].smallid);
                    $opi.text(smalls[i].smallname);
                    $("#small").append($opi);
                }
            }
        })
    })

</script>
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String choose = req.getParameter("choose");
    if (choose == null){//如果请求不带参数,则查询大分类
        List<Big> bigs = CategoryService.getInstance().queryAllBig();
        req.setAttribute("bigs",bigs);
        req.getRequestDispatcher("/WEB-INF/categorylist.jsp").forward(req,resp);//转发回去,准备调用ajax
        return;
    }else if (choose.equals("small")){
        //按照 大分类id 查询对应的小分类数据
        String bigid = req.getParameter("bigid");
        int bid = Integer.parseInt(bigid);
        List<Small> smalls = CategoryService.getInstance().querySmallsByBigid(bigid);
        
        //页面ajax请求 预期的 json数据
        ObjectMapper om = new ObjectMapper();
        String jsonStr = om.writeValueAsString(smalls);
        resp.setCharacterEncoding("UTF-8");
        resp.getWriter().write(jsonStr);
    }

}
  • 用户名是否重复查询

jsp页面内:

function chkuser() {
    //当 用户名 输入框 失去焦点的时候 发送ajax异步请求 问 是否重名
    let b = false;
    b = chksome("#username",/[a-z0-9]{4,8}/i,"#usernameHelp");
    if (!b) return b;
    let username = $("#username").val();
    let datas = {"username":username,"op":"findname"};
    $.ajax({
        url:"${pageContext.request.contextPath}/member",//传递到后端服务器
        method:"post",
        data:datas,
        async:true,
        dataType:"text",
        success:function (msg) {
            if (msg == "1"){//返回
                $("#usernameHelp").removeClass("text-muted");
                $("#usernameHelp").removeClass("text-danger");
                $("#usernameHelp").addClass("text-success");
                $("#usernameHelp").text("√");
                b = true;
            }else {
                $("#usernameHelp").removeClass("text-muted");
                $("#usernameHelp").removeClass("text-success");
                $("#usernameHelp").addClass("text-danger");
                $("#usernameHelp").text("此用户名太受欢迎了,请再换一个!");
                b = false;
            }

        }
    })
    return b;
}

后端处理程序:
在这里插入图片描述

注: jdbc工具查询数据进行封装,当没有查到数据时会报错空结果集异常

后端功能

退出登录

在web.xml中配置会话自动失效的时间,tomcat默认配置30mins


在这里插入图片描述

注: 已设置post登录,可设置get退出

​ 此处设置会话立即失效

验证码

在这里插入图片描述

  1. 验证码刷新
<script>
    $("#verifyImg").click(function () {
        let num = Math.random();
        $(this).attr("src","${pageContext.request.contextPath}/verify?num="+num);
    })
</script>

ps:

  1. 带参可以使图片不被浏览器缓存

  2. 也可使用button

  3. 将验证码设置到会话域中

  4. 判断登录是否成功

String verify = req.getParameter("verify");//用户输入
HttpSession session = req.getSession();

String verifyCode = (String) session.getAttribute("verifyCode");//随机生成的原码,被设置在session
if (verify==null||!verify.equals(verifyCode)){
    req.setAttribute("msg","验证码输入错误!");
    req.getRequestDispatcher("/login.jsp").forward(req, resp);
    return;//不进行后续操作
}
//
  1. 登录成功后(进入首页前)

    session.removeAttribute("verifyCode");//登录后验证码失效
    

十天免登录

参照cookie

角色列表

在这里插入图片描述

头像上传

  • 上传图片回显
  1. 设置文件上传表单组件:

    <form ... enctype="multipart/form-data"> //编码改变
        .....
    </form>
    
<div class="custom-file">
    <input type="file" class="custom-file-input" id="customFile" name="headimg">
    <label class="custom-file-label" for="customFile">Choose file</label>
</div>
  1. 设置回显函数:
<script>
    $("#customFile").change(function () {
        let file0 = this.files[0];
        let url = URL.createObjectURL(file0);
        $("#showimg").attr("src",url).removeClass("d-none");//给img标签加
    })

    if (${param.msg=='upok'}){
        alert("上传头像成功!");
    }
</script>
  1. 表单提交到用户处理servlet
    1. 更新头像文件,上传到服务器对应的文件夹下

    2. login.setImageName() 设置登录用户的头像名

    3. 更新数据库中该用户的头像名,刷新头像

  • 图片服务器:

conf/server.xml : 修改端口 8005 8080(都在localhost)

conf/web.xml:添加
在这里插入图片描述
在这里插入图片描述

在listener中设置:

//用户头像文件所在的服务器的路径
application.setAttribute("headpath","http://localhost:7070/IMAGESERVER/heads/");

servelt中的代码:

protected void upheadimg(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //表单有文件上传组件,则编码格式发生改变,java的req无法获取请求参数
    /*String hidden = req.getParameter("hidden");
    System.out.println(hidden);*/

    //apache-commons-fileupload工具 解析 格式发生改变的 请求参数
    //磁盘文件项工厂
    DiskFileItemFactory factory = new DiskFileItemFactory();
    ServletFileUpload sfu = new ServletFileUpload(factory);

    sfu.setFileSizeMax(1024L*1024*20);//可解析的单个文件大小20MB
    sfu.setSizeMax(1024L*1024*20*20);//本次请求所有文件大小 最多 400MB

    try {
        //servlet请求的解析器 可以把请求参数的数据 解析为一个个的 文件项 对象
        List<FileItem> fileItems = sfu.parseRequest(req);
        //FileItem对象 有可能保存了 表单里的文件上传数据 还有可能保存了普通的请求数据
        for (FileItem fileItem : fileItems) {
            if (fileItem.isFormField()){//判断此文件项对象 是不是 普通的请求数据(表单组件)
                //获取 请求参数的名和值
                String fieldName = fileItem.getFieldName();
                System.out.println("普通表单组件:"+fieldName+"\t值:");
                System.out.println(fileItem.getString("UTF-8"));
            }else {
                ServletContext application = this.getServletContext();
                Object headpath = application.getAttribute("headpath");//应用域里 图片服务器保存图片的文件夹的路径

                String name = fileItem.getName();
                System.out.println("上传的文件名:"+name);
                InputStream inputStream = fileItem.getInputStream();


               /* //演示1:将用户的头像(文件)上传到 服务器上该应用内部的upload文件夹内
                    //使用应用域对象 来获取 upload文件 在 服务器所在机器的真实路径
                ServletContext application = this.getServletContext();
                String uploadRealPath = application.getRealPath("/upload");
                //这里IDEA和eclipse等其他编程软件做法不一样,IDEA将web应用放在工作区里的target目录里
                String upFileName = UUID.randomUUID().toString().replace("-","")+name;
                File upFile = new File(uploadRealPath,upFileName);
                FileOutputStream fos = new FileOutputStream(upFile);
                IOUtils.copy(inputStream,fos);*/

                //演示2:将文件传输到7070服务器
                String upFileName = UUID.randomUUID().toString().replace("-","")+name;//防止同名文件的覆盖
                //8080是7070的客户端
                Client client = new Client();
                //根据 资源路径 获取对应的web资源
                WebResource resource = client.resource(headpath+upFileName);
                //上传数据
                resource.put(inputStream);

                //更新数据库中 该用户的 headimg字段的值
                //调用用户业务层 将upFileName文件名 修改到 该用户的 headimg字段
                HttpSession session = req.getSession();
                User login = (User) session.getAttribute("login");//获取该用户
                UserService us = UserService.getInstance();
                us.updateHeadimg(login.getUid(),upFileName);//更新数据库图片
                login.setHeadimg(upFileName);//修改登录用户的字段
                req.getRequestDispatcher("/userdetails.jsp?msg=upok").forward(req,resp);
            }
        }
    } catch (FileUploadException e) {
        e.printStackTrace();
    }
}

tomcat/config/web.xml 含有mime类型 用来判断文件是什么类型

​ 应用: Content-Type
在这里插入图片描述

下载文件

  • 本地
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String action = req.getParameter("action");
    if (action!=null && "download".equals(action)) {
        //获取文件的真实路径
        ServletContext application = req.getServletContext();
        String realPath = application.getRealPath("/WEB-INF/files/生产记录.xlsx");
        
        File file = new File(realPath);
        FileInputStream fis = new FileInputStream(file);//输入流

        String fileName = file.getName();
        long length = file.length();
        
        //1.要下载的文件名是非中文,不用其他设置
        //2.要下载的文件名带有中文,文件名需要使用base64来转码
        fileName = URLEncoder.encode(fileName,"UTF-8");
        
        //设置本次响应的 响应头为 带有附件的
        resp.setHeader("content-disposition","attachment;filename="+fileName);
        
        //设置本次附件的大小
        resp.setContentLengthLong(length);
        IOUtils.copy(fis,resp.getOutputStream());//上传
    }
}
  • 网络

    protected void down(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取文件真实路径
        ServletContext app = req.getServletContext();
        String softPath = (String) app.getAttribute("sp");
        //app.setAttribute("sp","http://localhost:7070/share/soft/");
    
        String path = req.getParameter("path");
        String url = softPath+path;
    
        URL downURL = new URL(url);
        URLConnection urlConnection = downURL.openConnection();
        InputStream is = urlConnection.getInputStream();
    
        String contentType = urlConnection.getContentType();//MIME
        long length = urlConnection.getContentLengthLong();//内容大小
    
        //设置内容头
        resp.setContentType(contentType);
        //设置内容大小头
        resp.setContentLengthLong(length);
        //设置内容处理方式
        resp.setHeader("Content-Disposition","attachment;filename="+path);
    
        IOUtils.copy(is,resp.getOutputStream());
    }
    

分页

bean - servlet - service - dao - servlet - jsp

  • 不止用户数据需要分页显示,图片,公告…数据量多的都需要分页显示
  • 此类为设计的一个 方便在页面上展示每页数据和相关页码等的 数据模型类 / javabean
  • 通过物理分页 结合 mysql limit语句 n m
  • n:查询起始记录的下标
  • m:本次查几条(每页的条数)
  • n = (查询第几页 - 1) * m

数据模型类

public class PageModel<T> {
    //最主要的 每一页的 n条数据组成的集合
    private List<T> data;
    private int pagesize = 6;//每页几条数据 程序设定
    private int total;//总记录条数,必须查询数据得到
    private int lastPage;//总页数=最后一页页码  经过total pagesize计算
    private int currentpage = 1;//本次查询的页码 当前页 用户决定
    private int showFirst;//是否显示首页/上一页 默认=0:不显示 1:显示 经过currentpage计算
    private int showLast;//是否显示尾页/下一页 默认=0:不显示 1:显示 经过currentpage计算
    private int start;//页码的开始 经过currentpage计算
    private int end;//页码的结束 经过currentpage计算

    public List<T> getData() {
        return data;
    }

    public void setData(List<T> data) {
        this.data = data;
    }

    public int getPagesize() {
        return pagesize;
    }

    public void setPagesize(int pagesize) {
        this.pagesize = pagesize;
    }

    public int getTotal() {
        return total;
    }

    public void setTotal(int total) {
        this.total = total;
        //当确定好total以后 计算sum总页数 也就是 最后一页
        lastPage = total%pagesize == 0? total/pagesize : total/pagesize + 1;
        showFirst = currentpage == 1 ? 0:1;
        showLast = currentpage == lastPage ? 0 : 1;

        start = currentpage - 3;
        end = currentpage + 3;
        start = start <=0 ? 1 : start;
        end = end >= lastPage ? lastPage : end;
    }

    public int getLastPage() {
        return lastPage;
    }

    public void setLastPage(int lastPage) {
        this.lastPage = lastPage;
    }

    public int getCurrentpage() {
        return currentpage;
    }

    public void setCurrentpage(int currentpage) {
        this.currentpage = currentpage;
    }

    public int getShowFirst() {
        return showFirst;
    }

    public void setShowFirst(int showFirst) {
        this.showFirst = showFirst;
    }

    public int getShowLast() {
        return showLast;
    }

    public void setShowLast(int showLast) {
        this.showLast = showLast;
    }

    public int getStart() {
        return start;
    }

    public void setStart(int start) {
        this.start = start;
    }

    public int getEnd() {
        return end;
    }

    public void setEnd(int end) {
        this.end = end;
    }

    public PageModel() {
    }
}

用户页面后端程序

@WebServlet(urlPatterns = {"/userlist.action"})
public class UserListServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //点击 用户列表 按钮时 查询第一页
        PageModel<User> pageModel = new PageModel<>();//空桶
        //如果查询指定页 肯定要带参数
        String currentpage = req.getParameter("currentpage");
        if (currentpage!=null) {
            pageModel.setCurrentpage(Integer.parseInt(currentpage));
        }

        //业务层查询 填满pageModal
        UserService userService = UserService.getInstance();
        pageModel = userService.userlistByPage(pageModel);

        //请求域里设置 pageModal的对象
        req.setAttribute("pm",pageModel);
        //转发到 userlist.jsp上
        req.getRequestDispatcher("WEB-INF/userlist.jsp").forward(req,resp);
    }
}

用户页面

注: 是从pageModule对象pm中取currentPage,更新page也是保存在这里面

<!-- 分页 -->
<nav aria-label="Page navigation example">
    <ul class="pagination pagination-lg justify-content-center">
        <c:if test="${requestScope.pm.showFirst == 1}">
            <li class="page-item">
                <a class="page-link" href="${pageContext.request.contextPath}/userlist.action?currentpage=1" aria-label="Previous">
                    <span aria-hidden="true">首页</span>
                </a>
            </li>
            <li class="page-item">
                <a class="page-link" href="${pageContext.request.contextPath}/userlist.action?currentpage=${requestScope.pm.currentpage - 1}" aria-label="Previous"> 
                    <span aria-hidden="true">&laquo;</span>
                </a>
            </li>
        </c:if>
<!-- 每页的编号 -->
        <c:forEach begin="${requestScope.pm.start}" end="${requestScope.pm.end}" step="1" var="i">
            <c:if test="${requestScope.pm.currentpage == i}">
                <li class="page-item active"><a class="page-link" href="${pageContext.request.contextPath}/userlist.action?currentpage=${i}">${i}</a></li>
            </c:if><!-- 每个li都包含一个超链接,一旦点击就使用get传递当前第i页到后端程序 -->
            <c:if test="${requestScope.pm.currentpage != i}">
                <li class="page-item"><a class="page-link" href="${pageContext.request.contextPath}/userlist.action?currentpage=${i}">${i}</a></li>
            </c:if>
        </c:forEach>

        <c:if test="${requestScope.pm.showLast == 1}">
            <li class="page-item">
                <a class="page-link" href="${pageContext.request.contextPath}/userlist.action?currentpage=${requestScope.pm.currentpage + 1}" aria-label="Next">
                    <span aria-hidden="true">&raquo;</span>
                </a>
            </li>
            <li class="page-item">
                <a class="page-link" href="${pageContext.request.contextPath}/userlist.action?currentpage=${requestScope.pm.lastPage}" aria-label="Next">
                    <span aria-hidden="true">尾页</span>
                </a>
            </li>
        </c:if>


    </ul>
</nav>

注册

  • 思路
    在这里插入图片描述

用户名是否重复:参照AJAX应用实例

大小分类级联

参照AJAX应用实例

国际化

参照fmt标签库-国际化

会员签到

  • 准备工作

    • 数据库&bean
      • 连续签到concount 金币/积分gold 上次签到日期signdate 今日是否签到signflag
        • concount 计算该次签到gold
        • signdate 上一次和今天是否连续 如果连续,concount++
  • 制作跳转签到页面

  • 设置签到按钮触发ajax请求

    • $("#signBtn").click(function () {
          $.ajax({
              url:"${pageContext.request.contextPath}/sign",
              type:'post',
              dataType:'json',
              success:function (res) {
                  $("#p1").text("签到成功!连续签到天数: "+res.map.concount);
                  $("#p2").text("获得积分 :"+ res.map.addGold);
                  $("#p3").text("目前总积分 :"+ res.map.sum);
                  $("#signA").attr("href","#").text("已签到");
              }
          })
          $("#signBtn").addClass("disabled");
      })
      
  • 后端

    • /sign (servelt)

      • protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            //签到
            Member member = (Member) req.getSession().getAttribute("member");
            Map<String, Object> data = getData(member);//保存更改后的数据
            data.put("id",member.getId());
            
            //调用业务层
            MemberService ms = new MemberServiceImpl();
            Member newMember = ms.sign(data);
            
            //更新session
            req.getSession().setAttribute("member",newMember);
            
            //响应
            Result result = new Result();
            result.setStatus("200");
            result.setMsg("okk");
            result.setMap(data);
        
            //将数据返回前端页面
            ObjectMapper m = new ObjectMapper();
            resp.getWriter().write(m.writeValueAsString(result));
        }
        
        //计算并封装要更新的数据
        public Map<String,Object> getData(Member member){
            Map<String,Object> map = new HashMap<>();//用于向后传递数据到dao层
            String signdate = member.getSigndate();//上一次签到日期
        
            int addGold = 1;//不连续为1 传给dao层的本次获得的积分数量
            boolean isContinue = true;//连续签到?有什么用?没用到
            Integer concount = member.getConcount();//连续签到次数
        
            //从未签到过 新账号!!!!!!!
            if (signdate==null){
                map.put("concount",1);
                isContinue = false;
                map.put("isContinue",isContinue);//第一天签到 和 断了之后的第一天签到 都即是连续又是不连续
                map.put("addGold",1);
                map.put("sum",member.getGold()+addGold);//总积分
                return map;
            }
        
            //不是新账号
            long between = DateUtil.between(new Date(), DateUtil.parseDate(signdate), DateUnit.DAY);//计算是否连续签到
            if (between==1){//连续
                map.put("concount",concount+1);//连续签到次数
                int cc = concount+1;
                switch (cc){//更新金币
                    case 2:addGold = 2;break;
                    case 3:addGold = 3;break;
                    case 4:addGold = 4;break;
                    default:addGold = 5;break;
                }
            }else {//不连续
                map.put("concount",1);
                isContinue = false;
            }
            map.put("addGold",addGold);
            map.put("isContinue",isContinue);
            map.put("sum",member.getGold()+addGold);
            return map;
        }
        
    • service

      • @Override
        public Member sign(Map<String, Object> data) {
            //1.签到
            md.sign(data);
            //2.重新查询该id的会员数据,用以更新会话域
            Member member = md.queryById(data.get("id"));
            return member;
        }
        
    • dao

      • @Override
        public void sign(Map<String, Object> data) {
            String sql = "update member set signflag = 1,gold = gold+? , concount = ?,signdate = curdate() where id = ?";
            Object[] args = {data.get("addGold"),data.get("concount"),data.get("id")};
            t.update(sql,args);
        }
        
        @Override
        public Member queryById(Object id) {
            String sql = "select * from member where id = ?";
            Object[] args = {id};
            return t.queryForObject(sql,new BeanPropertyRowMapper<>(Member.class),args);
        }
        
    • app监听器中设置定时任务

      • @WebListener
        public class AppLifeListener implements ServletContextListener {
            Timer timer = new Timer();//定时器
            @Override
            public void contextInitialized(ServletContextEvent sce) {
                ....
        
                //周期执行 将 会员签到改为0
                Calendar now = Calendar.getInstance();//目前日历
                now.set(Calendar.HOUR_OF_DAY,0);//调整时间
                now.set(Calendar.MINUTE,0);
                now.set(Calendar.SECOND,1);
                //调试时要注意天数是否为今天
                now.add(Calendar.DAY_OF_MONTH,1);//第二天 00:00:01
        
                Date firstDate = now.getTime();//第一次执行的时间:服务器启动之后的
                timer.scheduleAtFixedRate(new ClearTimerTask(),firstDate,1000L*60*60*24);
            }
        
        
            @Override
            public void contextDestroyed(ServletContextEvent sce) {
                if (timer!=null){//服务器下线时定时器取消运行
                    timer.cancel();
                }
            }
        
            class ClearTimerTask extends TimerTask{//定时器要运行的定时任务
                @Override
                public void run() {
                    MemberDao md = new MemberDaoImpl();
                    md.resetSign();
                }
            }
        
        }
        
    • 定时器操作dao

      • @Override
        public void resetSign() {
            String sql = "update member set signflag = 0";
            t.update(sql);
        }
        

hutool-maven-tomcat7插件

在这里插入图片描述
在这里插入图片描述

注意事项

  • 多表查询

  • 没有javabean(通常对应单表)对应

  • 设计一个专门的javabean对应本次查询,因为查询都是为了页面展示使用
    所以javabean可以起名为XXXXdto XXXXvo
    主要是为了JDBCTemplate 方便把查询的记录 封装为java对象

  • java查询的时候,使用Map去接收记录 一个map对应接收一个记录

  • 在页面上遍历资源

    • 注意将id更改(连接id/sta.count/sta.index)
  • 正则

    • java: str.matches(regx)

    • js: regx.test(str)

endar.getInstance();//目前日历
              now.set(Calendar.HOUR_OF_DAY,0);//调整时间
              now.set(Calendar.MINUTE,0);
              now.set(Calendar.SECOND,1);
              //调试时要注意天数是否为今天
              now.add(Calendar.DAY_OF_MONTH,1);//第二天 00:00:01
      
              Date firstDate = now.getTime();//第一次执行的时间:服务器启动之后的
              timer.scheduleAtFixedRate(new ClearTimerTask(),firstDate,1000L*60*60*24);
          }
      
      
          @Override
          public void contextDestroyed(ServletContextEvent sce) {
              if (timer!=null){//服务器下线时定时器取消运行
                  timer.cancel();
              }
          }
      
          class ClearTimerTask extends TimerTask{//定时器要运行的定时任务
              @Override
              public void run() {
                  MemberDao md = new MemberDaoImpl();
                  md.resetSign();
              }
          }
      
      }
  - 定时器操作dao

   
      @Override
      public void resetSign() {
          String sql = "update member set signflag = 0";
          t.update(sql);
      }
   

hutool-maven-tomcat7插件

在这里插入图片描述
在这里插入图片描述

注意事项

  • 多表查询

  • 没有javabean(通常对应单表)对应

  • 设计一个专门的javabean对应本次查询,因为查询都是为了页面展示使用
    所以javabean可以起名为XXXXdto XXXXvo
    主要是为了JDBCTemplate 方便把查询的记录 封装为java对象

  • java查询的时候,使用Map去接收记录 一个map对应接收一个记录

  • 在页面上遍历资源

    • 注意将id更改(连接id/sta.count/sta.index)
  • 正则

    • java: str.matches(regx)

    • js: regx.test(str)

  • 34
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值