⛰️个人主页:     蒾酒

🔥系列专栏《spring boot实战》


目录

写在前面

上文衔接

常规目录创建

common目录

exception.handle目录

result.handle目录

controller目录

service目录

mapper目录

entity目录

test目录

写在最后


写在前面

本文介绍了springboot开发后端服务,单模块项目工程搭建。单模块搭建出完会出多模块项目搭建。坚持看完相信对你有帮助。

同时欢迎订阅springboot系列专栏,持续分享spring boot的使用经验。

上文衔接

本文衔接上文,可以看一下:

新版idea(2023)创建spring boot3项目_新版idea2023创建springboot3-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/135785412?spm=1001.2014.3001.5501

上文我们已经通过spring官网下载了一个模板,本文继续搭建一个前后端分离架构中后端接口服务单模块工程

单模块结构优缺点

单模块项目将所有代码放在一个 Maven 项目中。它通常适用于小型团队、个人、单一应用程序或简单的项目架构。

优点

  • 简单:项目结构简单,适合初学者和小型团队。
  • 集中管理:所有代码在一个项目中,易于理解和管理。
  • 快速构建:没有模块之间的依赖关系,构建速度更快。

缺点

  • 可扩展性:当项目变大时,代码库可能变得臃肿,管理难度增加。
  • 团队合作:对于大型团队,代码集中可能导致协作上的困难。

常规目录创建

如图:

我们一个一个来讲解吧

common目录

此目录用于存放全局会用到的一些静态常量类、枚举类、业务异常类、工具类、自定义注解、切面类、DTO、VO、配置类等都可以放在该目录下

exception.handle目录

存放全局异常处理类。

感兴趣可以看看

Spring Boot3自定义异常及全局异常捕获_springboot 自定义异常获取-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/136110267?spm=1001.2014.3001.5501

result.handle目录

存放全局返回格式统一处理类。

感兴趣可以看看

Spring Boot3统一结果封装_spring boot结果集封装-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/136075039?spm=1001.2014.3001.5501

controller目录

此目录用于存放控制器类(负责接收用户的请求、调用适当的业务逻辑处理请求,并将处理结果返回给用户的类

例如userController:

import com.mijiu.commom.aop.annotation.RepeatSubmit;
import com.mijiu.commom.model.dto.UserLoginDTO;
import com.mijiu.commom.model.dto.UserSmsLoginDTO;
import com.mijiu.commom.model.vo.UserLoginVO;
import com.mijiu.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;


/**
 * <p>
 * 用户表 前端控制器
 * </p>
 *
 * @author 蒾酒
 * @since 2024-02-03
 */
@RestController
@RequestMapping("/user")
@CrossOrigin(origins = "*")//允许所有来源的请求跨域
@Tag(name = "用户模块")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/login")
    @RepeatSubmit(interval = 5000)
    @Operation(summary = "用户账密登录")
    public UserLoginVO login(@RequestBody @Validated UserLoginDTO userLoginDTO) {
        return userService.login(userLoginDTO);
    }

    @PostMapping("/login/sms")
    @Operation(summary = "用户短信验证登录")
    public UserLoginVO smsLogin(@RequestBody @Validated UserSmsLoginDTO userSmsLoginDTO) {
        return userService.smsLogin(userSmsLoginDTO);
    }

}

 上述代码中的@RepeatSubmit(interval = 5000)这个自定义注解用来防止重复提交此处用来防止重复登录,这个注解就是放在Common/annotation/目录下的。

这个防重复提交功能是基于自定义注解+AOP实现的,那对应的切面类就是放在Common/aop/目录下的。

通常控制层是不写任何业务逻辑的,它的作用主要把业务功能暴漏为接口,再者进行参数校验

spring boot3参数校验基本用法_springboot3使用校验类注解-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/136180252?spm=1001.2014.3001.5501就比用户控制器类包定义了两个接口,用户的账号密码登录和短信验证登录,那么它就要依赖下层的用户业务逻辑接口的实现类的对应实现方法。下面就介绍一下service目录

service目录

前面也提到过了service目录就是用来放各种业务功能规范接口和对应实现类的

例如UserService、UserServiceImpl:

import com.mijiu.commom.model.dto.UserLoginDTO;
import com.mijiu.commom.model.dto.UserSmsLoginDTO;
import com.mijiu.commom.model.vo.UserLoginVO;
import com.mijiu.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * <p>
 * 用户表 服务类
 * </p>
 *
 * @author 蒾酒
 * @since 2024-02-03
 */
public interface UserService extends IService<User> {


    /**
     *
     * @param userLoginDTO 用户登录表单
     * @return 用户信息返回
     */
     UserLoginVO login(UserLoginDTO userLoginDTO);

