ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SSM超市进销存系统实战:环境搭建、事务处理与避坑指南

SSM超市进销存系统实战:环境搭建、事务处理与避坑指南 简介面向Java毕设与课设场景这份基于SSM框架的美特超市进销存管理系统覆盖商品入库、库存管理、销售统计等核心业务适合需要完整可运行项目参考的计算机专业学生开发环境明确采用SSMVue前后端分离结构JDK1.8、Tomcat7、MySQL5.7即可部署附带管理员账号便于快速登录测试。资源包共717个文件约20.28MB以184个Java后台源码、130个Vue前端组件、162个SVG图标及JS、XML配置文件为主另有SQL数据库脚本、运行/构建批处理与项目配置文件目录层次分明可快速在eclipse/idea中导入运行。目前已有98人学习下载对于正在做进销存类毕设的同学可直接参考其模块划分、权限控制与前端页面实现也可作为修改扩展的基础模板省去从零搭建的时间。1. 基于Java美特超市进销存管理系统ssm-91crh.zip这个标题背后是什么值不值得跑通“基于Java美特超市进销存管理系统ssm-91crh.zip”这个标题翻译成大白话就是一个用Spring、SpringMVC、MyBatisSSM三件套写成的超市进货、售货、库存管理项目打好包通过ZIP分发。做过相关项目的人看到这个命名就知道里面是什么结构——解压后是标准JavaWeb工程JSP页面、Controller控制层、Service业务层、Mapper数据库访问层一层压一层。它解决的是传统超市把进销存从Excel表格挪进数据库让店长有一个能查流水、盯库存、管供应商的网页后台。这类项目最常见的来源是毕业设计和中小企业信息化改造价值不在于代码多难而在于业务闭环完整有商品档案、供应商档案、进货单、销售单、库存流水。适合三类人——想拿一个完整SSM项目练手的学生准备Java面试但项目经验偏理论、想补一个业务系统的从业者以及要给小超市做信息化的外包开发。跑通它比读十篇框架教程都管用因为你会真正看到一张进货单是怎么从页面流进MySQL的。2. 环境准备与项目导入JDK、Tomcat、MySQL版本选对后面才能少踩坑2.1 先看pom.xml再装环境SSM项目对版本配合极度敏感跑这种SSM项目最忌讳的就是把JDK、Tomcat装成最新版然后直接双击Open。我最早跑类似项目时用的是JDK 17加Tomcat 10结果项目里Spring还停留在4.xTomcat一启动就抛ClassNotFoundException整整折腾一个下午最后发现就是版本代差问题。后来养成的习惯是解压ZIP后第一件事不是运行而是找pom.xml、web.xml从依赖版本反推环境要求。SSM技术栈的成熟期集中在2018到2022年常见组合是JDK 1.8、Tomcat 8.5、MySQL 5.7、Maven 3.6.x。pom.xml里一般长这样properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target spring.version4.3.18.RELEASE/spring.version mybatis.version3.4.6/mybatis.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency /dependencies上面的依赖只给前后端交互的骨架。逻辑说明看到spring.version是4.x就老老实实用JDK 7或8看到mybatis是3.4.x配套的mybatis-spring一般也要用1.3.x。这几个库的版本咬合得很紧单独把其中一个升到新版本很容易踩到“方法不存在”或者“类找不到”的隐性问题。参数说明maven.compiler.source和target决定编译字节码版本source1.8编译出的class文件才能稳定跑在Tomcat 8.5上。如果本机已经装了JDK 17不需要卸载在IDEA里给这个项目单独设置Project SDK为1.8作用范围只在这个Module内比全局改JAVA_HOME安全得多。提示打开项目后先看IDEA右下角用的JDK版本如果显示17去Project Structure里改成1.8。这一步没做对后面所有启动报错都会被带偏方向。2.2 导入IDE与Maven依赖先认目录结构再配置镜像源解压这种ZIP后一个常见的Maven工程结构是这样ssm-91crh/ ├─ pom.xml ├─ src/main/java │ ├─ com/meite/controller │ ├─ com/meite/service │ ├─ com/meite/dao │ └─ com/meite/entity ├─ src/main/resources │ ├─ jdbc.properties │ ├─ log4j.properties │ └─ spring │ ├─ spring-mvc.xml │ └─ spring-mybatis.xml ├─ src/main/webapp │ ├─ WEB-INF/web.xml │ └─ jsp │ ├─ goods.jsp │ ├─ purchase.jsp │ ├─ sale.jsp │ └─ stock.jsp └─ README.md解压后在IDEA里直接Open定位到pom.xml等待右下角进度条把依赖拉完。如果用的是Eclipse则走Import、Existing Maven Projects也是一样定位到pom.xml。目录分得这么清楚是有原因的com.meite.controller只处理页面请求service只写业务逻辑dao只碰数据库entity跟数据库表一一对应。后面改需求时入口在controller逻辑在serviceSQL在mapper不会跑错地方。依赖下载慢或者直接报Could not resolve dependency是国内网络环境下的常见情况。解决办法是给Maven配一个可用的镜像源在settings.xml的mirrors节点里加mirrors mirror idaliyun/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/central/url /mirror /mirrors逻辑说明mirrorOf写central表示所有对中央仓库的请求都走这个镜像地址。IDEA里配置Maven的User settings file指向这个settings.xml改完点Reload All Maven Projects依赖会重新解析。这一步做完启动时的很多玄学报错会直接消失因为依赖没下全的项目连编译都过不了。2.3 数据库连接配置连不上库时先按这三处排查绝大多数SSM项目的数据库配置集中在src/main/resources下的jdbc.properties里。打开这个文件通常只需要核对三行jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/ssm_meite?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456参数说明MySQL 5.7直接用com.mysql.jdbc.Driver如果本地装的是MySQL 8.x这一行必须换成com.mysql.cj.jdbc.Driver否则启动时抛No suitable driver。jdbc.url里的characterEncodingutf8解决后续中文乱码问题serverTimezoneAsia/Shanghai是MySQL 8必填的时区参数。username和password按本地数据库实际值改这个估计没人会忘。数据库本身也得先建好。ZIP包里多数会带一个meite.sql或db.sql导入命令是这样mysql -uroot -p123456 meite.sql执行导入前建议先手动建库CREATE DATABASE IF NOT EXISTS ssm_meite DEFAULT CHARSET utf8mb4;否则导入时会报No database selected。导入完成后在Navicat或命令行里执行SHOW TABLES;确认核心表都在再回IDEA启动Tomcat。启动成功后在浏览器访问http://localhost:8080/ssm-91crh/看到登录页说明三件套已经和环境咬合上了。3. 数据库与核心表设计进销存系统的关键不在库存字段而在流水记录3.1 六张核心表的结构商品、供应商、进货、销售、库存各司其职进销存系统最少要六张核心表goods商品表、supplier供应商表、purchase进货单主表、purchase_item进货明细表、sale销售单主表、sale_item销售明细表。商品表存档案信息供应商表存供货商资料进货和销售两类单据各拆主表和明细表是为了支持一张单子包含多个商品。给出最常见的建表语句CREATE DATABASE IF NOT EXISTS ssm_meite DEFAULT CHARSET utf8mb4; CREATE TABLE goods ( goods_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 商品ID, goods_name VARCHAR(100) NOT NULL COMMENT 商品名称, barcode VARCHAR(32) COMMENT 条码, purchase_price DECIMAL(10,2) COMMENT 进价, sale_price DECIMAL(10,2) COMMENT 售价, stock INT DEFAULT 0 COMMENT 当前库存, unit VARCHAR(10) COMMENT 单位件/箱/斤, category VARCHAR(50) COMMENT 分类, is_deleted TINYINT DEFAULT 0 COMMENT 逻辑删除标记 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE supplier ( supplier_id INT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(100) NOT NULL, contact VARCHAR(50), phone VARCHAR(20) ); CREATE TABLE purchase ( purchase_id INT PRIMARY KEY AUTO_INCREMENT, supplier_id INT NOT NULL, purchase_time DATETIME, total_amount DECIMAL(12,2), status TINYINT DEFAULT 0 COMMENT 0未入库 1已入库 ); CREATE TABLE purchase_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, purchase_id INT NOT NULL, goods_id INT NOT NULL, quantity INT, price DECIMAL(10,2), subtotal DECIMAL(12,2) );逻辑说明goods表里的stock字段看起来是核心但真正决定库存值的是purchase和purchase_item两张流水表。进货时往purchase主表插一张单往明细表插对应商品数量销售时同理。goods.stock只是冗余汇总用空间换查询速度。参数说明DECIMAL(10,2)表示金额最大10位、保留2位小数超市商品几块钱到几百块完全够用。TINYINT DEFAULT 0用来做状态标记和逻辑删除比用1、2、3这种裸数字可读性好得多。外键在这个版本里先不建后面小节单独说原因。为什么说流水才是核心超市对账时问的不是“现在库存多少”而是“这个月一共进了多少、卖了多少、损耗在哪”。这些答案只有流水表能给。如果系统只维护goods.stock一个值一旦有人手工改库存或者程序写错连哪里错的都找不回来。流水账可以追溯冗余字段只能看到结果。3.2 外键和索引推迟建外键是为了业务灵活索引不能省销售单结构和进货单对称不再重复列SQL。但有一点值得单独说purchase表和purchase_item表之间、supplier表和商品表之间很多毕设源码会直接写外键约束。我一般会建议保留外键定义来保证参照完整性但要做两个调整。第一个调整是给所有关联字段建普通索引比如CREATE INDEX idx_purchase_supplier ON purchase(supplier_id); CREATE INDEX idx_item_purchase ON purchase_item(purchase_id); CREATE INDEX idx_item_goods ON purchase_item(goods_id);逻辑说明进销存系统的查询特征是按供应商查进货单、按商品查销售明细。没有索引时数据量到几万条就会明显卡顿MySQL只能全表扫描。加上这几个索引后关联查询走索引响应时间能从秒级降到毫秒级。第二个调整是商品删除用逻辑删除而不是物理删除也就是UPDATE goods SET is_deleted 1 WHERE goods_id ?而不是DELETE。原因很简单商品一旦被历史进货单引用物理删除会被外键约束拦下来而后台页面又要处理“删不掉”的报错。is_deleted字段加进来删除只是标记历史流水不受影响查询时默认过滤is_deleted 0即可。3.3 事务注解放对位置库存扣减不是一个步骤是一个原子操作进销存系统最容易出问题的操作是进货入库和销售出库。销售出库在代码上至少两步往sale表插记录更新goods表的stock字段。如果往sale表插完、更新库存前程序抛了异常数据库里就会出现“卖了货但库存没减”的脏数据。把这两步放进同一个事务才是正解。Service层代码常见做法是这样Service public class SaleService { Autowired private SaleMapper saleMapper; Autowired private GoodsMapper goodsMapper; Transactional(rollbackFor Exception.class) public void saleOut(SaleDTO dto) { saleMapper.insertSale(dto); for (SaleItem item : dto.getItems()) { goodsMapper.decreaseStock(item.getGoodsId(), item.getQuantity()); } } }逻辑说明Transactional把整个saleOut方法包成一个事务任何一个SQL出错前面所有已执行的SQL都回滚。rollbackFor Exception.class是关键细节因为Spring默认只在抛出RuntimeException时回滚而SQLException这类受检异常不会触发回滚显式声明后所有异常都能兜住。参数说明insertSale和decreaseStock是两次独立数据库操作事务管理器负责把它们绑在一起。MySQL里InnoDB引擎支持事务建表时ENGINEInnoDB不能省。如果哪天发现数据只写了一半优先检查Service方法有没有被Spring代理同类里this.saleOut()这种内部调用不会走事务代理事务会静默失效——这是Java业务系统里特别容易翻车的点。4. SSM三层架构与核心业务链路一张进货单从页面到数据库的完整路程4.1 SpringMVC请求链路JSP表单到Mapper接口中间经过七个环节跑通项目后要理解的不只是“能点”而是数据到底怎么流动。一张进货单从页面提交到写入数据库完整链路是浏览器表单、Ajax提交、DispatcherServlet、Controller、Service、Mapper接口、Mapper XML、MySQL。前端的进货页面通常会收集供应商ID和商品明细数组组装成JSON发给后端。Controller代码风格如下Controller RequestMapping(/purchase) public class PurchaseController { Autowired private PurchaseService purchaseService; RequestMapping(/add) ResponseBody public Result add(RequestBody PurchaseDTO dto) { purchaseService.stockIn(dto); return Result.success(); } }逻辑说明RequestBody把前端传来的JSON字符串反序列化成PurchaseDTO对象比传统request.getParameter一个个取值干净得多。ResponseBody把Result对象序列化成JSON返回给前端前端再根据success字段决定提示“保存成功”还是弹出错误。Controller本身不做业务判断只负责参数接收、调用Service、返回结果。参数说明RequestMapping(/add)与类上的RequestMapping(/purchase)拼出完整访问路径/purchase/add。PurchaseDTO里一般包含supplierId、List 、totalAmount三个字段正好对应进货单主表和明细表两份数据。前端Ajax这边常见做法是用jQuery的$.ajax或$.post把对象转成JSON字符串Content-Type设为application/json。好多跑不通这个环节的人问题不在后端而是前端默认提交了表单格式后端RequestBody接了个空对象。记住用了RequestBody前端就要JSON.stringify并且请求头带application/json两边对齐才不翻车。4.2 MyBatis动态SQL多条件查询和库存台账的写法和边界进销存页面总少不了条件筛选按供应商、按时间段、按商品分类查进货单。这种场景正是MyBatis动态SQL的用武之地。Mapper XML里常见的写法select idselectPurchaseHistory resultTypecom.meite.entity.Purchase SELECT * FROM purchase where if testsupplierId ! null AND supplier_id #{supplierId} /if if teststartTime ! null AND purchase_time gt; #{startTime} /if if testendTime ! null AND purchase_time lt; #{endTime} /if /where ORDER BY purchase_time DESC /select逻辑说明 会自动处理第一个AND条件都不传时生成SELECT * FROM purchase传了供应商ID则拼上AND supplier_id ?。和是XML里的转义写法对应SQL中的和不转义XML解析直接报错。库存台账的逻辑比单表查询复杂一步。需求通常是“每个商品累计进了多少、卖了多少、当前应该剩多少”。如果系统建了库存流水表stock_logSQL直接按商品ID聚合没有流水表时常见做法是把进货明细和销售明细UNION起来分组算MyBatis里可以写两个select分开查再在Service层合并或者写一条带UNION ALL的多表SQL。一条典型的台账SQL是这样SELECT goods_id, SUM(CASE WHEN type IN THEN quantity ELSE 0 END) AS total_in, SUM(CASE WHEN type OUT THEN quantity ELSE 0 END) AS total_out FROM stock_log WHERE goods_id #{goodsId} GROUP BY goods_id;说明这里的type字段用IN和OUT区分进货与销售SUM配合CASE把两条方向的数量折算成两个累加列。运行时只要这个SQL查出来的库存和goods.stock对不上就说明某个事务漏了日志或冗余字段没更新该去查代码了。4.3 SSM常用注解速查一眼看出一个类归谁管、一个方法有什么行为接手这种SSM项目最快上手的办法是看注解。注解是Spring给类的“身份标签”看到类上的注解就知道它待在哪一层。整理一张对照表注解标注位置作用常见误用Controller控制器类交给MVC层管理处理请求映射返回JSON时漏写ResponseBody页面显示乱码Service业务类标记Service组件纳入事务代理加在接口上导致实现类不被扫描RepositoryMapper实现DAO层组件同时转换数据库异常只写接口不加注解包扫描扫不到Autowired字段/构造器依赖注入多个同类型Bean时报NoUniqueBeanDefinitionExceptionTransactional方法或类声明事务边界同类内部调用、private方法上均失效RequestMapping类或方法绑定URL路径类路径和方法路径拼出斜杠问题逻辑说明Spring容器启动时会扫描指定包下的类发现类上有这些注解就创建Bean放进容器。Controller依赖Service、Service依赖Mapper全靠Autowired注入。面试里被问到SSM常用注解时把这张表的职责边界讲清楚就够了。包扫描配置分两个文件spring-mvc.xml里一般只扫描com.meite.controller包spring-mybatis.xml里扫com.meite.service和com.meite.dao。两层扫描范围如果重叠可能导致一个Bean被创建两次启动时抛BeanDefinitionStoreException或者事务代理失效。改配置时始终保持“MVC管控制层Spring管业务和持久层”这个边界。5. 避坑指南SSM进销存从启动到能用的五个高频问题排查5.1 Tomcat启动成功但打开页面404现象IDEA控制台Tomcat日志显示启动成功浏览器访问http://localhost:8080/ssm-91crh/却一直404。原因最常见是Artifact没部署或者IDEA里Application context配置的路径和实际访问路径不一致。第二个高发原因是web.xml里没配置欢迎页访问根路径找不着默认页。还有一个隐藏点是war exploded和war两种部署方式选了war包模式但没重新构建。解决打开Run Configuration确认Deployment下挂了当前项目的war explodedApplication context填/ssm-91crh和浏览器地址保持一致。然后检查web.xml里有没有 指向index.jsp没有就补上。改完配置重启看到“Artifact is deployed successfully”才算真正部署成功。5.2 中文乱码页面、数据库、连接串三个位置必须同步统一现象页面上输入“洗发水”保存后数据库看到的是“????”更夸张的是变成“å æ´æ°´”这种乱码。原因三个位置至少有一个不一致。jdbc.url里没有characterEncodingutf8MySQL驱动用默认编码传输或者数据库表是latin1字符集或者HTTP请求和响应编码没过滤。这三处只要有一处是latin1或ISO-8859-1中文就会变形。解决先改jdbc.propertiesURL追加useUnicodetruecharacterEncodingutf8再确认建表语句带了DEFAULT CHARSETutf8mb4已经建好的表用ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4;修复最后在web.xml里加Spring的CharacterEncodingFilter强制请求和响应都走UTF-8url-pattern配/*覆盖所有路径。5.3 MySQL驱动版本不匹配连不上库先看报错信息里的驱动类名现象启动时抛No suitable driver found或者Communications link failure有时还跟着Caused by的详细内容。原因本地安装的MySQL是8.x项目里却引了5.1.x版本的mysql-connector-java或反之。驱动与服务端版本差代太远握手协议对不上连接只能在建立前就被掐断。解决在pom.xml里检查mysql-connector-java的versionMySQL 5.7用5.1.4xMySQL 8.0用8.0.x。如果拿不准直接在Maven依赖树窗口看已解析版本改完重新导入。5.4 库存变负数并发扣减必须用SQL层原子操作现象两个收银台同时卖最后一件商品两边都显示成功商品库存变成-1。原因Service代码里先SELECT stock查库存判断大于0后再UPDATE减一。这两个动作之间有间隙两个请求同时读到的库存都是1各自走完判断各自做减一操作结果自然变成负的。解决把“校验库存和扣减库存”合并成一条UPDATE语句利用MySQL行锁保证原子性UPDATE goods SET stock stock - #{num} WHERE goods_id #{goodsId} AND stock #{num};逻辑说明SQL的WHERE条件里带上stock #{num}是让数据库在更新瞬间判断库存是否足够而不是靠Java代码先查再算。MyBatis执行这条SQL时返回受影响行数受影响行数为1说明扣减成功为0说明库存不足要回滚业务。参数说明这方案依赖InnoDB的行锁stock #{num}迫使数据库对命中行加锁两个并发请求到数据库层时会被串行执行后到的一个因为条件不满足直接返回0行。这是进销存系统保证数据一致性的基础做法面试问“Java怎么保证数据一致性”时也可以往这个方向答。5.5 Mapper绑定错误XML文件没编译进target是经典翻车点现象Tomcat启动成功页面一点按钮就抛BindingException提示Invalid bound statement (not found)后跟一堆Mapper接口名。原因MyBatis的Mapper接口和Mapper XML不在同一个目录或者XML文件放在src/main/java下但没有在pom.xml里声明resources包含这个目录导致编译时XML没有复制进target/classes。解决最简单是把XML放到resources/mapper目录并在spring-mybatis.xml里配置mapperLocations指向classpath:mapper/*.xml。如果XML必须和接口同包则在pom.xml里补充资源声明build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources /build逻辑说明Maven默认只把resources目录下的文件当作资源打包src/main/java下的XML不会自动进target。加上这段配置后Java目录里的XML才会在compile阶段被拷贝出去接口和实现才能对上。6. 跑通之后用Spring Test给库存服务上道保险再把慢SQL揪出来项目能跑不代表能放心用。我接手这类进销存系统时会先做两件事给库存服务写回归测试给MySQL开慢查询日志。库存这种数据偏一分月底对账就头疼十分。回归测试用Spring Test加载已有的spring-mybatis.xml真实连接测试库执行一次入库逻辑RunWith(SpringJUnit4ClassRunner.class) ContextConfiguration(locations classpath:spring-mybatis.xml) public class StockServiceTest { Autowired private PurchaseService purchaseService; Autowired private GoodsMapper goodsMapper; Test public void testStockIn() { PurchaseDTO dto new PurchaseDTO(); dto.setSupplierId(1); dto.addItem(new PurchaseItem(null, 1, 10, 2.5)); purchaseService.stockIn(dto); assertEquals(Integer.valueOf(10), goodsMapper.selectStock(1)); } }逻辑说明这个测试验证的不是某个页面而是“进货入库后库存是否精确增加”。断言assertEquals(10, stock)直接卡住最核心的业务规则。改过库存SQL、动过事务注解之后先跑一遍这个测试再谈其他功能能省很多手工点页面的时间。MySQL慢查询日志用来找性能瓶颈测试环境执行下面两行SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;long_query_time1表示执行超过1秒的SQL都会被记录。跑一轮进销存的主流程再查看慢查询日志文件集中优化那些频繁出现的全表扫描。常见优化方向就是第3章里说的——给外键字段补索引给商品表的关键字查询加联合索引。以前我跑通一套SSM系统后总觉得完工了后来被库存对账翻车教训过一次才明白跑通只是开始。现在我每次改完这类系统的业务代码都先把回归测试跑一遍再把慢查询日志打开心里才有底。这比“看起来能点”可靠得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表