简介这是一套面向高校计算机专业毕业设计的Java Web完整项目源码基于SSMSpringSpringMVCMyBatis框架配合MySQL数据库与Bootstrap前端实现了一个珠宝商城购物网站。项目采用Eclipse工具开发适合正在准备毕设、需要参考完整业务实现或进行二次开发的学生与开发者。压缩包共276个文件约15.37MB包含48个Java源文件与48个class文件、46个jar依赖、25个xml配置、19个jsp页面以及css、js、jpg等前端与静态资源并附有数据库脚本覆盖从后端逻辑到页面展示的完整链路。功能上涵盖登录验证、会员管理、商品类别与商品信息管理、订单信息管理及售后、购物车等模块Servlet与Service分层清晰便于理解SSM整合流程与MVC分层思想。目前已有90人学习下载可作为毕设选题参考、课程设计模板或SSM框架练手案例帮助快速搭建可运行环境并梳理业务模块间的调用关系。1. 珠宝商城毕设选 SSM 这套老架构到底图什么每年毕业季做 JavaWeb 毕设的同学十有八九会撞上同一个选题电商类网站。珠宝商城又是其中比较讨巧的一个——业务不复杂、页面好看、答辩时老师一眼就能看懂你在做什么。但真正动手时第一个卡住的地方往往不是业务而是技术选型SpringBoot 满天飞为什么还有人坚持用 SSM答案很现实SSMSpring SpringMVC MyBatis是大多数高校 JavaWeb 课程的教学主线环境依赖少、分层清晰、代码量大但不玄学答辩老师看得懂你自己也讲得清。再配上 MySQL 存数据、Bootstrap 做前端一套珠宝商城购物网站就能跑起来。这篇笔记就按「珠宝商城 SSM MyBatis MySQL Bootstrap」这条线把从建库到下单的完整落地路径拆开讲适合正在做毕设、想少走弯路的同学也适合想拿一个完整 JavaWeb 案例练手的初学者。珠宝商城的核心业务其实就四块用户注册登录、商品浏览与详情、购物车、下单结算。听起来简单但每一块背后都牵扯到数据库设计、MyBatis 映射、Spring 事务、前端交互。很多同学代码写了一半发现表结构不对回头改 SQL 改到崩溃这就是没在动手前把数据模型想清楚。SSM 的好处在于它逼你把 Controller、Service、Mapper 三层分明白坏处也在这——分层一多配置一错就报一堆看不懂的异常。所以这篇不堆概念直接从建库开始一步步把能跑的东西搭出来。提示毕设选题确定后先把需求文档里的功能点列成表再反推数据库字段比先写代码后补表要省至少一半返工时间。2. 珠宝商城数据库怎么设计从 ER 到建表脚本2.1 五张核心表撑起整个商城珠宝商城的数据库不需要太复杂五张表就能覆盖毕设答辩的所有功能点用户表、商品表、商品分类表、购物车表、订单表。订单表下面再挂一张订单明细表一共六张。设计时最容易翻车的地方是金额字段类型——千万别用 float 或 double用 decimal(10,2)否则结算时会出现 0.10.20.30000000000000004 这种经典问题答辩时被老师问一句就尴尬了。商品表里要留一个stock库存字段和一个status上下架状态字段这两个字段在购物车和下单逻辑里会反复用到。分类表用自关联或者单独一张表都行毕设建议单独一张category表字段少、查询简单。购物车表要记录user_id、product_id、quantity下单时根据购物车生成订单明细。订单表要有订单号、总金额、收货信息、订单状态订单号建议用时间戳加随机数生成不要用自增 ID 直接暴露给前端。下面是一段可直接执行的建表脚本字符集统一用 utf8mb4避免珠宝名称里的特殊符号乱码CREATE DATABASE jewelry_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE jewelry_mall; -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, phone VARCHAR(20), address VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort INT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, image VARCHAR(255), description TEXT, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 购物车表 CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(50) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, receiver VARCHAR(50), phone VARCHAR(20), address VARCHAR(200), status TINYINT DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段脚本里几个关键点值得展开说。price和total_amount用DECIMAL(10,2)表示最多 10 位数字、其中 2 位小数珠宝价格从几百到几十万都能存下。status字段用TINYINT而不是VARCHAR查询和判断都更快前端传 0/1 也方便。外键约束在毕设里建议加上答辩时能体现你懂关系型数据库的完整性约束但要注意插入顺序——先插分类再插商品否则外键会报错。字符集用utf8mb4而不是utf8因为珠宝描述里可能出现特殊符号或 emojiutf8存不下会直接报错。2.2 初始化数据与自增主键的坑建完表要插一些测试数据否则前端页面全是空白答辩演示时很尴尬。分类表插「戒指、项链、手镯、耳环」四条商品表插十几条带图片路径的假数据用户表插一个测试账号。这里有个血泪经验自增主键在删除数据后不会重置比如你删了 id1 的商品下一条插入的还是 id2 往后走。毕设演示时如果反复删改数据id 会变得很大虽然不影响功能但看起来不整洁。可以在测试阶段用TRUNCATE TABLE product清空并重置自增但注意TRUNCATE会连带清空外键关联生产环境别这么干。INSERT INTO category (name, sort) VALUES (戒指, 1), (项链, 2), (手镯, 3), (耳环, 4); INSERT INTO product (name, category_id, price, stock, image, description, status) VALUES (18K金钻石戒指, 1, 3999.00, 10, /images/ring1.jpg, 经典六爪镶嵌适合求婚, 1), (铂金素圈对戒, 1, 2599.00, 20, /images/ring2.jpg, 简约款日常佩戴, 1), (天然翡翠项链, 2, 8999.00, 5, /images/necklace1.jpg, A货翡翠附证书, 1), (珍珠手链, 3, 1299.00, 15, /images/bracelet1.jpg, 淡水珍珠7-8mm, 1); INSERT INTO user (username, password, phone, address) VALUES (test, 123456, 13800138000, 北京市朝阳区某街道);插入数据时注意image字段存的是相对路径前端 Bootstrap 页面里用img src${product.image}直接引用。如果图片放在webapp/static/images目录下路径要写对否则页面显示裂图。测试账号的密码在毕设里可以直接存明文但如果你想在答辩时加分可以提一句「实际项目会用 BCrypt 加密」然后展示一下加密后的字符串长什么样老师一般会点头。注意MySQL 8.0 默认认证插件是caching_sha2_password老版本的 JDBC 驱动可能连不上报Unable to load authentication plugin。解决办法是在连接串里加?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue或者把用户认证方式改成mysql_native_password。3. MyBatis 映射文件怎么写从 XML 到动态 SQL3.1 商品查询的三种映射写法MyBatis 是 SSM 里最容易出彩也最容易翻车的部分。出彩是因为 XML 里写动态 SQL 很灵活翻车是因为标签拼错一个字母就报BindingException。珠宝商城里最典型的查询是「按分类查商品」和「按关键词搜商品」这两种都需要动态 SQL。先看 Mapper 接口public interface ProductMapper { ListProduct selectByCondition(Param(categoryId) Integer categoryId, Param(keyword) String keyword, Param(offset) Integer offset, Param(limit) Integer limit); int countByCondition(Param(categoryId) Integer categoryId, Param(keyword) String keyword); Product selectById(Param(id) Integer id); int updateStock(Param(id) Integer id, Param(quantity) Integer quantity); }接口里用Param注解给参数起名XML 里就能用#{categoryId}引用。如果不加ParamMyBatis 会把参数当成param1、param2或者arg0、arg1XML 里写错名字就报错。这是新手最常见的坑之一记住多参数方法一律加Param。对应的 XML 映射文件mapper namespacecom.jewelry.mapper.ProductMapper select idselectByCondition resultTypecom.jewelry.entity.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 create_time DESC LIMIT #{offset}, #{limit} /select select idcountByCondition resultTypeint SELECT COUNT(*) 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 /select update idupdateStock UPDATE product SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity} /update /mapperwhere标签会自动处理第一个AND如果所有if都不成立它不会生成WHERE关键字避免 SQL 语法错误。LIKE CONCAT(%, #{keyword}, %)是 MySQL 里拼接模糊查询的标准写法比在 Java 里拼字符串安全能防 SQL 注入。updateStock里加了AND stock #{quantity}条件这是防超卖的关键——如果库存不够更新影响行数为 0Service 层根据返回值判断是否下单成功。3.2 MyBatis 二级缓存与配置打印热词里有人搜「mybatis二级缓存实现」和「mybatis配置打印」这两个在毕设里都能用上。二级缓存默认是关闭的要在 XML 里加cache/标签并且实体类实现Serializable接口。珠宝商城里商品详情页适合开二级缓存因为同一个商品被反复查看的概率高。但购物车和订单相关查询千万别开缓存否则用户加了商品看不到最新数据答辩演示时直接翻车。cache evictionLRU flushInterval60000 size512 readOnlytrue/evictionLRU表示最近最少使用淘汰flushInterval60000表示 60 秒刷新一次size512是最多缓存 512 个对象。readOnlytrue表示只读缓存性能好但要求对象不可变毕设里够用。配置打印更简单在 MyBatis 配置文件里加一行settings setting namelogImpl valueSTDOUT_LOGGING/ /settings这样控制台会打印每条执行的 SQL 和参数调试时非常有用。但答辩演示前记得关掉否则控制台刷屏老师看不清你的页面操作。另一个常见需求是「mybatis xml高亮」IDEA 里如果 XML 没有颜色检查文件头有没有加!DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd加上之后就有语法高亮和自动补全了。提示MyBatis 的#{}和${}区别一定要搞清楚。#{}是预编译参数能防注入${}是字符串拼接只在动态表名、动态排序字段时用用错地方就是安全漏洞。毕设答辩如果被问到能说清这一点很加分。4. Spring 事务与下单逻辑库存扣减怎么不翻车4.1 下单流程的 Service 层实现下单是珠宝商城里最复杂的业务涉及订单表插入、订单明细插入、库存扣减、购物车清空四个操作必须放在同一个事务里。Spring 的Transactional注解加在 Service 方法上任何一步抛异常就整体回滚。先看代码Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private OrderItemMapper orderItemMapper; Autowired private ProductMapper productMapper; Autowired private CartMapper cartMapper; Override Transactional(rollbackFor Exception.class) public String createOrder(Integer userId, ListInteger cartIds) { // 1. 查询购物车项 ListCart cartList cartMapper.selectByIds(cartIds); if (cartList null || cartList.isEmpty()) { throw new RuntimeException(购物车为空); } // 2. 计算总金额并扣库存 BigDecimal total BigDecimal.ZERO; for (Cart cart : cartList) { Product product productMapper.selectById(cart.getProductId()); if (product null || product.getStatus() 0) { throw new RuntimeException(商品已下架 cart.getProductId()); } int rows productMapper.updateStock(cart.getProductId(), cart.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足 product.getName()); } total total.add(product.getPrice().multiply(new BigDecimal(cart.getQuantity()))); } // 3. 插入订单 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 4. 插入订单明细 for (Cart cart : cartList) { OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setProductId(cart.getProductId()); item.setPrice(productMapper.selectById(cart.getProductId()).getPrice()); item.setQuantity(cart.getQuantity()); orderItemMapper.insert(item); } // 5. 清空购物车 cartMapper.deleteByIds(cartIds); return order.getOrderNo(); } private String generateOrderNo() { return System.currentTimeMillis() String.valueOf((int)(Math.random() * 9000 1000)); } }这段代码有几个关键设计。第一Transactional(rollbackFor Exception.class)里的rollbackFor必须写因为 Spring 默认只对RuntimeException回滚如果抛的是受检异常就不会回滚这是经典面试题也是实际踩坑点。第二库存扣减用updateStock的返回值判断而不是先查库存再更新这样在并发下也不会超卖——SQL 的WHERE stock #{quantity}是原子操作。第三订单号用时间戳加随机数保证唯一性的同时不暴露自增 ID。4.2 事务失效的三种典型场景Transactional不是万能的以下三种情况它会失效毕设答辩如果被问到能答上来说明你真动手跑过。第一种方法不是public的Spring AOP 代理不会拦截私有方法。第二种同类内部方法直接调用比如createOrder里调了this.otherMethod()走的是原始对象而不是代理对象事务不生效。第三种异常被 catch 了没往外抛事务管理器感知不到异常自然不会回滚。// 错误示范异常被吞掉事务不回滚 Transactional public void wrongMethod() { try { orderMapper.insert(order); int i 1 / 0; // 抛异常 } catch (Exception e) { e.printStackTrace(); // 异常被吃掉事务提交 } }正确的做法是让异常抛出去或者在 catch 里手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。毕设里建议直接用第一种简单可靠。另外MySQL 的表引擎必须是 InnoDB 才支持事务MyISAM 不支持建表时ENGINEInnoDB别写错。注意下单接口在演示时如果反复点击可能生成多笔订单。前端可以用按钮禁用防重复提交后端可以用 Redis 或数据库唯一索引做幂等但毕设里最简单的办法是下单成功后跳转到订单列表页用户看不到下单按钮就不会重复点。5. Bootstrap 前端页面与 SSM 数据对接的避坑清单5.1 商品列表页的分页与图片懒加载Bootstrap 做珠宝商城的前端很合适栅格系统排商品卡片整齐模态框做登录注册也方便。商品列表页用row加col-md-3一行放四个商品每个卡片里放图片、名称、价格和「加入购物车」按钮。分页用 Bootstrap 的pagination组件后端传pageNum和pageSize前端拼 URL 跳转。div classrow div classcol-md-3 th:eachproduct : ${pageInfo.list} div classcard mb-3 img th:src${product.image} classcard-img-top alt商品图片 loadinglazy div classcard-body h5 classcard-title th:text${product.name}/h5 p classcard-text text-danger th:text¥ ${product.price}/p button classbtn btn-primary btn-sm th:onclickaddToCart( ${product.id} )加入购物车/button /div /div /div /divloadinglazy是浏览器原生的图片懒加载属性商品多了之后能明显提升首屏速度。th:each是 Thymeleaf 的循环语法如果毕设用的是 JSP换成c:forEach就行。价格显示用¥ ${product.price}拼接注意DECIMAL类型传到前端会变成字符串显示没问题但参与计算时要转回数字。5.2 避坑清单五个让毕设演示翻车的细节现象一页面中文乱码。原因数据库字符集、JDBC 连接串、Tomcat 编码三者不一致。解决数据库用utf8mb4JDBC 连接串加useUnicodetruecharacterEncodingutf8web.xml里配CharacterEncodingFilter强制 UTF-8。现象二静态资源 404。原因SpringMVC 的DispatcherServlet拦截了所有请求包括 css、js、图片。解决在spring-mvc.xml里加mvc:resources mapping/static/** location/static//或者用mvc:default-servlet-handler/交给默认 Servlet 处理。现象三MyBatis 报Invalid bound statement。原因Mapper XML 的namespace和接口全限定名不一致或者mybatis-config.xml里没配mapper扫描路径。解决检查namespace是否一字不差mappers里用package namecom.jewelry.mapper/批量扫描。现象四事务不回滚。原因表引擎是 MyISAM或者异常被 catch 没抛出或者方法不是 public。解决建表用 InnoDB异常往外抛Transactional加在 public 方法上。现象五下单后库存没变。原因updateStock的 SQL 写成了SET stock #{quantity}而不是SET stock stock - #{quantity}或者 Service 层没调用更新方法。解决检查 SQL 逻辑在控制台看 MyBatis 打印的 SQL 和参数确认更新语句执行了。提示毕设演示前把数据库连接池配好c3p0或druid都行别用DriverManager每次新建连接页面一多就卡。Druid 自带监控页面答辩时打开给老师看连接池状态是个不错的加分项。6. 从能跑到能答辩三个验证技巧和我的习惯代码跑起来只是第一步答辩时老师会问「你怎么保证数据一致性」「并发下单会怎样」「这个功能能不能演示一下」。所以最后一章不讲新功能讲怎么验证你已经做的东西经得起问。第一个技巧是用 JMeter 或 Postman 并发调下单接口开 10 个线程同时下单同一件库存为 5 的商品看最终库存是不是 0、订单是不是只生成了 5 笔。如果超卖了说明updateStock的WHERE stock #{quantity}没写对或者事务没生效。这个测试做一遍答辩时被问并发就有底气。第二个技巧是手动制造异常验证事务回滚。在createOrder的插入订单明细之后加一行int i 1 / 0;然后调接口看订单表和订单明细表是不是都没有新增数据、库存有没有被扣。如果订单表多了数据但明细表没有说明事务没配好。验证完记得把这行删掉别留在最终代码里。第三个技巧是用 Navicat 或 MySQL Workbench 直接查数据。毕设演示时前端页面可能因为缓存或分页看不到最新数据直接开数据库客户端查orders和order_item表用数据说话。热词里有人搜「navicat for mysql 破解安装」其实毕设用社区版 MySQL Workbench 就够了免费且功能全连接串配好就能用。验证项工具预期结果不通过时先查并发下单不超卖Postman 并发库存归零订单数等于库存数updateStock 的 WHERE 条件事务回滚手动抛异常订单、明细、库存全部不变Transactional 配置和表引擎分页查询浏览器翻页每页固定条数总数正确LIMIT 参数和 count 查询中文不乱码插入中文商品名页面和数据库都显示正常字符集和编码过滤器静态资源加载浏览器 F12css/js/图片 200mvc:resources 配置最后说个我自己的习惯每做完一个模块先不写下一个而是把当前模块的增删改查在 Postman 里全跑一遍确认接口都通再动前端。很多同学喜欢先把所有页面画出来再写后端结果前端调接口时发现字段对不上回头改数据库改到崩溃。SSM 这套东西不复杂复杂的是细节多配置、映射、事务、编码任何一个环节出错都表现为「页面报 500」所以排查时从控制台日志往下追别瞎猜。珠宝商城毕设做完你对 JavaWeb 的完整链路就有感觉了后面换 SpringBoot 只是换个配置方式核心思想没变。希望帮到你。本文还有配套的精品资源点击获取