项目学子商城(加密,异常,拦截器SpringBoot没有xml配置文件,附常见配置错误)

本文详细介绍了基于SpringBoot开发项目的过程,包括用户注册、登录、修改密码、个人资料等功能,强调了异常处理和登录拦截器的实现。文章详细讲解了数据库设计、持久层、业务层和控制器层的实现,以及如何处理各种异常情况,如用户名重复、密码错误等。此外,还涵盖了密码加密、文件上传的注意事项和最佳实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

### 1. 项目的分析

接手到新的项目,首先,应该大致分析这个项目中有哪些类型的数据,例如:商品、商品分类、用户、收货地址、收藏、购物车、订单……

然后,对这些需要处理的数据排个开发顺序,通常遵守的原则有2个:由简到难,由基础数据开始!所以,以上数据的开发顺序应该是:用户 > 收货地址 > 商品分类 > 商品 > 收藏 > 购物车 > 订单。

接下来,根据需求(也可以根据现有的界面设计)分析每种数据处理时涉及的功能,例如用户数据的相关功能有:注册、登录、修改密码、修改个人资料、上传头像,并对这些功能的开发设计先后顺序,通常遵守增、查、删、改的顺序,可以是:注册 > 登录 > 修改密码 > 修改个人资料 > 上传头像。

针对每个功能,开发顺序应该是:数据库与数据表 > 实体类 > 持久层 > 业务层 > 控制器层 > 前端界面。

**做项目之前,一定要把某个数据或功能拆出来,一次只解决一个问题!**

### 2. 用户-注册-数据库与数据表

创建数据库:

CREATE DATABASE tedu_store;

使用数据库:

USE tedu_store;

创建用户数据表:

CREATE TABLE t_user (
		uid INT AUTO_INCREMENT COMMENT '用户id',
		username VARCHAR(20) UNIQUE NOT NULL COMMENT '用户名',
		password CHAR(32) NOT NULL COMMENT '密码',
		salt CHAR(36) COMMENT '盐值',
		gender INT COMMENT '性别,0-女性,1-男性',
		phone VARCHAR(20)  COMMENT '电话',
		email VARCHAR(50) COMMENT '邮箱',
		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=UTF8;

### 3. 用户-注册-实体类

打开`https://start.spring.io`,准备创建SpringBoot项目,勾选上MySQL和MyBatis,()war)将下载的项目导入到Eclipse中,由于添加了数据库相关依赖,首先,必须在`application.properties`中添加连接数据库的配置,否则运行时会报告错误:
1.spring.datasource.url=jdbc:mysql://localhost:3306/tedu_store?useUnicode=true&characterEncoding=utf-                8&serverTimezone=Asia/Shanghai
   2. spring.datasource.username=root
    3.spring.datasource.password=root

由于4个日志属性在各数据表中都将存在,对应的实体类也都需要添加这些属性,所以,先创建实体类公共的父类`cn.tedu.store.entity.BaseEntity`用于定义这4个日志属性:

/**
	 * 实体类的基类
	 */
	abstract class BaseEntity implements Serializable {
	
		private static final long serialVersionUID = -5882064199939706583L;
	
		private String createdUser;
		private Date createdTime;
		private String modifiedUser;
		private Date modifiedTime;

		// SET/GET/toString
	}

由于`BaseEntity`只在当前`cn.tedu.store.entity`包中使用,且不需要单独创建对象,所以,可以将访问权限设置为默认(删除`public`),并添加`abstract`修饰符。

然后,创建`cn.tedu.store.entity.User`实体类:

/**
	 * 用户数据的实体类
	 */
	public class User extends BaseEntity {
	
		private static final long serialVersionUID = 8777086855777796877L;
	
		private Integer uid;
		private String username;
		private String password;
		private String salt;
		private Integer gender;
		private String phone;
		private String email;
		private String avatar;
		private Integer isDelete;

		// GET/SET/toString

	}

### 4. 用户-注册-持久层

**1. 分析SQL语句**

增加数据的SQL语句:

    INSERT INTO t_user (除了uid以外的字段列表) VALUES (对应的值)

根据用户名查询用户数据的SQL语句:

    SELECT uid, password, salt, is_delete FROM t_user WHERE username=?

由于以上查询还可以应用于“登录”功能,所以,查询的字段列表中,还应该添加与“登录”相关的字段。(如果暂时无法考虑得特殊周全,可以后续再补充)

**2. 接口与抽象方法**

首先,创建持久层接口`cn.tedu.store.mapper.UserMapper`,并添加抽象方法:

    Integer insert(User user);

    User findByUsername(String username);

> 所有的增删改操作,返回值都使用Integer。

由于当前是第1次编写持久层接口,还需要在启动类上添加`@MapeprScan("cn.tedu.store.mapper")`,用于指定持久层接口所在的包。

**3. 配置映射**

首先,在`resources`下创建`mappers`文件夹,然后,复制粘贴得到`UserMapper.xml`文件。

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"      
 "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
 <!-- namespace:匹配的Java接口 -->
