news 2026/10/16 5:00:12

SSM+Vue前后端分离商品管理系统实战:从数据库设计到部署上线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM+Vue前后端分离商品管理系统实战:从数据库设计到部署上线

最近帮一家做五金配件批发的公司搭了一套在线商品管理系统,技术栈是SSM加Vue,前后端分离。说实话,这套组合在电商后台、进销存系统里已经算"老朋友"了,看着不起眼,但真正做完一遍,你会发现里面藏着不少值得细究的细节。尤其当你需要从零开始设计数据库、联调接口、处理并发库存这些事的时候,每一步都有讲究。这篇就把整个项目从选型到上线的过程拆开讲一讲,希望能给正在做或准备做类似系统的朋友一些参考。

先交代一下项目背景。需求方管理着一千多个SKU,单价不高但种类繁杂,库存变动频繁,之前全靠Excel记录。后来数据量大了,表格里重复、遗漏的问题越来越多,他们想要一个能多人同时访问、无需安装客户端的网页系统,支持商品录入、分类维护、库存查询、上下架管理这几块核心功能。听完需求我心里就有数了:标准的前后端分离业务系统,SSM提供API,Vue负责页面,部署到一台服务器上,完全够用。

如果你也在用SSM做课程设计、实习项目,或者正在给熟人公司写一套简单的商品管理后台,这篇内容很适合你。我会按项目推进的顺序讲:先聊为什么选这套技术栈,再讲数据库设计里容易踩的坑,然后是后端和前端的关键实现,最后把调试阶段遇到的几个棘手问题完整复盘一遍。

1. 项目定位与技术选型:为什么SSM配上Vue依然能打

1.1 这类系统真正的业务痛点

商品管理系统听起来就是增删改查,但真正的难点从来不在CRUD本身,而在数据关系的梳理。比如一个商品品类下面挂着多个规格(颜色、尺寸、包装单位),一个规格对应独立的库存数量,还要考虑上下架状态、预警阈值、最后修改时间等等。需求方之前用Excel管,最大的问题就是"一个商品一条记录"的思维固定住了,根本没办法体现规格和库存之间的关系。

所以系统设计的第一步,不是写代码,而是把数据分析清楚。我把他们的商品Excel拿过来翻了翻,发现很多商品会按照"系列-型号-规格"三层来组织。比如某款角阀有黄铜和锌合金两种材质,每种材质下还有不同的口径尺寸,不同组合的库存和价格不同。如果只建一张商品表,后面库存统计和商品检索一定会出问题。

1.2 SSM和Spring Boot的取舍逻辑

为什么现在还有人选SSM而不是直接上Spring Boot?不是SSM更好,而是SSM的"分层更显性"。Spring Boot通过自动配置把很多东西封装掉了,但SSM要求你手动整合Spring、SpringMVC和MyBatis,每一条配置都写在明面上。对于想理解框架原理、或者应付课程设计要求的人来说,SSM其实是更好的学习载体。

从项目实际角度看,SSM的生命力并没有一些人说的那么弱。它足够稳定,社区资料多,部署也灵活。这个项目规模不大,接口总共就二十几个,QPS撑死个位数,SSM的性能绰绰有余。而且需求方后续很可能找其他外包维护,SSM的代码结构直观,接手成本低。技术选型这事,最忌讳盲目追新,够用、好维护才是第一原则。

1.3 前后端分离的收益和成本

前后端分离对这个项目意味着什么?简单说:后端只出JSON,前端只做渲染。好处是两边可以并行开发,我写接口的同时,前端同事不用等我;坏处是联调成本上来了,跨域、鉴权、字段命名不一致,哪一样都能折腾半天。

选Vue是因为它对中小型项目最友好。Element UI这类组件库拿来就能用,表格、表单、弹窗、分页都现成,开发速度比纯手写快得多。而且Vue的响应式数据绑定处理表单这种高频交互场景非常顺手。项目最终用的是Vue 2 + Element UI,组件生态成熟,遇到问题一搜就有答案,对团队来说维护压力最小。

2. 数据库设计:商品系统最容易被忽略的表结构细节

2.1 核心表结构和字段设计

这个系统的数据库一共七张表:用户表、角色表、商品分类表、商品表、商品规格表、库存流水表、操作日志表。其中商品表和规格表是核心。

商品表我分了几个字段组:基本信息(商品编号、名称、分类ID、品牌)、销售信息(售价、进价、单位)、状态信息(上下架状态、审核状态、创建时间、更新时间)。最关键的字段是spu_id,它把"商品"和"规格"解耦开——一张商品表存的是"某个产品",规格表存的是"这个产品下的具体可售单元"。

