📋 概述
本文档详细描述了大型企业级 ERP 系统中复杂订单聚合根的设计和实现,展示了如何使用 Atlas Mapper 处理深度嵌套、循环引用和大数据量的复杂业务对象。
🏗️ 订单聚合根架构
领域模型设计图
classDiagram
class OrderAggregate {
+Long id
+String orderNumber
+OrderStatus status
+LocalDateTime orderDate
+BigDecimal totalAmount
+Customer customer
+OrderLine[] lines
+PaymentInfo payment
+ShippingInfo shipping
+OrderEvent[] events
+AuditInfo auditInfo
}
class Customer {
+Long customerId
+String customerCode
+String companyName
+CustomerType type
+Address billingAddress
+Address shippingAddress
+OrderAggregate[] orders
+CustomerCredit credit
}
class OrderLine {
+Long lineId
+Integer lineNumber
+Product product
+BigDecimal quantity
+BigDecimal unitPrice
+BigDecimal lineAmount
+OrderAggregate order
+InventoryReservation reservation
+OrderLineEvent[] events
}
class Product {
+Long productId
+String productCode
+String productName
+ProductCategory category
+ProductAttribute[] attributes
+PricingInfo pricing
+InventoryInfo inventory
}
class PaymentInfo {
+Long paymentId
+PaymentMethod method
+PaymentStatus status
+BigDecimal amount
+LocalDateTime paymentDate
+PaymentTransaction[] transactions
}
class ShippingInfo {
+Long shippingId
+ShippingMethod method
+ShippingStatus status
+Address shippingAddress
+LocalDateTime estimatedDelivery
+ShippingEvent[] trackingEvents
}
OrderAggregate --> OrderLine : contains
OrderAggregate --> Customer : belongs_to
OrderAggregate --> PaymentInfo : has
OrderAggregate --> ShippingInfo : has
OrderLine --> Product : references
Customer --> OrderAggregate : places
📊 核心实体类设计
1. 订单聚合根 (OrderAggregate)
java
package io.github.nemoob.atlas.mapper.example.domain;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.ArrayList;
/**
* 订单聚合根 - 企业级复杂业务对象
*
* 特点:
* 1. 深度嵌套关系(3-4层)
* 2. 双向关联和潜在循环引用
* 3. 大数据量(单个订单可能包含数百个行项目)
* 4. 复杂业务逻辑和状态管理
*/
@Entity
@Table(name = "orders")
@Data
@EqualsAndHashCode(exclude = {"customer", "lines"}) // 避免循环引用
@ToString(exclude = {"customer", "lines"}) // 避免循环引用
public class OrderAggregate {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false, length = 50)
private String orderNumber;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status;
@Column(nullable = false)
private LocalDateTime orderDate;
@Column(precision = 15, scale = 2)
private BigDecimal totalAmount;
@Column(precision = 15, scale = 2)
private BigDecimal taxAmount;
@Column(precision = 15, scale = 2)
private BigDecimal discountAmount;
// 客户信息 - 多对一关系
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
// 订单行项目 - 一对多关系
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<OrderLine> lines = new ArrayList<>();
// 支付信息 - 一对一关系
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "payment_id")
private PaymentInfo payment;
// 物流信息 - 一对一关系
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "shipping_id")
private ShippingInfo shipping;
// 订单事件历史 - 一对多关系
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@OrderBy("eventTime DESC")
private List<OrderEvent> events = new ArrayList<>();
// 审计信息
@Embedded
private AuditInfo auditInfo;
// 多租户支持
@Column(name = "tenant_id", nullable = false)
private Long tenantId;
// 业务方法
public void addOrderLine(OrderLine line) {
line.setOrder(this);
this.lines.add(line);
recalculateTotal();
}
public void removeOrderLine(OrderLine line) {
line.setOrder(null);
this.lines.remove(line);
recalculateTotal();
}
private void recalculateTotal() {
this.totalAmount = lines.stream()
.map(OrderLine::getLineAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public boolean isCompleted() {
return OrderStatus.COMPLETED.equals(this.status);
}
public boolean canBeCancelled() {
return OrderStatus.PENDING.equals(this.status) ||
OrderStatus.CONFIRMED.equals(this.status);
}
}
/**
* 订单状态枚举
*/
public enum OrderStatus {
DRAFT("草稿"),
PENDING("待确认"),
CONFIRMED("已确认"),
IN_PROGRESS("处理中"),
SHIPPED("已发货"),
DELIVERED("已送达"),
COMPLETED("已完成"),
CANCELLED("已取消"),
REFUNDED("已退款");
private final String description;
OrderStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
2. 客户实体 (Customer)
java
package io.github.nemoob.atlas.mapper.example.domain;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.persistence.*;
import java.util.List;
import java.util.ArrayList;
/**
* 客户实体 - 包含与订单的双向关联
*/
@Entity
@Table(name = "customers")
@Data
@EqualsAndHashCode(exclude = {"orders"}) // 避免循环引用
@ToString(exclude = {"orders"}) // 避免循环引用
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long customerId;
@Column(unique = true, nullable = false, length = 20)
private String customerCode;
@Column(nullable = false, length = 200)
private String companyName;
@Column(length = 100)
private String contactPerson;
@Column(length = 50)
private String contactPhone;
@Column(length = 100)
private String contactEmail;
@Enumerated(EnumType.STRING)
private CustomerType type;
@Enumerated(EnumType.STRING)
private CustomerLevel level;
// 账单地址
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "country", column = @Column(name = "billing_country")),
@AttributeOverride(name = "province", column = @Column(name = "billing_province")),
@AttributeOverride(name = "city", column = @Column(name = "billing_city")),
@AttributeOverride(name = "district", column = @Column(name = "billing_district")),
@AttributeOverride(name = "street", column = @Column(name = "billing_street")),
@AttributeOverride(name = "postalCode", column = @Column(name = "billing_postal_code"))
})
private Address billingAddress;
// 收货地址
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "country", column = @Column(name = "shipping_country")),
@AttributeOverride(name = "province", column = @Column(name = "shipping_province")),
@AttributeOverride(name = "city", column = @Column(name = "shipping_city")),
@AttributeOverride(name = "district", column = @Column(name = "shipping_district")),
@AttributeOverride(name = "street", column = @Column(name = "shipping_street")),
@AttributeOverride(name = "postalCode", column = @Column(name = "shipping_postal_code"))
})
private Address shippingAddress;
// 客户订单 - 一对多关系(潜在循环引用)
@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
private List<OrderAggregate> orders = new ArrayList<>();
// 客户信用信息
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "credit_id")
private CustomerCredit credit;
// 审计信息
@Embedded
private AuditInfo auditInfo;
// 多租户支持
@Column(name = "tenant_id", nullable = false)
private Long tenantId;
}
/**
* 客户类型枚举
*/
public enum CustomerType {
INDIVIDUAL("个人客户"),
ENTERPRISE("企业客户"),
GOVERNMENT("政府客户"),
PARTNER("合作伙伴");
private final String description;
CustomerType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 客户等级枚举
*/
public enum CustomerLevel {
BRONZE("青铜"),
SILVER("白银"),
GOLD("黄金"),
PLATINUM("铂金"),
DIAMOND("钻石");
private final String description;
CustomerLevel(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
3. 订单行项目 (OrderLine)
java
package io.github.nemoob.atlas.mapper.example.domain;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;
/**
* 订单行项目 - 包含产品信息和库存预留
*/
@Entity
@Table(name = "order_lines")
@Data
@EqualsAndHashCode(exclude = {"order"}) // 避免循环引用
@ToString(exclude = {"order"}) // 避免循环引用
public class OrderLine {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long lineId;
@Column(nullable = false)
private Integer lineNumber;
@Column(precision = 15, scale = 4)
private BigDecimal quantity;
@Column(precision = 15, scale = 2)
private BigDecimal unitPrice;
@Column(precision = 15, scale = 2)
private BigDecimal lineAmount;
@Column(precision = 15, scale = 2)
private BigDecimal discountAmount;
@Column(precision = 5, scale = 4)
private BigDecimal discountRate;
// 所属订单 - 多对一关系
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private OrderAggregate order;
// 产品信息 - 多对一关系
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
// 库存预留信息 - 一对一关系
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "reservation_id")
private InventoryReservation reservation;
// 行项目事件历史
@OneToMany(mappedBy = "orderLine", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@OrderBy("eventTime DESC")
private List<OrderLineEvent> events = new ArrayList<>();
// 多租户支持
@Column(name = "tenant_id", nullable = false)
private Long tenantId;
// 业务方法
@PrePersist
@PreUpdate
private void calculateLineAmount() {
if (quantity != null && unitPrice != null) {
BigDecimal grossAmount = quantity.multiply(unitPrice);
BigDecimal discount = discountAmount != null ? discountAmount : BigDecimal.ZERO;
this.lineAmount = grossAmount.subtract(discount);
}
}
public boolean isReserved() {
return reservation != null && reservation.isActive();
}
public void applyDiscount(BigDecimal discountRate) {
this.discountRate = discountRate;
this.discountAmount = unitPrice.multiply(quantity).multiply(discountRate);
calculateLineAmount();
}
}
4. 产品实体 (Product)
java
package io.github.nemoob.atlas.mapper.example.domain;
import lombok.Data;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;
/**
* 产品实体 - 包含复杂的产品属性和定价信息
*/
@Entity
@Table(name = "products")
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long productId;
@Column(unique = true, nullable = false, length = 50)
private String productCode;
@Column(nullable = false, length = 200)
private String productName;
@Column(length = 1000)
private String description;
@Column(length = 50)
private String brand;
@Column(length = 50)
private String model;
@Column(length = 20)
private String unit;
@Enumerated(EnumType.STRING)
private ProductStatus status;
// 产品分类 - 多对一关系
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private ProductCategory category;
// 产品属性 - 一对多关系
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<ProductAttribute> attributes = new ArrayList<>();
// 定价信息 - 一对一关系
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "pricing_id")
private PricingInfo pricing;
// 库存信息 - 一对一关系
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "inventory_id")
private InventoryInfo inventory;
// 审计信息
@Embedded
private AuditInfo auditInfo;
// 多租户支持
@Column(name = "tenant_id", nullable = false)
private Long tenantId;
// 业务方法
public boolean isAvailable() {
return ProductStatus.ACTIVE.equals(this.status) &&
inventory != null && inventory.getAvailableQuantity().compareTo(BigDecimal.ZERO) > 0;
}
public BigDecimal getCurrentPrice() {
return pricing != null ? pricing.getCurrentPrice() : BigDecimal.ZERO;
}
}
/**
* 产品状态枚举
*/
public enum ProductStatus {
DRAFT("草稿"),
ACTIVE("激活"),
INACTIVE("停用"),
DISCONTINUED("停产");
private final String description;
ProductStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
🔧 支持类和值对象
1. 地址值对象 (Address)
java
package io.github.nemoob.atlas.mapper.example.domain;
import lombok.Data;
import javax.persistence.Embeddable;
import javax.persistence.Column;
/**
* 地址值对象 - 可嵌入的复合值类型
*/
@Embeddable
@Data
public class Address {
@Column(length = 50)
private String country;
@Column(length = 50)
private String province;
@Column(length = 50)
private String city;
@Column(length = 50)
private String district;
@Column(length = 200)
private String street;
@Column(length = 20)
private String postalCode;
@Column(precision = 10, scale = 6)
private BigDecimal latitude;
@Column(precision = 10, scale = 6)
private BigDecimal longitude;
// 业务方法
public String getFullAddress() {
return String.format("%s %s %s %s %s",
country != null ? country : "",
province != null ? province : "",
city != null ? city : "",
district != null ? district : "",
street != null ? street : "").trim();
}
public boolean isComplete() {
return country != null && province != null &&
city != null && street != null;
}
}
2. 审计信息 (AuditInfo)
java
package io.github.nemoob.atlas.mapper.example.domain;
import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
/**
* 审计信息 - 记录创建和修改信息
*/
@Embeddable
@Data
public class AuditInfo {
@Column(name = "created_by", length = 50)
private String createdBy;
@Column(name = "created_time")
private LocalDateTime createdTime;
@Column(name = "updated_by", length = 50)
private String updatedBy;
@Column(name = "updated_time")
private LocalDateTime updatedTime;
@Version
@Column(name = "version")
private Long version;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
this.createdTime = now;
this.updatedTime = now;
// 在实际应用中,应该从安全上下文获取当前用户
this.createdBy = getCurrentUser();
this.updatedBy = getCurrentUser();
}
@PreUpdate
public void preUpdate() {
this.updatedTime = LocalDateTime.now();
this.updatedBy = getCurrentUser();
}
private String getCurrentUser() {
// 实际实现中应该从 Spring Security 上下文获取
return "system";
}
}
📊 DTO 设计
1. 订单聚合根 DTO
java
package io.github.nemoob.atlas.mapper.example.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 订单聚合根 DTO - 用于 API 响应
*/
@Data
public class OrderAggregateDto {
private Long id;
private String orderNumber;
private String status;
private String statusDescription;
private LocalDateTime orderDate;
private BigDecimal totalAmount;
private BigDecimal taxAmount;
private BigDecimal discountAmount;
// 客户信息(简化版本,避免循环引用)
private CustomerSummaryDto customer;
// 订单行项目
private List<OrderLineDto> lines;
// 支付信息
private PaymentInfoDto payment;
// 物流信息
private ShippingInfoDto shipping;
// 审计信息
private AuditInfoDto auditInfo;
// 计算字段
private Integer totalLines;
private BigDecimal averageLineAmount;
private Boolean canBeCancelled;
private Boolean isCompleted;
}
/**
* 客户摘要 DTO - 避免循环引用的简化版本
*/
@Data
public class CustomerSummaryDto {
private Long customerId;
private String customerCode;
private String companyName;
private String contactPerson;
private String type;
private String level;
private AddressDto billingAddress;
}
/**
* 订单行项目 DTO
*/
@Data
public class OrderLineDto {
private Long lineId;
private Integer lineNumber;
private BigDecimal quantity;
private BigDecimal unitPrice;
private BigDecimal lineAmount;
private BigDecimal discountAmount;
private BigDecimal discountRate;
// 产品信息(简化版本)
private ProductSummaryDto product;
// 库存预留状态
private Boolean isReserved;
private LocalDateTime reservationExpiry;
}
/**
* 产品摘要 DTO
*/
@Data
public class ProductSummaryDto {
private Long productId;
private String productCode;
private String productName;
private String brand;
private String model;
private String unit;
private String status;
private BigDecimal currentPrice;
private BigDecimal availableQuantity;
}
2. 分页和查询 DTO
java
package io.github.nemoob.atlas.mapper.example.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 订单查询 DTO - 用于列表查询和分页
*/
@Data
public class OrderQueryDto {
private Long id;
private String orderNumber;
private String status;
private LocalDateTime orderDate;
private BigDecimal totalAmount;
private String customerName;
private String customerCode;
private Integer totalLines;
private LocalDateTime createdTime;
private String createdBy;
}
/**
* 订单统计 DTO - 用于报表和仪表板
*/
@Data
public class OrderStatisticsDto {
private Long totalOrders;
private BigDecimal totalAmount;
private BigDecimal averageOrderAmount;
private Long completedOrders;
private Long pendingOrders;
private Long cancelledOrders;
private BigDecimal completionRate;
private BigDecimal cancellationRate;
}
/**
* 分页响应 DTO
*/
@Data
public class PageResponseDto<T> {
private List<T> content;
private Integer pageNumber;
private Integer pageSize;
private Long totalElements;
private Integer totalPages;
private Boolean first;
private Boolean last;
private Boolean hasNext;
private Boolean hasPrevious;
}
🎯 设计要点总结
1. 循环引用处理
- @EqualsAndHashCode(exclude = {...}):排除可能导致循环引用的字段
- @ToString(exclude = {...}):避免 toString 方法中的循环引用
- DTO 设计:使用简化的摘要 DTO 避免完整对象图的循环引用
2. 性能优化考虑
- 懒加载 :使用
FetchType.LAZY
避免不必要的数据加载 - 批量操作:设计支持批量处理的方法和查询
- 索引设计:在关键查询字段上建立数据库索引
- 分页支持:提供分页查询避免大数据量问题
3. 多租户支持
- 租户隔离 :每个实体都包含
tenantId
字段 - 数据安全:确保跨租户数据不会泄露
- 性能考虑:租户字段参与索引设计
4. 审计和追踪
- 审计信息:统一的创建和修改时间记录
- 版本控制 :使用
@Version
支持乐观锁 - 事件历史:记录重要业务操作的历史轨迹
5. 业务规则封装
- 领域方法:在实体中封装业务逻辑
- 状态管理:通过枚举和方法管理复杂状态
- 数据一致性:通过 JPA 回调确保数据一致性