首页 / 资讯中心 / 文章详情

SSM+Vue在线商品管理系统:从设计到部署的全栈实战解析

SSM+Vue在线商品管理系统:从设计到部署的全栈实战解析 ★ FEATURED ARTICLE
1. 项目概述与定位分析1.1 这个项目到底解决什么问题在线商品管理系统说白了就是一套卖东西的后台加前台。前台给普通用户逛商品、加购物车、下单后台给管理员管商品、管分类、处理订单。这个东西放在以前就是大家熟悉的电商网站只不过现在我们用一套更现代的技术把这事儿重新做了一遍。技术栈是SSMSpring 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;商品表相对复杂一点需要区分原始价格和促销价格。我在实操中发现一个特坑的地方——价格字段千万别用 float 或 doubleJava 那边计算订单总金额时会出现 0.10.20.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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表和订单明细表需要拆开因为这个是一对多的关系。一个订单可能包含多个商品如果只存一张表商品列表字段会非常臃肿后续要统计销量、退款数量都会很痛苦。订单表存订单级别信息比如总金额、状态、收货信息订单明细表存每个商品的下单快照。这里的快照很重要商品价格和名称都可能被管理员修改但订单生成后必须保留用户下单那一刻的信息。购物车表最容易被设计成用户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.xmlSSM整合最费时间的是配置文件之间的相互引用关系。Spring容器只管业务层和数据访问层的BeanSpringMVC容器只管Controller层两者是父子容器关系。如果你把Service的Bean空配到SpringMVC的配置里了事务会失效反过来Controller配到Spring容器里请求又映射不上。这个坑我踩过不少次建议先写 spring-mybatis.xml 把数据源和SqlSessionFactory搞定再写 spring-mvc.xml 只扫描 controller 包。数据源配置根据你本地的MySQL版本选驱动。MySQL 8以上用com.mysql.cj.jdbc.DriverMySQL 5.7用com.mysql.jdbc.Driver就行还要注意连接串加上useSSLfalseserverTimezoneAsia/Shanghai不然会报时区错误。3.2 统一返回结果与异常处理前后端分离之后后端不再返回视图页面所有接口都返回JSON。我最开始每个Controller都手动拼业务返回码后来发现维护性太差于是抽象了一个通用的 Result 类public class ResultT { private Integer code; // 200成功500失败401未登录 private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT 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_timeMySQL和Java实体映射后变成createTime前端 Mock 数据写的是createTime结果后端起服务后返回的却是create_time前端直接拿不到时间字段。解决方案是后端在实体类加JsonProperty注解或配置全局的驼峰映射。这个问题如果你遇到了大概率是 MyBatis 的 mapUnderscoreToCamelCase 配置没有打开settings setting namemapUnderscoreToCamelCase valuetrue/ /settings联调阶段另一个好用的技巧是先把所有接口在浏览器或 Postman 里跑通再和前端联调。这样如果出了问题可以快速定位是后端接口问题还是前端渲染问题而不是双方各执一词、来回拉扯。我的习惯是每完成一个模块的后端接口立刻用 Postman 验证接口正确性顺手把请求示例保存成集合分享给前端同学直接用。5.2 后端打war包还是jar包SSM 项目一般打成 war 包部署到 Tomcat这也是大多数毕设演示环境的标准做法。在 pom.xml 里配置打包方式packagingwar/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。解决办法是在连接串末尾加上serverTimezoneAsia/ShanghaiuseSSLfalse。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 8080Linux/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的在线商品管理系统值得你腾出一段时间亲自从头走一遍。
阅读完成 · 觉得有帮助?
咨询建站