☰
SpringBoot+Vue网上服装商城系统从0到1完整设计与实现
2026/10/5 8:01:23 网站建设 项目流程

做毕设或者个人项目时,我见过太多人一头扎进“最新最火”的技术堆里,Spring Cloud、微服务、Docker、K8s全往上垒,结果项目还没跑起来就先被环境折腾到怀疑人生。而SpringBoot+Vue这套组合,看着“老”,恰恰是当下Java Web开发里最皮实、最实用的搭配。这篇文章就来细拆一个基于SpringBoot+Vue的网上服装商城管理系统,从数据库设计、后端实现、前端交互到部署上线,把完整链路里那些文档里不会写清楚的细节全部摊开讲。不管你是准备做毕业设计,还是想自己撸一个能用的电商系统练手,这篇都能让你少走不少弯路。

先交代一下这个项目的技术底子:后端是SpringBoot 2.7.x + MyBatis + MySQL 8.0,前端是Vue 2.6 + Element UI + Axios,JDK用的8。这套配置在2025年的今天依然非常主流,招人市场上相关岗位遍地都是,而且社区资料极其丰富,遇到问题基本一搜就有答案。项目本身的功能不算复杂,核心就是围绕“服装商品”做增删改查、购物车、订单、用户登录注册、后台管理这些标准模块,但麻雀虽小五脏俱全,任何一个电商系统的骨架逻辑在这里都能找到对应。

我选择这套技术栈去设计整个系统,就是奔着“稳妥”两个字去的。下面按照实际开发的顺序,把整个系统的设计思路、核心实现、踩坑实录完整过一遍。

1. 项目整体架构与设计思路拆解

1.1 这个商城系统到底要解决哪些问题

网上服装商城,说穿了就是两件事:前台让用户能逛、能买,后台让管理员能管、能改。前台的核心流程是浏览商品、搜索筛选、加入购物车、提交订单、支付(演示环境通常只做模拟)、查看订单状态。后台管理则是商品上下架、库存调整、订单审核发货、用户管理、数据统计这些。

这种系统真正难的点不是功能本身,而是几个绕不开的细节:商品分类层级怎么设计才能灵活扩展,库存和订单之间的数据一致性怎么保证,登录状态怎么在前后端分离的架构下维持,图片上传后怎么访问和存储。这些问题如果不在一开始就想清楚,后面开发到一半再返工,成本是成倍上涨的。

我在设计这个项目时,把业务模块划分成了七个大块:用户模块、商品模块、购物车模块、订单模块、分类模块、后台管理模块、文件上传模块。每个模块内部自己管自己的逻辑,模块之间通过接口交互,这样后期改任何一个模块都不会牵连到其他部分。

1.2 为什么是SpringBoot+MyBatis而不是更花哨的组合

现在很多人一上来就推荐SpringBoot + MyBatis-Plus或者JPA,但我在设计这个项目时坚持用了原生MyBatis,原因很简单:MyBatis的SQL是自己写的,你能精确控制每一条查询语句。商城系统里商品列表往往需要多条件动态拼接SQL,比如按价格区间、按销量排序、按分类筛选,这些用MyBatis的动态SQL标签可以写得很优雅。

用原生MyBatis还有一个好处,就是面试的时候你对自己写过的SQL了如指掌。很多用MyBatis-Plus的人,问他分页底层怎么实现的、多表查询怎么优化,完全答不上来,因为自动生成的SQL掩盖了太多细节。我见过太多简历上写着“熟练使用MyBatis-Plus”的候选人,连resultMap的自动映射规则都说不清,这种基本功的差距在日常开发中是致命伤。

SpringBoot的选择同理。它把Spring家族的配置简化到了一个全新的高度,内嵌Tomcat让我们不再需要单独部署外部容器,直接java -jar就能跑起来。但我始终建议,享受SpringBoot便利的同时,一定要理解底层原理——自动配置只是帮你做了你原本该做的事,不是让你不用知道。

1.3 功能模块的详细规划

整个系统的模块划分,我按角色分成了前台和后台两个维度。

前台的用户端页面包括:首页(商品推荐和分类展示)、商品列表页(支持关键字搜索、价格筛选、销量排序)、商品详情页(图片、价格、库存、规格选择)、购物车页(数量调整、勾选结算)、订单确认页(收货地址、提交订单)、个人中心(订单列表、订单详情、收货地址管理)。