<mapper namespace="cn.tedu.store.mapper.UserMapper">
	<!-- 根据用户名查询用户数据 -->
	<!-- User findByUsername(String username) -->
	<select id="findByUsername" resultType="cn.tedu.store.entity.User">
		select uid,password,salt,is_delete as isDelete from t_user where username=#{username}
	</select>
	<!-- 插入用户数据 -->
	<!-- Integer insert(User user) -->
	<insert id="insert">
		insert into t_user(username,password,salt,gender,phone,email,avatar,is_delete) 
		values (#{username},#{password},#{salt},#{gender},#{phone},#{email},#{avatar},#{isDelete})
	</insert>
</mapper>

由于当前是第1次配置持久层映射,则需要在`application.properties`中配置`mybatis.mapper-locations=classpath:mappers/*.xml`,用于指定映射文件所在的位置。classpath默认在resources


# datasource 
spring.datasource.url=jdbc:mysql://localhost:3306/tedu_store?useUnicode=true&&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
# mybatis mapper
mybatis.mapper-locations=classpath:mappers/*.xml

完成后,应该编写并执行单元测试:在`src\test\java`下创建`cn.tedu.store.mapper.UserMapperTestCase`类,用于测试`UserMapper`接口中定义的抽象方法:

 @RunWith(SpringRunner.class)
    @SpringBootTest   
 
                必须加


	@RunWith(SpringRunner.class)
	@SpringBootTest
	public class UserMapperTestCase {
	
		@Autowired
		public UserMapper mapper;
		
		@Test
		public void insert() {
			User user = new User();
			user.setUsername("root");
			user.setPassword("1234");
			Integer rows = mapper.insert(user);
			System.err.println("rows=" + rows);
		}
		
		@Test
		public void findByUsername() {
			String username = "root";
			User result = mapper.findByUsername(username);
			System.err.println(result);
		}
		
	}

### 5. 用户-注册-业务层

**1. 设计异常**

在业务层中,应该把所有认为的操作失败(例如注册时用户名被占用、登录时用户名错误、登录时密码错误等等)设计出对应的异常!(登录成功返回user数据,但不能返回登录失败状态,所以想到用异常来处理)

则应该创建异常类:

cn.tedu.store.service.ex.ServiceException(继承自RuntimeException)
    cn.tedu.store.service.ex.UsernameDuplicateException(继承自ServiceException)
    cn.tedu.store.service.ex.InsertException(继承自ServiceException)

凡是自行抛出的异常,都应该是`RuntimeException`的子孙类异常,同时,为了便于后续的处理,应该自定义某个异常类,然后,当前项目中会抛出的异常都应该是它的子孙类异常。

> 凡涉及增删改操作都应该判断其返回值(受影响的行数),如果返回值与期望值不同,则抛出异常!

**2. 接口与抽象方法**

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

    void reg(User user) 
        throws UsernameDuplicateException, 
            InsertException;

返回值:以操作正确(例如注册成功、登录成功等)为前提,如果需要向外(向方法的调用者,也就是控制器,甚至向客户端)提供某些数据,如果需要,则以这个数据的类型作为返回值,如果不需要向外提供数据,则使用`void`即可;

方法名称:应该与某个业务(在用户来看是某个功能)相对应,例如注册功能的方法名可以使用`reg`,登录功能的方法名称可以使用`login`;

参数:必须通过该参数能够调用持久层中的那些方法,以注册为例,可能需要调用持久层中的`Integer insert(User user)`和`User findByUsername(String username)`方法,则当前业务层接口中的抽象方法的参数也能基本一系列的运算能调用这2个方法。

**3. 实现**

创建`cn.tedu.store.service.impl.UserServiceImpl`类,实现`IUserService`接口,添加`@Service`注解,在类中添加`@Autowired private UserMapper userMapper;`,即:

@Service
	public class UserServiceImpl implements IUserService {
	
		@Autowired private UserMapper userMapper;
		
		@Override
		public void reg(User user) 
			throws UsernameDuplicateException, 
				InsertException {
			// TODO Auto-generated method stub
	
		}
	
	}

在重写抽象方法之前,应该先将持久层接口中的方法复制到业务层实现类中,添加`private`权限,并实现这些方法。

如果是增删改方法,则应该判断返回值,并在返回值与期望值不相符时抛出对应的异常,方法原本的返回值类型修改为`void`;如果是查询方法,则直接调用持久层对象完成查询功能即可,并不抛出异常,因为同一个查询,有时查询到数据是正确的,而有时查询不到数据才是正确的,以“根据用户名查询用户数据”为例,在“注册”功能中,只有查询结果为null才能继续注册,即查询不到数据是正确的,但是在“登录”功能中,只有查询到数据才是正确的!

/**
	 * 插入用户数据
	 * @param user 用户数据
	 */
	private void insert(User user) {
		Integer rows = userMapper.insert(user);
		if (rows != 1) {
			throw new InsertException();
		}
	}
	
	/**
	 * 根据用户名查询用户数据
	 * @param username 用户名
	 * @return 匹配的用户数据,如果没有匹配的数据,则返回null
	 */
	private User findByUsername(String username) {
		return userMapper.findByUsername(username);
	}

然后,重写抽象方法:

@Override
	public void reg(User user) 
		throws UsernameDuplicateException, 
			InsertException {
		// 根据尝试注册的用户名查询用户数据
		String username = user.getUsername();
		User result = findByUsername(username);
		// 检查用户名是否被占用:如果查询到数据,则表示被占用,如果查询结果为null,则表示用户名没有被占用
		if (result == null) {
			// 未占用:执行注册
			insert(user);
		} else {
			// 已占用:抛出UsernameDuplicateException
			throw new UsernameDuplicateException();
		}
	}

可以看到,重写的方法中,并不直接调用持久层对象来实现增删改查,而是调用自身的私有方法,间接的调用到持久层对象来实现数据访问。

完成后,应该编写并执行单元测试:在`src\test\java`下创建`cn.tedu.store.service.UserServiceTestCase`类,用于测试`IUserService`接口中定义的抽象方法:

@RunWith(SpringRunner.class)
	@SpringBootTest
	public class UserServiceTestCase {
	
		@Autowired
		public IUserService service;
		
		@Test
		public void reg() {
			try {
				User user = new User();
				user.setUsername("mybatis");
				user.setPassword("1234");
				user.setGender(1);
				user.setPhone("13800138006");
				user.setEmail("mybatis@tedu.cn");
				user.setAvatar("http://www.tedu.cn/mybatis.png");
				service.reg(user);
				System.err.println("OK");
			} catch (ServiceException e) {
				System.err.println(e.getClass().getName());
				System.err.println(e.getMessage());
			}
		}
		
	}

注册后,通过查询数据表,可以发现,仍有一部分数据没有值,例如is_delete、created_user字段等,所以,业务层的实现类还有一项任务,就是“保障数据的完整性”,那些不由用户(客户端)提交的数据,应该在业务层中来生成,所以,需要调整业务层实现类中的代码:

@Override
	public void reg(User user) 
		throws UsernameDuplicateException, 
			InsertException {
		// 根据尝试注册的用户名查询用户数据
		String username = user.getUsername();
		User result = findByUsername(username);
		// 检查用户名是否被占用:如果查询到数据,则表示被占用,如果查询结果为null,则表示用户名没有被占用
		if (result == null) {
			// 设置is_delete
			user.setIsDelete(0);
			
			// 设置4项日志
			Date now = new Date();
			user.setCreatedUser(username);
			user.setCreatedTime(now);
			user.setModifiedUser(username);
			user.setModifiedTime(now);
			
			// TODO 密码加密
			
			// 执行注册
			insert(user);
		} else {
			// 已占用:抛出UsernameDuplicateException
			throw new UsernameDuplicateException(
				"注册失败!您尝试注册的用户名(" + username + ")已经被占用!");
		}
	}

密码加密的必要性:如果直接并密码使用明文方式存储,可能存在内部泄露或入侵盗取的风险。

通常,对密码进行加密时,并不会使用通用的加密算法,因为这些加密算法都是可以被逆向运算的,即:只要能够获得加密过程中的所有参数,就可以将密码逆向运算得到原始密码。

真正应用于密码加密的是“消息摘要算法”,消息摘要算法的特性:

1. 原文相同,则摘要数据一定相同;

2. 摘要算法不变,则摘要数据的长度不变,无视原文长度;

3. 原文不同,则摘要数据几乎不会相同。

常见的消息摘要算法有:SHA-128、SHA-256、SHA-384、SHA-512、MD2、MD4、MD5……

关于MD5的破解:

- 消息摘要算法不存在逆向运算,所谓的消息摘要算法的破解,是如何找到2个或更多不同的原文,却可以得到相同的摘要,以MD5为例,理论上运算2的128次方的次数就可以找到,而破解的核心在于运算更少的次数来找到这样的数据;

- 在线破解:本质是记录了原文与摘要数据的对应关系,当输入摘要数据时,查询得到原文,这些在线破解只能“破解”原文比较简单的数据。

进一步加强密码的安全性:

1. 加强原始密码的复杂程度;

2. 使用位数更长的加密算法;

3. 加盐;

4. 多重加密;

5. 综合以上用法。

在实际应用中,应该在业务层添加一个用于加密的方法,后续在注册、登录及其它需要验证密码的场合都可以直接调用该方法:

/**
	 * 获取执行MD5加密后的密码
	 * @param password 原密码
	 * @param salt 盐值
	 * @return 加密后的密码
	 */
	private String getMd5Password(
			String password, String salt) {
		// 加密规则:使用“盐+密码+盐”作为原始数据,执行5次加密
		String result = salt + password + salt;
		for (int i = 0; i < 5; i++) {
			result = DigestUtils
				.md5DigestAsHex(result.getBytes()).toUpperCase();
		}
		return result;
	}

并且,在注册过程中,需要执行加密,并且,将加密后的密码、生成的盐值都封装到执行注册的user对象中,以将这些数据插入到数据表中:

// 生成随机盐
	String salt = UUID.randomUUID().toString().toUpperCase();
	// 执行密码加密,得到加密后的密码
	String md5Password = getMd5Password(user.getPassword(), salt);
	// 将盐和加密后的密码封装到user中
	user.setPassword(md5Password);
	user.setSalt(salt);

注意:由于应用了新的密码功能,所以,此前产生的测试数据已经无法使用,应该将此前产生的测试数据全部删除,避免后续使用时出现错误。

### 6. 用户-注册-控制器层

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

@RestController
	@RequestMapping("/users")
	public class UserController {
	
		@Autowired
		private IUserService userService;
		
	}

**1. 设计请求**

    请求路径:/users/reg
    请求参数:User
    请求方式:POST
    响应数据:无

**2. 处理请求**

首先,应该创建用于响应操作结果的`cn.tedu.store.util.ResponseResult`类:

/**
	 * 用于向客户端响应操作结果的类型
	 * @param <T> 操作结果中包含的数据的类型
	 */
	public class ResponseResult<T> implements Serializable {
	
		private static final long serialVersionUID = -5368505763231357265L;
	
		private Integer state;
		private String message;
		private T data;
	
		// SET/GET

	}

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

@GetMapping("/reg")
	public ResponseResult<Void> reg(User user) {
		ResponseResult<Void> rr
			= new ResponseResult<Void>();
		
		try {
			userService.reg(user);
			rr.setState(1);
		} catch (ServiceException e) {
			rr.setState(2);
			rr.setMessage(e.getMessage());
		}
		
		return rr;
	}

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

测试完成后,将`@GetMapping`调整为`@PostMapping`。

### 6. 用户-注册-控制器层

**[附] 异常**

异常的体系结构:

    Throwable
        Error
            OutOfMemoryError
        Exception
            IOException
                FileNotFoundException
            RuntimeException
                NullPointerException
                NumberFormatException
                ClassCastException
                ArithmeticException
                IndexOutOfBoundsException
                    ArrayIndexOutOfBoundsException

关于异常的处理:从语法上,可以通过抛出(在方法体中使用`throw`抛出异常对象,并在方法签名中使用`throws`声明抛出)或捕获(使用`try...catch`语法包裹相关代码)这两种方式对异常进行处理!

抛出:在方法体中如果抛出了异常对象,在方法的签名中必须声明抛出,并且,如果是重写的方法,不可以抛出更多的异常;

捕获:捕获(`catch`)时,如果有多个异常,在捕获时可以不区分先后顺序,如果多个异常有继承关系,必须先捕获子级异常,然后再捕获父级异常。

如果需要处理的异常是`RuntimeException`或其子孙类异常,则不受以上处理的语法约束!主要因为:[1] 这些异常出现的频率可能极高;[2] 这些异常都可以通常事先的判断等操作,杜绝异常的发生。

关于异常的处理:首先,无论怎么处理,其实,该发生的异常已经发生了!所谓的处理,应该是“对已经发生的异常的补救”,通过这种处理,希望后续不再出现类似的“问题”。

在实际应用中,处理异常可能表现为“向用户提示错误信息”,因为,如果程序出现异常,类似于`NullPointerException`这种的信息是普通用户看不懂的,而专业人员可能从中分析出程序的流程或某些数据,所以,这类信息是不应该向外暴露给任何人的!所以,任何异常都是需要处理的!处理时,应该先考虑当前类或当前方法是否适合甚至能够`try...catch`进行处理,如果不行,则应该抛出!

**基于SpringMVC框架的异常处理**

可以自定义一个专门用于处理异常的方法,该方法要求:

1. 访问权限应该是`public`;

2. 返回值与普通处理请求的方法相同;

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

4. 方法的参数必须包含1个异常类型,例如`Throwable`或`Exception`等,表示将会捕获到的异常对象;

5. 必须添加`@ExceptionHandler`注解,该注解要求SSM环境中添加`<mvc:annotation-driven />`。

然后,在方法之前使用`@ExceptionHandler`注解,并在注解参数中定义需要处理的异常的类型:

@ExceptionHandler(ServiceException.class)
	public ResponseResult<Void> handleException(Throwable e) {
		ResponseResult<Void> rr
			= new ResponseResult<Void>();
		rr.setMessage(e.getMessage());
		
		if (e instanceof UsernameDuplicateException) {
			// 400-用户名冲突
			rr.setState(400);
		} else if (e instanceof InsertException) {
			// 500-插入数据异常
			rr.setState(500);
		}
		
		return rr;
	}

这种处理异常的方法,只能作用于当前控制器类中的处理请求的方法,即当前类中的代码出现异常才可以被处理!为了统一处理,应该创建`BaseController`基类,把处理异常的方法放在基类中,然后,当前项目中所有的控制器类都应该继承自这个基类!

### 7. 用户-注册-前端界面

### 8. 用户-登录-持久层

**1. 分析SQL语句**

登录应该是根据用户名查询用户数据,且根据查询结果中的密码和用户输入的密码进行对比,以判断登录成功与否。

    SELECT
        uid, username, password, salt, avatar, is_delete
    FROM 
        t_user 
    WHERE 
        username=?

> 在SQL语句中,不区分英文大小写,所以,在查询时,不应该把密码作为查询条件之一,而是应该把密码作为查询结果的一部分,后续通过Java程序的equals()方法来对比密码!

**2. 接口与抽象方法**

由于此前设计“注册”时,已经定义了相关方法,所以,无需再次开发!

**3. 配置映射**

由于此前设计“注册”时,已经配置了相关映射,所以,只需要添加查询的字段列表即可!
 

<!-- 根据用户名查询用户数据 -->
	<!-- User findByUsername(String username) -->
	<select id="findByUsername"
		resultType="cn.tedu.store.entity.User">
		SELECT 
			uid, username,
			password, salt, 
			avatar,
			is_delete AS isDelete
		FROM 
			t_user 
		WHERE 
			username=#{username}
	</select>

由于修改了程序代码,应该重新执行单元测试(无需重新编写),以测试功能是否正常。

### 9. 用户-登录-业务层

**1. 设计异常**

此次“登录”操作可能出现的异常有:用户名不存在、用户数据已被标记为删除、密码错误。

则应该在`cn.tedu.store.service.ex`创建异常类:

    UserNotFoundException
    PasswordNotMatchException

以上2个异常都应该继承自`ServiceException`。

**2. 接口与抽象方法**

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

User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException;

**3. 实现**

在`UserServiceImpl`中重写以上抽象方法:

@Override
	public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
		// 根据参数username查询用户:User findByUsername(String username)
		User result = findByUsername(username);
		// 判断查询结果是否为null
		if (result == null) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"登录失败!尝试登录的用户不存在!");
		}

		// 判断is_delete是否标记为已删除:isDelete属性值是否为1
		if (result.getIsDelete().equals(1)) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"登录失败!尝试登录的用户不存在!");
		}

		// 从查询结果中获取盐值
		String salt = result.getSalt();
		// 对参数password执行加密
		String md5Password = getMd5Password(password, salt);
		// 判断查询结果中的密码与刚加密结果是否一致
		if (result.getPassword().equals(md5Password)) {
			// 是:准备返回结果,先去除部分不需要对外使用的数据
			result.setPassword(null);
			result.setSalt(null);
			result.setIsDelete(null);
			// 返回查询结果
			return result;
		} else {
			// 否:抛出PasswordNotMatchException
			throw new PasswordNotMatchException(
				"登录失败!错误密码!");
		}
	}

