目录
1. 顺序图
2. 参数要求
3. 接口规范
4. 创建扩展 Mapper.xml
5. 修改 DAO
6. 创建 Service 接口
7. 实现接口
8. 测试接口
9. 实现 Controller
9.1 密码加密处理
10. 实现前端界面
业务实现过程中主要的包和目录及主要功能:
-
model 包:实体对象
-
dao 包:数据库访问
-
services 包:业务处理相关的接口与实现,所有业务都在 Services 中实现
-
controller 包:提供 URL 映射,用来接收参数并做校验, 调用 Service 中的业务代码 ,返回执行结果
-
src/main/resources/mapper 目录:Mybaits 映射文件,配置数据库实体与类之间的映射关系
-
src/main/resources/static 目录:前端资源
那么我们再来看一下各个包之间的调用关系:
- Controller 包:主要用来接收用户的参数,并封装 Service 层需要使用的对象,最终为客户端响应结果
- Service 包:处理业务逻辑,调用 dao 包完成数据的持久化。如果要执行多条数据库更新操作,那么就需要用事务管理
1. 顺序图
注意, 5 应该为根据用户名查询用户信息,即 selectByName(name)。
2. 参数要求
注册时,需要用户提交的参数列表:
参数名 | 描述 | 参数 | 默认值 | 条件 |
---|---|---|---|---|
username | 用户名 | String | 必须 | |
nickname | 昵称 | String | 与用户名相同 | 必须 |
password | 密码 | String | 必须 | |
passwordRepeat | 确认密码 | String | 必须,与密码相同 |
必须 即 需要进行非空校验,还需要进行两次密码输入校验。
3. 接口规范
// 请求
POST /user/register HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=user222&nickname=user222&password=123456&passwordRepeat=123456
// 响应
HTTP/1.1 200
Content-Type: application/json
{"code":0,"message":"成功","data":null}
接下来,就是实现 Service 层,具体通过以下步骤:
1. 在 Mapper.xml 中编写 SQL 语句
2. 在 Mapper.java 中定义方法
3. 定义 Service 接口
4. 实现 Service 接口
5. 单元测试
6. Controller 实现方法并对外提供的 API 接口
7. 测试 API 接口
8. 实现前端逻辑,完成前后端交互
4. 创建扩展 Mapper.xml
写入操作:已经存在(自动生成的)不需要手动编写
根据用户名查询信息:
为了避免在自动生成时重复生成相同的语句,我们在 mapper 包下,新建一个 extension 包,将 UserMapper.xml 文件重新复制一份,并重命名为:UserExtMapper
注意:
因为,命名相同的 xml 文件最终会被解析成一个大文件,里面定义的所有标签都可以相互调用。
接下来,我们先来查看一下此时数据库中的数据:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.UserMapper">
<!--
1. 注意 namespace 表⽰命名空间,要与 UserMapper.xml 中的 namespace 相同
2. 统⼀⽤ com.example.forum.dao.UserMapper, 也就是UserMapper的完全限定名(包名+类名)
3. 不同的映射⽂件指定了相同的namespace后,定义的所有⽤id或name标识的结果集映射都可以在不同的⽂件中共享
-->
<!-- 根据用户名查询用户信息 -->
<select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
from t_user
where username = #{username,jdbcType=VARCHAR}
</select>
</mapper>
可以看到我们用来代替 * 的语句中,使用的标签已经进行了全列的定义:
5. 修改 DAO
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return
*/
User selectByName(String username);
6. 创建 Service 接口
在 demo 包下创建 services 包,在 services 包下创建 IUserService 接口。
接口的命名规则: I + 实现类的名字 + Service
public interface IUserService {
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return
*/
User selectByName(String username);
/**
* 创建普通用户
* @param user 用户名
* @return
*/
User createNormalUser(User user);
}
7. 实现接口
在 services 包下新建 impl 包,在 impl 包下新建类:UserServiceimpl 类,并实现 IUserService 接口:
命名规则:实体类名 + Service + Impl
接下来 Alt + 回车 生成实现方法,并加入注解:
@Slf4j // 日志
@Service // 交给 Spring 管理
public class UserServiceImpl implements IUserService {
@Override
public User selectByName(String username) {
return null;
}
@Override
public User createNormalUser(User user) {
return null;
}
}
在实现的过程中需要对每一个必传参数做非空校验,从而保证程序的健壮性。
可以看到我们需要多次使用到非空校验的判断,因此我们可以将此处的语句抽取出来,放在一个公共的工具类中。在 demo 包下新建包 utils,在 utils 包下新建类 StringUtils:
public class StringUtils {
/**
* 校验指定的字符串是否为空
* @param value 待校验的字符串
* @return true 空 <br/> false 非空
*/
public static boolean isEmpty(String value){
if(value == null || value.isEmpty()){
return true;
}
return false;
}
}
接下来继续完善我们的实现接口部分:
@Slf4j // 日志
@Service // 交给 Spring 管理
public class UserServiceImpl implements IUserService {
@Resource
private UserMapper userMapper;
@Override
public User selectByName(String username) {
// 非空校验
if(StringUtils.isEmpty(username)){
// 打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
// 根据用户名查询用户信息
User user = userMapper.selectByName(username);
// 返回结果
return user;
}
@Override
public User createNormalUser(User user) {
return null;
}
}
接下来我们实现 createNormalUser 方法:
@Slf4j // 日志
@Service // 交给 Spring 管理
public class UserServiceImpl implements IUserService {
@Resource
private UserMapper userMapper;
@Override
public User selectByName(String username) {
// 非空校验
if(StringUtils.isEmpty(username)){
// 打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
// 根据用户名查询用户信息
User user = userMapper.selectByName(username);
// 返回结果
return user;
}
@Override
public void createNormalUser(User user) {
// 非空校验
if(user == null || StringUtils.isEmpty(user.getUsername())
|| StringUtils.isEmpty(user.getNickname())
|| StringUtils.isEmpty(user.getPassword())
|| StringUtils.isEmpty(user.getSalt())){
// 打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
// 校验用户是否存在
User existUser = selectByName(user.getUsername());
if(existUser != null){
// 打印日志
log.warn(ResultCode.FAILED_USER_EXISTS.toString() + "username = " + user.getUsername());
// 抛出异常,用户已存在
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));
}
// 为属性设置默认值
// 性别 0女 1男 2保密
if(user.getGender() != null){
if(user.getGender() < 0 || user.getGender() > 2){
user.setGender((byte)2);
}
}else{
user.setGender((byte)2);
}
// 发帖数
user.setArticleCount(0);
// 是否管理员
user.setIsAdmin((byte)0);
// 状态
user.setState((byte)0);
// 时间
Date date = new Date();
user.setCreateTime(date); // 创建时间
user.setUpdateTime(date); // 更新时间
// 写入数据库
int row = userMapper.insertSelective(user);
if(row != 1){
// 打印日志
log.warn(ResultCode.FAILED_CREATE.toString() + "注册用户失败,username = " + user.getUsername());
// 抛出异常,用户已存在
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
}
}
}
8. 测试接口
在 UserServiceImpl 类中右键生成测试方法:
加上 @SpringBootTest 注解:
@SpringBootTest
class UserServiceImplTest {
@Resource
private IUserService userService;
@Resource
private ObjectMapper objectMapper; // JSON 对象
@Test
void selectByName() throws JsonProcessingException {
// 调用 Service 查询用户信息
User user = userService.selectByName("bitboy");
System.out.println(objectMapper.writeValueAsString(user));
System.out.println("========================================");
user= userService.selectByName("bitboy111");
System.out.println(objectMapper.writeValueAsString(user));
System.out.println("========================================");
user = userService.selectByName(null);
System.out.println(objectMapper.writeValueAsString(user));
}
@Test
void createNormalUser() {
}
}
查询结果如下:
与数据库中的数据相符合,因此测试成功。
接下来添加 createNormalUser 的测试:
@SpringBootTest
class UserServiceImplTest {
@Resource
private IUserService userService;
@Resource
private ObjectMapper objectMapper; // JSON 对象
@Test
void selectByName() throws JsonProcessingException {
// 调用 Service 查询用户信息
User user = userService.selectByName("bitboy");
System.out.println(objectMapper.writeValueAsString(user));
System.out.println("========================================");
user= userService.selectByName("bitboy111");
System.out.println(objectMapper.writeValueAsString(user));
System.out.println("========================================");
// user = userService.selectByName(null);
// System.out.println(objectMapper.writeValueAsString(user));
}
@Test
void createNormalUser() {
// 构造用户
User user = new User();
user.setUsername("TestUser1");
user.setNickname("单元测试用户1");
user.setPassword("123456");
user.setSalt("123456");
// 调用 Service
userService.createNormalUser((user));
System.out.println("注册成功");
System.out.println("========================");
user.setUsername("bitboy");
// 调用 Service
userService.createNormalUser((user));
System.out.println("注册成功");
}
}
运行结果如下图所示:
测试成功。
9. 实现 Controller
在 controller 包下新建 UserController 类:
9.1 密码加密处理
由于在存储用户的密码时,不能直接存储明文,否则会泄露账户信息,因此需要通过盐进行加密处理,最终将盐和密文(明文密码+ 扰动字符串(盐))存储在数据库中。
盐 -> 随机字符
MD5(MD5(明文密码) + SALT)= 密码对应的密文
新建 UUIDUtils 类:
public class UUIDUtils {
/**
* 生成一个标准的36字符的UUID
*
* @return
*/
public static String UUID_36() {
return UUID.randomUUID().toString();
}
/**
* 生成一个32字符的UUID
*
* @return
*/
public static String UUID_32() {
return UUID.randomUUID().toString().replace("-", "");
}
}
在 ForumApplicationTests 类中添加测试方法:
@Test
void testUUID(){
System.out.println(UUIDUtils.UUID_36());
System.out.println(UUIDUtils.UUID_32());
}
结果如下图所示:
在 pom.xml 中添加依赖:
<!-- 编码解码加密⼯具包-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
在 utils 包下新建 MD5Utils 类:
import org.apache.commons.codec.digest.DigestUtils;;
public class MD5Utils {
/**
* 普通MD5加密
* @param str 原始字符串
* @return ⼀次MD5加密后的密⽂
*/
public static String md5 (String str) {
return DigestUtils.md5Hex(str);
}
/**
* 原始字符串与Key组合进⾏⼀次MD5加密
* @param str 原始字符串
* @param key
* @return 组合字符串⼀次MD5加密后的密⽂
*/
public static String md5 (String str, String key) {
return DigestUtils.md5Hex(str + key);
}
/**
* 原始字符串加密后与扰动字符串组合再进⾏⼀次MD5加密
* @param str 原始字符串
* @param salt 扰动字符串
* @return 加密后的密⽂
*/
public static String md5Salt (String str, String salt) {
return DigestUtils.md5Hex(DigestUtils.md5Hex(str) + salt);
}
/**
* 校验原⽂与盐加密后是否与传⼊的密⽂相同
* @param original 原字符串
* @param salt 扰动字符串
* @param ciphertext 密⽂
* @return true 相同, false 不同
*/
public static boolean verifyOriginalAndCiphertext (String original, String salt, String ciphertext) {
String md5text = md5Salt(original, salt);
if (md5text.equalsIgnoreCase(ciphertext)) {
return true;
}
return false;
}
}
继续在 UserController 类中编写代码:
@Api(tags = "用户接口")
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
@ApiOperation("用户注册")
@PostMapping("/register")
public AppResult register(@ApiParam("用户名") @RequestParam("username") @NonNull String username,
@ApiParam("昵称") @RequestParam("nickname") @NonNull String nickname,
@ApiParam("密码") @RequestParam("password") @NonNull String password,
@ApiParam("确认密码") @RequestParam("passwordRepeat") @NonNull String passwordRepeat){
// 校验密码是否一致
if (!password.equals(passwordRepeat)) {
// 返回错误信息
return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);
}
// 构造对象
User user = new User();
user.setUsername(username); // 用户名
user.setNickname(nickname); // 昵称
// 处理密码
// 1.生成盐
String salt = UUIDUtils.UUID_32();
// 2.生成密码的密文
String encryptPassword = MD5Utils.md5Salt(password, salt);
// 3 设置密码和盐
user.setPassword(encryptPassword);
user.setSalt(salt);
// 3. 调⽤Service
userService.createNormalUser(user);
// 4. 返回结果
return AppResult.success("注册成功");
}
}
运行启动类 ForumApplication:
打开:http://127.0.0.1:58080/swagger-ui/index.html
接下来进行密码测试:
测试成功:
继续测试用户名:
测试成功:
继续测试正确的输入:
测试成功:
查看数据库中的数据:
可以看到数据库中显示的密码已经进行了加密处理。
10. 实现前端界面
完成表单校验工作后,需要构造要提交的数据:
- 通过选择器找到需要提交的标签
- 获取标签中的值,并封装成JS对象
- 前后端交互时定义的参数列表:参数名=对象的属性 参数值=标签中的值(用户输入的值)
完整的前端代码可以参考:forum: 论坛项目 - Gitee.com
接下来我们打开注册界面,输入已有的用户名,可以看到提示框弹出:
接下来,我们在 html 文件中,自定义弹出的提示框的样式。
$.toast({
heading: '警告',
text: respData.message,
icon: 'Warning'
});
停止运行启动类,重新进行注册,可以看到:
接下来,我们正确的进行注册:
可以看到通过前端界面进行注册后,数据库中正确增加了一条数据。
并且注册成功后直接跳转至了登录界面:
用户注册的基本功能已经实现,在下篇文章中,我们将实现用户登录功能。