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

1392 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 # 应用服务层
```
---
## 🏛️ 架构原则与文件组织规范
### 目录层级规范
**严格遵守单层子文件夹原则**
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
```
### 命名规范
- **包名**:全部使用小写字母,单词间用下划线分隔(如 `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避免空指针
#### 代码示例
```java
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`
#### 接口定义
```java
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);
}
```
#### 实现类
```java
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`
#### 代码示例
```java
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`
**职责**: 复杂查询返回的数据对象,包含关联表字段
#### 代码示例
```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`
**职责**: 封装写操作的输入参数,用于接收前端传入的数据
#### 添加命令
```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;
}
```
#### 更新命令
```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 UpdateUserCommand extends AddUserCommand {
@ExcelColumn(name = "用户ID")
private Long userId;
}
```
**关键点**:
- 纯 POJO 类,仅用于数据传输
- 使用 `@ExcelColumn` 注解支持 Excel 导入导出
- 命令对象通常继承或包含基础字段
- 字段名与前端保持一致
---
### 6. Query查询对象
**位置**: `query/Search{Module}Query.java`
**职责**: 封装查询条件和分页参数,构建 MyBatis 查询条件
#### 代码示例
```java
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
```java
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包含更多选项数据
```java
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`
**职责**: 领域模型,封装核心业务逻辑,包括校验、状态转换等
#### 代码示例
```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 实例,管理依赖注入和错误处理
#### 代码示例
```java
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处理业务流程编排
#### 代码示例
```java
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 → 业务校验 → 保存
```
**示例**
```java
// 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
```
**示例**
```java
// 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 中可以从缓存获取关联数据:
```java
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 架构通过清晰的分层和职责分离,为企业级应用开发提供了强有力的支撑:
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