完成后,应该在`UserServiceTestCase`中编写并执行单元测试:

@Test
	public void login() {
		try {
			String username = "root";
			String password = "1234";
			User data = service.login(username, password);
			System.err.println(data);
		} catch (ServiceException e) {
			System.err.println(e.getClass().getName());
			System.err.println(e.getMessage());
		}
	}

### 10. 用户-登录-控制器层

**1. 处理异常**

此次的业务抛出了2种新的异常,分别是`UserNotFoundException`和`PasswordNotMatchException`,则应该在`BaseController`中添加对这2个异常的处理!

**2. 设计请求**

    请求路径:/users/login
    请求参数:String username(*), String password(*)
    请求方式:POST
    响应数据:User

**3. 处理请求**

@GetMapping("/login")
	public ResponseResult<User> login(
		@RequestParam("username") String username,
		@RequestParam("password") String password) {
		User data = userService.login(username, password);
		return new ResponseResult<>(SUCCESS, data);
	}

完成后,可以通过`http://localhost:8080/users/login?username=java&password=123456`进行测试,完成后,将方法之前的注解调整为`@PostMapping`

### 11. 用户-登录-界面

在`login.html`中添加以下代码(从`register.html`中复制过来,并修改得到):

<script type="text/javascript">
	$("#btn-login").click(function(){
		$.ajax({
			"url":"/users/login",
			"data":$("#form-login").serialize(),
			"type":"POST",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					alert("登录成功!");
				} else {
					alert(json.message);
				}
			}
		});
	});
	</script>

然后,检查HTML代码,确保表单的id、登录按钮的id与以上代码保持一致,表单中的用户名、密码的输入框的name值与服务器端控制器要求提交的参数名称保持一致,且登录按钮的type是button。

### 12. 用户-修改密码-持久层

**1. 分析SQL语句**

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

    UPDATE 
        t_user 
    SET 
        password=?, modified_user=?, modified_time=? 
    WHERE 
        uid=?

以上SQL语句中,password的值应该是用户提交的新密码经过加密后的值,即:后续在操作时,需要先获取当前用户的盐值,才可以完成整个功能!包括在这个功能中,还应该先验证用户的原密码,则需要先获取用户的原密码的值!则需要查询功能:

    SELECT 
        password, salt, is_delete 
    FROM 
        t_user 
    WHERE 
        uid=?

**2. 接口与抽象方法**

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

Integer updatePassword(
		@Param("uid") Integer uid, 
		@Param("password") String password, 
		@Param("modifiedUser") String modifiedUser, 
		@Param("modifiedTime") Date modifiedTime);

	User findByUid(Integer uid);

**3. 配置映射**

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

<!-- 更新用户密码 -->
	<!-- Integer updatePassword(
		    @Param("uid") Integer uid, 
		    @Param("password") String password, 
		    @Param("modifiedUser") String modifiedUser, 
		    @Param("modifiedTime") Date modifiedTime) -->
	<update id="updatePassword">
		UPDATE 
			t_user 
		SET 
			password=#{password}, 
			modified_user=#{modifiedUser}, 
			modified_time=#{modifiedTime} 
		WHERE 
			uid=#{uid}
	</update>

	<!-- 根据用户id查询用户数据 -->
	<!-- User findByUid(Integer uid) -->
	<select id="findByUid"
		resultType="cn.tedu.store.entity.User">
		SELECT 
			password, salt, 
			is_delete AS isDelete
		FROM 
			t_user 
		WHERE 
			uid=#{uid}
	</select>

完成后,在`UserMapperTestCase`中编写并执行以上2个抽象方法的单元测试:

@Test
	public void updatePassword() {
		Integer uid = 9;
		String password = "8888";
		String modifiedUser = "超级管理员";
		Date modifiedTime = new Date();
		Integer rows = mapper.updatePassword(uid, password, modifiedUser, modifiedTime);
		System.err.println("rows=" + rows);
	}

	@Test
	public void findByUid() {
		Integer uid = 9;
		User result = mapper.findByUid(uid);
		System.err.println(result);
	}

### 13. 用户-修改密码-业务层

**1. 设计异常**

当修改密码时,应该先执行查询操作,可能涉及的异常有`UserNotFoundException`(例如用户登录之后,数据被后台管理员删除,或者标记为删除)。

在执行修改之前,还应该验证原密码是否正确,则可能出现`PasswordNotMatchException`。

最后,在执行修改时,如果受影响的行数不是1,则应该抛出`UpdateException`,该异常尚不存在,则需要创建。

> 所有的增删改操作都是有匹配的异常的!

**2. 接口与抽象方法**

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

void changePassword(Integer uid, String username, String oldPassword, String newPassword) throws UserNotFoundException, PasswordNotMatchException, UpdateException;

 抽象方法的名称应该与持久层接口中的任何方法的名称都不一样!

**3. 实现**

首先,将持久层新添加的2个方法复制到业务层实现类,声明为私有,并实现,在实现修改操作时,应该将方法的返回值改为`void`,在方法体中需要判断操作返回值,不是预期值时抛出异常:

/**
	 * 更新用户密码
	 * @param uid 用户的id
	 * @param password 新密码
	 * @param modifiedUser 修改执行人
	 * @param modifiedTime 修改时间
	 */
	private void updatePassword(
			Integer uid, String password, 
		    String modifiedUser, Date modifiedTime) {
		Integer rows = userMapper.updatePassword(uid, password, modifiedUser, modifiedTime);
		if (rows != 1) {
			throw new UpdateException(
				"修改用户数据时出现未知错误!");
		}
	}

	/**
	 * 根据用户id查询用户数据
	 * @param uid 用户id
	 * @return 匹配的用户数据,如果没有匹配的数据,则返回null
	 */
	private User findByUid(Integer uid) {
		return userMapper.findByUid(uid);
	}

 只要在持久层接口中声明了新的抽象方法(新的数据访问功能),在业务层的实现类中应该添加与之匹配的私有方法!

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

@Override
	public void changePassword(Integer uid, String username, String oldPassword, String newPassword)
			throws UserNotFoundException, PasswordNotMatchException, UpdateException {
		// 根据uid查询用户数据
		User result = 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);
		// 判断查询结果中的密码(用户当前的真实密码)与oldMd5Password是否不匹配
		if (!result.getPassword().equals(oldMd5Password)) {
			// 是:抛出PasswordNotMatchException
			throw new PasswordNotMatchException(
				"修改密码失败!原密码错误!");
		}

		// 将newPassword结合盐值加密,得到newMd5Password
		String newMd5Password = getMd5Password(newPassword, salt);
		// 创建时间对象now
		Date now = new Date();
		// 执行修改密码:updatePassword(uid, newMd5Password, username, now)
		updatePassword(uid, newMd5Password, username, now);
	}

最后,在`UserServiceTestCase`中编写并执行单元测试:

@Test
	public void changePassword() {
		try {
			Integer uid = 1000;
			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().getName());
			System.err.println(e.getMessage());
		}
	}

### 14. 用户-修改密码-控制器层

**1. 处理异常**

此次在业务层抛了新的异常`UpdateException`,则需要在`BaseController`中进行处理。

**2. 设计请求**

    请求路径:/users/change_password
    请求参数:String old_password, String new_password, HttpSession
    请求方式:POST
    响应数据:无

**3. 处理请求**

首先,因为项目中多处需要获取当前登录的用户的id,则在`BaseController`中添加方法,以简化获取用户id的代码:\

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

