avatar

SpringBoot构建电商秒杀项目(二) 用户模块开发

第二章

2.1 项目整体框架

本项目采用springBoot开发,项目架构是Spring MVC框架
2_01
controller层:与前端页面UI交互的层
viewobject层:视图,封装了向前端展示的数据,避免领域模型的某些敏感数据直接传给前端
dao层:与底层数据库交互的层,进行数据库的增删改查
dataobject层:数据库表对于Java实体类的映射,是数据库表中数据的封装
model层:领域模型,将不同的表但属于一个领域的字段的整合
service层:处于上层controller与下层dao层的中间层,处理各种逻辑业务和服务请求
error层:统一错误的格式,拦截tomcat不能处理的各类异常与错误
response层:返回给前端统一的格式(response+data)

2.2 定义通用的返回对象——返回正确信息

考虑到我们的程序如果出错,返回给前端的缺少错误信息,用户体验不好,所以我们定义一个CommonReturnType类用status+data的格式来处理json序列化,避免使用HttpStatusCode和内嵌的tomcat error页

创建CommonReturnType类

在项目目录新建一个response文件夹,创建CommonReturnType类,返回的类是status(请求处理结果)+data(错误信息)格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CommonReturnType {

//表明对应请求的返回处理结果“success”或“fail”
private String status;

//若status=success,则data内返回前端需要的json数据
//若status=fail,则data内使用通用的错误码格式
private Object data;

//定义一个通用的创建方法
public static CommonReturnType create(Object result) {
return CommonReturnType.create(result, "success");
}

public static CommonReturnType create(Object result,String status) {
CommonReturnType type = new CommonReturnType();
type.setStatus(status);
type.setData(result);
return type;
}
}

2.3 定义通用的返回对象——返回错误的信息

在项目目录下新建一个error文件,定义标准的错误返回信息(errCode+errMsg格式)

1. 创建CommonError接口

1
2
3
4
5
public interface CommonError {
public int getErrCode();
public String getErrMsg();
public CommonError setErrMsg(String errMsg);
}

2. 创建EmBusinessError实现类

EmBusinessError类称为包装器业务异常类,为枚举类,也有自己的成员对象和成员函数。定义通用的错误码为10001,10002未知错误等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public enum EmBusinessError implements CommonError{
//通用错误类型10001
//很多不合法的情况可设置统一错误码
PARAMETER_VALIDATION_ERROR(10001,"参数不合法"),

UNKNOW_ERROR(10002,"未知错误"),

//20000开头为用户信息相关错误定义
USER_NOT_EXIST(20001,"用户不存在"),
USER_LOGIN_FAIL(20002,"用户手机号或密码不正确"),
USER_NOT_LOGIN(20003,"用户还未登陆"),
//30000开头为交易信息错误
STOCK_NOT_ENOUGH(30001,"库存不足")
;

private int errCode;
private String errMsg;

private EmBusinessError(int errCode,String errMsg) {
this.errCode = errCode;
this.errMsg = errMsg;
}
@Override
public int getErrCode() {
return errCode;
}

@Override
public String getErrMsg() {
return errMsg;
}

@Override
public CommonError setErrMsg(String errMsg) {
this.errMsg = errMsg;
return this;
}
}

3. 包装器业务异常类实现

包装器业务异常类不仅可以直接接收EmBusinessError的传参用于构造业务异常,也可以接收自定义errMsg的方式构造业务异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class BusinessException extends Exception implements CommonError{

//commonError其实就是EmBusinessError类
private CommonError commonError;

//构造函数
//直接接收EmBusinessError的传参用于构造业务异常
public BusinessException(CommonError commonError) {
//调用父类初始化
super();
this.commonError = commonError;
}

//接收自定义errMsg的方式构造业务异常
public BusinessException(CommonError commonError,String errMsg){
super();
this.commonError = commonError;
this.commonError.setErrMsg(errMsg);
}
@Override
public int getErrCode() {
return this.commonError.getErrCode();
}

@Override
public String getErrMsg() {
return this.commonError.getErrMsg();
}

@Override
public CommonError setErrMsg(String errMsg) {
this.commonError.setErrMsg(errMsg);
return this;
}
}

2.4 定义通用的返回对象——异常处理

定义ExceptionHandler