    /**
     *
     * @param userSmsLoginDTO 用户手机号登录表单
     * @return 用户信息返回
     */
     UserLoginVO smsLogin(UserSmsLoginDTO userSmsLoginDTO);


}
import java.util.Map;
import java.util.Objects;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 *
 * @author 蒾酒
 * @since 2024-02-03
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    private final UserMapper userMapper;
    private final JwtUtils jwtUtils;
    private final StringRedisTemplate stringRedisTemplate;

    public UserServiceImpl(UserMapper userMapper, JwtUtils jwtUtils, StringRedisTemplate stringRedisTemplate) {
        this.userMapper = userMapper;
        this.jwtUtils = jwtUtils;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public UserLoginVO login(UserLoginDTO userLoginDTO) {
        // 获取验证码id
        String captchaId = userLoginDTO.getCaptchaId();
        // 获取用户提交验证码
        String userCaptcha = userLoginDTO.getCaptcha();
        // 获取缓存验证码
        String cacheCaptcha = stringRedisTemplate.opsForValue().get("login:captcha:" + captchaId);
        // 比较验证码是否正确
        if (cacheCaptcha == null || !cacheCaptcha.equalsIgnoreCase(userCaptcha)) {
            throw new CaptchaErrorException(ResultEnum.USER_CAPTCHA_ERROR);
        }
        // 判断用户是否存在
        User loginUser = new LambdaQueryChainWrapper<>(userMapper)
                .select(User::getId, User::getUserAccount, User::getPassword,
                        User::getUserName, User::getUserRole,
                        User::getAvatar, User::getStatus)
                .eq(User::getUserAccount, userLoginDTO.getUserAccount())
                .one();
        if (loginUser == null) {
            throw new AccountNotFoundException(ResultEnum.USER_NOT_EXIST);
        }
        log.info("loginUser: {}", loginUser);
        // 判断密码是否正确
        String md5Password = DigestUtils.md5DigestAsHex(userLoginDTO.getPassword().getBytes());
        if (!md5Password.equals(loginUser.getPassword())) {
            throw new PasswordErrorException(ResultEnum.USER_PASSWORD_ERROR);
        }
        // 判断用户状态是否正常
        if (!loginUser.getStatus()) {
            throw new AccountForbiddenException(ResultEnum.USER_ACCOUNT_FORBIDDEN);
        }
        // 生成token
        String token = jwtUtils.generateToken(Map.of("userId", loginUser.getId(),
                        "userRole", loginUser.getUserRole()),
                "user");
        //构建响应对象
        return UserLoginVO.builder()
                .userName(loginUser.getUserName())
                .avatar(loginUser.getAvatar())
                .token(token)
                .build();
    }

    @Override
    public UserLoginVO smsLogin(UserSmsLoginDTO userSmsLoginDTO) {
        // 校验验证码是否存在
        HashOperations<String, String, String> hashOps = stringRedisTemplate.opsForHash();
        String captcha = hashOps.get("login:sms:captcha:" + userSmsLoginDTO.getPhone(), "captcha");

        if (StringUtils.isEmpty(captcha)) {
            log.error("手机号 {} 的验证码不存在或已过期", userSmsLoginDTO.getPhone());
            throw new CaptchaErrorException(ResultEnum.USER_CAPTCHA_NOT_EXIST);
        }

        // 查询用户是否已注册
        User loginUser = new LambdaQueryChainWrapper<>(userMapper).eq(User::getPhone, userSmsLoginDTO.getPhone()).one();

        // 如果未注册则进行注册
        if (Objects.isNull(loginUser)) {
            loginUser = register(userSmsLoginDTO.getPhone());
        }

        // 校验验证码是否正确
        if (!userSmsLoginDTO.getCaptcha().equals(captcha)) {
            log.error("手机号 {} 的验证码错误", userSmsLoginDTO.getPhone());
            throw new CaptchaErrorException(ResultEnum.AUTH_CODE_ERROR);
        }
        //判断用户是否被禁用
        if (!loginUser.getStatus()) {
            throw new AccountForbiddenException(ResultEnum.USER_ACCOUNT_FORBIDDEN);
        }
        log.info("手机号 {} 用户登录成功", userSmsLoginDTO.getPhone());

        return UserLoginVO.builder()
                .token(jwtUtils.generateToken(Map.of("userId", loginUser.getId()), "user"))
                .userName(loginUser.getUserName())
                .build();
    }

    private User register(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setUserName(phone);
        user.setStatus(true);
        if (userMapper.insert(user) < 1) {
            log.error("手机号 {} 用户注册失败!", phone);
            throw new AccountRegisterFailException(ResultEnum.USER_REGISTER_FAIL);
        }
        log.info("手机号 {} 用户注册成功", phone);
        return user;
    }
}

感兴趣这两种登录功能专业的实现方法的可以看下:

spring boot3登录开发-3(1账密登录逻辑实现)_springboot3登录-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/136124858?spm=1001.2014.3001.5501spring boot3登录开发-2(2短信验证码接口实现)-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/136888851?spm=1001.2014.3001.5501回到正题控制层依赖业务逻辑层,业务逻辑层则依赖下层mapper(DAO)层---数据访问层,

下面继续介绍mapper目录

mapper目录

