做校园跑腿这个项目,是我这几年看下来最经典的 SpringBoot + Vue 全栈练手选题之一。它不是那种“为了技术而技术”的空壳项目,而是真真切切能跑通完整业务闭环的系统:用户下单、跑腿员接单、配送完成、费用结算,再加上管理员后台,麻雀虽小五脏俱全。对于想找毕设方向或者想系统梳理前后端知识的人来说,这个题目能覆盖到的知识点非常密集,而且业务场景贴近生活,容易讲清楚,也容易演示。
这篇文章我会完全站在实际开发的角度,把这个项目的核心设计思路、表结构、状态机、前端的交互细节、以及我实测下来的高频 Bug 全部拆开来讲。不谈虚的,只说怎么做、为什么这么做,以及有哪些坑我已经替你们踩过了。
1. 项目整体设计与业务拆解
1.1 核心角色与业务定位
校园跑腿网站的业务模型不复杂,但角色划分必须清晰。我梳理下来,这个系统至少需要三类角色:
- 普通学生用户(下单方):发布跑腿需求,填写取件地点、送达地点、期望完成时间、小费金额,然后等待跑腿员接单。完成后可以确认收货并评价。
- 跑腿员(接单方):在接单大厅浏览可接的订单,按距离、价格、方向筛选合适的任务,接单后完成配送流程。这里通常涉及身份认证(比如学号验证、上传学生证照片),因为跑腿员信任度直接关系到平台安全。
- 管理员:负责审核跑腿员资质、处理用户投诉、下架违规订单、管理公告和系统参数(比如平台抽成比例、单笔最大金额限制)。
这个三角色模型看起来简单,但放到具体业务里就有很多细节。比如“余额充值”和“提现”怎么做?用户下单时是直接扣款冻结,还是跑腿员完成后从平台账户划转?我在设计时采用了“用户充值余额→下单冻结金额→完成后解冻划转给跑腿员”的方式,这样平台作为中间担保方,能有效降低双方的信任成本。跑腿员侧的“钱包”记录提现明细,管理员审核后打款,这套逻辑闭环是证明你系统设计能力的地方,也是答辩时老师最常追问的部分。
1.2 主业务流程与异常分支
整个系统的核心事务是“订单的生命周期”。正常流程是:
code复制用户发布订单 → 订单进入待接单池 → 跑腿员接单 → 跑腿员开始配送 → 跑腿员标记送达 → 用户确认收货 → 订单完成 → 资金解冻并结算
这中间有非常多的异常分支要处理。订单超时没人接单怎么办?跑腿员接了单但长时间不取货怎么办?用户临时取消订单怎么退款?跑腿员送到了一半用户联系不上怎么办?送达后发生争议由谁判定?
我建议在项目设计阶段就把这些状态变化做成一张订单状态机图,不是在文档里画完就完事,而是要真正映射到代码里。每个状态定义成一个枚举,所有状态流转都通过统一的 OrderService.changeState() 方法触发,里面做前置校验、状态记录、消息推送。我第一次做的时候图省事,直接在各处调用 orderMapper.update 去改状态,结果业务越写越乱,最后重构才明白状态机的价值。
1.3 单体架构还是微服务
很多同学一上来就问要不要用微服务,把Spring Cloud全家桶搬进来,Nacos、Gateway、OpenFeign全都上。我的建议非常明确:这种规模的校园项目,SpringBoot单体应用 + 模块化分包就是最优解。微服务带来的分布式事务、链路追踪、服务治理成本,在这个业务体量下完全没必要。你真正要做的是把包结构划分清楚,让代码具备可扩展性。
推荐的分包方式是按业务模块划分顶层包:
controller:接收 HTTP 请求,只做参数校验和结果封装,不写业务逻辑。service:业务逻辑层,所有状态变更、金额计算、事务控制写在这里。mapper:MyBatis 接口层,只负责数据库交互。entity:与数据库表对应的实体类。dto/vo:入参出参对象,避免直接暴露实体给前端。config:各类配置类,比如拦截器、跨域、支付回调、WebSocket。common:统一返回结果、异常处理、工具类、常量。
这样分层的好处是职责清晰,改动一个模块不会牵连其他模块。如果你的项目里还有定时任务(比如超时订单自动取消),建议单独建一个 task 包来放调度任务,不打乱整体结构。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与关键依赖
2.1 后端基础框架与版本选择
SpringBoot 版本选择是一个看起来不起眼、实际影响巨大的问题。我推荐的组合是 SpringBoot 2.7.x + JDK 8 + MyBatis-Plus 3.5.x,这个组合我用到现在踩坑最少。原因很简单:很多高校的课程实验环境和旧项目代码都基于 JDK 8,如果你选 SpringBoot 3.x,不仅强制要求 JDK 17,很多老版本的依赖(比如一些生成验证码、连接池的第三方库)还需要额外适配,排查起来非常痛苦。
另外,spring-boot-starter-web 和 spring-boot-starter-validation 是必备的。数据库连接池建议用 Druid,因为它自带监控页面,开发阶段看 SQL 执行情况很方便。在 pom.xml 里这样配:
xml复制<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.20</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
JWT 我用的是 jjwt 0.11.5 这个版本,注意它分为 api、impl、jackson 三个包,需要一起引入,否则运行时报 ClassNotFoundException。很多人在这里卡半天,其实只是少引了一个依赖。
2.2 分页插件与代码生成器
MyBatis-Plus 用起来确实提升效率,但注意分页插件必须显式配置,否则你调 selectPage 方法会发现分页完全不生效,它会把全部数据查出来再内存分页,数据量一大后端就卡死。我在 MybatisPlusConfig 里这样配置:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL);
pagination.setMaxLimit(100L);
interceptor.addInnerInterceptor(pagination);
return interceptor;
}
}
配置类写好之后,Service 里直接写:
java复制Page<OrderInfo> page = new Page<>(current, size);
LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderInfo::getStatus, 0).orderByDesc(OrderInfo::getCreateTime);
orderInfoMapper.selectPage(page, wrapper);
返回给前端的时候,page.getRecords() 是列表数据,page.getTotal() 是总记录数,代码量比手写 PageHelper 少很多。另外,我强烈建议你花半小时配置一下 MyBatis-Plus 的代码生成器,mybatis-plus-generator 配合 Velocity 模板,能从数据库表一键生成 entity、mapper、service、controller,虽然不是成品代码,但骨架能省不少时间。
2.3 前端框架与技术栈对比
前端我做过两版:一版是 Vue2 + Element UI,另一版是 Vue3 + Vite + Element Plus + Pinia。如果你的毕设要求不是必须兼容旧代码,直接上 Vue3 + Vite 是更长远的选择。Vue3 的 Composition API 配合 <script setup> 写业务逻辑非常顺手,尤其订单列表、接单大厅这种状态较多的页面,逻辑复用能力比旧版强很多。
开发环境用 Vite 而不是 Vue CLI,最大的感受就是“快”,冷启动基本上秒开,热更新也不会卡顿。但要注意 Vite 的 Node.js 版本要求,需要 16.0 以上,最好用 18 的 LTS 版本。前端代理跨域在 vite.config.js 里配置就好:
js复制export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
}
}
}
})
Element Plus 按需引入组件。很多人图省事直接在 main.js 里 app.use(ElementPlus) 全量引入,开发期没问题,但打包体积能大出一倍。Vite 项目用 unplugin-vue-components 和 unplugin-auto-import 这两个插件,配置之后组件和 API 都是按需的,能省不少流量。
2.4 第三方工具体系
校园跑腿项目的功能除了基本的 CRUD,还要考虑几个非功能性需求,我实测下来常用的第三方工具有这些:
- 短信验证码:阿里云短信服务,注册登录和更换手机号时会用到。接短信接口其实不复杂,主要花在申请签名和模板审核上,学生认证能免费申请几条。也可以为了演示方便,用本地图形验证码替代,很多毕设都是这样。
- 地图与距离计算:高德地图 JS API,用于取送地址的坐标标注和距离计算。跑腿费可以按“起步价 + 距离单价”来算,距离可以直接调高德的骑行路径规划 API,或者简单用“直线距离乘以系数”来估算,后者的稳定性更高,不受 API 配额限制。
- WebSocket 实时通知:订单状态变化时,跑腿员和用户需要即时感知。SpringBoot 集成 WebSocket 不算复杂,核心是写一个
WebSocketConfigurer注册拦截器,用 JWT 解析出用户身份后把 Session 存储到内存 Map,状态变化时按用户ID定向推送。前端用new WebSocket(url)接收消息,然后配合 Element Plus 的ElNotification弹通知。
3. 核心功能模块实现与实操
3.1 用户认证与权限控制
用户的注册、登录、角色识别是系统的地基。我的实现方案是 JWT + 拦截器,不走 Spring Security,原因是你只要能把认证逻辑完整写出来,在毕设答辩里已经是加分项了,Spring Security 配置繁琐且学习成本高,很容易把项目拖到失控状态。
逻辑是这样:用户登录成功之后,后端签发一个 JWT,subject 放用户ID,claim 里放角色标识,过期时间设为 24 小时。前端 axios 拦截器里从 localStorage 取 token,放到 Authorization 请求头。后端写一个 AuthInterceptor,排除掉登录、注册、接口文档等路径,其他所有 /api/** 请求先校验 token 再放行。
java复制public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
Claims claims = JwtUtil.parseToken(token);
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("role", claims.get("role"));
return true;
} catch (Exception e) {
// token过期或非法
}
}
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(Result.error(401, "未登录或登录已过期")));
return false;
}
}
还有一个必须处理的点:用户被封禁后 token 如何失效。简单的方案是登录时把 token 存一份到 Redis,拦截器每次请求都先查一次 Redis,管理员封禁用户时直接删掉对应 key,立刻生效。不用 Redis 的话,靠 token 过期时间,封禁操作最短也要等 token 过期才能阻止用户操作,这个体验是很糟糕的。
3.2 订单状态机与并发防抢
这是整个项目最值得拿出来讲的功能。抢单这个操作听起来简单,但实现时涉及并发问题:多个跑腿员同时看到同一个订单,同时点击“接单”,如果代码没有并发控制,就会出现多人接同一单的情况。
我第一次实现时只在 Service 里做了状态判断“如果订单状态是待接单,就更新为已接单”,这在单线程下没问题,高并发下必然出事。后来用了 UPDATE 语句带 WHERE 条件的方式做原子操作:
sql复制UPDATE order_info SET status = 1, runner_id = #{userId}, accept_time = NOW()
WHERE id = #{orderId} AND status = 0
MyBatis 里这条 UPDATE 返回受影响的行数,如果返回 1 说明抢单成功,0 说明已经被别人抢走。这是乐观锁思路的一种落地,不需要引入 Redis 就能解决大部分并发问题,实现成本最低、最稳定。如果你对并发要求更高,可以对订单ID加 synchronized 块,或者用 Redis 分布式锁 SETNX,但对校园跑腿这个业务场景,数据库原子的 UPDATE 已经足够。
订单状态我建议用 Integer 而不是 String 存储,后端用枚举定义常量:
java复制public enum OrderStatus {
WAITING(0, "待接单"),
ACCEPTED(1, "已接单"),
DELIVERING(2, "配送中"),
COMPLETED(3, "已完成"),
CANCELLED(4, "已取消"),
REFUNDING(5, "退款中");
private final Integer code;
private final String desc;
// getter...
}
状态机里我喜欢把所有可能的转移条件在某一个方法内集中处理。比如 cancelOrder(orderId, userId) 方法里,允许的状态转移包括“待接单 → 已取消”和“已接单 → 已取消(需扣除跑腿员违约金)”,其他状态一律抛异常。这样业务规则集中、清晰,前端的按钮显隐也可以直接从后端返回的当前状态来推导。
3.3 接单大厅的筛选与分页
接单大厅是跑腿员使用频率最高的页面,这里的核心难点是多条件组合查询。比如跑腿员想只看“从图书馆出发”或者“价格10元以上”的订单,前端把筛选条件以 query 参数传给后端,后端用 MyBatis-Plus 的 LambdaQueryWrapper 动态拼接查询条件:
java复制LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderInfo::getStatus, 0) // 只要待接单订单
.ge(OrderInfo::getReward, query.getMinReward()) // 小费不低于设定值
.like(StringUtils.hasText(query.getPickupPlace()), OrderInfo::getPickupPlace, query.getPickupPlace())
.orderByDesc(OrderInfo::getCreateTime);
前端还要做“刷新列表”的轮询机制。这里我的建议是 每 15 秒拉一次 而不是用 WebSocket 实时推送,因为新订单产生的频率没那么高,轮询足够,接口压力也小。感兴趣的话可以用 setTimeout + async/await 写个定时刷新函数,注意在组件卸载时清掉定时器,否则页面切成别的路由后请求还在发,浏览器控制台会报错,而且白白浪费带宽。
3.4 文件上传与用户头像
头像和跑腿证件的上传,不建议自己动手写磁盘存储逻辑,直接用本地磁盘存储 + 静态资源映射就够用了。SpringBoot 里设置一个 /upload/** 的静态资源映射到本地目录,例如 D:/upload/,然后上传接口里把文件保存成 UUID 文件名,返回文件访问 URL:
java复制@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty() || file.getSize() > 5 * 1024 * 1024) {
return Result.error("文件不能为空且大小不能超过5MB");
}
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString().replace("-", "") + suffix;
File saveFile = new File(UPLOAD_DIR, fileName);
file.transferTo(saveFile);
return Result.success("/upload/" + fileName);
}
图片验证码可以用开源的 kaptcha,把生成的验证码存入 Session,登录时比对。用户输入验证码时注意区分大小写的问题,我用的是忽略大小写的方案,实际体验会友好很多。前端头像上传用 Element Plus 的 el-upload,控制 action 指向后端的 /api/upload 接口,并在 headers 里带上 token。
4. 数据库设计:一张订单表是如何支撑整个业务的
4.1 核心表结构设计
这个项目的核心表其实不多,但字段设计得好不好,直接决定后续开发的顺不顺畅。我把表结构列出来,也是我最推荐参考的一套:
- user 表(用户表):id、username、password(加密后存储)、real_name、student_no(学号)、phone、avatar、role(0学生/1跑腿员/2管理员)、status(0正常/1封禁)、balance(余额)、create_time。
- order_info 表(订单表):id、order_no、publisher_id(下单人)、runner_id(接单人)、pickup_place、pickup_lat、pickup_lng、delivery_place、delivery_lat、delivery_lng、reward(小费金额)、status、remark、completion_code(完成验证码)、create_time、accept_time、finish_time。
- wallet_log 表(钱包流水表):id、user_id、change_amount(正负值)、balance_after、type(1充值/2下单扣款/3接单收入/4提现)、related_order_no、create_time。
- complaint 表(申诉与投诉表):id、order_id、complainant_id、defendant_id、reason、handle_status、handle_result、create_time。
- notice 表(公告表):id、title、content、create_time、publisher。
order_info 表里我特别加了 completion_code 字段,这是个非常实用的设计。跑腿员点击“标记送达”时,系统会生成一个 6 位数的取货码发给用户,用户见到跑腿员后把取货码报出来,跑腿员输入取货码才能完成订单。这个设计能有效防止“跑腿员虚假送达”或“用户不在场”的纠纷,系统安全性上一个台阶。我见过不少跑腿系统用“双方确认收货”的方式,但效率不如取货码高,推荐大家抄这个设计。
4.2 订单号生成与金额设计
订单号不要用数据库自增ID,太暴露业务量了。我推荐用时间戳 + 随机数生成:yyyyMMddHHmmss + 4位随机数,保证不重复的前提下,可读性也好。如果要处理更高的并发,可以用雪花算法(MyBatis-Plus 本身就集成了 IdWorker.getId()),生成的就是纯数字的分布式唯一ID。
金额字段必须用 BigDecimal,不要把金钱存成 double 或 float,否则会出现 0.1 + 0.2 = 0.30000000000000004 的精度问题,涉及提现和结算时影响更大。另外,数据库字段类型用 decimal(10, 2),Java实体类用 BigDecimal,前后端传递时用字符串或分单位的整数,避免 BigDecimal 被序列化成科学计数法显示。
4.3 索引设计:分页查询不卡的关键
订单表的数据量上来后,没有索引的分页查询会越来越慢。我实际优化时建了这几个复合索引:
- 接单大厅默认查询是
status = 0 ORDER BY create_time DESC,所以建索引(status, create_time)。 - 用户查看“我发布的订单”,查询条件是
publisher_id + create_time,建索引(publisher_id, create_time)。 - 跑腿员查看“我接的订单”,查询条件是
runner_id + status,建索引(runner_id, status)。
用 EXPLAIN 检查执行的 SQL 是否走索引,是很好的习惯。除此之外,分页深度大的时候(比如第 10000 条之后),可以用“延迟关联”的方式优化:
sql复制SELECT o.* FROM order_info o
INNER JOIN (SELECT id FROM order_info WHERE status = 0 ORDER BY create_time DESC LIMIT 10000, 10) t
ON o.id = t.id
这种写法先只查主键、跳过大量无用行,再回表查完整数据,实测比普通 LIMIT 快好几倍。
5. Vue 前端实现要点与交互细节
5.1 路由守卫与用户状态管理
前端用 Vue Router 做路由守卫,核心逻辑是未登录用户跳转到登录页,跑腿员和管理员访问越权页面时给出提示。这样路由级别的访问控制跟后端的接口权限拦截是双保险。
状态管理我用 Pinia(Vue3 推荐)来存用户信息、token、以及未读消息数。页面刷新后 Pinia 数据会丢失,所以初始化的时候要从 localStorage 读回来,或者在根组件 onMounted 里调一次 /api/user/info 重新获取用户信息。这个细节很多人忘记,实际刷新后页面会出现“效果还在但用户名变成空”的诡异情况,排查到怀疑人生。
5.2 表单防重复提交与校验
发布订单的提交按钮,我遇到过多次连点导致重复下单的问题。原因是用户点击“发布”后网络反馈慢,他又点了两三次,后端也没做幂等处理,结果产生了两个一模一样的订单。前端第一个要加 loading:
vue复制<el-button type="primary" :loading="submitting" @click="submitOrder">发布订单</el-button>
然后在 submitOrder 里加防重复判断:
js复制const submitting = ref(false)
async function submitOrder() {
if (submitting.value) return
submitting.value = true
try {
await api.createOrder(form.value)
ElMessage.success('发布成功')
} finally {
submitting.value = false
}
}
后端也要做兜底,可以在下单接口里检查“同一用户最近 30 秒是否已经发布了相同取送地址的订单”,发现重复就拒绝。这样前端加后端的双重校验,才能彻底避免重复数据。
表单校验规则用 rules 绑定,Element Plus 里做个必填校验和手机号格式校验即可。像用户收货地址的输入,建议把它做成“地址选择器”而不是自由文本,可以是三级联动(学校-校区-楼栋)加详细备注,这样既规范数据,也方便跑腿员快速定位。
5.3 实时通知的落地实现
WebSocket 前端需要封装成一个通用模块,连接保持和自动重连是主要难点。简单方案是封装一个 socket.js,模拟一个单例 WebSocket 连接,页面组件里通过事件监听接收消息:
js复制// socket.js
let socket = null
let listeners = {}
export function connectSocket(token) {
if (socket && socket.readyState === WebSocket.OPEN) return
socket = new WebSocket(`ws://localhost:8080/ws?token=${token}`)
socket.onmessage = (e) => {
const data = JSON.parse(e.data)
if (listeners[data.type]) listeners[data.type].forEach(fn => fn(data))
}
}
export function onMessage(type, callback) {
if (!listeners[type]) listeners[type] = []
listeners[type].push(callback)
}
需要注意 ws:// 协议在部署到 HTTPS 环境时需要换成 wss://,否则浏览器混用保护会拦截连接。另外,连接断开后要有重连机制,我是在 onerror 和 onclose 里做一个指数退避的重连,比如 1 秒后重试、3 秒后重试、10 秒后重试,最多重试 5 次,避免无限重连占用资源。
5.4 Vue 项目里的地图接入
如果要在 Web 端嵌入地图,高德地图 JS API 2.0 是最容易上手的方案。在 index.html 里引入:
html复制<script src="https://webapi.amap.com/maps?v=2.0&key=你的Key&plugin=AMap.PlaceSearch"></script>
在 Vue 组件里用 nextTick 确保 DOM 渲染完成后再初始化地图,然后监听点击事件取经纬度,再逆地理编码显示地址名称。如果地图组件需要在多个页面复用,可以封装成 MapPicker.vue 组件,props 接收初始坐标,emit 事件把选中的经纬度和地址返回给父组件。这个组件在发布订单和管理员展示订单位置时都会用到,封装一次长期复用,性价比很高。
6. 常见问题与排查技巧实录
6.1 MyBatis-Plus 分页不生效
症状:前端传了 pageNum 和 pageSize,但后端一次性返回全部数据。
这个 90% 的原因是 MybatisPlusInterceptor 没注册成功,或者注册了但配置类没有被 Spring 扫描到。还有一个容易忽略的点:如果代码里手动拼接了自定义 SQL,分页组件返回的其实是自定义 SQL 的全部结果,不会自动追加 LIMIT。这种情况需要把自定义 SQL 改造成 MP 的原生查询,或者在 mapper XML 里手动写分页参数。
6.2 跨域调用失败
症状:浏览器控制台报 CORS policy: No 'Access-Control-Allow-Origin' header。
排查思路分三步:确认后端是否在 WebMvcConfigurer 里正确配置了 allowedOrigins;确认前端开发环境的请求路径是否走了 Vite 代理;确认生产环境是否用了 Nginx 反向代理,如果用了,Nginx 的 proxy_set_header Host 和 Access-Control-Allow-Origin 也要对应配置。这里最容易出的问题是你配置了 allowedOrigins("*"),但实际请求里带了 Authorization 请求头,浏览器就会阻止。
6.3 JWT 登录后部分接口返回 401
症状:刷新页面后,部分请求失败,提示未登录;有些请求正常。
这个问题多数是前端 axios 实例混用导致的,有些请求用的实例是统一带 token 的,有些是后来新建的实例没配置请求拦截器。排查时在浏览器开发者工具里看一下失败请求的 Header,是否带上了 Authorization 字段。除此之外,多注意拦截器的 PathPattern 配置,/api/image/**、/api/login、/api/register 这些路径要排除在认证之外。
6.4 高并发抢单场景出现超卖
症状:订单剩余数量 1,但有两个跑腿员同时提示“接单成功”。
点击事件的响应顺序是前端先发起请求,后端再处理,如果你用的是“先 select 判断状态再 update”,并发必然出问题。解决方案就是我前面提到的原子 UPDATE。还有一个细节,接单成功后在内存里更新用户可见的“当前订单数”时要做乐观锁,否则统计数字也会乱。
6.5 提现功能的核心校验
如果你做了钱包提现功能,后端务必要校验以下三点:用户当前余额是否足够、单笔提现金额是否超过上限、用户是否实名认证过。这些验证缺一个都会出事。我实际做的时候,提现申请会生成一条 wallet_log 记录(类型为提现),状态为“审核中”,管理员在后台审核通过后才会真正扣除用户余额,这是非常典型的“先冻结再解冻”模式,比直接扣款要稳得多。
7. 上线部署与后续扩展建议
7.1 云服务器部署流程
项目开发完以后,别只停留在本地跑通,我强烈建议你至少在一台云服务器上完整部署一遍。我用的流程是:
- 后端用 Maven 打包成 jar,命令是
mvn clean package -DskipTests。 - 服务器装 JDK 8 和 MySQL 8.0,数据库文件用
mysqldump导出再导入。 - 用 Nginx 做反向代理,
/api开头的请求转发到localhost:8080,其他请求指向前端打包出的静态文件目录。 - 前端构建时执行
npm run build,生成dist目录,整个目录丢到 Nginx 的 html 目录下。
Nginx 核心配置片段:
nginx复制server {
listen 80;
server_name yourdomain.com;
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
部署期间最容易踩的坑是防火墙没放行 80 端口(云服务器安全组 + Linux 系统防火墙),以及数据库的 max_allowed_packet 太小导致导入 SQL 失败。
7.2 功能扩展:从“能跑”到“出彩”
如果你的毕设要求比较高,想在基础功能之外做亮点,我建议优先考虑这几个方向:
- 订单推荐算法:根据跑腿员的历史接单路线和常去区域,给跑腿员推荐顺路单。实现思路不复杂,可以按起止地点的距离 + 时间窗做简单过滤,看起来非常加分。
- 信用分体系:用户和跑腿员都有初始 100 分,接单后取消、超时、被投诉都会扣分,低分用户下单受限,低分跑腿员不能接高价单。这个逻辑不需要复杂机器学习,完全用规则引擎就能实现。
- 地图可视化:管理员后台用地图展示当天所有订单的分布和状态,这个借助高德地图的热力图插件,三十行代码就能做出来,但效果非常直观。
7.3 我的最后建议
做这类校园业务系统,最忌讳一开始就照搬网上的开源代码,尤其是 GitHub 上那种“精简速成版”项目。这些项目往往代码结构混乱、没有注释、没有异常处理,你拿过去改都无从下手。我更建议的方式是:先画业务流程图,再画数据库 ER 图,最后才写代码。业务跑通了,代码怎么写只是时间问题。反过来的话,边写边想业务流程,最后大概率会推翻重写。
还有一个容易被忽视的加分项:写好 README 和项目部署文档。答辩时老师通常会关注“这个项目能不能跑起来”,把一份详细的部署步骤写清楚,包括环境要求、数据库初始化脚本、账号密码列表,能让老师留下“这个学生做事认真”的第一印象。项目做完以后,把文档整理好、代码注释补齐、Git 提交记录写规范,这些细节的综合分,往往比功能本身多出来的几个模块更值钱。
