
## 1. 项目定位与整体设计思路1.1 为什么是商城论坛的组合先聊清楚这个项目到底做了什么。SSM231的电子竞技周边商城购物论坛名字虽然长拆开看就三件事用SSMSpring SpringMVC MyBatis做后端接口用Vue做前端页面业务上把电竞周边商城和购物论坛揉在了一起。说得直白一点这是一个面向电竞玩家的垂直电商社区既能逛商品、下单买外设和战队周边又能发帖讨论赛事、分享装备评测。这种商城社区的模式在现实中并不新鲜京东、得物、NGA都验证过这条路但放到一个练手项目里它的价值在于能一次性覆盖电商和内容社交两大场景几乎把所有Web开发的核心知识点都串起来了。为什么要把商城和论坛放在同一个系统里我从实际开发的角度说点体会。如果只做一个纯商城后端无非就是商品CRUD加订单流转前端就是列表、详情、购物车、结算做完了你会发现很多能力没有被锻炼到比如用户之间的互动、内容的审核管理、发帖和评论的数据关系。加上论坛之后业务复杂度上来了用户体系要扩展积分、等级、权限数据模型要新增帖子、评论、点赞、收藏更重要的是前后端交互的形态变多了——商城里是典型的数据驱动操作论坛里则是内容驱动操作这两套逻辑在组件设计、状态管理、接口命名上都有明显差异项目做完之后对不同业务模块怎么在同一套架构里共存这件事会理解得很透。再说这个系统的适用人群。我觉得它特别适合两类人一类是正在学Spring和MyBatis想找一个把SSM三件套真正整合起来跑通的实战项目的同学另一类是前端接触了Vue基础、但一直没做过完整前后端分离项目想看看登陆鉴权、路由守卫、动态渲染这些能力怎么落地的新手。有一定基础的开发者拿它当模板去扩展比如换成Spring Boot、加个Redis也完全不亏。1.2 技术选型背后的考量为什么还用SSM而不是Spring Boot现在聊技术选型。很多人一上来会问都什么年代了怎么还在用SSM不用Spring Boot吗这个问题我在做这个项目之前也纠结过但实际做完之后我的结论是SSM恰恰是这个项目最好的选择。理由主要有三条。第一SSM是Spring Boot的前身形态把Spring的IOC容器、SpringMVC的请求分发、MyBatis的持久层封装以最原始的方式暴露在你面前。你在SSM里手动配置过DispatcherServlet、applicationContext.xml、spring-mvc.xml你才会真正懂Spring Boot为什么能约定优于配置懂它帮我们省掉了什么。现在很多直接上Spring Boot的同学出了Bug连Bean扫描路径和事务管理器在哪都不清楚就是因为跳过了这个阶段。第二SSM框架整合本身就是一个高频面试考点面试官问起AOP代理失效怎么办MyBatis一级缓存和二级缓存区别SpringMVC的九大组件如果你只是背过答案而没有亲手配置过很难答出层次感。第三这个项目的复杂度决定了SSM完全够用没有高并发、没有分布式、没有微服务拆分SSM加MySQL加Vue已经可以非常舒服地完成全部需求杀鸡没必要用牛刀。前端选Vue而不是React我倒觉得不是单纯的哪个好的问题更核心的理由是Vue的上手曲线和这个项目的体量特别匹配。Vue的单文件组件、双向绑定、计算属性这些设计仁者见仁但不可否认它对中小型项目的开发效率极高尤其是Vue Router的路由配置和Vuex或Pinia的状态管理模式非常固定几乎没有自由度太高不知道怎么组织的困扰。而React的Hooks思维、不可变数据、性能优化链路对新手来说需要适应的时间明显更长。做一个课程级别的全栈项目更重要的是把业务逻辑跑通、把工程结构理清Vue在这个维度上是最稳的选择。最终我确定的技术栈如下。层次技术选型说明后端框架Spring 5 SpringMVC MyBatis经典SSM组合手动XML配置数据库MySQL 5.7InnoDB引擎UTF-8字符集连接池Druid 1.1.x监控与连接管理前端框架Vue 2 Vue Router Vuex组件化开发前端路由UI组件库Element UI后台管理端快速搭建HTTP通信Axios统一封装请求拦截开发工具IDEA Maven Tomcat 9本地联调环境版本控制Git Gitee项目备份与历史追踪这套组合合在一起前端的动和后端的稳刚好互补而且每个环节踩的坑都特别有教育意义。1.3 项目整体架构与核心功能模块这个项目的架构严格遵循前后端分离的思路。后端只暴露JSON接口不返回任何视图页面前端用Vue开发单页应用通过Axios调用后端接口完成数据交互。这个分离在很多真实项目里是默认配置但对于第一次完整经历前后端分离流程的开发者来说理解接口联调、跨域处理、Token传递这整套协作方式比单纯写某个功能更有价值。整个系统的功能模块我画成了四个大块这里用文字描述不依赖绘图工具也能看懂用户模块注册、登录、个人信息维护、收货地址管理、头像上传。登录成功之后后端返回Token前端存在本地存储并塞进请求头实现会话保持。商城模块商品分类展示、商品详情、关键词搜索、购物车管理、订单确认下单、模拟支付、订单状态流转。这部分是系统的核心交易链路。论坛模块帖子列表支持分页和按热度排序、发帖、帖子详情、评论与回复、点赞与收藏、积分增减。这是内容的沉淀区。管理后台商品上架下架、订单状态管理、用户管理、帖子置顶与删除、数据统计看板。管理员通过权限字段控制访问。四个模块加起来接口数量大概在六十个左右数据库表十二张左右。这个体量做SSM实战训练刚刚好——它能让你看到完整系统的全貌又不至于因为工程量过大导致烂尾。2. 数据库设计与核心表结构2.1 商城模块的库表设计数据库设计是整个后端开发的基石我当时在表结构上花了整整一个晚上反复推敲因为表设计一旦定下来后面的Mapper和业务代码全都基于它展开返工成本极高。商城模块我拆成了五张核心表t_user用户表、t_category商品分类表、t_goods商品表、t_cart购物车表、t_orders订单主表外加t_order_item订单明细表。订单为什么要分主表和明细表这是电商系统的经典设计一张订单可能包含多件商品如果每件商品都存订单号、总价、收货信息数据冗余非常大。拆开之后t_orders只存订单级别信息订单号、用户ID、总金额、状态、创建时间t_order_item存商品维度的信息商品ID、商品名称、单价、数量、小计两者通过order_id外键关联查询时一条SQL就能拼出完整订单视图。商品表有几个字段必须单独拎出来说。第一个是stock库存这个字段在并发下单场景下是兵家必争之地我先用的是update t_goods set stock stock - 1 where id ?这种直接扣减的方式没有加任何版本号控制结果用JMeter压了一轮就暴露了超卖问题后面才在t_goods里加上version字段做乐观锁每次更新时校验版本号。第二个是status我用0和1表示上架和下架状态下架的商品在前端列表页不可见但数据库里的数据保留着方便后续重上架。第三个是sales销量每次用户下单支付成功后销量加一用于前端按销量排序的查询条件。分类表其实是个典型的树形结构落地案例。电竞周边的分类大概是键盘/鼠标/耳机/显示器/战队周边/手办模玩这样一层但如果将来要扩展出机械键盘下的青轴/茶轴/RGB光效就需要parent_id自关联字段。我在这张表里直接保留了parent_id初始数据全为0表示一级分类为后续扩展留了后路。用户表除了常规的username、password、phone之外我还加了一个avatar字段存头像路径、一个role字段区分普通用户和管理员。password存储用的是MD5加盐虽然现在更推荐BCrypt但在课程项目里MD5加盐已经能说明你理解不能明文存密码这个底线了。2.2 论坛模块与商城模块的关联设计论坛模块的表设计更有意思因为它们要回答一个问题论坛和商城到底是怎么打通账号体系和积分体系的。我用的方案是用户表不单独为论坛开一张新表而是直接在t_user上增加score积分和level等级两个字段。用户在论坛发帖加5分、被点赞加2分、评论加1分这些积分在商城下单时按比例抵扣金额。这样一来引流和促活的逻辑就闭环了为了积分去论坛活跃为了花积分去商城消费消费之后又在论坛发评测整个系统自己就转起来了。论坛本身的表我设计了t_forum_post帖子表、t_forum_comment评论表、t_forum_like点赞记录表三张。帖子表的核心字段包括post_title标题、post_content正文用TEXT类型、author_id发帖用户ID、reply_count回复数、like_count点赞数、is_top是否置顶、status是否审核通过再加一个update_time用于按最新回复排序。评论表则设计成了一级评论楼层回复模式主评论挂在post_id下回复通过parent_id自关联指向上一层评论或回复这样前端展示时可以形成缩进树状结构和论坛社区常见的洋葱帖、高楼帖形态完全一致。点赞记录表t_forum_like是典型的防重复操作设计字段有user_id、post_id或comment_id、create_time并在这几个字段上建了联合唯一索引。用户点赞时先用一个查询判断记录是否存在存在就提示请勿重复点赞不存在才写入并把计数加一。这个方案在后端代码里多一步查询但换来了数据一致性比前端disabled控制靠谱得多。2.3 表关系梳理与关键索引设计整个项目的表关系其实是一场很典型的一对多、多对多的实践课。用户与订单一对多一个用户能下多笔订单订单与商品多对多通过订单明细表桥接用户与帖子一对多一个用户可以发多篇帖子帖子与评论一对多一个帖子下挂多条评论用户与点赞多对多通过点赞记录表唯一约束避免重复。索引的设计我当时是走了弯路的。第一版开发时为了让SQL简单我几乎只用了主键索引结果数据量一上来where user_id xxx这种高频条件全表扫描接口平均响应从几十毫秒涨到几百毫秒。后来在t_cart的user_id goods_id上、t_orders的user_id status上、t_forum_comment的post_id上全部补了普通索引性能才回到正常水平。这个教训让我对索引不是越多越好但高频查询条件必须有索引有了切肤体会。3. 后端SSM框架搭建与核心实现3.1 SSM整合配置与踩坑记录SSM的整合配置是整个后端开发的第一个大坎。很多人一上来就上网复制一份配置跑起来就以为会了结果遇到Bean依赖缺失、配置扫描路径错误、事务不生效这些问题完全不知道从哪排查。我这里把最核心的整合思路讲清楚。整个配置链路围绕web.xml展开。web.xml里注册了Spring的ContextLoaderListener负责加载applicationContext.xml和SpringMVC的DispatcherServlet负责加载springmvc.xml两个配置文件各有分工applicationContext.xml管数据源、事务管理器、MyBatis的SqlSessionFactory以及MapperScannerConfigurerspringmvc.xml管组件扫描、注解驱动、视图解析器和静态资源配置。注意这里的文档说是没有模板套路的实战整理所以我把常见方案对比一下很多教程习惯把Controller的组件扫描写在springmvc.xml里、把Service和Mapper扫描写在applicationContext.xml里这样确实是主流做法它的核心逻辑是让SpringMVC只负责Web层的Bean避免Controller被容器初始化两次。我按这个思路配置之后确实遇到过Service里事务不生效的情况排查到最后是因为MapperScannerConfigurer的basePackage写成了com.ssm.mapper但Mapper接口实际在com.ssm231.mapper包下包路径一错整个Mapper代理Bean根本没被创建Service依赖注入自然失败。所以配置扫描路径的时候一定要先确认包名完全一致。spring-mybatis.xml的配置同样容易出问题。SqlSessionFactoryBean里必须同时指定dataSource、mapperLocations指向XML文件路径和typeAliasesPackage实体类包名。如果你忘了mapperLocationsMyBatis会默认在Mapper接口同名路径下找XML找不到就报Invalid bound statement (not found)。这个报错我在刚开始时至少遇到过三次后来已经把检查XML路径刻进了DNA里。事务管理器使用的是DataSourceTransactionManager接上tx:annotation-driven开启注解事务。Service层的关键方法比如下单、扣库存都要标注Transactional遇到异常自动回滚。这里有一个细节值得注意Transactional只对RuntimeException非受检异常默认回滚如果你在业务代码里手动catch了异常但没有throw出去事务是不会感知到失败的数据就会产生脏写入。整合配置最终稳定之后我背下了SSM配置六要素数据源有、SqlSessionFactory有、Mapper扫描有、事务管理器有、组件扫描有、注解驱动有。每次从零搭新项目就按这个清单核对再也没因为框架整合问题卡壳过。3.2 商品管理、购物车与订单的完整实现商城模块的后端实现是典型的业务分层代码Controller拿到前端请求参数调用Service接口Service做业务判断调用Mapper完成数据库操作。我这里挑三个最有代表性的链路讲。商品列表接口的逻辑是分页加动态条件查询。Controller接四个参数pageNum第几页、pageSize每页几条、keyword搜索关键词、categoryId分类ID。Service层把这四个条件封装成一个Map传给Mapper在XML里用MyBatis的where、if标签拼SQL关键字匹配用like模糊查询。这里有一个很重要的防坑点MyBatis的like拼接不能写成like %{keyword}%这种直接拼的方式而是要用%加#{}加%的写法即like % #{keyword} %这样能避免SQL注入也能正确传递参数字符串。分页实现我用的PageHelper插件只需要在查询前调用PageHelper.startPage(pageNum, pageSize)插件会自动生成带limit的SQL再配合PageInfo封装分页结果返回给前端的数据就包含了总记录数、总页数、当前页这些标准分页要素。购物车链路走的增删改查四件套但有个交互细节值得展开前端页面加入购物车时后端要先判断这个用户是否已经把这个商品加过购物车了如果加过就把原有记录的数量加一而不是新建一条重复记录。这个逻辑听起来简单做起来却容易疏忽——很多新手直接insert结果购物车里同一件商品出现好几行用户体验极差。我在Mapper里加了一个select cart where user_id and goods_id?的查询再根据结果决定走update quantity还是insert。订单链路是整块业务里最复杂的。用户从购物车勾选商品、点击结算之后前端提交的不是下单指令而是一串购物车条目ID列表加收货地址ID。后端要做的事情按顺序拆开是这样的根据购物车条目ID查出对应的商品和数量遍历每条商品判断库存是否充足不充足直接抛异常回滚所有商品校验通过后生成一个唯一的订单号我用时间戳加用户ID拼的计算订单总金额写入订单主表和订单明细表把购物车中已下单的条目物理删除扣减商品库存。如果在第3步之后、第4步之前进程挂了事务回滚会把前面的操作全部撤销。我当时为了验证这个回滚机制故意在代码里加了一行int i 1 / 0;这会产生ArithmeticException跑起来发现订单表里确实没有脏数据这才放心。后面我又补充了一个模拟支付接口状态流转是待支付→已支付→待发货→已发货→已完成每一次状态变更都记录一条update_time前端订单列表就靠这个时间字段展示最新动态。3.3 论坛发帖、评论与积分机制的实现论坛模块的后端实现比商城简单直接但也更讲究读写不一致的问题。这里我重点说两个做法。第一个是帖子的浏览计数和热度排序。帖子详情接口每次被调用Service层就对visit_count加一这里先不用考虑并发去重因为浏览数本身就是模糊统计数字大一点小一点用户感知不强。热度排序我用的策略是一周内热度 浏览数 * 0.3 点赞数 * 0.5 评论数 * 0.2。为什么这样加权浏览数最容易灌水权重给低点点赞和评论是用户真实互动的体现权重给高点。排序在SQL里用ORDER BY加LIMIT实现因为数据量小不引入搜索中间件也完全够用。第二个是发帖加积分、评论加积分的机制。这部分我特意做了事务处理先插入帖子/评论记录再更新用户的score字段。这里容易踩的坑是用户积分更新失败后帖子却已经发出去了导致发帖白嫖加分的漏洞。我用Transactional保证这两个步骤要么同时成功、要么同时失败再从t_user里查一次最新积分根据积分区间自动更新level0-100积分为Lv1100-500为Lv2以此类推。用户在前端个人中心刷新就能看到等级变化对活跃用户的正向激励效果很明显。4. 前端Vue页面设计与交互实现4.1 Vue项目结构与路由设计前端项目我用Vue CLI初始化目录结构按视图组件接口层状态层的套路组织。src/views下放页面级组件如Home.vue、GoodsDetail.vue、Cart.vue、OrderList.vue、Forum.vue、PostDetail.vue、Login.vue、Adminsrc/components下放可复用的子组件如GoodsCard.vue、CommentItem.vue、Pagination.vuesrc/api里每个模块单独建一个JS文件统一导出封装好的Axios方法如api/goods.js里有getGoodsList、getGoodsDetailsrc/store用Vuex管理用户信息和购物车状态src/router配置路由表。路由设计上最有意思的是路由守卫权限控制这个环节。普通用户可以访问的页面有/home、/goods、/cart、/order、/forum等管理员才能访问的路由统一加在/admin路径下。我把管理员路由的meta属性设成{ requiresAdmin: true }然后在全局前置守卫里这样判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else if (to.meta.requiresAdmin ...用户角色不是管理员) { next({ path: /home }) } else { next() } })这段逻辑让用户跳转体验很顺畅没登录想加购会先跳到登录页登录完还能通过redirect参数自动回到之前停留的页面而不是粗暴地甩回首页。路由还有一个常踩的坑是刷新页面404。因为Vue是单页应用刷新之后浏览器会向服务器发起真实HTTP请求而服务器根本没有这个路径。我在前端开发阶段用Vue CLI的devServer解决得很无感但部署到Tomcat之后就暴露了。解决思路有两个一是后端加一个RequestMapping兜底转发SpringMVC里实现一个ViewContorller把非接口路径转发到index.html二是直接用Hash模式不依赖服务器配置。我在生产部署时用的是History模式加后端转发因为URL不带#号更好看也更接近真实项目。4.2 商城主流程的前端实现商城的前端交互分成浏览、加购、结算、订单管理四条主线。商品列表页我用了Element UI的Card组件栅格布局每个卡片展示商品图片、名称、价格和销量点击之后用Vue Router的params/query方式跳转到详情页。详情页的图片轮播、规格选择、数量增减都是常规操作这里重点说购物车状态管理。购物车在Vuex里的结构是一个Map映射goodsId - { goodsInfo, quantity, checked }。为什么用Map用数组更合适因为购物车需要频繁做判断某个商品是否存在按ID更新数量这类操作用Map查找的复杂度是O(1)用数组需要遍历而且Vue的响应式对数组下标修改有额外的更新规则限制。用户勾选或取消勾选时我通过commit一个mutation更新checked字段同时用getters里做计算属性哪些条目选中了、总金额多少、总数量多少全部由getters派生出来组件里只需要调用一次就能拿到最新值这样避免了多个页面各自维护一份手动计算的重复代码。订单结算页是把购物车数据和收货地址信息拼在一起展示的。因为我们在后端状态下钩了/cart所以结算页直接复用后端返回的数据不再二次调接口。做完这个项目之后我对状态提升State Lifting的理解加深了一层购物车状态放在Vuex里不管在哪个页面都能直接用不需要层层props传递这在组件层级变深时省去了大量心智负担。4.3 论坛页面的交互细节与性能优化论坛页面是唯一一个内容区块占主体的模块交互上更强调沉浸感和操作即时反馈。帖子列表页我用的是左侧分类导航右侧时间流的经典论坛布局每一条帖子卡片显示标题、摘要、作者、回复数、点赞数、最后回复时间。分类导航切换时前端通过watch监听当前选中分类然后重新调用getPostList接口拉取数据。这里我做了一个小的性能优化列表页的帖子和详情页的帖子数据体量差异很大列表接口只返回post_title、post_summary摘要、reply_count、like_count这些轻量字段详情接口才返回完整的post_content完整文本。刚开始图方便把完整内容也塞进了列表接口页面在数据量稍大时白屏时间明显变长这种字段裁剪的经验教训比什么教程都来得深刻。帖子详情页的评论模块是Vue组件递归用的一个天然练习场景。评论数据是树状结构主评论下挂若干回复回复下还能再挂回复我写了一个CommentItem.vue组件组件内部template又引用了自己Vue会递归渲染出完整的评论树。递归组件必须注意终止条件当comment.children.length 0时不再渲染子组件否则会无限循环。这里还需要处理一个当前登录用户是否点赞过这个帖子的问题做法是在详情接口返回时带上当前用户ID和点赞表的关联查询结果前端根据isLiked字段切换点赞按钮的高亮样式并做防重复点击的loading控制。论坛还有个操作——发帖。发帖页我用的是mavon-editor这个Markdown编辑器组件用户写标题和正文支持图片粘贴上传正文以Markdown源码提交给后端详情页再用marked解析成HTML渲染。用Markdown而不是富文本编辑器核心原因是电商社区的内容大多是图文结合的评测帖Markdown的排版规范、源码存档简单、XSS风险也更小后端只要过滤掉部分标签就行。我在这上面还遇到了一个回车键触发表单提交的小问题最后在keydown.enter里调了preventDefault才解决这些细节写出来都是实打实的经验。5. 常见问题排查与避坑实录5.1 跨域与接口联调问题前后端分离开发时跨域是绕不开的第一道坎。我在本地开发时Vue启动在localhost:8080后端Tomcat跑在localhost:8081浏览器直接发Axios请求会报CORS错误。解决方案有两道防线我两道都上了。第一道防线是后端配置CORS过滤器。在SpringMVC里我写了一个WebMvcConfigurer的子类实现addCorsMappings方法允许http://localhost:8080这个来源访问接口允许方法包括GET/POST/PUT/DELETE/OPTIONS并allowedHeaders(*)。注意跨域请求在正式请求之前会先发一个OPTIONS预检请求如果你拦截器没有放行OPTIONS前端会一直卡在Access-Control-Allow-Origin的报错上。第二道防线是前端的开发代理。在vue.config.js里配置devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } }这样前端请求/api/goods/list会代理到后端的/goods/list绕开跨域的同时还能顺带把/api前缀去掉。我实际开发时在开发环境用代理在生产环境用CORS配置两套方案各司其职联调阶段几乎没有再因为跨域问题卡过壳。5.2 数据一致性问题库存扣减与并发控制库存扣减是这个项目里最能体现数据一致性意识的地方。我一开始写的是先查库存再更新库存的秒杀式写法Goods goods goodsMapper.selectById(goodsId); if (goods.getStock() quantity) { goodsMapper.updateStock(goodsId, goods.getStock() - quantity); }这段代码在单线程下没有任何问题但一旦有多个用户同时下单同一件商品就会出现经典的并发超卖两个请求都查到库存还有10件然后都通过了判断分别扣成了9和9实际卖出了20件库存却只扣了1件。解决方式我选的是乐观锁在t_goods表加version字段更新SQL改成UPDATE t_goods SET stock stock - #{quantity}, version version 1 WHERE id #{goodsId} AND version #{version}更新操作返回影响行数如果返回0说明版本号不一致说明有人在操作过程中已经改过了这时Service层抛出库存不足或商品已更新请重试的异常提示用户。对于这个项目体量来说乐观锁已经够用但如果未来要支撑真正的秒杀场景还应该引入Redis预扣库存或者队列异步削峰那是另一个进阶话题了。订单状态流转和库存扣减是联动的。如果用户下单后一直不支付理论上库存应该被锁住而不是永久扣减。我在设计初期把扣库存放在了下单成功时刻之后用户不支付库存就一直被占用直到超时未支付才通过定时任务恢复。这个方案做起来有点重我最后采用了简化版扣库存放在支付成功之后下单时只校验并冻结一部分库存支付成功后再真正扣减退款时恢复库存。这样虽然SQL逻辑多了一步但至少不会因为下单不支付而卖空。5.3 Vue前端渲染与状态管理的坑前端这边值得记录的问题也挺多的。第一个是数组更新视图不刷新。我用Vuex管理购物车时更新某个商品的quantity直接写成state.cart[index].quantity newVal页面没有任何变化。排查之后发现Vue 2无法侦测到数组下标的直接赋值解决办法是改用Vue.set或使用数组的splice方法state.cart.splice(index, 1, newObj)。这个坑在我做项目的第一天就踩了也算是每个Vue 2开发者都要交的学费。第二个是路由参数变化但页面不刷新。用户从/goods/1跳到/goods/2商品详情接口没有重新请求页面里内容还是上一件商品。原因在于Vue Router复用了同一个组件实例created生命周期不会再次触发。解决方式是在组件里用watch监听$route.params.id的变化变化后重新调用getGoodsDetail方法。这个组件复用机制刚接触时特别反直觉但它背后的复用逻辑是符合性能优化的初衷的。第三个是Element UI表单校验的异步问题。注册表单里用户名是否已存在这种校验必须发请求给后端但校验规则里trigger: blur写起来很顺手实际运行时blur触发时异步校验还没返回导致提示延迟、多次触发。我后来把校验改成trigger: change加validator自定义校验函数再配合loading状态控制体验才算正常。5.4 常见问题速查表最后把这几个模块的高频问题整理成一个速查表方便你踩坑时直接查询对照。现象可能原因解决办法Invalid bound statementMapper接口与XML路径不匹配检查mapperLocations配置和namespace包路径跨域请求失败预检请求OPTIONS被拦截后端CORS配置放行OPTIONS请求商品重复加购缺少判断是否已存在的查询在插入之前先select判断存在则update数量订单提交后库存超卖并发场景没有锁机制采用乐观锁version字段更新不成功则提示重试Vue页面数组更新不刷新Vue 2无法侦测数组下标变更使用Vue.set或splice方法更新路由参数变化页面不变组件被复用created不触发使用watch监听$route变化后重新加载数据发帖成功后积分没增加两个数据操作没有放在同一事务Service方法上添加TransactionalToken失效后接口返回401拦截器只做了权限判断未处理异常全局Axios响应拦截器统一跳转登录页排查问题的核心思路其实就一句话先确认数据层SQL是否执行对、是否返回了预期数据再确认接口层请求参数和返回值是否完整最后才看视图层。顺序反了你会陷在咦我这里代码明明是对的的无限循环里。写在最后的小心得做这个项目最大的体会不是我学会了SSM和Vue这么简单而是第一次体会到了完整全栈系统三个字的重量。以前跟着教程一个个功能点学总觉得每个知识点都很简单直到把它们串起来才会发现真正的复杂度其实都在边界上跨域的那条HTTP message、事务回滚的那一行异常、路由守卫里的那一句判断、Deploy时的那一个路径配置。把这些边界问题一个个踩平你对Web开发的认识才算真正从能跑升级到了跑得稳。从扩展性上讲这个项目后续还可以做很多文章把SSM整体迁移到Spring Boot加MyBatis-Plus感受约定优于配置的爽快感把前端部署方式从npm run build换成Nginx加反向代理给论坛加上Elasticsearch做全文搜索把用户登录升级成JWT加刷新令牌机制甚至可以把积分系统扩展出积分商城兑换实物周边——这些方向我都列在待办清单里了如果你也在做类似的项目非常建议沿着这些方向继续折腾每换一个角度都会逼着你重新审视之前写的代码里有问题的假设。我自己的感受是这个项目最珍贵的是给了你一个足够复杂但也足够可控的试验场这才是它真正的价值。