☰
SSM+Vue在线商品管理系统:从设计到部署的全栈实战解析
2026/10/11 14:47:53 网站建设 项目流程

1. 项目概述与定位分析

1.1 这个项目到底解决什么问题

在线商品管理系统,说白了就是一套"卖东西"的后台加前台。前台给普通用户逛商品、加购物车、下单,后台给管理员管商品、管分类、处理订单。这个东西放在以前,就是大家熟悉的电商网站,只不过现在我们用一套更现代的技术把这事儿重新做了一遍。

技术栈是SSM(Spring + SpringMVC + MyBatis)加Vue,前后端完全分离。SSM负责提供接口、操作数据库,Vue负责页面展示和用户交互。两者通过JSON格式的数据交互,各自独立开发、独立部署。

我见过很多同学第一次看到这个项目标题,以为是个简单的CRUD,实际上把整个系统做完之后,你会发现里面涉及的技术点非常密集。从数据库设计、接口规范、前端组件通信,到跨域处理、登录鉴权、部署上线,一环扣一环。做完这个项目,Java后端和Vue前端的核心开发链路基本都过了一遍。

1.2 这套技术栈为什么还值得用

很多人觉得SSM已经过时了,现在都是Spring Boot的天下。这话有一定道理,但放到教学和毕业设计场景里,SSM的价值恰恰在于"简单、透明、容易讲清楚"。

Spring Boot把大量配置自动完成了,对新手来说反而是个黑盒。而SSM里,SpringMVC的控制器怎么接收请求、MyBatis的Mapper怎么跟数据库打交道,每一步都看得见摸得着。面试时候被问到底层原理,懂得SSM的人往往能把Spring的核心思想说得更清楚,因为配置是自己一行行写的。

加上Vue做前后端分离,这正好模拟了当前企业开发的主流模式——前端团队和后端团队各司其职,靠接口文档协作。我当时做完这个项目去面试,被问到"你做过前后端分离吗",直接把这个项目的架构讲一遍,对方立刻就明白了。

2. 系统核心功能与数据库设计

2.1 功能模块拆解与角色划分

这个系统我按角色拆成两条线,一条是用户端,一条是管理端。

用户端面向普通消费者,核心链路是:注册登录 → 浏览商品 → 加入购物车 → 生成订单。细节上还要有商品分类筛选、商品关键词搜索、商品详情查看、购物车数量加减、订单状态跟踪。这里最容易忽略的是"下单前的收货地址管理"和"库存扣减",很多人做到订单生成就停了,导致演示的时候看起来流程很完整,实际上漏洞不少。

管理端面向运营人员,核心功能是:商品分类维护、商品上下架、库存修改、订单审核发货、用户管理。管理端和用户端的商品列表可以通过一个 status 字段做隔离——上架的商品用户可见,下架的商品只有后台能看到。

权限控制我用的是拦截器加角色判断。后端设计两个角色代码,用户登录后把角色信息写进 Session,请求到达 Controller 之前由拦截器判断当前接口允许哪些角色访问。这里要说清楚,拦截器只做粗粒度控制,细粒度的权限校验还是要在业务代码里写,比如管理员不能修改别人的订单之类的规则。

2.2 数据表设计的关键细节

这个系统的核心表我设计了六张:用户表、分类表、商品表、购物车表、订单表、订单明细表。如果要做收货地址,再加一张地址表。

-- 用户表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码,建议BCrypt加密', `role` tinyint(4) DEFAULT 0 COMMENT '角色:0普通用户,1管理员', `phone` varchar(20) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

商品表相对复杂一点,需要区分原始价格和促销价格。我在实操中发现一个特坑的地方——价格字段千万别用 float 或 double,Java 那边计算订单总金额时会出现 0.1+0.2=0.30000000000000004 这种问题。要么用 BigDecimal,要么数据库里直接存整数分。我当时为了省事,让前端展示的时候除以100,后端所有计算都用整数,效果非常稳定。

