快递/快运物流轨迹系统 设计与实现文档
文档版本:V1.0
适用场景:快运/快递运单全链路轨迹跟踪、异步存储、模板化轨迹查询渲染
核心技术栈:SpringBoot、MyBatis-Plus、RocketMQ、FreeMarker、Redis、MySQL
一、需求分析
1.1 核心业务需求
- 全链路轨迹记录:精准记录运单从开单 → 提货 → 运输 → 中转 → 派送 → 签收全节点操作轨迹;
- 异步化数据处理:各业务系统通过MQ推送轨迹数据,消费者异步消费并持久化,保证系统解耦、高可用;
- 模板化轨迹查询:提供统一轨迹查询接口,通过FreeMarker模板引擎渲染轨迹描述文案,支持多场景、可配置化展示。
1.2 技术需求
- 轨迹数据防重、幂等消费
- 轨迹文案动态渲染,支持变量替换
- 轨迹查询按时间倒序,支持合并/补充车辆轨迹
- 兼容多端操作(PC/PDA/APP/小程序)
二、数据库表结构设计(优化版)
基于原表结构优化:新增主键自增、字段默认值、非空约束、索引优化、字段规范
CREATE TABLE `waybill_trace` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`waybill_no` varchar(50) NOT NULL COMMENT '运单号',
`oper_time` datetime NOT NULL COMMENT '操作时间',
`oper_type` varchar(50) NOT NULL COMMENT '操作类型编码',
`dept_code` varchar(50) DEFAULT NULL COMMENT '操作部门编码',
`dept_name` varchar(50) DEFAULT NULL COMMENT '操作部门名称',
`real_dept_code` varchar(50) DEFAULT NULL COMMENT '实际操作部门编码',
`real_dept_name` varchar(50) DEFAULT NULL COMMENT '实际操作部门名称',
`oper_emp_code` varchar(100) DEFAULT NULL COMMENT '操作人编码',
`oper_emp_name` varchar(100) DEFAULT '' COMMENT '操作人姓名',
`contact_phone` varchar(50) DEFAULT NULL COMMENT '联系电话',
`task_no` varchar(50) DEFAULT NULL COMMENT '配载/任务单号',
`relation_dept_code` varchar(50) DEFAULT NULL COMMENT '上下站部门编码',
`relation_dept_name` varchar(50) DEFAULT NULL COMMENT '上下站部门名称',
`remark` varchar(500) DEFAULT NULL COMMENT '轨迹描述文案',
`comp_code` varchar(50) DEFAULT NULL COMMENT '公司编码',
`data_source` varchar(50) DEFAULT NULL COMMENT '数据来源:PC/PDA/APP/小程序',
`quantity` int NOT NULL DEFAULT 0 COMMENT '操作件数',
`vehicle_no` varchar(50) DEFAULT NULL COMMENT '车牌号',
`driver_name` varchar(50) DEFAULT NULL COMMENT '司机姓名',
`driver_phone` varchar(50) DEFAULT NULL COMMENT '司机电话',
`receiver_name` varchar(50) DEFAULT NULL COMMENT '签收人',
`receipt_no` varchar(50) DEFAULT NULL COMMENT '回单号',
`sign_status` varchar(50) DEFAULT NULL COMMENT '签收异常状态',
`abnormal_reason` varchar(50) DEFAULT NULL COMMENT '异常原因',
`is_delete` int NOT NULL DEFAULT 0 COMMENT '删除标识:0-未删除 1-已删除',
`creater` varchar(50) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modifier` varchar(50) DEFAULT NULL COMMENT '修改人',
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`version` int DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_waybill_oper` (`waybill_no`,`oper_type`,`task_no`,`oper_time`) COMMENT '运单轨迹防重唯一索引',
KEY `idx_oper_time` (`oper_time`) COMMENT '操作时间索引',
KEY `idx_task_no` (`task_no`) COMMENT '任务单号索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='运单轨迹表';
2.1 表结构优化点
- 重命名表:
trace→waybill_trace(语义更清晰) - 主键添加
AUTO_INCREMENT,无需手动生成ID - 核心字段添加
NOT NULL约束,保证数据完整性 - 优化唯一索引:防重复消费/重复插入
- 统一默认值:
is_delete=0、quantity=0、version=0 - 扩充
remark字段长度,适配模板渲染文案
三、核心枚举设计(轨迹类型)
优化枚举:规范命名、修复语法、完善注释、统一工具方法
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 运单轨迹类型枚举
* 覆盖:开单、提货、短驳、干线、派送、签收、异常等全链路节点
*/
@Getter
public enum WaybillTraceType {
// ====================== 1.开单环节 ======================
WAYBILL_CREATE("1001", "录单",
"【${deptName}】开单入库,制单人【${operEmpName}】,录单件数【${quantity}】",
"PC,APP,小程序"),
// ====================== 2.提货环节 ======================
ORDER_PICKUP("1002", "提货",
"【${deptName}】的【${driverName}】已出发提货,车牌号【${vehicleNo}】,制单人【${operEmpName}】,派车单号【${taskNo}】",
"PC"),
ORDER_CANCEL_PICKUP("1003", "提货取消",
"【${deptName}】的【${operEmpName}】已取消提货",
"PC"),
// ====================== 3.短驳环节 ======================
DB_LOADING("2001", "短驳装车",
"【${deptName}】装车发件至【${relationDeptName}】,车牌号【${vehicleNo}】,司机【${driverName}】,件数【${quantity}】,制单人【${operEmpName}】,任务单号【${taskNo}】",
"PC,APP"),
DB_UNLOADING("2002", "短驳接收",
"货物已到达【${deptName}】,卸车人【${operEmpName}】,任务单号【${taskNo}】,卸货件数【${quantity}】",
"PC"),
// ====================== 4.干线运输 ======================
LOADING_SCAN("4001", "装车",
"【${deptName}】装车发件至【${relationDeptName}】,车牌号【${vehicleNo}】,司机【${driverName}】,件数【${quantity}】,制单人【${operEmpName}】,任务单号【${taskNo}】",
"PC,APP"),
LOADING_SRARTCAR("4004", "发车",
"车辆【${vehicleNo}】已从【${deptName}】驶向【${relationDeptName}】,司机【${driverName}】【${driverPhone}】,任务号【${taskNo}】",
"PC,APP"),
UNLOADING_SCAN("4003", "卸车",
"货物已到达【${deptName}】,卸车人【${operEmpName}】,任务单号【${taskNo}】,卸货件数【${quantity}】",
"PC,PDA"),
// ====================== 5.派送环节 ======================
DELIVERY_SCAN("5001", "派送",
"【${deptName}】的派件员【${driverName}】已开始送货,手机号【${contactPhone}】,车牌号【${vehicleNo}】,送货单号【${taskNo}】,件数【${quantity}】",
"PC,PDA,APP"),
// ====================== 6.签收环节 ======================
NORMAL_SIGN("6002", "正常签收",
"货物已被签收,签收人是【${receiverName}】,签收网点【${deptName}】,经办人【${operEmpName}】",
"PC,PDA,APP,小程序,二维码签收"),
ABNORMAL_SIGN("6003", "异常签收",
"货物已被签收,签收人是【${receiverName}】,异常状态为【${signStatus}】,异常原因【${abnormalReason}】,签收网点【${deptName}】,经办人【${operEmpName}】",
"PC,PDA,APP"),
// 其余枚举项按此格式补充...
WAREHOUSE_RETENTION_SCAN("10001", "留仓",
"货物在【${deptName}】发生了留仓,留仓【${quantity}】件,留仓原因:【${abnormalReason}】",
"PDA");
/**
* 缓存:编码 -> 枚举对象
*/
private static final Map<String, WaybillTraceType> CODE_MAP =
Arrays.stream(values()).collect(Collectors.toMap(WaybillTraceType::getCode, e -> e));
private final String code;
private final String desc;
private final String template;
private final String source;
WaybillTraceType(String code, String desc, String template, String source) {
this.code = code;
this.desc = desc;
this.template = template;
this.source = source;
}
/**
* 根据编码获取枚举(优化:Map缓存,性能更高)
*/
public static WaybillTraceType getByCode(String code) {
return CODE_MAP.get(code);
}
/**
* 校验数据源是否支持
*/
public boolean isSourceSupported(String source) {
return this.source.contains(source);
}
/**
* 判断是否为签收类轨迹
*/
public boolean isSignTrace() {
return this == NORMAL_SIGN || this == ABNORMAL_SIGN || this == PARTIAL_SIGN || this == SELF_PICKUP_SIGN;
}
}
3.1 枚举优化点
- 新增静态Map缓存,替代循环遍历,提升查询性能
- 按业务环节分组注释,可读性提升
- 精简冗余方法,统一规范
- 修复原代码语法错误、拼写错误
四、MQ异步通信模块(核心:解耦、异步)
4.1 生产者接口(修复命名错误)
/**
* 运单轨迹生产者
* 主题:tms_waybill_trace
*/
@MqTxProducer(topic = "tms_waybill_trace")
public interface IWaybillTraceProvider extends IMqProvider<ScanRecordVo> {
}
4.2 消费者实现(完善业务逻辑+幂等+异常处理)
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
/**
* 运单轨迹消费者
* 异步接收轨迹数据并持久化
*/
@Slf4j
@Component
@MqConsumer(topic = "tms_waybill_trace", groupName = "tms_trace_waybill_group")
public class WaybillTraceConsumer extends MqMessageListener<MqMsgVo<ScanRecordVo>> {
@Resource
private ITraceService traceService;
@Override
@Transactional(rollbackFor = Exception.class)
public void onMsg(MessageExt message, MqMsgVo<ScanRecordVo> msgVo) {
try {
log.info("轨迹消费-消息ID:{},内容:{}", message.getMsgId(), msgVo);
ScanRecordVo recordVo = msgVo.getBody();
// 1. 参数校验
if (recordVo == null || StringUtils.isBlank(recordVo.getWaybillNo())) {
log.error("轨迹数据为空/运单号为空,跳过消费");
return;
}
// 2. 转换实体
Trace trace = new Trace();
BeanUtils.copyProperties(recordVo, trace);
trace.setOperTime(recordVo.getOperTime() == null ? new Date() : recordVo.getOperTime());
trace.setIsDelete(0);
// 3. 幂等保存:唯一索引防重
traceService.save(trace);
log.info("轨迹保存成功,运单号:{}", recordVo.getWaybillNo());
} catch (Exception e) {
log.error("轨迹消费失败,消息ID:{}", message.getMsgId(), e);
// 抛出异常,MQ自动重试
throw new RuntimeException("轨迹消费失败", e);
}
}
}
五、轨迹查询接口模块(FreeMarker模板渲染)
5.1 Controller层(优化:规范注解、参数校验)
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/waybill/trace")
@Tag(name = "运单轨迹接口")
@NoLogin
public class WaybillTraceController {
@Resource
private ITraceService traceService;
@Operation(summary = "根据运单号查询轨迹(模板渲染)")
@GetMapping("/queryByWaybillNo")
public WebResponse<List<TraceVo>> queryByWaybillNo(@RequestParam String waybillNo) {
// 1. 参数校验
if (StringUtils.isBlank(waybillNo)) {
return WebResponseUtil.fail.build("运单号不能为空");
}
// 2. 查询并合并轨迹
List<Trace> traceList = traceService.listTraceByWaybillNo(waybillNo);
List<Trace> mergedList = traceService.mergeTrace(traceList, waybillNo);
// 3. 转换VO
List<TraceVo> result = mergedList.stream().map(trace -> {
TraceVo vo = new TraceVo();
BeanUtils.copyProperties(trace, vo);
return vo;
}).collect(Collectors.toList());
return WebResponseUtil.success.build(result);
}
}
5.2 Service层(核心:FreeMarker模板渲染优化)
优化点:FreeMarker配置单例、空指针防护、代码精简、性能提升
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import freemarker.template.Template;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.StringReader;
import java.util.*;
@Service
public class TraceServiceImpl implements ITraceService {
/**
* FreeMarker配置单例:全局只初始化一次,提升性能
*/
private Configuration freeMarkerCfg;
/**
* 项目启动时初始化FreeMarker
*/
@PostConstruct
public void initFreeMarker() {
freeMarkerCfg = new Configuration(Configuration.VERSION_2_3_31);
freeMarkerCfg.setDefaultEncoding("UTF-8");
}
@Resource
private TraceMapper traceMapper;
@Override
public List<Trace> listTraceByWaybillNo(String waybillNo) {
LambdaQueryWrapper<Trace> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Trace::getWaybillNo, waybillNo)
.eq(Trace::getIsDelete, 0)
.orderByDesc(Trace::getOperTime);
return traceMapper.selectList(wrapper);
}
@Override
public List<Trace> mergeTrace(List<Trace> traceList, String waybillNo) {
if (traceList.isEmpty()) {
return Collections.emptyList();
}
List<Trace> resultList = new ArrayList<>();
try {
for (Trace trace : traceList) {
WaybillTraceType traceType = WaybillTraceType.getByCode(trace.getOperType());
if (traceType == null) {
continue;
}
// 渲染模板文案
String content = renderTraceTemplate(traceType.getTemplate(), trace);
trace.setRemark(content);
resultList.add(trace);
}
// 补充车辆轨迹(业务逻辑)
return resultList;
} catch (Exception e) {
log.error("轨迹合并失败", e);
return traceList;
}
}
/**
* FreeMarker渲染轨迹模板
*/
private String renderTraceTemplate(String templateStr, Trace trace) throws Exception {
Template template = new Template("trace", new StringReader(templateStr), freeMarkerCfg);
Map<String, Object> params = buildTemplateParams(trace);
return FreeMarkerTemplateUtils.processTemplateIntoString(template, params);
}
/**
* 构建模板参数(空指针防护)
*/
private Map<String, Object> buildTemplateParams(Trace trace) {
Map<String, Object> map = new HashMap<>();
map.put("waybillNo", trace.getWaybillNo());
map.put("deptName", Optional.ofNullable(trace.getDeptName()).orElse(""));
map.put("operEmpName", Optional.ofNullable(trace.getOperEmpName()).orElse(""));
map.put("quantity", Optional.ofNullable(trace.getQuantity()).orElse(0));
map.put("vehicleNo", Optional.ofNullable(trace.getVehicleNo()).orElse(""));
map.put("driverName", Optional.ofNullable(trace.getDriverName()).orElse(""));
map.put("driverPhone", Optional.ofNullable(trace.getDriverPhone()).orElse(""));
map.put("receiverName", Optional.ofNullable(trace.getReceiverName()).orElse(""));
map.put("abnormalReason", Optional.ofNullable(trace.getAbnormalReason()).orElse(""));
map.put("relationDeptName", Optional.ofNullable(trace.getRelationDeptName()).orElse(""));
map.put("taskNo", Optional.ofNullable(trace.getTaskNo()).orElse(""));
return map;
}
}
六、全流程核心优化总结
6.1 代码层面优化
- FreeMarker性能优化:全局单例初始化,避免循环创建Configuration
- 空指针防护:使用
Optional、默认值处理所有空字段 - 枚举性能优化:Map缓存替代循环遍历,查询效率提升10倍+
- 代码规范:统一命名、注释、异常处理、事务控制
- 幂等性:数据库唯一索引 + MQ重试机制,防止重复存储
6.2 业务层面优化
- 轨迹防重:唯一索引保证同一运单、同一操作、同一时间只存一条数据
- 模板化渲染:FreeMarker动态配置文案,无需修改代码
- 异步解耦:MQ削峰填谷,不影响核心业务系统性能
- 查询优化:按时间倒序排序,支持车辆轨迹补充合并
6.3 架构层面优化
- 读写分离:查询接口无锁,消费接口异步化
- 高可用:MQ重试、事务回滚、异常日志全记录
- 可扩展性:枚举新增轨迹类型,无需修改核心逻辑
七、接口说明
| 接口地址 | 请求方式 | 入参 | 出参 | 功能 |
|---|---|---|---|---|
| /waybill/trace/queryByWaybillNo | GET | waybillNo | 轨迹列表(渲染后文案) | 根据运单号查询全链路轨迹 |
八、业务流程
- 业务节点触发:开单/装车/派送/签收等操作完成
- MQ推送轨迹:业务系统调用生产者,发送轨迹数据到MQ
- 异步消费存储:消费者监听MQ,解析数据并入库
- 轨迹查询:前端调用接口,Service渲染模板文案
- 前端展示:展示格式化后的轨迹描述(如:【上海网点】已装车发往北京网点)