基于Spring Boot+Vue的物流仓储管理系统实战:从选题到答辩全解析

发布时间:2026/9/9 2:10:39
基于Spring Boot+Vue的物流仓储管理系统实战:从选题到答辩全解析 从选题到答辩一次讲清基于Spring Boot Vue的物流仓储管理系统还是自己动手写最踏实每年三四月份计算机专业的群里全是同一种消息“有没有现成的毕设项目”“求一个能跑的XX管理系统源码。”“要Spring Boot的前端能不能配Vue”作为带过不少毕业设计、也被亲戚朋友问过无数次“帮我找个课题”的人我特别理解这种焦虑。学校催开题导师问进度自己心里其实最清楚三年代码没写几行突然要独立完成一个“系统”很难不慌。但说实话与其满世界找源码、提心吊胆怕查重、怕被抽检时一句话都答不上来不如踏踏实实自己把一个项目从头到尾做完一遍。比如我这次要拆解的课题——基于Spring Boot Vue的物流仓储管理系统就是一个非常适合拿来练手、又能过审、还能在答辩时讲出东西来的经典题目。先把这个项目是什么说清楚。它不是一个花里胡哨的“大厂级WMS”它的核心定位就是一套能够覆盖仓储业务基本流程的管理系统有货品信息管理、有仓库和库位管理、有入库单和出库单的流程流转、有库存的实时更新和查询、有客户和供应商的基础档案再加上角色权限控制和简单的统计分析。技术栈非常主流后端用Spring Boot搭RESTful接口配合MyBatis Plus操作MySQL数据库前端用Vue 2加Element UI做管理后台界面权限部分用JWT做登录态管理。这篇文章我不打算给你贴完整源码然后让你自己看去而是把整个项目从需求分析到数据库设计、从后端接口到前端页面、从环境搭建到打包部署的完整链路都过一遍。看完之后你不仅能理解每个模块为什么这么设计还能照着这个思路自己写出来。放心文中的每一段代码、每一个表结构我都会解释清楚它解决什么问题。这篇文章适合三类人正在做毕业设计、需要快速搭出一个能答辩的系统的在校生刚入门Spring Boot和Vue、想找一个完整项目练手的自学者以及想在公司内部快速落地一套轻量仓储工具、却被收费系统劝退的团队。1. 项目整体设计与方案选型1.1 为什么选物流仓储这个方向毕业设计选题有个潜规则题目既不能太冷门导致查不到资料也不能太宽泛导致做出来像玩具。物流仓储管理系统刚好卡在中间它是一个“看似不难、但做起来有东西可写”的题目。先说业务层面。仓储管理是任何有实物货物的企业都绕不开的环节——货品要入库、要出库、要盘点、要知道库存还剩多少、要能追溯到某个货品在哪一批进过库。这些需求非常具体面试官和答辩老师一听就懂不会出现“你讲完了他还不知道你要做什么”的尴尬。更重要的是它天然包含多个模块能撑起一份完整的毕业设计文档需求分析可以写用户角色和业务流程数据库设计可以画ER图、建表后端可以拆出用户、货品、入库、出库、库存、报表等多个接口模块前端可以设计不同的页面和交互状态。再说技术层面。这个系统几乎覆盖了Spring Boot和Vue生态里最常用的知识点Spring Boot的控制器、服务层、Mapper层三层架构MyBatis Plus的CRUD和条件构造器JWT的登录认证Vue的路由和组件通信Element UI的表格、表单、弹窗、树形控件ECharts的图表展示。做完这一个项目Spring Boot和Vue的核心用法基本就掌握了大半用在简历上也能拿得出手。我见过很多同学选“XX系统”这个方向结果做完只有一个表、三个页面答辩的时候PPT翻两页就没了。而物流仓储管理系统天然有足够多的实体和业务动作不会出现这种“一页PPT讲完所有功能”的情况。1.2 技术栈选型与版本说明技术选型是项目开始前最需要想清楚的一件事。很多人喜欢追新一上来就Spring Boot 3.x、Vue 3、JDK 17结果网上搜到的资料大半不兼容光配环境就配了两天。我的建议是毕业设计要用“学的人最多、搜得到答案”的版本组合而不是最新版本。推荐这套经过验证的组合组件推荐版本备注JDK1.8 或 11不要高于11兼容性最稳Spring Boot2.7.x2.x版本资料最多避免3.x的坑MyBatis Plus3.5.x比纯MyBatis省事很多MySQL5.7 或 8.0注意8.0的驱动配置区别Vue2.6.xElement UI最稳定的搭配Element UI2.15.x管理后台必备UI库Node.js14.x 或 16.x用于前端开发和构建Maven3.6.x后端依赖管理这套组合是2024年、2025年做毕设最稳妥的方案资料多、坑少、答辩时老师也认可。尤其是Spring Boot 2.7.x它在Spring Boot 2.x里属于维护期最长、bug修得最干净的版本。MyBatis Plus直接提供了内置的CRUD方法写单表操作的时候不需要手写XML能省下大量时间。提示使用Spring Boot 2.7.x时不要将相关依赖版本随意升级到3.x很多starter在3.x中有API变更改起来非常费劲。前端为什么不选Vue 3原因很简单Element UI对Vue 3的兼容一直不太好Vue 3项目往往搭配Element Plus而Element Plus的组件API和Element UI不完全一样网上能找到的模板和示例代码大部分还是Vue 2 Element UI的。项目核心是做管理后台Vue 2 Element UI是极其成熟、文档量极大的方案学习成本也低得多。2. 系统功能模块拆解与数据库设计2.1 功能模块的三个核心角色一个仓储管理系统先想清楚“谁在用”再想“有什么功能”。我不建议一上来就堆功能列表那样做出来的系统往往逻辑混乱、代码耦合严重。我习惯先画角色再画用例最后落到功能模块。这个系统我设计了三种角色系统管理员、仓库操作员、普通员工。系统管理员管的是“系统本身”可以创建用户、分配角色、重置密码也有全部业务功能的操作权限。仓库操作员是日常干活的人负责货品录入、入库单登记、出库单登记、库存调整。普通员工主要就是看——能看货品列表、库存数量和出入库记录但不能做修改操作。对应的系统功能模块可以拆成六大块系统管理用户管理、角色管理、菜单管理基础档案货品分类、货品信息、仓库信息、库位信息入库管理入库单创建、入库单审核、入库记录查询出库管理出库单创建、出库单审核、出库记录查询库存管理库存查询、库存盘点、库存预警数据统计库存数量统计、出入库趋势图这个模块划分有个好处每个模块的职责很清晰后期写代码的时候可以“一个模块一个包”代码结构天然就能让答辩老师看出你的设计能力。2.2 核心数据表结构设计数据库设计是项目的灵魂。很多人写系统先写代码后建表后期改起来痛不欲生。正确顺序是先根据业务理出实体关系再设计表结构最后才开始写代码。这个项目我设计了9张核心表专门列出来说用户表sys_user字段名类型说明idbigint主键usernamevarchar(50)用户名唯一passwordvarchar(100)BCrypt加密后的密码real_namevarchar(50)真实姓名phonevarchar(20)联系电话role_idbigint关联角色表statustinyint状态1启用 0禁用角色表sys_role字段名类型说明idbigint主键role_namevarchar(50)角色名称role_codevarchar(50)角色编码admin/operator/staffremarkvarchar(255)备注货品分类表wms_category字段名类型说明idbigint主键category_namevarchar(50)分类名称parent_idbigint父分类id支持多级分类sort_orderint排序号货品信息表wms_product字段名类型说明idbigint主键product_codevarchar(50)货品编码唯一product_namevarchar(100)货品名称category_idbigint所属分类specvarchar(100)规格型号unitvarchar(20)计量单位pricedecimal(10,2)参考单价low_stockint库存预警阈值statustinyint状态仓库表wms_warehouse字段名类型说明idbigint主键warehouse_codevarchar(50)仓库编码warehouse_namevarchar(100)仓库名称locationvarchar(255)仓库地址managervarchar(50)负责人库位表wms_location字段名类型说明idbigint主键warehouse_idbigint所属仓库location_codevarchar(50)库位编码location_typevarchar(20)库位类型如常温/冷藏入库单表wms_stock_in字段名类型说明idbigint主键in_novarchar(50)入库单号suppliervarchar(100)供应商in_timedatetime入库时间operatorvarchar(50)操作人remarkvarchar(255)备注statustinyint状态1待审核 2已审核 3已驳回入库单明细表wms_stock_in_item字段名类型说明idbigint主键in_idbigint关联入库单主表product_idbigint货品warehouse_idbigint仓库location_idbigint库位quantityint入库数量unit_pricedecimal(10,2)入库单价库存表wms_stock字段名类型说明idbigint主键product_idbigint货品warehouse_idbigint仓库location_idbigint库位quantityint当前库存数量出库相关的表和入库是镜像对称的我在这里就不重复列了但结构上保持一致。2.3 设计中容易忽略的几个点数据库设计有一些坑是我在写这个系统时踩过的提前说一下能帮你省不少时间。库存表一定要记录到“库位”维度。刚开始图省事我把库存只关联到仓库后来发现同样的货品在不同库位都有存放就改成了仓库库位双维度不然出库时根本定位不到货在哪儿。入库单和出库单要拆成“主表明细表”不能只用一个表存所有字段。因为一张入库单可能包含多个货品如果每行都复制一遍供应商、入库时间这些公共信息数据冗余严重而且查单很难查。主表存“一张单据”的公共信息明细表存“这单里每个货品的入库情况”通过外键关联。库存表不存“入库价和出库价的总和”这种冗余字段数量才是库存的核心金额通过实时计算得到。冗余字段容易造成数据不一致这是数据库设计的大忌。创建时间create_time、更新时间update_time这两个字段每张表都要加后面做统计和排查问题都用得上。MyBatis Plus支持字段自动填充加TableField(fill FieldFill.INSERT)就可以自动维护。3. 后端核心功能实现详解3.1 项目结构的分层思路写了这么多年代码我的一个体会是项目结构清晰比代码技巧重要得多。毕业设计答辩的时候老师不一定会细看你每一行代码但一定会看你的项目结构是否规范、分层是否合理。我推荐这样组织后端代码warehouse-system ├── src/main/java/com/example/warehouse │ ├── controller # 控制器层接收请求、返回结果 │ ├── service # 业务逻辑层处理核心业务 │ ├── mapper # 数据访问层接口继承BaseMapper │ ├── entity # 实体类对应数据库表 │ ├── dto # 数据传输对象接收前端参数 │ ├── vo # 视图对象返回给前端的数据 │ ├── config # 配置类CORS、JWT拦截器等 │ ├── common # 公共类统一返回结果、异常处理等 │ └── utils # 工具类JWT、日期处理等controller层只做参数接收和结果返回不写业务逻辑service层是核心所有业务规则都在这里mapper层只做数据操作。这个规则看起来简单但我在实际项目中见过太多人把业务逻辑写在controller里最终导致接口里面几百行代码维护起来极度痛苦。3.2 JWT登录认证与权限控制登录认证是每个管理系统都要做的事。传统的Session方案在前后端分离的架构下会遇到跨域和扩展性问题所以我用的是JWTJSON Web Token方案。JWT的原理一句话就能讲明白用户登录成功后服务器生成一个包含用户信息的加密字符串返回给前端。前端每次请求都带上这个字符串服务器解析成功后就知道“你是谁”不需要在服务器里存Session。集成JWT的步骤是这样的引入依赖jjwt写一个JwtUtil工具类包含生成token和解析token的方法写一个JwtInterceptor拦截器拦截所有需要登录的请求在拦截器中验证token把用户信息放入ThreadLocal写一个RequirePermission注解注解上标记需要的权限码配合AOP做权限校验核心实现思路如下public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE_TIME 7 * 24 * 60 * 60 * 1000; // 7天 public static String generateToken(Long userId, String username, String roleCode) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(roleCode, roleCode) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }这里有几个关键点需要展开说明一下。SECRET字符串一定不要硬编码在代码里否则一旦代码泄露别人就能伪造任意用户的token。实操中我建议放到application.yml配置文件里用Value注解读取。token过期时间设7天是合理的。太短用户总是要重新登录体验差太长token泄露后的风险窗口太大。7天恰好兼顾了“毕设演示不需要频繁登录”和“安全性不至于太差”两个需求。拦截器拿到token后不要只校验“能不能解析”还要校验用户是否仍然存在于数据库、状态是否正常。因为有可能管理员已经禁用了该用户而token还没过期。3.3 入库业务的完整流程实现入库是所有仓储业务里最重要的一环我以它为例把后端业务逻辑怎么写讲透。入库的核心业务规则有三条第一入库单需要审核审核通过后才能增加库存第二同一个货品如果已经在某个仓库的某个库位有库存了入库时应该累加数量而不是新增一条库存记录第三入库明细至少要有一条否则单据无效。对应的代码流程是Service public class StockInServiceImpl extends ServiceImplStockInMapper, StockIn implements StockInService { Override Transactional(rollbackFor Exception.class) public void createStockIn(StockInDTO dto) { // 1. 校验明细不能为空 if (dto.getItems() null || dto.getItems().isEmpty()) { throw new BusinessException(入库单明细不能为空); } // 2. 生成入库单号 String inNo IN System.currentTimeMillis(); StockIn stockIn new StockIn(); stockIn.setInNo(inNo); stockIn.setSupplier(dto.getSupplier()); stockIn.setStatus(1); stockIn.setOperator(LoginUserHolder.getUsername()); this.save(stockIn); // 3. 保存明细 for (StockInItemDTO item : dto.getItems()) { StockInItem detail new StockInItem(); detail.setInId(stockIn.getId()); detail.setProductId(item.getProductId()); detail.setWarehouseId(item.getWarehouseId()); detail.setLocationId(item.getLocationId()); detail.setQuantity(item.getQuantity()); stockInItemMapper.insert(detail); } } Override Transactional(rollbackFor Exception.class) public void auditStockIn(Long id) { StockIn stockIn this.getById(id); if (stockIn null || stockIn.getStatus() ! 1) { throw new BusinessException(单据状态不正确无法审核); } // 审核通过更新单据状态 stockIn.setStatus(2); this.updateById(stockIn); // 遍历明细更新库存 ListStockInItem items stockInItemMapper.selectList( new LambdaQueryWrapperStockInItem().eq(StockInItem::getInId, id)); for (StockInItem item : items) { updateStock(item.getProductId(), item.getWarehouseId(), item.getLocationId(), item.getQuantity()); } } }Transactional注解是我特别强调要加的。它保证了一个事务内的所有操作要么全部成功、要么全部失败回滚。入库审核的场景里如果库存更新了一半时报错了没有事务的话前几条库存已经加了后几条没有加数据就乱了。LambdaQueryWrapper是MyBatis Plus提供的条件构造器写起来非常顺手不用手写SQL。这也是我推荐用MyBatis Plus而不是纯MyBatis的原因CRUD效率提升太多了。更新库存时要注意“有则累加、无则新增”的逻辑对应的SQL类似这样INSERT INTO wms_stock (product_id, warehouse_id, location_id, quantity) VALUES (#{productId}, #{warehouseId}, #{locationId}, #{quantity}) ON DUPLICATE KEY UPDATE quantity quantity #{quantity}这里用到了MySQL的ON DUPLICATE KEY UPDATE语法前提是(product_id, warehouse_id, location_id)建了联合唯一索引。一次SQL请求就完成了“存在则累加不存在则新增”的操作效率高而且没有并发问题。3.4 库存扣减与并发安全出库时扣减库存这个动作看起来简单实际上坑最多。很多人写出的扣减逻辑是“先查库存判断数量够不够再更新”但这里存在一个经典的并发问题两个用户同时下单都查到库存剩10件都判断“够”然后都扣减最后库存变成-2。正确的扣减语句应该一条SQL搞定把判断和更新放在同一个原子操作里// 扣减库存库存充足才更新 int rows stockMapper.deductStock(productId, warehouseId, locationId, quantity); if (rows 0) { throw new BusinessException(库存不足或货品不存在); }UPDATE wms_stock SET quantity quantity - #{quantity} WHERE product_id #{productId} AND warehouse_id #{warehouseId} AND location_id #{locationId} AND quantity #{quantity}这条SQL的本质就是“乐观锁”的一种做法更新时带上条件判断如果quantity quantity不成立MySQL不会更新任何行受影响行数为0业务层捕获到这个结果就知道扣减失败抛出异常。这种写法在单机架构下完全够用。如果是分布式系统可能要考虑Redis分布式锁或更复杂的方案但毕业设计做到这一步已经比大多数项目优秀了。4. 前端页面设计与核心交互实现4.1 前端工程结构怎么搭前端我用Vue 2 Element UI Vue Router Axios ECharts这套组合在做管理后台时效率极高。前端工程结构我习惯这样组织warehouse-web ├── public # 静态资源 ├── src │ ├── api # 接口请求封装按模块拆分 │ ├── assets # 图片、样式等资源 │ ├── components # 通用组件 │ ├── router # 路由配置 │ ├── store # Vuex状态管理 │ ├── views # 页面组件 │ ├── utils # 工具函数如request.js封装axios │ ├── App.vue # 根组件 │ ├── main.js # 入口文件 │ ├── permission.js # 路由守卫 │ └── settings.js # 全局配置 └── package.jsonrouter目录里做了一件关键的事根据路由的meta信息里的角色权限动态生成可以访问的路由。这个在permission.js里用全局前置守卫实现router.beforeEach((to, from, next) { const token getToken() if (token) { if (to.path /login) { next({ path: / }) } else { const roles store.getters.roles if (roles roles.length 0) { next() } else { // 获取用户信息动态添加路由 store.dispatch(user/getInfo).then(() { next({ ...to, replace: true }) }) } } } else { if (whiteList.includes(to.path)) { next() } else { next(/login?redirect${to.path}) } } })这段路由守卫逻辑几乎是所有Vue管理后台的标配写法。它保证用户未登录时只能在登录页和少数白名单页面停留登录后根据后端返回的角色信息动态注册路由。这样不同角色登录后看到的菜单和页面天然就不同。4.2 Axios请求封装与拦截器前端调用后端接口不能每个请求都写一遍axios的配置必须封装一个统一的request.js。除了统一处理请求头、超时时间、错误提示更重要的是统一处理后端的返回结果。后端所有接口返回的统一格式是{ code: 200, message: 操作成功, data: { } }前端axios拦截器里根据code做统一处理service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { // token失效清除登录态跳转登录页面 store.dispatch(user/resetToken) router.push(/login?redirect${router.currentRoute.fullPath}) return Promise.reject(new Error(登录状态已过期)) } Message.error(res.message || 系统错误) return Promise.reject(new Error(res.message)) }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) } )这段代码解决了两个非常常见的问题。第一个是401处理token过期是所有管理系统都躲不过的场景如果不做统一处理每个接口都要写一遍“判断401并跳登录页”的代码而且很容易遗漏。第二个是错误提示后端返回的message字段统一用Element UI的Message组件弹出使用者不需要在每个调用接口的页面里重复写错误处理。关于请求头的处理在请求拦截器里加上service.interceptors.request.use( config { const token getToken() if (token) { config.headers[Authorization] Bearer token } return config }, error Promise.reject(error) )后端JWT拦截器解析的就是这个Authorization头。4.3 库存统计页面的ECharts可视化要做出一个能展示代码能力的功能点数据可视化是最快的方式。我用ECharts实现了两个图表一个是按货品分类统计的库存数量饼图一个是近30天出入库趋势折线图。拿折线图来说前端从后端接口拿到数据后格式大概是{ dates: [2025-04-01, 2025-04-02, ...], inData: [10, 20, 15, ...], outData: [5, 8, 12, ...] }然后在页面里初始化ECharts实例const chart echarts.init(this.$refs.chartRef) chart.setOption({ title: { text: 近30天出入库趋势 }, tooltip: { trigger: axis }, legend: { data: [入库量, 出库量] }, xAxis: { type: category, data: this.chartData.dates }, yAxis: { type: value, name: 数量 }, series: [ { name: 入库量, type: line, smooth: true, data: this.chartData.inData }, { name: 出库量, type: line, smooth: true, data: this.chartData.outData } ] })这里有一个容易踩的坑ECharts在初始化时如果容器是隐藏状态比如在Tabs切换页里图表宽度会变成0。解决办法是在容器显示后调用chart.resize()方法。我在项目里是监听Tabs组件的tab-click事件切换回统计页时主动调用resize。4.4 表单校验与交互体验的细节处理管理后台的表单非常典型新增货品、添加入库单、编辑用户信息每个都是“填表、校验、提交”的流程。Element UI的Form组件自带校验功能比自己在submit的时候一个一个判断要优雅得多。以货品信息表单为例核心校验规则可以这样定义rules: { productCode: [ { required: true, message: 请输入货品编码, trigger: blur }, { pattern: /^[A-Za-z0-9-]$/, message: 编码只能包含字母、数字和短横线, trigger: blur } ], productName: [ { required: true, message: 请输入货品名称, trigger: blur } ], categoryId: [ { required: true, message: 请选择货品分类, trigger: change } ], price: [ { required: true, message: 请输入参考单价, trigger: blur }, { type: number, min: 0, message: 价格不能为负数, trigger: blur } ] }提交的时候只需要在submit方法中调用this.$refs.form.validate(valid {...})校验通过才发送请求。关于表单交互有一个细节我觉得很值得提入库单明细的行内编辑。用户在录入入库单时需要动态添加入库明细行每一行要选择货品、仓库、库位、填写数量。这个功能如果用原生的JavaScript写会比较繁琐但Element UI的表格配合v-for控制行数据数组就能实现“添加一行”的效果。核心思路就是维护一个itemList数组点击添加按钮时push一个新对象点击删除时splice掉对应行。5. 环境配置与项目跑通全流程5.1 本地开发环境准备整个项目的依赖工具比较多但配置起来并不难关键是版本要对。按照下面这个顺序安装基本不会出幺蛾子第一步安装JDK 1.8并配置JAVA_HOME环境变量。注意安装完要用java -version验证一下版本很多人的电脑里装了多个JDK版本环境变量指向错了后边Maven编译会报错。第二步安装Maven 3.6.x配置settings.xml里的阿里云镜像。这一步非常必要不配镜像的话Maven下载依赖可能慢到怀疑人生。第三步安装MySQL 5.7或8.0。安装时设置root密码为简单好记的比如root方便本地开发。创建数据库CREATE DATABASE warehouse CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第四步安装Node.js 14.x或16.x。安装完在命令行输入node -v和npm -v确认安装成功。第五步如果直接使用我提供的数据库脚本只需要在命令行执行mysql -u root -p warehouse sql/warehouse.sql执行完成后9张表就全部创建好了同时会插入一些测试数据包括一个管理员账号admin/admin123。后端项目的application.yml配置如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/warehouse?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-secret-key-for-warehouse-system expire: 604800000MySQL 8.0的驱动类是com.mysql.cj.jdbc.Driver5.7版本的驱动类是com.mysql.jdbc.Driver这两个不要搞混。serverTimezoneAsia/Shanghai参数也很关键不加的话8.0版本经常报时区错误。5.2 前端启动与联调前端项目的启动分两步。先安装依赖npm install这里有个经验之谈如果npm install很慢或报错把npm源换成淘宝镜像执行npm config set registry https://registry.npmmirror.com依赖装完后启动开发服务器npm run dev启动成功后浏览器访问http://localhost:8081就能看到登录页。开发服务器的默认端口在vue.config.js里配置module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }proxy配置解决了前后端联调最大的痛点——跨域问题。开发模式下前端请求/api/user/list会被devServer代理转发到后端http://localhost:8080/user/list浏览器看到的请求是同源的就不会有跨域报错。这种方案比在后端加CrossOrigin注解规范得多生产环境也不需要再改代码。5.3 前后端打包与部署毕业设计要演示和提交一般需要把项目打包成可运行的版本。这里我讲两种方式。方式一独立部署前后端。后端执行mvn clean package在target目录下会生成warehouse-system-0.0.1.jar直接执行java -jar warehouse-system-0.0.1.jar前端执行npm run build在dist目录下生成静态文件用Nginx托管。Nginx配置示例server { listen 80; server_name localhost; # 前端静态文件 location / { root /usr/share/nginx/html; 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 $uri $uri/ /index.html;是为了解决Vue Router的history模式刷新页面404的问题不加这一行路由在子页面刷新就直接白屏。location /api/的proxy_pass末尾带斜杠和不带斜杠行为不同带斜杠会把/api前缀去掉再转发所以后端接口就不需要加/api前缀。方式二打成一个jar包一体化部署。把前端build后的静态文件拷贝到后端的src/main/resources/static目录下Maven打包时会将它们打进jar包。Spring Boot内置的Tomcat会自动托管static目录下的静态资源这样只需要启动一个进程就能同时提供前端页面和后端接口。毕业设计演示时用这种方式最省事。6. 常见问题与避坑记录6.1 问题速查表写这个项目的过程中我整理了开发中最常见的几个报错和解决方法放在一张表里方便查阅。问题现象根本原因解决方法前端请求接口报CORS错误跨域问题使用vue.config.js的proxy代理不要在后端乱加CrossOrigin数据库连接报时区错误MySQL 8.0需要指定时区JDBC URL加serverTimezoneAsia/Shanghai日期字段查询差8小时时区不一致数据库URL、MySQL全局时区、Jackson配置统一前端页面刷新后404路由history模式需要支持Nginx配置try_filesnpm install超时或报错网络原因切换淘宝npm镜像源查询列表慢但数据量不大缺少索引update_time、外键字段加索引库存数量对不上并发扣减导致超卖使用原子更新的UPDATE语句JWT登录态失效但前端无感知拦截器返回401未处理在axios响应拦截器中统一跳转登录页6.2 数据库层面的经典报错MySQL 5.7和8.0有一个很多人都会忽略的差异8.0默认的字符集是utf8mb45.7默认是utf8mb3。如果你用5.7版本创建数据库时没指定字符集插入生僻字或emoji时可能报Incorrect string value错误。最简单的处理方式就是建库时显式指定utf8mb4CREATE DATABASE warehouse DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;另一个常见的报错是数据库连接数不够。如果同时开了前端、后端、Navicat、还有别的项目占用数据库容易出现Too many connections。这是连接池没关闭或配置过大导致的可以在application.yml中将最大连接数调小一点spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 56.3 MyBatis Plus使用中的三个坑MyBatis Plus确实省事但有几个坑需要注意写代码稍不留神就会踩进去。第一个坑是字段自动填充失效。如果实体类里定义了createTime字段且加了TableField(fill FieldFill.INSERT)但没配置MetaObjectHandler插入时这个字段永远是null。必须在项目里写一个配置类实现MetaObjectHandler接口Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }第二个坑是逻辑删除配置后查询条件自动带上deleted0。这本身是好事但如果你在设计时没有deleted字段又在全局配置里开启了logic-delete-field所有查询都会报Unknown column deleted。不想用逻辑删除的话直接把全局配置的delete相关配置删掉就行。第三个坑是多表联查时直接使用MyBatis Plus的page方法联表查询。selectPage只能查单表联表需要自定义SQL配合分页参数。实操中我的通用做法是写一个专门的Mapper方法用Select注解手写SQL通过IPage参数接收分页信息Select(scriptSELECT p.*, c.category_name FROM wms_product p LEFT JOIN wms_category c ON p.category_id c.id where if testkeyword ! null and keyword ! \\ AND (p.product_name LIKE CONCAT(%, #{keyword}, %) OR p.product_code LIKE CONCAT(%, #{keyword}, %))/if /where ORDER BY p.id DESC/script) IPageProductVO selectProductPage(IPageProductVO page, Param(keyword) String keyword);6.4 答辩前最容易忽略的准备项目跑起来之后很多人以为就结束了其实答辩同样是重要环节。根据我带项目的经验答辩时老师会问的问题基本集中在这几个方向数据库为什么这么设计比如“为什么入库单要拆成主表和明细表”回答思路是“一张单据可能包含多个货品明细拆表可以避免数据冗余同时方便按单据维度统计”。JWT和Session的区别是什么重点讲清楚服务端是否存储状态、跨域支持、扩展性这几个点。库存扣减怎么避免超卖把UPDATE ... WHERE quantity #{quantity}这条SQL的逻辑讲清楚再提一下受影响行数判断法。为什么选MyBatis Plus对比原生MyBatis讲单表CRUD不用写SQL、条件构造器方便、内置分页插件。项目如何部署上线把5.3节的内容看明白能对着自己电脑上的jar包和Nginx配置指出来。还有一点要特别提醒如果项目里引入了别人的代码或参考了网上的教程一定要自己重新把核心逻辑敲一遍确保能讲清楚每一行代码的含义。答辩时最尴尬的场景不是不会做而是代码不是你写的老师一问就露馅。写在最后这个项目我从搭框架到完成所有功能实际用时大概两周。我要说的最后一个建议是不要把这个系统只当成一个“交差用的毕业设计”它完全可以成为你简历上一个拿得出手的项目经历。Spring Boot Vue这个组合在中小型公司里应用极广物流仓储的业务逻辑又很典型面试的时候可以深入聊数据库设计、并发处理、权限控制这些话题。我自己的体会是一个项目从零到一做完最大的收获不是那几行能跑的代码而是你终于知道了“一个系统是怎么从需求变成数据库、从数据库变成接口、从接口变成页面”的完整链路。这份感觉只有自己动手写过一遍才能真正内化。如果你在搭建过程中遇到任何问题不管是环境配不上、接口调不通、还是页面渲染不出来欢迎在评论区留言。这个项目的经验和代码我会继续整理分享。