-- 商品表 CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `category_id` int(11) NOT NULL COMMENT '分类ID', `name` varchar(200) NOT NULL COMMENT '商品名称', `subtitle` varchar(500) DEFAULT NULL COMMENT '副标题', `main_image` varchar(500) DEFAULT NULL COMMENT '主图地址', `detail` text COMMENT '商品详情', `price` int(11) NOT NULL COMMENT '价格,单位分', `stock` int(11) NOT NULL COMMENT '库存', `status` tinyint(4) DEFAULT 1 COMMENT '1上架,0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单表和订单明细表需要拆开,因为这个是一对多的关系。一个订单可能包含多个商品,如果只存一张表,商品列表字段会非常臃肿,后续要统计销量、退款数量,都会很痛苦。订单表存订单级别信息,比如总金额、状态、收货信息,订单明细表存每个商品的下单快照。这里的"快照"很重要,商品价格和名称都可能被管理员修改,但订单生成后必须保留用户下单那一刻的信息。

购物车表最容易被设计成"用户ID加商品ID联合唯一",这个思路没问题,但是如果用户未登录时想操作购物车,就会遇到麻烦。理想的方案是分两种情况:未登录用 localStorage 存本地购物车,登录后合并到服务端。我当时为了简化逻辑,要求用户必须登录才能加购物车,这个体验虽然差一点,但项目演示完全够用,也省了一堆合并冲突的Bug。

3. 后端核心实现:SSM框架的落地细节

3.1 项目结构与配置文件的搭建

后端我推荐用Maven聚合工程或者单模块都可以,我的习惯是单模块,结构清晰:

src/main/java/com/example/mall/ ├── controller/ # 控制器层 ├── service/ # 业务接口 ├── service/impl/ # 业务实现 ├── mapper/ # MyBatis的Mapper接口 ├── entity/ # 实体类 ├── common/ # 通用类,比如Result、Constants └── interceptor/ # 拦截器 src/main/resources/ ├── jdbc.properties ├── spring-mybatis.xml ├── spring-mvc.xml └── mybatis-config.xml

SSM整合最费时间的是配置文件之间的相互引用关系。Spring容器只管业务层和数据访问层的Bean,SpringMVC容器只管Controller层,两者是父子容器关系。如果你把Service的Bean空配到SpringMVC的配置里了,事务会失效;反过来Controller配到Spring容器里,请求又映射不上。这个坑我踩过不少次,建议先写 spring-mybatis.xml 把数据源和SqlSessionFactory搞定,再写 spring-mvc.xml 只扫描 controller 包。

数据源配置根据你本地的MySQL版本选驱动。MySQL 8以上用com.mysql.cj.jdbc.Driver,MySQL 5.7用com.mysql.jdbc.Driver就行,还要注意连接串加上useSSL=false&serverTimezone=Asia/Shanghai,不然会报时区错误。

3.2 统一返回结果与异常处理

前后端分离之后,后端不再返回视图页面,所有接口都返回JSON。我最开始每个Controller都手动拼业务返回码,后来发现维护性太差,于是抽象了一个通用的 Result 类:

public class Result<T> { private Integer code; // 200成功,500失败,401未登录 private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

这样设计之后,前端 axios 拦截器里可以直接对 code 做统一判断。凡是 code 不是200的,直接弹出 message 就完事,不需要每个组件单独写错误处理。

异常处理也要放在这一步统一做。我写了一个@ControllerAdvice注解的全局异常处理器,把 Service 层抛出的自定义业务异常,比如"库存不足""商品不存在",统一转换成 Result 返回。这里我在实操中发现,光捕获异常还不够,还要配合日志把完整的异常栈打出来,不然线上环境出了问题只能靠猜。

3.3 登录鉴权的两种实现路径

SSM项目做登录鉴权,最常见的是两种方案:Session方案和Token方案。

Session方案实现简单:登录成功把用户对象放进 session,拦截器里从 session 取用户,取不到就跳转登录。这个方案的问题是,如果前端和后端部署在不同端口,session 默认拿不到,还要处理跨域携带 Cookie 的问题,CORS 配置里必须写allowCredentials(true),同时前端 axios 要设置withCredentials: true,两个条件缺一不可。

Token方案更符合前后端分离的"无状态"思路。用户登录后,后端生成一个随机字符串或UUID作为token,把token存进Redis,过期时间设两个小时。前端每次请求在请求头里带Authorization: token,拦截器根据token查Redis得到用户信息。我当时为了减少依赖,没有引入Redis,直接把token存内存Map里,项目重启token就全部失效,演示量不大完全能接受。如果想做得专业一点,建议引入Redis,代码改动很小。

拦截器的注册要注意路径过滤规则。登录接口、注册接口、首页商品列表接口必须放行,其他接口都需要拦截。动态放行管理端接口时,我是在拦截器里先判断 token 对应用户的角色,角色不是管理员就直接返回"无权限",这比在前端写一堆路由守卫更可靠,因为接口层守住了,绕过前端也能防住。

3.4 商品分页与多条件查询

商品列表一定要做分页,不然数据一多页面就卡。我用的是 MyBatis 的 PageHelper 插件,配置非常轻量,引入依赖后在 service 层查列表前写一行PageHelper.startPage(pageNum, pageSize),紧接着的查询就会自动拼接 limit。

多条件查询是另一个关键点。用户端搜索商品需要支持按分类筛选、按价格区间筛选、按关键词模糊匹配。Mapper 里写动态SQL,用<where>标签加<if>判断条件,注意价格区间的边界问题——我建议用>=和<=,避免用户感觉"81块钱搜不到81块的商品"这种体验问题。

这里分享一个我实测很有用的技巧:商品列表返回给前端的数据,不要把商品详情(detail字段,很长的HTML文本)也查出来,那个字段在列表页根本用不到,白白增加数据库IO和网络传输。我是用 MyBatis 的 resultMap 做字段映射,列表查询只查基础字段,点击进详情页再单独查详情。这和"订单明细存快照"是同一个思路——考虑清楚每份数据的使用场景,让查询更精准。

4. 前端核心实现:Vue从零搭建到页面完成

4.1 Vue工程结构与依赖选择

前端我选用 Vue CLI 生成工程,Vue 版本用的2.6,配合 Element UI 组件库。为什么不用 Vue 3?不是 Vue 3 不好,而是 Element UI 对 Vue 3 的兼容方案是 Element Plus,如果在毕设场景里参考的旧代码、教程大部分是基于 Vue 2 的,遇到问题去搜解决方案时,Vue 2 的社区积累明显更丰富。你有时间踩 Vue 3 的坑当然可以,但求稳的话 Vue 2 完全够用。

创建工程后,一定要第一时间检查目录结构:

src/ ├── api/ # 按模块封装的接口请求 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Vuex状态管理 ├── views/ # 页面组件 ├── utils/ # 工具函数 ├── App.vue └── main.js

我的经验是,哪怕项目再小,也要把 api 目录单独拆出来。每个页面直接调自己 import 的接口函数,而不是在组件里写裸的 axios 调用。这样做的好处有两个:接口地址统一管理,后端路径变了只改一个文件;其次接口函数定义出来之后,语义非常清晰,比如getProductList(params)一看就知道是干嘛的。

4.2 axios封装与跨域代理配置

axios 封装是整个前后端通信的基石。我在 utils/request.js 里创建了一个 axios 实例,配置基础路径和超时时间,再用请求拦截器统一加 token,用响应拦截器统一处理 Result 结构。

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) request.interceptors.response.use(response => { const res = response.data if (res.code === 200) { return res.data } else { // 统一错误处理,弹出提示信息 return Promise.reject(new Error(res.message)) } }, error => { return Promise.reject(error) }) export default request

这里要特别说明 baseURL 设置成/api而不是完整的后端地址。开发阶段前端跑在8080端口,后端跑在8999端口,直接请求后端地址会触发跨域。我在 vue.config.js 里添加 devServer 的代理配置:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8999', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }

这样前端请求/api/product/list,开发服务器会转发到http://localhost:8999/product/list,浏览器里看到的请求是同源的,跨域问题从根上解决。生产环境部署时,用 Nginx 做同样的一段代理配置即可。

4.3 核心页面组件与路由设计

页面规划要跟后端接口一一对应。用户端核心页面是首页商品列表、商品详情、购物车、订单确认、订单列表。管理端核心页面是商品管理、分类管理、订单管理。

路由设计我采用嵌套路由,首页作为父路由,商品列表页、商品详情页都挂在它下面。这里有一个计算思维的小细节:点击商品进入详情页时,需要把商品ID传过去。我用的是路由传参this.$router.push({ path: '/product/' + id }),这样用户刷新页面后,详情页可以通过 URL 参数重新获取商品数据,而不是从内存状态里拿,避免刷新丢失数据。

Vuex 我只用来管理两样东西:购物车数量和用户登录信息。购物车数量在导航栏要实时展示,如果在每个页面都调一次详情接口就太累了。我是登录后调购物车列表接口,把数量存 Vuex,每次加购成功再重新更新 Vuex 里的值,这样所有页面导航栏的数量都同步了。用户信息存在 Vuex 加 localStorage 双保险,刷新页面后从 localStorage 恢复,同时用 token 调 getUserInfo 接口校准一遍。

4.4 Element UI的表单校验和列表渲染

Element UI 用起来顺手,但有几个细节值得注意。表单校验里,数字类型的字段校验比较特殊,比如价格、库存。我最初直接在 form 里定义了数字变量,校验规则里却写了required: true,结果输入0的时候校验通过不了,因为Element UI的 required 对数字0会判定为空。解决方法是用自定义校验函数validator: (rule, value, callback) => { if (value === '' || value === null) callback(new Error('请输入')) }。

列表渲染推荐用 el-table 组件,但要注意表格数据里如果有嵌套数据,比如商品信息里有分类名称,而后端返回的是分类ID,前端需要在拿到列表后做一次数据转换。我的做法是在后端联表查询时,直接把分类名称作为字段别名查出来,前端不用做二次处理。前端配置字段对齐的功夫能省则省,后端能直接把一件事做完整,就不要让前端再做一次。

5. 前后端联调与调试部署全流程

5.1 接口文档先行与联调技巧

前后端分离开发最大的风险是"各自为政,联调爆雷"。两个人或两组人开发时,必须把接口文档定在前面。我一般用 Swagger 或者直接写一份 Markdown 接口文档,规定好每个接口的路径、请求方式、入参类型、出参结构。

我踩过最大的联调坑是"字段名不一致"。后端字段叫create_time,MySQL和Java实体映射后变成createTime,前端 Mock 数据写的是createTime,结果后端起服务后返回的却是create_time,前端直接拿不到时间字段。解决方案是后端在实体类加@JsonProperty注解或配置全局的驼峰映射。这个问题如果你遇到了,大概率是 MyBatis 的 mapUnderscoreToCamelCase 配置没有打开:

<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>

联调阶段另一个好用的技巧是:先把所有接口在浏览器或 Postman 里跑通,再和前端联调。这样如果出了问题,可以快速定位是后端接口问题还是前端渲染问题,而不是双方各执一词、来回拉扯。我的习惯是每完成一个模块的后端接口,立刻用 Postman 验证接口正确性,顺手把请求示例保存成集合,分享给前端同学直接用。

5.2 后端打war包还是jar包

SSM 项目一般打成 war 包部署到 Tomcat,这也是大多数毕设演示环境的标准做法。在 pom.xml 里配置打包方式:

<packaging>war</packaging>

然后执行mvn clean package,把生成的 war 包丢到 Tomcat 的 webapps 目录下,启动 Tomcat 即可。启动后路径会变成http://localhost:8080/项目名/,SpringMVC 的接口路径前会自动带上上下文根路径。前端代理的 target 地址要对应改成http://localhost:8080/项目名。

部署过程中,我发现一个典型的遗漏:JDK 版本和 Tomcat 版本不匹配。比如 JDK 17 配 Tomcat 9 没问题,但配 Tomcat 7 直接报错。建议直接用 Tomcat 8.5 或 9,其它版本别乱试。另外数据库连接串上的 IP 也要检查,本机用jdbc:mysql://localhost:3306没问题,如果数据库在远程服务器,记得改IP和开放3306端口。

5.3 前端打包与Nginx上线

前端部署比后端简单,Vue 工程执行npm run build,生成 dist 目录,里面是纯静态文件。把这些文件放到 Nginx 的静态目录下,再配置一下接口请求的反向代理,就完成了。

server { listen 80; server_name localhost; location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; # 支持Vue路由刷新 } location /api/ { proxy_pass http://localhost:8080/项目名/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这里面的try_files特别关键。Vue Router 的 history 模式,前端路由如/product/12在服务端没有对应文件,如果不加 try_files 配置,用户直接访问这个地址就会 404。加上try_files $uri $uri/ /index.html后,所有请求都会走 index.html,路由由前端接管。

顺带说一句,production 环境的接口 baseURL 要保持/api,和后端代理路径一致,这样开发环境走 devServer 代理,生产环境走 Nginx 代理,代码完全不用改。

6. 常见问题与排查技巧实录

6.1 数据库连接失败与时区报错

这个问题的出现频率在所有问题里排第一。现象是启动后提示The server time zone value '???ú±ê׼ʱ¼ä' is unrecognized。解决办法是在连接串末尾加上serverTimezone=Asia/Shanghai&useSSL=false。MySQL 8 以上还必须配置时区,不配直接连不上。

还有一种情况是数据库账号密码没问题,但是连接串里写了jdbc:mysql://localhost:3306/mall?,mapping 名称写错或者数据库还没有创建。我建议在启动前用 Navicat 或命令行先确认一下use mall;能通过,再谈启动。

6.2 前后端联调跨域问题

如果已经配置了 devServer 代理,还出现跨域报错,八成是请求路径没走代理。比如代理配置只处理/api开头的请求,但 axios 的 baseURL 没设置成/api,真实请求地址变成了http://localhost:8080/product/list,浏览器直连 8080 端口,代理根本拦截不到。

另一个常见问题是,后端单独配置了 CORS 过滤器,前端又配了代理,两者同时生效后出现重复的跨域响应头。最佳实践是我前面提到的:开发环境只用前端代理解决跨域,后端不要额外加 CORS 配置,避免两边都管最后互相干扰。

6.3 登录状态丢失问题

用户刷新页面就退出登录,这是 Session 会话失效的典型症状。比如后端把用户放进 Session 时以user为 key,前端请求时 Cookie 里应该持有 JSESSIONID,但如果 CORS 或代理配置把 Cookie 拦掉了,Session 就找不到用户了。

我的排查步骤是:先看浏览器 Network 面板里登录请求的响应头有没有Set-Cookie字段,再看后续请求的请求头有没有 Cookie。没有 Set-Cookie 说明 Session 没创建成功,没有 Cookie 说明跨域时被浏览器拒绝了。用了 Token 方案就不用纠结这套了,Token 靠自己设置请求头发送,跟 Cookie 完全无关,这也是我后来推荐 Token 方案的原因。

6.4 Maven依赖冲突与插件问题

SSM 项目依赖的包很多,最容易冲突的包是jackson-databind和spring-web自带的 JSON 序列化库。我遇到过前端请求返回字符串而不是JSON字符串,就是多个 JSON 库同时存在导致 SpringMVC 的 MappingJackson2HttpMessageConverter 没有正确注册。

排查思路是使用mvn dependency:tree -Dverbose查看依赖树,找出重复的依赖并排除掉。Maven 的依赖冲突问题考验的就是排查能力,习惯使用这条命令后,很多问题五分钟就能定位。

6.5 端口被占用

开发过程中最常见的一个小问题:Tomcat 的 8080 端口被占用,或者 Vue devServer 的端口被占用。项目起不来的第一反应就是看端口。

Windows 上用netstat -ano | findstr 8080,Linux/macOS 上用lsof -i :8080,找到占用进程的PID,结束掉即可。如果是 Vue 的 8081 端口被占用,Vue CLI 会提示自动换端口,但最好还是主动统一端口配置,不然接口联调时前后端对不上数字就会很混乱。

7. 关于项目交付物与学习路线的个人建议

7.1 源码、文档与调试这三份东西怎么处理

这个标题里强调了"源码+文档+调试",很多人拿到源码就急着启动,结果第一步栽在"环境配置"。我看到的文档如果写得好,一般会包含三部分:环境要求、部署步骤、功能说明。环境要求里明确JDK版本、MySQL版本、Maven版本、Node版本、Tomcat版本,部署步骤里按顺序写清数据库导入、后端配置修改、启动后端、前端依赖安装和启动。功能说明把每个模块附近对应接口和表结构附上,方便排查问题。

调试阶段最忌讳的是"一上来就通宵改代码"。正确的做法是先理清启动顺序:数据库 → 后端 → 前端。如果后端数据源连不上,前端做了再多也白搭。我用一个检查清单来判断系统能不能跑起来:数据库表是否导入成功、后端日志是否出现"Startup complete"、前端浏览器控制台是否登录接口正常返回。这个清单花五分钟就能过一遍,能避免80%的报错。

7.2 在这个项目上可以扩展的方向

做完基础版之后,这个系统的可扩展性很强。往实用方向扩,可以加 Redis 缓存热点商品、加消息队列处理订单超时关闭、加 Elasticsearch 做商品搜索。往展示效果方向扩,可以给管理端加数据图表,比如按分类统计销量、按日统计订单量,前端用 ECharts 实现,后端加几个统计接口就行。

我在实际指导一些同学做这个项目时,一直强调一个观点:网上类似的项目源码很多,但真正拉开差距的不是代码本身,而是你对自己项目的理解深度。把每个模块为什么这么设计、每个接口为什么这么定义、每次异常为什么这么处理讲清楚,比单纯跑通效果值钱得多。

7.3 最后分享两个我自己实际测试下来的体会

第一个体会是关于联调。一开始我把前后端联调想得太简单,以为后端返回数据前端渲染就行了。等到真正跑起来才发现,接口字段类型对不上、返回结构不统一、分页参数名不一样,这些问题几乎都在最后联调阶段才暴露。后来我养成了一个习惯:后端接口写完后,先把返回的 JSON 结构自己看一下,再让前端对接,至少在字段层面上提前消灭一批低级错误。

第二个体会是前端的状态管理。很多新手刚开始用 Vuex 很兴奋,什么全局数据都往里塞,结果发现代码行数暴涨,而且刷新页面后所有数据清零。真正的全局状态应该只有"用户信息"和"购物车数量"这种跨页面共享的数据,页面细节的数据该用路由传参就用路由传参,该在页面内请求就页面内请求,保持单页面的独立性,维护起来会舒服很多。

做这种全栈小项目,最有成就感的一刻不是把所有功能做完,而是当你把部署好的链接发给别人,对方直接从浏览器里注册、下单、到后台管理全部跑通的那一刻。这中间的每一类报错、每一次排查,都会变成你评价"我会系统开发"这句话时真正的底气和尺度。如果你正准备从零开始做一个类似的Java方向实战项目,这个SSM加Vue的在线商品管理系统,值得你腾出一段时间亲自从头走一遍。

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

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

立即咨询