静态资源及sql文件分享
链接:https://pan.baidu.com/s/1X-yjmQcPD3PqS21x0HplNA?pwd=23gr
提取码:23gr

项目完整代码分享
链接:https://pan.baidu.com/s/1qcJdPeXas7HPXgMhlXO7Aw?pwd=uzic
提取码:uzic

文章目录

项目环境搭建

1.项目分析

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

  2. 开发顺序:注册,登录,用户管理,购物车,商品,订单模块

  3. 某一个模块的开发顺序:

    • 持久层开发:依据前端页面的设置规划相关的SQL语句,以及进行配置

    • 业务层开发:核心功能控制,业务操作以及异常的处理

    • 控制层开发:接收请求,处理响应

    • 前端开发:JS,Query,AJAX这些技术来连接后台

2.项目基本环境

  1. JDK:1.8版本及以上
  2. maven:需要配置到idea,3.6.1版本及以上
  3. 数据库:MariaDB,MySQL,要求是5.1版本及以上
  4. 开发的平台:idea开发

  1. 项目名称:store,表示商城
  2. 结构:com.cy.store
//三个基础jar包
java web
mybatis
mysql driver


勾选上述

  1. 资源文件:resources文件夹下(static,templates)
  2. 单元测试:test.com.cy.store

3.项目创建

  1. Create New Project->

  2. 选择Spring Initializr,点击next

  3. 跳转到Project Metadata页面,该页面的Group填写域com和自己起的域名cy(即com.cy)==;Artifact填写项目名store;==Java Version版本选择自己安装的版本,点击next

  4. 选择需要导入的jar包:

    • 前后端的连接jar包:Web目录下的Spring web
    • mybatis的jar包:SQL目录下的Mybatis Framework
    • mysql数据库的驱动:SQL目录下的MySQL Driver
  5. 创建一个数据库

    create database store character set utf8;
    
  6. 在application.properties文件中配置数据库的连接源信息

    spring.datasource.url=jdbc:mysql://localhost:3306/store?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/shanghai
    spring.datasource.username=root
    spring.datasource.password=root

4.项目测试

4.1测试能否成功连接数据库

  • 启动Springboot主类,看idea中是否有对应的spring图形输出

  • 若idea有对应的spring图形输出开始第二步测试:
    在单元测试中测试数据库的连接是否可以加载:

    @SpringBootTest
    class StoreApplicationTests {
    
        @Autowired //自动装配
        private DataSource dataSource;
    
        @Test
        void contextLoads() {
        }
    
        @Test
        void getConnection() throws SQLException {
            System.out.println(dataSource.getConnection());
        }
    }
    

    运行getConnection方法,若成功返回HikariProxyConnection@189194499 wrapping com.mysql.cj.jdbc.ConnectionImpl@2b0e9f30则说明成功连接数据库,其中Hikari是一个连接池,用来管理数据库的连接对象,是springboot默认内部整合的连接池,该连接池号称世界上最快的连接池,底层仍然采用c3p0来管理数据库的连接对象

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

将静态资源(SpringBoot电脑商城项目-V1.0\tools\pages_src\pages*)
复制到static目录下重启项目
并尝试访问localhost:8080/web/login.html
(因为static是默认根目录,所以不是localhost:8080/static/web/login.html)

如果这个过程访问失败,原因是idea对于JS代码的兼容性较差,编写了js代码但是有的时候不能正常去加载,解决办法有以下四种

  • clear-install:依次点击MavenProject->store->Lifecycle->clean,等待清哩项目完毕后点击同目录下的install重新部署

  • idea缓存清理:点击File下的Invalidate Caches/Restart…然后在弹出的窗口中选择Invalidate and Restart,此时就会自动清除缓存并重新启动idea

  • rebuild重新构建:点击工具栏的Build下的Rebuild Project

  • 重启电脑

用户注册功能

1.创建数据表

1.选中数据表:

use store

2.创建t_user表

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;

tips:

  • 注册页面的确认密码功能多数开发中交给前端做,如果两次密码输入不同就不能将数据传给后台

  • 创建t-user表时`password` CHAR(32) NOT NULL COMMENT ‘密码’,因为password是关键字,所以需要用``号(不是单引号,是esc下面的那个键)并且后面用到该字段时(比如往表中插入数据)也需要用``

  • 创建t_user表时salt CHAR(36) COMMENT ‘盐值’,是为了在用户注册时对用户的密码进行加密操作(后续再讲)

  • 数据库中的性别0代表女,1代表男,数据库中用数字而不是文字是因为前端的性别选项是单选框,提交给后台的是数字

  • 创建t_user表时is_delete INT COMMENT ‘是否删除:0-未删除,1-已删除’,的作用:网站中都有注销账号的功能,大部分的网站并不是真的将用户注销了,而是在下次用户登录时进行验证,如果是0就放行,如果是1就禁止登录

  • 创建t-user表时username VARCHAR(20) NOT NULL UNIQUE COMMENT ‘用户名’,的UNIQUE 作为约束条件使用户名唯一

  • 将来任何一张表都有以下四个字段:

    created_user VARCHAR(20) COMMENT ‘创建人’,

    created_time DATETIME COMMENT ‘创建时间’,

    modified_user VARCHAR(20) COMMENT ‘修改人’,

    modified_time DATETIME COMMENT ‘修改时间’,

    所以为了开发方便可以把这四个字段作为整个实体类

2.创建用户的实体类

1.通过表的结构提取出表的公共字段,放在一个实体类的基类中,起名BaseEntity基类中

public class BaseEntity implements Serializable {
    private String createdUser;
    private Date createdTime;
    private String modifiedUser;
    private Date emodifiedTime;
/**
 * get,set
 * equals和hashCode
 * toString
 */
}

2.创建用户的实体类,并使其继承BaseEntity基类

public class User extends BaseEntity {
    private Integer uid;
    private String username;
    private String PASSWORD;
    private String salt;
    private String phone;
    private String email;
    private Integer gender;
    private String avatar;
    private Integer isDelete;
/**
 * get,set
 * equals和hashCode
 * toString
 */
}

tips:

  • 实体类User因为要在网络中以流的形式传输,所以需要serialize序列化
    (但因为其继承的父类BaseEntity已经实现序列化,所以就不需要再写implements Serializable)

  • 实体类BaseEntity中自动导入Getter and Setter方法,euqals()方法,hashCode()方法,toString方法,其中euqals()方法,hashCode()方法自动导入步骤:

    1. enter+insert
    2. 点击euqals() and hashCode()
    3. 勾选Accept…和Use这两段话,并且选择Template为IntelliJ Default
    4. 一路next到底
  • ssm框架开发项目的时候
    需要在实体类上面加@Component然后spring才能自动进行对象的创建维护,而springboot不再需要,因为springboot遵循的原则是约定大于配置,如果字段名称相同那就可以自动完成字段的初始化

3.注册-持久层

通过Mybatis来操作数据库,也就是在做mybatis开发的流程

3.1规划需要执行的SQL语句

`1.用户的注册功能,从后端持久层来看相当于在做数据的插入操作

inser into t_user (username, password) values(值列表)

`2.在用户的注册时首先要去查询当前的用户名是否存在,如果存在则不能进行注册,相当于是一条查询语句

select * from t_user where username=?

3.2设计接口和抽象方法及实现

  1. 定义Mapper接口.在项目的目录结构下首先创建一个mapper包,在这个包下再根据不同的功能模块来创建mapper接口.注册功能需要在mapper包下创建UserMapper接口然后定义上述两个SQL语句的抽象方法
public interface UserMapper {

    /**
     * 插入用户的数据
     * @param user 用户的数据
     * @return 受影响的行数(增删改都将受影响的行数作为返回值,可以根据返回值来判断是否执行成功)
     */
    Integer insert(User user);

    /**
     * 根据用户名来查询用户的数据
     * @param username 用户名
     * @return 如果找到对应的用户则返回这个用户的数据,如果没有找到则返回null
     */
    User findByUsername(String username);
}
  1. ssm框架开发项目的时候需要在mapper接口上加@Mapper用于自动生成相应的接口实现类,在springboot也可以这样,但是后续会有很多mapper接口,每个接口分别加@Mapper太麻烦了,所以在启动类类里面指定当前项目的mapper接口在哪,然后项目启动的时候会自动加载所有的接口
@MapperScan("com.cy.mapper")

3.3编写映射

  1. 定义xml映射文件,与对应的接口进行关联.所有的映射文件都属于资源文件,需要放在resources目录下,为了管理方便我们在resources目录下创建一个mapper文件夹,然后在这个文件夹里存放mapper的映射文件

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

UserMapper.xml的配置在Mybatis官网

<?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.cy.store.mapper.UserMapper">

</mapper>
  1. 将配置接口的方法对应到SQL语句上
  • insert into () values (),因为values后面插入的值是动态值,mybatis规定需要用占位符来占位,并给占位符起一个变量的名字,且变量的名字需要在占位符#{}内部
  • 创建t_user表时uid INT AUTO_INCREMENT COMMENT ‘用户id’,中的AUTO_INCREMENT表示主键uid自增,所以需要useGeneratedKeys和keyProperty
<!--在sql语句的最上面借助ResultMap标签来自定义映射规则
    id属性:表示给这个映射规则分配一个唯一的id值,对应的就是resultMap="id属性值"
    type属性:取值是一个类,表示数据库中的查询结果与java中哪个实体类进行结果集的映射
 -->
<resultMap id="UserEntityMap" type="com.cy.store.entity.User">
    <!--将表的字段和类的属性名不一致的进行匹配指定,名称一致的也可以指定,但没必要
        但是,在定义映射规则时无论主键名称是否一致都不能省
        column属性:表示表中的字段名称
        property属性:表示类中的属性名称
        -->
    <id column="uid" property="uid"></id>
    <result column="is_delete" property="isDelete"></result>
    <result column="created_user" property="createdUser"></result>
    <result column="created_time" property="createdTime"></result>
    <result column="modified_user" property="modifiedUser"></result>
    <result column="modified_time" property="modifiedTime"></result>
</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>

sql语句匹配规则:如果在insert标签里面写了insert语句,首先将insert语句和某一个方法进行绑定,用到了id=“”,但是和哪里的方法进行绑定呢,就要用到namespace=“”,这两步映射就把唯一的SQL语句和唯一的方法进行了关联,实际上就是jdbc里面dao接口的的:

Integer insert(User user) {
    String SQL = "insert into () values ()";
}

用到映射的好处:使SQL语句和java代码分离,解耦了,方便后期代码的维护

4.将mapper文件的位置注册到properties对应的配置文件中.

在application.properties文件中增添:

mybatis.mapper-locations=classpath:mapper/*.xml

3.4单元测试

  1. 每个独立的层编写完毕后需要编写单元测试方法来测试当前的功能:在test包结构下创建一个mapper包,在这个包下再创建持久层的功能测试,单元测试方法是独立运行,不用启动整个项目,提高了代码的测试效率

  2. 因为测试方法要用到mapper层的接口来访问刚刚写的两个方法,所以要在类里面声明UserMapper对象:即private UserMapper userMapper;且需要加上@Autowired完成值的初始化,但此时会发现提示"Could not autowire.No beans of’UserMapper’type found",报错原因是idea有自动检测的功能,在java中接口是不能够直接创建bean的,所以idea认为这个语法不合理,但本质上在项目启动时mybatis自动创建了接口的动态代理实现类,所以从项目的运行角度讲这不能算是错.解决办法:

  • 在Settings里面搜索inspections,依次找到Spring->Spring Core->Code->Autowiring for Bean Class然后将Severity的Error改为Warning
//@SpringBootTest表示当前的类是一个测试类,不会随同项目一块打包
@SpringBootTest

/**
 * 1.@RunWith表示启动这个单元测试类,否则这个单元测试类是不能运行的,需要传递
 * 一个参数,该参数必须是SpringRunner.class即SpringRunner的实例类型
 * 2.敲完@RunWith(SpringRunner.class)后鼠标分别放在SpringRunner和@RunWith上按alt+enter分别导入包
 * 3.单元测试类中出现的方法必须是单元测试方法
 * 4.单元测试方法的特点:必须被@Test注解修饰;返回值类型必须是void;方法的参数列表不指定任何类型;方法的访问修饰符必须是public
 */
@RunWith(SpringRunner.class)
public class UserMapperTests {

    @Autowired
    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);
    }

    @Test
    public void findByUsername() {
        User user = userMapper.findByUsername("张三");
        System.out.println(user);
    }
}

4.注册-业务层

业务层的核心功能:

  • 接受前端从控制器流转过来的数据
  • 结合真实的注册业务来完成功能业务逻辑的调转和流程

所以这里要考虑到真实的业务场景,如果只考虑业务场景的话不完整,因为在整个业务执行的过程中会产生很多问题,从java角度来讲这些都是属于异常,所以在业务开发的时候就要规划相关的异常,以便把项目的错误控制在一定范围内

service下的目录结构(建议这样):

  • service包下创建ex包用来写异常类
  • service包下创建impl包用来写接口的实现类
  • 接口直接写在service包下,不再需要接口包

4.1规划异常

1.为什么会有异常:

比如,用户在进行注册时可能会产生用户名被占用的错误,这时需要抛出一个异常

2.怎么处理异常:

  • 异常不能用RuntimeException,太笼统了,开发者没办法第一时间定位到具体的错误类型上,我们可以定义具体的异常类型来继承这个异常.
  • 正常的开发中异常又要分等级,可能是在业务层产生异常,可能是在控制层产生异常,所以可以创建一个业务层异常的基类,起名ServiceException异常,并使其继承RuntimeException异常
  • 后期开发业务层时具体的异常可以再继承业务层的异常ServiceException

3.处理异常的具体步骤:

步骤一:在ex包下创建ServiceException类作为业务层异常的基类:

/**
 * 因为整个业务的异常只有一种情况下才会产生:只有运行时才会产生,不运行不会产生
 * 所以要求业务层的异常都要继承运行时异常RuntimeException并且重写父类的所有构造方法以便后期能抛出自已定义的异常
 */
public class ServiceException extends RuntimeException{
    //什么也不返回
    public ServiceException() {
        super();
    }

    //返回异常信息(常用)
    public ServiceException(String message) {
        super(message);
    }

    //返回异常信息和异常对象(常用)
    public ServiceException(String message, Throwable cause) {
        super(message, cause);
    }

    public ServiceException(Throwable cause) {
        super(cause);
    }

    protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

根据业务层不同功能来定义具体异常类型,统一继承ServiceException异常类

步骤二:后期再根据业务层不同的功能来详细定义具体的异常类型,并统一的继承ServiceException异常基类:

  • 用户在进行注册时可能会产生用户名被占用的错误,这时需要抛出一个UsernameDuplicatedException异常
public class UsernameDuplicatedException extends ServiceException{
    /**重写ServiceException的所有构造方法*/
}

  • 正在执行数据插入操作的时候,服务器宕机或数据库宕机.这种情况是处于正在执行插入的过程中所产生的异常,起名InsertException异常
//数据插入过程中产生异常
public class InsertException extends ServiceException{
    /**重写ServiceException的所有构造方法*/
}

4.2设计接口和抽象方法

1.在service包下创建IUserService接口(接口命名的默认规则:I+业务名字+层的名字)

/**用户模块业务层接口*/
public interface IUserService {
    /**
     * 用户注册方法
     * @param user 用户的数据对象
     */
    void reg(User user);
}

2.创建一个实现UserServiceImpl类,需要实现IUserService接口,并且实现抽象的方法

因为要将这个实现类交给spring管理,所以需要在类上加@Service

@Service
public class UserServiceImpl implements IUserService {

    //reg方法核心就是调用mapper层的方法,所以要声明UserMapper对象并加@Autowired注解
    @Autowired
    private UserMapper userMapper;

    @Override
    public void reg(User user) {
        //通过user参数来获取传递过来的username
        String username = user.getUsername();
        //调用mapper的findByUsername(username)判断用户是否被注册过了
        User result = userMapper.findByUsername(username);
        //判断结果集是否为null,不为null的话则需抛出用户名被占用的异常
        if (result != null) {
            //抛出异常
            throw new UsernameDuplicatedException("用户名被占用");
        }

        /**
         * 密码加密处理作用:
         * 1.后端不再能直接看到用户的密码2.忽略了密码原来的强度,提升了数据安全性
         * 密码加密处理的实现:
         * 串+password+串->交给md5算法连续加密三次
         * 串就是数据库字段中的盐值,是一个随机字符串
         */
        String oldpassword = user.getPassword();
        //1.随机生成一个盐值(大写的随机字符串)
        String salt = UUID.randomUUID().toString().toUpperCase();
        //2.将密码和盐值作为一个整体进行加密处理
        String md5Password = getMD5Password(oldpassword, salt);
        //3.将盐值保存到数据库
        user.setSalt(salt);
        //4.将加密之后的密码重新补全设置到user对象当中
        user.setPassword(md5Password);

        //补全数据:is_delete设置为0
        user.setIsDelete(0);
        //补全数据:四个日志字段信息
        user.setCreatedUser(user.getUsername());
        user.setModifiedUser(user.getUsername());
        Date date = new Date();//java.util.Date
        user.setCreatedTime(date);
        user.setModifiedTime(date);


        //执行注册业务功能的实现
        Integer rows = userMapper.insert(user);
        if (rows != 1) {
            throw new InsertException("在用户注册过程中产生了未知的异常");
        }
    }
}

md5加密算法以后可能还要多次用到,为了方便在UserServiceImpl类里面单独写一个getMD5Password方法

private String getMD5Password(String password,String salt) {
    for (int i = 0; i < 3; i++) {
        password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase();
    }
    return password;
}

4.3单元测试

在单元测试包下创建一个UserServiceTests类,在这个类中添加单元测试的功能(技巧:可以先在test.com.cy.store下创建service包,然后点击UserMapperTests并ctrl+c,然后点击service包ctrl+v会弹出修改类名,将UserMapperTests改为UserServiceTests即可,然后修改部分代码至如下这样)

@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTests {

    @Autowired
    private IUserService userService;

    @Test
    public void reg() {
        /**
         * 进行插入时可能会出错抛出异常,这时需要捕获异常:
         * 1.选中    User user = new User();
         *           user.setUsername("张7");
         *           user.setPassword("123456");
         *           userService.reg(user);
         *           System.out.println("OK");
         * 2.点击导航栏的Code,然后依次点击SurroundWith->try/catch就可以捕获异常了
         * 3.Exception e没有问题,但这里我们知道是Service层的异常,所以可以改为ServiceException e
         * 4.System.out.println(e.getClass().getSimpleName());获取异常对象再获取类的名称然后输出
         * 5.System.out.println(e.getMessage());输出异常信息(是自己在ServiceException的子类类具体设置的信息)
         */
        try {
            User user = new User();
            user.setUsername("张7");
            user.setPassword("123456");
            userService.reg(user);
            System.out.println("OK");
        } catch (ServiceException e) {
            System.out.println(e.getClass().getSimpleName());
            System.out.println(e.getMessage());
        }
    }
}

5.注册-控制层

5.1创建响应

状态码,状态描述信息,数据是所有控制层对应的方法都涉及到的操作,所以把这部分功能封装到一个类JsonResult中,将这个类作为方法的返回值返回给前端浏览器:

//因为所有的响应的结果都采用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();
    }
    /**以及属性的get和set方法*/
}

5.2设计请求

