Bootstrap

使用springboot搭建网上商城项目

 一共______DAY!!!

项目分析( Day1)

项目功能

登录,注册,热销商品,用户管理(密码,个人信息,头像,收货地址),购物车(展示,增加,删除),订单模块

开发顺序

  1. 注册
  2. 登录
  3. 用户管理
  4. 购物车
  5. 商品
  6. 订单模块
某一个模块的开发顺序
  1. 持久层开发:依据前端页面的设置规划相关的SQL语句,以及进行配置
  2. 业务层开发:核心功能控制,业务操作以及异常的处理
  3. 控制层开发:接收请求,处理响应
  4. 前端开发:JS,Query,AJAX这些技术来连接后台

环境搭建

在idea搭建spring initializr项目。

  • jdk 21.0.3
  • maven 3.9.5
  • mysql 8.0
注:出现connect timed out问题

第一次搭建springboot需要测试连接,否则报错Cannot download ‘https://start.spring.io‘: connect timed out

创建数据库

测试连接

  • spring是否有图形输出

直接运行StoreApplication,观察控制台:

  • 数据库是否正常加载

在test包下StoreApplicationTests进行测试:

@SpringBootTest
class StoreApplicationTests {
    @Autowired
    private DataSource dataSource;

    @Test
    void contextLoads() {
    }

    @Test
    void getConnection() throws SQLException {
        System.out.println(dataSource.getConnection());
    }

}

控制台输出以下即为成功:

HikariProxyConnection@964829290 wrapping com.mysql.cj.jdbc.ConnectionImpl@e72fb04
2024-05-16T19:26:55.579+08:00  INFO 21708 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2024-05-16T19:26:55.598+08:00  INFO 21708 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

  • 测试静态资源能否正常加载 

引入相关pages资源,并重启springboot。重启成功后在浏览器输入localhost:8080/web/login.html进行测试,如果成功显示登录界面则正常加载静态资源。

项目搭建

用户注册

