ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSM+Vue前后端分离商城与论坛项目实战解析

SSM+Vue前后端分离商城与论坛项目实战解析 打游戏的人都知道收藏一套喜欢的战队的周边、和同好聊比赛聊装备是比上分还上头的事。所以我接到SSM231这个项目时心里其实挺有数的老板要的不是一个花架子而是一个真正能用的电子竞技周边商城论坛。这个项目的技术栈一眼就能看明白——后端Spring SpringMVC MyBatis前端Vue全家桶典型的SSMVue前后端分离项目。SSM231是课程设计系统里的内部项目编号但整个系统的完整度已经可以当成小型商业项目来打磨了。做这类项目的人通常有三种一是找毕设题目的学生想用商城论坛这种通用业务练手二是想从前端或后端单边跳向全栈的开发者三是公司内部需要一套带社区功能的周边商城demo。这篇内容我不打算讲那些泛泛的框架介绍直接把我在做这个项目时踩过的坑、做过得选型、写过的关键代码和配置一次说清楚希望给正在做类似项目的人一条可以照着走的路。1. 项目整体架构与技术选型分析1.1 为什么选SSM而不是Spring Boot我知道很多人会问都什么年代了还SSM直接用Spring Boot不香吗说实话如果是我自己从零搭一个商业项目我肯定选Spring Boot。但SSM231这个项目不一样它有着强烈的教学和解构框架底层的需求。SSM这套组合能让你把Spring的IOC容器怎么装配、SpringMVC的请求分发链条怎么跑、MyBatis的SQL和Mapper怎么绑定从头到尾过一遍。这个过程是Spring Boot帮你隐藏掉的那些细节恰恰是这个项目最有价值的训练点。我在项目中选用的版本搭配是这样的组件版本说明Spring5.1.8.RELEASE核心IOC容器SpringMVC5.1.8.RELEASEWEB层框架与Spring同版本避免冲突MyBatis3.4.6ORM框架MyBatis-Spring2.0.1让MyBatis的Mapper纳入Spring管理MySQL5.7数据库Druid1.1.10连接池监控SQL方便Jackson2.9.9JSON序列化前后端分离必备版本这块我必须多说一句SSM这种老组合最怕的就是版本冲突。我见过太多人Spring用4.xSpringMVC却用5.x结果启动直接报BeanDefinitionStoreException。这里没有太多技术含量就是老老实实让Spring和SpringMVC大版本保持一致MyBatis-Spring和MyBatis的版本对照关系也去官方文档查清楚了再动手。另外JDK我用的是1.8Tomcat 8.5Maven 3.6这三个配合SSM是经过时间检验的稳定组合。1.2 前端为什么用Vue而不是JSP或其他SSM天然能和JSP配合为什么非要套一个Vue因为商城论坛这种交互密集型的项目用户要的是不刷新页面就能加购物车、切换商品分类、打开帖子评论JSP那套页面跳转的模式体验真的不行。Vue的优势在于组件化和响应式商品卡片是一个组件购物车数据变化页面自动跟着变这种开发体验和最终交互效果都舒服太多了。我选的Vue方案是Vue 2.6.x Element UI Vue Router Vuex Axios。为什么不选Vue 3因为SSM231立项的时候Vue 3和Element Plus还没完全稳定而且Vue 2的社区资料多到几乎你遇到的任何报错都有人踩过对于学习者来说这是最大的友好性。Vue Router用的3.x版本Vuex用的3.x这些都是Vue 2的配套版本。如果现在重新让我选我可能会考虑Vue 3但在当时这个技术栈里Vue 2是风险最低的决策。前端工程化我用的是Vue CLI 4.x用vue create ssm231-web初始化的项目。这里有个实践经验创建项目时建议手动选择特性至少勾上Router和Vuex不要用默认的babel-only模板省得后面自己手写路由和状态管理目录。1.3 前后端分离项目的目录结构设计项目根目录下我分成了两个部分ssm231-server是后端Maven工程ssm231-web是前端Vue工程。后端严格按Maven标准目录走controller、service、mapper、entity、common分包。前端则按视图维度分views下放商城首页、商品详情、购物车、订单列表、论坛帖子、登录注册这些页面组件components放公共组件router统一管理路由store管状态。开发阶段我是让前端跑在8080端口后端跑在8081端口通过Vue CLI的devServer代理解决跨域。生产部署则是前端打包成dist目录交给Nginx托管后端打成war包丢给Tomcat。这套结构的好处是前后端可以并行开发我定好接口文档后前端同事自己mock数据也能干活。对于一个人做全栈的情况来说这个结构也方便隔离问题——页面出问题先看前端控制台接口出问题直接打后端日志不用在一堆代码里乱翻。2. 数据库设计与核心模块拆解2.1 用户与权限模块的数据表设计商城和论坛都需要用户体系这个基础表的设计直接影响后面所有业务。我设计的用户表核心字段是user_id主键username唯一索引password存的是MD5加盐后的值加盐的盐值存在独立字段salt。这里我强调一下虽然MD5现在不算安全但作为项目展示和课程设计级别够用了。如果你要商用建议至少换成BCrypt或SHA-256加盐循环不过那又是另一套方案了。用户角色这块我没做复杂的RBAC就一张user_role表把用户和角色关联起来角色就两大类普通用户和管理员。管理员能进后台管理页面管理商品上下架、审核帖子普通用户只能浏览商城、下单和发帖回帖。权限控制在后端拦截器里面做前端路由也做了一层拦截。前端那些路由守卫不是真正的安全边界它只是优化体验不让普通用户看到管理入口而已真正的校验一定是在后端接口层。还有一张用户扩展信息表存头像、积分、注册时间、个性签名。积分这块是论坛活跃度的体现用户发帖加积分、回帖加积分积分高了显示等级徽章。虽然是周边商城为定位但这个积分体系让论坛有了社区氛围用户会更愿意留下来发帖互动。2.2 商城模块商品、购物车与订单表商城模块我拆成商品、购物车、订单三大块加上一个订单详情表。商品表的核心字段是product_id、product_name、category_id、price、stock、cover_image、detail_images、status。这里有个设计细节我要专门说detail_images我用的是JSON字符串存储多张图片用JSON.parse取出而不是单独建一张商品图片表。对于这种数量不超过几张图的场景JSON字段比关联表更省事查询还少一次关联。不过这种做法不适用于图片数量动态膨胀的业务你要结合场景判断。购物车表核心字段是cart_id、user_id、product_id、quantity、checked。我额外加了checked字段这是为了实现“勾选部分商品结算”的功能。很多购物车实现会把勾选状态存在前端Vuex里但那样刷新就丢了。我选择把勾选状态同步到后端刷新页面后购物车状态依然一致这个体验细节虽然小但用户能感受到用心。订单表做了主表加子表的经典结构。orders主表存order_id、user_id、total_price、status、create_time、pay_time、consignee_name、consignee_phone、consignee_addressorder_item子表存item_id、order_id、product_id、product_name、product_price、quantity。把商品名称和价格冗余到子表里是必须的不然订单历史里的商品改价或删除后订单记录就看不出来当时买的是什么了。2.3 论坛模块帖子、评论与点赞表论坛模块对于SSM231这种周边商城项目来说是增加用户粘性的关键。我设计了post帖子表、post_comment评论表、post_like点赞表、forum_category论坛分类表。帖子表字段包括post_id、user_id、category_id、title、content、view_count、like_count、comment_count、is_top、status、create_time。content字段我用的是TEXT类型存富文本内容。富文本编辑器前端用的Element UI的Upload配合wangEditor内容以HTML形式提交后端原样存。评论表做了两级结构comment_id、post_id、user_id、parent_id、content、create_time。parent_id为0表示一级评论非0表示对某条评论的回复。这样能实现类似“层中楼”的展示效果但深层次嵌套不做因为真正的多层递归评论对前端展示和后端查询都是负担在论坛场景里两层就够用了。点赞表最简单就是like_id、post_id、user_id、create_time加上一个唯一索引(post_id, user_id)。这个唯一索引很关键它保证了同一个用户对同一篇帖子只能点赞一次点赞和取消点赞都走这个表有记录就删、没记录就加。用唯一索引比应用层先查再插要安全得多多线程场景下不会出现脏数据。2.4 表关系与索引设计心得表关系上订单和用户是一对多订单和订单详情是一对多帖子和评论是一对多帖子和点赞是一对多。外键我其实没有在数据库层面建立只是逻辑关联。这个问题我想多说一点在互联网项目里我通常不建物理外键因为外键会造成插入和删除的性能开销而且当后续分库分表的时候物理外键根本没法迁移。SSM231我用的是逻辑外键在应用层保证引用关系这也是现在主流做法。索引这块我的原则是“查询优先”。商品表的category_id建了普通索引因为用户点分类要过滤订单表的user_id建了索引因为按用户查订单是最常见路径评论表的post_id建了索引打开一个帖子要拉它的全部评论点赞表的联合唯一索引前面说了。但一些低区分度的字段我不建索引比如订单状态status就只有那么几个值查出来的数据占比很大走索引反而不如全表扫描。索引不是越多越好这个度要靠自己对业务查询模式的理解来把握。3. 后端SSM实现要点与接口设计3.1 SSM框架整合的配置文件细节SSM三个框架整合配置文件是第一个坎。我在项目中用了三个核心配置文件applicationContext.xml管Springspringmvc.xml管SpringMVCjdbc.properties放数据库连接信息。我在applicationContext.xml里做了几件关键的事开启注解扫描并用context:exclude-filter排除掉Controller注解让Spring容器只管理Service和Mapper配置Druid连接池和数据源配置SqlSessionFactoryBean并指定typeAliasesPackage和mapperLocations配置MapperScannerConfigurer让MyBatis的Mapper接口自动代理。下面这个是我认为最关键的Mapper扫描配置bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.ssm231.mapper/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ /bean这段配置的意思是扫描com.ssm231.mapper包下所有Mapper接口自动生成代理实现并注入Spring容器。很多初学者在这里踩坑是因为用了sqlSessionFactory属性名而非sqlSessionFactoryBeanName如果Spring容器里存在多个数据源会导致扫描时拿错工厂。然后在springmvc.xml里我配置了组件扫描只扫Controller、注解驱动、视图解析器虽然前后端分离后基本不用但还是要配一个避免忘掉、JSON消息转换器、文件上传解析器。还有最关键的一个配置静态资源放行。因为前端开发阶段有代理但为了在后端直接访问静态文件测试方便我还是把/static/**和/upload/**都放行了。3.2 统一返回体与全局异常处理的实现前后端分离项目里接口返回格式必须统一否则前端每个接口都要单独判断返回结构那代码就没法维护了。我定义了一个Result类结构是这样的public class ResultT { private Integer code; // 200成功400参数错误401未登录500服务端异常 private String message; // 提示信息 private T data; // 业务数据 }所有接口都返回这个Result前端Axios拦截器统一判断code字段200放行业务逻辑非200弹出message提示。这里有一个经验教训不要直接用HTTP状态码作为业务返回值因为HTTP 200也可能携带业务上的“用户名已存在”或“库存不足”你没法用HTTP状态码精确表达业务错误所以我用code字段独立表达。全局异常处理我用的是SpringMVC的ControllerAdvice加ExceptionHandler。业务层抛出的自定义BusinessException会被统一捕获转化成Result返回并记录日志。这样Controller里的代码就会很干净RestController RequestMapping(/api/product) public class ProductController { GetMapping(/list) public ResultListProduct list(RequestParam Integer categoryId) { ListProduct list productService.listByCategory(categoryId); return Result.success(list); } }不用在每一个方法里写try-catch出什么错误都交给全局处理参数校验通过Validated自动完成。这一点我觉得是SSM项目的进阶用法很多人写SSM还在Controller里塞一大堆try-catch代码看着就头疼。3.3 用户登录与JWT令牌签发校验登录认证这块我没用Session因为前后端分离情况下Session跨域处理麻烦而且移动端也要复用接口。我用的是JWT方案流程是用户提交账号密码后端验证通过后签发JWT返回给前端前端存到localStorage每次请求在Header里带token字段后端拦截器统一解析。签发JWT我用了io.jsonwebtoken库关键代码如下String token Jwts.builder() .setSubject(username) .claim(userId, user.getUserId()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();有效时间我设置7天这样用户在论坛逛一圈、购物车放几天再结算都不会被踢出去。登录拦截器是一个HandlerInterceptor在preHandle里取出Header中的token解析失败或过期直接返回401并写出JSON前端收到401就自动跳到登录页。这里我踩过一个坑JWT的HS256是共享密钥对称加密密钥一定要放在后端配置里不能暴露如果放到前端代码里那等于没加密。还有一个细节不需要登录的接口一定要在配置里放行比如商品列表、商品详情、帖子列表、帖子详情这些。登录拦截器是配置在springmvc.xml里的mvc:interceptors mvc:interceptor mvc:mapping path/api/**/ mvc:exclude-mapping path/api/user/login/ mvc:exclude-mapping path/api/user/register/ mvc:exclude-mapping path/api/product/**/ mvc:exclude-mapping path/api/post/list/ mvc:exclude-mapping path/api/post/detail/ /mvc:interceptor /mvc:interceptors这个配置路径的先后顺序有讲究exclude-mapping的优先级低于mapping所以先声明mapping再列exclude才生效。我第一次写把这个颠倒了结果所有接口都被拦截排查了半天才发现是配置顺序问题。3.4 商城核心业务购物车结算与扣库存的并发控制购物车结算这个功能看起来简单但里面暗藏了一个并发问题两个用户同时购买同一个商品库存只剩1件如果不控制两个订单都能成功生成但库存变成负数了。我在SSM231里用了两种手段处理这个问题。第一种是数据库层面的乐观锁商品表加一个version字段更新库存时带上版本号条件UPDATE product SET stock stock - #{quantity}, version version 1 WHERE product_id #{productId} AND stock #{quantity} AND version #{version}如果影响行数为0说明库存不足或版本号不对事务回滚提示用户“商品库存已更新请重新下单”。stock #{quantity}这个条件是最重要的一层它从数据库层面杜绝了扣成负数的情况。第二种是Redis分布式锁我在下单前对每个商品ID加一个锁避免同一个商品的并发下单请求同时进入扣库存逻辑。其实对于SSM231这种单机部署项目用数据库本身的原子更新就够了但加上Redis锁能演示更完整的解决思路。这里需要注意锁的释放逻辑我用的是try-finally结构确保锁一定会释放不然死锁会让整个下单接口瘫痪。整个下单流程是前端提交购物车勾选的商品列表和收货地址后端开启事务逐件校验商品状态和库存价格再更新库存、生成主表和子表订单、清空购物车已结算商品最后提交事务。任何一步失败就throw new BusinessException让事务回滚同时全局异常处理器负责告知前端具体原因。3.5 论坛模块的接口设计思路论坛模块的接口我围绕着帖子列表、帖子详情、发布帖子、评论、点赞、分类管理来设计。帖子列表接口比较有代表性因为它涉及分页、分类筛选、置顶排序三个需求。分页我用的是PageHelper插件这是我的个人偏好因为它的侵入性极低。只要在Mapper查询前调用PageHelper.startPage(pageNum, pageSize)后面跟着的那条查询就会自动被插件改成分页SQL返回的Page对象里直接带着总数。这里有一个使用细节PageHelper.startPage只对紧接着的第一条SQL查询生效如果你在调用之后又执行了别的查询分页就会错乱所以必须保证代码里紧随其后就是分页查询那条SQL。帖子列表接口如下GetMapping(/api/post/list) public ResultPageResultPostVO list( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Integer categoryId) { PageHelper.startPage(pageNum, pageSize); ListPostVO posts postService.selectPostPage(categoryId); return Result.success(new PageResult(posts)); }帖子详情接口我额外做了浏览量自增每次打开详情view_count 1。浏览量这个数值不需要精确无比偶尔丢几次或重复加几次不影响大局所以直接更新就行没有必要加锁。点赞接口就是前面说的那个唯一索引的应用场景先查有无记录再决定插入还是删除同时更新帖子表里的like_count。评论接口则需要登录权限校验当前用户在线后插入评论记录。4. 前端Vue实现与前后端联调实战4.1 Vue环境搭建与依赖安装前端环境搭建说简单也简单说坑也多。首先Node版本就有讲究我当时用的Node 12.16.3配Vue CLI 4.5这是因为新版Node有时候会和旧版node-sass编译不兼容。所以如果你照着我的方案搭装的依赖里如果有sass建议直接用dart-sass而不是node-sassnpm install sass1.32.13 --save-dev npm install element-ui2.15.0 vue-router3.5.1 vuex3.6.2 axios0.21.1Vue项目目录结构我习惯按模块划分而不是单纯按文件类型划分。src/api目录下每个模块一个文件比如product.js放商品相关接口请求、post.js放论坛相关请求、user.js放用户相关请求每个接口导出一个具名函数。这样做的最大好处是页面组件里不直接散落axios.get所有请求入口都在api目录下接口地址改动只改一处。我还建议在项目初期就要配好vue.config.js里的开发代理devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }这样前端代码里所有的请求都写相对路径/api/xxx开发时由devServer转发到后端8081端口。如果不配这个代理你就要在每个请求里写死http://localhost:8081/api/xxx等部署到线上又要全部改成服务器域名那就非常被动了。4.2 前端路由设计与权限控制路由设计我分成两部分一是商城和论坛的用户页面二是后台管理页面。用户页面的路由包括/home、/product/detail/:id、/cart、/order/list、/post/list、/post/detail/:id、/post/create、/login、/register管理端路由包括/admin/product、/admin/post、/admin/category。路由懒加载我是必须用的按需加载能显著减少首屏包体积const ProductDetail () import(../views/product/ProductDetail.vue)权限控制用Vue Router的router.beforeEach导航守卫。我在每次路由跳转前做两件事一是读取Vuex里的token没有则跳登录页二是判断目标路由的meta.requiresAdmin当前用户角色不是管理员就重定向到首页。这个前端守卫只是为了隐藏页面入口真正安全要靠后端拦截器这个我在前面后端章节强调过。踩过的一个坑是Vue Router默认使用hash模式URL里带#刷新页面没问题但不好看。我换成了history模式配置mode: history后开发环境没问题但打包部署到服务器后刷新页面会404因为服务器上没有对应的物理路径。解决方式是让Nginx配置try_files $uri $uri/ /index.html把所有的路由都回退到index.html。4.3 Vuex管理登录态与购物车数据Vuex在这个项目里管两类状态一是用户登录信息二是购物车数据。用户信息这一块比较直白state里存token和userInfo登录成功后调commit更新退出登录就清空并跳转登录页。购物车状态我用了模块化管理store/modules/cart.js。但购物车的数据最终还是以服务端为准前端Vuex里的购物车数组只是为了页面响应速度。每次进购物车页面我会重新拉一遍购物车数据然后向服务器同步勾选状态。在商品详情页点击“加入购物车”时前端把商品ID、数量通过接口发给后端后端插入或更新购物车记录同时返回最新的购物车数量前端更新右上角Badge的小红点数字。这个流程串起来的体验是加了商品之后右下角立即弹出小提示右上角数字跳动用户不需要进购物车页面就能知道加入成功。有一个细节容易被忽略就是“退出登录时清空购物车状态”。如果不清下一个用户登录后会看到上一个用户残留的购物车闪现这就是明显的数据泄露了。我在actions/logout里先清除本地购物车状态再调接口通知后端清理会话相关数据最后跳转登录页。4.4 Axios封装与请求拦截器的用法Axios封装是我觉得前端工程化里最值得做的一件事。我在src/utils/request.js里创建了一个统一实例import axios from axios import { Message } from element-ui import router from ../router import store from ../store const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { if (store.state.user.token) { config.headers[token] store.state.user.token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) if (res.code 401) { store.dispatch(user/logout) router.push(/login) } return Promise.reject(new Error(res.message)) } return res.data }, error { Message.error(error.response?.data?.message || 网络异常请稍后重试) return Promise.reject(error) } )这段代码的价值在于所有请求自动带上token所有响应统一处理错误401自动退出登录业务数据直接返回data字段。页面里调用接口就不再重复写错误处理逻辑了。比如商品列表的接口export function getProductList(params) { return service.get(/product/list, { params }) }调用方拿到的是已经剥离外层包裹的data数组直接用就行。4.5 商品列表与论坛帖子的渲染细节商品列表页我用了Element UI的el-card加el-row/el-col栅格布局每个商品卡片显示封面图、品名、价格和“加入购物车”按钮。图片这块有个小程序该注意的问题数据库存的是相对路径比如/upload/123.jpg前端拼接时要加上后端图片服务器前缀。开发环境我是在vue.config.js里把/upload也代理到后端生产环境则让Nginx直接托管/upload目录。论坛帖子列表展示时每条帖子要显示标题、摘要、作者头像、分类标签、评论数、点赞数、发布时间。摘要我是在后端接口里直接截取的用正则去掉HTML标签后取前80个字符再放到PostVO里返回。这里要提到的点是不要在SQL层面做截取因为富文本的纯文本长度与HTML字符串长度是完全不同的概念先在Java里清洗掉标签再截就准确了。帖子详情页的富文本内容渲染我用了v-html直接输出。这里的安全性不能忽视如果内容来自用户输入且没有过滤v-html会直接执行恶意脚本造成XSS攻击。我的处理方案是后端在接口层级用Jsoup清洗用户提交的HTML只允许p、img、h2、strong这些安全标签禁用script、iframe和on*事件属性清洗之后再入库。5. 常见问题与排查技巧实录5.1 跨域请求失败从无响应到CORS配置开发阶段因为配了devServer代理所以没遇到跨域问题。但有一次我为了快速调试直接在前端页面里请求http://localhost:8081/api/...浏览器直接报No Access-Control-Allow-Origin header is present。这个报错的原因就是浏览器的同源策略后端接口没有返回允许跨域的响应头。解决办法有两种。一种是在后端添加CORS配置继承WebMvcConfigurerAdapter重写addCorsMappingsregistry.addMapping(/api/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600);另一种是坚持走代理不要让浏览器直接发起跨域请求。我的建议是开发环境无条件走代理因为代理模式下浏览器看到的请求是同源的根本不会触发CORS少了一大堆问题。CORS配置留着给那些确实需要直连的场景备用。5.2 登录状态失效token过期后的自动登录跳转我遇到过这样一个场景用户在论坛编辑半天的帖子去提交的时候接口返回401页面没有任何反应用户以为提交成功结果数据丢了。问题的根源是401只出现在网络面板里前端页面没有相应处理。后来我在Axios响应拦截器里遇到401就做两件事一是弹出“登录已过期请重新登录”的提示二是调用store.dispatch(user/logout)清掉本地token然后跳转到登录页并带上redirect参数登录成功之后再跳回原来想访问的页面。这个闭环搞定之后用户虽然会被强制重新登录但至少不会丢失操作预期体验上是可以接受的。我还在编辑帖子的提交按钮旁加了自动保存草稿到localStorage的逻辑每30秒存一次即使被踢下线重新登录后还能恢复内容。5.3 富文本图片上传后路径不正确第一次接富文本编辑器时我在wangEditor的配置里设置图片上传地址为/api/upload/image上传接口返回的图片URL是/upload/20230521/xxx.jpg。按理说这个相对路径在开发环境有代理没问题但编辑器生成的HTML里图片路径也被写成了/upload/20230521/xxx.jpg结果在帖子列表页通过摘要展示图片时那些img标签的src还是相对路径。这个bug最后修复方式是在编辑器上传成功回调里把返回的路径前面补上完整的图片服务前缀或者后端直接返回带域名前缀的绝对路径。考虑到生产环境的域名是会变的我最后选择了前端统一处理一个全局方法resolveImageUrl负责给所有相对路径补前缀。这样既不用在数据库里硬编码域名又能适配不同部署环境。5.4 Vue打包部署到Tomcat之后的路由404与空白页问题生产部署我想省一台服务器所以没有用Nginx直接把前端dist拷进了Tomcat的webapps/ROOT目录。结果明显两个问题第一访问首页是空白的控制台报找不到JS和CSS资源第二访问子路由刷新后直接404。第一个问题的原因是Vue构建时的静态资源路径默认是绝对路径/js/xxx.js而我的项目不是部署在域名根路径下。解决方案是修改vue.config.js里的publicPath配置写成./相对路径这样资源加载就能遵循当前页面所在目录。第二个问题前面说过Vue Router在history模式下需要服务器进行路由回退但Tomcat不像Nginx有try_files规则。解决方案有两个一是改用hash模式虽然URL里带个#但在这种轻量场景下确实省事二是在Tomcat里配置RewriteValve把不存在的路径重写到index.html。如果稳定性优先我建议直接选hash模式这是我在这个项目里最终采用的方式。5.5 问题排查速查表把我在SSM231项目里遇到的典型问题汇总成一张表方便你直接对照现象可能原因排查步骤后端启动报Bean创建异常Spring与SpringMVC版本冲突检查两个框架大版本是否一致前端请求404devServer代理未配置或路径错误查看网络面板实际请求URL前端请求成功但数据为空接口返回结构不匹配对比后端Result结构与前端的解析字段登录接口成功但跳转后立马退出登录后未正确存储token检查Vuex的token存取逻辑图片不显示路径前缀不对或静态资源未放行直接访问图片URL看响应下单成功但库存没变事务未提交或乐观锁冲突回滚看数据库日志和业务日志帖子内容显示异常富文本清洗规则过严检查Jsoup白名单配置打包后页面空白静态资源路径错误检查publicPath配置6. 这个项目还能怎么扩展SSM231做完了之后我经常想如果继续往下迭代我会优先做哪几件事。第一是搜索引擎优化帖子表和商品表都加了全文索引用MySQL的全文检索替代现在的LIKE %关键字%模糊查询查寻效率会提升一截。第二是支付模块现在订单结算只是模拟支付状态如果接上真实的微信或支付宝支付整个电商闭环就完整了。第三是消息推送用户在论坛的帖子被回复、商品降价通知这些场景用WebSocket推送到前端体验会更接近一线商业产品。不过这些扩展都有一个前提就是你的基本功要扎实。SSM这套技术栈虽然老了但它揭示的原理不会老Spring的依赖注入让你明白一个对象怎么被组装起来SpringMVC让你看清楚一条HTTP请求经过哪些环节到达业务代码MyBatis让你知道SQL和Java对象之间是怎么映射的。这些底层逻辑搞懂了以后去学Spring Boot、MyBatis-Plus甚至微服务都只是换一层更自动化的外衣而已。做这个项目我最深的体会是前后端分离开发最容易出问题的不是某个单独的技术难点而是前后端之间的“衔接缝”——数据结构没对齐、路径没对齐、错误处理逻辑没对齐。所以我在项目进行到一半时强制自己先写一份完整的接口文档每个接口都定义好请求参数、返回结构、错误码然后前后端严格按文档来写。这一步看似耽误时间实际省下来的联调时间远比写文档的时间多。如果你正在做类似的项目我建议你也把接口文档当作第一优先级来对待这比任何技术选型都更能决定项目能不能顺利跑通。
返回列表