接下来该向后端服务器发送请求以把用户数据插入到数据库,设计发送请求模块的第一步就是设计相关的请求

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

  • 请求路径:/users/reg
  • 请求参数:User user
  • 请求类型:POST
  • 响应结果:`JsonResult

5.3处理请求

创建一个控制层对应的UserController类,依赖于业务层的接口.编写完成后启动主服务验证一下

@RestController //其作用等同于@Controller+@ResponseBody
//@Controller
@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方法时可能出现异常,所以需要捕获异常
            userService.reg(user);
            result.setState(200);
            result.setMessage("用户注册成功");
        } catch (UsernameDuplicatedException e) {
            result.setState(4000);
            result.setMessage("用户名被占用");
        } catch (InsertException e) {
            result.setState(5000);
            result.setMessage("注册时产生未知的异常");
        }
        return result;
    }
}

5.4控制层优化设计

凡是业务层抛出的异常我们都在控制层进行了捕获,如果其他的业务模块也抛用户名被占用或者插入时异常,那么抛出异常的代码就要重复编写

优化方法:在控制层抽离出一个BaseController父类,在这个父类中统一处理关于异常的相关操作,优化如下:

1.在controller包下创建UserController类作为控制层下类的基类,用来做统一的异常捕获:

public class BaseController {

    //操作成功的状态码
    public static final int OK = 200;

    /**
     * 1.@ExceptionHandler表示该方法用于处理捕获抛出的异常
     * 2.什么样的异常才会被这个方法处理呢?所以需要ServiceException.class,这样的话只要是抛出ServiceException异常就会被拦截到handleException方法,此时handleException方法就是请求处理方法,返回值就是需要传递给前端的数据
     * 3.被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("用户名已经被占用");
        } else if (e instanceof InsertException) {
            result.setState(5000);
            result.setMessage("插入数据时产生未知的异常");
        }
        return result;
    }
}

2.让UserController继承BaseController并重构UserController下的reg方法使该方法只需要关注请求处理而不再需要关注异常捕获:

public JsonResult<Void> reg(User user) {
    userService.reg(user);
    return new JsonResult<>(OK);
}

6.注册-前端页面

6.1熟悉ajax

1.什么是ajax函数?

这是jQuery封装的一个函数,称为$.ajax()函数,
通过对象调用ajax()函数用来异步加载相关的请求.依靠的是JavaScript提供的一个对象:XHR(全称XmlHttpResponse)

2.ajax()函数的语法结构:

  • 使用ajax()时需要传递一个方法体作为方法的参数来使用(一对大括号就是一个方法体)
  • ajax接受多个参数时,参数与参数之间使用","分割
  • 每一组参数之间使用":"进行分割
  • 参数的组成部分一个是参数的名称(不能随便定义),另一个是参数的值(必须用字符串来表示)
  • 参数的声明顺序没有要求

演示一下语法结构:

$.ajax({
    url: "",
    type: "",
    data: "",
    dataType: "",
    success: function() {
        
    },
    error: function() {
        
    }
});
  1. ajax()函数参数的含义:
参数 功能描述
url 表示请求的地址(url地址),例如:url:“localhost:8080/users/reg”(1.不能包含参数列表部分的内容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参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上

6.2前端js编写

js代码可以独立声明在一个js文件里或者声明在一个script标签中.现在我们在register.html中编写js代码,js代码可以放在head标签中,也可以放在body标签中,可以放在任意一个位置,只要被script标签包裹就行了,这里我们放在整个body结束之前:

        <script>
            //1.监听注册按钮是否被点击,如果被点击可以执行一个方法(这里不能像ajax函数那样删去function()只留下{},这是官方规定的!)
            $("#btn-reg").click(function () {

                //let username = $("#username").val();
                //let pwd = $("#password").val();
                //上面这两行是动态获取表单中控件的数据,但是如果这样获取的话ajax函数中
                //就是data: "username="+username + "&password="+pwd,但太麻烦了,如
                // 果这个表单提交的是用户的兴趣爱好,那数据就很多了,一个表单20个数据都很正
                // 常,如果此时还用这种方式就太麻烦了,所以不建议用这种方式

                //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: 4000,message: "用户名
                        // 已经被占用",data: null}
                        if (json.state == 200) {
                            alert("注册成功")
                        } else {
                            alert("注册失败")
                        }
                    },
                    error: function (xhr) { //如果问题不在可控范围内,服务器就不会返回自己定
                        //义的json字符串:{state: 4000,message: "用户名已经被占用",data: null}
                        //而是返回一个XHR类型的对象,该对象也有一个状态码名字是status
                        alert("注册时产生未知的错误!"+xhr.status);
                    }
                });
            });
        </script>

此时可能会出现点击注册提交表单时没有任何响应,原因是idea对于JS代码的兼容性较差,编写了js代码但是有的时候不能正常去加载,解决办法有四种,同前面的:项目环境搭建->项目测试->测试静态资源能否正常加载

包括以后如果修改了前端页面,测试时没有报错也没有按照预想的响应,就考虑是编写的js代码还没有被加载,尝试用这四种方法解决

用户登录功能

先分析一下思路:当用户输入用户名和密码将数据提交给后台数据库进行查询,如果存在对应的用户名和密码则表示登录成功,登录成功之后跳转到系统的主页就是index.html页面,跳转在前端使用jQuery来完成

1.登录-持久层

规划需要执行的SQL语句

依据用户提交的用户名来做select查询

select \* from t\_user where username=? and password=?这种不太好,这种相当于在查询用户名时直接判断了用户和密码是否一致了,如果持久层把判断做了那业务层就没事干了,所以这里我们只查询用户名,判断用户名和密码是否一致交给业务层做

select * from t_user where username=?

分析完以后发现这个功能模块已经被开发完成(UserMapper接口的findByUsername方法),所以就可以省略当前的开发步骤,但是这个分析过程不能省略

后续的设计接口和抽象方法,编写映射,单元测试都不再需要进行,

2.登录-业务层

2.1规划异常

  • 用户名对应的密码错误,即密码匹配的异常,起名PasswordNotMatchException,这个是运行时异常
public class PasswordNotMatchException extends ServiceException{
    /**重写ServiceException的所有构造方法*/
}
  • 用户名没有被找到的异常,起名UsernameNotFoundException,这个也是运行时异常
public class UsernameNotFoundException extends ServiceException {
    /**重写ServiceException的所有构造方法*/
}

2.2设计接口和抽象方法及实现

1.在IUserService接口中编写抽象方法login(String username,String password)login(User user)也是可以的

细说一个事:登录成功某一个网站后,右上角会展示头像,昵称甚至电话号码等等,这些信息依赖于登陆成功后的信息,也就意味着一旦登录成功后在页面中切换到任意一个子页面写右上角都会展示这些信息.本质上就是查询出来这些信息,然后展示在右上角,但是这里实现查询不太现实:js中虽然打开一个html页面就自动发送一个请求,但这样就需要把这个查询的代码写在每一个html页面,显然不现实

这种情况下我们可以将当前登录成功的用户数据以当前用户对象的形式进行返回,然后进行状态管理:将数据保存在cookie或者session中,可以避免重复度很高的数据多次频繁操作数据库进行获取(这里我们用session存放用户名和用户id,用cookie存放用户头像,其中用户id是为因为有的页面展示依赖于id,用户头像也可以放在session中,而这里放在cookie是为了回顾一下cookie)

    /**
     * 用户登录功能
     * @param username 用户名
     * @param password 用户密码
     * @return 当前匹配的用户数据,如果没有则返回null
     */
    User login(String username,String password);

2.在抽象类UserServiceImpl中实现该抽象方法

    @Override
    public User login(String username, String password) {
        //根据用户名称来查询用户的数据是否存在,不存在则抛出异常
        User result = userMapper.findByUsername(username);
        if (result == null) {
            throw new UsernameNotFoundException("用户数据不存在");
        }

        /**
         * 检测用户的密码是否匹配:
         * 1.先获取数据库中加密之后的密码
         * 2.和用户传递过来的密码进行比较
         *  2.1先获取盐值
         *  2.2将获取的用户密码按照相同的md5算法加密
         */
        String oldPassword = result.getPassword();
        String salt = result.getSalt();
        String newMd5Password = getMD5Password(password, salt);
        if (!newMd5Password.equals(oldPassword)) {
            throw new PasswordNotMatchException("用户密码错误");
        }

        //判断is_delete字段的值是否为1,为1表示被标记为删除
        if (result.getIsDelete() == 1) {
            throw new UsernameNotFoundException("用户数据不存在");
        }

        //方法login返回的用户数据是为了辅助其他页面做数据展示使用(只会用到uid,username,avatar)
        //所以可以new一个新的user只赋这三个变量的值,这样使层与层之间传输时数据体量变小,后台层与
        // 层之间传输时数据量越小性能越高,前端也是的,数据量小了前端响应速度就变快了
        User user = new User();
        user.setUid(result.getUid());
        user.setUsername(result.getUsername());
        user.setAvatar(result.getAvatar());
        return user;
    }

2.3单元测试

在业务层的测试类UserServiceTests中添加测试方法:

    @Test
    public void login() {
        //因为login方法可能抛出异常,所以应该捕获异常,但是测试时没必要写那么严谨
        User user = userService.login("test02", "12");
        System.out.println(user);
    }

3.登录-控制层

3.1处理异常

业务层抛出的异常需要在统一异常处理类中进行统一的捕获和处理,如果该异常类型已经在统一异常类中曾经处理过则不需要重复添加

else if (e instanceof UsernameNotFoundException) {
    result.setState(4001);
    result.setMessage("用户数据不存在的异常");
} else if (e instanceof PasswordNotMatchException) {
    result.setState(4002);
    result.setMessage("用户名密码错误的异常");
}

3.2设计请求

  • 请求路径:/users/login
  • 请求参数:String username,String password
  • 请求类型:POST
  • 响应结果:`JsonResult

3.3处理请求

在UserController类中编写处理请求的方法.编写完成后启动主服务验证一下

    @RequestMapping("login")
    public JsonResult<User> login(String username,String password) {
        User data = userService.login(username, password);
        return new JsonResult<User>(OK,data);
    }

注意,控制层方法的参数是用来接收前端数据的,接收数据方式有两种:

  • 请求处理方法的参数列表设置为非pojo类型:

    SpringBoot会直接将请求的参数名和方法的参数名直接进行比较,如果名称相同则自动完成值的依赖注入

  • 请求处理方法的参数列表设置为pojo类型:

    SpringBoot会将前端的url地址中的参数名和pojo类的属性名进行比较,如果这两个名称相同,则将值注入到pojo类中对应的属性上

这两种方法都没有使用注解等等花里胡哨的,却能正常使用,原因是springboot是约定大于配置的,省略了大量配置以及注解的编写

4.登录-前端页面

在login.html加入script标签:

        <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和./ind
                            // ex.html完全一样,因为./就是表示当前目录
                            // 结构,也可以用../web/index.html
                            location.href = "index.html";
                        } else {
                            alert("登录失败")
                        }
                    },
                    error: function (xhr) {
                        //xhr.message可以获取未知异常的信息
                        alert("登录时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>

5.用session存储和获取用户数据

  • 在用户登录成功后要保存下来用户的id,username,avatar,并且需要在任何类中都可以访问存储下来的数据,也就是说存储在一个全局对象中,会话session可以实现
  • 把首次登录所获取的用户数据转移到session对象即可
  • 获取session对象的属性值用session.getAttribute(“key”),因为session对象的属性值在很多页面都要被访问,这时用session对象调用方法获取数据就显得太麻烦了,解决办法是将获取session中数据的这种行为进行封装
  • 考虑一下封装在哪里呢?放在一个干净的工具类里肯定可以,但就这个项目目录结构而言,只有可能在控制层使用session,而控制层里的类又继承BaseController,所以可以封装到BaseController里面

综上所述,该功能的实现需要两步:

1.在父类中封装两个方法:获取uid和获取username对应的两个方法(用户头像暂不考虑,将来封装到cookie中来使用)

    /**
     * 获取session对象中的uid
     * @param session session对象
     * @return 当前登录的用户uid的值
     */
    public final Integer getUidFromSession(HttpSession session) {
        //getAttribute返回的是Object对象,需要转换为字符串再转换为包装类
        return Integer.valueOf(session.getAttribute("uid").toString());
    }

    public final String getUsernameFromSession(HttpSession session) {
        return session.getAttribute("username").toString();
    }

2.把首次登录所获取的用户数据转移到session对象:

服务器本身自动创建有session对象,已经是一个全局的session对象,所以我们需要想办法获取session对象:如果直接将HttpSession类型的对象作为请求处理方法的参数,这时springboot会自动将全局的session对象注入到请求处理方法的session形参上:

  • 将登录模块的设计请求中的请求参数:String username,String password加上HttpSession session

  • 将登录模块的处理请求中login方法加上参数HttpSession session并修改代码如下:

        @RequestMapping("login")
        public JsonResult<User> login(String username, String password, HttpSession session) {
            User data = userService.login(username, password);
    
            //向session对象中完成数据的绑定(这个session是全局的,项目的任何位置都可以访问)
            session.setAttribute("uid",data.getUid());
            session.setAttribute("username",data.getUsername());
    
            //测试能否正常获取session中存储的数据
            System.out.println(getUidFromSession(session));
            System.out.println(getUsernameFromSession(session));
    
            return new JsonResult<User>(OK,data);
        }
    

6.拦截器

  • 拦截器的作用是将所有的请求统一拦截到拦截器中,可以在拦截器中定义过滤的规则,如果不满足系统设置的过滤规则,该项目统一的处理是重新去打开login.html页面(重定向和转发都可以,推荐使用重定向)
  • 拦截器在springboot中本质是依靠springMVC完成的.springMVC提供了一个HandlerInterceptor接口用于表示定义一个拦截器

1.所以想要使用拦截器就要定义一个类并使其实现HandlerInterceptor接口,在store下建包interceptor,包下建类LoginInterceptor并编写代码:

/**定义一个拦截器*/
public class LoginInterceptor implements HandlerInterceptor {
    /**
     *检测全局session对象中是否有uid数据,如果有则放行,如果没有重定向到登录页面
     * @param request 请求对象
     * @param response 响应对象
     * @param handler 处理器(把url和Controller映射到一块)
     * @return 返回值为true放行当前请求,反之拦截当前请求
     * @throws Exception
     */
    @Override
    //在DispatcherServlet调用所有处理请求的方法前被自动调用执行的方法
    //springboot会自动把请求对象给到request,响应对象给到response,适配器给到handler
    public boolean preHandle(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler) throws Exception {
        //通过HttpServletRequest对象来获取session对象
        Object obj = request.getSession().getAttribute("uid");
        if (obj == null) { //说明用户没有登录过系统,则重定向到login.html页面
            //不能用相对路径,因为这里是要告诉前端访问的新页面是在哪个目录下的新
            //页面,但前面的localhost:8080可以省略,因为在同一个项目下
            response.sendRedirect("/web/login.html");
            //结束后续的调用
            return false;
        }
        //放行这个请求
        return true;
    }
    //在ModelAndView对象返回给DispatcherServlet之后被自动调用的方法
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
   }
    //在整个请求所有关联的资源被执行完毕后所执行的方法
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
   }
}

2.注册过滤器:

注册过滤器的技术:借助WebMvcConfigure接口将用户定义的拦截器进行注册.所以想要注册过滤器需要定义一个类使其实现WebMvcConfigure接口并在其内部
添加黑名单(在用户登录的状态下才可以访问的页面资源)和
白名单(哪些资源可以在不登录的情况下访问:①register.html②login.html③index.html④/users/reg⑤/users/login⑥静态资源):

WebMvcConfigure是配置信息,建议在store包下建config包,再定义类LoginInterceptorConfigure

/**拦截器的注册*/
@Configuration //自动加载当前的类并进行拦截器的注册,如果没有@Configuration就相当于没有写类LoginInterceptorConfigure
public class LoginInterceptorConfigure implements WebMvcConfigurer {
    @Override
    //配置拦截器
    public void addInterceptors(InterceptorRegistry registry) {
        //1.创建自定义的拦截器对象
        HandlerInterceptor interceptor =  new LoginInterceptor();
        //2.配置白名单并存放在一个List集合
        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(interceptor);完成拦截
        // 器的注册,后面的addPathPatterns表示拦截哪些url
        //这里的参数/**表示所有请求,再后面的excludePathPatterns表
        // 示有哪些是白名单,且参数是列表
        registry.addInterceptor(interceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(patterns);
    }
}

修改密码

初步分析:需要用户提交原始密码和新密码,再根据当前登录的用户进行信息的修改操作

1.修改密码-持久层

1.1规划需要执行的SQL语句

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

    update t_user set password=?,modified_user=?, modified_time=? WHERE uid=?
    

    modified_user=?, modified_time=?是为了跟踪用户数据的变动,如果这条数据被错误修改了可以找到第一责任人

  • 在执行修改密码之前,还应检查用户数据是否存在或者用户数据是否被标记为"已删除"(比如登录账号后的几分钟在和朋友聊天,没有看页面,管理员错误删除了你的账号或者错误设置is_delete为1)、并检查原密码是否正确,这些检查都可以通过查询用户数据来辅助完成:

    SELECT * FROM t_user WHERE uid=?
    

1.2设计接口和抽象方法

UserMapper接口,将以上的两个方法的抽象定义出来,将来映射到sql语句上

    /**
     * 根据用户的uid来修改用户密码
     * @param uid 用户的id
     * @param password 用户输入的新密码
     * @param modifiedUser 表示修改的执行者
     * @param modifiedTime 表示修改数据的时间
     * @return 返回值为受影响的行数
     */
    Integer updatePasswordByUid(Integer uid,
                                String password,
                                String modifiedUser,
                                Date modifiedTime);

    /**
     * 根据用户的id查询用户的数据
     * @param uid 用户的id
     * @return 如果找到则返回对象,反之返回null值
     */
    User findByUid(Integer uid);

1.3编写映射

配置到映射文件UserMapper.xml中

    <update id="updatePasswordByUid">
        update t_user set
          `password`=#{password},
          modified_user=#{modifiedUser},
          modified_time=#{modifiedTime},
        where uid=#{uid}
    </update>

    <select id="findByUid" resultMap="UserEntityMap">
        select * from t_user where uid=#{uid}
    </select>

1.4单元测试

@Test
public void updatePasswordByUid(){
    userMapper.updatePasswordByUid(
        10,
        "321",
        "管理员",
        new Date());
}

@Test
public void findByUid(){
    System.out.println(userMapper.findByUid(10));
}

2.修改密码-业务层

2.1规划异常

  • 用户的原密码错误,抛PasswordNotMatchException异常(前面已创建)
  • 检测到is_delete字段为1和uid找不到都是抛出用户没有找到的异常,UsernameNotFoundException(前面已创建)
  • update在更新的时候,有可能产生未知的异常,抛UpdateException异常
/**用户在更新数据时产生的未知异常*/
public class UpdateException extends ServiceException{
    /**重写ServiceException的所有构造方法*/
}

2.2设计接口和抽象方法及实现

1.执行用户修改密码的核心方法:

    /**
     * changePassword方法需要什么参数:
     * 要先看底层持久层需要什么参数:uid,password,modifiedUser,modifiedTime
     * 1.修改人其实就是username,已经保存到session当中,通过控制层传递过来就行了
     * 2.在更新数据之前需要先根据uid查这个数据存不存在,uid也可以通过控制层传递
     * 3.新密码需要有
     * 4.修改时间不需要在参数列表,直接在方法内部new Date()就可以了
     * 5.旧密码
     * */
    void changePassword(Integer uid,
                       String username,
                       String oldPassword,
                       String newPassword);

2.在实现类中实现当前的抽象方法

