资讯动态

SSM+Vue小饰品商城实战:从数据库设计到部署上线

发布时间:2026/10/22 23:47:35 来源:尧图企业网站定制
这个项目做了大概三周多从需求梳理到部署上线。某小饰品城那边原本一直靠线下批发和档口零售疫情之后开始意识到线上展示的重要性但又不想要那种动不动就上微服务、前后端分离过度设计的方案所以最终定了SSMVue的组合来做这个网站。整套系统跑下来功能覆盖了商品展示、搜索筛选、购物车、订单、后台管理和会员体系虽然不复杂但涉及到的技术点很典型前后端联调、权限控制、文件上传、分页搜索都碰到了。如果你正在做类似的商城类项目或者准备用SSMVue交一个课程设计、毕业设计这篇博文会比较对你的胃口。我会从需求分析一路拆到数据库设计、接口实现、前端页面再到部署时踩过的坑把每步为什么这么做的理由一并讲清楚。1. 项目定位先行为什么小饰品城网站不能照搬通用商城模板很多人拿到商城类项目第一反应是去Gitee上找个开源电商系统改改但这类通用模板大多是为B2C平台设计的功能堆得很满营销模块、优惠券、秒杀、分布式文件存储、消息队列全都有。真拿到某小饰品城这个场景下根本跑不动店里的阿姨和档口老板也不会上传几十个SKU的复杂商品图文。1.1 线下市场的真实业务形态小饰品城本质上是一个“市场管理方 多个独立档口”的结构。市场方负责物业管理、品牌推广、活动策划档口老板各自独立进货、定价、销售。网站不能做成一个去中心化的C2C平台那样管理方会失去控制力也不能做成只有市场方自己卖货的B2C因为真正有货的是那几百个档口。最终定位是“线上展示 线下引流为主在线交易为辅”。会员可以看到各档口的在售商品、档口位置在线购买部分产品也可以加入购物车后到店取货。市场管理方在后台统一审核档口和商品的上架申请避免出现假货、违禁品和不规范描述。档口老板不用自己维护网站统一由市场方的运营人员录入商品图片统一拍摄、统一上传保证整个平台的视觉风格一致性。这样定义下来系统的用户角色就是三类会员、档口商家、市场管理员。角色少权限逻辑清晰数据库设计也简单很多。1.2 功能范围界限哪些做哪些不做需求评审阶段市场方提了一些看起来很好但明显会拖慢进度的需求比如在线直播、饰品3D展示、拼团、会员积分商城。这些最终都被砍掉了。核心技术团队必须守住边界否则这类小项目两个月都收不了尾。最终确认的功能清单前台首页轮播、商品分类导航、商品搜索、商品详情、购物车、下单、个人中心、档口展示。后台管理员登录、档口管理、商品管理、库存管理、订单管理、会员管理、轮播图管理。会员注册登录、收货地址管理、订单查询、订单状态流转待付款、待发货、已发货、已签收、已取消。这些功能听起来少但涉及到的表结构、接口数量已经不少前后端加起来大概做了40多个接口代码量一万多行。对于一个SSMVue的项目来说体量刚刚好既不是玩具项目也不会因为过度设计而失控。2. 前后端技术组合的选型逻辑SSMVue在2024年还值不值得用现在做新项目主流推荐是Spring Boot Vue 3 MyBatis-Plus那么为什么还要选SSMVue这种“古董组合”这得看项目背景。一方面是某高校的课程要求或者说部分外包项目方指定的技术栈SSM仍然是很多院校教学和毕设系统的默认要求另一方面SSM本身并不影响系统完成功能只要分层清晰迁移到Spring Boot只是配置方式的变化。2.1 与Spring Boot方案的对比对比维度SSMSpringSpringMVCMyBatisSpring Boot MyBatis-Plus配置复杂度需要手写XML配置、web.xml、扫描路径、拦截器自动配置依赖起步就行学习价值配置过程能让人理解Spring容器和MVC原理上手快但容易变成配置黑盒项目体积相对轻量部署只需要一个WAR包放Tomcat内嵌Tomcat一个JAR启动适合场景教学类、课程设计、维护老项目企业级新项目、快速迭代坑位版本兼容性需要自己把控版本相对统一社区推荐如果你纯粹为了快速交付赚钱选Spring Boot会省事很多如果是为了学习Spring底层或完成教学要求SSM项目反而是更好的练习材料。我这套项目在配置阶段就把Spring容器的初始化流程、拦截器注册、MyBatis的Mapper代理机制全摸了一遍这种收获是直接用Spring Boot的人体会不到的。2.2 前后端分离的架构设计这个项目的架构是后端SSM提供RESTful API接口按照Controller-Service-Mapper三层拆包。前端Vue 2 Element-UI Axios构建工具用的Vue CLI。部署前端打包成静态资源交给Nginx托管后端WAR包放在Tomcat通过反向代理解决跨域。为什么要拆成前后端分离而不是传统的Thymeleaf模板渲染因为市场方后续打算做小程序、做移动端H5如果模板引擎把页面都写死在服务端将来小程序端几乎要重新开发。拆成API模式小程序端、H5端、PC端可以共用同一套后端接口这是长期视角下的正确选择。3. 数据库设计与商品模型小饰品商品信息比想象中复杂小饰品行业有个特点商品规格杂、图片多、价格波动快。一条项链可能有镀金、纯银两种材质每种材质下有金色、银色、玫瑰金三种颜色每种颜色还有不同长度。如果按常规电商的“SPU SKU”表结构来做这类的组合数量会让数据库爆炸。3.1 分类表与商品属性的取舍完成这类需求第一版设计沿用通用的electricalCREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, parent_id INT DEFAULT 0, sort_order INT DEFAULT 0, level TINYINT );分类做成了无限级分类支持三级嵌套。一级是饰品大类如“项链”“耳饰”“手链”“发饰”“胸针”二级是风格如“复古”“韩版”“民族风”“简约”三级是材质或者使用场景如“纯银”“合金”“日常通勤”“宴会”。商品表不直接创建几百个规格字段而是设计成CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT, shop_id INT, title VARCHAR(150), subtitle VARCHAR(255), main_image VARCHAR(255), images TEXT, price DECIMAL(10,2), original_price DECIMAL(10,2), stock INT, sales_count INT, status TINYINT, description TEXT, create_time DATETIME );对于“颜色、尺寸”这类少量规格属性直接存到详情表里不在表结构层面做校验。因为后台是先由管理员人工录入录入时选择了“颜色”选项后自动生成一个JSON格式的规格数据存到specs字段前端在详情页动态渲染出来即可。这种反规范化设计适合这种小规模后台项目既避免了SKU表的大面积join又比全JSON存储更可控。3.2 图片存储方案小饰品图片有“白底主图 场景侧拍图 细节图”三种每件商品平均5到6张图。图片存储没有接OSS或七牛云而是直接传到了服务器本地路径/data/images/然后通过Nginx映射成http://localhost/images/xxx.jpg的访问路径。这样做的好处是省云存储费用部署也简单缺点是服务器硬盘如果不定期清理会堆积。做之前评估过一个饰品城的店铺每周上新大概50件商品每件就算10张图一年约几万个文件按单张200KB算一年也就几个GB普通云主机硬盘完全够用。所以我最后的结论是这种量级的项目普通本地存储完全够用没必要接对象存储。这是在项目里一个值得聊的最典型的技术选型问题。4. 后端核心模块实现权限过滤、商品检索、订单状态机后端这块我按“通用框架搭建 → 核心业务接口 → 权限控制”三步来推进。先花两天时间把Spring配置跑通再写商品和订单接口最后统一添加登录拦截和权限校验。4.1 Spring配置的难点扫描、注解驱动与拦截器SSM项目启动时最容易出问题的是包扫描冲突。Spring容器扫描Service和MapperSpringMVC容器扫描Controller如果两个容器扫到了同一个Bean要么事务失效要么接口404。在spring-mvc.xml里context:component-scan base-packagecom.shop.controllers /在applicationContext.xml里context:component-scan base-packagecom.shop.service, com.shop.mapper context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller / /context:component-scan两边的扫描范围严格分开避免一个Bean被两个容器都初始化。这个问题排查起来很隐蔽当时调了一天最后发现日志里有两次初始化同一个Service的痕迹才定位到。4.2 基于JWT的登录态设计会员登录后后端的登录接口返回一个Token前端把它存到localStorage每次请求在Authorization头里带上。后端用一个拦截器解析Token并取出用户ID放入ThreadLocal。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { writeUnauthorized(response); return false; } try { Integer userId JwtUtil.parseToken(token); UserContext.setUserId(userId); return true; } catch (Exception e) { writeUnauthorized(response); return false; } } }这里要特别提到一个很容易被忽略的细节拦截器只拦截/api/member/**、/api/cart/**、/api/order/**而商品列表/api/product/**是放行的。很多初学SSM的人在拦截器里配了/**导致前端商品页整个打不开然后开始怀疑跨域、怀疑前端代码一通排查最后发现是拦截器把公开接口也拦了。接口的公开性和私有性一定要在路径设计时就规划好别等出问题再改。4.3 商品分页搜索MyBatis动态SQL是核心商品列表接口是一个典型的多条件分页查询支持分类筛选、关键字模糊搜索、价格区间、销量排序、新品排序。使用MyBatis的where标签拼接条件比自己在Java代码里拼SQL串要安全、干净太多。select idsearchProductPage resultTypecom.shop.entity.Product SELECT p.*, c.name AS category_name FROM product p LEFT JOIN category c ON p.category_id c.id where p.status 1 if testcategoryId ! null and categoryId ! 0 AND (p.category_id #{categoryId} OR p.category_id IN ( SELECT id FROM category WHERE parent_id #{categoryId} )) /if if testkeyword ! null and keyword ! AND (p.title LIKE CONCAT(%, #{keyword}, %) OR p.subtitle LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND p.price gt; #{minPrice} /if if testmaxPrice ! null AND p.price lt; #{maxPrice} /if /where choose when testsort salesORDER BY p.sales_count DESC/when when testsort newORDER BY p.create_time DESC/when otherwiseORDER BY p.id DESC/otherwise /choose /select这里我特意不推荐在分类筛选时只查一级分类。因为饰品分类是树形的如果用户点了“项链”这个一级分类里面的二级分类“复古”“韩版”下的商品也应该同时出现在列表中。实现上可以用子查询找出所有子孙分类ID也可以在前端点击分类时就把子分类级联传递过来后者的代码逻辑更简单也减少了数据库递归查询的压力。MyBatis分页用的PageHelper插件这个插件在SSM下有个经典坑PageHelper版本和MyBatis版本不兼容时分页会不生效翻页返回的还是全部数据。排查方法是在日志里看生成的SQL末尾有没有LIMIT没有就是插件没生效多半是版本冲突解决方式是把MyBatis升级到3.5.x或者检查依赖是否被传递依赖覆盖了版本。4.4 订单状态流转订单模块我用的状态机设计字段是一个整型status各个状态之间的跳转在Service层校验。状态码含义可执行操作下一个状态0待付款支付/取消1 / 51待发货管理员发货22已发货会员确认收货33已签收申请售后/评价4售后中/6已完成4售后中管理员处理3 或 65已取消无无6已完成无无这里有个容易被忽视的业务点库存扣减的时机。我第一次设计是在用户点击“下单”时立刻扣减库存后来发现如果用户下单后一直不付款库存就锁死了。实际重新设计为“下单时不锁库存支付成功后扣库存”这样更符合线下商城“看货不下单不占库存”的经营习惯。购物车在下单后的处理则是支付成功后把对应购物车项删除而不是下单时删除因为用户可能在待付款状态时去再下一单。4.5 统一返回结果与全局异常前后端分离开发时如果每个接口的返回格式都不一样——有的返回{code:200, data:[]}、有的直接返回[{},{}]前端联调会是一个灾难。所以必须在一开始就约定统一返回体public class ResultT { private Integer code; // 200成功400业务失败401未登录500服务器异常 private String message; private T data; }全局异常处理用ControllerAdvice业务异常直接throw new BizException(...)由异常处理器统一打包成Result格式返回。这个设计避免了每个Controller都写try-catch代码清爽很多。5. 前端Vue页面路由组织、Axios封装与文件上传前端最开始打算用JSPJQuery后来因为要响应式适配手机端果断改成VueElementUI。小饰品城的顾客用手机浏览的比例超过八成所以设计上优先保证移动端体验。5.1 项目结构和路由项目目录按基础结构组织这里不做过多说明views下面分成home、product、cart、order、member、admin六个模块。路由用懒加载方式const routes [ { path: /, component: () import(../views/home/Home.vue) }, { path: /product/detail/:id, component: () import(../views/product/ProductDetail.vue) }, { path: /cart, component: () import(../views/cart/Cart.vue), meta: { requiresAuth: true } } ];路由守卫检查meta.requiresAuth如果是需要登录的页面看localStorage里有没有Token没有就跳转到登录页并携带redirect参数登录完成后回到原来的页面。这种体验是单页应用比传统JSP多出的优势。5.2 Axios拦截器省掉大量重复代码所有接口请求统一走同一个Axios实例const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code 401) { router.push(/login); return Promise.reject(new Error(登录过期)); } return res; }, error { Message.error(网络异常); return Promise.reject(error); } );联调阶段最大的便利是接口崩了、报错了、Token过期都统一在前端拦截器里处理联调的那一周基本没跟后端吵过架。5.3 富文本编辑器上传和图片压缩商品详情包含图文混排后台管理员用的富文本编辑器是wangEditor。这里遇到一个很经典的问题编辑器默认只传base64格式大图一张3MB的图片base64之后膨胀到4MB存进数据库的TEXT字段里完全没有问题但商品详情接口返回时前端要解析这个超大字符串页面直接卡死几秒。最后做了两个处理富文本里粘贴图片时先把图片转成Base64后通过上传接口传到服务端拿回图片URL再插入编辑器。前端拿到详情数据后如果发现description字段里的图片URL统一加了一个压缩参数Nginx的image_filter模块会自动按指定宽度输出压缩图减小传输体积。图片上传后端接口PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.fail(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); if (!Arrays.asList(.jpg, .jpeg, .png, .gif).contains(ext.toLowerCase())) { return Result.fail(图片格式不支持); } String fileName UUID.randomUUID().toString().replace(-, ) ext; File dest new File(uploadPath, fileName); file.transferTo(dest); String url /images/ fileName; return Result.success(url); }这里要强调一个后端细节上传目录必须和项目目录分离不能放到Tomcat的webapps内部。很多教程会把图片存到D:/upload或/data/upload但放Tomcat内的话每次重新部署WAR包都有被清空的风险。把上传路径配成外部磁盘路径再在Nginx里加一个location /images/的映射临时文件就和应用本体彻底解耦了。6. 部署与联调这五个坑每一个都浪费了至少半天时间程序写完之后部署上线阶段反而是最折磨人的。很多同学在本地开发环境下一切正常一上云服务器就各种玄学问题其实背后原因都差不多。我把这次踩到的坑整理成了一份清单希望你以后不要再踩。6.1 跨域问题前端打包后根本没跨域一开始前端通过http://localhost:8080/api请求后端后端开启CORSBean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:8080); // ... }本地开发的确流畅。但上线之后前端打包产物交给NginxNginx监听80端口后端Tomcat跑在8080端口这时如果Nginx没有配置反向代理页面请求还是直接打到8080大概率出现跨域错误。最佳解决办法不是开启CORS而是在Nginx配置反向代理location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }加上这层代理后前端请求的地址始终是同源的/api/xxx由Nginx转发到后端跨域问题彻底消失。6.2 Vue Router的history模式刷新404前端路由如果用了history模式刷新一个非首页的URL时Nginx找不到对应的静态文件直接返回404。这个坑只会在生产环境暴露本地dev server因为有history fallback永远都正常。解决方式是Nginx配置try_fileslocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这个配置的意思是如果用户访问/cart先找是否存在/cart文件找不到就返回/index.html由前端路由接管。6.3 MySQL时区导致的订单时间差8小时本地数据库连接串没配serverTimezone时开发环境因为MySQL和操作系统时区一致往往没问题。但云服务器默认可能是UTC时区插入数据库的create_time比北京时间少了8小时。解决办法是在JDBC连接串上加参数jdbc.urljdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse如果数据库之前已经存了不少错误时间的数据就没办法靠改配置回退了得写UPDATE语句修正。所以新项目上线第一天就要把这个参数配好。6.4 文件上传大小限制Tomcat默认单文件上传上限是1MB一台普通配置下传饰品细节图经常超过这个大小。根据框架版本不同配置位置也不一样。在spring-mvc.xml里配置bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value52428800/ property namemaxUploadSizePerFile value52428800/ /bean同时生产部署后还要确认Nginx的client_max_body_size也调大了client_max_body_size 50m;前后端不管哪一边没配置都会出现“明明接口没报错但图片就是传不上去”的诡异情况。6.5 商品图片列表的懒加载与预估高度列表页图片加载特别慢除了服务器带宽因素外还有一个前端性能问题。商品列表一次返回20个商品数据如果每个商品都立即加载五张高清大图页面首屏就卡死。我用了ElementUI的lazyload指令滚动到视口才真正加载图片同时给图片容器设一个固定高度避免列表滚动出现高度跳动。这一套优化做完首页响应速度从原来的2秒多降到了700毫秒左右。7. 项目之后还能怎么扩展网站上线只是第一步后续如果要继续演进有几个可以着手的方向。首先是把后台的商品录入流程做半自动化。现在管理员需要手工上传一张张图片再填描述效率低还容易出错。可以做一个批量导入功能管理员把Excel模板填好商品名称、价格、库存、分类图片打包成ZIP压缩包上传后端自动解压并建立图片和商品的关联关系一次能导入上百个商品。这个功能对维护每天上新频率较高的饰品市场几乎必做。其次是做小程序端。后端的接口已经按RESTful风格写好了小程序端直接复用现有的API和图片资源只是重新做一套前端页面工作量只有整套系统的三成左右。这个小程序还可以叠加做一个“扫码看档口”功能每个档口门口贴一个二维码顾客扫码直接进档口的产品页面把线上流量引到线下订货这才是小饰品市场最需要的场景。还有一个是供应链维度的优化目前后台只有商品库没有进货批次和供应商管理如果要让档口老板自己维护供货链那两个表的改动就能支撑起来。建立一个“进货批次”表和“供应商”表商品入库时关联批次盘点时按批次回溯进货成本。这样销售毛利、库存成本就可以相对准确地计算为后续的店铺盈利分析打基础。8. 总结与实操心得这套SSMVue的小饰品商城项目代码虽然不复杂但麻雀虽小五脏俱全在实际操作中踩过了不少坑也有了一些实践经验。最深刻的体会是做项目最重要的不是会多少框架而是能不能在动手前把业务拆明白。这几周实际开发下来在需求评审阶段花的一天半时间后面至少省下了三天的返工量比如一开始就想清楚“线下引流为主、在线交易为辅”的定位就不会去做复杂分销系统提前想好图片上传存储方案就不会中途从base64换成文件存储。如果你现在正要开始做类似的项目给你三个建议技术栈不重要到无关紧要但选一个自己最熟悉的方案然后把这个方案用通透。SSM配置繁琐但恰恰是这些繁琐让你理解Spring的容器和管理原理用Spring Boot更方便但别让自动配置掩盖了你对底层机制的理解。数据库表设计要谨慎商品信息、订单状态机这类关键模型多花时间讨论比后期写接口时再改表省事太多。一定要尽早做前后端接口联调而不是等所有后端写完再开始写前端。Axios拦截器、统一返回体的约定越早建立后面配合越顺畅。最后说一个小技巧后端接口的上线前一定要把每个接口在Postman里跑一遍边界条件包括未登录访问、传空参、传超大分页页码、非法ID等。很多在线上的“莫名其妙错误”其实都是早期联调时没覆盖的边界情况。这套项目当时提前做了这个步骤正式发布后几乎没有收到运营方报的Bug实测下来很稳。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