SSM+Vue混合架构生鲜电商项目开发与部署全解析
简介在Web系统开发中SSMSpringSpringMVCMyBatis与Vue.js的前后端分离架构是构建电商平台的常见方案。SSM通过Spring管理业务对象、SpringMVC处理请求映射、MyBatis封装SQL操作Vue则负责组件化页面与前端路由。在生鲜电商系统中订单库存扣减、评价唯一性、收藏关联查询等核心功能依赖严谨的数据库表设计与事务控制例如采用乐观锁避免超卖、唯一索引防止重复评价。这种架构清晰分离职责便于并行开发与后期维护尤其适用于毕业设计或中小型项目。从管理员后台的商品管理到用户前台的订单评价与收藏混合架构能支撑完整业务闭环。围绕一个SSMVue的社区生鲜电商项目工程落地中的前端依赖安装、跨域代理、路由懒加载及部署打包路径等关键实践值得深入梳理。1. 拿到 SSM 与 Vue 混合架构的生鲜电商项目先别急着跑社区生鲜电商这个题目在毕业设计里看似普通但把“后台 SSM 后台管理页面 Vue 前台 HTML”三套东西拼在一起后踩坑点反而不在后端而在前端依赖安装和打包路径上。源码里有 update-password.vue.bak、IndexAsideStatic.vue.bak 这类备份文件改造过 Vue 管理端的人一眼能看出后台是按组件拆写的。适合两类人想快速复现带订单评价和收藏功能的完整电商后台想把 SSM 分层和 Vue 路由、仓库状态管理在同一个项目里看清。下面按能跑的思路从前端到后端再到部署把关键环节拆开讲。2. SSM 分层与 MySQL 表设计从订单库存反推业务约束2.1 为什么是 javassm 而不是 Spring Boot先说结论javassm 社区生鲜电商平台里的 javassm对应 Spring SpringMVC MyBatis 三个框架。这三个框架在一线新项目里已经不多见但作为毕业设计反而容易讲清楚因为每一层都是显式配置。Spring 管业务对象SpringMVC 管 URL 映射MyBatis 管 SQL 映射后台管理页面交给 Vue前台用户页面仍然是 HTML 模板。这种混合架构的好处是答辩时既可以说“我做了前后端分离”又保留了传统 Java Web 项目的重后端逻辑。实际运行时我一般会让 Vue 跑在 8081 端口Tomcat 上的 SSM 跑在 8080 端口前端通过 axios 走代理访问后端接口。项目在 Eclipse、MyEclipse、STS、IDEA 里都能导入为 Maven Web 项目换 IDE 不换代码因此特别适合需要给多人演示的毕设场景。2.2 数据库表拆分从功能反推字段从摘要列出的功能看管理员要管用户、员工、商品分类、商品信息、订单评价用户要有自己的订单和收藏。最低限度要有下面这些表表名只是常见做法实际以源码里的 sql 脚本为准表名核心字段作用t_userid, username, password, phone前台注册用户t_employeeid, emp_no, emp_name, role后台管理员/员工t_categoryid, cat_name, sort商品分类t_goodsid, cat_id, name, price, stock, img商品信息t_orderid, order_no, user_id, total_price, status订单主表t_order_detailid, order_id, goods_id, price, num订单明细t_evaluationid, order_id, user_id, grade, content订单评价t_favorid, user_id, goods_id, add_time收藏建表时最容易被忽略的是订单明细里的 price。它是下单那一刻的商品快照价格不能下单后去 join 商品表实时取价否则将来商品改价历史订单统计就会乱。生鲜价格经常调整这个字段必须有。另外订单评价表里 order_id 要建唯一索引保证一个订单只能有一条评价这样“订单评价管理”和“我的评价”才能对得上。收藏表 t_favor 用 user_id goods_id 做联合唯一索引避免同一用户重复收藏同一商品前端收藏图标状态也更好判断。字段类型上价格字段用 DECIMAL(10,2)不要用 float库存用 INT 就够了订单号用 String 而不是自增 id因为自增 id 容易暴露订单量且后期分库分表不方便。这些细节在论文的数据表设计章节里写出来比单纯贴建表语句更有说服力。2.3 MyBatis 动态 SQL库存扣减与条件更新用户下单必然要扣库存。最笨的写法是先 SELECT stock在 Java 里判断 stock num再 UPDATE stock stock - num。但两个用户同时读到相同库存就会都通过判断最后实际扣超。只要把判断和更新合成一条 UPDATE数据库行锁会保证同一时间只有一个请求能修改该行。Mapper XML 写成!-- 扣减库存条件更新防止超卖 -- update iddeductStock UPDATE t_goods SET stock stock - #{num}, version version 1 WHERE id #{goodsId} AND stock #{num} /update这里#{goodsId}是商品主键#{num}是购买数量。更新影响行数为 1 说明扣减成功为 0 说明库存不够或商品不存在Service 层要据此抛出异常回滚整个订单事务。version字段是乐观锁标记每次都加 1后续做后台编辑时可以用它防止同一条数据被多人同时改。需要注意t_goods的 id 必须是主键否则条件更新会走全表扫描生鲜商品表数据量上来后更新会很慢。如果项目里有秒杀快照表也可以把该逻辑复制到快照表但毕设场景下直接改主表就够了。3. Vue 后台页面拆解与 SSM 接口联调3.1 从 .bak 文件反推组件结构源码压缩包里带着IndexAsideStatic.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak、update-password.vue.bak这类备份文件。.bak后缀通常是作者改动前留下的副本不会被 Maven 或 Webpack 打包却保留着原始版本。IndexAsideStatic是左侧菜单栏IndexHeader是顶部导航BreadCrumbs是面包屑update-password是修改密码的对话框组件。从命名风格能判断后台页面是按组件拆分的不是一整个 index.html 堆到底。组件与后端接口的对应关系大致如下Vue 组件页面作用对应后端接口IndexAsideStatic.vue侧边栏菜单无前端路由写死IndexHeader.vue顶部用户信息/退出退出接口BreadCrumbs.vue面包屑导航无update-password.vue修改密码密码更新接口Static这个单词很关键它说明侧边栏菜单是静态的。也就是说菜单不是等管理员登录后再从后端取而是写死在 Vue Router 里。这样实现简单管理员和用户各写一份路由数组即可。缺点是权限粒度只能在路由层做不能精确到按钮级别。如果答辩时被问到“为什么不用动态路由”你可以说静态菜单更适合本项目的角色数量因为只有两种角色动态菜单的维护成本反而更高。3.2 vue.config.js 与 axios开发环境跨域代理Vue 开发服务器默认在 8081SSM 的 Tomcat 在 8080请求从 8081 发到 8080浏览器会报跨域错误。解决方式有两种后端加 CORS 过滤器或者前端 devServer 配置代理。我更推荐前端代理因为部署后前端资源被 Tomcat 托管同一个端口就不存在跨域了。开发环境配置如下// vue.config.js将 /api 开头的请求转发到 SSM 后端 module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }这段配置的意思是Vue 页面里 axios 请求/api/goods/listdevServer 会把它转发到http://localhost:8080/goods/list并且把请求头里的 Origin 改写成目标地址让后端觉得是同源请求。pathRewrite是个容易看漏的配置如果后端 Controller 的 RequestMapping 里本来就写在/api下就去掉 pathRewrite如果没有就必须保留。否则要么 404要么接口路径重复。这里顺带提一个前后端分离项目里常见的误区开发环境能通部署后不通多半是后端没有把 Vue 的 dist 目录作为静态资源目录处理。SSM 项目里只需要把打包后的 dist 文件复制到 webapp 下再用mvc:resources映射即可。axios 封装上建议统一拦截响应。如果后端返回{ code: 200, data: ... }格式response 拦截器里先判断 code不等于 200 就统一弹出提示而不是每个页面重复写判断语句。这样联调时少很多重复代码。3.3 Vue Router 与登录守卫后台管理页面的 URL 是前端路由不是传统a href跳转。常见做法是把登录页放在/login其它页面放在/home下。路由表需要懒加载否则首屏加载时间太长下面是一段典型的路由示例const routes [ { path: /login, component: () import(/views/Login) }, { path: /home, component: () import(/layout/IndexLayout), redirect: /home/goods, children: [ { path: goods, component: () import(/views/goods/GoodsList) }, { path: order, component: () import(/views/order/OrderList) } ] } ]component: () import(...)是路由懒加载打包后每个页面单独成一个 chunk只在访问时加载。子路由放在父路由children里对应IndexAsideStatic里的二级菜单。登录守卫则在main.js中定义router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else { next() } })这个守卫只做前端跳转控制真正的安全由后端 Session 控制。很多毕设项目只靠这个守卫登录后不再校验后端接口权限管理员删除用户接口能被普通用户直接调用就是个明显的漏洞。至少要在后端做一个基于 Session 的拦截器或者在所有接口开头取 Session 判断角色。4. 角色权限、订单评价与收藏把业务逻辑串成闭环4.1 管理员、员工与用户的权限边界项目里有“用户管理”和“员工信息管理”两个功能说明系统把 C 端用户和后台操作人员完全分开了。用户在前台注册、下单、收藏、评价员工负责后台的商品管理、订单处理和评价管理管理员则额外管理用户和员工信息。后端最好按 URL 前缀划分权限/admin/**只能由管理员和员工访问/user/**只能由前台用户访问/public/**允许所有人访问在 SpringMVC 拦截器里preHandle 方法先看 Session 中存的 loginUser 是谁再看请求路径是否在白名单外。管理员不能因为会前端操作就去改用户密码用户也不能通过猜测/admin/user/delete路径去删别人账号。这种基于路径前缀的权限隔离比引入 Spring Security 简单又能挡住大多数越权操作。写论文时把拦截器类图和 Session 有效期画出来会比只写“有登录功能”更有深度。4.2 订单评价状态校验与唯一性约束订单评价的交互流程是用户确认收货后进入订单详情看到评价按钮填写评分和内容并提交管理员在后台能看到所有评价。实现时需要注意两点一是订单状态必须为已完成二是同一订单不能重复评价。Service 代码可以这样组织Override Transactional(rollbackFor Exception.class) public int addEvaluation(Evaluation eval) { // 校验订单是否存在且已完成status3 表示已完成 Order order orderMapper.selectById(eval.getOrderId()); if (order null || order.getStatus() ! 3) { throw new ServiceException(订单未完成不能评价); } // 通过 t_evaluation 的唯一索引防止重复评价 Integer count evaluationMapper.countByOrderId(eval.getOrderId()); if (count ! null count 0) { throw new ServiceException(该订单已评价); } return evaluationMapper.insert(eval); }Transactional(rollbackFor Exception.class)是事务注解这里的含义是从校验到插入评价任何一个环节抛异常数据库更新都会回滚。eval.getOrderId()是评价对象里的订单号前端提交时不能直接信任最好在 Controller 里从 Session 拿当前用户 ID再根据用户 ID 和订单号去查订单避免用户评价别人的订单。countByOrderId利用了之前建的唯一索引即使代码里忘记判断数据库也会抛 DuplicateKeyException 兜底。注意前端传回的 orderId 不能直接用来查订单更稳妥的做法是从 Session 中取 userId再用 userId orderId 去查防止用户评价别人的订单。4.3 我的收藏中间表联查与分页收藏功能本身不复杂但容易做得很难看进入收藏页时全部加载数据多就卡或者删除收藏后列表不更新。数据库层面收藏表是用户和商品的多对多中间表查询要用两条语句-- 统计当前用户收藏商品总数 SELECT COUNT(*) FROM t_favor f INNER JOIN t_goods g ON f.goods_id g.id WHERE f.user_id #{userId}; -- 分页查询当前用户收藏商品列表 SELECT g.id, g.name, g.price, g.img FROM t_favor f INNER JOIN t_goods g ON f.goods_id g.id WHERE f.user_id #{userId} ORDER BY f.add_time DESC LIMIT #{offset}, #{pageSize};#{offset}是 (pageNum - 1) * pageSize#{pageSize}是每页条数。为什么要分两条语句因为 MySQL 的LIMIT和ORDER BY会影响聚合结果如果只写一条带GROUP BY的 SQL总数会被列表分组干扰。前端拿到total和rows后在收藏页分页组件里绑定 total翻页时触发新请求。删除收藏时建议用user_id和goods_id作为 WHERE 条件不要只按收藏主键 id 删这样用户可以删除自己的重复收藏记录别人即使拿到接口也删不掉你的收藏。这里还要提一个容易被忽略的点收藏列表里展示的图片g.img如果是相对路径Vue 打包后路径会变成/static/img/...部署到 Tomcat 子目录下就会裂图。做法是在后端返回图片完整 URL或者像第 5 章的 publicPath 配置里把路径改成相对路径前后端配合才能避免。5. 用 install.bat 跑起来依赖安装、构建脚本与 Vue 打包布局问题5.1 三个批处理脚本的顺序源码里有 1-install.bat、2-run.bat、3-build.bat它们的职责分别是安装 Vue 依赖、启动开发服务器、打包生产资源。1-install.bat 里写的基本上是npm install偶尔会有作者加入npm install cnpm -g或指定 registry。如果一开始直接点 3-build.bat没有 node_modules构建会立刻报错如果直接点 2-run.bat 也一样。正确顺序是先 install再 run 或 build。run 用于本地联调build 用于把 Vue 页面打包成可交给 Tomcat 的静态文件。在 Windows 上最常遇到的失败是 node-sass 安装超时。判断方法报错信息里出现node-sass、python、MSBuild时优先切换 Node 版本或使用原作者说明文档里指定的版本。不要盲目升级依赖升级后可能连npm run serve都起不来。5.2 Vue 打包后布局异常publicPath 与路由模式前面 3.2 节说到开发环境通过代理联调到了部署阶段就要看打包产物。Vue 默认publicPath: /会让 JS/CSS 路径变成绝对路径/js/xxx.js如果 Tomcat 里的项目不是 ROOT而是部署在/ssm-shop目录下浏览器找的是http://localhost:8080/js/xxx.js实际资源在http://localhost:8080/ssm-shop/js/xxx.js于是页面只显示一堆没有样式的标签。解决办法是module.exports { publicPath: ./, productionSourceMap: false }publicPath: ./让所有静态资源路径变成相对 index.html 的路径这样不管目录多深都能找到。同时把 Vue Router 改回mode: hash因为 history 模式在 Tomcat 里刷新子路由会直接 404。最后把npm run build生成的 dist 目录复制到 SSM 项目的 webapp 下再把后端接口的baseURL从http://localhost:8080改成和页面同一来源整个项目就能打包给别人演示了。本文还有配套的精品资源点击获取