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(错误信息)格式

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接口

public interface CommonError {
    public int getErrCode();
    public String getErrMsg();
    public CommonError setErrMsg(String errMsg);
}

2. 创建EmBusinessError实现类

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

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的方式构造业务异常

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

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

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层

@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

public class UserVO {
    private Integer id;
    private String name;
    private Byte gender;
    private int age;
    private String telphone;
    private String thirdPartId;
}

3. 创建UserService类

UserService接口

UserModel getUserById(Integer id);

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

@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方法

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实现与后端异步通信

<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方法,但是发送失败,出现以下错误:

 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层部分

   //用户注册接口
    @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;
    }

引入做输入校验的依赖

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.7</version>
</dependency>

2. Service层部分

UserService接口

void register(UserModel userModel) throws BusinessException;

UserServiceImpl实现

@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页面添加注册成功的跳转页面

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中
 //用户登录接口
    @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接口

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

ServiceImpl实现类

@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方法

<!--校验-->
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>5.2.4.Final</version>
</dependency>

新建validator文件夹目录

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

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实现类

@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属性时加上相应的注解即可


【系列笔记已经更新完毕】

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值