JavaWeb开发知识总结(网上商城项目总结)

JavaWeb开发知识总结(网上商城项目小结)

1. 数据库设计

表的关系的设计如下:

数据库设计

2. 使用技术总结

网上商城前台业务流程

2.1 BaseServlet的设计

实现处理请求的方式1:

* 针对每一个请求均创建一个Servlet的实现类进行处理,弊端是:当业务较为复杂和请求较多时,会使得Servlet类过多。
// 请求方式:
// http://localhost:8080/website/UserLoginServlet -- 用户登陆的Servlet
public class UserLoginServlet extends HttpServlet {
  protected void doGet(HttpServletRequest req, HttpServletResponse resp){
  ...
  }
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)  {
     ...
    }
}
// http://localhost:8080/website/UserLogoutServlet    -- 用户退出的Servlet
public class UserLogoutServlet extends HttpServlet {
  protected void doGet(HttpServletRequest req, HttpServletResponse resp){
  ...
  }
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)  {
     ...
    }
}

实现处理请求的方式2:

* 每一个模块创建一个Servlet类,然后通过在请求的参数内传递要执行处理的方法名的方式进行整合,弊端:需要在Servlet中的get或post方法中使用if做多层判断,使得逻辑较为混乱。
// 请求方式:
// http://localhost:8080/website/UserServlet?method=login   -- 访问用户登陆
public class UserServlet extends HttpServlet {
  protected void doGet(HttpServletRequest req, HttpServletResponse resp){
      // 获取要执行的方法的名称,通过方法名称执行对应的方法
      String methodname = request.getParameter("method");
      if("login".equals(methodname)) {
          login(req,resp);
      } else if () {

      } ....
  }
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)  {
      this.doGet(req,resp);
    }
      public void login() {
      ...
      }
}

实现处理请求的方式3:较为常用

* 编写一个统一的继承HttpServlet类的Servlet类,重写service方法,在service方法中获取请求中method参数,根据该参数通过反射执行对应的方法。并且所有的请求都会经过Servlet,方便控制,不直接访问jsp页面
/**
 * 所有的Servlet的基类,该类不在web.xml中配置
 * 所有的Servlet均继承至该类
 * 该类封装方法的执行及资源的转发(封装的方式根据需要可以自定义)
 */
public class BaseServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;
  @Override
  // 重写service方法,封装所有的Servlet的请求处理方法
  protected void service(HttpServletRequest req, HttpServletResponse resp)
          throws ServletException, IOException {
      // 接收参数,获取方法的名称
      String methodName = req.getParameter("method");
      // 传递的执行的方法为空或者为空字符串时,直接返回
      if(methodName == null || "".equals(methodName)) {
          resp.setContentType("text/html;charset=UTF-8");
          resp.getWriter().println("执行的method方法为null");
          return;
      }
      // 获取当前对象的class对象,通过反射的方式执行对于该请求的对应的方法
      Class<? extends BaseServlet> clas = this.getClass();
      try {
          // 获取传递过来的方法名的方法
          Method method = clas.getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
          // 执行参数中的方法,返回值表示要转发的资源路径
          String path = (String) method.invoke(this, req, resp);
          if(path != null) {
              // 转发资源
              req.getRequestDispatcher(path).forward(req, resp);
          }
      } catch (Exception e) {
          e.printStackTrace();
      }
  }
}
// 用户的Servlet继承至BaseServlet
// 访问http://localhost:8080/website/UserServlet?method=userRegistUI   -- 访问用户注册页面
public class UserServlet extends BaseServlet {
  private static final long serialVersionUID = 1L;
      // 跳转用户注册的页面
  public String userRegistUI(HttpServletRequest request, HttpServletResponse response){
      return "/jsp/regist.jsp";
    }
}

2.2 IOC方式实现接口和实现类的解耦合

Java编程提倡面向接口编程,并且要遵循OCP(open-close-principle)原则(对程序扩展开放,对程序源码修改关闭)。


# 1. 面向接口编程:

  在设计中,提供dao层和service的接口,并提供实现类,在需要使用dao层处理数据时,直接创建dao层实现类的对象进行处理,这种方式使得接口和实现类出现了高度的耦合,当dao层实现类变化时,需要修改源码创建实现类对象,违背了OCP原则.

# 2.使用工厂设计模式:

  工厂模式是在工厂类中提供创建dao层和service接口的实现类对象的方法,在需要使用dao层或service接口的实现类时,工厂类提供创建接口实现类的对象,通过工厂类进行获取,但当接口的实现类发生变化时,需要在工厂类中进行改变,这种方式实际上是将接口和实现类的耦合转变为了接口和工厂类的耦合,并且也需要更改源代码。

# 3.IOC模式实现程序间接耦合:IOC-控制反转 --> 工厂模式+反射+配置文件

  在工厂方法中通过读取配置文件中配置接口的实现类的完全限定名,通过反射创建接口的实现类对象,当需求改变时,只需改变配置文件中接口对应的实现类即可。
  XML配置文件:配置接口和实现类的对应关系;
  dom4j:实现读取配置文件(使用XPath技术辅助读取)
  反射:使用反射获取读取接口对应的实现类的对象
/**
 * dao层和业务层接口的工厂类
 */