该层存放数据访问接口类通常只需要定义出接口具体的操作数据库的逻辑是借助ORM(对象关系映射)框架---mybatis/mybatis-plue/jpa等来快捷编写或者直接生成的。

例如UserMapper、UserMapper.xml:

import com.mijiu.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * <p>
 * 用户表 Mapper 接口
 * </p>
 *
 * @author 蒾酒
 * @since 2024-02-03
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {

}
<?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.mijiu.mapper.UserMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.mijiu.entity.User">
        <id column="id" property="id" />
        <result column="user_name" property="userName" />
        <result column="password" property="password" />
        <result column="user_account" property="userAccount" />
        <result column="user_role" property="userRole" />
        <result column="avatar" property="avatar" />
        <result column="create_time" property="createTime" />
        <result column="update_time" property="updateTime" />
        <result column="is_delete" property="isDelete" />
        <result column="gender" property="gender" />
    </resultMap>

</mapper>

因为我用的是mybatis-plus框架,不需要写mapper,框架本身提供的一组通用mapper也够用,

如果用的是mybatis的话就需要写数据访问接口了

@Mapper
public interface UserMapper extends BaseMapper<User> {

    //根据账号密码查询用户
    User selectUserByNameAndPassword(User user);
}
<?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.mijiu.mapper.UserMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.mijiu.entity.User">
        <id column="id" property="id" />
        <result column="user_name" property="userName" />
        <result column="password" property="password" />
        <result column="user_account" property="userAccount" />
        <result column="user_role" property="userRole" />
        <result column="avatar" property="avatar" />
        <result column="create_time" property="createTime" />
        <result column="update_time" property="updateTime" />
        <result column="is_delete" property="isDelete" />
        <result column="gender" property="gender" />
    </resultMap>

<!--    根据账号密码查询用户-->
    <select id="selectUserByNameAndPassword" resultMap="BaseResultMap">
        SELECT * FROM user WHERE user_account = #{userAccount} AND password = #{password}
    </select>
</mapper>

数据访问层依赖实体类层,去做属性映射接收sql执行返回数据集。下面继续介绍最后一层entity目录

entity目录

这个目录存放的Entity类通常与数据库表中的记录(Row)对应,它们之间存在一一对应的关系。

@Data
@TableName("user")
@ApiModel(value = "User对象", description = "用户表")
public class User implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    @ApiModelProperty("主键")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty("用户昵称")
    @TableField("user_name")
    private String userName;

    @ApiModelProperty("密码")
    @TableField("password")
    private String password;

    @ApiModelProperty("账号")
    @TableField("user_account")
    private String userAccount;

    @ApiModelProperty("用户角色:user / admin")
    @TableField("user_role")
    private String userRole;

    @ApiModelProperty("头像")
    @TableField("avatar")
    private String avatar;

    @ApiModelProperty("创建时间")
    @TableField("create_time")
    private LocalDateTime createTime;

    @ApiModelProperty("更新时间")
    @TableField("update_time")
    private LocalDateTime updateTime;

    @ApiModelProperty("逻辑删除:1删除/0存在")
    @TableField("is_delete")
    private Boolean isDelete;

    @ApiModelProperty("性别")
    @TableField("gender")
    private Boolean gender;

    @ApiModelProperty("状态:1正常0禁用")
    @TableField("status")
    private Boolean status;


    @ApiModelProperty("手机号")
    @TableField("phone")
    private String phone;
}

test目录

主要用来放mapper层、service层的测试用例类

例如UserMapperTest:

@SpringBootTest
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @MockBean
    private BaseMapper<User> baseMapper;

    @Test
    public void testSelectUserByNameAndPassword() {
        // 创建一个模拟的User对象,用于作为参数传入方法中
        User user = new User();
        user.setUserName("test");
        user.setPassword("password");

        // 创建一个模拟的查询结果
        User expectedResult = new User();
        expectedResult.setId(1L);
        expectedResult.setUserName("test");
        expectedResult.setPassword("password");

        // 模拟BaseMapper的行为,当调用其selectOne方法时,返回模拟的结果
        when(baseMapper.selectOne(new QueryWrapper<User>().eq("username", "test").eq("password", "password")))
                .thenReturn(expectedResult);

        // 调用被测试的方法
        User result = userMapper.selectUserByNameAndPassword(user);

        // 断言结果是否符合预期
        assertEquals(expectedResult, result);
    }
}

写在最后

项目模板已开源。

开源地址:
springboot3x-template: 本项目为单体架构spring boot3x版本的web后端服务开发模板,整合常用依赖,以及起步功能,适合中作为小项目的工程初始化脚手架。大幅度提高开发效率。 (gitee.com)icon-default.png?t=N7T8https://gitee.com/mi9688-wine/springboot-template

欢迎star任何问题评论区或私信讨论,欢迎指正。

Logo

欢迎加入西安开发者社区!我们致力于为西安地区的开发者提供学习、合作和成长的机会。参与我们的活动,与专家分享最新技术趋势,解决挑战,探索创新。加入我们,共同打造技术社区!

更多推荐