在导购返利类应用中,佣金结算系统堪称业务逻辑最复杂的模块之一。我经历过三个不同规模的返利平台开发,每次重构结算系统时都会发现:随着业务规则的增长,传统事务脚本式的代码会迅速腐化为难以维护的"大泥球"。这正是我们采用领域驱动设计(DDD)的根本原因。
典型的佣金结算涉及多维度业务规则:
这些规则在业务发展过程中会不断调整。比如去年我们接入了直播带货场景后,就需要支持"直播间专属优惠券"与"平台通用券"的佣金计算差异。如果采用传统三层架构,这些逻辑会散落在Service层的各个角落,任何改动都可能引发连锁反应。
经过多次业务梳理,我们将系统划分为以下核心限界上下文:
订单上下文:处理电商平台订单同步、状态追踪
佣金规则上下文:管理所有佣金计算规则
结算上下文:执行实际佣金计算与分发
重要经验:划分时我们曾犯过将"风控审核"与"结算"合并的错误,导致两个高频变更的领域互相影响。后来将其拆分为独立上下文后,系统稳定性显著提升。
以佣金计算聚合根为例,其设计经历了三次演进:
第一版(贫血模型)
java复制class Commission {
Long orderId;
BigDecimal amount;
// 只有getter/setter
}
class CommissionService {
public void calculate(Order order) {
// 所有计算逻辑集中在此
}
}
第二版(引入领域服务)
java复制class Commission {
private Order order;
private List<Rule> appliedRules;
public void applyRule(Rule rule) {
// 规则应用逻辑
}
}
class CommissionCalculator {
public Commission calculate(Order order) {
// 协调多个Commission对象协作
}
}
最终版(聚合根封装不变性)
java复制class Commission {
private CommissionId id;
private OrderSnapshot order;
private CalculationResult result;
private AuditStatus status;
public Commission(Order order, RuleEngine engine) {
validate(order);
this.result = engine.apply(order); // 不变逻辑封装在聚合内
}
public void adjust(BigDecimal newAmount) {
if (status != AuditStatus.PENDING) {
throw new IllegalStateException();
}
this.result = result.adjust(newAmount);
}
}
关键设计要点:
在对接外部电商平台时,我们设计了三种防腐层:
java复制interface OrderAPI {
List<OrderDTO> fetchOrders(LocalDateTime start, LocalDateTime end);
}
// 淘宝实现
class TaobaoOrderAPI implements OrderAPI {
// 转换淘宝特有参数格式
// 处理淘宝错误码体系
}
// 京东实现
class JDOrderAPI implements OrderAPI {
// 京东特有的签名机制
}
java复制@EventListener
public void handleOrderEvent(PlatformOrderEvent event) {
Order order = convert(event);
orderRepository.save(order);
// 触发后续领域事件
domainEventPublisher.publish(new OrderReceivedEvent(order));
}
java复制@Scheduled(cron = "0 0/5 * * * ?")
public void syncOrders() {
platformApis.forEach(api -> {
try {
List<Order> orders = api.fetchRecentOrders();
orders.forEach(this::processOrder);
} catch (Exception e) {
// 统一异常处理
monitor.recordSyncError(api.getPlatform());
}
});
}
实际应用中,我们发现事件订阅模式实时性最好,但对平台事件机制的可靠性要求较高。目前采用"事件订阅为主+定时同步兜底"的混合策略。
佣金规则的核心挑战在于:
我们的解决方案:
java复制public class RuleEngine {
private final RuleRepository repository;
private final RuleEvaluator evaluator;
public CalculationResult evaluate(Order order) {
List<Rule> rules = repository.findApplicableRules(order);
RuleContext context = new RuleContext(order);
// 按优先级排序执行
rules.sort(Comparator.comparingInt(Rule::getPriority));
for (Rule rule : rules) {
if (evaluator.matches(rule, context)) {
context.apply(rule);
}
}
return context.getResult();
}
}
配套的规则DSL示例:
json复制{
"name": "双11手机类目加码",
"priority": 100,
"condition": {
"allOf": [
{"field": "category", "operator": "EQUALS", "value": "手机"},
{"field": "activity", "operator": "CONTAINS", "value": "双11"}
]
},
"action": {
"type": "PERCENTAGE",
"value": "3%",
"cap": 500
}
}
佣金结算涉及多个上下文的协作,我们采用Saga模式保证最终一致性:
java复制class OrderCreatedHandler {
@Transactional
public void handle(OrderCreatedEvent event) {
Commission commission = new Commission(event.getOrder());
commissionRepository.save(commission);
// 触发规则计算
domainEventPublisher.publish(
new CommissionCalculationEvent(commission.getId())
);
}
}
java复制class CalculationHandler {
public void handle(CommissionCalculationEvent event) {
Commission commission = commissionRepository.findById(event.getId());
CalculationResult result = ruleEngine.evaluate(commission.getOrder());
commission.applyResult(result);
// 触发账务处理
domainEventPublisher.publish(
new AccountingEvent(commission.getId())
);
}
}
java复制class CalculationFailureHandler {
public void handle(CalculationFailedEvent event) {
Commission commission = commissionRepository.findById(event.getId());
commission.markAsFailed(event.getReason());
// 加入人工处理队列
manualReviewQueue.add(commission);
}
}
关键设计点:
我们发现90%的订单佣金计算会命中相同规则,因此设计三级缓存:
java复制LoadingCache<RuleKey, Rule> ruleCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(this::loadRuleFromDB);
java复制String cacheKey = "commission:" + order.getHashKey();
String cached = redis.get(cacheKey);
if (cached != null) {
return deserialize(cached);
}
java复制public Map<OrderId, CalculationResult> batchCalculate(List<Order> orders) {
// 批量加载规则
Set<RuleKey> ruleKeys = collectRuleKeys(orders);
Map<RuleKey, Rule> rules = ruleRepository.batchLoad(ruleKeys);
// 并行计算
return orders.parallelStream()
.collect(Collectors.toMap(
Order::getId,
order -> calculateInternal(order, rules)
));
}
实测在大促期间,缓存命中率达到78%,系统负载下降40%。
我们将系统拆分为:
java复制// 写侧
class CommissionCommandService {
@Transactional
public void adjustCommission(AdjustCommand command) {
Commission commission = repository.findById(command.getId());
commission.adjust(command.getNewAmount());
domainEventPublisher.publish(
new CommissionAdjustedEvent(commission)
);
}
}
// 读侧
class CommissionViewUpdater {
@EventListener
public void updateView(CommissionAdjustedEvent event) {
elasticsearchClient.update(
"commissions",
event.getCommissionId(),
buildUpdateScript(event)
);
}
}
过度聚合:曾将用户、订单、佣金放在一个聚合中,导致:
纠正方案:
问题1:事件包含完整聚合数据导致存储压力
java复制// 反例
class OrderCreatedEvent {
private Order order; // 包含所有订单明细
}
// 正解
class OrderCreatedEvent {
private OrderId orderId;
private UserId userId;
private BigDecimal amount;
}
问题2:事件版本兼容
java复制// 使用Jackson的@JsonTypeInfo处理多态
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = RuleAdded.class, name = "rule_added"),
@JsonSubTypes.Type(value = RuleModified.class, name = "rule_modified")
})
public abstract class RuleEvent {
private RuleId ruleId;
}
java复制@Test
void should_reject_negative_adjustment() {
Commission commission = createTestCommission();
assertThrows(InvalidAdjustmentException.class,
() -> commission.adjust(new BigDecimal("-1")));
}
java复制@Test
void should_trigger_accounting_after_calculation() {
// 发布订单事件
eventPublisher.publish(new OrderCreatedEvent(testOrder));
// 验证账务事件
ArgumentCaptor<AccountingEvent> captor = ArgumentCaptor.forClass(AccountingEvent.class);
verify(eventPublisher).publish(captor.capture());
assertNotNull(captor.getValue());
}
java复制@Pact(provider = "TaobaoAPI", consumer = "CommissionSystem")
public PactFragment taobaoOrderSync(PactDslWithProvider builder) {
return builder
.given("orders exist")
.uponReceiving("fetch orders request")
.path("/orders")
.method("GET")
.willRespondWith()
.status(200)
.body(/* Pact DSL */)
.toFragment();
}
经过两年多的DDD实践,我们的佣金结算系统在支持业务快速增长的同时,代码复杂度保持线性增长。新加入的开发者通常能在1周内熟悉核心领域逻辑,这验证了领域驱动设计的价值。对于准备尝试DDD的团队,我的建议是:从最复杂的业务核心开始,先建立精准的领域模型,技术实现反而会水到渠成。