简介:这是一份基于微信小程序的快递代取系统毕业设计源码,主要面向计算机相关专业学生和需要快速搭建小程序项目的开发者,适用于毕业设计、课程设计或真实业务演练。项目采用微信开发者工具与Java后端实现,功能覆盖用户信息管理、快递物流变化跟踪、代收地址填写修改、快递收入统计、取走包裹统计、代取费用结算六大模块,完整覆盖从下单到结算的代取流程。压缩包内共含2000个文件,既有前端页面资源(html、css、js、png、gif等),也有小程序配置(wxml、wxss、json),以及Java服务端代码与SQL数据库脚本(java、xml、jar、sql等),整体约14MB,结构清晰、分类明确,便于按模块检索学习。资源中涉及基于jQuery Mobile等移动端框架的界面,方便适配不同设备。目前已有174人学习,源码可直接导入开发工具运行,适合在此基础上扩展功能或定制界面,是完成毕业设计的高效参考。
1. 快递代取系统小程序:为什么这个毕设选题值得做,以及一条能跑通的主线
做毕业设计最怕的从来不是题目难,而是题目看起来简单、做起来全是坑。快递代取系统就是典型:表面上是“用户下单 + 骑手接单 + 跑腿送达”,但真做起来,登录、订单状态流转、消息通知、定位精度、并发接单,每一个环节都足够让人熬夜查 bug。这个题目能拿高分的关键,不是你用了多花哨的框架,而是你有没有把“代取”这件事的业务逻辑理清楚——谁会发单、谁会接单、订单在什么条件下从“已发布”变成“已完成”,这些才是答辩时老师盯着问的点。
这篇文章的目标读者很明确:正在选毕设题目的计算机相关专业学生,或者想用微信小程序做一个可演示、可部署、可扩展项目的开发者。全文会按我实际做过的一个代取系统方案来讲,技术栈选的是 uni-app 开发微信小程序端、Java Spring Boot 提供后端接口、MySQL 存数据——这个组合是面试和答辩都认的常见组合,而且 uni-app 的好处是以后想上线 App 端不用重写前端。下文会把这个系统拆成“业务建模 → 小程序端 → 后端接口 → 避坑 → 进阶”五条线一条条说透,每一步都给出能直接抄的代码和参数,你照着搭就能跑通。
2. 业务建模先行:代取订单的状态机与角色权限,别一上来就写页面
很多人做小程序毕业设计的第一步就是打开 HBuilderX 创建 uni-app 项目,然后疯狂写页面。这是最不需要着急的事。快递代取系统的核心不在界面,而在订单状态的流转——一个订单从发布到完成,中间要经过哪些状态、每个状态由谁触发、哪些操作会改变状态,这个模型不先定好,后面写代码就是无尽的 if else。
2.1 把代取流程拆成四个角色和七种状态
一个完整的代取业务,至少要有四种角色:发布代取任务的学生(用户)、接单的代取员(骑手)、后台管理员、以及系统本身。前两者是业务主角,管理员只负责异常订单和纠纷处理,系统则承担状态变更和通知。
订单的状态我建议设计成七种,用数字枚举存数据库。这个设计直接参照了电商订单的通用做法,但针对代取场景做了一些调整:
| 状态值 | 状态名 | 触发动作 | 下单人可见 | 接单人可见 |
|---|---|---|---|---|
| 0 | 已取消 | 用户付款前取消 | 是 | 是 |
| 1 | 待接单 | 用户发布成功 | 是 | 是 |
| 2 | 已接单 | 代取员领取订单 | 是 | 是 |
| 3 | 取件中 | 代取员标记已取件 | 是 | 是 |
| 4 | 配送中 | 代取员标记已出发 | 是 | 是 |
| 5 | 已送达 | 代取员标记已送达 | 是 | 是 |
| 6 | 已完成 | 用户确认收货 | 是 | 是 |
这七种状态的设计逻辑很简单:状态只能按数字从小到大推进,不允许跳转。特别要注意“已取消”这个状态,它虽然数值最小,但不代表订单流程的起点,而是独立的终态分支。如果用户下单后不想要了,订单直接从“待接单”跳到“已取消”,中间不能经过其他状态。
提示:状态值用数字而不是字符串,是为了后续写 SQL 统计时方便,比如统计“今日完成订单数”只需要一条
WHERE status = 6的查询。字符串状态看起来直观,但写进代码里容易拼写错误,而且没法做排序和范围查询。
2.2 角色权限与数据可见性:谁的数据谁能看
角色权限这层,很多毕业设计会忽略,但这是答辩时老师最喜欢问的点。快递代取系统的权限规则可以分成三条:
第一,用户只能看到自己和与自己相关的订单。用户发布订单后,能看到订单详情、当前状态、代取员联系方式;但在订单被接单之前,用户不能看到其他代取员的个人信息,否则会造成骚扰问题。第二,代取员可以看到所有“待接单”状态的订单,但一旦接了某一单,就只能看到自己接的订单。第三,管理员拥有全部订单的查看权限,可以对“异常状态”的订单进行干预,比如标记异常、强制退款。管理员不参与正常订单的状态推进,这是很多毕设容易做错的地方——把管理员当成一个超级代取员来用。
权限落到代码上,不是每个接口里写一次判断,而是在后端做一个拦截器统一校验。常见做法是定义一个 Role 枚举,然后在后端 Controller 层用自定义注解标记接口访问权限,Spring Boot 的拦截器会在请求进入业务逻辑之前做角色校验。
2.3 用状态机定义订单流转,拒绝 if 堆业务
状态机是代取系统业务建模里最值得写进答辩 PPT 的部分。如果不用状态机,订单状态判断会散落在各个接口里,比如接单接口里判断“当前状态是否等于待接单”,送达接口里判断“当前状态是否等于配送中”,一旦状态增多,这种判断逻辑会越写越乱。
我在这个系统里用的是 Spring StateMachine 框架,当然你也可以用简单的 Map + 条件判断实现一个轻量状态机,后者更容易在答辩时讲清楚。核心逻辑是:状态机只允许合法的状态迁移发生,非法迁移直接抛异常并返回错误信息。
这里给出一段后端状态机定义的关键代码,用 Java 实现,StateMachine 的配置类核心部分如下:
@Configuration @EnableStateMachine public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStatus, OrderEvent> { @Override public void configure(StateMachineStateConfigurer<OrderStatus, OrderEvent> states) throws Exception { states .withStates() .initial(OrderStatus.PENDING) // 初始状态:待接单 .state(OrderStatus.CANCELLED) // 终态一:已取消 .state(OrderStatus.COMPLETED) // 终态二:已完成 .states(EnumSet.allOf(OrderStatus.class)); } @Override public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderEvent> transitions) throws Exception { transitions .withExternal() .source(OrderStatus.PENDING) .target(OrderStatus.CANCELLED) .event(OrderEvent.CANCEL) // 用户取消订单 .and() .withExternal() .source(OrderStatus.PENDING) .target(OrderStatus.ACCEPTED) .event(OrderEvent.ACCEPT) // 代取员接单 .and() .withExternal() .source(OrderStatus.ACCEPTED) .target(OrderStatus.PICKING) .event(OrderEvent.PICK_UP) // 已取件 .and() .withExternal() .source(OrderStatus.PICKING) .target(OrderStatus.DELIVERING) .event(OrderEvent.DELIVER) // 已出发配送 .and() .withExternal() .source(OrderStatus.DELIVERING) .target(OrderStatus.DELIVERED) .event(OrderEvent.ARRIVED); // 已送达 } }这段配置代码的逻辑是:把订单的每个合法状态迁移都显式定义出来,不允许自定义跳转。比如从“已接单”直接跳到“已完成”就是非法的,因为代取员还没取件。参数说明:OrderStatus是订单状态枚举,值对应前文表格里的七种状态;OrderEvent是触发事件枚举,每个事件对应前端的一个用户操作或代取员操作。
引入状态机之后,业务代码里只需要一行stateMachine.sendEvent(OrderEvent.ACCEPT)就能完成状态变更,状态是否合法、能否迁移,由状态机本身来判断。这样答辩时你可以非常自信地说:订单状态变更逻辑集中在状态机配置中,没有散落在业务代码里。这是一个极其加分的工程化设计点。
3. 小程序端落地:登录、下单与订单列表的实现细节
业务模型定好之后,才轮到写小程序端。这一章是全篇最需要抄作业的部分,代码全部基于 uni-app 编写,可以直接运行到微信开发者工具。如果你之前只看过微信原生小程序,这里的写法也能看懂,因为 uni-app 最终会编译成微信小程序原生代码,只是语法上用了 Vue 单文件组件的方式。
3.1 微信登录流程与 token 管理:wx.login 只是第一步
微信小程序登录是整个系统的入口,但它的流程比很多人想象的要绕。核心逻辑是:小程序端调用uni.login拿到临时 code,把 code 发送到后端,后端拿 code 去微信接口换取 openid 和 session_key,再用 openid 去数据库找用户——找到了就是老用户,找不到就自动注册一个新用户。
这个流程有一个关键的坑:uni.login拿到的 code 五分钟内有效,而且只能用一次。小程序端拿到 code 之后必须立即发给后端,不能把 code 存起来复用。下面是小程序端的登录封装代码:
// utils/auth.js 登录封装 const login = () => { return new Promise((resolve, reject) => { uni.login({ provider: 'weixin', success: async (loginRes) => { // loginRes.code 是临时凭证,5分钟内有效 const code = loginRes.code; try { const res = await uni.request({ url: 'https://你的服务器域名/api/auth/login', method: 'POST', data: { code } }); const { token, userInfo } = res.data.data; // token 存 storage,后续每个请求带上 uni.setStorageSync('token', token); uni.setStorageSync('userInfo', userInfo); resolve({ token, userInfo }); } catch (e) { reject(e); } }, fail: (err) => reject(err) }); }); }; export default login;这段代码的逻辑是:uni.login成功后拿到 code,立即放进 POST 请求发给后端。后端返回的数据里包含token和userInfo,前端把这两个值存进本地缓存。注意token才是后续请求的身份凭证,userInfo只是用于界面展示,这两个东西要分开存。
注意:测试阶段后端可能跑在本地局域网 IP 上,但微信开发者工具默认不允许请求非 HTTPS 域名。解决方案是在开发者工具右上角“详情”里勾选“不校验合法域名”,这只是开发阶段的临时开关,上线前必须换成备案过的 HTTPS 域名。
3.2 代取订单的下单表单与地址选点
下单页是整个小程序端交互最重的页面,核心字段包括:取件地址、送达地址、包裹大小、快递公司、取件码、期望送达时间、小费金额。这里面最容易出错的是取件地址——快递代取的场景里,取件地址往往是一个快递柜或者驿站,它对定位精度的要求很高,用户手输文字很容易写错。
我在这个系统里用的是uni.chooseLocation接口,它可以直接调起微信内置的地址选择器,用户在地图上选点,返回经纬度和地址名称。这是真实项目里最常用的做法,比手写一个地图组件省事得多。下单页核心代码如下:
<template> <view class="order-form"> <view class="form-item"> <text class="label">取件地址</text> <input class="input" :value="pickupAddress" placeholder="点击选择取件地址" @tap="choosePickupLocation" /> </view> <view class="form-item"> <text class="label">取件码</text> <input class="input" v-model="pickupCode" placeholder="快递柜取件码" /> </view> <view class="form-item"> <text class="label">小费金额</text> <input class="input" type="digit" v-model="tipAmount" placeholder="0.00" /> </view> <button class="submit-btn" @tap="submitOrder">发布代取任务</button> </view> </template> <script> export default { data() { return { pickupAddress: '', pickupLatitude: 0, pickupLongitude: 0, pickupCode: '', tipAmount: '0.00' }; }, methods: { choosePickupLocation() { uni.chooseLocation({ success: (res) => { // res 包含 name、address、latitude、longitude this.pickupAddress = res.address || res.name; this.pickupLatitude = res.latitude; this.pickupLongitude = res.longitude; }, fail: (err) => { // 常见错误:用户拒绝授权定位权限 if (err.errMsg.includes('auth deny')) { uni.showModal({ title: '提示', content: '需要定位权限才能选择取件地址', success: (modalRes) => { if (modalRes.confirm) { uni.openSetting(); // 引导用户去设置页开启权限 } } }); } } }); }, submitOrder() { if (!this.pickupAddress) { uni.showToast({ title: '请选择取件地址', icon: 'none' }); return; } if (!this.pickupCode) { uni.showToast({ title: '请填写取件码', icon: 'none' }); return; } // 提交订单数据到后端 uni.request({ url: 'https://你的服务器域名/api/order/create', method: 'POST', header: { Authorization: uni.getStorageSync('token') }, data: { pickupAddress: this.pickupAddress, pickupLatitude: this.pickupLatitude, pickupLongitude: this.pickupLongitude, pickupCode: this.pickupCode, tipAmount: this.tipAmount }, success: (res) => { if (res.data.code === 0) { uni.showToast({ title: '发布成功', icon: 'success' }); uni.redirectTo({ url: '/pages/order/detail?id=' + res.data.data.id }); } } }); } } }; </script>这段代码最关键的两个参数是pickupLatitude和pickupLongitude,它们在后端会用于距离计算和派单排序。uni.chooseLocation返回的res.address有时会特别长,包含省市区街道,而res.name是地图上的 POI 名称,比如“菜鸟驿站(XX小区店)”。实际项目里我建议保存res.name作为展示地址,res.address作为辅助信息,这样订单列表里显示的地址更简洁。
3.3 订单列表的分页加载与下拉刷新
订单列表是用户打开频率最高的页面,它分为“我发布的”和“待接单”两个视图。这里有一个性能问题:如果一次请求把全部订单返回,数据库压力大、首屏加载慢,而且微信小程序单次 setData 的数据量不宜过大。标准做法是分页加载,每次请求 10 条或 20 条。
订单列表的分页逻辑有一个容易踩坑的点:下拉刷新应该重置页码,触底加载应该页码加一。如果刷新时忘记重置页码,会出现“刷新后列表里混着旧数据”的 bug。推荐写法如下:
// pages/order/list.vue 核心逻辑 export default { data() { return { orderList: [], page: 1, pageSize: 10, hasMore: true, loading: false }; }, methods: { // 加载订单列表 async loadOrders(page = this.page) { if (this.loading || !this.hasMore) return; this.loading = true; try { const res = await uni.request({ url: 'https://你的服务器域名/api/order/list', method: 'GET', data: { page, pageSize: this.pageSize, status: this.currentStatus } }); const { list, total } = res.data.data; // 关键逻辑:page=1 时用新数据替换旧数据,否则追加 const newList = page === 1 ? list : this.orderList.concat(list); this.orderList = newList; this.hasMore = this.orderList.length < total; this.page = page; } finally { this.loading = false; } } }, onPullDownRefresh() { // 下拉刷新:重置到第一页 this.page = 1; this.hasMore = true; this.loadOrders(1).finally(() => { uni.stopPullDownRefresh(); }); }, onReachBottom() { // 触底加载:加载下一页 if (this.hasMore) { this.loadOrders(this.page + 1); } } };这段代码里hasMore是一个布尔值,它决定触底时还要不要发请求。判断依据是当前已加载数量是否小于后端返回的 total。有一个血泪经验:如果后端返回的 total 是分页后的数量而不是总数量,hasMore的判断会失效,导致永远加载不完,所以后端接口一定要返回总数量,或者直接返回hasMore布尔值。
4. 后端接口与数据表设计:让毕设具备真正的后端工程能力
后端是很多做毕设的同学最心虚的部分,但快递代取系统对后端的要求其实不高:能处理登录、能管理订单、能保证数据一致性,就够了。这一章讲数据表设计和接口封装,按 Spring Boot + MyBatis-Plus 的常见组合来讲,这也是当前国内 Java 后端岗位最主流的技能栈组合。
4.1 数据表设计:五张核心表的字段与关系
快递代取系统的数据表可以精简为五张:用户表、订单表、接单记录表、通知记录表、反馈表。其中最重要的两张表是用户表和订单表。
用户表字段设计如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| openid | varchar(64) | 微信 openid,唯一索引 |
| nickname | varchar(32) | 用户昵称 |
| avatar_url | varchar(255) | 头像地址 |
| phone | varchar(11) | 手机号,接单联系方式 |
| role | tinyint | 角色:1普通用户 / 2代取员 |
| status | tinyint | 账号状态:0禁用 / 1正常 |
| create_time | datetime | 注册时间 |
订单表是核心中的核心,字段设计如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单号,展示给用户 |
| user_id | bigint | 下单用户 ID |
| courier_id | bigint | 接单代取员 ID,默认为空 |
| pickup_code | varchar(16) | 快递柜取件码 |
| pickup_address | varchar(255) | 取件地址文字描述 |
| pickup_lat | decimal(10,6) | 取件纬度 |
| pickup_lng | decimal(10,6) | 取件经度 |
| delivery_address | varchar(255) | 送达地址 |
| tip_amount | decimal(10,2) | 小费金额 |
| status | tinyint | 订单状态,对应状态机 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
一个容易忽视的细节是pickup_lat和pickup_lng的数据类型要用decimal(10,6)而不是 float,因为 float 有精度丢失问题。经纬度一旦精度不准,后面做距离计算时偏了一两米还算小事,偏了五十米就完全是另一个地方了。order_no建议用时间戳 + 随机数生成,比如20240601153012345678,这样用户报单号的时候可以一眼看出下单时间。
4.2 基于 uni.request 的请求封装与错误码约定
小程序端的请求都需要走 HTTPS,而且要带 token 作为身份凭证。如果每个页面都自己写一遍uni.request,会出现两个问题:一是代码大量重复,二是 token 过期后无法统一处理。常见的做法是封装一个request.js工具文件,所有请求统一走这个封装。
// utils/request.js // 统一请求封装 const request = (options) => { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token'); uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', // token 放在请求头,后端拦截器校验 'Authorization': token ? `Bearer ${token}` : '' }, success: (res) => { // 后端统一返回 { code, message, data } if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data); } else if (res.statusCode === 401) { // token 失效,跳回登录页 uni.removeStorageSync('token'); uni.reLaunch({ url: '/pages/login/login' }); reject(new Error('登录状态已过期')); } else { uni.showToast({ title: res.data.message || '请求失败', icon: 'none' }); reject(new Error(res.data.message)); } }, fail: (err) => { uni.showToast({ title: '网络异常,请检查网络', icon: 'none' }); reject(err); } }); }); }; export default request;这里的错误码约定是前后端一起定的:code = 0表示成功,非 0 表示业务失败,比如1001表示参数错误、1002表示无权限、1003表示订单状态不允许当前操作。token 过期用 HTTP 状态码401表示,前端收到 401 就统一清理登录态并跳回登录页。这个约定看起来很基础,但它决定了整个项目前后端协作是否顺畅,值得写进你的答辩文档。
4.3 订单通知的时机与模板消息替代方案
订单状态变化之后,怎么通知用户?这是代取系统体验好坏的分水岭。如果用户发完单之后只能自己刷新页面看状态,那这个产品的体验是非常原始的。
目前微信小程序已经不支持旧版的模板消息,主流的替代方案有两个:微信订阅消息(一次性订阅)和客服消息。订阅消息需要用户主动点击授权一次,每次授权只能发送一次消息,所以需要在关键节点提醒用户订阅。
常见做法是:用户下单成功后弹窗提示“订阅取件进度通知”,用户点了订阅之后,后端才可以在订单状态变化时给他推送订阅消息。如果用户没订阅,那只能通过短信补一条通知,或者让用户在订单详情页轮询状态。
// OrderNotifyService.java 部分逻辑 public void notifyStatusChange(Order order) { // 检查用户是否订阅了该订单的通知 SubscribeRecord record = subscribeRecordMapper.selectByOrderAndUser(order.getId(), order.getUserId()); if (record != null) { // 发送一次性订阅消息 wxTemplateService.sendSubscribeMessage(record.getOpenid(), order); // 发送过之后该订阅记录失效,置为已使用 record.setStatus(0); subscribeRecordMapper.updateById(record); } }这段代码的逻辑是:每次状态变更时,先查用户有没有订阅消息配额,有就发送,发送完把配额置为已使用。注意微信订阅消息的配额是“一次授权、一次发送”,所以如果订单要经历接单、取件、配送、送达四次通知,用户必须在下单时连续订阅四次。这是一个体验上的局限,也是答辩时可以展开讲的点,说明你有真实的业务思考。
5. 避坑指南:小程序审核、定位精度与并发接单的五个常见翻车点
这一章是血泪经验汇总。快递代取系统虽然业务不复杂,但真正跑起来、甚至上线测试的时候,会遇到几个让新手程序员整晚失眠的问题。每一条都按“现象 → 原因 → 解决”的结构写,方便你直接对号入座。
5.1 现象:代码审核被拒,理由是类目与资质不符
这是小程序上线的第一道坎。微信小程序类目中有“快递业”相关类目,如果你选择“快递、邮政”类目,审核会要求提供《快递业务经营许可证》,这不是学生个人能做出来的资质。很多人的快递代取小程序就是卡在这一步上不了线。
原因很简单:代取快递在微信官方的定义里属于快递业务的延伸服务,涉及末端配送和代收代发,因此对主体资质卡得很严。解决方式是调整产品定位:不要强调“代取快递”,而是把产品定位成“校内互助跑腿”——比如帮取外卖、帮拿快递、帮买零食。跑腿类目审核相对宽松,个人主体也能申请部分类目。也就是说,你要在应用描述、页面文案里统一用“互助跑腿”“生活服务”这类口径,不要用“快递代取”作为宣传标题。
5.2 现象:校园内定位偏差导致取件坐标不准
快递柜和驿站通常在建筑物旁边,定位偏差很容易把取件点定位到隔壁教学楼。测试时发现的典型现象是:用户在大门口选点,代取员按导航走过去,发现位置在马路对面,绕了一圈才找到快递柜。
这个问题的根源是校园场景下 GPS 信号被建筑遮挡,再加上chooseLocation返回的坐标是 WGS84 还是 GCJ02 坐标系,不同的坐标系之间本身就有几十到几百米的偏移。解决方式是两层:第一层,在后端保存坐标时统一转换成 GCJ02(火星坐标系),因为微信地图使用的是 GCJ02,不要直接存原生定位坐标;第二层,在取件地址旁边加一个备注字段,比如“XX 快递柜,从东门进右转 50 米”,让下单用户主动补充路线描述,这个比精确到米的定位可靠得多。
5.3 现象:同一单被多人同时接单,状态被覆盖
这是并发问题里最经典的场景。两个代取员同时打开待接单列表,都看到了订单 A,然后同时点击“接单”,后端如果只做了“判断当前状态为待接单,然后更新为已接单”,两个人都会成功——因为两个请求同时读到的是同一个旧状态,两个更新都执行了,最后一个写入的值覆盖了前一个。
原因是对“状态检查 + 状态更新”两个操作没有做原子性保证。解决的常见做法有两种:第一种是数据库层面加乐观锁,在订单表增加version字段,更新时带上版本号,UPDATE order SET status = 2, courier_id = ?, version = version + 1 WHERE id = ? AND version = ?,如果影响行数为 0,说明被别人抢先了;第二种更简单,直接用带状态条件的更新语句:
-- 接单操作的原子 SQL UPDATE `order` SET status = 2, courier_id = #{courierId}, update_time = NOW() WHERE id = #{orderId} AND status = 1 AND courier_id IS NULL;这条 SQL 的巧妙之处在于,它把“检查状态”和“更新状态”合并成了一个数据库操作。如果两个代取员同时执行这条 SQL,数据库的行锁会保证只有一个更新成功,另一个影响行数为 0,后端根据影响行数判断是否接单成功。这个方案是毕设里性价比最高的并发处理手段,代码量少、逻辑清晰、答辩加分。
5.4 现象:token 过期后用户操作全部失败,体验断崖
用户在小程序里用着用着,突然所有操作都返回 401,点击任何按钮都跳回登录页。更糟糕的是,登录页刷新后还登不进去,因为后端发现 token 已过期要求重新走wx.login,但前端的登录封装只处理了首次登录场景,没有处理“静默重新登录”。
原因是 token 过期后的重登录链路没有打通。解决方式是在 request 封装里增加 401 自动重试逻辑:收到 401 后,先调用uni.login换新 code,再用新 code 请求后端刷新 token,拿到新 token 后重放原请求。这个逻辑可以让用户几乎无感知地续期登录态,而不是生硬地跳回登录页。
常见的实现是给 request 封装增加一个isRetry标志,防止递归重试。第一个请求 401 后触发重登录,重登录成功后重新发起原请求,并把isRetry置为 true;如果重试后仍然 401,再跳登录页,避免无限循环。
5.5 现象:真机预览正常,体验版首页白屏
这是小程序开发里最折磨人的问题。开发者工具里一切正常,代码上传后生成体验版,手机上打开首页直接白屏,控制台报错信息为TypeError: Cannot read property 'xxx' of undefined之类,但同样的代码在开发者工具里没有任何报错。
现象背后的常见原因是 ES6 语法在低版本安卓机的 WebView 或小程序基础库中不受支持,或者某个使用了高级语法的第三方依赖没有被小程序编译器正确转译。另一种常见原因是体验版加载的接口域名和真机预览不一致——体验版强制校验合法域名,开发工具勾选的“不校验域名”选项在体验版不生效。
解决方式是两步走:第一步,在 manifest.json 源码视图里检查mp-weixin配置,确认ES6 转 ES5是否开启,这是微信开发者工具“本地设置”里的一个选项;第二步,把所有请求域名在微信公众平台“开发管理 → 服务器域名”里提前配置好,request 合法域名必须是 HTTPS 且已完成备案。先排查域名,再排查转译,经验告诉我前者占到 70% 以上的白屏原因。
6. 进阶落地:接入地图选点、跑腿费用计算与数据统计,把毕设做成答辩加分项
到这里,一个能跑通的快递代取系统已经成型了。这一章讲三个进阶方向,每一个都能让答辩评委看到你的工程能力:地图选点优化代取员找件效率、跑腿费用计算规则、以及基于订单数据的运营统计。
地图选点方面,uni.chooseLocation在校园场景的体验其实一般,因为它的 POI 数据偏商业场景,校园里的驿站经常搜不到。进阶做法是在后端维护一份“常用取件点”数据表,由管理员预先录入校园内所有驿站和快递柜的名称、位置、备注,下单时前端展示一份预置列表,同时保留地图选点作为兜底选项。这样输出的配送距离是精确计算过的,代取员的找件时间明显缩短。
费用计算规则可以做成分级计费:基础起步价 3 元(500 米内),每超出 500 米加 1 元,取件码代取服务加收 1 元,小费由用户自愿设置。这个规则听上去简单,但你要把它做成一个后端服务类,用清晰的参数配置,而不是散落在下单接口的代码里:
// DeliveryFeeCalculator.java 配送费用计算器 public BigDecimal calculateFee(double distanceMeters, boolean needPickupCode) { BigDecimal baseFee = new BigDecimal("3.00"); // 500米内起步价 double extraDistance = distanceMeters - 500; // 超出部分距离 BigDecimal distanceFee = BigDecimal.ZERO; if (extraDistance > 0) { // 超出部分每500米加1元,不足500米按500米算 int extraBlocks = (int) Math.ceil(extraDistance / 500); distanceFee = new BigDecimal(extraBlocks); } BigDecimal pickupCodeFee = needPickupCode ? new BigDecimal("1.00") : BigDecimal.ZERO; return baseFee.add(distanceFee).add(pickupCodeFee); }这段代码的参数含义:distanceMeters是从取件点到送达点的直线距离乘上一个路径系数(校园道路不是直的,常见的做法是直线距离乘 1.3 作为预估步行距离);needPickupCode表示代取员是否需要输入取件码才能取件,快递柜取件和驿站前台取件的服务成本不同。
数据统计这一块,建议在管理员端加三个数字:今日订单总量、平均接单时长、平均配送时长。平均接单时长在数据库里就是courier_id填入时间减去create_time,一条 SQL 就能算出来。这三个指标能切实反映代取服务的效率,比堆砌图表更能打动老师。我自己的习惯是,优先做对业务有解释力的单一指标,比如“高峰期平均接单时长超过 10 分钟,说明代取员供给不足”,这类结论比一堆饼图更有说服力。
最后说一个我自己的教训:这个项目最花时间的环节不是写代码,而是微信公众平台的各种配置——域名备案、业务域名、隐私协议、类目审核。这些流程性的东西看起来不产代码,但卡住一次就能耗掉好几天。建议你在开发的第一周就去把小程序账号注册好,在开发的时候同步提交域名备案申请,不要等代码写完了才走流程。希望这份方案能帮你把毕业设计从头到尾跑通,答辩论据也顺手有了。
本文还有配套的精品资源,点击获取