public class BeanFactory {
  // 获取业务层和dao层的接口的实现类
  public static Object getBean(String beanid) {
      try {
          // 读取配置文件,从xml配置文件中获取接口的实现类
          // 创建读取xml文件的对象
          SAXReader reader = new SAXReader();
          // 获取文件的Document对象
          Document document = reader.read(BeanFactory.class.getClassLoader().getResourceAsStream("applicationContext.xml"));
          // 获取指定id名称的子元素,通过XPath获取元素
          Element element = (Element) document.selectSingleNode("//bean[@id='"+beanid+"']");
          // 获取子元素中class属性的值
          String classname = element.attributeValue("class");
          // 通过反射获取class对象
          Class<?> class1 = Class.forName(classname);
          // 创建实例
          return class1.newInstance();
      } catch (Exception e) {
          e.printStackTrace();
      }
      return null;
  }
}
<!--applicationContext.xml   接口和实现类的配置文件,放置在项目的src目录下-->
<?xml version="1.0" encoding="UTF-8"?>
<beans>
  <!-- dao层的实现类配置 -->
  <!-- UserDao的配置实现类 -->
  <bean id="UserDao" class="com.itheima.store.dao.impl.UserDaoImpl"/>
  <!-- CategoryDao的配置实现类 -->
  <bean id="CategoryDao" class="com.itheima.store.dao.impl.CategoryDaoImpl"/>
  <!-- ProductDao的配置实现类 -->
  <bean id="ProductDao" class="com.itheima.store.dao.impl.ProductDaoImpl"/>
  <!-- OrderDao的配置实现类 -->
  <bean id="OrderDao" class="com.itheima.store.dao.impl.OrderDaoImpl"/>

  <!-- 业务层层的实现类配置 -->
  <!-- UserService的配置实现类 -->
  <bean id="UserService" class="com.itheima.store.service.impl.UserServiceImpl"/>
  <!-- CategoryService的配置实现类 -->
  <bean id="CategoryService" class="com.itheima.store.service.impl.CategoryServiceImpl"/>
  <!-- ProductService的配置实现类 -->
  <bean id="ProductService" class="com.itheima.store.service.impl.ProductServiceImpl"/>
  <!-- OrderService的配置实现类 -->
  <bean id="OrderService" class="com.itheima.store.service.impl.OrderServiceImpl"/>
</beans>

2.3 分页数据工具类的封装

分页数据的显示:分页显示有5个必要元素:当前页,每页显示条数,总记录数,总页数,要显示的数据.

将这5个必须元素封装到javabean类中,并且其中的总记录数和显示的数据需要通过查询获取,其他的可以直接计算,则需要定义两个方法获取总记录数和显示的数据的集合。

// 分页的javabean类封装,封装时数据集合采用泛型形式,可以进行重用
public class PageBean<T> {
  private Integer currPage; // 当前页
  private Integer totalPage; // 总页数
  private Integer totalCount; // 记录的总条数
  private Integer pageSize; // 每页显示的条数
  private List<T> list; // 查询的数据的集合
  ... get和set方法
}
/**
 * 生成分页数据的工具类
 */
public class PageUtils {
  // 定义每页显示的数据条数,默认值
  public static Integer pageSize = 12;
  /**
   * 分页数据封装
   * @param: classname 要执行Dao层实现类
   * @param: countmethod 计算总条数的方法名
   * @param: pagemethod 计算总页数的方法名
   * @param: param 查询数据的条件
   * @param: currPage 当前页
   * @return: PageBean<T>   返回PageBean对象
   */
  public static <T> PageBean<T> getPagebean(Object obj, String countmethod,String pagemethod, String param, Integer currPage, Integer spageSize) {
      // 每页显示记录默认是12条,当传入的参数为不为null是就显示定制显示的条数
      if(spageSize != null) {
          pageSize = spageSize;
      }
      PageBean<T> pageBean = null;
      try {
              // 获取class对象
          Class<?> c = obj.getClass();
              // 定义分页封装对象
          pageBean = new PageBean<T>();
          // 1.设置当前页
          pageBean.setCurrPage(currPage);
          // 2.设置查询的数据的总条数
          // 调用dao层查询数据的总条数
          Method method_count = c.getMethod(countmethod, String.class);
          Integer totalCount = (Integer) method_count.invoke(obj, param);
          pageBean.setTotalCount(totalCount);
          // 3.设置每页显示的条数
          pageBean.setPageSize(pageSize);
          // 4.设置总页数
          Double ceil = Math.ceil((totalCount*1.0 / pageSize));
          int totalPage = ceil.intValue();
          pageBean.setTotalPage(totalPage);
          // 5.查询数据
          // 调用业务层查询数据
          Method method_page = c.getMethod(pagemethod, String.class, Integer.class, Integer.class);
          int begin = (currPage - 1) * pageSize;
          List<T> list  = (List<T>) method_page.invoke(obj, param, begin, pageSize);
              //  将数据封装到PageBean对象中
          pageBean.setList(list);
      } catch (Exception e) {
          e.printStackTrace();
      }
      // 将每页显示的记录条数据设为初始值
      pageSize = 12;
      return pageBean;
  }
}

2.4 全局字符集编码过滤器

为解决通过request获取参数时中文乱码问题,可以设置统一字符集过滤器:

/**
 * 通过动态代理的方式,创建request对象的动态代理对象
 */
public class CharacterEncodingFilter implements Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
  }
  @Override
  public void doFilter(ServletRequest request, ServletResponse response,
          FilterChain chain) throws IOException, ServletException {
          // 将ServletRequest的对象转换为HttpServletRequest对象
      final HttpServletRequest hsrequest = (HttpServletRequest) request;
          // 通过动态代理的方式,在原始的request的获取参数之前设置编码格式,防止中文乱码
      HttpServletRequest myrequest = (HttpServletRequest) Proxy.newProxyInstance(hsrequest.getClass().getClassLoader(), hsrequest.getClass().getInterfaces(), new InvocationHandler() {
          @Override
          public Object invoke(Object proxy, Method method, Object[] args)
                  throws Throwable {
              // 获取请求的方式
              String reqmethod = hsrequest.getMethod();
              // get方式只增强getParameter方法
              // 获取方法的名称
              String methodName = method.getName();
              if("get".equalsIgnoreCase(reqmethod)) {
                  // 当方法名为getParameter改变编码
                  if("getParameter".equals(methodName)) {
                      // 获取参数的值,并进行转换编码
                      String value = (String) method.invoke(hsrequest, args);
                      return new String(value.getBytes("ISO-8859-1"), "UTF-8");
                  }
              // post方式只增强getParameter和getParameterMap方法
              } else if("post".equalsIgnoreCase(reqmethod)) {
                  if("getParameter".equals(methodName) || "getParameterMap".equals(methodName)) {
                      // 设置request域中的编码
                      hsrequest.setCharacterEncoding("UTF-8");
                  }
              }
              return method.invoke(hsrequest, args);
          }
      });
      chain.doFilter(myrequest, response);
  }
  @Override
  public void destroy() {
  }
}
// 在项目的web.xml文件中配置过滤器
<!-- 统一字符集编码过滤器 -->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>com.store.web.filter.CharacterEncodingFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern> // 过滤所有的请求
</filter-mapping>