    @Override
    public void changePassword(Integer uid,
                              String username,
                              String oldPassword,
                              String newPassword) {
        User result = userMapper.findByUid(uid);
        /**
         * 用户没找到:比如登录账号后的几分钟在和朋友聊天,没
         * 有看页面,管理员错误删除了你的账号或者错误设置is_delete为1)
         */
        if (result ==null || result.getIsDelete() == 1) {
            throw new UsernameNotFoundException("用户数据不存在");
        }

        //原始密码和数据库中密码进行比较
        String oldMd5Password = getMD5Password(oldPassword,result.getSalt());
        if (!result.getPassword().equals(oldMd5Password)) {
            throw new PasswordNotMatchException("密码错误");
        }

        //将新的密码加密后设置到数据库中(只要曾经注册过就用以前的盐值)
        String newMd5Password = getMD5Password(newPassword, result.getSalt());
        Integer rows = userMapper.updatePasswordByUid(uid, newMd5Password, username, new Date());

        if (rows != 1) {
            throw new UpdateException("更新数据产生未知的异常");
        }
    }

2.3单元测试

    @Test
    public void changePassword() {
        userService.changePassword(11,"管理员","123","321");
    }

3.修改密码-控制层

3.1处理异常

UsernameNotFoundException异常和PasswordNotMatchException异常在前面的章节中已经处理过,现在只需要把UpdateException异常配置到统一的异常处理方法中

else if (e instanceof UpdateException) {
            result.setState(5001);
            result.setMessage("更新数据时产生未知的异常");
        }

3.2设计请求

  • /users/change_password
  • post
  • String oldPassword,String newPassword,HttpSession session(uid和username可以通过session获取到,在处理方法的内部获取就可以了)//如果参数名用的是非pojo类型,就需要和表单中的name属性值保持一致
  • JsonResult<void>

3.3处理请求

@RequestMapping("change_password")
public JsonResult<Void> changePassword(String oldPassword,
                                       String newPassword,
                                       HttpSession session) {
    Integer uid = getUidFromSession(session);
    String username = getUsernameFromSession(session);
    userService.changePassword(uid,username,oldPassword,newPassword);
    return new JsonResult<>(OK);
}

启动服务,先登录账号然后在地址栏输入http://localhost:8080/users/change_password?oldPassword=321&newPassword=123看看是否成功

4.修改密码-前端页面

在password.html中添加ajax请求的处理

        <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>

个人资料

其中用户名是不可修改的并且是在进个人资料页面时就从session获取值并将值加入到控件中

1.个人资料-持久层

1.1规划SQL语句

  • 获取用户信息的SQL语句

    update t_user set 
    email=?,gender=?,modified_user=?,modified_time=?、
    where uid=?
    
  • 根据uid查询用户数据

    select * from t_user where uid=?
    

    根据uid查询用户数据不需要再重复开发

1.2设计接口和抽象方法

更新用户的信息方法的定义

    /**
     * 参数为user的方法
     * @param user 用户的数据
     * @return 返回值为受影响的行数
     */
    Integer updateInfoByUid(User user);//也可以用三个String的形参接收电话,邮箱,性别,但不如直接写个user省事

1.3编写映射

在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>

1.4单元测试

@Test
public void updateInfoByUid() {
    User user = new User();
    user.setUid(11);
    user.setPhone("13333688");
    user.setEmail("1454@qq.com");
    user.setGender(1);
    userMapper.updateInfoByUid(user);
}

2.个人资料-业务层

设计两个功能:

1.当打开页面时显示当前登录的用户的信息

2.点击修改按钮时更新用户的信息

2.1规划异常

  • 点击个人资料页面时可能找不到用户的数据
  • 点击修改按钮时可能找不到用户数据,也可能修改时出现未知错误

2.2设计接口和抽象方法及实现

1.业务层有两个功能模块,对应的是两个抽象方法的设计,并且这两个功能都涉及到用户是否存在的查询操作,所以需要在业务层设计根据用户uid查询数据的方法(持久层已经设计过该方法,但是没有在业务层实现该方法的调用)

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

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

2.在实现类中实现当前的抽象方法

    @Override
    public User getByUid(Integer uid) {
        //查询用户是否存在
        User result = userMapper.findByUid(uid);
        if (result == null || result.getIsDelete() == 1) {
            throw new UsernameNotFoundException("用户数据不存在");
        }

        //可以直接返回result给控制层,但是太臃肿了
        User user = new User();
        user.setUsername(result.getUsername());
        user.setPhone(result.getPhone());
        user.setEmail(result.getEmail());
        user.setGender(result.getGender());

        return user;
    }

    /**
     *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("更新数据时产生异常");
        }
    }

2.3单元测试

    @Test
    public void getByUid() {
        //err是为了让输出信息为红色
        System.err.println(userService.getByUid(11).getUsername());
    }

    @Test
    public void changeInfo() {
        User user = new User();
        //这四个属性值在真实开发中都是在控制层就已经自动封装在User对象中了
        //而uid需要由控制层传给业务层并在业务层封装到user中
        user.setPhone("123456789");
        user.setEmail("123@qq.com");
        user.setUsername("mxy");
        user.setGender(0);
        userService.changeInfo(11,user);
    }

3.个人资料-控制层

3.1处理异常

没有新的异常,所以这里不需要有操作

3.2设计请求

1.设计一打开页面就发送当前用户数据的查询

  • /users/get_by_uid
  • GET
  • HttpSession session(用于获取uid)
  • `JsonResult

2.点击修改按钮发送用户的数据修改操作

  • users/change_info
  • POST
  • User user,HttpSession session(用于获取uid)
  • `JsonResult

3.3处理请求

1.一打开页面就发送当前用户数据

@RequestMapping("get_by_uid")
public JsonResult<User> getByUid(HttpSession session) {
    User data = userService.getByUid(getUidFromSession(session));
    return new JsonResult<User>(OK,data);
}

启动服务,先登录账号然后在地址栏输入
http://localhost:8080/users/get_by_uid
看看状态码是否为200并且看data值是否不为null

2.点击修改按钮更改用户数据

@RequestMapping("change_info")
public JsonResult<Void> changeInfo(User user,HttpSession session) {
    //user对象中有四部分的数据:username,phone,email,gender
    //控制层给业务层传递uid,并在业务层通过user.setUid(uid);将uid封装到user中
    Integer uid = getUidFromSession(session);
    userService.changeInfo(uid,user);
    return new JsonResult<>(OK);
}

启动服务,先登录账号然后在地址栏输入http://localhost:8080/users/change_info?phone=175726&email=6695@qq.com&username=张9&gender=1观察状态码是否为200

4.个人资料-前端页面

1.在打开userdata.html(个人资料)页面自动发送ajax请求(get_by_uid),查询到的数据填充到这个页面

        <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);
                            var radio = json.data.gender == 0 ?
                                $("#gender-female") : $("#gender-male");
                            //prop()表示给某个元素添加属性及属性的值
                            radio.prop("checked","checked");
                        } else {
                            alert("用户的数据不存在")
                        }
                    },
                    error: function (xhr) {
                        //xhr.message可以获取未知异常的信息
                        alert("查询用户信息时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>

2.在检测到用户点击了修改按钮后发送一个ajax请求(change_info)

该ajax函数需要和上一个ajax同级

            $("#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);
                    }
                });
            });

上传头像

错误方法:把文件存到数据库中,需要图片时访问数据库,数据库将文件解析为字节流返回,最后写到本地的某一个文件.这种方法太耗费资源和时间了

正确方法:将对应的文件保存在操作系统上,然后再把这个文件路径记录下来,因为在记录路径的时候是非常便捷和方便的,将来如果要打开这个文件可以依据这个路径找到这个文件,所以说在数据库中保存该文件的路径即可.

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

1.上传头像-持久层

1.1SQL语句的规划

更新用户avatar字段的sql语句

update t_user 
set avatar=?,modified_user=?,modified_time=? 
where uid=?

1.2设计接口和抽象方法

在UserMapper接口中定义一个抽象方法用于修改用户的头像

    /**
     * 根据用户uid修改用户的头像
     * @param iddddd
     * @param avatar
     * @param modifiedUser
     * @param modifiedTime
     * @return
     */
    /**
     * 注解@Param("SQL映射文件中#{}占位符的变量名"),解决的问题:
     * 当SQL语句的占位符和映射的接口方法参数名不一致时,需要将某个参数强行注入到某个
     * 占位符变量上时,可以使用@Param这个注解来标注映射的关系
     * */
    Integer updateAvatarByUid(@Param("uid")Integer iddddd,//@Param("参数名")注解中的参数名需要和sql语句中
                              //的#{参数名}的参数名保持一致.该处表示iddddd中的变量值要注入到sql语句的uid中
                String avatar,
                String modifiedUser,
                Date modifiedTime); 


@Parm()相同可以省略,不同可以匹配
比如==@Parm(“uid”) String uid可以省略
但是如果
@Parm(“uid”) String userId==那么可以将userId参数注入到#{uid}中

当sql语句的占位符和映射接口方法中的参数名不一致的时候,需要将某个参数强行注入到某个占位符,那么可以使用@Parm()注解来指定接口参数和占位符参数名的映射关系

@Parm(“SQl映射文件中#{}占位符中的变量名”)

1.3编写映射

UserMapper.xml文件中编写映射的SQL语句

    <update id="updateAvatarByUid">
        update t_user
        set
            avatar = #{avatar},
            modified_user = #{modifiedUser},
            modified_time = #{modifiedTime}//要和对象属性名一致
        where
            uid = #{uid}
    </update>

1.4单元测试

@Test
public void updateAvatarByUid() {
    userMapper.updateAvatarByUid(
        11,
        "abc",
        "mxy",
        new Date());
}

2.上传头像-业务层

2.1规划异常

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

无需重复开发

2.2设计接口和抽象方法及实现

1.先分析一下业务层接口需要哪些参数:那就需要看持久层接口要的有什么参数:

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

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

2.编写业务层的更新用户头像的方法

@Override
public void changeAvatar(Integer uid, String avatar, String username) {
    //查询当前的用户数据是否存在
    User result = userMapper.findByUid(uid);
    if (result == null || result.getIsDelete() == 1) {
        throw new UsernameNotFoundException("用户数据不存在");
    }
    Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, new Date());
    if (rows!=1) {
        throw new UpdateException("更新用户头像时产生未知异常");
    }
}

2.3单元测试

@Test
public void changeAvatar() {
    userService.changeAvatar(11,"222","mmm");
}

3.上传头像-控制层

文件上传过程中产生的异常太多了,再比如文件类型不匹配或文件被损坏

3.1规划异常

客户端传递文件给服务器,服务器的控制端controller接收文件,接收时可能抛出异常,因为用户传过来的文件有可能超出了我们的大小限制

该异常能放在业务层抛出吗?没必要的,因为此时数据是从控制层往下传的,所以控制层产生的异常直接在这一层(控制层)抛就可以了

上传文件时的异常都是文件异常,所以可以先创建一个文件异常类的基类FileUploadException并使其继承RuntimeException

文件异常基类的子类有:

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

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

3.2处理异常

在基类BaseController中进行编写和统一处理

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);
}

异常统一处理方法的修饰符
@ExceptionHandler(ServiceException.class)表明我们现在创建的FileUploadException异常类不会被拦截到该方法中,
点进@ExceptionHandler注解可以发现传参可以传数组类型,
所以可以将异常统一处理方法上的注解改为:

@ExceptionHandler({ServiceException.class,FileUploadException.class})

3.3设计请求

  • /users/change_avatar
  • POST(GET请求提交数据只有2KB左右)
  • HttpSession session(获取uid和username),MultipartFile file
  • JsonResult<String>
    (不能是JsonResult<Void>:如果上传头像后浏览别的页面,然后再回到上传头像的页面就展示不出来了,所以图片一旦上传成功,就要保存该图片在服务器的哪个位置,这样的话一旦检测到进入上传头像的页面就可以通过保存的路径拿到图片,最后展示在页面上)

3.4处理请求


/**  
* 设置上传文件最大值 10M  
*/public static final int AVATAR_MAX_SIZE = 10 * 1024 * 1024;  

/**  
* 设置上传文件类型  
*/  
public static final List<String> AVATAR_TYPE = new ArrayList<>();  
static{  
AVATAR_TYPE.add("images/jpeg");  
AVATAR_TYPE.add("images/png");  
AVATAR_TYPE.add("images/bmp");  
AVATAR_TYPE.add("images/gif");  
}

@RequestMapping("change_avatar")
public JsonResult<String> changeAvatar(HttpSession session,
									   MultipartFile file) {
	/**
	 * 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
	 */
	//判断文件是否为null
	if (file.isEmpty()) {
		throw new FileEmptyException("文件为空");
	}
	if (file.getSize()>AVATAR_MAX_SIZE) {
		throw new FileSizeException("文件超出限制");
	}
	//判断文件的类型是否是我们规定的后缀类型
	String contentType = file.getContentType();
	//如果集合包含某个元素则返回值为true
	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,不包含父目录结构)用UUID
	// 工具生成一个新的字符串作为文件名(好处:避免了因文件名重复发生的覆盖)
	String originalFilename = file.getOriginalFilename();
	System.out.println("OriginalFilename="+originalFilename);
	int index = originalFilename.lastIndexOf(".");
	String suffix = originalFilename.substring(index);
	//filename形如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) {
		//这里不用打印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<>(OK,avatar);
}

4.上传头像-前端页面

1.在upload.html的上传头像的表单加上三个属性:

  • action=“/users/change_avatar”
  • method=“post”(get请求提交数据只有2KB左右)
  • enctype=“multipart/form-data”(如果直接使用表单进行文件的上传,需要给表单加该属性,这样不会将目标文件的数据结构做修改后再上传,这不同于字符串,字符串随意切割修改也能拼在一起,但文件不行)

2.确认 <input type=“file” name=“file”>
的type和name以及
<input type=“submit” class=“btn btn-primary” value=“上传” />中的type

5.前端页面优化——修复bug

5.1更改默认的大小限制

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

方式1:直接在配置文件application.properties中进行配置:

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

方式2:采用java代码的形式来设置文件的上传大小的限制:

1.该代码必须在主类中进行配置,因为主类是最早加载的,而配置文件必须是最早加载的

2.在主类中定义一个方法,方法名无所谓,但方法需要用@bean修饰,表示该方法返回值是一个bean对象,并且该bean对象被bean修饰,也就是这个方法返回了一个对象,然后把该对象交给bean管理,类似spring中的bean标签,含义是一样的,只是这里改为了注解

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

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

@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();
}

5.2上传后显示头像

上传头像成功后不能显示头像.

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

1.删掉在upload.html的上传头像的表单中加的三个属性:
action=“/users/change_avatar”,
method=“post”,
enctype=“multipart/form-data”.
加上id属性:id=“form-change-avatar”

2.把153行的input标签里面的type="submit"改为type=“button”(因为submit按钮不能添加事件,所以要改为普通的按钮)并加上属性id=“btn-change-avatar”

1.serialize():可以将表单数据自动拼接成key=value的结构提交给服务器,一般提交的是普通的控件类型中的数据(type=text/password/radio/checkbox等等)

2.FormData类:将表单中数据保持原有的结构进行数据提交.文件类型的数据可以使用FormData对象进行存储

使用方法:new FormData($(“form”)[0]);

这行代码的含义是将id="form"的表单的第一个元素的整体值作为创建FormData对象的数据

3.虽然我们把文件的数据保护下来了,但是ajax默认处理数据时按照字符串的形式进行处理,以及默认会采用字符串的形式进行数据提交.手动关闭这两个功能:

  • processData: false,//处理数据的形式,关闭处理数据
  • contentType: false,//提交数据的形式,关闭默认提交数据的形式

下面给提交表单加上事件:

        <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);
                        } else {
                            alert("头像修改失败")
                        }
                    },
                    error: function (xhr) {
                        alert("修改头像时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>

5.3登录后显示头像

将头像上传后会显示头像,但是关闭浏览器后再进入个人头像页面就不会显示头像了,因为只有点击"上传"才能发送ajax请求并显示头像.

可以在每次用户登录成功后将avatar保存在cookie中,登录的业务层返回给控制层user对象,该对象包含uid,username,avatar.所以要在登录页面login.html中将服务器返回的头像路径设置到cookie中,然后每次检测到用户打开上传头像页面,在这个页面中通过ready()方法来自动读取cookie中头像路径并设到src属性上

1.需要在login.html页面头部导入cookie.js文件

<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>

2.调用cookie方法保存路径

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

在ajax请求原有的代码上加$.cookie(“avatar”,json.data.avatar,{expires: 7});

success: function (json) {
    if (json.state == 200) {
        location.href = "index.html";
        $.cookie("avatar",json.data.avatar,{expires: 7});
    } else {
        alert("登录失败")
    }
},

3.需要在upload.html获取cookie中的值,所以要在页面头部导入cookie.js文件

<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>

4.在upload.html的script标签中加ready()自动读取cookie数据

$(document).ready(function(){
    var avatar = $.cookie("avatar");
    console.log(avatar);//调试用
    $("#img-avatar").attr("src",avatar);
})

5.4显示最新头像

上传头像后不重新登录而是浏览其他页面,然后再进入个人头像页面时展示的头像是上次上传的,因为此时cookie中的值是上次上传的头像的路径,所以需要上传头像后使用同名覆盖更改cookie中路径

在ajax函数的success属性值的if语句加:

$.cookie("avatar",json.data,{expires: 7});

完善后重启服务测试,结果若和预测的不一样,则参考项目环境搭建->项目测试->测试静态资源能否正常加载里面的四种解决方法

新增收货地址

1.创建数据表

1.选中数据表

use store

2.在store数据库中创建t_address表

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;

注意name是关键字,所以需要用``

2.创建收货地址的实体类

在entity包下创建实体类Address继承BaseEntity类

/**收货地址额实体类*/
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
 */
}

3.新增收货地址-持久层

3.1各功能的开发顺序

当前收货地址功能模块:

  • 第一个页面:列表的展示,修改,删除,设置默认
  • 第二个页面:新增收货地址

开发顺序:新增收货地址->列表的展示->设置默认收货地址->删除收货地址->修改收货地址

3.2规划需要执行的SQL语句

1.新增收货地址对应的是插入语句:

insert into t_address (aid以外的所有字段) values (字段值)

2.大部分平台都会规定一个用户的收货地址数量,这里规定最多20个.那么在插入用户新的地址之前就要先做查询操作.如果查询到的是刚好20,这并不是一个java语法的异常,可以认为是业务控制的异常,这个异常随后在service抛,在controller捕获

select count(*) from t_address where uid=?

3.3设计接口和抽象方法

创建接口AddressMapper,在这个接口中定义上面两个SQL语句抽象方法定义

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

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

3.4编写映射

1.快速创建一个AddressMapper.xml映射文件:

  • 鼠标放在UserMapper.xml文件上并ctrl+c,再把鼠标放到mapper文件夹上ctrl+v,在弹出的窗口中把UserMapper改为AddressMapper.
  • 进入AddressMapper.xml文件将mapper标签下的代码全部删除并修改mapper标签属性值为namespace=“com.cy.store.mapper.AddressMapper”

结果如下所示:

<?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">
<mapper namespace="com.cy.store.mapper.AddressMapper">
</mapper>

2.在mapper标签中配置Address类属性与数据库中表的字段映射

    <resultMap id="AddressEntityMap" type="com.cy.store.entity.Address">
        <id column="aid" property="aid"/>
        <result column="province_name" property="provinceName"/>
        <result column="province_code" property="provinceCode"/>
        <result column="city_name" property="cityName"/>
        <result column="city_code" property="cityCode"/>
        <result column="area_name" property="areaName"/>
        <result column="area_code" property="areaCode"/>
        <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>

判断该映射是否配置成功:按着ctrl并点击type="com.cy.store.entity.Address"中的Address,如果能跳转到Address类说明映射成功

3.在AddressMapper.xml中配置以上两个抽象方法的映射

    <insert id="insert" useGeneratedKeys="true" keyProperty="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>

    <!--resultType="java.lang.Integer"不写会报错,因为Integer不是基本数据类型-->
    <select id="countByUid"  resultType="java.lang.Integer">
        select count(*) from t_address where uid=#{uid}
    </select>

3.5单元测试

在test下的mapper文件夹下创建AddressMapperTests测试类

@SpringBootTest
@RunWith(SpringRunner.class)
public class AddressMapperTests {

    @Autowired
    private AddressMapper addressMapper;

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

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

4.新增收货地址-业务层

4.1规划异常

  • 插入数据时用户不存在(被管理员误删等等),抛UsernameNotFoundException异常(已经有了,不需要重复创建)

  • 当用户插入的地址是第一条时,需要将当前地址作为默认收货地址

    实现办法:如果查询到统计总数为0则将当前地址的is_default值设置为1

  • 如果查询的结果>=20,这时需要抛出业务控制的异常AddressCountLimitException

    /**收货地址总数超出限制的异常(20条)*/
    public class AddressCountLimitException extends ServiceException {
        /**重写ServiceException的所有构造方法*/
    }
    
  • 插入数据时产生未知的异常InsertException(已经有了,不需要重复创建)

4.2设计接口和抽象方法及实现

1.创建一个IAddressService接口,在接口中定义业务的抽象方法

因为mapper层接口该功能模块定义了两个抽象方法,所以就要在service层接口该功能模块也定义两个抽象方法?不是这样的,要看mapper层的这两个方法是依赖关系还是独立关系,如果某一个抽象方法依赖于另一个抽象方法,那就需要在业务层将这两个方法整合到一个方法中.一句话来说就是:一个功能模块可能需要多条sql语句

/**收货地址的业务层接口*/
@Service
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);
}

