shop-back-end/doc/DDD-CQRS开发指南-新增表完整实现.md

38 KiB
Raw Blame History

AgileBoot DDD/CQRS 开发指南 - 新增表完整实现

📖 文档概述

本文档详细说明了在 AgileBoot 框架中新增一张数据表后,如何按照 DDD/CQRS 架构规范写出完整的 commanddbdtomodelqueryApplicationService 等全套代码。

基于对 agileboot-domain 模块的深度分析,结合 system/usershopcabinet 等真实业务模块的代码实践,本指南提供了可直接使用的代码模板和最佳实践。


🏗️ 目录结构概览

新增一张表后,在 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  # 应用服务层

🏛️ 架构原则与文件组织规范

目录层级规范

严格遵守单层子文件夹原则

  1. 每个模块下最多只能有一层子文件夹,不允许出现多层嵌套的包结构

  2. 例外情况

    • command 包:可以直接包含多个 Command 类文件,无需再分层
    • query 包:可以直接包含多个 Query 类文件,无需再分层
    • dto 包:可以直接包含多个 DTO 类文件,无需再分层
    • model 包:可以直接包含 Model 和 ModelFactory 类文件,无需再分层
  3. 正确的组织方式

    ✓ 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
    

命名规范

  • 包名:全部使用小写字母,单词间用下划线分隔(如 commanddtomodel
  • 文件命名:采用 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.javadb/{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<>();

        wrapper.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<>();

        wrapper.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.toPage(), query.addQueryCondition());
    }

    @Override
    public Page<SearchUserDO> getUserListWithJoin(AbstractPageQuery<SearchUserDO> query) {
        // 自定义 SQL 查询,返回关联数据
        return this.getBaseMapper().selectUserListWithJoin(query.toPage(), 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.javaUpdate{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 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
  • 时间: 使用 DateLocalDateTime

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/ 目录下创建其他 CommandDelete{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 架构通过清晰的分层和职责分离,为企业级应用开发提供了强有力的支撑:

  1. Entity: 数据库映射,继承 BaseEntity
  2. Service: 数据库操作和简单业务逻辑
  3. Command: 写操作参数封装
  4. Query: 读操作查询条件
  5. DTO: 前端数据传输,支持缓存
  6. Model: 领域模型,封装核心业务逻辑
  7. ModelFactory: 统一创建 Model
  8. ApplicationService: 事务脚本层,协调各组件

关键架构原则

  • 单层子文件夹原则:严格遵守每个模块下最多只能有一层子文件夹的规范,避免深层嵌套
  • 职责分离:每个目录和文件都有明确的职责边界
  • 依赖倒置Domain 层不依赖 Infrastructure 层,通过 ApplicationService 协调

遵循本指南的规范,可以快速构建符合 DDD/CQRS 架构的高质量代码。


文档版本: v1.0 最后更新: 2025-01-01 作者: AgileBoot Team