在`UserController`中处理请求的代码如下:

@RequestMapping("/change_password")
	public ResponseResult<Void> changePassword(
		@RequestParam("old_password") String oldPassword,
		@RequestParam("new_password") String newPassword,
		HttpSession session) {
		// 从session中获取uid和username
		Integer uid = getUidFromSession(session);
		String username = session.getAttribute("username").toString();
		// 执行修改密码:service.changePassword(uid,username,oldPassword,newPassword)
		userService.changePassword(uid, username, oldPassword, newPassword);
		// 返回结果
		return new ResponseResult<>(SUCCESS);
	}

处理请求的方法的名称可以与业务层中抽象方法的名称一致。

完成后,打开浏览器,先登录,然后通过`http://localhost:8080/users/change_password?old_password=1234&new_password=8888`进行测试。

### 15. 登录拦截器

首先,创建拦截器类

/**
	 * 登录拦截器
	 */
	public class LoginInterceptor implements HandlerInterceptor {
	
		@Override
		public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
				throws Exception {
			HttpSession session = request.getSession();
			if (session.getAttribute("uid") == null) {
				response.sendRedirect("/web/login.html");
				return false;
			}
			return true;
		}
	
	}

在SpringBoot项目中,并没有xml的配置文件,相关配置都是通过实现`WebMvcConfigurer`接口,并在配置类之前添加`@Configration`注解来实现的:

@Configuration//必须加这个注解
	public class LoginInterceptorConfigurer
		implements WebMvcConfigurer {
	
		@Override
		public void addInterceptors(InterceptorRegistry registry) {
			// 拦截路径:必须登录才可以访问
			List<String> patterns = new ArrayList<>();
			patterns.add("/**");
			
			// 白名单:在黑名单范围内,却不需要登录就可以访问
			List<String> excludePatterns = new ArrayList<>();
			excludePatterns.add("/bootstrap3/**");//加入白名单,不然样式显示不出来
			excludePatterns.add("/css/**");
			excludePatterns.add("/js/**");
			excludePatterns.add("/images/**");
			
			excludePatterns.add("/web/register.html");
			excludePatterns.add("/users/reg");
			excludePatterns.add("/web/login.html");
			excludePatterns.add("/users/login");
			
			// 注册拦截器
			registry
				.addInterceptor(new LoginInterceptor())
				.addPathPatterns(patterns)
				.excludePathPatterns(excludePatterns);
		}
		
	}

从Spring3.0,@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。用@Configuration加载spring

 早期的做法是推荐继承自`WebMvcConfigurerAdapter`,这个抽象类是使用空实现的做法实现了`WebMvcConfigurer`接口中所有抽象方法在Java 8开始,允许在接口中直接将方法声明为空实现的,所以,`WebMvcConfigurerAdapter`就被声明为已过期的类。

### 16. 用户-个人资料-持久层

**1. 分析SQL语句**

执行修改个人资料的SQL语句是:

    UPDATE 
        t_user 
    SET 
        phone=?,email=?,
        gender=?,
        modified_user=?,modified_time=?
    WHERE 
        uid=?

在操作数据之前,还是应该执行相关的检查,检查数据是否存在,检查是否被标记为删除,当然,这些检查可以使用已有的查询功能来完成!

另外,在修改个人资料之前,还应该将当前登录的用户的资料显示在界面中,涉及的数据有:用户名、电话、邮箱、性别,则需要:

    SELECT username,phone,email,gender FROM t_user WHERE uid=?

当然,根据uid查询数据的功能已经存在,只需要在其映射的SQL语句中补充查询的字段即可!

**2. 接口与抽象方法**

关于修改个人资料的抽象方法:

    Integer updateInfo(User user);

**3. 配置映射**

更新个人资料的映射:

<!-- 更新个人资料 -->
	<!-- Integer updateInfo(User user) -->
	<update id="updateInfo">
		UPDATE 
			t_user 
		SET 
			phone=#{phone},email=#{email},
			gender=#{gender},
			modified_user=#{modifiedUser},
			modified_time=#{modifiedTime}
		WHERE 
			uid=#{uid}
	</update>

另外,修改原有`findByUid`方法的映射,补充查询的字段:

<!-- 根据用户id查询用户数据 -->
	<!-- User findByUid(Integer uid) -->
	<select id="findByUid"
		resultType="cn.tedu.store.entity.User">
		SELECT 
			username, phone,
			email, gender,
			password, salt, 
			is_delete AS isDelete
		FROM 
			t_user 
		WHERE 
			uid=#{uid}
	</select>

完成后,编写并执行单元测试

@Test
	public void updateInfo() {
		User user = new User();
		user.setUid(8);
		user.setGender(0);
		user.setPhone("13100131001");
		user.setEmail("root@tedu.cn");
		user.setModifiedUser("超级管理员");
		user.setModifiedTime(new Date());
		Integer rows = mapper.updateInfo(user);
		System.err.println("rows=" + rows);
	}

### 17. 用户-个人资料-业务层

**1. 设计异常**

修改个人资料之前,还是应该检查用户数据是否存在、是否被标记为删除,涉及`UserNotFoundException`,在执行更新时,涉及`UpdateException`。以上异常均已创建,无需创建新的异常。

**2. 接口与抽象方法**

void changeInfo(User user) throws UserNotFoundException, UpdateException;

**3. 实现**

首先,将持久层新添加的方法在业务层中实现为私有方法:

然后,实现接口中新添加的抽象方法:

public void changeInfo(User user) throws UserNotFoundException, UpdateException {
		// 根据user.getUid()查询用户数据
		// 判断查询结果是否为null
		// 是:抛出UserNotFoundException

		// 判断查询结果中isDelete是否为1
		// 是:抛出UserNotFoundException

		// 向user中封装modifiedUser和modifiedTime
		// 执行更新
	}

具体实现为:

@Override
	public void changeInfo(User user) throws UserNotFoundException, UpdateException {
		// 根据uid查询用户数据
		User result = findByUid(user.getUid());
		// 判断查询结果是否为null
		if (result == null) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"修改密码失败!尝试访问的用户不存在!");
		}

		// 判断查询结果中isDelete是否为1
		if (result.getIsDelete().equals(1)) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"修改密码失败!尝试访问的用户不存在!");
		}
				
		// 向user中封装modifiedUser和modifiedTime
		user.setModifiedUser(result.getUsername());
		user.setModifiedTime(new Date());
		// 执行更新
		updateInfo(user);
	}

完成后,编写并执行单元测试:

@Test
	public void changeInfo() {
		try {
			User user = new User();
			user.setUid(8);
			user.setGender(1);
			user.setPhone("13100131009");
			user.setEmail("root@qq.com");
			service.changeInfo(user);
			System.err.println("OK.");
		} catch (ServiceException e) {
			System.err.println(e.getClass().getName());
			System.err.println(e.getMessage());
		}
	}

### 18. 用户-个人资料-控制器层

**1. 处理异常**

此次业务层没有抛出新的异常,则无需处理!

**2. 设计请求**

    请求路径:/users/change_info
    请求参数:User user, HttpSesion session
    请求方式:POST
    响应数据:Void
    是否拦截:是,无需修改配置,因为默认拦截 /**

**3. 处理请求**

@RequestMapping("/change_info")
	public ResponseResult<Void> changeInfo(User user, HttpSession session) {
		// 封装uid
		Integer uid = getUidFromSession(session);
		user.setUid(uid);
		// 执行修改个人资料
		userService.changeInfo(user);
		// 返回
		return new ResponseResult<>(SUCCESS);
	}

完成后,可以通过`http://localhost:8080/users/change_info?phone=123456&email=hello@tedu.cn&gender=0`进行测试。

### 19. 用户-个人资料-界面

在“执行修改”之前,应该保证“当页面打开时就能显示当前登录的用户的信息”!

这些信息应该是打开页面时就直接向服务器端发出请求,由服务器端响应时提供的!

这就要求服务器提供“获取当前登录的用户的信息”功能!由于目前没有这个功能,所以需要从持久层开始开发这个功能

**获取当前登录的用户数据-持久层**

直接使用已有的`findByUid()`即可!无需再次开发!

**获取当前登录的用户数据-业务层**

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

User getByUid(Integer uid);

然后,在`UserServiceImpl`类中实现这个方法:

@Override
	public User getByUid(Integer uid) {
		// 根据uid查询用户数据
		User result = findByUid(uid);
		// 判断查询结果是否为null
		if (result == null) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"获取用户信息失败!尝试访问的用户不存在!");
		}

		// 判断查询结果中isDelete是否为1
		if (result.getIsDelete().equals(1)) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"获取用户信息失败!尝试访问的用户不存在!");
		}
		
		// 在返回之前隐藏不向外提供的数据
		result.setPassword(null);
		result.setSalt(null);
		result.setIsDelete(null);
		
		// 执行返回
		return result;
	}


完成后,编写并执行单元测试

@Test
	public void getByUid() {
		try {
			Integer uid = 8;
			User data = service.getByUid(uid);
			System.err.println(data);
		} catch (ServiceException e) {
			System.err.println(e.getClass().getName());
			System.err.println(e.getMessage());
		}
	}

**获取当前登录的用户数据-控制器层**

@GetMapping("/info")
	public ResponseResult<User> getByUid(HttpSession session) {
		// 获取uid
		Integer uid = getUidFromSession(session);
		// 查询用户数据
		User data = userService.getByUid(uid);
		// 返回
		return new ResponseResult<User>(SUCCESS, data);
	}

完成后,在浏览器中通过`http://localhost:8080/users/info`直接访问即可测试。

**发出请求并处理结果**

当页面加载时,应该向服务器发出请求,获取当前登录的用户数据:

<script type="text/javascript">
	$(document).ready(function(){
		$.ajax({
			"url":"/users/info",
			"type":"GET",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					// 将服务器响应的数据显示到各控件中
					$("#username").val(json.data.username);
					$("#phone").val(json.data.phone);
					$("#email").val(json.data.email);
					
					var genderRadio = json.data.gender == 0 
						? $("#gender-female") : $("#gender-male"); 
					genderRadio.attr("checked", "checked");
				} else {
					alert(json.message);
					// 退出登录
				}
			}
		});
	});
	</script>

当点击按钮时:

<script type="text/javascript">
	$("#btn-change-info").click(function(){
		$.ajax({
			"url":"/users/change_info",
			"data":$("#form-change-info").serialize(),
			"type":"POST",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					alert("修改成功!");
				} else {
					alert(json.message);
				}
			},
			"error":function() {
				alert("您的登录信息已经过期,请重新登录!");
				location.href = "login.html";
			}
		});
	});
	</script>

 ### 20. 用户-上传头像-持久层

**1. 分析SQL语句**

当用户上传头像时,服务器需要:

1. 存储用户上传的头像文件;

2. 将存储的文件的路径记录在数据库中。

对于持久层开发来说,需要做的就是将存储文件的路径更新到用户表中:

    UPDATE t_user 
    SET avatar=?, modified_user=?, modified_time=? 
    WHERE uid=?

**2. 接口与抽象方法**

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

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

**3. 配置映射**

配置的映射:


	<!-- 更新用户头像 -->
	<!-- Integer updateAvatar(
		    @Param("uid") Integer uid, 
		    @Param("avatar") String avatar, 
		    @Param("modifiedUser") String modifiedUser, 
		    @Param("modifiedTime") Date modifiedTime) -->
	<update id="updateAvatar">
		UPDATE 
			t_user 
		SET 
			avatar=#{avatar}, 
			modified_user=#{modifiedUser}, 
			modified_time=#{modifiedTime} 
		WHERE 
			uid=#{uid}
	</update>

单元测试:

@Test
	public void updateAvatar() {
		Integer uid = 10;
		String avatar = "这里应该是头像的路径";
		String modifiedUser = "超级管理员";
		Date modifiedTime = new Date();
		Integer rows = mapper.updateAvatar(uid, avatar, modifiedUser, modifiedTime);
		System.err.println("rows=" + rows);
	}

### 21. 用户-上传头像-业务层

**1. 设计异常**

请参考:修改个人资料

**2. 接口与抽象方法**

请参考:修改个人资料

抽象方法的声明:

/**
	 * 更新个人头像
	 * @param avatar 头像路径
	 * @throws UserNotFoundException 用户数据不存在
	 * @throws UpdateException 更新数据异常
	 */
	void changeAvatar(Integer uid, String avatar) 
			throws UserNotFoundException, 
				UpdateException;

