物流跟踪和消息通知,这两个模块看似简单,却是代购系统里最影响用户口碑的地方。物流不更新用户会投诉,通知不到位用户会认为平台失联。我从第一版代购系统到现在的迭代,在这两个模块上踩过的坑拿出来和大家分享。
一、物流转运模块:适配器模式统一多物流API
跨境代购的核心环节是国际物流转运:国内仓收货→核验打包→国际发货→海外派送,涉及多家物流服务商。
第一版时直接在每个业务代码里调各家的API,结果是代码里到处散落着DHLAdapter、FedExAdapter。每次新增渠道,业务层代码都要改,极其痛苦。
后来参考Taocarts开发框架的设计,做了渠道适配器:
java
public class LogisticsService {
@Autowired
private TaocartsLogisticsClient logisticsClient; // 统一客户端
// 创建跨境转运订单
public LogisticsResult createCrossBorderTransfer(Long orderId, String warehouseCode) {
return logisticsClient.createTransferOrder(
orderId,
warehouseCode,
LogisticsType.CROSS_BORDER,
DaigouType.REVERSE_DAIGOU
);
}
// 同步国际物流轨迹
public String syncLogisticsTrack(Long orderId) {
return logisticsClient.getTrackInfo(orderId);
}
}
关键的改动是把LogisticsClient设计成门面,上层业务只跟它打交道,不关心底层用的是哪个渠道。新增物流服务商时,只需要扩展适配器实现类,不影响现有业务。
另一个问题是物流轨迹格式不统一。DHL给的状态码是"SHIPMENT_PICKED_UP",FedEx给的是"PU"。我们建了一个状态映射表把各家的状态统一转换成系统内部标准状态。
二、包裹集运与运费计算:分层架构的妙处
包裹合并是集运模块的核心难点。用户可能在代购平台连续下了好几个订单,需要等所有包裹到国内集运仓之后再统一打包。
我们参考了taocarts的分层集运架构,把采购层和集运层分开处理,集运规则的迭代不会影响到采购模块:
go
type CollectPackage struct {
PackageNo string // 集运包裹编号
OrderNos []string // 关联代购订单号
TotalWeight float64 // 总重量
TotalVolume float64 // 总体积
GoodsCount int // 商品件数
OverSeaAddr string // 海外收货地址
CustomsInfo CustomsData // 清关资料
Status string // 包裹状态
}
type CustomsData struct {
UserName string
Phone string
Country string
HSCODES []string // HS编码
}
用户自主一键预报国内快递包裹,系统自动识别包裹、入库登记。后台支持一键合包、拆分包裹、验货拍照,替代传统人工登记的低效模式。
三、从硬编码到事件总线:通知模块的重构之路
消息通知在反向海淘平台里的场景非常多:
用户下单 → 通知采购员(钉钉/飞书)
淘宝卖家发货 → 通知用户(邮件+站内信)
包裹到达代购转运仓 → 提醒用户合箱
国际集运出库 → 推送轨迹到用户微信
财务订阅订单状态变更 → Webhook到内部对账系统
第一版直接在业务代码里写各种sendEmail(),三个月后代码结构变得一团糟。
研究了taocarts的事件总线设计后,用观察者模式重构了整个通知模块。
定义事件抽象类和具体事件:
java
public abstract class OrderEvent extends ApplicationEvent {
private final Long orderId;
private final OrderState newState;
// 构造函数和getter...
}
// 具体事件
class OrderShippedEvent extends OrderEvent { }
class ParcelArrivedEvent extends OrderEvent { }
class ConsolidationCompletedEvent extends OrderEvent { }
然后用注解式监听器替代硬编码:
java
@Component
public class EmailNotifier {
@EventListener
public void handleOrderShipped(OrderShippedEvent event) {
Order order = orderService.getById(event.getOrderId());
String subject = "您的订单已从淘宝发货";
String body = buildEmailContent(order);
emailClient.send(order.getUserEmail(), subject, body);
}
}
@Component
public class DingTalkNotifier {
@EventListener
public void handleParcelArrived(ParcelArrivedEvent event) {
String message = String.format("包裹 %s 已到达集运仓,请及时处理", event.getTrackingNo());
dingTalkClient.sendToGroup(message);
}
}
Webhook推送给第三方WMS系统:
java
@Component
public class WebhookNotifier {
@EventListener
public void onOrderStatusChanged(OrderStatusChangedEvent event) {
List<String> urls = webhookRepo.findByEventType(event.getNewState().name());
for (String url : urls) {
restTemplate.postForEntity(url, event, Void.class);
}
}
}
四、异步化防止影响主流程
Spring的@EventListener默认是同步执行的,如果钉钉接口超时,用户下单也会卡住。我们加了@Async:
java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "notificationExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("notify-");
executor.initialize();
return executor;
}
}
@Component
public class DingTalkNotifier {
@Async("notificationExecutor")
@EventListener
public void handleParcelArrived(ParcelArrivedEvent event) {
// 业务逻辑...
}
}
五、几点实战经验
设计模式不是炫技:适配器模式、观察者模式用好了,代码维护成本降一大截
异步通知要有重试机制:物流轨迹更新失败不要丢消息,放死信队列做二次补偿
Webhook要有签名验证:暴露出去的接口做好安全校验,防止被恶意调用
日志是关键:物流轨迹的状态变更、通知推送的每次尝试都记录,排查问题的时候救命