简介:围绕微信小程序与校园快递代取场景展开的毕业设计论文,面向高校计算机相关专业学生及需要完成课程设计、毕设选题的开发者。文档以校园快递代取系统为研究对象,梳理了从需求分析到功能落地的完整思路,涉及快递订单处理、接单信息更新、送达确认、代取评价与留言反馈等模块,并采用 Java、Spring Boot、MySQL 与 Tomcat 完成后端架构设计。压缩包仅含 1 个 docx 文件,大小约 6.51MB,内容为论文正文,包含中英文摘要、关键词、目录、绪论、研究意义、系统设计目的与思想等章节,便于直接参考论文结构、技术选型与数据库设计。已有 273 人学习下载,适合作为毕设写作、开题报告和系统方案设计的参考材料,也可用于了解微信小程序与 Spring Boot 分层开发的结合方式。
1. 从驿站门口那条长队说起,这套小程序后端要解决什么
双十一之后的校园菜鸟驿站,取件队伍能从门口一直排到马路牙子。有人愿意花两块钱拜托顺路的同学帮忙带一件回宿舍,也有人乐意顺手赚这个跑腿钱——校园快递代取就是这么一个典型的双边需求:一端是发单的学生,一端是接单的配送员,中间还需要有人管账号、管状态、管纠纷、管评价。这套系统把发单、接单、送达、代取评价、留言反馈整条链路塞进微信小程序里,用户不用装 App,扫一下就能用。
后端这边用的是 Java 加 Spring Boot,按 Controller / Service / DAO 三层切开,数据落在 MySQL,跑在 Tomcat 上。三层结构看着像论文里的套话,但真正写起来会发现它对应三个完全不同的问题:Controller 负责把小程序传上来的参数洗干净,Service 负责状态流转和并发控制,DAO 负责把订单行锁住。角色也分成三拨:普通用户发单和评价,配送员接单和确认送达,管理员管账号、公告和留言。适合读这篇的人有两类:正在做「小程序前端 + Java 后端」方向毕设的同学,以及想拿一个真实双边交易场景练接口契约、状态机和并发更新的人。
2. 小程序端与 Spring Boot 的接口契约:分层、登录态与统一响应
小程序和 Java 后端能不能顺利联调,八成取决于接口契约有没有在动手前定死。很多同学的做法是前端写到哪、后端加到哪,最后接口路径、字段名、返回结构全对不上,真机调试一跑就一堆 undefined。
2.1 Controller / Service / DAO 三层各自该切在哪
论文里写了三层,但落到代码,边界其实很具体。Controller 只做三件事:接参、校验、调用 Service 后包装返回;Service 里放业务规则,比如「只有状态为待接单的订单才能被抢」「配送员不能接自己发的单」「送达后才能评价」;DAO 只负责和 MySQL 说话,不带任何 if-else 业务判断。
@RestController @RequestMapping("/api/order") public class KuaidiOrderController { @Autowired private KuaidiOrderService orderService; // 配送员抢单:路径里带订单 id,账号从 token 里取,不信任前端传的账号 @PostMapping("/take/{id}") public R<Void> take(@PathVariable Long id, HttpServletRequest request) { String account = (String) request.getAttribute("account"); // 由拦截器解析 token 后塞入 orderService.takeOrder(id, account); return R.ok(); } // 用户发布快递订单 @PostMapping("/publish") public R<Long> publish(@RequestBody @Valid OrderPublishDTO dto, HttpServletRequest request) { String account = (String) request.getAttribute("account"); return R.ok(orderService.publish(dto, account)); } }这里的@Valid配合 DTO 上的@NotBlank、@Min做基础校验,比在方法体里写一堆 if 干净得多。account从拦截器塞进 request 属性,是因为小程序端传上来的任何账号字段都是可以被改的,后端必须以 token 里的身份为准。
2.2 微信登录态:别再用账号密码硬扛
论文里的登录模块是账号密码方案,用户表和配送员表各存一份mima。这在毕设答辩时够用,但小程序端体验很别扭:每次打开都要输一遍。更常见的做法是走wx.login拿 code,后端换 openid,首次登录时再引导绑定角色。
// 小程序端:静默登录,拿到 code 后立刻换 token wx.login({ success: (res) => { if (!res.code) return; wx.request({ url: BASE_URL + '/api/auth/login', method: 'POST', data: { code: res.code }, success: (r) => { // 后端返回 { token, role, needBind } wx.setStorageSync('token', r.data.data.token); if (r.data.data.needBind) { wx.redirectTo({ url: '/pages/bind/bind' }); // 未绑定角色,先去选 用户 / 配送员 } } }); } });后端拿到 code 后调用微信的会话接口换 openid,再用 openid 查用户表;查不到就落一条新记录并把needBind置为 true。token 用 JWT 签一个有效期两小时的短凭据,配合一个长期 refresh token 存库,避免用户每次打开小程序都要重新授权。
提示:小程序请求的合法域名必须是 HTTPS,且要在小程序后台配置。开发阶段可以在开发者工具里勾选「不校验合法域名」,但上线前一定要补上,否则真机一跑就是
request:fail url not in domain list。
2.3 统一返回体和全局异常,能省掉一半联调时间
前后端最容易吵架的地方就是返回结构不统一:有的接口返回数组,有的返回对象,报错时直接抛 500 带一页堆栈。给它定一个壳,所有接口都套上:
public class R<T> { private int code; // 0 成功,非 0 为业务错误码 private String msg; // 给用户看的提示,不要把 SQL 异常暴露出去 private T data; public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.code = 0; r.msg = "ok"; r.data = data; return r; } }再配一个@RestControllerAdvice把BizException、参数校验异常、兜底Exception分开处理,业务异常返回具体 code,系统异常统一返回 500 且日志里记录 traceId。小程序端只要判断code !== 0就弹 toast,不用每个页面单独写错误分支。
2.4 接口清单和角色边界
联调前把表列出来,比在群里喊「那个接口叫啥来着」高效得多:
| 路径 | 方法 | 可访问角色 | 说明 |
|---|---|---|---|
| /api/auth/login | POST | 全部 | code 换 token |
| /api/order/publish | POST | 用户 | 发布快递订单 |
| /api/order/list | GET | 全部 | 按状态分页查订单 |
| /api/order/take/{id} | POST | 配送员 | 抢单 |
| /api/deliver/finish | POST | 配送员 | 确认送达,生成送达订单 |
| /api/comment/add | POST | 用户 | 代取评价 |
| /api/feedback/add | POST | 全部 | 留言反馈 |
角色边界靠拦截器 + 自定义注解实现,比如@RequireRole("delivery"),在 HandlerInterceptor 里比对 token 中解析出的角色。这一步做完,后面接单接口被普通用户刷的漏洞就堵上了。
3. 订单数据库设计:从 E-R 图到可执行的建表语句
论文里的 E-R 图画得挺全,配送员、用户、快递订单、送达订单四个实体加一堆属性,但落到建表语句时会发现两个坑:一是字段名全用拼音,二是金额字段用了 double。前者无所谓,后者得改。
3.1 订单状态机先定,表结构跟着定
先把状态流转想清楚,再去写字段。校园代取这条链路的正常路径是:用户发布(待接单)→ 配送员抢单(已接单)→ 配送员送到(已送达)→ 用户评价(已评价)。旁路有两个:用户主动取消、超时未接单自动关闭。
| 状态值 | 含义 | 可执行动作 | 触发者 |
|---|---|---|---|
| 0 | 待接单 | 抢单、取消 | 配送员 / 用户 |
| 1 | 已接单 | 确认送达、放弃接单 | 配送员 |
| 2 | 已送达 | 评价 | 用户 |
| 3 | 已评价 | 无 | - |
| -1 | 已取消 | 无 | 用户 / 系统 |
状态值用 tinyint 存,别用字符串,否则索引和比较都吃亏。状态迁移的合法性判断写在 Service 层,比如takeOrder里必须是0 → 1,其余一律抛业务异常。
3.2 核心表结构与索引取舍
论文里快递订单表的字段是这些:kuaididanhao(快递单号)、kuaidimingcheng、jietu(截图)、kuaidileixing、kuaidibeizhu、daiqufeiyong、zhanghao(发单人账号)、shouji、quhuodizhi、mudedizhi、peisongzhanghao、lianxidianhua、songdashijian、peisongren、zhuangtai。按这个来,只做两处调整:
daiqufeiyong从 double 改成decimal(10,2)。double 做金额累加会出现 0.1 + 0.2 = 0.30000000000000004 这类问题,结算时对不上账。jietu存的是图片,用longtext存 base64 会让单行体积暴涨,查询拖慢。常见做法是存对象存储返回的 URL,长度给 varchar(500) 就够。
CREATE TABLE `kuaidi_order` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `kuaididanhao` varchar(200) DEFAULT NULL COMMENT '快递单号', `kuaidimingcheng` varchar(200) DEFAULT NULL COMMENT '快递名称', `jietu` varchar(500) DEFAULT NULL COMMENT '截图地址', `kuaidileixing` varchar(200) DEFAULT NULL COMMENT '快递类型', `kuaidibeizhu` varchar(200) DEFAULT NULL COMMENT '快递备注', `daiqufeiyong` decimal(10,2) DEFAULT '0.00' COMMENT '代取费用', `zhanghao` varchar(200) NOT NULL COMMENT '发单人账号', `shouji` varchar(200) DEFAULT NULL COMMENT '手机', `quhuodizhi` varchar(200) DEFAULT NULL COMMENT '取货地址', `mudedizhi` varchar(200) DEFAULT NULL COMMENT '目的地址', `peisongzhanghao` varchar(200) DEFAULT NULL COMMENT '配送账号', `peisongren` varchar(200) DEFAULT NULL COMMENT '配送人', `lianxidianhua` varchar(200) DEFAULT NULL COMMENT '联系电话', `songdashijian` datetime DEFAULT NULL COMMENT '送达时间', `zhuangtai` tinyint NOT NULL DEFAULT '0' COMMENT '0待接单 1已接单 2已送达 3已评价 -1已取消', `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_status_time` (`zhuangtai`, `addtime`), KEY `idx_sender` (`zhanghao`), KEY `idx_delivery` (`peisongzhanghao`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='快递订单';idx_status_time是给「待接单列表按时间倒序分页」这个最高频查询准备的。没有它,订单量上千之后列表页就要走全表扫描。idx_delivery给配送员的「我的接单」页用,idx_sender给用户自己的发单历史用。
3.3 抢单那一下,别用先查后改
并发抢单是这个系统里唯一真正有并发压力的地方。两个人同时点「接单」,如果 Service 里写成先select查状态,判断是 0,再update成 1,中间那几毫秒足够第二个人也查到 0,结果两个人抢到同一单。
正确写法是把判断塞进 where 条件,靠数据库的行锁保证原子性:
UPDATE kuaidi_order SET zhuangtai = 1, peisongzhanghao = #{account}, peisongren = #{name}, lianxidianhua = #{phone} WHERE id = #{id} AND zhuangtai = 0;Service 里判断返回的影响行数:
int affected = orderMapper.takeOrder(id, account, name, phone); if (affected == 0) { // 要么订单不存在,要么已经被别人抢了 throw new BizException(1001, "这一单已经被接走了"); }这个套路叫乐观更新或者条件更新,不需要显式加锁,也不用引入分布式锁。单机 MySQL 上,InnoDB 的行锁能保证同一行的 update 串行执行;抢失败的那一方拿到 0 行,直接提示用户即可。如果以后要扩展到多实例,这条语句照样成立,因为约束在数据库这一层。
注意:确认送达、评价这两步同理,都要带上
AND zhuangtai = ?做状态守卫,否则重复请求会把状态来回改,代取评价也能被刷好几条。
4. 抢单、送达、评价:三个核心接口的实现与真机联调排错
数据库和契约都定了,接下来是业务代码。这三个接口是整套系统里最容易出问题的地方,也最值得写细。
4.1 抢单接口:状态守卫加配送员校验
抢单除了并发,还要挡两种脏请求:自己抢自己发的单、配送员账号被封禁还来接单。
@Transactional(rollbackFor = Exception.class) public void takeOrder(Long orderId, String account) { KuaidiOrder order = orderMapper.selectById(orderId); if (order == null) { throw new BizException(1002, "订单不存在"); } if (account.equals(order.getZhanghao())) { throw new BizException(1003, "不能接自己发布的订单"); } Delivery delivery = deliveryMapper.selectByAccount(account); if (delivery == null || delivery.getStatus() == 0) { throw new BizException(1004, "账号状态异常,无法接单"); } int affected = orderMapper.takeOrder(orderId, account, delivery.getName(), delivery.getPhone()); if (affected == 0) { throw new BizException(1001, "这一单已经被接走了"); } }@Transactional加在这里其实不是必须的,因为只有一条 update,但保留它有个好处:将来如果有人在这个方法里再加一条「给发单人发订阅消息」的写库操作,事务边界已经是对的,不会出现订单改了、消息没发的情况。
4.2 送达确认与代取费用:把结算字段落到送达订单表
确认送达做两件事:更新快递订单状态为 2 并写入songdashijian,同时在送达订单表插一条记录。送达订单表字段和快递订单高度重合,多的是songdashijiandatetime和配送人信息,这是论文里的设计,好处是配送员的历史收入可以只查这一张表,不用扫全量订单。
@Transactional(rollbackFor = Exception.class) public void finish(Long orderId, String account) { KuaidiOrder order = orderMapper.selectById(orderId); if (order == null || !account.equals(order.getPeisongzhanghao())) { throw new BizException(1005, "无权操作该订单"); } int affected = orderMapper.finish(orderId); // WHERE id = ? AND zhuangtai = 1 if (affected == 0) { throw new BizException(1006, "订单状态已变化,请刷新后重试"); } DeliverOrder d = new DeliverOrder(); d.setKuaididanhao(order.getKuaididanhao()); d.setDaiqufeiyong(order.getDaiqufeiyong()); d.setPeisongzhanghao(account); d.setSongdashijian(LocalDateTime.now()); // 其余字段从 order 拷贝 deliverOrderMapper.insert(d); }金额字段用BigDecimal接,别用 double。取出来之后setScale(2, RoundingMode.HALF_UP)再入库,避免小数位溢出。
4.3 代取评价和留言反馈
评价表需要order_id唯一索引,一条订单只能评一次,这是数据库层面的兜底,比在 Service 里查快得多,也可靠得多:
CREATE TABLE `daiqu_comment` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL COMMENT '订单id', `zhanghao` varchar(200) DEFAULT NULL COMMENT '评价人账号', `peisongzhanghao` varchar(200) DEFAULT NULL COMMENT '被评价配送员', `pingfen` tinyint DEFAULT '5' COMMENT '评分 1-5', `content` varchar(500) DEFAULT NULL COMMENT '评价内容', `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order` (`order_id`), KEY `idx_delivery_acc` (`peisongzhanghao`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代取评价';uk_order建好之后,Service 里不用先查再插,直接 insert,捕获DuplicateKeyException转成「该订单已评价过」的业务异常。配送员的平均分可以定时任务算,也可以实时AVG(pingfen)查——订单量小的校园场景,实时查完全够用。
4.4 真机调试连不上后端,按这个顺序排
小程序开发里最耗时间的不是写代码,是联调。真机上请求发不出去,按下面顺序查基本能定位:
| 现象 | 大概率原因 | 处理 |
|---|---|---|
| 开发者工具正常,真机失败 | 合法域名未配置 | 后台配置 HTTPS 域名,或开发阶段开「不校验合法域名」 |
| 报 connection refused | BASE_URL 写了 localhost | 换成电脑局域网 IP,手机和电脑同一 WiFi |
| 415 / 400 | Content-Type 与后端不一致 | 小程序端显式设header: {'content-type':'application/json'} |
| token 失效但没跳登录 | 401 拦截没做 | 在 request 封装里统一处理 401,清 token 后跳登录页 |
| 图片上传失败 | 用了 request 而非 uploadFile | 文件走wx.uploadFile,后端接口用MultipartFile接 |
还有一个隐蔽的坑:小程序端setData更新的是视图层数据,不会同步回本地变量,如果没有把最新值重新赋值回去,页面看起来「刷新了」但提交的还是旧值。订单列表分页加载时尤其明显,每次追加数据都要用新数组去 setData。
5. 打包部署与接口回归:从 Tomcat 到能自己验一遍的脚本
代码写完,答辩前还得让系统真的跑起来。这一章讲怎么把它部署稳,以及怎么在没人帮你测的情况下自己验一遍。
5.1 打成 jar 还是 war,取决于你怎么用 Tomcat
论文里写服务器用 Tomcat,那就涉及一个选择。Spring Boot 内嵌了 Tomcat,打成可执行 jar 直接java -jar就能跑,部署最简单;如果学校机房给的是现成的 Tomcat 容器,那就得排除内嵌容器打成 war,把包丢进webapps。
<!-- 打 war 时要排除内嵌 Tomcat,否则和外部容器冲突 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency>主类要继承SpringBootServletInitializer并重写configure,否则 war 丢进 Tomcat 起不来,日志里只会看到 404,不报错,特别容易卡住。用 jar 的话,application.yml里把数据库连接、端口、文件上传路径都抽成环境变量,换机器不用改代码。
5.2 Nginx 转发和 HTTPS 是小程序的硬门槛
小程序线上环境必须走 HTTPS,所以部署里一定有一层反向代理。Nginx 配置大致是这样:
server { listen 443 ssl; server_name your.domain.com; ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 让后端能拿到真实 IP,日志排查用得上 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }proxy_pass末尾带不带斜杠差别很大:带斜杠会把/api/替换掉,不带则原样透传。配错了表现为接口 404 但后端日志里啥也没有,因为请求根本没匹配上 Controller。
5.3 用 curl 跑一轮回归,比手点快十倍
每次改完代码,手动点小程序验证一遍要十几分钟。写个脚本把关键路径跑一遍,几十秒就出结果:
#!/bin/bash BASE="https://your.domain.com/api" # 1. 登录取 token TOKEN=$(curl -s -X POST "$BASE/auth/login" \ -H 'Content-Type: application/json' \ -d '{"code":"test_code"}' | grep -o '"token":"[^"]*' | cut -d'"' -f4) # 2. 发布订单,拿到订单 id ORDER_ID=$(curl -s -X POST "$BASE/order/publish" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"kuaidimingcheng":"圆通","daiqufeiyong":2.00,"quhuodizhi":"东门驿站","mudedizhi":"5号楼"}' \ | grep -o '"data":[0-9]*' | cut -d':' -f2) # 3. 抢单:第一遍应该成功,第二遍应该返回 1001 curl -s -X POST "$BASE/order/take/$ORDER_ID" -H "Authorization: Bearer $TOKEN" echo "" curl -s -X POST "$BASE/order/take/$ORDER_ID" -H "Authorization: Bearer $TOKEN"关键看第三步:第一次返回code: 0,第二次返回「这一单已经被接走了」,就说明条件更新那行 SQL 真的生效了。这个用例在论文的测试章节里也能直接当测试记录用,比「点击按钮,功能正常」这种描述扎实得多。评价接口的幂等性同理,连续调两次评价,第二次必须报重复,否则唯一索引没建上。
最后再补一个细节:daiqufeiyong查出来要是2.00而不是2.0,说明字段类型改对了;如果出的是一长串小数,回去检查表结构是不是还留着 double。
本文还有配套的精品资源,点击获取