之前不少同学在准备毕业设计时,都会选“微信小程序 + 校园业务”这类题目。但真动手时会发现:前端小程序要处理登录、页面状态、接口联调,后端要考虑鉴权、订单状态流转、数据库设计,资料又零散。本文围绕“基于微信小程序的校园跑腿系统”从需求分析、表结构设计、小程序端逻辑、后端接口实现、联调验证到常见报错排查完整拆解,项目结构可以直接作为毕业设计或课程设计的底座,代码思路也能迁移到同类型系统中。
1. 系统背景与技术选型
1.1 校园跑腿系统解决什么问题
校园跑腿的业务场景很典型:同学A在宿舍不想下楼拿快递,同学B刚好有空且愿意跑一趟,双方通过平台完成“发单—接单—送达—确认”的闭环。比起普通电商系统,它更强调即时性和LBS属性。
从毕业设计角度看,一个完整的校园跑腿系统应当包含:
- 用户侧:微信授权登录、发布跑腿订单、浏览待接单列表、确认完成、订单评价。
- 接单侧:抢单/接单、查看订单详情、标记取件/送达。
- 管理侧:用户管理、订单管理、分类管理、数据统计。
这类系统的价值在于业务链路清晰,技术点覆盖广,适合作为毕业设计、课程设计、实训项目的原型,也方便做功能扩展和二次开发。
1.2 技术选型与系统架构
系统采用前后端分离架构:
| 端 | 技术选型 | 作用 |
|---|---|---|
| 小程序端 | 微信小程序原生框架(WXML + WXSS + JS) | 用户交互、登录、发单、接单 |
| 后端服务 | Spring Boot + MyBatis | 提供 RESTful API、业务处理、鉴权 |
| 数据库 | MySQL 5.7 / 8.0 | 用户、订单、分类等数据存储 |
| 云开发(可选) | 微信云开发 | 适合不想维护后端的快速实现 |
后端技术栈以 Java 为基础,也可以换成 Node.js、Python Flask/Django、PHP 等,本文讲解以 Spring Boot + MyBatis 为主,因为课程设计和毕业设计中这一套最常用。
整体流程如下:
微信小程序 (WXML/JS) ↓ HTTPS + JSON Spring Boot 后端 (Controller → Service → Mapper) ↓ JDBC MySQL 数据库开发者需要掌握的核心点包括:
- 微信小程序登录流程。
- 自定义 Token 鉴权方案。
- 订单状态机设计。
- 前后端接口联调与真机调试。
2. 功能模块与数据库设计
2.1 功能模块拆分
按照“电商 + 任务派发”的思路,功能模块分为三类角色:
- 普通用户:登录、发布订单、查看自己的订单、确认完成。
- 跑腿者:浏览大厅订单、接单、更新任务状态。
- 管理员:后台管理,查看用户列表、订单列表、订单分类统计。
小程序端页面至少包含:
pages/index/index 首页(发单入口 + 订单大厅) pages/order/detail 订单详情 pages/my/orders 我的订单 pages/my/index 个人中心 pages/publish/publish 发布跑腿订单后端接口按照资源维度拆分:
POST /api/user/login 微信登录 GET /api/user/info 获取用户信息 POST /api/order/publish 发布订单 GET /api/order/list 订单列表(分页 + 状态筛选) POST /api/order/accept 接单 POST /api/order/complete 确认完成 POST /api/order/cancel 取消订单2.2 数据库表结构设计
一个可运行的跑腿系统只需要三类核心表:用户表、订单表、分类表。下面给出 SQL 设计,中间省略部分非必要字段,保留了扩展字段的位置。
-- 用户表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信用户唯一标识', `nickname` varchar(64) DEFAULT '' COMMENT '昵称', `avatar` varchar(255) DEFAULT '' COMMENT '头像URL', `phone` varchar(20) DEFAULT '' COMMENT '手机号', `student_no` varchar(32) DEFAULT '' COMMENT '学号', `dormitory` varchar(64) DEFAULT '' COMMENT '宿舍信息', `role` tinyint(4) DEFAULT 1 COMMENT '角色:1-普通用户 2-跑腿者 3-管理员', `balance` decimal(10,2) DEFAULT 0.00 COMMENT '账户余额', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';-- 订单表 CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` int(11) NOT NULL COMMENT '发布人ID', `type_id` int(11) DEFAULT 1 COMMENT '订单类型:取快递/代购/代办', `title` varchar(100) NOT NULL COMMENT '标题', `description` varchar(500) DEFAULT '' COMMENT '详细描述', `pickup_address` varchar(255) DEFAULT '' COMMENT '取件地址', `delivery_address` varchar(255) DEFAULT '' COMMENT '送达地址', `reward` decimal(10,2) DEFAULT 0.00 COMMENT '跑腿费/悬赏金', `status` tinyint(4) DEFAULT 0 COMMENT '状态:0-待接单 1-进行中 2-已完成 3-已取消', `accept_id` int(11) DEFAULT NULL COMMENT '接单用户ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `accept_time` datetime DEFAULT NULL COMMENT '接单时间', `finish_time` datetime DEFAULT NULL COMMENT '完成时间', PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='跑腿订单表';-- 订单分类表 CREATE TABLE `order_type` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL COMMENT '分类名称', `icon` varchar(255) DEFAULT '' COMMENT '图标', `sort` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单分类表';2.3 核心表关系与设计说明
用户表与订单表是一对多关系:一个用户可发布多个订单。订单表中的accept_id表示接单人,它同样是用户表 id 的外键逻辑,但一个订单只能有一个接单人。
这里的status字段是整个系统的核心状态机:
- 0 待接单:发布成功后的初始状态。
- 1 进行中:跑腿者接单后,订单进入履约状态。
- 2 已完成:用户确认后闭环。
- 3 已取消:发布人取消或后台取消。
在业务层,我们需要保证状态只能按合法路径流转,例如“待接单 -> 进行中 -> 已完成”,不能从“待接单”直接跳到“已完成”,也不能让非接单人修改订单状态。状态字段的约束既可以通过后端代码控制,也可以通过数据库触发器等兜底,但在课程设计阶段,优先在 Service 层实现状态校验。
3. 微信小程序端开发
3.1 小程序项目结构
小程序端采用原生框架,建议按照页面和公共模块拆分目录:
miniprogram/ ├── app.js // 小程序入口,处理全局登录态 ├── app.json // 全局配置 ├── app.wxss // 全局样式 ├── utils/ │ └── request.js // 封装 wx.request ├── pages/ │ ├── index/ // 首页/订单大厅 │ ├── publish/ // 发布订单 │ ├── order/ // 订单详情 │ └── my/ // 个人中心 └── components/ // 公共组件3.2 微信登录与用户身份获取
微信小程序登录推荐使用wx.login获取临时 code,然后把 code 发送到后端,由后端调用微信接口换取openid和session_key。
不建议在开发者工具中调试时直接模拟 getUserProfile 来跳过登录,因为真机上线后登录凭证必须走正规流程。
小程序端登录核心代码:
// 文件路径:utils/auth.js function login() { return new Promise((resolve, reject) => { wx.login({ success(res) { if (res.code) { wx.request({ url: 'https://你的域名/api/user/login', method: 'POST', data: { code: res.code }, success(loginRes) { const data = loginRes.data || {}; if (data.code === 0 && data.data.token) { wx.setStorageSync('token', data.data.token); wx.setStorageSync('userInfo', data.data.userInfo); resolve(data.data); } else { reject(new Error(data.msg || '登录失败')); } }, fail: reject }); } else { reject(new Error('wx.login 获取 code 失败')); } }, fail: reject }); }); } module.exports = { login };后端收到 code 后,需要调用微信接口:
GET https://api.weixin.qq.com/sns/jscode2session ?appid=APPID &secret=SECRET &js_code=CODE &grant_type=authorization_code微信返回结构:
{ "openid": "oXXXX", "session_key": "tXXXX", "unionid": "uXXXX", "errcode": 0, "errmsg": "" }后端拿到openid后查询用户表,如果不存在则自动注册。注意:appid 和 secret 只能保存在后端,不能出现在小程序代码中,否则会泄露密钥。
3.3 首页订单列表
首页展示待接单的订单大厅,核心页面是pages/index/index.js。
分页加载是必须的,否则订单量增长后页面会卡顿。下面给出列表请求的核心逻辑:
// 文件路径:pages/index/index.js Page({ data: { orderList: [], page: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.loadOrders(); }, async loadOrders() { if (this.data.loading || !this.data.hasMore) return; this.setData({ loading: true }); const token = wx.getStorageSync('token'); const res = await new Promise((resolve) => { wx.request({ url: 'https://你的域名/api/order/list', method: 'GET', header: { Authorization: 'Bearer ' + token }, data: { page: this.data.page, pageSize: this.data.pageSize, status: 0 }, success: resolve, fail: () => resolve({ data: { code: -1 } }) }); }); const data = res.data; if (data.code === 0) { const list = data.data.list || []; this.setData({ orderList: this.data.orderList.concat(list), page: this.data.page + 1, hasMore: list.length >= this.data.pageSize, loading: false }); } else { this.setData({ loading: false }); wx.showToast({ title: data.msg || '加载失败', icon: 'none' }); } }, onReachBottom() { this.loadOrders(); } });onReachBottom是页面滚动到底部的回调,配合hasMore实现简单的分页,避免一次性加载过多数据。
3.4 发布订单页面
发布订单页面需要收集订单类型、标题、详细描述、取件地址、送达地址和跑腿费。
表单校验不能只在小程序端做,后端也要做二次校验,否则接口被直接调用时可以绕过前端规则。
// 文件路径:pages/publish/publish.js Page({ data: { typeList: [], form: { typeId: 1, title: '', description: '', pickupAddress: '', deliveryAddress: '', reward: '' } }, onLoad() { this.loadTypes(); }, loadTypes() { const token = wx.getStorageSync('token'); wx.request({ url: 'https://你的域名/api/order/typeList', header: { Authorization: 'Bearer ' + token }, success: (res) => { if (res.data.code === 0) { this.setData({ typeList: res.data.data || [] }); } } }); }, handleSubmit() { const form = this.data.form; if (!form.title.trim()) { wx.showToast({ title: '请填写标题', icon: 'none' }); return; } if (!form.pickupAddress.trim() || !form.deliveryAddress.trim()) { wx.showToast({ title: '请完善取件和送达地址', icon: 'none' }); return; } const token = wx.getStorageSync('token'); wx.request({ url: 'https://你的域名/api/order/publish', method: 'POST', header: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, data: form, success: (res) => { if (res.data.code === 0) { wx.showToast({ title: '发布成功', icon: 'success' }); setTimeout(() => { wx.switchTab({ url: '/pages/index/index' }); }, 1500); } else { wx.showToast({ title: res.data.msg || '发布失败', icon: 'none' }); } } }); } });此处wx.switchTab是因为订单大厅通常配置在 TabBar 中,普通页面间跳转则使用wx.navigateTo,两者不能混用。
4. 后端接口设计与实现
4.1 后端项目结构
后端采用 Spring Boot + MyBatis,目录结构如下:
src/main/java/com/example/running/ ├── RunningApplication.java ├── controller/ │ ├── UserController.java │ └── OrderController.java ├── service/ │ ├── UserService.java │ └── OrderService.java ├── mapper/ │ ├── UserMapper.java │ └── OrderMapper.java ├── entity/ │ ├── User.java │ └── Order.java ├── common/ │ ├── Result.java │ └── TokenUtils.java └── config/ └── WebConfig.java4.2 统一返回结果
前后端分离接口需要统一的返回格式,便于小程序端统一处理错误码。一般定义如下:
public class Result<T> { private int code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(0); result.setMsg("success"); result.setData(data); return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.setCode(500); result.setMsg(msg); return result; } // getter / setter 省略 }4.3 用户登录接口
后端登录接口需要调用微信 API,同时负责用户注册逻辑。核心接口:
@RestController @RequestMapping("/api/user") public class UserController { @Resource private UserService userService; @PostMapping("/login") public Result<Map<String, Object>> login(@RequestBody LoginRequest request) { return userService.login(request.getCode()); } }Service 层核心逻辑:
public Result<Map<String, Object>> login(String code) { // 1. 调用微信接口,获取 openid String openid = wxService.getOpenid(code); if (openid == null) { return Result.error("登录失败"); } // 2. 查询用户是否存在,不存在则注册 User user = userMapper.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户"); user.setRole(1); userMapper.insert(user); } // 3. 生成自定义 token String token = TokenUtils.generateToken(String.valueOf(user.getId())); Map<String, Object> data = new HashMap<>(); data.put("token", token); data.put("userInfo", user); return Result.success(data); }这里的 Token 可以使用 JWT,也可以使用随机 UUID 配合 Redis 保存。毕业设计如果不想引入 Redis,可以简单生成 UUID 后存数据库,或用 JWT 自包含用户信息。推荐使用 JWT,因为它无状态,便于横向扩展。
拦截器统一校验 Token:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); } Integer userId = TokenUtils.parseToken(token); if (userId == null) { response.setStatus(401); return false; } request.setAttribute("userId", userId); return true; } }4.4 发布订单接口
发布订单需要处理的核心业务逻辑:
- 参数校验(标题、地址必填)。
- 生成唯一订单号。
- 保存订单,初始状态为“待接单”。
public Result<String> publish(Integer userId, Order order) { // 参数校验 if (order.getTitle() == null || order.getTitle().trim().isEmpty()) { return Result.error("订单标题不能为空"); } if (order.getPickupAddress() == null || order.getDeliveryAddress() == null) { return Result.error("取件地址和送达地址不能为空"); } order.setUserId(userId); order.setOrderNo(generateOrderNo()); order.setStatus(0); order.setCreateTime(new Date()); orderMapper.insert(order); return Result.success(order.getOrderNo()); }generateOrderNo()的生成规则可以选择“日期 + 随机数”:
private String generateOrderNo() { return "RT" + System.currentTimeMillis() + RandomUtil.randomNumbers(4); }4.5 接单与完成接口
接单接口是整个系统中的重点,必须防止并发情况下多个人同时抢到同一订单,因此需要使用乐观锁或行级锁保证并发安全。
示例采用 SQL 条件更新:
public Result<String> accept(Integer userId, Long orderId) { Order order = orderMapper.selectById(orderId); if (order == null || order.getStatus() != 0) { return Result.error("订单不存在或已被接走"); } if (order.getUserId().equals(userId)) { return Result.error("不能接自己发布的订单"); } int rows = orderMapper.updateStatusWithLock(orderId, userId, 0, 1); if (rows == 0) { return Result.error("订单已被其他人接走"); } return Result.success("接单成功"); }对应 Mapper 中的条件更新:
<update id="updateStatusWithLock"> UPDATE orders SET status = #{newStatus}, accept_id = #{userId}, accept_time = NOW() WHERE id = #{orderId} AND status = #{expectedStatus} </update>这里的核心思想是CAS(Compare And Swap):更新时不仅设置新状态,还带上期望状态,数据库执行 UPDATE 时会自动加行锁,从而避免并发覆盖。
完成订单接口同理:
public Result<String> complete(Integer userId, Long orderId) { Order order = orderMapper.selectById(orderId); if (order == null || order.getStatus() != 1) { return Result.error("订单状态异常"); } // 只有接单人本人可以标记完成 if (!order.getAcceptId().equals(userId)) { return Result.error("无权操作该订单"); } int rows = orderMapper.updateStatus(orderId, 1, 2); if (rows == 0) { return Result.error("操作失败"); } return Result.success("订单完成"); }在实际毕设项目中,还可以扩展支付模块:用户发布订单时先扣减余额或虚拟币,接单人完成后结算跑腿费。由于涉及资金,不建议在课设阶段直接接入真实微信支付,可以用“模拟金额”或“积分”方案替代。
5. 运行与验证
5.1 环境准备
开发前需要准备:
- 微信开发者工具(稳定版即可)。
- 微信小程序 AppID(测试账号可用测试号)。
- JDK 1.8+。
- Maven 3.6+。
- MySQL 5.7+。
- 后端 IDE:IDEA 或 Eclipse。
版本说明:JDK、Spring Boot、MySQL 版本需要根据你本机环境调整,本文示例以 Spring Boot 2.x + JDK 1.8 为常见环境,如果你使用 JDK 17 或 Spring Boot 3.x,部分依赖包名与配置会有差异,需要按实际情况修改。
5.2 后端配置
在application.yml中配置数据库与微信参数:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_run?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver wx: appid: 你的小程序appid secret: 你的小程序secret其中wx.appid和wx.secret在微信公众平台的小程序后台获取。注意不要提交到公开仓库,生产环境建议放到配置中心或环境变量中。
5.3 启动与联调
后端启动后,在微信开发者工具中导入小程序项目,将utils/request.js中的接口地址改成http://localhost:8080。这里要注意:开发工具中需要关闭“合法域名校验”,真机预览时则必须使用https域名。
// 文件路径:utils/request.js const BASE_URL = 'http://localhost:8080'; function request(url, method = 'GET', data = {}) { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, success: resolve, fail: reject }); }); } module.exports = { request };推荐先跑通“登录 -> 发布订单 -> 列表展示 -> 接单 -> 完成”这条主链路,再逐步完善个人中心和管理员功能。
6. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
登录失败,报错getUserProfile:fail can only be invoked by user TAP gesture | 登录按钮在onLoad或onShow中直接调用 | 必须由用户点击事件触发,不能在小程序启动时自动调用 |
真机测试报错failed net::ERR_CONNECTION_RESET | 请求地址不是合法域名 / 未配置 request 合法域名 | 在公众平台配置 HTTPS 合法域名,或开发阶段勾选“不校验合法域名” |
| 小程序获取登录后的微信用户失败 | code需要在有效期内使用,通常 5 分钟,且只能使用一次 | 检查后端 jscode2session 调用是否正确,code 是否被重复使用 |
| 在 HBuilderX 中修改小程序 id 无效 | 项目配置与 manifest 配置不同步,或打开的是旧项目缓存 | 在manifest.json的微信小程序配置项中重新填写 appid,并重新编译 |
| 请求返回 401 | Token 缺失或过期 | 检查请求头 Authorization 是否携带,后端拦截器是否放行登录接口 |
| 订单发布成功但列表不展示 | 状态筛选条件错误,或订单列表只查询了 status=0 | 检查列表接口的状态参数,以及发布后是否跳转到订单大厅刷新 |
| 多人同时接单,订单被重复接走 | 缺少状态更新时的并发控制 | 使用 UPDATE ... WHERE status=0 条件更新,避免先查询再更新 |
6.1 开发工具调试注意事项
开发者工具中可以选择“不使用合法域名”,这样本地开发可以直接请求http://localhost:8080。但真机预览时,微信要求所有网络请求必须为 HTTPS 且配置到小程序后台的“request 合法域名”中。
如果只是在校园局域网内测试,可以暂时开启“不校验合法域名”开关,但不能作为正式上线方案。上线前需要将后端部署到带 HTTPS 证书的服务器。
6.2 登录态失效
Token 过期、用户删掉小程序重新打开、微信 session_key 过期都可能导致登录态失效。小程序端应在全局封装“登录后重试”逻辑:
// 简化思路:请求返回 401 时自动重新登录并重发原请求 function requestWithAuth() { // 先检查本地 token // 不存在则先 login(),再执行 request // 返回 401 则清除 token,重新 login 后再次请求 }这个逻辑建议在utils/request.js中统一封装,避免每个页面重复写。
7. 最佳实践与工程建议
7.1 小程序端建议
- 请求统一封装:不要在页面中散落大量
wx.request,统一走request.js,方便做 Token 注入、错误处理和加载状态管理。 - 页面传参谨慎:订单列表到详情页传
orderId,不要让前端直接传递整个订单对象,避免数据不一致或占用缓存。 - 分包加载:如果页面数量增多,可以启用微信小程序分包,降低首屏加载耗时。
- 列表使用虚拟滚动或分页:不要一次渲染全部订单,分页是底线。
7.2 后端建议
- 接口鉴权不能只在业务方法里手动判断:统一使用拦截器或 Spring Security,对
/api/**下的接口做 Token 校验,排除登录接口。 - 数据库字段使用逻辑删除:订单删除时建议采用
deleted字段标记,避免硬删除导致统计失真。 - 订单状态流转放在同一个事务里:涉及“接单”“完成”这种多表更新时,保证事务一致性。
- 金额计算避免浮点误差:跑腿费使用
Decimal类型,不要用float/double存储金额。 - 敏感配置不要写死在代码里:微信 secret、数据库密码通过环境变量或配置中心管理。
- 日志规范:订单操作必须保留日志,比如谁在什么时间修改了订单状态,方便纠纷回溯。
7.3 并发安全
抢单场景是跑腿系统的核心并发问题。多次点击“接单”按钮时,后端接口可能被同时调用,因此必须用条件 UPDATE 防止重复接单。不要使用“先 select 再 update”的方式,因为并发下可能两个请求都查到 status=0,然后都更新成功,造成一单多接。
7.4 安全边界
- 用户只能查看和操作自己的订单。
- 用户不能接自己发布的订单。
- 订单状态变更必须校验当前状态。
- 后端接口必须做参数校验,避免 SQL 注入和越权。
8. 后续扩展方向
毕业设计如果只做到“发单/接单/完成”已经满足基本功能要求,但为了答辩展示和功能完整度,可以继续扩展:
- 消息通知:订单被接单后通过订阅消息通知发布人。
- 信用评价:订单完成后双方互评,记录用户信用分。
- 余额充值:模拟充值跑腿费,发布订单时先冻结该笔费用,完成后结算给接单者。
- 管理员后台:用 Vue + Element UI 或小程序管理端实现订单统计、用户管理。
- LBS 定位:基于微信小程序
wx.getLocation获取当前位置,按距离排序订单。 - 地图选点:发布订单时使用腾讯地图插件选择取件地址和送达地址,前端展示配送路径。
这些扩展点都可以作为论文中的“系统创新点”或“后续展望”,同时也能让系统从“演示 Demo”走向“可体验原型”。
核心开发思路其实不复杂:先跑通登录链路,再实现订单主流程,最后补充管理功能和细节优化。希望这篇教程能帮你快速搭起一个能运行、能讲解、能答辩的完整项目。如果后续遇到具体报错,也可以按照“错误现象 -> 接口层日志 -> 数据层日志 -> 微信开发者工具 Network 面板”的顺序逐步定位,多数问题都能在半小时内排查清楚。