2.5 用户模块:

2.5.1 用户注册

异步校验注册的用户名是否可用:

通过Ajax的技术,在用户输入注册的用户名时进行向后台校验输入的用户名是否可用。

/**
 * 用户模块的Servlet
 */
public class UserServlet extends BaseServlet {
  private static final long serialVersionUID = 1L;
  /**
   * 用户名的异步校验是否可用
   */
  public String checkUsername(HttpServletRequest request, HttpServletResponse response){
      try {
          // 接收参数
          String username = request.getParameter("username");
          // 调用业务层进行查询
          // UserService userService = new UserServiceImpl();
          UserService userService = (UserService) BeanFactory.getBean("UserService");
          // 查询用户
          User existUser = userService.findByUsername(username);
          response.setContentType("text/html;charset=UTF-8");
          if(existUser == null) {
              // 用户名可以使用,返回ok
              // json表达式需要加双引号,否则在jsp页面中无法识别为json数据
              response.getWriter().println("{\"msg\":\"ok\"}");
          } else {
              // 用户名已被占用,返回no
              response.getWriter().println("{\"msg\":\"no\"}");
          }
      } catch (Exception e) {
          e.printStackTrace();
      }
      return null; // Ajax的请求不进行转发
  }
$.post(path+"/UserServlet",{method:"checkUsername",username:$("#username").val()},
       function(data){
      var state = data.msg;
      // msg为ok代表用户名可用
      if(state == "ok") {
          // 提示信息
          layer.tips('用户名可用', '#username', {
                tips: [2, '#038E09'] //还可配置颜色 绿色:#00FF00  红色:FF0000
          });
          $("#subbtn").attr("disabled", false);
      // msg为no代表用户名不可用
      } else {
          layer.tips('用户名已被占用', '#username', {
                tips: [2, '#FF0000'] //还可配置颜色 绿色:#00FF00  红色:FF0000
          });
          $("#subbtn").attr("disabled", true);
      }
  },"json");

用户注册:使用BeanUtils封装数据时,封装时间类型的数据,需要自定义转换类,然后在Beanutils封装数据时进行注册。

// 注册的方法
public String registUser(HttpServletRequest request, HttpServletResponse response){
  try {

      // 获取令牌口口令的字符串,防止表单的重复提交
      // 获取session域中的口令
      String stoken = (String) request.getSession().getAttribute("token");
      // 当session中的口令数据为null,代表表单是重复提交,直接返回到msg.jsp页面进行提示
      if(stoken == null) {
          request.setAttribute("msg", "亲,您注册请求已提交,请您不要重复提交!");
          return "/jsp/msg.jsp";
      }
      // 当session中的口令数据不为空时,表示该表单是第一次提交
      // 获取表单中隐藏字段的口令数据
      String ftoken = request.getParameter("token");
      // 当session域中的口令数据和表单中的口令数据不一致时,表示表单中的口令被篡改,直接返回提示
      if(!stoken.equals(ftoken)) {
          request.setAttribute("msg", "亲,您的注册数据被篡改,请您重新注册!");
          return "/jsp/msg.jsp";
      }

      // 当session中的口令数据和表单中的口令数据一致时进行用户的注册
      // 接收参数,封装数据
      Map<String, String[]> parameterMap = request.getParameterMap();
      User user = new User();

      // 定义字符串转换为date类型
      ConvertUtils.register(new DateConver(), Date.class);
      // 调用beanutils进行封装数据
      BeanUtils.populate(user, parameterMap);
      // 调用业务层进行注册用户
      // UserService userService = new UserServiceImpl();
      UserService userService = (UserService) BeanFactory.getBean("UserService");
      int status = userService.registUser(user);
      // 当status不为0时表示注册成功
      if(status != 0) {
          request.setAttribute("msg", "欢迎您的注册,请前往您的 "+user.getEmail()+" 邮箱进行激活!");
      } else {
          request.setAttribute("msg", "对不起,注册失败,请重新注册!");
      }

      // 用户注册完毕后需要将session域中的令牌口令数据清除
      request.getSession().removeAttribute("token");

  } catch (Exception e) {
      e.printStackTrace();
  }
  return "/jsp/msg.jsp";
}
/**
 * 日期和字符串转换的工具类
 */
public class DateConver implements Converter {
  @Override
  /**
   * 将value 转换 c 对应类型
   * 存在Class参数目的编写通用转换器,如果转换目标类型是确定的,可以不使用c 参数
   */
  public Object convert(Class c, Object value) {
      // 将String转换为Date --- 需要使用日期格式化
      DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
      try {
          Date date = dateFormat.parse((String)value);
          return date;
      } catch (ParseException e) {
          e.printStackTrace();
      }
      return null;
  }
}
2.5.2 邮箱链接激活用户

邮箱相关内容:

邮件发送的相关的概念:
* 邮箱服务器   :如果一台电脑安装了邮箱服务器的软件,这台电脑称为是邮箱服务器.
* 电子邮箱:其实就是邮箱服务器上的一块空间,通过电子邮箱账号访问这块空间的数据.
* 收发邮件的协议:
    * 发邮件:SMTP协议:SMTPSimple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 25默认端口号
    * 收邮件:POP3协议:POP3,全名为“Post Office Protocol - Version 3”,即"邮局协议版本3"。是TCP/IP协议族中的一员。默认端口是110
    * 收邮件:IMAP协议:IMAPInternet Mail Access ProtocolInternet邮件访问协议)以前称作交互邮件访问协议(Interactive Mail Access Protocol)。IMAP是斯坦福大学在1986年开发的一种邮件获取协议。
* 收发邮件的过程:收邮件都要经过有邮件的POP3服务器,发送邮件都要经过SMTP服务器,两个邮箱间不是点对点通信.

java中发送邮件:需要jar包:mail.jar

