案例03-附件A-订单实体设计

📋 概述

本文档详细描述了大型企业级 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 回调确保数据一致性
相关推荐
杨杨杨大侠2 小时前
案例03-附件C-性能优化
java·开源·github
杨杨杨大侠2 小时前
案例03-附件D-监控系统
java·开源·github
少女续续念2 小时前
国产 DevOps 崛起!Gitee 领衔构建合规、高效的企业协作工具链
git·开源
uhakadotcom3 小时前
什么是OpenTelemetry?
后端·面试·github
少女续续念5 小时前
AI 不再是 “旁观者”!Gitee MCP Server 让智能助手接管代码仓库管理
git·开源
华仔啊5 小时前
主线程存了用户信息,子线程居然拿不到?ThreadLocal 背锅
java·后端
间彧5 小时前
Spring Boot项目中,Redis 如何同时执行多条命令
java·redis