**3. 实现**

请参考:修改个人资料

先私有化实现持久层中新添加的方法:

/**
	 * 更新用户头像
	 * @param uid 用户的id
	 * @param avatar 头像的路径
	 * @param modifiedUser 修改执行人
	 * @param modifiedTime 修改时间
	 */
	private void updateAvatar(
			Integer uid, String avatar, 
		    String modifiedUser, Date modifiedTime) {
		Integer rows = userMapper.updateAvatar(uid, avatar, modifiedUser, modifiedTime);
		if (rows != 1) {
			throw new UpdateException(
				"修改用户数据时出现未知错误!");
		}
	}

实现的抽象方法:

@Override
	public void changeAvatar(Integer uid, String avatar) throws UserNotFoundException, UpdateException {
		// 根据uid查询用户数据
		User result = findByUid(uid);
		// 判断查询结果是否为null
		if (result == null) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"修改头像失败!尝试访问的用户不存在!");
		}

		// 判断查询结果中isDelete是否为1
		if (result.getIsDelete().equals(1)) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"修改头像失败!尝试访问的用户不存在!");
		}
						
		// 向user中封装modifiedUser和modifiedTime
		String modifiedUser = result.getUsername();
		Date modifiedTime = new Date();
		// 执行更新
		updateAvatar(uid, avatar, modifiedUser, modifiedTime);
	}

单元测试:

@Test
	public void changeAvatar() {
		try {
			Integer uid = 10;
			String avatar = "新头像的路径";
			service.changeAvatar(uid, avatar);
			System.err.println("OK.");
		} catch (ServiceException e) {
			System.err.println(e.getClass().getName());
			System.err.println(e.getMessage());
		}
	}

### 22. 用户-上传头像-控制器层

**1. 处理异常**

创建上传时可能涉及的异常类:

    RuntimeException
    -- cn.tedu.store.controller.ex.FileUploadException
    -- -- cn.tedu.store.controller.ex.FileEmptyException
    -- -- cn.tedu.store.controller.ex.FileSizeException
    -- -- cn.tedu.store.controller.ex.FileContentTypeException
    -- -- cn.tedu.store.controller.ex.FileIllegalStateException
    -- -- cn.tedu.store.controller.ex.FileIOException

然后,需要在`BaseController`中添加对以上5种实际抛出的异常(不包括FileUploadException)的处理,在处理之前,应该修改原有的处理异常的方法之前的注解:

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

然后,在处理异常的方法中添加5组else if语句,对这5种异常进行处理!

**2. 设计请求**

    请求路径:/users/change_avatar
    请求参数:HttpServletRequest request, MultipartFile file
    请求方式:POST
    响应数据:ResponseResult<String>
    是否拦截:是,无需修改配置

**3. 处理请求**

// 确定上传文件的名称:UPLOAD_DIR
    private static final String UPLOAD_DIR = "upload";
    // 确定上传文件的最大大小
    private static final long UPLOAD_MAX_SIZE = 1 * 1024 * 1024;
    // 确定允许上传的类型的列表
    private static final List<String> UPLOAD_CONTENT_TYPES
        = new ArrayList<>();

    static {
        UPLOAD_CONTENT_TYPES.add("xxx");
        UPLOAD_CONTENT_TYPES.add("xxx");
        UPLOAD_CONTENT_TYPES.add("xxx");
        UPLOAD_CONTENT_TYPES.add("xxx");
    }

    @RequestMapping("/change_avatar")
    public ResponseResult<String> changeAvatar(
        HttpServletRequest request, 
        @RequestParam("file") MultipartFile file) {
        // 检查文件是否为空
        // 为空:抛出异常:FileEmptyException

        // 检查文件大小
        // 超出范围(> UPLOAD_MAX_SIZE):抛出异常:FileSizeException

        // 检查文件类型
        // 类型不符(contains()为false):抛出异常:FileContentTypeException

        // 确定文件夹路径:request.getServletContext().getRealPath(UPLOAD_DIR);
        // 创建上传文件夹的File对象parent
        // 检查文件夹是否存在,如果不存在,则创建

        // 获取原文件名:file.getOriginalFilename()
        // 从原文件名中得到扩展名
        // 确定文件名:uuid/nanoTime/...

        // 创建dest对象:new File(parent, filename);
        // try
        // 执行上传:file.transferTo(dest);
        // catch:IllegalStateException:抛出FileIllegalStateException
        // catch:IOException:抛出FileIOException

        // 获取uid:getUidFromSession(request.getSession());
        // 生成avatar:/UPLOAD_DIR/文件名.扩展名

        // 执行更新:userService.changeAvatar(uid, avatar);
        // 返回成功
    }

### 23. 用户-上传头像-界面

关于前端界面的处理,由于服务器端依然是将响应JSON数据,则前端还是应该通过AJAX发出异步请求然后获取JSON数据,并处理结果。

相对传统的AJAX访问,关于文件上传时,区别在于:

- 提交到服务器端的数据应该是:`new FormData($("#form-change-avatar")[0])`

- 必须配置`"contentType":false,`和`"processData":false,`

完整代码例如:


	<script type="text/javascript">
	$("#btn-change-password").click(function(){
		$.ajax({
			"url":"/users/change_password",
			"data":$("#form-change-password").serialize(),
			"type":"POST",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					alert("修改成功!");
					$("#form-change-password")[0].reset();
				} else {
					alert(json.message);
				}
			},
			"error":function() {
				alert("您的登录信息已经过期,请重新登录!");
				location.href = "login.html";
			}
		});
	});
	</script>

关于在前端界面显示新上传的头像,首先,请检查服务器端的控制器是否返回了头像的路径,即:

return new ResponseResult<String>(SUCCESS, avatar);

如果需要测试,可以在客户端通过`alert(json.data);`测试是否获取到了头像路径。

如果能够正确的获取到上传的文件的路径,则通过jQuery选择器,选中显示头像的`<img>`标签,修改其`src`属性即可:

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

至此,上传头像并显示新头像的功能已经完成,但是,刷新或重新打开页面时,并不会显示用户的头像图片

由于显示界面使用的是html,所以,应该将头像数据存储在cookie,则html可以随时访问到cookie中的头像数据!

关于cookie的访问,如果需要向cookie中存入数据,可以是:

$.cookie("username", "root", {
		expire: 7
	});


反之,如果需要取出cookie中的数据,则应该是:

$.cookie("username");

需要注意的是,这种使用cookie的方式是jQuery封装的函数,使用之前需要引入对应的文件:

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

所以,需要实现“打开页面就能直接显示当前登录的用户的头像”,则应该:

1. 必须保证登录时的查询(findByUsername)对应的SQL查询中,已经查询了avatar字段;

2. 当用户成功登录时,将头像路径存入到cookie中;

3. 当加载“上传头像”页面时,从cookie中取出头像路径,如果值是有效的,则显示;

4. 当成功上传头像后,修改cookie中存放的头像路径;

最后,SpringBoot项目默认已经集成了上传所需的环境和配置,并且,限制了上传的文件大小,如果上传太大的文件,则会报告错误!通常,项目中的配置,是整个项目中任何上传功能都不允许超出文件大小范围,而在控制器中,限制的是特定的某个功能的上传文件大小的范围!整个项目的上传大小应该设置为比较大的值!

通常,可以在启动类中添加配置:

