一、报价单实体设计:一切从这里开始

报价单,说白了就是整个系统的「门面」。我做了这么多年报价系统,发现很多团队一上来就堆字段,结果后期维护成本高得吓人。今天咱们就聊聊,一个靠谱的报价单实体到底长什么样。

1.1 核心字段设计

我个人习惯把报价单字段分成三组:身份信息业务信息状态信息。这样设计,后期扩展起来特别方便。

字段分类 字段名 类型 说明
身份信息 quote_id UUID 全局唯一标识
quote_number VARCHAR(32) 业务编号,如 Q20240101-001
version INT 版本号,用于乐观锁
业务信息 customer_id UUID 客户ID
supplier_id UUID 供应商ID
currency VARCHAR(3) 币种,如 CNY/USD
total_amount DECIMAL(18,2) 报价总金额
状态信息 status VARCHAR(20) 草稿/已发送/已确认/已过期
valid_from DATETIME 有效期开始
valid_until DATETIME 有效期截止
我的经验:quote_number 一定要设计成可读的格式。我曾经见过一个系统用纯 UUID 做编号,业务人员查单子时直接崩溃。后来改成「Q+日期+序号」,大家都说好。

1.2 状态机设计

报价单的状态流转,其实是个典型的有限状态机。我建议用枚举来管理,别用字符串硬编码。

public enum QuoteStatus {
    DRAFT("草稿"),
    PENDING_APPROVAL("待审批"),
    APPROVED("已审批"),
    SENT("已发送"),
    CONFIRMED("已确认"),
    EXPIRED("已过期"),
    CANCELLED("已取消");

    private String description;
    // 构造方法、getter 省略
}
注意:状态流转一定要做校验。比如「已过期」的报价单不能直接变成「已确认」。我曾经在项目中遇到过这种 bug,客户拿着过期的报价单要求按原价执行,差点出大问题。

二、行项目模型:报价单的骨架

行项目,就是报价单里每一行的明细。你想想看,一份报价单可能包含几十甚至上百个产品,每个产品的数量、单价、折扣都不一样。怎么设计才能既灵活又高效?

2.1 行项目核心结构

我一般把行项目设计成「头-行」结构。报价单是头,行项目是行。这样设计的好处是:一个报价单可以包含多种类型的产品,而且每种产品可以独立计算价格。

public class LineItem {
    private String lineItemId;      // 行项目ID
    private String quoteId;         // 所属报价单ID
    private Integer lineNumber;     // 行号,用于排序
    private String productId;       // 产品ID
    private String productName;     // 产品名称(冗余存储)
    private Integer quantity;       // 数量
    private BigDecimal unitPrice;   // 单价
    private BigDecimal discount;    // 折扣率,如 0.1 表示10%
    private BigDecimal lineTotal;   // 行总价 = 数量 * 单价 * (1 - 折扣)
    private String remark;          // 备注
}
关键点:productName 为什么要冗余存储?因为产品信息可能会变。如果客户拿着历史报价单来对账,发现产品名称变了,那可就说不清了。所以行项目里要保留一份快照。

2.2 行项目计算逻辑

行总价的计算,看起来简单,其实坑不少。我见过很多新手直接写 quantity * unitPrice * (1 - discount),但遇到精度问题就傻眼了。

正确的做法是:

public BigDecimal calculateLineTotal() {
    // 使用 BigDecimal 避免浮点精度问题
    BigDecimal qty = new BigDecimal(this.quantity);
    BigDecimal price = this.unitPrice;
    BigDecimal discountFactor = BigDecimal.ONE.subtract(this.discount);
    
    // 先乘数量,再乘折扣因子
    return qty.multiply(price).multiply(discountFactor)
              .setScale(2, RoundingMode.HALF_UP);
}
避坑指南:我曾经在项目中遇到过一个问题:折扣率存的是 0.1,但计算时用了 0.1f,结果浮点精度导致总价差了 0.01 元。后来统一用 BigDecimal,再也没出过这种问题。

三、价格策略模型:报价系统的灵魂

价格策略,说白了就是「怎么算钱」。不同的客户、不同的产品、不同的数量,价格可能都不一样。我见过最复杂的策略引擎,有 200 多条规则,跑一次要 3 秒。但核心模型其实很简单。