/**
 * 邮箱工具类
 */
public class MailUtils {
  public static void sendMail(String to, String code) {
      try {
          // 获得连接:
          Properties props = new Properties();
          Session session = Session.getInstance(props, new Authenticator() {
              @Override
              protected PasswordAuthentication getPasswordAuthentication() {
                  return new PasswordAuthentication("somnus@store.com",
                          "somnus");
              }
          });
          // 构建邮件:
          Message message = new MimeMessage(session);
          message.setFrom(new InternetAddress("somnus@store.com"));
          // 设置收件人:
          // TO:收件人 CC:抄送 BCC:暗送,密送.
          message.addRecipient(RecipientType.TO, new InternetAddress(to));
          // 主题:
          message.setSubject("来自商城的激活邮件!");
          // 正文:
          message.setContent(
                  "<h1>来自购物天堂商城的激活邮件:请点击下面链接激活!</h1><h3><a href='http://localhost:8080/store_v2.0/UserServlet?method=active&code="
                          + code
                          + "'>http://localhost:8080/store_v2.0/UserServlet?method=active&code="
                          + code + "</a></h3>", "text/html;charset=UTF-8");
          // 发送邮件:
          Transport.send(message);
      } catch (MessagingException e) {
          e.printStackTrace();
      }
  }
}
2.5.3 用户登录模块
登陆的异步校验及自动登陆和记住用户名

登陆信息的异步校验:点击登陆时进行Ajax请求校验用户名和密码及验证码是否正确,返回校验的标识。

自动登陆:用户登陆成功后将用户信息保存在session中,并将用户名和密码保存到客户端的Cookie中;再次访问时,通过配置的过滤器拦截,先查看session中含有用户的信息(浏览器未关闭,重新请求页面),当session中含有用户信息则直接放行;当session中没有用户信息时,检查用户请求带过来的Cookie中是否有保存的用户信息,如果没有用户信息则直接放行;如果有用户的信息则查询数据库校验Cookie中的用户名和密码是否正确(防止恶意更改数据),如果校验成功,则将用户的信息保存在session中并放行,如果校验失败(用户信息被篡改)直接放行;用户点击安全退出时,需要将Cookie和session中的保存的用户的信息进行清除。

记住用户名:用户登录成功后,将用户名写到客户端的Cookie中并设置保存时间,用户访问登陆页面时,在跳转之前,获取用户带来的Cookie中的用户名信息,如果存在,则将用户名取出保存在request域中,在登陆的jsp页面取出作为用户名输入框的value值即可;用户取消记住用户名时将用户端的Cookie中的用户名清除即可。