@SpringBootApplication
	@Configuration
	@MapperScan("cn.tedu.store.mapper")
	public class StoreApplication {
	
		public static void main(String[] args) {
			SpringApplication.run(StoreApplication.class, args);
		}
	
		@Bean
		public MultipartConfigElement multipartConfigration() {
			MultipartConfigFactory mcf
				= new MultipartConfigFactory();
			DataSize maxFileSize = DataSize.ofBytes(100 * 1024 * 1024);
			DataSize maxRequestSize = DataSize.ofBytes(100 * 1024 * 1024);
			mcf.setMaxFileSize(maxFileSize);
			mcf.setMaxRequestSize(maxRequestSize);
			MultipartConfigElement mce
				= mcf.createMultipartConfig();
			return mce;
		}
		
	}

注意(推荐):由于配置的代码是写在启动类中的,所以,启动类需要添加`@Configuration`注解,且配置的方法需要添加`@Bean`注解。

注意:某些做法是在`application.properties`中进行配置,这种做法在某些版本中是无效的,推荐使用以上Java代码进行配置。

 

### 24. 阶段小结

- 掌握SpringBoot的基本使用;

- 理解MVC的编程理念,理解其中的各层的定位与作用,特别是Model中的Service;

- 掌握异常的作用,掌握抛出、处理异常的原则,掌握统一处理异常的做法;

- 掌握密码的加密;

- 掌握文件上传的做法;

### 25. 收货地址-增加收货地址-数据库与数据表

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

开发顺序应该是:增加收货地址 > 显示收货地址列表 > 设为默认 > 删除收货地址 > 修改收货地址。

创建“收货地址”的数据表:


	CREATE TABLE t_address (
		aid INT AUTO_INCREMENT COMMENT '收货地址的id',
		uid INT NOT NULL COMMENT '所属用户的id',
		name VARCHAR(20) COMMENT '收货人姓名',
		province CHAR(6) COMMENT '省的代号',
		city CHAR(6) COMMENT '市的代号',
		area CHAR(6) COMMENT '区的代号',
		district VARCHAR(50) COMMENT '省市区的名称',
		zip CHAR(6) COMMENT '邮政编码',
		address VARCHAR(50) COMMENT '详细地址',
		phone VARCHAR(20) COMMENT '手机',
		tel VARCHAR(20) COMMENT '固话',
		tag VARCHAR(10) COMMENT '地址类型,如:家/公司/学校',
		is_default INT COMMENT '是否默认,0-非默认,1-默认',
		created_user VARCHAR(20) COMMENT '创建执行人',
		created_time DATETIME COMMENT '创建时间',
		modified_user VARCHAR(20) COMMENT '修改执行人',
		modified_time DATETIME COMMENT '修改时间',
		PRIMARY KEY(aid)
	) DEFAULT CHARSET=UTF8;

### 25. 收货地址-增加收货地址-实体类

创建`cn.tedu.store.entity.Address`实体类,继承自`BaseEntity`

/**
	 * 收货地址的实体类
	 */
	public class Address extends BaseEntity {
	
		private static final long serialVersionUID = 8491523504331195543L;
	
		private Integer aid;
		private Integer uid;
		private String name;
		private String province;
		private String city;
		private String area;
		private String district;
		private String zip;
		private String address;
		private String phone;
		private String tel;
		private String tag;
		private Integer isDefault;

		// SET/GET/toString

	}

### 26. 收货地址-增加收货地址-持久层

**1. 分析SQL语句**

执行增加数据的SQL语句应该是:

    INSERT INTO t_address (除了aid以外的所有字段) VALUE (对应的属性列表);

后续,在“增加收货地址”的业务中,可以制定规则“当用户第1次增加收货地址时,该收货地址直接是默认的,后续增加的每一条都不是默认的”,可以通过“判断当前用户有没有收货地址”来实现,表现为“查询当前用户的收货地址数据的数量”,要保证该规则的应用,还需要:

    SELECT COUNT(aid) FROM t_address WHERE uid=?

**2. 接口与抽象方法**

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


	/**
	 * 处理收货地址的持久层接口
	 */
	public interface AddressMapper {
	
		/**
		 * 增加收货地址数据
		 * @param address 收货地址数据
		 * @return 受影响的行数
		 */
		Integer insert(Address address);
	
		/**
		 * 统计指定用户的收货地址数据的数量
		 * @param uid 用户的id
		 * @return 用户的收货地址数据的数量
		 */
		Integer countByUid(Integer uid);
		
	}

**3. 配置映射**

复制得到`AddressMapper.xml`文件,修改根节点的`namespace`属性值,然后添加以上2个抽象方法的映射的配置:

<mapper namespace="cn.tedu.store.mapper.AddressMapper">
		
		<!-- 增加收货地址数据 -->
		<!-- Integer insert(Address address) -->
		<insert id="insert">
			INSERT INTO t_address (
				uid, name,
				province, city,
				area, district,
				zip, address,
				phone, tel,
				tag, is_default,
				created_user, created_time,
				modified_user, modified_time
			) VALUE (
				#{uid}, #{name},
				#{province}, #{city},
				#{area}, #{district},
				#{zip}, #{address},
				#{phone}, #{tel},
				#{tag}, #{isDefault},
				#{createdUser}, #{createdTime},
				#{modifiedUser}, #{modifiedTime}
			)
		</insert>
		
		<!-- 统计指定用户的收货地址数据的数量 -->
		<!-- Integer countByUid(Integer uid) -->
		<select id="countByUid" 
			resultType="java.lang.Integer">
			SELECT 
				COUNT(aid) 
			FROM 
				t_address 
			WHERE 
				uid=#{uid}
		</select>
		
	</mapper>


完成后,创建新的单元测试类`cn.tedu.store.mapper.AddressMapperTestCase`,并对以上2个方法执行测试:

@RunWith(SpringRunner.class)
	@SpringBootTest
	public class AddressMapperTestCase {
	
		@Autowired
		public AddressMapper mapper;
		
		@Test
		public void insert() {
			Address address = new Address();
			address.setUid(8);
			address.setName("小李同学");
			Integer rows = mapper.insert(address);
			System.err.println("rows=" + rows);
		}
		
		@Test
		public void countByUid() {
			Integer uid = 8;
			Integer count = mapper.countByUid(uid);
			System.err.println("count=" + count);
		}
		
	}

### 27. 收货地址-增加收货地址-业务层

**1. 设计异常**

由于此次主要执行增加数据操作,可能涉及`InsertException`。

另外,还将查询某用户的数据量,但是,无论是否存在数据,都不会被视为错误操作,所以,不涉及异常。(如果增加规则“每个用户只允许创建??条收货地址”,则应该有对应的异常)

**2. 接口与抽象方法**

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

void addnew(Address address, String username) 
			throws InsertException;

**3. 实现**

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

首先,将持久层中的2个方法在业务层实现类中实现为私有方法,其中,增加数据的方法需要判断其返回值,当返回值不为1时抛出异常,且该方法返回值类型为`void`。

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

/**
	 * 处理收货地址数据的业务层实现类
	 */
	@Service
	public class AddressServiceImpl implements IAddressService {
		
		@Autowired
		private AddressMapper addressMapper;
	
		@Override
		public void addnew(Address address, String username) throws InsertException {
			// 查询用户的收货地址的数量:countByUid(Integer uid),参数值来自address.getUid();
			Integer count = countByUid(address.getUid());
			// 判断数量是否为0
			// 是:当前将增加第1条收货地址,则:address.setIsDefault(1)
			// 否:当前增加的不是第1条收货地址,则:address.setIsDefault(0)
			address.setIsDefault(count == 0 ? 1 : 0);
	
			// TODO 处理district
	
			// 4项日志:时间是直接创建对象得到,用户名使用参数username
			Date now = new Date();
			address.setCreatedUser(username);
			address.setCreatedTime(now);
			address.setModifiedUser(username);
			address.setModifiedTime(now);
	
			// 执行增加:insert(Address address);
			insert(address);
		}
		
		/**
		 * 增加收货地址数据
		 * @param address 收货地址数据
		 */
		private void insert(Address address) {
			Integer rows = addressMapper.insert(address);
			if (rows != 1) {
				throw new InsertException(
					"增加收货地址数据时出现未知错误!");
			}
		}
	
		/**
		 * 统计指定用户的收货地址数据的数量
		 * @param uid 用户的id
		 * @return 用户的收货地址数据的数量
		 */
		private Integer countByUid(Integer uid) {
			return addressMapper.countByUid(uid);
		}
	
	}

完成后创建`cn.tedu.store.service.AddressServiceTestCase`测试类,对以上功能进行测试:


	@RunWith(SpringRunner.class)
	@SpringBootTest
	public class AddressServiceTestCase {
	
		@Autowired
		public IAddressService service;
		
		@Test
		public void addnew() {
			try {
				Address address = new Address();
				address.setUid(10);
				address.setName("小刘同学");
				String username = "小森同学";
				service.addnew(address, username);
				System.err.println("OK.");
			} catch (ServiceException e) {
				System.err.println(e.getClass().getName());
				System.err.println(e.getMessage());
			}
		}
		
	}

### 28. 收货地址-增加收货地址-控制器层

**1. 处理异常**

无,业务层没有抛出新的异常,则不需要处理。

**2. 设计请求**

    请求路径:/addresses/addnew
    请求参数:Address address, HttpSession session
    请求方式:POST
    响应数据:ResponseResult<Void>
    是否拦截:是,无需修改配置

**3. 处理请求**

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

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

    @RequestMapping("/addnew")
    public ResponseResult<Void> addnew(Address address, HttpSession session) {
        // 从session中获取uid
        // 将uid封装到address中
        // 从session中获取username
        // 调用业务层对象执行:addressService.addnew(address, username);
        // 返回成功
    }

完成后,打开浏览器,先登录,通过`http://localhost:8080/addresses/addnew?name=Henry`进行测试。

### 29. 收货地址-增加收货地址-界面