方法addNewAddress中三个参数的由来:

  • 首先肯定要有address

  • 业务层需要根据uid查询该用户收货地址总数及新建地址时给字段uid赋值

    但新建收货地址的表单中并没有哪个控件让输入用户uid,所以需要控制层将uid传给业务层并在业务层封装到address对象中

  • 业务层在创建/修改收货地址时需要同时修改数据库中创建人/修改人的字段

    但新建收货地址的表单中并没有哪个控件让输入用户username,所以需要控制层将username传给业务层并在业务层封装到address对象中

可以用HttpSession session代替Integer uid, String username,但这样写的话就需要把BaseController类下获取uid,username的方法重新封装到一个类中并让AddressServiceImpl实现类继承该类,这样就需要微调一下代码逻辑,太麻烦,并且,最好每一层只处理该层需要做的事情,session对象是控制层传递的,所以就把session对象定义封装在控制层中,不需要在业务层中额外处理,这样可以降低耦合

2.创建一个AddressServiceImpl类实现接口中抽象方法

/**新增收货地址的实现类*/
public class AddressServiceImpl implements IAddressService {
    @Autowired
    private AddressMapper addressMapper;
    @Autowired
    private UserMapper userMapper;

    /**
     * 为了方便日后修改最大收货地址数量,可以在配置文件
     * application.properties中定义user.address.max-count=20
     */
    //spring读取配置文件中数据:@Value("${user.address.max-count}")
    @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("用户收货地址超出上限");
        }

        //uid,isDefault
        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 rows = addressMapper.insert(address);
        if (rows != 1) {
            throw new InsertException("插入用户的收货地址时产生未知异常");
        }
    }
}

别忘了在配置文件application.properties中定义user.address.max-count=20

4.3单元测试

在test下的service文件夹下创建AddressServiceTests测试类

@SpringBootTest
@RunWith(SpringRunner.class)
public class AddressServiceTests {
    @Autowired
    private IAddressService addressService;

    @Test
    public void addNewAddress() {
        Address address = new Address();
        address.setPhone("175726");
        address.setName("男朋友");
        addressService.addNewAddress(11,"mxy",address);
    }
}

5.新增收货地址-控制层

5.1处理异常

义务层抛出了收货地址总数超出上限的异常,在BaseController中进行捕获处理

else if (e instanceof AddressCountLimitException) {
    result.setState(4003);
    result.setMessage("用户的收货地址超出上限的异常");
}

5.2设计请求

  • /addresses/add_new_address
  • post
  • Address address,HttpSession session
  • JsonResult

5.3处理请求

在controller包下创建AddressController并继承BaseController,该类用来处理用户收货地址的请求和响应

@RequestMapping("addresses")
@RestController
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<>(OK);
    }
}

启动服务器,登录账号后在地址栏输入http://localhost:8080/addresses/add_new_address?name=tom&phone=98745612进行测试

6.新增收货地址-前端页面

        <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>

获取省市区列表

新增收货地址页面的三个下拉列表的内容展示没有和数据库进行交互,而是通过前端实现的(将代码逻辑放在了distpicker.data.js文件中),实现方法是在加载新增收货地址页面时加载该js文件,这种做法不可取(我不知道为啥)

正确做法是:把这些数据保存到数据库中,用户点击下拉列表时相应的数据会被详细的展示出来,然后监听用户选择了哪一项以便后面的下拉列表进行二级关联

1.创建数据表

1.创建t_dict_district表

CREATE TABLE t_dict_district (
  id INT(11) NOT NULL AUTO_INCREMENT,
  parent VARCHAR(6) DEFAULT NULL,
  `code` VARCHAR(6) DEFAULT NULL,
  `name` VARCHAR(16) DEFAULT NULL,
  PRIMARY KEY (id)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
  • code和name需要加``
  • parent代表父区域的代码号
  • code代表自身的代码号
  • 省的父代码号是+86,代表中国

2.向该表中插入省市区数据

LOCK TABLES t_dict_district WRITE;
INSERT INTO t_dict_district VALUES (1,'110100','110101','东城区'),(2,'110100','110102','西城区')等等等等;
UNLOCK TABLES;

LOCK和UNLOVK干嘛用的?

2.创建省市区的实体类

在包entity下创建实体类District(不需要继承BaseEntity,但因为没有继承BaseEntity所以需要实现接口Serializable序列化)

/**省市区的数据实体类*/
public class District implements Serializable {
    private Integer id;
    private String parent;
    private String code;
    private String name;
 /**
 * get,set
 * equals和hashCode
 * toString
 */
}

3.获取省市区列表-持久层

3.1规划需执行的SQL语句

select * from t_dict_district where parent=? order by ASC

3.2设计接口和抽象方法

日后可能开发新的模块仍要用到省市区列表,那么为了降低耦合性,就要创建新的接口

在mapper层下创建接口DistrictMapper

public interface DistrictMapper {

    /**
     * 根据父代码号查询区域信息
     * @param parent 父代码号
     * @return 某个父区域下所有的区域列表
     */
    List<District> findByParent(String parent);//查询的结果可能是多个,所以放在集合中
}

3.3编写映射

创建一个DistrictMapper.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">
<mapper namespace="com.cy.store.mapper.DistrictMapper">
    <select id="findByParent" resultType="com.cy.store.entity.District">
        select * from t_dict_district where parent=#{parent}
        order by code ASC
    </select>
</mapper>

3.4单元测试

创建DistrictMapperTests测试类编写代码进行测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class DistrictMapperTests {

    @Autowired
    private DistrictMapper districtMapper;
    
    @Test
    public void findByParent() {
        List<District> list = districtMapper.findByParent("210100");
        for (District district : list) {
            System.out.println(district);
        }
    }

}

4.获取省市区列表-业务层

4.1规划异常

没有异常需要处理

4.2设计接口和抽象方法及实现

1.创建一个接口IDistrictService,并定义抽象方法

public interface IDistrictService {

    /**
     * 根据父代码号来查询区域信息(省或市或区)
     * @param parent 父代码号
     * @return 多个区域的信息
     */
    List<District> getByParent(String parent);
}

2.创建DistrictServiceImpl实现类来实现抽象方法

@Service
public class DistrictServiceImpl implements IDistrictService {

    @Autowired
    private DistrictMapper districtMapper;

    @Override
    public List<District> getByParent(String parent) {
        List<District> list = districtMapper.findByParent(parent);
        /**
         * 在进行网络数据传输时,为了尽量避免无效数据的传递,可以将无效数据
         * 设置为null,这样既节省流量,又提升了效率
         */
        for (District district : list) {
            district.setId(null);
            district.setParent(null);
        }
        return list;
    }
}

4.3单元测试

在test下的service文件夹下创建DistrictServiceTests测试类

@SpringBootTest
@RunWith(SpringRunner.class)
public class DistrictServiceTests {
    @Autowired
    private IDistrictService districtService;

    @Test
    public void getByParent() {
        //86代表中国,所有的省父代码号都是86
        List<District> list = districtService.getByParent("86");
        for (District district : list) {
            System.err.println(district);
        }
    }
}

5.获取省市区列表-控制层

5.1设计请求

  • /districts/
  • GET
  • String parent
  • JsonResult<List>

5.2处理请求

1.创建一个DistrictController类,在类中编写处理请求的方法

@RequestMapping("districts")
@RestController
public class DistrictController extends BaseController{
    @Autowired
    private IDistrictService districtService;

    /**
     * 请求路径和父路径相同时用@RequestMapping({"/",""}),表
     * 示districts后面跟/或者什么也不跟都会进入这个方法
     * 点进RequestMapping发现参数类型是String[],且传入一
     * 个路径时默认有{},传入一个以上路径时需要手动添加{}
     */
    @RequestMapping({"/",""})
    public JsonResult<List<District>> getByParent(String parent) {
        List<District> data = districtService.getByParent(parent);
        return new JsonResult<>(OK,data);
    }
}

2.为了能不登录也可以访问该数据,需要将districts请求添加到白名单中:

在LoginInterceptorConfigure类的addInterceptors方法中添加代码:patterns.add(“/districts/**”);

3.启动服务器,不登录账号,直接在地址栏输入http://localhost:8080/districts?parent=86测试能否正常获取数据

6.获取省市区列表-前端页面

1.原始的下拉列表展示是将数据放在js,再动态获取js中的数据,而目前为止我们已经将数据放在了数据库,所以不能让它再使用这种办法了,所以需要注释掉addAddress.html页面的这两行js代码:

<script type="text/javascript" src="../js/distpicker.data.js"></script>
<script type="text/javascript" src="../js/distpicker.js"></script>

关于这两行js代码:前者是为了获取数据,后者是为了将获取到的数据展示到下拉列表中

2.检查前端页面在提交省市区数据时是否有相关name属性和id属性(name用于提交数据,id用于监听用户的点击)

3.启动服务器,在前端验证一下是否还可以正常保存数据(除了省市区)

获取省市区名称

上一个模块获取省市区列表是通过父代码号获取子代码号完成联动,该模块获取省市区名称是通过自身的code获取自身的name

1.获取省市区名称-持久层

3.1规划需要执行的SQL语句

根据当前code来获取当前省市区的名称,对应就是一条查询语句

select * from t_dict_district where code=?

3.2设计接口和抽象方法

在DistrictMapper接口定义findNameByCode方法

String findNameByCode(String code);

3.3编写映射

在DistrictMapper.xml文件中添加findNameByCode方法的映射

<select id="findNameByCode" resultType="java.lang.String">
    select name from t_dict_district where code=#{code}
</select>

3.4单元测试

在DistrictMapperTests编写测试代码

@Test
public void findNameByCode() {
    String name = districtMapper.findNameByCode("610000");
    System.out.println(name);
}

2.获取省市区名称-业务层

2.1规划异常

没有异常需要处理

2.2设计接口和抽象方法及实现

1.在IDistrictService接口定义对应的业务层接口中的抽象方法

String getNameByCode(String code);

2.在DistrictServiceImpl实现此方法

@Override
public String getNameByCode(String code) {
    return districtMapper.findNameByCode(code);
}

2.3单元测试

业务层只是调用持久层对应的方法然后返回,没有什么额外的实现,可以不用测试(一般超过8行的代码都要进行测试)

3.获取省市区名称-控制层

实际开发中在获取省市区名称时并不需要前端传控制层,然后传业务层,再传持久层,而是在新增收货地址的业务层需要获取省市区名称,也就是说获取省市区名称的模块不需要控制层,只是需要被新增收货地址的业务层所依赖

4.获取省市区名称-业务层优化

1.在新增收货地址的业务层需要对address进行封装,使其存有所有数据,然后将address传给持久层(记住,持久层只会根据传过来的参数调用某个方法与数据库交互,永远不会有额外的实现),而此时新增收货地址的业务层并没有省市区的数据,所以需要依赖于获取省市区列表的业务层对应的接口中的getNameByCode方法

所以需要在业务层实现类AddressServiceImpl中加

@Autowired
private IDistrictService districtService;

2.在AddressServiceImpl的方法中将DistrictService接口中获取到的省市区数据封装到address对象,此时address就包含了所有用户收货地址的数据

/**
* 对address对象中的数据进行补全:省市区的名字看前端代码发现前端传递过来的省市区的name分别为:
* provinceCode,cityCode,areaCode,所以这里可以用address对象的get方法获取这三个的数据
 */
String provinceName = districtService.getNameByCode(address.getProvinceCode());
String cityName = districtService.getNameByCode(address.getCityCode());
String areaName = districtService.getNameByCode(address.getAreaCode());
address.setProvinceName(provinceName);
address.setCityName(cityName);
address.setAreaName(areaName);

5.获取省市区名称-前端页面

在addAddress.html页面中来编写对应的省市区展示及根据用户的不同选择来限制对应的标签中的内容

分析:

  • 在加载该页面时三个下拉列表的内容都显示为"-----请选择-----"
  • 没有选择市时如果点击区的下拉列表则列表中只有一个"-----请选择-----"
  • 加载该页面时需要自动发送一个请求把parent=86发送出去,然后将返回的省/直辖市填充到select标签中
  • 点击四川省后发送请求获取其下的市,并且将获取到的市罗列在市区域下拉列表中
  • 省点击"-----请选择-----“则需要把市,县内容填充为”-----请选择-----"终止请求而不是程序继续跑下去
  • 切换省份时,市,县内容更换为"-----请选择-----"

在addAddress.html中编写js代码

            /**因为清空后下拉列表的select标签没有option标签,所以需要设置一个默认的option标
             * 签并给市,县加上该标签.option标签并不会把内容发送到后端,而是将value值发
             * 送给后端,所以用value表示当前这个区域的code值
             * */
            var defaultOption="<option value='0'>-----请选择-----</option>";
            $(document).ready(function () {
                //加载省的数据罗列时代码量较多,建议定义在外部方法中,然后在这里调用定义的方法
                showProvinceList();

                //将省,市,县的下拉列表内容设为"-----请选择-----"
                /**
                 * select标签默认获取第一个option的内容填充到下拉列表中,所以即使加载
                 * 页面时省区域的下拉列表中已经有了所有省但仍然会显示-----请选择-----
                 * */
                $("#province-list").append(defaultOption);

                $("#city-list").append(defaultOption);
                $("#area-list").append(defaultOption);
            });

            //省的下拉列表数据展示
            function showProvinceList() {
                $.ajax({
                    url: "/districts",//发送请求用于获取所有省对象
                    type: "POST",
                    data: "parent=86",
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            var list = json.data;//获取所有省对象的List集合
                            for (var i = 0; i < list.length; i++) {
                                var opt =
                                    "<option value='"+list[i].code+"'>"+list[i].name+"</option>";
                                $("#province-list").append(opt);
                            }
                        } else {
                            <!--这个其实永远不会执行,因为没有编写
                            异常,控制层返回的状态码永远是OK-->
                            alert("省/直辖区的信息加载失败")
                        }
                    }
                    //这里没有写属性error,不知道为啥不用写,感觉写了更好
                });
            }

            /**
             * change()函数用于监听某个控件是否发生改变,一旦发生改变就
             * 会触发参数形式的函数,所以参数需要是function(){}
             * */
            $("#province-list").change(function () {
                //先获取到省区域父代码号
                var parent = $("#province-list").val();

                /**
                 * 如果我选择了河南省洛阳市涧西区,然后又选择了河北省,此时需要
                 * 将市,县下拉列表的所有option清除并显示内容-----请选择-----
                 * empty()表示某标签的所有子标签(针对此页面来说select的子标
                 * 签只有option)
                 * */
                $("#city-list").empty();
                $("#area-list").empty();
                //填充默认值:-----请选择-----
                $("#city-list").append(defaultOption);
                $("#area-list").append(defaultOption);

                if (parent == 0) {//如果继续程序,后面的ajax接收的json数据中的data是
                    return;//空集合[],进不了for循环,没有任何意义,所以直接在这里终止程序
                }
                $.ajax({
                    url: "/districts",
                    type: "POST",
                    data: "parent="+parent,
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            var list = json.data;
                            for (var i = 0; i < list.length; i++) {
                                var opt =
                                    "<option value='"+list[i].code+"'>"+list[i].name+"</option>";
                                $("#city-list").append(opt);
                            }
                        } else {
                            alert("市的信息加载失败")
                        }
                    }
                });
            });

            $("#city-list").change(function () {
                var parent = $("#city-list").val();
                $("#area-list").empty();
                $("#area-list").append(defaultOption);

                if (parent == 0) {
                    return;
                }
                $.ajax({
                    url: "/districts",
                    type: "POST",
                    data: "parent="+parent,
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            var list = json.data;
                            for (var i = 0; i < list.length; i++) {
                                var opt =
                                    "<option value='"+list[i].code+"'>"+list[i].name+"</option>";
                                $("#area-list").append(opt);
                            }
                        } else {
                            alert("县的信息加载失败")
                        }
                    }
                });
            });

收货地址列表展示

1.收货地址列表展示-持久层

1.1规划需要执行的SQL语句

数据库数据的查询操作

select * from t_address where uid ? order by is_default DESC,created_time DESC

其中order by is_default DESC是为了让默认收货地址展示在最上面,order by可以有多个字句,中间用逗号隔开,后面加的create_time DESC是为了让非默认收货地址创建的越晚越展示在上面

1.2设计接口和抽象方法

在AddressMapper接口追加抽象方法findByUid

/**
* 根据用户的uid查询用户的收货地址数据
* @param uid 用户uid
* @return 收货地址数据
*/
List<Address> findByUid(Integer uid);

1.3编写映射

在xml文件添加相应的sql语句映射

    <select id="findByUid" resultMap="AddressEntityMap">
        select * from t_address where uid=#{uid}
        order by is_default DESC,created_time DESC
    </select>

1.4单元测试

@Test
public void findByUid () {
    List<Address> list = addressMapper.findByUid(11);
    System.out.println(list);
}

2.收货地址列表展示-业务层

2.1规划异常

该模块只是为了展示列表,不涉及到增删改,即便没有拿到任何数据,那无非就是不展示呗,所以不涉及到异常,不需要在业务层抛出异常

2.2设计接口和抽象方法及实现

1.定义抽象方法

List<Address> getByUid(Integer uid);

2.实现该方法

