航空散货调度系统:SpringBoot+RBAC与贪心算法实践

发布时间:2026/9/16 17:08:49
航空散货调度系统:SpringBoot+RBAC与贪心算法实践 简介一套面向毕业设计场景的航空散货调度系统Java实现资料包基于SpringBoot与Vue前后端分离架构覆盖用户权限管理、航班信息维护、散货调度计划生成、实时货物追踪及运输路线优化等核心模块并附带毕业论文与答辩PPT适合计算机相关专业学生用于课程设计、毕业设计或SpringBoot项目实战参考。压缩包共431个文件约9.88MB主要包含124个Java后端源码文件、107个Vue前端页面文件、43个JS脚本及CSS样式文件另有SQL数据库脚本、XML配置、PNG/JPG图片素材和文档资料目录划分清晰可对照前后端代码理解调度流程与权限控制实现。已有96人学习下载适合需要参考完整项目结构、业务流程与论文章节的读者。资源价值在于从数据库设计到前后端联调均有完整代码支撑并可通过条形码/二维码跟踪、异常报警等代码片段学习物流监控思路PPT与论文则能直接辅助答辩展示与文档撰写节省从零搭建系统的时间。1. 航空散货调度为什么需要一套独立系统当年我接过一个机场货运站的散货调度小项目最有感触的不是算法而是调度员每天打开Excel按航班时间手工挑货、排顺序超载了再拿出来装漏了只能靠货主打电话来催。散货不像集装箱货有固定舱位它的体积、重量、优先级、目的地全部混在一起航班货仓剩余容量又每分钟都在变人工判断很容易出错。所以我们需要一套能管住“用户权限—航班—货物—调度计划—跟踪记录”的航空散货调度系统用SpringBoot做后端最合适它自带嵌入式TomcatREST接口开发快配合MyBatis和MySQL就能把CRUD做得很扎实这套设计也是计算机专业毕设里很有价值的一个方向。适合我正在做的调度场景也适合准备拿SpringBoot做毕业设计的你。2. RBAC权限模型与SpringBoot数据层落地2.1 角色拆分与权限边界先说为什么不能一张user表加一个role字段了事。航空散货调度里管理员要管用户和航班调度员要生成调度计划、处理异常货站操作员扫码更新状态货代只能看自己委托的货物。不同角色对同一份数据的操作范围完全不同。如果只靠role字段代码里到处都是 if(role1) 的判断改一个需求就要改一圈。所以采用RBAC模型把权限拆成“用户—角色—权限”三层。角色可访问接口示例权限范围系统管理员/api/user/, /api/flight/全部调度员/api/dispatch/, /api/track/航班与调度货站操作员/api/track/scan更新货物位置货代/api/cargo/my仅本人委托货物这个模型建模为四张表user、role、user_role、role_permission。权限粒度的设计我倾向于控制在接口级别比如“可以访问 /dispatch/**”就是一个权限而不是细化到菜单按钮因为散货调度系统的用户通常是自己人权限太大导致能调整数据风险反而不高最主要的需求是防止货代看到别的货代货物。所以角色与权限的映射关系写死在初始化SQL里即可。这样做还有一个好处新增一个“审计员”角色时只要在role_permission表里插入几条记录代码不用动如果使用 PreAuthorize(hasRole(ADMIN))注意这个注解条件是OR关系多个角色时用 hasAnyRole(ADMIN,DISPATCHER) 更合适。2.2 核心实体设计与字段约束围绕调度业务核心实体有用户、角色、航班、货物、调度单、跟踪记录。下面给出Cargo实体属性尽量贴合散货场景package com.airfreight.entity; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(cargo) public class Cargo { private Long id; private String barcode; // 条形码/二维码 private String cargoName; // 货物名称 private Integer cargoType; // 货物类型1-普通 2-急件 3-冷链 private Integer quantity; // 件数 private Double weight; // 总重量 kg private Double volume; // 总体积 m3 private Integer priority; // 优先级 1低 2中 3高 private String destination; // 目的地城市 private Integer status; // 状态0待调度 1已配载 2在途 3到达 4已提取 private Long flightId; // 当前关联航班 private Long ownerId; // 委托货代用户ID private LocalDateTime createTime; }barcode是给扫码枪用的一般对应一个唯一idweight和volume是调度核心约束priority来源可以是货主支付时效加急费或保鲜要求需要单独维护ownerId用于货代只能看自己数据的隔离。建表时给barcode建立唯一索引状态字段加普通索引这样按航班查货物、按条码查轨迹都不至于全表扫描。用户实体不在上面展开但要注意密码必须加密存储。我一般用Spring Security自带BCryptPasswordEncoder注册时encode登录时matches不要把明文密码写在日志里。登录接口返回的JWT里面只放userId和role过期时间设为8小时方便调度员连续操作。航班实体没有完整展开但必须包含flightNo、departureCity、arrivalCity、departureTime、arrivalTime、maxWeight、maxVolume、status这几个字段调度算法会直接读取maxWeight和maxVolume作为约束status用于判断航班是否处于OPEN状态只有OPEN状态的航班才能继续调度。2.3 MyBatis动态SQL与常用查询数据层我建议用MyBatis-Plus因为课程设计和毕设里最少写CRUD最怕手写大量重复标签。但动态SQL还是要掌握例如按条件查询货物列表select idselectCargoByCondition resultTypecom.airfreight.entity.Cargo SELECT * FROM cargo where if testflightId ! null AND flight_id #{flightId} /if if teststatus ! null AND status #{status} /if if testdestination ! null and destination ! AND destination #{destination} /if if testownerId ! null AND owner_id #{ownerId} /if /where ORDER BY priority DESC, create_time ASC /selectwhere会自动去掉第一个多余的ANDif判断参数是否为null或空字符串这样调度员可以只传航班号看该航班所有货物也可以只传目的地做汇总ORDER BY priority DESC能保证高优先级货物排在最前面后续调度算法直接取列表就行。参数说明flightId对应航班主键status取值0到4destination使用城市编码避免中文模糊ownerId来自token解析后的当前用户不信任前端传值。2.3.1 用拦截器校验角色权限控制如果不引入Spring Security可以用一个轻量HandlerInterceptor在进入Controller之前校验。示例public class RoleInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String role request.getHeader(role); // 实际项目从JWT解析 if (!ADMIN.equals(role) !DISPATCHER.equals(role)) { response.setStatus(403); return false; } return true; } }这个简化版为了演示逻辑直接把role放在请求头实际项目应该从JWT里解析payload中的role字段可以用jjwt库读取。注册拦截器时注意只拦截需要校验的路径静态资源不要拦否则扫描二维码的图片都加载不出来。3. 散货调度计划生成贪心装载算法与扫码跟踪接口3.1 为什么散货装载需要实时计算航班货仓有最大载重和最大容积散货每一件都不是标准集装箱重量体积比差异很大比如一小箱芯片和一大包纺织品同样重量但体积差出几倍。单纯按“先到先装”会让货仓出现大量剩余空间或者重货把载重占满、容积还空着。因此调度计划生成的核心是在满足航班载重和容积约束下优先保证高优先级货物被装上同时对剩余空间做尽量好的利用。3.2 贪心调度算法实现我采用的是“优先级降序 密度降序”的二级排序策略。先按priority从高到低排再按weight/volume从高到低排逐个尝试放入航班。完整代码如下Service public class DispatchService { public DispatchPlan createPlan(Flight flight, ListCargo cargoList) { ListCargo sorted new ArrayList(cargoList); sorted.sort(Comparator .comparingInt(Cargo::getPriority).reversed() .thenComparingDouble(c - c.getWeight() / c.getVolume()) .reversed() ); double remainWeight flight.getMaxWeight(); double remainVolume flight.getMaxVolume(); double epsilon 0.0001; DispatchPlan plan new DispatchPlan(); plan.setFlightId(flight.getId()); plan.setItems(new ArrayList()); for (Cargo cargo : sorted) { if (cargo.getStatus() ! 0) continue; // 只处理待调度货物 if (remainWeight epsilon cargo.getWeight() remainVolume epsilon cargo.getVolume()) { remainWeight - cargo.getWeight(); remainVolume - cargo.getVolume(); plan.getItems().add(cargo); cargo.setStatus(1); cargo.setFlightId(flight.getId()); cargoService.updateById(cargo); } } plan.setUsedWeight(flight.getMaxWeight() - remainWeight); plan.setUsedVolume(flight.getMaxVolume() - remainVolume); return plan; } }排序时用了 comparingInt(priority).reversed()然后 thenComparingDouble 按货物密度倒序外面的 reversed 是让密度也从高到低。每件货物只有status为0才能被调度调度后立即更新状态避免并发场景下同一票货物被重复装入两个航班。参数说明flight.getMaxWeight()单位是kggetMaxVolume()单位是m3cargoService.updateById是MyBatis-Plus的更新方法用于落库。epsilon用来处理浮点误差避免实际载重刚好卡线时因为double精度误判。为什么不用0-1背包求全局最优我在答辩时被问过这个问题。散货调度确实可建模成背包问题但0-1背包动态规划的复杂度是O(n*W)W是载重离散化后的公斤数最大能到几万甚至十几万矩阵会非常大加上每个航班还有容积第二维约束就变成二维背包DP状态空间更夸张。工程上贪心的好处是可解释、可复现、毫秒级出结果调度员能理解为什么这批货被装上那批货被留下。如果后续要追求最优解可以在贪心结果上做局部交换优化即把已装货物和未装货物中密度相近的互换看看能否装下优先级更高的货。3.3 扫码跟踪与状态机散货从入库到装机要经历多个节点收货、安检、组板、装机、到达、提取。每个节点贴的条形码都会被扫码枪扫一下。系统需要提供一个极简的扫码上报接口RestController RequestMapping(/api/track) public class TrackController { PostMapping(/scan) public Result scan(RequestBody TrackScanRequest req) { Cargo cargo cargoService.findByBarcode(req.getBarcode()); if (cargo null) { return Result.error(条码不存在); } if (!checkNodeValid(cargo.getStatus(), req.getNode())) { return Result.error(节点状态不合法当前状态 cargo.getStatus()); } CargoTrack track new CargoTrack(); track.setCargoId(cargo.getId()); track.setNode(req.getNode()); track.setLocation(req.getLocation()); track.setOperatorId(req.getOperatorId()); track.setCreateTime(LocalDateTime.now()); trackService.save(track); cargo.setStatus(nodeToStatus(req.getNode())); cargoService.updateById(cargo); return Result.ok(track); } }scan接口是典型的“扫码枪触发”输入体里只有barcode、node、location、operatorId不需要手工传货物主键。checkNodeValid会做状态机校验比如货物还在“待调度”时不可能扫到“已装机”节点避免误操作。状态机如下当前status允许扫描node0待调度收货、安检1已配载组板、装机2在途到达3到达提取4已提取无接口将扫码记录写入track表同时更新cargo主状态。货代查“我的货物”时可以按cargoId把track列表按时间正序返回这样在Web端就能看到完整轨迹。另外扫码接口要设计成幂等同一节点重复扫描时不报错直接返回当前记录因为扫码枪有时会双击不能因为一次重复扫码让状态机卡死。幂等可以给track表加唯一约束(barcode, node, cargo_id)重复插入时捕获DuplicateKeyException后忽略即可。4. 异常监控与运营统计SpringBoot定时任务与ActiveMQ消息通知4.1 需要监控的异常场景调度系统不只是录入和查询它要在问题发生前提醒操作员。常见异常包括货物入库后超过24小时没有调度航班状态从准时变成延误或取消已经配载的货物需要重新调度货物装机后超过预计到达时间还没扫码航班实际载重超过计划的90%预警。这些规则可以用一张配置表存阈值也可以用代码写死。我一般把常用规则写死把阈值做成系统参数方便调整。异常类型判断条件默认处理未调度超时入库时间距今24h且status0告警并通知调度员航班状态变更航班状态变为延误或取消重算已配载货物装机未到达装机后12h无到达扫码告警并通知货代航班负载预警已配载重量/最大重量90%提醒调度员复核4.2 定时扫描任务实现SpringBoot里实现轮询很简单在Service方法上标注 Scheduled 即可。下面这个任务每5分钟检查一次“入库超过24小时且未调度”的货物Component public class MonitorTask { private static final Logger log LoggerFactory.getLogger(MonitorTask.class); Scheduled(fixedDelay 300000) public void checkUnplannedCargo() { LocalDateTime deadline LocalDateTime.now().minusHours(24); ListCargo overdue cargoService.findByStatusAndCreateTimeBefore(0, deadline); for (Cargo cargo : overdue) { Alert alert Alert.builder() .cargoId(cargo.getId()) .alertType(UNPLANNED_TIMEOUT) .content(货物 cargo.getBarcode() 入库超过24小时仍未调度) .status(0) .createTime(LocalDateTime.now()) .build(); alertService.save(alert); activeMqSender.send(queue.alert, alert); } } }fixedDelay300000表示上一次任务执行结束后5分钟再执行区别于fixedRate的固定频率避免任务超时重叠。deadline是24小时前的时刻findByStatusAndCreateTimeBefore会转化为SQL条件 status0 and create_time #{deadline}。注意扫描出的告警要先落库再发消息防止消息发送成功但记录丢失导致后续重复处理。4.3 整合ActiveMQ做异步通知为什么要把告警发给消息队列而不是直接在任务里调短信服务因为短信或邮件接口可能超时会让定时任务阻塞有了ActiveMQ告警产生后立即入队由消费端慢慢消费。配置先加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-activemq/artifactId /dependency然后在application.yml里配置broker地址spring: activemq: broker-url: tcp://127.0.0.1:61616 user: admin password: admin jms: pub-sub-domain: false发送端用JmsTemplate消费端用 JmsListenerComponent public class AlertConsumer { JmsListener(destination queue.alert) public void onAlert(Alert alert) { // 根据alertType调用短信、微信公众号或邮件通道 log.info(dispatch alert: {}, alert.getContent()); } }pub-sub-domainfalse表示走Queue模式由多个消费者竞争消费语义上一条告警只通知一次如果希望广播给所有管理人员改成true并配置Topic。JMS的ObjectMessage可以传对象但注意实体要实现Serializable或者直接传JSON字符串避免兼容问题。ActiveMQ在这里的定位是削峰和解耦不是业务核心所以不要花太多时间研究持久化和集群本地单机部署足够支撑毕设演示。4.4 统计报表SQL与REST接口数据统计方面最常用的报表是航班利用率、货物周转率、收入分析。用一句SQL就能得到基础数据SELECT f.flight_no, SUM(dd.weight) AS load_weight, f.max_weight, SUM(dd.weight) / f.max_weight * 100 AS utilisation_rate FROM flight f JOIN dispatch_detail dd ON f.id dd.flight_id WHERE f.departure_time BETWEEN #{startTime} AND #{endTime} GROUP BY f.id, f.flight_no, f.max_weight ORDER BY utilisation_rate DESC这里用了JOIN和GROUP BY得到每个航班实际装载重量和利用率。可以在Service里把这个SQL映射到StatisticDTO再通过Controller的 GET /api/statistic/utilisation?startTime...endTime... 暴露。调用时需要注意时区前端传的是字符串后端用 DateTimeFormat(patternyyyy-MM-dd HH:mm:ss) 解析否则会报类型转换错误。除了航班利用率我还会统计每个货代的货物总周转天数AVG(提取时间-入库时间)这个直接按owner_id分组数据量到万级时也不会慢。如果要做大屏这些接口都建议在SQL层完成聚合避免把全表List加载到内存里再用Java算那样内存会被打满。同理绩效评估也可以按owner_id统计订单完成量和平均时效作为货代评级依据。5. 多模式路线优化与SpringBoot部署技巧5.1 用Dijkstra规划空地联运路径运输路线优化不能只看直航很多时候散货要先陆运到枢纽机场再飞目的城市最后陆运派送。我把机场和城市抽象成节点航空段和地面段抽象成边边的权重是“时间×系数 成本×系数”系数根据货物特性调整。求最短路径时用Dijkstra算法public ListString bestRoute(String start, String end, MapString, ListEdge graph, double timeWeight, double costWeight) { MapString, Double dist new HashMap(); MapString, String prev new HashMap(); PriorityQueueNode queue new PriorityQueue( Comparator.comparingDouble(n - n.dist)); queue.add(new Node(start, 0)); dist.put(start, 0.0); while (!queue.isEmpty()) { Node cur queue.poll(); if (cur.id.equals(end)) break; for (Edge edge : graph.getOrDefault(cur.id, List.of())) { double nd cur.dist edge.time * timeWeight edge.cost * costWeight; if (nd dist.getOrDefault(edge.to, Double.MAX_VALUE)) { dist.put(edge.to, nd); prev.put(edge.to, cur.id); queue.add(new Node(edge.to, nd)); } } } return buildPath(prev, start, end); }timeWeight和costWeight是两个入参冷链货可以把timeWeight调高便宜货可以把costWeight调高dist保存每个节点当前最优值PriorityQueue保证每次取到未确定节点中距离最小的。该算法是O(E log V)对几十个城市完全没压力。实际使用时要给边加上航班时刻表的日期窗口只有时间上能衔接的航班边才加入图里否则算出来的路径可能是昨天的航班。5.2 敏感配置加密与前后端分离部署数据库密码、ActiveMQ口令直接写在application.yml里不安全。我习惯用jasypt-spring-boot把密码替换为 ENC(...) 密文启动时加参数-Djasypt.encryptor.password密钥这样密文放到代码仓库也不会泄露。注意如果SpringBoot版本太高jasypt版本也要跟着升级否则解密器初始化的包名对不上会报错。这个毕设前端是Vue工程构建后静态资源会生成app和chunk-vendors等带哈希的css、js文件。把这些文件放到SpringBoot项目的static目录下再启动jar包同一个端口就可以同时提供页面和接口避免跨域配置。如果你在Idea里创建SpringBoot项目时经常超时先检查Maven镜像仓库和JDK环境变量配置不要反复重试环境变量配好后创建项目通常几秒钟就能完成。5.3 启动参数与防重复扫码细节部署时我还习惯在启动命令里加上--server.tomcat.max-threads200 --server.tomcat.accept-count500分别控制最大工作线程数和队列等待长度接口压力集中在扫码上报时线程数太小会拖垮页面访问。扫码枪本质是个键盘输入设备焦点放在输入框时按回车会提交前端做防重复提交时要等接口返回后再清空条码框避免同一票货被连续扫两次。本文还有配套的精品资源点击获取