准备工作

  1. 创建数据表
    CREATE TABLE t_user (
                            uid INT AUTO_INCREMENT COMMENT '用户id',
                            username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名',
                            password CHAR(32) NOT NULL COMMENT '密码',
                            salt CHAR(36) COMMENT '盐值',
                            phone VARCHAR(20) COMMENT '电话号码',
                            email VARCHAR(30) COMMENT '电子邮箱',
                            gender INT COMMENT '性别:0-女,1-男',
                            avatar VARCHAR(50) COMMENT '头像',
                            is_delete INT COMMENT '是否删除:0-未删除,1-已删除',
                            created_user VARCHAR(20) COMMENT '日志-创建人',
                            created_time DATETIME COMMENT '日志-创建时间',
                            modified_user VARCHAR(20) COMMENT '日志-最后修改执行人',
                            modified_time DATETIME COMMENT '日志-最后修改时间',
                            PRIMARY KEY (uid)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  2. 创建用户实体类:
    1. 提取公共字段作为实体基类BaseEntity。例如:
      created_user VARCHAR(20) COMMENT '创建人',
      created_time DATETIME COMMENT '创建时间',
      modified_user VARCHAR(20) COMMENT '修改人',
      modified_time DATETIME COMMENT '修改时间',
    2. 用户实体类,继承BaseEntity
       uid INT AUTO_INCREMENT COMMENT '用户id',
      username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名',
      password CHAR(32) NOT NULL COMMENT '密码',
      salt CHAR(36) COMMENT '盐值',
      phone VARCHAR(20) COMMENT '电话号码',
      email VARCHAR(30) COMMENT '电子邮箱',
      gender INT COMMENT '性别:0-女,1-男',
      avatar VARCHAR(50) COMMENT '头像',
      is_delete INT COMMENT '是否删除:0-未删除,1-已删除',

DAO层 

通过Mybatis来操作数据库。

需要执行哪些SQL?
  •  插入新创建的用户:
    insert into t_user (username,password) values(...,...)
  •  检查是否创建过该用户,若存在则提醒用户名被占用:
    select * from t_user where username=?
    
设计接口和抽象方法

根据不同的功能模块来创建mapper接口。

  • UserMapper
    public interface UserMapper {
        /**
         * 插入用户的数据
         * @param user 用户的数据
         * @return 受影响的行数(增删改都将受影响的行数作为返回值,根据返回值来判断是否执行成功)
         */
        Integer insert(User user);
    
        /**
         * 根据用户名来查询用户的数据
         * @param username 用户名
         * @return 如果找到对应的用户则返回这个用户的数据,如果没有找到则返回null
         */
        User findByUsername(String username);
    }
编写映射

在resources目录下创建一个mapper文件夹,然后在这个文件夹里存放mapper的映射文件。

创建接口的映射文件,需要和接口的名称保持一致.如UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace用于指定当前的映射文件和哪个接口进行映射,需要指定接口的文件路径,路径需要是包的完整路径结构-->
<mapper namespace="com.liber.store.mapper.UserMapper">

    <resultMap id="UserEntityMap" type="com.liber.store.entity.User">
        <!--将表的字段和类的属性名不一致的进行匹配指定,名称一致的也可以指定,但没必要;但是,在定义映射规则时无论主键名称是否一致都不能省
        column属性:表示表中的字段名称
        property属性:表示类中的属性名称-->

        <id column="uid" property="uid"/>
        <result column="is_delete" property="isDelete"/>
        <result column="created_user" property="createdUser"/>
        <result column="created_time" property="createdTime"/>
        <result column="modified_user" property="modifiedUser"/>
        <result column="modified_time" property="modifiedTime"/>
    </resultMap>



    <!--id属性:表示映射的接口中方法的名称,直接标签的内容部来编写SQL语句-->
    <!--useGeneratedKeys="true"表示开启某个字段的值递增(大部分都是主键递增) keyProperty="uid"表示将表中哪个字段进行递增 -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="uid">
        insert into t_user
            ( username, password, salt, phone, email, gender, avatar, is_delete,
             created_user, created_time, modified_user, modified_time) values (
             #{username},#{password},#{salt},#{phone},#{email},#{gender},#{avatar},
             #{isDelete},#{createdUser},#{createdTime},#{modifiedUser},#{modifiedTime})
    </insert>


    <!--select语句在执行的时候查询的结果有两种:一个对象或多个对象
    resultType:表示查询的结果集类型,用来指定对应映射类的类型,且包含完整的包结构,
    但此处不能是resultType="com.cy.store.entity.User",因为这种写法要求表的字段的名字(以_分割)和类的属性名(驼峰命名法)一模一样
    编写resultMap:表示当表的字段和类的对象属性名不一致时,来自定义查询结果集的映射规则-->
    <select id="findByUsername" resultMap="UserEntityMap" >
        select * from t_user where username = #{username}
    </select>
</mapper>
单元测试

在test下创建mapper包进行测试。

 注意点:

  • @MapperScan("com.liber.store.mapper") 很重要,否则扫不到Mapper
  • /“无法自动装配。找不到 'UserMapper' 类型的 Bean。”,在UserMapper类加上@Mapper注释即可解决
    
package com.liber.store.mapper;


import com.liber.store.entity.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


// 表示标注当前的类是测试类 不会随同项目一同打包发送
@SpringBootTest
// 表示启动单元测试类,需要传参SpringRunner的实例类型
@RunWith(SpringRunner.class)

@MapperScan("com.liber.store.mapper")
public class UserMapperTests {
    /*
     单元测试类必须被@test注释修饰
     返回值类型为void
     方法没有参数
     方法的返回修饰符必须为public

     这样单元测试类才可以单独独立运行 不用启动整个项目 提升代码测试效率
     */

    @Autowired()
//    无法自动装配。找不到 'UserMapper' 类型的 Bean。
//    报错是接口不能直接创建bean
    private UserMapper userMapper;

    @Test
    public void insert(){
        User user = new User();
        user.setUsername("张三");
        user.setPassword("123456");
        Integer rows = userMapper.insert(user);
        System.out.println(rows);
    }
}

业务层(Day2)

分析
  • 业务层的核心功能
    • 接收前端的从控制器流转过来的数据
    • 结合真实的注册业务来完成功能业务逻辑的调转和流程
  • 异常
    • 规划
      • 用户在进行注册时可能会产生用户名被占用的错误,这时需要抛出一个异常
    • 处理
      • 不能用RuntimeException,过于宽泛且开发者没办法定位到具体的错误类型并处理
      • 定义具体的异常类型来继承这个异常
      • 异常要分等级,可能是在业务层产生异常,可能是在控制层产生异常
        • 创建一个业务层异常的基类ServiceException,继承RuntimeException, 后期开发业务层时具体的异常可以再继承业务层的异常ServiceException
具体实现
异常

 根据业务层不同的功能来详细定义具体的异常类型,并继承ServiceException异常基类。

  1. 用户在进行注册时可能会产生用户名被占用的错误,这时需要抛出一个UsernameDuplicatedException异常
  2. 正在执行数据插入操作的时候,服务器宕机或数据库宕机.这种情况是处于正在执行插入的过程中所产生的异常,起名InsertException异常
设计接口和抽象方法
接口
package com.liber.store.service;


import com.liber.store.entity.User;

//用户模块业务层接口
public interface IUserService {
    /**
     * 用户注册
     * @param user 需要插入的用户的相关数据
     */
    void reg(User user);
}
实现类
package com.liber.store.service.Impl;

import com.liber.store.entity.User;
import com.liber.store.mapper.UserMapper;
import com.liber.store.service.Ex.InsertException;
import com.liber.store.service.Ex.UsernameDuplicatedException;
import com.liber.store.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;

@Service
public class UserServiceImpl implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public void reg(User user) {
        // 获取新输入的用户名
        String username = user.getUsername();
        // 检查用户名是否被占用
        User user1 = userMapper.findByUsername(username);
        // 被占用则user1不为空
        if (user1 != null){
            // 抛出异常
            throw new UsernameDuplicatedException("NAME has been used!!");
        }

        // 不被占用 注册

        // 补全其他[必备]信息 4个日志字段+1个"is_delete":
        //  is_delete INT COMMENT '是否删除:0-未删除,1-已删除',
        //  created_user VARCHAR(20) COMMENT '日志-创建人',
        //  created_time DATETIME COMMENT '日志-创建时间',
        //  modified_user VARCHAR(20) COMMENT '日志-最后修改执行人',
        //  modified_time DATETIME COMMENT '日志-最后修改时间',
        Date date = new Date();
        user.setIsDelete(0);
        user.setCreatedUser(username);
        user.setCreatedTime(date);
        user.setModifiedUser(username);
        user.setModifiedTime(date);

        //  getMD5Password位置

        // 基本逻辑
        Integer insert = userMapper.insert(user);
        if (insert != 1){
            throw new InsertException("Insert failed! Try again.");
        }
    }
}
 单元测试
package com.liber.store.service;


import com.liber.store.entity.User;
import com.liber.store.mapper.UserMapper;
import com.liber.store.service.Ex.ServiceException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


// 表示标注当前的类是测试类 不会随同项目一同打包发送
@SpringBootTest
// 表示启动单元测试类,需要传参SpringRunner的实例类型
@RunWith(SpringRunner.class)

@MapperScan("com.liber.store.mapper")
public class UserSeiviceTests {
    /*
     单元测试类必须被@test注释修饰
     返回值类型为void
     方法没有参数
     方法的返回修饰符必须为public

     这样单元测试类才可以单独独立运行 不用启动整个项目 提升代码测试效率
     */

    @Autowired()
//    无法自动装配。找不到 'UserMapper' 类型的 Bean。
//    报错是接口不能直接创建bean
    private IUserService userService;

    @Test
    public void reg(){
        try {
            User user = new User();
            user.setUsername("李四");
            user.setPassword("123456");
            userService.reg(user);
            System.out.println("success.");
        } catch (ServiceException e) {
            System.out.println(e.getClass().getSimpleName());
            System.out.println(e.getMessage());
        }
    }
}

注:

  • 此时插入成功,但是数据库中可以直接显示用户设置的密码,信息很容易被泄露。采取的方法是在service层进行一个密码加密处理(md5加密处理算法),在reg()中
    private String getMD5Password(String password, String salt){
            for (int i = 0; i < 3; i++) {
                password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase();
            }
            return password;
        }

控制层 

创建相应

状态码,状态描述信息,数据是所有控制层对应的方法都涉及到的操作。

将三者封装到一个类JsonResult中:

package com.liber.store.util;


import java.io.Serializable;

// 所有的响应的结果都采用Json格式的数据进行响应,所以需要实现Serializable接口
public class JsonResult<E> implements Serializable {
    //状态码
    private Integer state;
    //描述信息
    private String message;
    //数据类型不确定,用E表示任何的数据类型,一个类里如果声明的有泛型的数据类型,类也要声明为泛型
    private E data;

    // 无参构造
    public JsonResult() {
    }

    public JsonResult(Integer state) {
        this.state = state;
    }

    public JsonResult(Integer state, E data) {
        this.state = state;
        this.data = data;
    }

    public JsonResult(Throwable e) {
        this.message = e.getMessage();
    }

    public Integer getState() {
        return state;
    }

    public void setState(Integer state) {
        this.state = state;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public E getData() {
        return data;
    }

    public void setData(E data) {
        this.data = data;
    }
}

,将这个类作为方法的返回值返回给前端浏览器

向后端服务器发送请求【把用户数据插入到数据库】
设计请求

依据当前的业务功能模块进行请求的设计:

  • 请求路径:/users/reg
  • 请求参数:User user
  • 请求类型:POST
  • 响应结果:JsonResult<void>
 处理请求
  1. 创建一个控制层对应的UserController类,依赖于业务层的接口
    package com.liber.store.controller;
    
    import com.liber.store.entity.User;
    import com.liber.store.service.Ex.InsertException;
    import com.liber.store.service.Ex.UsernameDuplicatedException;
    import com.liber.store.service.IUserService;
    import com.liber.store.util.JsonResult;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    //@Controller + @ResponseBody = @RestController
    @RequestMapping("users")
    public class UserController {
        @Autowired
        private IUserService userService;
    
        @RequestMapping("reg")
    //    @ResponseBody // 表示该方法以json的方式进行数据响应给到前端
        public JsonResult<Void> reg(User user){
            //创建响应结果对象即JsonResult对象
            JsonResult<Void> result = new JsonResult<>();
            try {
                userService.reg(user);
                result.setState(200);
                result.setMessage("Success.");
            } catch (UsernameDuplicatedException e) {
                result.setState(4000);
                result.setMessage("NAME has been used.");
            }catch (InsertException e) {
                result.setState(5000);
                result.setMessage("Insert failed.");
            }
            return result;
        }
    
    }
    

  2. 但是,以上Controller在捕获异常时显得冗余。此时可以进行优化设计,在控制层抽出一个BaseController父类,在这个父类中统一处理关于异常的相关操作:

    package com.liber.store.controller;
    
    import com.liber.store.service.Ex.InsertException;
    import com.liber.store.service.Ex.ServiceException;
    import com.liber.store.service.Ex.UsernameDuplicatedException;
    import com.liber.store.util.JsonResult;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    
    // 控制层类的基类 主要是异常捕获
    public class BaseController {
        public static final int SUCCESS = 200;
    
        /*
         * 1.@ExceptionHandler表示该方法用于处理捕获抛出的异常是哪一类
         * 2.所以需要ServiceException.class,只要是抛出ServiceException异常就会被拦截到handleException方法,
         * 3.此时handleException方法就是请求处理方法,返回值就是需要传递给前端的数据JsonResult
         * 4.被ExceptionHandler修饰后如果项目发生异常,那么异常对象就会被自动传递给此方法的参数列表上,所以形参就需要写Throwable e用来接收异常对象
         */
        @ExceptionHandler(ServiceException.class)
        public JsonResult<Void> handleException(Throwable e){
            JsonResult<Void> result = new JsonResult<>(e);
            if (e instanceof UsernameDuplicatedException) {
                result.setState(4000);
                result.setMessage("NAME has been used.");
            } else if (e instanceof InsertException) {
                result.setState(5000);
                result.setMessage("Insert failed.");
            }
            return result;
        }
    }
    

    优化后:使该方法只需要关注请求处理而不再需要关注异常捕获

    //    优化后
        public JsonResult<Void> reg(User user){
            //创建响应结果对象即JsonResult对象
            JsonResult<Void> result = new JsonResult<>();
            userService.reg(user);
            return new JsonResult<>(SUCCESS);
        }

前端页面

补充知识
ajax函数

加载页面时,有导航栏等相对固定的组件,也有动态广告窗等需要频繁更新的组件。如果把一整个页面看做整体,统一向服务器发送请求,效率很低;不如将某一个页面的某个局部看做独立部分。jQuery封装的$.ajax()函数用来异步加载相关的请求(将某一个页面的某个局部看做独立部分),依靠的是JavaScript提供的一个对象:XHR(全称XmlHttpResponse).

语法结构
参数功能描述
url

表示请求的地址(url地址),例如:url:“localhost:8080/users/reg”

1. 不能包含参数列表部分(?name=...&password=...)的内容

2. 如果提交的请求是项目内部的一个url,那么端口号前面的都可以省略掉,即url:“/users/reg”

type请求类型(GET和POST请求的类型).例如:type:“POST”(get和post不区分大小写)
data

向指定的请求url地址提交的数据.

例如:data:“username=tom&pwd=123”

dataType提交的数据的类型,一般指定为json类型(前后端分离).例如:dataType:“json”(json不区分大小写)
success当服务器正常响应客户端时,会自动调用success参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上
error当服务器未正常响应客户端时,会自动调用error参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上
  • 使用ajax()时需要传递一个方法体作为方法的参数来使用(一对{...}就是一个方法体)
  • ajax接受多个参数时,参数与参数之间使用","分割; 每一组参数之间使用":"进行分割
  • 参数的组成部分一个是参数的名称(不能随便定义),另一个是参数的值(必须用字符串来表示)
  • 参数的声明顺序没有要求
$.ajax({
    url: "",
    type: "",
    data: "",
    dataType: "",
    success: function(参数列表) {
        
    },
    error: function() {
        
    }
});
js

js代码可以独立声明在一个js文件里或者声明在一个script标签中

编写前端
<body>

            ......................


<script>
            //1.监听注册按钮是否被点击,如果被点击可以执行一个方法(这里不能像ajax函数那样删去function()只留下{},这是官方规定的!)
            $("#btn-reg").click(function () {
                //let username = $("#username").val();
                //let pwd = $("#password").val();
                //上面这两行是动态获取表单中控件的数据
				// 如果这样获取的话ajax函数中就是data: "username="+username + "&password="+pwd,但太麻烦了,
				// 如果这个表单提交的是用户的兴趣爱好,那数据就很多了,此时还用这种方式就太麻烦了,所以不建议用这种方式

            //2.发送ajax()的异步请求来完成用户的注册功能
                $.ajax({
                    url: "/users/reg",
                    type: "POST",
                    //serialize这个API会自动检测该表单有什么控件,每个控件检测后还会获取每个控件的值,
					// 拿到这个值后并自动拼接成形如username=Tom&password=123的结构
                    data: $("#form-reg").serialize(),
                    dataType: "JSON",
                    success: function (json) {
						//1.js是弱数据类型,这个地方不用声明json的数据类型
                        //2.如果服务器成功响应就会将返回的数据传给形参,比如:
						// 				{
						// 					"state": 200,
						// 						"message": null,
						// 						"data": null
						// 				}
                        if (json.state == 200) {
                            alert("注册成功")
                        } else {
                            alert("注册失败")
                        }
                    },
                    error: function (xhr) {
						//如果问题不在可控范围内,
						// 服务器就不会返回自己定义的json字符串:{state: 4000,message: "用户名已经被占用",data: null}
                        //而是返回一个XHR类型的对象,该对象也有一个状态码名字是status
                        alert("注册时产生未知的错误!"+xhr.status);
                    }
                });
            });
        </script>
	</body>

用户登录(Day3)

当用户输入用户名和密码将数据提交给后台数据库进行查询,如果存在对应的用户名和密码则表示登录成功,登录成功之后跳转到系统的主页就是index.html页面

持久层

需要执行的SQL语句

依据用户提交的用户名来做select查询。(注册编写时已经抽取到UserMapper.xml中

持久层只查询用户名,判断用户名和密码是否一致交给业务层做:

select * from t_user where username=?

 注册编写时已经抽取到UserMapper.xml中,后续的设计接口和抽象方法,编写映射,单元测试都不再需要进行。

业务层

异常

继承ServiceException并重写所有构造方法。

  • 密码匹配异常PasswordNotMatchException
  • 用户名没有被找到异常UsernameNotFoundException
 设计接口和抽象方法

在IUserService接口中编写抽象方法User login(String username,String password) / User  login(User user)

package com.liber.store.service.Impl;

import com.liber.store.entity.User;
import com.liber.store.mapper.UserMapper;
import com.liber.store.service.Ex.InsertException;
import com.liber.store.service.Ex.PasswordNotMatchException;
import com.liber.store.service.Ex.UsernameDuplicatedException;
import com.liber.store.service.Ex.UsernameNotFoundException;
import com.liber.store.service.IUserService;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.util.Date;
import java.util.UUID;

// 将当前类的对象交给spring管理,能自动创建对象并维护
@Service
@MapperScan("com.liber.store.mapper")
public class UserServiceImpl implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public void reg(User user) {
        // 获取新输入的用户名
        String username = user.getUsername();
        // 检查用户名是否被占用
        User user1 = userMapper.findByUsername(username);
        // 被占用则user1不为空
        if (user1 != null){
            // 抛出异常
            throw new UsernameDuplicatedException("NAME has been used!!");
        }

        // 不被占用 注册

        // 补全其他[必备]信息 4个日志字段+1个"is_delete":
        //  is_delete INT COMMENT '是否删除:0-未删除,1-已删除',
        //  created_user VARCHAR(20) COMMENT '日志-创建人',
        //  created_time DATETIME COMMENT '日志-创建时间',
        //  modified_user VARCHAR(20) COMMENT '日志-最后修改执行人',
        //  modified_time DATETIME COMMENT '日志-最后修改时间',
        Date date = new Date();
        user.setIsDelete(0);
        user.setCreatedUser(username);
        user.setCreatedTime(date);
        user.setModifiedUser(username);
        user.setModifiedTime(date);

        // 密码加密处理作用:
        //      后端不再能直接看到用户的密码
        //      忽略了密码原来的强度,提升了数据安全性
        // 密码加密处理的实现:
        //      串+password+串->交给md5算法连续加密三次
        //      盐值 + password + 盐值-> 盐值是随机的串
        //      串就是数据库字段中的盐值,是一个随机字符串
        String oldPassword = user.getPassword();
        // salt需要被记录下来 不然无法验证
        String salt = UUID.randomUUID().toString().toUpperCase();
        user.setSalt(salt);
        // 直接在外部定义一个加密方法getMD5Password
        String md5Password = getMD5Password(oldPassword, salt);
        user.setPassword(md5Password);

        // 基本逻辑
        Integer insert = userMapper.insert(user);
        if (insert != 1){
            throw new InsertException("Insert failed! Try again.");
        }
    }

    @Override
    public User log(String username, String password) {
        User user = userMapper.findByUsername(username);
//        用户不存在 报错
        if (user == null){
            throw new UsernameNotFoundException("user "+ username +"is not existed.");
        }
//        用户存在
//        * 检测用户的密码是否匹配:
//         * 1.先获取数据库中加密之后的密码
//                * 2.和用户传递过来的密码进行比较
//                *  2.1先获取盐值
//                *  2.2将获取的用户密码按照相同的md5算法加密
        String old = user.getPassword();
        String salt = user.getSalt();
        String md5Password = getMD5Password(password, salt);
        if (!md5Password.equals(old)){
            throw new PasswordNotMatchException("password is wrong");
        }
//        匹配 需查看是否被删除
        if (user.getIsDelete() == 1){
            throw new UsernameNotFoundException("user "+ username +"is not existed.");
        }
        //方法login返回的用户数据是为了辅助其他页面做数据展示使用(只会用到uid,username,avatar)
        //所以可以new一个新的user只赋这三个变量的值,
        // 这样使层与层之间传输时数据体量变小,后台层与层之间传输时数据量越小性能越高,前端也是的,数据量小了前端响应速度就变快了
        User existedUser = new User();
        existedUser.setUid(user.getUid());
        existedUser.setUsername(user.getUsername());
        existedUser.setAvatar(user.getAvatar());
        return existedUser;
    }

    private String getMD5Password(String password, String salt){
        for (int i = 0; i < 3; i++) {
            password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase();
        }
        return password;
    }
}
 单元测试
@Test
    public void log(){
        try {
            User user = new User();
            user.setUsername("王五");
            user.setPassword("1256");
//            user.setPassword("123456");
            userService.log(user.getUsername(),user.getPassword());
            System.out.println("success.");
        } catch (ServiceException e) {
            System.out.println(e.getClass().getSimpleName());
            System.out.println(e.getMessage());
        }
    }

控制层

处理异常
// baseController
else if (e instanceof UserPrincipalNotFoundException) {
            result.setState(4001);
            result.setMessage("user has not existed.");
        }else if (e instanceof PasswordNotMatchException) {
            result.setState(4002);
            result.setMessage("password is wrong.");
        }
设计请求

登录成功后跳转到login:

  • 请求路径:/users/login
  • 请求参数:String username,String password,HttpSession session(session优化时需要)
  • 请求类型:POST
  • 响应结果:JsonResult<User>
处理请求
// 表示该方法以json的方式进行数据响应给到前端 在UserController写入
    @RequestMapping("login")
    // 控制层方法的参数是用来接收前端数据的,接收数据方式有两种:
    //      log————请求处理方法的参数列表设置为非pojo类型 eg.String:
    //          SpringBoot会直接将请求的参数名和方法的参数名直接进行比较,如果名称相同则自动完成值的依赖注入
    //      reg————请求处理方法的参数列表设置为pojo类型:
    //          SpringBoot会将前端的url地址中的参数名和pojo类的属性名进行比较,如果这两个名称相同,则将值注入到pojo类中对应的属性上
    // 这两种方法都没有使用注解,因是springboot是约定大于配置的,省略了大量配置以及注解的编写
    public JsonResult<User> log(String username, String password){
        User user = userService.log(username, password);
        return new JsonResult<User>(SUCCESS,user);
         }
    }

前端页面

在login.html编写ajax:

<script>
            $("#btn-login").click(function () {
                $.ajax({
                    url: "/users/login",
                    type: "POST",
                    data: $("#form-login").serialize(),
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            alert("登录成功")
                            //跳转到系统主页index.html
                            //index和login在同一个目录结构下
							// 可以用相对路径index.html来确定跳转的页面,index.html和./index.html完全一样,
							// 因为./就是表示当前目录结构,也可以用../web/index.html
                            location.href = "index.html";
                            $.cookie("avatar",json.data.avatar,{expires: 7});
                        } else {
                            alert("登录失败")
                        }
                    },
                    error: function (xhr) {
                        //xhr.message可以获取未知异常的信息
                        alert("登录时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>

 优化(Day4)

优化1:数据获取

用户数据只能在登录的前端获取,在其他页面无法获取。

优化思路:存储在一个全局对象中 利用会话Session实现。把首次登录所获取的用户数据转移到session对象

Session

主要存储在服务器端,可以保存服务器的临时数据对象,该数据可以在整个项目中通过访问获取。

具体步骤
分析
  • 获取session对象的属性值用session.getAttribute(“key”)。但是session对象的属性值在很多页面都要被访问,用session对象调用方法获取数据就麻烦。解决办法是将获取session中数据的这种行为进行封装
  • 就目录结构而言,只有可能在控制层使用session,而控制层里的类继承BaseController,所以可以封装到BaseController。
实施
  • 封装Session对象中的数据获取(封装在父类中,一登录就获取)与设置(用户成功登陆后进行数据设置) 

    • 在父类中封装两个方法:获取uid和获取username对应的两个方法

      /*
           * 对session行为进行封装
           */
          /**
           * 获取uid
           * @param session session对象
           * @return 当前登录uid的值
           */
          public final Integer getUidFromSession(HttpSession session){
              Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
              return uid;
          }
      
          /**
           * 获取用户名
           * @param session session对象
           * @return 当前登录的用户名
           */
          public final String getUsernameFromSession(HttpSession session){
              return session.getAttribute("username").toString();
          }
    • 把首次登录所获取的用户数据转移到session对象

      public JsonResult<User> log(String username, String password, HttpSession session){
              User user = userService.log(username, password);
              session.setAttribute("uid",user.getUid());
              session.setAttribute("username",user.getUsername());
              return new JsonResult<User>(SUCCESS,user);
               }
      • 注:服务器本身自动创建有session对象,是一个全局的session对象,所以需要想办法获取session对象。Springboot可以直接使用session,即直接将HttpSession类型的对象作为请求处理方法的参数,自动将全局的session对象注入到请求处理方法的session形参上。

      • 测试是否成功(成功则在终端输出):

                // 向session对象完成数据绑定
                session.setAttribute("uid",user.getUid());
                session.setAttribute("username",user.getUsername());
                //测试能否正常获取session中存储的数据
                System.out.println(getUidFromSession(session));
                System.out.println(getUsernameFromSession(session));

         

优化2:安全性

目前项目可以跳过登录页面直接访问其他页面,这样非常不安全!!要通过拦截器实现安全保证。

拦截器

将所有的请求统一拦截到拦截器中。可以在拦截器中定义过滤的规则,如果不满足系统设置的过滤规则则拒绝访问。

拦截器在springboot中本质是依靠springMVC完成的,springMVC提供了一个HandlerInterceptor接口用于表示定义一个拦截器。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.web.servlet;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;

public interface HandlerInterceptor {
// 执行时机:调用所有处理请求的方法之前被自动调用执行
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

// 执行时机:modelAndView返回之后被自动调用执行
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }
// 执行时机:整个请求所有的关联的资源被执行完毕之后被自动调用执行
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}
具体步骤

过滤的规则:重新打开login.html页面(重定向和转发都可以,推荐使用重定向)

  • 拦截器设计:定义一个类并使其实现HandlerInterceptor接口
    package com.liber.store.interceptor;
    
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    public class LoginInterceptor implements HandlerInterceptor {
        /**
         * 检测全局对象中是否有uid数据,有则放行;没有则重定向
         * @param request 请求对象
         * @param response 响应对象
         * @param handler 处理器
         * @return 返回值为true表示放行;false为拦截
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            Object uid = request.getSession().getAttribute("uid");
            if (uid == null){
                // 重定向
                response.sendRedirect("/web/login.html");
                return false;
            }
            return true;
        }
    }
    
  • 拦截器注册:定义一个类使其实现WebMvcConfigure接口。借助WebMvcConfigure接口将用户定义的拦截器进行注册,才能生效使用。主要是为了添加白名单(①register.html②login.html③index.html④/users/reg⑤/users/login⑥静态资源⑦产品详情页)和黑名单(在用户登录的状态下才可以访问的页面资源)。
    package com.liber.store.config;
    
    import com.liber.store.interceptor.LoginInterceptor;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    import java.util.ArrayList;
    import java.util.List;
    
    // 注册 自定义的拦截器
    public class LoginInterceptorConfigure implements WebMvcConfigurer {
        // 创建自定义拦截对象
        HandlerInterceptor handlerInterceptor = new LoginInterceptor();
        /**
         * 添加 自定义的拦截器
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
    
            // 配置白名单
            List<String> patterns = new ArrayList<>();
            // 静态资源
            patterns.add("/bootstrap3/**");
            patterns.add("/css/**");
            patterns.add("/images/**");
            patterns.add("/js/**");
            // 注册
            patterns.add("/web/register.html");
            // 登录
            patterns.add("/web/login.html");
            // 主页
            patterns.add("/web/index.html");
            // 产品页
            patterns.add("/web/product.html");
            
            patterns.add("/users/reg");
            patterns.add("/users/login");
    
            registry.addInterceptor(handlerInterceptor)
                    .addPathPatterns("/**") // 表示要拦截的url /** 代表拦截所有
                    .excludePathPatterns(patterns);  // 接收List集合 白名单
        }
    }
    
    • 注:配置信息建议存放在config包内

    • 不要总尝试,可能会提示重定向次数过多,login也打不开了

修改密码(Day4)

根据前端分析功能:

  1. 需要用户提交初始密码和新密码
  2. 初始密码是否正确?
    • 正确,则可以修改
    • 错误,则重新输入

持久层

需要执行的SQL语句

根据用户的uid查询用户:是否存在、是否删除、原始密码是否正确

SELECT * FROM t_user WHERE uid=?

根据用户的uid修改用户password值

update t_user set password=?,modified_user=?, modified_time=? WHERE uid=?
 设计接口和抽象方法

usermapper接口中编写 :


/**根据用户uid来修改用户密码
     *
     * @param uid uid
     * @param password 修改的新密码
     * @param modifiedUser 修改者
     * @param modifiedTime 修改时间
     * @return 受影响的行数
     */
    Integer updatePasswordByUid(Integer uid, String password, String modifiedUser, Date modifiedTime);

    /**
     * 根据用户uid来查询数据
     * @param uid uid
     * @return 返回对象
     */
    User findByUid(Integer uid);
编写映射
<!--  修改密码时使用  -->
    <select id="findByUid" resultType="UserEntityMap">
        select * from t_user where uid = #{uid}
    </select>
    <!--  修改密码时使用  -->
    <update id="updatePasswordByUid">
        update t_user set
                          password=#{password},modified_time=#{modifiedTime},modified_user=#{modifiedUser}
                      where uid=#{uid}
    </update>
 单元测试
    @Test
    public void fidByUid(){
        User user = userMapper.findByUid(5);
        System.out.println(user);
    }

    @Test
    public void updatePasswordByUid(){
        Integer test = userMapper.updatePasswordByUid(1, "654321", "root_test", new Date());
        System.out.println(test);
    }

业务层

异常
  • 密码错误异常(已创建)
  • 用户没找到异常(已创建)
  • update在更新的时候,有可能产生未知的异常:UpdateException异常
 设计接口和抽象方法

IUserService接口:

    /**
     * 修改密码
     * @param uid uid
     * @param username 用户名
     * @param oldPassword 旧密码
     * @param newPassword 新密码
     */
    void changePassword(Integer uid, String username, String oldPassword, String newPassword);

方法实现: 

@Override
    public void changePassword(Integer uid, String username, String oldPassword, String newPassword) {
        User user = userMapper.findByUid(uid);
        // 是否存在
        // 不存在
        if (user == null || user.getIsDelete() == 1){
            throw new UsernameNotFoundException("未找到该用户!");
        }
        // 存在, 旧密码是否输入正确
        String salt = user.getSalt();
        // 不正确
        if (!((user.getPassword())).equals(getMD5Password(oldPassword,salt))){
            throw new PasswordNotMatchException("密码输入错误!");
        }
        //正确,把新密码存入
        Integer row = userMapper.updatePasswordByUid(uid, getMD5Password(newPassword, salt), username, new Date());
        if (row != 1){
            throw new UpdateException("更新时产生异常!");
        }
    }
单元测试
@Test
    public void changePassword(){
        userService.changePassword(5,"root_text","654321","123456");
    }

控制层

处理异常

在Basecontroller中:

else if (e instanceof UpdateException) {
            result.setState(5003);
            result.setMessage("Update failed.");
        }
设计请求
  • 请求路径:/users/change_password
  • 请求参数(要和前端表单属性值保持一致):String oldPassword,String newPassword, HttpSession session(session优化时需要)
  • 请求类型:POST
  • 响应结果:JsonResult<Void>
处理请求

在UserController中:

// 表示该方法以json的方式进行数据响应给到前端
    @RequestMapping("change_password")
    public JsonResult<Void> changPassword(String oldPassword,String newPassword, HttpSession session){
        Integer uid = getUidFromSession(session);
        String username = getUsernameFromSession(session);
        userService.changePassword(uid,username,oldPassword,newPassword);
        return new JsonResult<>(SUCCESS);
    }
 前端页面password.xml
<!--页脚结束-->
        <script>
            $("#btn-change-password").click(function () {
                $.ajax({
                    url: "/users/change_password",
                    type: "POST",
                    data: $("#form-change-password").serialize(),
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            alert("密码修改成功")
                        } else {
                            alert("密码修改失败")
                        }
                    },
                    error: function (xhr) {
                        //xhr.message可以获取未知异常的信息
                        alert("修改密码时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>
  •  未实现“再次输入新密码”

个人资料(Day5)

持久层

需要执行的SQL语句
  • 根据uid查询用户数据
    select * from t_user where uid=?
    

    前面已实现。

  • 更新用户信息
    update t_user set python=?,email=?,gender=?,modified_user=?,modified_time=? where uid=?
    
设计接口和抽象方法
  • public interface UserMapper:

        /**
         * 参数为user的方法
         * @param user 用户的数据
         * @return 返回值为受影响的行数
         */
        Integer updateInfoByUid(User user);//也可以用三个String的形参接收电话,邮箱,性别,但不如直接写个user省事
    
编写映射 
  • UserMapper.xml 
 <!--  更新个人信息  -->
    <update id="updateInfoByUid">
        update t_user set
        <!--if是条件判断标签,属性test接受的是一个返回值为boolean类型的条件,
            如果test条件的结果为true则执行if标签内部的语句,注意逗号也要在标签内-->
                          <if test="phone!=null">phone = #{phone},</if>
                          <if test="email!=null">email = #{email},</if>
                          <if test="gender!=null">gender = #{gender},</if>
                          modified_user = #{modifiedUser},
                          modified_time = #{modifiedTime}
                      where uid=#{uid}
    </update>
单元测试
@Test
    public void updateInfoByUid(){
        User user = new User();
        user.setUid(1);
        user.setPhone("13333688");
        user.setEmail("[email protected]");
        user.setGender(1);
        user.setModifiedUser("root");
        user.setModifiedTime(new Date());
        Integer test = userMapper.updateInfoByUid(user);
        System.out.println(test);
    }

业务层

功能设计
  1. 打开页面时显示用户信息
  2. 点击修改按钮时更新用户信息
异常
  • 点击个人资料页面时可能找不到用户的数据
  • 点击修改按钮时可能找不到用户数据,也可能修改时出现未知错误
设计接口和抽象方法

 IUserService接口:

/**
     * 根据用户的uid查询用户数据
     * @param uid 用户uid
     * @return 用户数据
     */
    User getByUid(Integer uid);

    /**
     * uid通过控制层在session中获取然后传递给业务层,并在业务层封装到User对象中
     * */
    void changeInfo(Integer uid,User user);

 方法实现:

@Override
    public User getByUid(Integer uid) {
        User user = userMapper.findByUid(uid);
        if (user == null){
            throw new UsernameNotFoundException("用户数据不存在");
        }
//        return user; // 直接返回的话 信息太冗余
        User user1 = new User();
        user1.setUsername(user.getUsername());
        user1.setPhone(user.getPhone());
        user1.setEmail(user.getEmail());
        user1.setGender(user.getGender());
        return user1;
    }

    /**
     *User对象中的数据只有phone,email,gender,username,因为springboot进行依赖
     * 注入的时候只注入表单中数据的值,所以需要手动将uid封装到user中
     */
    @Override
    public void changeInfo(Integer uid, User user) {
        User result = userMapper.findByUid(uid);
        if (result == null || result.getIsDelete() == 1) {
            throw new UsernameNotFoundException("用户数据不存在");
        }
        user.setUid(uid);
        user.setModifiedUser(user.getUsername());
        user.setModifiedTime(new Date());

        Integer rows = userMapper.updateInfoByUid(user);
        if (rows!=1) {
            throw new UpdateException("更新数据时产生异常");
        }
    }
单元测试
@Test
    public void updateInfoByUid(){
        User user = new User();
        user.setUid(1);
        user.setPhone("5555555");
        user.setEmail("[email protected]");
        user.setGender(1);
        user.setModifiedUser("root");
        user.setModifiedTime(new Date());
        Integer test = userMapper.updateInfoByUid(user);
        System.out.println(test);
    }

控制层

处理异常

没有新异常。

设计请求
打开页面可以查看当前用户信息
  • 请求路径:/users/get_by_uid
  • 请求参数(要和前端表单属性值保持一致):HttpSession session(用于获取uid)
  • 请求类型:GET
  • 响应结果:JsonResult<User>
修改按钮可以提交修改信息
  • 请求路径:/users/change_info
  • 请求参数(要和前端表单属性值保持一致):User user, HttpSession session(用于获取uid)
  • 请求类型:POST
  • 响应结果:JsonResult<Void>
处理请求
 @RequestMapping("get_by_uid")
    public JsonResult<User> getByUid(HttpSession session){
        User user = userService.getByUid(getUidFromSession(session));
        return new JsonResult<User>(SUCCESS, user);
    }

    @RequestMapping("change_info")
    public JsonResult<Void> changeInfo(User user,HttpSession session){
        Integer uid = getUidFromSession(session);
        userService.changeInfo(uid,user);
        return new JsonResult<>(SUCCESS);
    }
测试
  • 启动服务,先登录账号然后在地址栏输入http://localhost:8080/users/get_by_uid看看状态码是否为200并且看data值是否不为null
  • 启动服务,先登录账号然后在地址栏输入http://localhost:8080/users/change_info?phone=175726&[email protected]&username=张9&gender=1观察状态码是否为200
前端
<script>
            /**
             * 作用:一旦检测到当前的页面被加载就会触发ready方法
             * $(document).ready(function(){
             *     //编写业务代码
             * });
             */
            //点击"个人资料"四个字加载userdata.html页面时$(document).ready(function(){});就会起作用发送ajax请求(不需要点击任何按钮)
            $(document).ready(function() {
                $.ajax({
                    url: "/users/get_by_uid",
                    type: "GET",
                    //data为null也可以,因为这里get是从数据库拉取数据,不需要data
                    data: $("#form-change-info").serialize(),
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            //将查询到的数据设置到控件中
                            $("#username").val(json.data.username);
                            $("#phone").val(json.data.phone);
                            $("#email").val(json.data.email);
                            let radio = json.data.gender == 0 ? $("#gender-female") : $("#gender-male");
                            //prop()表示给某个元素添加属性及属性的值 如果性别为0,选择女性单选按钮,否则选择男性单选按钮
                            radio.prop("checked","checked"); //  设置单选按钮为选中状态
                        } else {
                            alert("用户的数据不存在")
                        }
                    },
                    error: function (xhr) {
                        alert("查询用户信息时产生未知的异常!"+xhr.message);
                    }
                });
            });
			// 点击修改资料按钮就会提交修改
            $("#btn-change-info").click(function () {
                $.ajax({
                    url: "/users/change_info",
                    type: "POST",
                    data: $("#form-change-info").serialize(),
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            alert("用户信息修改成功")
                            //修改成功后重新加载当前的页面
                            location.href = "userdata.html";
                        } else {
                            alert("用户信息修改失败")
                        }
                    },
                    error: function (xhr) {
                        //xhr.message可以获取未知异常的信息
                        alert("用户信息修改时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>

上传头像

持久层

想法
  • 把文件存到数据库中,需要图片时访问数据库,数据库将文件解析为字节流返回,最后写到本地的某一个文件。
  • 将对应的文件保存在操作系统上,然后再把这个文件路径记录下来,记录路径非常便捷。将来如果要打开这个文件可以依据这个路径找到这个文件,所以说在数据库中保存该文件的路径即可.

第二种可行。大公司一般都会将所有的静态资源(图片,文件,其他资源文件)放到某台电脑上,再把这台电脑作为一台单独的服务器使用。

需要执行的SQL语句

头像对应的字段是avatar,类型为varchar。查询到用户头像所在的路径并存入该字段。

  • 更新用户avatar字段的sql语句
update t_user set avatar=?,modified_user=?,modified_time=? where uid=?
设计接口和抽象方法 
/**
     * 修改用户头像
     * @param uid uid
     * @param avatar 头像存放路径
     * @param modifiedUser 修改人
     * @param modifiedTime 修改时间
     * @return 数据库中被修改的行数
     */
    Integer updateAvatarUid(@Param("uid") Integer uid,  // @Param()相同可以省略,不同可以强行匹配
                            @Param("avatar")String avatar, 
                            @Param("modifiedUser")String modifiedUser, 
                            @Param("modifiedTime")Date modifiedTime);
编写映射
    <!--  上传头像  -->
    <update id="updateAvatarUid">
        update t_user set 
                          avatar=#{avatar},
                          modified_user=#{modifiedUser},
                          modified_time=#{modifiedTime} 
                      where uid=#{uid}
    </update>
单元测试
 @Test
    public void updateAvatarUid() {
        Integer integer = userMapper.updateAvatarUid(1,
                "abc",
                "root",
                new Date());
        System.out.println(integer);
    }

业务层

异常
  • 用户数据不存在,找不到对应的用户数据
  • 更新的时候,出现未知异常

无需开发。

设计接口和抽象方法

看mapper层需要什么数据

Integer uid, String avatar, String modifiedUser, Date modifiedTime);

  • 其中modifiedTime是在方法中创建的,不须保留
  • uid和modifiedUser从session中获取。但是session对象是在控制层的并不会出现在业务层,所以业务层要保留这两个参数,以便控制层可以传递过来

 IUserService接口:

/**
     * 修改用户的头像
     * @param uid 用户uid
     * @param avatar 用户头像的路径
     * @param username 用户名称
     */
    void changeAvatar(Integer uid,
                      String avatar,
                      String username);//业务层一般叫username而不叫modifiedUser【业务层并没有直接和数据库关联】

方法实现:

    @Override
    public void changeInfo(Integer uid, User user) {
        User result = userMapper.findByUid(uid);
        // 是否存在
        // 不存在
        if (result == null || result.getIsDelete() == 1) {
            throw new UsernameNotFoundException("用户数据不存在");
        }
        // 存在则写入uid 修改人 修改时间
        user.setUid(uid);
        user.setModifiedUser(user.getUsername());
        user.setModifiedTime(new Date());
        // 更新phone,email,gender
        Integer rows = userMapper.updateInfoByUid(user);
        if (rows!=1) {
            throw new UpdateException("更新数据时产生异常");
        }
    }
单元测试
    @Test
    public void changeAvatar(){
        userService.changeAvatar(1,"sdfgg","root");
    }

控制层

处理异常
分析
  • 客户端传递文件给服务器,服务器的控制端controller接收文件,接收时可能抛出异常。因为用户传过来的文件有可能超出了大小限制。
  • 文件类型不匹配,被损坏,文件后缀修改...

传递的文件需要符合限制条件,不符合就要抛出异常

因为此时数据是从控制层往下传的,所以控制层产生的异常直接在这一层(控制层)抛就可以了。可以效仿业务层的异常:创建一个文件异常基类FileUploadException并继承RuntimeException。

以下异常继承FileUploadException:

  • FileEmptyException:文件为空的异常(没有选择上传的文件就提交了表单,或选择的文件是0字节的空文件)
  • FileSizeException:文件大小超出限制
  • FileTypeException:文件类型异常(上传的文件类型超出了限制)
  • FileUploadIOException:文件读写异常
  • FileStateException:文件状态异常(上传文件时该文件正在打开状态)

在controller包下创子包ex,在ex包里面创建文件异常类的基类和上述五个文件异常类,创建的六个类都重写其父类的五个构造方法。

处理
else if (e instanceof FileEmptyException) {
    result.setState(6000);
} else if (e instanceof FileSizeException) {
    result.setState(6001);
} else if (e instanceof FileTypeException) {
    result.setState(6002);
} else if (e instanceof FileStateException) {
    result.setState(6003);
} else if (e instanceof FileUploadIOException) {
    result.setState(6004);
}

 注:要把handleException注释修改一下,便于将异常拦截到该方法中

@ExceptionHandler({ServiceException.class,FileUploadException.class})
设计请求 
  • 请求路径:/users/change_avatar
  • 请求参数(要和前端表单属性值保持一致): MultipartFile file, HttpSession session(用于获取uid和username)
  • 请求类型:POST(GET请求提交数据只有2KB左右)
  • 响应结果:JsonResult<String>

 响应结果不能是JsonResult<Void>:

        如果是JsonResult<Void>,上传头像后浏览别的页面,然后再回到上传头像的页面就展示不出来了。

        所以图片一旦上传成功,就要保存该图片在服务器的哪个位置<String>,这样的话一旦检测到进入上传头像的页面就可以通过保存的路径拿到图片,最后展示在页面上。

参数名必须是file:
       在upload.html页面的147行<input type="file" name="file">中的name="file",所以必须有一个方法的参数名为file用于接收前端传递的该文件。
       如果想要参数名和前端的name不一样,用@RequestParam("file")MultipartFile ffff:把表单中name="file"的控件值传递到变量ffff上。

参数类型必须是MultipartFile:
        这是springmvc中封装的一个包装接口,如果类型是MultipartFile并且参数名和前端上传文件的name相同,则会自动把整体的数据包传递给file。


处理请求 
    // 文件最大值
    public static final int AVATAR_MAX_SIZE = 10 * 1024 * 1024;
    // 文件类型
    private static final List<String> AVATAR_TYPE = new ArrayList<>();

    // 静态块在类加载时执行,确保了AVATAR_TYPE列表在类第一次被使用之前就已经被初始化
    static {
        AVATAR_TYPE.add("image/jpeg");
        AVATAR_TYPE.add("image/png");
        AVATAR_TYPE.add("image/bmp");
        AVATAR_TYPE.add("image/gif");

    }

    @RequestMapping("change_avatar")
    /*
      1.参数名必须用file:
             在upload.html页面的147行<input type="file" name="file">中的name="file",
             所以必须有一个方法的参数名为file用于接收前端传递的该文件.
             如果想要参数名和前端的name不一样:
             @RequestParam("file")MultipartFile ffff:把表单中name="file"的控件值传递到变量ffff上
     * 2.参数类型必须是MultipartFile:
             这是springmvc中封装的一个包装接口,
             如果类型是MultipartFile并且参数名和前端上传文件的name相同,则会自动把整体的数据包传递给file
     */
    public JsonResult<String> changeAvatar(HttpSession session, @RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) { // 文件为空
            throw new FileEmptyException("文件为空");
        }
        // 文件不为空 判断文件大小
        if (file.getSize() > AVATAR_MAX_SIZE) {
            throw new FileSizeException("文件超出限制");
        }
        // 大小合适 判断文件的类型是否是规定的后缀类型
        String contentType = file.getContentType();
        if (!AVATAR_TYPE.contains(contentType)) {
            throw new FileTypeException("文件类型不支持");
        }
        //上传的文件路径:.../upload/文件名.png
        /*
          session.getServletContext()获取当前Web应用程序的上下文对象(每次启动tomcat都会创建一个新的上下文对象)
          getRealPath("/upload")的/代表当前web应用程序的根目录,通过该相对路径获取绝对路径,返回一个路径字符串,
          如果不能进行映射返回null,单斜杠可要可不要
         */
        String parent = session.getServletContext().getRealPath("/upload");
//        System.out.println(parent);//调试用
        //File对象指向这个路径,通过判断File是否存在得到该路径是否存在
        File dir = new File(parent);
        if (!dir.exists()) {//检测目录是否存在
            dir.mkdirs();//创建当前目录
        }

        //获取这个文件名称(文件名+后缀,如avatar01.png,不包含父目录结构)
        String originalFilename = file.getOriginalFilename();
        System.out.println("OriginalFilename=" + originalFilename);
        // 获取后缀
        /*
        String类的lastIndexOf()方法用于查找指定字符)在字符串中最后一次出现的位置。
        如果找到了该字符,lastIndexOf()会返回该字符的索引;如果没有找到,它会返回-1。
        */
        int index = originalFilename.lastIndexOf(".");
        String suffix = originalFilename.substring(index);
        // 工具生成一个新的字符串作为文件名(好处:避免了因文件名重复发生的覆盖) 用UUID【具有唯一性】
        // 形如SAFS1-56JHIOHI-HIUGHUI-5565TYRF.png
        String filename = UUID.randomUUID().toString().toUpperCase() + suffix;

        //在dir目录下创建filename文件(此时是空文件)
        File dest = new File(dir, filename);
        //java可以把一个文件的数据直接写到同类型的文件中:将参数file中的数据写入到空文件dest中
        try {
            file.transferTo(dest);//transferTo是一个封装的方法,用来将file文件中的数据写入到dest文件
            /*
             * 先捕获FileStateException再捕获IOException
             * 因为后者包含前者,如果先捕获IOException那么FileStateException就永远不可能会被捕获
             */
        } catch (FileStateException e) {
            throw new FileStateException("文件状态异常");
        } catch (IOException e) {
            //用FileUploadIOException类并抛出文件读写异常
            throw new FileUploadIOException("文件读写异常");
        }
        Integer uid = getUidFromSession(session);
        String username = getUsernameFromSession(session);
        String avatar = "/upload/" + filename;
        userService.changeAvatar(uid, avatar, username);
        //返回用户头像的路径给前端页面,将来用于头像展示使用
        return new JsonResult<>(SUCCESS, avatar);

    }
测试 

没办法测试,因为file无法定义。

前端
  1. 在upload.html的上传头像的表单加上三个属性:
    <div class="panel-body">
    						<!--上传头像表单开始-->
    						<form id="form-change-avatar"
    							  action="/users/change_avatar"
    							  method="post"
    							  enctype="multipart/form-data"
                                  class="form-horizontal" role="form">
    1.  enctype="multipart/form-data"如果直接使用表单进行文件的上传,需要给表单加该属性,这样不会将目标文件的数据结构做修改后再上传
    2. 文件不同于字符串,字符串随意切割修改也能拼在一起,但文件不行)
  2.  确认<input type=“file” name=“file”>的type和name以及<input type=“submit” class=“btn btn-primary” value=“上传” />中的type
存在问题
  • 上传成功后,页面并没有展示头像
  • 默认大小的限制需要修改
  • 未解决问题:每次启动springboot都会新创建一个tomcat副本用于存储用户信息(包括头像),重新登陆该用户以后就已经新开了一个副本,肯定找不到上次上传的头像。就会显示无法展示。
前端优化
更改默认的文件大小限制

springmvc默认为1MB文件可以进行上传,如果刚好是1024*1024=1048576 bytes则会报代码错误。在控制层设置的public static final int AVATAR_MAX_SIZE = 10*1024*1024;需要在不超过原有大小的情况下才会起作用,要手动修改springmvc默认上传文件的大小。有两个方法:

  1. 直接在配置文件application.properties中进行配置
    1. spring.servlet.multipart.max-file-size=10MB(表示上传的文件最大是多大)

    2. spring.servlet.multipart.max-request-size=15MB(整个文件是放在了request中发送给服务器的,请求当中还会有消息头等其他携带的信息,这里设置请求最大为15MB

  2. 采用java代码的形式来设置文件的上传大小的限制
    @Bean
    public MultipartConfigElement getMultipartConfigElement() {
        //1.创建一个配置的工厂类对象
        MultipartConfigFactory factory = new MultipartConfigFactory();
    
        //2.设置需要创建的对象的相关信息
        factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES));
        factory.setMaxRequestSize(DataSize.of(15,DataUnit.MEGABYTES));
    
        //3.通过工厂类创建MultipartConfigElement对象
        return factory.createMultipartConfig();
    }
    
     
    1. 必须在主类StoreApplication中进行配置,因为主类是最早加载的,而配置文件必须是最早加载的

    2. 主类中定义一个方法,方法名无所谓,但方法需要用@bean修饰。表示该方法返回值是一个bean对象,也就是这个方法返回了一个对象;然后把该对象交给bean管理,类似spring中的bean标签。

    3. 用@Configuration修饰主类使@bean注解生效,但其实@SpringBootApplication是@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解的合并,所以可以不需要@Configuration

    4. 方法返回值是MultipartConfigElement类型,表示所要配置的目标的元素。

 上传后显示头像 + 登录后显示图像 + 显示最新的头像

在upload.html页面中通过ajax请求来提交文件,提交完成后返回了json串,解析出json串中的data数据设置到img标签的src属性上。

<script>
            $("#btn-change-avatar").click(function () {
                $.ajax({
                    url: "/users/change_avatar",
                    type: "POST",
                    data: new FormData($("#form-change-avatar")[0]),
                    processData: false,//处理数据的形式,关闭处理数据
                    contentType: false,//提交数据的形式,关闭默认提交数据的形式
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            alert("头像修改成功")
                            //将服务器端返回的头像地址设置到img标签的src属性上
                            //attr(属性,属性值)用来给某个属性设值
                            $("#img-avatar").attr("src",json.data);
                            //将每次新avatar从cookie中读取
                            $.cookie("avatar",json.data,{expires: 7});
                        } else {
                            alert("头像修改失败")
                        }
                    },
                    error: function (xhr) {
                        alert("修改头像时产生未知的异常!"+xhr.message);
                    }
                });
            });
			// // 读取avatar
            $(document).ready(function(){
                var avatar = $.cookie("avatar");
                console.log(avatar);//调试用
                $("#img-avatar").attr("src",avatar);
            })
</script>

注:

  • serialize():可以将表单数据自动拼接成key=value的结构提交给服务器,一般提交的是普通的控件类型中的数据,text/password/radio/checkbox等。文件类型不行。
  • FormData类:将表单中数据保持原有的结构进行数据提交.文件类型的数据可以使用FormData对象进行存储。
    • new FormData($(“form”)[0])
    • 将id="form"的表单的第一个元素的整体值作为创建FormData对象的数据
  • 虽然把文件的数据保护下来了,但是ajax默认处理数据时按照字符串的形式进行处理,默认会采用字符串的形式进行数据提交.手动关闭这两个功能:
    • processData: false,//处理数据的形式,关闭按照字符串的形式处理数据

    • contentType: false,//提交数据的形式,关闭默认按照字符串的形式提交数据

  • 将avatar保存在cookie中:每次检测到用户打开上传头像界面or登录时,在页面中通过ready()方法自动检测来读取cookie中的头像并设置到src上。
    • 导入cookie.js文件

      <script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>
      
    • 调用cookie方法保存路径

      $.cookie(key,value,time);//time单位:天
      

在login.html收集头像信息:

if (json.state == 200) {
                            alert("登录成功")
                            //跳转到系统主页index.html
                            //index和login在同一个目录结构下
							// 可以用相对路径index.html来确定跳转的页面,index.html和./index.html完全一样,
							// 因为./就是表示当前目录结构,也可以用../web/index.html
                            location.href = "index.html";
							// 将服务器端返回的头像设置到cookie中
                            $.cookie("avatar",json.data.avatar,{expires: 7});
                        } 

新增收货地址(DAY 6)

创建数据库表和实体

数据库:

use store;
CREATE TABLE t_address (
                           aid INT AUTO_INCREMENT COMMENT '收货地址id',
                           uid INT COMMENT '归属的用户id',
                           `name` VARCHAR(20) COMMENT '收货人姓名',
                           province_name VARCHAR(15) COMMENT '省-名称',
                           province_code CHAR(6) COMMENT '省-行政代号',
                           city_name VARCHAR(15) COMMENT '市-名称',
                           city_code CHAR(6) COMMENT '市-行政代号',
                           area_name VARCHAR(15) COMMENT '区-名称',
                           area_code CHAR(6) COMMENT '区-行政代号',
                           zip CHAR(6) COMMENT '邮政编码',
                           address VARCHAR(50) COMMENT '详细地址',
                           phone VARCHAR(20) COMMENT '手机',
                           tel VARCHAR(20) COMMENT '固话',
                           tag VARCHAR(6) COMMENT '标签',
                           is_default INT COMMENT '是否默认:0-不默认,1-默认',
                           created_user VARCHAR(20) COMMENT '创建人',
                           created_time DATETIME COMMENT '创建时间',
                           modified_user VARCHAR(20) COMMENT '修改人',
                           modified_time DATETIME COMMENT '修改时间',
                           PRIMARY KEY (aid)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

实体:

package com.liber.store.entity;

import java.util.Objects;

/**收货地址额实体类*/
public class Address extends BaseEntity {
    // 与数据库对应 但采用驼峰命名
    private Integer aid;
    private Integer uid;
    private String name;
    private String provinceName;
    private String provinceCode;
    private String cityName;
    private String cityCode;
    private String areaName;
    private String areaCode;
    private String zip;
    private String address;
    private String phone;
    private String tel;
    private String tag;
    private Integer isDefault;
    /*
     * get,set
     * equals和hashCode
     * toString
     */

    @Override
    public String toString() {
        return "Address{" +
                "aid=" + aid +
                ", uid=" + uid +
                ", name='" + name + '\'' +
                ", provinceName='" + provinceName + '\'' +
                ", provinceCode='" + provinceCode + '\'' +
                ", cityName='" + cityName + '\'' +
                ", cityCode='" + cityCode + '\'' +
                ", areaName='" + areaName + '\'' +
                ", areaCode='" + areaCode + '\'' +
                ", zip='" + zip + '\'' +
                ", address='" + address + '\'' +
                ", phone='" + phone + '\'' +
                ", tel='" + tel + '\'' +
                ", tag='" + tag + '\'' +
                ", isDefault=" + isDefault +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if (!super.equals(o)) return false;

        Address address1 = (Address) o;

        if (!Objects.equals(aid, address1.aid)) return false;
        if (!Objects.equals(uid, address1.uid)) return false;
        if (!Objects.equals(name, address1.name)) return false;
        if (!Objects.equals(provinceName, address1.provinceName))
            return false;
        if (!Objects.equals(provinceCode, address1.provinceCode))
            return false;
        if (!Objects.equals(cityName, address1.cityName)) return false;
        if (!Objects.equals(cityCode, address1.cityCode)) return false;
        if (!Objects.equals(areaName, address1.areaName)) return false;
        if (!Objects.equals(areaCode, address1.areaCode)) return false;
        if (!Objects.equals(zip, address1.zip)) return false;
        if (!Objects.equals(address, address1.address)) return false;
        if (!Objects.equals(phone, address1.phone)) return false;
        if (!Objects.equals(tel, address1.tel)) return false;
        if (!Objects.equals(tag, address1.tag)) return false;
        return Objects.equals(isDefault, address1.isDefault);
    }

    @Override
    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + (aid != null ? aid.hashCode() : 0);
        result = 31 * result + (uid != null ? uid.hashCode() : 0);
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + (provinceName != null ? provinceName.hashCode() : 0);
        result = 31 * result + (provinceCode != null ? provinceCode.hashCode() : 0);
        result = 31 * result + (cityName != null ? cityName.hashCode() : 0);
        result = 31 * result + (cityCode != null ? cityCode.hashCode() : 0);
        result = 31 * result + (areaName != null ? areaName.hashCode() : 0);
        result = 31 * result + (areaCode != null ? areaCode.hashCode() : 0);
        result = 31 * result + (zip != null ? zip.hashCode() : 0);
        result = 31 * result + (address != null ? address.hashCode() : 0);
        result = 31 * result + (phone != null ? phone.hashCode() : 0);
        result = 31 * result + (tel != null ? tel.hashCode() : 0);
        result = 31 * result + (tag != null ? tag.hashCode() : 0);
        result = 31 * result + (isDefault != null ? isDefault.hashCode() : 0);
        return result;
    }

    public Integer getAid() {
        return aid;
    }

    public void setAid(Integer aid) {
        this.aid = aid;
    }

    public Integer getUid() {
        return uid;
    }

    public void setUid(Integer uid) {
        this.uid = uid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getProvinceName() {
        return provinceName;
    }

    public void setProvinceName(String provinceName) {
        this.provinceName = provinceName;
    }

    public String getProvinceCode() {
        return provinceCode;
    }

    public void setProvinceCode(String provinceCode) {
        this.provinceCode = provinceCode;
    }

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }

    public String getCityCode() {
        return cityCode;
    }

    public void setCityCode(String cityCode) {
        this.cityCode = cityCode;
    }

    public String getAreaName() {
        return areaName;
    }

    public void setAreaName(String areaName) {
        this.areaName = areaName;
    }

    public String getAreaCode() {
        return areaCode;
    }

    public void setAreaCode(String areaCode) {
        this.areaCode = areaCode;
    }

    public String getZip() {
        return zip;
    }

    public void setZip(String zip) {
        this.zip = zip;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getTel() {
        return tel;
    }

    public void setTel(String tel) {
        this.tel = tel;
    }

    public String getTag() {
        return tag;
    }

    public void setTag(String tag) {
        this.tag = tag;
    }

    public Integer getIsDefault() {
        return isDefault;
    }

    public void setIsDefault(Integer isDefault) {
        this.isDefault = isDefault;
    }
}

或者用注释:

  • @Data:表示set和get
  • @Setter @Getter
  • @Constructor @AllConstructor:表示构造方法

持久层

各功能开发顺序
  • 列表展示
  • 修改
  • 删除
  • 设置默认
  • 新增

开发顺序:新增→列表展示→设置默认→删除→修改

规划SQL语句
  • 新增收货地址:
insert into t_address (aid以外的所有字段) values (字段值)
  •  规定一个用户的收货地址数量,规定最多20个(超出的话,抛出一个逻辑控制异常,在serve层抛出并在controller捕获):
select count(*) from t_address where uid=?
实现接口和抽象方法

创建接口AddressMapper:

package com.liber.store.mapper;

import com.liber.store.entity.Address;

public interface AddressMapper {
    /**
     * 插入用户的收货地址数据
     * @param address 收货地址数据
     * @return 受影响的行数
     */
    Integer insert (Address address);

    /**
     * 根据用户的uid统计收货地址数量
     * @param uid 用户的uid
     * @return 当前用户的收货地址总数
     */
    Integer countByUid(Integer uid);

}
编写映射

一个接口只能映射一个文件,namespace只能接受一个对象。

创建AddressMapper.xml映射文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace用于指定当前的映射文件和哪个接口进行映射,需要指定接口的文件路径,路径需要是包的完整路径结构-->
<mapper namespace="com.liber.store.mapper.AddressMapper">

    <resultMap id="AddressEntityMap" type="com.liber.store.entity.Address">
        <!--将表的字段和类的属性名不一致的进行匹配指定,名称一致的也可以指定,但没必要;但是,在定义映射规则时 无论主键名称是否一致都 不能省
        column属性:表示表中的字段名称
        property属性:表示类中的属性名称-->

        <id column="aid" property="aid"/>
        <result column="province_name" property="isDelete"/>
        <result column="province_code" property="createdUser"/>
        <result column="city_name" property="createdTime"/>
        <result column="city_code" property="modifiedUser"/>
        <result column="area_name" property="modifiedTime"/>
        <result column="area_code" property="modifiedTime"/>
        <result column="is_default" property="isDefault"/>
        <result column="created_user" property="createdUser"/>
        <result column="created_time" property="createdTime"/>
        <result column="modified_user" property="modifiedUser"/>
        <result column="modified_time" property="modifiedTime"/>
    </resultMap>
<!--  计算当前地址是否超过限制数目  -->
    <select id="countByUid" resultType="java.lang.Integer">
        select count(*) from t_address where uid=#{uid}
    </select>
<!--  新增地址  -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="aid">  <!-- 对主键aid开启自增 -->
        insert into t_address (uid, `name`, province_name, province_code,
                               city_name, city_code, area_name, area_code,
                               zip, address, phone, tel, tag, is_default,
                               created_user, created_time, modified_user, modified_time
                               ) values (
                                        #{uid}, #{name}, #{provinceName}, #{provinceCode},
                                         #{cityName}, #{cityCode}, #{areaName},#{areaCode},
                                         #{zip}, #{address}, #{phone}, #{tel}, #{tag}, #{isDefault},
                                         #{createdUser},#{createdTime}, #{modifiedUser}, #{modifiedTime}
                                                )
    </insert>
</mapper>
单元测试 

创建AddressMapperTests测试类:

package com.liber.store.mapper;

import com.liber.store.entity.Address;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

// 表示标注当前的类是测试类 不会随同项目一同打包发送
@SpringBootTest
// 表示启动单元测试类,需要传参SpringRunner的实例类型
@RunWith(SpringRunner.class)

@MapperScan("com.liber.store.mapper")
public class AddressMapperTests {
    @Autowired
    private AddressMapper addressMapper;

    @Test
    public void insert(){
        Address address = new Address();
        address.setUid(11);
        address.setPhone("133336");
        address.setName("女朋友");
        Integer insert = addressMapper.insert(address);
        System.out.println(insert);
    }

    @Test
    public void countByUid(){
        Integer integer = addressMapper.countByUid(11);
        System.out.println(integer);
    }
}

业务层(DAY 7)

异常
  • 插入数据时用户不存在(被管理员误删等等),抛UsernameNotFoundException异常(已经有了)
  • 当用户插入的地址是第一条时,需要将当前地址作为默认收货地址:如果count用户的地址数量为0则将当前地址设为默认。
  • 查询的结果>=20,抛出业务异常AddressCountLimitException
设计接口和抽象方法
  • 创建一个IAddressService接口,在接口中定义业务的抽象方法
    • 看mapper层方法是依赖关系还是独立关系,如果某一个抽象方法依赖于另一个抽象方法,那就需要在业务层将这两个方法整合到一个方法中.一句话来说就是:一个功能模块可能需要多条sql语句
      package com.liber.store.service;
      
      import com.liber.store.entity.Address;
      
      /**收货地址的业务层接口*/
      
      public interface IAddressService {
          /**
           *这三个参数的由来:
           * 1.首先肯定要有address
           * 2.业务层需要根据uid查询该用户收货地址总数及新建地址时给字段uid赋值
           * 但新建收货地址的表单中并没有哪个控件让输入用户uid,所以需要控制层将uid传给业务层
           * 3.业务层在创建/修改收货地址时需要同时修改数据库中创建人/修改人的字段
           * 但新建收货地址的表单中并没有哪个控件让输入用户username,所以需要控制层将username传给业务层
           * 注意:
           * 可以用HttpSession session代替Integer uid, String username,
           * 但这样写的话就需要把BaseController类下获取uid,username的方法重新封装到一个类中
           * 并让IAddressServiceImp实现类继承该类,这样就需要微调一下代码逻辑
           * 并且最好每一层只处理该层需要做的事情,
           * session对象是控制层传递的
           *
           * 把session对象定义封装在控制层中,不需要在业务层中额外处理以降低耦合
           */
          void addNewAddress(Integer uid, String username, Address address);
      }
      
  •  抽象方法实现
    package com.liber.store.service.Impl;
    
    import com.liber.store.entity.Address;
    import com.liber.store.entity.User;
    import com.liber.store.mapper.AddressMapper;
    import com.liber.store.mapper.UserMapper;
    import com.liber.store.service.Ex.*;
    import com.liber.store.service.IAddressService;
    import com.liber.store.service.IUserService;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.util.DigestUtils;
    
    import java.util.Date;
    import java.util.UUID;
    
    // 将当前类的对象交给spring管理,能自动创建对象并维护
    @Service
    @MapperScan("com.liber.store.mapper")
    public class AddressServiceImpl implements IAddressService {
    
        @Autowired
        private AddressMapper addressMapper;
        @Autowired
        private UserMapper userMapper;
    //    为了方便日后修改最大收货地址数量,可以在配置文件
    //    application.properties中定义user.address.max-count=20
        @Value("${user.address.max-count}")
        private Integer maxCount;
        @Override
        public void addNewAddress(Integer uid, String username, Address address) {
            // 判断执行插入地址的用户是否存在
            User result = userMapper.findByUid(uid);
            if (result ==null || result.getIsDelete() == 1) {
                throw new UsernameNotFoundException("用户数据不存在");
            }
            Integer count = addressMapper.countByUid(uid);
            if (count >= maxCount){
                throw new AddressCountLimitException("地址数量到达限制数量!");
            }
            address.setUid(uid);
            // 是否是默认地址
            Integer isDefault = (count == 0 ? 1 : 0);//1表示默认收货地址,0反之
            address.setIsDefault(isDefault);
            //补全四项日志
            address.setCreatedUser(username);
            address.setModifiedUser(username);
            address.setCreatedTime(new Date());
            address.setModifiedTime(new Date());
            // 插入收货地址
            Integer insert = addressMapper.insert(address);
            if (insert != 1){
                throw  new InsertException("插入地址失败!");
            }
    
    
        }
    }
    

单元测试

创建AddressServiceTests :

package com.liber.store.service;


import com.liber.store.entity.Address;
import com.liber.store.entity.User;
import com.liber.store.service.Ex.ServiceException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Date;


// 表示标注当前的类是测试类 不会随同项目一同打包发送
@SpringBootTest
// 表示启动单元测试类,需要传参SpringRunner的实例类型
@RunWith(SpringRunner.class)

@MapperScan("com.liber.store.mapper")
public class AddressServiceTests {
    /*
     单元测试类必须被@test注释修饰
     返回值类型为void
     方法没有参数
     方法的返回修饰符必须为public

     这样单元测试类才可以单独独立运行 不用启动整个项目 提升代码测试效率
     */

    @Autowired()
//    无法自动装配。找不到 'UserMapper' 类型的 Bean。
//    报错是接口不能直接创建bean
    private IAddressService addressService;

   @Test
    public void addNewAddress(){
       Address address = new Address();
       address.setIsDefault(1);
       address.setAddress("nanjing");
       address.setPhone("1234567");
       address.setName("男朋友");
       addressService.addNewAddress(11, "lili",address);
   }
}

控制层

处理异常
else if (e instanceof AddressCountLimitException) {
            result.setState(4003);
            result.setMessage("AddressCount over.");
        }
设计请求
  • 请求路径:/users/add_new_address
  • 请求参数(要和前端表单属性值保持一致): Address address, HttpSession session(用于获取uid和username)
  • 请求类型:POST(GET请求提交数据只有2KB左右)
  • 响应结果:JsonResult<Void>
处理请求
package com.liber.store.controller;

import com.liber.store.entity.Address;
import com.liber.store.service.IAddressService;
import com.liber.store.util.JsonResult;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
//@Controller + @ResponseBody = @RestController
@RequestMapping("users")
public class AddressController extends BaseController{
    @Autowired
    private IAddressService addressService;

    @RequestMapping("add_new_address")
    public JsonResult<Void> addNewAddress(Address address, HttpSession session){
        Integer uid = getUidFromSession(session);
        String username = getUsernameFromSession(session);
        addressService.addNewAddress(uid,username,address);
        return new JsonResult<>(SUCCESS);
    }
}
测试

启动服务,先登录账号然后在地址栏输入localhost:8080/users/add_new_address?name=tom&phone=12345664观察状态码是否为200

前端 
 <script>
            $("#btn-add-new-address").click(function () {
                $.ajax({
                    url: "/addresses/add_new_address",
                    type: "POST",
                    data: $("#form-add-new-address").serialize(),
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            alert("新增收货地址成功")
                        } else {
                            alert("新增收货地址失败")
                        }
                    },
                    error: function (xhr) {
                        alert("新增收货地址时产生未知的异常!"+xhr.message);
                    }
                });
            });
 </script>

注:其余见使用springboot搭建网上商城项目(二)

;