@Override
public List<Address> getByUid(Integer uid) {
    List<Address> list = addressMapper.findByUid(uid);
    /**
    * 收货地址列表在前端只展示了四个数据,这里让其他值为空降低数据量
    * ProvinceName,CityName,AreaName,aid,tel(确认订单页展示展示用户地
    * 址时用到tel)在展示地址列表用不到,但是后面提交订单时的地址会用到,所以这里不设为null
    * */
    for (Address address : list) {
        //address.setAid(null);
        address.setUid(null);
        //address.setProvinceName(null);
        address.setProvinceCode(null);
        //address.setCityName(null);
        address.setCityCode(null);
        //address.setAreaName(null);
        address.setAreaCode(null);
        address.setZip(null);
        //address.setTel(null);
        address.setIsDefault(null);
        address.setCreatedTime(null);
        address.setCreatedUser(null);
        address.setModifiedTime(null);
        address.setModifiedUser(null);
    }
    return list;
}

2.3单元测试

这里不再进行单元测试

3.收货地址列表展示-控制层

3.1处理异常

因为业务层没有抛出异常,所以这里不需要处理异常

3.2设计请求

  • /addresses
  • HttpSession session
  • get(该功能模块只需要uid,不需要别的数据,而且uid也是在后端封装的,所以前端没有提交什么数据,体量很小可以用get)
  • JsonResult<List
    >

3.3处理请求

实现请求方法的编写

@RequestMapping({"","/"})
public JsonResult<List<Address>> getByUid(HttpSession session) {
    Integer uid = getUidFromSession(session);
    List<Address> data = addressService.getByUid(uid);
    return new JsonResult<>(OK,data);
}

启动服务,登录账号后在地址栏输入http://localhost:8080/addresses测试能否拿到数据

###4.收货地址列表展示-前端页面

在address.html中编写js代码

<script>
    $(document).ready(function () {
    showAddressList();
});