定义exceptionHandler解决未被controller层吸收的exception

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BaseController {

//定义exceptionHandler解决未被controller层吸收的exception
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Object handlerException(HttpServletRequest request, Exception ex) {
Map<String, Object> responseData = new HashMap<>();
if (ex instanceof BusinessException) {
BusinessException businessException = (BusinessException) ex;
responseData.put("errCode", businessException.getErrCode());
responseData.put("errMsg", businessException.getErrMsg());
} else {
responseData.put("errCode", EmBusinessError.UNKNOWN_ERROR.getErrCode());
responseData.put("errMsg", EmBusinessError.UNKNOWN_ERROR.getErrMsg());
}
return CommonReturnType.create(responseData, "fail");
}

}

2.5 使用SpringMVC方式开发用户信息

1. 创建UserModel模型

用户信息包括用户名name,性别gender,年龄age,手机号telphone,注册码registerMode,第三方ID thirdPartId,加密密码encrptPassword

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class UserModel {
private Integer id;

//引入NotBlank注释,不能为空字符串或NULL,否则报错提示
@NotBlank(message = "用户名不能为空")
private String name;

@NotNull(message = "性别不能不填写")
private Byte gender;

@NotNull(message = "年龄不能不填写")
@Min(value = 0,message = "年龄必须大于0")
@Max(value = 150,message = "年龄必须小于150")
private int age;

@NotBlank(message = "手机号不能为空")
private String telphone;

@NotBlank(message = "密码不能为空")
private String registerMode;
private String thirdPartId;

private String encrptPassword;
}

2.创建UserController层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RequestMapping("/get")
@ResponseBody
public CommonReturnType getUser(@RequestParam(name = "id") Integer id) throws BusinessException {
//调用service服务获取对应Id的用户对象并返回给前端
UserModel userModel = userService.getUserById(id);

//若获取的对应用户信息不存在
if(userModel == null) {
throw new BusinessException(EmBusinessError.USER_NOT_EXIST);
//userModel.setEncrptPassword("123");
}

//将核心领域模型用户对象转化成可供UI使用的viewobject对象
UserVO userVO = convertFromModel(userModel);

//返回通用对象
return CommonReturnType.create(userVO);
}
private UserVO convertFromModel (UserModel userModel) {
if(userModel == null) {
return null;
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userModel,userVO);
return userVO;
}

通过前端传来的id,在userService中查询userModel对象,然后由于userModel中包含有用户的密码,不能直接传递给前端,所以要定义ViewObject类UserVO

1
2
3
4
5
6
7
8
public class UserVO {
private Integer id;
private String name;
private Byte gender;
private int age;
private String telphone;
private String thirdPartId;
}

3. 创建UserService类

UserService接口

1
UserModel getUserById(Integer id);

UserServiceImpl实现类
UserServiceImpl实现类首先通过用户id查找对应用户的userDO,然后在userPasswordDOMapper中查询对应用户的加密密码信息,最后将userDOuserPasswordDO整合在一起实现converFromDataObject函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public UserModel getUserById(Integer id) {
//调用userdomapper获取到对应的用户dataobject
UserDO userDO = userDOMapper.selectByPrimaryKey(id);
if(userDO == null) {
return null;
}
//通过用户id获取对应用户的加密密码信息
UserPasswordDO userPasswordDO = userPasswordDOMapper.selectByUserId(userDO.getId());
return convertFromDataObject(userDO,userPasswordDO);
}
//userDO+userPassword 组装DataObject->UserModel
private UserModel convertFromDataObject(UserDO userDo, UserPasswordDO userPasswordDO) {

if(userDo == null) {
return null;
}

UserModel userModel = new UserModel();
//copyProperties 将userDo赋值给userModel
BeanUtils.copyProperties(userDo,userModel);
if(userPasswordDO!=null) {
userModel.setEncrptPassword(userPasswordDO.getEncriptPassword());
}
return userModel;
}

2.6 用户模型管理——otp验证码获取

