想在毕业设计这道坎上选一个“常见但又不太水”的题目,SpringBoot + Vue 的校园跑腿网站确实是个很稳的选项。它既不像纯管理系统那样只有增删改查,又不至于像分布式秒杀那样要压测、要消息队列、要 Redis 集群,难度刚好卡在一个本科生跳一跳够得着的位置。
这篇博文我按实际开发的顺序来讲,从需求拆解、数据库设计、后端接口、前端页面,到最后的部署和答辩准备,全部走一遍。适合正在做毕设、课设,或者想在小团队里快速搭出一套前后端分离项目的人参考。你要是只想要一个能过查重、能演示的完整系统,这篇内容可以直接照着落地。
先说结论:校园跑腿这个业务,核心就三句话——用户发单、骑手接单、跑完结算。把这三点做成闭环,系统就成立。剩下的全部是锦上添花。
1. 需求分析与技术选型:先想清楚再动手
很多人拿到题目就急着建工程,写了两天代码才发现订单状态不知道该设计成什么样,用户权限也乱成一团。这种问题根子不在编码,而在需求没有理清。
1.1 校园跑腿的“真需求”到底是什么
校园跑腿和美团、饿了么这类平台是有本质区别的。校内的配送半径小、用户群体固定、订单金额低,最典型的场景就是“宿舍楼下取快递”“食堂带饭”“超市代买”。因此系统设计不需要考虑复杂的运力调度,不需要算配送费,也不需要LBS实时派单,核心就是供需两侧的高效匹配。
我的用户角色拆分是三端:普通学生、跑腿骑手、系统管理员。普通学生要能发布订单、查看订单状态、取消订单、确认送达并评价;骑手要能浏览订单大厅、抢单、更新配送状态、查看自己的收入;管理员要能做用户审核、订单管理、公告管理、基础数据配置。
功能模块我最终收敛成这些:登录注册、个人中心、订单大厅、发布订单、接单/配送、评论、钱包充值、公告通知、后台管理。那些类似于“社交功能”“实时聊天”“路线规划”的,全部砍掉。不是不能做,而是做出来大概率会卡住进度,而且演示时根本不是评委关注的重点。
1.2 为什么选SpringBoot+Vue而不是其他组合
这个选型说实话有点“被生态推着走”的意思,但确实是目前国内Java后端项目里最稳的组合。
后端选SpringBoot,主要看中它的“约定大于配置”。以前用SSM框架写一个项目,XML配置文件能写到怀疑人生,SpringBoot通过自动配置把这些都简化了。再加上内置Tomcat,打一个Jar包就能跑,部署成本非常低。而且SpringBoot的生态太完整了,认证用JWT、持久层用MyBatis-Plus、文档用Swagger,几乎每踩一个坑都有现成方案。
前端选Vue,原因是它足够轻、文档亲民、社区资料多。Vue的响应式数据绑定和组件化开发让页面开发效率明显提升,Element UI / Element Plus一套组件库拿过来,后台界面的颜值立刻就有保障。相比React,Vue的上手曲线更平滑,遇到问题在中文搜索引擎里几乎都能找到答案。
这里要劝一句:除非你有特别强的理由,否则不建议在这个项目上引入微服务、分布式事务、消息队列这类重型组件。你是在做毕业设计,不是在做高并发的电商平台。技术栈应该为业务服务,而不是为了炫技把项目搞成四不像。
1.3 需要提前划清的边界
项目开始前,一定要把“能做”和“不做”写清楚,这是后期不烂尾的关键。
支付环节,我直接做成了“模拟支付”。也就是用户点击支付按钮后,系统默认支付成功并从余额中扣除,不接入真实微信/支付宝。原因很简单,个人开发者申请支付接口需要营业执照,而且真实支付涉及回调、证书、退款等一堆问题,不适合毕设场景。
定位环节,初期只用了校内热门地点的下拉选择,比如“1号宿舍楼”“图书馆”“东门菜鸟驿站”。如果要做地图选点,前端可以再集成腾讯地图或高德地图的JavaScript API,但这属于加分项而非必备项。
这些边界确定以后,项目一下子变得可落地了,至少不会在中期突然发现某个功能做不下去。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 架构设计与数据模型:先画图再写代码
我见过太多同学把数据库建得乱七八糟,用户表里放地址、订单表里放好评率,最后查询时全靠Java代码里各种循环赋值,效率极低。数据库设计是后端开发的骨架,这里省时间,后面一定加倍还回来。
2.1 前后端分离的工程结构与目录规划
我把项目分成了两个独立工程:
text复制school-runner-front
├── public
├── src
│ ├── api // 接口请求统一封装
│ ├── assets
│ ├── components // 公共组件
│ ├── router // 路由配置
│ ├── store // 状态管理(Vuex / Pinia)
│ ├── utils // 工具函数
│ ├── views // 页面组件
│ ├── App.vue
│ └── main.js
school-runner-server
├── src/main/java/com/example/runner
│ ├── config // 配置类(CORS、拦截器、MyBatisPlus)
│ ├── controller // 控制层
│ ├── service // 业务层
│ ├── mapper // 数据访问层
│ ├── entity // 实体类
│ ├── common // 统一返回体、异常处理、工具类
│ └── RunnerApplication.java
├── src/main/resources
│ ├── mapper // MyBatis XML文件
│ ├── application.yml
│ └── sql/init.sql
这个结构是当前Java后端项目最普遍的分层方式。控制层只接收参数和返回结果,业务逻辑全在Service层,数据操作在Mapper层。这样写的好处有两个:一是后期如果改了需求,只需要动对应层级的代码,不用满项目翻;二是答辩问“三层架构”的时候,你能讲得比书上的例子更实在。
2.2 核心数据表的设计与字段取舍
我的数据库一共有8张表,核心表如下:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | id, openid, username, password, role, phone, avatar, balance, status | 其中role区分学生/骑手/管理员 |
| orders | id, order_no, user_id, rider_id, type_id, start_point, end_point, amount, commission, remark, status, create_time | 订单主表,状态字段最核心 |
| order_type | id, name, price, unit | 跑腿类型,如普通快递、代买急需品 |
| wallet_log | id, user_id, change_amount, balance, type, remark | 余额变动流水 |
| comments | id, order_id, user_id, rider_id, content, score | 订单评价 |
| notice | id, title, content, create_time | 系统公告 |
订单表里我单独把order_no提出来,不光是为了好看,而是考虑到用户可以按订单号查询,管理员也需要一个不暴露自增主键的单号来沟通。status字段我用tinyint存储,注释说明含义,因为订单状态在Java代码里会有对应的枚举类,数据库里存数字效率更高也更好维护。
有一张表我差点忘了写,后来才发现很关键:wallet_log。用户余额变动必须留下流水,不然用户说“我充过钱怎么没了”时,你连查证的方式都没有。流水表只做增加,不做更新和删除,这种表在业内叫“流水账”,设计原则就是只追加、不修改。
2.3 统一返回体与接口规范
接口返回格式如果不统一,前端处理起来就是一场灾难。一会儿{success:true, data:{}},一会儿{code:200, result:{}},前端每个请求都要单独判断。所以第一步就是定死返回体。
我的统一返回结构如下:
java复制@Data
public class Result<T> {
private Integer code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
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;
}
}
顺便也把全局异常处理做了。后端最怕的就是未捕获异常抛给前端,前端拿到一串英文堆栈,用户一脸懵。用@RestControllerAdvice加@ExceptionHandler统一捕获,业务异常返回500 + 具体提示,参数校验异常返回400 + 字段错误信息,这样可以保证任何情况下前端拿到的都是规范JSON。
接口命名上也遵循RESTful风格,比如:
POST /api/user/register注册POST /api/user/login登录POST /api/order发布订单GET /api/order/page分页查询订单PUT /api/order/status更新订单状态POST /api/order/cancel取消订单
RESTful的好处是接口路径本身就能表达语义,评审老师看着也直观。
3. 后端核心模块落地:SpringBoot里的那些关键动作
后端是整个系统的心脏,我挑几个最值得展开的模块详细讲,这几个模块做完,整个后端基本就成型了。
3.1 项目初始化的版本坑:JDK1.8和SpringBoot 2.7.18是绝配
先说一个非常现实的问题:很多人用IDEA创建SpringBoot项目时发现Spring Initializr里默认版本已经是3.x了,而本机JDK还是1.8,项目创建直接失败。这个坑我自己踩过,折腾了一晚上才明白。
SpringBoot 3.x默认要求JDK 17及以上,同时javax包换成了jakarta包,很多老教程不能用。如果你是在校生,建议直接用JDK 1.8 + SpringBoot 2.7.18这个组合。为什么是2.7.18?因为2.7是SpringBoot 2.x的最后一个维护版本,社区补丁最全,兼容性也最好。对毕设项目来说,稳定压倒一切。
创建项目时如果IDEA里选不了2.7.18,可以先去https://start.spring.io或阿里云镜像站生成项目压缩包,再导入到IDEA中。或者手动修改pom.xml中的parent版本:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
核心依赖再放一份,都是后续要用的:
xml复制<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2 常用注解与分层编码的规矩
SpringBoot开发中注解是基础中的基础,我列几个高频的,也是答辩时容易被问到的:
@RestController:类上的组合注解,等于@Controller+@ResponseBody,所有方法返回JSON。@RequestMapping/@GetMapping/@PostMapping:映射HTTP请求路径。@Service:标记业务层组件,交给Spring管理。@Mapper:标记MyBatis的Mapper接口,让MyBatis扫描到代理实现。@Autowired:按类型注入依赖。@Validated/@Valid:参数校验,配合@NotNull、@Email等使用。@Transactional:事务注解,写订单、扣余额这类多表操作必须有。
以注册接口为例,我习惯的Controller写法是这样的:
java复制@RestController
@RequestMapping("/api/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/register")
public Result<String> register(@RequestBody @Valid RegisterDTO dto) {
userService.register(dto);
return Result.success("注册成功");
}
}
Service层是业务逻辑的核心,注册的完整逻辑是:先校验两次密码是否一致,再查用户名是否已存在,然后MD5加盐加密密码,最后插入用户表并赠送10元初始余额。这套逻辑放在Service层,Controller只做参数接收和结果返回。
3.3 登录认证:JWT与拦截器的组合拳
很多同学会优先想到用Session做登录,但前后端分离项目我不建议用Session。因为前端是独立部署的,SessionId存在Cookie里要处理跨域携带的问题,而且在移动端测试时还得额外处理Cookie的兼容性。JWT的优点是服务端无状态,Token由前端保存,每次请求放到请求头里,服务端只验签不存状态,对分布式部署也友好。
我的实现分三步。
第一步:登录成功后签发Token。用当前用户ID、角色、过期时间(我设24小时)生成JWT:
java复制String token = Jwts.builder()
.setSubject(String.valueOf(user.getId()))
.claim("role", user.getRole())
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000))
.signWith(secretKey)
.compact();
第二步:写一个拦截器,统一校验请求头里的Token。每次请求进来先判断路径是否在不需要认证的白名单里(比如登录、注册、首页轮播图),如果在就直接放行;不在就解析Token,解析失败就返回401 未登录。
第三步:拦截器注册到WebMvcConfigurer中,并配置拦截路径为/**,排除登录注册和静态资源路径。
这里有个细节,我在请求头中不只是传Token,还会把当前用户信息放进ThreadLocal,这样在Service层任何地方都能拿到当前用户ID,不用每个方法都传参。
3.4 订单状态机:跑腿业务最核心的设计
订单表里我已经强调了status字段,现在说状态流转。这个设计是跑腿系统最容易出逻辑漏洞的地方,很多人的实现是“前端点按钮,后端改状态”,结果用户取消已完成的订单也能成功,骑手接了单还能被另一个人再接一次。
我定义的状态流转如下:
text复制待支付(0) -> 待接单(1) -> 已接单(2) -> 配送中(3) -> 已完成(4)
\ \
\ -> 退款中(5) -> 已完成(6)
-> 已取消(7)
核心规则:
- 用户发布订单后先模拟支付,支付成功进入“待接单”。
- 待接单状态下,用户可取消;骑手可抢单。
- 已接单状态下,只能由该骑手操作,状态流转到“配送中”。
- 配送中状态下,用户点击“确认送达”则完成订单,或者骑手点击“已送达”后由用户确认。
- 退款逻辑必须校验是否处于“已接单之前”的状态。
实现上,我写了一个订单状态变更方法,统一做状态校验,避免到处散落状态判断:
java复制public void changeOrderStatus(Long orderId, Integer targetStatus, Long operatorId) {
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("订单不存在");
}
// 状态流转校验
if (!OrderStatusTransition.canTransition(order.getStatus(), targetStatus)) {
throw new BusinessException("当前状态不允许该操作");
}
// 角色校验
// 例如骑手接单,必须校验订单当前是待接单状态,且骑手未被封禁
order.setStatus(targetStatus);
orderMapper.updateById(order);
}
OrderStatusTransition是一个静态工具类,里面用Map维护“当前状态 -> 可流转到哪些状态”的映射,每次变更前查一下。这套设计既能防止前端绕过按钮直接调接口改状态,也方便答辩时讲“状态机”的设计思想。
顺带一提,有同学看到Flowable、Activiti这些工作流框架就套餐进去,说要用它管订单审核。这个项目真没必要,订单状态机用代码维护比工作流引擎轻得多,引入Flowable还得配流程定义文件,属于给自己挖坑。
3.5 MyBatis-Plus:分页查询和条件构造器真香
持久层我选的是MyBatis-Plus,它对单表CRUD的封装非常彻底,BaseMapper直接提供了insert、deleteById、selectById、updateById这些方法,省去了一大堆XML文件。两个高频使用场景展开说。
第一个是分页查询。订单大厅、用户列表、评论列表全是分页列表,用MyBatis-Plus的分页插件非常方便。
先配置分页插件:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
查询接口直接传分页参数:
java复制public Page<Order> pageOrders(Page<Order> page, OrderQueryDTO dto) {
LambdaQueryWrapper<Order> wrapper = Wrappers.lambdaQuery();
wrapper.eq(dto.getStatus() != null, Order::getStatus, dto.getStatus());
wrapper.like(StringUtils.hasText(dto.getKeyword()), Order::getRemark, dto.getKeyword());
wrapper.eq(dto.getUserId() != null, Order::getUserId, dto.getUserId());
wrapper.orderByDesc(Order::getCreateTime);
return orderMapper.selectPage(page, wrapper);
}
这段代码解释一下:eq的第一个参数是布尔条件,条件为真才拼SQL,前端不传状态就不加这个筛选条件,不会出现空参数报错。like用来做订单备注的模糊搜索,够用了。
第二个是逻辑删除。用户做“删除订单”其实不是物理删除,而是在表中加一个deleted字段,默认0,删除时更新为1。MyBatis-Plus的@TableLogic注解加上以后,所有查询会自动追加deleted=0条件,物理删除从业务上就杜绝了。
3.6 SpringBoot配置管理:多环境切换与敏感信息处理
配置这块虽然不起眼,但如果一开始就乱,后面部署上线绝对要返工。我的application.yml拆成了三个文件:
text复制application.yml // 公共配置
application-dev.yml // 开发环境
application-prod.yml // 生产环境
启动时通过spring.profiles.active=dev或prod选择环境。数据库、Redis、日志路径都按环境区分,避免在本地测试时不小心连到线上数据库。
敏感配置不要直接写进yml。比如阿里云短信密钥、微信AppSecret,我放在环境变量里,Java代码用@Value("${sms.app-id}")读取,环境变量在部署机器上配置。毕设可能没这么严格,但养成这个习惯总没错。
4. 前端实现:Vue项目的工程化细节
前端部分我用的是Vue 2 + Element UI组合。Vue 3和Element Plus当然更好,但如果你是按教程复刻,Vue 2的教程量最大、坑最少。我在这里按Vue 2讲,Vue 3的迁移逻辑是差不多的。
4.1 环境准备:Node、Vue CLI和npm依赖安装
前端开发环境需要先装Node.js,我建议装14.x或16.x的LTS版本。Vue CLI对环境版本有要求,Node版本太高反而可能出现Node Sass、webpack编译不兼容的问题。
创建项目用Vue CLI最省事:
bash复制npm install -g @vue/cli
vue create school-runner-front
如果你用Vite创建项目,语法差异不大,但部分老组件库可能要额外配置。
安装依赖失败是最常见的问题。npm install装到一半崩了,十有八九是网络问题,解决方案是切换国内镜像:
bash复制npm config set registry https://registry.npmmirror.com
如果报了node-sass相关的版本错误,多半是Node版本不匹配。Vue 2项目里干脆把sass和sass-loader换成dart-sass实现,再不行就锁定Node 14。
安装完成后建议装上Vue Devtools浏览器插件,调试组件状态、路由跳转、Vuex数据一变就能看到,省太多时间了。
4.2 前端目录结构与核心依赖
在前端工程里,我习惯这样组织:
text复制src
├── api
│ ├── modules
│ │ ├── user.js
│ │ └── order.js
│ └── request.js
├── router
│ └── index.js
├── store
│ ├── modules
│ │ ├── user.js
│ │ └── order.js
│ └── index.js
├── views
│ ├── Home.vue
│ ├── OrderHall.vue
│ ├── PublishOrder.vue
│ ├── OrderDetail.vue
│ ├── MyOrders.vue
│ ├── UserCenter.vue
│ └── admin
│ ├── UserManage.vue
│ └── OrderManage.vue
├── App.vue
└── main.js
核心依赖是:
vue-router:路由管理vuex:状态管理axios:HTTP请求element-ui:UI组件库dayjs:时间格式化
4.3 路由设计:路由参数传递与守卫控制
路由是前端项目的骨架。我的路由分为普通用户和管理员两块,通过路由meta字段标记权限。
用户下单和骑手接单的流程都依赖路由参数。比如从订单大厅点击一条订单后,需要跳转到详情页并带上订单ID,典型用法是这样:
js复制this.$router.push({
name: 'OrderDetail',
params: { orderId: row.id }
})
对应路由配置:
js复制{
path: '/order/detail/:orderId',
name: 'OrderDetail',
component: () => import('@/views/OrderDetail.vue'),
meta: { requiresAuth: true }
}
在组件里取出参数:
js复制this.orderId = this.$route.params.orderId;
这里有一个很经典的坑:params传参在页面刷新后会丢失,而query传参(?orderId=123)刷新后依然存在。所以关键业务ID,我建议统一用query方式传,或者把ID放到store中持久化。我最终是改成query方式,稳妥很多。
路由守卫是权限控制的实现位置。每个路由都配置了meta.requiresAuth,全局守卫里做判断:
js复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) {
next('/login');
return;
}
if (to.meta.role && to.meta.role !== store.state.user.role) {
next('/403');
return;
}
next();
});
这样用户未登录时点任何需要登录的页面都会自动跳到登录页,管理员页面对普通用户直接禁止访问。
4.4 Axios请求封装:统一Token注入和错误处理
前端所有请求我都走request.js,统一封装axios实例:
js复制import axios from 'axios';
import { Message } from 'element-ui';
import router from '@/router';
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 15000
});
// 请求拦截器:自动携带token
service.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
return config;
});
// 响应拦截器:统一处理错误
service.interceptors.response.use(
response => {
const res = response.data;
if (res.code !== 200) {
Message.error(res.msg || '请求失败');
if (res.code === 401) {
router.push('/login');
}
return Promise.reject(new Error(res.msg));
}
return res.data;
},
error => {
Message.error('网络异常,请稍后重试');
return Promise.reject(error);
}
);
这种封装的好处是:登录状态失效时全站自动跳登录页,接口报错时统一弹提示,不用每个页面单独处理error。
4.5 核心页面实现:发布订单、订单大厅、确认送达
挑三个典型页面的实现逻辑讲。
发布订单页面是Vue双向绑定的典型场景。表单字段包括跑腿类型、取件地点、送达地点、备注、悬赏金额。这些字段都通过v-model绑定到表单对象上:
html复制<el-form :model="orderForm" ref="orderForm" label-width="80px">
<el-form-item label="跑腿类型" prop="typeId">
<el-select v-model="orderForm.typeId">
<el-option v-for="item in typeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="取件地点">
<el-input v-model="orderForm.startPoint" placeholder="例如:东门菜鸟驿站" />
</el-form-item>
<el-form-item label="送达地点">
<el-input v-model="orderForm.endPoint" placeholder="例如:3号宿舍楼318" />
</el-form-item>
<el-form-item label="备注">
<el-input type="textarea" v-model="orderForm.remark" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitOrder">发布订单</el-button>
</el-form-item>
</el-form>
提交前做一次二次确认弹窗,提示用户将支付酬金,确认后调用后端接口。这个交互虽然简单,但能省下很多测试时误操作的麻烦。
订单大厅是骑手端的核心页面。它本质是一个分页列表加筛选条件,用el-table渲染,每一行放一个“去接单”按钮。点击按钮时调用接单接口,成功后刷新列表。这个页面的重点是数据刷新策略,我用了“手动刷新 + 定时轮询5秒”,让骑手能看到最新单子,又不至于频繁请求后端。
确认送达这个操作,我放在订单详情页。页面从后端加载订单详情,根据当前订单状态动态渲染“当前状态卡片”和“可执行操作按钮”。比如状态为配送中时,用户端显示的是“确认送达”,骑手端显示的是“申请送达”。这种由状态驱动UI的方式,和后面的状态机设计一脉相承,逻辑很清晰。
4.6 顺手的扩展点:自定义v-model、地图定位与实用工具
有前端面试机会的同学,可以在项目里埋几个“亮点”讲给面试官听。我加了一个比较有意思的组件:自定义一个v-model间接实现“分类多选标签选择器”。平时用v-model是Vue内置的,但自定义组件也能实现,原理就是接收value属性,再通过$emit('input', newVal)回传更新。我当时封装了一个选择跑腿标签的组件,父组件直接v-model绑定,子组件内部管理展开状态和选项逻辑:
js复制export default {
props: {
value: {
type: Array,
default: () => []
}
},
methods: {
handleSelect(item) {
const newVal = this.value.includes(item.id)
? this.value.filter(id => id !== item.id)
: [...this.value, item.id];
this.$emit('input', newVal);
}
}
};
能在答辩或者面试时把自定义v-model的原理讲明白,是加分项,因为很多人写了一年v-model都不知道它底层其实是value + input事件。
地图定位这个扩展,我实现了一个简化版:集成腾讯地图JavaScript API,用户在发布订单时可以直接在地图上点击选点,逆地址解析出地点名称,存入订单的start_point字段。API Key申请本身是免费的,前端在index.html里引入SDK,然后在地图组件里创建Map实例并绑定点击事件。这一块可以作为“系统亮点”写在论文里,但我建议放最后做,不影响主线功能。
5. 联调、部署与踩坑实录
整个项目开发完以后,真正磨人的是联调和部署阶段的那些零碎问题。这里我把最常遇到的坑集中整理出来,每个都是我自己踩过的。
5.1 前后端联调:跨域问题从根上解决
前端页面通过axios请求后端接口时,第一道坎就是跨域。前端跑在http://localhost:8080,后端跑在http://localhost:8081,浏览器默认会拦截跨域请求。解决方案有两个方向。
后端方式:在配置类里加CORS映射:
java复制@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);
}
}
注意allowedOriginPatterns和allowCredentials(true)要配合使用,如果用allowedOrigins("*")同时allowCredentials(true),部分浏览器会直接报错。
前端方式:通过vue.config.js里的devServer代理转发:
js复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true
}
}
}
};
联调阶段我更推荐用前端代理方案,这样可以模拟生产环境“同域名下访问接口”的场景,避免在开发阶段就依赖后端的CORS配置。后面上线时,再用Nginx把/api转发到SpringBoot服务。
5.2 常见问题排查:版本、缓存、请求异常
这次开发过程中我整理了一个高频问题速查表,照着查基本能解决80%的问题:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
项目启动报Invalid bound statement |
Mapper接口与XML映射文件不匹配 | 检查namespace和statement id,确认resources下XML编译到了classes目录 |
| 前端图标不显示/F12报字体404 | 字体文件路径写死 | 注意public目录和src目录的资源引用方式,使用相对路径或import方式 |
| axios请求后台进不去 | 拦截器拦截了OPTIONS预检请求 | 后端CORS配置放行OPTIONS请求,或拦截器白名单加上OPTIONS |
| 数据库时间差8小时 | 时区配置问题 | JDBC连接串加serverTimezone=Asia/Shanghai,Jackson配置统一时区 |
接口报406 Not Acceptable |
消息转换器无法处理返回类型 | 确认引入了Jackson依赖,Controller返回对象而非String |
| 订单列表数据重复 | 分页插件拦截器没配置 | MyBatis-Plus分页必须显式添加PaginationInnerInterceptor |
还有一个非常容易忽略的点:IDEA里改了application.yml不重启导致配置不生效。SpringBoot在开发环境不会自动热更新配置文件,很多同学明明改对了端口,页面还在访问旧的端口,排查半天。
5.3 打包与部署:从本地到服务器全流程
部署这部分我按“可用、能演示”为标准,流程如下。
后端打包:
bash复制mvn clean package -DskipTests
打出来的Jar在target目录下,一般有几十MB。启动命令:
bash复制nohup java -jar school-runner-server.jar --spring.profiles.active=prod > app.log 2>&1 &
生产环境的数据库配置在application-prod.yml里指向服务器上的MySQL。数据库初始化时直接执行项目里的init.sql,建库建表。
前端打包:
bash复制npm run build
生成dist目录,里面是纯静态文件。我用Nginx做静态文件服务和反向代理,关键配置如下:
nginx复制server {
listen 80;
server_name your.domain.com;
# 前端静态文件
root /usr/share/nginx/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# 后端接口反向代理
location /api/ {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
try_files $uri $uri/ /index.html;这行特别重要。Vue Router默认是History模式,如果直接刷新/order/detail/1这个路径,Nginx会去磁盘上找对应的文件然后报404,加了这行它就会回退到index.html,由前端路由接管。
没有云服务器的同学,也可以直接把这套东西跑在本地虚拟机里演示。只要保证数据库、后端、前端三个服务都活着,页面能打开,演示场景能走通,效果上是一样的。
5.4 演示脚本与答辩准备
毕设答辩现场最容易翻车的不是“你没做”,而是“你做了但演示时找不到入口”。我强烈建议准备一份演示脚本,至少完整跑一遍这两个场景。
场景一:普通学生用户注册登录 -> 发布一个“代取快递”订单 -> 模拟支付 -> 在“我的订单”里跟踪订单状态 -> 骑手接单后确认送达 -> 发布评价。
场景二:骑手用户登录 -> 进入订单大厅 -> 筛选待接单 -> 抢单 -> 更新状态为配送中 -> 用户确认送达 -> 查看钱包余额增加。
答辩时评委常问的问题也提前准备起来:为什么用JWT不用Session?订单状态为什么用数字不用字符串?数据库为什么这么设计?前端怎么控制页面权限?这些问题,这篇文章前面其实都已经覆盖了。关键是你要说得出来,最好能引申一句“我最初想过用X方案,但后来发现Y问题,所以改成现在的方案”,这种对比表述在答辩中非常加分。
6. 做完这个系统后,我最后悔的三件事
项目收尾阶段复盘,有几个地方如果能重来一遍,我会做得更好。
第一件,是我没有在第一天就搭好统一的接口规范。前一周各个接口返回结构是各写各的,联调时前端同事找了我三回,说这个接口返回data是数组,那个接口返回data是对象,统一收口浪费了不少时间。这个项目你第一个就要做统一返回体。
第二件,是消息通知能力缺失。骑手接单后,用户端没有明确的通知。我目前的方案是订单列表定时刷新,用户隔一会儿自己刷新看状态,体验上很粗糙。如果当时引入WebSocket,订单状态变化时后端主动推送消息给用户,系统完整度会明显上一个台阶。思路不复杂,spring-boot-starter-websocket加上去,在订单状态更新的地方广播一条消息就行。
第三件,是搜索功能太弱。需求里我用了remark like做模糊搜索,用户想按“快递类型”“价格区间”筛选就不行了。如果做进阶版,可以在订单大厅增加多条件组合搜索,甚至引入全文索引。搜索是技术上没有难度但能极大提升使用体验的部分。
再往大了说,这个项目后续可以扩展的方向其实不少:跑腿员的信用分体系、订单的定时取消策略、用户举报处理、系统的数据统计看板,这些都是“说得清楚、能落地、有亮点”的演进方向。如果你准备拿这个项目找工作,挑一个方向深入做透,就完全能写进简历项目里了。
我自己做完这个项目的最大体会是:一个看起来“普通”的课设题目,认真拆解后也能学到很多教科书之外的东西。状态机怎么设计、跨域怎么处理、部署时Nginx怎么配,这些不是背概念能学会的,必须自己一台机器一个端口踩过去。校园跑腿网站不算什么惊艳的作品,但它把业务流程、技术选型、工程规范、部署运维这些环节完整串了起来,对一个刚入行的开发者来说,这就够了。