public class UserServlet extends BaseServlet {
 ...
/**
 * 用户登陆页面
 */
public String userLoginUI(HttpServletRequest request, HttpServletResponse response){
  // 获取客户端请求中的Cookie  实现记住用户名功能
  Cookie[] cookies = request.getCookies();
  // 查找是否记住用户名的Cookie
  Cookie findCookie = CookieUtils.findCookie(cookies, "remember");
  // 当Cookie有记住用户名时,将用户名保存在request域中
  if(findCookie != null) {
      request.setAttribute("remember", findCookie.getValue());
  }
  return "/jsp/login.jsp";
}

/**
 * 用户登陆的校验
 */
public String checkLogin(HttpServletRequest request, HttpServletResponse response){
  // 获取表单中的数据
  String code = request.getParameter("code");
  String username = request.getParameter("username");
  String password = request.getParameter("password");
  // 校验表单中的数据
  response.setContentType("text/html;charset=UTF-8");
  try {
      if(code == null || "".equals(code) || username == null || "".equals(username) || password == null || "".equals(password)) {
          response.getWriter().println("{\"msg\":\"null\"}");
          return null;
      }
  } catch (IOException e) {
      e.printStackTrace();
  }

  // 获取session中的验证码
  String incode = (String) request.getSession().getAttribute("iconCode");
  // 清除session中本次的验证码
  request.getSession().removeAttribute("iconCode");

  // 校验验证码是否正确
  if(!incode.equalsIgnoreCase(code)) {
      try {
          response.getWriter().println("{\"msg\":\"no\"}");
      } catch (IOException e) {
          e.printStackTrace();
      }
      return null;
  }

  // 封装数据
  User user = new User();
  user.setUsername(username);
  user.setPassword(password);
  // 调用业务层进行查询用户是否存在
  // UserService userService = new UserServiceImpl();
  UserService userService = (UserService) BeanFactory.getBean("UserService");
  try {
      User existUser  = userService.checkUser(user);
      // 判断用户是否存在
      if(existUser == null) {
          response.getWriter().println("{\"msg\":\"no\"}");
      } else {
          // 用户登陆成功
          // 将用户信息保存在session中

          // 判断是否勾选自动登陆,如果自动登陆勾选,则将用户的登陆信息保存在cookie中
          String autoLogin = request.getParameter("autoLogin");
          if("true".equals(autoLogin)) {
              // 将用户名和密码以  username敏password形式存储在Cookie中
              String username_password = existUser.getUsername()+"敏"+existUser.getPassword();
              // 将用户名和密码进行加密,将加密后的字符串保存在Cookie中
              String encrypt = DesUtils.encrypt(username_password);
              Cookie cookie  = new Cookie("autoLogin",encrypt);
              // 设置Cookie的有效路径
              cookie.setPath(request.getContextPath()); // 有效路径是当前项目的路径
              // 设置Cookie的有效时间,7天
              cookie.setMaxAge(60 * 60 * 24 * 7);
              // 将Cookie写到客户端
              response.addCookie(cookie);
          }
          // 判断是否勾选记住用户名,勾选时,则将用户名保存在Cookie中
          String remember = request.getParameter("remember");
          if("true".equals(remember)) {
              Cookie rcookie = new Cookie("remember", existUser.getUsername());
              // 设置Cookie的有效路径
              rcookie.setPath(request.getContextPath()); // 有效路径是当前项目的路径
              // 设置Cookie的有效时间,7天
              rcookie.setMaxAge(60 * 60 * 24 * 7);
              // 将Cookie写到客户端
              response.addCookie(rcookie);
          } else {
              Cookie[] cookies = request.getCookies();
              Cookie findCookie = CookieUtils.findCookie(cookies, "remember");
              if(findCookie != null) {
                  // 设置Cookie的有效路径
                  findCookie.setPath(request.getContextPath()); // 有效路径是当前项目的路径
                  // 设置Cookie的时间为0
                  findCookie.setMaxAge(0);
                  // 将Cookie写到客户端
                  response.addCookie(findCookie);
              }
          }

          // 将用登陆的信息保存在session中
          request.getSession().setAttribute("existUser", existUser);
          response.getWriter().println("{\"msg\":\"ok\"}");
      }
      return null;
  } catch (Exception e) {
      e.printStackTrace();
  }
  return null;
}

/**
 * 用户安全退出的方法
 * @Title: userLogOut
 * @Description: TODO(安全退出)
 * @param: @param request
 * @param: @param response
 * @param: @return   
 * @return: String   
 */
public String userLogOut(HttpServletRequest request, HttpServletResponse response){
  // 1. 清除session中的信息
  request.getSession().removeAttribute("existUser");
  // 2. 清除Cookie中保存的用户的信息
  Cookie[] cookies = request.getCookies();
  Cookie findCookie = CookieUtils.findCookie(cookies, "autoLogin");
  if(findCookie != null) {
      // 设置Cookie的有效路径
      findCookie.setPath(request.getContextPath()); // 有效路径是当前项目的路径
      // 设置Cookie的时间为0
      findCookie.setMaxAge(0);
      // 将Cookie写到客户端
      response.addCookie(findCookie);
  }
  // 跳转到主页面
  return "/index.jsp";
}
...
/**
 * 用户自动登录的过滤器
 */
public class AutoLoginFilter implements Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {}
  @Override
  public void doFilter(ServletRequest request, ServletResponse response,
          FilterChain chain) throws IOException, ServletException {
      HttpServletRequest hsrequest = (HttpServletRequest) request;
      // 1.校验session中是否含有用户信息
      User existUser = (User) hsrequest.getSession().getAttribute("existUser");
      // 1.1 session中含有用户的登陆信息,直接放行
      if(existUser != null) {
          chain.doFilter(hsrequest, response);
          return;
      }
      // 1.2 session中不含有用户的登陆信息,则查找Cookie中是否有用户的登陆信息
      Cookie[] cookies = hsrequest.getCookies();
      Cookie findCookie = CookieUtils.findCookie(cookies,"autoLogin");
      // 2. Cookie中不含有用户的信息时,直接放行
      if(findCookie == null) {
          chain.doFilter(hsrequest, response);
          return;
      }
      // 3. Cookie中含有用户的信息,则取出Cookie中的用户名和密码,到数据库中进行校验
      String cookiValue = findCookie.getValue();
      // 进行解密
      try {
          String username_password = DesUtils.decrypt(cookiValue);
          User user = new User();
          user.setUsername(username_password.split("敏")[0]);
          user.setPassword(username_password.split("敏")[1]);

          // 调用业务层进行查询
          // UserService userService = new UserServiceImpl();
          UserService userService = (UserService) BeanFactory.getBean("UserService");
          User checkUser = userService.checkUser(user);
          // 3.1 如果用户信息校验不通过,直接放行
          if(checkUser == null) {
              chain.doFilter(hsrequest, response);
              return;
          }
          // 3.2 如果用户信息校验成功,则将用户的信息保存在session域中,然后放行
          hsrequest.getSession().setAttribute("existUser", checkUser);
          chain.doFilter(hsrequest, response);
      } catch (Exception e) {
          e.printStackTrace();
      }
  }
  @Override
  public void destroy() {
  }
}
<!-- 自动登录过滤器  web.xml文件配置 -->
<filter>
  <filter-name>AutoLoginFilter</filter-name>
  <filter-class>com.store.web.filter.AutoLoginFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>AutoLoginFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

2.6分类模块

使用缓存技术实现分类数据的加载:首页加载需要显示分类数据,在首页加载时使用Ajax异步请求分类数据,由于分类数据是很少变动,可以将分类数据保存在缓存中,再次请求分类数据时可以不用从数据库查询,以提高效率。


# 常用的缓存技术:常用的有EHCache,Memcached,Redis等缓存技术,本次使用EHCache技术

 * EHCache:Hibernate框架二级缓存使用,使用时如果配置了overflowToDisk="true" 则需要将需要序列化的javabean实现序列化接口

EHCache的配置文件:

<!--ehcache.xml配置文件,放置在项目的src目录下-->
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
  <!-- 缓存超出限制的数量时序列化到硬盘的路径 -->
  <diskStore path="E:/develop/apache-tomcat-7.0.69/webapps/store_v2.0/ehcache" />
  <!-- cache可以配置多个,通过配置的name属性进行区分 -->
  <!-- 
      name:代表缓冲的名称
      maxElementsInMemory:内存中能够缓冲数据的条数
      timeToLiveSeconds:缓冲的存活时间
      overflowToDisk:当超出内存中缓存数量时是否写到本地硬盘
   -->
      <!--categoryCache:分类数据的缓存名称-->
  <cache 
      name="categoryCache" 
      maxElementsInMemory="10000"
      eternal="false"
      timeToIdleSeconds="120" 
      timeToLiveSeconds="120" 
      overflowToDisk="true"
      maxElementsOnDisk="10000000" 
      diskPersistent="false"
      diskExpiryThreadIntervalSeconds="120" 
      memoryStoreEvictionPolicy="LRU" />
</ehcache>

缓存的使用:在分类模块的业务层进行使用