规格表里每条记录就是一套实际的SKU:规格名(比如"黄铜-4分-冷热通用")、独立库存、独立条码、售价可覆盖。这么做的好处是,同一个SPU下可以无限挂规格,价格库存单独管理,页面展示也能自动按规格展开。下面这个是建表的核心片段:

CREATE TABLE `spu` ( `id` bigint NOT NULL AUTO_INCREMENT, `spu_code` varchar(32) NOT NULL COMMENT '商品编码', `name` varchar(128) NOT NULL COMMENT '商品名称', `category_id` bigint NOT NULL COMMENT '分类id', `brand` varchar(64) DEFAULT NULL COMMENT '品牌', `status` tinyint NOT NULL DEFAULT '1' COMMENT '上下架状态:1上架 0下架', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.2 库存字段该放在哪张表

库存设计是这类系统里最容易出问题的地方。我见过很多新手把库存数量直接放进商品表,每次修改都update product set stock = stock - 1,这在单测环境下没问题,但一旦多人同时操作,就会出现超卖或者库存不准。

我的做法是:库存只存在于SKU表里,所有库存变动必须走库存流水表。也就是说,任何盘点、销售、入库操作,都在stock_flow表里记一条流水(商品ID、SKU ID、变动类型、变动前数量、变动后数量、操作人、时间),SKU表里的stock字段只作为当前快照。这样即使某天发现库存对不上,也可以通过流水逆推出哪些环节出了问题。

库存扣减的SQL也值得注意。不要先查再减,直接用条件更新:

UPDATE sku SET stock = stock - #{quantity} WHERE id = #{skuId} AND stock >= #{quantity};

受影响行数大于0才说明扣减成功。这个写法在并发下比"查询-判断-更新"安全得多。

2.3 状态字段和逻辑删除

商品系统的状态字段比想象中复杂。一个商品是"草稿"、"已上架"还是"已下架",不同状态下允许的操作完全不同。草稿可以编辑,上架后只能改价格和库存,下架后可以重新上架。这些业务规则如果不在字段设计上提前规范,写业务逻辑的时候就会变成一锅粥。

所以我直接用了一个status字段:0表示草稿、1表示已上架、2表示已下架。接口层面再根据状态做校验,比如编辑接口只允许草稿和下架状态的商品被修改。

另外,商品表和SKU表我都加了deleted字段做逻辑删除。因为商品数据有历史意义,物理删除会导致统计报表缺失和关联记录断裂。所有查询条件里都会带上deleted = 0,这点通过MyBatis的通用SQL片段统一处理,避免每个方法里漏写。

3. 后端实现:SSM框架下的分层开发与关键代码

3.1 工程结构和依赖配置

后端工程就是标准的Maven多模块单工程:controller、service、mapper、entity、common五个包。有人觉得包多繁琐,但对于维护来说,这种分层能逼着你把代码放在该放的位置。尤其MyBatis的Mapper接口和XML文件分开管理,找起来非常快。

Maven的依赖里最值得注意的几个:mybatis-spring的版本必须和Spring版本匹配,fastjson或jackson负责JSON序列化,druid做数据库连接池。另外SSM项目里web.xml的配置很容易出错,DispatcherServlet和ContextLoaderListener的加载顺序决定了Spring容器和SpringMVC容器谁能先初始化,这个顺序一旦反了,注入就会报错。贴一段核心的Spring配置:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.example.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean>

3.2 登录认证与拦截器

前后端分离以后,登录态不能依赖Session了,我用的是Token方案。用户登录成功后,服务端生成一个Token返回给前端,前端每次请求都带上。后端通过拦截器统一校验Token,不需要每个接口重复写鉴权逻辑。

这里有一个细节很容易踩坑:拦截器校验Token时排除登录接口本身,否则登录接口也会被拦截。更稳妥的做法是把"需要放行的URL"维护成一个白名单列表,比如登录、验证码、健康检查。

Token生成我用的是UUID加盐,然后存到Redis里设置过期时间。团队里有人建议直接JWT,但考虑到这个项目没有分布式需求,Redis存储的Token可以随时强制失效,更便于做"账号被顶下线"这类管理操作。拦截器核心逻辑大概是这样:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !redisService.exists(token)) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; } User user = redisService.get(token); request.setAttribute("currentUser", user); return true; }

3.3 商品管理核心接口实现

商品管理模块的接口可以归为三类:分类接口、商品接口、SKU接口。每类接口都遵循统一返回格式——{code, message, data},前端只要判断code是否为0就能决定是否进入正常逻辑。

