文章目录
项目环境搭建 1.项目分析
项目功能:登录,注册,热销商品,用户管理(密码,个人信息,头像,收货地址),购物车(展示,增加,删除),订单模块
开发顺序:注册,登录,用户管理,购物车,商品,订单模块
某一个模块的开发顺序:
持久层开发:依据前端页面的设置规划相关的 SQL 语句,以及进行配置
业务层开发:核心功能控制,业务操作以及异常的处理
控制层开发:接收请求,处理响应
前端开发:JS,Query,AJAX 这些技术来连接后台
2.项目基本环境
JDK:1.8 版本及以上
maven:需要配置到 idea,3.6.1 版本及以上
数据库:MariaDB,MySQL,要求是 5.1 版本及以上
开发的平台:idea 开发
项目名称:store,表示商城
结构:com.cy.store
资源文件:resources 文件夹下(static,templates)
单元测试:test.com.cy.store
3.项目创建
Create New Project->
选择 Spring Initializr,点击 next
跳转到 Project Metadata 页面,该页面的 Group 填写域 com 和自己起的域名 cy(即 com.cy)==;Artifact 填写项目名 store;==Java Version 版本选择自己安装的版本,点击 next
选择需要导入的 jar 包:
前后端的连接 jar 包:Web 目录下的 Spring web
mybatis 的 jar 包:SQL 目录下的 Mybatis Framework
mysql 数据库的驱动:SQL 目录下的 MySQL Driver
创建一个数据库
1 create database store character set utf8;
在 application.properties 文件中配置数据库的连接源信息
1 2 3 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 图形输出开始第二步测试:在单元测试中测试数据库的连接是否可以加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @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.选中数据表:
2.创建 t_user 表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 基类中
1 2 3 4 5 6 7 8 9 10 11 public class BaseEntity implements Serializable { private String createdUser; private Date createdTime; private String modifiedUser; private Date emodifiedTime; }
2.创建用户的实体类,并使其继承 BaseEntity 基类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; }
tips:
实体类 User 因为要在网络中以流的形式传输,所以需要 serialize 序列化(但因为其继承的父类 BaseEntity 已经实现序列化,所以就不需要再写 implements Serializable)
实体类 BaseEntity 中自动导入 Getter and Setter 方法,euqals()方法,hashCode()方法,toString 方法,其中 euqals()方法,hashCode()方法自动导入步骤:
enter+insert
点击 euqals() and hashCode()
勾选 Accept…和 Use 这两段话,并且选择 Template 为 IntelliJ Default
一路 next 到底
ssm 框架开发项目的时候需要在实体类上面加@Component 然后 spring 才能自动进行对象的创建维护,而 springboot 不再需要,因为 springboot 遵循的原则是约定大于配置,如果字段名称相同那就可以自动完成字段的初始化
3.注册-持久层 通过 Mybatis 来操作数据库,也就是在做 mybatis 开发的流程
3.1 规划需要执行的 SQL 语句 1.用户的注册功能,从后端持久层来看相当于在做数据的插入操作
1 inser into t_user (username)
2.在用户的注册时首先要去查询当前的用户名是否存在,如果存在则不能进行注册,相当于是一条查询语句
1 select * from t_user where username=?
3.2 设计接口和抽象方法及实现 1.定义 Mapper 接口.在项目的目录结构下首先创建一个 mapper 包,在这个包下再根据不同的功能模块来创建 mapper 接口.注册功能需要在 mapper 包下创建 UserMapper 接口然后定义上述两个 SQL 语句的抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public interface UserMapper { Integer insert (User user) ; User findByUsername (String username) ; }
2.ssm 框架开发项目的时候需要在 mapper 接口上加@Mapper 用于自动生成相应的接口实现类,在 springboot 也可以这样,但是后续会有很多 mapper 接口,每个接口分别加@Mapper 太麻烦了,所以在启动类类里面指定当前项目的 mapper 接口在哪,然后项目启动的时候会自动加载所有的接口
1 @MapperScan("com.cy.mapper")
3.3 编写映射 1.定义 xml 映射文件,与对应的接口进行关联.所有的映射文件都属于资源文件,需要放在 resources 目录下,为了管理方便我们在 resources 目录下创建一个 mapper 文件夹,然后在这个文件夹里存放 mapper 的映射文件
2.创建接口的映射文件,需要和接口的名称保持一致.如 UserMapper.xml
UserMapper.xml 的配置在 Mybatis 官网
1 2 3 4 5 6 7 8 <?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.UserMapper" > </mapper >
3.将配置接口的方法对应到 SQL 语句上
insert into () values (),因为 values 后面插入的值是动态值,mybatis 规定需要用占位符来占位,并给占位符起一个变量的名字,且变量的名字需要在占位符#{}内部
创建 t_user 表时 uid INT AUTO_INCREMENT COMMENT ‘用户 id’,中的 AUTO_INCREMENT 表示主键 uid 自增,所以需要 useGeneratedKeys 和 keyProperty
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <resultMap id ="UserEntityMap" type ="com.cy.store.entity.User" > <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 > <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 id ="findByUsername" resultMap ="UserEntityMap" > select * from t_user where username=#{username} </select >
sql 语句匹配规则:如果在 insert 标签里面写了 insert 语句,首先将 insert 语句和某一个方法进行绑定,用到了 id=“”,但是和哪里的方法进行绑定呢,就要用到 namespace=“”,这两步映射就把唯一的 SQL 语句和唯一的方法进行了关联,实际上就是 jdbc 里面 dao 接口的的:
1 2 3 Integer insert(User user) { String SQL = "insert into () values ()" ; }
用到映射的好处:使 SQL 语句和 java 代码分离,解耦了,方便后期代码的维护
4.将 mapper 文件的位置注册到 properties 对应的配置文件中.
在 application.properties 文件中增添:
1 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @SpringBootTest @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.怎么处理异常:
步骤一:在 ex 包下创建 ServiceException 类作为业务层异常的基类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 异常基类:
用户在进行注册时可能会产生用户名被占用的错误,这时需要抛出一个 UsernameDuplicatedException 异常
1 2 3 4 public class UsernameDuplicatedException extends ServiceException { }
正在执行数据插入操作的时候,服务器宕机或数据库宕机.这种情况是处于正在执行插入的过程中所产生的异常,起名 InsertException 异常
1 2 3 4 public class InsertException extends ServiceException { }
4.2 设计接口和抽象方法 1.在 service 包下创建 IUserService 接口(接口命名的默认规则:I+业务名字+层的名字)
1 2 3 4 5 6 7 8 public interface IUserService { void reg (User user) ; }
2.创建一个实现 UserServiceImpl 类,需要实现 IUserService 接口,并且实现抽象的方法
因为要将这个实现类交给 spring 管理,所以需要在类上加@Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Service public class UserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; @Override public void reg (User user) { String username = user.getUsername(); User result = userMapper.findByUsername(username); if (result != null ) { throw new UsernameDuplicatedException ("用户名被占用" ); } String oldpassword = user.getPassword(); String salt = UUID.randomUUID().toString().toUpperCase(); String md5Password = getMD5Password(oldpassword, salt); user.setSalt(salt); user.setPassword(md5Password); user.setIsDelete(0 ); user.setCreatedUser(user.getUsername()); user.setModifiedUser(user.getUsername()); Date date = new Date (); user.setCreatedTime(date); user.setModifiedTime(date); Integer rows = userMapper.insert(user); if (rows != 1 ) { throw new InsertException ("在用户注册过程中产生了未知的异常" ); } } }
md5 加密算法以后可能还要多次用到,为了方便在 UserServiceImpl 类里面单独写一个 getMD5Password 方法
1 2 3 4 5 6 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 即可,然后修改部分代码至如下这样)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @SpringBootTest @RunWith(SpringRunner.class) public class UserServiceTests { @Autowired private IUserService userService; @Test public void reg () { 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 中,将这个类作为方法的返回值返回给前端浏览器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class JsonResult <E> implements Serializable { private Integer state; private String message; 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(); } }
5.2 设计请求 接下来该向后端服务器发送请求以把用户数据插入到数据库,设计发送请求模块的第一步就是设计相关的请求
依据当前的业务功能模块进行请求的设计:
请求路径:/users/reg
请求参数:User user
请求类型:POST
响应结果:JsonResult
5.3 处理请求 创建一个控制层对应的 UserController 类,依赖于业务层的接口.编写完成后启动主服务验证一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @RestController @RequestMapping("users") public class UserController { @Autowired private IUserService userService; @RequestMapping("reg") public JsonResult<Void> reg (User user) { JsonResult<Void> result = new JsonResult <>(); try { 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 类作为控制层下类的基类,用来做统一的异常捕获:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class BaseController { public static final int OK = 200 ; @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 方法使该方法只需要关注请求处理而不再需要关注异常捕获:
1 2 3 4 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 接受多个参数时,参数与参数之间使用”,”分割
每一组参数之间使用”:”进行分割
参数的组成部分一个是参数的名称(不能随便定义),另一个是参数的值(必须用字符串来表示)
参数的声明顺序没有要求
演示一下语法结构:
1 2 3 4 5 6 7 8 $.ajax ({ url : "" , type : "" , data : "" , dataType : "" , success : function ( ) {}, error : function ( ) {}, });
3.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 结束之前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <script> $("#btn-reg" ).click (function ( ) { $.ajax ({ url : "/users/reg" , type : "POST" , data : $("#form-reg" ).serialize (), dataType : "JSON" , success : function (json ) { if (json.state == 200 ) { alert ("注册成功" ) } else { alert ("注册失败" ) } }, error : function (xhr ) { alert ("注册时产生未知的错误!" +xhr.status ); } }); }); </script>
此时可能会出现点击注册提交表单时没有任何响应,原因是 idea 对于 JS 代码的兼容性较差,编写了 js 代码但是有的时候不能正常去加载,解决办法有四种,同前面的:项目环境搭建->项目测试->测试静态资源能否正常加载
包括以后如果修改了前端页面,测试时没有报错也没有按照预想的响应,就考虑是编写的 js 代码还没有被加载,尝试用这四种方法解决
用户登录功能 先分析一下思路:当用户输入用户名和密码将数据提交给后台数据库进行查询,如果存在对应的用户名和密码则表示登录成功,登录成功之后跳转到系统的主页就是 index.html 页面,跳转在前端使用 jQuery 来完成
1.登录-持久层 规划需要执行的 SQL 语句 依据用户提交的用户名来做 select 查询
select * from t_user where username=? and password=?这种不太好,这种相当于在查询用户名时直接判断了用户和密码是否一致了,如果持久层把判断做了那业务层就没事干了,所以这里我们只查询用户名,判断用户名和密码是否一致交给业务层做
1 select * from t_user where username=?
分析完以后发现这个功能模块已经被开发完成(UserMapper 接口的 findByUsername 方法),所以就可以省略当前的开发步骤,但是这个分析过程不能省略
后续的设计接口和抽象方法,编写映射,单元测试都不再需要进行,
2.登录-业务层 2.1 规划异常
用户名对应的密码错误,即密码匹配的异常,起名 PasswordNotMatchException,这个是运行时异常
1 2 3 public class PasswordNotMatchException extends ServiceException { }
用户名没有被找到的异常,起名 UsernameNotFoundException,这个也是运行时异常
1 2 3 public class UsernameNotFoundException extends 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)
1 2 3 4 5 6 7 User login (String username,String password) ;
2.在抽象类 UserServiceImpl 中实现该抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Override public User login (String username, String password) { User result = userMapper.findByUsername(username); if (result == null ) { throw new UsernameNotFoundException ("用户数据不存在" ); } String oldPassword = result.getPassword(); String salt = result.getSalt(); String newMd5Password = getMD5Password(password, salt); if (!newMd5Password.equals(oldPassword)) { throw new PasswordNotMatchException ("用户密码错误" ); } if (result.getIsDelete() == 1 ) { throw new UsernameNotFoundException ("用户数据不存在" ); } User user = new User (); user.setUid(result.getUid()); user.setUsername(result.getUsername()); user.setAvatar(result.getAvatar()); return user; }
2.3 单元测试 在业务层的测试类 UserServiceTests 中添加测试方法:
1 2 3 4 5 6 @Test public void login () { User user = userService.login("test02" , "12" ); System.out.println(user); }
3.登录-控制层 3.1 处理异常 业务层抛出的异常需要在统一异常处理类中进行统一的捕获和处理,如果该异常类型已经在统一异常类中曾经处理过则不需要重复添加
1 2 3 4 5 6 7 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 类中编写处理请求的方法.编写完成后启动主服务验证一下
1 2 3 4 5 @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 标签:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <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 ("登录成功" ) location.href = "index.html" ; } else { alert ("登录失败" ) } }, error : function (xhr ) { 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 中来使用)
1 2 3 4 5 6 7 8 9 10 11 12 13 public final Integer getUidFromSession (HttpSession session) { 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 形参上:
6.拦截器
拦截器的作用是将所有的请求统一拦截到拦截器中,可以在拦截器中定义过滤的规则,如果不满足系统设置的过滤规则,该项目统一的处理是重新去打开 login.html 页面(重定向和转发都可以,推荐使用重定向)
拦截器在 springboot 中本质是依靠 springMVC 完成的.springMVC 提供了一个 HandlerInterceptor 接口用于表示定义一个拦截器
1.所以想要使用拦截器就要定义一个类并使其实现 HandlerInterceptor 接口,在 store 下建包 interceptor,包下建类 LoginInterceptor 并编写代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object obj = request.getSession().getAttribute("uid" ); if (obj == null ) { response.sendRedirect("/web/login.html" ); return false ; } return true ; } }
2.注册过滤器:
注册过滤器的技术:借助 WebMvcConfigure 接口将用户定义的拦截器进行注册.所以想要注册过滤器需要定义一个类使其实现 WebMvcConfigure 接口并在其内部添加黑名单(在用户登录的状态下才可以访问的页面资源)和白名单(哪些资源可以在不登录的情况下访问:①register.html②login.html③index.html④/users/reg⑤/users/login⑥ 静态资源):
WebMvcConfigure 是配置信息,建议在 store 包下建 config 包,再定义类 LoginInterceptorConfigure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Configuration public class LoginInterceptorConfigure implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { HandlerInterceptor interceptor = new LoginInterceptor (); 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("/**" ) .excludePathPatterns(patterns); } }
修改密码 初步分析:需要用户提交原始密码和新密码,再根据当前登录的用户进行信息的修改操作
1.修改密码-持久层 1.1 规划需要执行的 SQL 语句
根据用户的 uid 修改用户 password 值
1 update t_user set password= ?,modified_user= ?, modified_time= ? WHERE uid= ?
modified_user=?, modified_time=?是为了跟踪用户数据的变动,如果这条数据被错误修改了可以找到第一责任人
在执行修改密码之前,还应检查用户数据是否存在或者用户数据是否被标记为”已删除”(比如登录账号后的几分钟在和朋友聊天,没有看页面,管理员错误删除了你的账号或者错误设置 is_delete 为 1)、并检查原密码是否正确,这些检查都可以通过查询用户数据来辅助完成:
1 SELECT * FROM t_user WHERE uid= ?
1.2 设计接口和抽象方法 UserMapper 接口,将以上的两个方法的抽象定义出来,将来映射到 sql 语句上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Integer updatePasswordByUid (Integer uid, String password, String modifiedUser, Date modifiedTime) ;User findByUid (Integer uid) ;
1.3 编写映射 配置到映射文件 UserMapper.xml 中
1 2 3 4 5 6 7 8 9 10 11 <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 单元测试 1 2 3 4 5 6 7 8 9 10 11 12 13 @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 异常
1 2 3 4 public class UpdateException extends ServiceException { }
2.2 设计接口和抽象方法及实现 1.执行用户修改密码的核心方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 void changePassword (Integer uid, String username, String oldPassword, String newPassword) ;
2.在实现类中实现当前的抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Override public void changePassword (Integer uid, String username, String oldPassword, String newPassword) { User result = userMapper.findByUid(uid); 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 单元测试 1 2 3 4 @Test public void changePassword () { userService.changePassword(11 ,"管理员" ,"123" ,"321" ); }
3.修改密码-控制层 3.1 处理异常 UsernameNotFoundException 异常和 PasswordNotMatchException 异常在前面的章节中已经处理过,现在只需要把 UpdateException 异常配置到统一的异常处理方法中
1 2 3 4 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
3.3 处理请求 1 2 3 4 5 6 7 8 9 @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 请求的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <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 ) { alert ("修改密码时产生未知的异常!" +xhr.message ); } }); }); </script>
个人资料 其中用户名是不可修改的并且是在进个人资料页面时就从 session 获取值并将值加入到控件中
1.个人资料-持久层 1.1 规划 SQL 语句
获取用户信息的 SQL 语句
1 update t_user set python= ?,email= ?,gender= ?,modified_user= ?,modified_time= ? where uid= ?
根据 uid 查询用户数据
1 select * from t_user where uid= ?
根据 uid 查询用户数据不需要再重复开发
1.2 设计接口和抽象方法 更新用户的信息方法的定义
1 2 3 4 5 6 Integer updateInfoByUid (User user) ;
1.3 编写映射 在 UserMapper.xml 文件中进行映射编写
1 2 3 4 5 6 7 8 9 10 11 12 <update id ="updateInfoByUid" > update t_user set <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 单元测试 1 2 3 4 5 6 7 8 9 @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 查询数据的方法(持久层已经设计过该方法,但是没有在业务层实现该方法的调用)
1 2 3 4 5 6 7 8 9 10 11 User getByUid (Integer uid) ; void changeInfo (Integer uid,User user) ;
2.在实现类中实现当前的抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Override public User getByUid (Integer uid) { User result = userMapper.findByUid(uid); if (result == null || result.getIsDelete() == 1 ) { throw new UsernameNotFoundException ("用户数据不存在" ); } User user = new User (); user.setUsername(result.getUsername()); user.setPhone(result.getPhone()); user.setEmail(result.getEmail()); user.setGender(result.getGender()); return 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 单元测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void getByUid () { System.err.println(userService.getByUid(11 ).getUsername()); } @Test public void changeInfo () { User user = new 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.一打开页面就发送当前用户数据
1 2 3 4 5 @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.点击修改按钮更改用户数据
1 2 3 4 5 6 7 8 @RequestMapping("change_info") public JsonResult<Void> changeInfo (User user,HttpSession session) { 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),查询到的数据填充到这个页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <script> $(document ).ready (function ( ) { $.ajax ({ url : "/users/get_by_uid" , type : "GET" , 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" ); radio.prop ("checked" ,"checked" ); } else { alert ("用户的数据不存在" ) } }, error : function (xhr ) { alert ("查询用户信息时产生未知的异常!" +xhr.message ); } }); }); </script>
2.在检测到用户点击了修改按钮后发送一个 ajax 请求(change_info)
该 ajax 函数需要和上一个 ajax 同级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $("#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 ) { alert ("用户信息修改时产生未知的异常!" + xhr.message ); }, }); });
上传头像 错误方法:把文件存到数据库中,需要图片时访问数据库,数据库将文件解析为字节流返回,最后写到本地的某一个文件.这种方法太耗费资源和时间了
正确方法:将对应的文件保存在操作系统上,然后再把这个文件路径记录下来,因为在记录路径的时候是非常便捷和方便的,将来如果要打开这个文件可以依据这个路径找到这个文件,所以说在数据库中保存该文件的路径即可.
稍微大一点的公司都会将所有的静态资源(图片,文件,其他资源文件)放到某台电脑上,再把这台电脑作为一台单独的服务器使用
1.上传头像-持久层 1.1SQL 语句的规划 更新用户 avatar 字段的 sql 语句
1 update t_user set avatar= ?,modified_user= ?,modified_time= ? where uid= ?
1.2 设计接口和抽象方法 在 UserMapper 接口中定义一个抽象方法用于修改用户的头像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Integer updateAvatarByUid (@Param("uid") Integer iddddd,//@Param("参数名") 注解中的参数名需要和sql语句中 //的#{参数名}的参数名保持一致.该处表示iddddd中的变量值要注入到sql语句的uid中 String avatar, String modifiedUser, Date modifiedTime) ;
1.3 编写映射 UserMapper.xml 文件中编写映射的 SQL 语句
1 2 3 4 5 6 7 8 9 <update id ="updateAvatarByUid" > update t_user set avatar = #{avatar}, modified_user = #{modifiedUser}, modified_time = #{modifiedTime} where uid = #{uid} </update >
1.4 单元测试 1 2 3 4 5 6 7 8 @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 对象是在控制层的并不会出现在业务层,所以业务层要保留这两个参数,以便控制层可以传递过来
1 2 3 4 5 6 7 8 9 10 void changeAvatar (Integer uid, String avatar, String username) ;
2.编写业务层的更新用户头像的方法
1 2 3 4 5 6 7 8 9 10 11 12 @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 单元测试 1 2 3 4 @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 中进行编写和统一处理
1 2 3 4 5 6 7 8 9 10 11 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(不能是 JsonResult:如果上传头像后浏览别的页面,然后再回到上传头像的页面就展示不出来了,所以图片一旦上传成功,就要保存该图片在服务器的哪个位置,这样的话一旦检测到进入上传头像的页面就可以通过保存的路径拿到图片,最后展示在页面上)
3.4 处理请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 @RequestMapping("change_avatar") public JsonResult<String> changeAvatar (HttpSession session, MultipartFile file) { if (file.isEmpty()) { throw new FileEmptyException ("文件为空" ); } if (file.getSize()>AVATAR_MAX_SIZE) { throw new FileSizeException ("文件超出限制" ); } String contentType = file.getContentType(); if (!AVATAR_TYPE.contains(contentType)) { throw new FileTypeException ("文件类型不支持" ); } String parent = session.getServletContext().getRealPath("/upload" ); System.out.println(parent); File dir = new File (parent); if (!dir.exists()) { dir.mkdirs(); } String originalFilename = file.getOriginalFilename(); System.out.println("OriginalFilename=" +originalFilename); int index = originalFilename.lastIndexOf("." ); String suffix = originalFilename.substring(index); String filename = UUID.randomUUID().toString().toUpperCase()+suffix; File dest = new File (dir, filename); try { file.transferTo(dest); } catch (FileStateException e) { throw new FileStateException ("文件状态异常" ); } catch (IOException e) { 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 的上传头像的表单加上三个属性:
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 类型,表示所要配置的目标的元素
1 2 3 4 5 6 7 8 9 10 11 12 @Bean public MultipartConfigElement getMultipartConfigElement () { MultipartConfigFactory factory = new MultipartConfigFactory (); factory.setMaxFileSize(DataSize.of(10 , DataUnit.MEGABYTES)); factory.setMaxRequestSize(DataSize.of(15 ,DataUnit.MEGABYTES)); 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,//提交数据的形式,关闭默认提交数据的形式
下面给提交表单加上事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <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-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 文件
1 2 3 4 5 <script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8" ></script>
2.调用 cookie 方法保存路径
1 $.cookie (key, value, time);
在 ajax 请求原有的代码上加$.cookie(“avatar”,json.data.avatar,{expires: 7});
1 2 3 4 5 6 7 8 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 文件
1 2 3 4 5 <script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8" ></script>
4.在 upload.html 的 script 标签中加 ready()自动读取 cookie 数据
1 2 3 4 5 $(document ).ready (function ( ) { var avatar = $.cookie ("avatar" ); console .log (avatar); $("#img-avatar" ).attr ("src" , avatar); });
5.4 显示最新头像 上传头像后不重新登录而是浏览其他页面,然后再进入个人头像页面时展示的头像是上次上传的,因为此时 cookie 中的值是上次上传的头像的路径,所以需要上传头像后使用同名覆盖更改 cookie 中路径
在 ajax 函数的 success 属性值的 if 语句加:
1 $.cookie ("avatar" , json.data , { expires : 7 });
完善后重启服务测试,结果若和预测的不一样,则参考项目环境搭建->项目测试->测试静态资源能否正常加载里面的四种解决方法
新增收货地址 1.创建数据表 1.选中数据表
2.在 store 数据库中创建 t_address 表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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; }
3.新增收货地址-持久层 3.1 各功能的开发顺序 当前收货地址功能模块:
第一个页面:列表的展示,修改,删除,设置默认
第二个页面:新增收货地址
开发顺序:新增收货地址->列表的展示->设置默认收货地址->删除收货地址->修改收货地址
3.2 规划需要执行的 SQL 语句 1.新增收货地址对应的是插入语句:
1 insert into t_address (aid以外的所有字段) values (字段值)
2.大部分平台都会规定一个用户的收货地址数量,这里规定最多 20 个.那么在插入用户新的地址之前就要先做查询操作.如果查询到的是刚好 20,这并不是一个 java 语法的异常,可以认为是业务控制的异常,这个异常随后在 service 抛,在 controller 捕获
1 select count (* ) from t_address where uid= ?
3.3 设计接口和抽象方法 创建接口 AddressMapper,在这个接口中定义上面两个 SQL 语句抽象方法定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public interface AddressMapper { Integer insert (Address address) ; 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”
结果如下所示:
1 2 3 4 5 6 <?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 类属性与数据库中表的字段映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <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 中配置以上两个抽象方法的映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 < 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 > < ! < select id= "countByUid" resultType= "java.lang.Integer"> select count (* ) from t_address where uid= #{uid} < / select >
3.5 单元测试 在 test 下的 mapper 文件夹下创建 AddressMapperTests 测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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
1 2 3 4 public class AddressCountLimitException extends ServiceException { }
插入数据时产生未知的异常 InsertException(已经有了,不需要重复创建)
4.2 设计接口和抽象方法及实现 1.创建一个 IAddressService 接口,在接口中定义业务的抽象方法
因为 mapper 层接口该功能模块定义了两个抽象方法,所以就要在 service 层接口该功能模块也定义两个抽象方法?不是这样的,要看 mapper 层的这两个方法是依赖关系还是独立关系,如果某一个抽象方法依赖于另一个抽象方法,那就需要在业务层将这两个方法整合到一个方法中.一句话来说就是:一个功能模块可能需要多条 sql 语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Service public interface IAddressService { 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 类实现接口中抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class AddressServiceImpl implements IAddressService { @Autowired private AddressMapper addressMapper; @Autowired private UserMapper userMapper; @Value("${user.address.max-count}") private Integer maxCount; @Override public void addNewAddress (Integer uid, String username, Address address) { User result = userMapper.findByUid(uid); if (result ==null || result.getIsDelete() == 1 ) { throw new UsernameNotFoundException ("用户数据不存在" ); } Integer count = addressMapper.countByUid(uid); if (count >= maxCount) { throw new AddressCountLimitException ("用户收货地址超出上限" ); } address.setUid(uid); Integer isDefault = count == 0 ? 1 : 0 ; 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 测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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 中进行捕获处理
1 2 3 4 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,该类用来处理用户收货地址的请求和响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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.新增收货地址-前端页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <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 表
1 2 3 4 5 6 7 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.向该表中插入省市区数据
1 2 3 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 序列化)
1 2 3 4 5 6 7 8 9 10 11 12 public class District implements Serializable { private Integer id; private String parent; private String code; private String name; }
3.获取省市区列表-持久层 3.1 规划需执行的 SQL 语句 1 select * from t_dict_district where parent= ? order by ASC
3.2 设计接口和抽象方法 日后可能开发新的模块仍要用到省市区列表,那么为了降低耦合性,就要创建新的接口
在 mapper 层下创建接口 DistrictMapper
1 2 3 4 5 6 7 8 9 public interface DistrictMapper { List<District> findByParent (String parent) ; }
3.3 编写映射 创建一个 DistrictMapper.xml 映射文件并配置上述抽象方法的映射
1 2 3 4 5 6 7 8 9 10 <?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 测试类编写代码进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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,并定义抽象方法
1 2 3 4 5 6 7 8 9 public interface IDistrictService { List<District> getByParent (String parent) ; }
2.创建 DistrictServiceImpl 实现类来实现抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Service public class DistrictServiceImpl implements IDistrictService { @Autowired private DistrictMapper districtMapper; @Override public List<District> getByParent (String parent) { List<District> list = districtMapper.findByParent(parent); for (District district : list) { district.setId(null ); district.setParent(null ); } return list; } }
4.3 单元测试 在 test 下的 service 文件夹下创建 DistrictServiceTests 测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @SpringBootTest @RunWith(SpringRunner.class) public class DistrictServiceTests { @Autowired private IDistrictService districtService; @Test public void getByParent () { List<District> list = districtService.getByParent("86" ); for (District district : list) { System.err.println(district); } } }
5.获取省市区列表-控制层 5.1 设计请求
/districts/
GET
String parent
JsonResult>
5.2 处理请求 1.创建一个 DistrictController 类,在类中编写处理请求的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RequestMapping("districts") @RestController public class DistrictController extends BaseController { @Autowired private IDistrictService districtService; @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 代码:
1 2 <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 来获取当前省市区的名称,对应就是一条查询语句
1 select * from t_dict_district where code= ?
3.2 设计接口和抽象方法 在 DistrictMapper 接口定义 findNameByCode 方法
1 String findNameByCode (String code) ;
3.3 编写映射 在 DistrictMapper.xml 文件中添加 findNameByCode 方法的映射
1 2 3 <select id ="findNameByCode" resultType ="java.lang.String" > select name from t_dict_district where code=#{code} </select >
3.4 单元测试 在 DistrictMapperTests 编写测试代码
1 2 3 4 5 @Test public void findNameByCode () { String name = districtMapper.findNameByCode("610000" ); System.out.println(name); }
2.获取省市区名称-业务层 2.1 规划异常 没有异常需要处理
2.2 设计接口和抽象方法及实现 1.在 IDistrictService 接口定义对应的业务层接口中的抽象方法
1 String getNameByCode (String code) ;
2.在 DistrictServiceImpl 实现此方法
1 2 3 4 @Override public String getNameByCode (String code) { return districtMapper.findNameByCode(code); }
2.3 单元测试 业务层只是调用持久层对应的方法然后返回,没有什么额外的实现,可以不用测试(一般超过 8 行的代码都要进行测试)
3.获取省市区名称-控制层 实际开发中在获取省市区名称时并不需要前端传控制层,然后传业务层,再传持久层,而是在新增收货地址的业务层需要获取省市区名称,也就是说获取省市区名称的模块不需要控制层,只是需要被新增收货地址的业务层所依赖
4.获取省市区名称-业务层优化 1.在新增收货地址的业务层需要对 address 进行封装,使其存有所有数据,然后将 address 传给持久层(记住,持久层只会根据传过来的参数调用某个方法与数据库交互,永远不会有额外的实现),而此时新增收货地址的业务层并没有省市区的数据,所以需要依赖于获取省市区列表的业务层对应的接口中的 getNameByCode 方法
所以需要在业务层实现类 AddressServiceImpl 中加
1 2 @Autowired private IDistrictService districtService;
2.在 AddressServiceImpl 的方法中将 DistrictService 接口中获取到的省市区数据封装到 address 对象,此时 address 就包含了所有用户收货地址的数据
1 2 3 4 5 6 7 8 9 10 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 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 var defaultOption="<option value='0'>-----请选择-----</option>" ;$(document ).ready (function ( ) { showProvinceList (); $("#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 ; 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 ("省/直辖区的信息加载失败" ) } } }); } $("#province-list" ).change (function ( ) { var parent = $("#province-list" ).val (); $("#city-list" ).empty (); $("#area-list" ).empty (); $("#city-list" ).append (defaultOption); $("#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>" ; $("#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 语句 数据库数据的查询操作
1 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
1 2 3 4 5 6 List<Address> findByUid (Integer uid) ;
1.3 编写映射 在 xml 文件添加相应的 sql 语句映射
1 2 3 4 < select id= "findByUid" resultMap= "AddressEntityMap"> select * from t_address where uid= #{uid} order by is_default DESC ,created_time DESC < / select >
1.4 单元测试 1 2 3 4 5 @Test public void findByUid () { List<Address> list = addressMapper.findByUid(11 ); System.out.println(list); }
2.收货地址列表展示-业务层 2.1 规划异常 该模块只是为了展示列表,不涉及到增删改,即便没有拿到任何数据,那无非就是不展示呗,所以不涉及到异常,不需要在业务层抛出异常
2.2 设计接口和抽象方法及实现 1.定义抽象方法
1 List<Address> getByUid (Integer uid) ;
2.实现该方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public List<Address> getByUid (Integer uid) { List<Address> list = addressMapper.findByUid(uid); for (Address address : list) { address.setUid(null ); address.setProvinceCode(null ); address.setCityCode(null ); address.setAreaCode(null ); address.setZip(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>
3.3 处理请求 实现请求方法的编写
1 2 3 4 5 6 @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 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <script> $(document ).ready (function ( ) { showAddressList (); }); function showAddressList ( ) { $("#address-list" ).empty (); $.ajax ({ url : "/addresses" , type : "get" , 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>\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>' ; 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); } $(".add-def:eq(0)" ).hide (); } else { <!--这个其实永远不会执行,因为没有编写 异常,控制层返回的状态码永远是OK --> alert ("用户收货地址数据加载失败" ) } } }); } </script>
设置默认收货地址 1.设置默认收货地址-持久层 1.1 规划需要执行的 SQL 语句 无论选择的是哪一条数据,都把所有的数据设为非默认,再把当前数据设为默认
我们可能会想着把第一条设为非默认,再将该条设为默认,但这样处理的话需要额外做一条查询语句拿到默认地址的数据
1.检测当前用户想设置为默认收货地址的这条数据是否存在
1 select * from t_address where aid= ?
2.在修改用户的默认收货地址之前先将所有的收货地址设置为非默认
1 update t_address set is_default= 0 where uid= ?
3.将用户选中的这条记录设置为默认收货地址
1 update t_address set is_default= 1 ,modified_user= ?,modified_time= ? where aid= ?
1.2 设计接口和抽象方法 在 AddressMapper 接口中来定义实现该模块所需的三个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Address findByAid (Integer aid) ; Integer updateNonDefault (Integer uid) ; Integer updateDefaultByAid ( @Param("aid") Integer aid, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime) ;
1.3 编写映射 在 AddressMapper.xml 中编写映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <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 单元测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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 类
1 2 3 4 public class AddressNotFoundException extends ServiceException { }
1 2 3 4 public class AccessDeniedException extends ServiceException { }
2.2 设计接口和抽象方法及实现 1.在 IAddressService 接口中编写抽象方法 setDefault,并使其在方法内部统一实现持久层的三个方法
分析一下该方法需要什么参数:
先看持久层的三个方法需要什么参数:aid,uid,modifiedUser,modifiedTime.
其中 aid 是从前端一步一步传到业务层的,所以需要该参数
uid 和 modifiedUser 是一样的,都是由控制层从 session 获取的 uid 并传给业务层,所以需要该参数
modifiedTime 可以在业务层 new Date,所以不需要该参数
1 2 3 4 5 6 7 void setDefault (Integer aid,Integer uid,String username) ;
2.在 AddressServiceImpl 类编写该方法的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Override public void setDefault (Integer aid, Integer uid, String username) { Address result = addressMapper.findByAid(aid); if (result == null ) { throw new AddressNotFoundException ("收货地址不存在" ); } if (!result.getUid().equals(uid)) { throw new AccessDeniedException ("非法数据访问" ); } Integer rows = addressMapper.updateNonDefault(uid); if (rows < 1 ) { throw new UpdateException ("更新数据时产生未知的异常" ); } rows = addressMapper.updateDefaultByAid(aid, username, new Date ()); if (rows != 1 ) { throw new UpdateException ("更新数据时产生未知的异常" ); } }
2.3 单元测试 在 AddressServiceTests 类中编写单元测试方法
1 2 3 4 @Test public void setDefault () { addressService.setDefault(9 ,11 ,"管理员" ); }
3.设置默认收货地址-控制层 3.1 处理异常 在 BaseController 类中处理业务层抛出的两个异常
1 2 3 4 5 6 7 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”)
1 2 3 4 5 6 7 8 9 10 @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 函数传参,可以随便写,只要给占位符赋值时对应就可以了,.注意,即使调用的是无参函数也要加括号
1 2 3 4 5 <td> <a onclick ="setDefault(#{aid})" class ="btn btn-xs add-def btn-default" > 设为默认 </a > </td>
2.在 for 循环中为占位符赋值:
1 tr = tr.replace ("#{aid}" , list[i].aid );
3.完成 setDefault 方法的声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function setDefault (aid ) { $.ajax ({ url : "/addresses/" + aid + "/set_default" , type : "POST" , 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 语句
1 delete from t_address where aid= ?
3.需要判断删除的地址是否是默认地址(使用 aid 查询到的地址对象的 getIsDefault 方法),如果判断出删的是默认地址,则还需要定义把哪个地址设为默认,这里定义最新修改的为默认地址.
开发该 SQL 语句
1 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 接口中进行抽象方法的设计
1 2 3 4 5 6 7 8 9 10 11 12 13 Integer deleteByAid (Integer aid) ; Address findLastModified (Integer uid) ;
1.3 编写映射 在 AddressMapper.xml 文件中进行映射
1 2 3 4 5 6 7 8 9 <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 单元测试 1 2 3 4 5 6 7 8 9 10 @Test public void deleteByAid () { addressMapper.deleteByAid(11 ); } @Test public void findLastModified () { System.out.println(addressMapper.findLastModified(11 )); } }
2.删除收货地址-业务层 2.1 规划异常
可能没有该条地址数据(已开发)
可能地址数据归属错误(已开发)
在执行删除的时候可能会产生未知的异常导致数据不能够删除成功,则抛出 DeleteException 异常,在 service 创建该异常并使其继承业务层异常
1 2 3 4 public class DeleteException extends 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.判断删除的是否是默认地址.这七步就是业务层完整的开发流程
1 2 3 4 5 6 7 void delete (Integer aid,Integer uid,String username) ;
2.实现该抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Override public void delete (Integer aid, Integer uid, String username) { Address result = addressMapper.findByAid(aid); if (result == null ) { throw new AddressNotFoundException ("收货地址数据不存在" ); } if (!result.getUid().equals(uid)) { throw new AccessDeniedException ("非法数据访问" ); } Integer rows = addressMapper.deleteByAid(aid); if (rows != 1 ) { throw new DeleteException ("删除数据时产生未知的异常" ); } if (result.getIsDefault() == 0 ) { return ; } Integer count = addressMapper.countByUid(uid); if (count == 0 ) { return ; } Address address = addressMapper.findLastModified(uid); rows = addressMapper.updateDefaultByAid(address.getAid(), username, new Date ()); if (rows != 1 ) { throw new UpdateException ("更新数据时产生未知的异常" ); } }
2.3 单元测试 1 2 3 4 @Test public void delete () { addressService.delete(1 ,11 ,"4.11删除" ); }
3.删除收货地址-控制层 3.1 处理异常 需要在 BaseController 类中处理异常类
1 2 3 4 else if (e instanceof DeleteException) { result.setState(5002 ); result.setMessage("删除数据时产生未知的异常" ); }
3.2 设计请求
/addresses/{aid}/delete
POST
Integer aid,HttpSession session
JsonResult
3.3 处理请求 1 2 3 4 5 6 7 8 @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)方法
1 2 3 4 5 <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)方法的声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function setDefault (aid) { $.ajax({ url: "/addresses/" +aid+"/set_default" , type: "POST" , dataType: "JSON" , success: function (json) { if (json.state == 200 ) { showAddressList(); } else { alert("删除收货地址失败" ) } }, error: function (xhr) { alert("删除收货地址时产生未知的异常!" +xhr.message); } }); }
商品热销排行 1.创建数据表 1.在 store 数据库中创建 t_product 数据表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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.向该表插入数据
1 2 3 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 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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; }
3.商品热销排行-持久层 3.1 规划需要执行的 SQL 语句 查询热销商品列表的 SQL 语句
1 SELECT * FROM t_product WHERE status= 1 ORDER BY priority DESC LIMIT 0 ,4
3.2 设计接口和抽象方法 在 mapper 包下创建 ProductMapper 接口并在接口中添加查询热销商品 findHotList()的方法
1 2 3 4 5 6 7 public interface ProductMapper { List<Product> findHotList () ; }
3.3 编写映射 在 main\resources\mapper 文件夹下创建 ProductMapper.xml 文件,并在文件中配置 findHotList()方法的映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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()方法
1 2 3 4 5 6 7 public interface IProductService { List<Product> findHotList () ; }
2.在业务层创建 ProductServiceImpl 类并实现该方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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>
5.3 处理请求 1.创建 ProductController 类并使其继承 BaseController 类,在类中编写处理请求的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 @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 方法中添加代码:
1 patterns.add("/products/**" );
6.商品-热销排行-前端页面 1.在 index.html 页面给“热销排行”列表的 div 标签设置 id 属性值
1 2 3 <div id ="hot-list" class ="panel-body panel-item" > </div >
2.在 index.html 页面中添加展示热销排行商品的 js 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <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 语句
1 SELECT * FROM t_product WHERE id= ?
1.2 设计接口和抽象方法 在 ProductMapper 接口中添加抽象方法
1 2 3 4 5 6 Product findById (Integer id) ;
1.3 编写映射 在 ProductMapper.xml 文件中配置 findById(Integer id)方法的映射
1 2 3 <select id ="findById" resultMap ="ProductEntityMap" > select * from t_product where id=#{id} </select >
2.显示商品详情-业务层 2.1 规划异常 如果商品数据不存在,应该抛出 ProductNotFoundException,所以创建 ProductNotFoundException 异常类并使其继承 ServiceException
1 2 3 4 public class ProductNotFoundException extends ServiceException { }
2.2 设计接口和抽象方法及实现 1.在业务层 IProductService 接口中添加 findById(Integer id)抽象方法
1 2 3 4 5 6 Product findById (Integer id) ;
2.在 ProductServiceImpl 类中,实现接口中的 findById(Integer id)抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public Product findById (Integer id) { Product product = productMapper.findById(id); if (product == null ) { throw new ProductNotFoundException ("尝试访问的商品数据不存在" ); } product.setPriority(null ); product.setCreatedUser(null ); product.setCreatedTime(null ); product.setModifiedUser(null ); product.setModifiedTime(null ); return product; }
3.显示商品详情-控制层 3.1 处理异常 在 BaseController 类中的 handleException()方法中添加处理 ProductNotFoundException 的异常
1 2 3 4 else if (e instanceof ProductNotFoundException) { result.setState(4006 ); result.setMessage("访问的商品数据不存在的异常" ); }
3.2 设计请求
/products/{id}/details
Integer id
GET
JsonResult
3.3 处理请求 在 ProductController 类中添加处理请求的 getById()方法
1 2 3 4 5 @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 文件
1 <script type="text/javascript" src="../js/jquery-getUrlParam.js" ></script>
2.在 product.html 页面中 body 标签内部的最后添加获取当前商品详情的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <script type="text/javascript" > 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 ); $("#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 数据库
2.在 store 数据库中创建 t_cart 用户数据表
1 2 3 4 5 6 7 8 9 10 11 12 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
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Cart extends BaseEntity { private Integer cid; private Integer uid; private Integer pid; private Long price; private Integer num; }
3.加入购物车-持久层 3.1 规划需要执行的 SQL 语句 1.向购物车表中插入商品数据的 SQL 语句
1 insert into t_cart (除了cid以外的所有字段) values (匹配的值列表);
2.如果当前商品已经在购物车存在,则直接更新商品即可
1 update t_cart set num=? where cid=?
3.在插入或者更新具体执行哪个语句,取决于数据库中是否有当前的这个购物车商品的数据,需要查询语句才能确定
1 select * from t_cart where uid=? and pid=?
3.2 设计接口和抽象方法 在 mapper 包下创建 CartMapper 接口,并添加抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public interface CartMapper { Integer insert (Cart cart) ; Integer updateNumByCid ( @Param("cid") Integer cid, @Param("num") Integer num, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime) ; Cart findByUidAndPid ( @Param("uid") Integer uid, @Param("pid") Integer pid) ;}
3.3 编写映射 在 resources.mapper 文件夹下创建 CartMapper.xml 文件,并在文件中配置以上三个方法的映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?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 > <select id ="findByUidAndPid" resultMap ="CartEntityMap" > select * from t_cart where uid=#{uid} AND pid=#{pid} </select > </mapper >
3.4 单元测试 创建 CartMapperTests 测试类进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @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 对象)
1 2 3 4 5 6 7 8 9 10 public interface ICartService { void addToCart (Integer uid, Integer pid, Integer amount, String username) ; }
2.创建 CartServiceImpl 类,并实现 ICartService 接口.在类中声明 CartMapper 持久层对象和 IProductService 处理商品数据的业务对象,并实现业务层的抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @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) { Cart result = cartMapper.findByUidAndPid(uid, pid); Integer cid = result.getCid(); Date date = new Date (); if (result == null ) { Cart cart = new Cart (); cart.setUid(uid); cart.setPid(pid); cart.setNum(amount); Product product = productMapper.findById(pid); cart.setPrice(product.getPrice()); cart.setCreatedUser(username); cart.setCreatedTime(date); cart.setModifiedUser(username); cart.setModifiedTime(date); Integer rows = cartMapper.insert(cart); if (rows != 1 ) { throw new InsertException ("插入数据时出现未知异常" ); } } else { Integer num = result.getNum() + amount; Integer rows = cartMapper.updateNumByCid( result.getCid(), num, username, date); if (rows != 1 ) { throw new InsertException ("更新数据时产生未知异常" ); } } } }
4.3 单元测试 创建测试类 CartServiceTests 并编写测试方法。
1 2 3 4 5 6 7 8 9 10 11 @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()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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 参数的数据设置的方式
这里表单里面有很多无用参数,所以不使用表单提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $("#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 语句大致是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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_cartleft 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 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; }
2.在 CartMapper 接口中添加抽象方法
1 2 3 4 5 6 List<CartVO> findVOByUid (Integer uid) ;
1.3 编写映射 1.在 CartMapper.xml 文件中添加 findVOByUid()方法的映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <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()方法的测试
1 2 3 4 5 @Test public void findVOByUid () { List<CartVO> list = cartMapper.findVOByUid(11 ); System.out.println(list); }
2.显示购物车列表-业务层 2.1 规划异常 查询不到就返回空,不需要规划异常
2.2 设计接口和抽象方法及实现 1.在 ICartService 接口中添加 findVOByUid()抽象方法
1 2 3 4 5 6 List<CartVO> getVOByUid (Integer uid) ;
2.在 CartServiceImpl 类中重写业务接口中的抽象方法
1 2 3 4 @Override public List<CartVO> getVOByUid (Integer uid) { return cartMapper.findVOByUid(uid); }
2.3 单元测试 该业务层只是调用了持久层的方法并返回,可以不再测试
3.显示购物车列表-控制层 3.1 处理异常 业务层没有抛出异常,所以这里不需要处理异常
3.2 设计请求
/carts/
GET
HttpSession session
JsonResult>
3.3 处理请求 在 CartController 类中编写处理请求的代码。
1 2 3 4 5 @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 文件注释掉(这个就是文件的功能:点击”+“,”-“,“删除”,”全选”等按钮时执行相应的操作)
1 <!-- <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 标签中编写展示购物车列表的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <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.首先进行查询需要操作的购物车数据信息
1 SELECT * FROM t_cart WHERE cid= ?
1.2 设计接口和抽象方法 在 CartMapper 接口中添加抽象方法
1 Cart findByCid (Integer cid) ;
1.3 编写映射 在 CartMapper 文件中添加 findByCid(Integer cid)方法的映射
1 2 3 <select id ="findByCid" resultMap ="CartEntityMap" > select * from t_cart where cid=#{cid} </select >
1.4 单元测试 在 CartMapperTests 测试类中添加 findByCid()测试方法
1 2 3 4 @Test public void findByCid () { System.out.println(cartMapper.findByCid(1 )); }
2.增加购物车商品数量-业务层 2.1 规划异常
在更新时产生 UpdateException 未知异常,此异常类无需再次创建
可能该购物车列表数据归属不是登录的用户,抛 AccessDeniedException 异常,此异常类无需再次创建
要查询的数据不存在.抛出 CartNotFoundException 异常,创建该异常类并使其继承 ServiceException
1 2 3 4 public class CartNotFoundException extends 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 类型.这样的话就把数据库中更新后的数量层层传给前端,前端接收后填充到控件中就可以了
1 2 3 4 5 6 7 8 Integer addNum (Integer cid,Integer uid, String username) ;
3.在 CartServiceImpl 类中实现接口中的抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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 异常类的统一管理
1 2 3 4 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()方法
1 2 3 4 5 6 7 8 @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()事件,如果已经添加无需重复添加
1 <input class ="num-btn" type="button" value="+" onclick="addNum(#{cid})" />
2.在 script 标签中定义 addNum()函数并编写增加购物车数量的逻辑代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 ); 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 语句大致是。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 select cid, uid, pid, t_cart.price, t_cart.num, title, t_product.price as realPrice, image from t_cartleft join t_product on t_cart.pid = t_product.idwhere cid in (?,?,?) order by t_cart.created_time desc
注意 where cid in (?,?,?),这里是需要传入 cid 的集合
1.2 设计接口和抽象方法 在 CartMapper 接口中添加 findVOByCids 抽象方法
1 List<CartVO> findVOByCids (Integer[] cids) ;
1.3 配置映射 1.在 CartMapper.xml 文件中添加 SQL 语句的映射配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <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 方法进行测试
1 2 3 4 5 6 7 8 @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()抽象方法
1 List<CartVO> getVOByCids (Integer uid, Integer[] cids) ;
2.在 CartServiceImpl 类中重写业务接口中的抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override public List<CartVO> getVOByCids (Integer uid, Integer[] cids) { List<CartVO> list = cartMapper.findVOByCids(cids); Iterator<CartVO> it = list.iterator(); while (it.hasNext()) { CartVO cart = it.next(); if (!cart.getUid().equals(uid)) { it.remove(); } } return list; }
2.3 单元测试 业务层只是调用持久层获取数据并判断归属是否正确,这里不再测试
3.确认订单-控制层 3.1 处理异常 业务层没有抛出异常,所以不需要处理异常
3.2 设计请求
/carts/list
GET
Integer[] cids, HttpSession session
JsonResult>
3.3 处理请求 1.在 CartController 类中添加处理请求的 getVOByCids()方法。
1 2 3 4 5 @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 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <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 控件中并将第一个地址显示出来
1 2 3 4 $(document ).ready (function ( ) { showCartList (); showAddressList (); });
2.在 orderConfirm.html 页面中编写 showAddressList 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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++) { var opt = '<option value="#{aid}">#{name} #{tag} #{provinceName}#{cityName}#{areaName}#{address} #{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 数据库。
2.在 store 数据库中创建 t_order 和 t_order_item 数据表
针对该模块可以将 t_order_item 表和 t_order 表合并,但是以后可能开发某个模块可能单独用到 t_order_item(比如用户查看订单时只需要 t_order_item 表就可以实现)所以,建议这两个表分开创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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; }
2.在 com.cy.store.entity 包下创建 OrderItem 实体类并继承 BaseEntity 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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; }
3.创建订单-持久层 3.1 规划需要执行的 SQL 语句 1.插入订单数据的 SQL 语句
1 inert into t_order (aid除外的所有字段) values (字段的值)
2.插入某一个订单中商品数据的 SQL 语句
1 inert into t_order (id除外的所有字段) values (字段的值)
3.2 实现接口和抽象方法 在 mapper 包下创建 OrderMapper 接口并在接口中添加抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public interface OrderMapper { Integer insertOrder (Order order) ; Integer insertOrderItem (OrderItem orderItem) ; }
3.3 编写映射 创建 OrderMapper.xml 文件,并添加抽象方法的映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <?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 测试类并添加测试方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @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()方法
1 Address getByAid (Integer aid, Integer uid) ;
2.在 AddressServiceImpl 类中实现接口中的 getByAid()抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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 业务层接口并添加抽象方法用于创建订单
1 2 3 public interface IOrderService { Order create (Integer aid, Integer[] cids, Integer uid, String username) ; }
返回值是 Order 是因为还要在下个页面展示订单详细信息
4.在 impl 包下创建 OrderServiceImpl 并编写代码实现订单和订单中所有商品数据的插入操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 @Service public class OrderServiceImpl implements IOrderService { @Autowired private OrderMapper orderMapper; @Autowired private IAddressService addressService; @Autowired private ICartService cartService; 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 (); 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()方法进行功能测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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 类,在类中编写请求方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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 请求不能拼接字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $("#btn-create-order" ).click (function ( ) { var aid = $("#address-list" ).val (); var cids = location.search .substr (1 ); $.ajax ({ url : "/orders/create" , data : "aid=" + aid + "&" + cids, 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。
使用步骤:
首先定义一个类,将这个类作为切面类
在这个类中定义切面方法(5 种:前置,后置,环绕,异常,最终)
将这个切面方法中的业务逻辑对应的代码进行编写和设计
通过连接点来连接目标方法,就是用粗粒度表达式和细粒度表达式来进行连接
2.切面方法 1.切面方法的访问权限是 public。
2.切面方法的返回值类型可以是 void 或 Object,如果该方法被@Around 注解修饰,必须使用 Object 作为返回值类型,并返回连接点方法的返回值;如果使用的注解是@Before 或@After 等其他注解时,则自行决定。
3.切面方法的名称可以自定义。
4.切面方法可以接收参数,参数是 ProccedingJoinPoint 接口类型的参数.但是@Around 所修饰的方法必须要传递这个参数.其他注解修饰的方法要不要该参数都可以
3 统计业务方法执行时长 1.因为 AOP 不是 Spring 内部封装的技术,所以需要进行导包操作:在 pom.xml 文件中添加两个关于 AOP 的依赖 aspectjweaver 和 aspectjtools。
1 2 3 4 5 6 7 8 <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 容器维护)
1 2 3 4 @Aspect @Component public class TimerAspect {}
3.在类中添加切面方法,这里使用环绕通知的方式来进行编写
参数 ProceedingJoinPoint 接口表示连接点,也就是是目标方法的对象
1 2 3 4 5 6 7 8 9 10 public Object around (ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); long end = System.currentTimeMillis(); System.out.println("耗时:" +(end-start)); return result; }
4.将当前环绕通知映射到某个切面上,也就是指定连接的点.给 around 方法添加注解@Around
1 @Around("execution(* com.cy.store.service.impl.*.*(..))")