SpringBoot+Vue雪具销售系统:毕业设计从架构到部署全解析
1. 为什么我把毕业设计做成雪具销售系统做毕设那会儿我其实挺纠结的。学校既没限制题目范围又要求系统要能实际跑起来、有完整业务逻辑图书馆管理系统、学生选课系统这种题被做了不知道多少遍答辩老师看一眼封面就不想翻正文了。后来我定下做SpringBootVue雪具销售系统管理平台源码主要是因为这几年冰雪运动在国内越来越火滑雪装备销售是个有真实场景、有完整交易链条的领域不像那些纯CRUD的管理系统评委一听背景就会多几分兴趣。当然选题好看只是第一层。真正让我确定下来的是这套系统的业务覆盖面商品展示、分类检索、购物车、下单结算、库存扣减、订单管理、后台数据维护几乎是电商业务的标准缩影。你把这些模块一个个做扎实等于把 JavaWeb 课程里最重要的一圈知识全串起来了。而且技术栈选的是当前就业市场最主流的组合——SpringBoot 做后端接口、Vue 做前端交互、MySQL 存数据、MyBatis-Plus 操作数据库无论你之后找工作还是想在此基础上继续扩展这套组合都不会白学。这个项目适合谁如果你是准备毕业设计的学生可以直接参考它的业务划分和代码结构如果你在做课程设计可以把它砍掉一部分模块只保留商品管理和下单流程如果你是打算转行做 Java 开发的初学者这套系统的代码量大概在几千行级别能读明白、能自己改改需求你对前后端分离开发的理解会上一个台阶。我写这篇文章是想把它拆开讲讲每个模块为什么这么设计、数据库为什么这么建、哪些代码是认真写过的、哪些坑是踩过的希望对正在找方向的你有点帮助。顺带说一句这套系统本身我也整理成了可以直接下载运行的源码数据库 SQL 文件、部署文档都是配齐的属于拿到手能跑起来的程度。文章里我会把核心设计思路和关键实现都讲明白这样你不仅能跑起来还能在答辩时把每一处设计理由说清楚。1.1 电商类系统为什么特别适合当毕设很多人有个误区觉得电商系统已经很泛滥了再做一个没新意。但你要思考的是毕设的核心目标是证明你掌握了开发技能而不是搞科研创新。电商系统包含前端页面交互、后端接口设计、数据库关系建模、业务状态流转、权限控制、事务处理这些元素恰好对应了大学阶段最重要的几门专业课。用一套完整的电商业务去承载这些知识点比做一百个空壳管理系统更能体现你的真实水平。雪具销售在电商里又有个独特优势商品属性比较特殊。滑雪板分单板双板雪鞋分固定器尺码滑雪服分防水指数这些属性天然需要分类、筛选、详情展示等逻辑不会像图书管理系统那样只需要一个书名一个作者就完事了。你还能借此在报告里写一句针对雪具商品的规格属性设计了灵活的分类和检索机制这在答辩时是加分项。1.2 这套系统整体能做什么从用户角度看系统分成前端商城和后端管理两大部分。前端商城面向普通消费者注册登录、浏览首页推荐商品、按分类筛选雪板雪鞋护具、查看商品详情、加入购物车、提交订单、查看自己的订单状态。后端管理台面向管理员仪表盘统计、商品上下架、库存修改、分类维护、订单发货、会员列表管理。权限上区分为普通用户和管理员两种角色不同角色登录后进入不同界面、调用不同接口。这个规模拿来做毕设刚刚好不多不少。课设的话可以去掉统计报表和部分管理功能只保留完整的用户购物流程如果想冲优秀毕设可以在此基础上加 Redis 缓存、Excel 导出、支付模拟等我在文章结尾会单独聊聊扩展方向。2. 系统架构与核心模块划分前后端分离怎么落地先看整体架构这套系统是标准的B/S 架构 前后端分离。后端 SpringBoot 打包成独立服务提供 RESTful 风格的 JSON 接口端口默认 8081前端 Vue 工程通过 Vite 启动开发服务器端口 5173所有请求通过 axios 发送到后端地址。两边通过 HTTP 通信前端只管页面渲染和用户交互后端只管数据处理和业务规则互不干扰。这种架构的好处很实际第一前后端可以并行开发我自己是先写后端接口再用 Postman 测通然后再写前端页面调试方便很多第二项目分层清晰前端是 Vue 工程后端是标准的 controller-service-mapper 三层结构答辩时讲解代码结构很直观第三以后想加小程序端或者管理后台只需要复用后端接口不用重写业务逻辑。代码层面后端按功能拆分成 config、controller、service、mapper、entity、common 几个包。config 放跨域配置和 MyBatis-Plus 分页配置controller 只做参数接收和结果返回service 写业务逻辑mapper 是数据库操作层。前端则按页面视图拆分成 views按功能拆分成 componentsstore 里管理用户登录态和购物车状态。下面我把各个功能模块拆开细讲。2.1 用户端核心模块用户端涉及页面的模块有这几个用户模块注册、登录、个人信息查看和修改。密码不是明文存数据库的我用的是 MD5 加盐的方式虽然现在看安全性不算高但相比明文存储已经是在毕设里能解释得通的方案了。登录成功后后端签发一个 Token 给前端前端存在 localStorage 里之后每次请求都在请求头里带上后端通过拦截器校验 Token 判断登录状态。商品模块首页商品推荐列表、全部分类下的商品列表、按关键词搜索、商品详情页。商品列表支持分页加载这里用到了 MyBatis-Plus 的分页插件后面我会说配置方式。购物车模块加购、修改数量、勾选商品、删除、清空。这里我前端用 Vuex 维护了一份购物车状态后端也建了购物车表做持久化两者同步的策略是用户点击加购时立即调后端接口同时在本地更新视图状态这样刷新页面数据也不会丢。订单模块从购物车勾选商品生成订单、填写收货地址、确认下单、查看我的订单列表、取消订单。下单是整个系统里事务最复杂的地方后面我用专门的小节展开。2.2 管理端核心模块管理端是另一套页面路由前缀是 /admin。登录时后端接口会判断用户角色只有 role 字段为管理员才允许访问管理端接口。管理端模块包括统计面板商品总数、今日新增订单数、订单总金额、用户总数这些数据用简单的 SQL 聚合查询算出来。商品管理商品列表、新增商品、编辑商品、上下架、删除。商品上下架用 status 字段控制下架商品在前端商城不可见。分类管理针对雪具商品的分类比如按滑雪板、滑雪鞋、滑雪服、护具、配件划分可以新增、修改、删除分类。订单管理查看全部订单、按订单号或用户搜索、修改订单状态待付款、待发货、已发货、已完成、已取消。管理员的发货动作本质上就是更新订单状态字段。用户管理查看注册用户列表、启用或禁用账号。2.3 权限控制思路这个项目的权限控制没有引入 Spring Security 和 Shiro因为我评估过毕设阶段用轻量方案足够应付而且面试时还能讲清楚拦截器与 Filter 的区别。我选择的是写一个拦截器在 WebMvcConfigurer 里注册拦截所有 /api/** 请求只放行登录接口、注册接口、商品查询接口。其他接口统一从请求头取 Token校验不过就返回 401 状态码前端收到 401 后自动跳回登录页。管理员接口再叠加一层角色校验在 Token 里放入 userId 和 role 两个字段拦截器校验 Token 合法性后把 userId 和 role 存入 ThreadLocal管理端接口需要的角色是管理员时从 ThreadLocal 里取出来比对不匹配直接返回无权限。这套逻辑写下来不超过一百行代码但把认证和鉴权两个概念讲得很清楚。3. 数据库设计六张表怎么撑起完整的交易闭环数据库是整个系统的基础表结构设计得合不合理直接决定你写业务代码时是顺畅还是到处打补丁。这个项目一共设计了六张核心表用户表、分类表、商品表、购物车表、订单表、订单明细表。下面我把每张表的关键字段和设计理由说出来建表 SQL 在源码里已经配好这里重点讲为什么这么建。3.1 用户表设计用户表是最简单的一张表但有几个字段要注意。id 用 bigint 自增主键username 加唯一索引password 存加密后的字符串。除了基本信息我额外设计了 role 字段tinyint 类型0 表示普通用户1 表示管理员读者看到这里可能会问为什么不用字符串存admin我用 numeric 类型的考虑是查询效率更高、判断更简单而且不至于在数据库里存大段含义不明确的文本。status 字段也是 tinyint0 禁用 1 正常用于管理员拉黑用户。这张表在设计上有一个容易被忽略的点create_time 和 update_time 两个时间字段我让 MyBatis-Plus 的自动填充功能来处理插入时自动写 create_time更新时自动刷新 update_time避免业务代码里到处手动 set 时间。3.2 商品表和分类表设计商品表是信息量最大的表。核心字段包括分类 id、商品名称、副标题、主图地址、详情图片、原价、现价、库存、销量、状态、创建时间。金额我是用 decimal(10,2)不是 float 更不是 double原因很直接——浮点数在计算金额时会有精度误差订单金额算错了在答辩时是一个很低级的减分点。状态字段 status 用 tinyint 标记0 下架 1 上架下架商品不会出现在商城列表里。分类表考虑到雪具商品可能有多级分类比如滑雪板下面还能分单板双板我设计了 parent_id 自关联字段如果不做多级分类parent_id 填 0 即可。商品表和分类表通过 category_id 关联这是一个典型的外键关系但我没有在数据库层面强制建外键约束原因是在实际开发中外键会影响插入删除性能也容易在删除分类时被约束卡住业务层自己去保证数据一致性更灵活。这个点面试官问到时你可以这么答。3.3 购物车和订单表设计购物车表字段相对简单用户 id、商品 id、购买数量、勾选状态。这里有一个细节我建了 user_id product_id 的联合唯一索引防止同一个用户把同一个商品重复加购物车如果用户重复点加购后端接口先查询有没有该商品记录有则做数量累加而不是新增一行数据就不会冗余混乱。订单表是整个系统的重头戏。订单号 order_no 用 varchar 存储格式是 yyyyMMddHHmmss 四位随机数在设计上尽量避免同一秒下单撞号当然严谨的方案应该是雪花算法如果你项目里想体现自己对并发场景的理解可以改成雪花算法生成订单号这个我可以放到扩展部分说。订单表还存了总金额、支付金额、订单状态、收货人姓名、电话、地址、下单时间等字段。注意我特意在订单表冗余了收货人信息而不是通过用户 id 去关联用户表再查地址原因是订单收货信息在下单那一刻就应该被固定下来用户之后改了个人资料历史订单的收货信息不应该跟着变这一点在讲解数据冗余时是很好的素材。订单明细表保存订单里每一个商品的快照商品 id、商品名称、商品主图、下单时的单价、购买数量、小计金额。为什么是快照因为商品的价格和名称可能会变动甚至商品可能被删除但订单明细必须保留买家下单那一刻看到的信息才能在后续对账时有据可依。这是电商系统订单设计的重要思路。3.4 表关系与索引设计这六张表的关系可以概括为用户与订单是一对多订单与订单明细是一对多商品与分类是多对一用户与购物车记录是一对多购物车记录与商品是多对一。整体的数据流转逻辑是用户浏览商品 - 把商品加入购物车 - 把购物车勾选商品生成订单 - 订单同时生成明细 - 扣减商品库存 - 清空购物车中已下单的商品。索引方面我在用户表的 username、商品表的 category_id 和 status、订单表的 user_id 和 create_time 上建了索引。原因是这些字段是查询的高频条件比如查看我的订单列表就是 where user_id ? order by create_time desc两个字段联合起来走索引查询会快很多。项目数据量小的时候可能感觉不到索引的作用但毕设报告里把这层设计意图写清楚老师会觉得你不是在傻乎乎的建表。4. 后端 SpringBoot 核心实现接口设计与关键业务逻辑后端工程我用 Spring Initializr 初始化JDK 选 8SpringBoot 版本用的 2.7.x。这里多说一句如果你自己从零建工程SpringBoot 3.x 虽然也可以但很多第三方 starter 对它的兼容不如 2.7 稳作为毕设使用 2.7 是更稳妥的选择。依赖方面核心只需要 spring-boot-starter-web、mybatis-plus、mysql-connector-j、lombok、validation。4.1 统一响应体和全局异常处理前后端分离项目最忌接口返回格式不统一一会儿返回对象一会儿返回字符串前端 axios 拦截器就写得很痛苦。我在 common 包里定义了一个 Result 类所有接口都返回这个结构包含 code、message、data 三个字段code 为 200 表示成功401 未登录500 服务异常。前端统一判断 code不是 200 就弹出错误提示。配合统一响应体我写了全局异常处理器用 RestControllerAdvice 标注里面分别捕获业务异常、参数校验异常和兜底异常。业务异常我自定义了一个 BizException例如下单时库存不足就抛一个库存不足的业务异常全局处理器捕获后统一包装成 Result 返回。这样做最大的好处是控制器里的代码非常干净不需要每个方法都写 try-catch只要把核心业务逻辑写在 service 里异常自然会汇聚到全局处理器。说句实在话光是整洁的异常处理这一点老师写评语时就比别人高一眼。4.2 用户注册登录与 Token 机制注册接口没什么特别的校验用户名唯一、密码加密后入库。登录接口做的事多一点先根据用户名查出用户比对密码是否一致一致则用 Token 工具类生成一个带过期时间的字符串Token 内容包含 userId 和 role。我没有引入 JWT 库是自己在工具类里把 userId role 过期时间拼接后做签名再转成 Base64 字符串。毕设层面这个方案已经足够讲清楚原理Token 是一段携带用户信息且能校验真伪的凭证服务端无需存储 Session天然适配前后端分离和横向扩展。之后每次请求前端在 axios 请求拦截器里自动从 localStorage 取 Token 放到 header 的 Authorization 字段后端拦截器校验这个字段的签名和过期时间。这部分代码是面试常问场景建议你自己动手写一遍 Token 生成和校验的逻辑而不是直接复制源码因为原理懂了换成 JWT 或者 Spring Security 都是分分钟的事。4.3 商品列表的分页与筛选商品查询接口在后端是最常用的。参数接收 pageNum、pageSize、categoryId、keywordService 层用 MyBatis-Plus 的 LambdaQueryWrapper 做条件组装categoryId 非空就加等值条件keyword 非空就对商品名做 like 模糊匹配。分页用 MyBatis-Plus 分页插件配置方法很简单在 config 包里注册一个 MybatisPlusInterceptor添加 PaginationInnerInterceptor。数据库方言会自动识别 MySQL。这里有一个容易踩的坑如果你用了分页插件但查询没有生效大概率是忘了在配置类里把这个拦截器声明成 Bean或者 MyBatis-Plus 版本和 SpringBoot 版本不匹配。另外分页查询返回的总条数插件是通过自动执行一条 count 查询拿到的如果列表本身已经查出数据但 total 一直不对检查一下 Wrapper 里是否把排序字段写到了 select 中。4.4 下单事务一个注解搞定多表一致性下单操作涉及的数据表多、改动大是系统里最核心的方法。我以购物车勾选的商品生成订单为例把流程拆解给你看根据用户 id 和购物车勾选状态查出要下单的商品列表。计算订单总金额。生成订单号插入订单表。遍历商品列表逐条插入订单明细表。扣减每个商品库存同时增加销量。删除购物车中已下单的记录。返回订单号给前端。这段逻辑看起来不难但难点在于如果第四步插入明细失败第三步的订单记录就必须回滚否则就会出现只有订单没有明细的脏数据。解决办法是在 service 方法上标注 Transactional让整个方法处于一个事务中任何一步抛异常数据库操作全部回滚。Spring 的声明式事务就是干这个的。我最初写完这段时出现过一个问题自调用导致事务失效。也就是说同一个类里的 A 方法调用 B 方法B 上标了 Transactional 不会生效因为 Spring 事务是通过代理实现的自调用不走代理。如果你在写代码时发现事务没有回滚优先检查是不是自己类内部调用了事务方法。这个问题我在答辩前踩过分享出来帮你省一次排查时间。4.5 库存扣减怎么防止超卖下单扣库存很多人想当然地写成三步先查库存数量判断是否够再执行 update。这个逻辑在并发场景下会出大问题两个用户同时查询到库存都是 1都判断够然后都用 update 把库存改成 0但两个订单都下单成功了这就是超卖。我的做法是用带条件的 UPDATE 语句一步完成扣减UPDATE t_product SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{productId} AND stock #{quantity}执行后判断受影响行数如果为 0说明库存不足直接抛出业务异常事务回滚。这种用数据库行锁保证原子性的做法在并发量不大的毕设系统里是可靠且简洁的方案。如果你之后想体现更高的技术水平可以再提乐观锁版本号或者 Redis 预扣库存的方案那就是扩展方向了。5. 前端 Vue 实现页面组织、组件封装与状态管理前端工程我用的 Vue 3 Vite Element Plus。相比 Vue 2 的 Options APIVue 3 的组合式 API 写起来更清爽而且 Element Plus 的组件比较好看省了自己堆 CSS 的精力。目录结构上views 目录按模块组织Home、Category、ProductDetail、Cart、Order、User、Admin 等router 里配置对应的路由规则。5.1 axios 封装统一处理请求前端写接口请求如果每个页面都手动写 axios代码会非常零散。我在 utils 目录下封装了一个 request.js创建 axios 实例时设置 baseURL 指向后端地址http://localhost:8081/api同时设置超时时间为 10 秒。请求拦截器里从 localStorage 取 Token存在就加到 header 上。响应拦截器里做两件事第一后端返回的 code 不是 200 时用 Element Plus 的 ElMessage 弹出后端返回的错误信息第二如果 code 是 401说明 Token 过期或未登录清掉本地状态并跳转到登录页。这样封装完之后页面里调接口就变得非常简洁比如获取商品列表request.get(/product/list, { params: { pageNum, pageSize, keyword } }).then(res { // res.data 就是封装好的数据 })5.2 用户登录态和购物车状态管理登录态我放在 Pinia 的一个 store 里维护 userInfo 和 token 两个字段登录成功后把后端返回的数据写入这两个字段同时持久化到 localStorage。这样页面之间切换时组件的导航栏能实时显示欢迎 xxx还是请登录不需要每进一个页面重新请求用户信息。购物车状态我也放在 store 里维护一个 cartList 数组。加购操作时先调用后端接口把数据写入数据库成功后更新本地数组。为什么不是只存本地因为用户换一台设备登录或者清除浏览器缓存购物车数据不应该丢。前后端各存一份展示以本地状态为准持久化由后端完成这种策略在毕设里够实用也够合理。5.3 路由守卫实现登录拦截和权限控制前端路由不是每个页面都能直接访问的比如购物车、个人中心必须登录后才能看管理端必须管理员才能进。我用 Vue Router 的全局前置守卫每次跳转前检查目标路由的 meta 字段如果 meta.requiresAuth 为 true 且本地没有 Token就 next 到登录页并带上 redirect 参数登录成功后跳回原始页面。如果 meta.role 是 admin就再校验 store 里的 role 字段。前后端同时做权限控制非常重要前端做路由守卫是为了用户体验让无权用户直接看到无权限页面后端做拦截器才是真正的安全防线防止有人绕过前端直接调用接口。这个前端控制展示、后端控制安全的思路在答辩时值得展开讲。5.4 组件化拆分与页面复用写页面时我尽量把可复用的部分抽成组件。比如商品卡片组件在首页推荐、分类列表、搜索结果三个地方都用到了那就把商品名称、价格、图片、销量这些展示逻辑封装成一个 GoodsCard 组件父组件只需要传一个商品对象进去。再比如分页组件列表页都有分页需求我把分页逻辑封装成公共组件内部直接用 Element Plus 的 Pagination 组件父组件监听页码变化事件去重新拉数据。这种组件化改造看起来费时间但对理解前端工程化的帮助巨大。你可以在源码基础上试着把另一个模块也改成组件复用把这个过程写进开发日志这就是优秀的项目复盘素材。6. 本地环境搭建与部署流程从零到跑通全记录源码拿到手第一步是先把项目跑起来跑了之后才能真正看懂代码、再做修改。这个环节也是很多初学者卡壳最多的地方问题往往不是代码本身而是环境没配好。我把整套环境的安装和配置过程按顺序写出来你照着一步步来就行。6.1 JDK 与 Maven后端是 Java 项目先装 JDK 8 或 11。装完之后在终端执行java -version能显示出版本号才算成功。如果你之前看教程装过 JDK 但命令不识别大概率是环境变量没配上去系统变量里加 JAVA_HOME 并把%JAVA_HOME%\bin加入 Path 即可。Maven 的作用是管理第三方依赖。SpringBoot 项目启动时会从 Maven 中央仓库下载一大堆 jar 包国内网络直连中央仓库经常超时我强烈建议把 localRepository 指定到本地目录并把仓库地址换成阿里云镜像。在 settings.xml 里加上mirror idaliyunmaven/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror配好镜像后依赖下载速度会快非常多否则项目启动光等依赖就能把人等崩溃。6.2 MySQL 安装与初始化MySQL 我用的是 8.0 版本。Windows 环境下建议直接下载安装包按向导安装到选择认证方式时选 MySQL 8.0 的默认方式就行。安装完成后默认端口 3306默认用户名 root密码是你安装时自己设的。还要记住一个关键点连接数据库时 JDBC URL 要带上时区参数spring: datasource: url: jdbc:mysql://localhost:3306/ski_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码serverTimezoneAsia/Shanghai是用来解决数据库连接时报错The server time zone value Öйú±ê׼ʱ¼ä的问题。这个问题根源是 MySQL 8.0 默认时区与 JDBC 驱动不一致写法加在连接串里就能解决。数据库表结构的初始化很简单用 Navicat 连上本地 MySQL新建数据库名称建议与源码配置保持一致比如 ski_mall字符集选 utf8mb4然后右键数据库选择运行 SQL 文件选中源码里的 init.sql 执行六张表就建好了。你可以先查一下表是否都在SHOW TABLES;6.3 Node.js 与前端依赖安装前端工程需要 Node.js 环境我建议装 Node 16 以上的 LTS 版本。安装完成后用node -v和npm -v验证。因为 npm 默认源在国外同样建议换成国内镜像npm config set registry https://registry.npmmirror.com然后在项目前端目录下执行npm install这一步会安装所有前端依赖包括 Vue、Element Plus、Axios、Pinia、Vue Router 等。如果中途报错优先检查 Node 版本是否过旧或者删除 node_modules 目录重新执行一次安装。6.4 启动后端和前端先启动后端。用 IDEA 打开后端工程等 Maven 依赖加载完成找到主类 Application右键 Run。控制台出现 SpringBoot 启动成功的日志并且监听端口为 8081说明后端已经就绪。再启动前端。在终端进入前端目录执行npm run devVite 启动后控制台会打印出访问地址一般是http://localhost:5173浏览器打开就能看到商城首页了。此时你注册一个账号去加购商品、下单走一遍再把数据库里 role 字段改成 1重新登录就能进入管理后台。整个流程跑通项目就真正属于你了。6.5 端口冲突和常见连接报错两个最常遇到的问题第一后端启动报端口被占用说明 8081 端口已经被其他进程占了解决办法是改 application.yml 里的 server.port改成 8082 等都行第二前端请求后端连接超时检查后端是否启动成功以及 request.js 里的 baseURL 是否和本地后端端口一致。这两个问题属于环境类的经典问题排查思路一点也不复杂顺着链路查就行。7. 开发过程中踩过的坑与排查思路这个部分我本来想放在最后写但想想还是放在部署之后讲更合适因为很多坑是在运行时才暴露的。项目做出来不难难的是排查一堆莫名其妙的问题。下面这五个坑是我真实遇到的每个都写了排查过程你可以照思路走一遍能给你节省大量时间。7.1 跨域请求被前端拦截第一个坑几乎每个做前后端分离的人都会遇到。前端启动在 5173 端口后端在 8081 端口JavaScript 出于同源策略会拦截跨端口请求浏览器控制台报错CORS policy: No Access-Control-Allow-Origin header is present。解法有两种我选的是在后端写一个跨域配置类实现 WebMvcConfigurer 重写 addCorsMappings设置允许的来源、请求头和方法registry.addMapping(/**) .allowedOriginPatterns(*) .allowedHeaders(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600);这里要注意allowedOrigins 设置成*又在开启 allowCredentials 的情况下浏览器还是会拦截换成 allowedOriginPatterns 就不会报这个错。这也是一个很细微但很常见的坑。7.2 LocalDateTime 返回给前端格式不对项目中时间字段用的 LocalDateTime后端返回给前端默认序列化后是一长串数组格式前端没法直接用并格式化显示。解决思路是约定一个统一的时间格式在 application.yml 里配置 Jackson 的序列化规则spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这是全局生效的。如果你希望某些字段单独格式化可以在实体类的时间字段上标注JsonFormat(pattern yyyy-MM-dd HH:mm:ss)。我当时是两种都用上了为了保证前端各个页面拿到的时间字符串都是干净统一的。7.3 图片上传成功但页面不显示商品要传主图前端用 Element Plus 的 Upload 组件把图片传到后端后端保存到本地磁盘指定目录然后把保存路径返回给前端。一开始我发现上传接口返回路径没问题但浏览器访问该路径 404 图片不显示。根因是 SpringBoot 默认不会把磁盘上的自定义目录映射成静态资源 URL。需要在配置类里加一个虚拟路径映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file:D:/ski-mall/upload/); }这样前端图片地址写成http://localhost:8081/upload/xxx.jpg就能正常访问了。磁盘路径不要写在代码里写死我后续改成了在 application.yml 里配置自定义的 upload.path 属性代码通过Value读取方便部署时修改。7.4 前端传参后端接收不到这种问题主要出在 GET 请求和 POST 请求的参数传递方式没分清。GET 请求的查询参数后端用RequestParam接收前端要用 params 传POST 请求的 JSON 格式数据后端用RequestBody接收前端要用 data 传。如果你把 params 传到 POST 请求里后端用 RequestBody 接就会报参数缺失或者拿到空对象。排查方式比较直接打开浏览器开发者工具的网络面板看请求的 Payload 格式到底是 query string 还是 request payload然后对应修改后端的接收方式。我是在统一封装 axios 之后定义 get 和 post 两个方法内部按照规范传参从根上避免这类问题。7.5 MyBatis-Plus 的 updateById 更新不了某些字段这个坑很有隐蔽性。用户修改个人信息时调用了 updateById 方法发现密码字段没有更新。原因是 MyBatis-Plus 默认的更新策略是字段值为 null 时不更新如果你把前端传来的空密码赋值给了实体类的 password 字段它不会生成 update 语句。如果你的需求确实要把某个字段更新为 null可以在实体字段上加TableField(updateStrategy FieldStrategy.IGNORED)或者在更新前判断字段值。我当时的方案是自定义一个 update SQL明确写出要更新的字段而不是整实体传过去这样既灵活又不容易误更新。8. 答辩常见问题与二次扩展方向系统做完了论文也写了接下来就是你站在答辩教室、老师坐在对面翻你源码的时刻。结合我在校期间旁听及被问的经验把项目相关的高频问题整理一下你自己先对着答案过一遍。8.1 答辩时老师最喜欢问什么第一个问题是你这个系统的角色权限是怎么实现的。回答思路前端路由守卫控制页面可见性后端拦截器控制接口访问权限Token 里携带 userId 和 role核心逻辑在拦截器里校验。顺便把认证和授权的概念一起说清楚这道题基本稳了。第二个问题是订单超卖你怎么防止。回答思路不要用先查再改的老三样而是用单条 UPDATE 语句带库存判断条件受影响行数为 0 则抛异常回滚保障原子性。这里可以补充说生产中高并发场景会引入 Redis 预扣库存 MQ 异步处理订单毕设场景用数据库条件更新足够。第三个问题是前后端分离项目遇到跨域怎么办。回答思路同源策略背景、后端配置 CORS 映射、允许来源和方法、注意 allowedOriginPatterns 和 allowCredentials 的兼容问题。第四个问题是为什么用 MyBatis-Plus 不用原生 MyBatis。回答思路MyBatis-Plus 提供单表 CRUD 方法免写 XML内置分页插件LambdaQueryWrapper 让条件组装更安全直观但多表联查依然可以使用自定义 XML。8.2 从毕设到项目可以怎么扩展源码本身是可跑的但如果你想把它写进简历或者让答辩档次更高可以考虑下面几个方向加 Redis 缓存把商品列表和首页推荐数据做缓存减少数据库查询压力也可以把 Token 存 Redis控制会话的过期与踢出。实现支付模拟在订单流程中增加去支付步骤前端展示二维码页面后端用字符串模拟支付回调支付成功后订单状态从待付款变成待发货。这能让你把异步回调、状态机更新讲清楚。接入 Excel 导出管理员把订单列表导出成 Excel后端用 EasyExcel 或者 POI 生成文件解决 poi word 能不能生成图表这类延伸问题。做一个小程序端复用后端接口用 uniapp 写个简单的小程序商城页面只需要展示商品和下订单工作量可控简历里可以写多端覆盖。8.3 我个人做完这个项目的体会最后聊一点不是技术的东西。整个系统从前端页面到后端接口到数据库表是我一个人从零到一写出来的中间确实被各种报错折磨过不少次尤其是前几次跨域问题、时区问题、事务不生效问题每次都要翻资料、打断点、一步步确认才解决。但恰恰是这些排错过程让我真正把知识内化了。一开始抄别人的代码是能跑可一旦需求变动你就得理解每一处逻辑才能改得动这种被需求逼着改代码的体验才是学习的关键。如果你手里有这套源码我的建议是先完整跑一遍再照着这篇拆解把每个模块的设计理由过一遍然后挑一个环节自己动手改比如增加一个优惠券功能或者把商品模块改成多图上传。改完你就知道毕设的真实难度从来不是技术选项有多高级而是你有多了解自己写的每一行代码。这套源码里的命名、注释和表结构都是按实际开发习惯做的数据库连接信息我改成了通用 localhost 配置你拿到后只需要按第六节的内容把环境和数据库配置好就能直接启动体验。希望这篇文章能帮你在毕设路上少走点弯路也祝你答辩顺利。