与此前处理的基本一致:先复制得到ajax请求相关代码,修改按钮id、表单id、请求路径,并在HTML代码部分确定各id值,及表单中各控件的name值。

在“增加收货地址”的页面处理中,需要以下功能:

- 获取全国所有省的列表:SELECT * FROM t_dict_district WHERE parent='86'

- 获取某个省所有市的列表:SELECT * FROM t_dict_district WHERE parent=省code

- 获取某个市所有区的列表:SELECT * FROM t_dict_district WHERE parent=市code

可以发现,以上3个功能对应的SQL语句是一样的:

    SELECT * FROM t_dict_district WHERE parent=?

在开发时,应该先创建`cn.tedu.store.entity.District`实体类:

/**
	 * 省/市/区数据的实体类
	 */
	public class District implements Serializable {
	
		private static final long serialVersionUID = -2777570570541589252L;
	
		private Integer id;
		private String parent;
		private String code;
		private String name;

		// SET/GET/toString
	}

然后,创建`cn.tedu.store.mapper.DistrictMapper`持久层接口,并添加抽象方法:

List<District> findByParent(String parent);

再复制得到`DistrictMapper.xml`文件,配置以上抽象方法的映射:

<mapper namespace="cn.tedu.store.mapper.DistrictMapper">
		
		<!-- 获取所有省/某省所有市/某市所有区的列表 -->
		<!-- List<District> findByParent(String parent) -->
		<select id="findByParent"
			resultType="cn.tedu.store.entity.District">
			SELECT 
				code, name
			FROM 
				t_dict_district 
			WHERE 
				parent=#{parent}
			ORDER BY 
				code ASC
		</select>
		
	</mapper>

完成后,创建测试类,编写并执行测试方法:

@RunWith(SpringRunner.class)
	@SpringBootTest
	public class DistrictMapperTestCase {
	
		@Autowired
		public DistrictMapper mapper;
		
		@Test
		public void findByParent() {
			String parent = "999999";
			List<District> list = mapper.findByParent(parent);
			System.err.println("BEGIN:");
			for (District data : list) {
				System.err.println(data);
			}
			System.err.println("END.");
		}
		
	}

接下来应该是业务层,通常,查询功能没有太多业务,也没有相关的异常,则直接创建`cn.tedu.store.service.IDistrictService`业务层接口,并添加抽象方法:

List<District> getByParent(String parent);

再创建`cn.tedu.store.service.impl.DistrictServiceImpl`业务层实现类,添加`@Service`注解,在类中添加持久层对象`@Autowired private DistrictMapper districtMapper`,私有化实现持久层方法,该类将实现`IDistrictService`接口,重写抽象方法:

/**
	 * 处理省/市/区数据的业务层实现类
	 */
	@Service
	public class DistrictServiceImpl implements IDistrictService {
		
		@Autowired
		private DistrictMapper distrctMapper;
	
		@Override
		public List<District> getByParent(String parent) {
			return findByParent(parent);
		}
		
		/**
		 * 获取所有省/某省所有市/某市所有区的列表
		 * @param parent 获取省列表时,使用86;获取市列表时,使用省的代号;获取区列表时,使用市的代号
		 * @return 所有省/某省所有市/某市所有区的列表
		 */
		private List<District> findByParent(String parent) {
			return distrctMapper.findByParent(parent);
		}
	
	}

完成后,仍创建测试类,编写并执行测试方法:


	@RunWith(SpringRunner.class)
	@SpringBootTest
	public class DistrictServiceTestCase {
	
		@Autowired
		public IDistrictService service;
		
		@Test
		public void getByParent() {
			String parent = "86";
			List<District> list = service.getByParent(parent);
			System.err.println("BEGIN:");
			for (District data : list) {
				System.err.println(data);
			}
			System.err.println("END.");
		}
		
	}

最后,还需要创建`cn.tedu.store.controller.DistrictController`控制器类,继承自`BaseController`,添加`@RestController`和`@RequestMapping("/districts")`注解,在类中声明`@Autowired private IDistrictService districtService;`业务层对象,然后,分析处理请求的方法:

    请求路径:/districts/
    请求参数:String parent(*)
    请求方式:GET
    响应数据:ResponseResult<List<District>>
    是否拦截:否,需要添加白名单

所以,先在`LoginInterceptorConfigurer`中添加白名单:

excludePatterns.add("/districts/**");

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

@RestController
	@RequestMapping("/districts")
	public class DistrictController extends BaseController {
	
		@Autowired
		private IDistrictService districtService;
		
		@GetMapping("/")
		public ResponseResult<List<District>> 
			getByParent(@RequestParam("parent") String parent) {
			List<District> data
				= districtService.getByParent(parent);
			return new ResponseResult<>(SUCCESS, data);
		}
		
	}

最后,打开浏览器,无需登录,直接通过`http://localhost:8080/districts/?parent=86`进行测试。

关于前端界面的代码:

<script type="text/javascript">
	$(document).ready(function(){
		showProvinceList();
		
		$("#city").append('<option value="0">----- 请选择 -----</option>');
		$("#area").append('<option value="0">----- 请选择 -----</option>');
	});
	
	$("#province").change(function() {
		showCityList();
		
		$("#area").empty();
		$("#area").append('<option value="0">----- 请选择 -----</option>');
	});
	
	$("#city").change(function() {
		showAreaList();
	});
	
	function showProvinceList() {
		$("#province").append('<option value="0">----- 请选择 -----</option>');
		
		$.ajax({
			"url":"/districts/",
			"data":"parent=86",
			"type":"GET",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					var list = json.data;
					for (var i = 0; i < list.length; i++) {
						console.log(list[i].name);
						var op = '<option value="' + list[i].code + '">' + list[i].name + '</option>';
						$("#province").append(op);
					}
				} else {
					alert(json.message);
				}
			}
		});
	}
	
	function showCityList() {
		$("#city").empty();
		
		$("#city").append('<option value="0">----- 请选择 -----</option>');
		
		if ($("#province").val() == 0) {
			return;
		}
		
		$.ajax({
			"url":"/districts/",
			"data":"parent=" + $("#province").val(),
			"type":"GET",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					var list = json.data;
					for (var i = 0; i < list.length; i++) {
						console.log(list[i].name);
						var op = '<option value="' + list[i].code + '">' + list[i].name + '</option>';
						$("#city").append(op);
					}
				} else {
					alert(json.message);
				}
			}
		});
	}
	
	function showAreaList() {
		$("#area").empty();
		
		$("#area").append('<option value="0">----- 请选择 -----</option>');
		
		if ($("#city").val() == 0) {
			return;
		}
		
		$.ajax({
			"url":"/districts/",
			"data":"parent=" + $("#city").val(),
			"type":"GET",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					var list = json.data;
					for (var i = 0; i < list.length; i++) {
						console.log(list[i].name);
						var op = '<option value="' + list[i].code + '">' + list[i].name + '</option>';
						$("#area").append(op);
					}
				} else {
					alert(json.message);
				}
			}
		});
	}
	</script>
		
	<script type="text/javascript">
	$("#btn-addnew").click(function(){
		$.ajax({
			"url":"/addresses/addnew",
			"data":$("#form-addnew").serialize(),
			"type":"POST",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					alert("增加成功!");
				} else {
					alert(json.message);
				}
			},
			"error":function() {
				alert("您的登录信息已经过期,请重新登录!");
				location.href = "login.html";
			}
		});
	});
	</script>

后续,为了得到省、市、区的中文名,还应该设计“根据代号得到名称”的功能!则先应该在`DistrictMapper`持久层接口中添加抽象方法:

District findByCode(String code);

然后配置映射:

<!-- 根据代号获取省/市/区的信息 -->
	<!-- District findByCode(String code) -->
	<select id="findByCode"
		resultType="cn.tedu.store.entity.District">
		SELECT 
			name
		FROM 
			t_dict_district 
		WHERE 
			code=#{code}
	</select>

编写并执行单元测试:

@Test
	public void findByCode() {
		String code = "320102";
		District data = mapper.findByCode(code);
		System.err.println(data);
	}

持久层开发完毕后,再完成业务层,仍然先在业务层接口中添加抽象方法:

/**
	 * 根据代号获取省/市/区的信息
	 * @param code 省/市/区的代号
	 * @return 匹配的省/市/区的信息,如果没有匹配的信息,则返回null
	 */
	District getByCode(String code);

在业务层实现类中实现以上方法:

@Override
	public District getByCode(String code) {
		return findByCode(code);
	}

	/**
	 * 根据代号获取省/市/区的信息
	 * @param code 省/市/区的代号
	 * @return 匹配的省/市/区的信息,如果没有匹配的信息,则返回null
	 */
	private District findByCode(String code) {
		return distrctMapper.findByCode(code);
	}

最后,编写并执行测试:

@Test
	public void getByCode() {
		String code = "330102";
		District data = service.getByCode(code);
		System.err.println(data);
	}

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

**1. 分析SQL语句**

    SELECT 
        aid, name, district, address, phone, is_default, tag FROM t_address WHERE uid=? ORDER BY  is_default DESC,     modified_time DESC

**2. 接口与抽象方法**

List<Address> findByUid(Integer uid);

**3. 配置映射**

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

**1. 设计异常**

**2. 接口与抽象方法**

List<Address> getByUid(Integer uid);

**3. 实现**

### 32. 收货地址-显示收货地址列表-控制器层

**1. 处理异常**

**2. 设计请求**

    请求路径:/addresses/
    请求参数:HttpSession session
    请求方式:GET
    响应数据:ResponseResult<List<Address>>
    是否拦截:是

**3. 处理请求**

### 33. 收货地址-显示收货地址列表-界面

### 34. 收货地址-设为默认-持久层

**1. 分析SQL语句**

如果要将指定的收货地址设置为默认:

    UPDATE xx SET is_default=1 WHERE aid=?

为了保证将原有的默认收货地址设置为非默认,还应该在执行以上操作之前:

    UPDATE xx SET is_default=0 WHERE uid=?

