学子商城项目1(项目 第十六阶段)

本文档详细介绍了学子商城项目的开发过程,包括项目分析、数据库创建、用户模块(注册、登录、修改密码、上传头像、收货地址管理)以及商品展示、购物车、订单创建等功能的实现。项目遵循增查删改的顺序,涉及持久层、业务层、控制器和前端页面的开发,同时涵盖了密码加密、文件上传、Spring JDBC事务处理和RESTful API设计。
摘要由CSDN通过智能技术生成

目录

学子商城

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()`测试方法,以检查测试环境是否正常。

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

  @Autowired
    private DataSource dataSource;

    @Test
    public 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
                *
            FROM
                t_user
            WHERE
                username=#{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)
    @SpringBootTest
    public class UserMapperTests {
        
        @Autowired
        private UserMapper mapper;
        
        @Test
        public void insert() {
            User user = new User();
            user.setUsername("mybatis");
            user.setPassword("1234");
            Integer rows = mapper.insert(user);
            System.err.println("rows=" + rows);
        }
        
        @Test
        public 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`注解,并在类中添加持久层对象:

  @Service
    public class UserServiceImpl implements IUserService {
        
        @Autowired
        private 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;
    }

    @Override
    public void reg(User user) {
        // 根据参数user获取尝试注册的用户名
        String username = user.getUsername();
        // 调用持久层的User findByUsername(String username)方法,根据用户名查询用户数据
        User result = userMapper.findByUsername(username);
        // 判断查询结果是否不为null
        if (result != null) {
            // 是:表示用户名已经被占用,抛出UsernameDuplicateException
            throw 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);
        // 判断受影响的行数是否不为1
        if (rows != 1) {
            // 是:插入数据时出现某种错误,抛出InsertException
            throw new InsertException();
        }
    }

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

 @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserServiceTests {
    
        @Autowired
        private IUserService service;
        
        @Test
        public 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")
    @RestController
    public class UserController {

        @Autowired
        private 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")
    @RestController
    public class UserController extends BaseController {
    
        @Autowired
        private 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`实体类中添加并实现以上方法,具体实现为:

 @Override
    public User login(String username, String password) {
        // 调用userMapper的findByUsername(),根据参数username查询用户数据
        User result = userMapper.findByUsername(username);
        // 判断查询结果是否为null
        if (result == null) {
            // 是:抛出UserNotFoundException
            throw new UserNotFoundException("用户名错误");
        }

        // 判断查询结果中的isDelete是否为1
        if (result.getIsDelete() == 1) {
            // 是:抛出UserNotFoundException
            throw new UserNotFoundException("用户名错误");
        }

        // 从查询结果中获取盐值
        String salt = result.getSalt();
        // 调用getMd5Password()将参数password和salt结合起来加密
        String md5Password = getMd5Password(password, salt);
        // 判断查询结果中的密码,与以上加密得到的密码,是否不一致
        if (!result.getPassword().equals(md5Password)) {
            // 是:抛出PasswordNotMatchException
            throw 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`中编写并完成单元测试:

    @Test
    public 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">
        UPDATE
            t_user
        SET
            password=#{password},
            modified_user=#{modifiedUser},
            modified_time=#{modifiedTime}
        WHERE
            uid=#{uid}
    </update>

    <!-- 根据用户id查询用户数据 -->
    <!-- User findByUid(Integer uid) -->
    <select id="findByUid"
        resultMap="UserEntityMap">
        SELECT
            *
        FROM
            t_user
        WHERE
            uid=#{uid}
    </select>

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

  @Test
    public 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);
    }
    
    @Test
    public 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`实现以上抽象方法:

    @Override
    public void changePassword(Integer uid, String username, String oldPassword, String newPassword) {
        // 调用userMapper的findByUid()方法,根据参数uid查询用户数据
        User result = userMapper.findByUid(uid);
        // 检查查询结果是否为null
        if (result == null) {
            // 是:抛出UserNotFoundException
            throw new UserNotFoundException("用户数据不存在");
        }

        // 检查查询结果中的isDelete是否为1
        if (result.getIsDelete().equals(1)) {
            // 是:抛出UserNotFoundException
            throw new UserNotFoundException("用户数据不存在");
        }

        // 从查询结果中取出盐值
        String salt = result.getSalt();
        // 将参数oldPassword结合盐值加密,得到oldMd5Password
        String oldMd5Password = getMd5Password(oldPassword, salt);
        // 判断查询结果中的password与oldMd5Password是否不一致
        if (!result.getPassword().contentEquals(oldMd5Password)) {
            // 是:抛出PasswordNotMatchException
            throw new PasswordNotMatchException("原密码错误");
        }

        // 将参数newPassword结合盐值加密,得到newMd5Password
        String newMd5Password = getMd5Password(newPassword, salt);
        // 创建当前时间对象
        Date now = new Date();
        // 调用userMapper的updatePasswordByUid()更新密码,并获取返回值
        Integer rows = userMapper.updatePasswordByUid(
            uid, newMd5Password, username, now);
        // 判断以上返回的受影响行数是否不为1
        if (rows != 1) {
            // 是:抛了UpdateException
            throw new UpdateException("更新用户数据时出现未知错误,请联系系统管理员");
        }
    }

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

 @Test
    public 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和username
        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=xx&newPassword=xx`进行测试。

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

17. 拦截器

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

先创建拦截器类:

  public class LoginInterceptor implements HandlerInterceptor {
    
        @Override
        public 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`注解:

   @Configuration
    public class LoginInterceptorConfigurer implements WebMvcConfigurer {
    
        @Override
        public 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) 规划需要

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值