SSM+Vue混合架构生鲜电商项目开发与部署全解析
2026/9/14 1:47:26 网站建设 项目流程

简介:在Web系统开发中,SSM(Spring+SpringMVC+MyBatis)与Vue.js的前后端分离架构是构建电商平台的常见方案。SSM通过Spring管理业务对象、SpringMVC处理请求映射、MyBatis封装SQL操作,Vue则负责组件化页面与前端路由。在生鲜电商系统中,订单库存扣减、评价唯一性、收藏关联查询等核心功能依赖严谨的数据库表设计与事务控制,例如采用乐观锁避免超卖、唯一索引防止重复评价。这种架构清晰分离职责,便于并行开发与后期维护,尤其适用于毕业设计或中小型项目。从管理员后台的商品管理到用户前台的订单评价与收藏,混合架构能支撑完整业务闭环。围绕一个SSM+Vue的社区生鲜电商项目,工程落地中的前端依赖安装、跨域代理、路由懒加载及部署打包路径等关键实践值得深入梳理。

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 id="deductStock"> 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.bakBreadCrumbs.vue.bakIndexHeader.vue.bakupdate-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 开发服务器默认在 8081,SSM 的 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/list,devServer 会把它转发到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) { // 校验订单是否存在且已完成,status=3 表示已完成 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 的LIMITORDER BY会影响聚合结果,如果只写一条带GROUP BY的 SQL,总数会被列表分组干扰。前端拿到totalrows后,在收藏页分页组件里绑定 total,翻页时触发新请求。删除收藏时建议用user_idgoods_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-sasspythonMSBuild时,优先切换 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 下,再把后端接口的baseURLhttp://localhost:8080改成和页面同一来源,整个项目就能打包给别人演示了。

本文还有配套的精品资源,点击获取

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

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

立即咨询