// 分类模块的Servlet
public class CategoryServlet extends BaseServlet {
  private static final long serialVersionUID = 1L;
  /**
   * 查询所有的分类数据
   */
  public String findAllCategory(HttpServletRequest request, HttpServletResponse response) {
      // 调用业务层进行查询所有的分类数据
      // CategoryService categoryService = new CategoryServiceImpl();
      CategoryService categoryService = (CategoryService) BeanFactory.getBean("CategoryService");
      try {
          List<Category> list = categoryService.findAllCategory();
          // 将数据转换为json格式,传送给主页面
          JSONArray jsonArray = JSONArray.fromObject(list);
          // 将数据传递给主页面
          response.setContentType("text/html;charset=UTF-8");
          response.getWriter().println(jsonArray.toString());
      } catch (Exception e) {
          e.printStackTrace();
      }
      return null;
  }
}

/**
 * 分类的业务层的实现类CategoryServiceImpl
 */
public class CategoryServiceImpl implements CategoryService {
  // 定义当缓存中有分类数据时,是否重新查询数据库分类数据,当为true时不查询数据库数据,
  // 当为false时,表明后台对分类数据进行修改,需要重新查询分类数据
  private static Boolean flag = true;
  @Override
  // 查询所有的分类数据
  public List<Category> findAllCategory() throws SQLException {
      // 使用缓存技术优化EHCache
      // 1.请求查询所有的分类数据时,先查询缓冲中是否存在数据
      // 读取配置文件,创建缓存CacheManager对象
      CacheManager cacheManager = CacheManager.create(this.getClass().getClassLoader().getResourceAsStream("ehcache.xml"));
      // 获取指定名称的缓存
      Cache cache = cacheManager.getCache("categoryCache");
      // 获取缓存中存储的指定名称的缓存数据
      Element element = cache.get("list");
      List<Category> list = null;
      if(element == null || !flag){
          // 3.缓冲中没有数据时,到数据库中查询数据,并将数据保存在缓冲中
          // 当指定缓存为null时,查询数据库,并将查询的记过保存到缓存中
          // 调用dao层查询数据
          // CategoryDao categoryDao = new CategoryDaoImpl();
          CategoryDao categoryDao = (CategoryDao) BeanFactory.getBean("CategoryDao");
          list = categoryDao.findAll();
              // 将查询的数据保存在缓冲中
          cache.put(new Element("list", list));
      } else {
          // 2.缓冲中存在数据则直接返回到前台页面
          // 当缓存中存在数据,则直接返回该数据
          list = (List<Category>) element.getObjectValue();
      }
      // 修改标志位
      flag = true;
      return list;
  }
  ...
}

2.7 商品模块

商品浏览记录和分类显示商品

商品浏览记录:用户浏览商品时,需要通过Servlet查询查询商品信息,则记录浏览商品的id,将id以一定格式写到客户端的Cookie中,用户访问商品分类时,会带着浏览的记录的商品id,在返回页面之前将Cookie中的浏览记录的商品的id查询出商品的具体数据,并将数据保存在request域中,在jsp页面中循环遍历即可。

分类显示商品:根据用户请求访问的商品分类,查询所有的属于该分类下所有的商品的记录并进行分页,将数据保存到request域中,同时将浏览记录中的商品查询并保存在request域中,到jsp页面进行显示。

/**
 * 商品模块的Servlet
 */
public class ProductServlet extends BaseServlet {
  private static final long serialVersionUID = 1L;
  // 根据商品的分类id查询商品数据(分页显示)
  public String findPageByCid(HttpServletRequest request, HttpServletResponse response) {
      try {
          // 接收参数
          // 获取分类的id
          String cid = request.getParameter("cid");
          // 获取当前页
          String currPage = request.getParameter("currPage");

          // 调用业务层查询商品数据
          // ProductService productService = new ProductServiceImpl();
          ProductService productService = (ProductService) BeanFactory.getBean("ProductService");
          PageBean<Product> pageBean = productService.findPageByCid(cid, Integer.parseInt(currPage));
          // 将查询的数据保存在request域中
          request.setAttribute("pageBean", pageBean);

          // 读取Cookie中的浏览历史数据
          Cookie[] cookies = request.getCookies();
          Cookie findCookie = CookieUtils.findCookie(cookies, "history");
          if(findCookie != null) {
              String pid = findCookie.getValue();
              String[] ids = pid.split("-");
              List<Product> list = new LinkedList<Product>();
              for (String id : ids) {
                  Product product = productService.findByPid(id);
                  list.add(product);
              }
              request.setAttribute("history", list);
          }
      } catch (Exception e) {
          e.printStackTrace();
      }
      return "/jsp/product_list.jsp";
  }

  // 根据商品id查询商品信息
  public String findByPid(HttpServletRequest request, HttpServletResponse response) {
      // 获取商品id
      String pid = request.getParameter("pid");
      // 将浏览的商品的id保存到Cookie中
      Cookie[] cookies = request.getCookies();
      Cookie findCookie = CookieUtils.findCookie(cookies, "history");
      String history = null;
      if(findCookie == null) {
          // 没有查询到Cookie时,表示Cookie中没有商品浏览记录
          history = pid;
      } else {
          // 查到Cookie表示浏览过商品,将当前浏览的商品添加到Cookie中
          String ids = findCookie.getValue();
          String[] split = ids.split("-");
          LinkedList<String> list = new LinkedList<String>(Arrays.asList(split));
          if(list.contains(pid)) {
              list.remove(pid);
              list.addFirst(pid);
          } else {
              if(list.size() >= 6) {
                  list.removeLast();
                  list.addFirst(pid);
              } else {
                  list.addFirst(pid);
              }
          }
          StringBuilder sb = new StringBuilder();
          for (String id : list) {
              sb.append(id).append("-");
          }
          history = sb.toString().substring(0, sb.length()-1);
      }
      // 将浏览记录写到客户端Cookie中,保存时间是7天
      findCookie = new Cookie("history", history);
      findCookie.setPath(request.getContextPath());
      findCookie.setMaxAge(60 * 60 * 24 * 7);
      response.addCookie(findCookie);
      try {
          // 调用业务层查询数据
          // ProductService productService = new ProductServiceImpl();
          ProductService productService = (ProductService) BeanFactory.getBean("ProductService");
          Product product = productService.findByPid(pid);
          // 将数据保存在request域中
          request.setAttribute("product", product);
      } catch (Exception e) {
          e.printStackTrace();
      }
      return "/jsp/product_info.jsp";
  }
}

