快递物流系统(详细叙述文档 + 源码)

快递物流系统

1. 项目概述

  • 后端项目地址为:后端
  • Web端项目地址为:Web

本项目旨在提供一套高效、可靠的物流管理系统,涵盖从运单创建、包裹揽收、区域网点处理、分拣中心处理、转运中心处理、末端网点派送到用户签收与支付的全流程管理。系统的设计目标是提升包裹处理效率、提高运输过程的透明度和用户体验,并确保包裹的安全性和准确性。

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. 数据库设计

系统通过以下关键表格设计,实现快递物流全流程管理:

  1. 客户表(Customer)
  2. 员工表(Employee)
  3. 网点表(Logistic)
  4. 运单表(Shipment)
  5. 包裹表(Package)
  6. 转运批次表(Batch)
  7. 载具表(Vehicle)
  8. 包裹位置表(Location)
  9. 载具位置表(Vehicle_location)
3.1.1. customer(客户表)
列名数据类型说明
idbigint客户ID(主键)
usernamevarchar(255)用户名(非空)
phonevarchar(20)电话号码
emailvarchar(255)邮箱
password_hashvarchar(255)Hash密码(非空)
addressjson地址
3.1.2. employee(员工表)
列名数据类型说明
idbigint员工ID(主键)
namevarchar(255)用户名(非空)
phonevarchar(20)电话号码
emailvarchar(255)邮箱
password_hashvarchar(255)Hash密码(非空)
serve_atbigint工作网点
3.1.3. logistic(网点表)
列名数据类型说明
idbigint网点ID(主键)
namevarchar(255)网点名称(非空)
parent_idbigint父级网点id
levelenum (‘province’,‘city’,‘district’)网点等级(三类:省、市、县)
districtvarchar(255)区/县
cityvarchar(255)城市
provincevarchar(255)省份
contact_infovarchar(255)联系方式
3.1.4. shipment(运单表)
列名数据类型说明
idbigint运单ID(主键)
originbigint出发地,外键logistic.id
destinationbigint目的地,外键logistic.id
pricedecimal(10,2)运单价格
statusenum (‘pending’,‘cod pending’, ‘paid’, ‘cancelled’)运单状态
customer_idbigint客户id,外键customer.id
create_datetimestamp创建时间
typeint运单类型
3.1.5. package(包裹表)
列名数据类型说明
idbigint包裹ID(主键)
create_datetimestamp创建时间
statusenum (‘pending’,‘processing’,‘in transit’,‘delivering’,‘signed’, ‘cancelled’)包裹状态
shipment_idbigint运单id
batch_idbigint批次id
receiver_idbigint接收者id
receiver_namevarchar(50)接收者姓名
receiver_addressvarchar(255)接收者地址
receiver_phonevarchar(20)接收者手机号
weightdouble重量
sizevarchar(50)尺寸
sign_datetimestamp签收日期
3.1.6. batch(转运批次表)
列名数据类型说明
idbigint批次ID(主键)
create_datetimestamp创建时间
originbigint批次出发地
destinationbigint批次目的地
responsiblebigint批次责任人
statusenum (‘in trans’, ‘arrive’)批次状态
vehicle_idbigint载具id
3.1.7. vehicle(载具表)
列名数据类型说明
idbigint载具ID(主键)
typevarchar(255)载具类型
shiftvarchar(255)载具车牌号/班次号
3.1.8. location(包裹位置表)
列名数据类型说明
idbigint包裹ID(主键)
coordinatepoint位置信息
timetimestamp时间信息
3.1.9. vehicle_location(载具位置表)
列名数据类型说明
idbigintID(主键)
vehicle_idbigint载具id
coordinatepoint位置信息
timetimestamp时间信息

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部署项目

  • 27
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值