目录
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.1Content-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 {@Overridepublic User selectByName(String username) {return null;}@Overridepublic 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 {@Resourceprivate UserMapper userMapper;@Overridepublic 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;}@Overridepublic User createNormalUser(User user) {return null;}
}
接下来我们实现 createNormalUser 方法:
@Slf4j // 日志
@Service // 交给 Spring 管理
public class UserServiceImpl implements IUserService {@Resourceprivate UserMapper userMapper;@Overridepublic 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;}@Overridepublic 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 {@Resourceprivate IUserService userService;@Resourceprivate ObjectMapper objectMapper; // JSON 对象@Testvoid 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));}@Testvoid createNormalUser() {}
}
查询结果如下:
与数据库中的数据相符合,因此测试成功。
接下来添加 createNormalUser 的测试:
@SpringBootTest
class UserServiceImplTest {@Resourceprivate IUserService userService;@Resourceprivate ObjectMapper objectMapper; // JSON 对象@Testvoid 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));}@Testvoid createNormalUser() {// 构造用户User user = new User();user.setUsername("TestUser1");user.setNickname("单元测试用户1");user.setPassword("123456");user.setSalt("123456");// 调用 ServiceuserService.createNormalUser((user));System.out.println("注册成功");System.out.println("========================");user.setUsername("bitboy");// 调用 ServiceuserService.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 类中添加测试方法:
@Testvoid 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 {@Resourceprivate 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. 调⽤ServiceuserService.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'});
停止运行启动类,重新进行注册,可以看到:
接下来,我们正确的进行注册:
可以看到通过前端界面进行注册后,数据库中正确增加了一条数据。
并且注册成功后直接跳转至了登录界面:
用户注册的基本功能已经实现,在下篇文章中,我们将实现用户登录功能。