目录

学子商城

1. 项目分析

2. 创建数据库

3. 用户-创建数据表

4. 用户-创建实体类

用户-注册

5. 用户-注册-持久层

6. 用户-注册-业务层

附1:密码加密

7. 用户-注册-控制器

8. 用户-注册-前端页面

用户-登录

9. 用户-登录-持久层

10. 用户-登录-业务层

11. 用户-登录-控制器

12. 用户-登录-前端页面

用户-修改密码

13. 用户-修改密码-持久层

14. 用户-修改密码-业务层

15. 用户-修改密码-控制器

16. 用户-修改密码-前端页面

17. 拦截器

用户-修改资料

18. 用户-修改资料-持久层

19. 用户-修改资料-业务层

20. 用户-修改资料-控制器

21. 用户-修改资料-前端页面

附22. 基于SpringMVC的文件上传

用户-上传头像

23. 用户-上传头像-持久层

24. 用户-上传头像-业务层

25. 用户-上传头像-控制器

26. 用户-上传头像-前端页面

27. 用户-上传头像-设置上传文件的大小限制

28. 用户-上传头像-前端页面-解决BUG

用户管理模块结束!

收货地址模块开始...

29. 收货地址-创建数据表

30. 收货地址-创建实体类31. 收货地址-增加-持久层

32. 收货地址-增加-业务层

33. 收货地址-增加-控制器

34. 收货地址-增加-前端页面

获取省/市/区的列表

34. 获取省/市/区的列表-持久层

35. 获取省/市/区的列表-业务层

36. 获取省/市/区的列表-控制器

37. 获取省/市/区的列表-前端页面

根据省/市/区的行政代号获取省/市/区的名称

38. 根据省/市/区的行政代号获取省/市/区的名称-持久层

39. 根据省/市/区的行政代号获取省/市/区的名称-业务层

40. 在“增加收货地址”的业务中补全数据

收货地址-显示列表

41. 收货地址-显示列表-持久层

42. 收货地址-显示列表-业务层

43. 收货地址-显示列表-控制器

44. 收货地址-显示列表-前端页面

收货地址-设置默认

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

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

46. 收货地址-设置默认-控制器

收货地址-删除

附1:基于SpringJDBC的事务处理

附2:关于RESTful风格的API

52. 商品-创建实体类

商品-热销排行

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

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

54. 商品-热销排行-控制器

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

商品-显示商品详情

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

58. 商品-显示商品详情-控制器

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

60. 购物车-创建数据表

61. 购物车-创建实体类

购物车-将商品添加到购物车

62. 购物车-将商品添加到购物车-持久层

63. 购物车-将商品添加到购物车-业务层

64. 购物车-将商品添加到购物车-控制器

65. 购物车-将商品添加到购物车-前端页面

购物车-显示列表

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

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

68. 购物车-显示列表-控制器

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

购物车-增加商品数量

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

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

72. 购物车-增加商品数量-控制器

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

显示确认订单页-显示勾选的购物车数据

74. 显示确认订单页-显示勾选的购物车数据-持久层

76. 显示确认订单页-显示勾选的购物车数据-控制器-参考购物列表

77. 显示确认订单页-显示勾选的购物车数据-前端页面-参考购物列表

创建订单

78. 创建订单-创建数据表

80. 创建订单-持久层

81. 创建订单-业务层

82. 创建订单-控制器层

Spring AOP


学子商城


1. 项目分析

首先,应该分析该项目中需要处理哪些类型的数据,以本次项目为例,需要处理的有:商品类别、商品、收藏、购物车、订单、用户、收货地址……

然后,为这些需要处理的数据设计开发的先后顺序,原则上,应该先做基础数据,先做数据处理比较简单的,以上需要处理的数据的开发顺序应该是:用户 > 收货地址 > 商品类别 > 商品 > 收藏 > 购物车 > 订单。

接下来,应该分析第1个/每一个需要处理的数据对应的功能,以用户数据为例,涉及的功能就有:个人资料、修改密码、上传头像、注册、登录。

然后,也确定这些功能的开发顺序,原则上,应该先做基础功能,应该遵循**增查删改**的顺序来开发,则以上功能的开发顺序应该是:注册 > 登录 > 修改密码 > 修改个人资料 > 上传头像。

当然,在实际开发时,应该先创建该项目的数据库,在开发每种数据的功能之前,应该先创建这种数据的数据表,并创建该数据表对应的实体类。

在开发某个具体的功能时,还应该遵循开发顺序:持久层 > 业务层 > 控制器 > 前端页面。

2. 创建数据库

CREATE DATABASE tedu_store;USE tedu_store;

3. 用户-创建数据表

   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)) DEFAULT CHARSET=utf8mb4;

4. 用户-创建实体类

导入项目,先在**application.properties**中添加配置:

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

然后,执行`cn.tedu.store.StoreApplication`启动类中的`main()`方法,以检查项目是否可以正常启动。

执行**src/test/java**下的`cn.tedu.store.StoreApplicationTests`测试类中的`contextLoads()`测试方法,以检查测试环境是否正常。

在以上测试类中添加获取数据库连接的测试方法:

  @Autowiredprivate DataSource dataSource;@Testpublic void getConnection() throws Exception {Connection conn = dataSource.getConnection();System.err.println(conn);}

以检查数据库连接的配置是否正确。

