功能开发流程

1. 开发流程

正常的项目开发流程大致是:

  • 先整理出当前项目涉及的数据的类型
    • 例如:电商类包含用户、商品、购物车、订单等
  • 再列举各种数据类型涉及的数据操作
    • 例如:用户类型涉及注册、登录等
  • 再挑选相对简单的数据类型先处理
    • 简单的易于实现,且可以积累经验
  • 在各数据类型涉及的数据操作中,大致遵循增、查、删、改的开发顺序
    • 只有先增,还可能查、删、改
    • 只有查了以后,才能明确有哪些数据,才便于实现删、改
    • 删和改相比,删一般更加简单,所以先开发删,再开发改
  • 在开发具体的数据操作时,应该大致遵循持久层 >> 业务逻辑层 >> 控制器层 >> 前端页面的开发顺序

2. 管理员登录-持久层

2.1. 创建或配置

如果是整个项目第1次开发持久层,在Spring Boot项目中,需要配置:

  • 使用@MapperScan配置接口所在的根包

  • 在配置文件中通过mybatis.mapper-locations配置XML文件的位置
    如果第1次处理某种类型数据的持久层访问,需要:

  • 创建接口

  • 创建XML文件
    本次需要开发的“管理员登录”并不需要再做以上操作

2.2. 规划需要执行的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_countgmt_last_login等数据,此次暂不考虑。

2.3. 在接口中添加抽象方法(含创建必要的VO类)

提示:所有的查询结果,都应该使用VO类,而不要使用实体类,根据阿里的开发规范,每张数据表中都应该有idgmt_creategmt_modified这3个字段,而gmt_creategmt_modified这2个字段都是用于特殊情况下排查问题的,一般情况下均不会使用,所以,如果使用实体类,必然存在多余的属性,同时,由于不使用星号作为字段列表,则一般也不会查询这2个字段的值,会导致实体类对象中永远至少存在2个属性为null

根据以上提示,以前已经写好的getByUsername()是不规范的,应该调整已存在此方法,本次并不需要添加新的抽象方法。

则先创建cn.tedu.boot.demo.pojo.vo.AdminSimpleVO类,添加此次查询时需要的属性:

package cn.tedu.boot.demo.pojo.vo;

@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);

注意:一旦修改了原有代码,则调用了原方法的代码都会出现错误,包括:

  • 测试
  • 业务逻辑层的实现类
    应该及时修改错误的代码,但是,由于此时还未完成SQL配置,所以,相关代码暂时并不能运行。

2.4. 在XML中配置SQL

AdminMapper.xml中,需要调整:

  • 删除<sql>中不必查询的字段,注意:此处的字段列表最后不要有多余的逗号
  • 修改<resultMap>节点的type属性值
  • <resultMap>节点下,删除不必要的配置
<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.tedu.boot.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>

2.5. 编写并执行测试

此次并不需要编写新的测试,使用原有的测试即可!

注意:由于本次是修改了原“增加管理员”就已经使用的功能,应该检查原功能是否可以正常运行。

3. 管理员登录-业务逻辑层

3.1. 创建

如果第1次处理某种类型数据的业务逻辑层访问,需要:

  • 创建接口
  • 创建类,实现接口,并在类上添加@Service注解
    本次需要开发的“管理员登录”并不需要再做以上操作

3.2. 在接口中添加抽象方法(含创建必要的DTO类)

在设计抽象方法时,如果参数的数量超过1个,且多个参数具有相关性(是否都是客户端提交的,或是否都是控制器传递过来的等),就应该封装!

在处理登录时,需要客户端提交用户名和密码,则可以将用户名、密码封装起来:

package cn.tedu.boot.demo.pojo.dto;

@Data
public class AdminLoginDTO implements Serializable {
    private String username;
    private String password;
}

IAdminService中添加抽象方法:

AdminSimpleVO login(AdminLoginDTO adminLoginDTO);

3.3. 在实现类中设计(打草稿)业务流程与业务逻辑(含创建必要的异常类)

此次业务执行过程中,可能会出现:

  • 用户名不存在,导致无法登录
  • 用户状态为【禁用】,导致无法登录
  • 密码错误,导致无法登录
    关于用户名不存在的问题,可以自行创建新的异常类,例如,在cn.tedu.boot.demo.ex包下创建UserNotFoundException类表示用户数据不存在的异常,继承自ServiceException,且添加5款基于父类的构造方法:
package cn.tedu.boot.demo.ex;

public class UserNotFoundException extends ServiceException {
    // 自动生成5个构造方法
}

再创建UserStateException表示用户状态异常:

package cn.tedu.boot.demo.ex;

public class UserStateException extends ServiceException {
    // 自动生成5个构造方法
}

再创建PasswordNotMatchException表示密码错误异常:

package cn.tedu.boot.demo.ex;

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异常
}

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;
}

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);
    });
}

4. 管理员登录-控制器层

4.1. 创建

如果是整个项目第1次开发控制器层,需要:

  • 创建统一处理异常的类

    • 添加@RestControllerAdvice
  • 创建统一的响应结果类型及相关类型

    • 例如:JsonResultState
      如果第1次处理某种类型数据的控制器层访问,需要:
  • 创建控制器类

    • 添加@RestController
    • 添加@RequestMapping
      本次需要开发的“管理员登录”并不需要再做以上操作

4.2. 添加处理请求的方法,验证请求参数的基本有效性

AdminLoginDTO的各属性上添加验证基本有效性的注解,例如:

package cn.tedu.boot.demo.pojo.dto;

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);
}

4.3. 处理异常(按需)

先在State中添加新创建的异常对应枚举:

public enum State {

    OK(200),
    ERR_USERNAME(201),
    ERR_PASSWORD(202),
    ERR_STATE(203), // 新增
    ERR_BAD_REQUEST(400),
    ERR_INSERT(500);
    
    // ===== 原有其它代码 =====
}

GlobalExceptionHandlerhandleServiceException()方法中添加更多分支,针对各异常进行判断,并响应不同结果:

@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());
    }
}

4.4. 测试

启动项目,暂时通过 http://localhost:8080/admins/login?username=admin001&password=123456 类似的URL测试访问。注意:在测试访问之前,必须保证数据表中的数据状态是符合预期的。

5. 管理员登录-前端页面

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值