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

1392 lines
38 KiB
Markdown
Raw Normal View History

# 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