去年选毕设题目的时候我给自己挖了一个坑最后做出来的是“基于uniapp的快递e站协同外卖配送系统”。说白了就是把快递驿站代取代寄的需求塞进外卖骑手的配送流程里用户在小程序里下单一单“代取快递”骑手在送外卖的路上顺路去驿站扫码取件最后送到用户手中。前端用uniapp一套代码跑微信小程序和H5后端自己用Spring Boot写接口MySQL存数据整条链路完全跑通。这篇文章不写官方项目申报书那种正确的废话只讲我在做这个计算机毕设过程中的真实选题逻辑、技术选型、数据库怎么拆、接口怎么设计、以及踩过的坑。如果你最近也在挑毕设题目或者已经选了类似的东西却不知道从哪下手这篇内容应该能给你省下很多瞎琢磨的时间。1. 项目选题与整体定位1.1 这个题目到底在解决什么问题我一开始的直觉是快递驿站和外卖配送是两个不同场景硬凑在一起会不会很别扭。但去我们学校快递驿站门口蹲了几天之后我发现这个问题是真实存在的中午取件的人排长队有些人只是取一个小快递却要等十几分钟。另一边外卖骑手送完午高峰之后会有零散的空闲时间或者有些订单配送方向刚好经过驿站。把这些“代取快递”的需求发到骑手端让骑手在配送路径上“顺便接一单”对用户省时间对骑手增加收入对驿站减轻压力这就是系统核心价值。在毕设题目里这类系统通常叫“协同配送”或“众包配送”。和纯外卖平台相比这个题目的新意在于订单不只是“商家→用户”而是“驿站→用户”加“外卖点→用户”混合流转。所以后端要处理的不是一套简单下单流程而是快递单状态和配送订单状态的联动这正好是计算机专业学生展示系统设计能力的地方。1.2 题目的复杂度和边界控制选毕设题目有个优先级可完成 能演示 有深度 显得高级。这个项目的规模属于“一个人在三到四周内能做完、但是又不能纯靠复制粘贴糊弄过去”的程度。前端需要用户端、骑手端、驿站端、管理端四个角色入口后端需要处理订单状态机、登录鉴权、位置计算、扫码出库数据库至少要六张表以上。这个工作量和本科毕设的要求匹配度是合适的。为了把范围讲清楚我当时在开题报告里做了一张简单的功能矩阵表后续编码和答辩也基本照着这个表走端核心功能说明用户端注册登录、发布代取代送订单、在线支付或到付选择、订单跟踪、取消订单小程序为主H5也能跑骑手端抢单大厅、查看顺路度、接单、取件、送达确认、收入记录核心是地图和状态流转驿站端快递入库、扫码出库、快递单绑定、异常记录偏轻量后台不需要花哨界面管理端用户管理、骑手审核、订单统计、配送数据趋势用表格和图表展示把四个角色一列出来整个项目立刻有说服力了而且每个端都能拿出来单独演示答辩现场不容易冷场。1.3 为什么选uniapp而不是原生开发或纯Web选uniapp最大的理由是跨端复用。我只需要写一套Vue代码就能在微信小程序、H5和安卓App上运行。做一个毕设项目如果给用户端做小程序、给骑手端做App、给管理端做Web那工作量直接翻好几倍三个月都未必能打磨完。uniapp Vue3 uview-plus是我验证过比较顺手的组合uview-plus是uview的Vue3版本组件该有的都有表单、弹窗、导航、时间选择器基本不用自己写。后端我选了Spring Boot加MyBatis Plus。之前也纠结过要不要用uniapp官方的uniCloud云开发因为云数据库和云函数确实能省掉服务器。但考虑再三还是没用原因是云开发把很多逻辑黑盒化了答辩老师问“你的接口怎么做鉴权”“数据库表结构怎么设计的”你要么回答不上来要么只能秀一套IDE操作。自己写Spring Boot从Controller到Service到Mapper全在代码里每个环节都能讲出设计依据。如果你后端基础偏弱MyBatis Plus可以帮你少写大量重复SQL这是我给动手能力一般的同学的建议。2. 核心流程与数据库设计2.1 订单状态机让每一单都有明确去处这个系统里最容易被忽视、也最容易翻车的是订单状态管理。用户下单后订单可能被骑手抢、驿站确认、配送中、送达、取消、异常如果不在数据库层面和代码层面一起约束最终会出现“用户看订单已经取消了骑手还在送”这种极度影响演示效果的事故。我给订单定义了这样几个状态并且在数据库里用TINYINT存数字状态值状态名含义0CREATED刚创建等待骑手接单1ACCEPTED骑手已接单等待取件2PICKED_UP驿站已出库骑手正在配送3DELIVERING骑手已取到商品途中与2可合并4FINISHED用户已确认或系统自动确认完成5CANCELED用户取消/超时取消6ABNORMAL订单异常如包裹丢失具体落地时我建了一张order_log表专门记录订单的每一次状态变化包括从哪个状态变成哪个状态、是谁操作的、操作时间是什么。这张表看似不起眼但答辩时老师问“你的系统怎么追溯问题”我直接说状态日志然后打开页面展示给老师看效果远比干讲要好。2.2 核心数据表快递单和配送单怎么拆如果只把快递信息和订单信息塞进一张表后期绝对会乱。我分了5张核心表用户表、快递包裹表、驿站表、配送订单表、状态日志表。其中难点在于如何关联“订单”和“包裹”。一个外卖订单可以没有快递包裹直接就是商家送餐一个快递代取代寄订单用户可能同时有多个包裹要取。所以我没有把包裹信息直接写进订单表而是单独建一张parcel表再用delivery_order里的order_id和parcel_id建立关联。这样设计的好处是一个订单包含多个包裹时只需要在包裹表里标记order_id不会产生数据冗余。下面是我当时建delivery_order表的简化版本CREATE TABLE delivery_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号前台展示用, user_id bigint NOT NULL COMMENT 下单用户ID, rider_id bigint DEFAULT NULL COMMENT 接单骑手ID, order_type tinyint DEFAULT 0 COMMENT 0-快递代取, 1-外卖配送, status tinyint DEFAULT 0 COMMENT 状态: 0待接单,1待取件,2配送中,3已完成,4取消, pickup_point varchar(255) DEFAULT NULL COMMENT 取件点文案驿站地址或商家地址, pickup_lat decimal(10,6) DEFAULT NULL, pickup_lng decimal(10,6) DEFAULT NULL, delivery_address varchar(255) DEFAULT NULL COMMENT 送达地址文案, delivery_lat decimal(10,6) DEFAULT NULL, delivery_lng decimal(10,6) DEFAULT NULL, fee decimal(10,2) DEFAULT 0.00 COMMENT 配送费, expect_time datetime DEFAULT NULL COMMENT 期望送达时间, remark varchar(500) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;包裹表CREATE TABLE parcel ( id bigint NOT NULL AUTO_INCREMENT, tracking_no varchar(64) NOT NULL COMMENT 快递单号, station_id bigint NOT NULL COMMENT 所属驿站ID, user_id bigint DEFAULT NULL COMMENT 绑定用户ID, order_id bigint DEFAULT NULL COMMENT 关联的配送订单ID为空表示未下单, status tinyint DEFAULT 0 COMMENT 0-已入库,1-待取件,2-已出库,3-异常, pickup_code varchar(20) DEFAULT NULL COMMENT 取件码, arrive_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;我在订单表里冗余了取件点的经纬度和地址文案而不是通过驿站表去join。原因是骑手端列表要频繁展示“从哪取、送到哪”如果每次都实时join驿站表接口响应会变慢代码也更啰嗦。对毕设项目来说一点冗余能换来简单直接的开发体验是值得的。2.3 协同派单逻辑不装高深但要能自圆其说协同配送的“协同”两字是题目亮点也是最容易被答辩老师追问的地方。我一开始想用最短路径算法去算骑手配送路线后来发现数据源根本搞不定没有完整路网、没有实时路况、没有拥堵指数硬算出来的路径也只是“看起来高科技”的纸面方案。所以我最终用的是“顺路度评分 人工抢单”的组合。具体做法是骑手打开抢单大厅服务端根据骑手当前经纬度计算当前订单的取件点、送达点与骑手已有任务路径的直线距离差生成一个0到100分的顺路度分数默认按分数从高到低排序。骑手自己决定接不接后台不给强制派单。这样既体现了“协同”的思想又不至于把项目复杂度拖垮。如果老师接着问“你这个顺路度没有考虑实际道路会不会不准”你的回答思路是毕设阶段保留扩展点后续可以用地图SDK的路径规划接口替换直线距离已经预留了算法接口。这就够了没有人会要求一个本科毕设真的跑通全局最优派单。3. uniapp前端实现与关键模块3.1 HBuilderX建项目与uview-plus接入细节第一个坑就是UI库版本。网上搜到“uview”教程很多都是基于Vue2的而新创建的uniapp项目默认是Vue3直接装uview会出现组件无法渲染、控制台报错一堆的问题。正确做法是在HBuilderX的插件市场里搜“uview-plus”这名字看起来像山寨但它是正儿八经适配Vue3的UI库。项目创建我选的模板是“默认模板Vue3”然后在main.js里注册组件库import uviewPlus from uview-plus; import { createSSRApp } from vue; export function createApp() { const app createSSRApp(App); app.use(uviewPlus); return { app }; }同时还需要在uni.scss里引入主题变量并在pages.json里配置easycom规则。这里有个经验不要漏掉import uview-plus/index.scss否则按钮、输入框的颜色会很奇怪。配置好easycom之后页面里可以直接用u-button、u-empty这类组件不需要手动import能省掉很多重复代码。3.2 四个角色的页面规划和地图选点用户端页面我做得比较常规登录页、首页推荐快递单和附近驿站、创建订单页、订单详情页、个人中心页。真正容易出问题的是骑手端。骑手端需要一个“任务大厅页面”进入后要申请定位权限然后拉取附近待接单订单。这里地图组件用uniapp内置的map配腾讯地图或高德地图的key。manifest.json里需要填写对应平台的key否则真机上地图黑屏。创建订单时用户要从地图选点或者手动输入地址。我做了个“地图选点”页面把picker和map组件结合。以下是核心逻辑的简写template map idpickerMap :latitudelat :longitudelng taponTapMap / /template script setup const lat ref(39.9); const lng ref(116.4); function onTapMap(e) { lat.value e.detail.latitude; lng.value e.detail.longitude; // 调用腾讯地图位置解析接口得到文字地址 } /script注意uniapp的map组件点击事件返回的坐标是gcj02坐标系存储到后端时一定要标清楚坐标系。我之前没在意结果小程序模拟器正常安卓真机上用户选的点和实际送达地址偏移了几百米排查了半天才想起来是坐标系的锅。3.3 订单状态同步轮询比WebSocket划算用户端要实时看到订单状态从“待接单”变到“配送中”很多人第一反应上WebSocket我觉得对毕设来说没必要。WebSocket维护成本和心跳逻辑麻烦而且答辩时很难解释清楚服务器端连接管理。我用的是轮询10秒一次订单详情页onShow时启动定时器onHide时清除接口返回的状态变化足够满足演示效果。let timer null; function startPolling(orderId) { stopPolling(); timer setInterval(async () { const res await getOrderDetail(orderId); if (res.data.status 3) { // 已送达停止轮询 stopPolling(); uni.showToast({ title: 订单已送达 }); } }, 10000); } function stopPolling() { if (timer) clearInterval(timer); }这个方法虽然没什么技术含量但很稳定不会因为弱网情况下连接断开就丢状态。对真实业务量不大的校园场景来说10秒级延迟用户根本感知不到。这也是我后来跟同学强调的项目里所有技术选型都要考虑场景轮询不是丢人乱上WebSocket才是给自己找罪受。4. 后端接口设计与实现4.1 统一返回结构和登录鉴权早期写接口时我每个Controller返回类型都不一样有的返回Map有的直接返回实体前端拿到数据后处理逻辑非常混乱。后来我把所有接口改成统一返回ResultT{ code: 200, msg: success, data: {} }判断成功就看code是不是200前端封装一个request.js统一拦截非200直接弹出错误提示。登录鉴权用的是token方案用户登录成功后生成一个随机UUID把userId存到数据库token表之后所有请求都在header里带Authorization: token。后端加一个拦截器对/api路径下的接口做校验放行名单是登录、注册和地图配置接口。这个方案不用依赖Redis避免多一个中间件还要跟老师解释为什么用Redis。4.2 用户下单到骑手送达的完整调用链最能展示项目深度的是把这个流程串成一条逻辑闭环。第一步用户POST /api/order/create提交订单参数包括orderType、stationId、deliveryAddress、deliveryLat、deliveryLng、fee。后端先生成订单号比如用时间戳加随机数然后创建订单记录并把对应包裹表的order_id更新为该订单包裹状态设为“待取件”。这个动作必须放在同一个事务里否则会出现订单创建成功但包裹没绑定上。第二步骑手GET /api/rider/order/list拉取待接单列表。后端遍历待接单订单计算当前骑手位置到取件点的距离以及取件点距离终点的长度生成顺路度按倒序返回。这里不用太精确用球面距离公式足够。第三步骑手POST /api/order/accept接单。接口里不能只做update delivery_order set rider_id ? where id ?必须加一个状态条件防止两个骑手同时抢同一单UPDATE delivery_order SET rider_id #{riderId}, status 1 WHERE id #{orderId} AND status 0返回影响行数如果影响行数为0说明已经被别人抢了提示“手慢了”。这套乐观锁思路虽然简单但能完美回答并发场景下的数据一致性问题。第四步驿站工作人员扫包裹上的取件码调用POST /api/station/confirm后端先校验包裹的order_id是否等于当前订单校验通过后把包裹状态改为“已出库”订单状态改为“配送中”。第五步骑手到用户楼下点送达调用POST /api/order/finish后端修改订单状态为“已完成”同时写一条状态日志。到这里整条链路闭环。4.3 管理端统计和性能思考管理端我计划了四个统计维度每日订单量、骑手完成单量、各时段订单分布、驿站包裹库存情况。实现用SQL聚合就够了SELECT DATE(create_time) AS day, COUNT(*) AS order_count FROM delivery_order GROUP BY DATE(create_time) ORDER BY day DESC LIMIT 7如果数据量大了再加索引毕设阶段完全不需要考虑分库分表。但要注意的一点是轮询接口最好建一个联合索引(status, create_time)我实测没有索引的时候小程序端三个页面同时轮询本机MySQL CPU直接飙到30%加了索引之后降到3%左右。这算是优化接口性能最简单的实操也能在答辩时提一句显得你对索引有概念。5. 常见问题与排查技巧实录5.1 H5端跨域和本地联调后端默认跑在8080端口uniapp运行到浏览器访问的是HBuilderX内置服务器的端口两边端口不同必然跨域。我一开始在后端写了CrossOrigin注解但只能解决单个Controller后来直接加了一个全局配置类统一放行跨域请求。核心代码Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }加完之后H5端请求后端一切正常小程序端因为不存在跨域问题所以不用额外处理。有的同学喜欢在HBuilderX里配置proxy代理我也试过但只对web开发环境有效而且配置起来容易出错直接后端CORS是最快路径。5.2 小程序真机预览时的域名问题小程序上线或真机预览时request接口域名必须是HTTPS且在小程序后台配置合法域名。毕设阶段没有备案域名和证书是很正常的。我在开发调试时用两个办法过渡一个是在微信开发者工具右上角点击“详情”勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”这样本地开发完全不受影响另一个是把Spring Boot项目部署到云服务器用Nginx配HTTPS但这一套对时间紧张的同学来说成本略高。如果只为了校内演示勾选不校验合法域名就够了。5.3 地图定位漂移和坐标系不一致这个问题在安卓真机上特别明显。uniapp内置map组件使用的坐标和腾讯位置服务都是GCJ02火星坐标系而部分手机返回的定位结果是WGS84原始坐标两者相差数百米。我当时的排查思路是先在echarts里把问题订单取件点和实际驿站位置打出来发现取件点拖到马路对面了再看数据库存的经纬度是从地图选点拿到的还是从腾讯SDK逆地址解析拿到的最后统一在入库前做一次坐标转换。如果后端拿到的坐标来自第三方API也一定要确认对方返回的坐标系。5.4 答辩老师最可能追问的几个点第一个是“为什么不用WebSocket”回答重点是10秒轮询满足实时性要求实现简单可靠。第二个是“如果用户取消订单已经取件的骑手怎么处理”我的做法是取件前用户可无条件取消取件后不能直接取消只能联系驿站或客服变为异常订单骑手把包裹送回驿站。第三个是“你怎么保证数据安全”可以提密码加盐哈希、token鉴权、SQL预编译防注入。这些问题提前想好答辩基本不会冷场。6. 源码获取与二次开发建议6.1 拿到源码后怎么跑通很多同学拿到源码第一反应是先看页面其实正确顺序应该是先配环境再初始化数据库。本项目需要的环境非常常规JDK1.8及以上、Maven 3.6、MySQL 5.7或8.0、HBuilderX、微信开发者工具。运行步骤很简单创建一个空的数据库执行项目里提供的sql初始化脚本里面包含建表语句和初始数据。修改后端application.yml把数据库用户名密码改成自己的。启动Spring Boot后端控制台出现端口号就说明成功。用HBuilderX打开uniapp前端项目在utils/request.js里把baseURL改成http://localhost:8080/api。运行到微信开发者工具或浏览器用初始化脚本里的测试账号登录。有个常见问题数据库脚本里导入的中文乱码解决方法是连接数据库时指定characterEncodingutf8建表语句本身也统一用utf8mb4。这个搞不定的话后面所有页面都会看到乱码提示直接影响第一印象。6.2 从毕设升级到实习项目的几个方向如果做完这个毕设还有余力想往作品集方向打磨我建议优先做三件事。一是接入微信支付和uniPush推送把在线支付和订单状态通知做成真正的业务闭环二是增加多驿站分站支持让一个驿站的数据不由全局管理端直接改而是由对应驿站员工管理三是引入地图路径规划SDK把顺路度评分从“直线距离”升级为“实际骑行距离”。这些方向不用一次全做完选一个做深放到简历里都能碾压大多数只会复制管理系统项目的候选人。最后分享一个我做这个项目时最深刻的体会做这种多角色系统最大的坑不是技术难点而是你自己以为的“优化思路”在拖后腿。我一开始花了两天设计派单算法结果发现连测试数据都构造不好后来果断砍掉用抢单大厅加顺路度排序项目反而顺利走通。毕设的核心是先让它完整地跑起来再让它有点亮眼的设计最后才轮到“算法优化”这种锦上添花的事。如果你正在被某个功能卡住想一想能不能用更朴素的方法替代能演示、能讲清楚就已经赢了大多数同学。