每年到了毕设季我的留言区就会被同一类问题刷屏有没有一套SpringBootVue的完整项目能直接跑起来、有数据库脚本、接口说明还写得清楚的那种。说实话网上能搜到的Java Web毕设源码不少但真正能让你在一周内看懂、跑通、还能在答辩时讲明白的项目包并不算多。我这次整理的这套农产品预售平台就是按这个标准来的——SpringBoot后端、Vue前端、SQL初始化脚本、接口文档四件套齐全业务上也选了预售这个比普通增删改查有深度一点的场景。这套项目适合谁用两类人一类是正在准备开题、想要一个完整工程作为基础再改改的毕业生另一类是刚学完SpringBoot和Vue、想找一个真实业务练手的技术新手。预售这个场景的好处在于它既有商品、订单、支付这类电商共性功能又带着定金、尾款、批次、超时取消这些特殊业务规则难度比学生管理系统高一个档次但又没复杂到企业级电商那种让人望而却步的程度刚好卡在毕设最理想的位置。1. 农产品预售平台这个选题为什么适合做Java Web毕设1.1 预售业务自带复杂度恰好填满工作量这一栏很多毕设项目的问题不是不会做而是太简单。比如纯商品展示加留言后端就是几张表的增删改查前端是几个列表页写到中期你自己都觉得答辩时没什么可讲的。农产品预售不一样它的核心链路是农户发布某批农产品比如果园的晚熟桃、养殖场的大闸蟹设定本批次的预售时间、定金比例和预计发货日期用户在预售期内先付一笔定金锁定名额等临近发货期再支付尾款平台方安排发货最后确认收货。这条链路里藏着好几个有含金量的设计点商品的库存不是简单扣减而是按批次维度做预售限额超卖要拦得住。订单状态不是只有已下单/已完成而是要经历待付定金、已付定金、待付尾款、待发货、已发货、已完成中间还掺着用户主动取消、超时未付尾款的自动取消。金额计算涉及定金和尾款拆分涉及单价、数量、比例的组合浮点数精度问题在这里必须处理。这些点堆在一起项目的业务深度就出来了。答辩时老师问这个项目的难点是什么你至少能拿出状态机设计、定时任务、库存校验这三样来聊而不是支支吾吾说我用了SpringBoot。1.2 前后端分离的技术选型正好覆盖评分表高频项毕设评分通常看重几个维度技术栈的现代程度、功能完整性、代码规范性、数据库设计的合理性。这套农产品预售平台在技术栈上刚好踩中大部分评审老师的偏好评分方向项目中的落点后端框架SpringBoot 2.7.x涉及自动装配、Starter、配置绑定这些机制持久层MyBatis-PlusSQL写在XML或注解里能讲清楚ORM原理前端框架Vue Vue Router Pinia组件化、路由、状态管理全覆盖数据库MySQL8张以上的业务表含索引设计安全机制JWT登录鉴权 接口白名单 密码BCrypt加密异步/定时Spring定时任务做尾款超时扫描这套组合的好处是网上资料多、你踩坑时搜得到解法而且每个点都对应一道经典面试题SpringBoot自动装配原理、JWT和Session的区别、Vue路由守卫怎么用、MyBatis和MyBatis-Plus的区别。等于做项目的过程中顺便把面试题复习了一遍。1.3 一个完整的项目包应该包含哪四块内容很多同学在网上下的所谓完整源码打开以后只有后端一个文件夹数据库脚本没有前端还缺依赖跑起来全是报错。一个能真正用于毕设的项目包我建议至少要有四块前后端完整源码后端Maven工程可以直接编译前端npm依赖完整不是残缺版。SQL脚本包括建库、建表、初始化数据最好拆成init.sql和data.sql方便分步执行。接口文档列出所有接口的地址、请求方式、入参、出参前端对接和答辩画架构图都靠它。运行说明JDK/Node/MySQL版本要求、启动步骤、默认账号这一步能帮你少走两天弯路。这套农产品预售平台就是按这四件套来整理的。后面我会逐个拆开讲后端订单状态机怎么落地、前端页面体系怎么搭、SQL表结构为什么这样设计、接口文档怎么用最后再把跑通源码的步骤和几个高频坑过一遍。2. 后端SpringBoot部分模块划分与预售订单状态机的落地2.1 Maven工程结构与技术选型后端是一个标准的SpringBoot Maven工程包结构按controller / service / mapper / entity / config / common / utils划分。这种分包方式不算花哨但在毕设里是加分项因为评审老师一眼就能看出你清楚分层职责。核心依赖选型我多说两句spring-boot-starter-web提供MVC和内嵌Tomcat这是整个后端的基础。mybatis-plus-boot-starter比原生MyBatis省事单表CRUD不需要写XML复杂多表查询再补注解或XML。mysql-connector-java数据库驱动注意版本要跟MySQL对应。jjwt生成和校验JWT做登录鉴权用。lombok省掉getter/setter模板代码让实体类清爽很多。hutool工具类库字符串、日期、加密这些常用操作不用自己造轮子。pom.xml里不少同学会踩版本坑比如SpringBoot版本太高导致某些依赖冲突。毕设场景我建议用SpringBoot 2.7.x搭配JDK8这套组合最稳网上的教程也基本都按这个版本来写。如果你用JDK17甚至JDK21很多旧依赖会报包不存在之类的问题排查起来很痛苦。2.2 预售订单状态机从定金到尾款的流转设计订单模块是整个预售平台的业务核心。我把它设计成一张订单表加一个状态字段用整数表示订单状态同时配合系统的数据字典说明状态含义。订单状态流转大概是这样的待付定金 → 已付定金等待支付尾款 → 已付尾款商家待发货 → 已发货 → 已完成中间有两个分叉支付定金后用户主动取消进入已取消尾款超时未支付系统自动取消。这里要注意状态机是单向推进的不允许订单从已完成跳回已付定金这是业务逻辑校验要做死的事情。在代码实现上我用service层封装了状态变更的入口方法所有状态切换必须经过统一方法而不是在controller里直接改状态字段。这样做的好处是后续如果要加退款中退款完成这些状态只需要在状态机里补分支不影响调用方。订单创建的核心逻辑是库存校验。用户提交定金订单时要锁定预售批次的部分库存public PresaleOrder createPresaleOrder(Long batchId, Integer buyCount, Long addressId) { PresaleBatch batch presaleBatchMapper.selectById(batchId); if (batch null || batch.getStatus() ! 1) { throw new BizException(该批次不在预售中); } if (batch.getRemainStock() buyCount) { throw new BizException(该批次剩余库存不足); } // 计算定金单价 * 数量 * 定金比例保留两位小数 BigDecimal deposit batch.getPrice() .multiply(BigDecimal.valueOf(buyCount)) .multiply(batch.getDepositRatio()) .setScale(2, RoundingMode.HALF_UP); // 扣减库存 presaleBatchMapper.cutStock(batchId, buyCount); // 创建订单状态为待付定金 // ... }这里有个关键细节金额一律用BigDecimal不能用double否则定金、尾款的精度问题会在支付环节给你上一课。扣库存用update语句里的原子操作比如UPDATE presale_batch SET remain_stock remain_stock - #{count} WHERE id #{id} AND remain_stock #{count}从数据库层面避免并发超卖。2.3 尾款超时自动取消用Spring定时任务实现预售场景有个很现实的规则用户付了定金但发货前没付尾款这个名额不能无限期占着。我在系统里加了一个定时任务每分钟扫描一次所有已付定金且已超过尾款支付截止时间的订单自动置为已取消并把占用的库存释放回批次。用Spring的Scheduled注解就能实现不需要引入额外的任务调度框架Component public class TailPayTimeoutJob { Scheduled(cron 0 0/1 * * * ?) public void scanTimeoutOrders() { ListPresaleOrder timeoutOrders orderMapper.selectDepositPaidButTimeoutOrders(new Date()); for (PresaleOrder order : timeoutOrders) { // 关闭订单释放库存 orderService.cancelOrderByTimeout(order.getId()); } } }注意在启动类上要加EnableScheduling。这个定时任务写出来之后你的项目里就有异步任务这个技术点了答辩的时候可以顺带提一嘴分布式环境下可以用xxl-job替代单体定时任务显得你考虑过生产级方案。2.4 自动装配与配置绑定答辩高频问题的准备素材SpringBoot的项目如果只是写几个RestController答辩时老师随便问一句SpringBoot为什么能自动配置就可能卡住。建议把原理吃透一点SpringBootApplication里包含EnableAutoConfiguration它会去读META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件把里面的自动配置类按条件装配进容器ConditionalOnMissingBean、ConditionalOnProperty这些条件注解决定某个配置在什么条件下生效。在application.yml里我们把数据源、JWT密钥、文件上传路径这些配置统一管理。比如server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/agri_presale?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 jwt: secret: your-secret-key-at-least-32-characters expire-minutes: 120有一个非常隐蔽的坑JWT的HS256算法要求密钥长度不能太短如果你随便写个几位数的字符串运行时会在生成token时抛异常报错信息还不太直观。建议密钥至少32个字符这个我会在后面第6节再强调一遍。3. 前端Vue部分路由、状态管理与Axios拦截器的实际用法3.1 用户端与管理端两套页面的路由设计前端我按用户端和管理端拆成两块。用户端面向普通消费者核心页面有首页、商品列表、商品详情、预售批次详情、购物车、订单列表、支付页、个人中心管理端面向运营人员核心页面有商品管理、预售批次管理、订单管理、用户管理、数据统计。两边路由分开通过路由守卫做权限控制。Vue Router的路由配置本身不复杂复杂的是哪些页面需要登录才能进管理端怎么拦住普通用户。我在router的meta里挂角色信息再在全局前置守卫里判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) return } if (to.meta.role) { const user JSON.parse(localStorage.getItem(userInfo) || {}) if (user.role ! to.meta.role) { next(/403) return } } next() })登录后把token和用户信息落到localStorage刷新页面时再从本地恢复。这里顺便说一句如果你用的是Vue3的组合式API可以用Pinia管理用户状态比localStorage到处读要规范得多也更好向老师解释状态集中管理。3.2 Axios拦截器统一处理token和错误码前端所有请求我都走axios并在封装的request模块里加了拦截器。请求拦截器负责在每次请求头里带上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 // 后端统一返回体 { code, message, data } if (res.code ! 200) { if (res.code 401) { localStorage.clear() location.href /login } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )后端返回体设计成{ code, message, data }这种统一格式后前端每个页面就不用再自己判断status是200还是500逻辑全集中在拦截器里。这也是接口文档里最核心的约定前后端必须在这个结构上达成一致否则联调阶段会非常痛苦。3.3 组件化实践预售倒计时和商品卡片前端页面里比较有含金量的组件有两个一个是预售倒计时一个是商品卡片。预售倒计时需要处理批次预售还剩多久结束以及距离尾款支付截止还有多久我把它封装成一个独立的CountDown组件用setInterval每秒刷新剩余时间并在组件卸载时清掉定时器防止内存泄漏onMounted(() { timer setInterval(() { const remain endTime - Date.now() if (remain 0) { clearInterval(timer) emit(timeout) } state.remainText formatRemain(remain) }, 1000) }) onUnmounted(() clearInterval(timer))商品卡片则用Vue插槽slot做成可插拔的结构——默认样式展示图片、名称、价格管理端需要显示上下架按钮时通过具名插槽塞进去。这种做法正好是Vue面试题里常考的插槽应用也避免了同一份代码复制两遍的尴尬。3.4 环境搭建vue安装依赖、版本选择与镜像源前端环境最常出问题的就是npm install那一步。node-sass这类老依赖在Node高版本下经常编译失败所以我在前端选型时直接绕开它Vue脚手架用ViteUI库用Element Plus这样对Node版本的要求宽松很多。如果你拿到的项目用的是旧版vue-cli加node-sass先确认Node版本是否符合依赖要求不行就装nvm切换版本或者把镜像源切到国内源再试。# 使用国内镜像源安装依赖能省下大量等待时间 npm config set registry https://registry.npmmirror.com npm install npm run dev启动之后默认端口一般是5173Vite或8080vue-cli本地开发时前后端端口不同所以前端vite.config.js里通常要配一层代理或者后端放开跨域。这个会在第5节的接口联调部分一起讲。4. SQL脚本里的表结构预售场景的建表思路与字段细节4.1 核心表清单与职责划分整套系统我建了9张表不算多但每张表都有明确的业务职责表名职责sys_user用户表区分普通用户和管理员存手机号、密码BCrypt加密、角色product农产品表存商品名称、简介、主图、分类、上下架状态presale_batch预售批次表存批次名称、总库存、剩余库存、定金比例、预售时间、发货时间product_image商品轮播图表一对多挂在商品下presale_order预售订单表存订单状态、定金金额、尾款金额、地址快照payment_record支付流水表记录定金支付、尾款支付各一次order_address地址表用户收货地址cart购物车表category商品分类表这里有个设计细节值得展开商品和预售批次是分开的两张表。原因很简单——同一款农产品可以发起多轮预售不同轮次的定价、定金比例、发货时间都不一样如果把批次字段堆在商品表里产品一换季你就得改表结构。拆开后商品是静态信息批次是动态信息一个商品关联多个预售批次属于非常自然的1:N关系。4.2 订单表为什么要做快照订单表里我没有直接存商品ID 数量而是把商品名称、主图、单价、批次名称这些下单时的信息冗余了一份进去。这种做法在电商系统里叫订单快照。假设用户下单时商品卖60块三天后商家把价格改成80块如果订单只存productId用户打开订单列表看到的就会是新的80块这显然不合理。把价格、名称、图片在下单那一刻固化进订单表之后商品怎么改价都不影响历史订单展示。这个点虽然小但在答辩时属于考虑到了实际业务问题的加分细节。4.3 字段类型、时间字段与逻辑删除建表的时候有几个细节要养成习惯金额一律DECIMAL(10,2)禁止float/double。数据库里浮点数的二进制表示会导致金额对不上账审计的时候很难解释。状态字段用tinyint配合COMMENT注释说明每个取值含义比如1预售中 2已结束 3已取消。SQL脚本里的注释质量某种程度上代表你的工程素养。每张表都保留created_time、updated_time并设置DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP查询和排查问题都方便。用户、商品这类数据用逻辑删除deleted字段而不是物理DELETE保留历史数据也防止外键关联断裂。预售批次表的建表SQL大概长这样CREATE TABLE presale_batch ( id bigint NOT NULL AUTO_INCREMENT, product_id bigint NOT NULL COMMENT 关联商品ID, batch_name varchar(100) NOT NULL COMMENT 批次名称如早熟红富士第一轮, price decimal(10,2) NOT NULL COMMENT 本批次单价, total_stock int NOT NULL COMMENT 本批次总库存, remain_stock int NOT NULL COMMENT 剩余库存, deposit_ratio decimal(5,2) NOT NULL DEFAULT 0.30 COMMENT 定金比例0.30表示30%, presale_start_time datetime NOT NULL COMMENT 预售开始时间, presale_end_time datetime NOT NULL COMMENT 预售结束时间, tail_pay_deadline datetime NOT NULL COMMENT 尾款支付截止时间, delivery_time datetime NOT NULL COMMENT 预计发货时间, status tinyint NOT NULL DEFAULT 1 COMMENT 1预售中 2已结束 3已取消, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_product_id (product_id), KEY idx_status_endtime (status, presale_end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT农产品预售批次表;索引设计上idx_product_id用来支撑按商品查批次idx_status_endtime用来支撑定时任务扫描预售中且已过期的批次。SQL脚本里把索引建好说明你考虑过查询路径而不是随便给主键就完事。4.4 初始化数据脚本的编排我会把初始化脚本拆成init.sql建库建表和data.sql演示数据。演示数据里至少要有一个管理员账号admin/123456、一个普通用户、3个商品、每个商品各2个预售批次一个预售中、一个已结束、几笔不同状态的订单。为什么要准备这些数据因为答辩演示的时候你总不能现场注册账号、现场下单、现场等定时任务跑。提前把已付定金、已付尾款、已发货、已完成的订单都铺进去演示时点到哪个页面都有数据可看老师体验会好很多。导入脚本时注意SET FOREIGN_KEY_CHECKS0避免因表之间的逻辑关系导致导入顺序报错。5. 接口文档怎么用起来联调效率、答辩素材和二次开发5.1 统一返回体与分页结构接口文档不是应付检查的摆设前后端联调全靠它。项目里所有接口都遵循统一返回结构{ code: 200, message: success, data: {} }分页查询的data里再包一层{ records: [], total: 32, current: 1, size: 10 }code的含义在文档里写明200成功、400参数错误、401未登录或token过期、500服务器异常、501业务异常比如库存不足。前端拦截器只需要认这几种code就能把登录失效自动跳转业务失败弹提示这类通用逻辑做掉。5.2 核心接口清单与一个完整示例接口按模块分成几组认证模块、商品模块、预售批次模块、订单模块、支付模块、管理端模块。核心接口我用表格列一下方便你对着源码找接口方法说明是否需要登录/api/auth/loginPOST登录返回token和用户信息否/api/product/pageGET商品分页查询否/api/product/{id}GET商品详情含批次列表否/api/presale/batch/{id}GET批次详情否/api/cart/*POST/GET购物车增删查是/api/presale/orderPOST提交预售订单付定金是/api/presale/tail-payPOST支付尾款是/api/order/listGET我的订单列表是/api/order/{id}/cancelPOST取消订单是/api/admin/batchPOST管理端发布预售批次管理员/api/admin/order/pageGET管理端订单列表管理员/api/admin/order/deliverPOST管理端发货管理员拿提交预售订单举例接口文档里要写清楚请求参数POST /api/presale/order Content-Type: application/json Authorization: Bearer {token} { batchId: 2, buyCount: 2, addressId: 5, remark: }后端响应示例{ code: 200, message: success, data: { orderId: 20241101001, orderNo: PS2024110100001, productName: 烟台红富士苹果 第一轮预售, totalAmount: 120.00, depositAmount: 36.00, tailAmount: 84.00, status: 1, statusName: 待付定金 } }呼应后端逻辑定金 单价 × 数量 × 定金比例这里60元 × 2件 × 30% 36元尾款就是84元。响应里把金额拆清楚前端支付页就不用自己再算一遍避免前后端计算结果不一致。5.3 接口文档在答辩中的三个用法接口文档除了开发时对齐答辩时其实特别好用第一展示工作量。老师问系统有哪些功能你不用背页面直接按接口清单讲一遍认证、商品、批次、订单、支付、管理模块化讲下来逻辑清楚又显得规范。第二讲解业务规则。比如一个批次到底能不能重复下单接口文档里会写明参数校验规则同一用户同一批次只能下一笔定金订单未取消前再次提交会返回501业务异常。这类边界规则是答辩时最有说服力的素材。第三支撑二次开发。如果你后续想加优惠券、加满减接口文档就是扩展的基础。第7节我会给几个现成的扩展方向。5.4 跨域与鉴权白名单接口联调最容易翻车的两个点本地开发时前端跑在5173后端跑在8080前端的axios请求必然触发跨域。解决方式有两种一种是在后端加CORS配置另一种是前端用Vite代理。毕设项目我建议两种都要懂后端加CORS是为了打包部署后也能跨域访问Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }JWT拦截器那边则要配置白名单登录、注册、商品查询、批次查询这些公开接口放行其余接口校验token。白名单配置在代码里集中维护答辩时能讲清楚为什么商品详情不需要登录也能看而下单必须登录这个产品逻辑。6. 跑通这套源码的完整步骤与三个高频坑6.1 从环境到前后端启动跑通这套项目我建议按这个顺序操作安装JDK8、Maven 3.6、MySQL 5.7、Node 16确认java -version、mvn -v、node -v都能正常输出。用Navicat或命令行创建数据库agri_presale字符集选utf8mb4依次执行init.sql和data.sql。打开后端的application.yml改成你本地数据库的用户名密码必要时调整端口。在backend目录执行mvn spring-boot:run看到Tomcat started就说明后端起来了。在frontend目录依次执行npm install、npm run dev浏览器访问前端地址用admin/123456登录。后端单独启动时可以用命令行带参数覆盖配置适合不想改yml的情况mvn spring-boot:run -Dspring-boot.run.arguments--server.port8081 --spring.datasource.passwordxxxx6.2 前端打包进SpringBoot的单体部署方式有些同学答辩时需要一个jar搞定演示不想现场开两个终端。这种场景下的做法是前端执行npm run build把生成的dist目录里的静态文件复制到后端src/main/resources/static下重新打包成jar。启动后访问http://localhost:8080/就能同时拿到页面和接口。注意两个细节一是前端请求地址要改成相对路径/api/...而不是写死http://localhost:8080/api/...否则打进jar后请求还是指向固定端口换个环境就得改代码二是Vue Router如果用的是history模式刷新页面会出现404因为SpringBoot的静态资源路由不认前端路由需要在后端加一个转发规则把所有非/api的路径转发到index.html。如果是hash模式就没有这个问题毕设图省事可以直接用hash模式。6.3 三个高频坑的排查链路结合我平时帮人看代码的经验这套项目跑不起来的报错基本集中在三个地方。第一个是数据库连接失败。报Access denied for user是密码写错报Unknown database是库没建报The server time zone value是URL里少了serverTimezoneAsia/Shanghai。这些都是配置层问题对照application.yml逐项检查就能解决。第二个是端口被占用。报Port 8080 was already in use时Windows下执行netstat -ano | findstr 8080找到占用进程的PID任务管理器结束它或者干脆改后端端口。前端5173被占用同理。第三个是JWT拦截器把所有请求都拦了前端登录都登不进去。排查顺序先看后端日志里请求有没有进入controller再看拦截器注册时是否把/api/auth/login放进了排除列表最后看前端是否把token正确塞进了请求头。最常见的坑是拦截器里excludePathPatterns写错了路径导致登录接口也被拦返回401前端就一直卡在登录页。排查的时候我习惯先看后端日志再看浏览器Network面板里的请求状态码最后看数据库里有没有数据。这个顺序能帮你快速定位70%的问题比瞎猜高效很多。6.4 部署演示的小建议如果老师要求现场演示或者你要录视频交作业建议提前准备一台干净环境的机器把MySQL、JDK都装好启动步骤写成文档。演示用的数据在第4节已经铺好了你只要保证启动后能登录、能打开几个不同状态的订单页面讲解的时候再结合接口文档里那几条业务规则整个演示就很完整了。7. 想让答辩更有亮点三个可以直接落地的扩展方向7.1 MinIO文件存储把商品图片和宣传视频从本地目录挪走原项目的商品图片如果直接放在本地磁盘目录简单是简单但答辩时容易被问生产环境图片放服务器磁盘有什么问题。标准答案是不利于水平扩展、备份困难、请求压力全在应用服务器上。更合理的方案是接入MinIO做对象存储。MinIO的接入思路不复杂后端引入minio Java SDK配置endpoint、accessKey、secretKey、bucket上传接口接收MultipartFile后转存MinIO返回文件访问URL前端商品管理页用el-upload对接这个上传接口。这样一个改动就能同时在项目里增加文件上传对象存储两个技术点。7.2 预售宣传视频的m3u8播放给商品详情页加视频农产品预售往往需要展示种植环境、采摘过程静态图片说服力有限所以可以给商品详情加一个宣传视频模块。视频处理上比较实用的做法是先用ffmpeg把mp4转成hls切片m3u8 ts文件上传到MinIO前端用hls.js播放不需要额外装插件。前端播放的核心逻辑是import Hls from hls.js if (Hls.isSupported()) { const hls new Hls() hls.loadSource(videoUrl) // 指向 xxx/index.m3u8 hls.attachMedia(videoRef.value) }这个方案的卖点是免安装播放器、按需分片加载、适合移动端弱网环境讲出来就是活的项目亮点。ffmpeg转切片的具体命令你也可以写进项目README让老师看到你有落地的工程能力。7.3 数据可视化大屏用ECharts把预售数据盘活管理端如果能有一个数据概览页整个项目的档次会明显不一样。用ECharts做几个图表各批次预售金额的柱状图、商品分类销售占比的饼图、近30天订单量和付款金额的折线图、尾款支付率的仪表盘。后端对应加一个/api/admin/stats/overview接口返回这几个图表需要的数据聚合结果SQL里用GROUP BY和DATE_FORMAT就可以完成。前端用ECharts的init方法渲染整个页面差不多一百行代码就能搞定。视觉上的冲击力远大于你在答辩PPT里写满技术名词。这三个扩展点都基于现有项目结构改动量不大但每一个都能在答辩问答环节给你多留一个我确实思考过工程化问题的印象。最后说点我个人的体会。这套农产品预售平台我前前后后帮人跑过很多遍印象最深的一次是一个同学把数据脚本导进去后登录一直报错折腾一晚上才发现是JWT密钥太短。所以拿到任何项目先别急着改业务代码按建库 → 导数据 → 起后端 → 起前端的顺序把环境跑通再带着业务去读代码效率会高得多。答辩前的晚上也建议你亲手从零到一再部署一遍那比背十页PPT都有用。