news 2026/9/8 3:50:46

快递轨迹外挂组件设计方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
快递轨迹外挂组件设计方案

目标:WMS 发货后推送快递单号到外挂系统,由外挂负责持续抓取并存储快递轨迹,轨迹保留3 年,并向业务方(WMS/客服/用户)提供稳定、可审计的查询与告警能力。

1 概览

  • 痛点:WMS 每次跳转第三方查询耦合强、无法长期保存轨迹;需要统一管理并保存 3 年以供审计/客服查询。

  • 目标:实现「一次推送、外挂持续跟踪直至签收、三年归档」的可扩展、可观测、可运维系统。

  • 关键特性:异步解耦、按状态动态轮询(Adaptive Polling)、限流保护、幂等入库、分层存储与归档。

2 总体架构

3 核心设计要点

3.1 接入模式

  • 首选:WMS 采用 HTTP POST 接入(幂等)。

  • 鉴权:HMAC 签名或 OAuth2,接口需限流与白名单。

3.2 调度模型(关键)

  • 任务驱动:每个单号在shipment_task中维护next_check_timecheck_interval_secondsstatusno_update_count

  • 批量扫描:调度器按next_check_time <= now()批量读取(分页/limit),并按MOD(id, N)做分片,推入 MQ。

  • Adaptive Polling:根据状态与最近是否有更新动态调整check_interval(指数退避,上限 6 小时)。

  • 停止条件:检测到DELIVERED(签收),停止轮询并进入归档计划。

3.3 执行策略

  • 固定大小 Worker 池:Worker 为常驻进程(容器),消费者组并发处理 MQ 任务,避免为每单创建线程。

  • 限流:按快递公司使用 Redisson RateLimiter 或令牌桶保护对方接口。

  • 幂等写入:通过shipment_id + event_time + desc_hash做唯一约束或写入前存在性判断。

  • 异常处理:失败重试(指数退避),失败超过阈值进入 DLQ 并告警。

3.4 存储与归档

  • 热存:最近 90/180 天的轨迹保存在 MySQL(或 NoSQL)便于快速查询。

  • 冷存:超过 N 天(如 90 天)将原始 payload 与老事件归档到对象存储,主库写入归档路径索引。

  • 分区/分表courier_event按月分区以便批量删除/迁移。

3.5 可观测性与告警

  • • 指标:队列深度、处理吞吐、QPS、每家快递的失败率、平均延迟(发货→首次轨迹)、未签收超过阈值数量。

  • • 日志:存储原始请求/响应(至少保留 30—90 天)并脱敏。

  • • 告警:连续 N 次抓取失败、队列长度异常增长、接口被限流。

4 数据模型(示例 DDL)

-- 任务表:负责调度 CREATE TABLE shipment_task ( id BIGINTPRIMARY KEY AUTO_INCREMENT, order_id VARCHAR(64), tracking_no VARCHAR(64) NOT NULL, courier_code VARCHAR(32), status VARCHAR(32) DEFAULT'NEW', last_event_time DATETIME, last_check_time DATETIME, next_check_time DATETIME, check_interval_seconds INTDEFAULT1800, no_update_count INTDEFAULT0, retry_count INTDEFAULT0, max_retry INTDEFAULT10, delivered BOOLEANDEFAULTFALSE, raw_meta JSON, created_at DATETIME DEFAULTCURRENT_TIMESTAMP, updated_at DATETIME DEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP, UNIQUE KEY uq_tracking (tracking_no, courier_code), INDEX idx_next_check (next_check_time), INDEX idx_status (status) ); -- 轨迹事件(按月分区) CREATE TABLE courier_event ( id BIGINTPRIMARY KEY AUTO_INCREMENT, shipment_id BIGINTNOT NULL, event_time DATETIME NOT NULL, location VARCHAR(128), description TEXT, status_code VARCHAR(32), raw_payload JSON, created_at DATETIME DEFAULTCURRENT_TIMESTAMP, UNIQUE KEY uq_event (shipment_id, event_time, MD5(description(1000))) ); -- 归档路径记录 CREATE TABLE courier_archive ( id BIGINTPRIMARY KEY AUTO_INCREMENT, shipment_id BIGINTNOT NULL, s3_path VARCHAR(255), archived_at DATETIME, note VARCHAR(255) );

