快递物流系统
1. 项目概述
本项目旨在提供一套高效、可靠的物流管理系统,涵盖从运单创建、包裹揽收、区域网点处理、分拣中心处理、转运中心处理、末端网点派送到用户签收与支付的全流程管理。系统的设计目标是提升包裹处理效率、提高运输过程的透明度和用户体验,并确保包裹的安全性和准确性。
2. 需求
2.1. 功能需求
2.1.1. 运单创建
用户通过系统创建运单,需录入包裹的重量、尺寸、数量及收件人信息。系统生成唯一的运单号用于包裹跟踪,并支持查看、编辑和取消运单。
2.1.2. 包裹揽收
揽收任务分配给快递员后,快递员使用移动设备扫描包裹并更新状态为“已揽收”,记录时间和地点。包裹被运至区域网点,并确保揽收信息的实时更新。
2.1.3. 区域网点处理
包裹到达区域网点后,根据大小进行分装处理。大包裹单独建立转运批次,小包裹合并打包成转运批次。记录转运批次的详细信息,确保数据准确性和可追溯性。
2.1.4. 分拣中心处理
分拣中心利用自动化技术对包裹进行拆包和重新分配转运批次,确保每个包裹被准确分配。系统记录分拣情况并更新包裹状态为“运输中”。
2.1.5. 转运中心处理
目的地转运中心接收转运批次后,进行拆包和处理,根据目的地地址重新建立转运批次,更新包裹状态并记录处理详情。
2.1.6. 末端网点派送
包裹到达末端网点后,进行拆包并分配派送任务。快递员进行最后几公里配送,系统实时更新包裹状态为“派送中”。
2.1.7. 用户签收与支付
快递员完成配送后,用户签收确认。系统提供电子签名和拍照确认等方式,更新运单状态为“已签收”。如果运单是到付,用户在签收时完成支付,系统记录支付状态并更新运单状态为“已支付”。
2.1.8. 后台管理端统计
管理员可查看员工、客户、包裹、批次和网点的统计信息。
员工信息管理
管理员可以查询、修改、删除和添加员工信息。
客户信息管理
管理员可以查询、修改和删除客户信息。
报表统计
系统提供包裹报表7天统计、批次报表7天统计和网点报表统计。
2.2. 非功能需求
2.2.1. 性能
系统应支持高并发请求,确保在高峰时段保持良好的响应速度。采用负载均衡、缓存技术和异步处理提升性能。运单和包裹状态的更新应做到实时同步。
2.2.2. 安全性
用户和员工的密码应采用强加密算法进行存储,传输过程中采用HTTPS协议。不同角色应有严格的权限控制,每次请求都必须携带有效的身份验证Token。
2.2.3. 可靠性
系统应定期进行数据备份,并提供数据恢复机制。系统具备自动检测和处理故障的能力,提供日志记录和监控报警功能。
3. 系统设计
3.1. 数据库设计
系统通过以下关键表格设计,实现快递物流全流程管理:
- 客户表(Customer)
- 员工表(Employee)
- 网点表(Logistic)
- 运单表(Shipment)
- 包裹表(Package)
- 转运批次表(Batch)
- 载具表(Vehicle)
- 包裹位置表(Location)
- 载具位置表(Vehicle_location)
3.1.1. customer(客户表)
列名 | 数据类型 | 说明 |
---|---|---|
id | bigint | 客户ID(主键) |
username | varchar(255) | 用户名(非空) |
phone | varchar(20) | 电话号码 |
varchar(255) | 邮箱 | |
password_hash | varchar(255) | Hash密码(非空) |
address | json | 地址 |
3.1.2. employee(员工表)
列名 | 数据类型 | 说明 |
---|---|---|
id | bigint | 员工ID(主键) |
name | varchar(255) | 用户名(非空) |
phone | varchar(20) | 电话号码 |
varchar(255) | 邮箱 | |
password_hash | varchar(255) | Hash密码(非空) |
serve_at | bigint | 工作网点 |
3.1.3. logistic(网点表)
列名 | 数据类型 | 说明 |
---|---|---|
id | bigint | 网点ID(主键) |
name | varchar(255) | 网点名称(非空) |
parent_id | bigint | 父级网点id |
level | enum (‘province’,‘city’,‘district’) | 网点等级(三类:省、市、县) |
district | varchar(255) | 区/县 |
city | varchar(255) | 城市 |
province | varchar(255) | 省份 |
contact_info | varchar(255) | 联系方式 |
3.1.4. shipment(运单表)
列名 | 数据类型 | 说明 |
---|---|---|
id | bigint | 运单ID(主键) |
origin | bigint | 出发地,外键logistic.id |
destination | bigint | 目的地,外键logistic.id |
price | decimal(10,2) | 运单价格 |
status | enum (‘pending’,‘cod pending’, ‘paid’, ‘cancelled’) | 运单状态 |
customer_id | bigint | 客户id,外键customer.id |
create_date | timestamp | 创建时间 |
type | int | 运单类型 |
3.1.5. package(包裹表)
列名 | 数据类型 | 说明 |
---|---|---|
id | bigint | 包裹ID(主键) |
create_date | timestamp | 创建时间 |
status | enum (‘pending’,‘processing’,‘in transit’,‘delivering’,‘signed’, ‘cancelled’) | 包裹状态 |
shipment_id | bigint | 运单id |
batch_id | bigint | 批次id |
receiver_id | bigint | 接收者id |
receiver_name | varchar(50) | 接收者姓名 |
receiver_address | varchar(255) | 接收者地址 |
receiver_phone | varchar(20) | 接收者手机号 |
weight | double | 重量 |
size | varchar(50) | 尺寸 |
sign_date | timestamp | 签收日期 |
3.1.6. batch(转运批次表)
列名 | 数据类型 | 说明 |
---|---|---|
id | bigint | 批次ID(主键) |
create_date | timestamp | 创建时间 |
origin | bigint | 批次出发地 |
destination | bigint | 批次目的地 |
responsible | bigint | 批次责任人 |
status | enum (‘in trans’, ‘arrive’) | 批次状态 |
vehicle_id | bigint | 载具id |
3.1.7. vehicle(载具表)
列名 | 数据类型 | 说明 |
---|---|---|
id | bigint | 载具ID(主键) |
type | varchar(255) | 载具类型 |
shift | varchar(255) | 载具车牌号/班次号 |
3.1.8. location(包裹位置表)
列名 | 数据类型 | 说明 |
---|---|---|
id | bigint | 包裹ID(主键) |
coordinate | point | 位置信息 |
time | timestamp | 时间信息 |
3.1.9. vehicle_location(载具位置表)
列名 | 数据类型 | 说明 |
---|---|---|
id | bigint | ID(主键) |
vehicle_id | bigint | 载具id |
coordinate | point | 位置信息 |
time | timestamp | 时间信息 |
4.实现
4.1.客户功能实现
4.1.1.下单功能实现
通过运单DTO传参,在Service层赋值给运单实体数据,最后把运单实体数据插入数据库表格shipment
,之后自动计算单个包裹的运费,将所有包裹费用综合在一起后为整个运单的价格。
/**
* 新建运单
* @param DTO 新建运单信息
* @return 运单ID
*/
public Long createShipment(CreateShipmentDTO DTO) {
Shipment shipment = Shipment.builder()
.id(Long.parseLong(generateShipmentId(DTO.getOrigin())))
.origin(DTO.getOrigin())
.destination(DTO.getDestination())
.price(0.0)
.status("cod_pending".equals(DTO.getPayMethod()) ? Shipment.statusEnum.COD_PENDING.getStatus() : Shipment.statusEnum.PENDING.getStatus())
.customerId(DTO.getCustomerId())
.type(DTO.getType())
.createDate(new java.sql.Timestamp(System.currentTimeMillis()))
.build();
shipmentMapper.insert(shipment);
return shipment.getId();
}
/**
* 计算运单中单个包裹的运费
* @param calculatePriceDTO 计算运费的信息
* @return 运费
*/
public Double calculatePrice(CalculatePriceDTO calculatePriceDTO) {
Double weight = calculatePriceDTO.getWeight();
String size = calculatePriceDTO.getSize();
Integer type = calculatePriceDTO.getType();
Double L = Double.parseDouble(size.split(",")[0]);
Double W = Double.parseDouble(size.split(",")[1]);
Double H = Double.parseDouble(size.split(",")[2]);
Long origin = calculatePriceDTO.getOrigin();
Long destination = calculatePriceDTO.getDestination();
double volume = L * W * H;
double VWeight = 0;
switch (type) {
case 0: // 标快
if(weight < 30) {
VWeight = volume / 12000;
} else {
VWeight = volume / 6000;
}
break;
case 1: // 特快
if(shipmentService.isSameArea(origin, destination)) {
VWeight = volume / 12000;
} else {
VWeight = volume / 6000;
}
break;
}
double finalWeight = Math.max(weight, VWeight);
if(finalWeight <= 10) {
finalWeight = Math.ceil(finalWeight * 10) / 10;
} else if (finalWeight <= 100) {
finalWeight = Math.ceil(finalWeight * 2) / 2;
} else {
finalWeight = Math.round(finalWeight);
}
return finalWeight * 10; // 10元/kg
}
4.1.2.查看运输状态功能实现
将包裹状态设为5种枚举:等待揽收、揽收、运输中、派送、签收。用户登录后可以直接查看包裹的运输状态以及当前包裹位置。
/**
* 插入包裹位置信息
* @param locationDTO 位置信息
*/
@PostMapping("/insertPackageLocation")
public Result insertPackageLocation(@RequestBody LocationDTO locationDTO) {
try {
locationService.insertPackageLocation(locationDTO);
return Result.ok().message("插入包裹位置信息成功");
} catch (Exception e) {
e.printStackTrace();
return Result.error("插入包裹位置信息失败");
}
}
/**
* 根据包裹id获取包裹位置历史信息
* @param id 包裹id
* @return 查询到的包裹位置历史信息
*/
@GetMapping("/getPackageLocation")
public Result getPackageLocation(@RequestParam(required = true) Long id) {
return Result.ok(locationService.getPackageLocation(id)).message("获取包裹位置成功");
}
4.1.3.获取包裹历史运输信息功能实现
设计时间链条,每个时间点对应一个包裹状态的改变,并且包裹对应的批次也会相应变换。
/**
* 获取包裹历史
* @param packageId 包裹id
* @return 包裹历史
*/
public List<Map<Timestamp, String>> getPackageHistory(Long packageId) {
String status = packageMapper.selectById(packageId).getStatus();
Timestamp signDate = packageMapper.selectById(packageId).getSignDate();
List<HistoryDTO> history = batchService.getBatchByPackageId(packageId);
List<Map<Timestamp, String>> result = new ArrayList<>();
Timestamp createDate = packageMapper.selectById(packageId).getCreateDate();
Timestamp updateDate = packageMapper.selectById(packageId).getSignDate();
result.add(Map.of(createDate, "包裹已创建,正在处理。"));
for(HistoryDTO h : history){
String originName = logisticService.getLogisticName(h.getOrigin());
String destinationName = logisticService.getLogisticName(h.getDestination());
if(h.getStatus().equals(Batch.statusEnum.IN_TRANS.getStatus())){
result.add(Map.of(updateDate, "包裹正从" + originName + "发往" + destinationName + "。"));
} else {
result.add(Map.of(updateDate, "包裹已由" + originName + "抵达" + destinationName + "。"));
}
}
switch (status) {
case "pending":
break;
case "in_transit":
break;
case "arrived":
result.add(Map.of(updateDate, "包裹已抵达目的地,正在等待派送。"));
break;
case "signed":
result.add(Map.of(updateDate, "包裹已被签收,签收时间为" + signDate + "。"));
break;
default:
result.add(Map.of(updateDate, "包裹状态更新中。"));
break;
}
return result;
}
4.2.员工功能实现
4.2.1.查看未揽收包裹功能实现
根据前端传回的网点id,后端查询数据库,查询完成后返回所有与网点id相关的所有包裹,作为List数据返回。
/**
* 获取未揽收的包裹
* @param logisticId 网点id
* @return 未揽收的包裹
*/
public List<Package> getUnpickedPackages(Long logisticId) {
List<Long> shipmentIds = shipmentService.getShipmentIdsByOrigin(logisticId);
QueryWrapper<Package> queryWrapper = new QueryWrapper<>();
if(!shipmentIds.isEmpty()) {
queryWrapper.in("shipment_id", shipmentIds);
queryWrapper.eq("status", Package.statusEnum.PENDING.getStatus());
} else {
return null;
}
return packageMapper.selectList(queryWrapper);
}
4.2.2.包裹状态更新
在员工对包裹进行等待揽收、揽收、运输中、派送、签收的状态转换时,加入一个更新包裹状态的私有方法,所有包裹状态更新均可以调用该方法进行状态转换。
/**
* 更新包裹状态私有方法
* @param packageId 包裹id
* @param status 包裹状态
* @return 是否更新成功
*/
private boolean updatePackageStatus(Long packageId, String status) {
Package aPackage = packageMapper.selectById(packageId);
aPackage.setStatus(status);
return packageMapper.updateById(aPackage) == 1;
}
4.2.3.获取包裹下一个目的地
在员工运输包裹的过程中,系统根据前端传来的包裹ID以及当前网点ID,自动计算最近距离,返回包裹的下一个目的地。
/**
* 获取包裹的下一个目的地
* @param packageId 包裹id
* @param currentLogisticId 当前网点id
* @return 下一个目的地
*/
public String getNextDestination(Long packageId, Long currentLogisticId) {
Package aPackage = packageMapper.selectById(packageId);
Long shipmentId = aPackage.getShipmentId();
Shipment shipment = shipmentService.getShipmentById(shipmentId);
return logisticService.getNextDestination(shipment.getOrigin(), shipment.getDestination(), currentLogisticId);
}
4.3.管理员功能实现
4.3.1.员工增删改查功能实现
展示员工分页查询及增加员工的功能,其余的删改功能可以通过MyBatis-Plus进行轻松实现。
/**
* 查询员工信息的分页方法
* @param pageNo 当前页码
* @param pageSize 每页显示的记录数
* @return 分页对象,包含查询结果和分页信息
*/
public BackPage<Employee> queryEmployeesPage(Long pageNo, Long pageSize) {
BackPage<Employee> EmployeeBackPage = new BackPage<>();
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
Page<Employee> EmployeePage = new Page<>(pageNo, pageSize);
IPage<Employee> EmployeeIPage = page(EmployeePage, wrapper);
EmployeeBackPage.setContentList
(EmployeeIPage.getRecords());
EmployeeBackPage.setTotalElements(EmployeeIPage.getTotal());
EmployeeBackPage.setTotalPages(EmployeeIPage.getPages());
return EmployeeBackPage;
}
/**
* 增加员工信息
* @param employee 新员工信息
* @return 增加是否成功
*/
public boolean insertEmployee(Employee employee) {
try {
employeeMapper.insert(employee);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
4.3.2.运输线路生成
选择起点与终点后,自动生成运输路径,包括起点中转站与中转站之间的运输线路,最后生成目的地路径。
/**
* 获取物流点的路径
* @param start 起点
* @param end 终点
* @return 物流点路径
*/
public List<Logistic> getLogisticPath(Long start, Long end) {
List<Logistic> path = new ArrayList<>();
Queue<Long> queue = new LinkedList<>();
Map<Long, Boolean> visited = new HashMap<>();
queue.add(start);
while (!queue.isEmpty()) {
Long logisticId = queue.poll();
visited.put(logisticId, true);
Logistic logistic = logisticMapper.selectById(logisticId);
path.add(logistic);
if(logistic.getId().equals(end)) {
break;
}
List<Long> neighbors = logisticMapper.selectNeighbors(logisticId);
for(Long neighbor : neighbors) {
if(!visited.containsKey(neighbor)) {
queue.add(neighbor);
}
}
}
return path;
}
4.3.3.生成运输费用表
在展示运单数据的过程中,对其包裹数据分别计算费用,自动生成费用表并展示。
/**
* 生成费用表
* @param shipmentId 运单id
* @return 费用表
*/
public Map<Long, Double> generateCostTable(Long shipmentId) {
Map<Long, Double> costTable = new HashMap<>();
List<Package> packages = packageMapper.selectByShipmentId(shipmentId);
for(Package aPackage : packages) {
CalculatePriceDTO calculatePriceDTO = CalculatePriceDTO.builder()
.weight(aPackage.getWeight())
.size(aPackage.getSize())
.type(aPackage.getType())
.origin(shipmentService.getShipmentById(aPackage.getShipmentId()).getOrigin())
.destination(shipmentService.getShipmentById(aPackage.getShipmentId()).getDestination())
.build();
Double price = calculatePrice(calculatePriceDTO);
costTable.put(aPackage.getId(), price);
}
return costTable;
}
4.3.4.员工权限设置
采用基于角色的访问控制(Role-Based Access Control,RBAC)对员工权限进行设置,管理员可以为不同员工赋予不同权限。
/**
* 为员工赋予角色
* @param employeeId 员工id
* @param role 角色
* @return 是否赋予成功
*/
public boolean assignRoleToEmployee(Long employeeId, String role) {
Employee employee = employeeMapper.selectById(employeeId);
employee.setRole(role);
return employeeMapper.updateById(employee) == 1;
}
5.运行
最后再补充一句,上述环境开发完毕之后,如果想学习服务部署Docker的话,可以看这篇(●ˇ∀ˇ●)!
点击这里------------------>:Docker部署项目