3.1 策略模型设计

我习惯把价格策略抽象成三个要素:条件动作优先级。条件决定什么时候触发,动作决定怎么算钱,优先级决定多条规则冲突时听谁的。

public class PriceStrategy {
    private String strategyId;          // 策略ID
    private String name;                // 策略名称
    private Integer priority;           // 优先级,数字越小越优先
    private List<Condition> conditions; // 触发条件
    private List<Action> actions;       // 执行动作
    private String description;         // 策略说明
}

public class Condition {
    private String field;       // 字段名,如 customer.level
    private String operator;    // 操作符,如 EQUALS、GREATER_THAN
    private Object value;       // 比较值
}

public class Action {
    private String type;        // 动作类型,如 DISCOUNT、FIXED_PRICE
    private BigDecimal value;   // 动作值
}

3.2 策略执行流程

策略执行的流程,我一般用「匹配-排序-执行」三步走:

  1. 匹配:遍历所有策略,找出满足条件的策略
  2. 排序:按优先级排序,优先级高的先执行
  3. 执行:依次执行动作,后执行的可以覆盖先执行的
举个例子:某客户是 VIP 等级,系统先匹配到「VIP 客户打 9 折」的策略,然后又匹配到「满 1000 减 100」的策略。两条策略都满足,按优先级执行后,最终价格是 (原价 * 0.9) - 100。

四、客户与供应商模型:报价系统的左右手

客户和供应商,是报价系统的两个核心角色。客户负责「买」,供应商负责「卖」。但实际业务中,一个客户可能同时是供应商,这就有点意思了。

4.1 统一伙伴模型

我建议用「伙伴模型」来统一管理客户和供应商。这样设计,既减少了重复代码,又方便扩展。

public class Partner {
    private String partnerId;       // 伙伴ID
    private String name;            // 名称
    private PartnerType type;       // 类型:CUSTOMER、SUPPLIER、BOTH
    private String contactPerson;   // 联系人
    private String phone;           // 电话
    private String email;           // 邮箱
    private String address;         // 地址
    private String taxId;           // 税号
    private Integer creditLevel;    // 信用等级,1-5
    private List<PriceStrategy> strategies; // 关联的价格策略
}

public enum PartnerType {
    CUSTOMER,   // 仅客户
    SUPPLIER,   // 仅供应商
    BOTH        // 既是客户又是供应商
}
我的经验:很多企业既有采购业务,又有销售业务。如果客户和供应商分开管理,同一个公司要维护两套数据,容易出错。用统一伙伴模型,一个公司一条记录,省心多了。

4.2 伙伴与策略的关联

每个伙伴可以关联多个价格策略。比如 VIP 客户有专属折扣,大客户有阶梯定价。这些策略在报价时会被自动匹配。

我一般用「伙伴-策略」关联表来实现多对多关系:

CREATE TABLE partner_strategy (
    partner_id UUID NOT NULL,
    strategy_id UUID NOT NULL,
    effective_date DATE NOT NULL,
    expiry_date DATE,
    PRIMARY KEY (partner_id, strategy_id, effective_date)
);
注意:策略关联一定要有有效期。我曾经见过一个系统,策略关联后忘了设有效期,结果客户用了三年前的折扣价来报价,亏了一大笔。后来我们加上了有效期校验,过期策略自动失效。

五、核心数据模型关系图

说了这么多,咱们用一张图来总结一下这些模型之间的关系。我画了一张 SVG 图,方便你理解。

报价单 (Quote) 行项目 (LineItem) 价格策略 (PriceStrategy) 客户 (Customer) 供应商 (Supplier) 伙伴模型 (Partner) 1:N N:M N:1 N:1 1:1 1:1 N:M 核心实体 抽象模型 多对多关系

从这张图可以看出,报价单是核心,它关联了行项目、客户、供应商和价格策略。而客户和供应商最终都统一到伙伴模型上。这样设计,整个数据模型既清晰又灵活。

总结一下:报价单实体是「骨架」,行项目是「血肉」,价格策略是「灵魂」,客户和供应商是「左右手」。把这四个模型设计好了,报价系统就稳了。

专注资料整理