商品新增接口是最复杂的,因为它涉及事务:插入SPU主记录、批量插入SKU列表、可能还要写一条库存初始化流水。所以这个Service方法一定要加@Transactional,并且注意异常时要回滚。我当时就遇到过一次:SKU插入时有一行数据重复,数据库报错,但SPU已经插进去了,导致页面上出现一个没有规格的空商品。排查了半小时才发现是事务没生效——因为那个方法在同一个类的内部调用,Spring的事务代理没走进去。这是个老生常谈但又特别容易犯的错。

3.4 分页与条件查询的实现细节

商品列表页一个很重要的交互是条件查询加分页:按分类筛选、按上下架状态筛选、按商品名称模糊搜索、按更新时间排序。这个功能如果用一句话概括就是"动态SQL拼接",但拼接的方式有讲究。

我在Mapper的XML里用<where>标签配合<if>做条件组合,避免手动拼接SQL时出现多余的AND。同时分页用PageHelper插件,只需要在Service层调用PageHelper.startPage(pageNum, pageSize),后面的查询就会自动带上LIMIT。注意一点:PageHelper.startPage只对紧接着的第一次查询生效,所以如果有其他查询语句插在前面,分页就会失效。这个细节坑过不少人。

4. Vue前端实现与接口联调:从路由到页面组件的设计

4.1 前端工程和路由规划

前端用的是Vue 2加Element UI,工程结构按views、components、router、api、utils分包。路由规划上做了两级:一级是登录页和布局页,二级是布局页下面的商品列表、分类管理、库存管理、系统设置等子页面。

这里有个经验:路由配置一定要加meta信息,比如requiresAuth和title。requiresAuth配合全局前置守卫做登录校验,title配合afterEach动态设置浏览器标签页标题。这样维护起来很清晰,新增页面的时候不会漏掉权限校验。

router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !localStorage.getItem('token')) { next({ path: '/login', query: { redirect: to.fullPath } }); } else { next(); } });

4.2 axios封装与登录态管理

前端和后端联调,第一步就是把axios封装好。我在utils/request.js里统一创建axios实例,设置baseURL和超时时间,然后在请求拦截器里从localStorage取Token塞到Authorization请求头里。响应拦截器里统一处理后端返回的code,如果code为401就跳转登录页。

有一个容易忽略的点:文件下载接口的响应类型是blob,如果拦截器统一用JSON去解析响应体,下载就会异常。所以封装时要允许某些接口绕过JSON解析,我当时用了一个额外的responseType参数来区分,这个小改动省了后面很多麻烦。

4.3 商品列表页的组件拆解

商品列表页是整系统最复杂的页面,我拆成了三个组件:筛选区(SearchBar)、表格区(ProductTable)、弹窗区(ProductDialog)。筛选区负责收集查询条件,点击查询后触发父组件的加载方法;表格区负责展示数据和操作按钮;弹窗区负责商品新增和编辑的表单。

组件拆分的好处是单个文件不会膨胀到上千行,后续改需求的时候能精准定位。尤其商品编辑弹窗里的表单,字段有十几个,还涉及SKU动态增减行,这种复杂的交互如果全部堆在一个页面文件里,代码几乎没法维护。

联调时一个常见问题是字段命名不一致。后端习惯用下划线spu_code,前端习惯驼峰spuCode。最省事的做法是后端JSON序列化时统一转成驼峰,而不是前端到处做转换。我在Backend的配置里开启了map-underscore-to-camel-case,同时把Jackson的命名策略改成驼峰,一次解决所有字段对齐问题。

5. 调试阶段最值得记录的三个问题

5.1 跨域请求的连锁反应

第一次把前后端跑起来对接登录接口,浏览器直接报跨域错误。这是前后端分离项目必遇的问题。解决方式是在后端加一个CORS过滤器,或者SpringMVC的<mvc:cors>配置。我用的是实现Filter的方式,设置允许的来源、请求头和请求方法。

但跨域问题最坑的还不是配置本身,而是它会造成两个假象:一是调试的时候误以为是接口地址写错,于是改来改去浪费了很多时间;二是"预检请求"如果没处理好,POST请求会失败但GET请求正常,让人摸不着头脑。正确做法是先确认浏览器Network里有没有OPTIONS请求,有的话,过滤器必须放行所有的OPTIONS请求,否则后续业务请求永远到不了后端。

5.2 时间格式和JSON序列化的坑

商品列表里要显示创建时间,前端传回来的是"2024-05-12T08:30:00.000Z"这种UTC格式,直接显示非常难看。这个问题有两种解决路径:一个是后端序列化时统一指定格式,另一个是前端用过滤器格式化。

我最终选了后端方案,在SpringMVC的JSON转换器里配置了日期格式:

<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"> <property name="serializers"> <map> <entry key="java.util.Date" value-ref="dateSerializer"/> </map> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>

千万不要让时间格式的问题在前端到处用split去处理,那是在埋雷。统一出口比到处打补丁可靠得多。

