资讯动态

Spring Boot气体城市货运系统:订单调度与签收闭环实现

发布时间:2026/10/3 9:06:26 来源:尧图企业网站定制
最近我把一个基于Spring Boot的“锦宇气体城市货运系统”从零到一完整做了一遍。这个选题原本是计算机毕设常见的方向但如果你只是按“增删改查”糊弄过去那就浪费了它背后一整套业务逻辑。锦宇气体是一家做工业气体城市配送的公司客户是工厂、医院、实验室配送物品是氧气、氮气、氩气、乙炔这类的气瓶。这类业务不像普通快递气体有安全属性气瓶要跟踪订单要调度配送完成还要回单签收。所以这个系统核心就是订单怎么进来、车辆怎么调度、气瓶怎么流转、签收怎么闭环。整条链路捋通就是一个完整体面的毕设项目。这篇文章我会把整个项目的设计思路、技术选型、数据库建模、关键代码实现和坑点全部拆开讲清楚。适合正在做Spring Boot毕设的同学也适合想用真实业务场景练手的开发者。你不用照着我的代码抄但可以把这个业务模型当作参考套到你自己的题目里。1. 项目到底在做什么需求拆解与系统边界1.1 气体城市货运的业务痛点与系统定位先别急着写代码把业务搞清楚比任何技术都重要。气体城市货运和普通快递配送最大的区别在于“货”的特殊性。以锦宇气体为例它配送的工业气体钢瓶属于压力容器每一瓶都有出厂编号、检验日期、充装记录而且客户下单往往不是买一个瓶子而是买“一瓶充好气的瓶子”用完以后旧瓶还要回收。也就是说系统里要同时管理订单、气瓶档案、配送任务和回瓶记录。这个定位想清楚之后系统就不是一个简单的“订单管理系统”而是一个轻量级的业务闭环系统。它要解决的核心问题有三个第一客户打电话或在小程序下单后业务员能快速生成销售订单不用Excel来回传第二调度员能根据订单地域分布、车辆在途状态把订单合并成配送单指派给司机第三司机完成配送后客户签收信息能实时回传气瓶的去向能查得到。所以我的项目定位是一个“给中小型气体贸易公司使用的内部运营系统”目标用户是业务员、调度员、司机、财务和管理员。不需要做C端商城那样复杂的营销功能但业务流程必须完整、状态必须可追踪。这个边界定下来后面所有的表结构和接口设计就都有依据了。1.2 角色划分与核心用例基于这个定位我把系统划分为四类角色。普通用户业务员负责录入订单、维护客户信息调度员看订单池把待配送订单组合成配送单指派车辆和司机司机端我做了简化用Web页面模拟移动端操作司机登录后能看到自己的配送任务点击“出发”、“送达”上传签收情况管理员负责用户管理、数据统计和基础数据维护。核心用例围绕订单生命周期展开。我整理了五个关键流程客户下单、订单审核、调度派车、配送执行、签收归档。客户下单可以由业务员代录也可以预留接口给小程序订单审核是为了防止超卖或余额不足调度派车是整个系统的核心涉及订单合并、车辆匹配配送执行和签收归档则把“货”和“单”全部闭环。这几个用例画成用例图其实很简单但你要能讲清楚为什么需要每个环节。比如“订单审核”这个步骤很多毕设项目会省掉但气体配送涉及安全责任订单必须确认气瓶库存足够才能派车。你多设计一个审核状态答辩时就能顺手讲出业务合理性。1.3 功能清单与优先级功能清单我分成三档。第一档是核心功能必须完整实现包括登录认证、客户管理、气瓶档案管理、订单管理、配送单管理、签收回单。第二档是增强功能包括车辆管理、司机管理、路线统计、订单看板。第三档是拓展功能包括报表导出、短信通知、小程序下单接口有时间再做。我自己定的优先级是先跑通“订单-调度-签收”这条主链路再去补统计和报表。很多同学做毕设喜欢先做权限管理再做一堆基础资料的增删改查最后发现核心业务流程没时间做这是大忌。记住答辩老师最想看到的不是你有多少张表而是你能不能把一条完整业务线讲清楚。2. 技术选型为什么是Spring Boot这套组合2.1 用Spring Boot做后端的真实理由Spring Boot在这类毕设项目里几乎是标配但你要能说出为什么而不是“大家都在用”。我的理解是它帮你把以前Spring MVC项目里大量繁琐的XML配置全部自动化了。就拿这个项目举例我需要内嵌Tomcat、需要自动配置数据源、需要快速集成MyBatis-Plus这些在Spring Boot里只需要在pom.xml里加依赖然后写几行配置就行。另外一个好处是它非常适合“前后端分离 单体部署”的毕设形态。我不需要搭微服务不需要注册中心一个Spring Boot应用就能承载所有后端接口前端用Vue或原生页面都能对接。打包成jar包后在一台机器上就能跑起来演示环境非常好搭。我建议用Spring Boot 2.7版本而不是最新的3.x。原因是我实测下来3.x对JDK版本、部分第三方库的兼容性要求更严格很多网上的教程和现成依赖都是基于2.x。毕设求稳用2.7加JDK 8或11最不容易踩版本坑。2.2 配套技术栈的选型和理由后端除了Spring Boot我选了MyBatis-Plus作为持久层框架。选它不是因为流行而是它把单表的增删改查简化到了极致比如根据ID查询、分页查询、条件构造器都不用写XML这能帮我把大量时间节省到业务逻辑上。同时它对复杂SQL又保留了手写的空间不会像JPA那样在复杂查询时让人头疼。数据库用MySQL 5.7或8.0都可以这个项目表结构不复杂InnoDB引擎就够。权限认证我用的是Sa-Token比Spring Security轻量很多。Spring Security学习成本高配置一堆SecurityFilterChain对毕设来说太重Sa-Token只需要登录后给前端发一个token然后通过拦截器校验就行了半天就能搞定。前端方面如果图省事直接用Thymeleaf模板加Bootstrap服务端渲染不用跨域部署最简单。不过我这次为了页面好看用了Vue 3加Element Plus通过axios调后端接口。这个组合不冲突Spring Boot只负责JSON接口前端页面单独打包放到静态目录下照样一个jar包搞定。2.3 项目结构规划项目结构我按照常见的分层思路来核心是清晰。包名用com.jinyu.gas下面分成controller、service、mapper、entity、dto、common。common里放统一返回结果类、异常处理器、常量定义。controller只做参数接收和响应业务逻辑全部放在service层mapper只访问数据库。这样做最大的好处是代码好讲。答辩的时候老师随便点一个方法你都能说清楚“这里Controller只做了参数校验具体的订单状态计算在Service里”。而且后期维护也方便比如要加一个“订单作废”操作不需要动Controller直接在Service里加方法就行。3. 核心设计与数据库建模3.1 实体关系梳理数据库是整个项目的灵魂我反反复复改了三版才定下来。核心实体包括用户、角色、客户、气瓶、订单、订单明细、配送单、配送单明细、签收记录、车辆、司机。关键关系是这样的一个客户可以有多张订单一张订单包含多个气瓶明细调度员把多张订单合并到一张配送单一辆车对应一张配送单一个司机对应一张配送单司机完成配送后针对配送单生成签收记录同时更新气瓶状态。这里最容易出错的是订单和配送单的关系。一开始我设计成“订单表里加一个配送单ID”后来发现一个订单里的气瓶可能被拆到两辆车上比如客户一次下单10瓶氧气仓库只有6瓶先送6瓶剩下4瓶再派另一辆车。所以我把订单和配送单设计成多对多通过配送单明细表关联这样拆单、合单都能支持。这个细节非常加分答辩时可以重点讲。3.2 数据库表结构详解我整理一下核心表的设计思路。每张表都加了create_time、update_time这两个公共字段用MyBatis-Plus的自动填充功能维护。订单主表order_main主要字段包括order_no单号、customer_id、total_amount、order_status、remark。气瓶明细表order_item包含order_id、gas_bottle_id、gas_type、quantity、unit_price。注意气瓶明细表和订单强关联一条订单记录对应对条明细。配送单表delivery_main包含delivery_no、vehicle_id、driver_id、delivery_status、start_time、end_time。配送单明细表delivery_item包含delivery_id、order_id、order_item_id、actual_quantity。这张表是拆单合单的关键它不直接存气瓶而是存“订单里的明细项”。气瓶表gas_bottle包含bottle_no、gas_type、capacity、bottle_status在库/在途/已回收、last_fill_time。签收表receipt包含delivery_id、customer_sign_person、sign_time、sign_remark、return_bottle_count。字段命名统一用下划线避免使用关键字作为表名。比如不要用order作为表名我改成order_main省得SQL里到处加反引号。3.3 状态流转设计状态流转是最能体现系统设计能力的部分。订单状态我定义成待审核(0)、已审核待调度(1)、已调度(2)、配送中(3)、已完成(4)、已取消(5)。配送单状态定义成待派车(0)、待出发(1)、配送中(2)、已完成(3)。这两个状态必须联动。订单从“已审核待调度”变成“已调度”是在配送单生成的时候批量更新配送单变成“配送中”时订单状态同步变成“配送中”配送单完成后订单状态变成“已完成”。我建议不要散落地在业务代码里写状态值而是定义常量类或者枚举避免魔法数字。这里有一个很实用的技巧用状态机思路来约束操作。比如“待审核”的订单不允许直接取消可以允许但“配送中”的订单不允许取消。我在Service层统一做一个checkOrderStatusChange方法每次变更前先校验校验不通过直接抛业务异常。实测下来这个校验能挡掉80%的脏数据问题。4. 关键模块实操从0到1实现核心流程4.1 搭建Spring Boot工程我用Spring Initializr或直接用IDEA创建项目选择Spring Boot 2.7、Java 8依赖勾选Web、MySQL Driver、Lombok。创建完以后pom.xml里再加MyBatis-Plus和Sa-Token的依赖。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.34.0/version /dependency配置文件application.yml我习惯这样写。注意数据库密码不要写在代码里可以用环境变量占位。本地调试时再回填。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/jinyu_gas?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ${DB_PASSWORD:123456} mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0启动类加上MapperScan注解指向mapper包。然后写一个简单的健康检查接口确认项目能跑通再继续后续开发。很多人一上来就写几十个接口结果启动都报错不如先把空工程跑起来。4.2 订单模块实现Controller/Service/Mapper三层订单模块是整个系统的入口我拆成三层来实现。先看实体类用MyBatis-Plus的注解标识主键为自增ID。Data TableName(order_main) public class OrderMain { TableId(type IdType.AUTO) private Long id; private String orderNo; private Long customerId; private BigDecimal totalAmount; private Integer orderStatus; private String remark; private LocalDateTime createTime; private LocalDateTime updateTime; }Mapper接口就是一个简单的继承不用写任何SQL。public interface OrderMainMapper extends BaseMapperOrderMain { }Service层写具体的业务逻辑。创建订单时先生成订单号再插入主表然后循环插入订单明细。整个过程加Transactional因为主表和明细表必须同时成功否则就会有一条孤立的订单没有明细。Service public class OrderServiceImpl extends ServiceImplOrderMainMapper, OrderMain implements OrderService { Override Transactional(rollbackFor Exception.class) public void createOrder(OrderCreateDTO dto) { OrderMain order new OrderMain(); order.setOrderNo(generateOrderNo()); order.setCustomerId(dto.getCustomerId()); order.setOrderStatus(OrderStatusEnum.PENDING_AUDIT.getCode()); this.save(order); ListOrderItem items dto.getItems().stream() .map(item - { OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setGasBottleId(item.getGasBottleId()); oi.setGasType(item.getGasType()); oi.setQuantity(item.getQuantity()); return oi; }).collect(Collectors.toList()); orderItemService.saveBatch(items); } }Controller层只接收DTO返回统一结果对象。我用Result类包装前端看到code为200就是成功否则把message弹出来。RestController RequestMapping(/api/order) public class OrderController { PostMapping(/create) public ResultLong create(RequestBody OrderCreateDTO dto) { orderService.createOrder(dto); return Result.success(); } }这里有个经验更新操作不要直接把实体类丢给前端一定要用DTO。因为实体类里可能有数据库自增字段、逻辑删除字段直接拿DTO去接收JSON会造成字段混淆也容易有安全隐患。4.3 调度派车的核心逻辑调度是系统里最有“算法感”的部分虽然我没做复杂算法但合并订单的逻辑足以讲出水平。我先查询所有状态为“已审核待调度”的订单再按客户所在区域分组然后把同一个区域的订单组合成一张配送单指定一辆车和一个司机。这个逻辑用代码写出来就是public void dispatch(DispatchDTO dto) { // 1. 生成配送单主记录 DeliveryMain delivery new DeliveryMain(); delivery.setDeliveryNo(generateDeliveryNo()); delivery.setVehicleId(dto.getVehicleId()); delivery.setDriverId(dto.getDriverId()); delivery.setDeliveryStatus(DeliveryStatusEnum.PENDING_DEPARTURE.getCode()); deliveryService.save(delivery); // 2. 建立配送单与订单明细的关联 ListDeliveryItem items new ArrayList(); for (Long orderId : dto.getOrderIds()) { OrderMain order orderService.getById(orderId); if (order null || order.getOrderStatus() ! OrderStatusEnum.AUDITED.getCode()) { throw new BizException(订单不存在或不在待调度状态); } DeliveryItem item new DeliveryItem(); item.setDeliveryId(delivery.getId()); item.setOrderId(orderId); items.add(item); } deliveryItemService.saveBatch(items); // 3. 联动更新订单状态 orderMapper.updateOrderStatusByIds(dto.getOrderIds(), OrderStatusEnum.DISPATCHED.getCode()); }我强烈建议在调度前做数据校验不要光写查询。这里至少校验两件事车辆是否空闲订单是否存在且状态正确。车辆空闲状态可以简单用“车辆状态字段”判断更严谨的做法是查一下该车辆是否还有状态为“配送中”的配送单。这个校验就能避免一辆车同时被派两单。状态联动更新我用了一个自定义SQL写在XML里update idupdateOrderStatusByIds update order_main set order_status #{targetStatus} where id in foreach collectionids itemid open( separator, close) #{id} /foreach and order_status #{sourceStatus} /update后面加一个and order_status #{sourceStatus}是防止并发情况下把一个已经配送中的订单再次更新成待出发。虽然毕设项目并发量不大但你写出这个细节老师会觉得你有工程意识。4.4 签收回单与气瓶状态更新司机端流程是登录后查看配送单列表点击“送达”跳转到签收页面。签收页面里能看到这辆车上所有气瓶的编号司机勾选实际签收数量、填写回瓶数量、上传签收人姓名。提交后后端做三件事更新配送单状态为已完成、更新关联订单状态为已完成、更新气瓶状态。更新气瓶状态时需要小心。一个气瓶签收后如果客户没有回瓶那气瓶状态应该变成“在客户处”等下次回收如果有回瓶那状态变成“已回收”。我设计了一个简单策略前端提交回瓶数量后端把回瓶的气瓶状态改成已回收未回瓶的改成在客户处。不要试图用复杂逻辑自动判断毕设项目里手动维护一个状态字段完全够用。签收接口我用事务包裹因为涉及多张表的更新。核心代码示意如下Transactional(rollbackFor Exception.class) public void signReceipt(ReceiptDTO dto) { DeliveryMain delivery deliveryService.getById(dto.getDeliveryId()); if (delivery null || delivery.getDeliveryStatus() ! DeliveryStatusEnum.DELIVERING.getCode()) { throw new BizException(配送单不存在或不在配送中状态); } // 1. 保存签收记录 receiptMapper.insert(dto.toEntity()); // 2. 更新配送单状态 delivery.setDeliveryStatus(DeliveryStatusEnum.COMPLETED.getCode()); delivery.setEndTime(LocalDateTime.now()); deliveryService.updateById(delivery); // 3. 更新订单状态 orderMapper.updateStatusByDeliveryId(dto.getDeliveryId(), OrderStatusEnum.COMPLETED.getCode()); // 4. 更新气瓶状态 gasBottleService.updateStatusByDelivery(dto.getDeliveryId(), dto.getReturnBottleIds()); }整套流程跑完后数据库里的数据就能串起来了客户下的订单变成配送单司机签收气瓶状态变化所有表都有据可查。做到这一步主业务闭环就完整了。5. 部署、测试与踩坑记录5.1 本地运行、前后端联调与打包我建议开发时用前端代理解决跨域问题后端不单独配CORS。Vue项目在vite.config.js里配置proxy把/api开头的请求转发到localhost:8080。这样后端只负责处理业务不用每天跟跨域纠缠。export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })后端打包前先跑一遍单元测试或者用Postman把核心接口过一遍。打包命令很简单mvn clean package -DskipTests打完以后target目录下会生成一个jar文件。如果你用的是Vue前端先把前端代码build一遍把dist目录下的静态文件复制到后端src/main/resources/static目录下再一起打包这样整个系统只有一个jar包。我遇到过一个坑前端build后的静态资源路径是绝对路径/dist后端访问时总是404。后来把vue.config.js的publicPath改成相对路径./问题就解决了。如果你用Vite就在vite.config.js里配base: ./。这个问题很常见但不查半天真的不会想到。5.2 常见问题排查表我把项目过程中踩过的坑整理成一张表希望对你有用。问题现象可能原因解决办法启动报错Failed to configure a DataSource没有配置数据库连接或者依赖里多了数据源自动配置检查application.yml里的url、username、password或者排除DataSourceAutoConfiguration插入数据时id为空实体类主键没有加TableId注解或设置成IdType.INPUT加TableId(type IdType.AUTO)保证数据库主键自增前端请求接口报403Sa-Token默认拦截了所有请求没有配置放行在Sa-Token配置里把/login接口和静态资源加入排除列表查询结果字段全是nullMyBatis-Plus驼峰映射没开启或者实体类字段与表字段不一致配置map-underscore-to-camel-case: true或者用TableField指定字段名打包后前端页面白屏前端静态资源路径不对把publicPath或base设置为./重新build再复制到static目录时间字段差了8小时数据库连接串没有设置serverTimezoneurl末尾加serverTimezoneAsia/Shanghai最后一条时间字段的问题很多人都会遇到实际上只要在连接串上加了时区基本能一次解决。另外实体类的时间字段建议用LocalDateTime不要用java.util.Date否则JSON序列化出来的格式比较难看还得额外写格式化配置。5.3 答辩准备如何把项目讲出亮点代码写完只是第一步答辩才是把这套系统的价值真正展现出来的地方。我的经验是围绕“业务闭环”而不是“技术功能”来组织讲解逻辑。开门见山告诉老师这是一个面向城市气体配送的业务管理系统核心解决的是订单、调度、签收的协同问题。这句话就比“我做了一个增删改查项目”要有吸引力得多。准备两张图一张就是业务流程图另一张是数据库关系图。不用花哨但要能讲清每个表之间为什么这样关联尤其重点讲订单和配送单的多对多拆分逻辑。老师大概率会问为什么配送单明细表里存的是order_item_id而不是gas_bottle_id你要回答因为一个订单明细可能拆到多个配送单存明细ID才能追溯清楚每个订单的履约情况。技术部分准备好三个“为什么”为什么用Spring Boot为什么用MyBatis-Plus为什么状态要联动更新。每个问题都能答出上面的理由答辩基本就稳了。还有一个加分项主动说“我目前状态字段用的是常量类后续可以改成枚举或状态机模式来进一步规范操作”这句话比你自己吹十句都管用因为它显示了你有扩展思维。6. 这套系统后续还能往哪儿扩展这个项目做完以后我其实又想了几个方向。第一个是给系统加一个“气瓶库存预警”。现在气瓶状态是分开维护的完全可以在每次签收后统计一下“在库”的气瓶数量低于安全库存就在首页做一个预警提示。这个功能不复杂但一加进去系统就从“记录工具”变成了“管理工具”档次完全不一样。第二个是订单自动拆单。现在我是手动选择哪些订单放进同一张配送单后续可以做一个简单的启发式算法根据订单地址的经纬度距离和车辆载重自动推荐配送单。不需要做太复杂能按区域分组、按载重上限拆分就够了加上这个就很好讲了。第三个是数据统计可视化。用ECharts做一个驾驶舱页面把本周订单量、配送完成率、每个气瓶类型的出库量统计出来。这个对毕设展示特别加分因为页面一出来就很直观答辩时老师不用听你讲代码一眼就能看出系统价值。扩展方向不要贪多加一两个自己能掌控的就行。关键是每个扩展点都要围绕现有系统来展开不能凭空说一些“接入大数据”、“上微服务”之类的大话。踏踏实实把一个业务闭环做好再往前想半步就已经超过大多数学生项目了。我个人做完这个项目的体会是Spring Boot本身只是一个工具真正值钱的是你对自己要做的业务理解到不到位。你先把气体配送这件事怎么运转想清楚了代码只是用手敲出来的结果而已。如果读者里有人正在做类似的毕设题目希望这篇内容能帮你在动手前把系统边界、核心流程和关键表结构想明白别一上来就闷头写代码。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