ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM实战:从零构建水果蔬菜商城系统

SSM实战:从零构建水果蔬菜商城系统 1. 项目概述与整体设计思路1.1 SSM到底是什么这个商城系统能做什么先把这个标题拆开说清楚。SSM指的是Spring、SpringMVC、MyBatis这三件套的组合是Java Web开发里非常经典的一套后端技术栈。很多计算机专业的学生做毕业设计或课程设计时都会选这套组合因为它结构清晰、文档多、面试也常被问到属于性价比很高的一套技术方案。这个水果蔬菜商城系统核心是解决“生鲜电商”场景下的商品展示、购物车、下单、订单管理这一整条交易链路。跟普通的服饰、数码类电商相比生鲜类目有自己的特点商品需要按斤、按份售卖库存变动频繁配送时效敏感所以系统在设计时要特别处理计量单位、库存扣减、订单状态流转这几个环节。整个系统分成两端前台商城面向普通消费者包括用户注册登录、蔬菜水果分类浏览、商品详情、搜索、加入购物车、提交订单、在线模拟支付、订单查询等功能。后台管理面向运营人员包括商品上下架、库存管理、分类管理、订单处理、用户管理等。这种角色分离的设计在电商类课设里是最标准的做法既能把SSM三个框架的核心能力都用上又能比较完整地体现业务逻辑。1.2 为什么选SSM而不选别的技术组合先说结论SSM在今天看起来不算新潮但在教学和毕设场景下它依然是“稳”字当头的一个选择。相比Servlet JSP的纯传统写法SSM把控制层、业务层、持久层分得清清楚楚代码组织更符合工程化思维。答辩的时候面试官或老师问起分层架构你也能讲得比较系统。相比Spring Boot MyBatis Plus这套更现代的搭配SSM的配置是显式写在XML或配置类里的也就是说每一步依赖注入、每个拦截器、每次事务管理你都能看到来龙去脉。这听起来麻烦但对理解框架原理反而有帮助——Spring Boot自动配置太省事了省到很多新手根本不知道背后发生了什么。相比JSPServlet的祖传方案SSM引入了Spring的IoC容器和声明式事务、MyBatis的SQL映射机制代码量明显减少开发效率更高也更容易维护。一句话总结SSM是“能学到框架原理”和“能快速完成功能开发”之间的一个平衡点。能把这个系统完整做下来Spring的IoC/AOP、SpringMVC的请求流程、MyBatis的动态SQL这些核心知识点基本都能串联起来。2. 数据库设计与核心表结构解析做商城类系统数据库设计是地基。地基没打好后面写代码会非常痛苦。这个项目的数据表我按业务模块拆成六张核心表和两张辅助表下面逐个说。2.1 用户、商品、分类这三张基础表用户表userCREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码MD5加密后存储, phone varchar(20) DEFAULT NULL COMMENT 手机号, address varchar(255) DEFAULT NULL COMMENT 收货地址, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码存储我强烈建议不要用明文。虽然很多人课设里图省事直接存了明文但这是个坏习惯。至少用MD5加盐或者直接用BCrypt项目里我用的是MD5加固定盐的方式安全性和实现复杂度都比较适中。商品分类表category和商品表product商品分类是树形结构的话为了简单起见这个系统只做了一级分类就是“蔬菜类”“水果类”“肉类”这种级别。如果你的项目想做得有亮点可以加一个parent_id字段做二级分类属于低成本的加分项。商品表字段设计上有几个容易踩坑的地方CREATE TABLE product ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 商品名称, category_id int(11) DEFAULT NULL COMMENT 分类ID, price decimal(10,2) NOT NULL COMMENT 单价, unit varchar(20) DEFAULT 斤 COMMENT 计量单位斤/份/个, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, image varchar(255) DEFAULT NULL COMMENT 商品图片URL, description text COMMENT 商品描述, status tinyint(1) DEFAULT 1 COMMENT 状态1上架 0下架, sales int(11) DEFAULT 0 COMMENT 销量, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里最值得注意的就是price字段一定要用decimal而不是float或double。浮点数在运算时会出现精度丢失比如0.1加0.2会得到0.30000000000000004这在涉及钱的场景里是绝对不能接受的。2.2 购物车、订单、订单项这三张业务表购物车表cart购物车有几种实现方案存Session、存Cookie、存数据库。这个项目用的是数据库存方案原因很简单Session存购物车用户一关浏览器就丢了Cookie存购物车有大小限制数据库存最持久也最好在后台查看数据。缺点是每次操作购物车都要查一次库但课设这个量级完全不用担心性能。CREATE TABLE cart ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 用户ID, product_id int(11) NOT NULL COMMENT 商品ID, quantity int(11) NOT NULL DEFAULT 1 COMMENT 数量, checked tinyint(1) DEFAULT 1 COMMENT 是否选中, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id,product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;user_id和product_id加唯一索引是为了避免同一个用户往购物车添加同一个商品时产生多条重复记录。正确逻辑是如果已经存在就增加quantity不存在就插入一行。订单表orders和订单项表order_item订单和订单项是典型的主从表结构一个订单包含多个商品条目。为什么订单项要单独一张表因为一个订单里可能有多个商品又不可能在订单主表里存一个“商品列表”字段所以就用子表存每个商品的快照信息。这里有一个非常关键的点订单项里要存“商品名称、单价、图片”这些冗余字段而不是只存product_id。因为商品信息后续可能改价、改名甚至下架但用户的订单历史里应该保留购买那一刻的原始快照。这是电商系统里的一个经典设计原则。CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id int(11) NOT NULL, total_price decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(1) DEFAULT 0 COMMENT 状态0待付款 1已付款 2已发货 3已完成 4已取消, receiver_name varchar(50) DEFAULT NULL, receiver_phone varchar(20) DEFAULT NULL, receiver_address varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;order_no订单编号必须是唯一且有一定规则的字符串。我采用的是“时间戳 用户ID 随机数”拼接的方式时间戳精确到毫秒基本上不会重复。3. 核心功能实现与实操详解3.1 SpringMVC请求流程与Controller层设计SpringMVC的请求流转逻辑是前端页面发送HTTP请求DispatcherServlet作为统一入口接收请求通过HandlerMapping找到对应的Controller方法执行完毕后返回ModelAndView或JSON数据。Controller层的类设计我按业务模块划分UserController注册、登录、退出、个人信息修改ProductController商品列表、商品详情、按分类查询、搜索CartController查看购物车、添加商品、修改数量、删除商品OrderController提交订单、订单列表、订单详情、取消订单、模拟支付一个典型的Controller方法长这样Controller RequestMapping(/cart) public class CartController { Autowired private CartService cartService; RequestMapping(/add) public String add(HttpSession session, int productId, int quantity) { User user (User) session.getAttribute(user); if (user null) { return redirect:/user/login; } cartService.addToCart(user.getId(), productId, quantity); return redirect:/cart/list; } }这里有个细节Controller层只负责接收参数、调用Service、返回视图或重定向实际的业务逻辑都放Service层处理。常见的分层是Controller - Service - MapperController层代码越薄越好。3.2 商品列表的分页查询与多条件检索商品列表页是整个商城访问量最高的页面分页是必须做的。MyBatis做分页有两种方式手动传limit offset参数在SQL里写limit #{offset}, #{pageSize}使用PageHelper插件PageHelper用起来更方便但它是通过拦截器在运行时改写SQL实现的原理上属于“魔法操作”。课设项目我建议用手动limit方式逻辑直白、答辩好解释select idfindPage resultTypecom.example.pojo.Product SELECT * FROM product where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if AND status 1 /where ORDER BY id DESC LIMIT #{offset}, #{pageSize} /select这段SQL里的动态where标签是MyBatis最实用的特性。有分类条件就拼接category_id有搜索关键词就模糊匹配什么都没有就查全部上架商品。三个条件自由组合不用写多条SQL特别适合商品筛选这种“可选条件多”的场景。3.3 购物车模块与库存扣减的事务处理购物车加入商品时要查一遍商品是否存在以及是否上架提交订单时涉及的操作就多了包括查询购物车中选中的商品检查库存是否充足扣减库存生成订单主记录生成订单明细记录清空购物车对应商品这六步操作必须全部成功或者全部失败不能出现“库存扣了但订单没生成”的尴尬情况所以要用事务。Spring的声明式事务只需要在Service方法上加一行注解Transactional(rollbackFor Exception.class) public Order submitOrder(Integer userId) { // 1. 查询购物车 // 2. 校验库存 // 3. 扣减库存 // 4. 创建订单 // 5. 创建订单项 // 6. 清空购物车 return order; }需要注意默认情况下Spring事务只在运行时异常RuntimeException时回滚检查异常Exception不会触发回滚。所以我要显式声明rollbackFor Exception.class保证任何异常都能回滚。库存扣减是并发场景下的重点。最简单安全的SQL是带条件的更新UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这个SQL利用数据库行锁保证了原子性只有库存充足时才扣减成功返回受影响行数。如果受影响行数为0说明库存不足直接抛出异常。这比“先select查库存再update扣减”的方式安全得多后者在并发情况下会出现超卖。4. 后台管理与前端页面实操4.1 管理员模块的职责划分后台管理功能主要包含管理员登录验证、商品管理、分类管理、订单管理。管理员和普通用户是两种不同的角色但这个系统的管理员表很简单相当于一个独立的admin用户表登录后Session里存一个标记来区分身份。为了防止普通用户通过地址直接访问后台页面需要写一个拦截器Interceptor。SpringMVC的拦截器可以在请求到达Controller之前做身份校验public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Admin admin (Admin) request.getSession().getAttribute(admin); if (admin null) { response.sendRedirect(request.getContextPath() /admin/login); return false; } return true; } }然后在SpringMVC配置里注册这个拦截器并设置路径拦截规则mvc:interceptors mvc:interceptor mvc:mapping path/admin/**/ mvc:exclude-mapping path/admin/login/ mvc:exclude-mapping path/admin/doLogin/ bean classcom.example.interceptor.AdminInterceptor/ /mvc:interceptor /mvc:interceptors这里有个坑如果用JSP做页面CSS、JS、图片这些静态资源默认也会被拦截器拦截所以还需要额外配置放行静态资源否则后台页面打开后样式全丢只剩一堆HTML文字。4.2 商品图片上传的两种实现思路商品管理模块里图片上传是个绕不开的需求。实现方案有两种方案一是传统方式在SpringMVC配置文件中配置MultipartResolver使用commons-fileupload组件处理文件上传把图片保存到服务器本地磁盘数据库里存图片的相对访问路径。方案二是相对偷懒的方式使用Base64编码直接把图片以字符串形式存到数据库或者使用第三方图床保存图片链接。这个方式实现起来简单但会撑大数据库不适合生产环境。我建议采用方案一。需要注意上传路径的配置如果把图片保存在IDEA工作目录下的某个文件夹那么重启IDE后文件可能被清除。更稳妥的方式是保存到系统的某个固定目录比如D:/upload/然后配置一个虚拟路径映射让Tomcat能把URL对应到这个磁盘目录bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value5242880/ /bean4.3 前端页面中JSP、EL表达式和JSTL的使用前台页面使用JSP EL表达式 JSTL标签库实现。JSP可以直接在页面里嵌套Java代码但那样页面会非常混乱正确的做法是用EL表达式获取后端存入request域的数据用JSTL标签做循环和判断。以首页商品列表为例c:forEach items${productList} varp div classproduct-card a href${pageContext.request.contextPath}/product/detail?id${p.id} img src${p.image} alt${p.name} /a h3${p.name}/h3 p classprice¥${p.price}/${p.unit}/p a href${pageContext.request.contextPath}/cart/add?productId${p.id}quantity1加入购物车/a /div /c:forEachEL表达式中的${productList}对应Controller里放进request域的名称。JSTL的c:forEach相当于Java的for-each循环把后端传来的List逐条渲染成HTML。c:if标签则用于条件判断比如判断登录状态显示不同的按钮。JSP开发中一个常见痛点是${pageContext.request.contextPath}几乎在每个链接里都要出现因为它能动态获取项目部署路径避免因为项目名变化导致路径404。我建议从第一行代码起就用这个表达式写路径不要图省事写死路径。5. 常见问题与排查技巧实录5.1 运行环境相关的典型报错这个项目从零搭建到最后跑通大概率会遇到以下这些问题我把解决办法整理成速查表报错现象根本原因解决办法启动Tomcat时端口被占用8080端口被其他程序占用修改Tomcat的server.xml配置文件端口号或用cmd命令netstat -ano找出占用进程并结束数据库连接报Communications link failureMySQL服务没启动或连接地址写错确认MySQL服务已启动检查jdbc.properties中url、用户名、密码是否正确页面中文乱码请求或响应编码不一致在web.xml中配置CharacterEncodingFilter过滤器设置编码为UTF-8检查JSP页面pageEncoding是否为UTF-8找不到Mapper接口或XML文件Mapper扫描路径配置错误检查Mapper接口的MapperScan注解路径和MyBatis的mapper-locations路径是否匹配数据库时区问题值得一提。如果JDBC连接串没指定serverTimezone在高版本MySQL下会报“The server time zone value”相关的错误。在jdbc.properties中数据库连接URL末尾加上?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf-8即可解决。5.2 框架配置类问题与排查思路SSM项目里配置问题占了调试工作的大头。最常见的一类问题是我的Mapper接口的包路径、Mapper XML文件路径、resultType的包路径不一致导致的运行报错。比如接口全限定名是com.example.mapper.UserMapper对应的XML文件放在resources/mapper/UserMapper.xml那么在MyBatis配置或Spring整合配置里如果mapper-locations配置的是classpath:mapper/*.xml但XML文件的namespace写成了别的Mapper接口的全限定名就会在注入UserMapper时报“Invalid bound statement (not found)”错误。排查思路是先确认namespace和接口的全限定名完全一致再看Mapper接口里方法名和XML里的id是否一致。还有一类容易踩坑的是LazyLoading懒加载问题。MyBatis开启懒加载后如果配置不当在Jackson序列化返回JSON时报“No serializer found for class org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory”之类的错误。报出这种问题一般建议关闭懒加载改成激进加载同时配置jackson依赖的序列化设置。5.3 业务逻辑层的隐藏Bug排查业务逻辑的Bug往往比环境问题更隐蔽。举几个典型例子。商品库存是整数但是生鲜场景下经常出现“0.5斤”这种购买数量如果quantity字段用int用户想买半斤白菜单就会出现数量丢失。我是在前端页面限制数量必须是整数后台再校验一次同时把quantity字段设置成整数配合商品的计量单位字段来区分“份”“斤”等不同售卖方式。商品表里虽然有unit字段但目前系统还未支持小数购买的场景。购物车商品数量校验问题。很多人加了购物车发现数量能一直往上加加到超过库存了也没有任何提示。这是因为CartService里添加购物车时没查库存。合理的校验是加入购物车时查询当前商品在购物车里的数量加上新加数量不能超过库存量。订单取消后的库存回补。用户下单扣减库存后如果在“待付款”状态取消了订单库存没有加回来会导致系统里的商品越来越少直到售罄。所以在取消订单的Service方法里除了更新订单状态还要遍历订单项、恢复商品库存。6. 项目部署与运行经验补充6.1 从IDEA到生产环境的部署要点开发环境下项目在IDEA里点一下Run就能跑。但从“能跑”到“能给别人演示、递交项目答辩”还有一段距离。项目部署前需要把数据库导出成SQL文件确保对方导入后表结构和初始数据完整。这个SQL文件里建表和插入初始数据都要有商品分类、几个测试用户、十几条商品样例数据都是必须的。否则对方一运行页面空空如也观感很差。导出SQL要注意编码问题。用Navicat导出时选择“包含建表语句”和“包含数据”导出前确认字符集是utf8mb4。数据里有中文如果字符集选错导入后就是一堆乱码。导出项目的时候只提交源代码和SQL文件就行了。target目录、.idea目录、*.iml文件这些IDE生成物不需要交对方导入后会自动生成。如果用了本地绝对路径比如图片上传路径写死D:/upload建议在说明文档里写清楚。6.2 给毕设答辩准备的系统亮点如果这是毕设项目答辩时不能只讲“我做了个商城”要有几个能展开讲的技术亮点。第一个亮点是事务控制。提交订单整个流程涉及多步数据库操作用Spring声明式事务保证原子性。讲到这可以提一句库存扣减用了行锁和条件更新防止超卖这会显得你考虑到了并发问题。第二个亮点是权限拦截。管理员功能和用户功能分离用SpringMVC拦截器做登录和角色校验未登录的用户无法访问后台管理模块。第三个亮点是MyBatis动态SQL。多条件商品检索用到了动态where标签支持分类、关键词、上下架状态任意组合查询做到了一个方法应对多种筛选场景。第四个亮点是数据库设计的冗余策略。订单项中保存商品快照避免因商品信息变化影响历史订单。这是电商系统的经典设计思路讲到会加分。6.3 我踩过的几个最深的坑最后说几个我实际操作过程中真实踩过的坑希望看到这篇博文的人能少走弯路。第一个坑是Maven依赖冲突。Spring、SpringMVC、MyBatis整合时如果版本号不统一容易报ClassNotFoundException或者NoSuchMethodError。建项目时不要去网上随便复制别人的依赖坐标要确保Spring核心包的版本统一。第二个坑是日志配置缺失导致排查困难。SSM项目如果不配log4j或logback报错信息会非常有限出了错也不知道在哪里。配好日志后SSM框架的SQL执行、异常堆栈都能打印出来调试效率提升一个档次。第三个坑是前端JSP页面改完不生效。浏览器缓存会命中旧页面双击运行之后看到的效果和代码不一致。解决方法是浏览器按CtrlF5强制刷新或者在JSP页面head区域添加禁用缓存的meta标签。第四个坑是测试数据质量。商品图片尽量引用网络图片或本地真实图片不要所有商品共用一个占位图。商品描述和价格也要合理比如不能出现“白菜单价9999元”这种离谱数据。演示效果好不好很大程度上取决于初始数据的质量。
返回列表