1. 项目概述:一套能直接跑起来的完整前后端分离方案
最近毕业设计季和课程设计扎堆,我身边好几个学弟学妹都在做校园电商类项目,但大多数卡在了同一个地方:代码东拼西凑能跑起来,但一问到“为什么这么设计”“上线部署怎么搞”,直接就懵了。与其反复给别人讲思路,不如把一套我实际维护过的前后端分离校园网上店铺系统完整拆开来讲,技术栈就是标题里写的这套——SpringBoot + Vue + MyBatis + MySQL,从数据库设计到接口开发,从页面渲染到服务器部署,一条线全走通。
先聊清楚这个系统到底是干嘛的。它的定位是“校园内网电商平台”,解决的是高校里二手书籍、生活用品、校园超市商品交易的需求。核心业务围绕三个字展开:逛、买、管。学生可以逛商品、加购物车、下单支付(这里通常对接模拟支付或校园卡接口),管理员可以在后台管理商品上下架、处理订单、查看销售统计。和普通电商项目最大的区别在于它的业务范围限定在校园内,用户角色简单,交易流程比淘宝京东轻得多,非常适合用来做毕设或者项目实战练手。
这个项目适合谁?两类人。第一类是为毕设、课设发愁的同学,你需要一个逻辑完整、代码清晰、能写进论文里的系统——这篇文章会告诉你每一步为什么要这么做。第二类是刚学完Java和前端基础、想通过完整项目打通全栈的开发者——你不用从零造轮子,跟着这篇文章把架构思路吃透,再结合源码,会比纯看视频课收获大得多。
需要提前说明的是,下文中涉及的源码结构、部署步骤、参数配置,我会以这套系统最常见的标准实现方式来讲。你拿到手的代码可能在某些细节上和我这里描述的略有差异,但核心链路是通用的,理解了原理,改起来非常快。
2. 技术选型的逻辑:为什么偏偏是这四件套
先把技术栈拆开看,每一层都有它不可替代的理由,也有它绕不开的坑。我自己维护这个项目一年多,对这四件套的体会比写代码时深得多。
2.1 前端:Vue 2还是Vue 3,别选错
这套系统前端用的是Vue 全家桶:Vue作为核心框架,Vue Router管页面跳转,Vuex(或Pinia,取决于你用Vue 2还是Vue 3)管全局状态,Axios管接口请求。如果你拿到的源码是Vue 2版本,那Element UI就是最顺手的组件库;如果是Vue 3版本,那Element Plus基本是标配。我的建议是:课程设计用Vue 2 + Element UI完全够,且生态最稳定;打算找前端工作相关的项目经验,就上Vue 3 + Element Plus + Vite。
很多人会在脚手架工具上卡住。Vue 2时代用Vue CLI,vue create一下搞定;Vue 3时代官方推荐Vite,速度确实快——冷启动几乎秒开,热更新也流畅得多。但Vite的坑在于配置代理、环境变量这些和Vue CLI略有差异,下文我会专门说。
2.2 后端:SpringBoot为什么舒服
SpringBoot解决的核心问题是“配置地狱”。没有它之前,搭一个SpringMVC项目要做的事包括但不限于:写web.xml、配置DispatcherServlet、配数据源、配事务管理器、配一堆XML……SpringBoot把这些全干掉了,约定优于配置,你只需要一个@SpringBootApplication注解就能启动一个内嵌Tomcat的Web服务。
我在实战里体会最深的一点是:SpringBoot把“启动一个后端服务”这件事的复杂度降到了一个几乎不需要动脑的程度。这对学生项目来说极其重要——因为你的精力应该花在业务逻辑上,而不是浪费在让Tomcat正常跑起来这种破事上。另外SpringBoot的生态整合能力很强,接MySQL、接Redis、接消息队列,都有对应的starter,一行依赖搞定。
2.3 持久层:MyBatis还是MyBatis-Plus
这个选择值得单独说。原版项目往往用的是MyBatis原生,写XML里的SQL,一个表一套增删改查模板。这个方案的好处是SQL可控,适合你写论文时画“数据访问层设计”的图;坏处是写起来真的啰嗦,一张表5个字段就要写20行XML,10张表就是200行。
我强烈建议你拿到源码后,再看看能不能升级成MyBatis-Plus。它不是一个新框架,而是在MyBatis基础上做的增强,核心价值在于:单表CRUD不用写SQL了,一个BaseMapper接口直接继承,selectPage、selectList、updateById这些方法开箱即用,代码量直接砍掉一半。保留MyBatis原生的XML能力,复杂多表查询依然自己写SQL。如果你想跟面试官聊“MyBatis缓存机制”“动态SQL原理”,底层知识依然是通用的,这点不用担心。
2.4 数据库:MySQL就完事了
校园店铺系统的数据量级,MySQL的承受能力绰绰有余。我见过有人非得上PostgreSQL、MongoDB,说实话没必要。MySQL 5.7还是8.0,我推荐直接上8.0,因为:8.0的窗口函数、CTE写法更现代,且默认字符集是utf8mb4,存Emoji和生僻字不会乱码。5.7的坑在于字符集要自己配utf8mb4,时区问题也容易出幺蛾子——这一点下面的部署章节我会重点讲。
当然,MySQL本身只是个存储引擎,真正考验数据库设计的是表结构本身。这个项目的表怎么设计,直接决定了订单、商品、用户三个核心模块的代码复杂度,我把表结构的设计逻辑单独拿出来讲。
3. 数据库设计:一张用户表怎么拆出角色权限
校园店铺系统的数据模型不需要像电商巨头那么复杂,但核心表之间的关联关系必须理清。我见过不少同学做的版本里,订单表里直接存了一个“商品快照”字符串,这是典型的坏味道。下面我按表的角色来拆解设计思路。
3.1 用户与角色:单表设计还是分表
用户表是最基础的。一张user表至少要有这些字段:id、username、password、nickname、avatar、phone、role、status、create_time。注意role字段,我用的是tinyint类型,0表示学生用户,1表示管理员,2表示店铺商家(如果系统支持多商家入驻的话)。为什么不单独建一张role表再用中间表关联?因为校园店铺系统的角色就三种,固定不变,你用RBAC那套设计反而把简单问题复杂化。这里的原则是:权限模型跟着业务复杂度走,业务固定就简单化,业务未来可能会扩展才需要上RBAC。
密码存储必须做哈希处理。我见过不少低质量源码里直接明文存密码的,这是重大的安全隐患。用Spring Security的BCryptPasswordEncoder,或者至少用MD5加盐。BCrypt是spring-security-crypto里现成的工具类,加一份依赖就行。
3.2 商品与分类:一对多怎么设计最合理
商品表product的核心字段:id、category_id、name、subtitle(副标题)、main_image(主图)、detail(富文本详情)、price(价格,用decimal(10,2))、stock(库存)、status(上下架状态)、create_time、update_time。分类表category就是最标准的父子结构:id、parent_id、name、sort_order。二级分类足够覆盖校园场景,比如“图书教材”下面分“二手教材”“考试教辅”,“电子产品”下面分“手机”“笔记本”“外设”。
商品主图这里有个坑:很多人直接把图片存到MySQL的blob字段里,或者用Base64塞进JSON返回给前端,这全是坏实践。正确的做法是:图片文件上传到服务器本地目录或对象存储,数据库里只存访问路径URL。本项目中我建议把上传目录配置成/data/upload,然后通过SpringBoot的静态资源映射把这个目录映射到/upload/**,这样前端直接<img src="/upload/xxx.jpg">就能访问。如果你做到部署阶段用Nginx,更进一步的做法就是由Nginx直接托管这个目录,性能更好。
3.3 购物车与订单:这里必须有一张订单明细表
购物车cart表比较简单:id、user_id、product_id、quantity、checked(是否选中结算),加一个唯一索引unique(user_id, product_id),防止同一个用户反复添加同一商品时产生多条脏数据。
订单这块是最容易设计错的。很多人只建一张order表,把购买的商品ID和数量用逗号拼成一个字符串塞进order_item字段,这是大忌——后续要统计“哪本书卖得最好”“这个月销售额怎么构成”,你全得靠字符串解析。正确的做法是拆两张表:order主表存订单全局信息:order_no(订单号)、user_id、total_price、status、create_time、pay_time、consignee、shipping_address、phone;order_item明细表存每个商品条目:order_id、product_id、product_name(快照)、product_image(快照)、current_price(成交价)、quantity。为什么明细表要存商品名和价格快照?因为商品表里价格会变,名字也可能改,但订单一旦生成,用户看到的信息必须锁定。这就是“快照”的意义。
status字段我用tinyint:0待付款、1待发货、2待收货、3已完成、4已取消。这个状态流转要在后端的Service层写死,不能让前端随便传一个数字改状态,否则会出现“已取消的订单又变成待收货”这种逻辑漏洞。
4. 后端核心实现:从Controller到Mapper的一层层设计
后端这部分是实现的重点,我按照分层架构的调用顺序,从入口到数据库一层层捋,每层说清楚“为什么要这么写”以及“常见坑在哪里”。
4.1 工程结构:包名怎么分包才能让论文好写
一个清晰的包结构,比代码本身更能体现你的设计能力。我的习惯是:
com.campus.shop ├── controller(控制层) ├── service(业务层接口) ├── service.impl(业务层实现) ├── mapper(数据访问层接口) ├── entity(实体类) ├── dto(前端传参封装) ├── vo(返回给前端的视图对象) ├── config(配置类) ├── common(通用工具、统一返回结果、异常处理)这里有几个细节请你留意:Controller只做“参数接收和结果返回”,不做任何业务判断;Service层写业务逻辑,事务注解@Transactional打在业务方法上;Mapper接口只定义方法,具体的SQL写在resources/mapper/*.xml里。这种分层带来的直接好处是,论文里的“系统架构图”和“模块设计图”你可以直接照着包结构画,答辩时老师问“你这个设计好不好”,你能说出每一层的职责边界,分数不会低。
4.2 统一返回结果:前端为什么不用判断各种状态码
前后端分离的项目,接口返回格式必须统一。我定义的统一返回类是Result<T>,包含三个核心字段:code(状态码,200表示成功,500表示业务异常,401表示未登录)、msg(提示信息)、data(泛型数据)。配合全局异常处理器@RestControllerAdvice,后端代码里不需要到处写try-catch,业务层抛出业务异常,全局处理器统一捕获、统一转成Result返回。这样做前端拿到的永远是固定结构,处理起来极其舒服。
也许你会问,直接用HTTP状态码不就行了?HTTP状态码是给浏览器用的,但前后端分离的接口返回是一个“业务信封”,你的业务错误码和HTTP状态码没必要一一对应。比如“用户名已经存在”在业务上是一个预期内的错误,你返回HTTP 200、业务code 500、msg写清楚原因,前端处理起来更顺手。这一点在面试里聊到“如何设计接口规范”时也算一个小亮点。
4.3 用户登录与鉴权:JWT还是Session
校园店铺系统涉及多角色(学生/管理员),接口必须做权限控制。我不会推荐用Session,因为前后端分离下Session面临跨域、移动端复用等问题。我用的方案是JWT(JSON Web Token):用户登录成功后,后端用密钥生成一个包含用户ID、用户名、角色信息的Token返回给前端。前端把它存到localStorage里,每次请求在HTTP头的Authorization字段带上。后端写一个拦截器(HandlerInterceptor)或者直接用Spring Security的OncePerRequestFilter,校验Token合法后把用户信息放进ThreadLocal或Request作用域。
JWT的坑在于:注销/修改密码后,旧的Token在过期前依然有效。校园项目一般不需要处理这个极端场景,但你要是想写在论文的“未来展望”里,可以提一句“引入Redis黑名单机制实现Token即时失效”,这比写一堆空话实在。
另外,跨域问题CORS一定要在后端正确处理。写一个WebMvcConfigurer实现addCorsMappings,允许http://localhost:8081(前端开发服务器地址)跨域访问,允许的请求方法写全GET, POST, PUT, DELETE, OPTIONS。不配跨域,你在前端开发时发接口请求必挂,这是新手踩得最多的坑之一。
4.4 MyBatis的XML里:动态SQL和返回映射
MyBatis的核心配置在resources/application.yml里三件事:数据源(spring.datasource.url、username、password)、Mapper接口扫描(@MapperScan("com.campus.shop.mapper"))、XML位置(mybatis.mapper-locations=classpath:mapper/*.xml)。这三行不配对,项目直接启动报错。
XML里的动态SQL是你真正的武器。比如商品列表的分页查询,条件可能是分类ID、关键字、价格区间,这些条件用户可能传也可能不传。你用<where>标签配合<if test="...">动态拼接SQL,前端传什么就拼什么条件,比写死三条SQL优雅得多。商品列表要分页,用PageHelper插件——一个PageHelper.startPage(pageNum, pageSize)后面紧跟着的查询就会自动带上LIMIT。注意PageHelper有坑:它生效是有ThreadLocal的,所以startPage后面必须紧跟你要分页的那一条Mapper查询,中间不能插入其他查询,否则分页会作用到错误的SQL上。
另外一个高频问题:MyBatis的驼峰映射。数据库字段一般是create_time,Java实体类是createTime,如果你的application.yml里没有配map-underscore-to-camel-case: true,查询结果里createTime永远是null。这行配置我建议直接写进MyBatis的核心配置里,配置完后全局生效。
5. 前端核心实现:Vue的页面、路由和状态管理
前端这块从工程初始化到页面实现,我按自己实践的最佳路径来讲,不绕弯路。
5.1 项目初始化与目录结构
用Vue CLI或Vite创建项目后,src目录下我会这么组织:
src ├── api(所有接口请求封装,按模块分文件) ├── assets(静态资源) ├── components(通用组件) ├── router(路由配置) ├── store(Vuex/Pinia状态管理) ├── views(页面级组件) ├── utils(工具函数:axios封装、Token存取、格式化) ├── App.vue └── main.js接口请求封装是前端整洁度的关键。我写一个utils/request.js,在里面创建Axios实例,设置baseURL、超时时间,用拦截器统一处理:请求拦截器里从localStorage取出Token加到Header,响应拦截器里统一处理HTTP错误和业务code——code是200就返回response.data.data,不是200就弹出错误提示。这样所有业务页面里调接口只需要写:
// 例如商品列表页 import { getProductList } from '@/api/product' const res = await getProductList({ pageNum: 1, pageSize: 10 }) this.productList = res.list不需要每个页面都写一遍“取Token、拼请求头、处理错误”,这是提升开发效率最明显的一步。
5.2 路由与导航守卫:未登录就逛不了店
路由配置里,/重定向到/home首页,商城页面是/home、/product/:id、/cart、/order/confirm、/order/list,管理后台规划一个独立的子路由模块/admin,包含商品管理、订单管理、用户管理等页面。
页面守卫必须做。我用Vue Router的beforeEach全局前置守卫:判断目标路由是否requiresAuth,如果是,看看localStorage有没有Token,没有就跳转到/login。同时区分Admin模块——Token对应的用户角色不是管理员,直接跳404或者提示无权限。这里补充一点:导航守卫只能控制前端页面的展示,真正的权限控制永远在后端接口的鉴权逻辑里,前端守卫只是用户体验层面的过滤而已。
5.3 购物车与状态管理:全局数据放哪
购物车数量这个数据,在头部导航、购物车页、结算页都要用到。如果每个页面都自己调接口拿一遍,会出现“在购物车页删了一件商品,头部角标还在显示老数量”的尴尬。用Vuex/Pinia管理这个问题就解决了:cartCount放在全局state里,任何页面通过mutation/action修改后,所有引用它的地方自动更新。我遇到比较多的新手错误是,在几个组件里各写各的data,导致状态不同步,最后写满了$emit和$bus还是乱。出现这种苗头时,请马上迁到状态管理。
5.4 组件库和页面实现要点
管理后台的表格、表单、弹窗这些,你没必要手写一个轮子。Element UI/Element Plus的表单校验、分页表格、消息提示,开箱即用,非常省时间。商城前台的部分,商品卡片、轮播图这些简单的组件,自己写CSS实现反而更灵活。一个常见场景:商品详情页的图片预览,用el-image配合preview-src-list属性就能实现点击放大。
如果你打算“前端白屏也得搞定图片显示”,可以留意一个细节:Vue项目打包后访问图片路径404,通常是因为publicPath没配好。Vue CLI的项目在vue.config.js里设publicPath: './',Vite项目在vite.config.js里设base: './',否则打包部署到服务器子目录时,资源加载路径会指向根目录导致404。
6. 部署上线:从本地开发到服务器可访问的完整流程
开发完成只是走完一半,能不能部署到服务器、让同学通过浏览器访问,才算真正“交付”。这一段我按照Linux服务器+Caddy/Nginx的传统路径讲,步骤可以照着一步步操作。
6.1 前端构建与产物处理
前端部署的本质是:把源代码用构建工具打包成静态文件(HTML、JS、CSS),再让Web服务器托管这些文件。
用Vue CLI的项目,在项目根目录执行:
npm run build构建产物生成在dist目录下。里面有一个index.html,一个static或assets目录(随构建配置不同而不同)。这个dist目录整个拷贝到服务器,用Nginx指过去就行。深入一步说,dist里的JS文件是经过压缩混淆的,但你最好手动检查一下index.html里引用的资源路径是不是相对路径——如果你的站点部署在域名根路径(如https://myshop.example.com),绝对路径/assets/xxx.js没问题;如果部署在子路径(如https://myshop.example.com/shop/),绝对路径就会全部404,这就是上一节提到publicPath的坑最直接的体现。
另外关注一下history模式刷新404。Vue Router的history模式(URL路径不带#号,比如https://myshop.example.com/product/1),直接刷新这个路径时,Nginx会去找服务器上不存在这个文件,返回404。解决办法是在Nginx配置里做try_files回退:
location / { try_files $uri $uri/ /index.html; }如果你不想处理这个配置,另一种方案是用hash模式(URL带#),路由路径变成https://myshop.example.com/#/product/1,刷新时不会请求服务器路径,自然没有404问题。hash模式牺牲一点优雅度,换来部署零配置,做课程设计完全可以用。
6.2 后端打包与进程守护
后端打包有两件事:打成Jar包、传到服务器。SpringBoot项目用Maven打包:
mvn clean package -DskipTests在target目录下得到shop-0.0.1-SNAPSHOT.jar。把Jar包传到服务器,用java -jar启动是最基础的做法。但直接java -jar有两个问题:一是终端一关进程就死,二是如果Jar包运行崩溃没有自动拉起机制。
我的建议是用systemd管理服务。在/etc/systemd/system/shop.service里写服务配置文件:
[Unit] Description=Campus Shop Application After=network.target [Service] User=www WorkingDirectory=/data/shop ExecStart=/usr/bin/java -jar /data/shop/shop.jar --spring.profiles.active=prod Restart=always RestartSec=10 [Install] WantedBy=multi-user.target然后执行systemctl daemon-reload、systemctl enable shop、systemctl start shop。这样只要服务器不宕机,SpringBoot进程挂了会自动重启,而且开机自启,这对长期演示很有意义。
6.3 Nginx反代与前后端联调
生产环境我建议用Nginx把前后端整合在同一个域下:80端口监听,/api路径的请求反向代理到SpringBoot的8080端口,其余请求都指向前端dist目录的静态文件。
核心配置片段参考:
server { listen 80; server_name myshop.example.com; root /data/shop/dist; index index.html; location / { 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /upload/ { alias /data/upload/; } }这里有个小细节:proxy_pass http://127.0.0.1:8080;后面的路径是否有斜线,会导致转发路径的不同。不带斜线时,Nginx会把原始URI拼上去,比如/api/user/login变成http://127.0.0.1:8080/api/user/login;带了斜线(http://127.0.0.1:8080/),则匹配到的/api/部分会被替换成/,变成http://127.0.0.1:8080/user/login。所以后端接口的@RequestMapping前缀写的是/api时,proxy_pass就不加路径;如果后端接口路径本身已经标识出来了,两种方式都能配通,关键是你得知道自己后端的ContextPath是什么。
6.4 数据库初始化与数据迁移
数据库部署用两种方法:一是把本地MySQL导出SQL文件,上传到服务器执行:mysql -u root -p < shop.sql;二是用SpringBoot的spring.sql.init机制在启动时自动执行schema.sql和data.sql。生产环境我更推荐第一种,因为迁移文件独立可控,不会每次重启都执行一遍导致主键冲突。
MySQL 8.0在服务器上安装后的第一件事:检查字符集。在/etc/my.cnf里配置:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci不设置的话,默认latin1会导致中文乱码。另一个常见坑是时区问题——连接串里必须带上serverTimezone=Asia/Shanghai:
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/campus_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=falseuseSSL=false很关键,因为本地开发MySQL通常没配SSL证书,连接时默认尝试SSL握手会报Communications link failure或者SSL connection error——热词里“mysql ssl连接错误”应该就是这个问题。这个坑我踩过不止一次。
7. 常见问题与排查技巧实录
这部分整理我在部署和维护这套系统时最常遇到的几个问题,每个都是切切实实花过时间排查的,按“症状-原因-解决办法”列出来,方便你遇到时直接查表。
7.1 SpringBoot启动失败排查路径
启动失败的原因五花八门,但95%集中在以下几类:
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
Field userMapper in ... required a bean of type '...' | 忘记加@MapperScan或@Mapper注解 | 在启动类加@MapperScan("com.campus.shop.mapper") |
Table 'campus_shop.user' doesn't exist | 数据库连接对但库没初始化 | 执行SQL脚本初始化库表 |
Access denied for user 'root'@'localhost' | 数据库密码错误或账号权限不足 | 检查application.yml账号密码,或给账号授权 |
Port 8080 was already in use(端口被占用) | 之前的进程没杀干净 | `netstat -tlnp |
Caused by: java.sql.SQLException: The server time zone value ... | 连接串少serverTimezone | 按上文配置时区参数 |
排查思路有一个顺序:先看application.yml配置对不对,再看数据库表和账号有没有,最后看端口和依赖冲突。SpringBoot的启动日志已经把90%的错误原因打出来了,不要只截图不读日志。
7.2 前端接口请求失败的两种典型场景
第一种是开发环境跨域报错。浏览器Console报Access to XMLHttpRequest at 'http://localhost:8080/api/...' from origin 'http://localhost:8081' has been blocked by CORS policy——这就是后端没配CORS。解决方式:后端加@CrossOrigin(Controller类级别)或者配全局CORS配置类。我在项目里的习惯是全局配置,避免每个Controller写一遍注解。
第二种是生产环境404或502。部署后浏览器打开页面白屏,F12看Network——/api/xxx请求返回404,说明Nginxproxy_pass路径配置不合理;返回502,说明后端进程挂了或者没启动。一次次排查的顺序是:先确认后端Jar包进程在不在(ps aux | grep java),再确认端口通不通(curl http://127.0.0.1:8080/api/health),最后才查Nginx配置。
7.3 MyBatis相关的高频坑
如果Mapper方法执行后一直返回null或空列表,首先查SQL日志。在application.yml里配置:
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台会打印完整的SQL语句和参数。我遇到的Case是:明明数据库有数据,查询结果却是空——原因是实体类字段名和数据库列名对不上,又没有开启驼峰映射。还有一种:<if test="categoryId != null">一直不生效,排查后发现前端传的参数名是category_id,而实体字段是categoryId,对象没映射上。这类问题一律靠打印SQL日志定位。
如果SQL日志显示正常,但结果还是不对,那就看返回映射的resultMap——最经典的问题是把resultType写成了resultMap,XML直接报错;或者是写了resultMap但没配autoMapping="true",导致有些字段映射不了。这种问题用“逐个字段检查”法:先用SELECT *确认数据库列名,再对着实体类字段名逐一比对,一定找得到差异。
7.4 部署后图片不显示的定位思路
图片不显示要先确定问题在哪一层。打开浏览器Network,看图片请求的URL是什么状态码:
- 404:路径不对。要么是数据库存的路径和Nginx托管路径对不上,要么是
location /upload/的alias路径配错。 - 403:文件权限问题。Nginx运行用户(通常是
www-data或nginx)没有读取目录权限,需要chmod -R 755 /data/upload。 - 200但图片裂了:文件本身损坏,可能是上传时被中断,或者上传接口写入不完整。
这里顺带说一下上传组件的实现建议:前端用el-upload,后端接收MultipartFile,存储路径按日期分目录,文件名用UUID避免中文乱码和重名覆盖。数据库存相对路径/upload/2026/06/xxx.jpg,展示时前端拼上后端域名或Nginx配置的路径。这个设计虽然朴素,但比起存在数据库里,维护性高得多。
7.5 订单状态异常的排查逻辑
如果用户下单后订单状态一直停在“待付款”不变,或者支付后状态没推进,大概率是状态流转逻辑没触发。以最常见的模拟支付为例:前端调支付接口,后端创建支付流水后更新订单状态为“待发货”。排查路径:看后端日志,确认支付回调或支付接口有没有被调用;看订单表的pay_time有没有写入;看Service层的update是否带了@Transactional——如果事务没提交,状态改了但数据库没变,也是常见根因之一。
另一个隐性问题是购物车库存不足校验。下单时前端只传了商品ID和数量,后端Service必须重新校验库存是否充足,否则会出现“超卖”。我的实现里会在事务里SELECT ... FOR UPDATE锁住商品行,再判断库存扣减,防止并发下单库存变负数。这一块写进论文里是很漂亮的“并发控制”案例,面试也能聊。
8. 扩展思路:怎么让这个系统在答辩时显得更完整
课程设计或毕设答辩时,老师不只听你说“功能做出来了”,更关注你有没有思考过“如果用户量变大、需求变多,这些功能怎么演进”。我这里抛几个经过验证的扩展方向,供你在论文和演示里补充。
第一个方向是引入Redis。Redis可以在两个场景创造价值:缓存和分布式Session/Token管理。商品详情页是热点接口,把商品详情缓存到Redis,设置过期时间,能显著降低MySQL压力。用Redis做Token管理,登录后把Token写入Redis并设置过期时间,配合拦截器校验Token是否存在,就能实现“退出登录/修改密码后Token立即失效”。代码量不大,但汇报时能体现你对性能和安全都有考虑。
第二个方向是引入搜索能力。当前商品搜索是LIKE '%keyword%',在几百条数据量下没问题,但告诉你一个事实:LIKE前置通配符会让索引彻底失效,数据多了以后全表扫描,你马上就会发现搜索慢。升级方案是引入Elasticsearch或轻量级的MeiliSearch。校园店铺系统用ES可能有点重,但论文里写“引入Elasticsearch + IK中文分词实现商品搜索”,比只写“MySQL的like查询”高一个档次。
第三个方向是支付方式对接。课程设计用模拟支付页面就够了,但能对接真实支付网关(比如接入第三方支付平台的沙箱环境)是亮点中的亮点。申请需要资质,但沙箱环境比较开放。对接的复杂度在于异步回调的处理和签名验证,这部分有真实的工程价值。
这些方向我不建议全部做进去,贪多嚼不烂。挑一个做深做透,答辩时能把这个点讲清楚,配合你亲手画的架构图和流程图,说服力远胜过罗列一堆功能名字。
9. 我最后想说的几句建议
这套系统,我在学生时代自己也完整做了一遍,后来工作了又带人重构过一版,前后踩过的坑比这篇文章里写到的多得多。如果你是按毕设/课设的标准来做这个项目,我以一个“过来人”的身份再叮嘱几句。
第一,不要拿到源码就直接跑起来然后什么都不管。哪怕你只认真读完这篇文章,再对照源码看一遍Mapper里的SQL和Service里的事务边界,你的收获都会翻倍。真正的核心不是“跑起来”,而是“说清楚为什么这么写”。答辩时老师问你“订单明细表为什么要冗余商品名和价格”,你能答出“快照,防止商品信息变更影响历史订单”这一句话,比堆砌十个技术名词都有用。
第二,一定要亲手从零部署一遍。开发环境跑通和你打包部署到服务器是两种完全不同的体验。你在本地改了代码热更新随便刷,部署后一次构建出问题你才知道生产环境的痛。至少完整走一遍“打包→上传→启动→配Nginx→浏览器访问”的流程,哪怕中途炸成一片,也是最有价值的学习过程。
第三,注意保留过程性材料。开发日记、Git提交记录、架构演进草稿图、接口调试截图、部署过程的报错与解决记录,这些在写论文和答辩材料时都是现成的素材。我见过太多人事后回想“我当时是怎么做的”脑子里一片空白,被迫重新回忆甚至伪造过程,非常痛苦。从第一天开始,就养成分阶段记录的习惯。
网页店铺也好,管理系统也好,本质上都是同一套技术栈在不同业务场景下的排列组合。你把这一套吃透了,换一个业务域,你能做的依然是产品列表、购物流程、后台管理这三板斧,但你已经知道每块板子为什么这么拼了。这也是全栈项目训练最核心的诉求。