后台的管理端页面包括:登录页、仪表盘(关键数据统计)、商品管理(新增编辑删除上下架)、商品分类管理、订单管理(查看、发货、关闭)、用户管理(禁用、重置密码)。

接口层面,RESTful风格走起,比如GET /api/product/list是分页查询商品,POST /api/order是创建订单,PUT /api/order/{id}/ship是后台发货。前后端约定统一返回格式,我采用的数据结构是{code, message, data},code为200表示成功,500表示业务异常,401表示未登录或登录过期。

2. 数据库设计与核心表结构解析

2.1 基础字段设计与公共约定

数据库设计是整个系统的地基。很多人做表结构特别随意,id用int就完事,创建时间更新时间不加,逻辑删除字段没有,等业务跑起来才发现追数据、做统计的时候处处受限。

我在这个项目里定了一条铁律:每张业务表必须有主键id、create_time(创建时间)、update_time(更新时间),这两个时间字段在MyBatis里用数据库的DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP自动维护,Java代码里完全不用手动设置。

用户表我额外加了del_flag逻辑删除字段,这么做的好处是数据永远不物理删除,用户的订单关联查询时不会因为用户被删除而断链。比如用户注销后,后台仍然需要能查到他的历史订单记录。物理删除在电商这类强关联业务中是一种偷懒且危险的做法。

2.2 核心业务表的数据模型

下面把几张核心表的结构核心点列出来,这些都是我在实际设计时踩过坑之后形成的最终方案。

  • 用户表 user:id、username、password、nickname、avatar、phone、email、role(1表示普通用户,0表示管理员)、status(1启用,0禁用)、del_flag、create_time、update_time。密码存储用的是BCrypt加盐哈希,不是明文,也不是简单的MD5。MD5现在用彩虹表破解成本极低,电商系统涉及交易数据,密码安全是底线。

  • 商品表 product:id、name、subtitle(副标题)、main_image(主图URL)、detail(富文本详情)、category_id(分类id)、price(原价)、promote_price(促销价)、stock(库存)、sales(销量)、status(1上架,0下架)、create_time、update_time。price我用的是decimal(10,2),注意这里有一个重点:涉及金额的字段永远不要用double或float,浮点数的二进制表示会导致金额出现0.1+0.2=0.30000000000000004这种问题,这是行业里踩过无数次的坑。

  • 商品分类表 category:id、name、parent_id、sort_order、status。parent_id支撑无限层级分类,顶级分类的parent_id为0。服装商城通常需要两级分类,比如“男装”下挂“T恤”“外套”,“女装”下挂“连衣裙”“半身裙”,这套结构在后台新增三级分类时也不用改表结构。

  • 购物车表 cart:id、user_id、product_id、quantity、checked(是否选中结算)、create_time、update_time。购物车加了一个UNIQUE KEY(user_id, product_id),防止同一用户重复添加同一商品产生两条脏数据。数量更新走ON DUPLICATE KEY UPDATE,插入时若唯一键冲突就直接累加数量。

  • 订单表 order:id、order_no(订单编号)、user_id、total_amount(订单总金额)、pay_amount(实付金额)、status(订单状态枚举)、receiver_name、receiver_phone、receiver_address、create_time、pay_time、ship_time、finish_time、close_time。order_no的生成规则我用了时间戳+用户id+随机数,保证唯一性的同时还能从订单号里大致看出下单时间。

  • 订单明细表 order_item:id、order_id、product_id、product_name(快照)、product_image(快照)、current_price(下单时价格快照)、quantity、total_price。明细表必须存商品快照信息,因为商品改名、改价、下架后,历史订单仍然应该显示下单时的商品信息。这是电商领域的标准做法,也是很多新手最容易忽略的细节。

2.3 索引设计与状态字段的取值约定

索引设计直接影响查询性能。商品表我建了idx_category_id、idx_status、idx_sales三个索引,分别支撑分类查询、上下架过滤、销量排序。订单表建了idx_user_id,支撑个人中心“我的订单”列表——这个查询是高频操作。订单状态字段我用的是tinyint存数字状态值,0待付款、1待发货、2待收货、3已完成、4已关闭,比直接存中文字符串要省空间且查询更快,Java侧用枚举类做映射。

