
做毕设或者个人项目时我见过太多人一头扎进“最新最火”的技术堆里Spring Cloud、微服务、Docker、K8s全往上垒结果项目还没跑起来就先被环境折腾到怀疑人生。而SpringBootVue这套组合看着“老”恰恰是当下Java Web开发里最皮实、最实用的搭配。这篇文章就来细拆一个基于SpringBootVue的网上服装商城管理系统从数据库设计、后端实现、前端交互到部署上线把完整链路里那些文档里不会写清楚的细节全部摊开讲。不管你是准备做毕业设计还是想自己撸一个能用的电商系统练手这篇都能让你少走不少弯路。先交代一下这个项目的技术底子后端是SpringBoot 2.7.x MyBatis MySQL 8.0前端是Vue 2.6 Element UI AxiosJDK用的8。这套配置在2025年的今天依然非常主流招人市场上相关岗位遍地都是而且社区资料极其丰富遇到问题基本一搜就有答案。项目本身的功能不算复杂核心就是围绕“服装商品”做增删改查、购物车、订单、用户登录注册、后台管理这些标准模块但麻雀虽小五脏俱全任何一个电商系统的骨架逻辑在这里都能找到对应。我选择这套技术栈去设计整个系统就是奔着“稳妥”两个字去的。下面按照实际开发的顺序把整个系统的设计思路、核心实现、踩坑实录完整过一遍。1. 项目整体架构与设计思路拆解1.1 这个商城系统到底要解决哪些问题网上服装商城说穿了就是两件事前台让用户能逛、能买后台让管理员能管、能改。前台的核心流程是浏览商品、搜索筛选、加入购物车、提交订单、支付演示环境通常只做模拟、查看订单状态。后台管理则是商品上下架、库存调整、订单审核发货、用户管理、数据统计这些。这种系统真正难的点不是功能本身而是几个绕不开的细节商品分类层级怎么设计才能灵活扩展库存和订单之间的数据一致性怎么保证登录状态怎么在前后端分离的架构下维持图片上传后怎么访问和存储。这些问题如果不在一开始就想清楚后面开发到一半再返工成本是成倍上涨的。我在设计这个项目时把业务模块划分成了七个大块用户模块、商品模块、购物车模块、订单模块、分类模块、后台管理模块、文件上传模块。每个模块内部自己管自己的逻辑模块之间通过接口交互这样后期改任何一个模块都不会牵连到其他部分。1.2 为什么是SpringBootMyBatis而不是更花哨的组合现在很多人一上来就推荐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 核心业务表的数据模型下面把几张核心表的结构核心点列出来这些都是我在实际设计时踩过坑之后形成的最终方案。用户表 userid、username、password、nickname、avatar、phone、email、role1表示普通用户0表示管理员、status1启用0禁用、del_flag、create_time、update_time。密码存储用的是BCrypt加盐哈希不是明文也不是简单的MD5。MD5现在用彩虹表破解成本极低电商系统涉及交易数据密码安全是底线。商品表 productid、name、subtitle副标题、main_image主图URL、detail富文本详情、category_id分类id、price原价、promote_price促销价、stock库存、sales销量、status1上架0下架、create_time、update_time。price我用的是decimal(10,2)注意这里有一个重点涉及金额的字段永远不要用double或float浮点数的二进制表示会导致金额出现0.10.20.30000000000000004这种问题这是行业里踩过无数次的坑。商品分类表 categoryid、name、parent_id、sort_order、status。parent_id支撑无限层级分类顶级分类的parent_id为0。服装商城通常需要两级分类比如“男装”下挂“T恤”“外套”“女装”下挂“连衣裙”“半身裙”这套结构在后台新增三级分类时也不用改表结构。购物车表 cartid、user_id、product_id、quantity、checked是否选中结算、create_time、update_time。购物车加了一个UNIQUE KEY(user_id, product_id)防止同一用户重复添加同一商品产生两条脏数据。数量更新走ON DUPLICATE KEY UPDATE插入时若唯一键冲突就直接累加数量。订单表 orderid、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_itemid、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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue 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 idselectByCondition resultTypecom.example.mshop.entity.Product SELECT * FROM product where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR subtitle LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND promote_price #{minPrice} /if if testmaxPrice ! null AND promote_price lt; #{maxPrice} /if AND status 1 /where choose when testsortType priceAsc ORDER BY promote_price ASC /when when testsortType priceDesc ORDER BY promote_price DESC /when when testsortType 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和收货地址信息。后端处理的完整顺序是校验用户登录状态。根据购物车ids查出商品数据校验商品是否存在、是否上架。逐个校验库存是否充足库存不足则中断并提示具体哪个商品库存不够。重新从数据库查询最新价格计算订单总金额不能信任前端传来的价格前端改价攻击是电商系统最基本的安全攻防场景。生成订单号和订单主记录。生成订单明细记录写入商品快照。批量扣减库存UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}。删除购物车中已下单的商品项。整个流程用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其实很顺畅。这里如果项目是你自己从零搭也可以用ViteVue 3开发体验会好很多原理上差别不大。路由设计上我把页面分成了不需要登录就能访问的公开页面和必须登录才能访问的受保护页面。在Vue Router的全局前置守卫beforeEach里做登录校验如果目标是受保护页面且本地没有Token直接跳转登录页。这个守卫的逻辑虽然简单但是整个前端访问控制的基础缺少了它用户可以直接通过修改URL访问到订单页面接口层当然还有JWT做兜底但前端的路由控制能带来更好的用户体验——未登录用户被直接引导到登录页而不是点了按钮才收到接口报错。Vuex里我用的模块化结构有user模块和cart模块。user模块保存用户基本信息、登录状态、Tokencart模块保存购物车数量。页面刷新时从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-serverutf8mb4和collation-serverutf8mb4_unicode_ci。时区问题也要尽早配对。JDBC连接串里加上serverTimezoneAsia/Shanghai同时MySQL启动参数加上default-time-zone8:00否则Java侧的日期和数据库侧的日期会差8个小时订单时间记录会全部错乱排查起来极其痛苦。初始化脚本里建表SQL我会把每个表都加上ENGINEInnoDB DEFAULT CHARSETutf8mb4显式指定存储引擎防止哪天有人改了MySQL默认引擎导致事务失效。是的只有InnoDB支持事务MyISAM引擎下Transactional是静默失效的扣库存到一半失败也不会回滚这是线上事故级别的隐患。6.2 SpringBoot配置多环境与打包环境区分是项目上线前必须做的一件事。我在application.yml同目录下拆了application-dev.yml和application-prod.ymldev环境打印SQL日志、数据库指向本地prod环境关闭日志输出、数据库指向云服务器。启动时通过--spring.profiles.activeprod切换环境IDEA里在Run Configuration的Active Profiles里填dev即可。这套多环境配置逻辑很简单但它保证了开发和生产环境的配置隔离不会出现把本地数据库密码带到线上的低级事故。打包时直接用Maven的package命令生成可执行jar包SpringBoot内嵌了Tomcatjar包在装有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-casetrue新增和修改操作报SQL语法错误把数据库的保留字当成了字段名比如order、desc给表和字段加反引号或者改名成order_no等非保留字DELETE请求报405前端axios传的Content-Type是application/json后端接口方法参数没加RequestParam删除请求统一用路径参数或者后端用Long类型直接接收路径变量前端请求报跨域错误后端却看不到请求日志CORS预检OPTIONS请求被Spring Security或自定义拦截器拦截拦截器放行OPTIONS请求或对CORS预检做特殊处理中文乱码数据库表编码不是utf8mb4或JDBC连接串没配characterEncoding统一使用utf8mb4编码连接串加characterEncodingutf8每次重启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证书搞得很复杂实际上在本地开发环境直接在连接串里加useSSLfalse就能解决先排除最小可能性的问题再逐步上复杂度。生产环境调试又不一样。数据库上线后很多SQL问题在本地跑得好好的线上就是不对常见原因是线上MySQL版本和本地不一致导致的语法兼容、索引未建导致全表扫描慢查询、数据量大了之后分页越深越慢。分页深了可以用延迟关联或者限定最大页码来限制我实现时在分页查询里做了边界保护当页码超过100时直接强制为100防性能损耗也防恶意请求。8. 这套源码后续还能怎么扩展一个商城系统做到这里核心链路已经完整跑通了。但作为长期维护的项目它还有很多可以加深加广的空间这里给几个我建议的方向。第一个方向是引入Redis做缓存和会话管理。当前版本的热门商品列表和分类信息每次请求都要查库访问量上来以后数据库压力会肉眼可见地增大。把商品列表页、首页推荐数据缓存到Redis设置5-10分钟过期时间配合Spring Cache注解或者手动读写Redis代码侵入量非常小但性能提升立竿见影。第二个方向是消息队列解耦。下单成功后的后续动作比如发短信通知、更新销量统计、记录操作日志这些都不需要同步完成可以引入MQ异步削峰。但如果是课程设计和面试展示不需要一上来就上消息中间件简单场景下直接Spring的Async异步方法就够了。第三个方向是增加管理端的数据可视化。用ECharts把每天的订单量、销售额、热门商品Top10、用户增长趋势做成图表仪表盘数据可以从订单表按时间聚合查询。这个功能对电商系统的价值很大运营人员关心的核心指标一目了然实现难度不高但展示效果提升非常明显。第四个方向是支付模块对接真实支付。当前阶段支付就是模拟接口想接真实支付网关的话流程上需要注册商户、配置回调地址、处理签名验签工作量不小但流程是标准化的。如果没有商户资质做不了真实接入用沙箱环境模拟一遍完整的支付回调流程也是不错的练手。这套系统的所有设计决策从手写分页到原生MyBatis从外部存储图片到库存扣减的SQL写法每一次取舍背后都有具体的业务场景在支撑。我不太建议直接把“能跑”作为最终目标代码能用和用得好是两码事。停留在能跑的程度项目就只是一堆功能的堆叠上升到用得好你会发现每一个设计决策都在为后续的维护、扩展和性能优化留出空间。做完这一整套我最大的感受是写代码最大的成本不是实现功能的那部分而是维护、调试、排查问题反复折腾消耗的时间。前期每一个合理的设计都会在后期替你省下大量时间。希望这篇拆解能帮你把该避的坑提前避掉。