5.3 MyBatis动态SQL里最容易翻车的<if>判断

查询列表时,我遇到过一个诡异的问题:传入商品状态status=0时,查询条件失效,返回了所有状态的数据。排查到最后发现,动态SQL里我写的是<if test="status != '' and status != null">,而Java里传入的status是Integer类型,为0时status != ''判断结果是true,理论上条件应该生效。

真正的问题出在传参方式上。我把多个查询条件封装在了一个POJO对象里传进Mapper,但XML里没有写@Param注解,MyBatis在解析<if test="status != null">时会直接从对象里取属性,这没问题。但当我同时判断字符串和数字时,如果这个字段类型是Integer,status != ''这种写法会导致类型比较的歧义,某些MyBatis版本下会直接跳过条件。

后来我统一了写法:数字类型只判null,字符串类型才判断'',彻底解决。这个问题花了大半天,原因是网上很多老帖子里的写法并不严格区分类型,照着抄就翻车了。

6. 打包部署与后续扩展方向

6.1 环境准备与部署流程

系统部署用的是经典组合:一台Linux服务器装JDK 8和Tomcat 8.5,前端构建后的静态文件交给Nginx托管,Nginx再把/api开头的请求反向代理到Tomcat。SSM项目打war包丢进Tomcat的webapps目录就能跑,但记得把配置文件里的数据库地址、Redis地址改成生产环境的。

前端构建时最容易出问题的就是接口地址。axios的baseURL如果在代码里写死成localhost:8080,部署后必然调不通。我是这样处理的:在Nginx配置里把/api转发到后端,前端所有请求统一走相对路径/api,这样无论本地还是服务器都不用改前端配置。这个思路对于中小型前后端分离项目是最省心的。

location / { root /opt/frontend/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

6.2 这个系统还能往哪些方向扩展

如果需求方后续业务量上来了,这个系统的扩展路径其实很清晰。第一个方向是加采购入库和销售出库模块,把库存流水和业务单据打通;第二个方向是加数据统计图表,比如按品类看库存周转率、按时间段看销量趋势;第三个方向是引入更细粒度的权限控制,比如不同用户能看到不同品类的商品价格。

从架构角度看,SSM这套代码也可以平滑迁移到Spring Boot,因为业务逻辑和Mapper层基本不受影响,主要改的是配置方式。我建议如果还有下一次迭代,优先做库存预警的主动推送——库存低于阈值时在首页给管理员弹提醒,这个功能对商贸公司的日常运营帮助很大,而且实现起来不算复杂。

数据库的事务隔离级别也需要持续关注。当前用的是MySQL默认的REPEATABLE READ,在商品扣减这种高频更新场景下没什么问题,但如果后续加了对账、统计功能,要注意锁等待和死锁的情况,必要时把库存扣减单独拆出来用SELECT ... FOR UPDATE处理。

最后说点个人体会。做这种"看起来普通"的管理系统,真正考验人的不是框架用得有多花哨,而是对业务数据的理解和对细节的敏感度。一个商品有几个状态、库存什么时候变、谁改的、改成了多少,这些看起来不起眼的事情,恰恰是系统稳定性的来源。我在调试阶段被时间格式、动态SQL、事务生效这几种问题各坑了一次之后,现在写代码会下意识先确认边界条件,而不是急着把功能跑通就交差。如果你也在做类似的项目,希望这篇复盘能让你少走一点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 4:58:48

Simulink卫星姿态轨道控制仿真搭建全记录

接到一个任务&#xff1a;用Simulink搭一套卫星姿态轨道控制的仿真&#xff0c;验证控制算法。一开始我觉得挺简单&#xff0c;翻翻资料&#xff0c;把方程一列&#xff0c;模型一拼&#xff0c;跑通就行。真动起手来才发现&#xff0c;资料多到让人眼花&#xff0c;但能直接落…

作者头像 李华
网站建设 2026/10/16 4:58:17

LangChain LCEL表达式语法详解:从管道组合到RAG实战

1. 为什么LCEL值得专门花时间学1.1 从传统调用方式到LCEL的痛点转变先说个背景。我最初用LangChain的时候&#xff0c;写一条链路是这样的&#xff1a;手动定义一个Prompt模板&#xff0c;拿去生成文本&#xff0c;塞给模型调用&#xff0c;再用一个函数去解析模型返回的内容。…

作者头像 李华
网站建设 2026/10/16 4:58:09

手写Spring Boot Starter:从自动配置原理到条件注解实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 4:58:06

鸢尾花数据集聚类分析实战:从KMeans到评估指标全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 4:57:34

MonoDepth2精读:自监督单目深度估计原理与复现全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华