目录
UserController 中添加简单处理请求的方法,并测试是否可以访问URL:
稻草人项目
项目的数据处理流程 - - -了解项目的数据处理流程
在项目中,么偶次处理用户提交的请求时,用户请求数据的走向应该是
用户界面---->控制器层---->业务层---->持久层
以上各层的分工如下:
- 用户界面 : 复测显示数据,提供用户操作入口,并提交请求,获取服务器端响应的结果:
- 控制器层 : 负责接收请求,并发出响应结果;
- 业务层 : 负责业务流程和业务逻辑,已保证数据的 安全性 ( 数据必须按照业务所设定的规则而产生或发生变化 ) 和 完整性 ( );
- 持久层 : 负责数据访问及操作,增删改查
在开发项目时,开发顺序应该是:
持久层 ----> 业务层 ----> 控制器层 ----> 用户界面
在开发项目时,开发顺序应该是:
持久层 ----> 业务层 ----> 控制器层 ----> 用户界面
学生注册---持久层
用户注册的本质是性用户数据表中插入数据,然后,为了保护用户名或手机号或某字段唯一,还应该在插入数据之前通过查询进行查询
由于使用了 MyBatisPlus ,常规的数据增删改查 已经完后,可以不必自行开发所需功能!
MyBatisPlus的使用存在一些争议,
主要表现为:方法的调用可能比较麻烦,
例如可能
需要使用到QueryWrapper来封装WHERE子句的条件,另外,执行效率可能略低。
学生注册--业务层
由于存在规则"学生注册时必须填写已知邀请码( 在数据表中有记录 )才可以注册,将可以把学生根据邀请安分配到不同的班级",所以必须先保证:"能够验证学生在注册时填写的邀请码是否正确!"
先从笔记服务器下载class_info 表的SQL 脚本,登录MySQL 控制台,使用straw 数据路,通过命令导入该SQL 脚本:
<
source 脚本文件的路径
>
注意:脚本文件的路径,可以直接将脚本文件拖拽到控制台窗口自动得到,如果得到的路径被添加了引号框住,需要手动删除引号,source
与路径之前必须存在空白,且该命令不要使用分号表示结束。
由于使用了新的数据库,则需要通过straw-generator来生成新数据表在项目中需要使用到的各个文件!直接执行CodeGenerator
,输入表名class_info
,执行完成后,将straw-generator中生成的关于class_info
表的相关文件(4个Java文件夹,1个配置SQL语句的XML文件)都复制到straw-portal子模块项目中,完成后,将straw-generator中刚刚生成的文件删除掉。
在执行 "学生注册"时,可能 出现异常的原因:
- 邀请码错误
- 班级已被禁用
- 手机号码已经被占用
- 插入用户数据失败
在项目找那个,当需要抛出异常时,推荐使用RuntimeException 的子孙类异常,通常,都会自定义异常,关于如何自定义异常
- 自定义 1 个异常,在异常中声明某个属性,该属性的值不同时,就表示不同类型的错误;
- 自定义若干个异常,每 1 种对应 1 种错误:
当前项目将始终使用以上第二种做法,首先,应该创建自定义异常的 基类异常 , 便于表示:"自定义业务异常"
则在"cn.tedu.portal.service.ex"包中创建
ServiceException :
该异常类需要继承RuntimeException,并添加与父类相同的构造方法:
package cn.tedu.straw.portal.service.ex;
/**
* 业务异常的基类
*/
public class ServiceException extends RuntimeException{
public ServiceException() {
}
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
public ServiceException(Throwable cause) {
super(cause);
}
public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
接下来,创建以上列举的4种错误对应的异常,这4个异常都必须继承自以上创建的ServiceException
:
邀请码错误异常
public class InviteCodeException extends ServiceException{
//构造方法
}
班级已被禁用异常
public class ClassDisabledException extends ServiceException{
//构造方法
}
手机号码已经被占用错误异常
public class PhoneDuplicateException extends ServiceException{
//构造方法
}
插入用户数据失败错误异常
public class InsertException extends ServiceException{
//构造方法
}
在接口中定义业务的抽象方法
当开发业务层时,应该先在接口中定义业务的抽象方法,然后,再在实现类实现该方法.
本次需要开发"注册"功能,声明的抽象方法应该是
<
void registerStudent( User user ,String inviteCode);
>
关于业务方法的返回值类型设计:
关于业务方法的返回值类型设计:
仅以<操作成功为前提来设计返回值类型! >不需要考虑操作失败问题<当操作失败时,都会抛出某种异常的对象!>
当抽象方法已经设计好,就应该在该实现类以上实现抽象方法:
@Autowired
ClassInfoMapper classInfoMapper;
@Autowired
UserMapper userMapper;
public void seristerStudent(User user,String inviteCode){
//[由于当前项目的设计规则是 :"学生账号通过 手机号码注册,登录",必须保证手机号码的唯一]
<
//根据 参数inviteCode 邀请码 调用ClassInfoMapper 对象的selectOne() 方法 查询class_info表
//判断用户是否为空
//是 : 表示没有找到有效的验证码,不允许注册,抛出异常
InviteCodeException
>
//从 以上查询到的班级信息 获取enabled ,判断是否0
//是 :表示该班级已禁用,不允许注册该班级的学生账号,抛出异常
ClassDisabledException
//从参数user中取出手机号码
//调用 UserMapper 对象的 selectOne() 方法 <持久层的方法>,根据手机号码查询学生账号信息
//判断查询结果是否为null
//是 : **找到学生信息,表示手机号码已经被占用,则不允许注册....抛出异常
PhoneDuplicateException
// ---抛出异常,表示手机号码被占用----
// >>没有找到了学生信息,表示手机号码没有被占用,则允许注册....
//^^确保参数 User 中的信息全部是有效的
//--取出User 中的密码, 调用私有的encode( )方法进行加密 进行加密 ,并将加密后的密码封装回到User中
// - classId : 通过验证邀请码时得到的结果
// - createdTime : 当前时间, LocaleDateTime.now()
// - enabled : 1 允许使用
// - locked : 0 ,不锁定
// - type : 0 表示学生
//调用 UserMapper( 持久层的方法 ) 对象的 insert( ) 方法,根据参数user 插入数据,获取返回值
//判断返回值 < 受影响的行数 > 是否不为1
//是 : 受影响的行数不是1 则插入用户数据失败 ,抛出异常
InsertException
if(){
return / throw
}
}
将密码加密的方法
@Autowired
PasswordEncoder passwordEncoder
private String encode(String rawPassword){
String encodePassword = passwordEncoder.encode(rawPassword);
return encodePassword;
}
在编写业务代码时,尽量根据抛出异常"或其它能够使得方法运行结果为标准 if语句判断,以避免出现太多的if 嵌套"
具体代码实现:
package cn.tedu.straw.portal.service.impl;
import cn.tedu.straw.portal.mapper.ClassInfoMapper;
import cn.tedu.straw.portal.mapper.UserMapper;
import cn.tedu.straw.portal.model.ClassInfo;
import cn.tedu.straw.portal.model.User;
import cn.tedu.straw.portal.service.IUserService;
import cn.tedu.straw.portal.service.ex.ClassDisabledException;
import cn.tedu.straw.portal.service.ex.InsertException;
import cn.tedu.straw.portal.service.ex.InviteCodeException;
import cn.tedu.straw.portal.service.ex.PhoneDuplicateException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author tedu.cn
* @since 2020-07-14
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private ClassInfoMapper classInfoMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void registerStudent(User user, String inviteCode) {
// 【由于当前项目设计的规则是“学生账号通过手机号码注册、登录”,必须保证手机号码唯一】
// 调用ClassInfoMapper对象的selectOne()方法,根据参数inviteCode邀请码,查询class_info表
QueryWrapper<ClassInfo> classQueryWrapper = new QueryWrapper<>();
classQueryWrapper.eq("invite_code", inviteCode);
ClassInfo classInfo = classInfoMapper.selectOne(classQueryWrapper);
// 判断查询结果是否为空
if (classInfo == null) {
// 是:表示没有找到有效的邀请码,不允许注册,抛出InviteCodeException
throw new InviteCodeException();
}
// 从以上查询到的班级信息中取出enabled,判断是否为0
if (classInfo.getEnabled() == 0) {
// 是:表示该班级已禁用,不允许注册该班级的学生账号,抛出ClassDisabledException
throw new ClassDisabledException();
}
// 从参数user中取出手机号码
String phone = user.getPhone();
// 调用UserMapper对象的selectOne()方法,根据手机号码查询学生账号信息
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("phone", user.getPhone());
User result = userMapper.selectOne(userQueryWrapper);
// 判断查询结果是否不为null
if (result != null) {
// 是:找到了学生信息,表示手机号码已经被占用,则不允许注册,抛出PhoneDuplicateException
throw new PhoneDuplicateException();
}
// 没有找到学生信息,表示手机号码没有被占用,则允许注册……
// 确保参数user中的数据全部是有效的
// - 取出参数user中的密码,调用私有的encode()方法进行加密,并将加密后的密码封装回到user中
String rawPassword = user.getPassword();
String encodePassword = encode(rawPassword);
user.setPassword(encodePassword);
// - classId:此前验证邀请码时得到的结果
user.setClassId(classInfo.getId());
// - createdTime:当前时间,LocalDateTime.now()
user.setCreatedTime(LocalDateTime.now());
// - enabled:1,允许使用
user.setEnabled(1);
// - locked:0,不锁定
user.setLocked(0);
// - type:0,表示学生
user.setType(0);
// 调用UserMapper对象的insert()方法,根据参数user插入数据,获取返回值
int rows = userMapper.insert(user);
// 判断返回值(受影响的行数)是否不为1
if (rows != 1) {
// 是:受影响的行数不是1,则插入用户数据失败,抛出InsertException
throw new InsertException();
}
}
/**
* 执行密码加密
*
* @param rawPassword 原密码
* @return 根据原密码执行加密得到的密文
*/
private String encode(String rawPassword) {
String encodePassword = passwordEncoder.encode(rawPassword);
return encodePassword;
}
}
完成后,在src/test/java的cn.tedu.straw.portal
包中创建service
子包,并在这个包中创建UserServiceTests
测试类,以测试以上方法:
package cn.tedu.straw.portal.service;
import cn.tedu.straw.portal.model.User;
import cn.tedu.straw.portal.service.ex.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
public class UserServiceTests {
@Autowired
IUserService userService;
@Test
void registerStudent() {
try {
User user = new User();
user.setPhone("13800138002");
user.setNickname("小王同学");
user.setPassword("1234");
String inviteCode = "JSD2003-005803";
userService.registerStudent(user, inviteCode);
log.debug("register student > OK.");
} catch (ServiceException e) {
log.debug("register student > failure.");
log.debug("cause : {}", e.getClass());
}
}
}
操作失败以异常抛出来处理
<
操作失败以异常抛出来处理,
用户名错误异常: UserNotFoundException
密码错误异常 PasswordNotMatchException
通过不同的错误,抛出不同的异常,在一个方法中处理登录失败操作!
User login() throws UserNotFoundException,PasswordNotMatchException;
>
<
try{
User user = login();
//执行成功!
}catch( UserNotFoundException ){
//用户名错误!
}catch( PasswordNotMatchException ){
//密码错误!
}
>
学生注册 - - - 控制器
由于当前项目已经集成了Spring Security框架,默认情况下,当前站点的所有请求都是要求登录后才可以访问的(具体如何设置某些请求可以免登录后面会讲),可以在application.properties中添加临时使用的用户名和密码:
spring.security.user.name=root
spring.security.user.password=$2a$10$tsM03ULkiifEpSCWtQ5Mq.yrLZIPKVr5vHwU1FGjtT9B1vPlswa.C
以上密码值是将1234
作为原密码通过Bcrypt算法进行加密的,当前项目中已经自动装配了BcryptPasswordEncoding
密码加密器,Spring Security框架会自动将用户在页面中输入的密码通过该密码加密器进行加密后再对比,所以,在以上配置文件中配置的是1234
对应的密文。如果需要临时使用其它密码,可以先通过单元测试生成密文,再将密文配置到以上spring.security.user.password
属性中。
UserController 中添加简单处理请求的方法,并测试是否可以访问URL:
// http://localhost:8080/portal/user/student/register?
//inviteCode=JSD2003-111111&nickname=Hello&phone=13800138002
@RequestMapping("/student/register")
public String studentRegister() {
return "studentRegister";
}
JSON数据
在实际处理请求时,最终响应客户端的应该是,JSON数据,为了保证可以响应JSON数据,先在cn.tedu.straw.portal.vo 包中创建" R "类( 表示 Result ) :
package cn.tedu.straw.portal.vo;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain=true)
public class R {
private Integer state;
private String message;
}
然后,调整控制器中处理请求的方法:
package cn.tedu.straw.portal.controller;
import cn.tedu.straw.portal.model.User;
import cn.tedu.straw.portal.service.IUserService;
import cn.tedu.straw.portal.service.ex.ClassDisabledException;
import cn.tedu.straw.portal.service.ex.InsertException;
import cn.tedu.straw.portal.service.ex.InviteCodeException;
import cn.tedu.straw.portal.service.ex.PhoneDuplicateException;
import cn.tedu.straw.portal.vo.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* </p>
*
* @author tedu.cn
* @since 2020-07-14
*/
@RestController
@RequestMapping("/portal/user")
public class UserController {
@Autowired
private IUserService userService;
// http://localhost:8080/portal/user/student/register?inviteCode=JSD2003-111111&nickname=Hello&phone=13800138002&password=1234
@RequestMapping("/student/register")
public R studentRegister(User user, String inviteCode) {
try {
userService.registerStudent(user, inviteCode);
return new R().setState(1).setMessage("注册成功!");
} catch (InviteCodeException e) {
return new R().setState(2).setMessage("注册失败!邀请码错误!");
} catch (ClassDisabledException e) {
return new R().setState(3).setMessage("注册失败!班级已经被禁用!");
} catch (PhoneDuplicateException e) {
return new R().setState(4).setMessage("注册失败! 手机号码已经被注册!");
} catch (InsertException e) {
return new R().setState(5).setMessage("注册失败!服务器忙,请稍后再次尝试!");
}
}
}
完成后,重启项目,打开浏览器,登录后,通过以上URL进行测试。