即:先将当前用户的所有收货地址都设置为非默认,再将指定的那一条设置为默认!这样操作,就可以不必关心此前哪一条才是默认收货地址!

当然,在执行这些操作之前,还需要确保“被操作的数据是存在的,并且,是归属当前用户的”!则需要执行的查询:

    SELECT uid FROM xx WHERE aid=? 

**2. 接口与抽象方法**

以上SQL语句对应的抽象方法可以是:

Address findByAid(Integer aid);

	Integer updateNonDefault(Integer uid);

	Integer updateDefault(Integer aid);

**3. 配置映射**

配置的映射:

<!-- 将指定用户的所有收货地址设置为非默认 -->
	<!-- Integer updateNonDefault(Integer uid) -->
	<update id="updateNonDefault">
		UPDATE
			t_address
		SET
			is_default=0
		WHERE 
			uid=#{uid}
	</update>
	
	<!-- 将指定的收货地址设置为默认 -->
	<!-- Integer updateDefault(Integer aid) -->
	<update id="updateDefault">
		UPDATE
			t_address
		SET
			is_default=1
		WHERE 
			aid=#{aid}
	</update>

	<!-- 根据收货地址id查询匹配的数据 -->
	<!-- Address findByAid(Integer aid) -->
	<select id="findByAid"
		resultType="cn.tedu.store.entity.Address">
		SELECT 
			uid
		FROM 
			t_address 
		WHERE 
			aid=#{aid} 
	</select>

对应的单元测试:

@Test
	public void updateNonDefault() {
		Integer uid = 8;
		Integer rows = mapper.updateNonDefault(uid);
		System.err.println("rows=" + rows);
	}
	
	@Test
	public void updateDefault() {
		Integer aid = 10;
		Integer rows = mapper.updateDefault(aid);
		System.err.println("rows=" + rows);
	}
	
	@Test
	public void findByAid() {
		Integer aid = 9;
		Address data = mapper.findByAid(aid);
		System.err.println(data);
	}

### 35. 收货地址-设为默认-业务层

**1. 设计异常**

整个数据的操作流程应该是:先检查,再全部设置为非默认,再把指定的那条设置为默认!

在检查时,可能出现“查询不到匹配的数据”的问题,此时,应该抛出`AddressNotFoundException`异常;并且,也可能出现非法访问,数据的uid与当前登录的用户的uid不相符,即尝试访问他人的数据,此时,应该抛出`AccessDeniedException`异常。

后续还会执行2次更新,都可能涉及`UpdateException`。

所以,此次需要创建2个新的异常类:`AddressNotFoundException`、`AccessDeniedException`。

**2. 接口与抽象方法**

void setDefault(Integer uid, Integer aid) throws AddressNotFoundException, AccessDeniedException, UpdateException;

**3. 实现**

步骤分析为:

在使用了SpringJDBC后(无视使用哪种数据库,或使用哪种持久层框架),当需要使用事务来保障数据操作时,只需要在相关的业务方法之前添加`@Transactional`注解即可!

@Override
	@Transactional
	public void setDefault(Integer uid, Integer aid)
			throws AddressNotFoundException, AccessDeniedException, UpdateException {
		// 根据aid查询数据
		Address result = findByAid(aid);
		// 判断数据是否为null
		if (result == null) {
			// 是:AddressNotFoundException
			throw new AddressNotFoundException(
				"设置默认收货地址失败!尝试访问的数据不存在!");
		}

		// 判断参数uid与查询结果中的uid是否不一致
		if (!result.getUid().equals(uid)) {
			// 是:AccessDeniedException
			throw new AccessDeniedException(
				"设置默认收货地址失败!数据归属错误!");
		}

		// 全部设置为非默认
		updateNonDefault(uid);

		// 把指定的设置为默认
		updateDefault(aid);
	}

### 36. 收货地址-设为默认-控制器层

**1. 处理异常**

此次抛了2个新的异常:`AddressNotFoundException`、`AccessDeniedException`,应该在`BaseController`中进行处理。

**2. 设计请求**

    请求路径:/addresses/{aid}/set_default
    请求参数:HttpSession session
    请求方式:POST
    响应数据:ResponseResult<Void>

**3. 处理请求**

处理请求的方法应该是:

@RequestMapping("/{aid}/set_default")
	public ResponseResult<Void> setDefault(
		@PathVariable("aid") Integer aid,
		HttpSession session) {
		// 从session中获取uid
		Integer uid = getUidFromSession(session);
		// 调用业务层对象执行
		addressService.setDefault(uid, aid);
		// 返回
		return new ResponseResult<>(SUCCESS);
	}

完成后,可以通过`http://localhost:8080/addresses/7/set_default`进行测试。

### 37. 收货地址-设为默认-界面

<script type="text/javascript">
	$(document).ready(function(){
		showAddressList();
	});
	
	function showAddressList() {
		$.ajax({
			"url":"/addresses/",
			"type":"GET",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					var list = json.data;
					$("#address-list").empty();
					for (var i = 0; i < list.length; i++) {
						console.log(list[i].name);
						var html = '<tr>'
								+ '<td>#{tag}</td>'
								+ '<td>#{name}</td>'
								+ '<td>#{district}#{address}</td>'
								+ '<td>#{phone}</td>'
								+ '<td><a class="btn btn-xs btn-info" ><span class="fa fa-edit"></span> 修改</a></td>'
								+ '<td><a class="btn btn-xs add-del btn-info" ><span class="fa fa-trash-o"></span> 删除</a></td>'
								+ '<td><a class="btn btn-xs add-def btn-default" onclick="setDefault(#{aid})">设为默认</a></td>'
								+ '</tr>';
								
						html = html.replace("#{aid}", list[i].aid);	
						html = html.replace("#{tag}", list[i].tag);	
						html = html.replace("#{name}", list[i].name);	
						html = html.replace("#{district}", list[i].district);	
						html = html.replace("#{address}", list[i].address);	
						html = html.replace("#{phone}", list[i].phone);	
								
						$("#address-list").append(html);
					}
					$(".add-def:eq(0)").hide();
				} else {
					alert(json.message);
				}
			}
		});
	}
	
	function setDefault(aid) {
		$.ajax({
			"url":"/addresses/" + aid + "/set_default",
			"type":"POST",
			"dataType":"json",
			"success":function(json) {
				if (json.state == 200) {
					// alert("修改成功!");
					showAddressList();
				} else {
					alert(json.message);
				}
			},
			"error":function() {
				alert("您的登录信息已经过期,请重新登录!");
				location.href = "login.html";
			}
		});
	}
	</script>

### 38. 收货地址-删除-持久层

**1. 分析SQL语句**

执行删除的SQL语句:

    DELETE FROM t_address WHERE aid=?

与“设为默认”相同,在执行删除之前还应该检查数据是否存在,及数据归属是否正确,该功能已经完成,无需再次开发。

另外,如果删除的收货地址是默认收货地址,则:如果仍有收货地址数据,将最后一次修改的收货地址设置为默认。对应:

- 如何判断刚才删除的是默认收货地址?由于删除之前本来就会查询,则在原有的查询功能中,添加查询is_default字段即可!

- 如何判断删除后是否仍有收货地址数据?通过原有的`countByUid(uid)`,如果统计结果为0,则没有数据了,即刚才删除的就是最后一条,如果统计结果不为0,则表示删除后仍有数据!

- 如何得到最后一次修改的收货地址?对应的SQL语句是:`SELECT aid FROM t_address WHERE uid=? ORDER BY modified_time DESC, aid DESC LIMIT 0,1`

- 如何把某地址设为默认?通过现有的`updateNonDefault(uid)`和`updateDefault(aid)`即可实现。

**2. 接口与抽象方法**

在持久层接口中添加新的抽象方法:
 

Integer deleteByAid(Integer aid);

	Address findLastModified(Integer uid);

**3. 配置映射**

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

**1. 设计异常**

**2. 接口与抽象方法**

**3. 实现**

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

**1. 处理异常**

**2. 设计请求**

    请求路径:
    请求参数:
    请求方式:
    响应数据:

**3. 处理请求**

### 41. 收货地址-删除-界面

 

### 【附】 常见错误


【错误描述】 Caused by: com.mysql.cj.exceptions.WrongArgumentException: No timezone mapping entry for 'Asia/Shanghaispring.datasource.username'

【错误原因】 数据库连接字符串有误

【错误描述】 Caused by: java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)

【错误原因】 数据库密码错误

【错误描述】 NoSuchBeanDefinitionException ... 'cn.tedu.store.mapper.UserMapper' ... expected at least 1 bean ...

【错误原因】 没有找到UserMapper类型的对象,可能是因为在启动类(StoreApplication)上没有添加`@MapperScanner`注解,或注解中填写的包名是错误的,

【错误描述】 java.lang.NullPointerException at cn.tedu.store.mapper.UserMapperTestCase.findByUsername(UserMapperTestCase.java:29)

【错误原因】 所有的`NullPointerException`都是因为某个为null的值调用了属性或方法,应该根据下一行错误提示找到对应的代码,例如`User result = mapper.findByUsername(username);`,在这一行代码中,找到`.`左侧的对象,极有可能它是null值!如果一行代码中有多个`.`,则每个`.`的左侧都有可能是`null`值,甚至在某个方法的调用中,参数为null也会导致`NullPointerException`。如果某个值应该是自动装配的,则检查它有没有添加`@Autowired`注解,或它所在的类是否被Spring所管理。

【错误描述】 BadSqlGrammarException

【错误原因】 尝试执行的SQL语句存在语法错误,可以通过进一步的提示找出错误,如果进一步的提示中包含`near`关键字,则找`near`提示的代码的左侧的SQL语句部分,也可以直接去检查SQL语句

【错误描述】 There is no getter for property named ...

【错误原因】 在配置映射时,应该填写类中的属性名时,所填写的名称在类中并不存在
 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值