先创建`cn.tedu.store.entity.BaseEntity`类,作为实体类的基类,在类中声明:

  abstract class BaseEntity implements Serializable {private String createdUser;private Date createdTime;private String modifiedUser;private Date modifiedTime;// GET/SET/toString()}

创建`cn.tedu.store.entity.User`类,继承自以上`BaseEntity`类,在类中声明属性:

   public class User extends BaseEntity {private Integer uid;private String username;private String password;private String salt;private String phone;private String email;private Integer gender;private String avatar;private Integer isDelete;// GET/SET/hashCode()/equals()/toString()}

用户-注册

5. 用户-注册-持久层

(a) 规划需要执行的SQL语句

用户注册的本质是向用户表中插入数据,需要执行的SQL语句大致是:

insert into t_user (除了uid以外的字段列表) values (匹配的值列表)

因为用户名是唯一的,所以,在插入数据之前,还应该检查该用户名对应的数据是否存在,所以,还需要根据用户名查询数据,需要执行的SQL语句大致是:

 select * from t_user where username=?

(b) 接口与抽象方法

创建`cn.tedu.store.mapper.UserMapper`接口,并在接口中添加抽象方法:

    /*** 处理用户数据操作的持久层接口*/public interface UserMapper {/*** 插入用户数据* @param user 用户数据* @return 受影响的行数*/Integer insert(User user);/*** 根据用户名查询用户数据* @param username 用户名* @return 匹配的用户数据,如果没有匹配的数据,则返回null*/User findByUsername(String username);}

> 由于这是项目中第1次创建持久层接口,还应该在`StoreApplication`启动类之前添加`@MapperScan("cn.tedu.store.mapper")`注解,以配置接口文件的位置。

(c) 配置SQL映射

在**src/main/resources**下创建**mappers**文件夹,在该文件夹中,复制粘贴得到**UserMapper.xml**文件,在该文件中配置以上2个抽象方法的映射:

   <mapper namespace="cn.tedu.store.mapper.UserMapper"><resultMap id="UserEntityMap"type="cn.tedu.store.entity.User"><id column="uid" property="uid"/><result column="is_delete" property="isDelete"/><result column="created_user" property="createdUser"/><result column="created_time" property="createdTime"/><result column="modified_user" property="modifiedUser"/><result column="modified_time" property="modifiedTime"/></resultMap><!-- 插入用户数据 --><!-- Integer insert(User user) --><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><!-- 根据用户名查询用户数据 --><!-- User findByUsername(String username) --><select id="findByUsername"resultMap="UserEntityMap">SELECT*FROMt_userWHEREusername=#{username}</select></mapper>

> 由于这是项目中第1次使用SQL映射,所以需要在**application.properties**中添加`mybatis.mapper-locations`属性的配置,以指定XML文件的位置!

完成后,应该及时执行单元测试,以检查以上开发的功能是否可以正确运行!所以,在**src/test/java**下创建`cn.tedu.store.mapper.UserMapperTests`单元测试类,在测试类的声明之前添加`@RunWith(SpringRunner.class)`和`@SpringBootTest`注解,然后在类中编写并执行单元测试:

  @RunWith(SpringRunner.class)@SpringBootTestpublic class UserMapperTests {@Autowiredprivate UserMapper mapper;@Testpublic void insert() {User user = new User();user.setUsername("mybatis");user.setPassword("1234");Integer rows = mapper.insert(user);System.err.println("rows=" + rows);}@Testpublic void findByUsername() {String username = "mybatis";User result = mapper.findByUsername(username);System.err.println(result);}}

6. 用户-注册-业务层

业务的定位:

业务:一套完整的数据处理过程,通常,表现为用户认为的1个功能,但是,在开发时,对应多项数据操作。

在项目中,通过业务控制每个“功能”(例如注册、登录等)的处理流程和相关逻辑。

流程:先做什么,后做什么,例如:注册时,需要先判断用户名是否被占用,然后再决定是否完成注册;

逻辑:能干什么,不能干什么,例如:注册时,如果用户名被占用,则不允许注册,反之,则允许注册。

业务的主要作用是保障数据安全和数据的完整性、有效性。

(a) 规划异常

为了便于统一管理自定义异常,应该先创建`cn.tedu.store.service.ex.ServiceException`自定义异常的基类异常,继承自`RuntimeException`。

当用户注册时,可能会因为用户名被占用,而导致无法成功注册,需要抛出用户名被占用的异常,所以,需要先创建`cn.tedu.store.service.ex.UsernameDuplicateException`异常类,继承自`ServiceException`。

后续执行注册时,会执行数据库的INSERT操作,该操作也是有可能失败的!则创建`cn.tedu.store.service.ex.InsertException`,继承自`ServiceException`。

所以,目前异常的继承结构是:

  RuntimeException
        ServiceException
            UsernameDuplicateException
            InsertException

> 所有的自定义异常,都应该是`RuntimeException`的子孙类异常。

(b) 接口与抽象方法

先创建`cn.tedu.store.se-rvice.IUserService`业务层接口,并在接口中添加抽象方法,关于业务层的抽象方法的设计原则:

1. 仅以操作成功为前提来设计返回值类型,不考虑操作失败的情况;

2. 方法名称可以自定义,通常,应该与用户体验到的功能相关;

3. 参数列表可以根据执行完整的数据操作需要哪些数据,就设计哪些参数,所以,参数需要足以调用持久层对应的相关功能,同时,参数还应该是客户端到控制器之后,可以提供的;

4. 使用抛出异常的方式表示操作失败!

    void reg(User user);

> 之所以需要创建业务层接口,目的是为了解耦。

(c) 实现抽象方法

创建`cn.tedu.store.service.impl.UserServiceImpl`业务层实现类,实现以上接口,在类之前添加`@Service`注解,并在类中添加持久层对象:

  @Servicepublic class UserServiceImpl implements IUserService {@Autowiredprivate UserMapper userMapper;}

当实现接口后,需要重写接口中的抽象方法,实现过程:

    /*** 执行密码加密* @param password 原密码* @param salt 盐值* @return 加密后的密文*/private String getMd5Password(String password, String salt) {// 【加密规则】// 1. 无视原始密码的强度;// 2. 使用UUID作为盐值,在原始密码的左右两侧拼接;// 3. 循环加密3次。for (int i = 0; i < 3; i++) {password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase();}return password;}@Overridepublic void reg(User user) {// 根据参数user获取尝试注册的用户名String username = user.getUsername();// 调用持久层的User findByUsername(String username)方法,根据用户名查询用户数据User result = userMapper.findByUsername(username);// 判断查询结果是否不为nullif (result != null) {// 是:表示用户名已经被占用,抛出UsernameDuplicateExceptionthrow new UsernameDuplicateException();}// 创建当前时间对象Date now = new Date();// 补全数据:加密后的密码String salt = UUID.randomUUID().toString().toUpperCase();String md5Password = getMd5Password(user.getPassword(), salt);user.setPassword(md5Password);// 补全数据:盐值user.setSalt(salt);// 补全数据:isDelete(0)user.setIsDelete(0);// 补全数据:4项日志属性user.setCreatedUser(username);user.setCreatedTime(now);user.setModifiedUser(username);user.setModifiedTime(now);// 表示用户名没有占用,允许注册// 调用持久层的Integer insert(User user)方法执行注册,并获取返回值(受影响的行数)Integer rows = userMapper.insert(user);// 判断受影响的行数是否不为1if (rows != 1) {// 是:插入数据时出现某种错误,抛出InsertExceptionthrow new InsertException();}}

完成后,在**src/test/java**下创建`cn.tedu.store.service.UserServiceTests`测试类,编写并执行以上方法的单元测试:

 @RunWith(SpringRunner.class)@SpringBootTestpublic class UserServiceTests {@Autowiredprivate IUserService service;@Testpublic void reg() {try {User user = new User();user.setUsername("service");user.setPassword("1234");service.reg(user);System.err.println("注册成功!");} catch (ServiceException e) {System.err.println("注册失败!" + e.getClass().getSimpleName());}}}

附1:密码加密

密码加密可以有效的防止数据泄密后带来的账号安全问题。

通常,程序员不需要考虑加密过程中使用的算法,因为已经存在非常多成熟的加密算法可以直接使用!但是,所有的加密算法都不适用于对密码进行加密,因为加密算法都是可以逆向运算的,即:如果能够获取加密过程中所有的参数,就可以根据密文得到原文!

对密码进行加密时,应该使用消息摘要算法!摘要算法的特点是:

1. 原文相同时,使用相同的摘要算法得到的摘要数据一定相同;

2. 使用相同的摘要算法进行运算,无论原文的长度是多少,得到的摘要数据的长度是固定的;

3. 如果摘要数据相同,则原文几乎相同,但也可能不同,可能性极低;

也就是说,不同的原文,有一定的概率得到相同的摘要数据,发生这样现象称之为**碰撞**。

以MD5算法为例,运算得到的结果是128位长的二进制数,如果悲观的设想碰撞概念,可以认为是2的128次方分之一!在密码的应用领域中,通常会限制密码的长度的最小值和最大值,可以,密码的种类是有限的,发生碰撞在概率可以认为是不存在的!

常见的摘要算法有SHA(Secure Hash Argorithm)家族和MD(Message Digest)系列的算法。

关于MD5的破解,主要来自2方面,一个是关于王小云教授的破解,学术上的破解其实是研究消息摘要算法的碰撞,也就是更快的找到2个不同的原文却对应相同的摘要,并不是假想中的“根据密文逆向运算得到原文”!

另外,还在MD5的破解,是所谓的“在线破解”,是使用数据库记录大量的原文与摘要的对应关系,当尝试“破解”,本质上是查询这个数据库,根据摘要查询原文。

为了进一步保障密码安全,可以:

1. 要求用户使用安全强度更高的原始密码;

2. 加盐;

3. 多重加密;

4. 综合以上所有应用方式。

7. 用户-注册-控制器

(a) 处理结果集

先创建`cn.tedu.store.util.JsonResult`响应结果类型:

   public class JsonResult<E> {private Integer state; // 状态private String message; // 错误描述private E data; // 数据}

(b) 设计请求

设计用户提交的请求,并设计响应的方式:

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

(c) 处理请求

创建`cn.tedu.store.controller.UserController`控制器类,在类的声明之前添加`@RestController`和`@RequestMapping("users")`注解,在类中添加`@Autowired private IUserService userService;`业务对象:

   @RequestMapping("users")@RestControllerpublic class UserController {@Autowiredprivate IUserService userService;}

然后,在类中添加处理请求的方法:

 @RequestMapping("reg")public JsonResult<Void> reg(User user) {// 创建返回值JsonResult<Void> jr = new JsonResult<>();try {// 调用业务对象执行注册userService.reg(user);// 响应成功jr.setState(1);} catch (UsernameDuplicateException e) {// 用户名冲突:被占用jr.setState(2);jr.setMessage("用户名已经被占用");} catch (InsertException e) {// 插入数据异常jr.setState(3);jr.setMessage("注册失败,请联系系统管理员");}return jr;}

完成后,启动项目,打开浏览器,通过`http://localhost:8080/users/reg?username=controller&password=1234`进行测试。

(d) 补充:关于异常

1. 请列举你认识的不少于10种异常;

Throwable
        Error
            OutOfMemoryError(OOM)
        Exception
            SQLException
            IOException
                FileNotFoundException
            RuntimeException
                NullPointerException
                ArithmeticException
                ClassCastException
                IndexOutOfBoundsException
                    ArrayIndexOutOfBoundsException
                    StringIndexOutOfBoundsException

2. 异常的处理方式和处理原则。

异常的处理方式有:捕获处理(`try`...`catch`...`finally`),声明抛出(`throw`/`throws`)。

如果当前方法适合处理,则捕获处理,如果不适合处理,则声明抛出!

(e) 控制器层的调整

先在`JsonResult`类中添加多个构造方法,以便于创建对象的同时,快速的为其中的属性赋值:

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

然后,创建提供控制器类的基类,在其中定义表示响应成功的状态码,及统一处理异常的方法:

  /*** 控制器类的基类*/public class BaseController {/*** 操作成功的状态码*/public static final int OK = 2000;@ExceptionHandler(ServiceException.class)public JsonResult<Void> handleException(Throwable e) {JsonResult<Void> jr = new JsonResult<>(e);if (e instanceof UsernameDuplicateException) {jr.setState(4000);} else if (e instanceof InsertException) {jr.setState(5000);}return jr;}}

最后,简化控制器类中的代码:

   /*** 处理用户相关请求的控制器类*/@RequestMapping("users")@RestControllerpublic class UserController extends BaseController {@Autowiredprivate IUserService userService;@RequestMapping("reg")public JsonResult<Void> reg(User user) {// 调用业务对象执行注册userService.reg(user);// 返回return new JsonResult<>(OK);}}

8. 用户-注册-前端页面

在**register.html**的最后,添加`<script>`标签用于编写JavaScript程序,并:

   $("#btn-reg").click(function() {$.ajax({"url":"/users/reg","data":$("#form-reg").serialize(),"type":"POST","dataType":"json","success":function(json) {if (json.state == 2000) {alert("注册成功!");} else {alert("注册失败!" + json.message + "!");}}});});

然后,在HTML代码部分,配置`<form>`和注册按钮的`id`属性,配置用户名和密码输入框的`name`属性即可。

注意:由于没有验证数据,即使没有填写用户名或密码,也可以注册成功!

用户-登录

9. 用户-登录-持久层

(a) 规划需要执行的SQL语句

登录需要执行的SQL语句是根据用户名查询用户数据,后续再在Java程序判断密码。SQL语句大致是:

    select * from t_user where username=?

以上SQL语句对应的开发已经完成!无需再次开发!

(b) 接口与抽象方法

无需再次开发!

(c) 配置SQL映射

无需再次开发!

10. 用户-登录-业务层

(a) 规划异常

如果用户名不存在,则登录失败,则抛出`cn.tedu.store.service.ex.UserNotFoundException`;

如果用户的`isDelete`被标记为“已删除”,则登录失败,也抛出`UserNotFoundException`;

如果密码错误,则登录失败,则抛出`cn.tedu.store.service.ex.PasswordNotMatchException`;

需要创建以上异常类,以上异常类应该继承自`ServiceException`。

(b) 接口与抽象方法

在`IUserService`接口中添加抽象方法:

    User login(String username, String password);

> 当登录成功后,需要获取该用户的id,以便于后续识别该用户的身份,并且,还需要获取该用户的用户名、头像等数据,用于显示在软件的界面中,则应该使用可以封装以上3项数据的类型作为方法的返回值!

(c) 实现抽象方法

在`UserServiceImpl`实体类中添加并实现以上方法,具体实现为:

 @Overridepublic User login(String username, String password) {// 调用userMapper的findByUsername(),根据参数username查询用户数据User result = userMapper.findByUsername(username);// 判断查询结果是否为nullif (result == null) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户名错误");}// 判断查询结果中的isDelete是否为1if (result.getIsDelete() == 1) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户名错误");}// 从查询结果中获取盐值String salt = result.getSalt();// 调用getMd5Password()将参数password和salt结合起来加密String md5Password = getMd5Password(password, salt);// 判断查询结果中的密码,与以上加密得到的密码,是否不一致if (!result.getPassword().equals(md5Password)) {// 是:抛出PasswordNotMatchExceptionthrow new PasswordNotMatchException("密码错误");}// 创建新的User对象User user = new User();// 将查询结果中的uid、username、avatar封装到新User对象中user.setUid(result.getUid());user.setUsername(result.getUsername());user.setAvatar(result.getAvatar());// 返回新User对象return user;}

完成后,在`UserServiceTests`中编写并完成单元测试:

    @Testpublic void login() {try {String username = "spring";String password = "1234";User user = service.login(username, password);System.err.println("登录成功!" + user);} catch (ServiceException e) {System.err.println("登录失败!" + e.getClass().getSimpleName());System.err.println(e.getMessage());}}

注意:不要使用错误的数据尝试登录,例如最早期通过持久层测试增加的数据,都应该删除!

11. 用户-登录-控制器

(a) 处理异常

此次处理登录时,在业务层抛出了`UserNotFoundException`和`PasswordNotMatchException`,这2个异常均未被处理过!则应该在`BaseController`的处理异常的方法中,添加2个分支,进行处理!

(b) 设计请求

设计用户提交的请求,并设计响应的方式:

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

(c) 处理请求

在`UserController`中添加处理请求的方法:

 @RequestMapping("login")public JsonResult<User> login(String username, String password) {// 调用业务对象的方法执行登录,并获取返回值// 将以上返回值和状态码OK封装到响应结果中,并返回}

先在`JsonResult`类中添加新的构造方法:

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

处理请求的具体代码为:

    @RequestMapping("login")public JsonResult<User> login(String username, String password) {// 调用业务对象的方法执行登录,并获取返回值User data = userService.login(username, password);// 将以上返回值和状态码OK封装到响应结果中,并返回return new JsonResult<>(OK, data);}

完成后,启动项目,打开浏览器,通过`http://localhost:8080/users/login?username=xx&password=xx`进行测试。

12. 用户-登录-前端页面

用户-修改密码

13. 用户-修改密码-持久层

(a) 规划需要执行的SQL语句

修改密码时需要执行的SQL语句大致是:

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

在执行修改之前,还应该检查用户数据是否存在,并检查用户数据是否被标记为“已删除”,并检查原密码是否正确,这些检查都可以通过查询用户数据来辅助完成:

    select * from t_user where uid=?

(b) 接口与抽象方法

在`UserMapper`接口添加抽象方法:

 Integer updatePasswordByUid(@Param("uid") Integer uid, @Param("password") String password, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime);User findByUid(Integer uid);

(c) 配置SQL映射

在**UserMapper.xml**中配置以上2个抽象方法的映射:

 <!-- 根据uid更新用户的密码 --><!-- Integer updatePasswordByUid(@Param("uid") Integer uid, @Param("password") String password, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime) --><update id="updatePasswordByUid">UPDATEt_userSETpassword=#{password},modified_user=#{modifiedUser},modified_time=#{modifiedTime}WHEREuid=#{uid}</update><!-- 根据用户id查询用户数据 --><!-- User findByUid(Integer uid) --><select id="findByUid"resultMap="UserEntityMap">SELECT*FROMt_userWHEREuid=#{uid}</select>

在`UserMapperTests`中编写并执行单元测试:

  @Testpublic void updatePasswordByUid() {Integer uid = 17;String password = "1234";String modifiedUser = "超级管理员";Date modifiedTime = new Date();Integer rows = mapper.updatePasswordByUid(uid, password, modifiedUser, modifiedTime);System.err.println("rows=" + rows);}@Testpublic void findByUid() {Integer uid = 17;User result = mapper.findByUid(uid);System.err.println(result);}

14. 用户-修改密码-业务层

(a) 规划异常

在修改之前,需要检查用户数据是否存在,及用户数据是否被标记为“已删除”,如果检查不通过,则应该抛出`UserNotFoundException`;

修改密码时,可能因为原密码错误,导致修改失败,则应该抛出`PasswordNotMatchException`;

在执行修改时,如果返回的受影响行数与预期值不同,则应该抛出`UpdateException`。

则需要创建`cn.tedu.store.service.ex.UpdateException`,继承自`ServiceException`。

(b) 接口与抽象方法

在`IUserService`中添加抽象方法:

    void changePassword(Integer uid, String username, String oldPassword, String newPassword);

(c) 实现抽象方法

在`UserServiceImpl`实现以上抽象方法:

    @Overridepublic void changePassword(Integer uid, String username, String oldPassword, String newPassword) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据User result = userMapper.findByUid(uid);// 检查查询结果是否为nullif (result == null) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户数据不存在");}// 检查查询结果中的isDelete是否为1if (result.getIsDelete().equals(1)) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户数据不存在");}// 从查询结果中取出盐值String salt = result.getSalt();// 将参数oldPassword结合盐值加密,得到oldMd5PasswordString oldMd5Password = getMd5Password(oldPassword, salt);// 判断查询结果中的password与oldMd5Password是否不一致if (!result.getPassword().contentEquals(oldMd5Password)) {// 是:抛出PasswordNotMatchExceptionthrow new PasswordNotMatchException("原密码错误");}// 将参数newPassword结合盐值加密,得到newMd5PasswordString newMd5Password = getMd5Password(newPassword, salt);// 创建当前时间对象Date now = new Date();// 调用userMapper的updatePasswordByUid()更新密码,并获取返回值Integer rows = userMapper.updatePasswordByUid(uid, newMd5Password, username, now);// 判断以上返回的受影响行数是否不为1if (rows != 1) {// 是:抛了UpdateExceptionthrow new UpdateException("更新用户数据时出现未知错误,请联系系统管理员");}}

在`UserServiceTests`中编写并执行单元测试:

 @Testpublic void changePassword() {try {Integer uid = 18;String username = "密码管理员";String oldPassword = "1234";String newPassword = "8888";service.changePassword(uid, username, oldPassword, newPassword);System.err.println("OK");} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}

15. 用户-修改密码-控制器

(a) 处理异常

凡是已经处理过的异常,无需重复处理!此次的业务中抛出了新的`UpdateException`,需要在`BaseController`中进行处理。

(b) 设计请求

设计用户提交的请求,并设计响应的方式:

请求路径:/users/change_password
    请求参数:String oldPassword, String newPassword, HttpSession session
    请求类型:POST
    响应结果:JsonResult<Void>

(c) 处理请求

在`UserController`中添加处理请求的方法:

  @RequestMapping("change_password")public JsonResult<Void> changePassword(String oldPassword, String newPassword, HttpSession session) {// 调用session.getAttribute()获取uid和username// 调用业务对象执行修改密码// 返回成功}

在编写代码之前,先在父类`BaseController`中添加从Session中获取`uid`和`username`的方法,以便于后续快捷的获取这2个属性值:

   /*** 从Session中获取uid* @param session HttpSession对象* @return 当前登录的用户的id*/protected final Integer getUidFromSession(HttpSession session) {return Integer.valueOf(session.getAttribute("uid").toString());}/*** 从Session中获取用户名* @param session HttpSession对象* @return 当前登录的用户名*/protected final String getUsernameFromSession(HttpSession session) {return session.getAttribute("username").toString();}

关于控制器中的代码实现:

  @RequestMapping("change_password")public JsonResult<Void> changePassword(String oldPassword, String newPassword, HttpSession session) {// 调用session.getAttribute()获取uid和usernameInteger uid = getUidFromSession(session);String username = getUsernameFromSession(session); // 调用业务对象执行修改密码userService.changePassword(uid, username, oldPassword, newPassword);// 返回成功return new JsonResult<>(OK);}

完成后,启动项目,打开浏览器,**先登录**,通过`http://localhost:8080/users/change_password?oldPassword=xx&newPassword=xx`进行测试。

16. 用户-修改密码-前端页面

17. 拦截器

后续将有很多操作都是需要先登录才可以执行的,如果在每个处理请求之前都编写代码检查Session中有没有登录信息,是不现实的!所以,应该使用拦截器解决该问题!

先创建拦截器类:

  public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {if (request.getSession().getAttribute("uid") == null) {response.sendRedirect("/web/login.html");return false;}return true;}}

然后,创建`cn.tedu.store.config.LoginInterceptorConfigurer`拦截器的配置类,实现`WebMvcConfigurer`接口,这种配置类需要添加`@Configruation`注解:

   @Configurationpublic class LoginInterceptorConfigurer implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 创建拦截器对象HandlerInterceptor interceptor= new LoginInterceptor();// 白名单List<String> patterns = new ArrayList<>();patterns.add("/bootstrap3/**");patterns.add("/js/**");patterns.add("/css/**");patterns.add("/images/**");patterns.add("/web/register.html");patterns.add("/web/login.html");patterns.add("/users/reg");patterns.add("/users/login");// 通过注册工具添加拦截器registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns(patterns);}}

在前端页面,如果先打开了修改密码页面,然后登录信息过期,点击“修改”按钮时,仍会向`users/change_password`发出请求,会被拦截器重定向到登录页面,由于整个过程是由`$.ajax()`函数处理的,是异步的处理方式,所以,重定向也是由异步任务完成的,在页面中没有任何表现,就会出现“用户登录信息超时后点击按钮没有任何反应”的问题!

可以在`$.ajax()`中补充`"error"`属性的配置,该属性的值是一个回调函数,当HTTP响应码不是成功的响应码时,例如出现302、400、404、405、500等响应码时,将会调用该函数:

   "error":function(xhr) {alert("您的登录信息已经过期,请重新登录!\n\nHTTP响应码:" + xhr.status);// location.href = "login.html";}

用户-修改资料

18. 用户-修改资料-持久层

(a) 规划需要执行的SQL语句

执行修改用户资料的SQL语句大致是:

    update t_user set phone=?,email=?,gender=?,modified_user=?,modified_time=? where uid=?

在执行修改之前,当用户刚刚打开修改资料的页面时,就应该把当前登录的用户信息显示到页面中!显示资料可以通过:

    select * from t_user where uid=?

该功能已经实现,无需再次开发!

包括在执行修改之前,还应该检查用户数据是否存在、是否标记为“已删除”,也可以通过以上查询来实现!

(b) 接口与抽象方法

在`UserMapper`接口中添加:

    Integer updateInfoByUid(User user);

(c) 配置SQL映射

在**UserMapper.xml**中配置以上抽象方法的映射:

 <!-- 根据uid更新用户资料 --><!-- Integer updateInfoByUid(User user) --><update id="updateInfoByUid">UPDATEt_userSET<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}WHEREuid=#{uid}</update>

在`UserMapperTests`中编写并执行单元测试:

    @Testpublic void updateInfoByUid() {User user = new User();user.setUid(19);user.setPhone("13800138019");user.setEmail("root@tedu.cn");user.setGender(1);user.setModifiedUser("系统管理员");user.setModifiedTime(new Date());Integer rows = mapper.updateInfoByUid(user);System.err.println("rows=" + rows);}

19. 用户-修改资料-业务层

(a) 规划异常

关于修改资料,是由2个功能组成的:打开页面时显示当前登录的用户的信息,点击修改按钮时更新用户的信息。

关于“打开页面时显示当前登录的用户的信息”,可能会因为用户数据不存在、用户被标记为“已删除”而无法正确的显示页面,将抛出`UserNotFoundException`;

关于“点击修改按钮时更新用户的信息”,在执行修改之前仍应该再次检查以上项,也可能抛出`UserNotFoundException`,并且,在执行修改过程中,还可能抛出`UpdateException`。

(b) 接口与抽象方法

在`IUserService`中添加2个抽象方法,分别对应以上2个功能:

    User getByUid(Integer uid);void changeInfo(Integer uid, String username, User user);

(c) 实现抽象方法

在`UserServiceImpl`实现类实现以上抽象方法:

   @Overridepublic User getByUid(Integer uid) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据User result = userMapper.findByUid(uid);// 检查查询结果是否为nullif (result == null) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户数据不存在");}// 检查查询结果中的isDelete是否为1if (result.getIsDelete().equals(1)) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户数据不存在");}// 创建新的User对象User user = new User();// 将以上查询结果中的username/phone/email/gender封装到新User对象中user.setUsername(result.getUsername());user.setPhone(result.getPhone());user.setEmail(result.getEmail());user.setGender(result.getGender());// 返回新User对象return user;}@Overridepublic void changeInfo(Integer uid, String username, User user) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据User result = userMapper.findByUid(uid);// 检查查询结果是否为nullif (result == null) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户数据不存在");}// 检查查询结果中的isDelete是否为1if (result.getIsDelete().equals(1)) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户数据不存在");}// 向参数user中补全数据:uiduser.setUid(uid);// 向参数user中补全数据:modifiedUser(username)user.setModifiedUser(username);// 向参数user中补全数据:modifiedTime(new Date())user.setModifiedTime(new Date());// 调用userMapper的updateInfoByUid(User user)方法执行修改,并获取返回值Integer rows = userMapper.updateInfoByUid(user);// 判断以上返回的受影响行数是否不为1if (rows != 1) {// 是:抛了UpdateExceptionthrow new UpdateException("更新用户数据时出现未知错误,请联系系统管理员");}}

单元测试:

  @Testpublic void getByUid() {try {Integer uid = 20;User user = service.getByUid(uid);System.err.println(user);} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}@Testpublic void changeInfo() {try {Integer uid = 20;String username = "数据管理员";User user = new User();user.setGender(1);user.setPhone("13900139020");user.setEmail("user20@tedu.cn");service.changeInfo(uid, username, user);System.err.println("OK.");} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}

20. 用户-修改资料-控制器

(a) 处理异常

(b) 设计请求

设计用户提交“显示当前登录的用户信息”的请求,并设计响应的方式:

请求路径:/users/get_by_uid
    请求参数:Uid(HttpSession session)
    请求类型:GET
    响应结果:JsonResult<User>

设计用户提交“执行修改用户信息”的请求,并设计响应的方式:

请求路径:/users/change_info
    请求参数:User user, HttpSession session
    请求类型:POST
    响应结果:JsonResult<Void>

(c) 处理请求

在`UserController`添加处理请求的方法,具体代码为:

 @GetMapping("get_by_uid")public JsonResult<User> getByUid(HttpSession session) {// 从Session中获取uidInteger uid = getUidFromSession(session);// 调用业务对象执行获取数据User data = userService.getByUid(uid);// 响应成功和数据return new JsonResult<>(OK, data);}

完成后,启动项目,先登录,然后通过`http://localhost:8080/users/get_by_uid`测试。

具体代码为:

   @RequestMapping("change_info")public JsonResult<Void> changeInfo(User user, HttpSession session) {// 从Session中获取uid和usernameInteger uid = getUidFromSession(session);String username = getUsernameFromSession(session);// 调用业务对象执行修改用户资料userService.changeInfo(uid, username, user);// 响应成功return new JsonResult<>(OK);}

完成后,启动项目,打开浏览器,先登录,然后通过`http://localhost:8080/users/change_info?phone=13100131000&email=admin@tedu.cn&gender=0`测试。

21. 用户-修改资料-前端页面

附22. 基于SpringMVC的文件上传

22.1. 创建项目

创建项目,**Group Id**为`cn.tedu`,**Artifact Id**为`SpringMVC-Upload`,**Packaging**选择`war`。

创建完成后,生成**web.xml**,复制依赖,添加Tomcat环境,复制Spring配置文件,复制**web.xml**中的配置。

关于文件上传,需要`spring-webmvc`依赖,另外,还需要添加`commons-fileupload`依赖:

<dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.4</version>
    </dependency>

22.2. 设计页面

在**webapp**下创建**index.html**页面,在页面中添加:

    <form method="post" enctype="multipart/form-data"><input type="file" /><input type="submit" value="上传" /></form>

注意:表单`<form>`的请求方式必须是`POST`,并配置`enctype="multipart/form-data"`!**

22.3. 使用控制器接收客户端的请求

检查**web.xml**中,`DispatcherServlet`加载的配置文件必须是**spring-upload.xml**!

在**spring-upload.xml**中添加组件扫描:

    <context:component-scan base-package="cn.tedu.spring" />

创建`cn.tedu.spring.UploadController`控制器类,在类的声明之前添加`@Controller`注解:

    package cn.tedu.spring;import org.springframework.stereotype.Controller;@Controllerpublic class UploadController {}

然后,在控制器类中添加处理请求的方法:

 @RequestMapping("upload.do")@ResponseBodypublic String upload() {System.out.println("UserController.upload()");return "OK.";}

> 此次并不关心上传成功后如果显示页面,所以,添加`@ResponseBody`显示某个字符串即可!

最后,在页面中的`<form>`标签中添加`action="upload.do"`,然后,启动项目,测试是否可以提交请求!

22.4. 处理上传

首先,在**spring-upload.xml**中添加配置:

    <!-- CommonsMultipartResolver --><bean id="multipartResolver"class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>

**注意:以上配置中的`id`值必须是`multipartResolver`!!!**

编辑页面,将上传控件的`name`设置为`file`:

    <input type="file" name="file" />

然后,在处理请求的方法中,添加类型为`MultipartFile`接口类型的参数,且该参数需要添加`@RequestParam`注解,表示客户端上传的文件:

   @RequestMapping("upload.do")@ResponseBodypublic String upload(@RequestParam("file") MultipartFile file) {System.out.println("UserController.upload()");return "OK.";}

最后,在处理请求的过程中,调用`MultipartFile`参数对象的`transferTo()`方法即可保存上传的文件:

    @RequestMapping("upload.do")@ResponseBodypublic String upload(@RequestParam("file") MultipartFile file) {System.out.println("UserController.upload()");try {File dest = new File("D:/1.jpg");file.transferTo(dest);} catch (IllegalStateException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return "OK.";}

 22.5. 关于MultipartFile的API

- `String getOriginalFilename()`:获取上传文件的原始文件名,即该文件在客户端中的文件名;

- `boolean isEmpty()`:判断上传的文件是否为空,当没有选择文件就直接上传,或者选中的文件是0字节的空文件时,返回`true`,否则,返回`false`;

- `long getSize()`:获取上传的文件大小,以字节为单位;

- `String getContentType()`:根据所上传的文件的扩展名决定该文件的MIME类型,例如上传`.jpg`格式的图片,将返回`image/jpeg`;

- `InputStream getInputStream()`:获取读取上传的文件的输入字节流,通常用于自定义读取所上传的文件的过程,该方法与`transferTo()`方法不可以同时使用;

- `void transferTo(File dest)`:保存上传的文件,该方法与`getInputStream()`方法不可以同时使用;

22.6. 关于MultipartResolver

`MultipartResolver`可以将上传过程中产生的数据封装为`MultipartFile`类型的对象!

在配置`MultipartResovler`时,可以为其中的几个属性注入值:

- `maxUploadSize`:上传文件的最大大小,假设设置值为10M,一次性上传5个文件,则5个文件的大小总和不允许超过10M;

- `maxUploadSizePerFile`:每个上传文件的最大大小,假设设置值为10M,一次性上传5个文件,则每个文件的大小都不可以超过10M,但是,5个文件的大小总和可以接近50M;

- `defaultEncoding`:默认编码;

用户-上传头像

23. 用户-上传头像-持久层

(a) 规划需要执行的SQL语句

上传文件的操作其实是:先将用户上传的文件保存到服务器端的某个位置,然后将保存文件的路径记录在数据库中!当后续需要使用该文件时,从数据库中读出文件的路径,即可实现在线访问该文件!

在持久层处理数据库中的数据时,只需要关心如何记录头像文件的路径,并不需要考虑上传时保存文件的过程!

所以,需要执行的SQL语句大致是:

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

(b) 接口与抽象方法

在`UserMapper`接口中添加抽象方法:

    Integer updateAvatarByUid(@Param("uid") Integer uid, @Param("avatar") String avatar, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime);

(c) 配置SQL映射

配置映射:

  <!-- 根据uid更新用户的头像 --><!-- Integer updateAvatarByUid(@Param("uid") Integer uid, @Param("avatar") String avatar, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime) --><update id="updateAvatarByUid">UPDATEt_userSETavatar=#{avatar},modified_user=#{modifiedUser},modified_time=#{modifiedTime}WHEREuid=#{uid}</update>

单元测试:

    @Testpublic void updateAvatarByUid() {Integer uid = 19;String avatar = "1234";String modifiedUser = "超级管理员";Date modifiedTime = new Date();Integer rows = mapper.updateAvatarByUid(uid, avatar, modifiedUser, modifiedTime);System.err.println("rows=" + rows);}

24. 用户-上传头像-业务层

(a) 规划异常

在修改头像的值之前,还是应该检查用户数据状态,可能抛出`UserNotFoundException`,由于最终执行的是修改操作,还可能抛出`UpdateException`。

(b) 接口与抽象方法

在`IUserService`中添加抽象方法:

    void changeAvatar(Integer uid, String username, String avatar);

(c) 实现抽象方法

在`UserServiceImpl`中实现以上方法,具体实现:

@Overridepublic void changeAvatar(Integer uid, String username, String avatar) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据User result = userMapper.findByUid(uid);// 检查查询结果是否为nullif (result == null) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户数据不存在");}// 检查查询结果中的isDelete是否为1if (result.getIsDelete().equals(1)) {// 是:抛出UserNotFoundExceptionthrow new UserNotFoundException("用户数据不存在");}// 创建当前时间对象Date now = new Date();// 调用userMapper的updateAvatarByUid()方法执行更新,并获取返回值Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, now);// 判断以上返回的受影响行数是否不为1if (rows != 1) {// 是:抛出UpdateExceptionthrow new UpdateException("更新用户数据时出现未知错误,请联系系统管理员");}}

单元测试:

 @Testpublic void changeAvatar() {try {Integer uid = 19;String username = "头像管理员";String avatar = "1234";service.changeAvatar(uid, username, avatar);System.err.println("OK");} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}

25. 用户-上传头像-控制器

(a) 处理异常

在处理上传的文件的过程中,用户可能选择错误的文件上传,就应该抛出对应的异常,并进行处理!所以,需要创建文件上传相关异常的基类,继承自`RuntimeException`:

cn.tedu.store.controller.ex.FileUploadException

然后,创建各具体的错误对应的异常类:

// 上传的文件为空
    cn.tedu.store.controller.ex.FileEmptyException
    // 上传的文件大小超出了限制值
    cn.tedu.store.controller.ex.FileSizeException
    // 上传的文件类型超出了限制
    cn.tedu.store.controller.ex.FileTypeException
    // 上传的文件状态异常
    cn.tedu.store.controller.ex.FileStateException
    // 上传文件时读写异常
    cn.tedu.store.controller.ex.FileUploadIOException

以上异常类都需要继承自`FileUploadException`。

然后,在`BaseController`的`handleException()`的注解中添加对上传相关异常的处理:

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

然后在方法中处理这些异常。

(b) 设计请求

设计用户提交的请求,并设计响应的方式:

请求路径:/users/change_avatar
    请求参数:MultipartFile file, HttpSession session
    请求类型:POST
    响应结果:JsonResult<String>

(c) 处理请求

26. 用户-上传头像-前端页面

27. 用户-上传头像-设置上传文件的大小限制

SpringBoot默认将`MultipartResolver`的最大文件大小设置为1M,如果尝试上传的文件的大小超过1M,则会报告异常!
如果需要调整上传的限制值,可以在启动类中添加:

 @Beanpublic MultipartConfigElement getMultipartConfigElement() {MultipartConfigFactory factory= new MultipartConfigFactory();DataSize dataSize = DataSize.ofMegabytes(2);factory.setMaxFileSize(dataSize);factory.setMaxRequestSize(dataSize);return factory.createMultipartConfig();}

并且,在启动类之前添加`@Configuration`注解!

除了以上编写方法配置上传的上限值以外,还可以通过在**application.properties** / **application.yml**中添加配置来实现:

低版本: 1.X

spring.http.multipart.max-file-size=10MB
    spring.http.multipart.max-request-size=10MB

高版本: 2.X

spring.servlet.multipart.max-file-size=30Mb
    spring.servlet.multipart.max-request-size=30Mb

或者

spring.servlet.multipart.maxFileSize=10MB
    spring.servlet.multipart.maxRequestSize=20MB

28. 用户-上传头像-前端页面-解决BUG

1. 上传成功后,应该显示上传的图片

在**upload.html**中,使用了一个`<img>`标签显示头像图片,首先,给这个标签一个id值,便于后续访问该标签,然后,`<img>`标签是通过`src`属性来决定显示哪张图片的,所以,修改该属性的值即可设置需要显示的图片:

    $("#img-avatar").attr("src", json.data);

2. 登录成功后,显示当前登录的用户的头像

首先,应该先检查登录成功后,是否返回了头像的数据: http://localhost:8080/users/login?username=root&password=1234

例如头像、用户名等数据,都属于常用数据,在客户端的许多页面都可能需要显示,如果每次都向服务器提交请求获取这些数据,是非常不合适的!可以在登录成功后,将这些数据存储在客户端本地,后续,在客户端中,需要显示这些数据时,直接从本地获取即可,无需再向服务器请求这些数据!

在客户端本地存取数据时,可以使用Cookie技术!

总的思路就是:当用户登录成功时,将服务器返回的头像路径存储到本地的Cookie中,再打开“上传头像”页面时,从本地的Cookie中读取头像路径并显示即可!

所以,在登录页面中,当登录成功后:

    $.cookie("avatar", json.data.avatar, { expires: 7 });console.log("cookie中的avatar=" + $.cookie("avatar"));

注意:在**upload.html**中,默认并没有引用jqueyr.cookie文件,也就无法识别`$.cookie()`函数,所以,需要在该文件中补充:

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

然后,在页面打开时显示头像:

    $(document).ready(function() {let avatar = $.cookie("avatar");console.log("取出cookie中的头像=" + avatar);$("#img-avatar").attr("src", avatar);});

3. 上传成功后,刷新页面也是显示最新的头像

以上代码表示“每次打开页面时,读取Cookie中的头像并显示”,如果新上传过头像,Cookie中没有更新,只要刷新页面,还是显示此前的头像!所以,当上传头像成功后,还应该把新头像的路径更新到Cookie中!

所以,在上传头像成功时,补充:

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

用户管理模块结束!

收货地址模块开始...

29. 收货地址-创建数据表

30. 收货地址-创建实体类
31. 收货地址-增加-持久层

**各功能的开发顺序**

关于收货地址数据的管理,涉及的功能有:增加,删除,修改,设为默认,显示列表。

以上功能的开发顺序:增加 > 显示列表 > 设为默认 > 删除 > 修改。

**(a) 规划需要执行的SQL语句**

增加收货地址的本质是插入新的收货地址数据,需要执行的SQL语句大致是:

    insert into t_address (除了aid以外的字段列表) values (匹配的值列表);

后续,在处理业务时,还需要确定“即将增加的收货地址是不是默认收货地址”,可以设定规则“用户的第1条收货地址是默认的,以后添加的每一条都不是默认的”,要应用该规则,就必须知道“即将增加的收货地址是不是第1条”,可以“根据用户id统计收货地址的数量”,如果统计结果为0,则即将增加的就是该用户的第1条收货地址,如果统计结果不是0,则该用户已经有若干条收货地址了,即将增加的就一定不是第1条!关于统计的SQL语句大致是:

    select count(*) from t_address where uid=?

一般,电商平台都会限制每个用户可以创建的收货地址的数量,例如“每个用户最多只允许创建20个收货地址”,也可以通过以上查询来实现。

**(b) 接口与抽象方法**

创建`cn.tedu.store.mapper.AddressMapper`接口,并在接口中添加抽象方法:

    Integer insert(Address address);Integer countByUid(Integer uid);

**(c) 配置SQL映射**

在**src/main/resources/mappers**下复制粘贴得到**AddressMapper.xml**文件,修改根节点的`namespace`属性对应以上接口,并在子级节点配置以上2个抽象方法的映射:

    <mapper namespace="cn.tedu.store.mapper.AddressMapper"><!-- 插入收货地址数据 --><!-- Integer insert(Address address) --><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><!-- 统计某用户的收货地址数据的数量 --><!-- Integer countByUid(Integer uid) --><select id="countByUid"resultType="java.lang.Integer">SELECTCOUNT(*)FROMt_addressWHEREuid=#{uid}</select></mapper>

在**src/test/java**下创建`cn.tedu.store.mapper.AddressMapperTests`测试类,在类之前添加测试的2个注解,并在类中编写并执行以上2个抽象方法的测试:

  @RunWith(SpringRunner.class)@SpringBootTestpublic class AddressMapperTests {@Autowiredprivate AddressMapper mapper;@Testpublic void insert() {Address address = new Address();address.setUid(1);address.setName("小王");Integer rows = mapper.insert(address);System.err.println("rows=" + rows);}@Testpublic void countByUid() {Integer uid = 1;Integer count = mapper.countByUid(uid);System.err.println("count=" + count);}}

32. 收货地址-增加-业务层

**(a) 规划异常**

无论用户即将增加的收货地址是不是默认收货地址,都是正确的!即:通过`countByUid()`统计的结果是不是`0`,都不是错误的操作!

在执行插入收货地址数据之前,还应该判断以上统计结果是否超出了上限值,如果已经达到上限值,则抛出`AddressCountLimitException`;

在执行插入时,还可能抛出`InsertException`;

所以,需要创建`cn.tedu.store.service.ex.AddressCountLimitException`,继承自`ServiceException`。

**(b) 接口与抽象方法**

创建`cn.tedu.store.service.IAddressService`业务层接口,并添加抽象方法:

    void addnew(Integer uid, String username, Address address);

**(c) 实现抽象方法**

创建`cn.tedu.store.service.impl.AddressServiceImpl`业务层实现类,在类之前添加`@Service`注解,实现以上接口,在类中添加`@Autowired private AddressMapper addressMapper;`持久层对象:

    @Servicepublic class AddressServiceImpl implements IAddressService {@Autowiredprivate AddressMapper addressMapper;}

然后,准备重写接口中的抽象方法:

 @Value("${project.max-count}")public int maxCount;@Overridepublic void addnew(Integer uid, String username, Address address) {// 根据参数uid调用addressMapper的countByUid()方法,统计当前用户的收货地址数据的数量Integer count = addressMapper.countByUid(uid);// 判断数量是否达到上限值if (count >= maxCount) {// 是:抛出AddressCountLimitExceptionthrow new AddressCountLimitException("收货地址数量已经达到上限(" + maxCount + ")!");}// 补全数据:将参数uid封装到参数address中address.setUid(uid);// 补全数据:根据以上统计的数量,得到正确的isDefault值,并封装Integer isDefault = count == 0 ? 1 : 0;address.setIsDefault(isDefault);// 补全数据:4项日志Date now = new Date();address.setCreatedUser(username);address.setCreatedTime(now);address.setModifiedUser(username);address.setModifiedTime(now);// 调用addressMapper的insert()方法插入收货地址数据,并获取返回的受影响行数Integer rows = addressMapper.insert(address);// 判断受影响行数是否不为1if (rows != 1) {// 是:抛出InsertExceptionthrow new InsertException("插入收货地址数据时出现未知错误,请联系系统管理员!");}}

在**src/test/java**下创建`cn.tedu.store.service.AddressServiceTests`测试类,在类中测试以上方法:

  @Testpublic void addnew() {try {Integer uid = 2;String username = "管理员";Address address = new Address();address.setName("小赵");service.addnew(uid, username, address);System.err.println("OK.");} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}

33. 收货地址-增加-控制器

**(a) 处理异常**

处理`AddressCountLimitException`。

**(b) 设计请求**

设计用户提交的请求,并设计响应的方式:

请求路径:/addresses/addnew
    请求参数:Address address, HttpSession session
    请求类型:POST
    响应结果:JsonResult<Void>

**(c) 处理请求**

创建`cn.tedu.store.controller.AddressController`控制器类,继承自`BaseController`,在类的声明之前添加`@RequestMapping("addresses")`和`@RestController`注解,在类中声明`@Autowired private IAddressService addressService;`业务对象:

  @RequestMapping("addresses")@RestControllerpublic class AddressController extends BaseController {@Autowired private IAddressService addressService;}

然后,在类中添加处理请求的方法:

 @RequestMapping("addnew")public JsonResult<Void>    addnew(Address address, HttpSession session) {// 从Session中获取uid和usernameInteger uid = getUidFromSession(session);String username = getUsernameFromSession(session);// 调用业务对象的方法执行业务addressService.addnew(uid, username, address);// 响应成功return new JsonResult<>(OK);}

完成后,启动项目,通过`http://localhost:8080/addresses/addnew?name=Mike`测试。

34. 收货地址-增加-前端页面

获取省/市/区的列表

34. 获取省/市/区的列表-持久层

**(a) 规划需要执行的SQL语句**

需要获取全国所有省/某省所有市/某市所有区的查询SQL语句大致是:

    select * from t_dict_district where parent=? order by code;

**(b) 接口与抽象方法**

创建`cn.tedu.store.mapper.DistrictMapper`接口,添加抽象方法:

    List<District> findByParent(String parent);

**(c) 配置SQL映射**

在**src/main/resources/mappers**中复制得到**DistrictMapper.xml**,修改根节点的`namespace`属性的值为以上接口文件,并配置以上抽象方法的映射:

   <mapper namespace="cn.tedu.store.mapper.DistrictMapper"><!-- 获取全国所有省/某省所有市/某市所有区 --><!-- List<District> findByParent(String parent) --><select id="findByParent"resultType="cn.tedu.store.entity.District">SELECT*FROMt_dict_districtWHEREparent=#{parent}ORDER BY code ASC</select></mapper>

在**src/test/java**下创建`cn.tedu.store.mapper.DistrictMapperTests`测试类,编写并执行以上抽象方法的测试:

    @RunWith(SpringRunner.class)@SpringBootTestpublic class DistrictMapperTests {@Autowiredprivate DistrictMapper mapper;@Testpublic void findByParent() {String parent = "510000";List<District> list = mapper.findByParent(parent);System.err.println("count=" + list.size());for (District item : list) {System.err.println(item);}}}

35. 获取省/市/区的列表-业务层

**(a) 规划异常**

**(b) 接口与抽象方法**

创建`cn.tedu.store.service.IDistrictService`接口,并添加抽象方法:

    List<District> getByParent(String parent);

**(c) 实现抽象方法**

创建`cn.tedu.store.service.impl.DistrictServiceImpl`类,实现以上接口,在类之前添加`@Service`注解,并在类中添加`@Autowired private DistrictMapper districtMapper;`持久层对象:

   @Servicepublic class DistrictServiceImpl implements IDistrictService {@Autowiredpirvate DistrictMapper districtMapper;}

然后,重写接口中的抽象方法:

   @Overridepublic List<District> getByParent(String parent) {List<District> list = districtMapper.findByParent(parent);for (District district : list) {district.setId(null);district.setParent(null);}return list;}

在**src/test/java**下创建`cn.tedu.store.service.DistrictServiceTests`测试类,编写并执行单元测试:

 @RunWith(SpringRunner.class)@SpringBootTestpublic class DistrictServiceTests {@Autowiredprivate IDistrictService service;@Testpublic void getByParent() {try {String parent = "86";List<District> list = service.getByParent(parent);System.err.println("count=" + list.size());for (District item : list) {System.err.println(item);}} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}}

36. 获取省/市/区的列表-控制器

**(a) 处理异常**

**(b) 设计请求**

设计用户提交的请求,并设计响应的方式:

请求路径:/districts/
    请求参数:String parent
    请求类型:GET
    响应结果:JsonResult<List<District>>
    是否拦截:否,需要在拦截器的配置中添加白名单

**(c) 处理请求**

创建`cn.tedu.store.controller.DistrictController`控制器类,继承自`BaseController`类,在类之前添加`@RestController`和`@RequestMapping("districts")`注解,并在类中添加`@Autowired private IDistrictService districtService;`业务层对象:

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

并在类中添加处理请求的方法:

  @GetMapping({"", "/"})public JsonResult<List<District>> getByParent(String parent) {List<District> data = districtService.getByParent(parent);return new JsonResult<>(OK, data);}

完成后,启动项目,不需要登录,通过`http://localhost:8080/districts?parent=86`测试。

37. 获取省/市/区的列表-前端页面

根据省/市/区的行政代号获取省/市/区的名称

38. 根据省/市/区的行政代号获取省/市/区的名称-持久层

**(a) 规划需要执行的SQL语句**

需要执行的SQL语句大致是:

    select name from t_dict_district where code=?

**(b) 接口与抽象方法**

在`DistrictMapper`中添加抽象方法:

    String findNameByCode(String code);

**(c) 配置SQL映射**

在**DistrictMapper.xml**中配置映射:

  <!-- 根据省/市/区的行政代号获取省/市/区的名称 --><!-- String findNameByCode(String code) --><select id="findNameByCode"resultType="java.lang.String">SELECTnameFROMt_dict_districtWHEREcode=#{code}</select>

然后,编写并执行测试:

@Testpublic void findNameByCode() {String code = "540000";String name = mapper.findNameByCode(code);System.err.println(name);}

39. 根据省/市/区的行政代号获取省/市/区的名称-业务层

**(a) 规划异常**

**(b) 接口与抽象方法**

在`IDistrictService`中添加抽象方法:

  /*** 根据省/市/区的行政代号获取省/市/区的名称* @param code 省/市/区的行政代号* @return 匹配的省/市/区的名称,如果没有匹配的数据,则返回null*/String getNameByCode(String code);

**(c) 实现抽象方法**

在`DistrictServiceImpl`中重写以上方法:

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

在`DistrictServiceTests`中编写并执行单元测试:

@Testpublic void getNameByCode() {try {String code = "430000";String result = service.getNameByCode(code);System.err.println(result);} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}

40. 在“增加收货地址”的业务中补全数据

在`AddressServiceImpl`中声明处理省/市/区数据的业务对象:

    @Autowiredprivate IDistrictService districtService;

然后,在增加过程中,补全数据:

    // 补全数据:省、市、区的名称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);

收货地址-显示列表

41. 收货地址-显示列表-持久层

**(a) 规划需要执行的SQL语句**

显示当前登录的用户的收货地址列表的SQL语句大致是:

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

**(b) 接口与抽象方法**

在`AddressMapper`中添加抽象方法:

    List<Address> findByUid(Integer uid);

**(c) 配置SQL映射**

在**AddressMapper.xml**中配置:

    <resultMap id="AddressEntityMap"type="cn.tedu.store.entity.Address"><id column="aid" property="aid"/><result column="province_code" property="provinceCode"/><result column="province_name" property="provinceName"/><result column="city_code" property="cityCode"/><result column="city_name" property="cityName"/><result column="area_code" property="areaCode"/><result column="area_name" property="areaName"/><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><!-- 查询某用户的收货地址数据列表 --><!-- List<Address> findByUid(Integer uid) --><select id="findByUid"resultMap="AddressEntityMap">SELECT*FROMt_addressWHEREuid=#{uid}ORDER BYis_default DESC, created_time DESC</select>

测试:

 @Testpublic void findByUid() {Integer uid = 19;List<Address> list = mapper.findByUid(uid);System.err.println("count=" + list.size());for (Address item : list) {System.err.println(item);}}

42. 收货地址-显示列表-业务层

**(a) 规划异常**

**(b) 接口与抽象方法**

在`IAddressService`中添加:

   /*** 查询某用户的收货地址数据列表* @param uid 用户的id* @return 该用户的收货地址数据列表*/List<Address> getByUid(Integer uid);

**(c) 实现抽象方法**

在`AddressServiceImpl`中实现:

   @Overridepublic 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.setCreatedUser(null);address.setCreatedTime(null);address.setModifiedUser(null);address.setModifiedTime(null);}return list;}

测试:

    @Testpublic void getByUid() {Integer uid = 19;List<Address> list = service.getByUid(uid);System.err.println("count=" + list.size());for (Address item : list) {System.err.println(item);}}

43. 收货地址-显示列表-控制器

**(a) 处理异常**

**(b) 设计请求**

设计用户提交的请求,并设计响应的方式:

请求路径:/addresses
    请求参数:HttpSession session
    请求类型:GET
    响应结果:JsonResult<List<Address>>

**(c) 处理请求**

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

测试URL:`http://localhost:8080/addresses`

44. 收货地址-显示列表-前端页面

收货地址-设置默认

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

**(a) 规划需要执行的SQL语句**

将某收货地址设置为默认,需要执行的SQL语句大致是:

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

另外,还需要提前将原有的默认收货地址设置为非默认,或将该用户的所有收货地址人全部设置为非默认:

    update t_address set is_default=0 where uid=?

在执行操作之前,还应该检查该收货地址数据是否存在,并检查数据归属是否正确,可以通过查询数据来判断:

    select * from t_address where aid=?

**(b) 接口与抽象方法**

在`AddressMapper`接口中添加抽象方法:

  Integer updateNonDefaultByUid(Integer uid);Integer updateDefaultByAid(@Param("aid") Integer aid, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime);Address findByAid(Integer aid);

**(c) 配置SQL映射**

映射:

    <!-- 将某用户的所有收货地址设置为非默认 --><!-- Integer updateNonDefaultByUid(Integer uid) --><update id="updateNonDefaultByUid">UPDATEt_addressSETis_default=0WHEREuid=#{uid}</update><!-- 将指定的收货地址设置为默认 --><!-- Integer updateDefaultByAid(@Param("aid") Integer aid, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime) --><update id="updateDefaultByAid">UPDATEt_addressSETis_default=1,modified_user=#{modifiedUser},modified_time=#{modifiedTime}WHEREaid=#{aid}</update><!-- 根据收货地址数据id,查询收货地址详情 --><!-- Address findByAid(Integer aid) --><select id="findByAid"resultMap="AddressEntityMap">SELECT*FROMt_addressWHEREaid=#{aid}</select>

测试:

 @Testpublic void updateNonDefaultByUid() {Integer uid = 19;Integer rows = mapper.updateNonDefaultByUid(uid);System.err.println("rows=" + rows);}@Testpublic void updateDefaultByAid() {Integer aid = 27;String modifiedUser = "User27";Date modifiedTime = new Date();Integer rows = mapper.updateDefaultByAid(aid, modifiedUser, modifiedTime);System.err.println("rows=" + rows);}@Testpublic void findByAid() {Integer aid = 27;Address result = mapper.findByAid(aid);System.err.println(result);}

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

**(a) 规划异常**

在执行设置默认收货地址之前,需要先检查该收货地址数据是否存在,如果不存在,则抛出`AddressNotFoundException`;

然后,还需要检查数据归属是否正确,也就是不可以操作他人的数据,如果该数据中记录的uid与当前登录的用户的uid不一致,则抛出`AccessDeniedException`;

检查通过后,可以先全部设置为非默认,然后将指定的收货地址设置为默认,这2种操作都是更新数据的操作,则可能抛出`UpdateException`。

所以,需要创建:
cn.tedu.store.service.ex.AddressNotFoundException
cn.tedu.store.service.ex.AccessDeniedException

**(b) 接口与抽象方法**

在`IAddressService`中添加抽象方法:

    void setDefault(Integer aid, Integer uid, String username);

**(c) 实现抽象方法**

在`AddressServiceImpl`中重写以上方法:

**注意:该方法需要添加`@Transactional`注解!**
具体实现:

   @Override@Transactionalpublic void setDefault(Integer aid, Integer uid, String username) {// 根据参数aid,调用addressMapper中的findByAid()查询收货地址数据Address result = addressMapper.findByAid(aid);// 判断查询结果是否为nullif (result == null) {// 是:抛出AddressNotFoundExceptionthrow new AddressNotFoundException("尝试访问的收货地址数据不存在");}// 判断查询结果中的uid与参数uid是否不一致(使用equals()判断)if (!result.getUid().equals(uid)) {// 是:抛出AccessDeniedException:非法访问throw new AccessDeniedException("非常访问");}// 调用addressMapepr的updateNonDefaultByUid()将该用户的所有收货地址全部设置为非默认,并获取返回的受影响的行数Integer rows = addressMapper.updateNonDefaultByUid(uid);// 判断受影响的行数是否小于1(不大于0)if (rows < 1) {// 是:抛出UpdateExceptionthrow new UpdateException("设置默认收货地址叶出现未知错误[1]");}// 调用addressMapepr的updateDefaultByAid()将指定aid的收货地址设置为默认,并获取返回的受影响的行数rows = addressMapper.updateDefaultByAid(aid, username, new Date());// 判断受影响的行数是否不为1if (rows != 1) {// 是:抛出UpdateExceptionthrow new UpdateException("设置默认收货地址叶出现未知错误[2]");}}

测试:

   @Testpublic void setDefault() {try {Integer aid = 2300;Integer uid = 19;String username = "系统管理员";service.setDefault(aid, uid, username);System.err.println("OK.");} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}

46. 收货地址-设置默认-控制器

**(a) 处理异常**

需要处理2个新创建的异常。

**(b) 设计请求**

设计用户提交的请求,并设计响应的方式:

请求路径:/addresses/{aid}/set_default
    请求参数:@PathVaraible("aid") Integer aid, HttpSession sesion
    请求类型:POST
    响应结果:JsonResult<Void>

**(c) 处理请求**

    @RequestMapping("{aid}/set_default")public JsonResult<Void> setDefault(@PathVariable("aid") Integer aid, HttpSession session) {Integer uid = getUidFromSession(session);String username = getUsernameFromSession(session);addressService.setDefault(aid, uid, username);return new JsonResult<>(OK);}

完成后,先登录,打开浏览器,通过`http://localhost:8080/addresses/25/set_default`测试。

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

收货地址-删除

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

**(a) 规划需要执行的SQL语句**

删除指定的收货地址的SQL语句大致是:

delete from t_address where aid=?

在删除之前,还可以使用此前已经完成的功能,来实现检查:数据是否存在,数据归属是否正确。

如果删除的这条数据是默认收货地址,则应该将剩余的收货地址中的某一条设置为默认,可以设定规则“将最近修改的设置为默认收货地址”,要实现设置,首先,就必须要知道“最近修改的收货地址的id是多少”,则可以通过查询:

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

当然,在执行以上操作之前,还应该检查该用户的收货地址数据的数量,如果删除的是收货地址,但是,也是最后一条收货地址,则无需执行任务操作!统计数量的功能此前已经完成,无需再次开发!

**(b) 接口与抽象方法**

在`AddressMapper`中添加:

Integer deleteByAid(Integer aid);

Address findLastModified(Integer uid);

**(c) 配置SQL映射**

映射:

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

/**
     * 查询某用户最后修改的收货地址
     * @param uid 用户的id
     * @return 该用户最后修改的收货地址,如果该用户没有收货地址数据,则返回null
     */
    Address findLastModified(Integer uid);

测试:

@Test
    public void deleteByAid() {
        Integer aid = 23;
        Integer rows = mapper.deleteByAid(aid);
        System.err.println("rows=" + rows);
    }

@Test
    public void findLastModified() {
        Integer uid = 19;
        Address result = mapper.findLastModified(uid);
        System.err.println(result);
    }

### 49. 收货地址-删除-业务层

**(a) 规划异常**

与以前设置默认的异常基本一致!

由于将执行删除操作,所以,可能抛出`DeleteException`。

所以,需要创建`cn.tedu.store.service.ex.DeleteException`。

**(b) 接口与抽象方法**

在`IAddressService`中添加:

void delete(Integer aid, Integer uid, String username);

**(c) 实现抽象方法**

实现:

@Override
    @Transactional
    public void delete(Integer aid, Integer uid, String username) {
        // 根据参数aid,调用addressMapper中的findByAid()查询收货地址数据
        Address result = addressMapper.findByAid(aid);
        // 判断查询结果是否为null
        if (result == null) {
            // 是:抛出AddressNotFoundException
            throw new AddressNotFoundException("尝试访问的收货地址数据不存在");
        }

// 判断查询结果中的uid与参数uid是否不一致(使用equals()判断)
        if (!result.getUid().equals(uid)) {
            // 是:抛出AccessDeniedException:非法访问
            throw new AccessDeniedException("非常访问");
        }

// 根据参数aid,调用持久层的deleteByAid()执行删除,并获取返回的受影响的行数
        Integer rows = addressMapper.deleteByAid(aid);
        // 判断受影响的行数是否不为1
        if (rows != 1) {
            // 是:抛出DeleteException
            throw new DeleteException("删除收货地址数据时出现未知错误,请联系系统管理员");
        }

// 判断查询结果中的isDefault是否为0
        if (result.getIsDefault() == 0) {
            return;
        }

// 调用持久层的countByUid()统计目前还有多少收货地址
        Integer count = addressMapper.countByUid(uid);
        // 判断目前的收货地址的数量是否为0
        if (count == 0) {
            return;
        }

// 调用持久层的findLastModified()找出最近修改的收货地址数据
        Address lastModified = addressMapper.findLastModified(uid);
        // 从以上查询结果中找出aid属性值
        Integer lastModifiedAid = lastModified.getAid();
        // 根据以上aid,调用持久层的updateDefaultByAid()把这条收货地址设置为默认,并获取返回的受影响的行数
        rows = addressMapper.updateDefaultByAid(lastModifiedAid, username, new Date());
        // 判断受影响的行数是否不为1
        if (rows != 1) {
            // 是:抛出UpdateException
            throw new UpdateException("更新收货地址数据时出现未知错误,请联系系统管理员");
        }
    }

测试:

@Test
    public void delete() {
        try {
            Integer aid = 2300;
            Integer uid = 19;
            String username = "系统管理员";
            service.delete(aid, uid, username);
            System.err.println("OK.");
        } catch (ServiceException e) {
            System.err.println(e.getClass().getSimpleName());
            System.err.println(e.getMessage());
        }
    }

**关于业务层实现类的调整**

1. 每在持久层接口(例如`AddressMapper`接口)中定义了新的抽象方法,都在业务层实现类(例如`AddressServiceImpl`)中都添加同名的私有的方法,如果是增删改类型的方法,添加私有方法时,返回值类型改为`void`,如果是查询类型的方法,则返回值类型不变,在处理增删改方法的内部,应该获取返回的受影响的行数,如果不是预期值,则抛出对应的异常,如果是查询类型的方法,可以直接返回调用持久层对象的查询结果;

2. 在业务层实现类(例如`AddressServiceImpl`)中的公有方法都是处理业务的方法,在这些方法中,都不再调用持久层对象来实现增删改查,而是调用以上添加的私有方法来完成增删改查;

3. 由于在于业务层实现类中,都会添加与持久层接口中定义的方法相同的私有方法,所以,业务层中的公有方法,名称就不要与持久层接口中的方法名称一样,避免发生冲突!

### 50. 收货地址-删除-控制器

**(a) 处理异常**

需要处理`DeleteException`。

**(b) 设计请求**

设计用户提交的请求,并设计响应的方式:

请求路径:/addresses/{aid}/delete
    请求参数:@PathVariable("aid") Integer aid, HttpSession session
    请求类型:POST
    响应结果:JsonResult<Void>

**(c) 处理请求**

处理:

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

测试:

http://localhost:8080/addresses/26/delete

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

附1:基于SpringJDBC的事务处理

事务:Transaction,使用事务可以保证一系列的增删改操作,要么全部执行成功,要么全部执行失败!

例如存在银行账户信息:
    账户        余额
    苍松        1000
    国斌        10000
如果需要实现:国斌向苍松转账5000元!则需要执行的SQL语句是:
    update 表 set 余额=余额-5000 where 账户='国斌';
    update 表 set 余额=余额+5000 where 账户='苍松';
如果刚好第1条SQL语句执行结束,出现某些意外,导致第2条SQL语句没有执行!则最后的数据就会出现问题!

解决这种问题的解决方案就是事务!

事务的执行过程大致是:

1. **开启**事务,关闭数据库的自动提交(auto_commit):begin
    2. 执行一系列的数据操作
    3. 如果全部成功,则最终**提交**,即写入到硬盘:commit
    4. 如果出现错误,则**回滚**,即放弃所有修改:rollback

基于SpringJDBC的应用中,需要使用事务时,只需要在业务方法之前添加`@Transactional`注解即可!

该注解对事务的处理大致是:

开启事务:begin
    try {
        执行一系列操作
        提交:commit
    } catch (RuntimeException) {
        回滚:rollback
    }

所以,为了保证事务机制能够被触发,需要:

1. 所有的增、删、改操作,都必须及时获取并判断受影响的行数;

2. 如果受影响的行数不是预期值,必须抛出某种`RuntimeException`异常对象;

3. 如果某个业务方法涉及2次或更多次的增、删、改操作(例如:2次Update操作,或1次Update操作加1次Delete操作),必须添加`@Transactional`注解!

另外,该`@Transactional`注解还可以添加在业务类之前,则该业务类中的方法均以事务的机制运行,但是,一般并不推荐这样处理!

后续,请了解:事务的ACID、传播、隔离……

附2:关于RESTful风格的API

目前的开发模式中,服务器端设计多种URL,客户端只需要向这些URL发出指定类型的请求,提交必要的参数,就可以完成相关的功能,并获取对应的操作结果!这种模式,也可以称之为服务器向客户端提供了API!

> RESTFUL是一种网络应用程序的设计风格和开发方式,基于HTTP,可以使用XML格式定义或JSON格式定义。RESTFUL适用于移动互联网厂商作为业务使能接口的场景,实现第三方OTT调用移动网络资源的功能,动作类型为新增、变更、删除所调用资源。

> REST(英文:Representational State Transfer,简称
REST)描述了一个架构样式的网络系统,比如 web 应用程序。它首次出现在2000年Roy Fielding的博士论文中,Roy Fielding是HTTP规范的主要编写者之一。在目前主流的三种Web服务交互方案中,REST相比于SOAP(Simple Object Access protocol,简单对象访问协议)以及XML-RPC更加简单明了,无论是对URL的处理还是对Payload的编码,REST都倾向于用更加简单轻量的方法设计和实现。

> 值得注意的是REST并没有一个明确的标准,而更像是一种设计的风格。

REST=Representational State Transfer,它是一种简单轻量的设计URL的风格!**注意:它并不是一个标准!**

RESTful最典型的表现就是:**将核心参数体现在URL的主体路径中,而并不是放在URL的末尾使用`?`连接!**

例如:传统的URL设计可以是:    http://localhost:8080/addresses/set_default?aid=25

使用RESTful的风格,可以是:    http://localhost:8080/addresses/25/set_default

当自行设计URL时,可以采用以下格式:
    resources/id/command

SpringMVC框架是支持RESTful的,在设计URL时,可以使用`{}`格式的占位符,例如:
   /addresses/{id}/set_default

当需要使用该占位符的值时,在处理请求的方法的参数列表中,使用`@PathVariable`注解即可,例如:

public void test(@PathVariable("id") Integer i) {}

52. 商品-创建实体类

创建`cn.tedu.store.entity.Product`类,继承自`BaseEntity`:
  /*** 商品数据的实体类*/public class Product extends BaseEntity {private static final long serialVersionUID = -199568590252555336L;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;}

商品-热销排行

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

**(a) 规划需要执行的SQL语句**

查询热销商品列表的SQL语句大致是:

    select * from t_product where status=1order by priority desc limit 0,4

**(b) 接口与抽象方法**

创建`cn.tedu.store.mapper.ProductMapper`接口,添加方法:

    List<Product> findHostList();

**(c) 配置SQL映射**

复制得到**ProductMapper.xml**文件,配置以上方法的映射:

 <mapper namespace="cn.tedu.store.mapper.ProductMapper"><resultMap id="ProductEntityMap"type="cn.tedu.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><!-- 查询热销商品的前4名 --><!-- List<Product> findHostList() --><select id="findHostList"resultMap="ProductEntityMap">SELECT*FROMt_productWHEREstatus=1ORDER BYpriority DESCLIMIT 0,4</select></mapper>

创建`ProductMapperTests`测试类,测试:

  @RunWith(SpringRunner.class)@SpringBootTestpublic class ProductMapperTests {@Autowiredprivate ProductMapper mapper;@Testpublic void findHostList() {List<Product> list = mapper.findHostList();System.err.println("count=" + list.size());for (Product item : list) {System.err.println(item);}}}

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

**(a) 规划异常**

**(b) 接口与抽象方法**

创建`cn.tedu.store.service.IProductService`接口,并添加:

    List<Product> getHotList();

**(c) 实现抽象方法**

创建`cn.tedu.store.service.impl.ProductServiceImpl`类,实现以上接口,添加`@Service`注解,在类中声明持久层对象:

  @Servicepublic class ProductServiceImpl implements IProductService {@Autowiredprivate ProductMapper productMapper;public List<Product> getHotList() {}}

在业务层实现中私有实现持久层的方法:

    private List<Product> findHotList() {return productMapper.findHotList();}

然后,实现抽象方法:

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

最后,测试:

  @RunWith(SpringRunner.class)@SpringBootTestpublic class ProductServiceTests {@Autowiredprivate IProductService service;@Testpublic void getHotList() {try {List<Product> list = service.getHotList();System.err.println("count=" + list.size());for (Product item : list) {System.err.println(item);}} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}}

54. 商品-热销排行-控制器

**(a) 处理异常**

**(b) 设计请求**
设计用户提交的请求,并设计响应的方式:

请求路径:/products/hot_list
    请求参数:无
    请求类型:GET
    响应结果:JsonResult<List<Product>>
    是否拦截:否,需要将index.html和products/**添加到白名单

**(c) 处理请求**

创建`cn.tedu.store.controller.ProductController`控制器类,继承自`BaseController`,添加`@RestController`和`@RequestMapping("products")`注解,并在类中添加业务层对象:

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

在类中添加处理请求的方法:

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

测试:http://localhost:8080/products/hot_list

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

商品-显示商品详情

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

**(a) 规划需要执行的SQL语句**

根据商品id显示详情的SQL语句大致是:

    select * from t_product where id=?

**(b) 接口与抽象方法**

在`ProductMapper`中添加抽象方法:

    Product findById(Integer id);

**(c) 配置SQL映射**
映射:

   <!-- 根据商品id查询商品详情 --><!-- Product findById(Integer id) --><select id="findById"resultMap="ProductEntityMap">SELECT*FROMt_productWHEREid=#{id}</select>

测试:

 @Testpublic void findById() {Integer id = 10000017;Product result = mapper.findById(id);System.err.println(result);}

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

**(a) 规划异常**

如果商品数据不存在,应该抛出`ProductNotFoundException`。
所以,需要创建`cn.tedu.store.service.ex.ProductNotFoundException`。

**(b) 接口与抽象方法**
在`IProductService`中添加:

    Product getById(Integer id);

**(c) 实现抽象方法**
在`ProductServiceImpl`中,先私有实现持久层新添加的方法:

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

然后,重写业务接口中的抽象方法:

    @Overridepublic Product getById(Integer id) {Product product = 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;}

测试方法:

 @Testpublic void getById() {try {Integer id = 100000179;Product result = service.getById(id);System.err.println(result);} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}

58. 商品-显示商品详情-控制器

**(a) 处理异常**

需要处理`ProductNotFoundException`。

**(b) 设计请求**
设计用户提交的请求,并设计响应的方式:

请求路径:/products/{id}/details
    请求参数:@PathVariable("id") Integer id
    请求类型:GET
    响应结果:JsonResult<Product>

**(c) 处理请求**
处理:

    @GetMapping("{id}/details")public JsonResult<Product> getById(@PathVariable("id") Integer id) {// 调用业务对象执行获取数据Product data = productService.getById(id);// 返回成功和数据return new JsonResult<>(OK, data);}

测试: http://localhost:8080/products/10000017/details    http://localhost:8080/products/1/details

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

60. 购物车-创建数据表

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)) DEFAULT CHARSET=UTF8;

61. 购物车-创建实体类

购物车-将商品添加到购物车

62. 购物车-将商品添加到购物车-持久层

**(a) 规划需要执行的SQL语句**

向数据表中插入新的购物车数据的SQL语句大致是:

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

并不是所有时候点击“加入购物车”都会插入新的数据,如果该用户曾经将该商品加入到购物车了,后续点击时,只会增加商品的数量:

    update t_cart set num=? where cid=?

关于判断“到底应该插入数据,还是修改数量”,可以通过“查询某用户是否已经添加某商品到购物车”,SQL语句大致是:

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

以上查询,如果查询到某结果,就表示该用户已经将该商品加入到购物车了,如果查询结果为null,则表示该用户没有添加过该商品。

**(b) 接口与抽象方法**

创建`cn.tedu.store.mapper.CartMapper`接口,添加抽象方法:

 /*** 处理购物车数据的持久层接口*/public interface CartMapper {/*** 插入购物车数据* @param cart* @return*/Integer insert(Cart cart);/*** 修改购物车数据中商品的数量* @param cid 购物车数据的id* @param num 新的数量* @param modifiedUser 修改执行人* @param modifiedTime 修改时间* @return 受影响的行数*/Integer updateNumByCid(@Param("cid") Integer cid, @Param("num") Integer num,@Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime);/*** 根据用户id和商品id查询购物车中的数据* @param uid 用户id* @param pid 商品id* @return 匹配的购物车数据,如果该用户的购物车中并没有该商品,则返回null*/Cart findByUidAndPid(@Param("uid") Integer uid, @Param("pid") Integer pid);}

**(c) 配置SQL映射**
映射:

 <mapper namespace="cn.tedu.store.mapper.CartMapper"><resultMap id="CartEntityMap"type="cn.tedu.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><!-- 插入购物车数据 --><!-- Integer insert(Cart cart) --><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><!-- 修改购物车数据中商品的数量 --><!-- Integer updateNumByCid(@Param("cid") Integer cid, @Param("num") Integer num,@Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime) --><update id="updateNumByCid">UPDATE t_cart SETnum=#{num},modified_user=#{modifiedUser},modified_time=#{modifiedTime}WHEREcid=#{cid}</update><!-- 根据用户id和商品id查询购物车中的数据 --><!-- Cart findByUidAndPid(@Param("uid") Integer uid, @Param("pid") Integer pid) --><select id="findByUidAndPid"resultMap="CartEntityMap">SELECT*FROMt_cartWHEREuid=#{uid} AND pid=#{pid}</select></mapper>

测试:

@RunWith(SpringRunner.class)@SpringBootTestpublic class CartMapperTests {@Autowiredprivate CartMapper mapper;@Testpublic void insert() {Cart cart = new Cart();cart.setUid(1);cart.setPid(2);cart.setNum(3);cart.setPrice(4L);Integer rows = mapper.insert(cart);System.err.println("rows=" + rows);}@Testpublic void updateNumByCid() {Integer cid = 1;Integer num = 10;String modifiedUser = "购物车管理员";Date modifiedTime = new Date();Integer rows = mapper.updateNumByCid(cid, num, modifiedUser, modifiedTime);System.err.println("rows=" + rows);}@Testpublic void findByUidAndPid() {Integer uid = 1;Integer pid = 2;Cart result = mapper.findByUidAndPid(uid, pid);System.err.println(result);}}

63. 购物车-将商品添加到购物车-业务层

**(a) 规划异常**

当插入数据时,可能抛出`InsertException`,当修改数据时,可能抛出`UpdateException`;

如果不限制购物车中的记录的数量,则没有其它异常。

**(b) 接口与抽象方法**
创建`cn.tedu.store.service.ICartService`接口,添加抽象方法:

    void addToCart(Integer pid, Integer amount, Integer uid, String username);

**(c) 实现抽象方法**

创建`cn.tedu.store.service.impl.CartServiceImpl`类,实现以上接口,添加`@Service`注解,添加`@Autowired private CartMapper cartMapper;`持久层对象,添加`@Autowired private IProductService productService;`处理商品数据的业务对象:

   @Servicepublic class CartServiceImpl implements ICartService {@Autowiredprivate CartMapper cartMapper;@Autowiredprivate IProductService productService;}

先将持久层中定义的3个方法复制到业务层实现类中,改为私有方法,并实现:

 /*** 插入购物车数据* @param cart*/private void insert(Cart cart) {Integer rows = cartMapper.insert(cart);if (rows != 1) {throw new InsertException("插入商品数据时出现未知错误,请联系系统管理员");}}/*** 修改购物车数据中商品的数量* @param cid 购物车数据的id* @param num 新的数量* @param modifiedUser 修改执行人* @param modifiedTime 修改时间*/private void updateNumByCid(Integer cid, Integer num,String modifiedUser, Date modifiedTime) {Integer rows = cartMapper.updateNumByCid(cid, num, modifiedUser, modifiedTime);if (rows != 1) {throw new InsertException("修改商品数量时出现未知错误,请联系系统管理员");}}/*** 根据用户id和商品id查询购物车中的数据* @param uid 用户id* @param pid 商品id* @return 匹配的购物车数据,如果该用户的购物车中并没有该商品,则返回null*/private Cart findByUidAndPid(Integer uid, Integer pid) {return cartMapper.findByUidAndPid(uid, pid);}

然后,重写业务接口中定义的抽象方法,实现:

@Overridepublic void addToCart(Integer pid, Integer amount, Integer uid, String username) {// 根据参数pid和uid查询购物车中的数据Cart result = findByUidAndPid(uid, pid);Date now = new Date();// 判断查询结果是否为nullif (result == null) {// 是:表示该用户并未将该商品添加到购物车// 创建Cart对象Cart cart = new Cart();// 封装数据:uid,pid,amountcart.setUid(uid);cart.setPid(pid);cart.setNum(amount);// 调用productService.getById()查询商品数据,得到商品价格Product product = productService.getById(pid);// 封装数据:pricecart.setPrice(product.getPrice());// 封装数据:4个日志cart.setCreatedUser(username);cart.setCreatedTime(now);cart.setModifiedUser(username);cart.setModifiedTime(now);// 调用insert(cart)执行将数据插入到数据表中insert(cart);} else {// 否:表示该用户的购物车中已有该商品// 从查询结果中取出原数量,与参数amount相加,得到新的数量Integer num = result.getNum() + amount;// 执行更新数量updateNumByCid(result.getCid(), num, username, now);}}

测试:

 @RunWith(SpringRunner.class)@SpringBootTestpublic class CartServiceTests {@Autowiredprivate ICartService service;@Testpublic void addToCart() {try {Integer pid = 10000007;Integer amount = 1;Integer uid = 2;String username = "不知道";service.addToCart(pid, amount, uid, username);System.err.println("OK.");} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}}

64. 购物车-将商品添加到购物车-控制器

**(a) 处理异常**

**(b) 设计请求**

设计用户提交的请求,并设计响应的方式:

   请求路径:/carts/add_to_cart请求参数:Integer pid, Integer amount, HttpSession session请求类型:POST响应结果:JsonResult<Void>

**(c) 处理请求**

创建`cn.tedu.store.controller.CartController`,继承自`BaseController`,添加`@RequestMapping("carts")`和`@RestController`注解,添加`@Autowired private ICartService cartService;`业务对象:

  @RequestMapping("carts")@RestControllerpublic class CartController extends BaseController {@Autowiredprivate ICartService cartService;}

然后,在类中加处理请求的方法:

 @RequestMapping("add_to_cart")public JsonResult<Void> addToCart(Integer pid, Integer amount, HttpSession session) {// 从Session中获取uid和usernameInteger uid = getUidFromSession(session);String username = getUsernameFromSession(session);// 调用业务对象执行添加到购物车cartService.addToCart(pid, amount, uid, username);// 返回成功return new JsonResult<>(OK);}

测试: http://localhost:8080/carts/add_to_cart?pid=10000005&amount=3

65. 购物车-将商品添加到购物车-前端页面

参考“修改密码”。
使用`$.ajax()`函数发出AJAX请求并处理结果时,其中的`"data"`表示将提交的请求参数,请求参数可以有4种方式来处理:
(1) "data" : $("#表单id").serialize() // 适用于参数较多,且都在同一个表单中

(2) "data" : new FormData($("#表单id")[0]) // 仅适用于上传

(3)"data" : "pid=10000005&amount=3"

(4)  "data" : {
        "pid":10000005,
        "amount":3
    }

购物车-显示列表

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

**(a) 规划需要执行的SQL语句**

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

    select cid,pid,uid,title,image,t_cart.price,t_product.price AS realPrice,t_cart.num from t_cart left join t_product on t_cart.pid=t_product.id where uid=19 order by t_cart.created_time desc;

**(b) 接口与抽象方法**
由于涉及多表关联查询,必然没有哪个实体类可以封装此次的查询结果!需要创建VO类!所以,先创建`cn.tedu.store.vo.CartVO`类:

  public class CartVO implements Serializable {private Integer cid;private Integer uid;private Integer pid;private String title;private String image;private Long price;private Long realPrice;private Integer num;// SET/GET// 基于cid的equals()和hashCode()// toString()}

在`CartMapper`接口中添加抽象方法:

    List<CartVO> findVOByUid(Integer uid);

**(c) 配置SQL映射**
映射:

 <!-- 查询某用户的购物车数据 --><!-- List<CartVO> findVOByUid(Integer uid) --><select id="findVOByUid"resultType="cn.tedu.store.vo.CartVO">SELECTcid, pid,uid, title,image, t_cart.price,t_product.price AS realPrice,t_cart.num FROMt_cartLEFT JOINt_productONt_cart.pid=t_product.idWHEREuid=#{uid}ORDER BYt_cart.created_time DESC</select>

测试:

@Testpublic void findVOByUid() {Integer uid = 19;List<CartVO> list = mapper.findVOByUid(uid);System.err.println("count=" + list.size());for (CartVO item : list) {System.err.println(item);}}

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

(a) 规划异常

(b) 接口与抽象方法

在`ICartService`中添加抽象方法:

    List<CartVO> getVOByUid(Integer uid);

(c) 实现抽象方法

在`CartServiceImpl`中,先添加持久层的方法,调整为私有方法并实现:

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

然后,重写业务接口中的抽象方法:

    @Overridepublic List<CartVO> getVOByUid(Integer uid) {return findVOByUid(uid);}

测试:

    @Testpublic void getVOByUid() {Integer uid = 19;List<CartVO> list = service.getVOByUid(uid);System.err.println("count=" + list.size());for (CartVO item : list) {System.err.println(item);}}

68. 购物车-显示列表-控制器

(a) 处理异常

(b) 设计请求

设计用户提交的请求,并设计响应的方式:

请求路径:/carts/
    请求参数:HttpSession session
    请求类型:GET
    响应结果:JsonResult<List<CartVO>>

(c) 处理请求

代码:

    @GetMapping({"", "/"})public JsonResult<List<CartVO>> getVOByUid(HttpSession session) {// 从Session中获取uidInteger uid = getUidFromSession(session);// 调用业务对象执行查询数据List<CartVO> data = cartService.getVOByUid(uid);// 返回成功与数据return new JsonResult<>(OK, data);}

测试:http://localhost:8080/carts

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

购物车-增加商品数量

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

(a) 规划需要执行的SQL语句

先查询需要操作的购物车数据:

    select * from t_cart where cid=?

然后,计算出新的数据值,如果允许更新,则更新:

    update t_cart set num=? where cid=?

(b) 接口与抽象方法

在`CartMapper`中添加:

    Cart findByCid(Integer cid);

(c) 配置SQL映射

映射:

  <!-- 根据购物车数据id查询购物车数据详情 --><!-- Cart findByCid(Integer cid) --><select id="findByCid"resultMap="CartEntityMap">SELECT*FROMt_cartWHEREcid=#{cid}</select>

测试:

 @Testpublic void findByCid() {Integer cid = 10;Cart result = mapper.findByCid(cid);System.err.println(result);}

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

(a) 规划异常

如果尝试访问的购物车数据不存在,则抛出`CartNotFoundException`;

如果尝试访问的数据并不是当前登录用户的数据,则抛出`AccessDeniedException`;

最终将执行更新操作,则可能抛出`UpdateException`;

则需要创建`cn.tedu.store.service.ex.CartNotFoundException`。

(b) 接口与抽象方法

在`ICartService`中添加抽象方法:

    Integer addNum(Integer cid, Integer uid, String username);

(c) 实现抽象方法

在`CartServiceImpl`中,先添加持久层的方法,私有实现:

 /*** 根据购物车数据id查询购物车数据详情* @param cid 购物车数据id* @return 匹配的购物车数据详情,如果没有匹配的数据,则返回null*/private Cart findByCid(Integer cid) {return cartMapper.findByCid(cid);}

实现抽象方法,实现代码:

    @Overridepublic Integer addNum(Integer cid, Integer uid, String username) {// 调用findByCid(cid)根据参数cid查询购物车数据Cart result = findByCid(cid);// 判断查询结果是否为nullif (result == null) {// 是:抛出CartNotFoundExceptionthrow new CartNotFoundException("尝试访问的购物车数据不存在");}// 判断查询结果中的uid与参数uid是否不一致if (!result.getUid().equals(uid)) {// 是:抛出AccessDeniedExceptionthrow new AccessDeniedException("非法访问");}// 可选:检查商品的数量是否大于多少(适用于增加数量)或小于多少(适用于减少数量)// 根据查询结果中的原数量增加1得到新的数量numInteger num = result.getNum() + 1;// 创建当前时间对象,作为modifiedTimeDate now = new Date();// 调用updateNumByCid(cid, num, modifiedUser, modifiedTime)执行修改数量updateNumByCid(cid, num, username, now);// 返回新的数量return num;}

测试:

    @Testpublic void addNum() {try {Integer cid = 12;Integer uid = 19;String username = "不知道";Integer num = service.addNum(cid, uid, username);System.err.println("OK. New num=" + num);} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}

72. 购物车-增加商品数量-控制器

(a) 处理异常

需要处理`CartNotFoundException`。

(b) 设计请求

设计用户提交的请求,并设计响应的方式:

请求路径:/carts/{cid}/num/add
    请求参数:@PathVariable("cid") Integer cid, HttpSession session
    请求类型:POST
    响应结果:JsonResult<Integer>

(c) 处理请求

代码:

    @RequestMapping("{cid}/num/add")public JsonResult<Integer> addNum(@PathVariable("cid") Integer cid, HttpSession session) {// 从Session中获取uid和usernameInteger uid = getUidFromSession(session);String username = getUsernameFromSession(session);// 调用业务对象执行增加数量Integer data = cartService.addNum(cid, uid, username);// 返回成功return new JsonResult<>(OK, data);}

测试:http://localhost:8080/carts/10/num/add

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

显示确认订单页-显示勾选的购物车数据

74. 显示确认订单页-显示勾选的购物车数据-持久层

(a) 规划需要执行的SQL语句

在“确认订单页”显示的商品信息,应该来自前序页面(购物车列表)中勾选的数据,所以,显示的信息其实是购物车中的数据!到底显示哪些,取决于用户的勾选操作,当用户勾选了若干条购物车数据后,这些数据的id应该传递到当前“确认订单页”中,该页面根据这些id获取需要显示的数据列表!
所以,在持久层需要完成“根据若干个不确定的id值,查询购物车数据表,显示购物车中的数据信息”!
则需要执行的SQL语句大致是:

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

(b) 接口与抽象方法

在`CartMapper`接口中添加:

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

(c) 配置SQL映射

映射:

    <!-- 根据若干个购物车数据id查询详情的列表 --><!-- List<CartVO> findVOByCids(Integer[] cids) --><select id="findVOByCids"resultType="cn.tedu.store.vo.CartVO">SELECTcid, pid,uid, title,image, t_cart.price,t_product.price AS realPrice,t_cart.num FROMt_cartLEFT JOINt_productONt_cart.pid=t_product.idWHEREcid IN (<foreach collection="array"item="cid" separator=",">#{cid}</foreach>)ORDER BYt_cart.created_time DESC</select>

测试:

    @Testpublic void findVOByCids() {Integer[] cids = {8,10,12,15,20,30};List<CartVO> list = mapper.findVOByCids(cids);System.err.println("count=" + list.size());for (CartVO item : list) {System.err.println(item);}}

75. 显示确认订单页-显示勾选的购物车数据-业务层-参考购物列表

(a) 规划异常

(b) 接口与抽象方法

在`ICartService`中添加抽象方法:

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

(c) 实现抽象方法

在`CartServiceImpl`中添加私有实现的持久层方法:

    /*** 根据若干个购物车数据id查询详情的列表* @param cids 若干个购物车数据id* @return 匹配的购物车数据详情的列表*/private List<CartVO> findVOByCids(Integer[] cids) {return cartMapper.findVOByCids(cids);}

然后,重写业务接口中的抽象方法:

    @Overridepublic List<CartVO> getVOByCids(Integer[] cids, Integer uid) {List<CartVO> list = findVOByCids(cids);// for (CartVO cart : list) {//     if (!cart.getUid().equals(uid)) {//     list.remove(cart);//     }// }Iterator<CartVO> it = list.iterator();while (it.hasNext()) {CartVO cart = it.next();if (!cart.getUid().equals(uid)) {it.remove();}}return list;}

测试:

    @Testpublic void getVOByCids() {Integer[] cids = {7,13,14,15,16,17};Integer uid = 19;List<CartVO> list = service.getVOByCids(cids, uid);System.err.println("count=" + list.size());for (CartVO item : list) {System.err.println(item);}}

76. 显示确认订单页-显示勾选的购物车数据-控制器-参考购物列表

(a) 处理异常

(b) 设计请求

设计用户提交的请求,并设计响应的方式:

请求路径:/carts/list
    请求参数:Integer[] cids, HttpSession session
    请求类型:GET
    响应结果:JsonResult<List<CartVO>>

(c) 处理请求

代码:

    @GetMapping("list")public JsonResult<List<CartVO>> getVOByCids(Integer[] cids, HttpSession session) {// 从Session中获取uidInteger uid = getUidFromSession(session);// 调用业务对象执行查询数据List<CartVO> data = cartService.getVOByCids(cids, uid);// 返回成功与数据return new JsonResult<>(OK, data);}

测试: http://localhost:8080/carts/list?cids=7&cids=8&cids=13&cids=14&cids=17&cids=18

77. 显示确认订单页-显示勾选的购物车数据-前端页面-参考购物列表

创建订单

78. 创建订单-创建数据表

   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)) DEFAULT CHARSET=utf8mb4;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)) DEFAULT CHARSET=utf8mb4;

79. 创建订单-创建实体类

   /*** 订单数据的实体类*/public class Order extends BaseEntity {private static final long serialVersionUID = -3216224344757796927L;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;}
    /*** 订单中的商品数据*/public class OrderItem extends BaseEntity {private static final long serialVersionUID = -2108307989215829978L;private Integer id;private Integer oid;private Integer pid;private String title;private String image;private Long price;private Integer num;}

80. 创建订单-持久层

创建`cn.tedu.store.mapper.OrderMapper`接口,并在接口中添加:

    Integer insertOrder(Order order);Integer insertOrderItem(OrderItem orderItem);

映射:

    <mapper namespace="cn.tedu.store.mapper.OrderMapper"><!-- 插入订单数据 --><!-- Integer insertOrder(Order order) --><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><!-- 插入订单商品数据 --><!-- Integer insertOrderItem(OrderItem orderItem) --><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>

测试:

    @RunWith(SpringRunner.class)@SpringBootTestpublic class OrderMapperTests {@Autowiredprivate OrderMapper mapper;@Testpublic void insertOrder() {Order order = new Order();order.setUid(1);order.setRecvName("小王");Integer rows = mapper.insertOrder(order);System.err.println("rows=" + rows);}@Testpublic void insertOrderItem() {OrderItem orderItem = new OrderItem();orderItem.setOid(1);orderItem.setPid(2);orderItem.setTitle("高档铅笔");Integer rows = mapper.insertOrderItem(orderItem);System.err.println("rows=" + rows);}}

81. 创建订单-业务层

由于处理过程中还需要涉及收货地址数据的处理,所以,需要先在`IAddressService`中添加:

    Address getByAid(Integer aid, Integer uid);

并在`AddressServiceImpl`中实现:

    @Overridepublic Address getByAid(Integer aid, Integer uid) {Address address = 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;}

先创建`cn.tedu.store.service.IOrderService`业务层接口,并添加抽象方法:

    Order create(Integer aid, Integer[] cids, Integer uid, String username);

再创建`cn.tedu.store.service.impl.OrderServiceImpl`业务层实现类,实现以上接口,在类之前添加`@Service`注解,在类中添加`@Autowired private OrderMapper orderMapper;`持久层对象和`@Autowired private IAddressService addressService;`、`@Autowired private ICartService cartService;`处理收货地址和购物车数据的对象:

   @Servicepublic class OrderServiceImpl implements IOrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate IAddressService addressService;@Autowiredprivate ICartService cartService;}

然后,在类中添加持久层的2个方法,私有实现:

    /*** 插入订单数据* @param order 订单数据*/private void insertOrder(Order order) {Integer rows = orderMapper.insertOrder(order);if (rows != 1) {throw new InsertException("插入订单数据时出现未知错误,请联系系统管理员");}}/*** 插入订单商品数据* @param orderItem 订单商品数据*/private void insertOrderItem(OrderItem orderItem) {Integer rows = orderMapper.insertOrderItem(orderItem);if (rows != 1) {throw new InsertException("插入订单商品数据时出现未知错误,请联系系统管理员");}}

最后,重写业务接口中的抽象方法,实现代码:

    @Override@Transactionalpublic Order create(Integer aid, Integer[] cids, Integer uid, String username) {// 创建当前时间对象Date now = new Date();// 根据cids查询所勾选的购物车列表中的数据List<CartVO> carts = cartService.getVOByCids(cids, uid);// 计算这些商品的总价long totalPrice = 0;for (CartVO cart : carts) {totalPrice += cart.getRealPrice() * cart.getNum();}// 创建订单数据对象Order order = new Order();// 补全数据:uidorder.setUid(uid);// 查询收货地址数据Address address = addressService.getByAid(aid, uid);// 补全数据:收货地址相关的6项order.setRecvName(address.getName());order.setRecvPhone(address.getPhone());order.setRecvProvince(address.getProvinceName());order.setRecvCity(address.getCityName());order.setRecvArea(address.getAreaName());order.setRecvAddress(address.getAddress());// 补全数据:totalPriceorder.setTotalPrice(totalPrice);// 补全数据:statusorder.setStatus(0);// 补全数据:下单时间order.setOrderTime(now);// 补全数据:日志order.setCreatedUser(username);order.setCreatedTime(now);order.setModifiedUser(username);order.setModifiedTime(now);// 插入订单数据insertOrder(order);// 遍历carts,循环插入订单商品数据for (CartVO cart : carts) {// 创建订单商品数据OrderItem item = new OrderItem();// 补全数据:oid(order.getOid())item.setOid(order.getOid());// 补全数据:pid, title, image, price, numitem.setPid(cart.getPid());item.setTitle(cart.getTitle());item.setImage(cart.getImage());item.setPrice(cart.getRealPrice());item.setNum(cart.getNum());// 补全数据:4项日志item.setCreatedUser(username);item.setCreatedTime(now);item.setModifiedUser(username);item.setModifiedTime(now);// 插入订单商品数据insertOrderItem(item);}// 返回return order;}

测试:

    @RunWith(SpringRunner.class)@SpringBootTestpublic class OrderServiceTests {@Autowiredprivate IOrderService service;@Testpublic void create() {try {Integer aid = 33;Integer[] cids = {9,12,13,14,15,16};Integer uid = 19;String username = "订单管理员";service.create(aid, cids, uid, username);System.err.println("OK");} catch (ServiceException e) {System.err.println(e.getClass().getSimpleName());System.err.println(e.getMessage());}}}

82. 创建订单-控制器层

关于创建订单的请求的设计:

请求路径:/orders/create
    请求参数:Integer aid, Integer[] cids, HttpSession session
    请求类型:POST
    响应结果:JsonResult<Order>

然后,先创建`cn.tedu.store.controller.OrderController`控制器类,继承自`BaseController`,添加`@RestController`和`@RequestMapping("orders")`注解,在类中添加`@Autowired private IOrderService orderService;`业务对象,然后,添加处理请求的方法:

    @RestController@RequestMapping("orders")public class OrderController extends BaseController {@Autowiredprivate IOrderService orderService;@RequestMapping("create")public JsonResult<Order> create(Integer aid, Integer[] cids,HttpSession session) {// 从Session中取出uid和usernameInteger uid = getUidFromSession(session);String username = getUsernameFromSession(session);// 调用业务对象执行业务Order data = orderService.create(aid, cids, uid, username);// 返回成功与数据return new JsonResult<>(OK, data);}}

测试: http://localhost:8080/order/create?aid=33&cids=8&cids=9&cids=10&cids=11

Spring AOP

AOP:面向切面(Aspect)编程。

AOP并不是Spring框架的特性,只是Spring很好的支持了AOP!

一般数据的处理流程是:

注册:    客户端 -> Controller -> Service.reg() -> Mapper

登录:    客户端 -> Controller -> Service.login() -> Mapper

改密:    客户端 -> Controller -> Service.changePassword() -> Mapper

如果需要在处理每个业务时,都执行特定的代码!则可以假设在整个数据处理流程中存在某个切面,切面中可以定义某些方法,当处理流程执行到切面时,就会自动执行切面中的方法!最终,实现的效果就是:只需要定义好切面方法,配置好切面的位置(连接点),在不需要修改原有数据处理流程的代码的基础之上,就可以使得若干个流程都执行相同的代码!

在使用Spring AOP编程时,需要先添加2个关于AOP的依赖:

<dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjtools</artifactId>
        <version>1.9.4</version>
    </dependency>

假设需要实现**统计每个业务方法的执行耗时**!

则先创建`cn.tedu.store.aop.TimerAspect`切面类,在类之前添加`@Aspect`和`@Component`注解:

    @Aspect@Componentpublic class TimerAspect {}

然后,在类中添加切面方法,关于切面方法的声明:

1. 应该使用`public`权限;

2. 返回值类型可以是`void`或`Object`,如果使用的注解是`@Around`时,必须使用`Object`作为返回值类型,并返回连接点方法的返回值,如果使用的注解是`@Before`或`@After`时,则自行决定;

3. 方法名称可以自定义;

4. 参数列表中可以添加`ProceedingJoinPoint`接口类型的对象,该对象表示连接点,也可以理解调用切面所在位置对应的方法的对象,如果使用的注解是`@Around`时,必须添加该参数,反之,则不是必须添加;

则添加方法:

    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {// 记录起始时间long start = System.currentTimeMillis();// 执行连接点方法,即切面所在位置对应的方法 // 本例中表示执行注册或执行登录等Object result = pjp.proceed();// 记录结果时间long end = System.currentTimeMillis();// 计算耗时System.err.println("耗时:" + (end - start) + "ms.");// 返回连接点方法的返回值return result;}

然后,需要在方法之前添加注解,以配置连接点,即哪些方法需要应用该切面:

    @Around("execution(* cn.tedu.store.service.impl.*.*(..))")

达内课程结束!!!

学子商城项目1(项目 第十六阶段)相关推荐

  1. .NET Core实战项目之CMS 第十六章 用户登录及验证码功能实现

    前面为了方便我们只是简单实现了基本业务功能的增删改查,但是登录功能还没有实现,而登录又是系统所必须的,得益于 ASP.NET Core的可扩展性因此我们很容易实现我们的登录功能.今天我将带着大家一起来 ...

  2. 优秀的 Verilog/FPGA开源项目介绍(三十六)-RISC-V(新增一)

    关于RISC-V的二三事 risc-v官网 ❝ https://riscv.org/ RISC-V(跟我读:"risk----------------five")是一个基于精简指令 ...

  3. 项目实训(十六)——总结

    通过这次项目实训学到了很多自己之前没有接触过的知识,对unity游戏开发和C#语言有了更深入的了解.开发的过程非常艰难,但最终还是解决了不少问题,完成了许多功能.在这里我要感谢一下我的队友们和指导老师 ...

  4. Android项目实战(二十六):蓝牙连接硬件设备开发规范流程

    前言: 最近接触蓝牙开发,主要是通过蓝牙连接获取传感器硬件设备的数据,并进行处理. 网上学习一番,现整理出一套比较标准的 操作流程代码. 如果大家看得懂,将来只需要改下 硬件设备的MAC码 和 改下对 ...

  5. 项目过程管理(十六)项目周报

    原则 有事起奏无事退朝 项目经理可在周一上午召开站会收集信息,各职能负责人需积极配合. 周一下午3点前发出邮件 邮件 接着立项邮件全体回复,每周接着上一周发直到结项 收件人:项目组群 标题:[项目周报 ...

  6. 【java_wxid项目】【第十六章】【Spring Cloud Alibaba Sentinel集成】

    主项目链接:https://gitee.com/java_wxid/java_wxid 项目架构及博文总结: 点击:[使用Spring Boot快速构建应用] 点击:[使用Spring Cloud O ...

  7. Android项目实战(三十六):给背景加上阴影效果

    圆角背景大家应该经常用: 一个drawable资源文件  里面控制corner圆角 和solid填充色 <shape xmlns:android="http://schemas.and ...

  8. 项目实训(十六)FPS游戏之PUN角色位移同步,动画状态同步

    文章目录 前言 一.PUN角色位移同步 二.PUN角色动画状态同步 前言 FPS游戏之PUN角色位移同步,动画状态同步 一.PUN角色位移同步 首先添加一个photon transform view的 ...

  9. Android 项目必备(二十六)-->获取手机中所有 APP

    效果图 代码 添加依赖 implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30' implementation ...

  10. Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)十六(商品排序,Thymeleaf快速入门,商品详情页的展示)

    Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)十六(商品详情页的展示) 一.商品排序 1.完善页面信息 这是用来做排序的,默认按照综合排序 ...

最新文章

  1. 密码学基础知识(七)公钥密码
  2. Python基础教程:条件语句的七种写法
  3. spring中注解无法修饰静态变量
  4. Python--简单的端口扫描脚本
  5. 7月送书中奖名单,快看!
  6. javascript入门_您需要一个JavaScript入门工具包
  7. python读取文件编码错误_Python 读取文本文件编码错误解决方案(未知文本文件编码情况下解决方案)...
  8. html css浮动标签,12种超酷HTML5 SVG和CSS3浮动标签效果
  9. linux mint 14 shurufa
  10. sqlServer对内存的管理
  11. python模块:array数组模块
  12. 修改mysql 表的字符编码
  13. 近年来最流行网络词汇及论坛用语
  14. 小甲鱼python课后题答案_小甲鱼python课后习题总结
  15. CCNA培训视频教程下载
  16. Linux fstab文件详解
  17. 苦涩的技术我该怎么学?Akka 实战
  18. 从0开始,如何运营一个公众号?
  19. LayaBox微信小游戏截图功能 利用微信API实现完美截图
  20. 秀米数字编号实用知识点

热门文章

  1. PSP1000/2000/3000 PSPgo全主机介绍(2)
  2. hadoop学习使用
  3. 最强联合!北大清华互相开放本科课程(附课程名单)
  4. DBeaver数据库连接工具的简单操作
  5. nfine mysql_全开源版NFine快速开发框架C#源码
  6. 2021最新计算机二级C语言试题
  7. 数据分析项目实战—信用卡客户违约概率预测
  8. 极路由大部分机型官方固件
  9. Keil5编译环境搭建流程----STM32和GD32
  10. html连接sql数据库详解,HTML5 Web SQL数据库使用详解