尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

SSM框架衡水特产展销系统实战:从表结构设计到订单事务处理

发布时间:2026/9/24 22:38:41

资讯中心
01
ARTICLE

SSM框架衡水特产展销系统实战:从表结构设计到订单事务处理

SSM框架衡水特产展销系统实战:从表结构设计到订单事务处理
做毕设或者课程设计的时候很多人一看到“XX管理系统”“XX展销系统”这类题目第一反应就是找一套现成的代码改改应付过去。我当年也是这么想的直到真正动手做了一个SSM框架的衡水特产展销系统才明白这类题目反而是最能锻炼JavaWeb全栈能力的机会。这篇文章就把我做这个项目的完整思路、表结构设计、核心代码实现、以及踩过的坑都整理出来给准备做类似商城类系统的同学一个可以直接参考的路线。做这个系统的出发点其实很简单衡水本地特产种类不少老白干、深州蜜桃、武强年画、阜城鸭梨、故城龙凤贡面每一样都有地域特色但线上销售渠道一直比较零散。用SSM写一个特产展销系统一方面能把商品展示、购物车、订单管理这些电商核心流程完整走一遍另一方面也贴合实际场景后面想扩展成真的商用项目也有底子。如果你是正在选课题的学生或者想练手SSM框架整合的开发者这篇内容可以直接当作一个从零到一的实战参考。1. 项目整体设计先别急着写代码把需求拆清楚很多人拿到题目第一件事就是打开IDE建项目这是最容易翻车的做法。SSM项目最怕的就是边写边想写到后面发现表结构对不上业务Controller里面塞了一堆乱七八糟的逻辑改起来想死的心都有。我建议动手前先用半天时间把需求和模块边界画清楚。1.1 为什么选SSM而不是Spring Boot题目明确写了SSM那技术栈就锁定了Spring SpringMVC MyBatis。虽然现在企业里Spring Boot已经占了主流但学校课程设计普遍还停留在SSM阶段原因是SSM把Spring的核心概念暴露得更彻底Spring的IOC容器怎么管理Bean、SpringMVC的DispatcherServlet怎么分发请求、MyBatis的Mapper代理怎么生成这些在Spring Boot里都被自动配置掩盖了在SSM里每一步都要自己配置反而能把底层原理吃透。我选的版本是这样的Spring 5.1.8、SpringMVC 5.1.8、MyBatis 3.5.3数据库用MySQL 8.0连接池用Druid 1.1.21前端页面用JSP JSTL Bootstrap 3。这套组合比较成熟网上资料多遇到问题容易搜到解决方案。1.2 功能模块的前台与后台划分特产展销系统的核心是“展销”两个字展是展示特产销是完成交易所以系统天然分成两个端口前台用户端负责商品浏览和交易。游客可以看商品列表、按分类筛选、搜关键词注册登录后能加购物车、下订单、查看个人订单列表。这里我额外加了一个公告栏用来展示平台动态和促销信息代码量不大但能让首页看起来不那么空。后台管理员端负责商品和订单管理。管理员登录后可以维护分类、添加或编辑商品、上下架商品、处理订单状态、发布公告。用户管理的功能我也做了但只保留了禁用和启用操作没有做复杂的权限控制毕竟课程设计级别的项目RBAC做太深反而喧宾夺主。模块划分用一张表看更清楚模块角色核心功能用户模块游客/用户注册、登录、个人信息维护商品模块游客/用户商品列表、分类筛选、关键词搜索、商品详情购物车模块用户加入购物车、修改数量、删除商品、结算订单模块用户提交订单、取消订单、查看历史订单管理后台管理员商品管理、分类管理、订单管理、公告管理登录鉴权用户/管理员拦截未登录请求区分用户与管理员权限2. 数据库设计商城类项目的表结构是有套路的SSM项目里MyBatis的核心工作就是操作数据库所以表结构设计的好坏直接决定了后面写Mapper的难度。商城类系统做过一次之后就会发现表结构套路高度一致用户表、商品表、分类表、购物车表、订单表、订单明细表这几张是底子再根据业务需要增加公告表、轮播图表、地址表之类的辅助表。2.1 八张核心表的关联关系我的库名是ssm_specialty一共建了八张表。用户表user和地址表user_address是一对多关系因为一个用户可以维护多个收货地址分类表category和商品表product是一对多一个分类下有多个特产购物车表cart是用户和商品的多对多关系通过联合唯一索引实现即一个用户对同一个商品只能有一条购物车记录订单表orders和订单明细表order_item是一对多一个订单包含多个商品。订单和商品为什么中间要加一个订单明细表这是很多新手容易忽略的点。订单表存的是订单整体的信息比如订单编号、总金额、下单时间、收货地址的快照但一个订单里可能有老白干和蜜桃两种商品每个商品的数量、当时的单价、小计金额都不一样这些必须拆到子表里存。等发货、售后或者对账的时候明细表能还原出每一笔交易的真实构成。另外注意订单里存的一定是下单那一刻的商品价格快照不能去关联商品表实时查价格因为商品价格后续可能会调整。2.2 重点表的字段设计细节商品表product是核心字段设计上要兼顾展示和交易两个场景。基础字段包括id、category_id、name、subtitle副标题/卖点、main_image主图、detail富文本详情、price价格、stock库存、sales销量、status上下架状态、create_time、update_time。价格字段用DECIMAL(10,2)而不是FLOAT或DOUBLE。因为浮点数在计算机里是近似存储做金额计算时会出现0.1 0.2 ! 0.3的问题电商场景里金额差一分钱都是事故。销量字段很关键列表页的商品排序默认按sales降序这样卖得好的特产自然排前面不需要额外开发推荐逻辑。订单表orders的status字段我用TINYINT存状态值手动维护一套状态机状态值含义操作0待付款用户可取消1待发货用户可申请退款2待收货管理员发货后进入用户可确认收货3已完成用户确认收货后进入4已取消用户取消或超时未支付订单编号我用时间戳加用户ID和随机数拼接生成比如20250607153012001这种格式虽然丑了点但足够保证唯一性不用额外引入分布式ID生成方案。订单表里还要冗余存储收货人姓名、电话、详细地址的字符串快照不能只存一个address_id因为用户改了地址后历史订单的收货信息不能跟着变。3. 开发环境搭建与核心业务实现SSM项目的搭建过程有固定套路Maven工程建好之后按web.xml → spring-mvc.xml → spring-dao.xml → mybatis-config.xml的顺序配置中间遇到问题按日志一层层排查。我把关键配置和核心逻辑拆开讲。3.1 Maven工程结构和关键配置工程结构我按标准的三层架构划分包名统一以com.hs.specialty开头com.hs.specialty ├── controller # SpringMVC控制层 ├── service # 业务逻辑层 │ └── impl # 业务实现类 ├── dao # MyBatis的Mapper接口 ├── pojo # 实体类 │ ├── User.java │ ├── Product.java │ └── Orders.java ├── interceptor # 登录拦截器 ├── util # 工具类MD5加密等 └── vo # 视图对象多表查询结果封装pom.xml里的核心依赖要注意版本兼容。Spring和SpringMVC版本必须一致MyBatis和mybatis-spring要匹配不然运行时会报ClassNotFound之类的错误。我自己用的关键依赖如下properties spring.version5.1.8.RELEASE/spring.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.3/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.3/version /dependency !-- 数据库连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.1.21/version /dependency !-- JSON序列化 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.9.9/version /dependency !-- JSP标准标签库 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency /dependenciesspring-dao.xml配置数据源和MyBatis整合的时候我建议打开MyBatis的下划线转驼峰映射这样数据库字段order_status可以直接映射到实体类的orderStatus属性不用每个字段都写resultMapbean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/ssm_specialty?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfigLocation valueclasspath:mybatis-config.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.hs.specialty.dao/ /bean3.2 前台商品展示与详情页实现商品列表页我做了分类筛选加关键词搜索的组合条件查询。前端页面通过GET请求传递categoryId和keyword两个参数Controller接收后传给Service再映射到Mapper的查询条件里。这里有个小技巧用MyBatis的动态SQL处理可选条件而不是写死两个单独的查询方法这样代码复用率更高。ProductMapper.xml里的核心查询长这样select idselectProductList resultTypecom.hs.specialty.pojo.Product SELECT id, category_id AS categoryId, name, subtitle, main_image AS mainImage, price, stock, sales, status, create_time AS createTime FROM product where status 1 if testcategoryId ! null and categoryId ! 0 AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY sales DESC, create_time DESC /select商品详情页相对简单根据id查商品信息顺手查一下同分类下其它商品放到“相关推荐”区域增加页面的浏览深度。详情页的富文本内容我直接存在product表的detail字段里类型用LONGTEXT管理端用富文本编辑器写入前端用JSP的${product.detail}直接输出HTML内容。3.3 购物车和订单流程事务边界要控制好购物车我用了经典的数据库存储方案。表结构是cart(id, user_id, product_id, quantity, checked, create_time)其中user_id和product_id建了联合唯一索引防止同一个用户重复添加同一商品。加购的时候先查是否已存在存在就数量加一不存在就插入新记录。提交订单是整个项目里业务复杂度最高的地方涉及购物车数据读取、订单主表插入、订单明细批量插入、商品库存扣减、清空购物车五个步骤这些操作任一步失败都可能造成数据不一致所以必须放在同一个事务里。Spring声明式事务只需要在Service方法上加Transactional注解即可Transactional(rollbackFor Exception.class) Override public Orders createOrder(Integer userId, ListInteger cartIds, Integer addressId) { // 1. 根据cartIds查购物车记录关联商品表查出当前价格和库存 // 2. 计算订单总金额校验库存是否充足 // 3. 生成订单号插入orders表 // 4. 循环插入order_item明细 // 5. 更新product表库存 stock stock - quantity, sales sales quantity // 6. 删除对应cart记录 }这里有一个容易踩的坑库存扣减我建议用乐观锁的方式在更新语句里加库存条件的判断update iddeductStock UPDATE product SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{productId} AND stock #{quantity} /update检查dao.update返回的影响行数如果为0说明库存不足或者并发情况下别人已经把库存抢走了直接抛出异常回滚事务。这种方式写起来简单对于课程设计级别的并发量完全够用。不要天真地只是查一下库存数值再去扣减那是典型的并发不安全写法两个用户同时下单可能把库存扣成负数。3.4 登录拦截器和用户权限控制系统里除了商品列表页和详情页其它页面都需要登录才能访问。我用SpringMVC的HandlerInterceptor实现了一个登录拦截器在spring-mvc.xml里配置拦截路径mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/user/login/ mvc:exclude-mapping path/user/register/ mvc:exclude-mapping path/product/**/ mvc:exclude-mapping path/category/**/ mvc:exclude-mapping path/search/ mvc:exclude-mapping path/resources/**/ mvc:exclude-mapping path// /mvc:interceptor /mvc:interceptorsUserInterceptor的核心逻辑是从session里拿当前登录用户拿不到就跳转到登录页。管理员操作再做一个角色判断只有角色为admin的用户可以访问/admin/**的路径。4. 实操过程中的踩坑记录与排查方式这个项目从搭建到跑通我遇到过的各种报错加起来比写代码的时间还长。整理几个最高频的问题给后来人省点时间。4.1 高频问题速查表问题现象根本原因解决方案启动Tomcat直接报ClassNotFoundException冲突或缺失jar包检查依赖版本清理本地Maven仓库重新importJSP页面中文乱码请求和响应编码不一致在web.xml配置CharacterEncodingFilter统一UTF-8查询结果是null的字段实体类属性和数据库列名不匹配开启mapUnderscoreToCamelCase或用别名映射页面样式和JS加载失败静态资源被SpringMVC拦截在mvc配置中放行/resources/**Ajax请求返回JSON报错缺少Jackson依赖或配置pom引入jackson-databindspring-mvc.xml开启注解驱动点击“结算”一直没反应事务未生效或SQL错误查看控制台SQL日志检查Transactional配置4.2 两个值得展开的典型问题第一个问题是懒加载和JSON序列化冲突。我在订单列表页想通过Ajax返回用户的订单数据订单对象里关联了订单明细列表明细里又关联了商品对象。由于MyBatis的一对多关联默认用的是懒加载Jackson序列化时如果触发了调用而连接Session已经关闭就会报LazyInitializationException。解决思路有几种一种是在查询时用JOIN FETCH或编写联表查询一次性把关联数据查出来避免懒加载另一种是直接把关联对象标记为JsonIgnore在视图层再拼装需要的DTO。我最后选择了写VO类的方式单独为前端定义展示对象结构更清晰不会出现实体类被JSON序列化污染的问题。第二个是事务不生效。有朋友按照网上的模板写完代码发现提交订单时故意把库存扣成负数也没报错查了一圈才发现他是直接在Controller里写了SQL操作压根没走Service层的事务代理。Spring声明式事务是基于AOP代理的只有通过代理对象调动方法才有事务增强同类内部调用this.createOrder()会让注解失效。另外Transactional默认只在RuntimeException时回滚如果代码里手动catch住了异常不往外抛事务也会像没事发生一样提交成功。5. 项目打包部署与实际应用场景SSM项目的部署和Spring Boot不太一样没有内嵌的Tomcat需要打包成WAR包扔到外部Tomcat的webapps目录下。看清楚这个区别部署的时候能少走很多弯路。5.1 WAR包打包流程我用Maven的package命令打包但要注意一点pom.xml里packagingwar/packaging要写对同时在pom里配置maven-war-plugin插件并且把Spring的配置文件路径和打包后的class目录对应上。打包前先跑一遍测试确认没有编译错误。打完包后把target目录下的.war文件复制到Tomcat的webapps目录启动Tomcat后它会自动解压。如果发现静态资源访问不了多半是应用路径前缀的问题部署后访问地址是http://localhost:8080/ssm_specialty/如果直接配了/作为访问路径要去Tomcat的conf/server.xml里调整Context配置。5.2 图片上传与虚拟目录映射特产展销系统的图片处理是个容易被忽略但工作量不小的地方。我用的方案是上传到本地磁盘的指定目录然后通过Tomcat配置虚拟目录映射让前端可以通过URL访问。上传接口的核心逻辑是接收MultipartFile生成唯一的文件名时间戳加随机数写入到磁盘路径然后把相对路径存到数据库的main_image字段里。这里有个实际经验文件存储路径不要写成硬编码放到properties配置里方便换环境时修改。虚拟目录映射在Tomcat的server.xml的Host节点下增加一行Context path/upload docBase/home/webapp/upload reloadabletrue/这样前端的图片地址就是http://localhost:8080/upload/xxx.jpg数据库里存的也是/upload/xxx.jpg相对路径页面展示时拼上服务器地址即可。这种本地存储方案适合学习和中小型场景真正的生产环境一般会用云存储对象但在课程设计里完全够用。5.3 这个项目还能怎么扩展系统跑通以后我从工程和业务两个角度梳理过可以继续深化的方向都是性价比比较高的第一个方向是工程层面升级。把JSP页面换成Vue Element UI做前后端分离后端提供纯JSON接口或者直接把SSM升级成Spring Boot数据访问层和Service层代码大部分可以复用特别适合验证自己对主流框架的掌握程度。也可以引入Redis缓存热点商品信息和Session共享解决部署多台服务器时的登录状态保持一致问题。第二个方向是业务层面增强。比如增加秒杀或限时折扣功能订单模块可以对接支付宝沙箱环境集成真实支付商品模块可以增加多图展示和规格选择管理端增加基于ECharts的销售统计报表。这些功能每扩展一个都能在简历上多写一行有含金量的项目经历。写在最后的个人体会把SSM的衡水特产展销系统完整做下来之后我对SSM框架的理解和刚拿到题目时完全是两个层次。以前看网上的教程觉得Spring、SpringMVC、MyBatis是三个割裂的东西等自己把配置文件一行行写出来、把请求从DispatcherServlet走到Controller再走进Service和Mapper、把一条数据从数据库查出来渲染到页面上才真正理解了它们是怎么协作的。如果你也在做类似的课设项目我的建议只有一条不要满足于把功能跑通试着去改一改框架的配置、换一种查询方式、解决一个并发问题那些才是真正会留存在你脑子里的东西。做项目时遇到卡住的地方是必然的保持耐心逐个击破最后你会发现收获远不止一份代码。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。