经常有朋友问我,一个能上线、能跑通完整交易流程的网上购物商城系统,到底应该怎么设计和实现?框架教程网上多得是,但真到了把用户、商品、购物车、订单、支付这条链路串起来的时候,很多人就卡住了。最近我把一套基于SpringBoot、Vue、MySQL和MyBatis的网上购物商城系统从零完整做了一遍,数据库建模、后端接口、前端页面、打包部署这些环节全部跑通,这里把整个设计和实现过程整理出来,给准备做商城系统、做练手项目或者需要一套完整源码参考的朋友一条清晰的路径。
这个系统不是只做几个CRUD页面交差,而是真正把商城的管理端和用户端都打通了:前端用户能注册登录、浏览商品、加购物车、下单支付;后台管理员能维护商品分类、上下架商品、处理订单、查看用户列表。项目整体采用前后端分离架构,后端提供REST接口,前端用Vue负责渲染,数据库用MySQL存储业务数据,数据访问层用MyBatis封装SQL操作。整套代码结构清晰,拿来学习也好、二次扩展也好,都能直接上手。
1. 开工之前,先把商城系统的功能边界理清楚
很多新手做这类系统容易犯一个错:上来就写代码,写到一半发现这里少张表、那里缺个接口。我做完这套系统后最大的感受是,需求分析阶段多花半天,后面能省出好几天返工时间。
1.1 用户端与管理端,两个视角看功能清单
商城系统天然包含两个使用方,它们的功能诉求完全不同。我在动手前先列了一张功能清单,把用户端和管理端分开梳理,后面数据库设计和接口设计都围绕这张表展开。
用户端主要有这些功能:
- 账号相关:注册、登录、退出登录、个人资料查看与修改
- 商品相关:首页商品推荐、商品分类浏览、商品搜索、商品详情查看
- 购物车相关:加入购物车、修改数量、删除商品、勾选商品
- 订单相关:提交订单、订单列表、订单详情、取消订单、确认收货
- 地址相关:收货地址的增删改查,下单时选择默认地址
- 支付相关:模拟支付流程,接收并处理支付结果
管理端的功能则偏向后台运营:
- 管理员登录,权限与用户端隔离
- 商品管理:新增、编辑、上下架商品,维护商品图片和库存
- 分类管理:维护商品分类树,调整排序
- 订单管理:订单列表筛选、查看订单详情、发货操作
- 用户管理:查看注册用户列表,禁用异常账号
- 数据概览:统计商品数、订单数、销售额
这张功能清单的好处是,每个功能都能对应到具体的表和接口。比如用户端要有购物车,那数据库就该有cart表;管理端要发货,那order表就要有发货状态发货时间的字段。需求明确后,后续设计就有了抓手。
1.2 从浏览到收货:核心业务流程串讲
功能清单解决的是"有哪些功能"的问题,业务流程解决的是"这些功能怎么串联"的问题。商城主链路我总结成一条线:注册登录、浏览商品、加入购物车、提交订单、完成支付、商家发货、确认收货。
我建议把这条链路画在纸上,然后逐个环节追问状态变化。比如提交订单时,系统要做什么?至少要做四件事:生成订单记录、生成订单明细、扣减库存、清空购物车。这几件事必须放在一个事务里,否则用户会发现订单创建了但库存没减少,或者购物车商品被清空了但订单没生成。
订单状态是整个系统的核心骨架。我把订单状态设计成数字枚举,每一步流转都对应明确的业务操作:
| 状态值 | 含义 | 触发时机 |
|---|---|---|
| 0 | 待支付 | 用户提交订单 |
| 1 | 已支付/待发货 | 支付回调成功 |
| 2 | 已发货 | 管理员操作发货 |
| 3 | 已完成 | 用户确认收货 |
| 4 | 已取消 | 用户主动取消或超时未支付 |
这个状态机贯穿商品、订单、支付所有模块,后端写业务逻辑时状态判断就靠它。一开始就把状态流转理清楚,后面写接口的if/else判断就顺手很多。
1.3 非功能性需求:安全、性能与可维护性
除了功能,我还提前确定了几个非功能性要求,这是系统能不能真正"扛用"的关键。
安全性方面,密码不能明文存储,我统一用BCrypt加密后再入库;登录鉴权用JWT,拿token访问受限接口;后端接口做参数校验,MyBatis的SQL一律用#{}传参,防止SQL注入。
性能方面,商品列表会高频访问,把分类和商品热点数据缓存到Redis,减少数据库压力;列表查询用分页而不是一次把全表数据查出来;订单号生成避免用数据库自增,因为订单号需要全局唯一且有一定规则。
可维护性方面,前后端完全分离,前端工程和后端工程独立部署;后端按controller、service、mapper分层,各层职责单一,方便扩展。这些要求看似基础,但真正影响系统后续能不能稳定跑下去。
2. 技术选型复盘:为什么是SpringBoot、Vue、MySQL、MyBatis
标题里这四个技术词不是随手组合的,它们各自解决一类问题,组合在一起恰好覆盖了商城系统的所有关键环节。这里逐个说说选型理由,也顺便对比一下其他方案的优劣。
2.1 后端框架:SpringBoot替代传统SSM的原因
如果是早几年,这套系统可能用SpringMVC加MyBatis加Servlet容器构建,需要写大量XML配置,部署也要装Tomcat。现在用SpringBoot,最直接的变化是"约定大于配置"。
SpringBoot内嵌Tomcat,打成jar包直接java -jar就能运行,不用单独装服务器。自动配置机制帮我们省去了数据源、事务管理器、视图解析器这些繁琐配置。对商城这种业务密集型的项目来说,把精力集中在业务逻辑上更重要。
另外SpringBoot的生态非常成熟,集成JWT、Redis、文件上传、参数校验都有对应组件,官方文档和社区资料都很全。我从项目立项到接口跑通,最花时间的部分不是环境配置,而是业务设计,这就是SpringBoot带来的实际效率提升。
2.2 持久层:MyBatis与JPA之间的取舍
持久层选MyBatis不是因为它比JPA更高级,恰恰是因为商城系统的SQL复杂度和可控性要求,MyBatis更合适。
电商业务天然有大量多表关联查询和动态查询。比如商品列表要从product表关联category表拿分类名称,还要按关键词搜索、按分类筛选、按销量排序。这些动态SQL在MyBatis里用<if>标签写起来很直观,每一条SQL都是自己控制的,性能如何心里有数。我在做库存扣减时还用了自定义的原子更新SQL,这种灵活控制JPA反而不好实现。
JPA虽然在CRUD上更省代码,但遇到复杂查询时要么写JPQL,要么走原生SQL,反而不如MyBatis顺畅。对于想彻底搞懂SQL逻辑的开发者来说,MyBatis也是更好的学习对象。
2.3 前端方案:Vue全家桶加UI组件库
前端选Vue,核心原因是它的渐进式架构适合这种前后端分离的商城项目。Vue Router负责页面路由,Pinia或Vuex做状态管理,组件化开发把商品卡片、订单列表这类高频模块抽出来复用。
我实际用的是Vue3加Element Plus组件库。Element Plus的表单、表格、弹窗、分页组件在管理后台非常顺手,商品管理、订单管理页面基本都是套表格加表单的模式,开发效率很高。用户端的首页、商品列表、购物车则自己写组件做样式定制,视觉上不会显得千篇一律。
这类成熟组件库还有一个好处,就是表单校验、日期选择、上传组件这些功能开箱即用,比如后台添加商品时图片上传,直接用组件库的Upload组件配合后端接口就行。
2.4 中间组件:Redis与文件存储的定位
MySQL负责持久化核心业务数据,Redis负责缓存和临时数据,两者互补。商城系统里热门商品列表、分类列表这类读多写少的数据,放Redis里能显著降低MySQL压力。我在项目里把首页轮播图数据、商品分类列表、热门商品列表都做了缓存,过期时间设置为三十分钟。订单号生成和Session相关的临时数据也会用到Redis。
文件存储方面,用户端要展示商品图片,管理端要上传图片。轻量做法是把图片传到服务器本地指定目录,通过nginx映射成静态资源访问。如果以后图片量大再迁到对象存储,替换存储实现即可,接口设计上预留这个空间就行。
3. 数据库设计:一张张表把电商核心数据模型讲清楚
数据库设计直接决定业务逻辑好不好写。商城系统的表我分成三组:用户侧、商品侧、交易侧,每组之间通过外键逻辑关联。这里按照实际建表顺序把核心表结构梳理一遍。
3.1 用户与权限:用户表、收货地址表、管理员表
用户表是整个系统的起点。注册登录、个人中心、下单收货人都离不开它。我的用户表主要字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 用户名,唯一索引 |
| password | varchar(100) | BCrypt加密后的密码 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(200) | 头像URL |
| phone | varchar(20) | 手机号 |
| varchar(50) | 邮箱 | |
| gender | tinyint | 性别,0未知1男2女 |
| status | tinyint | 状态,0正常1禁用 |
| created_time | datetime | 注册时间 |
密码字段长度故意留到100位,因为BCrypt加密结果本身有60位左右,varchar50虽然够用但不够规范,100位更保险。用户名加唯一索引,防止重复注册,同时在登录查询时也能走索引加速。
收货地址表与用户表是一对多关系:
CREATE TABLE address ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, receiver VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, province VARCHAR(50), city VARCHAR(50), district VARCHAR(50), detail VARCHAR(200), is_default TINYINT DEFAULT 0, created_time DATETIME, updated_time DATETIME, KEY idx_user (user_id) );管理员表结构和用户表类似,但独立建表不混在一起。原因是管理员需要的字段和权限模型与前端用户不同,分开建可以避免业务逻辑互相干扰。管理员账号由初始化SQL预置,不做开放注册。
3.2 商品侧:分类表、商品表、商品图片表
商品表和分类表是商城的内容基石。分类表用parent_id支持二级分类,比如"服饰"下面可以挂"男装""女装"。商品表通过category_id关联分类。
商品表核心字段包括:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| category_id | bigint | 分类ID,索引 |
| name | varchar(100) | 商品名称 |
| sub_title | varchar(200) | 副标题/卖点 |
| main_image | varchar(200) | 主图URL |
| detail | text | 商品详情 |
| price | decimal(10,2) | 售价 |
| original_price | decimal(10,2) | 划线价 |
| stock | int | 库存数量 |
| sales | int | 销量 |
| status | tinyint | 0下架1上架 |
| created_time | datetime | 创建时间 |
| updated_time | datetime | 更新时间 |
价格字段必须用decimal,不能图省事用double。double在计算金额时会有精度丢失,比如0.1加0.2等于0.30000000000000004,哪怕做一次乘法都可能出错。decimal(10,2)可以精确表示小数点后两位的金额,满足电商业务需求。
商品图片我单独建product_image表,不把所有图片塞在商品表的一个字段里。这样每个图片一行记录,排序、增删都方便。商品详情里的长图文暂时放product表的detail字段,实际项目如果内容复杂可以再拆富文本表。
3.3 交易侧:购物车表、订单表、订单项表、支付记录表
购物车表结构相对简单,user_id、product_id、quantity、checked是核心字段。加上唯一索引(user_id, product_id),保证同一用户同一商品不会在购物车里出现多行。用户反复加入同一商品时,走的是"存在则更新数量"的逻辑,而不是无脑插入新记录。
订单相关的表是整个数据库设计最需要讲究的部分。我把订单拆成订单主表和订单明细表,而不是把商品信息直接存在订单表里,原因有二。
第一,一个订单可能包含多个商品,如果只存在订单表的一个字段里,后期查订单明细、统计销量都会很难受。拆成两张表,order表管订单整体状态,order_item表管每个商品快照,职责清晰。
第二,订单明细里的商品名、商品图片、价格都是下单那一刻的"快照"。商品后续可能改价、改名甚至下架,但历史订单必须保持下单时的样子。所以在order_item表里冗余了product_name、product_image、current_price字段,这些数据在下单时从商品表拷贝过来,之后不再跟随商品表变更。
订单主表的主要字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单号,唯一索引 |
| user_id | bigint | 下单用户 |
| total_amount | decimal(10,2) | 商品总金额 |
| pay_amount | decimal(10,2) | 实付金额 |
| freight_amount | decimal(10,2) | 运费 |
| status | tinyint | 订单状态 |
| receiver_name | varchar(50) | 收货人 |
| receiver_phone | varchar(20) | 收货电话 |
| receiver_address | varchar(200) | 收货地址 |
| pay_time | datetime | 支付时间 |
| delivery_time | datetime | 发货时间 |
| finish_time | datetime | 完成时间 |
| remark | varchar(255) | 用户备注 |
收货信息为什么不直接存address_id,而是把收货人、电话、地址都冗余到订单表?因为地址后续可能被用户修改或删除,但历史订单的收货信息必须保持原样。和商品快照同理,订单是交易凭证,不能受基础数据变更影响。
支付记录表单独建,记录每笔支付流水,和订单一对一关联。字段包括payment_no、order_no、user_id、pay_amount、pay_type、status、callback_time。这样即使支付回调重复推送,也能通过支付记录表做幂等校验。
3.4 索引、外键与金额字段的设计取舍
数据库设计里我刻意少用外键约束,只保留逻辑外键关系。原因是外键会影响插入和更新性能,而且在分库分表场景下外键基本不可用。商城系统的关联关系通过业务代码保证,表之间用user_id、product_id、order_no这些字段在应用层关联即可。
索引的设计原则是"查询频率高的字段建索引,更新频率高的字段少建索引"。我实际建的索引包括user表的username唯一索引、product表的category_id索引、order表的order_no唯一索引和user_id索引、order_item表的order_id索引。这些都是高频查询路径。
订单号我不用数据库自增id,而是用代码生成。生成规则是时间戳加用户ID加随机数,保证全局唯一性同时带有一定辨识度。支付系统要求订单号唯一,自增id在分布式环境下容易冲突,代码生成更可控。
4. 后端核心链路:鉴权、商品、购物车、下单与扣库存
数据库设计完成,后端开发就有章可循。我的开发顺序是先搭通用能力(统一返回、异常处理、JWT鉴权),再写业务模块。这里重点讲几个最核心的链路:登录鉴权、商品分页、购物车、下单与库存扣减。
4.1 JWT登录鉴权的实现思路
登录鉴权我选JWT加拦截器方案,没有引入Spring Security这种重量级框架。商城系统的权限模型比较简单,用户端和管理员端各有一种角色,用拦截器加JWT足够,引入Security反而增加学习成本。
实现思路是这样:用户提交用户名密码,后端校验通过后生成一个JWT字符串返回,前端存到localStorage。后续每个请求在Header里带上Authorization: Bearer <token>,后端拦截器解析token,合法就放行,不合法返回401。
JWT本身包含三段:Header、Payload、Signature。Payload里我存放userId和role两个字段:
{ "sub": "用户ID", "role": "ROLE_USER", "iat": 1699999999, "exp": 1700086399 }后端用jjwt库解析和校验签名。由于JWT是无状态的,服务端不需要维护Session,水平扩展时不需要做Session共享,这在部署时省了不少事。
拦截器注册要注意放行规则的顺序。登录、注册、商品列表、商品详情、分类列表这些接口必须放行,否则用户还没登录连商品都看不见。我整理了一个放行列表,其他路径全部拦截。
实际开发中还有一个细节:拦截器里解析出的用户信息不能直接用static变量存,因为多线程环境下会互相覆盖。我用ThreadLocal保存当前登录用户ID,请求结束后在afterCompletion方法里清理,避免线程池复用导致数据串号。
4.2 商品分页、条件查询与缓存设计
商品列表是所有商城系统的门面接口,查询条件多、调用频繁,必须合理设计。
分页我用PageHelper插件,但使用时要特别注意一个坑:PageHelper.startPage()只对紧接着的下一条SQL查询生效。如果中间有赋值操作、循环查询或者其他SQL,分页就会失效或拦截错对象。我总结的规范是startPage后紧跟着写mapper查询,然后立刻用PageInfo包装返回。
商品列表的查询参数包含分类ID、关键词、价格区间、排序方式。排序方式我用一个sort字段传给SQL,在XML里用<choose>标签动态拼接ORDER BY条件。因为排序字段在白名单内可控,不直接用前端传的字符串拼SQL,防止SQL注入。
考虑到商品列表是热点数据,我做了Redis缓存:以"商品列表+分页参数"为key,缓存列表数据,设置三十分钟过期。但这里有个权衡,缓存一致性不好保证,商品改价、上下架后缓存可能不是最新的。所以我只针对首页热销商品做了缓存,搜索和分类筛选结果走数据库实时查询。折中方案既可以减轻数据库压力,又不会因为缓存问题出现明显的价格错误。
4.3 购物车接口设计与价格计算逻辑
购物车的接口相对直白,但有一个细节值得说明:购物车列表展示时,前端需要显示实时价格,而后端返回的购物车记录里存了product_id,所以查询购物车时必须关联商品表把当前价格取出来。
我的做法是购物车列表接口做一次关联查询,返回以下结构:
{ "cartId": 1, "productId": 10, "productName": "无线蓝牙耳机", "productImage": "图片URL", "price": 199.00, "quantity": 2, "checked": 1, "stock": 500 }关联查询的好处是可以直接拿到商品实时价格、库存和主图,前端结算页不需要再发多个请求拼数据。
加入购物车的接口要处理两种情况:购物车里还没有这个商品,就插入一条新记录;已经有这个商品,就更新数量。我在代码里先按user_id和product_id查询,存在则update,不存在则insert。这个逻辑配合唯一索引(user_id, product_id)做兜底,即使并发下出现重复插入,也不会产生脏数据。
结算页的价格计算我放在后端完成,不在前端加减乘除。前端把勾选的购物车记录ID传给后端,后端汇总商品金额、计算运费、得到实付金额返回给前端确认。所有金额计算后端统一处理,前端只负责展示,这样改价、优惠等活动逻辑好维护,也避免前后端金额不一致的问题。
4.4 下单流程与库存扣减:用乐观锁防超卖
下单是商城系统最核心的接口,涉及多个数据变更。我把它放在一个事务方法里,按顺序处理五件事:
- 根据前端传来的购物车记录ID,查到本次要结算的商品明细
- 校验商品状态是上架状态,库存足够
- 生成订单号和订单主表记录
- 生成订单明细记录
- 扣减商品库存、增加销量
- 删除已结算的购物车记录
这里最需要仔细设计的是库存扣减。很多新手会写成"先查库存,若大于0则update库存减一",这在并发下必然出现超卖。两个用户同时查到库存为1,都判断有货,都执行扣减,库存就变成-1了。
正确做法是使用带条件的原子更新:
UPDATE product SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{productId} AND stock >= #{quantity}这条SQL把"检查库存够不够"和"扣减库存"合并成一个原子操作。数据库的行锁保证同一时刻只有一个事务能更新同一行商品,影响行数为0说明库存不足,业务层拿到结果后抛出异常回滚整个下单事务。
一个容易忽略的细节是,扣减多个商品的库存时,如果按商品ID顺序执行update,可以避免死锁。比如订单包含商品A和商品B,事务1先扣A再扣B,事务2先扣B再扣A,就可能互相等待形成死锁。所以我在代码里把待扣减商品按productId排序后再逐个执行更新,从源头规避死锁。
下单事务里还要注意异常回滚。我使用@Transactional注解,默认只回滚RuntimeException,如果代码里手动捕获了异常却没抛出,订单和数据就会不一致。我的规范是Service层不捕获业务异常,由统一的异常处理机制兜底。
4.5 订单状态机与支付回调幂等处理
订单创建后状态是0待支付。支付环节我接的是模拟支付,但设计上遵循真实支付系统回调的规范。
支付成功后,支付平台会异步回调项目后端接口,携带订单号和支付结果。回调接口第一个动作不是直接改订单状态,而是先查支付记录:
- 根据order_no查询支付记录是否存在
- 如果支付记录状态已经是成功,直接返回成功,不再重复处理
- 如果不存在或未成功,才更新支付记录、更新订单状态为已支付
这叫幂等处理。支付平台出于可靠性会重试回调,如果回调接口不做幂等,用户支付一次但订单状态被重复处理,就可能出现重复发货或数据错乱。
订单状态流转的判断我集中放在一个方法里,每次变更状态前校验当前状态是否允许跳转。比如待支付状态只能取消或变成已支付,已发货状态不能再变回待支付。这种状态机的写法虽然多几行判断,但能防止页面上异常操作导致的数据混乱。
管理员发货接口调用后,订单状态变为2已发货,记录delivery_time,用户可以确认收货进入已完成状态。整个链路从下单到完成,每个环节都有状态和时间的双重记录,后续对账和售后排查都有据可查。
5. 前端Vue工程:页面结构、请求封装与状态管理
前端工程我用Vue3加Vite搭建,配合Vue Router、Pinia、Element Plus。整体分为用户端和管理后台两套布局,共用同一套请求封装和鉴权逻辑。
5.1 用户端路由与页面模块划分
用户端的路由设计遵循"越扁平越好"的原则。首页、商品列表、商品详情、购物车、结算页、订单列表、订单详情、登录注册都在一级路由,路径语义清晰,方便SEO和分享。
商品详情页和管理后台的路径设计我特别说明一下。详情页用动态路由/product/:id,在组件里通过route.params.id拿到商品ID,然后请求详情接口。后台路由用嵌套路由结构,/admin作为父路由,下面挂商品管理、订单管理、用户管理等子页面,统一带侧边栏布局。
路由守卫用来做登录校验。我在router.beforeEach里检查目标路由是否在公开列表内,如果在就放行;否则看本地有没有token,没有就跳转到登录页并带上redirect参数,登录成功后回跳原页面。这个交互看着简单,但对用户体验提升很大。
5.2 Axios请求封装与Token过期处理
前端所有接口请求统一走Axios实例,不在组件里散落调用。我封装了一个request.js,包含以下逻辑:
- 统一baseURL,开发环境走Vite代理,生产环境走nginx转发
- 请求拦截器从localStorage读token,放到Authorization头
- 响应拦截器统一处理HTTP状态码和业务状态码
- 业务状态码为401时,清理token并跳转登录页
- 网络错误统一弹出消息提示
这里最值得说的是Token过期处理。JWT有有效期,商城用户可能登录后放置很久再操作,token过期后接口返回401。如果直接跳登录页,用户的填写的表单内容会丢失,体验很差。
我做了两件事:第一,页面跳转登录页时带上当前路由,用户重新登录后可以回到原页面;第二,在响应拦截器里把401和真正的权限错误区分开,401统一处理成登录过期提示。这个细节在实际使用中很关键,用户反馈"莫名其妙被踢出去"的情况基本都来自没有处理Token过期。
5.3 商品列表、购物车与结算页的组件化
用户端需求里商品展示贯穿始终,我把商品卡片抽成独立组件ProductCard,接收一个product对象渲染主图、名称、价格。首页推荐、商品列表、搜索结果三个页面都复用这个组件,只通过传入的数组做v-for渲染。
购物车页面的数量加减和勾选操作比较关键。我采取的方案是:本地交互先行,接口同步。用户点加减或勾选时,页面先更新本地状态,同时调用后端接口同步。接口失败时回滚本地状态并提示错误。这种方式界面响应快,同时保证数据最终一致。
结算页是信息汇总页,从购物车勾选记录拉取结算预览数据。页面上展示商品明细、收货地址、金额明细三项。金额部分完全以后端返回为准,前端不自己算总价。这样做的好处是即使有人篡改前端数据,后端支付金额也是正确的。
5.4 管理后台:表格页与表单页的快速搭建
管理后台的开发效率主要靠Element Plus。商品管理页就是典型的表格加弹窗表单组合:表格展示商品列表,提供搜索和分页;点击新增或编辑弹出表单对话框,表单里包含商品名称、分类选择、价格、库存、图片上传等字段。
表单校验用Element Plus自带的rules,必填项、价格范围、库存非负都配置在rules里。提交时触发表单校验,通过后再调接口。这套模式在商品、分类、订单多个管理页面重复使用,熟练后一个表格页半小时就能搭完。
商品图片上传组件需要和后端约定返回格式。我让上传接口成功返回图片URL,表单提交时把URL存入商品字段。上传时前端组件显示进度条,后端做了图片大小限制和类型校验,防止用户传超大文件或非图片格式拖垮服务器。
6. 从本地到线上:运行环境、打包部署与问题排查
代码写完后,最折腾的往往是运行环境和部署环节。这个项目我踩过不少坑,把关键步骤和问题处理一起整理在这里。
6.1 本地开发环境的准备
本地需要装的东西有这些:JDK8以上、Maven 3.6以上、MySQL 5.7或8.0、Redis、Node.js 16以上。我建议用版本管理工具统一版本,避免环境差异导致的问题。
数据库方面,建库时字符集必须设置utf8mb4,否则中文会乱码。创建数据库后执行初始化SQL脚本,脚本里包含所有建表语句和管理员初始账号。连接配置写在application.yml里:
spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpasswordserverTimezone=Asia/Shanghai这个参数经常被漏掉,不配的话MySQL驱动会报时区错误。Redis的地址和密码也在配置文件里统一设置,本地默认localhost:6379无密码即可。
6.2 前端构建与后端打包含坑记录
后端打包相对简单,mvn clean package -DskipTests生成jar文件,然后java -jar就能启动。但第一次打包时经常遇到依赖下载失败或插件版本问题,处理办法是配置阿里云镜像仓库,并固定SpringBoot父版本号。
前端打包有几个典型的坑:
第一个是路由history模式刷新404。前端用history路由后,部署在nginx上刷新页面时,nginx会去磁盘找对应的静态文件路径,找不到返回404。必须在nginx配置try_files兜底:
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }第二个是接口地址配置问题。前端axios的baseURL必须区分开发环境和生产环境。开发环境用Vite代理转发到后端,生产环境走nginx的/api/前缀转发。我在axios里用环境变量区分,避免上线后接口明明可用却404。
nginx的代理转发配置也很容易踩坑。proxy_pass最后一个斜杠决定了路径拼接方式,我统一这样做:
location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样前端请求/api/product/list会被转发到后端的/product/list。同时后端不开启CORS跨域,因为同域名同端口下没有跨域问题,配置也简单了。
6.3 上线后的常见问题与处理建议
上线后第一个容易遇到的问题就是图片404。本地开发时图片上传到本地上传目录,部署后nginx需要配置静态资源映射,否则图片URL指向的路径在nginx下找不到文件。
第二个常见问题是文件上传大小超限。SpringBoot默认上传限制只有1MB,管理端上传商品图片时稍大一点就报错。需要在配置里调整:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB第三个问题是接口返回的金额类型。前后端交互时JSON序列化会把decimal转成数字,前端JavaScript处理大数字时可能会丢精度。虽然商城金额一般不大,但稳妥做法是在后端对金额字段做统一序列化处理,转成字符串返回。
还有一个我在排查问题时很受益的方法:上线后用curl -i直接测接口,先确认HTTP状态码和后端返回结构,再去看前端页面效果。很多看起来像是前端问题的情况,实际是nginx代理或者后端接口返回格式不对。按"后端接口、nginx转发、前端渲染"的顺序逐层排查,定位效率会高很多。
这套商城系统做到这里,已经是一个能正常跑通完整交易流程的完整项目了:用户注册登录、浏览加购、下单支付,管理员维护商品、处理订单,数据链路完整,前后端职责清晰。我在实际开发过程中体会最深的一点是,代码写得多快不是关键,数据库设计和业务流程梳理才是决定系统质量的核心。如果你打算拿这套源码做二次开发,建议先按自己的业务需求调整order状态字段和商品字段,再扩展营销活动、优惠券之类的功能,这套基础架构都能支持。