前后端分离的在线家具商城,听起来像是个学生项目或者练手作品,但真完整做下来,你会发现它几乎涵盖了Web开发日常要用到的所有核心技能:SpringBoot接口开发、Vue前端交互、MyBatis数据持久化、MySQL表设计,还有一个完整商城该有的用户、商品、购物车、订单、支付(或模拟支付)流程。这篇文章就是来拆这套系统的,从设计思路到核心代码,再一路讲到部署,把能踩的坑也一并列出来。
这套项目形态非常适合两类人:一是刚学完SpringBoot和Vue,想找一个完整全栈项目作为毕设或者简历亮点的开发者;二是已经在做后端或者前端,想补一下另一侧技术栈的全栈手感。照着这套路子走一遍,你至少能梳理清楚一个问题——一个前后端分离的电商系统,从零到上线,究竟要经历哪些环节。
1. 项目整体设计与技术选型思路
1.1 为什么选择前后端分离架构
在做家具商城这类管理系统时,最早期的方案通常是模板引擎渲染,后端把数据塞进页面,前端只负责显示。这种模式在小项目里确实省事,但一旦页面交互变复杂,问题就很明显:前端想调整一个按钮位置,后端工程师要跟着改模板;接口返回的数据结构变动,前端页面就崩。前后端分离则把两者拆开,前端只通过接口拿数据,后端只管业务逻辑和数据库操作,各自独立开发、独立部署。
这套家具商城选择前后端分离还有一个很实际的考虑:商城类项目天然有PC端、管理端、甚至是未来移动端的需求。管理端和商城端虽然业务不同,但可以共用同一套后端接口,只要后端把权限控制做好就够了。如果后面前端想换成小程序,后端接口基本上不用动。
另一个好处是开发和调试的体验。开发阶段前端用Node服务跑Vue,后端用SpringBoot跑在8080端口,通过代理转发解决跨域问题,改前端页面不用重启后端,效率比传统模式高出不少。对这个项目的定位来说(毕设或练手),这种架构也更能体现“系统设计”的完整性和模块化思维。
1.2 技术栈选型的理由
SpringBoot作为后端基础框架,几乎是当前主流的选择。它最大的价值在于自动配置——很多原本需要手动显式配置的内容(数据源、事务、Web容器)都变成了约定,项目启动直接从main方法跑起来,不用再折腾繁琐的XML配置。家具商城这种规模的项目,SpringBoot的效率优势明显。
Vue选择的是Vue 2或Vue 3,两者在这个项目里差异不大。Vue的核心是组件化开发,把首页、商品列表、购物车、订单结算拆成一个个组件,数据通过props和事件或Vuex(如果是Vue 3可以用Pinia)在组件间流动。相比传统jQuery操作DOM的方式,Vue把数据和视图做了双向绑定,页面状态被修改后视图自动更新,省掉了大量手动拼接HTML的逻辑。
MyBatis作为持久层框架,主要是看中它可以直接编写SQL,对于复杂的多表查询(比如家具商品分类、订单明细、库存更新),SQL的可控性比全自动ORM更强。配合逆向工程生成实体类和Mapper接口,CRUD的样板代码量也能降到很低。
MySQL则不用多说,开源稳定、资料多、安装简单。缓存可以用Redis优化,但基础版这个项目不引入Redis也完全跑得通,对新手更友好。
1.3 项目模块划分
这套系统的整体结构可以分为两个端:用户端和管理端。
用户端功能包括:注册登录、浏览家具分类和商品、商品搜索、商品详情查看、加入购物车、购物车管理、订单提交、模拟支付、订单查询、个人信息管理。管理端功能包括:管理员登录、商品分类管理、商品管理(增删改查)、订单管理(发货、状态修改)、用户管理、数据统计看板。
从开发顺序上,一般是先做后端接口,再写前端页面,最后联调。后端的模块可以按权限分为两类:公开接口(注册登录、商品浏览)和鉴权接口(购物车、订单、个人信息),用拦截器统一校验登录状态。
2. 数据库设计与核心模块拆解
2.1 数据表设计
家具商城的核心业务流程不算复杂,但涉及的数据关系需要理清楚。整套系统的表我把它分成三组。
第一组是用户相关:用户表(user)。用户字段一般包括id、用户名、密码、昵称、手机号、邮箱、头像、角色(普通用户/管理员)、创建时间。密码不要明文存储,注册的时候用MD5加盐或BCrypt加密,登录时比对密文。
第二组是商品相关:分类表(category)和商品表(product)。分类表字段是id、名称、父级id、图标、排序。商品表字段包括id、分类id、名称、描述、主图、轮播图、价格、原价、库存、销量、状态(上架/下架)、创建时间。家具商品往往会有多个规格,比如颜色、尺寸、材质,如果要做得全一点,可以再加一个商品规格表,但基础版把规格信息直接拆到商品名称和描述里也能接受。
第三组是交易相关:购物车表(cart)、订单表(orders)、订单项表(order_item)。购物车表字段是id、用户id、商品id、数量、勾选状态。订单表是核心,字段包括id、订单编号、用户id、总金额、实付金额、收货人姓名、收货地址、联系电话、订单状态(待付款/已付款待发货/已发货/已完成/已取消)、创建时间、支付时间。订单项表记录订单中的每个商品快照,字段有id、订单id、商品id、商品快照名称、商品快照图片、商品快照价格、数量。
订单为什么要把商品信息做成快照而不是直接关联商品表?这是个细节,但很关键。商品的价格、名称随时可能被管理员修改,如果订单关联的是最新商品信息,用户下单后的订单记录就可能对不上,比如商家改了价格,用户历史订单里的金额就变了。快照的设计保证了订单交易的确定性。
2.2 数据库建模的注意事项
表设计阶段最容易犯的错就是字段类型和长度选得太随意。价格字段一定不要用float或double,电商场景下用decimal,精度至少decimal(10, 2),否则对账时会遇到浮点数精度问题。库存字段用int,检查库存时要避免出现负数,更新库存的SQL要在条件里加上stock >= 购买数量。
主键建议用自增id,虽然分库分表的时候不太友好,但这个规模的项目自增id最简单可靠。如果图省事用UUID做主键,整张表数据量大之后,B+树索引的随机写入性能会比较差,而且前端拿到id做路由的时候还需要处理字符串参数。
如果要预留扩展能力,可以用逻辑删除字段(deleted)替代物理删除,尤其是商品和订单表。用户下单后,订单数据是不能轻易物理删除的,保留删除标记既能规避这个问题,也方便后期做数据统计。
2.3 权限控制方案
权限用最传统的角色判断:用户表里加一个role字段,0表示普通用户,1表示管理员。后端接口统一使用Spring拦截器(HandlerInterceptor)进行登录校验,拦截器在进入Controller之前执行,从请求头中读取token,校验通过后把用户信息放入ThreadLocal或request attribute中,Controller里直接获取当前用户id。
对于管理员接口,可以在拦截器里额外判断用户角色,也可以单独在Controller层的注解上做控制。基础版项目用拦截器+角色字段就够了,不需要引入Shiro或Spring Security这种重量级框架,理由有两个:
一是学习成本。Spring Security虽然功能强大,但配置复杂,新手经常被它的过滤器链搞得晕头转向。二是项目需求简单,不需要细粒度权限(比如按钮权限、数据权限),一个角色字段足够。如果你后续想升级,再引入Spring Security/JWT也不迟,但起步阶段保持简单很容易。
3. 前后端接口对接与关键功能实现
3.1 后端代码结构
后端包结构我习惯这样组织:
- config:存放配置类,比如跨域配置、拦截器注册、MyBatis配置
- controller:接口层,负责接收请求和返回结果
- service:业务逻辑层,接口+实现类
- mapper:数据访问层(MyBatis的Mapper接口)
- entity:数据库实体类
- dto:前端传入的数据封装对象(比如注册信息、订单提交信息)
- vo:返回给前端的数据封装对象(比如购物车列表VO、订单详情VO)
- common:通用类,比如统一返回结果、异常处理、JWT工具类
这样分包的好处是依赖方向清晰:controller依赖service,service依赖mapper,数据模型统一从entity取,避免循环依赖的同时也方便团队协作。
统一返回结果类是接口设计里容易被忽略的环节。我习惯定义一个Result类,包含code、message、data三个字段,code为200表示成功,其他表示异常。所有接口都返回Result对象,前端拿到后根据code判断业务结果,再决定是展示数据还是弹出错误提示。这个类虽然简单,但统一之后,前端的请求封装会极其顺畅。
3.2 登录鉴权与用户身份传递
用户登录后需要一种机制让后端记住“我是谁”。最传统的是Session,但前后端分离项目里,前端域名和后端域名可能不同,Session配合Cookie的跨域处理比较麻烦,而且横向扩展时Session同步也是问题。所以建议直接用JWT(JSON Web Token)。
简单说,用户登录成功后,后端生成一个包含用户id和用户名的token字符串返回给前端。前端把它存在localStorage或Vuex/Pinia里,之后每次请求都在请求头里带上Authorization: token。后端拦截器解析这个Token,验证签名和有效期,解析出用户id放入请求上下文。
实现Token解析时有两个坑需要提前规避。一是在拦截器中解析Token失败的时候,要直接返回401状态码和对应的JSON,让前端能弹出“登录已过期”的提示,而不是继续执行Controller。二是Token密钥要独立写在配置文件中,不要硬编码在代码里,否则代码泄露后Token就可以被伪造。
3.3 商品列表与搜索
商品列表是用户端最重要的入口,需要支持按分类查看、关键字搜索、价格排序、分页显示。核心接口的设计一般是GET /api/product/list,参数包括:categoryId、keyword、page、size、sortField、sortOrder。
这里要注意分页插件还是手写SQL。MyBatis的分页可以用PageHelper,只要在查询前调用PageHelper.startPage(page, size),后面的查询就会自动拼上LIMIT。手写SQL则是用LIMIT #{offset}, #{size},前者方便但不完全透明,后者可控但需要自己算offset。为了学习效果,我建议基础版手写分页,这样能真正理解limit的作用,之后再用PageHelper也毫无障碍。
搜索功能不要一上来就上ElasticSearch,那是过度设计。MySQL的LIKE模糊搜索在这个数据量级完全够用。如果家具商品只有几千条,即使搜索条件加上两个LIKE,查询时间也基本在毫秒级。等数据量真正大了,再考虑全文索引或者搜索引擎也不迟。
3.4 购物车与订单流程
购物车表设计简单,但前端交互细节不少。接口方面要有:加入购物车、修改数量、删除、勾选、获取购物车列表。这里比较重要的是购物车列表返回数据结构,因为前端要展示图片和商品信息,所以连表查询是必须的。建议直接返回CartVO,字段包括购物车id、商品id、商品名称、商品图片、单价、数量、选中状态、商品库存(用于前端提示库存不足)。
购物车合并逻辑容易被忽略。用户没有登录时也可以往购物车加商品,但一旦登录,客户端的临时购物车数据应该能合并到服务端。这个功能如果要做,一般逻辑是:前端把本地购物车数据在登录成功后同步到后端,后端按用户id和商品id合并数量。
订单流程是整套系统的核心链路。提交订单的后端逻辑要用事务保护,大致步骤如下:
- 接收前端传来的订单信息(购物车商品列表、收货人、地址、电话)
- 根据购物车记录联查商品,确认商品存在且在售
- 计算总金额,对比前端传入金额(可选步骤,严谨的话要以后端计算为准)
- 检查库存是否充足
- 扣除商品库存
- 生成订单主表记录,状态设为待付款
- 生成订单项表记录,保存商品快照
- 清空对应购物车记录
- 提交事务,返回订单编号
这些步骤只要一步失败,前面的操作就必须回滚。比如库存扣了但订单生成失败,会导致库存无端减少,这就是必须加@Transactional的原因。在使用Spring事务时,注意一个问题:事务只对RuntimeException起作用,如果你在代码里catch了异常却没往外抛,事务是不会回滚的。
下单后模拟支付,状态从待付款改为待发货。这里可以做一个支付页,也可以直接一个“模拟支付”按钮。支付成功后的核心操作有两个:更新订单状态、累加商品销量。
3.5 管理端功能
管理端是典型的后台CRUD页面,开发者自己用起来比写用户端还顺手。商品管理需要实现:分页查询、新增商品、编辑商品、上架下架、删除(逻辑删除)。分类管理维护的是前端页面的分类导航栏。
订单管理则是后台核心,管理员需要按状态筛选订单,查看订单详情(用户信息、商品明细、总金额),并执行发货操作,发货后订单状态变为已发货,用户端就能看到物流状态。
管理端页面建议单独放在一个路由前缀下,比如/admin,与商城端分开。菜单栏放在左侧,内容区放在右侧,典型的后台管理布局。由于前后端分离,管理端接口必须做权限校验,或者说在拦截器中检查用户角色,非管理员访问管理接口一律返回403。
4. 前端核心实现与交互细节
4.1 项目初始化与路由配置
前端用Vue CLI或Vite初始化项目,技术栈包括Vue、Vue Router、Axios、Element UI(或Element Plus,看Vue版本)。Vue Router需要配置好路由表,页面级组件包括:首页、商品列表页、商品详情页、购物车页、登录页、注册页、个人中心页、订单列表页、订单详情页、结算页、支付结果页、后台管理页面。
路由需要做两件事:一是全局前置守卫,判断访问需要登录的页面时userInfo是否存在,不存在则跳转登录页;二是把后台管理相关的路由统一放在一个懒加载组件里,避免首屏加载过重。
Axios封装也很重要。统一的请求拦截器里给每个请求加上token;响应拦截器里处理统一返回结果,如果code不等于200,则弹出Message提示错误;如果HTTP状态码是401,则清除用户信息并跳转到登录页。这个封装可以省掉每个页面里大量的重复错误处理代码。
4.2 状态管理与购物车同步
Vue全家桶里的状态管理,Vue 2用Vuex,Vue 3用Pinia。购物车数量、用户信息这类会被多个组件共享的数据,应该放进全局状态里管理。比如首页导航栏要显示购物车商品总数,商品详情页加入购物车之后这个数字要立刻变,如果不用全局状态,就得靠事件总线或者刷新页面,很笨拙。
购物车商品数量和购物车详情是两套数据,要注意区分。导航栏的小角标只需要总数,所以可以在加入购物车、修改数量、删除这些操作后,用一个轻量接口重新获取购物车总数并更新到全局状态。完整购物车列表则进购物车页再拉取。
4.3 前端交互细节
商品卡片上的价格显示,建议处理后端返回的以分为单位的价格,或者后端直接返回元为单位的小数,前端用toFixed(2)保证显示两位小数。图片的懒加载可以借助Vue的指令或者Element组件内置属性,家具商城图片多,尤其首页和列表页,不要一次性把几十张高清图片全部加载完。
商品详情页的“立即购买”和“加入购物车”是两个不同的入口,立即购买要直接跳转结算页,但购买的商品数据是临时组装的一份,不能影响购物车原有数据。这里最常见的实现方式是点击立即购买时,前端单独维护一个buyNowItem对象,提交订单时和后端约定只处理这个商品而不是购物车列表。
5. 部署教程与踩坑记录
5.1 本地启动流程
整个项目从下载到运行,顺序是:先启动MySQL导入数据库脚本,再启动后端,最后启动前端。
具体步骤如下:
- 安装JDK(建议1.8或更高),配置JAVA_HOME
- 安装Maven,配置仓库镜像为国内镜像,加快依赖下载
- 安装MySQL,创建数据库,导入项目里的sql文件
- 修改后端application.yml中的数据库地址、用户名、密码
- 在项目根目录执行mvn spring-boot:run启动后端
- 确认8080端口访问后,进入前端目录
- 前端安装依赖npm install(或者yarn)
- 配置前端代理:开发环境下访问/api路径时,代理转发到http://localhost:8080,这样能绕开跨域问题
- 执行npm run dev启动前端开发服务器
- 浏览器访问前端地址完成联调
开发环境跨域最简单的方案是配置Vue CLI的devServer.proxy。比如前端运行在8081端口,后端是8080端口,如果不做代理,前端直接请求http://localhost:8080/api/xxx会被浏览器拦截。配了代理之后,前端请求/api/xxx,DevServer帮忙转发到后端,浏览器看到的是同源请求,就没有跨域问题了。
生产环境打包时,前端执行npm run build生成dist静态文件,这些文件一般交给Nginx托管,然后Nginx配置一个/api反向代理到后端服务。这里的关键是Nginx的location配置要同时处理好静态文件路由和前端路由history模式的页面刷新。
配置示例(示意):
server { listen 80; server_name localhost; # 前端静态文件 location / { root /home/www/furniture; 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这行非常关键,它解决的是Vue Router的history模式在刷新页面时返回404的问题。如果不配置,用户访问商城首页再刷新没问题,但直接刷新/shop/cart这样的路径,Nginx会去找对应的静态文件,找不到就404,加上try_files之后会统一回退到index.html,由前端路由接管。
5.2 后端部署
后端打包执行mvn clean package,生成jar包后直接java -jar xxx.jar运行。如果机器内存吃紧,可以加-Xms256m -Xmx512m限制堆大小。Linux服务器上建议用nohup放在后台运行,日志重定向到文件中方便排查问题。
如果要保证后端异常退出后自动重启,可以写一个systemd服务文件,或者用supervisor管理进程。这个项目阶段选哪个都行,重点是把端口占用、日志路径、进程启停命令搞清楚。
5.3 常见问题速查表
部署和联调阶段最容易踩的坑,我直接整理成了表格,方便你对照排查。
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 后端启动报数据库连接失败 | MySQL没启动或账号密码不对 | 检查MySQL服务,核对application.yml配置 |
| 前端请求接口一直报跨域 | 开发环境没配代理,或后端CORS没配置 | 开发环境配devServer.proxy;生产用Nginx反代 |
| 登录之后刷新页面就掉线 | token只存在内存,没有持久化 | 把token存到localStorage,刷新时重新读 |
| 管理员接口返回403 | 用户角色不是管理员或token未携带 | 检查登录账号角色,确认前端请求头带Authorization |
| 前端npm install非常慢或失败 | npm源是官方源 | 切换为国内npm镜像,或设置代理 |
| 订单提交后库存不减少 | 事务未生效或SQL条件写错 | 检查Service方法是否标注@Transactional,检查更新SQL |
| Maven下载依赖慢 | 仓库源是中央仓库 | 修改settings.xml镜像为阿里云镜像 |
| 部署后页面刷新404 | Nginx没有配置try_files | 补上try_files $uri $uri/ /index.html; |
| 数据库导入中文乱码 | 数据库或表字符集不是utf8mb4 | 建库时指定utf8mb4,sql文件本身也要是utf8编码 |
| 前后端字段对不上,接口返回null | Java实体字段名和前端驼峰不一致 | 统一命名规范,或用@JsonProperty映射 |
这些坑我基本上每次做全栈项目都会遇到几个,尤其是前端跨域和部署后刷新404,属于“不踩一遍根本想不到”的问题。建议你在联调阶段先把接口用Postman或Apifox全部过一遍,确认接口没问题,再开始绑定页面,这样能省掉一半的排查时间。
5.4 部署安全与配置小建议
部署上线前有几点建议顺手做一下。第一是数据库密码不要使用弱口令,后端配置文件的密码不要写死默认值。第二是后端的跨域配置如果做了全局CORS,生产环境最好只允许实际域名访问,不要用*通配符。第三是管理端接口要确保被拦截器保护,可以单独定义一个拦截路径规则,比如/admin/**必须校验管理员角色。
如果项目只是用在毕设答辩或者简历展示,部署到本地或者一台云服务器就够了,不需要做高可用、负载均衡这些重型方案,那是去生产环境才需要考虑的事情。把重点放在代码能跑、流程完整、能讲清楚设计细节上,面试官和老师更看重这些。
6. 拓展方向与个人实操体会
这套家具商城做完之后,往哪个方向拓展都有很高的性价比。如果想让系统更“值钱”,可以先引入Redis缓存商品详情和分类页,降低数据库压力;然后引入Spring Security或更严格的Token鉴权机制;接着把订单模块接上真实的第三方支付SDK(沙箱环境就可以);再往上可以加上定时任务处理超时未支付订单,用消息队列处理订单创建后的异步通知。这些点每一个单独拿出来,都足够成为简历上的一个亮点。
就我个人实操经验来说,做这套项目最重要的不是把功能敲完,而是要把几个核心链路彻底打通理解透:从用户注册登录到浏览商品,从加购到下单,从下订单到扣库存,从支付到订单状态流转。这条链路只要你能不看代码,完整地向别人讲清楚每一步发生了什么事、为什么这么设计,这个项目的价值就真正体现在你身上了。
最后还是那句老话:项目不怕简单,怕的是做完之后脑子里还是一片空白。找一个自己感兴趣的业务场景(家具就不错),按真实流程去设计表结构、写接口、调页面,遇到问题就查资料修掉,这一步一步走下来比看十个教程都有用。尤其是部署环节,千万不要觉得“能本地跑就行”,真正把jar包和dist文件扔到一台干净服务器上跑通,那种成就感是本地开发完全比不了的。