电商项目实战--用户相关

1. 项目分析

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

然后,确定以上这些数据的开发顺序,原则上应该先开发基础数据和简单的数据相关的功能,所以,以上数据的开发顺序应该是:用户 > 收货地址 > 商品类别 > 商品 > 收藏 > 购物车 > 订单。

接下来,分析第1种数据的相关功能,即“用户”数据的管理中,有哪些功能需要开发:注册,登录,修改密码,个人资料,上传头像。

分析完成后,再确定以上功能的开发顺序,通常开发顺序的基本原则是:增、查、删、改,所以,以上功能的开发顺序应该是:注册 > 登录 > 修改密码 > 个人资料 > 上传头像。

在具体的开发某个功能时,应该遵循的顺序是:创建数据表 > 创建实体类 > 持久层 > 业务层 > 控制器层 > 前端界面。

2. 用户-创建数据表

首先应该创建数据库:

CREATE DATABASE tedu_store;

然后使用该数据库:

USE tedu_store

再创建数据表:

CREATE TABLE t_user (
	uid INT AUTO_INCREMENT COMMENT '用户id',
	username VARCHAR(50) 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(100) COMMENT '头像',
	is_delete INT COMMENT '是否删除,0-未删除,1-已删除',
	created_user VARCHAR(50) COMMENT '创建人',
	created_time DATETIME COMMENT '创建时间',
	modified_user VARCHAR(50) COMMENT '最后修改人',
	modified_time DATETIME COMMENT '最后修改时间',
	PRIMARY KEY (uid)
) DEFAULT CHARSET=utf8;

3. 用户-创建实体类

先下载共享的项目文件,解压得到项目文件夹,将其移动到Workspace中,通过Import中的Existing Maven Projects导入项目,然后,在此前的springboot项目中复制数据库连接的配置信息到新项目中,并修改需要连接到的数据库名称为tedu_store

然后,在src/test/java下的测试类,先执行原有的空的测试方法,以检验环境是否正确,然后,在该测试类测试获取数据库连接对象,以检验连接配置信息是否正确:

@Autowired
DataSource dataSource;

@Test
public void getConnection() throws SQLException {
	Connection conn = dataSource.getConnection();
	System.err.println(conn);
}

然后,先创建cn.tedu.store.entity.BaseEntity实体的父类,称之为实体类的“基类”,实现序列化接口,该类的作用就是用于被继承的,所以应该添加abstract修饰符,并且,该类也只需要被子类访问,子类都与它在同一个包中,所以,使用默认的访问权限即可:

abstract class BaseEntity implements Serializable {
	private String createdUser;
	private Date createdTime;
	private String modifiedUser;
	private Date modifiedTime;
}

cn.tedu.store.entity.User实体类,继承自以上BaseEntity类:

public class User extends BaseEntity {
	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;

	// SET/GET/基于uid生成hashCode和equals/Serializable
}

4. 用户-注册-持久层

(a) 规划SQL语句

首先,应该分析该功能需要执行的SQL语句,当用户注册时,本质是向数据表中插入数据,则需要执行SQL语句大致是:

insert into t_user (
	除了uid以外的所有字段
) values (
	?,?,?,?,?...?
);

由于设计了“用户名唯一”的规则,在插入数据之前,还应该检查该用户名是否已经被占用,则可以“根据用户名查询数据,并判断是否查询到有效结果”作为判断依据,需要执行的SQL语句大致是:

select uid from t_user where username=?

(b) 接口与抽象方法

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

Integer addnew(User user);

User findByUsername(String username);

注意:完成后,应该在启动类之前添加@MapperScan("cn.tedu.store.mapper")注解,以配置MyBatis中的持久层接口在哪个包中。

© 配置映射

src/main/resources下创建mappers文件夹,并在该文件夹中粘贴持久层映射的XML文件,确定文件名应该是UserMapper.xml,配置好根节点的namespace属性,并配置以上接口中2个抽象方法的映射:

	<mapper namespace="cn.tedu.store.mapper.UserMapper">
	
		
		
		<insert id="addnew"
			useGeneratedKeys="true"
			keyProperty="uid">
			INSERT INTO t_user (
				username, password,
				salt, gender,
				phone, email,
				avatar, is_delete,
				created_user, created_time,
				modified_user, modified_time
			) VALUES (
				#{username}, #{password},
				#{salt}, #{gender},
				#{phone}, #{email},
				#{avatar}, #{isDelete},
				#{createdUser}, #{createdTime},
				#{modifiedUser}, #{modifiedTime}
			)
		insert>
		
		
		
		<select id="findByUsername"
			resultType="cn.tedu.store.entity.User">
			SELECT 
				uid 
			FROM 
				t_user 
			WHERE 
				username=#{username}
		select>
	
	mapper>