2.8 订单模块

BeanUtils工具类能封Map集合的数据到javabean对象中,则查询数据库时,多表查询的结果存放在Map集合中,也可以使用BeanUtils工具类进行封装到javabean对象中。

// 订单的实体类
public class Order {
  private String oid; // 订单id
  private Date ordertime; // 订单时间
  private Double total; // 订单总金额
  private Integer state; // 订单状态,1:未付款 2:已付款但为发货 3:已发货 4:确认收货
  private String address; // 收货地址
  private String name; // 收货人
  private String telephone; // 收货人联系方式
  private User user; // 订单所属用户
  /**
   * 保存该订单中的所有的订单项,方便在查询用户订单时使用
   */
  private List<OrderItem> orderItems = new LinkedList<OrderItem>();
  ...get/set方法
}
// 订单项实体类
public class OrderItem {
  private String itemid; // 订单项id
  private Integer count; // 商品个数
  private Double subtotal; // 金额小计
  private Product product; // 订单项中的商品
  private Order order; // 订单项所属的订单
 ...get/set方法
}
@Override
/**
 * 根据用户的id查询该用户的所有订单
 */
public List<Order> findByUid(String uid, Integer begin, Integer pageSize) throws SQLException, IllegalAccessException, InvocationTargetException {
  QueryRunner queryRunner = new QueryRunner(JDBCUtils.getDataSource());
  // 先查询该用户所有的订单
  String sql = "select * from orders where uid=? order by ordertime desc limit ?,?";
  List<Order> order_list = queryRunner.query(sql, new BeanListHandler<Order>(Order.class), uid, begin, pageSize);
  // 查询订单对应的订单项
  for (Order order : order_list) {
      // 查询该订单下的订单项对应所有商品信息
      sql = "select * from orderitem o,product p where o.pid=p.pid and oid=?";
      List<Map<String, Object>> map_list = queryRunner.query(sql, new MapListHandler() , order.getOid());
      for (Map<String, Object> map : map_list) {
          // 封装订单项数据
          OrderItem item = new OrderItem();
          BeanUtils.populate(item, map);
          item.setOrder(order);
          // 封装订单项中商品数据
          Product product = new Product();
          BeanUtils.populate(product, map);
          item.setProduct(product);
          // 将订单项添加到订单中
          order.getOrderItems().add(item);
      }
  }
  return order_list;
}

@Override
/**
 * 根据订单id查询订单信息
 */
public Order findByOid(String oid) throws SQLException, IllegalAccessException, InvocationTargetException {
  QueryRunner queryRunner = new QueryRunner(JDBCUtils.getDataSource());
  // 1.查询订单信息
  String sql = "select * from orders where oid = ?";
  Order order = queryRunner.query(sql, new BeanHandler<Order>(Order.class), oid);
  // 2.根据订单id查询订单项
  sql = "select * from orderitem o,product p where o.pid=p.pid and o.oid=?";
  List<Map<String, Object>> map = queryRunner.query(sql, new MapListHandler(), oid);
  for (Map<String, Object> map2 : map) {
      // 3.封装商品数据
      Product product = new Product();
      BeanUtils.populate(product, map2);
      // 4.封装订单项数据
      OrderItem orderItem = new OrderItem();
      BeanUtils.populate(orderItem, map2);
      orderItem.setProduct(product);
      orderItem.setOrder(order);
      // 5.将订单项添加到订单中
      order.getOrderItems().add(orderItem);
  }
  return order;
}

在线支付的流程:通常有:直接使用银行的接口(需要自己和银行进行对接);使用第三方的提供的支付接口。


# 方式1:直接和银行网银进行对接

  缺点:开发人员需要了解各个银行的网银接口,当银行的网银系统进行升级时,需要修改源代码。
  优点:银行和自身账号直接对接,资金流通快。

# 方式2:使用第三方提供的网银接口

  优点:开发人员只需和第三方支付公司进行对接,不需要了解各个银行的网银接口,网银升级时,不需要进行代码修改,第三方公司负责和银行对接。
  缺点:资金需要通过第三方公司进行中转,可能会对资金链造成影响。


# 使用第三方支付公司接口的流程:

  1.网站的支付链接会先经过第三方公司的网站;
  2.第三方公司的网站根据用户选用的支付通道(选择的银行)跳转到指定的银行的网银页面;
  3.银行网银处理完毕后会将结果转给第三方公司;
  4.第三方公司会将支付的结果转到自身的网站。

2.9 后台管理

文件上传:使用fileupload 工具类实现商品图片的上传

文件上传工具类:fileUpload工具类 ,两个jar包commons-fileupload-1.2.1.jar'和commons-io-1.4.jar`


# 文件上传三要素:

1. form表单的提交方式必须是POST方式
2. form表单中必须有字段为file类型的字段  <input type="file" name="upload" />
3. 表单的enctype属性值必须是:enctype="multipart/form-data"

# fileUpload上传文件的步骤:

1. 创建磁盘项工厂类,用于对上传文件进行配置
2. 通过工厂类获得Servlet的上传文件的核心解析类
3. 通过核心类解析request对象,获取所有字段的集合,集合中的内容是分割线分成的每个部分
4. 遍历集合中每个部分
  * 如果是普通项:直接获取属性名称和属性值
  * 如果是文件项:通过输入输出流读取文件

商品添加的Servlet实现:文件上传将表单的enctype修改,BaseServlet中通过request的getParameter方法无法获取要执行的方法的名称,则需要使用单独的Servlet完成文件的上传

/**
 * 后台管理添加商品的Servlet
 * 含有文件表单的处理不能使用request的获取参数的方法,
 * 继承baseservlet的方式是在baseservlet中使用request的获取参数的方法进行方法名的获取
 * 则使用继承至baseservlet的方式无法正确解析文件上传
 */
