每年到了毕业设计季,总能看到大量同学在“电动车租赁系统”、“共享单车系统”、“校园二手交易平台”这类题目之间反复横跳。这题目看着平淡无奇,但真上手去做,从技术选型、数据库设计到联调部署,每一步都藏着不少门道。这篇博文我就以“高校电动车租赁系统平台”为例,把整个项目的设计思路、核心代码实现、数据库建模、部署流程从头到尾过一遍,顺带把那些我在实操中踩过的坑和总结出的经验一并交代清楚,希望能给正在做同类课题的同学们一些参考,也想让刚接触SpringBoot和Vue全栈开发的朋友知道,一个完整的毕业设计项目到底是怎么从零到一落地的。
这个项目我用的是SpringBoot + Vue + MySQL的组合,这三个词放到今天的就业市场里,属于最务实的一套Java全栈解决方案。SpringBoot负责后端接口和业务逻辑,Vue负责前端页面和交互,MySQL负责数据持久化。系统本身要解决的场景也很明确:高校校园内师生有短途出行需求,校方或运营方投放一批电动车供租赁,学生通过小程序或者网页完成注册、扫码租车、计费、还车、支付这一整条流程。比共享单车多了个校园封闭场景,比传统租车行多了个线上自助化的要求,规模不大但五脏俱全,非常适合拿来练手和当毕设题目。
你可能会问,这种项目网上源码一大把,为什么还要认真做?我的看法是:毕设和实战项目的区别在于,毕设不仅要求功能跑得通,还要求你能把技术点讲清楚,让答辩老师觉得你的系统是经过思考设计的,而不是盲目抄来的。所以后面我讲的可复制操作,不只是贴代码,也会把为什么这么设计的逻辑一并拆开。
1. 为什么这个题目适合当毕设,技术栈又是怎么定的
1.1 选题价值和需求场景分析
高校电动车租赁这个题目的优势,在于它的业务边界非常清晰。需求方是校园内的学生和教职工,车辆投放范围是校园及周边,租赁时长普遍是几十分钟到半天,使用场景是上课、取快递、去食堂、跨校区通勤。相比一个泛泛的“商品租赁系统”,它多了一层校园管理的属性,比如学生身份认证、车辆状态实时监控、乱停乱放的管理约束。
从毕设评分角度来看,这类题目在功能完整度、技术难度、论文可写性三方面比较均衡:功能上能拆出用户端、管理端、车辆端三个视角,技术上能覆盖前后端分离、权限认证、地理位置、支付流程、报表统计,论文上每一块都有话可说,不至于憋不出字数。
我当时拿到这个题目后,第一步不是写代码,而是花了整整两天做用户角色分析。我把系统的使用人群拆成了三类:学生用户、系统管理员、车辆运维人员。学生用户关心的是“附近有没有车、多少钱、怎么还车”,系统管理员关心的是“哪辆车被租了、订单多少钱、什么时间段是高峰”,运维人员关心的是“哪辆车电量低、哪辆车报修了、需要调度到哪里”。这三种角色对系统功能的需求差异很大,能同时覆盖这三类人,系统的功能面就已经像样了。
1.2 SpringBoot、Vue、MySQL三者是怎么分工配合的
很多同学会纠结“SpringBoot和SSM到底选哪个”,我的建议非常直接:没有特殊要求,就用SpringBoot。SpringBoot的自动配置机制把SpringMVC、MyBatis等框架的繁琐配置大量收敛了,你只需要通过@SpringBootApplication启动一个应用,通过application.yml维护数据源和端口,剩下的交给框架约定。这对毕设阶段的学生来说友好得多,可以省下时间集中处理业务逻辑。
Vue端我用的是Vue 2 + Element UI的组合,这是目前新手最不陌生的方案。组件化开发让页面可以拆成车辆列表、订单卡片、支付弹窗等独立单元,视图层逻辑清晰,配合Vue Router做页面跳转、Vuex管理登录态和全局数据,写出来的代码不会因为项目小就变得凌乱。
MySQL在其中承担的核心任务是事务性数据存储。订单、支付流水、车辆状态变更这些操作为什么必须用MySQL而不是直接把数据放内存里?因为租赁业务涉及金额结算,一旦订单状态和支付状态不一致,后续对账就是灾难。MySQL的ACID特性配合SpringBoot里的事务注解@Transactional,能保证“创建订单扣减余额”这类组合操作要么全部成功,要么全部回滚。表结构上我后面会详细画出来,这里先有个整体认知就可以。
2. 项目整体功能设计与数据库建模
2.1 系统模块划分:用户端到底要做什么,管理端又要管理什么
高校电动车租赁系统的功能设计,我倾向于分为前台(用户端)和后台(管理端)两大块。前台的核心流程是:用户注册登录 → 浏览车辆列表 → 查看车辆详情(电量、时租金) → 选择车辆并下单 → 模拟开锁/还车 → 支付费用 → 查看历史订单。
管理端则是对前台业务的治理:车辆信息管理(新增车辆、上下架、设定电量阈值)、订单管理(查询所有订单、处理异常订单、手动结算)、用户管理(学生认证、禁用账号)、计费规则配置(设置不同车型的时租金、设置押金金额)、数据统计看板(日订单量、收入趋势、车辆使用率排行榜)。
如果想让系统看起来更有论文价值,可以再加一个运维模块,处理车辆的故障上报和维护记录。我自己的项目里加了车辆维护记录这张表,本来是想凑字段,结果答辩时老师专门夸了这点,说能考虑到车辆全生命周期的状态管理,说明不是单纯做了一个交易网站。
2.2 数据库表结构设计:五张核心表与字段定义
数据库设计是这个项目里最值得讲的一部分。我建了这些表:用户表(user)、车辆表(vehicle)、订单表(rental_order)、支付流水表(payment_record)、计费规则表(charge_rule)、维护记录表(maintenance_record)。下面挑核心的几张表讲。
用户表字段:id、username、password(加密存储)、real_name、student_no、role(1-用户 2-管理员 3-运维)、phone、balance(账户余额)、status、create_time。密码用BCrypt加密保存,禁止明文,这是很多毕设容易丢分的地方。balance字段用来做预充值消费,租赁结束后从余额扣款,比每次单独调支付接口更适合校园场景。
车辆表字段:id、vehicle_no、name、type(车型)、battery(电量百分比)、status(0-空闲 1-使用中 2-维护中 3-已下架)、longitude、latitude、price_per_hour、park_location、create_time。其中status字段是整个系统状态流转的核心,所有业务操作都在围绕着这个字段的变更展开。
订单表字段:id、order_no(业务单号)、user_id、vehicle_id、start_time、end_time、duration_minutes、total_amount、status(0-进行中 1-已完成 2-已取消 3-异常)、pay_status。订单表的设计要注意索引,user_id和status经常作为查询条件,必须加普通索引,否则数据量一上来查询就会变慢。
支付流水表字段:id、order_id、user_id、amount、pay_type(余额/微信)、trade_no、status、create_time。这张表的意义在于留痕。学生论文里写“实现了支付功能”的时候,如果只有订单表里的pay_status字段,不好证明资金流转的完整性。单独拆一张流水表,既有技术价值,也有审计价值。
在定字段类型的时候,有两点特别提醒:金额一律用DECIMAL(10, 2),不要用FLOAT或DOUBLE,因为浮点数在数据库里是近似值,金额算错了很难查。时间字段用DATETIME,别用TIMESTAMP,否则2038年问题且带时区处理容易出幺蛾子。车辆位置字段如果追求精度可以用POINT类型配合空间索引,但毕设阶段用DECIMAL(9, 6)存经纬度完全够用。
3. 后端核心业务实现与关键代码解析
3.1 项目结构划分与统一返回体设计
后端我用的包结构是标准的controller / service / mapper / entity / common五层。entity对应数据库表实体,mapper是MyBatis-Plus的数据访问层,service写业务逻辑,controller只做参数接收和结果返回,common放全局异常处理、统一返回结果、JWT工具类。很多同学喜欢把逻辑直接写在controller里,图省事,但答辩的时候老师一问“你这个模块的复用性怎么样”就露馅了。
统一返回体我在项目一开始就定义好了,这也是SpringBoot项目的常规操作:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.code = 500; result.message = message; return result; } }有了这个统一返回体,前端在封装Axios的时候就可以统一处理状态码,不需要对每个接口单独做判断。我见过一些项目,每个接口返回的字段名都不一样,有的返回data,有的返回result,前端联调的时候那叫一个痛苦。从设计规范上讲,统一返回体是团队协作的基本功,放在毕设里也很加印象分。
3.2 基于JWT的登录认证与权限控制
高校电动车租赁系统涉及三种角色,接口必须有权限控制。我用的方案是JWT + SpringBoot拦截器。用户登录成功后,后端签发一个有效期2小时的Token,包含userId和role信息,前端拿到Token存到localStorage里,每次请求在请求头里带上Authorization: Bearer <token>。
后端的拦截器里只需要校验Token是否有效、当前用户是否有权访问对应接口。核心代码如下:
@Component public class JwtInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/user/login") || request.getRequestURI().contains("/user/register")) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token) || !token.startsWith("Bearer ")) { throw new RuntimeException("未登录或登录已过期"); } String userInfo = jwtUtil.parseToken(token.replace("Bearer ", "")); if (userInfo == null) { throw new RuntimeException("Token无效"); } // 将用户信息存入request,方便后续业务获取当前用户 request.setAttribute("userId", userInfo); return true; } }这里有一个比较关键的细节:Token里放了userId之后,业务代码里千万不要再去查询用户是否存在。因为用户可能被管理员删除了,但Token还没过期,这种情况应该直接拦截。我是通过Redis或者数据库查一次来校验的,但很多项目在校验上偷懒,导致接口能操作一个不存在的用户下的数据,后期排查很费劲。
JWT的好处是服务端无状态,多台服务器部署时不需要共享Session存储;缺点是Token无法主动失效。毕设场景下这个缺点不明显,但我在项目里还是加了“用户修改密码后强制重新登录”的逻辑,做法是登录时记录一个token_version,JWT里带上,拦截器比较版本号,不一致就拒绝。
3.3 租赁核心流程:下单、计费、还车的链路设计
租赁业务的核心链路是:用户选择空闲车辆 → 创建订单(订单状态置为进行中,车辆状态置为使用中) → 用户点击“还车结算” → 计算时长与金额 → 扣减余额 → 更新订单状态与车辆状态。
下单接口的实现我用了事务控制,因为“创建订单”和“修改车辆状态”必须同时成功或同时失败,否则会出现车被租走但订单没建成的严重问题:
@Transactional(rollbackFor = Exception.class) public RentalOrder createOrder(Integer userId, Integer vehicleId) { // 1. 查询车辆,判断状态 Vehicle vehicle = vehicleMapper.selectById(vehicleId); if (vehicle == null || vehicle.getStatus() != 0) { throw new RuntimeException("该车辆不可租赁"); } // 2. 生成唯一订单号 String orderNo = "R" + System.currentTimeMillis() + RandomUtil.randomNumbers(4); // 3. 创建订单 RentalOrder order = new RentalOrder(); order.setOrderNo(orderNo); order.setUserId(userId); order.setVehicleId(vehicleId); order.setStartTime(LocalDateTime.now()); order.setStatus(0); order.setPayStatus(0); rentalOrderMapper.insert(order); // 4. 修改车辆状态为使用中 vehicle.setStatus(1); vehicleMapper.updateById(vehicle); return order; }计费逻辑是另一个容易出错的点。计费规则要支持“按小时计价”和“按分钟计价”两种模式,而且不足一小时的场景很多。我的设计是:订单创建时活取车型的price_per_hour,还车时把分钟数除以60得到小时数,不足15分钟的部分按15分钟计算,向上取整。用代码表示就是Math.max(1, (int)Math.ceil(durationMinutes / 15.0)) * (pricePerHour / 4),这样半小时就是两格,20分钟也是两格,不会出现比实际便宜的情况。
如果你在论文里写“支持押金功能”,建议同时实现“免押金学生认证”流程。就是在用户信息里加一个is_verified字段,认证过校园卡的用户可以免押金租车,没有认证的需要冻结一定金额。这里的冻结金额不是真的扣钱,而是在下单时锁定余额,还车后再解冻,我用了一张deposit_record表来追踪,比直接把balance减掉再还回来更清晰。
3.4 MyBatis-Plus的用法和那些提升开发效率的配置
数据层的技术选型我用了MyBatis-Plus而不是原生MyBatis,最大原因是开发效率高。单表查询完全不用写SQL,通过QueryWrapper就能搞定,分页插件也是开箱即用:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInnerInterceptor.setMaxLimit(500L); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }分页查询是管理端列表页的标配需求,顺序不能乱。先配置拦截器,然后在Service里用Page<T>对象接收参数:
public PageResult<OrderVO> getOrderPage(OrderQuery query) { Page<RentalOrder> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<RentalOrder> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.isNotBlank(query.getStatus()), RentalOrder::getStatus, query.getStatus()) .orderByDesc(RentalOrder::getCreateTime); rentalOrderMapper.selectPage(page, wrapper); // 类型转换:将实体转成VO,补充用户名、车辆名称等信息 return buildPageResult(page); }这里有个从实战中总结的教训:分页查询返回的数据,尽量不要把实体类直接返回给前端。比如订单列表里,前端需要看到的是用户姓名而不是user_id,需要看到车辆品牌型号而不是vehicle_id。就为这个需求,我专门定义了VO类,查询完后再做一次字段映射。可能有人觉得多此一举,但是当你把系统做完回头改需求的时候,就知道VO层和实体层分离有多重要了。否则前端每要一个字段,你就得改动数据库表结构或者实体类,这是很糟糕的维护体验。
4. 前端Vue实现细节与接口联调要点
4.1 前端工程化结构与路由设计
前端我用Vue CLI脚手架生成的项目,主要目录分成views(页面)、components(公共组件)、router(路由)、store(Vuex)、api(接口请求)。为什么要把接口请求单独建一个目录?因为在实际项目中,同一个接口会被多个页面调用,如果每个页面都写一次axios请求,万一后端接口变了,你就要全局搜索替换。把所有请求集中到一个对于管理是很好的维护习惯。
路由设计上我用了路由懒加载,通过component: () => import('...')的方式引入页面组件,这样首屏加载时不会把所有页面都打进同一个bundle里,页面打开速度会快不少。同时配置了全局前置守卫做登录校验:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else if (token && to.path === '/login') { next('/') } else { next() } })如果你用了Vuex管理用户信息,建议在路由守卫里同时检查用户信息是否已加载,避免刷新页面后Vuex里userInfo丢失导致页面数据显示异常。这是我做联调时碰到的经典问题:登录后一切正常,一刷新页面就跳回登录页,排查了好一会儿才发现是Vuex的 store 在刷新后会被重置,需要配合localStorage持久化或者重新调获取用户信息接口。
4.2 Axios封装与跨域问题处理
前端请求后端接口,第一个拦路虎就是跨域。开发环境下我用的是Vue CLI的代理方案,在vue.config.js里配置:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }后端接口地址统一以/api开头,代理到后端服务时去掉前缀,这样前端代码里请求的都是相对路径,不写死IP和端口。为什么这套能解决问题?因为跨域限制是浏览器的行为,开发环境下请求先发到Vite或者webpackDevServer同源地址,再由服务端转发,浏览器感知不到。
axios封装方面,我在request拦截器里统一放Token,在response拦截器里统一处理错误码。后端返回的code如果不是200,就直接弹出错误提示;如果是401或Token过期,就清空登录状态并跳转到登录页。这套机制能让你在联调阶段节省大量时间。
4.3 Element UI表单校验与页面组件化实践
管理端的车辆管理页面是我觉得最值得讲的,它涵盖了表格展示、新增修改弹窗、表单校验、删除确认这一整套标准交互流程。Element UI的表格组件通过el-table绑定数据,配合el-pagination做分页。新增和修改共用同一个弹窗组件,通过判断是否有id来决定调用的接口。表单校验规则配置在data里,比如车辆编号必填、价格必须大于0,提交前调this.$refs.form.validate()。
用户端的租车页面则需要多一点设计,比如用卡片展示车辆状态:绿色代表空闲、灰色代表维护中、黄色代表使用中。电量展示用了el-progress组件,圆形进度条直观呈现剩余电量。在选择车辆时,我用了一个地图插件的简化版,在校园地图上标注车辆位置,这样用户看起来比纯列表更有体验感。
前端页面里我特别重视的一件事是“空状态展示”。列表没有数据时,展示一张空状态的插图和提示文字,而不是干巴巴的空白页。这个细节虽然简单,但在答辩演示时很讨喜,老师会觉得你考虑问题全面。
5. 本地启动、项目打包与服务器部署全流程
5.1 本地环境准备:JDK、Node、MySQL一个都不能少
在开始写代码之前,环境必须一次配好,不然后面处处踩坑。我建议的阵容是:JDK 1.8(SpringBoot 2.x兼容性最好,别一上来就整JDK 17)、Node 14或16、MySQL 5.7或8.0。这里特别说明:SpringBoot 2.6.x对应Java 8没问题,但如果你用SpringBoot 3.x就必须上Java 17,很多老教程根本不适用,所以版本匹配一定要留心。
MySQL安装后要检查默认字符集,项目初始化时我执行了这样一条命令:
CREATE DATABASE IF NOT EXISTS ev_rental DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;为什么要强调utf8mb4?因为utf8在MySQL里存不了emoji表情和一些生僻字,大学生用户昵称里表情符号出现概率极高,不用utf8mb4很容易在写入时报"Incorrect string value"错误。
5.2 初始化数据库脚本与后端配置文件的连接细节
数据库初始化我准备了一个init.sql,包含建表语句和基础数据(管理员账号、示例车辆、计费规则)。这样做的好处是项目换了一台新电脑也能一键恢复环境,尤其是给答辩老师演示的时候,不会因为环境问题掉链子。
后端的application.yml配置要注意时区和服务端口:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ev_rental?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true这个配置非常关键,它能让数据库的user_id自动映射成Java的userId,少写大量XML映射。日志输出配置成StdOutImpl,方便开发阶段在控制台看SQL语句。等部署上线前,记得把日志级别调到warn,否则生产环境控制台会疯狂打印SQL,影响性能。
5.3 前端打包与SpringBoot构建发布
前端开发完成后需要执行npm run build,产物会生成在dist目录。这时你需要决定部署方式,我推荐的是最省事的方案:把dist目录里的静态文件复制到SpringBoot的src/main/resources/static目录下,然后重新打包成单个jar包。这样你在服务器上只需要启动一个进程,前后端都齐了,管理成本极低。
后端打包命令:
mvn clean package -DskipTests打包完成后,target目录下会有一个ev-rental-0.0.1-SNAPSHOT.jar文件。把这个文件传到服务器上,执行:
nohup java -jar ev-rental-0.0.1-SNAPSHOT.jar > app.log 2>&1 &用nohup的目的是让进程在SSH会话断开后依然存活。这个细节很重要,很多同学部署时直接在终端前台跑jar包,一关终端服务就停了,答辩前一晚调试到凌晨也不想再经历一次。
如果想让系统看起来更“产品化”,可以在服务器上安装一个Nginx,把80端口指向SpringBoot的静态文件目录,并做反向代理到8080端口。Nginx配置也不复杂:
server { listen 80; server_name localhost; location / { root /opt/ev-rental/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files这行是前端路由History模式的关键,不然你在页面上点刷新按钮就会得到404。这也是我在部署时遇到的经典问题,后面会详细说。
6. 开发过程中最常踩的坑与排查经验总结
6.1 数据库连接报错:时区问题和驱动版本问题
在项目启动阶段,最常见的报错就是数据库连接失败。很多同学用的连接串是jdbc:mysql://localhost:3306/ev_rental,然后遇到一个“The server time zone”的警告。这个警告的背后是MySQL 8.0以上的版本默认时区跟JDBC驱动不一致,解决方法是url上面加serverTimezone=Asia/Shanghai。另外MySQL 8.0需要使用com.mysql.cj.jdbc.Driver而不是老版的com.mysql.jdbc.Driver,如果用的驱动版本过旧还会直接报“ClassNotFoundException”。
排查这个问题的思路是分步骤确认:先用Navicat或者命令行确认MySQL服务已启动并能登录,再确认数据库和账号权限没问题,最后才怀疑连接串配置问题。而不是上来就卸载重装MySQL,那样只会更加绝望。
6.2 前后端联调时的跨域与Token失效问题
跨域问题在开发环境下已经用代理解决了,但部署到服务器之后,如果不做Nginx代理就会出现新的跨域报错。因为浏览器访问的是Nginx的80端口,后端跑在8080端口,前端的请求头里带了Token,Nginx默认可能不转发请求头。解决方案就是我前面写的,在Nginx配置里加上proxy_set_header Authorization $http_authorization;。一定要知道,这种“打包后打不开”的问题90%是Nginx配置或static路径不对,而不是代码逻辑问题。
Token失效的问题也很烦。JWT里默认不设置过期时间的话,Token永远不会失效,这在安全上是个隐患。我当时设置了2小时过期,用户在还车结算时发现Token已过期,直接报错弹回登录页。我的解决方案是:在支付接口和还车接口里做一个Token自动续期的逻辑,只要用户还在操作,就给他重新签发一个过期时间更晚的Token。这样既保证了安全性,又不会太影响用户体验。
6.3 订单金额计算错误:浮点数问题与精度问题
计费模块如果直接用double类型计算金额,在Java里会得到一个很像“0.30000000000000004”的数。查到支付流水时发现这种数字,只能是DB字段类型设计时没注意浮点数精度。我用Decimal类型后来才明白,金额计算要用BigDecimal,或者让MySQL直接算。推荐方案是先用分钟数乘以BigDecimal.valueOf(pricePerHour).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP)得出金额,再做数据库写入。
如果你在论文里批判性地提到“double计算金额会有精度问题,所以我选择了Decimal”,答辩老师会觉得你有实践敏感性。这种细节是拉开档次的地方,虽然只是几行代码的事,但体现了你是不是真的理解了项目的每一处角落。
6.4 前端刷新404和页面上线后样式不生效
部署之后前端页面刷新404,这个问题我在前面已经说了,根本原因是Vue Router的History模式依赖服务器端配合。解决方法是Nginx的try_files配置,或者换成Hash模式(mode: 'hash'),后者不用配置服务器但对某些评委来说会显得技术选型不够前沿。我更建议上手就把Nginx配置写对,这才是真实项目里会遇到的场景。
还有一个打包相关的问题:前端dist目录里引用的静态资源路径是绝对路径/js/app.js,如果你部署在子路径下就会白屏。解决办法是在vue.config.js里设置publicPath: './',让所有资源引用变成相对路径。
6.5 毕业设计论文相关的配套准备
最后聊一些技术之外但影响“生死”的部分。论文结构上,我建议是绪论(背景意义、国内外现状)、相关技术介绍(SpringBoot、Vue、MySQL)、系统分析(可行性、需求分析、用例图)、系统设计(架构图、功能模块图、数据库E-R图)、系统实现(核心功能讲解配截图)、系统测试(测试用例表与结果分析)、总结。
如果你想让项目更容易过审,建议在测试阶段不要只做“功能能跑通”的验证,而是写一份简单的测试用例表,把测试步骤、预期结果、实际结果列清楚。比如“用户选择空闲车辆下单”,预期结果是“订单生成、车辆状态变为使用中”,实际结果打勾。这个表格能在很大程度上说明你的系统做过系统的验证,而不是随随便便交了份作业。
数据库设计这一部分,画E-R图的时候可以用draw.io或者ProcessOn把实体关系画清楚。用户和订单是1对多,车辆和订单是1对多,用户和支付流水是1对多,订单和支付流水是1对1。车辆和订单之间是否要建立外键我可以多说一句:毕设里建议加外键约束,这样数据一致性有保证,虽然在大型互联网项目中为了性能会去掉外键,但写论文时外键存在更有“数据库设计规范”的说服力。
7. 写在最后的经验之谈
做毕业设计这几个月,我最大的体会是:一个杂乱的系统之所以看起来“像作业”,往往不是因为功能少,而是因为细节没有打磨到位。同一个项目,有的人交了源码就完事,有的人整理出完整的部署文档、数据库脚本、测试用例和演示视频,后者的评分通常都比前者高一档。技术能力在这个阶段固然重要,但认真和规范可能更重要。
如果这个项目你想在答辩之后继续扩展,我建议优先考虑三个方向:一是接入微信小程序端,把车辆定位和扫码开锁放到小程序里,这会让系统更接近真实产品;二是引入Redis做热点缓存,解决高峰期车辆位置和订单状态的查询压力;三是把支付对接换成微信支付的沙箱环境,这个在论文里写出来含金量会明显提升。
我自己在做类似课程设计和毕业设计时,一直坚持“先想清楚再动手,先跑通再优化”的原则。前期花在需求分析和数据库设计上的时间,都会在后面的编码阶段以数倍的效率返还给你。如果你正卡在某个技术点上,或者在这个项目的某个环节上反复折腾过,欢迎在评论区交流,我也很乐意把自己踩过的坑再详细复盘一遍。