Atlas Mapper 案例 03:企业级订单实体设计文档

📋 概述

本文档详细描述了大型企业级 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 回调确保数据一致性

本文档展示了如何设计复杂的企业级领域模型,为 Atlas Mapper 的高级映射功能提供了丰富的测试场景。

相关推荐
咖啡Beans2 小时前
使用MapStruct映射对象属性
java·spring boot
许雪里2 小时前
XXL-TOOL v2.1.0 发布 | Java工具类库
后端·github·代码规范
杨杨杨大侠2 小时前
手把手教你写 httpclient 框架(二)- 核心注解系统设计与实现
java·开源·github
vker3 小时前
第 2 天:工厂方法模式(Factory Method Pattern)—— 创建型模式
java·后端·设计模式
准时睡觉3 小时前
SpringSecurity的使用
java·后端
绝无仅有3 小时前
面试经验之mysql高级问答深度解析
后端·面试·github
绝无仅有3 小时前
Java技术复试面试:全面解析
后端·面试·github
对不起初见3 小时前
如何在后端优雅地生成并传递动态错误提示?
java·spring boot
tingyu3 小时前
JAXB 版本冲突踩坑记:SPI 项目中的 XML 处理方案升级
java