public class AdminAddProductServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      this.doPost(req, resp);
  }
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      try {
          // 接收表单数据
          // 使用fileupload的方式进行上传文件的获取
          // fileupload的使用步骤
          // 1.创建本地磁盘工厂,用于设置上传文件的设置信息
          DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
          // 设置上传文件的缓冲区的大小,如果文件的大小超过了缓冲区的大小,就会产生临时文件
          diskFileItemFactory.setSizeThreshold(3*1024*1024); // 3M
          // 设置临时文件的存放路径
          // diskFileItemFactory.setRepository(new File(this.getServletContext().getRealPath("/temp")));
          // 2.获取Servlet解析上传文件的核心类
          ServletFileUpload fileUpload = new ServletFileUpload(diskFileItemFactory);
          // 设置表单中单个文件的大小,如果上传的文件超过限制,则会抛出异常
          fileUpload.setFileSizeMax(1 * 1024 * 1024); // 1M
          // 设置表单中所有字段的文件的总大小
          fileUpload.setSizeMax(10 * 1024 * 1024); // 10M
          // 3.获取request中所有的参数数据的集合
          // 将表单的enctype改变为multipart/form-data后,表单中的字段数据被分割成若干项的文件项组成
          // 每一个文件项含有一个表单字段的数据
          // 获取的文件项的集合,集合中文件项本质是DiskFileItem类型,DiskFileItem实现FileItem接口
          List<FileItem> list = fileUpload.parseRequest(req);
          // 设置文件上传的头信息中的数据的编码方式
          // 是解决文件上传项的中文文件名乱码的方法
          fileUpload.setHeaderEncoding("UTF-8");
          // 封装表单中的数据
          Product product = new Product();
          Category category = new Category();
          // 创建map集合用于封装参数
          Map<String, String> map = new HashMap<String, String>();
          // 4.遍历文件项集合,取出字段中数据
          // 获取上传文件的名称
          String filename = null;
          for (FileItem fileItem : list) {
              // 5.判断文件项字段是否是表单数据的普通文件项
              if(fileItem.isFormField()) { // 是普通文件项,就是表单中除文件类型的其他字段
                  // 获取该文件项的字段的名称,就是表单中字段的name属性值
                  String name = fileItem.getFieldName();
                  // 获取的是参数的值,就是表单中字段的value属性值
                  // String value = fileItem.getString();
                  // 获取的是参数的值,就是表单中字段的value属性值,指定编码方式
                  // 是普通文件项解决中文乱码问题的方法
                  String value2 = fileItem.getString("UTF-8");
                  map.put(name, value2);
              } else { // 是文件项数据文件字段
                  // 获取上传文件的名称,注意getFieldName是获取表单中的name属性值,而不是文件名称
                      //  当没有选择要上传的文件时,getName获取的值为空字符串""
                  filename = fileItem.getName();
                  // 获取上传文件的输入流对象
                  InputStream is = fileItem.getInputStream();
                  // 创建文件输出流对象
                  // 创建上传文件的存储位置
                  String path = this.getServletContext().getRealPath("/products/new");
                  File file = new File(path);
                      // 文件夹不存在就进行创建
                  if(!file.exists()) {
                      file.mkdir();
                  }
                  // 将输入流和输出流对象进行包装,使用缓冲流提高效率
                  InputStream nis = new BufferedInputStream(is);
                  OutputStream os = new BufferedOutputStream(new FileOutputStream(path+"/"+filename));
                  // 读取文件
                  int len = 0;
                  byte[] bys = new byte[1024];
                  while((len = nis.read(bys)) != -1)  {
                      os.write(bys, 0, len);
                      os.flush();
                  }
                  // 释放资源
                  os.close();
              }
          }

          // 将数据封装到product中
          BeanUtils.populate(category, map);
          BeanUtils.populate(product, map);
          // 设置分类,日期,图片数据,id,是否下架
          product.setCategory(category);
          product.setPdate(new Date());
          product.setPimage("products/new/"+filename);
          product.setPid(UUIDUtils.getUUID());
          product.setPflag(0); // 0表示未下架

          // 调用业务层更新数据
          ProductService productService = (ProductService) BeanFactory.getBean("ProductService");
          productService.add(product);
          // 跳转到查询所有商品的页面
          resp.sendRedirect(req.getContextPath()+"/AdminProductServlet?method=findByPage&currPage=1");
      } catch (Exception e) {
          e.printStackTrace();
      }
  }
}

2.10 AOP的简单使用

AOP(Aspect-Oriented-Programming):面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,是OOP技术的延续。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分间的耦合度降低,提高程序的可重用性和开发效率。

需求:在所有方法名以update(指增加数据方法)开始的方法操纵数据库的方法执行前添加权限的校验

// 在工厂类中,获取接口的实现类后,使用动态代理方式实现权限过滤
// 简单的AOP, 面向的是所有方法名以update开始的方法
/**
 * dao层和业务层的工厂类
 */
public class BeanFactory {
  /**
   * 获取业务层和dao层的接口的实现类
   */
  public static Object getBean(String beanid) {
      try {
          // 读取配置文件,从xml配置文件中获取接口的实现类
          // 创建读取xml文件的对象
          SAXReader reader = new SAXReader();
          // 获取文件的Document对象
          Document document = reader.read(BeanFactory.class.getClassLoader().getResourceAsStream("applicationContext.xml"));
          // 获取指定id名称的子元素,通过XPath获取元素
          Element element = (Element) document.selectSingleNode("//bean[@id='"+beanid+"']");
          // 获取子元素中class属性的值
          String classname = element.attributeValue("class");
          // 通过反射获取class对象
          Class<?> class1 = Class.forName(classname);
          // 创建实例
          final Object obj = class1.newInstance();
          // 创建动态代理对象
          Object proxy_obj = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new InvocationHandler() {
              @Override
              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                  // 获取方法的名称
                  String methodname = method.getName();
                  // 如果方法是以update开始,则添加权限校验
                  if(methodname.startsWith("update")) {
                      System.out.println("权限校验");
                      return method.invoke(obj, args);
                  }
                  // 不是以update开始的方法,执行原始的方法即可
                  return method.invoke(obj, args);
              }
          });
          // 返回代理对象
          return proxy_obj;
      } catch (Exception e) {
          e.printStackTrace();
      }
      return null;
  }
}
  • 36
    点赞
  • 265
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值