这里要特别说一个关于订单状态的细节设计:状态流转是有方向的,不能随意跳变。我在后端Service层对状态流转做了校验,比如待发货订单不能直接变成已完成,必须经过待收货。这个约束在代码里写清楚,可以防止联调时前端乱传状态值把订单数据搞脏。

3. 后端核心模块的实现逻辑

3.1 分层结构与代码职责边界

后端代码我按Controller、Service、Mapper三层来组织,package结构如下:

  • controller:接收HTTP请求、参数校验、调用Service、返回统一响应体。Controller层只做路由转发,不写任何业务逻辑。
  • service:业务逻辑都在这层,事务控制也在这层。比如创建订单这个操作,涉及扣库存、生成订单、生成明细、清空购物车等多个步骤,整个操作必须放在同一个事务里,任何一个环节失败都要整体回滚。
  • mapper:只做数据库交互,一个方法对应一条SQL或一个动态SQL块。

这个分层看似基础,但它带来的好处是切切实实的:代码可读性强,任何人都能从方法命名上马上判断出这是接口层还是业务层还是数据层;可测试性强,Service层可以直接用单元测试覆盖,不需要启动Web容器;遇到问题时定位快,接口参数问题直接去Controller看,SQL问题直接在Mapper里看。

很多同学习惯把业务逻辑写在Controller里,图省事,觉得代码少。这个习惯在项目初期似乎没问题,但项目一旦超过20张表,Controller层会变成一团乱麻,一个方法几百行,改一个地方要牵动全局,这种代码维护起来真的是灾难。

3.2 MyBatis的配置细节与动态SQL实战

MyBatis的配置,我拆成三个部分:application.yml里的数据源配置、mybatis-config.xml里的全局配置、Mapper接口对应的XML文件。

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mshop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.mshop.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

其中map-underscore-to-camel-case这个配置必开,它能把数据库的create_time自动映射到Java实体类的createTime属性,省去一堆resultMap手写映射的繁琐代码。log-impl配成StdOutImpl后,控制台会直接打印SQL语句和参数、结果集,开发联调阶段排查问题神器。

动态SQL是MyBatis的核心战斗力。商品列表页的多条件查询就是典型场景:用户可能选了分类,可能输入了关键字,可能选了价格区间,每种组合都不一样。用if标签拼接条件,用where标签自动处理多条件时AND的拼接问题。

<select id="selectByCondition" resultType="com.example.mshop.entity.Product"> SELECT * FROM product <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR subtitle LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="minPrice != null"> AND promote_price >= #{minPrice} </if> <if test="maxPrice != null"> AND promote_price &lt;= #{maxPrice} </if> AND status = 1 </where> <choose> <when test="sortType == 'priceAsc'"> ORDER BY promote_price ASC </when> <when test="sortType == 'priceDesc'"> ORDER BY promote_price DESC </when> <when test="sortType == 'salesDesc'"> ORDER BY sales DESC </when> <otherwise> ORDER BY create_time DESC </otherwise> </choose> </select>

这个SQL里的choose标签相当于Java的switch,根据用户选择的排序方式动态拼接不同的ORDER BY子句,既灵活又防住了SQL注入——参数都是#{}预编译绑定,不可能被拼接进SQL语句。

分页查询我没有引入PageHelper插件,而是手写了LIMIT #{offset}, #{pageSize},配合一个COUNT查询得到总条数。逻辑非常简单,也方便理解分页原理。用PageHelper虽然省事,但它对复杂SQL有时会生成错误的COUNT语句,排查起来非常头疼,手写分页反而更可控。

3.3 拦截器、登录鉴权与参数校验

在前后端分离架构下,登录状态的方案选择我最终敲定了JWT。流程很标准:用户登录成功后,后端生成一个包含用户id和角色的Token返回给前端,前端把Token存在localStorage,之后每次请求在请求头里带上Authorization: Bearer xxx。后端写一个拦截器,对需要登录的接口做Token校验。

拦截器的实现我放在了WebMvcConfigurer里注册。在SpringBoot里可以继承HandlerInterceptorAdapter或者实现HandlerInterceptor接口,把resolveToken、校验签名、校验过期、提取用户信息这一套逻辑跑完,然后把用户信息放入ThreadLocal或Request attribute里,Controller里直接取用。

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); } try { Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } } }

注册拦截器时需要注意路径放行的配置。通常拦截所有/api/**,然后放行登录注册接口和商品浏览接口。这里有一个我踩过的坑:前端在开发环境用axios请求时会先发一个OPTIONS预检请求,如果拦截器没有对OPTIONS放行,会导致所有跨域请求都报401,看起来是跨域问题,实际是拦截器把预检请求拦了。这个坑调试起来特别迷惑,我先后面会展开。

参数校验方面,我采用了JSR-303的@Validated注解方案,在实体类的字段上加@NotNull、@NotBlank、@Email、@Pattern等约束注解,Controller层方法参数前加@Validated注解就能自动完成参数验证,不用在方法里写一堆if-else判断。校验失败会抛出MethodArgumentNotValidException,我在全局异常处理器里统一捕获并返回清晰的中文错误提示。

3.4 商品、购物车、订单的完整链路实现要点

商品搜索排序、分类筛选、库存变更这些核心逻辑,以及购物车和订单的实现要点,我想重点展开讲一讲,因为它们之间的数据流转最容易出错。

购物车的核心逻辑。加入购物车时,先查询购物车表里是否已有该用户和该商品的记录,有则更新数量,没有则插入。这里我用了前面提到的唯一索引加ON DUPLICATE KEY UPDATE的方式,一条SQL搞定,避免了先查再插两步操作带来的并发重复问题。购物车列表查询需要关联product表查出当前价格、库存、主图、上下架状态,用JOIN查询一次搞定,而不是在Java层做两次查询再拼接。

下单的核心逻辑。这是整个系统最复杂、最容易出问题的环节,我单独拎出来说。下单接口接收的参数是购物车中勾选的商品项ids和收货地址信息。后端处理的完整顺序是:

  1. 校验用户登录状态。
  2. 根据购物车ids查出商品数据,校验商品是否存在、是否上架。
  3. 逐个校验库存是否充足,库存不足则中断并提示具体哪个商品库存不够。
  4. 重新从数据库查询最新价格计算订单总金额(不能信任前端传来的价格,前端改价攻击是电商系统最基本的安全攻防场景)。
  5. 生成订单号和订单主记录。
  6. 生成订单明细记录,写入商品快照。
  7. 批量扣减库存:UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}。
  8. 删除购物车中已下单的商品项。
  9. 整个流程用@Transactional包裹,任何一步异常整体回滚。

这里扣库存的SQL语句相当关键。UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity},这个条件判断的含义是:只有当当前库存大于等于购买数量时才执行扣减,否则影响行数为0。我们通过返回的影响行数判断扣减是否成功,成功则继续,失败则抛异常回滚。这种写法在单机数据库层面可以有效防止超卖问题,比先query再update的方式要可靠得多。

4. 前端工程化与Vue交互细节

4.1 项目搭建、路由与状态管理

前端用的Vue 2.6 + Element UI + Vue Router + Vuex + Axios,脚手架用的Vue CLI 4。Vue 3现在确实是趋势,但市面上大量现存项目尤其是教学型的还是在Vue 2的生态里,学会了Vue 2再看Vue 3的Composition API其实很顺畅。这里如果项目是你自己从零搭,也可以用Vite+Vue 3,开发体验会好很多,原理上差别不大。

路由设计上,我把页面分成了不需要登录就能访问的公开页面和必须登录才能访问的受保护页面。在Vue Router的全局前置守卫beforeEach里做登录校验:如果目标是受保护页面且本地没有Token,直接跳转登录页。这个守卫的逻辑虽然简单,但是整个前端访问控制的基础,缺少了它用户可以直接通过修改URL访问到订单页面,接口层当然还有JWT做兜底,但前端的路由控制能带来更好的用户体验——未登录用户被直接引导到登录页,而不是点了按钮才收到接口报错。

Vuex里我用的模块化结构,有user模块和cart模块。user模块保存用户基本信息、登录状态、Token,cart模块保存购物车数量。页面刷新时,从localStorage重新读取用户信息和购物车数量,恢复状态。

4.2 Axios封装与接口对接规范

axios怎么封装,决定了前后端联调的效率。我把所有https请求的公共逻辑收敛到了src/utils/request.js里,做了四件事:

第一,设置baseURL为/api,配合Vue CLI的devServer.proxy配置,开发环境下自动把请求转发到后端8080端口,解决开发阶段跨域问题。

第二,请求拦截器统一从localStorage取Token塞进请求头。这个逻辑只需写一次,后面所有业务接口都不用再管Token的事。

第三,响应拦截器统一处理返回状态。code为200时直接返回data给业务层,code为401时清除本地登录信息并跳转登录页,code为500时用Element UI的Message组件弹出后端返回的错误信息。前端业务代码里只用关心成功的数据,异常处理全部集中在拦截器里,代码会清爽很多。

第四,对Blob类型响应做特殊处理。下载、导出场景需要单独判断响应类型,否则拦截器会尝试JSON.parse二进制流导致解析异常。

service.interceptors.response.use( response => { if (response.data instanceof Blob) { return response.data } const res = response.data if (res.code === 200) { return res.data } if (res.code === 401) { store.commit('user/RESET_USER') router.push('/login') return Promise.reject(new Error('登录已过期')) } Message.error(res.message || '系统错误') return Promise.reject(new Error(res.message)) }, error => { Message.error(error.message || '网络异常') return Promise.reject(error) } )

4.3 商品列表筛选、购物车与订单流程的页面实现

商品列表页的筛选区、排序区、分页区是交互最密集的地方。筛选条件和分页参数需要和URL同步,我用了Vue Router的query参数管理,这样用户点击浏览器前进后退按钮时页面状态能正确恢复,把商品列表的筛选条件拼进URL也是电商系统的标准体验。刷新页面后根据query参数初始化筛选条件,触发重新拉取数据。

购物车页面我实现了三个连动交互:勾选商品后底部栏实时计算已选总金额,修改数量时同步更新后端数据并重新计算金额,删除商品时确认弹窗防误触。这里的一个经验是:数量修改要防抖,用户疯狂点击+/-按钮时,每隔300ms才发送一次后端请求,避免短时间内请求轰炸。

订单流程前端接收后端返回的订单信息,用steps组件展示当前所处的订单状态,待支付时展示“模拟支付”按钮,点击后调用后端支付接口模拟支付成功。个人中心的订单列表支持按状态Tab筛选,点进详情页可以看到订单状态的时间轴记录(下单时间、支付时间、发货时间、完成时间),这些数据全部来自后端返回的时间字段,前端只做格式化展示。

5. 系统安全与权限控制的实战处理

安全问题在课程设计和自己练手阶段最容易忽视,但我建议从第一天写代码就养成好的安全习惯。这个项目里我做了四层防护:

第一层是密码加密。用户注册和登录的密码用BCryptPasswordEncoder做哈希,每次校验拿明文密码和数据库里的哈希值比对。BCrypt的优势是每次生成的哈希值都不同,自带随机盐,同一密码的哈希结果不固定,防彩虹表破解的能力远超MD5。

第二层是SQL注入防护。所有MyBatis SQL都用#{}预编译参数绑定的方式,简单的说我传什么它都是字符串数据,不会被当成SQL语句执行。这里要特别注意一个坑:表名、列名、排序字段这些结构体不能用#{}拼接,只能用${},而这种场景必须先用白名单校验。我的商品排序实现,就是在Java代码里把用户传的排序字段映射成固定的列名字符串,而不是直接拼接用户输入,不让用户的原始输入进入SQL结构体。

第三层是接口权限控制。后台管理接口全部要求role为0的管理员权限,普通用户Token访问后台接口直接返回403。我在JWT里注入了role字段,拦截器校验完Token后会把角色也放进去,后台接口的Controller方法上用自定义注解@RequireRole("admin")标记,再写一个AOP切面统一判断。用AOP而不是在每个方法里手动if判断,代码量少很多,后期加权限点也方便。

第四层是文件上传安全。商品图片上传接口,我做了三重限制:校验文件扩展名(白名单.jpg/.png/.jpeg/.webp,不能用黑名单,黑名单永远堵不全)、校验文件大小(限制单张不超过2MB)、在IO层面读取文件头魔数判断真实文件类型。为什么这么严格?因为图片上传后是放在静态资源目录里通过URL直接访问的,如果允许恶意上传JSP或PHP文件,这是极其可怕的漏洞,相当于直接把自己的服务器大门敞开了。扩展名骗人很容易,但文件内容开头几个字节对应的真实类型很难伪装。

6. 打包、部署与环境配置全流程

6.1 MySQL安装与初始化要点

MySQL的安装网上一堆教程,这里我只说几个项目启动前必须重点确认的配置点。

字符集一定要用utf8mb4,不是utf8。utf8在MySQL里最多存3个字节,存不了Emoji表情和一些特殊字符,商品详情里万一有特殊符号就会报错。在MySQL 8.0里可以在my.ini配置文件里直接写上character-set-server=utf8mb4和collation-server=utf8mb4_unicode_ci。

时区问题也要尽早配对。JDBC连接串里加上serverTimezone=Asia/Shanghai,同时MySQL启动参数加上default-time-zone='+8:00',否则Java侧的日期和数据库侧的日期会差8个小时,订单时间记录会全部错乱,排查起来极其痛苦。

初始化脚本里,建表SQL我会把每个表都加上ENGINE=InnoDB DEFAULT CHARSET=utf8mb4,显式指定存储引擎,防止哪天有人改了MySQL默认引擎导致事务失效。是的,只有InnoDB支持事务,MyISAM引擎下@Transactional是静默失效的,扣库存到一半失败也不会回滚,这是线上事故级别的隐患。

6.2 SpringBoot配置多环境与打包

环境区分是项目上线前必须做的一件事。我在application.yml同目录下拆了application-dev.yml和application-prod.yml,dev环境打印SQL日志、数据库指向本地,prod环境关闭日志输出、数据库指向云服务器。启动时通过--spring.profiles.active=prod切换环境,IDEA里在Run Configuration的Active Profiles里填dev即可。这套多环境配置逻辑很简单,但它保证了开发和生产环境的配置隔离,不会出现把本地数据库密码带到线上的低级事故。

打包时直接用Maven的package命令生成可执行jar包,SpringBoot内嵌了Tomcat,jar包在装有JDK8的服务器上直接java -jar mshop.jar运行就行。这里有个细节,如果服务器机器内存不大,启动参数里显式指定JVM堆大小会更好,比如java -Xms128m -Xmx256m -jar mshop.jar,避免JVM默认拿机器25%内存导致其他服务崩溃。

6.3 Vue构建产物与部署方案的两种选择

Vue项目构建,在项目根目录执行npm run build,生成的dist目录里就是纯静态的HTML、CSS、JS文件。部署方式有两种,我根据实际经验把两者都写清楚,你可以根据自己服务器的情况选择。

第一种是前后端分离各自部署。dist目录用Nginx托管,Nginx配置把/api前缀的请求转发给后端的8080端口。这种方式的好处是前端静态资源加载快,Nginx的静态文件处理能力非常强,后端服务可以独立重启不影响页面访问。上线后的标准做法,我推荐这种。

第二种是省事做法:把dist目录下的文件复制到SpringBoot的src/main/resources/static目录下,重新打包jar。这样访问后端域名可以直接打开前端页面,不需要额外装Nginx。这种方式适合演示、毕设验收阶段,数据量不大、访问量低的时候完全够用。要注意一个问题:前端路由用了history模式的话,直接访问某个深层路由地址会404,需要在后端加一个转发规则,把非/api路径的请求转发到index.html。在实际SpringBoot实现上可以写一个简单的ViewController把未匹配路径forward到index.html,但如果用Nginx就一行配置搞定。

开发阶段的跨域问题,除了用Vue CLI的proxy方案,也可以在SpringBoot的WebMvcConfigurer里配置CorsMapping。但我更推荐前端proxy方案,因为生产环境用Nginx反代后根本不存在跨域问题,开发和生产环境的环境一致性更好。后端配置CORS会在浏览器层面做预检,每个请求都多一次OPTIONS往返,纯属浪费时间。

7. 常见问题与排查技巧实录

项目开发过程中踩坑是一件必然的事,这里把我在完整开发这个商城系统过程中遇到的最典型的问题和排查方法整理出来,做个速查表。

问题现象根本原因解决方案
控制台打印SQL但查询结果一直为null数据库字段是下划线风格,Java实体是驼峰风格,map-underscore-to-camel-case没开启mybatis.configuration.map-underscore-to-camel-case=true
新增和修改操作报SQL语法错误把数据库的保留字当成了字段名,比如order、desc给表和字段加反引号,或者改名成order_no等非保留字
DELETE请求报405前端axios传的Content-Type是application/json,后端接口方法参数没加@RequestParam删除请求统一用路径参数,或者后端用Long类型直接接收路径变量
前端请求报跨域错误,后端却看不到请求日志CORS预检OPTIONS请求被Spring Security或自定义拦截器拦截拦截器放行OPTIONS请求,或对CORS预检做特殊处理
中文乱码数据库表编码不是utf8mb4,或JDBC连接串没配characterEncoding统一使用utf8mb4编码,连接串加characterEncoding=utf8
每次重启jar包,图片就都丢失了图片上传到了jar包内的static目录,重启后文件被新jar包覆盖图片上传路径配置为外部绝对路径,如/opt/mshop/upload,通过映射对外提供访问
页面刷新后404Vue Router history模式下,Nginx没有配置try_filesNginx加location / { try_files $uri $uri/ /index.html; }
下单并发时库存被扣成负数扣库存SQL没有带stock >= #{quantity}条件UPDATE ... WHERE id=? AND stock>=?,并检查影响行数
长时间不操作再提交订单报未登录JWT过期时间太短把过期时间设为24小时或更长,或实现刷新Token机制;注意生产环境要对敏感操作缩短有效期

除了表格里的这些问题,我还想单独说一个非常容易让新手崩溃的场景:MyBatis配置了打印SQL日志的战果,你看到SQL已经执行了且结果正确,但Java代码里拿到的数据却是null。这种问题几乎都是映射配置的问题,要么是resultType写错,要么是实体类属性名和数据库列名对不上,要么是开启了驼峰映射但MyBatis版本不支持。排查顺序我建议先看实体类属性名是否和数据库列名完全对应,再检查resultType是否是目标实体类型,最后看配置是否生效。

另外一个我在实际项目里深有体会的经验是:遇到问题时先确认最简单的原因。MySQL连接报SSL错误时,很多人第一反应是去配置SSL证书搞得很复杂,实际上在本地开发环境直接在连接串里加useSSL=false就能解决,先排除最小可能性的问题,再逐步上复杂度。

生产环境调试又不一样。数据库上线后很多SQL问题在本地跑得好好的,线上就是不对,常见原因是线上MySQL版本和本地不一致导致的语法兼容、索引未建导致全表扫描慢查询、数据量大了之后分页越深越慢。分页深了可以用延迟关联或者限定最大页码来限制,我实现时在分页查询里做了边界保护,当页码超过100时直接强制为100,防性能损耗也防恶意请求。

8. 这套源码后续还能怎么扩展

一个商城系统做到这里,核心链路已经完整跑通了。但作为长期维护的项目,它还有很多可以加深加广的空间,这里给几个我建议的方向。

第一个方向是引入Redis做缓存和会话管理。当前版本的热门商品列表和分类信息每次请求都要查库,访问量上来以后数据库压力会肉眼可见地增大。把商品列表页、首页推荐数据缓存到Redis,设置5-10分钟过期时间,配合Spring Cache注解或者手动读写Redis,代码侵入量非常小但性能提升立竿见影。

第二个方向是消息队列解耦。下单成功后的后续动作,比如发短信通知、更新销量统计、记录操作日志,这些都不需要同步完成,可以引入MQ异步削峰。但如果是课程设计和面试展示,不需要一上来就上消息中间件,简单场景下直接Spring的@Async异步方法就够了。

第三个方向是增加管理端的数据可视化。用ECharts把每天的订单量、销售额、热门商品Top10、用户增长趋势做成图表仪表盘,数据可以从订单表按时间聚合查询。这个功能对电商系统的价值很大,运营人员关心的核心指标一目了然,实现难度不高但展示效果提升非常明显。

第四个方向是支付模块对接真实支付。当前阶段支付就是模拟接口,想接真实支付网关的话,流程上需要注册商户、配置回调地址、处理签名验签,工作量不小但流程是标准化的。如果没有商户资质做不了真实接入,用沙箱环境模拟一遍完整的支付回调流程也是不错的练手。

这套系统的所有设计决策,从手写分页到原生MyBatis,从外部存储图片到库存扣减的SQL写法,每一次取舍背后都有具体的业务场景在支撑。我不太建议直接把“能跑”作为最终目标,代码能用和用得好是两码事。停留在能跑的程度,项目就只是一堆功能的堆叠;上升到用得好,你会发现每一个设计决策都在为后续的维护、扩展和性能优化留出空间。做完这一整套,我最大的感受是:写代码最大的成本不是实现功能的那部分,而是维护、调试、排查问题反复折腾消耗的时间。前期每一个合理的设计,都会在后期替你省下大量时间。希望这篇拆解,能帮你把该避的坑提前避掉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询