领域驱动设计(DDD)项目开发规范,包含领域模型、聚合、仓储等DDD概念的实现指导。
## DDD领域开发规范 (基于Scorpio Framework) [scorpio-framework-develop-spec.mdc](mdc:.cursor/rules/scorpio-framework-develop-spec.mdc)
### 0. 项目结构
项目目录结构遵循maven风格。
src/main/java 是项目代码的根目录。
src/main/resources 是项目资源文件的根目录。
src/test/java 是项目测试代码的根目录。
src/test/resources 是项目测试资源文件的根目录。
#### 0.1 根包
根包是是最顶层的package,包命名规范为: `com.{组织名}.{项目名}`
项目信息章节会明确根包。
### 1. 领域层开发规范
#### 1.1 包结构规范
领域的包名是domain, 是根包的子目录。
domain包下按照领域模型来划分模块, 每个领域模型一个包。
领域模型的包下必须包含以下目录:
```
domain.{module}
├── aggregate # 聚合根
├── entity # 实体
├── valueobject # 值对象
├── repository # 仓储接口
├── service # 领域服务
├── event # 领域事件
├── validator # 验证器
└── trouble # 异常定义
```
#### 1.2 聚合根(Aggregate Root)规范
1. **基本要求**
- 必须实现 `Congestive` 接口
- 必须继承 `BaseEntity`
- 必须使用 `@CongestiveId` 定义业务标识
- 必须定义对应的 `Repository`
- 必须定义对应的 `Validator`
- 必须放在 aggregate 包下
- 必须通过工厂方法创建实例
- 禁止使用 @Getter 和 @Setter 注解
- 所有属性访问方法必须显式定义
2. **命名规范**
```java
User // 聚合根类名
UserRepository // 仓储接口名
UserValidator // 验证器名
```
3. **代码规范**
```java
@Entity
@Table(name = "ppi_user")
public class User extends BaseEntity implements Congestive {
@CongestiveId(generator = SnowflakeGenerator.class)
private String userId;
@NotBlank(message = "用户名不能为空")
private String username;
@Enumerated(EnumType.STRING)
private UserStatus status;
// 值对象
@Embedded
private Address address;
// 实体集合
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "user_id")
private List contactInfos = new ArrayList<>();
// protected 构造函数,供JPA使用
protected User() {}
// 工厂方法
public static User create(String username, Address address) {
User user = new User();
user.username = username;
user.address = address;
user.status = UserStatus.INACTIVE;
user.contactInfos = new ArrayList<>();
log.info("Created new user with username [{}]", username);
return user;
}
// 显式定义必要的getter方法
public String getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public UserStatus getStatus() {
return status;
}
public Address getAddress() {
return address;
}
public List getContactInfos() {
return Collections.unmodifiableList(contactInfos);
}
// 业务方法
public void activate() {
if (status != UserStatus.INACTIVE) {
throw UserTrouble.INVALID_STATUS_TRANSITION.thrown();
}
status = UserStatus.ACTIVE;
log.info("User [{}] activated", userId);
}
public void addContactInfo(ContactInfo contactInfo) {
contactInfos.add(contactInfo);
log.info("Added contact info for user [{}]", userId);
}
public void changeAddress(Address newAddress) {
this.address = newAddress;
log.info("Changed address for user [{}]", userId);
}
}
```
#### 1.3 实体(Entity)规范
1. **基本要求**
- 必须继承 `BaseEntity`
- 必须放在 entity 包下
- 不允许被外部直接访问
- 必须通过聚合根进行访问和修改
- 禁止使用 @Getter 和 @Setter 注解
- 所有属性访问方法必须显式定义
2. **代码规范**
```java
@Entity
@Table(name = "contact_info")
public class ContactInfo extends BaseEntity {
@NotBlank
private String type;
@NotBlank
private String value;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
// protected 构造函数,供JPA使用
protected ContactInfo() {}
// 工厂方法
public static ContactInfo create(String type, String value) {
ContactInfo contactInfo = new ContactInfo();
contactInfo.type = type;
contactInfo.value = value;
contactInfo.validate();
return contactInfo;
}
// 显式定义getter方法
public String getType() {
return type;
}
public String getValue() {
return value;
}
// 实体的业务方法
public void validate() {
if ("phone".equals(type) && !value.matches("^1[3-9]\\d{9}$")) {
throw new IllegalArgumentException("Invalid phone number");
}
}
}
```
#### 1.4 值对象(Value Object)规范
1. **基本要求**
- 必须是不可变的(所有字段都是final)
- 必须重写 equals() 和 hashCode() 方法
- 必须放在 valueobject 包下
- 推荐使用 @Value 注解(Lombok)
2. **代码规范**
```java
@Value
@Embeddable
public class Address {
String province;
String city;
String district;
String detail;
// 工厂方法创建值对象
public static Address of(String province, String city, String district, String detail) {
return new Address(province, city, district, detail);
}
// 值对象的业务方法返回新实例
public Address changeDetail(String newDetail) {
return new Address(this.province, this.city, this.district, newDetail);
}
}
```
#### 1.5 领域事件(Domain Event)规范
1. **基本要求**
- 必须放在 event 包下
- 事件类名以 Event 结尾
- 事件必须是不可变的
- 包含事件发生时间和相关数据
2. **代码规范**
```java
@Value
public class UserCreatedEvent {
String userId;
String username;
LocalDateTime occurredTime;
public static UserCreatedEvent of(User user) {
return new UserCreatedEvent(
user.getUserId(),
user.getUsername(),
LocalDateTime.now()
);
}
}
```
#### 1.6 领域异常(Domain Exception)规范
1. **基本要求**
- 必须放在 trouble 包下
- 使用 TroubleEnum 定义异常
- 异常码要有明确的业务含义
2. **代码规范**
```java
@TroublePrefix("USER")
public enum UserTrouble implements TroubleEnum {
@Trouble(
code = "001",
message = "用户不存在"
)
USER_NOT_FOUND,
@Trouble(
code = "002",
message = "用户名已被使用",
status = HttpStatus.CONFLICT
)
USERNAME_ALREADY_EXISTS,
@Trouble(
code = "003",
message = "无效的状态转换"
)
INVALID_STATUS_TRANSITION
}
```
#### 1.7 验证器(Validator)规范
1. **基本要求**
- 必须实现 CongestiveValidator 接口
- 必须使用 @Component 注解
- 验证方法必须显式声明 throws CongestiveNotValidException
- 验证失败必须抛出 CongestiveNotValidException
2. **代码规范**
```java
@Component
public class UserValidator implements CongestiveValidator {
@Override
public void validateSave(User user) throws CongestiveNotValidException {
// 验证用户名唯一性
user.repository().findByAttribute("username", user.getUsername())
.filter(t -> !t.getUserId().equals(user.getUserId()))
.ifPresent(t -> {
throw new CongestiveNotValidException("用户名已存在");
});
// 验证联系方式
if (user.getContactInfos().isEmpty()) {
throw new CongestiveNotValidException("至少需要一个联系方式");
}
}
@Override
public void validateDelete(User user) throws CongestiveNotValidException {
if (user.getStatus() == UserStatus.ACTIVE) {
throw new CongestiveNotValidException("活跃用户不能删除");
}
}
}
```
#### 1.8 仓储接口(Repository)规范
1. **基本要求**
- 必须继承 CongestiveRepository
- 必须使用 @Repository 注解
- 优先使用框架提供的通用方法
2. **代码规范**
```java
@Repository
public interface UserRepository extends CongestiveRepository {
// 使用框架提供的通用方法
default Optional findByUsername(String username) {
return findByAttribute("username", username);
}
// 自定义复杂查询
@Query("SELECT u FROM User u " +
"LEFT JOIN FETCH u.contactInfos " +
"WHERE u.status = :status")
List findActiveUsersWithContacts(@Param("status") UserStatus status);
}
```
#### 1.9 最佳实践
1. **聚合设计**
- 保持聚合尽可能小
- 通过业务边界识别聚合
- 避免跨聚合的实时一致性
2. **实体设计**
- 实体应该是充血模型
- 避免公共setter
- 通过方法表达业务含义
3. **值对象设计**
- 优先使用值对象
- 确保不可变性
- 通过方法返回新实例
4. **仓储设计**
- 优先使用框架提供的通用方法
- 复杂查询考虑使用QueryService
- 避免在仓储中包含业务逻辑
5. **异常处理**
- 使用领域异常表达业务规则
- 异常信息应该对用户友好
- 合理使用HTTP状态码
### 2. 应用层开发规范
#### 2.1 包结构规范
应用层的包名是application, 是根包的子目录。
应用层的包下按照应用模型来划分模块, 每个应用模型一个包。
应用模型的包下必须包含以下目录:
```
application.{module}
├── controller # 控制器
├── service # 应用服务
│ └── impl # 应用服务实现
├── criteria # 查询条件
├── request # 请求对象
├── result # 结果对象
└── assembler # 对象转换器
```
#### 2.2 控制器(Controller)规范
1. **基本要求**
- 必须使用 @RestController 注解
- 必须使用 @RequestMapping 定义基础路径
- 路径命名必须使用连字符(kebab-case)
- 返回结构必须使用 Scorpio 的 RestResult 统一返回格式
- 必须使用 SpringDoc 注解完整描述 API 文档
- 类和方法必须有清晰的注释
2. **SpringDoc 注解规范**
- 类上必须使用 @Tag 注解标注模块名称
- 方法上必须使用 @Operation 注解描述接口功能
- 请求参数必须使用 @Parameter 注解描述参数含义
- 分页参数必须使用 @PageableAsQueryParam 注解描述
3. **代码规范**
```java
@Tag(name = "用户管理", description = "用户相关的 API 接口")
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserApplicationService userService;
private final UserQueryService userQueryService;
@Operation(summary = "创建用户",
description = "创建一个新的用户,包括基本信息和联系方式")
@PostMapping
public ResponseEntity createUser(
@RequestBody @Valid CreateUserRequest request
) {
UserResult result = userService.createUser(request);
return RestResult.ok(result);
}
@Operation(summary = "获取用户详情",
description = "根据用户ID获取用户的详细信息")
@GetMapping("/{userId}")
public ResponseEntity getUser(
@Parameter(description = "用户ID", example = "USER_123456")
@PathVariable String userId
) {
UserResult result = userService.getUser(userId);
return RestResult.ok(result);
}
@Operation(summary = "查询用户列表",
description = "根据条件分页查询用户列表")
@PageableAsQueryParam
@GetMapping
public ResponseEntity> queryUsers(
@Parameter(description = "查询条件")
UserCriteria criteria,
@Parameter(hidden = true)
Pageable pageable
) {
Page page = userQueryService.queryUsers(criteria, pageable);
return RestResult.ok(page);
}
@Operation(summary = "更新用户状态",
description = "更新指定用户的状态")
@PutMapping("/{userId}/status")
public ResponseEntity updateUserStatus(
@Parameter(description = "用户ID", example = "USER_123456")
@PathVariable String userId,
@RequestBody @Valid UpdateUserStatusRequest request
) {
UserResult result = userService.updateUserStatus(userId, request);
return RestResult.ok(result);
}
@Operation(summary = "删除用户",
description = "删除指定的用户")
@DeleteMapping("/{userId}")
public ResponseEntity> deleteUser(
@Parameter(description = "用户ID", example = "USER_123456")
@PathVariable String userId
) {
userService.deleteUser(userId);
return RestResult.noContent();
}
}
```
4. **请求对象的文档注解**
```java
@Schema(description = "创建用户请求")
@Data
public class CreateUserRequest implements Serializable {
@Schema(description = "用户名", example = "zhangsan")
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;
@Schema(description = "省份", example = "浙江省")
@NotBlank(message = "省份不能为空")
private String province;
@Schema(description = "城市", example = "杭州市")
@NotBlank(message = "城市不能为空")
private String city;
@Schema(description = "区县", example = "西湖区")
@NotBlank(message = "区县不能为空")
private String district;
@Schema(description = "详细地址", example = "文一西路998号")
@NotBlank(message = "详细地址不能为空")
private String detail;
@Schema(description = "联系方式列表")
@NotEmpty(message = "至少需要一个联系方式")
private List contactInfos;
}
```
5. **结果对象的文档注解**
```java
@Schema(description = "用户信息结果")
@Data
public class UserResult implements Serializable {
@Schema(description = "用户ID", example = "USER_123456")
private String userId;
@Schema(description = "用户名", example = "zhangsan")
private String username;
@Schema(description = "用户状态", example = "ACTIVE")
private UserStatus status;
@Schema(description = "地址信息")
private AddressResult address;
@Schema(description = "联系方式列表")
private List contactInfos;
@Schema(description = "创建时间", example = "2024-01-01T12:00:00")
private LocalDateTime createdTime;
@Schema(description = "更新时间", example = "2024-01-01T12:00:00")
private LocalDateTime updatedTime;
}
```
#### 2.3 应用服务(Application Service)规范
1. **基本要求**
- 接口必须以 ApplicationService 结尾
- 实现类必须放在 impl 包下
- 实现类必须使用 @Service 注解
- 必须使用构造器注入依赖
- 必须添加事务注解
2. **代码规范**
```java
public interface UserApplicationService {
UserResult createUser(CreateUserRequest request);
UserResult getUser(String userId);
UserResult updateUserStatus(String userId, UpdateUserStatusRequest request);
void deleteUser(String userId);
}
@Service
@RequiredArgsConstructor
public class UserApplicationServiceImpl implements UserApplicationService {
private final UserAssembler assembler;
@Transactional
@Override
public UserResult createUser(CreateUserRequest request) {
// 1. 转换请求对象到领域对象
Address address = Address.of(
request.getProvince(),
request.getCity(),
request.getDistrict(),
request.getDetail()
);
// 2. 调用领域服务或聚合根方法
User user = User.create(request.getUsername(), address);
// 3. 添加联系方式
request.getContactInfos().forEach(contact -> {
ContactInfo contactInfo = ContactInfo.create(
contact.getType(),
contact.getValue()
);
user.addContactInfo(contactInfo);
});
// 4. 保存并返回结果
user.save();
return assembler.toResult(user);
}
@Transactional(readOnly = true)
@Override
public UserResult getUser(String userId) {
User user = Congestive.get(userId, User.class);
return assembler.toResult(user);
}
}
```
#### 2.4 查询服务(Query Service)规范
1. **基本要求**
- 接口必须以 QueryService 结尾
- 实现类必须放在 impl 包下
- 实现类必须使用 @Service 注解
- 必须使用构造器注入依赖
- 必须使用 @Transactional(readOnly = true) 注解
- 方法名必须以 query 开头
2. **代码规范**
```java
public interface UserQueryService {
Page queryUsers(UserCriteria criteria, Pageable pageable);
List queryUsersByStatus(UserStatus status);
Optional queryUserByPhone(String phoneNo);
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserQueryServiceImpl implements UserQueryService {
private final QueryExpressionService> queryService;
private final UserAssembler assembler;
@Override
public Page queryUsers(UserCriteria criteria, Pageable pageable) {
Page userPage = queryService.queryPage(criteria, pageable);
return assembler.toUserResultPage(userPage);
}
@Override
public List queryUsersByStatus(UserStatus status) {
// 使用 Congestive 提供的静态查询方法
List users = Congestive.findAllByAttribute(
User_.status,
status,
User.class
);
return assembler.toUserResults(users);
}
@Override
public Optional queryUserByPhone(String phoneNo) {
// 使用 Congestive 提供的静态查询方法
return Congestive.findByAttribute(User_.phoneNo, phoneNo, User.class)
.map(assembler::toUserResult);
}
}
```
3. **查询方法规范**
- 简单查询优先使用 Congestive 提供的静态查询方法
- 复杂查询使用 QueryExpressionService
- 所有查询方法必须返回 Result 对象,不能返回领域对象
- 查询条件统一使用 Criteria 对象
- 支持分页的查询必须使用 Pageable 参数
4. **性能优化规范**
- 查询必须使用 readOnly 事务
- 避免 N+1 查询问题
- 合理使用索引
- 大数据量查询必须使用分页
- 需要时使用 @QueryHints 优化查询
5. **代码示例 - 复杂查询**
```java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderQueryServiceImpl implements OrderQueryService {
private final QueryExpressionService> queryService;
private final OrderAssembler assembler;
@Override
@QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true"))
public Page queryOrders(OrderCriteria criteria, Pageable pageable) {
// 1. 创建基础查询
JpaQueryExpression expression = queryService.createExpression(criteria);
// 2. 添加关联查询
expression.addJoinFetch("items", JoinType.LEFT);
expression.addJoinFetch("buyer", JoinType.LEFT);
// 3. 添加额外条件
if (criteria.getNeedItems()) {
expression.addPredicate(
Order_.items.isNotEmpty()
);
}
// 4. 执行查询并转换结果
Page orderPage = queryService.queryPage(expression, pageable);
return assembler.toOrderResultPage(orderPage);
}
@Override
public List queryOrderStatistics(
OrderStatisticsCriteria criteria
) {
// 1. 创建统计查询
JpaQueryExpression expression = queryService.createExpression(criteria);
// 2. 设置分组和聚合
expression.addGroupBy(Order_.status);
expression.addSelection(Order_.status);
expression.addSelection(
"count(*)",
Long.class,
"orderCount"
);
expression.addSelection(
"sum(totalAmount)",
BigDecimal.class,
"totalAmount"
);
// 3. 执行查询并转换结果
List tuples = queryService.queryList(expression, Tuple.class);
return assembler.toOrderStatisticsResults(tuples);
}
}
```
6. **最佳实践**
- 查询服务只负责数据查询,不包含业务逻辑
- 复杂查询条件应该封装在 Criteria 对象中
- 统计查询应该使用专门的结果对象
- 查询方法应该是幂等的
- 合理使用缓存提升查询性能
- 大数据量的统计查询考虑使用异步处理
- 关联查询时注意性能影响
#### 2.5 查询条件(Criteria)规范
1. **基本要求**
- 类名必须以 Criteria 结尾
- 必须使用 @Expression 注解定义查询条件
- 必须实现 Serializable 接口
- 必须使用 @Data 注解
- 必须使用Spring Doc 注解对参数进行描述
- 每一个聚合根对应一个Criteria类
2. **代码规范**
```java
@Data
public class UserCriteria implements Serializable {
@Expression(
path = @Path("username"),
operator = Operator.CONTAINS
)
private String usernameKeyword;
@Expression(path = @Path("status"))
private UserStatus status;
@Expression(
path = @Path(
attribute = "province",
embed = @Embed(attribute = "address")
)
)
private String province;
@Expression(
path = @Path(
attribute = "city",
embed = @Embed(attribute = "address")
)
)
private String city;
}
```
#### 2.6 请求对象(Request)规范
1. **基本要求**
- 类名必须以 Request 结尾
- 必须使用 validation 注解进行参数校验
- 必须实现 Serializable 接口
- 必须使用 @Data 注解
- 必须使用 Spring Doc 注解对参数进行描述
2. **代码规范**
```java
@Data
public class CreateUserRequest implements Serializable {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
@NotBlank(message = "区县不能为空")
private String district;
@NotBlank(message = "详细地址不能为空")
private String detail;
@NotEmpty(message = "至少需要一个联系方式")
private List contactInfos;
}
@Data
public class ContactInfoRequest implements Serializable {
@NotBlank(message = "联系方式类型不能为空")
private String type;
@NotBlank(message = "联系方式不能为空")
private String value;
}
```
#### 2.7 结果对象(Result)规范
1. **基本要求**
- 类名必须以 Result 结尾
- 必须实现 Serializable 接口
- 必须使用 @Data 注解
- 应该只包含需要返回的数据
- 必须使用Spring Doc 注解对参数进行描述
2. **代码规范**
```java
@Data
public class UserResult implements Serializable {
private String userId;
private String username;
private UserStatus status;
private AddressResult address;
private List contactInfos;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
}
@Data
public class AddressResult implements Serializable {
private String province;
private String city;
private String district;
private String detail;
}
@Data
public class ContactInfoResult implements Serializable {
private String type;
private String value;
}
```
#### 2.8 对象转换器(Assembler)规范
1. **基本要求**
- 类名必须以 Assembler 结尾
- 必须使用 @Component 注解
- 必须提供领域对象到结果对象的转换方法
- 建议使用 MapStruct 框架
- 方法名必须使用 "to + 返回类型" 的形式
- 列表转换方法使用复数形式
2. **代码规范**
```java
@Mapper(componentModel = "spring")
public interface UserAssembler {
// 单个对象转换
UserResult toUserResult(User user);
AddressResult toAddressResult(Address address);
ContactInfoResult toContactInfoResult(ContactInfo contactInfo);
// 列表转换
List toUserResults(List users);
List toContactInfoResults(List contactInfos);
// 分页转换
default Page toUserResultPage(Page page) {
return page.map(this::toUserResult);
}
// 请求对象转换
Address toAddress(CreateUserRequest request);
ContactInfo toContactInfo(ContactInfoRequest request);
List toContactInfos(List requests);
}
```
#### 2.9 最佳实践
1. **控制器设计**
- 保持控制器简单,只负责请求处理和响应转换
- 使用统一的响应格式
- 合理使用 HTTP 方法和状态码
- 提供清晰的 API 文档
2. **应用服务设计**
- 应用服务方法应该对应一个完整的用例
- 保持事务边界的正确性
- 不包含业务规则,只负责协调
- 处理好异常转换
3. **查询条件设计**
- 合理使用查询操作符
- 考虑查询性能
- 提供必要的查询条件组合
4. **请求对象设计**
- 提供完整的参数验证
- 使用嵌套对象表达复杂结构
- 考虑向后兼容性
5. **结果对象设计**
- 只返回必要的数据
- 考虑数据敏感性
- 提供合适的序列化配置
6. **转换器设计**
- 使用 MapStruct 提高开发效率
- 处理好空值转换
- 提供批量转换方法
### 3. 基础设施层开发规范
#### 3.1 包结构规范
基础设施层的包名是infrastructure,是根包的子目录。
包含以下目录:
```
infrastructure
├── config # 配置类
├── persistence # 持久化实现
│ └── repository # 仓储实现类
├── integration # 外部系统集成
├── common # 公共组件
│ ├── utils # 工具类
│ └── constants # 常量定义
└── security # 安全相关
```