/**展示用户收货地址数据列表*/
function showAddressList() {
    $("#address-list").empty();
    $.ajax({
        url: "/addresses",
        type: "get",
        //data: "parent=86",//不需要提交数据,所以data可以删去
        dataType: "JSON",
        success: function (json) {
            if (json.state == 200) {
                var list = json.data;
                console.log(list);//调试用
                for (var i = 0; i < list.length; i++) {
                    //先写一个var tr = ''; 然后去上面的地址展示中找一个tr标签复制粘贴到单引号里面,再删去多余的制表符
                    var tr = '<tr>\n' +
                        '<td>#{tag}</td>\n' +
                        '<td>#{name}</td>\n' +
                        '<td>#{address}</td>\n' +
                        '<td>#{phone}</td>\n' +
                        '<td><a class="btn btn-xs btn-info"><span class="fa fa-edit"></span> 修改</a></td>\n' +
                        '<td><a class="btn btn-xs add-del btn-info"><span class="fa fa-trash-o"></span> 删除</a></td>\n' +
                        '<td><a class="btn btn-xs add-def btn-default">设为默认</a></td>\n' +
                        '</tr>';
                    //下面用正则表达式更改字符串,上面的#{tag}#{name}等等只
                    //是占位符,没有任何意义,我也可以把#{tag}写成任何想写的东西
                    //replace的第一个参数/占位符/g可以,"占位符"也可以
                    tr = tr.replace(/#{tag}/g,list[i].tag);
                    tr = tr.replace(/#{name}/g,list[i].name);
                    tr = tr.replace("#{address}",list[i].address);
                    tr = tr.replace("#{phone}",list[i].phone);

                    $("#address-list").append(tr);
                }

                //用hide方法将第一个收货地址的"设为默认"元素隐藏,.add-def:eq(0)表
                //示第一个class为add-def的标签,这样就可以保证隐藏的是第一个收货地址
                $(".add-def:eq(0)").hide();
            } else {
                <!--这个其实永远不会执行,因为没有编写
                异常,控制层返回的状态码永远是OK-->
                alert("用户收货地址数据加载失败")
            }
        }
    });
}
</script>

设置默认收货地址

1.设置默认收货地址-持久层

1.1规划需要执行的SQL语句

无论选择的是哪一条数据,都把所有的数据设为非默认,再把当前数据设为默认

我们可能会想着把第一条设为非默认,再将该条设为默认,但这样处理的话需要额外做一条查询语句拿到默认地址的数据

1.检测当前用户想设置为默认收货地址的这条数据是否存在

select * from t_address where aid=?

2.在修改用户的默认收货地址之前先将所有的收货地址设置为非默认

update t_address set is_default=0 where uid=?

3.将用户选中的这条记录设置为默认收货地址

update t_address set is_default=1,modified_user=?,modified_time=? where aid=?

1.2设计接口和抽象方法

在AddressMapper接口中来定义实现该模块所需的三个方法

/**
* 根据aid查询收货地址数据
* @param aid 收货地址aid
* @return 收货地址数据,如果没有找到则返回null值
*/
Address findByAid(Integer aid);

/**
* 根据用户uid修改用户的收货地址统一设置为非默认
* @param uid 用户uid
* @return 受影响的行数
*/
Integer updateNonDefault(Integer uid);


Integer updateDefaultByAid(
    @Param("aid") Integer aid,
    @Param("modifiedUser") String modifiedUser,
    @Param("modifiedTime") Date modifiedTime);

1.3编写映射

在AddressMapper.xml中编写映射

<select id="findByAid" resultMap="AddressEntityMap">
    select * from t_address where aid=#{aid}
</select>

<update id="updateNonDefault">
    update t_address
    set is_default=0
    where uid=#{uid}
</update>

<update id="updateDefaultByAid">
    update t_address
    set is_default=1,
    modified_user=#{modifiedUser},
    modified_time=#{modifiedTime}
    where aid=#{aid}
</update>

1.4单元测试

    @Test
    public void findByAid() {
        System.err.println(addressMapper.findByAid(9));
    }

    @Test
    public void updateNonDefault() {
        System.out.println(addressMapper.updateNonDefault(11));//有几条数据影响行数就输出几
    }

    @Test
    public void updateDefaultByAid() {
        addressMapper.updateDefaultByAid(9,"明明",new Date());
    }

2.设置默认收货地址-业务层

2.1规划异常

  • 在执行更新时产生未知的UpdateException异常,已经创建无需重复创建
  • 访问的数据不是当前登录用户的收货地址数据,属于非法访问,AccessDeniedException异常(就比如说,展示收货地址列表的sql语句写错了,然后这里展示的是别人的收货地址,此时想要将某个收货地址改为默认就属于非法访问了)
  • 收货地址可能不存在的AddressNotFoundException异常,(比如,刚展示完收货地址列表,管理员误删地址了,此时地址就不存在了)

在业务层的ex包下创建如下两个异常类,并使其继承ServiceException类

/**收货地址数据不存在的异常*/
public class AddressNotFoundException extends ServiceException {
    /**重写ServiceException的所有构造方法*/
}
/**非法访问的异常*/
public class AccessDeniedException extends ServiceException {
    /**重写ServiceException的所有构造方法*/
}

2.2设计接口和抽象方法及实现

1.在IAddressService接口中编写抽象方法setDefault,并使其在方法内部统一实现持久层的三个方法

分析一下该方法需要什么参数:

先看持久层的三个方法需要什么参数:aid,uid,modifiedUser,modifiedTime.

其中aid是从前端一步一步传到业务层的,所以需要该参数

uid和modifiedUser是一样的,都是由控制层从session获取的uid并传给业务层,所以需要该参数

modifiedTime可以在业务层new Date,所以不需要该参数

/**
* 修改某个用户的某条收货地址数据为默认收货地址
* @param aid 收货地址的id
* @param uid 用户id
* @param username 修改执行人
*/
void setDefault(Integer aid,Integer uid,String username);

2.在AddressServiceImpl类编写该方法的实现

    @Override
    public void setDefault(Integer aid, Integer uid, String username) {

        //1.检测是否有该条收货地址数据
        Address result = addressMapper.findByAid(aid);
        if (result == null) {
            throw new AddressNotFoundException("收货地址不存在");
        }

        //2.检测当前获取到的收货地址数据的归属
        if (!result.getUid().equals(uid)) {
            throw new AccessDeniedException("非法数据访问");
        }

        //3.先将所有的收货地址设置为非默认
        Integer rows = addressMapper.updateNonDefault(uid);
        if (rows < 1) {
            throw new UpdateException("更新数据时产生未知的异常");
        }

        //4.将用户选中的地址设置为默认收货地址
        rows = addressMapper.updateDefaultByAid(aid, username, new Date());
        if (rows != 1) {
            throw new UpdateException("更新数据时产生未知的异常");
        }
    }

2.3单元测试

在AddressServiceTests类中编写单元测试方法

@Test
public void setDefault() {
    addressService.setDefault(9,11,"管理员");
}

3.设置默认收货地址-控制层

3.1处理异常

在BaseController类中处理业务层抛出的两个异常

else if (e instanceof AddressNotFoundException) {
    result.setState(4004);
    result.setMessage("用户的收货地址数据不存在的异常");
} else if (e instanceof AccessDeniedException) {
    result.setState(4005);
    result.setMessage("收货地址数据非法访问的异常");
}

3.2设计请求

  • /addresses/{aid}/set_default(以前的数据是通过表单直接提交的,还有一种提交方式就是RestFul风格,这种提交方式可以提交更多的数据,这里用这个提交方式)
  • GET
  • Integer aid,HttpSession session(如果这里是id那就必须在Integer aid前加@PathVariable(“aid”)强行将aid的值注入到id中)
  • JsonResult

3.3处理请求

在AddressController类中编写请求处理方法.

RestFul编写时不管参数名和占位符是否一致都必须加@PathVariable(“aid”)

//RestFul风格的请求编写
@RequestMapping("{aid}/set_default")
public JsonResult<Void> setDefault(
    @PathVariable("aid") Integer aid,HttpSession session) {
    addressService.setDefault(
        aid,
        getUidFromSession(session),
        getUsernameFromSession(session));
    return new JsonResult<>(OK);
}

启动服务,登录账号后在地址栏输入http://localhost:8080/addresses/8/set_default进行测试

4.设置默认收货地址-前端页面

观察address.html代码发现"设为默认"按钮没有id属性,那应该怎么获取"设为默认"按钮以监听是否被点击了呢?

法一:给"设为默认"的标签添加id属性(我觉得不对,因为id必须是唯一的,如果给该按钮加id属性,那么该用户有几个收货地址就会给几个按钮加同样的id,这显然不对,我认为可以用按钮上本就存在的class属性)。

法二:给"设置默认"按钮添加一个onclick属性,指向一个方法的调用,在这个方法中来完成ajax请求的方法

在这里用第二种方法:

1.展示用户收货地址数据列表的js函数中用for循环给页面增加地址数据的tr标签,我们需要在for循环中为每一个tr标签增加onclick属性并指向setDefault(#{aid})函数,括号里面占位符是为了给外部的setDefault函数传参,可以随便写,只要给占位符赋值时对应就可以了,.注意,即使调用的是无参函数也要加括号

<td><a onclick="setDefault(#{aid})" class="btn btn-xs add-def btn-default">设为默认</a></td>

2.在for循环中为占位符赋值:

tr = tr.replace("#{aid}",list[i].aid);

3.完成setDefault方法的声明

function setDefault(aid) {
    $.ajax({
        url: "/addresses/"+aid+"/set_default",
        type: "POST",
        //data: $("#form-change-password").serialize(),
        dataType: "JSON",
        success: function (json) {
            if (json.state == 200) {
                //重新加载收货地址列表页面
                showAddressList();
            } else {
                alert("设置默认收货地址失败")
            }
        },
        error: function (xhr) {
            alert("设置默认收货地址时产生未知的异常!"+xhr.message);
        }
    });
}

删除收货地址

1.删除收货地址-持久层

1.1规划需要执行的SQL语句

1.在删除之前判断该数据是否存在,需要执行查询语句看能否查到该数据,还需要根据返回的aid获取uid并和session中的uid进行比较判断归属是否正确,这一条SQL语句在设置收货地址时已经开发,无需重复开发

2.开发执行删除的SQL语句

delete from t_address where aid=?

3.需要判断删除的地址是否是默认地址(使用aid查询到的地址对象的getIsDefault方法),如果判断出删的是默认地址,则还需要定义把哪个地址设为默认,这里定义最新修改的为默认地址.

开发该SQL语句

select * from t_address where uid=? order by modified_time DESC limit 0,1

其中limit 0,1表示查询到的第一条数据(limit (n-1),pageSize),这样查询后就只会获得第一条数据

4.如果用户本身就只有一条地址,那么删除后其他操作就可以不进行了,所以需要查询该用户的所有地址数量,在设置收货地址时已经开发,无需重复开发

1.2设计接口和抽象方法

在AddressMapper接口中进行抽象方法的设计

/**
 * 根据收货地址id删除收货地址数据
 * @param aid 收货地址的id
 * @return 受影响的行数
 */
Integer deleteByAid(Integer aid);

/**
 * 根据用户uid查询用户最后一次被修改的收货地址数据
 * @param uid 用户id
 * @return 收货地址数据
 */
Address findLastModified(Integer uid);

1.3编写映射

在AddressMapper.xml文件中进行映射

<delete id="deleteByAid">
    delete from t_address where aid=#{aid}
</delete>

<select id="findLastModified" resultMap="AddressEntityMap">
    select * from t_address
    where uid=#{uid}
    order by modified_time DESC limit 0,1
</select>

1.4单元测试

@Test
public void deleteByAid() {
    addressMapper.deleteByAid(11);
}

@Test
public void findLastModified() {
    System.out.println(addressMapper.findLastModified(11));
}
}

2.删除收货地址-业务层

2.1规划异常

  • 可能没有该条地址数据(已开发)

  • 可能地址数据归属错误(已开发)

  • 在执行删除的时候可能会产生未知的异常导致数据不能够删除成功,则抛出DeleteException异常,在service创建该异常并使其继承业务层异常

/**删除数据时产生的异常*/
public class DeleteException extends ServiceException{
    /**重写ServiceException的所有构造方法*/
}

2.2设计接口和抽象方法及实现

1.在IAddressService接口中定义抽象方法

需要给抽象方法声明哪些参数呢:

根据分析可得,该抽象方法的实现依赖于持久层的以下方法:

1.findByAid:查询该条地址数据是否存在,参数是aid

3.deleteByAid:删除地址数据,参数是aid

5.countByUid:统计用户地址数量,参数是uid

6.findLastModified:查询得到最后修改的一条地址,参数是uid

7.updateDefaultByAid:设置默认收货地址,参数是aid,modifiedUser,modifiedTime

稍加分析可以得出接下来定义的抽象方法的参数是:aid,uid,username

把上面的分析补上:2.判断地址数据归属是否正确4.判断删除的是否是默认地址.这七步就是业务层完整的开发流程

/**
* 删除用户选中的收货地址数据
* @param aid 收货地址id
* @param uid 用户id
* @param username 用户名
*/
void delete(Integer aid,Integer uid,String username);

2.实现该抽象方法

@Override
public void delete(Integer aid, Integer uid, String username) {
    Address result = addressMapper.findByAid(aid);
    //1.
    if (result == null) {
        throw new AddressNotFoundException("收货地址数据不存在");
    }
    //2.
    if (!result.getUid().equals(uid)) {
        throw new AccessDeniedException("非法数据访问");
    }
    //3.
    Integer rows = addressMapper.deleteByAid(aid);
    if (rows != 1) {
        throw new DeleteException("删除数据时产生未知的异常");
    }
    //4.如果删除的是非默认地址则不需要再做后面的任何操作,终止程序
    if (result.getIsDefault() == 0) {
        return;
    }
    //5.
    Integer count = addressMapper.countByUid(uid);
    if (count == 0) {
        return;
    }
    //6.
    Address address = addressMapper.findLastModified(uid);
    //7.
    rows = addressMapper.updateDefaultByAid(address.getAid(), username, new Date());
    if (rows != 1) {
        throw new UpdateException("更新数据时产生未知的异常");
    }
}

2.3单元测试

@Test
public void delete() {
    addressService.delete(1,11,"4.11删除");
}

3.删除收货地址-控制层

3.1处理异常

需要在BaseController类中处理异常类

else if (e instanceof DeleteException) {
    result.setState(5002);
    result.setMessage("删除数据时产生未知的异常");
}

3.2设计请求

  • /addresses/{aid}/delete
  • POST
  • Integer aid,HttpSession session
  • JsonResult

3.3处理请求

@RequestMapping("{aid}/delete")
public JsonResult<Void> delete(@PathVariable("aid") Integer aid,HttpSession session) {
    addressService.delete(
        aid,
        getUidFromSession(session),
        getUsernameFromSession(session));
    return new JsonResult<>(OK);
}

3.4单元测试

在AddressController类编写请求处理方法的实现

这个方法就只是调用业务层方法然后给前端返回一些信息,可以选择不用测试

4.删除收货地址-前端页面

处理该前端页面的所有步骤和处理"设置默认收货地址"的一样

1.给"删除"按钮添加onclick属性并指向deleteByAid(aid)方法

<td><a onclick="delete(#{aid})" class="btn btn-xs add-del btn-info"><span class="fa fa-trash-o"></span> 删除</a></td>

2.给占位符赋值

因为处理"设置默认收货地址"时已经编写tr = tr.replace(“#{aid}”,list[i].aid);用来给占位符#{aid}赋值,所以这里不需要再写.但是需要把replace改为replaceAll

3.完成deleteByAid(aid)方法的声明

function setDefault(aid) {
    $.ajax({
        url: "/addresses/"+aid+"/set_default",
        type: "POST",
        //data: $("#form-change-password").serialize(),
        dataType: "JSON",
        success: function (json) {
            if (json.state == 200) {
                //重新加载收货地址列表页面
                showAddressList();
            } else {
                alert("删除收货地址失败")
            }
        },
        error: function (xhr) {
            alert("删除收货地址时产生未知的异常!"+xhr.message);
        }
    });
}

商品热销排行

1.创建数据表

1.在store数据库中创建t_product数据表

CREATE TABLE t_product (
  id int(20) NOT NULL COMMENT '商品id',
  category_id int(20) DEFAULT NULL COMMENT '分类id',
  item_type varchar(100) DEFAULT NULL COMMENT '商品系列',
  title varchar(100) DEFAULT NULL COMMENT '商品标题',
  sell_point varchar(150) DEFAULT NULL COMMENT '商品卖点',
  price bigint(20) DEFAULT NULL COMMENT '商品单价',
  num int(10) DEFAULT NULL COMMENT '库存数量',
  image varchar(500) DEFAULT NULL COMMENT '图片路径',
  `status` int(1) DEFAULT '1' COMMENT '商品状态  1:上架   2:下架   3:删除',
  priority int(10) DEFAULT NULL COMMENT '显示优先级',
  created_time datetime DEFAULT NULL COMMENT '创建时间',
  modified_time datetime DEFAULT NULL COMMENT '最后修改时间',
  created_user varchar(50) DEFAULT NULL COMMENT '创建人',
  modified_user varchar(50) DEFAULT NULL COMMENT '最后修改人',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.向该表插入数据

LOCK TABLES t_product WRITE;
INSERT INTO t_product VALUES (10000001,238,'牛皮纸记事本','广博(GuangBo)10本装40张A5牛皮纸记事本子日记本办公软抄本GBR0731','经典回顾!超值特惠!',23,99999,'/images/portal/00GuangBo1040A5GBR0731/',1,62,'2017-10-25 15:08:55','2017-10-25 15:08:55','admin','admin'),等等等等;
UNLOCK TABLES;

2.创建商品的实体类

创建Product实体类并使其继承BaseEntity类

/** 商品数据的实体类 */
public class Product extends BaseEntity {
    private Integer id;
    private Integer categoryId;
    private String itemType;
    private String title;
    private String sellPoint;
    private Long price;
    private Integer num;
    private String image;
    private Integer status;
    private Integer priority;
 /**
 * get,set
 * equals和hashCode
 * toString
 */
}

3.商品热销排行-持久层

3.1 规划需要执行的SQL语句

查询热销商品列表的SQL语句

SELECT * FROM t_product WHERE status=1 ORDER BY priority DESC LIMIT 0,4

3.2 设计接口和抽象方法

在mapper包下创建ProductMapper接口并在接口中添加查询热销商品findHotList()的方法

public interface ProductMapper {
    /**
     * 查询热销商品的前四名
     * @return 热销商品前四名的集合
     */
    List<Product> findHotList();
}

3.3 编写映射

在main\resources\mapper文件夹下创建ProductMapper.xml文件,并在文件中配置findHotList()方法的映射

<?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">
<mapper namespace="com.cy.store.mapper.ProductMapper">
    <resultMap id="ProductEntityMap" type="com.cy.store.entity.Product">
        <id column="id" property="id"/>
        <result column="category_id" property="categoryId"/>
        <result column="item_type" property="itemType"/>
        <result column="sell_point" property="sellPoint"/>
        <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="findHotList" resultMap="ProductEntityMap">
        select * from t_product where status=1 order by priority desc limit 0,4
    </select>
</mapper>

4.商品热销排行-业务层

4.1 规划异常

只要是查询,不涉及到增删改的,都没有异常,无非就是没有该数据然后返回空

4.2 设计接口和抽象方法及实现

1.创建IProductService接口,并在接口中添加findHotList()方法

public interface IProductService {
    /**
     * 查询热销商品的前四名
     * @return 热销商品前四名的集合
     */
    List<Product> findHotList();
}

2.在业务层创建ProductServiceImpl类并实现该方法

package com.cy.store.service.impl;
import com.cy.store.entity.Product;
import com.cy.store.mapper.ProductMapper;
import com.cy.store.service.IProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

/** 处理商品数据的业务层实现类 */
@Service
public class ProductServiceImpl implements IProductService {
    @Autowired
    private ProductMapper productMapper;

    @Override
    public List<Product> findHotList() {
        List<Product> list = productMapper.findHotList();
        for (Product product : list) {
            product.setPriority(null);
            product.setCreatedUser(null);
            product.setCreatedTime(null);
            product.setModifiedUser(null);
            product.setModifiedTime(null);
        }
        return list;
    }
}

5.商品热销排行-控制层

5.1 处理异常

无异常。

5.2 设计请求

  • /products/hot_list
  • GET
  • 不需要请求参数
  • JsonResult<List>

5.3 处理请求

1.创建ProductController类并使其继承BaseController类,在类中编写处理请求的方法

@RestController
@RequestMapping("products")
public class ProductController extends BaseController {
    @Autowired
    private IProductService productService;

    @RequestMapping("hot_list")
    public JsonResult<List<Product>> getHotList() {
        List<Product> data = productService.findHotList();
        return new JsonResult<List<Product>>(OK, data);
    }
}

2.为了能不登录也可以访问该数据,需要将products/**请求添加到白名单中:

在LoginInterceptorConfigure类的addInterceptors方法中添加代码:

patterns.add("/products/**");

6.商品-热销排行-前端页面

1.在index.html页面给“热销排行”列表的div标签设置id属性值

<div id="hot-list" class="panel-body panel-item">
<!-- ... -->
</div>

2.在index.html页面中添加展示热销排行商品的js代码

<script type="text/javascript">
    $(document).ready(function() {
    showHotList();
});

function showHotList() {
    $("#hot-list").empty();
    $.ajax({
        url: "/products/hot_list",
        type: "GET",
        dataType: "JSON",
        success: function(json) {
            var list = json.data;
            for (var i = 0; i < list.length; i++) {
                console.log(list[i].title);//调试用
                var html = '<div class="col-md-12">'
                + '<div class="col-md-7 text-row-2"><a href="product.html?id=#{id}">#{title}</a></div>'
                + '<div class="col-md-2">¥#{price}</div>'
                + '<div class="col-md-3"><img src="..#{image}collect.png" class="img-responsive" /></div>'
                + '</div>';

                html = html.replace(/#{id}/g, list[i].id);
                html = html.replace(/#{title}/g, list[i].title);
                html = html.replace(/#{price}/g, list[i].price);
                html = html.replace(/#{image}/g, list[i].image);

                $("#hot-list").append(html);
            }
        }
    });
}
</script>

关于image标签里面的属性src=“…#{image}collect.png” class=“img-responsive”

  • …代表跳到父文件夹,即index.html的父文件夹static
  • …后面和collect前面不需要单斜杠,因为数据库中图片地址的数据前面后面加的有

关于a标签里面的href=“product.html?id=#{id}”

  • 这里是为了点击超链接进入商品详情页时可以把商品id传给详情页,使两个页面形成联系

显示商品详情

1.显示商品详情-持久层

1.1规划需要执行的SQL语句

根据商品id显示商品详情的SQL语句

SELECT * FROM t_product WHERE id=?

1.2设计接口和抽象方法

在ProductMapper接口中添加抽象方法

/**
 * 根据商品id查询商品详情
 * @param id 商品id
 * @return 匹配的商品详情,如果没有匹配的数据则返回null
 */
Product findById(Integer id);

1.3编写映射

在ProductMapper.xml文件中配置findById(Integer id)方法的映射

<select id="findById" resultMap="ProductEntityMap">
    select * from t_product where id=#{id}
</select>

2.显示商品详情-业务层

2.1规划异常

如果商品数据不存在,应该抛出ProductNotFoundException,所以创建ProductNotFoundException异常类并使其继承ServiceException

/** 商品数据不存在的异常 */
public class ProductNotFoundException extends ServiceException {
    /**重写ServiceException的所有构造方法*/
}

2.2设计接口和抽象方法及实现

1.在业务层IProductService接口中添加findById(Integer id)抽象方法

/**
 * 根据商品id查询商品详情
 * @param id 商品id
 * @return 匹配的商品详情,如果没有匹配的数据则返回null
 */
Product findById(Integer id);

2.在ProductServiceImpl类中,实现接口中的findById(Integer id)抽象方法

@Override
public Product findById(Integer id) {
    Product product = productMapper.findById(id);
    // 判断查询结果是否为null
    if (product == null) {
        throw new ProductNotFoundException("尝试访问的商品数据不存在");
    }
    // 将查询结果中的部分属性设置为null
    product.setPriority(null);
    product.setCreatedUser(null);
    product.setCreatedTime(null);
    product.setModifiedUser(null);
    product.setModifiedTime(null);

    return product;
}

3.显示商品详情-控制层

3.1处理异常

在BaseController类中的handleException()方法中添加处理ProductNotFoundException的异常

else if (e instanceof ProductNotFoundException) {
result.setState(4006);
    result.setMessage("访问的商品数据不存在的异常");
}

3.2 设计请求

  • /products/{id}/details
  • Integer id
  • GET
  • JsonResult

3.3处理请求

在ProductController类中添加处理请求的getById()方法

@GetMapping("{id}/details")
public JsonResult<Product> getById(@PathVariable("id") Integer id) {
    Product data = productService.findById(id);
    return new JsonResult<Product>(OK, data);
}

4.显示商品详情-前端页面

1.首页将商品id发送给详情页后,详情页需要从url中裁取获得该id,实现方法在jquery-getUrlParam.js中(目前怎么实现裁取可以先不学),所以需要在product.html页面中导入该js文件,这里我在body标签内部的最后引入该js文件

<script type="text/javascript" src="../js/jquery-getUrlParam.js"></script>

2.在product.html页面中body标签内部的最后添加获取当前商品详情的代码

<script type="text/javascript">
            //调用jquery-getUrlParam.js文件的getUrlParam方法获取商品id
            var id = $.getUrlParam("id");
            console.log("id=" + id);
            $(document).ready(function() {
                $.ajax({
                    url: "/products/" + id + "/details",
                    type: "GET",
                    dataType: "JSON",
                    success: function(json) {
                        if (json.state == 200) {
                            console.log("title=" + json.data.title);
                            //html()方法:
                            // 假设有个标签<div id="a"></div>
                            //那么$("#a").html(<p></p>)就是给该div标签加p标签
                            //$("#a").html("我爱中国")就是给该div标签填充"我爱中国"内容
                            $("#product-title").html(json.data.title);
                            $("#product-sell-point").html(json.data.sellPoint);
                            $("#product-price").html(json.data.price);

                            for (var i = 1; i <= 5; i++) {
                                $("#product-image-" + i + "-big").attr("src", ".." + json.data.image + i + "_big.png");
                                $("#product-image-" + i).attr("src", ".." + json.data.image + i + ".jpg");
                            }
                        } else if (json.state == 4006) { // 商品数据不存在的异常
                            location.href = "index.html";
                        } else {
                            alert("获取商品信息失败!" + json.message);
                        }
                    }
                });
            });
        </script>

加入购物车

1.创建数据表

1.使用use命令先选中store数据库

USE store;

2.在store数据库中创建t_cart用户数据表

CREATE TABLE t_cart (
cid INT AUTO_INCREMENT COMMENT '购物车数据id',
uid INT NOT NULL COMMENT '用户id',
pid INT NOT NULL COMMENT '商品id',
price BIGINT COMMENT '加入时商品单价',
num INT COMMENT '商品数量',
created_user VARCHAR(20) COMMENT '创建人',
created_time DATETIME COMMENT '创建时间',
modified_user VARCHAR(20) COMMENT '修改人',
modified_time DATETIME COMMENT '修改时间',
PRIMARY KEY (cid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.创建购物车的实体类

在entity包下创建购物车的Cart实体类并使其继承BaseEntity

/**购物车数据的实体类*/
public class Cart extends BaseEntity {
    private Integer cid;
    private Integer uid;
    private Integer pid;
    private Long price;
    private Integer num;
/**
 * get,set
 * equals和hashCode
 * toString
 */
}

3.加入购物车-持久层

3.1规划需要执行的SQL语句

1.向购物车表中插入商品数据的SQL语句

insert into t_cart (除了cid以外的所有字段) values (匹配的值列表);

2.如果当前商品已经在购物车存在,则直接更新商品即可

update t_cart set num=? where cid=?

3.在插入或者更新具体执行哪个语句,取决于数据库中是否有当前的这个购物车商品的数据,需要查询语句才能确定

select * from t_cart where uid=? and pid=?

3.2设计接口和抽象方法

在mapper包下创建CartMapper接口,并添加抽象方法

public interface CartMapper {
    
    /**
     * 插入购物车数据
     * @param cart 购物车数据
     * @return 受影响的行数
     */
    Integer insert(Cart cart);

    /**
     * 修改购物车数据中商品的数量
     * @param cid 购物车数据的id
     * @param num 新的数量
     * @param modifiedUser 修改执行人
     * @param modifiedTime 修改时间
     * @return 受影响的行数
     */
    Integer updateNumByCid(
            @Param("cid") Integer cid,
            @Param("num") Integer num,
            @Param("modifiedUser") String modifiedUser,
            @Param("modifiedTime") Date modifiedTime);

    /**
     * 根据用户id和商品id查询购物车中的数据
     * @param uid 用户id
     * @param pid 商品id
     * @return 匹配的购物车数据,如果该用户的购物车中并没有该商品,则返回null
     */
    Cart findByUidAndPid(
            @Param("uid") Integer uid,
            @Param("pid") Integer pid);
}

3.3编写映射

在resources.mapper文件夹下创建CartMapper.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">
<mapper namespace="com.cy.store.mapper.CartMapper">
    <resultMap id="CartEntityMap" type="com.cy.store.entity.Cart">
        <id column="cid" property="cid"/>
        <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>

    <!-- 插入购物车数据-->
    <insert id="insert" useGeneratedKeys="true" keyProperty="cid">
        insert into t_cart (uid, pid, price, num, created_user, created_time, modified_user, modified_time)
        values (#{uid}, #{pid}, #{price}, #{num}, #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime})
    </insert>

    <!-- 修改购物车数据中商品的数量-->
    <update id="updateNumByCid">
        update t_cart set
            num=#{num},
            modified_user=#{modifiedUser},
            modified_time=#{modifiedTime}
        where cid=#{cid}
    </update>

    <!-- 根据用户id和商品id查询购物车中的数据-->
    <select id="findByUidAndPid" resultMap="CartEntityMap">
        select * from t_cart where uid=#{uid} AND pid=#{pid}
    </select>
</mapper>

3.4单元测试

创建CartMapperTests测试类进行测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class CartMapperTests {
    @Autowired
    private CartMapper cartMapper;

    @Test
    public void insert() {
        Cart cart = new Cart();
        cart.setUid(11);
        cart.setPid(10000001);
        cart.setNum(3);
        cart.setPrice(4L);//长整型
        cartMapper.insert(cart);
    }

    @Test
    public void updateNumByCid() {
        cartMapper.updateNumByCid(1, 4, "张三", new Date());
    }

    @Test
    public void findByUidAndPid() {
        Cart cart = cartMapper.findByUidAndPid(11, 10000001);
        System.out.println(cart);
    }
}

4.加入购物车-业务层

4.1规划异常

在插入数据时,可能抛出InsertException异常;在修改数据时,可能抛出UpdateException异常.这两个异常已开发

4.2设计接口和抽象方法及实现

1.在com.cy.store.service包下创建ICartService接口,并添加抽象方法

该抽象方法都需要哪些参数呢,还是依据持久层,看持久层三条sql语句的实现需要什么参数:

findByUidAndPid:查询购物车数据,参数是uid,pid

insert:插入购物车数据,参数是cart对象(属性有cid,uid,pid,price,num)

updateNumByCid:修改购物车中商品数量,参数是cid,num,modifiedUser,modifiedTime

price可以通过业务层中调用ProductMapper接口的findById获取,modifiedTime在业务层实现类的内部创建,所以需要的参数是uid,pid,num,username

经过这次分析结合以前给业务层方法声明参数,可以发现即使持久层的方法参数是实体类对象,业务层的方法参数也大多不是实体类对象,因为实体类的部分属性是可以在业务层进行拼接然后封装到实体类对象中,再传给持久层(比如这里的price),这样的话就降低了前端传递数据的压力,如果该对象的所有方法都必须由前端传递过来,那么业务层方法参数可以是实体类对象(如注册用户时业务层的方法参数就是User对象)

public interface ICartService {
    /**
     * 将商品添加到购物车
     * @param uid 当前登录用户的id
     * @param pid 商品的id
     * @param amount 增加的数量
     * @param username 当前登录的用户名
     */
    void addToCart(Integer uid, Integer pid, Integer amount, String username);
}

2.创建CartServiceImpl类,并实现ICartService接口.在类中声明CartMapper持久层对象和IProductService处理商品数据的业务对象,并实现业务层的抽象方法

@Service
public class CartServiceImpl implements ICartService {
    /**购物车的业务层依赖于购物车的持久层以及商品的持久层*/
    @Autowired
    private CartMapper cartMapper;
    @Autowired
    private ProductMapper productMapper;

    @Override
    public void addToCart(Integer uid, Integer pid, Integer amount, String username) {

        //根据参数pid和uid查询购物车中该商品是否已经存在
        Cart result = cartMapper.findByUidAndPid(uid, pid);

        Integer cid = result.getCid();
        Date date = new Date();
        if (result == null) {
            Cart cart = new Cart();

            //封装数据:uid,pid,amount
            cart.setUid(uid);
            cart.setPid(pid);
            cart.setNum(amount);//注意前端传来amount时并没有和数据库商品数量进行求和

            //查询商品数据,得到商品价格并封装
            Product product = productMapper.findById(pid);
            cart.setPrice(product.getPrice());

            //封装数据:4个日志
            cart.setCreatedUser(username);
            cart.setCreatedTime(date);
            cart.setModifiedUser(username);
            cart.setModifiedTime(date);

            Integer rows = cartMapper.insert(cart);
            if (rows != 1) {
                throw new InsertException("插入数据时出现未知异常");
            }
        } else {
            //从查询结果中取出原数量,与参数amount相加,得到新的数量
            Integer num = result.getNum() + amount;//加入购物车时只会有+不可能有-

            Integer rows = cartMapper.updateNumByCid(
                result.getCid(),
                num,
                username,
                date);
            if (rows != 1) {
                throw new InsertException("更新数据时产生未知异常");
            }
        }
    }
}

4.3单元测试

创建测试类CartServiceTests并编写测试方法。

@RunWith(SpringRunner.class)
@SpringBootTest
public class CartServiceTests {
    @Autowired
    private ICartService cartService;

    @Test
    public void addToCart() {
        cartService.addToCart(11, 10000002, 5, "Tom");
    }
}

5.加入购物车-控制层

5.1处理异常

InsertException异常和UpdateException异常都已经设置到BaseController类中了,这里无需重复开发

5.2设计请求

  • /carts/add_to_cart
  • post
  • Integer pid, Integer amount, HttpSession session
  • JsonResult

5.3处理请求

在controller包下创建CartController类并继承BaseController类,在类中添加处理请求的addToCart()方法

@RestController
@RequestMapping("carts")
public class CartController extends BaseController {
    @Autowired
    private ICartService cartService;

    @RequestMapping("add_to_cart")
    public JsonResult<Void> addToCart(Integer pid, Integer amount, HttpSession session) {
        cartService.addToCart(
                getUidFromSession(session),
                pid,
                amount,
                getUsernameFromSession(session));
        return new JsonResult<Void>(OK);
    }
}

启动服务,登录账号后在地址栏输入http://localhost:8080/carts/add_to_cart?pid=10000002&amount=5进行测试

6.加入购物车-前端页面

在product.html页面中的body标签内的script标签里为“加入购物车”按钮添加点击事件

回顾一下在ajax函数中data参数的数据设置的方式

  • data:$(“选择的form表单”).serialize()。当需要提交的参数过多并且在同一个表单中时使用

  • data:new FormData($(“选择的form表单”)[0])。只适用提交文件

  • data:“username=TOM”。手动拼接,适合参数值固定并且参数值列表有限.等同于

    var user = "控件某属性值或控件文本内容或自己声明的值"
    data: "username="+user
    
  • 使用JSON格式提交数据

    data: {
        "username": "Tom",
        "age": 18
    }
    
  • 使用RestFul风格不属于前端给后端传参数

这里表单里面有很多无用参数,所以不使用表单提交

$("#btn-add-to-cart").click(function() {
    $.ajax({
        url: "/carts/add_to_cart",
        type: "POST",
        data: {
            "pid": id,
            "amount": $("#num").val()
        },
        dataType: "JSON",
        success: function(json) {
            if (json.state == 200) {
                alert("增加成功!");
            } else {
                alert("增加失败!" + json.message);
            }
        },
        error: function(xhr) {
            alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + xhr.status);
            location.href = "login.html";
        }
    });
});

点击"加入购物车"按钮后页面跳转的实现:product.html导入的product.js文件里面实现了点击后跳转

显示购物车列表

1.显示购物车列表-持久层

1.1规划需要执行的SQL语句

这里需要将商品表和购物车表进行连表查询

显示某用户的购物车列表数据的SQL语句大致是

select
cid, #日后勾选购物车商品模块需要用到cid来确定勾选的是购物车表的哪一条数据

uid, #感觉没必要,因为uid可以从session中拿的呀,难道是为
#了后面提交购物车订单时判断提交的商品的uid和登录的uid是否一致?

pid, #日购提交订单模块需要用到pid来确定购买的是商品表的哪件商
#品,然后对商品表的该商品的库存,销售热度等信息进行修改

t_cart.price, #两个表都有该字段,需要指定获取的是哪个数据表的

t_cart.num, #两个表都有该字段且含义不同,需要指定获取的是哪个数据表的

title,

t_product.price as realPrice, #为了在购物车列表页展示两个价格的差值

image

from t_cart
left join t_product on t_cart.pid = t_product.id #把t_cart作为主表(老师说现在处理的是购物车表的数据所以让其为主表,我不明白)
where
uid = #{uid}
order by
t_cart.created_time desc #进行排序使最新加入购物车的在最上面

1.2设计接口和抽象方法

VO全称Value Object,值对象。当进行select查询时,查询的结果属于多张表中的内容,此时发现结果集不能直接使用某个POJO实体类来接收,因为POJO实体类不能包含多表查询出来的信息,解决方式是:重新去构建一个新的对象,这个对象用于存储所查询出来的结果集对应的映射,所以把这个对象称之为值对象.

在store包下创建一个vo包,在该包下面创建CartVO类,不需要继承BaseController类,那相应的就需要单独实现Serializable接口

/** 购物车数据的Value Object类 */
public class CartVO implements Serializable {
    private Integer cid;
    private Integer uid;
    private Integer pid;
    private Long price;
    private Integer num;
    private String title;
    private Long realPrice;
    private String image;
/**
 * get,set
 * equals和hashCode
 * toString
 */
}

2.在CartMapper接口中添加抽象方法

/**
 * 查询某用户的购物车数据
 * @param uid 用户id
 * @return 该用户的购物车数据的列表
 */
List<CartVO> findVOByUid(Integer uid);

1.3编写映射

1.在CartMapper.xml文件中添加findVOByUid()方法的映射

<!-- 查询某用户的购物车数据:List<CartVO> findVOByUid(Integer uid) -->
<select id="findVOByUid" resultType="com.cy.store.vo.CartVO">
    select
        cid,
        uid,
        pid,
        t_cart.price,
        t_cart.num,
        title,
        t_product.price as realPrice,
        image
    from t_cart
    left join t_product on t_cart.pid = t_product.id
    where
    uid = #{uid}
    order by
    t_cart.created_time desc
</select>

1.4单元测试

在CartMapperTests测试类中添加findVOByUid()方法的测试

@Test
public void findVOByUid() {
    List<CartVO> list = cartMapper.findVOByUid(11);
    System.out.println(list);
}

2.显示购物车列表-业务层

2.1 规划异常

查询不到就返回空,不需要规划异常

2.2设计接口和抽象方法及实现

1.在ICartService接口中添加findVOByUid()抽象方法

/**
 * 查询某用户的购物车数据
 * @param uid 用户id
 * @return 该用户的购物车数据的列表
 */
List<CartVO> getVOByUid(Integer uid);

2.在CartServiceImpl类中重写业务接口中的抽象方法

@Override
public List<CartVO> getVOByUid(Integer uid) {
    return cartMapper.findVOByUid(uid);
}

2.3单元测试

该业务层只是调用了持久层的方法并返回,可以不再测试

3.显示购物车列表-控制层

3.1 处理异常

业务层没有抛出异常,所以这里不需要处理异常

3.2 设计请求

  • /carts/
  • GET
  • HttpSession session
  • JsonResult<List>

3.3 处理请求

在CartController类中编写处理请求的代码。

@RequestMapping({"", "/"})
public JsonResult<List<CartVO>> getVOByUid(HttpSession session) {
    List<CartVO> data = cartService.getVOByUid(getUidFromSession(session));
    return new JsonResult<List<CartVO>>(OK, data);
}

启动服务,登录后在地址栏输入http://localhost:8080/carts进行测试

4.显示购物车列表-前端页面

1.将cart.html页面的head头标签内引入的cart.js文件注释掉(这个就是文件的功能:点击"+“,”-",“删除”,"全选"等按钮时执行相应的操作)

<!-- <script src="../js/cart.js" type="text/javascript" charset="utf-8"></script> -->

多说一下,form标签的action="orderConfirm.html"属性(规定表单数据提交到哪里)和结算按钮的类型"type=submit"是必不可少的,这样点击"结算"时才能将数据传给"确认订单页"并在"确认订单页"展示选中的商品数据

当然也可以把这两个删掉,然后给结算按钮添加"type=button"然后给该按钮绑定一个点击事件实现页面跳转和数据传递,但是这样太麻烦了

2.在cart.html页面body标签内的script标签中编写展示购物车列表的代码

<script type="text/javascript">
    $(document).ready(function() {
    showCartList();
});

    //展示购物车列表数据
    function showCartList() {
        $("#cart-list").empty();
        $.ajax({
            url: "/carts",
            type: "GET",
            dataType: "JSON",
            success: function(json) {
                var list = json.data;
                for (var i = 0; i < list.length; i++) {
                    var tr = '<tr>\n' +
                        '<td>\n' +
                        '<input name="cids" value="#{cid}" type="checkbox" class="ckitem" />\n' +
                        '</td>\n' +
                        '<td><img src="..#{image}collect.png" class="img-responsive" /></td>\n' +
                        '<td>#{title}#{msg}</td>\n' +
                        '<td>¥<span id="goodsPrice#{cid}">#{singlePrice}</span></td>\n' +
                        '<td>\n' +
                        '<input type="button" value="-" class="num-btn" οnclick="reduceNum(1)" />\n' +
                        '<input id="goodsCount#{cid}" type="text" size="2" readonly="readonly" class="num-text" value="#{num}">\n' +
                        '<input class="num-btn" type="button" value="+" οnclick="addNum(#{cid})" />\n' +
                        '</td>\n' +
                        '<td><span id="goodsCast#{cid}">#{totalPrice}</span></td>\n' +
                        '<td>\n' +
                        '<input type="button" οnclick="delCartItem(this)" class="cart-del btn btn-default btn-xs" value="删除" />\n' +
                        '</td>\n' +
                        '</tr>';
                    tr = tr.replaceAll(/#{cid}/g, list[i].cid);
                    tr = tr.replaceAll(/#{image}/g, list[i].image);
                    tr = tr.replaceAll(/#{title}/g, list[i].title);
                    tr = tr.replaceAll(/#{singlePrice}/g, list[i].realPrice);
                    tr = tr.replaceAll(/#{num}/g, list[i].num);
                    tr = tr.replaceAll(/#{totalPrice}/g, list[i].realPrice * list[i].num);
                    if (list[i].realPrice < list[i].price) {
                        tr = tr.replace(/#{msg}/g, "比加入时降价" + (list[i].price - list[i].realPrice) + "元");
                    } else {
                        tr = tr.replace(/#{msg}/g, "");
                    }
                    $("#cart-list").append(tr);
                }
            },
            error: function (xhr) {
                alert("加载购物车列表数据时产生未知的异常"+xhr.status);
            }
        });
    }
</script>

这tr变量是怎么声明的呢:

先敲下var=‘’;然后在上面的html里面找到tbody下的任意一个tr标签复制在单引号里面,然后删掉制表符.最后对该字符串稍加改动:

1.第18行name=“cids” value="#{cid}"是为"点击结算按钮跳转到确认订单页面"模块做准备。这两个属性都是自己添加的,在tbody复制的tr标签里面没有,这两个属性是为了跳转到"确认订单页"时能够携带该参数(比如传递cids=1)

2.第26οnclick="addNum(#{cid})“是为"在购物车列表增加商品数量"模块做准备。是为了点击”+"后能调用addNum函数并传入对应的cid

3.第22行id="goodsPrice#{cid}"和第25行id="goodsCount#{cid}"和第28行id="goodsCast#{cid}"都是为"在购物车列表增加商品数量"模块做准备。在后端更新完商品数量相应的前端页面也要更新:

  • 根据id="goodsCount#{cid}"获取数量相关的控件后更新其value属性的值(value属性用.val()赋值)
  • 根据id="goodsPrice#{cid}"获取价格相关的控件后拿到其单价
  • 将单价和数量相乘后,根据id="goodsCast#{cid}"获取总价相关的控件并更新其文本值(文本用.html()更新)

4.上面这三条都是和本模块无关的,其余的修改都是和本模块相关的,在tbody复制的tr标签里面都有,比葫芦画瓢就可以了

点击"结算"按钮页面跳转的实现:在cart.html页面点击"结算"后会跳转到"确认订单页"并将表单中的数据作为参数传递给"确认订单页"

增加商品数量

购物车详情页点击"+“”-"修改商品数量时必须和数据库进行交互,因为这是即使展示给用户的,不能说用户看到的数量是5,结果数据库的购物车表中的数量是4吧?

但是在商品详情页点击"+“”-“修改商品数量时可以不用和数据库进行交互而是等到用户点击"加入购物车"后再进行交互,因为在用户点击"加入购物车"之前并不需要将商品数量更新到购物车表,可以去看看这个项目的商品详情页,那里点击”+“”-“修改商品数量时就是js实现的,并没有和数据库交互.(如果加一个模块:商品详情页点击”+“”-"时要知道库存够不够用户选择的这个数量,此时就需要和数据库交互了)

1.增加购物车商品数量-持久层

1.1规划需要执行的SQL语句

1.更新该商品的数量.此SQL语句无需重复开发

2.首先进行查询需要操作的购物车数据信息

SELECT * FROM t_cart WHERE cid=?

1.2设计接口和抽象方法

在CartMapper接口中添加抽象方法

Cart findByCid(Integer cid);

1.3编写映射

在CartMapper文件中添加findByCid(Integer cid)方法的映射

<select id="findByCid" resultMap="CartEntityMap">
    select * from t_cart where cid=#{cid}
</select>

1.4单元测试

在CartMapperTests测试类中添加findByCid()测试方法

@Test
public void findByCid() {
    System.out.println(cartMapper.findByCid(1));
}

2.增加购物车商品数量-业务层

2.1规划异常

  • 在更新时产生UpdateException未知异常,此异常类无需再次创建
  • 可能该购物车列表数据归属不是登录的用户,抛AccessDeniedException异常,此异常类无需再次创建
  • 要查询的数据不存在.抛出CartNotFoundException异常,创建该异常类并使其继承ServiceException
/** 购物车数据不存在的异常 */
public class CartNotFoundException extends ServiceException {
    /**重写ServiceException的所有构造方法*/
}

2.2设计接口和抽象方法及实现

在业务层ICartService接口中添加addNum()抽象方法

1.先判断需要哪些参数,该抽象方法的实现依赖于CartMapper接口的两个方法:

updateNumByCid方法.参数是cid,num,String modifiedUser,Date modifiedTime

findByCid方法.参数是cid

在业务层中从购物车表查询到该商品的数量,然后再和前端传过来的增加的数量进行求和得到num

所以该方法的参数是cid,uid,username

2.判断一下该方法的返回值:

  • 该方法返回值void.这样的话就需要在前端页面加location.href使该页面自己跳转到自己,实现刷新页面(不建议,每次都加载整个页面,数据量太大了)
  • 返回值是Integer类型.这样的话就把数据库中更新后的数量层层传给前端,前端接收后填充到控件中就可以了
/**
* 增加用户的购物车中某商品的数量
* @param cid
* @param uid
* @param username
* @return 增加成功后新的数量
*/
Integer addNum(Integer cid,Integer uid, String username);

3.在CartServiceImpl类中实现接口中的抽象方法

@Override
public Integer addNum(Integer cid, Integer uid, String username) {
    Cart result = cartMapper.findByCid(cid);
    if (result == null) {
        throw new CartNotFoundException("数据不存在");
    }
    if (!result.getUid().equals(uid)) {
        throw new AccessDeniedException("数据非法访问");
    }
    Integer num = result.getNum() + 1;
    Integer rows = cartMapper.updateNumByCid(cid, num, username, new Date());
    if (rows != 1) {
        throw new UpdateException("更新数据时产生未知异常");
    }
    return num;
}

2.3单元测试

就接收个参数,然后业务层将其加一后返回,不需要再测了

3.增加购物车商品数量-控制层

3.1处理异常

在BaseController类中添加CartNotFoundException异常类的统一管理

else if (e instanceof CartNotFoundException) {
    result.setState(4007);
    result.setMessage("购物车表不存在该商品的异常");
}

3.2设计请求

  • /carts/{cid}/num/add
  • post
  • @PathVariable(“cid”) Integer cid, HttpSession session
  • JsonResult

3.3处理请求

在CartController类中添加处理请求的addNum()方法

@RequestMapping("{cid}/num/add")
public JsonResult<Integer> addNum(@PathVariable("cid") Integer cid, HttpSession session) {
    Integer data = cartService.addNum(
        cid,
        getUidFromSession(session),
        getUsernameFromSession(session));
    return new JsonResult<Integer>(OK, data);
}

启动服务,登录后在地址栏输入http://localhost:8080/carts/1/num/add进行验证

4.增加购物车商品数量-前端页面

1.首先确定在showCartList()函数中动态拼接的增加购物车按钮是绑定了addNum()事件,如果已经添加无需重复添加

<input class="num-btn" type="button" value="+" onclick="addNum(#{cid})" />

2.在script标签中定义addNum()函数并编写增加购物车数量的逻辑代码

function addNum(cid) {
    $.ajax({
        url: "/carts/"+cid+"/num/add",
        type: "POST",
        dataType: "JSON",
        success: function (json) {
            if (json.state == 200) {
                $("#goodsCount"+cid).val(json.data);//字符串+整数cid后结果为字符串

                //更新该商品总价
                /*
                            html()方法:
                            不传参:是获取某个标签内部的内容(文本或标签)
                            传参:将参数放到标签里面替换掉该标签原有内容
                            * */
                var price = $("#goodsPrice"+cid).html();
                var totalPrice = price * json.data;

                //将商品总价更新到控件中
                $("#goodsCast"+cid).html(totalPrice);
            } else {
                alert("增加购物车商品数量失败"+json.message);
            }
        },
        error: function (xhr) {
            alert("增加购物车商品数量时产生未知的异常!"+xhr.message);
        }
    });
}

确认订单

1.确认订单-持久层

1.1规划需要执行的SQL语句

用户在购物车列表页中通过随机勾选相关的商品,在点击"结算"按钮后跳转到"确认订单页",在这个页面中需要展示用户在上个页面所勾选的"购物车列表页"中对应的数据.说白了也就是列表展示,且展示的内容还是来自于购物车表.但是用户勾选了哪些商品呢,所以"购物车列表页"需要将用户勾选的商品id传递给"确认订单页"

所以在持久层需要完成“根据若干个不确定的id值,查询购物车数据表,显示购物车中的数据信息”。则需要执行的SQL语句大致是。

select
    cid,
    uid,
    pid,
    t_cart.price,
    t_cart.num,
    title,
    t_product.price as realPrice,
    image
from t_cart
left join t_product on t_cart.pid = t_product.id
where
cid in (?,?,?)
order by
t_cart.created_time desc

注意where cid in (?,?,?),这里是需要传入cid的集合

1.2设计接口和抽象方法

在CartMapper接口中添加findVOByCids抽象方法

List<CartVO> findVOByCids(Integer[] cids);

1.3配置映射

1.在CartMapper.xml文件中添加SQL语句的映射配置

<select id="findVOByCids" resultType="com.cy.store.vo.CartVO">
    select
        cid,
        uid,
        pid,
        t_cart.price,
        t_cart.num,
        title,
        t_product.price as realPrice,
        image
    from t_cart
    left join t_product on t_cart.pid = t_product.id
    where
        cid in (
            <foreach collection="array" item="cid" separator=",">
                #{cid}
            </foreach>
            )
    order by
    t_cart.created_time desc
</select>

foreach循环就是一个for循环

  • collection标识循环的是list集合还是数组,如果是list集合就用collection=“list”
  • item用来接收每次循环获取的值
  • separator标识循环出来的值中间用什么隔开,且最后循环出来的值后面不加

1.4单元测试

在CartMapperTests测试类中添加findVOByCids方法进行测试

@Test
public void findVOByCids() {
    Integer[] cids = {1, 2, 6, 8, 100};//可以写表中不存在的,无非就是查不到数据,并不会报错
    List<CartVO> list = cartMapper.findVOByCids(cids);
    for (CartVO item : list) {
        System.out.println(item);
    }
}

2.确认订单-业务层

2.1规划异常

查询语句,没有需要规划的异常,在业务层判断这几条购物车商品的数据归属是否正确,如果不正确也不需要抛出异常,直接从查询到的数据中移除该商品就行了

2.2设计接口和抽象方法及实现

1.在ICartService接口中添加getVOByCids()抽象方法

List<CartVO> getVOByCids(Integer uid, Integer[] cids);//uid是为了判断数据归属是否正确

2.在CartServiceImpl类中重写业务接口中的抽象方法

@Override
public List<CartVO> getVOByCids(Integer uid, Integer[] cids) {
    List<CartVO> list = cartMapper.findVOByCids(cids);

    //可以使用for遍历,这里玩个新的,用迭代器遍历
    Iterator<CartVO> it = list.iterator();
    while (it.hasNext()) {

        //指向的是该元素之前,所以需要next得到该元素
        CartVO cart = it.next();

        if (!cart.getUid().equals(uid)) {
            /**
             * 不能用list.remove(cart)
             * 在迭代器进行遍历的时候不能使用集合的移除
             * 方法,需要用迭代器特有的移除方法
             */
            it.remove();
        }
    }
    return list;
}

2.3单元测试

业务层只是调用持久层获取数据并判断归属是否正确,这里不再测试

3.确认订单-控制层

3.1处理异常

业务层没有抛出异常,所以不需要处理异常

3.2设计请求

  • /carts/list
  • GET
  • Integer[] cids, HttpSession session
  • JsonResult<List>

3.3处理请求

1.在CartController类中添加处理请求的getVOByCids()方法。

@RequestMapping("list")
public JsonResult<List<CartVO>> findVOByCids(Integer[] cids, HttpSession session) {
    List<CartVO> data = cartService.getVOByCids(getUidFromSession(session), cids);
    return new JsonResult<>(OK, data);
}

启动服务,登录后在地址栏输入http://localhost:8080/carts/list?cids=1&cids=5&cids=7进行测试

4.确认订单-前端页面

4.1显示勾选的购物车数据

1.检查cart.html页面,里面form标签的action="orderConfirm.html"属性(规定表单数据提交到哪里)和结算按钮的类型"type=submit"是必不可少的,这样点击"结算"时才能将数据传给"确认订单页"并在"确认订单页"展示选中的商品数据

2.在orderConfirm.html页面中实现自动加载从cart.html页面中传递过来的cids数据,再去请求ajax,然后将后端返回的数据填充在页面的某个区域中

3.orderConfirm.js文件中

  • $(“.link-pay”).click(……)作用:点击"在线支付"后跳转到支付页面,这个其实就是下个模块要做的"创建订单"功能,该功能需要和数据库交互,所以不是在前端实现的,所以这行代码无用

  • $(“.link-success”).click(…):在orderConfirm.html页面没有class为link-success的标签,所以这行代码不会被执行

    综上两条,orderConfirm.js文件在orderConfirm.html页面中无用,但存在可能会和下个模块"创建订单"功能冲突(下个模块会实现点击"创建订单"后页面跳转),所以注释掉


下面在orderConfirm.html页面编写js代码

<script type="text/javascript">
    $(document).ready(function() {
    showCartList();
});

    function showCartList() {
        $("#cart-list").empty();
        $.ajax({
            url: "/carts/list",
            type: "GET",
            data: location.search.substr(1),
            dataType: "JSON",
            success: function(json) {
                if (json.state == 200) {
                    var list = json.data;
                    console.log(location.search.substr(1));//调试用

                    //声明两个变量用于更新"确认订单"页的总件数和总价
                    var allCount = 0;
                    var allPrice = 0;

                    for (var i = 0; i < list.length; i++) {
                        var tr = '<tr>\n' +
                            '<td><img src="..#{image}collect.png" class="img-responsive" /></td>\n' +
                            '<td>#{title}</td>\n' +
                            '<td>¥<span>#{price}</span></td>\n' +
                            '<td>#{num}</td>\n' +
                            '<td><span>#{totalPrice}</span></td>\n' +
                            '</tr>';
                        tr = tr.replace("#{image}",list[i].image);
                        tr = tr.replace("#{title}",list[i].title);
                        tr = tr.replace("#{price}",list[i].realPrice);
                        tr = tr.replace("#{num}",list[i].num);
                        tr = tr.replace("#{totalPrice}",list[i].realPrice*list[i].num);
                        $("#cart-list").append(tr);

                        //更新"确认订单"页的总件数和总价
                        allCount += list[i].num;
                        allPrice += list[i].realPrice*list[i].num;
                    }
                    $("#all-count").html(allCount);
                    $("#all-price").html(allPrice);
                }
            },
            error: function (xhr) {
                alert("在确认订单页加载勾选的购物车数据时发生未知的异常"+xhr.status);
            }
        });
    }
</script>

1.为什么点击购物车列表页面的"结算"按钮后地址栏中会请求http://localhost:8080/web/orderConfirm.html?cids=6&cids=5呢,因为该按钮有一个type=submit属性,且表单有一个action="orderConfirm.html"属性,所以点击该按钮后会携带表单中参数自动跳转

会携带哪些参数呢:把表单中有name属性的标签的value值传递出去,针对这个请求传递的是name"cids",其value值根据勾选的商品而定,可以是1或3或10

2.data: location.search.substr(1)这个API的参数为0表示截取地址栏中?后面的数据,即参数

如果这个API的参数为0则表示截取地址栏中?前面的数据,即请求地址

4.2显示选择收货地址

收货地址存放在前端的一个select下拉列表中,我们需要将查询到的当前登录用户的收货地址动态的加载到这个下拉列表中.从数据库的角度看,是一个select查询语句,在"收货地址列表展示"模块已经编写了该持久层,业务层,控制层,所以这里只需要编写对应的前端页面就可以了

1.在orderConfirm.html页面中的ready函数中添加showAddressList方法的调用,使确认订单页加载时能够自动从后端获取该用户地址填充到select控件中并将第一个地址显示出来

$(document).ready(function() {
    showCartList();
    showAddressList();
});

2.在orderConfirm.html页面中编写showAddressList方法

function showAddressList() {
    $("#address-list").empty();
    $.ajax({
        url: "/addresses",
        type: "GET",
        dataType: "JSON",
        success: function(json) {
            if (json.state == 200) {
                var list = json.data;
                for (var i = 0; i < list.length; i++) {

                    /*
                                value="#{aid}"在该模块没有用,但是扔写上,只要是从数据库查到到的数据,都要让前端页
                                面的该条数据和id绑定(因为可能干别的什么时需要用到,就比如说下一个"创建订单"模块
                                就需要根据前端传给后端的aid查询用户选中的是哪一个地址然后将其加入订单表)
                     * */
                    var opt = '<option value="#{aid}">#{name}&nbsp;&nbsp;&nbsp;#{tag}&nbsp;&nbsp;&nbsp;#{provinceName}#{cityName}#{areaName}#{address}&nbsp;&nbsp;&nbsp;#{tel}</option>';
                    opt = opt.replace("#{aid}",list[i].aid);
                    opt = opt.replace("#{name}",list[i].name);
                    opt = opt.replace("#{tag}",list[i].tag);
                    opt = opt.replace("#{provinceName}",list[i].provinceName);
                    opt = opt.replace("#{cityName}",list[i].cityName);
                    opt = opt.replace("#{areaName}",list[i].areaName);
                    opt = opt.replace("#{address}",list[i].address);
                    opt = opt.replace("#{tel}",list[i].tel);

                    $("#address-list").append(opt);
                }
            }
        },
        error: function (xhr) {
            alert("在确认订单页加载用户地址时发生未知的异常"+xhr.status);
        }
    });
}

创建订单

1.创建数据表

1.使用use命令先选中store数据库。

USE store;

2.在store数据库中创建t_order和t_order_item数据表

针对该模块可以将t_order_item表和t_order表合并,但是以后可能开发某个模块可能单独用到t_order_item(比如用户查看订单时只需要t_order_item表就可以实现)所以,建议这两个表分开创建

CREATE TABLE t_order (
oid INT AUTO_INCREMENT COMMENT '订单id',
uid INT NOT NULL COMMENT '用户id',
recv_name VARCHAR(20) NOT NULL COMMENT '收货人姓名',
recv_phone VARCHAR(20) COMMENT '收货人电话',
recv_province VARCHAR(15) COMMENT '收货人所在省',
recv_city VARCHAR(15) COMMENT '收货人所在市',
recv_area VARCHAR(15) COMMENT '收货人所在区',
recv_address VARCHAR(50) COMMENT '收货详细地址',
total_price BIGINT COMMENT '总价',
status INT COMMENT '状态:0-未支付,1-已支付,2-已取消,3-已关闭,4-已完成',
order_time DATETIME COMMENT '下单时间',
pay_time DATETIME COMMENT '支付时间',
created_user VARCHAR(20) COMMENT '创建人',
created_time DATETIME COMMENT '创建时间',
modified_user VARCHAR(20) COMMENT '修改人',
modified_time DATETIME COMMENT '修改时间',
PRIMARY KEY (oid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE t_order_item (
id INT AUTO_INCREMENT COMMENT '订单中的商品记录的id',
oid INT NOT NULL COMMENT '所归属的订单的id',
pid INT NOT NULL COMMENT '商品的id',
title VARCHAR(100) NOT NULL COMMENT '商品标题',
image VARCHAR(500) COMMENT '商品图片',
price BIGINT COMMENT '商品价格',
num INT COMMENT '购买数量',
created_user VARCHAR(20) COMMENT '创建人',
created_time DATETIME COMMENT '创建时间',
modified_user VARCHAR(20) COMMENT '修改人',
modified_time DATETIME COMMENT '修改时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.创建用户的实体类

1.entity包下创建Order实体类并继承BaseEntity类

/** 订单数据的实体类 */
public class Order extends BaseEntity {
    private Integer oid;
    private Integer uid;
    private String recvName;
    private String recvPhone;
    private String recvProvince;
    private String recvCity;
    private String recvArea;
    private String recvAddress;
    private Long totalPrice;
    private Integer status;
    private Date orderTime;
    private Date payTime;
    /**
     * get,set
     * equals和hashCode
     * toString
     */
}    

2.在com.cy.store.entity包下创建OrderItem实体类并继承BaseEntity类

/** 订单中的商品数据 */
public class OrderItem extends BaseEntity {
    private Integer id;
    private Integer oid;
    private Integer pid;
    private String title;
    private String image;
    private Long price;
    private Integer num;
    /**
     * get,set
     * equals和hashCode
     * toString
     */
}    

3.创建订单-持久层

3.1规划需要执行的SQL语句

1.插入订单数据的SQL语句

inert into t_order (aid除外的所有字段) values (字段的值)

2.插入某一个订单中商品数据的SQL语句

inert into t_order (id除外的所有字段) values (字段的值)

3.2实现接口和抽象方法

在mapper包下创建OrderMapper接口并在接口中添加抽象方法

public interface OrderMapper {
    /**
     * 插入订单数据
     * @param order 订单数据
     * @return 受影响的行数
     */
    Integer insertOrder(Order order);

    /**
     * 插入某一个订单中商品数据
     * @param orderItem 订单中商品数据
     * @return 受影响的行数
     */
    Integer insertOrderItem(OrderItem orderItem);
}

3.3编写映射

创建OrderMapper.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">
<mapper namespace="com.cy.store.mapper.OrderMapper">

    <!-- 插入订单数据 -->
    <insert id="insertOrder" useGeneratedKeys="true" keyProperty="oid">
        insert into t_order (
        uid, recv_name, recv_phone, recv_province, recv_city, recv_area, recv_address,
        total_price,status, order_time, pay_time, created_user, created_time, modified_user,
        modified_time
        ) values (
        #{uid}, #{recvName}, #{recvPhone}, #{recvProvince}, #{recvCity}, #{recvArea},
        #{recvAddress}, #{totalPrice}, #{status}, #{orderTime}, #{payTime}, #{createdUser},
        #{createdTime}, #{modifiedUser}, #{modifiedTime}
        )
    </insert>

    <!-- 插入订单商品数据 -->
    <insert id="insertOrderItem" useGeneratedKeys="true" keyProperty="id">
        insert into t_order_item (
        oid, pid, title, image, price, num, created_user,
        created_time, modified_user, modified_time
        ) values (
        #{oid}, #{pid}, #{title}, #{image}, #{price}, #{num}, #{createdUser},
        #{createdTime}, #{modifiedUser}, #{modifiedTime}
        )
    </insert>
</mapper>

3.4单元测试

创建OrderMapperTests测试类并添加测试方法

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderMapperTests {
    @Autowired
    private OrderMapper orderMapper;

    @Test
    public void insertOrder() {
        Order order = new Order();
        order.setUid(31);
        order.setRecvName("小王");
        order.setRecvPhone("133333");
        orderMapper.insertOrder(order);
    }

    @Test
    public void insertOrderItem() {
        OrderItem orderItem = new OrderItem();
        orderItem.setOid(1);
        orderItem.setPid(10000001);
        orderItem.setTitle("高档铅笔");
        orderMapper.insertOrderItem(orderItem);
    }
}

4.创建订单-业务层

4.1规划异常

无异常

4.2实现接口和抽象方法及实现

查看订单表的字段从而分析业务层方法需要哪些参数:

oid:主键自增,所以不需要该参数

uid:由控制层获取session中uid传给业务层,所以需要该参数
recv_name:通过"确认订单页"传递选中的地址aid,根据aid在在业务层调用已经声明的findByAid方法(该方法是在做"设置默认地址"模块时创建的,只在持久层创建了,并没有在业务层继续实现),所以需要参数aid
recv_phone:同上
recv_province:同上
recv_city:同上
recv_area:同上
recv_address:同上
total_price:根据前端传来的cids查询出每类商品数量和单价,然后相乘后求和,所以需要参数Integer[] cids
status:默认是0,所以不需要该参数
order_time:业务层实现方法内部可以声明,所以不需要该参数
pay_time:"创建订单"模块不需要此参数
created_user:由控制层获取session中username传给业务层,所以需要该参数
created_time:业务层实现方法内部可以声明,所以不需要该参数
modified_user:由控制层获取session中username传给业务层,所以需要该参数
modified_time:业务层实现方法内部可以声明,所以不需要该参数

综上分析,需要的参数是uid和aid,且需要在IAddressService接口添加getByAid()方法来获取选中的收货地址的详细数据:

1.在IAddressService接口中添加getByAid()方法

Address getByAid(Integer aid, Integer uid);

2.在AddressServiceImpl类中实现接口中的getByAid()抽象方法

@Override
public Address getByAid(Integer aid, Integer uid) {

    Address address = addressMapper.findByAid(aid);

    if (address == null) {
        throw new AddressNotFoundException("收货地址数据不存在的异常");
    }
    if (!address.getUid().equals(uid)) {
        throw new AccessDeniedException("非法访问");
    }
    address.setProvinceCode(null);
    address.setCityCode(null);
    address.setAreaCode(null);
    address.setCreatedUser(null);
    address.setCreatedTime(null);
    address.setModifiedUser(null);
    address.setModifiedTime(null);
    return address;
}

3.在service包下创建IOrderService业务层接口并添加抽象方法用于创建订单

public interface IOrderService {
    Order create(Integer aid, Integer[] cids, Integer uid, String username);
}

返回值是Order是因为还要在下个页面展示订单详细信息

4.在impl包下创建OrderServiceImpl并编写代码实现订单和订单中所有商品数据的插入操作

@Service
public class OrderServiceImpl implements IOrderService {

    @Autowired
    private OrderMapper orderMapper;

    //需要调用业务层的getByAid方法
    @Autowired
    private IAddressService addressService;

    //需要调用业务层的getVOByCids方法
    @Autowired
    private ICartService cartService;

    //需要调用业务层的getByUid方法
    private IUserService userService;

    @Override
    public Order create(Integer aid, Integer[] cids, Integer uid, String username) {

        //返回的列表中的对象都是即将下单的
        List<CartVO> list = cartService.getVOByCids(uid, cids);

        long totalPrice = 0L;
        for (CartVO cartVO : list) {
            totalPrice += cartVO.getRealPrice()*cartVO.getNum();

        }
        Address address = addressService.getByAid(aid, uid);
        Order order = new Order();
        order.setUid(uid);

        //封装收货地址
        order.setRecvName(address.getName());
        order.setRecvPhone(address.getPhone());
        order.setRecvProvince(address.getProvinceName());
        order.setRecvCity(address.getCityName());
        order.setRecvArea(address.getAreaName());
        order.setRecvAddress(address.getAddress());

        //封装创建时间,支付状态和总价
        order.setOrderTime(new Date());
        order.setStatus(0);
        order.setTotalPrice(totalPrice);

        //封装四个日志
        order.setCreatedUser(username);
        order.setCreatedTime(new Date());
        order.setModifiedUser(username);
        order.setModifiedTime(new Date());
        Integer rows = orderMapper.insertOrder(order);
        if (rows != 1) {
            throw new InsertException("插入数据时产生未知的异常");
        }

        //插入数据——将某条订单的所有商品的详细数据插入
        for (CartVO cartVO : list) {
            OrderItem orderItem = new OrderItem();

            /**
             * 此时获取的oid不为空,因为在配置文件里面开启了oid主
             * 键自增,所以上面的代码执行插入时就自动将oid赋值了
             */
            orderItem.setOid(order.getOid());

            orderItem.setPid(cartVO.getPid());
            orderItem.setTitle(cartVO.getTitle());
            orderItem.setImage(cartVO.getImage());
            orderItem.setPrice(cartVO.getRealPrice());
            orderItem.setNum(cartVO.getNum());

            orderItem.setCreatedUser(username);
            orderItem.setCreatedTime(new Date());
            orderItem.setModifiedUser(username);
            orderItem.setModifiedTime(new Date());

            rows = orderMapper.insertOrderItem(orderItem);
            if (rows != 1) {
                throw new InsertException("插入数据时产生未知的异常");
            }
        }
        return order;
    }
}

4.3单元测试

创建OrderServiceTests测试类并添加create()方法进行功能测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class OrderServiceTests {
    @Autowired
    private IOrderService orderService;

    @Autowired
    IUserService userService;

    @Test
    public void create() {
        Integer[] cids = {2,4,6};
        Order order = orderService.create(13, cids, 11, "小红");
        System.out.println(order);
    }
}

5.创建订单-控制层

5.1处理异常

没有异常需要处理

5.2设计请求

  • /orders/create
  • GET
  • Integer aid, Integer[] cids, HttpSession session
  • JsonResult

5.3处理请求

controller包下创建OrderController类,并继承自BaseController类,在类中编写请求方法

@RestController
@RequestMapping("orders")
public class OrderController extends BaseController {
    @Autowired
    private IOrderService orderService;

    @RequestMapping("create")
    public JsonResult<Order> create(Integer aid, Integer[] cids, HttpSession session) {
        Order data = orderService.create(
                aid,
                cids,
                getUidFromSession(session),
                getUsernameFromSession(session));
        return new JsonResult<>(OK,data);
    }
}

6.创建订单-前端页面

在"确认订单页"添加发送请求的处理方法使点击"在线支付"按钮可以创建订单并跳转到"支付信息页"(支付页显示详细商品信息这个功能这里不做了)

请求参数是通过字符串拼接得到的,那么就必须用get请求,因为post请求不能拼接字符串

$("#btn-create-order").click(function() {
    var aid = $("#address-list").val();//12
    var cids = location.search.substr(1);//cids=4&cids=6&cids=8
    $.ajax({
        url: "/orders/create",
        data: "aid=" + aid + "&" + cids,//aid=12&cids=4&cids=6&cids=8
        type: "GET",
        dataType: "JSON",
        success: function(json) {
            if (json.state == 200) {
                location.href = "payment.html";
            } else {
                alert("创建订单失败!" + json.message);
            }
        },
        error: function(xhr) {
            alert("创建订单数据时产生未知的异常" + xhr.status);
        }
    });
});

AOP

检测项目所有业务层方法的耗时(开始执行时间和结束执行时间只差值),再在不改变项目主体流程代码的前提条件下完成此功能,就要用到AOP

如果我们想对业务某一些方法同时添加相同的功能需求,并且在不改变业务功能逻辑的基础之上进行完成,就可以使用AOP的切面编程进行开发

1.Spring AOP

AOP:面向切面(Aspect)编程。AOP并不是Spring框架的特性(Spring已经被整合到了SpringBoot中,所以如果AOP是Spring框架的特性,那么就不需要手动导包,只需要在一个类上写@Aspect注解,鼠标放到该注解上按alt+enter就可以自动导包了,但是事与愿违,所以说AOP并不是Spring框架的特性),只是Spring很好的支持了AOP。

使用步骤:

  1. 首先定义一个类,将这个类作为切面类
  2. 在这个类中定义切面方法(5种:前置,后置,环绕,异常,最终)
  3. 将这个切面方法中的业务逻辑对应的代码进行编写和设计
  4. 通过连接点来连接目标方法,就是用粗粒度表达式和细粒度表达式来进行连接

2.切面方法

1.切面方法的访问权限是public。

2.切面方法的返回值类型可以是void或Object,如果该方法被@Around注解修饰,必须使用Object作为返回值类型,并返回连接点方法的返回值;如果使用的注解是@Before或@After等其他注解时,则自行决定。

3.切面方法的名称可以自定义。

4.切面方法可以接收参数,参数是ProccedingJoinPoint接口类型的参数.但是@Around所修饰的方法必须要传递这个参数.其他注解修饰的方法要不要该参数都可以

3 统计业务方法执行时长

1.因为AOP不是Spring内部封装的技术,所以需要进行导包操作:在pom.xml文件中添加两个关于AOP的依赖aspectjweaver和aspectjtools。

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjtools</artifactId>
</dependency>

2.在com.cy.store.aop包下创建TimerAspect切面类,给类添加两个注解进行修饰:

  • @Aspect(将当前类标记为切面类)
  • @Component(将当前类的对象创建使用维护交由Spring容器维护)
@Aspect
@Component
public class TimerAspect {
}

3.在类中添加切面方法,这里使用环绕通知的方式来进行编写

参数ProceedingJoinPoint接口表示连接点,也就是是目标方法的对象

public Object around(ProceedingJoinPoint pjp) throws Throwable {
    //开始时间
    long start = System.currentTimeMillis();
    //调用目标方法,比如login方法,getByUid方法
    Object result = pjp.proceed();
    //结束时间
    long end = System.currentTimeMillis();
    System.out.println("耗时:"+(end-start));
    return result;
}

4.将当前环绕通知映射到某个切面上,也就是指定连接的点.给around方法添加注解@Around

@Around("execution(* com.cy.store.service.impl.*.*(..))")
  • 第一个*表示方法返回值是任意的
  • 第二个*表示imp包下的类是任意的
  • 第三个*表示类里面的方法是任意的
  • (…)表示方法的参数是任意的

5.启动项目,在前端浏览器访问任意一个功能模块进行功能的测试