5 核心代码

5.1 接入 Controller(幂等 + 入队)

@RestController @RequestMapping("/api/v1/shipments") publicclassShipmentController { privatefinal ShipmentTaskService taskService; @PostMapping public ResponseEntity<?> accept(@RequestBody ShipmentDto dto, @RequestHeader("X-Signature") String sig) { // 1. 验证签名(HMAC) if (!AuthUtil.verify(sig, dto)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } // 2. 幂等写入或更新任务表 ShipmentTasktask= taskService.upsertFromDto(dto); // 3. 推送到 MQ(由调度器负责更完整的调度逻辑,此处可简化为立即推一次) taskService.enqueueImmediateCheck(task.getId()); return ResponseEntity.accepted().body(Map.of("shipmentId", task.getId())); } }

5.2 调度器(批量扫描 & 分片)

@Component publicclassScheduler { @Autowired private ShipmentTaskRepo repo; @Autowired private MQProducer mqProducer; @Scheduled(cron = "0 * * * * *")// 每分钟触发一次 publicvoidscanAndEnqueue() { LocalDateTimenow= LocalDateTime.now(); intshardCount= Integer.parseInt(env.getProperty("app.shard.count", "4")); intmyShard= Integer.parseInt(env.getProperty("app.shard.index", "0")); List<ShipmentTask> due = repo.findDueTasks(now, myShard, shardCount, 1000); for (ShipmentTask t : due) { mqProducer.send("tracking_query_topic", t.getId()); // update next_check_time optimistically to avoid重复选中 t.setNextCheckTime(now.plusSeconds(t.getCheckIntervalSeconds())); repo.save(t); } } }

findDueTasksSQL 示例:

SELECT * FROM shipment_task WHERE next_check_time <= :now AND status IN ('NEW','IN_TRANSIT','PENDING_UPDATE') AND MOD(id, :shardCount) = :shardIndex ORDER BY next_check_time LIMIT :limit

5.3 Worker(MQ 消费者)

@RocketMQMessageListener(topic = "tracking_query_topic", consumerGroup = "tracking_fetchers") publicclassTrackingFetcherimplementsRocketMQListener<Long> { @Autowired private ShipmentTaskRepo repo; @Autowired private CourierAdapterFactory adapterFactory; @Autowired private RedissonClient redisson; @Override publicvoidonMessage(Long taskId) { ShipmentTasktask= repo.findById(taskId).orElse(null); if (task == null) return; // rate limit key per courier RRateLimiterlimiter= redisson.getRateLimiter("courier:" + task.getCourierCode()); if (!limiter.tryAcquire()) { // 未获取到令牌,延迟重试 task.setNextCheckTime(LocalDateTime.now().plusSeconds(30)); repo.save(task); return; } CourierClientclient= adapterFactory.getClient(task.getCourierCode()); try { TrackingResponseresp= client.query(task.getTrackingNo()); List<Event> events = client.parse(resp); booleanchanged= persistEvents(task, events, resp); if (resp.isDelivered()) { task.setStatus("DELIVERED"); task.setDelivered(true); task.setNextCheckTime(null); } else { adjustIntervalAfterCheck(task, changed); } } catch (Exception ex) { task.setRetryCount(task.getRetryCount() + 1); if (task.getRetryCount() > task.getMaxRetry()) task.setStatus("EXCEPTION"); task.setNextCheckTime(LocalDateTime.now().plusSeconds(60)); } finally { repo.save(task); } } }

5.4 适配器接口(扩展快递方)

public interfaceCourierClient { TrackingResponse query(String trackingNo)throws IOException; List<Event> parse(TrackingResponse resp); } publicclassSFExpressClientimplementsCourierClient { public TrackingResponse query(String trackingNo) { /* HTTP 调用 SF API */ } public List<Event> parse(TrackingResponse resp) { /* 解析 SF 返回 */ } }

6 性能估算与容量规划

输入参数

  • • 年度出库单量:1,000,000 单/年

  • • 单均轨迹事件数:10 条

  • • 平均事件 JSON 大小:600 字节

  • • 保留期限:3 年

存储估算

  • • 每单事件数据:10 * 600 = 6,000 bytes ≈ 6KB

  • • 年度数据:1,000,000 * 6KB = 6,000,000 KB ≈ 5.72 GB

  • • 3 年数据 ≈ 17.2 GB(不含索引和原始 payload)

  • • 建议预留 3×—5× 因为索引/原始 payload/日志:建议 100GB 存储预算

吞吐估算(高峰)

  • • 假设同时在途 100k 单号,平均每小时查一次 → 每小时 100k 次 → 每秒 ~28 次。

  • • Worker 节点:每节点 200 并发可以轻松处理,故 1-3 节点即可。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 19:29:38

AI产品经理工作全流程详解:大模型开发从需求到上线的实战指南

本文详细介绍了AI产品经理的工作全流程&#xff0c;包括需求定义、模型预研、数据准备、模型构建、模型宣讲与验收及业务开发上线等环节。与传统产品经理不同&#xff0c;AI产品经理需理解算法模型知识&#xff0c;与算法工程师紧密协作&#xff0c;特别关注特征工程和模型评估…

作者头像 李华
网站建设 2026/9/3 2:19:12

基于Comsol的储层降压开采渗流 - 应力耦合算例探索

基于comsol的储层降压开采过程中的渗流-应力耦合算例 提供基于comsol的储层降压开采过程中的渗流-应力耦合算例&#xff0c;可在此基础上熟悉降压开采过程中的渗流-应力耦合计算方法。 在石油开采等领域&#xff0c;理解储层降压开采过程中的渗流 - 应力耦合机制至关重要。Com…

作者头像 李华
网站建设 2026/9/2 23:31:21

一篇讲透DNS劫持:从攻击链条到全面防御

一、核心定义&#xff1a;互联网的“电话簿”被篡改了 想象一下&#xff0c;互联网就像一本巨大的电话簿&#xff08;DNS&#xff09;。你想访问“百度”&#xff0c;不是直接输入复杂的IP地址&#xff08;如 39.156.66.10&#xff09;&#xff0c;而是输入好记的域名 www.bai…

作者头像 李华
网站建设 2026/9/7 14:28:17

【开题答辩全过程】以 基于安卓的普法教育App设计与实现为例,包含答辩的问题和答案

个人简介 一名14年经验的资深毕设内行人&#xff0c;语言擅长Java、php、微信小程序、Python、Golang、安卓Android等 开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧。 感谢大家…

作者头像 李华
网站建设 2026/9/7 4:32:37

Fluke福禄克1550C 1555绝缘电子测试仪

福禄克1550C绝缘电阻测试仪&#xff1a;高压设备的安全守护者 一、核心功能与基础参数 福禄克1550C是一款专为高压设备设计的绝缘电阻测试仪&#xff0c;测试电压覆盖250V至5000V&#xff0c;可精准测量0.01μF至15μF的电容值&#xff0c;以及1nA至2mA的漏电流。其核心参数包括…

作者头像 李华
网站建设 2026/9/2 22:29:24

Java 25 发布:更简洁、更高效、更现代!我们先来学习下~

Java 25 已经正式发布。作为 长期支持&#xff08;LTS&#xff09;版本&#xff0c;它不仅稳定&#xff0c; 还带来了大量新特性。对于开发者来说&#xff0c;这意味着未来几年可以放心使 用&#xff0c;而新手也能通过这些更新更轻松地入门 Java。Java 25 的更新覆盖了 语言语…

作者头像 李华