基于SSM的眼镜店销售管理系统:从SKU到订单的库存扣减与事务实践 📅 发布时间:2026/9/17 19:30:11 👁 浏览次数: 简介这是一套基于SpringSpringMVCMyBatisMavenMySQL的眼镜店销售管理系统毕业设计项目面向需要完成商城类毕设的高校学生与Java初学者。系统完整实现用户注册登录、眼镜个性推荐、购物车、立即购买、订单管理、积分充值、申请退款、留言板、客服、轮播图管理、眼镜上下架与库存调整、用户及公告管理等业务前端采用HTMLVue后端分层清晰可在Eclipse或IDEA中直接运行。资源包共843个文件大小35MB以Java源码、Vue组件、JavaScript脚本、HTML/CSS页面、SVG图标、XML配置等源码和界面资源为主另含SQL数据库脚本、毕业论文、答辩PPT及演示视频音频等材料并附有install、run、build启动脚本便于快速部署。目前已有72人学习浏览内容完整度较高能帮助读者免去从零搭建项目和撰写文档的精力适合用作毕业设计参考、课程设计实践或商城系统二次开发基础。1. 眼镜店系统为什么要单独做 SKU 管理消费品行业里眼镜是比较特殊的一类同样一款镜架折射率、度数、散光轴位不一样就对应不同库存如果把眼镜当成普通商品直接挂数量盘点时会对不上。这套基于 spring springmvc mybatis maven mysql 的眼镜店销售管理系统把商品属性拆成镜架、镜片参数和库存三个维度再通过购物车、订单、积分、退款和后台库管理串成一条完整业务闭环。系统覆盖管理员和普通用户两类角色管理员负责眼镜上下架、增减库存、积分记录和轮播图维护用户端则包含注册登录、个性推荐、留言、充值、下单、退款、收货地址管理等模块。对准备做毕设、或者想找一套 SSM 分层规范的电商类项目做参考的开发者来说它的价值在于工程结构清晰、表关系完整值得拆开看一遍。2. Maven 与 SSM 整合工程结构、依赖配置与启动链路2.1 为什么选 Spring SpringMVC MyBatis 而不是 Spring Boot现在新项目普遍直接用 Spring Boot但很多院校毕设和企业遗留系统仍然是 SSM 分层。这套系统坚持用 spring springmvc mybatis maven mysql好处在于每一层职责非常明确适合用来理解 Java Web 的传统请求链路JSP 或静态页面发起请求DispatcherServlet 根据映射找到 ControllerService 处理业务Mapper 接口由 MyBatis 代理生成实现类最终操作 MySQL。对比 Spring Boot 的自动装配SSM 的配置是显式的所有 bean 的创建、依赖注入和事务管理都能在 XML 里看到排查问题时更容易定位是哪一层出了问题。Maven 在这里承担的是依赖管理和构建编排。项目根目录下提供了 1-install.bat、2-run.bat、3-build.bat 三个脚本分别对应依赖安装、启动运行和打包构建。在 pom.xml 里需要重点关注四类依赖spring-webmvc 提供 MVC 框架mybatis 与 mybatis-spring 负责 ORM 映射mysql-connector-java 驱动连接数据库druid 或 dbcp 连接池管理数据库连接。如果本地 Maven 仓库没有这些依赖1-install.bat 会执行mvn clean install拉取。pom.xml 中核心片段如下properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target spring.version5.2.8.RELEASE/spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency /dependencies这里spring-webmvc会连带引入 spring-context、spring-aop 等基础依赖不需要重复声明。maven.compiler.source和target指定编译版本为 JDK 1.8避免在 JDK 8 以上的环境下出现默认源码版本过低导致的编译告警。mysql-connector-java版本 5.1.x 对应 MySQL 5.7 是最稳的搭配如果换成 8.x 驱动需要同步调整驱动类名为com.mysql.cj.jdbc.Driver并且数据库 URL 里要额外指定时区参数。2.2 spring-mybatis.xml 配置与 bean 装配SSM 整合的核心在 spring-mybatis.xml它负责把数据源、SqlSessionFactory、Mapper 接口扫描和事务管理器串起来。数据源使用 druid 连接池连接参数从 jdbc.properties 里读取避免硬编码。一个常见的问题是数据库 URL 中如果不加characterEncodingutf8插入中文数据会出现乱码另外 MySQL 5.7 默认的事务隔离级别为 REPEATABLE READ读写并发高时要考虑锁等待超时时间设置。context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.glasses.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.glasses.dao/ /beanmapperLocations指定 MyBatis 映射文件所在路径typeAliasesPackage让 XML 里写 resultType 时可以直接用类名首字母小写不用写全限定名。MapperScannerConfigurer会自动扫描 dao 包下的接口并注册成 Mapper 代理 bean所以 Service 注入 mapper 时不需要手动写 Bean。注意不要重复扫描basePackage 范围过大会把无关接口也注册进去启动时会报 TypeException 或 Invalid bound statement 错误。2.3 三个 bat 脚本的执行顺序与作用项目根目录下的 1-install.bat、2-run.bat、3-build.bat 是给用户快速上手的入口它们在 Windows 环境下直接双击即可执行。1-install.bat 内部执行 Maven 依赖安装和数据库脚本导入前的环境检查2-run.bat 调用 Tomcat 插件启动 web 容器3-build.bat 则执行mvn package生成 war 包。一个容易踩的坑是 2-run.bat 默认使用tomcat7-maven-plugin如果你的 JDK 版本高于 1.8 且没有切换编译参数访问项目首页时会出现 500 错误错误日志中常见的提示是 UnsupportedClassVersionError。这时候不要急着改代码先检查本地 Maven 的 settings.xml 中 jdk 配置是否和 pom.xml 的maven.compiler.source一致。Maven 默认使用 JDK 1.5 编译的坑很多人遇到过配置不对时编译产物和运行环境不一致排错时间远大于修复时间。3. 数据库设计与权限控制订单、积分、库存的表结构逻辑3.1 核心表关系与 E-R 映射这套系统的数据表设计围绕用户与商品展开核心表包括 user、glasses、cart、orders、order_item、address、points_record、banner、notice、message。其中 user 与 orders 是一对多关系orders 与 order_item 是一对多关系glasses 与 cart 是一对多关系user 与 points_record 是一对多关系。管理员账号和普通用户共存在 user 表中通过 role 字段区分没有单独建管理员表这样登录鉴权时就一条 SQL 搞定不用多表 join。眼镜表的字段设计上除了基础的价格、图片、描述外还应该包含库存字段 stock 和上下架状态 status。admin 后台的「下架」「增加库存」「减少库存」操作本质就是 update glasses 语句。一个值得注意的细节是库存字段应该用 int 而非 string因为 MyBatis 返回 resultMap 时如果实体类的 stock 字段是 String前端拿到后会做字符串拼接1和10会出现排序错误。建议所有数值字段在数据库层就固定用 int 或 decimal。CREATE TABLE glasses ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, price decimal(10,2) NOT NULL, stock int(11) NOT NULL DEFAULT 0, status tinyint(1) NOT NULL DEFAULT 1, click_count int(11) NOT NULL DEFAULT 0, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的status字段 1 代表上架、0 代表下架click_count记录点击次数。用户查看眼镜详情时Controller 里应该执行UPDATE glasses SET click_count click_count 1 WHERE id #{id}这是个性推荐的数据来源之一。注意不要在查询详情时用SELECT ... FOR UPDATE来更新点击量锁粒度太粗高并发下会阻塞正常的商品浏览。3.2 用户登录鉴权与密码加密方式系统分为管理员和普通用户两套操作界面登录后的权限校验在拦截器层完成。常见的做法是定义一个 LoginInterceptor在 preHandle 中从 session 中取出登录用户判断 role 是否匹配请求路径前缀。比如/admin/**开头的请求要求 roleadmin普通用户访问时直接重定向到登录页。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.html); return false; } if (request.getRequestURI().startsWith(request.getContextPath() /admin/)) { if (!admin.equals(user.getRole())) { response.sendError(403); return false; } } return true; }密码存储上demo 项目里很多直接用明文但实际部署时建议用 MD5 加盐或者 BCrypt。如果系统里已经有注册功能注册时对密码做一次加密再入库登录时把用户输入的密码做同样处理再比对。这里要理解 MyBatis 的参数传递机制接口方法上有多个参数时需要加 Param 注解否则 MyBatis 会以 arg0、arg1 作为占位符访问SQL 里写 #{username} 会找不到对应的参数导致 BindingException。3.3 MyBatis 缓存机制对学校项目的意义热词里反复出现 mybatis 缓存和一级缓存二级缓存这套系统的查询场景正好能验证缓存效果。MyBatis 一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 中执行两次相同的 select 会命中缓存。但每次操作数据库都用到新的 session 时一级缓存就形同虚设。二级缓存是 mapper 级别的需要在 XML 中显式开启cache/并且实体类要实现 Serializable 接口否则抛异常。对于眼镜店这种读取量大、写入量相对小的场景可以在眼镜查询的 mapper XML 里开启二级缓存rollback 和更新时 MyBatis 会自动清空相关缓存区域。但积分记录、订单这类高频写入的表不要开启缓存否则刚插入的记录查不到前端会显示数据没更新排查半天发现是缓存问题。mapper namespacecom.glasses.dao.GlassesDao cache evictionLRU flushInterval60000 size512 readOnlyfalse/ select idfindById resultTypeGlasses useCachetrue SELECT * FROM glasses WHERE id #{id} /select /mappereviction指定回收策略为 LRUflushInterval为 60 秒刷新一次size最多缓存 512 个对象readOnly为 false 表示返回对象可以被修改。实用角度讲毕设答辩时被问到「缓存如何防止数据不一致」直接回答「更新操作时会调用 clearCache 或者 commit 触发清空」就可以了。4. 购物车到订单的完整链路库存扣减、积分核算与状态流转4.1 加入购物车与立即购买的区别系统在商品详情页同时提供了「加入购物车」和「立即购买」两个入口。加入购物车是把商品写入 cart 表用户可以在购物车页面合并结算立即购买则是跳转到 confirm 页面并携带当前商品 id 和数量提交订单时只处理这一件商品。两者的差异在数据库操作上有体现购物车场景需要先查询当前用户是否已存在相同商品的购物车记录有则 update 数量加 1没有则 insert 新记录。public void addToCart(Integer userId, Integer glassesId, Integer count) { Cart cart cartMapper.findByUserIdAndGlassesId(userId, glassesId); if (cart null) { Cart newCart new Cart(); newCart.setUserId(userId); newCart.setGlassesId(glassesId); newCart.setCount(count); cartMapper.insert(newCart); } else { cart.setCount(cart.getCount() count); cartMapper.update(cart); } }这里没有用INSERT ... ON DUPLICATE KEY UPDATE因为 cart 表没有对 user_id 和 glasses_id 建联合唯一索引。如果要改可以在建表时加上UNIQUE KEY uk_user_glasses (user_id, glasses_id)这样一条 SQL 就能替代上述查询再更新的两步操作并发时也不会出现重复记录。商品库存上限的判断应该放在这里count 超过库存就提示「库存不足」不要等到提交订单再校验用户体验差且浪费数据库操作。4.2 提交订单中的事务边界与库存防超卖订单提交是这套系统里最需要关注事务一致性的操作涉及三步向 orders 表插入订单主记录向 order_item 插入商品明细更新 glasses 表扣减库存。三步必须在一个事务里执行任何一步失败都要回滚。实现方式是给 Service 方法加 Transactional(rollbackFor Exception.class)注意默认只回滚 RuntimeExceptionchecked exception 不会回滚所以方法中捕获异常后要手动抛出 RuntimeException 或者在注解里明确 rollbackFor。库存扣减是高并发场景的超卖风险点。常见的错误是先查询库存是否足够、再执行 update 扣减这个流程在并发下会超卖。正确做法是直接执行条件更新Transactional(rollbackFor Exception.class) public Boolean submitOrder(OrderDTO dto) { int rows glassesMapper.reduceStock(dto.getGlassesId(), dto.getCount()); if (rows 0) { throw new RuntimeException(库存不足扣减失败); } orderMapper.insert(dto.getOrder()); return true; }对应的 mapper XML 中 SQL 如下update idreduceStock UPDATE glasses SET stock stock - #{count} WHERE id #{glassesId} AND stock #{count} /updatestock stock - #{count}是原子操作AND stock #{count}保证扣减后库存不为负。reduceStock返回的影响行数 rows 等于 0 时说明库存不足直接抛异常回滚事务。这是防超卖最简单有效的写法比 select for update 的性能损耗小比分布式锁的实现成本低非常适合中小型系统的业务体量。4.3 订单状态机与申请退款的处理逻辑订单表里需要有一个 status 字段表示当前状态一般用 int 类型配合常量类定义。常见状态0 待付款、1 待发货、2 待收货、3 已完成、4 已退款。用户在前端点击「申请退款」时要把状态从待付款或待发货迁移到退款中管理员后台审核后才会真正更新为已退款并把积分回退。模拟这些状态流转时建议在订单详情的查询 mapper 中用 resultMap 做关联映射把 order 与 order_item 一次性查出来避免 N1 次查询。resultMap idOrderDetailMap typeOrder id propertyid columnid/ result propertyorderNo columnorder_no/ result propertystatus columnstatus/ collection propertyitems ofTypeOrderItem id propertyid columnitem_id/ result propertyglassesName columnglasses_name/ result propertyprice columnitem_price/ /collection /resultMapcollection中的columnitem_id对应 order_item 表的主键propertyitems对应 Order 实体类中的ListOrderItem items字段。当订单包含多条明细时MyBatis 会根据 id 列去重合并不需要在 Java 代码里做额外处理。注意如果查询 SQL 里用了别名resultMap 中 column 必须和别名一致否则映射不到值数据明明存在却显示 null。4.4 积分核算与充值逻辑积分体系通常与充值和消费关联充值金额按比例换算成积分下单时抵扣或累计。user 表应该包含 amount 余额字段和 points 积分字段。充值操作的逻辑是 update user set amount amount #{value} where id #{userId}同时往 points_record 插入一条积分变动记录。积分记录的字段设计成 type 字段区分增加还是减少description 记录变更原因。用户在前端个人中心查看积分记录时直接按 create_time 倒序查即可。注意查询积分记录时如果使用 MyBatis 的二级缓存刚插入的记录会查不到因为 insert 操作默认不会触发二级缓存刷新。这时候要么在 mapper XML 中把 flushCache 设置为 true要么在插入记录后手动调用 SqlSession 的 clearCache 方法。5. 部署运行、演示模式与常见坑的排查姿势以这套眼镜店销售管理系统为例部署时踩过最多的坑集中在 MySQL 版本、Tomcat 端口、Maven 仓库和前端资源路径四块。数据库要求 5.7 版本是合理的很多开源项目在 MySQL 8.0 上跑不起来原因是 8.x 默认的认证插件是 caching_sha2_password老版本驱动不识别。解决办法是安装 MySQL 5.7 或者建库建用户时用mysql_native_password认证插件。如果手头只有 MySQL 8.0可以执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码然后再重启服务连接测试。Tomcat 默认端口 8080 经常被占用启动失败时不要急着改代码命令行执行netstat -ano | findstr 8080找到占用进程 PID到任务管理器结束后台 Java 进程再重新执行 2-run.bat。如果看不到任何启动日志直接闪退用 cmd 手动运行脚本窗口停留的错误信息就是排查线索。系统首页加载时样式丢失优先看浏览器 Network 面板里 css 文件和 js 文件的状态码是不是 404是的话说明项目的 context-path 配置不一致检查 springmvc.xml 中静态资源映射mvc:resources location/static/ mapping/static/**/ mvc:default-servlet-handler/mapping中的/**表示匹配任意层级的资源路径location指向 webapp 下的静态资源目录。default-servlet-handler 兜底处理未映射的资源请求。部署到 Tomcat 的 webapps 后项目可能是放在 ROOT 下、也可能是子目录两者访问 URL 路径不一样静态资源全部用${pageContext.request.contextPath}拼接最稳。有一个安全提示该项目使用 spring 5.2.8.RELEASE网上有关于 spring framework 目录遍历漏洞CVE-2024-38819的公开讨论毕设项目如果部署到公网环境或者作为开源模板传播建议在答辩前顺手把 spring 版本升到 5.3.39 及以上。这个版本跨度下 spring-webmvc 的核心 API 没有破坏性变更替换依赖版本后清理一下 Maven 本地仓库旧的 jar 包大概率可以直接编译。关于演示模式的技巧管理员登录后台时优先看轮播图管理、公告管理、客服管理三个模块因为它们都是单表 CRUD没有复杂的联合查询演示时不容易出 bug。重点讲眼镜管理里的增加库存和减少库存配合商品列表页面实时展示库存变化比空谈架构更有说服力。答辩时如果被问「怎么证明库存扣减是安全的」直接展示 reduceStock 的 SQL 和 Transactional 注解讲解事务回滚条件再加一句「超卖问题是互联网电商的核心难题之一」就足够回答这个层面了。本文还有配套的精品资源点击获取