38 KiB
AgileBoot DDD/CQRS 开发指南 - 新增表完整实现
📖 文档概述
本文档详细说明了在 AgileBoot 框架中新增一张数据表后,如何按照 DDD/CQRS 架构规范写出完整的 command、db、dto、model、query、ApplicationService 等全套代码。
基于对 agileboot-domain 模块的深度分析,结合 system/user、shop、cabinet 等真实业务模块的代码实践,本指南提供了可直接使用的代码模板和最佳实践。
🏗️ 目录结构概览
新增一张表后,在 agileboot-domain/src/main/java/com/agileboot/domain/{module}/ 目录下需要创建以下结构:
{module}/
├── command/ # 命令对象(写操作)
│ ├── Add{Module}Command.java
│ ├── Update{Module}Command.java
│ ├── Delete{Module}Command.java
│ └── ...
├── db/ # 数据层
│ ├── {Module}Entity.java # 实体类
│ ├── {Module}Service.java # 服务接口
│ ├── {Module}ServiceImpl.java # 服务实现
│ ├── {Module}Mapper.java # 数据访问
│ └── Search{Module}DO.java # 数据对象(复杂查询)
├── dto/ # 数据传输对象
│ ├── {Module}DTO.java
│ ├── {Module}DetailDTO.java
│ └── {Module}ProfileDTO.java
├── model/ # 领域模型
│ ├── {Module}Model.java
│ └── {Module}ModelFactory.java
├── query/ # 查询对象
│ └── Search{Module}Query.java
└── {Module}ApplicationService.java # 应用服务层
🏛️ 架构原则与文件组织规范
目录层级规范
严格遵守单层子文件夹原则:
-
每个模块下最多只能有一层子文件夹,不允许出现多层嵌套的包结构
-
例外情况:
- command 包:可以直接包含多个 Command 类文件,无需再分层
- query 包:可以直接包含多个 Query 类文件,无需再分层
- dto 包:可以直接包含多个 DTO 类文件,无需再分层
- model 包:可以直接包含 Model 和 ModelFactory 类文件,无需再分层
-
正确的组织方式:
✓ db/{Module}Entity.java ✓ db/{Module}Service.java ✓ db/{Module}ServiceImpl.java ✓ db/{Module}Mapper.java ✓ db/Search{Module}DO.java ✗ 错误:db/entity/{Module}Entity.java ✗ 错误:db/service/{Module}Service.java ✗ 错误:db/service/impl/{Module}ServiceImpl.java ✗ 错误:db/mapper/{Module}Mapper.java ✗ 错误:db/do/Search{Module}DO.java
命名规范
- 包名:全部使用小写字母,单词间用下划线分隔(如
command、dto、model) - 文件命名:采用 PascalCase 命名法,体现类的职责
- 目录名:采用小写字母,与包名保持一致
目录职责划分
| 目录 | 职责 | 文件类型 |
|---|---|---|
command/ |
封装写操作请求参数 | Add/Update/Delete 命令对象 |
query/ |
封装读操作查询条件 | Search 查询对象 |
dto/ |
向前端传输数据 | DTO、详情DTO、配置文件DTO |
model/ |
领域模型和业务逻辑 | Model、ModelFactory |
db/ |
数据访问层 | Entity、Service、Mapper、DO |
| ApplicationService | 应用服务层 | 事务脚本、流程编排 |
📝 核心组件详解
1. Entity(实体类)
位置: db/{Module}Entity.java
职责: 数据库表的 Java 映射,继承 BaseEntity 获得通用字段
关键特性:
- 继承
BaseEntity<SELF> - 实现
pkVal()返回主键 - 使用 MyBatis-Plus 注解:
@TableName、@TableId、@TableField - 所有字段使用包装类型(Long, Integer),避免空指针
代码示例
package com.agileboot.domain.example.user.db;
import com.agileboot.common.core.base.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户信息表
*
* @author your-name
* @since 2025-01-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("example_user")
@ApiModel(value = "ExampleUserEntity对象", description = "用户信息表")
public class ExampleUserEntity extends BaseEntity<ExampleUserEntity> {
private static final long serialVersionUID = 1L;
@ApiModelProperty("用户ID")
@TableId(value = "user_id", type = IdType.AUTO)
private Long userId;
@ApiModelProperty("用户姓名")
@TableField("user_name")
private String userName;
@ApiModelProperty("年龄")
@TableField("age")
private Integer age;
@ApiModelProperty("邮箱")
@TableField("email")
private String email;
@ApiModelProperty("余额")
@TableField("balance")
private BigDecimal balance;
@ApiModelProperty("状态(1正常 2停用)")
@TableField("status")
private Integer status;
@ApiModelProperty("部门ID")
@TableField("dept_id")
private Long deptId;
@ApiModelProperty("备注")
@TableField("remark")
private String remark;
@Override
public Serializable pkVal() {
return this.userId;
}
}
关键点:
- 类名:
{Module}Entity,表名使用下划线命名{module}_{name} - 主键字段:
{module}_id,类型为Long,注解@TableId(type = IdType.AUTO) - 继承
BaseEntity获得:creatorId,createTime,updaterId,updateTime,deleted
2. Service(服务层)
位置: db/{Module}Service.java 和 db/{Module}ServiceImpl.java
职责: 封装数据库操作和简单业务逻辑,继承 MyBatis-Plus 的 IService
接口定义
package com.agileboot.domain.example.user.db;
import com.agileboot.common.core.page.AbstractPageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 用户信息表 服务类
*
* @author your-name
* @since 2025-01-01
*/
public interface ExampleUserService extends IService<ExampleUserEntity> {
/**
* 检测邮箱是否唯一
*
* @param email 邮箱
* @param userId 用户ID(更新时传入,排除自身)
* @return 是否唯一
*/
boolean isEmailUnique(String email, Long userId);
/**
* 检测用户名是否唯一
*
* @param userName 用户名
* @return 是否唯一
*/
boolean isUserNameUnique(String userName);
/**
* 获取用户的部门信息
*
* @param userId 用户ID
* @return 部门信息
*/
Long getDeptIdOfUser(Long userId);
/**
* 根据条件分页查询用户列表
*
* @param query 查询参数
* @return 用户信息集合
*/
Page<ExampleUserEntity> getUserList(AbstractPageQuery<ExampleUserEntity> query);
/**
* 根据条件分页查询用户列表(带额外字段)
*
* @param query 查询参数
* @return 用户信息集合(含关联字段)
*/
Page<SearchUserDO> getUserListWithJoin(AbstractPageQuery<SearchUserDO> query);
}
实现类
package com.agileboot.domain.example.user.db.impl;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.core.page.AbstractPageQuery;
import com.agileboot.domain.example.user.db.ExampleUserEntity;
import com.agileboot.domain.example.user.db.SearchUserDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 用户信息表 服务实现类
*
* @author your-name
* @since 2025-01-01
*/
@Service
@RequiredArgsConstructor
public class ExampleUserServiceImpl extends ServiceImpl<ExampleUserMapper, ExampleUserEntity> implements ExampleUserService {
@Override
public boolean isEmailUnique(String email, Long userId) {
if (StrUtil.isEmpty(email)) {
return true;
}
LambdaQueryWrapper<ExampleUserEntity> wrapper = new LambdaQueryWrapper<>()
.eq(ExampleUserEntity::getEmail, email)
.eq(ExampleUserEntity::getDeleted, 0);
// 如果是更新操作,排除自身
if (userId != null) {
wrapper.ne(ExampleUserEntity::getUserId, userId);
}
return !this.count(wrapper) > 0;
}
@Override
public boolean isUserNameUnique(String userName) {
if (StrUtil.isEmpty(userName)) {
return true;
}
LambdaQueryWrapper<ExampleUserEntity> wrapper = new LambdaQueryWrapper<>()
.eq(ExampleUserEntity::getUserName, userName)
.eq(ExampleUserEntity::getDeleted, 0);
return !this.count(wrapper) > 0;
}
@Override
public Long getDeptIdOfUser(Long userId) {
if (userId == null) {
return null;
}
ExampleUserEntity entity = this.getById(userId);
return entity != null ? entity.getDeptId() : null;
}
@Override
public Page<ExampleUserEntity> getUserList(AbstractPageQuery<ExampleUserEntity> query) {
return this.page(query.buildPage(), query.addQueryCondition());
}
@Override
public Page<SearchUserDO> getUserListWithJoin(AbstractPageQuery<SearchUserDO> query) {
// 自定义 SQL 查询,返回关联数据
return this.getBaseMapper().selectUserListWithJoin(query.buildPage(), query);
}
}
关键点:
- 接口继承
IService<Entity> - 实现类继承
ServiceImpl<Mapper, Entity> - 简单业务逻辑在 Service 中处理
- 复杂业务逻辑交给 Model 处理
3. Mapper(数据访问层)
位置: db/{Module}Mapper.java
职责: MyBatis 数据访问接口,通常使用 MyBatis-Plus 的 BaseMapper
代码示例
package com.agileboot.domain.example.user.db;
import com.agileboot.common.core.page.AbstractPageQuery;
import com.agileboot.domain.example.user.db.SearchUserDO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 用户信息表 Mapper 接口
*
* @author your-name
* @since 2025-01-01
*/
@Mapper
public interface ExampleUserMapper extends BaseMapper<ExampleUserEntity> {
/**
* 分页查询用户列表(带关联数据)
*
* @param page 分页参数
* @param query 查询条件
* @return 用户列表
*/
@Select({
"<script>",
"SELECT u.*, d.dept_name, r.role_name",
"FROM example_user u",
"LEFT JOIN sys_dept d ON u.dept_id = d.dept_id",
"LEFT JOIN sys_role r ON u.role_id = r.role_id",
"WHERE u.deleted = 0",
"<if test='query.userName != null and query.userName != \"\"'>",
" AND u.user_name LIKE CONCAT('%', #{query.userName}, '%')",
"</if>",
"<if test='query.email != null and query.email != \"\"'>",
" AND u.email LIKE CONCAT('%', #{query.email}, '%')",
"</if>",
"ORDER BY u.create_time DESC",
"</script>"
})
Page<SearchUserDO> selectUserListWithJoin(
Page<SearchUserDO> page,
@Param("query") AbstractPageQuery<SearchUserDO> query
);
}
关键点:
- 继承
BaseMapper<Entity> - 复杂查询使用
@Select注解或 XML 文件 - 使用
#{}进行参数绑定,避免 SQL 注入
4. DO(数据对象)
位置: db/Search{Module}DO.java
职责: 复杂查询返回的数据对象,包含关联表字段
代码示例
package com.agileboot.domain.example.user.db;
import com.baomidou.mybatisplus.annotation.TableField;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
/**
* 用户查询数据对象(包含关联字段)
*
* @author your-name
* @since 2025-01-01
*/
@Data
public class SearchUserDO {
private Long userId;
private String userName;
private Integer age;
private String email;
private BigDecimal balance;
private Integer status;
private Long deptId;
@TableField("dept_name")
private String deptName;
@TableField("role_name")
private String roleName;
private Date createTime;
private Date updateTime;
private String remark;
}
关键点:
- 用于复杂查询,包含关联表字段
- 字段名使用数据库列名或
as别名 - 不需要继承任何类
5. Command(命令对象)
位置: command/Add{Module}Command.java、Update{Module}Command.java
职责: 封装写操作的输入参数,用于接收前端传入的数据
添加命令
package com.agileboot.domain.example.user.command;
import com.agileboot.common.annotation.ExcelColumn;
import lombok.Data;
/**
* 添加用户命令
*
* @author your-name
* @since 2025-01-01
*/
@Data
public class AddUserCommand {
@ExcelColumn(name = "用户姓名")
private String userName;
@ExcelColumn(name = "年龄")
private Integer age;
@ExcelColumn(name = "邮箱")
private String email;
@ExcelColumn(name = "余额")
private String balance; // 使用 String 接收,前端传入
@ExcelColumn(name = "状态")
private Integer status;
@ExcelColumn(name = "部门ID")
private Long deptId;
@ExcelColumn(name = "备注")
private String remark;
}
更新命令
package com.agileboot.domain.example.user.command;
import com.agileboot.common.annotation.ExcelColumn;
import lombok.Data;
/**
* 更新用户命令
*
* @author your-name
* @since 2025-01-01
*/
@Data
public class UpdateUserCommand extends AddUserCommand {
@ExcelColumn(name = "用户ID")
private Long userId;
}
关键点:
- 纯 POJO 类,仅用于数据传输
- 使用
@ExcelColumn注解支持 Excel 导入导出 - 命令对象通常继承或包含基础字段
- 字段名与前端保持一致
6. Query(查询对象)
位置: query/Search{Module}Query.java
职责: 封装查询条件和分页参数,构建 MyBatis 查询条件
代码示例
package com.agileboot.domain.example.user.query;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.core.page.AbstractPageQuery;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户查询条件
*
* @author your-name
* @since 2025-01-01
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class SearchUserQuery<T> extends AbstractPageQuery<T> {
private Long userId;
private String userName;
private String email;
private Integer status;
private Long deptId;
private Integer minAge;
private Integer maxAge;
@Override
public QueryWrapper<T> addQueryCondition() {
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
queryWrapper.like(StrUtil.isNotEmpty(userName), "user_name", userName)
.like(StrUtil.isNotEmpty(email), "email", email)
.eq(userId != null, "user_id", userId)
.eq(status != null, "status", status)
.eq(deptId != null, "dept_id", deptId)
.eq("deleted", 0)
.between(minAge != null && maxAge != null, "age", minAge, maxAge);
// 设置时间范围排序字段
this.timeRangeColumn = "create_time";
return queryWrapper;
}
}
关键点:
- 继承
AbstractPageQuery<T> - 实现
addQueryCondition()方法构建查询条件 - 使用链式调用构建查询条件
- 非空判断:
StrUtil.isNotEmpty()和!= null - 设置默认排序字段
7. DTO(数据传输对象)
位置: dto/{Module}DTO.java、{Module}DetailDTO.java、{Module}ProfileDTO.java
职责: 向前端传输数据,支持缓存集成和字段转换
基础 DTO
package com.agileboot.domain.example.user.dto;
import cn.hutool.core.bean.BeanUtil;
import com.agileboot.common.annotation.ExcelColumn;
import com.agileboot.common.annotation.ExcelSheet;
import com.agileboot.domain.common.cache.CacheCenter;
import com.agileboot.domain.example.user.db.ExampleUserEntity;
import com.agileboot.domain.example.user.db.SearchUserDO;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
/**
* 用户DTO
*
* @author your-name
* @since 2025-01-01
*/
@ExcelSheet(name = "用户列表")
@Data
public class UserDTO {
public UserDTO(ExampleUserEntity entity) {
if (entity != null) {
BeanUtil.copyProperties(entity, this);
// 从缓存中获取关联数据
if (entity.getDeptId() != null) {
// 示例:获取部门名称(如果缓存中有)
// this.deptName = CacheCenter.deptCache.get(entity.getDeptId() + "")...;
}
}
}
public UserDTO(SearchUserDO entity) {
if (entity != null) {
BeanUtil.copyProperties(entity, this);
}
}
@ExcelColumn(name = "用户ID")
private Long userId;
@ExcelColumn(name = "用户姓名")
private String userName;
@ExcelColumn(name = "年龄")
private Integer age;
@ExcelColumn(name = "邮箱")
private String email;
@ExcelColumn(name = "余额")
private BigDecimal balance;
@ExcelColumn(name = "状态")
private String status; // 可以转换为文字描述
@ExcelColumn(name = "部门ID")
private Long deptId;
@ExcelColumn(name = "部门名称")
private String deptName;
@ExcelColumn(name = "创建时间")
private Date createTime;
@ExcelColumn(name = "备注")
private String remark;
}
详情 DTO(包含更多选项数据)
package com.agileboot.domain.example.user.dto;
import com.agileboot.domain.example.user.db.ExampleUserEntity;
import com.agileboot.domain.system.dept.dto.DeptDTO;
import com.agileboot.domain.system.role.dto.RoleDTO;
import java.util.List;
import lombok.Data;
/**
* 用户详情DTO(包含选项数据)
*
* @author your-name
* @since 2025-01-01
*/
@Data
public class UserDetailDTO {
private UserDTO user;
private List<DeptDTO> deptOptions;
private List<RoleDTO> roleOptions;
}
关键点:
- 支持从 Entity 或 DO 转换
- 使用
BeanUtil.copyProperties()进行属性复制 - 可以集成多级缓存获取关联数据
- 支持 Excel 导入导出(
@ExcelColumn) - 可包含选项数据(如部门列表、角色列表)
8. Model(领域模型)
位置: model/{Module}Model.java
职责: 领域模型,封装核心业务逻辑,包括校验、状态转换等
代码示例
package com.agileboot.domain.example.user.model;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.domain.example.user.command.AddUserCommand;
import com.agileboot.domain.example.user.command.UpdateUserCommand;
import com.agileboot.domain.example.user.db.ExampleUserEntity;
import com.agileboot.domain.example.user.db.ExampleUserService;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 用户领域模型
*
* @author your-name
* @since 2025-01-01
*/
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
public class UserModel extends ExampleUserEntity {
private ExampleUserService userService;
public UserModel(ExampleUserEntity entity, ExampleUserService userService) {
if (entity != null) {
BeanUtil.copyProperties(entity, this);
}
this.userService = userService;
}
public UserModel(ExampleUserService userService) {
this.userService = userService;
}
/**
* 加载添加用户命令
*/
public void loadAddUserCommand(AddUserCommand command) {
if (command != null) {
BeanUtil.copyProperties(command, this, "userId");
// 转换余额字段
if (StrUtil.isNotEmpty(command.getBalance())) {
try {
this.setBalance(new BigDecimal(command.getBalance()));
} catch (NumberFormatException e) {
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "余额格式不正确");
}
}
// 设置默认值
if (this.getStatus() == null) {
this.setStatus(1); // 默认正常状态
}
}
}
/**
* 加载更新用户命令
*/
public void loadUpdateUserCommand(UpdateUserCommand command) {
if (command != null) {
loadAddUserCommand(command);
}
}
/**
* 校验用户名唯一性
*/
public void checkUserNameIsUnique() {
if (!userService.isUserNameUnique(getUserName())) {
throw new ApiException(ErrorCode.Business.USER_NAME_IS_NOT_UNIQUE);
}
}
/**
* 校验邮箱唯一性
*/
public void checkEmailIsUnique() {
if (!userService.isEmailUnique(getEmail(), getUserId())) {
throw new ApiException(ErrorCode.Business.USER_EMAIL_IS_NOT_UNIQUE);
}
}
/**
* 校验关联数据是否存在
*/
public void checkFieldRelatedEntityExist() {
// 示例:校验部门是否存在
if (getDeptId() != null) {
// 可以通过其他 Service 校验部门是否存在
// Example: deptService.getById(getDeptId());
}
}
/**
* 校验是否可以删除
*/
public void checkCanBeDelete() {
// 业务逻辑:例如检查用户是否有未完成的订单
// if (hasUnfinishedOrders()) {
// throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "用户存在未完成的订单,无法删除");
// }
// 示例:检查是否为系统管理员
if (this.getIsAdmin() != null && this.getIsAdmin()) {
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "系统管理员不能删除");
}
}
/**
* 设置密码(加密)
*/
public void setEncryptedPassword(String rawPassword) {
// 示例:加密密码
// this.setPassword(PasswordUtils.encrypt(rawPassword));
}
/**
* 校验年龄是否合法
*/
public void checkAgeIsValid() {
if (getAge() != null && (getAge() < 0 || getAge() > 150)) {
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "年龄必须在0-150之间");
}
}
/**
* 预保存校验
*/
public void validateBeforeSave() {
if (StrUtil.isEmpty(getUserName())) {
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "用户名不能为空");
}
checkUserNameIsUnique();
checkEmailIsUnique();
checkFieldRelatedEntityExist();
checkAgeIsValid();
}
}
关键点:
- 继承实体类,获得所有字段
- 注入 Service 用于数据操作
- 包含
load*Command()方法加载命令数据 - 包含
check*()方法进行业务校验 - 包含业务逻辑处理方法
- 在 Model 中封装所有业务规则
9. ModelFactory(模型工厂)
位置: model/{Module}ModelFactory.java
职责: 统一创建 Model 实例,管理依赖注入和错误处理
代码示例
package com.agileboot.domain.example.user.model;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.domain.example.user.db.ExampleUserEntity;
import com.agileboot.domain.example.user.db.ExampleUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 用户模型工厂
*
* @author your-name
* @since 2025-01-01
*/
@Component
@RequiredArgsConstructor
public class UserModelFactory {
private final ExampleUserService userService;
/**
* 根据ID加载用户模型
*
* @param userId 用户ID
* @return 用户模型
*/
public UserModel loadById(Long userId) {
ExampleUserEntity entity = userService.getById(userId);
if (entity == null) {
throw new ApiException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, userId, "用户");
}
return new UserModel(entity, userService);
}
/**
* 创建新用户模型
*
* @return 用户模型
*/
public UserModel create() {
return new UserModel(userService);
}
}
关键点:
- 使用
@Component注解注入 Spring 容器 - 使用
@RequiredArgsConstructor自动注入依赖 loadById()检查数据是否存在,不存在则抛异常create()创建空模型,用于新增操作
10. ApplicationService(应用服务)
位置: {Module}ApplicationService.java
职责: 事务脚本层,协调 Model 和 Service,处理业务流程编排
代码示例
package com.agileboot.domain.example.user;
import cn.hutool.core.convert.Convert;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.domain.common.command.BulkOperationCommand;
import com.agileboot.domain.example.user.command.AddUserCommand;
import com.agileboot.domain.example.user.command.UpdateUserCommand;
import com.agileboot.domain.example.user.db.SearchUserDO;
import com.agileboot.domain.example.user.dto.UserDTO;
import com.agileboot.domain.example.user.dto.UserDetailDTO;
import com.agileboot.domain.example.user.model.UserModel;
import com.agileboot.domain.example.user.model.UserModelFactory;
import com.agileboot.domain.example.user.query.SearchUserQuery;
import com.agileboot.domain.system.dept.db.SysDeptService;
import com.agileboot.domain.system.dept.dto.DeptDTO;
import com.agileboot.domain.example.user.db.ExampleUserEntity;
import com.agileboot.domain.example.user.db.ExampleUserService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户应用服务
*
* @author your-name
* @since 2025-01-01
*/
@Service
@RequiredArgsConstructor
public class UserApplicationService {
private final ExampleUserService userService;
private final SysDeptService deptService;
private final UserModelFactory userModelFactory;
/**
* 获取用户列表(分页)
*/
public PageDTO<UserDTO> getUserList(SearchUserQuery<SearchUserDO> query) {
Page<SearchUserDO> userPage = userService.getUserListWithJoin(query);
List<UserDTO> userDTOList = userPage.getRecords()
.stream()
.map(UserDTO::new)
.collect(Collectors.toList());
return new PageDTO<>(userDTOList, userPage.getTotal());
}
/**
* 获取用户详情
*/
public UserDetailDTO getUserDetailInfo(Long userId) {
ExampleUserEntity userEntity = userService.getById(userId);
UserDetailDTO detailDTO = new UserDetailDTO();
detailDTO.setUser(new UserDTO(userEntity));
// 获取部门选项
List<DeptDTO> deptOptions = deptService.list()
.stream()
.map(DeptDTO::new)
.collect(Collectors.toList());
detailDTO.setDeptOptions(deptOptions);
return detailDTO;
}
/**
* 添加用户
*/
@Transactional(rollbackFor = Exception.class)
public void addUser(AddUserCommand command) {
UserModel model = userModelFactory.create();
model.loadAddUserCommand(command);
// 业务校验
model.validateBeforeSave();
// 保存
model.insert();
}
/**
* 更新用户
*/
@Transactional(rollbackFor = Exception.class)
public void updateUser(UpdateUserCommand command) {
UserModel model = userModelFactory.loadById(command.getUserId());
model.loadUpdateUserCommand(command);
// 业务校验
model.checkEmailIsUnique();
model.checkFieldRelatedEntityExist();
model.checkAgeIsValid();
// 更新
model.updateById();
}
/**
* 删除用户(单个)
*/
@Transactional(rollbackFor = Exception.class)
public void deleteUser(Long userId) {
UserModel model = userModelFactory.loadById(userId);
// 业务校验
model.checkCanBeDelete();
// 软删除
model.deleteById();
}
/**
* 批量删除用户
*/
@Transactional(rollbackFor = Exception.class)
public void deleteUsers(BulkOperationCommand<Long> command) {
for (Long userId : command.getIds()) {
UserModel model = userModelFactory.loadById(userId);
model.checkCanBeDelete();
model.deleteById();
}
}
/**
* 更改用户状态
*/
@Transactional(rollbackFor = Exception.class)
public void changeUserStatus(Long userId, Integer status) {
UserModel model = userModelFactory.loadById(userId);
model.setStatus(status);
model.updateById();
}
/**
* 重置用户密码
*/
@Transactional(rollbackFor = Exception.class)
public void resetPassword(Long userId, String newPassword) {
UserModel model = userModelFactory.loadById(userId);
model.setEncryptedPassword(newPassword);
model.updateById();
}
}
关键点:
- 使用
@Service注解 - 使用
@RequiredArgsConstructor注入依赖 - 写操作使用
@Transactional注解保证事务 - 协调 Model 和 Service
- 返回 DTO 给 Controller
- 包含完整的 CRUD 操作
🔄 CQRS 流程
Command(写操作)流程
Controller → Command → ApplicationService → Model → 业务校验 → 保存
示例:
// 1. Controller 接收参数,封装为 Command
AddUserCommand command = new AddUserCommand();
// ... 设置参数
// 2. ApplicationService 处理
userApplicationService.addUser(command);
// 3. ApplicationService 创建 Model 并加载 Command
UserModel model = userModelFactory.create();
model.loadAddUserCommand(command);
// 4. Model 进行业务校验
model.validateBeforeSave();
// 5. Model 执行保存操作
model.insert();
Query(读操作)流程
Controller → Query → Service → Mapper → DTO
示例:
// 1. Controller 接收查询参数,封装为 Query
SearchUserQuery<SearchUserDO> query = new SearchUserQuery<>();
// ... 设置查询条件
// 2. ApplicationService 处理
PageDTO<UserDTO> result = userApplicationService.getUserList(query);
// 3. Service 执行查询
Page<SearchUserDO> userPage = userService.getUserListWithJoin(query);
// 4. 转换为 DTO
List<UserDTO> userDTOList = userPage.getRecords()
.stream()
.map(UserDTO::new)
.collect(Collectors.toList());
return new PageDTO<>(userDTOList, userPage.getTotal());
📌 最佳实践
1. 命名规范
| 类型 | 命名规则 | 示例 |
|---|---|---|
| Entity | {Module}Entity |
SysUserEntity |
| Service 接口 | {Module}Service |
SysUserService |
| Service 实现 | {Module}ServiceImpl |
SysUserServiceImpl |
| Mapper | {Module}Mapper |
SysUserMapper |
| Model | {Module}Model |
UserModel |
| ModelFactory | {Module}ModelFactory |
UserModelFactory |
| Command | {Operation}{Module}Command |
AddUserCommand |
| Query | Search{Module}Query |
SearchUserQuery |
| DTO | {Module}DTO |
UserDTO |
| ApplicationService | {Module}ApplicationService |
UserApplicationService |
2. 字段命名规范
- Entity: 使用下划线命名,与数据库保持一致
user_id,user_name,create_time
- Command/Query/DTO: 使用驼峰命名,与前端保持一致
userId,userName,createTime
3. 类型选择
- 主键: 使用
Long(包装类型) - 状态/数量: 使用
Integer(包装类型) - 金额: 使用
BigDecimal(避免精度问题) - 字符串: 避免基本类型,使用
String - 时间: 使用
Date或LocalDateTime
4. 空值处理
- 始终使用包装类型,避免 NPE
- 使用
StrUtil.isNotEmpty()判断字符串 - 使用
!= null判断对象和数字 - 使用 MyBatis-Plus 的链式调用:
eq(condition, "column", value)
5. 业务校验位置
- 唯一性校验: Model 层
- 关联数据存在性校验: Model 层
- 字段格式校验: Model 层
- 业务规则校验: Model 层
- 数据库约束: Entity 层
6. 缓存集成
DTO 中可以从缓存获取关联数据:
public UserDTO(ExampleUserEntity entity) {
if (entity != null) {
BeanUtil.copyProperties(entity, this);
// 从缓存获取部门名称
if (entity.getDeptId() != null) {
SysDeptEntity dept = CacheCenter.deptCache.get(entity.getDeptId() + "");
this.deptName = dept != null ? dept.getDeptName() : "";
}
}
}
7. 事务管理
- ApplicationService 负责事务管理
- 使用
@Transactional(rollbackFor = Exception.class) - 写操作必须开启事务
- 读操作可以不加事务(但建议加
@Transactional(readOnly = true))
8. 异常处理
- 业务异常使用
ApiException - 错误码使用
ErrorCode枚举 - Model 层抛出异常,ApplicationService 不捕获
- Controller 层统一处理异常
9. 代码复用
- 公共方法抽取到工具类
- 通用 DTO/Entity 放在
common模块 - 公共 Service 方法抽取到基类
- 使用组合而非继承
🛠️ 开发步骤清单
新增一张表后的完整开发流程:
1. 数据库层面
- 创建数据库表(
sql/YYYYMMDD_feature_name.sql) - 添加表注释和字段注释
- 创建必要的索引
2. Entity 层
- 在
db/目录下创建{Module}Entity.java,继承BaseEntity - 配置
@TableName、@TableId、@TableField注解 - 实现
pkVal()方法
3. Mapper 层
- 在
db/目录下创建{Module}Mapper.java,继承BaseMapper<Entity> - 添加自定义查询方法(如需要)
4. Service 层
- 在
db/目录下创建{Module}Service.java接口 - 在
db/目录下创建{Module}ServiceImpl.java实现类 - 继承
ServiceImpl<Mapper, Entity> - 实现核心业务方法
5. Command 层
- 在
command/目录下创建Add{Module}Command.java - 在
command/目录下创建Update{Module}Command.java(可选:继承基础 Command) - 在
command/目录下创建其他 Command(如Delete{Module}Command.java)
6. Query 层
- 在
query/目录下创建Search{Module}Query.java - 继承
AbstractPageQuery<T> - 实现
addQueryCondition()方法
7. DTO 层
- 在
dto/目录下创建{Module}DTO.java - 添加构造函数(从 Entity/DO 转换)
- 添加
@ExcelColumn注解(如果需要导出) - 集成缓存获取关联数据(如果需要)
8. Model 层
- 在
model/目录下创建{Module}Model.java,继承 Entity - 添加
load*Command()方法 - 添加
check*()业务校验方法 - 添加业务逻辑处理方法
9. ModelFactory 层
- 在
model/目录下创建{Module}ModelFactory.java - 使用
@Component注解 - 添加
loadById()方法 - 添加
create()方法
10. ApplicationService 层
- 创建
{Module}ApplicationService.java(位于模块根目录) - 使用
@Service注解 - 注入 Service 和 ModelFactory
- 实现 CRUD 方法
- 添加
@Transactional注解
11. Controller 层(在 agileboot-admin 中)
- 创建 Controller
- 注入 ApplicationService
- 定义 RESTful API
- 添加
@Operation注解
12. 测试
- 编写单元测试(Service、Model)
- 编写集成测试
- 测试 CRUD 操作
- 测试业务校验
- 测试缓存(如果使用)
13. 文档
- 更新 API 文档
- 补充业务说明
- 更新数据库文档
📚 参考资料
代码结构参考
- System 用户管理:
agileboot-domain/src/main/java/com/agileboot/domain/system/user/ - Shop 电商模块:
agileboot-domain/src/main/java/com/agileboot/domain/shop/ - Cabinet 智能柜:
agileboot-domain/src/main/java/com/agileboot/domain/cabinet/
核心类参考
- BaseEntity:
com.agileboot.common.core.base.BaseEntity - AbstractPageQuery:
com.agileboot.common.core.page.AbstractPageQuery - PageDTO:
com.agileboot.common.core.page.PageDTO - ApiException:
com.agileboot.common.exception.ApiException - ErrorCode:
com.agileboot.common.exception.error.ErrorCode - CacheCenter:
com.agileboot.domain.common.cache.CacheCenter
🎯 总结
AgileBoot 的 DDD/CQRS 架构通过清晰的分层和职责分离,为企业级应用开发提供了强有力的支撑:
- Entity: 数据库映射,继承 BaseEntity
- Service: 数据库操作和简单业务逻辑
- Command: 写操作参数封装
- Query: 读操作查询条件
- DTO: 前端数据传输,支持缓存
- Model: 领域模型,封装核心业务逻辑
- ModelFactory: 统一创建 Model
- ApplicationService: 事务脚本层,协调各组件
关键架构原则
- 单层子文件夹原则:严格遵守每个模块下最多只能有一层子文件夹的规范,避免深层嵌套
- 职责分离:每个目录和文件都有明确的职责边界
- 依赖倒置:Domain 层不依赖 Infrastructure 层,通过 ApplicationService 协调
遵循本指南的规范,可以快速构建符合 DDD/CQRS 架构的高质量代码。
文档版本: v1.0 最后更新: 2025-01-01 作者: AgileBoot Team