1. 创建getotp方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class UserController extends BaseController{

@Autowired
private UserService userService;

@Autowired
private HttpServletRequest httpServletRequest;

//用户获取otp短信接口
@RequestMapping("/getotp")
@ResponseBody
public CommonReturnType getOtp(@RequestParam(name = "telphone") String telphone) {
//需要按照一定的规则生成OTP验证码
Random random = new Random();
int randomInt = random.nextInt(99999);
randomInt += 10000;
String otpCode = String.valueOf(randomInt);

//将OTP验证码同对应用户的手机号关联,使用httpsession的方式绑定手机号与OTPCDOE
httpServletRequest.getSession().setAttribute(telphone, otpCode);

//将OTP验证码通过短信通道发送给用户,省略
System.out.println("telphone=" + telphone + "&otpCode=" + otpCode);

return CommonReturnType.create(null);
}

2. 引入Metronic模板

新建static文件夹保存模板文件,实现前端getotp.html文件,使用jQuary与Ajax实现与后端异步通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<html>
<head>
<meta charset="UTF-8">
<script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script>
<title>Title</title>
</head>
<body>
<div>
<h3>获取otp信息</h3>
<div>
<label>手机号</label>
<div>
<input type="text" placeholder="手机号" name="telphone" id="telphone"/>
</div>
</div>
<div>
<button id="getotp" type="submit">
获取otp短信
</button>
</div>
</div>

</body>

<script>
jQuery(document).ready(function () {

//绑定otp的click事件用于向后端发送获取手机验证码的请求
$("#getotp").on("click",function () {

var telphone=$("#telphone").val();
if (telphone==null || telphone=="") {
alert("手机号不能为空");
return false;
}


//映射到后端@RequestMapping(value = "/getotp", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
$.ajax({
type:"POST",
contentType:"application/x-www-form-urlencoded",
url:"http://localhost:8080/user/getotp",
data:{
"telphone":$("#telphone").val(),
},
success:function (data) {
if (data.status=="success") {
alert("otp已经发送到了您的手机,请注意查收");
}else {
alert("otp发送失败,原因为" + data.data.errMsg);
}
},
error:function (data) {
alert("otp发送失败,原因为"+data.responseText);
}
});
});
});
</script>
</html>

进行测试,测试controller层getotp方法,但是发送失败,出现以下错误:

1
getotp.html?_ijt=cqdae6hmhq9069c9s4muooakju:1 Access to XMLHttpRequest at 'http://localhost:8080/user/getotp' from origin 'http://localhost:63342' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

经过分析发现是出现了跨域请求错误
解决办法是在Controller层上加入@CrossOrigin注解

2.7 用户模型管理——注册功能实现

1.Controller层部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//用户注册接口
@RequestMapping(value = "/register",method = {RequestMethod.POST},consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType register(@RequestParam(name="telphone") String telphone,
@RequestParam(name="otpCode") String otpCode,
@RequestParam(name="name") String name,
@RequestParam(name="gender")Byte gender,
@RequestParam(name="age") Integer age,
@RequestParam(name="password") String password) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
//验证手机号和对应的otpcode相符合
String inSessionOptCode = (String) this.httpServletRequest.getSession().getAttribute(telphone);
if(!StringUtils.equals(inSessionOptCode,otpCode)){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"短信验证码不符合");
}
//用户的注册流程
UserModel userModel = new UserModel();

userModel.setName(name);
userModel.setAge(age);
userModel.setGender(gender);
userModel.setTelphone(telphone);
userModel.setRegisterMode("byphone");
userModel.setEncrptPassword(this.EncodeByMd5(password));

userService.register(userModel);
return CommonReturnType.create(null);
}

//JDK MD5方式实现只支持16位,所以修改为64位
public String EncodeByMd5(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
//确定计算方法
MessageDigest md5 = MessageDigest.getInstance("MD5");
BASE64Encoder base64Encoder = new BASE64Encoder();
//加密字符串
String newstr = base64Encoder.encode(md5.digest(str.getBytes("utf-8")));
return newstr;
}

引入做输入校验的依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>

2. Service层部分

UserService接口

1
void register(UserModel userModel) throws BusinessException;

UserServiceImpl实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
@Transactional
//@Transactional保证UserDO和UserPasswordDo在一个事务
public void register(UserModel userModel) throws BusinessException {
if(userModel == null) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}

//使用Validate来校验
ValidationResult result = validator.validate(userModel);
if(result.isHasErrors()) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,result.getErrMsg());

//实现model转换成dataObject
UserDO userDO = convertFromModel(userModel);
try{
userDOMapper.insertSelective(userDO);
}catch (DuplicateKeyException ex) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"手机号已重复注册");
}
userModel.setId(userDO.getId());
UserPasswordDO userPasswordDO = convertPasswordFromModel(userModel);
userPasswordDOMapper.insertSelective(userPasswordDO);
//使用insertSelective而不使用insert的原因:
//因为insertSelective是当某一字段不为空时插入数据,为null不改变;而insert操作可能会造成数据覆盖成null
return ;
}

