网站首页 > 技术文章 正文
17.1. 开发流程
项目开发流程大致是:
- - 先整理出当前项目涉及的数据的类型
- - 例如:电商类包含用户、商品、购物车、订单等
- - 再列举各种数据类型涉及的数据操作
- - 例如:用户类型涉及注册、登录等
- - 再挑选相对简单的数据类型先处理
- - 简单的易于实现,且可以积累经验
- - 在各数据类型涉及的数据操作中,大致遵循增、查、删、改的开发顺序
- - 只有先增,还可能查、删、改
- - 只有查了以后,才能明确有哪些数据,才便于实现删、改
- - 删和改相比,删一般更加简单,所以先开发删,再开发改
- - 在开发具体的数据操作时,应该大致遵循持久层 >> 业务逻辑层 >> 控制器层 >> 前端页面的开发顺序
17.2. 管理员登录-持久层
17.2.1. 创建或配置
如果是整个项目第1次开发持久层,在Spring Boot项目中,需要配置:
- 使用`@MapperScan`配置接口所在的根包
- 在配置文件中通过`mybatis.mapper-locations`配置XML文件的位置
如果第1次处理某种类型数据的持久层访问,需要:
- - 创建接口
- - 创建XML文件
本次需要开发的“管理员登录”并不需要再做以上操作
17.2.2. 规划需要执行的SQL语句
需要执行的SQL语句大致是:
//sql语句
select * from ams_admin where username=?
由于在`ams_admin`表中有大量字段,同时,不允许使用星号表示字段列表,则以上SQL语句应该细化为:
//不允许* 则罗列出来
select id, username, password, nickname, avatar, is_enable
from
ams_admin where username=?
【提示】理论上,还应该查出`login_count`,当登录成功后,还应该更新`login_count`、`gmt_last_login`等数据,此次暂不考虑。
17.2.3. 在接口中添加抽象方法(含创建必要的VO类)
【提示】所有的查询结果,都应该使用VO类,而不要使用实体类,根据阿里的开发规范,每张数据表中都应该有`id`、`gmt_create`、`gmt_modified`这3个字段,而`gmt_create`、`gmt_modified`这2个字段都是用于特殊情况下排查问题的,一般情况下均不会使用,所以,如果使用实体类,必然存在多余的属性,同时,由于不使用星号作为字段列表,则一般也不会查询这2个字段的值,会导致实体类对象中永远至少存在2个属性为`null`。
根据以上提示,以前已经写好的`getByUsername()`是不规范的,应该调整已存在此方法,本次并不需要添加新的抽象方法。
//pojo.vo.AdminSimpleVO`类
@Data
public class AdminSimpleVO implements Serializable {
private Long id;
private String username;
private String password;
private String nickname;
private String avatar;
private Integer isEnable;
}
然后,在`AdminMapper`接口文件中,将原有的`Admin getByUsername(String username);`改为:
//接口
AdminSimpleVO getByUsername(String username);
17.2.4. 在XML中配置SQL
在`AdminMapper.xml`中,需要调整:
<select id="getByUsername" resultMap="BaseResultMap">
select
<include refid="BaseQueryFields" />
from
ams_admin
where
username=#{username}
</select>
<sql id="BaseQueryFields">
<if test="true">
id,
username,
password,
nickname,
avatar,
is_enable
</if>
</sql>
<resultMap id="BaseResultMap" type="cn.demo.pojo.vo.AdminSimpleVO">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="nickname" property="nickname" />
<result column="avatar" property="avatar" />
<result column="is_enable" property="isEnable" />
</resultMap>
17.2.5. 编写并执行测试
此次并不需要编写新的测试,使用原有的测试即可!
注意:由于本次是修改了原“增加管理员”就已经使用的功能,应该检查原功能是否可以正常运行。
17.3. 管理员登录-业务逻辑层
17.3.1. 创建
第1次处理某种类型数据的业务逻辑层访问,需要:
- - 创建接口
- - 创建类,实现接口,并在类上添加`@Service`注解
本次需要开发的“管理员登录”并不需要再做以上操作
17.3.2. 在接口中添加抽象方法(含创建必要的DTO类)
在设计抽象方法时,如果参数的数量超过1个,且多个参数具有相关性(是否都是客户端提交的,或是否都是控制器传递过来的等),就应该封装!
//需要客户端提交用户名和密码,则可以将用户名、密码封装起来
@Data
public class AdminLoginDTO implements Serializable {
private String username;
private String password;
}
在`IAdminService`中添加抽象方法:
//接口
AdminSimpleVO login(AdminLoginDTO adminLoginDTO);
17.3.3. 在实现类中设计(打草稿)业务流程与业务逻辑(含创建必要的异常类)
此次业务执行过程中,可能会出现:
- - 用户名不存在,导致无法登录
- - 用户状态为【禁用】,导致无法登录
- - 密码错误,导致无法登录
关于用户名不存在的问题,可以自行创建新的异常类,例如,在`cn.celinf.boot.demo.ex`包下创建`UserNotFoundException`类表示用户数据不存在的异常,继承自`ServiceException`,且添加5款基于父类的构造方法:
package cn.celinf.boot.demo.ex;
public class UserNotFoundException extends ServiceException {
// 自动生成5个构造方法
}
再创建`UserStateException`表示用户状态异常:
public class UserStateException extends ServiceException {
// 自动生成5个构造方法
}
再创建`PasswordNotMatchException`表示密码错误异常:
public class PasswordNotMatchException extends ServiceException {
// 自动生成5个构造方法
}
登录过程大致是:
public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) {
// 通过参数得到尝试登录的用户名
// 调用adminMapper.getByUsername()方法查询
// 判断查询结果是否为null
// 是:表示用户名不存在,则抛出UserNotFoundException异常
// 【如果程序可以执行到此步,则可以确定未抛出异常,即查询结果不为null】
// 【以下可视为:存在与用户名匹配的管理员数据】
// 判断查询结果中的isEnable属性值是否不为1
// 是:表示此用户状态是【禁用】的,则抛出UserStateException异常
// 【如果程序可以执行到此步,表示此用户状态是【启用】的】
// 从参数中取出此次登录时客户端提交的密码
// 调用PasswordEncoder对象的matches()方法,对客户端提交的密码和查询结果中的密码进行验证
// 判断以上验证结果
// true:密码正确,视为登录成功
// -- 将查询结果中的password、isEnable设置为null,避免响应到客户端
// -- 返回查询结果
// false:密码错误,视为登录失败,则抛出PasswordNotMatchException异常
}
17.3.4. 在实现类中实现业务
在`AdminServiceImpl`中重写接口中新增的抽象方法:
@Override
public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) {
// 日志
log.debug("即将处理管理员登录的业务,尝试登录的管理员信息:{}", adminLoginDTO);
// 通过参数得到尝试登录的用户名
String username = adminLoginDTO.getUsername();
// 调用adminMapper.getByUsername()方法查询
AdminSimpleVO queryResult = adminMapper.getByUsername(username);
// 判断查询结果是否为null
if (queryResult == null) {
// 是:表示用户名不存在,则抛出UserNotFoundException异常
log.warn("登录失败,用户名不存在!");
throw new UserNotFoundException("登录失败,用户名不存在!");
}
// 【如果程序可以执行到此步,则可以确定未抛出异常,即查询结果不为null】
// 【以下可视为:存在与用户名匹配的管理员数据】
// 判断查询结果中的isEnable属性值是否不为1
if (queryResult.getIsEnable() != 1) {
// 是:表示此用户状态是【禁用】的,则抛出UserStateException异常
log.warn("登录失败,此账号已经被禁用!");
throw new UserNotFoundException("登录失败,此账号已经被禁用!");
}
// 【如果程序可以执行到此步,表示此用户状态是【启用】的】
// 从参数中取出此次登录时客户端提交的密码
String rawPassword = adminLoginDTO.getPassword();
// 调用PasswordEncoder对象的matches()方法,对客户端提交的密码和查询结果中的密码进行验证
boolean matchResult = passwordEncoder.matches(rawPassword, queryResult.getPassword());
// 判断以上验证结果
if (!matchResult) {
// false:密码错误,视为登录失败,则抛出PasswordNotMatchException异常
log.warn("登录失败,密码错误!");
throw new PasswordNotMatchException("登录失败,密码错误!");
}
// 密码正确,视为登录成功
// 将查询结果中的password、isEnable设置为null,避免响应到客户端
queryResult.setPassword(null);
queryResult.setIsEnable(null);
// 返回查询结果
log.debug("登录成功,即将返回:{}", queryResult);
return queryResult;
}
17.3.5. 编写并执行测试
在`AdminServiceTests`中添加测试:
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginSuccessfully() {
// 测试数据
String username = "admin001";
String password = "123456";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断言不会抛出异常
assertDoesNotThrow(() -> {
// 执行测试
AdminSimpleVO adminSimpleVO = service.login(adminLoginDTO);
log.debug("登录成功:{}", adminSimpleVO);
// 断言测试结果
assertEquals(1L, adminSimpleVO.getId());
assertNull(adminSimpleVO.getPassword());
assertNull(adminSimpleVO.getIsEnable());
});
}
@Sql({"classpath:truncate.sql"})
@Test
public void testLoginFailBecauseUserNotFound() {
// 测试数据
String username = "admin001";
String password = "123456";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断言会抛出UserNotFoundException
assertThrows(UserNotFoundException.class, () -> {
// 执行测试
service.login(adminLoginDTO);
});
}
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginFailBecauseUserDisabled() {
// 测试数据
String username = "admin005"; // 通过SQL脚本插入的此数据,is_enable为0
String password = "123456";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断言会抛出UserStateException
assertThrows(UserStateException.class, () -> {
// 执行测试
service.login(adminLoginDTO);
});
}
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginFailBecausePasswordNotMatch() {
// 测试数据
String username = "admin001";
String password = "000000000000000000";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断言会抛出PasswordNotMatchException
assertThrows(PasswordNotMatchException.class, () -> {
// 执行测试
service.login(adminLoginDTO);
});
}
17.4. 管理员登录-控制器层
17.4.1. 创建
如果是整个项目第1次开发控制器层,需要:
- - 创建统一处理异常的类
- - 添加`@RestControllerAdvice`
- - 创建统一的响应结果类型及相关类型
- 例如:`JsonResult`及`State`
如果第1次处理某种类型数据的控制器层访问,需要:
- - 创建控制器类
- - 添加`@RestController`
- - 添加`@RequestMapping`
本次需要开发的“管理员登录”并不需要再做以上操作
17.4.2. 添加处理请求的方法,验证请求参数的基本有效性
在`AdminLoginDTO`的各属性上添加验证基本有效性的注解,例如:
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
public class AdminLoginDTO implements Serializable {
@NotNull(message = "登录失败,请提交用户名!") // 新增
private String username;
@NotNull(message = "登录失败,请提交密码!") // 新增
private String password;
}
在`AdminController`中添加处理请求的方法:
@RequestMapping("/login") // 暂时使用@RequestMapping,后续改成@PostMapping
public JsonResult<AdminSimpleVO> login(@Validated AdminLoginDTO adminLoginDTO) {
AdminSimpleVO adminSimpleVO = adminService.login(adminLoginDTO);
return JsonResult.ok(adminSimpleVO);
}
17.4.3. 处理异常(按需)
先在`State`中添加新创建的异常对应枚举:
public enum State {
OK(200),
ERR_USERNAME(201),
ERR_PASSWORD(202),
ERR_STATE(203), // 新增
ERR_BAD_REQUEST(400),
ERR_INSERT(500);
// ===== 原有其它代码 =====
}
在`GlobalExceptionHandler`的`handleServiceException()`方法中添加更多分支,针对各异常进行判断,并响应不同结果:
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException e) {
if (e instanceof UsernameDuplicateException) {
return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
} else if (e instanceof UserNotFoundException) { // 从此行起,是新增的
return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
} else if (e instanceof UserStateException) {
return JsonResult.fail(State.ERR_STATE, e.getMessage());
} else if (e instanceof PasswordNotMatchException) {
return JsonResult.fail(State.ERR_PASSWORD, e.getMessage()); // 新增结束标记
} else {
return JsonResult.fail(State.ERR_INSERT, e.getMessage());
}
}
17.4. 管理员登录-前端页面
<template>
<div class="home">
<h1>添加管理员</h1>
<el-row type="flex" justify="center">
<el-col :span="8">
<h2>注册</h2>
<el-divider></el-divider>
<el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="手机" prop="phone">
<el-input v-model="ruleForm.phone"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="ruleForm.email"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="ruleForm.nickname"></el-input>
</el-form-item>
<el-form-item label="个人简介" prop="description">
<el-input v-model="ruleForm.description"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
data() {
var checkUsername = (rule, value, callback) => {
if (!value) {
return callback(new Error('用户名不能为空'));
}
callback();
};
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
}
callback();
};
return {
ruleForm: {
password: '',
username: '',
nickname: '',
phone: '',
email: '',
description: ''
},
rules: {
password: [
{ validator: validatePass, trigger: 'blur' }
],
username: [
{ validator: checkUsername, trigger: 'blur' }
]
}
};
}
}
</script>
控制器层的测试
关于控制器层,也可以写测试方式进行测试,在Spring Boot项目中,可以使用`MockMvc`进行模拟测试,例如:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@SpringBootTest
@AutoConfigureMockMvc // 自动配置MockMvc
public class AdminControllerTests {
@Autowired
MockMvc mockMvc; // Mock:模拟
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginSuccessfully() throws Exception {
// 准备测试数据,不需要封装
String username = "admin001";
String password = "123456";
// 请求路径,不需要写协议、服务器主机和端口号
String url = "/admins/login";
// 执行测试
// 以下代码相对比较固定
mockMvc.perform( // 执行发出请求
MockMvcRequestBuilders.post(url) // 根据请求方式决定调用的方法
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("username", username) // 请求参数,有多个时,多次调用param()方法
.param("password", password)
.accept(MediaType.APPLICATION_JSON)) // 接收的响应结果的文档类型
.andExpect( // 预判结果,类似断言
MockMvcResultMatchers
.jsonPath("state") // 预判响应的JSON结果中将有名为state的属性
.value(200)) // 预判响应的JSON结果中名为state的属性的值
.andDo( // 需要执行某任务
MockMvcResultHandlers.print()); // 打印日志
}
}
执行以上测试时,并不需要启动当前项目即可测试。
在执行以上测试时,响应的JSON中如果包含中文,可能会出现乱码,需要在配置文件(`application.properties`或`application.yml`这类文件)中添加配置。
//在`.properties`文件中:
server.servlet.encoding.force=true
server.servlet.encoding.charset=utf-8
//在`.yml`文件中:
server:
servlet:
encoding:
force: true
charset: utf-8
学习记录,如有侵权请联系删除
- 上一篇: 前端权限控制;接口权限;路由权限;按钮权限
- 下一篇: 以“用户登录”测试谈用例编写
猜你喜欢
- 2024-11-24 测试开发基础,教你做一个完整功能的Web平台之登录认证
- 2024-11-24 iframe嵌入页面实现免登录
- 2024-11-24 用Java三层架构写一个登录案例
- 2024-11-24 带你掌握springboot集成SpringSecurity认证授权
- 2024-11-24 .NET云原生应用实践(五):使用Blazor WebAssembly实现前端页面
- 2024-11-24 我觉得再没有登录页卷的啦,简单需求非要惊艳效果
- 2024-11-24 一步步使用SpringBoot实现登录和用户管理功能源码分享
- 2024-11-24 登录页漂亮不漂亮,来走两步。
- 2024-11-24 再来一波登录页,就这个样被征服了。
- 2024-11-24 自定义 WordPress 登录以回复链接及文字
你 发表评论:
欢迎- 07-10Oracle 与 Google Cloud 携手大幅扩展多云服务
- 07-10分享收藏的 oracle 11.2.0.4各平台的下载地址
- 07-10Oracle 和 Microsoft 推出 Oracle Exadata 数据库服务
- 07-10Oracle Database@Azure 推进到南美等新区域并增加了新服务
- 07-10Oracle宣布推出 Oracle Database@AWS 的有限预览版
- 07-10Oracle与Nextcloud合作,推出主权云上的安全协作平台
- 07-10NodeRED魔改版连接MsSql、PostgreSQL、MySQL、OracleDB存储无忧
- 07-10对于企业数据云备份,“多备份”承诺的是成本更低,管理更高效#36氪开放日深圳站#
- 601℃几个Oracle空值处理函数 oracle处理null值的函数
- 593℃Oracle分析函数之Lag和Lead()使用
- 581℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 578℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 573℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 566℃【数据统计分析】详解Oracle分组函数之CUBE
- 552℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 547℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
-
- Oracle 与 Google Cloud 携手大幅扩展多云服务
- 分享收藏的 oracle 11.2.0.4各平台的下载地址
- Oracle 和 Microsoft 推出 Oracle Exadata 数据库服务
- Oracle Database@Azure 推进到南美等新区域并增加了新服务
- Oracle宣布推出 Oracle Database@AWS 的有限预览版
- Oracle与Nextcloud合作,推出主权云上的安全协作平台
- NodeRED魔改版连接MsSql、PostgreSQL、MySQL、OracleDB存储无忧
- 对于企业数据云备份,“多备份”承诺的是成本更低,管理更高效#36氪开放日深圳站#
- 解读丨《归档文件整理规则》— 电子文件元数据存储
- Data Guard跳归档恢复的实践(dataguard failover)
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端路由 (61)
- 前端数组 (73)
- 前端js面试题 (50)
- 前端定时器 (59)
- 前端获取当前时间 (50)
- Oracle RAC (76)
- oracle恢复 (77)
- oracle 删除表 (52)
- oracle 用户名 (80)
- oracle 工具 (55)
- oracle 内存 (55)
- oracle 导出表 (62)
- oracle约束 (54)
- oracle 中文 (51)
- oracle链接 (54)
- oracle的函数 (57)
- 前端调试 (52)
本文暂时没有评论,来添加一个吧(●'◡'●)