注意:需要检查在application.properties中是否配置了XML映射文件的位置mybatis.mapper-locations=classpath:/mappers/*.xml

完成后,在src/test/java下创建cn.tedu.store.mapper.UserMapperTests单元测试类,并在该类中编写并执行持久层2个功能的测试方法:

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

5. 用户-注册-业务层

(a) 规划异常

首先,应该分析用户在执行此次操作时,可能有哪些失败的原因。

应该为所有的自定义异常创建一个公共的父类,以确定自定义异常的类别,所以,创建cn.tedu.store.service.ex.ServiceException,它应该继承自RuntimeException

在当前项目中,设置了“用户名唯一”的规则,如果用户尝试注册的用户名已经被占用,则应该抛出自定义的cn.tedu.store.service.ex.UsernameDuplicateException异常,继承自ServiceException

此次“注册”将执行INSERT语句,则可能存在某些不控的因素导致插入失败,所以,还应该为这种失败的可能设计对应的cn.tedu.store.service.ex.InsertException异常。

(b) 接口与抽象方法

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

void reg(User user) throws UsernameDuplicateException, InsertException;

在设计抽象方法时,方法的返回值仅以操作成功为前提进行设计,如果需要考虑操作失败,则使用抛出异常的方式来解决。

© 实现抽象方法

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

@Service
public class UserServiceImpl implements IUserService {

	@Autowired
	private UserMapper userMapper;

	public void reg(User user) {
	}

}

然后,重写抽象方法:

public void reg(User user) {
	// 根据参数user中的getUsername()获取尝试注册的用户名
	// 根据以上用户名查询用户数据
	// 判断查询结果是否不为null
	// 是:用户名已经被占用,抛出UsernameDuplicateException
	// 用户名未被占用,允许注册
	// TODO 向参数user中补全属性:盐值
	// TODO 取出参数user中的原始密码
	// TODO 将原始密码加密
	// TODO 向参数user中补全属性:加密后的密码
	// 向参数user中补全属性:isDelete-0
	// 向参数user中补全属性:4项日志
	// 执行注册
}

初步实现为:

	@Override
	public void reg(User user) throws UsernameDuplicateException, InsertException {
		// 根据参数user中的getUsername()获取尝试注册的用户名
		String username = user.getUsername();
		// 根据以上用户名查询用户数据
		User result = userMapper.findByUsername(username);
		// 判断查询结果是否不为null
		if (result != null) {
			// 是:用户名已经被占用,抛出UsernameDuplicateException
			throw new UsernameDuplicateException(
				"注册失败!尝试注册的用户名(" + username + ")已经被占用!");
		}
		
		// 用户名未被占用,允许注册
		// TODO 向参数user中补全属性:盐值
		// TODO 取出参数user中的原始密码
		// TODO 将原始密码加密
		// TODO 向参数user中补全属性:加密后的密码
		// 向参数user中补全属性:isDelete-0
		user.setIsDelete(0);
		// 向参数user中补全属性:4项日志
		Date now = new Date();
		user.setCreatedUser(username);
		user.setCreatedTime(now);
		user.setModifiedUser(username);
		user.setModifiedTime(now);
		// 执行注册
		Integer rows = userMapper.addnew(user);
		if (rows != 1) {
			throw new InsertException(
				"注册失败!插入用户数据时出现未知错误!请联系管理员!");
		}
	}

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

	@RunWith(SpringRunner.class)
	@SpringBootTest
	public class UserServiceTests {
	
		@Autowired
		IUserService service;
		
		@Test
		public void reg() {
			try {
				User user = new User();
				user.setUsername("Service");
				user.setPassword("1234");
				service.reg(user);
				System.err.println("OK");
			} catch (ServiceException e) {
				System.err.println(e.getClass().getName());
				System.err.println(e.getMessage());
			}
		}
		
	}

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

(a) 统一处理异常

创建cn.tedu.store.controller.BaseController控制类的基类,后续创建的每个控制器类都应该继承自这个基类,在基类中添加处理处理异常的方法,则每个子级的控制器类都相当于拥有这个方法:

	public abstract class BaseController {

		@ExceptionHandler(ServiceException.class)
		@ResponseBody
		public JsonResult<Void> handleException(Throwable e) {
			JsonResult<Void> jr = new JsonResult<Void>();
			jr.setMessage(e.getMessage());

			if (e instanceof UsernameDuplicateException) {
				jr.setState(2);
			} else if (e instanceof InsertException) {
				jr.setState(3);
			}

			return jr;
		}

	}

(b) 设计请求

设计“用户注册”的请求方式:

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

© 处理请求

首先,需要创建cn.tedu.store.util.JsonResult响应结果类型:

	public class JsonResult<T> {
		private Integer state;
		private String message;
		private T data;
		// SET/GET
	}

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

	@RestController
	@RequestMapping("users")
	public class UserController extends xxxController {

		@Autowired
		private IUserService userService;

	}

接下来,在类中添加处理请求的方法:

	@RequestMapping("reg")
	public JsonResult<Void> reg(User user) {
		JsonResult<Void> jr = new JsonResult<Void>();
		userService.reg(user);
		jr.setState(1);
		return jr;
	}

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

测试完成后,将方法之前的@RequestMapping替换为@PostMapping

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

8. 用户-登录-持久层

(a) 规划SQL语句

用户登录时,需要检查用户名是否存在,密码是否匹配,且is_delete的值是否表示“未删除”,关于“用户名”是否存在,可以通过查询数据库得到:

select * from t_user where username=?

如果查询到有效结果,则用户名是存在,可以进行后续的验证,如果查询的结果是null,则用户名尚未注册,不允许使用这个用户名登录!

关于密码、is_delete的验证应该在后续的业务层中进行判断,只需要保证以上查询时,能够查到与验证相关的字段即可,例如:

select password, salt, is_delete from t_user where username=?

并且,如果用户登录成功,还应该将用户(客户端)提供用户的相关数据,例如用户的用户名、头像,还需要在Session中记录该用户的id和用户名,则查询时还要查询这些字段:

select uid, username, password, salt, avatar, is_delete from t_user where username=?

(b) 接口与抽象方法

UserMapper.java接口中已经存在User findByUsername(String username)方法,则无需重复定义抽象方法。

© 配置映射

此次需要在findByUsername()方法的映射中补充查询更多的字段:




完成后,再次执行已经存在的单元测试。

9. 用户-登录-业务层

(a) 规划异常

此时,应该穷举用户在“登录”过程中可能出现的任何“错误”,例如,可能出现“用户名不存在”,或“密码错误”,或“用户数据被标记为已删除”。

所以,需要为以上“错误”创建对应的异常类:

cn.tedu.store.service.ex.UserNotFoundException

cn.tedu.store.service.ex.PasswordNotMatchException

创建的异常类都应该继承自ServiceException

(b) 接口与抽象方法

IUserService接口中添加“登录”的抽象方法:

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

© 实现抽象方法

UserServiceImpl实现类中重写以上抽象方法:

public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
	// 根据参数username执行查询
	// 判断查询结果是否为null
	// 是:抛出UserNotFoundException

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

	// 从查询结果中获取盐值
	// 基于参数password和盐值执行加密
	// 判断以上加密结果与查询结果中的password是否不匹配
	// 是:抛出PasswordNotMatchException

	// 将查询结果中的password设置为null
	// 将查询结果中的salt设置为null
	// 将查询结果中的isDelete设置为null
	// 返回查询结果
}

具体实现为:

	@Override
	public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
		// 根据参数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();
		// 基于参数password和盐值执行加密
		String md5Password = getMd5Password(password, salt);
		// 判断以上加密结果与查询结果中的password是否不匹配
		if (!md5Password.equals(result.getPassword())) {
			// 是:抛出PasswordNotMatchException
			throw new PasswordNotMatchException(
				"登录失败!密码错误!");
		}

		// 将查询结果中的password设置为null
		result.setPassword(null);
		// 将查询结果中的salt设置为null
		result.setSalt(null);
		// 将查询结果中的isDelete设置为null
		result.setIsDelete(null);
		// 返回查询结果
		return result;
	}

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

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

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

(a) 统一处理异常

BaseController的处理异常的方法中,添加更多的分支,对以上新抛出的2种异常进行处理:

	if (e instanceof UsernameDuplicateException) {
		jr.setState(4000);
	} else if (e instanceof UserNotFoundException) {
		jr.setState(4001);
	} else if (e instanceof PasswordNotMatchException) {
		jr.setState(4002);
	} else if (e instanceof InsertException) {
		jr.setState(5000);
	}

(b) 设计请求

设计“用户登录”的请求方式:

请求路径:/users/login
请求参数:String username, String password, HttpSession session
请求方式:POST
响应数据:JsonResult

© 处理请求

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

@RequestMapping("login")
public JsonResult login(String username, String password, HttpSession session) {
	// 调用业务层对象的“登录”方法,获取返回结果
	// 向Session中存入用户id和用户名
	// 返回
}

具体代码为:

	@RequestMapping("login")
	public JsonResult<User> login(String username, String password, HttpSession session) {
		// 调用业务层对象的“登录”方法,获取返回结果
		User data = userService.login(username, password);
		// 向Session中存入用户id和用户名
		session.setAttribute("uid", data.getUid());
		session.setAttribute("username", data.getUsername());
		// 返回
		return new JsonResult<>(SUCCESS, data);
	}

完成后,可以通过http://localhost:8080/users/login?username=rootx&password=1234x进行单元测试,完成后,将@RequestMapping改成@PostMapping

对于输出的JSON结果过于复杂,且将为null的结果都已经输出为JSON数据,这种做法并不合理,存在浪费资源的问题,并且还暴露项目的特征数据,要解决该问题,可以在application.properties中添加配置,使得为null的属性将不被输出到JSON结果中:

spring.jackson.default-property-inclusion=non_null

11. 用户-登录-前端界面

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

(a) 规划SQL语句

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

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

在执行修改之前,还需要验证原密码是否正确,但是,不应该是:

update t_user set password=? where uid=? and password=?

之所以不将密码作为查询条件之一,是因为在SQL语句中不区分大小写,而密码应该匹分大小写,另外,“需要验证原密码”是软件开发者所设计的业务规则,应该在业务层中去体现,并不应该写在SQL语句中。

为了保证“验证原密码”的功能,还需要查询出原密码和盐值,则需要执行:

select password, salt from t_user where uid=?

另外,在查询时,还应该检查用户的is_delete是否正常,所以:

select password, salt, is_delete from t_user where uid=?

(b) 接口与抽象方法

UserMapper.java接口中添加抽象方法:

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

User findByUid(Integer uid);

© 配置映射

UserMapper.xml中配置以上2个抽象方法的映射,如果不希望每次查询时都需要自定义别名,可以事先配置好

	
	<resultMap id="UserEntityMap" 
		type="cn.tedu.store.entity.User">
		<id column="uid" property="uid"/>
		<result column="username" property="username"/>
		<result column="password" property="password"/>
		<result column="salt" property="salt"/>
		<result column="gender" property="gender"/>
		<result column="phone" property="phone"/>
		<result column="email" property="email"/>
		<result column="avatar" property="avatar"/>
		<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>

	
	
	<update id="updatePassword">
		UPDATE
			t_user
		SET
			password=#{password},
			modified_user=#{modifiedUser},
			modified_time=#{modifiedTime}
		WHERE
			uid=#{uid}
	update>

	
	
	<select id="findByUid"
		resultMap="UserEntityMap">
		SELECT 
			password, salt,
			is_delete
		FROM 
			t_user 
		WHERE 
			uid=#{uid}
	select>

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

@Test
public void updatePassword() {
	Integer uid = 8;
	String password ="1234";
	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 = 8;
	User result = mapper.findByUid(uid);
	System.err.println(result);
}

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

(a) 规划异常

本次执行的主要是更新数据的操作,则可能出现UpdateException

在执行更新之前,还需要根据uid查询用户数据,检查数据是否存在,检查数据的is_delete,都可能出现UserNotFoundException

在执行更新之前,还需要验证原密码是否正确,则可能出现PasswordNotMatchException

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

(b) 接口与抽象方法

IUserService中添加“修改密码”的抽象方法:

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

关于抽象方法的参数的设计原则:要么是客户端提交的,要么是服务器端的控制器中提供的,并且,穷举所有参数后,足以调用持久层的相关功能!

© 实现抽象方法

UserServiceImpl实现类中添加新的抽象方法并实现:

public void changePassword(Integer uid, String oldPassword, String newPassword, String modifiedUser) throws UserNotFoundException, PasswordNotMatchException, UpdateException {
	// 根据参数uid查询用户数据
	// 判断查询结果是否为null:UserNotFoundException

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

	// 从查询结果中获取盐值
	// 对参数oldPassword执行加密,得到oldMd5Password
	// 判断查询结果中的密码与oldMd5Password是否不匹配:PasswordNotMatchException

	// 对参数newPassword执行加密,得到newMd5Passowrd
	// 执行更新,获取返回值(受影响的行数)
	// 判断受影响的行数是否不为1:UpdateException
}

具体实现为:

	@Override
	public void changePassword(Integer uid, String oldPassword, String newPassword, String modifiedUser)
			throws UserNotFoundException, PasswordNotMatchException, UpdateException {
		// 根据参数uid查询用户数据
		User result = userMapper.findByUid(uid);
		// 判断查询结果是否为null:UserNotFoundException
		if (result == null) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"修改密码失败!用户数据不存在!");
		}

		// 判断查询结果中的isDelete是否为1:UserNotFoundException
		if (result.getIsDelete() == 1) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"修改密码失败!用户数据不存在!");
		}
		
		// 从查询结果中获取盐值
		String salt = result.getSalt();
		// 对参数oldPassword执行加密,得到oldMd5Password
		String oldMd5Password = getMd5Password(oldPassword, salt);
		// 判断查询结果中的密码与oldMd5Password是否不匹配:PasswordNotMatchException
		if (!result.getPassword().equals(oldMd5Password)) {
			// 是:抛出PasswordNotMatchException
			throw new PasswordNotMatchException(
				"修改密码失败!原密码错误!");
		}

		// 对参数newPassword执行加密,得到newMd5Passowrd
		String newMd5Password = getMd5Password(newPassword, salt);
		// 执行更新,获取返回值(受影响的行数)
		Integer rows = userMapper.updatePassword(uid, newMd5Password, modifiedUser, new Date());
		// 判断受影响的行数是否不为1:UpdateException
		if (rows != 1) {
			throw new UpdateException(
				"修改密码失败!更新数据时出现未知错误!");
		}
	}

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

@Test
public void changePassword() {
	try {
		Integer uid = 9;
		String oldPassword = "8888";
		String newPassword = "1234";
		String modifiedUser = "系统管理员";
		service.changePassword(uid, oldPassword, newPassword, modifiedUser);
		System.err.println("OK");
	} catch (ServiceException e) {
		System.err.println(e.getClass().getName());
		System.err.println(e.getMessage());
	}
}

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

(a) 统一处理异常

需要在BaseController中添加对UpdateException的处理。

(b) 设计请求

设计“修改密码”的请求方式:

请求路径:/users/change_password
请求参数:@RequestParam("old_password") String oldPassword, @RequestParam("new_password") String newPassword, HttpSession session
请求方式:POST
响应数据:JsonResult

© 处理请求

BaseController中添加各子级控制器都可能需要执行的方法,例如获取uid的方法:

protected final Integer getUidFromSession(HttpSession session) {
	return Integer.valueOf(session.getAttribute("uid").toString());
}

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

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

	@RequestMapping("change_password")
	public JsonResult<Void> changePassword(
		@RequestParam("old_password") String oldPassword,
		@RequestParam("new_password") String newPassword, 
		HttpSession session) {
		// 从session中获取uid
		Integer uid = getUidFromSession(session);
		// 从session中获取username
		String username = getUsernameFromSession(session);
		// 调用service对象执行修改密码
		userService.changePassword(uid, oldPassword, newPassword, username);
		// 响应成功
		return new JsonResult<>(SUCCESS);
	}

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

15. 添加拦截器

由于后续的越来越多的操作都是需要事先登录的,不登录则不允许执行相关操作,例如修改密码、修改资料、上传头像、创建收货地址、操作购物车、生成订单等……所以,可以在项目中添加登录拦截器,对用户是否登录进行验证,如果没有登录,则不执行后续的请求处理,而是直接重定向到登录页,避免发生错误!

首先,创建cn.tedu.store.interceptor.LoginInterceptor拦截器类,并定义拦截处理方式:

	public class LoginInterceptor implements HandlerInterceptor {
		@Override
		public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
				throws Exception {
			// 获取HttpSession对象
			HttpSession session = request.getSession();
			// 判断session中是否有登录信息
			if (session.getAttribute("uid") == null) {
				// 没有登录信息,则重定向到登录页
				response.sendRedirect("/web/login.html");
				// 执行拦截
				return false;
			}
			// 放行
			return true;
		}
	
	}

然后,需要对拦截器进行配置,在SpringBoot中,需要自定义配置类,以对拦截器进行配置:

	@Configuration
	public class InterceptorConfigurer implements WebMvcConfigurer {
	
		@Override
		public void addInterceptors(InterceptorRegistry registry) {
			// 创建拦截器对象
			HandlerInterceptor interceptor = new LoginInterceptor();
			
			// 白名单
			List<String> patterns = new ArrayList<>();
			patterns.add("/bootstrap3/**");
			patterns.add("/css/**");
			patterns.add("/js/**");
			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);
		}
		
	}

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

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

(a) 规划SQL语句

执行修改资料时需要执行的SQL语句大致是:

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

在执行修改之前,还是需要验证用户数据是否存在、是否标记为删除,该功能对应的SQL语句在“修改密码”时已经完成。

另外,在刚刚打开页面时,还需要显示当前登录的用户的信息,则需要根据用户的uid查询相关需要显示的信息:

select username,phone,email,gender from t_user where uid=?

(b) 接口与抽象方法

UserMapper.java中添加抽象方法:

Integer updateInfo(User user);

关于查询的功能在“修改密码”时已经存在User findByUid(Integer uid)方法,则无需重复声明。

© 配置映射

UserMapper.xml配置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>

然后,找到User findByUid(Integer uid)的映射,补充查询字段:

	
	
	<select id="findByUid"
		resultMap="UserEntityMap">
		SELECT 
			username, phone,
			email, gender,
			password, salt,
			is_delete
		FROM 
			t_user 
		WHERE 
			uid=#{uid}
	select>

完成后,编写并执行“更新密码”的单元测试:

@Test
public void updateInfo() {
	User user = new User();
	user.setUid(7);
	user.setPhone("13700137001");
	user.setEmail("[email protected]");
	user.setGender(1);
	Integer rows = mapper.updateInfo(user);
	System.err.println("rows=" + rows);
}

并再次执行测试“根据用户id查询信息”的功能。

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

(a) 规划异常

更新操作必然可能出现UpdateException,在执行更新之前,应该检查用户数据,则可能出现UserNotFoundexception

(b) 接口与抽象方法

IUserService中添加抽象方法:

void changeInfo(Integer uid, String username, User user) throws UserNotFoundException, UpdateException;

User getByUid(Integer uid);

© 实现抽象方法

UserServiceImpl中实现以上抽象方法:

public void changeInfo(Integer uid, String username, User user) throws UserNotFoundException, UpdateException {
	// 根据参数uid查询用户数据
	// 判断查询结果是否为null:UserNotFoundException

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

	// 将参数uid和参数username封装到参数user的uid和modifiedUser属性中
	// 执行更新,获取返回值(受影响的行数)
	// 判断受影响的行数是否不为1:UpdateException
}

public User getByUid(Integer uid) {
	// 根据参数uid查询用户数据
	// 判断查询结果是否为null:UserNotFoundException

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

	// 将查询结果中不相关的数据设置为null
	// 返回查询结果
}

具体实现为:

	@Override
	public void changeInfo(Integer uid, String username, User user) throws UserNotFoundException, UpdateException {
		// 根据参数uid查询用户数据
		User result = userMapper.findByUid(uid);
		// 判断查询结果是否为null:UserNotFoundException
		if (result == null) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"修改个人资料失败!用户数据不存在!");
		}

		// 判断查询结果中的isDelete是否为1:UserNotFoundException
		if (result.getIsDelete() == 1) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"修改个人资料失败!用户数据不存在!");
		}
		
		// 将参数uid和参数username封装到参数user的uid和modifiedUser属性中
		user.setUid(uid);
		user.setModifiedUser(username);
		user.setModifiedTime(new Date());
		// 执行更新,获取返回值(受影响的行数)
		Integer rows = userMapper.updateInfo(user);
		// 判断受影响的行数是否不为1:UpdateException
		if (rows != 1) {
			throw new UpdateException(
				"修改个人资料失败!更新数据时出现未知错误!");
		}
	}

	@Override
	public User getByUid(Integer uid) throws UserNotFoundException {
		// 根据参数uid查询用户数据
		User result = userMapper.findByUid(uid);
		// 判断查询结果是否为null:UserNotFoundException
		if (result == null) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"获取用户资料失败!用户数据不存在!");
		}

		// 判断查询结果中的isDelete是否为1:UserNotFoundException
		if (result.getIsDelete() == 1) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"获取用户资料失败!用户数据不存在!");
		}
		
		// 将查询结果中不相关的数据设置为null
		User user = new User();
		user.setUsername(result.getUsername());
		user.setPhone(result.getPhone());
		user.setEmail(result.getEmail());
		user.setGender(result.getGender());
		// 返回查询结果
		return user;
	}

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

@Test
public void changeInfo() {
	try {
		Integer uid = 700;
		String username = "系统管理员";
		User user = new User();
		user.setGender(1);
		user.setPhone("13700137007");
		user.setEmail("[email protected]");
		service.changeInfo(uid, username, user);
		System.err.println("OK");
	} catch (ServiceException e) {
		System.err.println(e.getClass().getName());
		System.err.println(e.getMessage());
	}
}

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

19. 用户-修改资料-控制器层

(a) 统一处理异常

没有新的异常,则无需处理。

(b) 设计请求

设计“获取用户数据”的请求方式:

请求路径:/users/get_by_uid
请求参数:HttpSession session
请求方式:GET
响应数据:JsonResult

设计“执行更新用户个人资料”的请求方式:

请求路径:/users/change_info
请求参数:User user, HttpSession session
请求方式:POST
响应数据:JsonResult

© 处理请求

UserController中:

@RequestMapping("change_info")
public JsonResult changeInfo(User user, HttpSession session) {
	// 从session中获取uid和username
	// 执行修改
	// 返回成功
}

@GetMapping("get_by_uid")
public JsonResult getByUid(HttpSession session) {
	// 执行获取数据
	// 返回成功与数据
}

具体实现为:

	
	@RequestMapping("change_info")
	public JsonResult<Void> changeInfo(User user, HttpSession session) {
		// 从session中获取uid和username
		Integer uid = getUidFromSession(session);
		String username = getUsernameFromSession(session);
		// 执行修改
		userService.changeInfo(uid, username, user);
		// 返回成功
		return new JsonResult<>(SUCCESS);
	}

	@GetMapping("get_by_uid")
	public JsonResult<User> getByUid(HttpSession session) {
		// 执行获取数据
		Integer uid = getUidFromSession(session);
		User data = userService.getByUid(uid);
		// 返回成功与数据
		return new JsonResult<>(SUCCESS, data);
	}
	

完成后,可以执行测试:

http://localhost:8080/users/get_by_uid
http://localhost:8080/users/change_info?gender=1&phone=111&email=222

20. 用户-修改资料-前端界面

21. 基于SpringMVC的文件上传(Upload:上载)

21.1. 目标

在页面中点击浏览按钮,可以将指定的文件上传到服务器。

21.2. 创建项目

创建Maven Project,Group Id为cn.tedu.spring,Artifact Id为SPRINGMVC-UPLOAD,Packaging为war,创建好项目后,生成web.xml文件,添加spring-webmvc依赖,添加Tomcat运行环境(必须保证Eclipse中的Servers项目是打开的),复制spring的配置文件,删除所有已经存在的配置,复制前序项目中web.xml中的配置。

另外,为了实现文件上传,还需要添加依赖:


	commons-fileupload
	commons-fileupload
	1.4

21.3. 页面

webapp下创建index.html页面,并在页面中设计:

请选择要上传的文件:

以上代码中,

method属性值必须是postenctype属性值必须是multipart/form-data,且上传控件的type属性值必须是file

21.4. 使用控制器接收请求

确定页面中表单的action属性的值为upload.do

检查spring.xml中配置的组件扫描为cn.tedu.spring

创建cn.tedu.spring.UploadController控制器类,并在类之前添加@Controller注解,然后,添加处理请求的方法:

@Controller
public class UploadController {

	@RequestMapping("upload.do")
	@ResponseBody
	public String upload() {
		System.out.println("UploadController.upload()");
		
		return "OK";
	}
	
}

21.5. 处理上传

在SpringMVC框架中,使用了CommonsMultipartResolver对上传的数据进行处理,需要在spring.xml中配置该类:



在配置时,必须指定id,且值必须是multipartResolver

关于以上节点,可以暂不配置详细信息。

然后,在处理请求的方法中,添加MultipartFile file参数,该参数就是用户上传的文件的数据,并在参数之前添加@RequestParam注解。

最后,调用参数对象的void transferTo(File dest)方法即可将用户上传的文件保存到服务器的硬盘中!

	@RequestMapping("upload.do")
	@ResponseBody
	public String upload(
		@RequestParam("file") MultipartFile file) 
			throws IllegalStateException, IOException {
		System.out.println("UploadController.upload()");
		
		// 执行保存文件
		File dest = new File("F:/1/a.jpg");
		file.transferTo(dest);
		
		return "OK";
	}

........

	@RequestMapping("upload.do")
	@ResponseBody
	public String upload(@RequestParam("file") MultipartFile file,
			HttpServletRequest request) 
			throws IllegalStateException, IOException {
		System.out.println("UploadController.upload()");
		
		// 判断上传的文件是否为空
		boolean isEmpty = file.isEmpty();
		System.out.println("\tisEmpty=" + isEmpty);
		if (isEmpty) {
			throw new RuntimeException("上传失败!上传的文件为空!");
		}
		
		// 检查文件大小
		long fileSize = file.getSize();
		System.out.println("\tsize=" + fileSize);
		if (fileSize > 1 * 1024 * 1024) {
			throw new RuntimeException("上传失败!上传的文件大小超出了限制!");
		}
		
		// 检查文件MIME类型
		String contentType = file.getContentType();
		System.out.println("\tcontentType=" + contentType);
		List<String> types = new ArrayList<String>();
		types.add("image/jpeg");
		types.add("image/png");
		types.add("image/gif");
		if (!types.contains(contentType)) {
			throw new RuntimeException("上传失败!不允许上传此类型的文件!");
		}
		
		// 准备文件夹
		String parentDir = request.getServletContext().getRealPath("upload");
		// request.getSession().getServletContext().getRealPath("");
		// request.getRealPath("");
		System.out.println("\tpath=" + parentDir);
		File parent = new File(parentDir);
		if (!parent.exists()) {
			parent.mkdirs();
		}
		
		// 获取原始文件名
		String originalFilename = file.getOriginalFilename();
		System.out.println("\toriginalFilename=" + originalFilename);
		
		// 确定最终保存时使用的文件
		String filename = UUID.randomUUID().toString();
		String suffix = "";
		int beginIndex = originalFilename.lastIndexOf(".");
		if (beginIndex != -1) {
			suffix = originalFilename.substring(beginIndex);
		}
		
		// 执行保存文件
		File dest = new File(parent, filename + suffix);
		file.transferTo(dest);
		
		return "OK";
	}

21.6. 确定上传文件的名称

为了避免文件被覆盖,每个文件的路径或文件名应该不发生冲突,例如可以使用唯一的文件名,要保证文件名唯一,可以考虑使用时间、随机数等数据作为文件名的一部分,也可以使用用户的唯一数据,例如用户的id、用户名等作为文件名的一部分,具体规则可自行决定。

关于文件的扩展名部分,可以通过MultipartFileString getOriginalFilename()方法获取原始文件名,即用户上传的文件在客户端时使用的文件名,然后对文件名进行分析处理,得到原始的扩展名,例如:

String originalFilename = file.getOriginalFilename();
String suffix = "";
int beginIndex = originalFilename.lastIndexOf(".");
if (beginIndex != -1) {
	suffix = originalFilename.substring(beginIndex);
}

21.7. 确定上传文件的文件夹

上传的文件必须在Tomcat部署项目的目录中,否则,上传的文件将无法通过http协议进行访问,可以HttpServletRequest对象获取在webapp下指定名称的文件夹的实际路径,然后创建出所需的文件夹,例如,先在处理请求的方法中添加HttpServletRequest作为参数,然后:

String parentDir = request.getServletContext().getRealPath("upload");
File parent = new File(parentDir);
if (!parent.exists()) {
	parent.mkdirs();
}

21.8. 关于MultipartFile中的常用方法

  • String getOriginalFilename():获取原始文件名,即用户上传的文件在客户端时使用的文件名;

  • boolean isEmpty():判断上传的文件是否为空,如果用户没有选择文件就提交了上传请求,或选择的文件是0字节的,则视为空,将返回true,否则返回false

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

  • String getContentType():获取文件的MIME类型,例如返回image/jpeg,关于扩展名与MIME的对应关系,可以上网查阅资料,也可以在Tomcat的conf目录下的web.xml中查找,文件的扩展名不同,得到的MIME类型可能是不同的;

  • InputStream getInputStream():获取文件的输入字节流,用于需要自定义处理用户上传的数据的应用场景,例如上传的文件较大,在存储时需要自定义缓冲区等,该方法不可与transferTo()方法同时使用;

  • void transferTo(File dest):保存客户端上传的文件,该方法不可以与getInputStream()方法同时使用。

21.9. 关于CommonsMultipartResolver的配置

spring.xml中可以对CommonsMultipartResolver的属性注入值:

  • maxUploadSize:最大上传的数据量,以字节为单位,假设设置为10M,则单次请求的最大数据就是10M,可能这次上传过程中有2个文件,则2个文件的总和不允许超过10M;

  • maxUploadSizePerFile:上传的每个文件的最大大小,假设设置为10M,且这次上传过程中有2个文件,则每个文件都不可以超过10M,但是这次请求的数据总大小可能接近20M;

  • defaultEncoding:默认字符集,用于设置上传时同一个表单中可能提交的其它字符的编码。

以上设置请求数据大小的配置,检查的时间节点比较靠前,在控制器还没有处理请求时就会检查;这些设置并不能取代在控制器通过getSize()获取文件大小并进行的相关检查,在配置文件中的这些配置是项目的全局化配置,即同一个项目中,无论是上传头像,还是上传附件,还是上传商品图片等,所有的上传功能都必须符合这些配置,而控制器中的检查是单项功能的检查,毕竟每个控制器只处理某1种请求,所以,上传头像的方法里自定义头像的大小,而上传商品图片的方法里自定义商品图片的大小,是根据具体功能作出的限制。

21.10. 如何一次性上传多个文件

如果需要上传的文件的数量是固定的,且文件的定位是不同的,例如上传身份证的正面和反面照片,则在客户端可以使用多个控件,且在服务器处理请求的方法中添加多个MultipartFile类型的参数即可。

另外,如果上传的文件的定位是相同的,且数量不固定,则在客户端使用控件,添加multiple="multiple属性,即:


这样的文件浏览控件在操作时,可以按住Ctrl键,是可以一次性选中多个文件的。

然后,在服务器端的控制器中,将参数声明为MultipartFile[]即可。

21.11. 存储方式

所有的上传,都应该将文件存储到服务器的硬盘中,另外,在数据库中记录下文件的存储路径,当需要使用文件时,可以查询数据库获取指定文件的路径,再对文件进行访问。

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

(a) 规划SQL语句

执行上传头像的数据库操作部分,只是修改用户数据的avatar字段的值,需要执行的SQL语句大致是:

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

在执行修改之前,还应该检查用户数据是否存在、是否标记为已删除,对应的功能已经存在,无需再次开发。

(b) 接口与抽象方法

UserMapper.java接口中添加抽象方法:

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 = 7;
	String avatar ="1234";
	String modifiedUser = "超级管理员";
	Date modifiedTime = new Date();
	Integer rows = mapper.updateAvatar(uid, avatar, modifiedUser, modifiedTime);
	System.err.println("rows=" + rows);
}

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

(a) 规划异常

本次执行的主要是更新数据的操作,则可能出现UpdateException

在执行更新之前,还需要根据uid查询用户数据,检查数据是否存在,检查数据的is_delete,都可能出现UserNotFoundException

此次操作无需创建新的异常类。

(b) 接口与抽象方法

IUserService中添加“修改头像”的抽象方法:

void changeAvatar(Integer uid, String avatar, String modifiedUser) throws UserNotFoundException, UpdateException;

© 实现抽象方法

UserServiceImpl实现类中添加新的抽象方法并实现:

public void changeAvatar(Integer uid, String avatar, String modifiedUser) throws UserNotFoundException, UpdateException {
	// 根据参数uid查询用户数据
	// 判断查询结果是否为null:UserNotFoundException

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

	// 执行更新,获取返回值(受影响的行数)
	// 判断受影响的行数是否不为1:UpdateException
}

具体实现为:

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

		// 判断查询结果中的isDelete是否为1:UserNotFoundException
		if (result.getIsDelete() == 1) {
			// 是:抛出UserNotFoundException
			throw new UserNotFoundException(
				"修改头像失败!用户数据不存在!");
		}
		
		// 执行更新,获取返回值(受影响的行数)
		Integer rows = userMapper.updateAvatar(uid, avatar, modifiedUser, new Date());
		// 判断受影响的行数是否不为1:UpdateException
		if (rows != 1) {
			throw new UpdateException(
				"修改头像失败!更新数据时出现未知错误!");
		}
	}

单元测试:

@Test
public void changeAvatar() {
	try {
		Integer uid = 7;
		String avatar = "88888888";
		String modifiedUser = "系统管理员";
		service.changeAvatar(uid, avatar, modifiedUser);
		System.err.println("OK");
	} catch (ServiceException e) {
		System.err.println(e.getClass().getName());
		System.err.println(e.getMessage());
	}
}

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

(a) 统一处理异常

在处理上传文件之前,应该对用户选择并提交的文件数据进行检查,例如文件是否为空、文件大小是否超出限制、文件类型是否超出限制,如果不符合标准,应该抛出对应的异常:

cn.tedu.store.controller.ex.FileEmptyException
cn.tedu.store.controller.ex.FileSizeException
cn.tedu.store.controller.ex.FileTypeException

这些都继承自cn.tedu.store.controller.ex.FileUploadException,而该FileUploadException继承自RuntimeException

另外,在执行保存文件时,调用的transferTo()方法也是会抛出异常的:

try {
	file.transferTo(dest);
} catch (IllegalStateException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
}

当捕获到以上这些异常时,可以在捕获后抛出自定义异常FileStateExceptionFileUploadIOException,也是和以上异常在同一个包中,也都继承自FileUploadException,然后,在BaseController中对自定义异常进行统一处理!

(b) 设计请求

设计“上传头像”的请求方式:

请求路径:/users/change_avatar
请求参数:MultipartFile file, HttpServletRequest request
请求方式:POST
响应数据:JsonResult

© 处理请求

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

@PostMapping("change_avatar")
public JsonResult changeAvatar(@RequestParam("file") MultipartFile file, HttpServletRequest request) {

}

25. 用户-上传头像-前端界面

  1. 当登录成功时,将用户的头像路径响应给客户端,且客户端将头像路径保存到Cookie中。首先,应该检查在UserMapper.xmlfindByUsername()查询的字段列表中是否包含avatar字段;然后,在登录的前端界面中,当响应结果表示登录成功时,通过$.cookie("avatar", obj.data.avatar, {"expires":7});将服务器端响应的头像路径保存到客户端的Cookie中。

  2. 打开上传头像页面时,通过Cookie中保存的头像路径,显示当前登录的用户的头像:

26. 用户-上传头像-自定义上传大小的限制

在SpringBoot项目中,所有的上传默认都不允许超过1M,否则就会报错!

先在启动类StoreApplication之前添加@Configuration注解,然后,在类中添加:

	@Bean
	public MultipartConfigElement getMultipartConfigElement() {
		MultipartConfigFactory factory
			= new MultipartConfigFactory();
		
		DataSize maxFileSize = DataSize.ofMegabytes(500);
		factory.setMaxFileSize(maxFileSize);
		
		DataSize maxRequestSize = DataSize.ofMegabytes(500);
		factory.setMaxRequestSize(maxRequestSize);
		
		return factory.createMultipartConfig();
	}

你可能感兴趣的:(练手,学习日志)