一、报价单实体设计:一切从这里开始
报价单,说白了就是整个系统的「门面」。我做了这么多年报价系统,发现很多团队一上来就堆字段,结果后期维护成本高得吓人。今天咱们就聊聊,一个靠谱的报价单实体到底长什么样。
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 | 有效期截止 |
1.2 状态机设计
报价单的状态流转,其实是个典型的有限状态机。我建议用枚举来管理,别用字符串硬编码。
public enum QuoteStatus {
DRAFT("草稿"),
PENDING_APPROVAL("待审批"),
APPROVED("已审批"),
SENT("已发送"),
CONFIRMED("已确认"),
EXPIRED("已过期"),
CANCELLED("已取消");
private String description;
// 构造方法、getter 省略
}
二、行项目模型:报价单的骨架
行项目,就是报价单里每一行的明细。你想想看,一份报价单可能包含几十甚至上百个产品,每个产品的数量、单价、折扣都不一样。怎么设计才能既灵活又高效?
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; // 备注
}
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);
}
三、价格策略模型:报价系统的灵魂
价格策略,说白了就是「怎么算钱」。不同的客户、不同的产品、不同的数量,价格可能都不一样。我见过最复杂的策略引擎,有 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 策略执行流程
策略执行的流程,我一般用「匹配-排序-执行」三步走:
- 匹配:遍历所有策略,找出满足条件的策略
- 排序:按优先级排序,优先级高的先执行
- 执行:依次执行动作,后执行的可以覆盖先执行的
四、客户与供应商模型:报价系统的左右手
客户和供应商,是报价系统的两个核心角色。客户负责「买」,供应商负责「卖」。但实际业务中,一个客户可能同时是供应商,这就有点意思了。
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 图,方便你理解。
从这张图可以看出,报价单是核心,它关联了行项目、客户、供应商和价格策略。而客户和供应商最终都统一到伙伴模型上。这样设计,整个数据模型既清晰又灵活。