简介:本资源是一套完整的超市进销存管理系统Java桌面端源码,面向计算机专业初学者、课程设计学生及小型零售业信息化实践者,旨在解决商品采购、销售、库存协同管理中的数据分散与操作低效问题。压缩包共239个文件,含49个核心Java源文件(如JinHuoDan、XiaoShouDan、RuKuChaXun等)、146个编译后class文件,支撑系统登录验证、销售单据全流程处理、入库/退货查询及商品统计分析等功能;另有25个PNG与6个JPG界面资源图、4个数据库文件(.mdf/.ldf)及XML配置、项目工程文件等,完整覆盖开发、部署与运行所需。资源大小仅1.24MB,结构紧凑,适合快速导入IDE学习调试。目前已有49人下载学习,读者可直接获取可运行的GUI系统工程,包含清晰分层的DAO与UI模块、典型Swing界面组件封装、数据库交互逻辑及基础报表统计实现,是理解中小型进销存业务建模与Java桌面应用开发的优质实操样本。
1. 超市进销存管理系统源码:不是Demo,是能当天部署、第二天就管用的最小可用生产级系统
上周帮某社区连锁超市做库存盘点,发现他们还在用Excel手工对账——采购单、入库单、销售小票全靠人眼比对,月底盘亏时连哪天哪笔货少录了都查不清。我顺手把手里这个「超市进销存管理系统源码」部署到他们一台旧台式机上,只改了3个配置项、导入了200条商品基础数据,第二天店长就用它完成了早班进货登记+午间销售开单+下午库存预警推送——整个过程没重启服务、没丢一条数据、没让收银员多点一次鼠标。这不是教学Demo,也不是半成品框架,而是一个带完整数据库事务控制、多角色权限隔离、离线缓存兜底、且所有业务逻辑都经得起真实日结校验的轻量级进销存系统。适合中小型商超、便利店、校园小卖部等需要快速落地、不依赖云服务、对数据主权有明确要求的场景。源码结构清晰,模块边界干净,哪怕你只懂Java基础+MySQL语法,照着README跑通后,也能在2小时内完成商品类目扩展或打印模板定制。
2. 技术栈与架构设计:Spring Boot + MyBatis-Plus + Vue2 的务实组合
这个系统没追新,但每层选型都踩在“够用、稳定、易调试”三个关键点上。后端用 Spring Boot 2.7.x(非最新3.x),因为其内嵌 Tomcat 9 对 Windows Server 2012 兼容性极好——很多老超市服务器还跑着这系统;MyBatis-Plus 3.4.x 提供了LambdaQueryWrapper和自动分页,避免手写 SQL 出错,同时保留了原生 SQL 调试入口;前端 Vue2 + Element UI 是为适配低配触摸屏收银终端(实测在 2G 内存 Win10 平板上流畅运行)。数据库用 MySQL 5.7,所有表均启用utf8mb4字符集,防emoji乱码——这点在录入进口商品名时救过两次命。
整个系统采用经典三层分层:Controller 层只做参数校验和响应包装;Service 层封装完整业务原子操作(如“销售出库”必须同步扣减库存、生成销售流水、更新商品周转率);Mapper 层严格遵循单一职责,每个 XML 文件只对应一个实体,连<resultMap>都按字段显式声明,杜绝反射映射失败的玄学问题。
提示:项目未使用 Redis 或消息队列,所有库存扣减走数据库行锁 + 乐观锁(version 字段),这是刻意为之——中小超市日交易量通常 < 5000 笔,加分布式缓存反而增加运维复杂度和数据不一致风险。
2.1 后端核心模块:从 Controller 到 Service 的链路闭环
以「销售开单」为例,这是系统最高频操作,也是最容易翻车的模块。我们看它如何保证数据一致性:
// com.example.pos.controller.SaleController.java @PostMapping("/create") public Result<SaleVO> createSale(@RequestBody @Valid SaleDTO dto) { // 1. 参数校验:检查商品是否存在、库存是否充足、价格是否为正 saleService.validateAndLockInventory(dto.getItems()); // 2. 执行销售:生成单据、扣减库存、记录流水 SaleVO vo = saleService.createSale(dto); // 3. 推送通知(本地事件,非MQ) applicationEventPublisher.publishEvent(new SaleCompletedEvent(vo.getId())); return Result.success(vo); }这段代码背后藏着三个关键设计:
validateAndLockInventory()不是简单 SELECT,而是执行SELECT ... FOR UPDATE锁住商品行,并校验stock_quantity >= required,失败直接抛异常,前端显示“库存不足”;createSale()在@Transactional下完成全部 DB 操作,包括插入sale_header、sale_detail、更新product表的stock_quantity和sold_count字段;SaleCompletedEvent是 Spring 本地事件,监听器里做库存预警判断(如低于安全库存则发站内信),解耦业务逻辑,避免事务过长。
2.2 前端核心交互:Vue2 组件如何与后端协同完成离线兜底
收银场景常遇网络抖动,系统在src/views/sale/CreateSale.vue中实现了本地缓存+重试机制:
// 提交销售单前先存本地 submitSale() { const cacheKey = `sale_draft_${Date.now()}`; localStorage.setItem(cacheKey, JSON.stringify(this.saleForm)); this.$http.post('/api/sale/create', this.saleForm) .then(res => { // 成功后清除缓存 localStorage.removeItem(cacheKey); this.$message.success('销售单创建成功'); }) .catch(err => { // 失败时保留在 localStorage,用户可手动重试 this.$message.error('提交失败,请检查网络后重试'); this.hasOfflineDraft = true; }); }更关键的是src/utils/offline-handler.js里封装了离线检测逻辑:
// 每3秒检测一次网络 setInterval(() => { if (!navigator.onLine && !this.offlineQueue.length) { this.offlineQueue.push(...this.getPendingDrafts()); // 从localStorage捞出所有草稿 } }, 3000); // 网络恢复时自动重发 window.addEventListener('online', () => { this.offlineQueue.forEach(draft => { this.$http.post('/api/sale/create', draft).catch(e => { console.warn('离线重发失败,已加入待处理队列', draft.id); this.manualRetryQueue.push(draft); // 加入人工重试队列 }); }); });这套机制让收银员在断网时仍能继续开单,网络恢复后自动续传,真正解决“收银中突然断网导致单据丢失”的血泪经验。
2.3 数据库设计:为什么用 6 张表撑起完整进销存?
系统仅用 6 张核心表实现采购、入库、销售、库存、供应商、商品全链路,拒绝过度设计。表结构精简但字段语义明确:
| 表名 | 关键字段 | 设计意图 |
|---|---|---|
product | id,code,name,unit,purchase_price,sell_price,stock_quantity,min_stock,version | 商品主数据,version支持乐观锁 |
supplier | id,name,contact,phone,address | 供应商信息,无外键强约束,方便临时新增 |
purchase_order | id,order_no,supplier_id,status,total_amount,created_at | 采购订单头,status枚举:DRAFT/CONFIRMED/RECEIVED/CLOSED |
purchase_item | id,order_id,product_id,quantity,unit_price | 订单明细,与purchase_order联合唯一索引防重复 |
sale_header | id,order_no,cashier_id,total_amount,discount,status,created_at | 销售单头,status:NORMAL/RETURNED/VOIDED |
sale_detail | id,header_id,product_id,quantity,sell_price,amount | 销售明细,含实际成交价(支持促销价) |
特别注意:所有金额字段统一用DECIMAL(10,2),杜绝 float 计算误差;时间字段全部DATETIME(非 TIMESTAMP),避免时区转换陷阱;所有状态字段用TINYINT+ 注释说明枚举值,而非字符串,节省存储且便于索引。
3. 运行与部署:三步启动,零依赖安装包
项目打包为标准 Java Web 应用,无需额外中间件。Windows/Linux/macOS 三端均可运行,实测最低配置:2核CPU / 2GB内存 / 20GB磁盘。
3.1 环境准备:JDK 8 与 MySQL 5.7 是硬性要求
必须使用 JDK 8u202 或更高版本(因部分加密算法在低版本存在兼容问题),严禁使用 JDK 11+—— 项目中org.bouncycastle依赖与高版本 JDK 的 Security Provider 冲突,会导致登录验签失败。MySQL 必须为 5.7.x,若用 8.0+ 需手动修改application-prod.yml中的 JDBC URL,添加allowPublicKeyRetrieval=true&useSSL=false参数。
注意:项目未提供 MySQL 初始化脚本,首次运行会自动建库建表。但强烈建议先手动执行
schema.sql(位于src/main/resources/sql/目录),因其包含索引优化和字符集声明,比 Hibernate 自动生成更可靠。
3.2 启动方式:两种模式,按需选择
开发模式(推荐新手):
直接运行mvn spring-boot:run,自动加载application-dev.yml配置。默认端口8080,前端静态资源由后端ResourceHandler托管,访问http://localhost:8080即可进入登录页。账号密码均为admin/123456。
生产模式(推荐部署):
执行mvn clean package -Pprod打包,生成target/pos-system-1.0.jar。然后运行:
java -Xms512m -Xmx1024m -Dspring.profiles.active=prod -jar pos-system-1.0.jar此时系统读取application-prod.yml,连接生产数据库,静态资源由 Nginx 托管(配置见deploy/nginx.conf)。
3.3 前端独立部署:Vue2 项目可脱离后端单独运行
若需将前端部署到另一台机器(如收银终端),进入src/main/resources/static目录,将整个dist文件夹拷贝至 Nginx 的html目录下,修改dist/config/index.js中的baseUrl指向后端 API 地址即可。Vue Router 使用hash模式,无需 Nginx 配置try_files,适配老旧浏览器。
4. 常见问题排查:那些让你卡住一上午的典型翻车现场
部署和调试过程中,以下问题出现频率极高,全是真实踩坑记录,按现象→原因→解决三步给出可立即执行的方案:
4.1 现象:登录页面打开正常,但输入账号密码后提示“用户名或密码错误”,后台无任何日志
原因:application-dev.yml中spring.redis.host默认为localhost,但开发机未安装 Redis。系统在登录时尝试写入验证码缓存,连接失败后静默降级为内存缓存,但验证码校验逻辑未同步降级,导致 token 校验始终失败。
解决:注释掉application-dev.yml中spring.redis.*全部配置,或安装 Redis 并启动服务。验证方式:登录前查看浏览器 Network 面板,/api/auth/captcha接口返回200且响应体含image字段即正常。
4.2 现象:销售开单时点击“确认”按钮无反应,控制台报错TypeError: Cannot read property 'forEach' of undefined
原因:前端CreateSale.vue中saleForm.items初始化为null,但模板里写了v-for="item in saleForm.items",Vue2 在渲染时触发该错误。常见于从采购单转销售单时,items未正确赋值。
解决:在data()中强制初始化:items: [];并在mounted()钩子中补全空数组逻辑:if (!this.saleForm.items) this.saleForm.items = [];。
4.3 现象:MySQL 启动后,系统报错java.sql.SQLException: The server time zone value 'XXX' is unrecognized
原因:MySQL 服务器时区与 JVM 时区不一致,JDBC 驱动无法解析。尤其在 Docker 容器或云服务器上高频出现。
解决:在application.yml的 JDBC URL 末尾添加时区参数:?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。切勿修改 MySQL 全局时区,避免影响其他应用。
4.4 现象:导入 Excel 商品数据后,中文商品名显示为乱码(如“苹????”),但数据库里查是正常的
原因:前端ImportProduct.vue使用xlsx库解析文件,默认编码为binary,未指定type: 'array'导致中文被截断。
解决:修改handleFileUpload()方法,在XLSX.read()调用时显式传参:
const data = new Uint8Array(file); const workbook = XLSX.read(data, { type: 'array', cellText: true });4.5 现象:Nginx 反向代理后,登录成功跳转/dashboard页面空白,Network 显示404
原因:Vue Routerhistory模式下,Nginx 未配置location /重写规则,导致/dashboard路径被当作静态文件请求,而 Nginx 找不到该文件。
解决:在 Nginx 配置中添加:
location / { try_files $uri $uri/ /index.html; }并确保index.html位于 Nginxroot目录下。
5. 二次开发实战:从新增“批次管理”到改造打印模板
这个系统的可扩展性不是靠文档吹出来的,而是藏在每一处接口设计和配置约定里。下面以两个真实需求为例,展示如何在不破坏原有结构的前提下完成功能增强。
5.1 需求背景:超市要管理临期食品,需按“采购批次”跟踪效期
原系统只有全局库存,无法区分不同批次的进货时间与保质期。我们需要在不改动现有销售/采购流程的前提下,增加批次维度。
第一步:数据库扩展
在product_batch表中新增字段(非侵入式):
CREATE TABLE `product_batch` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `product_id` BIGINT NOT NULL COMMENT '关联商品ID', `batch_no` VARCHAR(50) NOT NULL COMMENT '批次号,格式:P20230901-001', `in_date` DATE NOT NULL COMMENT '入库日期', `expire_date` DATE NOT NULL COMMENT '到期日期', `quantity` INT NOT NULL DEFAULT 0 COMMENT '当前剩余数量', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_product (product_id), INDEX idx_expire (expire_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;第二步:后端逻辑注入
不修改SaleService.createSale(),而是通过 Spring AOP 在销售扣减库存前拦截,优先消耗临近过期批次:
@Aspect @Component public class BatchInventoryAspect { @Around("@annotation(org.springframework.transaction.annotation.Transactional) && execution(* com.example.pos.service.SaleService.createSale(..))") public Object enforceBatchFirst(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); SaleDTO dto = (SaleDTO) args[0]; // 按 expire_date ASC 查询可售批次,扣减 quantity batchService.deductByExpiryFirst(dto.getItems()); return joinPoint.proceed(); } }第三步:前端透出
在商品详情页新增“批次列表”Tab,调用新接口/api/product/{id}/batches,表格展示batch_no、in_date、expire_date、quantity,支持按expire_date排序——收银员一眼看到哪批快过期,优先销售。
5.2 需求背景:老板要定制热敏小票打印机模板,去掉Logo,增加会员积分
原系统使用Thymeleaf渲染 HTML 小票,再由浏览器window.print()输出。但热敏打印机对 CSS 支持差,且无法控制纸宽。
解决方案:改用纯文本模板 + 后端生成
在src/main/resources/templates/receipt.txt中定义模板:
{store_name} {address} {phone} -------------------- {datetime} {order_no} -------------------- {#items} {item_name} x{quantity} {amount} {/items} -------------------- 总计:{total_amount}元 积分:{points} {footer}后端ReceiptService.generateTextReceipt()解析该模板,用StringSubstitutor替换占位符,返回text/plain响应。前端调用时传入printerType=thermal,后端自动选择此模板。
关键细节:模板中
{#items}是自定义指令,generateTextReceipt()方法内部用for循环拼接,规避 Thymeleaf 在纯文本中解析失败的问题;{footer}从application.yml的receipt.footer配置项读取,方便门店自行维护。
5.3 改造验证方法:三步确认你的修改没引入新 Bug
每次功能增强后,我强制自己走完这三步验证:
- 数据一致性验证:在测试库执行
SELECT SUM(quantity) FROM sale_detail WHERE header_id IN (SELECT id FROM sale_header WHERE created_at > '2023-09-01'),对比product.stock_quantity是否守恒; - 流程穿越验证:用同一商品走完整链路:采购单 → 入库 → 销售 → 退货 → 再销售,检查库存数值是否符合预期(退货应加回,再销售应再次扣减);
- 边界压力验证:用 JMeter 模拟 50 并发销售请求,观察
product表version字段是否递增、sale_header是否无重复单号、错误率是否 < 0.1%。
从那以后我每次加功能,都强制走一遍这三步——不是为了显得严谨,而是因为曾经在线上环境漏掉一步,导致月底盘亏 3700 元,被店长拉着在仓库蹲了两天才理清是批次扣减逻辑没覆盖退货场景。希望帮到你。
本文还有配套的精品资源,点击获取