注意的有以下方面:

  • 数据库增的请求不用insert而用insertSelective,因为insertSelective允许输入为空
  • 跨域请求要加上@CrossOrigin(allowCredentials = “true”,allowedHeaders = “*”) 实现session共享
  • 为了保证数据库手机号的唯一性,还要在数据库添加UNIQUE索引

3. 前端页面

在getotp页面添加注册成功的跳转页面

1
2
3
4
5
6
7
8
success:function (data) {
if (data.status=="success") {
alert("otp已经发送到了您的手机,请注意查收");
window.location.href="register.html";
}else {
alert("otp发送失败,原因为" + data.data.errMsg);
}
},

2.8 用户模型管理——登陆功能实现

1. Controller层部分

用户登陆的主要流程包括:

  • 入参校验
  • 校验用户登陆是否合法
  • 将登陆凭证加入用户登陆成功的session中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//用户登录接口
@RequestMapping(value = "/login",method = {RequestMethod.POST},consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType login(@RequestParam(name = "telphone")String telphone,
@RequestParam(name = "password")String password) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
//入参校验
if(org.apache.commons.lang3.StringUtils.isEmpty(telphone)||
StringUtils.isEmpty(password)){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}
//用户登录服务,用来校验用户登录是否合法
UserModel userModel = userService.validateLogin(telphone,this.EncodeByMd5(password));

//将登录凭证加入到用户登陆成功的session内
this.httpServletRequest.getSession().setAttribute("IS_LOGIN",true);
this.httpServletRequest.getSession().setAttribute("LOGIN_USER",userModel);

return CommonReturnType.create(null);
}

2. Service部分

Service接口

1
UserModel validateLogin(String telphone,String encrptPassword) throws BusinessException;

ServiceImpl实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public UserModel validateLogin(String telphone, String encrptPassword) throws BusinessException {
//通过用户的手机获取用户信息
UserDO userDO = userDOMapper.selectByTelphone(telphone);
if(userDO == null) {
throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL);
}
UserPasswordDO userPasswordDO = userPasswordDOMapper.selectByUserId(userDO.getId());

UserModel userModel = convertFromDataObject(userDO,userPasswordDO);

//对比用户信息内加密的密码是否和传输进来的密码相匹配
if(!StringUtils.equals(encrptPassword,userPasswordDO.getEncriptPassword())) {
throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL);
}
return userModel;
}

2.9 优化校验规则

1. 引入hibernate库

引入hibernate库来实现validator方法

1
2
3
4
5
6
<!--校验-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.4.Final</version>
</dependency>

新建validator文件夹目录

2. 新建ValidationResult类返回校验结果集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ValidationResult {
//校验结果是否有错
private boolean hasErrors = false;

//存放错误信息的map
private Map<String, String> errorMsgMap = new HashMap<>();

public boolean isHasErrors() {
return hasErrors;
}

public void setHasErrors(boolean hasErrors) {
this.hasErrors = hasErrors;
}

public Map<String, String> getErrorMsgMap() {
return errorMsgMap;
}

public void setErrorMsgMap(Map<String, String> errorMsgMap) {
this.errorMsgMap = errorMsgMap;
}

//实现通用的通过格式化字符串信息获取错误结果的msg方法
public String getErrMsg() {
return StringUtils.join(errorMsgMap.values().toArray(), ",");
}
}

3. 新建ValidatorImpl实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
public class ValidatorImpl implements InitializingBean {

private Validator validator;

//实现校验方法并返回校验结果
public ValidationResult validate(Object bean) {
final ValidationResult result = new ValidationResult();
Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(bean);
if (constraintViolationSet.size() > 0) {
//有错误
result.setHasErrors(true);
constraintViolationSet.forEach(constraintViolation ->{
String errMsg = constraintViolation.getMessage();
String propertyName = constraintViolation.getPropertyPath().toString();
result.getErrorMsgMap().put(propertyName, errMsg);
});
}
return result;
}

@Override
public void afterPropertiesSet() throws Exception {
//将hibernate validator通过工厂的初始化方式使其实例化
this.validator = Validation.buildDefaultValidatorFactory().getValidator();
}
}

至此以后校验模型model属性时加上相应的注解即可

文章作者: SkironYong
文章链接: https://skironyong.github.io/SkironYong.github.io/posts/a3a191c9.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SkironYong
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论