从零搭建可运行的 Spring Boot 用户模块,覆盖分层设计、参数校验、MyBatis-Plus CRUD 与接口自测。

从 0 到 1 写一个 Spring Boot 用户模块(实战版)
/ Update
4 mins
707 words
Loading views

从 0 到 1 写一个 Spring Boot 用户模块(实战版)h1

这篇是怎么来的h2

这篇其实是我给自己补“工程手感”时写的。

前面我把注解、IOC、AOP 都看过一遍,但一到自己开项目就会卡:目录怎么分?校验放哪?查询接口怎么做分页才不乱?

所以我给自己定了个约束:不用追求大而全,只把 用户注册 + 用户列表 做到能联调、能自测、能讲清楚。

本文目标h2

完成一个可以联调的用户模块,覆盖:

  • 分层结构:Controller -> Service -> Mapper
  • DTO 入参校验:@NotBlank@Email
  • MyBatis-Plus 基础查询与写入
  • 注册接口的重复邮箱拦截
  • 查询接口的分页返回

技术栈h2

  • Java 17
  • Spring Boot 3
  • MySQL 8
  • MyBatis-Plus
  • Lombok

先建表h2

CREATE TABLE `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(32) NOT NULL,
`email` VARCHAR(64) NOT NULL,
`password` VARCHAR(128) NOT NULL,
`create_time` DATETIME NOT NULL,
`update_time` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_email` (`email`)
);

项目结构h2

src/main/java/com/example/demo
├── controller
├── service
│ └── impl
├── mapper
├── entity
├── dto
└── common

第一步:先把主链路跑通h2

先定义四类核心代码:

  • User(实体)
  • UserMapper extends BaseMapper<User>
  • UserService / UserServiceImpl
  • UserController

Controller 先保留两个接口:

@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/register")
public Result<Long> register(@RequestBody @Valid UserRegisterDTO dto) {
return Result.ok(userService.register(dto));
}
@GetMapping("/page")
public Result<Page<UserVO>> page(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestParam(required = false) String keyword) {
return Result.ok(userService.pageQuery(pageNum, pageSize, keyword));
}
}

第二步:DTO 校验入参h2

@Data
public class UserRegisterDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度应为 6~20")
private String password;
}

我当时改了两次目录,最后保留了这三条约定:

  • 入参用 DTO
  • 数据库映射用 Entity
  • 返回给前端用 VO

第三步:实现注册逻辑h2

@Override
public Long register(UserRegisterDTO dto) {
User exist = userMapper.selectOne(
Wrappers.<User>lambdaQuery().eq(User::getEmail, dto.getEmail())
);
if (exist != null) {
throw new BizException(40001, "邮箱已被注册");
}
User user = new User();
user.setUsername(dto.getUsername());
user.setEmail(dto.getEmail());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
userMapper.insert(user);
return user.getId();
}

这里至少要做两件事:

  • 唯一性检查(避免重复注册)
  • 密码加密(不要明文入库)

第四步:实现分页查询h2

@Override
public Page<UserVO> pageQuery(Integer pageNum, Integer pageSize, String keyword) {
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> qw = Wrappers.lambdaQuery(User.class)
.like(StringUtils.hasText(keyword), User::getUsername, keyword)
.orderByDesc(User::getCreateTime);
Page<User> userPage = userMapper.selectPage(page, qw);
Page<UserVO> voPage = new Page<>(pageNum, pageSize, userPage.getTotal());
List<UserVO> records = userPage.getRecords().stream().map(this::toVO).toList();
voPage.setRecords(records);
return voPage;
}

第五步:统一返回体(最小版本)h2

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> ok(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> fail(Integer code, String message) {
return new Result<>(code, message, null);
}
}

这篇只放“能跑通模块”的最小版本。

我交付前会过一遍h2

  • 空用户名是否被拦截
  • 非法邮箱是否被拦截
  • 重复邮箱注册是否提示
  • 密码是否加密后入库
  • 分页查询是否按时间倒序

评论