Project(3)
1、分析项目
当需要开发某个项目时,首先,应该分析这个项目中,需要处理哪些种类的数据!例如:用户、商品、商品类别、收藏、订单、购物车、收货地址…
然后,将以上这些种类的数据的处理排个顺序,即先处理哪种数据,后处理哪种数据!通常,应该先处理基础数据,再处理所相关的数据,例如需要先处理商品数据,才可以处理订单数据,如果多种数据之间没有明显的关联,则应该先处理简单的,再处理较难的!
则以上这些数据的处理顺序应该是:用户 > 收货地址 > 商品类别 > 商品 > 收藏 > 购物车 > 订单
当确定了数据处理顺序后,就应该分析某个用户对应的功能有哪些,以“用户”数据为例,相关功能有:注册、登录、修改密码、修改资料、上传头像…
然后,还是需要确定以上功能的开发顺序,通常,遵循“增 > 查 > 删 > 改”的顺序,则以上功能的开发顺序应该是:注册 > 登录 > 修改密码 > 修改资料 > 上传头像。
每个功能的开发都应该遵循 创建数据表 > 创建实体类 > 持久层 > 业务层 > 控制器层 > 前端页面
一次只解决一个问题
大问题拆成小问题
2、用户 - 注册 - 创建数据表
3、用户 - 注册 - 创建实体类
4、用户 - 注册 - 持久层
a.规划SQL语句
b.接口与抽象方法
c.配置映射
5、用户 - 注册 - 业务层
业务层的基本定位
a.规划异常
b.接口与抽象方法
c.实现类与重写方法
6、用户 - 注册 - 控制器层
a.处理异常
b.设计请求
c.处理请求
7、用户 - 注册 - 前端页面
8、用户 - 登录 - 持久层
a.规划SQL语句
登录验证的做法应该是:根据用户名查询数据是否存在,如果存在,则取出必要的数据,例如密码,然后,在Java程序中验证密码即可。
如果用户名匹配的数据是存在的,需要取出的数据有:密码,盐,是否标记为删除,uid,用户名。对应的SQL语句大致是:
SELECT
uid,username,
password,salt,
is_delete
FROM
t_user
WHERE
username=?
b.接口与抽象方法
在接口中已经存在findByUsername()
方法,则无须重复添加。
c.配置映射
只需在原有的findByUsername()
方法映射的SQL语句中,添加查询更多的字段即可!
<!-- 根据用户名查询用户数据 -->
<!-- User findByUsername(String username); -->
<select id="findByUsername" resultType="cn.tedu.store.entity.User">
SELECT
uid,username,
password,salt,
is_delete AS isDelete
<!-- is_delete要取别名,否则查询结果无法封装到user中 -->
FROM
t_user
WHERE
username=#{username}
</select>
然后执行单元测试
9、用户 - 登录 - 业务层
a.规划异常
规划异常,应该是列举此次操作中可能存在的操作失败,包括用户提交不合理甚至错误的数据,或不符合逻辑的数据,都是失败的!
在“登录”时,用户提交的用户名可能是未被注册的,即不存在的,对于这种情况,应该抛出对应的异常:UserNotFoundException
;
也可能查询到了用户名匹配的数据,但是,是被标记为删除的,这种用户数据也是不允许登录的,也应该抛出异常:UserNotFoundException
;
在验证密码时,还可能出现密码不匹配的问题,也是不允许登录的,则抛出异常:PasswordNotMatchException
。
则需要创建cn.tedu.store.service.ex.UserNotFoundException
和PasswordNotMatchException
,它们都是ServiceException
的子类。
b.接口与抽象方法
在IUserService
接口中添加新的抽象方法:
/**
* 用户登录
* @param username 用户名
* @param password 密码
* @return 登录成功的用户的信息
* @throws UserNotFoundException 用户名不存在异常
* @throws PasswordNotMatchException 密码错误异常
*/
User login(String username, String password)
throws UserNotFoundException, PasswordNotMatchException;
c.实现类与重写方法
在UserServiceImpl
中重写接口中的抽象方法:
@Override
public User login(String username, String password)
throws UserNotFoundException, PasswordNotMatchException {
// 根据 username 查询数据是否为 null
// 是 -- 抛出 UserNotFoundException
// 判断 isDelete 是否为 1
// 是 -- 抛出 UserNotFoundException
// 获取数据库中的密码 mysqlPassword
// 获取盐值 salt
// 将用户提交的密码 password 与 salt 加密得到 md5Password
// 比较 mysqlPassword 与 md5Password 是否不一致
// 是-- 抛出 PasswordNotMatchException
// 将查询结果中的 password、salt、isDelete 设为 null
// 返回查询结果
}
代码实现:
@Override
public User login(String username, String password)
throws UserNotFoundException, PasswordNotMatchException {
// 根据 username 查询数据是否为 null
// 是 -- 抛出 UserNotFoundException
// 判断 isDelete 是否为 1
// 是 -- 抛出 UserNotFoundException
User result = userMapper.findByUsername(username);
if(result == null) {
throw new UserNotFoundException("登陆失败!用户名不存在!");
}
if(result.getIsDelete() == 1) {
throw new UserNotFoundException("登陆失败!用户名不存在!");
}
// 获取数据库中的密码 mysqlPassword
// 获取盐值 salt
// 将用户提交的密码 password 与 salt 加密得到 md5Password
// 比较 mysqlPassword 与 md5Password 是否不一致
// 是-- 抛出 PasswordNotMatchException
String mysqlPassword = result.getPassword();
String salt = result.getSalt();
String md5Password = getMd5Password(password, salt);
if(!mysqlPassword.equals(md5Password)) {
throw new PasswordNotMatchException("登陆失败!密码错误!");
}
// 将查询结果中的 password、salt、isDelete 设为 null
result.setPassword(null);
result.setSalt(null);
result.setIsDelete(null);
// 返回查询结果
return result;
}
在UserServiceTests
中编写新的测试方法,以执行单元测试:
@Test
public void testLogin() {
try {
String username = "root";
String password = "123";
User user = service.login(username, password);
System.err.println(user);
}catch(ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
10、用户 - 登录 - 控制器层
a.处理异常
此次业务层抛出了新的异常,则需在BaseController
的处理异常的方法中,添加更多的分支,对这些新的异常进行处理。
/**
* 控制器类的基类,实现统一处理异常
*/
public abstract class BaseController {
/**
* 操作结果的“成功”状态
*/
public static final Integer SUCCESS = 2000;
// 只处理ServiceException及其子孙类异常,避免异常过度处理
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleException(Throwable e) {
JsonResult<Void> jr = new JsonResult<Void>();
jr.setMessage(e.getMessage());
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);
}
return jr;
}
}
b.设计请求
请求路径:/users/login
请求参数:String username, String password, HttpSession session
请求方式:POST
响应数据:JsonResult<User>
通常,请求路径中,后半部分表示当前功能的名称,可以与业务层方法的名称保持一致!
c.处理请求
在UserController
类中:
//使用@RequestMapping注解方便在地址栏上测试!
@RequestMapping("login")
public JsonResult<User> login(String username, String password, HttpSession session) {
// 执行登录,获取登录返回结果
User user = userService.login(username, password);
// 向session中封装数据
session.setAttribute("uid", user.getUid());
session.setAttribute("username", user.getUsername());
// 向客户端响应操作成功
// 因为要返回响应结果,所以需要在JsonResult类中在添加一个新的构造方法
return new JsonResult<User>(SUCCESS, user);
}
JsonResult类中添加新的构造方法:
/**
* 向客户端响应操作结果的数据类型
* @param <T> 向客户端响应的数据的类型
*/
public class JsonResult<T> {
....
....
....
public JsonResult(Integer state, T data) {
super();
this.state = state;
this.data = data;
}
}
注:
因为登录的返回的user对象中,有很多属性值为null,且这样的返回user对象会暴露我们所设计的数据结构:
所以可以再返回值类型的类之前添加@JsonInclude(Include.NON_NULL)
注解:
/**
* 用户数据的实体类
* @author DELL
*
*/
@JsonInclude(Include.NON_NULL)
public class User extends BaseEntity {
....
....
}
/**
* 向客户端响应操作结果的数据类型
*
* @param <T> 向客户端响应的数据的类型
*/
@JsonInclude(Include.NON_NULL)
public class JsonResult<T> {
....
....
}
新的返回值:
该@JsonInclude(Include.NON_NULL)
注解还可以加在类的属性前,使用更灵活。
也可在spring配置文件中统一配置:
spring.jackson.default-property-inclusion=non-null
11、用户 - 登录 - 前端页面
<script type="text/javascript">
// 页面加载完成后执行的方法(函数)
$(document).ready(function() {
// 在button控件上添加id:btn-reg
$("#btn-login").click(function() {
$.ajax({
// url: 请求交到哪里去
// data: 请求提交的参数,以上的form表单中,在需要提交的参数的控件中添加name属性;
// form表单上添加id属性
// type: 请求方式
// dataType: 服务器端响应的结果的类型
// success: 响应成功时的处理函数
"url" : "/users/login",
"data" : $("#form-login").serialize(),
"type" : "post",
"dataType" : "json",
"success" : function(json) {
if (json.state == 2000) {
alert("登录成功!");
//跳转到某个页面
} else {
alert(json.message);
}
}
});
});
});
</script>
12、用户 - 修改密码 - 持久层
a.规划SQL语句
修改密码的SQL语句大致是:
UPDATE t_user SET password=?, modified_user=?, modified_time=? WHERE uid=?
在执行更新之前,还应检查用户数据是否正常,并验证其密码是否正确,则需要执行:
SELECT is_delete, password, salt FROM t_user WHERE uid=?
b.接口与抽象方法
需要在UserMapper
接口中添加新的抽象方法:
/**
* 修改密码
* @param uid 用户id
* @param password 用户提交的新密码
* @param modifiedUser 修改人
* @param modifiedTime 修改时间
* @return 受影响的行数
*/
Integer updatePassword(
@Param("uid") Integer uid,
@Param("password") String password,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime);
/**
* 根据用户id查询用户数据
* @param uid 要查询的用户的id
* @return 匹配的用户数据,如果没有匹配的数据,则返回null
*/
User findByUid(Integer uid);
注:在接口中,应将各方法按照某种顺序进行排列。
c.配置映射
在UserMapper.xml
中配置以上两个方法的映射:
<!-- 修改密码 -->
<!-- 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
is_delete AS isDelete, password,
salt
FROM
t_user
WHERE
uid=#{uid}
</select>
然后再UserMapperTests
中编写并执行单元测试:
@Test
public void testUpdatePassword() {
String password = "000";
Integer uid = 22;
Date now = new Date();
String modifiedUser = "Tom3";
Date modifiedTime = now;
Integer rows = userMapper.updatePassword(uid, password, modifiedUser, modifiedTime);
System.err.println(rows);
}
@Test
public void testFindByUid() {
Integer uid = 22;
User user = userMapper.findByUid(uid);
System.err.println(user);
}
注:用于测试的数据,在测试完成之后,需要将这些数据删除。
13、用户 - 修改密码 - 业务层
a.规划异常
此次更新密码之前,需要检查用户数据是否正常,如:用户数据是否存在、用户数据是否被标记为已删除,则可能抛出:UserNotFoundException
;
在执行更新之前还需验证原密码是否正确,则可能抛出:PasswordNotMatchException
;
在执行更新过程中,也可能出现更新失败,返回的受影响行数不符合预期值,则可能抛出:UpdateException
。
所以需要创建:cn.tedu.store.service.ex.UpdateException
:
/**
* 更新数据异常
* @author DELL
*
*/
public class UpdateException extends ServiceException {
// 序列化接口
// 五个构造方法
}
b.接口与抽象方法
在IUserService
中添加抽象方法:
业务层抽象方法设计原则:
- 返回值:仅以操作成功为前提来设计返回值(因为通常失败会以异常的方式返回给用户,不必考虑失败的返回值);
- 方法名:尽量体现业务例如使用reg或regist或register表示注册,使用login表示登录;
- 参数列表:一定是客户端可以提供的数据,或来自于Session中的数据,且足够调用持久层的各方法;
- 异常:把用户操作失败的可能,都设计成各种异常,并把这些异常都添加到方法的声明中。
/**
* 修改密码
* @param uid 用户id
* @param username 用户名
* @param oldPassword 旧密码
* @param newPassword 新密码
* @throws UserNotFoundException 用户不存在异常
* @throws PasswordNotMatchException 密码错误异常
* @throws UpdateException 更新失败异常
*/
void changePassword(Integer uid, String username, String oldPassword, String newPassword)
throws UserNotFoundException, PasswordNotMatchException, UpdateException;
c.实现类与重写方法
在UserServiceImpl
中重写修改密码的方法:
void changePassword(Integer uid, String username, String oldPassword, String newPassword)
throws UserNotFoundException, PasswordNotMatchException, UpdateException{
// 根据 uid 查询用户数据 result 是否存在
// 是 -- 抛出 UserNotFoundException
// 判断 isDelete 是否为 1
// 是 -- 抛出 UserNotFoundException
// 根据 result 获取数据库中的密码 mysqlPassword
// 根据 result 获取盐值 salt
// 将 oldPassword 和 salt 加密获得 oldMd5Password
// 比较 mysqlPassword 与 oldMd5Password 是否不一致
// 是-- 抛出 PasswordNotMatchException
// 将 newPassword 和 salt 加密获得 newMd5Password
// uid、username、newMd5Password,获取当前时间
// 执行修改密码
}
代码实现:
@Override
public void changePassword(Integer uid, String username, String oldPassword, String newPassword)
throws UserNotFoundException, PasswordNotMatchException, UpdateException {
// 根据 uid 查询用户数据 result 是否存在
// 是 -- 抛出 UserNotFoundException
// 判断 isDelete 是否为 1
// 是 -- 抛出 UserNotFoundException
User result = userMapper.findByUid(uid);
if(result == null) {
throw new UserNotFoundException("修改密码失败!用户不存在!");
}
if(result.getIsDelete() == 1) {
throw new UserNotFoundException("修改密码失败!用户不存在!");
}
// 根据 result 获取数据库中的密码 mysqlPassword
// 根据 result 获取盐值 salt
// 将 oldPassword 和 salt 加密获得 oldMd5Password
// 比较 mysqlPassword 与 oldMd5Password 是否不一致
// 是-- 抛出 PasswordNotMatchException
String mysqlPassword = result.getPassword();
String salt = result.getSalt();
String oldMd5Password = getMd5Password(oldPassword, salt);
// System.err.println("salt = " + salt);
// System.err.println("oldPassword = " + oldPassword);
// System.err.println("mysqlPassword = " + mysqlPassword);
// System.err.println("oldMd5Password = " + oldMd5Password);
if(!mysqlPassword.equals(oldMd5Password)) {
throw new PasswordNotMatchException("修改密码失败!原密码错误!");
}
// 将 newPassword 和 salt 加密获得 newMd5Password
// uid、username、newMd5Password,获取当前时间
// 执行修改密码
String newMd5Password = getMd5Password(newPassword, salt);
Date now = new Date();
Integer rows = userMapper.updatePassword(uid, newMd5Password, username, now);
if(rows != 1) {
throw new UpdateException("修改密码失败!出现未知错误!请联系系统管理员!");
}
}
/**
* 对密码进行加密
*
* @param password 原始密码
* @param salt 盐值
* @return 加密后的密码
*/
String getMd5Password(String password, String salt) {
// 规则:对 原始密码+盐值 3重加密
String str = password + salt;
for (int i = 0; i < 3; i++) {
str = DigestUtils.md5Hex(str.getBytes());
}
return str;
}
在UserServiceTests
中编写并执行测试代码:
@Test
public void testUpdatePassword() {
try {
Integer uid = 20;
String username = "Tom0";
String oldPassword = "123";
String newPassword = "000";
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、用户 - 修改密码 - 控制器层
a.处理异常
此次业务层抛出了新的异常:UpdateException
,则需要在BaseController
中进行处理!
b.设计请求
请求路径:/users/change_password
请求参数:String oldPassword, String newPassword, HttpSession session
请求方式:POST
响应数据:JsonResult<Void> //绝大部分情况下,JsonResult泛型的类型与业务层的抽象方法的返回值类型一致
c.处理请求
// 设计为RequestMapping是为了方便在地址栏中进行测试
@RequestMapping("change_password")
public JsonResult<Void> changePassword(
@RequestParam("old_password") String oldPassword,
@RequestParam("new_password") String newPassword,
HttpSession session){
// 从session中获取uid和username
Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
String username = session.getAttribute("username").toString();
System.err.println("uid+username:"+uid+"--"+username);
// 执行修改密码
userService.changePassword(uid, username, oldPassword, newPassword);
// 响应修改成功
return new JsonResult<Void>(SUCCESS);
}
完成后,启动项目,在浏览器中,先登录,然后通过http://localhost:8080/users/change_password?old_password=123&new_password=000
进行测试
15、用户 - 修改密码 - 前端页面
<script type="text/javascript">
// 页面加载完成后执行的方法(函数)
$(document).ready(function() {
// 在button控件上添加id:btn-reg
$("#btn-change-password").click(function() {
$.ajax({
// url: 请求交到哪里去
// data: 请求提交的参数,以上的form表单中,在需要提交的参数的控件中添加name属性;
// form表单上添加id属性
// type: 请求方式
// dataType: 服务器端响应的结果的类型
// success: 响应成功时的处理函数
"url" : "/users/change_password",
"data" : $("#form-change-password").serialize(),
"type" : "post",
"dataType" : "json",
"success" : function(json) {
if (json.state == 2000) {
alert("修改成功!");
//跳转到某个页面
} else {
alert(json.message);
}
}
});
});
});
</script>