
简介面向超市零售场景的进销存管理系统完整源码适合Java学习者、毕业设计者以及需要快速搭建库存管理系统的开发者。系统覆盖登录验证、销售管理、商品统计等核心模块结合Java与数据库技术实现商品采购、销售、库存的联动处理。压缩包共239个文件包含146个class编译文件、49个java源文件、25个png界面截图及相关jpg图片、4个db数据库文件另有项目配置文件、Hibernate映射文件等可对照源码与截图理解界面及数据流转。资源仅1.24MB轻量紧凑便于本地导入和二次开发。已有48人学习下载。源码中可见登录面板、销售单、进货退货、入库查询等功能类通过阅读源代码可掌握订单创建、金额计算、库存更新及统计报表的实现思路适合用于课程设计或中小型超市管理系统的改造参考。1. 超市进销存管理系统源码拿回家先跑通这套再谈改造超市进销存管理系统源码在电商、课设和中小超市三个圈子里被找得最多。它解决的是一类具体问题商品信息、供应商、进货单、销售单、库存数量这五样东西能不能在一个后台里管起来并且每笔流水都能追溯到单号。很多人拿到源码后第一件事就是启动但真正跑通的人不多问题往往不在代码而在建库、配置和路径这三处。这套资源适合正在做课设或刚接触前后端分离项目的学生也适合想抄一套基础后台模板来改的小团队。下面的内容把从环境准备到核心代码怎么跑全过一遍顺便把最容易翻车的地方给你提前指出来。2. 系统架构与数据模型先看懂这套源码的分层和表设计2.1 前后端分离还是单体这套源码的模块划分这套超市进销存管理系统源码按前后端分离结构组织后端是一个 Spring Boot 工程前端是一个 Vue 工程。后端用 Maven 管理依赖Java 8 及以上都可以跑内置端口是 8080前端基于 Vue 2 Element UI开发服务器跑在 8081通过代理把/api转发到后端。对于进销存这种业务来说前后端分离的好处是权限校验、业务逻辑和页面渲染互不干扰课设答辩时可以单独演示后端接口也可以直接展示页面。后端按模块分包controller接收前端请求service放业务逻辑mapper负责数据库操作entity对应表结构config里面放拦截器和跨域配置。前端则按视图拆分views下是登录、商品、进货、销售、库存、报表六个页面router配置路由utils里封装了 axios 实例。这个模块划分是进销存类项目最常见的一种如果你想改成餐厅点餐系统或便利店零售系统只需替换views和部分 service 逻辑骨架可以直接复用。2.2 核心数据表商品、供应商、入库单、销售单的字段与关系进销存的数据模型核心在于「单据 明细 库存流水」三层。先看商品表product它不只是存名称和价格还包含barcode条形码、category_id分类、purchase_price进货价、sale_price销售价、stock当前库存、min_stock最低库存预警值、pic商品图片。这里有个关键设计库存直接冗余在商品表上而不是像纯 ERP 那样每次查询都去 join 流水表这样列表查询速度快代价是每次出入库必须同步更新stock字段。再看出入库相关表purchase_order和sale_order是主表分别记录供应商或操作员、总金额、单号和状态purchase_order_item和sale_order_item是明细表记录每个商品的进货/销售、数量和单价。最后是inventory_log流水表每次库存变动都会写一条记录包含type进/销/退/报损、quantity、balance变动后余额、remark。这三层结构保证了你能回答「某一个商品当前库存多少」和「这个商品的库存是怎么变的」两个问题缺了流水表进销存就是黑匣子。2.3 技术栈选型为什么用 Spring Boot Vue MySQL选这套组合不是拍脑袋。Spring Boot 对事务和 MyBatis-Plus 的支持非常成熟进销存的核心操作是多个表的写操作必须依赖Transactional来保证要么全部成功要么全部回滚。Vue 2 Element UI 是前后端分离课设和私人项目的常青搭配表格、分页、对话框这些后台管理界面组件都是现成的不用自己造轮子。MySQL 则在建表和索引方面教育成本最低也最容易导出 SQL 脚本。从资源落地的角度看这套技术栈部署门槛不高后端只需 JDK 和 Maven前端只需 Node.js数据库只需一个开源 MySQL。相比微服务或云原生方案它更接近一场「一个人能搞定」的搭建。下面部署部分用的命令也是这套源码配套文档里的标准做法我按实际跑通顺序写给你。3. 部署与启动从环境准备到本地跑起来的完整步骤3.1 环境清单JDK、Node、MySQL 版本怎么配别急着启动项目先确认环境。后端要求 JDK 8 以上我建议直接用 JDK 1.8因为很多老源码的 Maven 配置对新版本 JDK 的兼容性并不好Maven 用 3.6 以上即可。前端要求 Node.js 14 到 16Vue 2 项目在 Node 18 以上的旧依赖安装经常报 OpenSSL 错误。MySQL 要求 5.7 或 8.0这套源码默认按 8.0 写的连接配置如果你的机器是 5.7需要注意驱动和建表语法兼容性。我一般会在项目根目录建一个env.md把这些版本号写清楚免得换电脑后重新折腾。下面是环境检查命令复制到终端跑一遍就能确认java -version mvn -version node -v npm -v mysql --version这里java -version看到 1.8 最好mvn -version提示 Java version 与java -version一致才说明 Maven 用的是正确的 JDKnode -v如果是 v18 以上建议装一个 nvm 切回 16。MySQL 版本信息只要不低于 5.7 就能继续后面建库时认证插件要选对。3.2 初始化数据库执行 SQL 脚本与账号配置这套源码的database目录下有一个db_supermarket.sql里面包含建库语句、建表语句和初始数据。用命令行或 Navicat 执行都可以我个人习惯用命令行执行语句是CREATE DATABASE IF NOT EXISTS db_supermarket DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE db_supermarket; SOURCE /your/path/db_supermarket.sql;建库语句里的utf8mb4很重要如果只写utf8存商品名称里的生僻字或者特殊符号时会变成问号。SOURCE 后面要写 SQL 文件的实际路径Windows 下就用反斜杠Linux 和 Mac 用正斜杠。执行成功后可以用SHOW TABLES;看一下正常会列出上文说的product、purchase_order、sale_order、inventory_log等表。初始数据里通常会带一个管理员账号密码一般是 MD5 加密后的字符串不要去数据库里手动改明文登录逻辑里的密码比对方式决定了你必须用源码里约定好的加密形式。如果你想重置密码看一下sys_user表里现有 admin 行的 password 值直接复制那串密文覆盖你要改的账号就行。3.3 启动后端application.yml 里最容易改错的三个参数后端工程在backend目录下配置文件是src/main/resources/application.yml。需要动的主要有三个位置数据库连接、端口、MyBatis 日志级别。先看一个最小可运行的配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/db_supermarket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: trueURL 里的serverTimezoneAsia/Shanghai是很多人都踩过的地方不写时 JDBC 驱动会拿不到正确的时区连接直接报错。allowPublicKeyRetrievaltrue是为了兼容 MySQL 8.0 的 caching_sha2_password 认证方式如果你用的是 MySQL 8.0 但没加这个参数会一直提示Public Key Retrieval is not allowed。driver-class-name必须是com.mysql.cj.jdbc.Driver注意是带.cj的老写法的com.mysql.jdbc.Driver在 MySQL 8.0 下已经废弃。改完这些后在backend目录下启动mvn spring-boot:run看到Started Application in ...就说明后端起来了。如果端口被占用优先改server.port而不是去杀进程因为前端代理会跟着端口走端口保持一致会省很多麻烦。3.4 启动前端Vue 项目的依赖安装与跨域代理前端工程在frontend目录下先装依赖再启动。这里要注意镜像源问题用默认 npm 源在中国环境下载经常卡住一般我会先切到国内镜像cd frontend npm config set registry https://registry.npm.taobao.org npm install如果npm install中途报错大概率是node_modules缓存问题删掉重新装一次一般能解决。安装完成后开发服务器配置在vue.config.js里核心是跨域代理module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这段配置的意思是前端把所有以/api开头的请求转发到http://localhost:8080也就是后端的 8080 端口。启动命令是npm run dev看到Local: http://localhost:8081后用浏览器打开登录页输入初始管理员账号能跳转进首页就算彻底跑通了。整个部署过程我按顺序走了一遍最耗时间的不是代码启动而是环境版本和配置不一致导致的杂音。4. 核心业务流程实测进货、销售、退货、库存查询4.1 进货入库从供应商到库存增加的状态流转进货是整个进销存系统的业务起点。前端页面上选择供应商、添加商品、填写单价和数量提交后后端会生成一张进货单同时更新商品库存。这里的核心问题是进货不是只插一张表就完了它至少涉及purchase_order主表、purchase_order_item明细表、product库存、inventory_log流水四类写操作任何一个失败进货数据都不能完整落库。后端 Service 层会在这个方法上加Transactional代码如下Transactional public void createPurchaseOrder(PurchaseOrderRequest request) { PurchaseOrder order new PurchaseOrder(); order.setSupplierId(request.getSupplierId()); order.setTotalAmount(calcTotal(request.getItems())); order.setStatus(1); // 1 表示已入库 purchaseOrderMapper.insert(order); for (PurchaseItem item : request.getItems()) { PurchaseOrderItem orderItem new PurchaseOrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setQuantity(item.getQuantity()); orderItem.setPrice(item.getPrice()); orderItem.setAmount(item.getQuantity() * item.getPrice()); purchaseOrderItemMapper.insert(orderItem); productMapper.increaseStock(item.getProductId(), item.getQuantity()); inventoryLogMapper.insert(new InventoryLog( item.getProductId(), 进, item.getQuantity(), productMapper.selectById(item.getProductId()).getStock(), 进货入库)); } }注意Transactional这只注解没有它的话如果循环到第 3 个商品时数据库报错前 2 个商品的库存已经加了整张进货单就是残缺的。increaseStock这个方法在 Mapper XML 里的 SQL 是UPDATE product SET stock stock #{quantity} WHERE id #{id}而不是先查再加这样能避免并发覆盖。这里一个容易忽略的点是我写「更新库存后重新查询库存作为流水余额」这一下多了两次查询但为了让流水表留下准确余额这是值得的。4.2 销售出库库存扣减的时机与并发销售出库比进货复杂的地方在于库存不足时报错、扣减库存的并发控制、销售单号生成。先看扣减这一层常见的做法是在写销售单明细前先把对应商品行锁住再判断库存够不够Transactional public void createSaleOrder(SaleOrderRequest request) { SaleOrder order new SaleOrder(); order.setOperatorId(request.getOperatorId()); order.setTotalAmount(calcTotal(request.getItems())); saleOrderMapper.insert(order); for (SaleItem item : request.getItems()) { Product product productMapper.selectByIdForUpdate(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new RuntimeException(商品库存不足: product.getName()); } SaleOrderItem saleItem new SaleOrderItem(); saleItem.setOrderId(order.getId()); saleItem.setProductId(item.getProductId()); saleItem.setQuantity(item.getQuantity()); saleItem.setPrice(product.getSalePrice()); saleItem.setAmount(item.getQuantity() * product.getSalePrice()); saleOrderItemMapper.insert(saleItem); productMapper.decreaseStock(item.getProductId(), item.getQuantity()); inventoryLogMapper.insert(new InventoryLog( item.getProductId(), 销, item.getQuantity(), product.getStock() - item.getQuantity(), 销售出库)); } }selectByIdForUpdate是行级锁它的作用是让两个店员同时扫同一个商品条码时后一个事务必须等前一个事务提交才能读到最新库存。没有这行两个请求同时读到库存 100各卖 80最后库存变成 20 而不是 -60这在进销存里是严重事故。销售单价不是从前端传来的而是从商品表里取sale_price这一点很关键否则店员改一下请求里的价格就能低价买走货。4.3 退货与报损库存回补的边界条件退货分两种情况供应商退货给采购方以及顾客退货给超市。前一种要减少库存后一种要增加库存它们对应inventory_log里不同的type值。这套源码里把退货做成对原单的反向操作但并没有真的删除原单而是新增一条负数量的流水。实现逻辑一般是Transactional public void returnGoods(Long orderItemId, int quantity) { SaleOrderItem saleItem saleOrderItemMapper.selectById(orderItemId); if (saleItem null || quantity 0) { throw new RuntimeException(无效的退货明细); } if (saleItem.getQuantity() quantity) { throw new RuntimeException(退货数量超过原销售数量); } productMapper.increaseStock(saleItem.getProductId(), quantity); inventoryLogMapper.insert(new InventoryLog( saleItem.getProductId(), 退, quantity, productMapper.selectById(saleItem.getProductId()).getStock(), 销售退货)); }这里最容易踩的坑是「退货数量超过原销售数量」。如果代码里只判断quantity 0漏掉saleItem.getQuantity() quantity这个条件退货 1000 件但原单只卖 1 件库存瞬间被刷上去盘点就彻底没法做了。报损的逻辑同理只是它没有原单关联单独走一个Type报损的流水库存减少。4.4 库存报表统计 SQL 的聚合逻辑库存报表是进销存系统里老板看得最多的页面它统计的是某段时间内每种商品的进货量、销售量、当前库存。如果直接用 MySQL 的 SUM 聚合很容易忽略一个细节必须先按商品分组再聚合否则多张表 join 后数据会翻倍。比较稳的查询方式是分开统计两张明细表然后再合并SELECT p.id, p.name, p.stock, IFNULL(s.total_sale, 0) AS sale_qty, IFNULL(pu.total_purchase, 0) AS purchase_qty FROM product p LEFT JOIN ( SELECT product_id, SUM(quantity) AS total_sale FROM sale_order_item WHERE create_time BETWEEN 2025-01-01 AND 2025-01-31 GROUP BY product_id ) s ON p.id s.product_id LEFT JOIN ( SELECT product_id, SUM(quantity) AS total_purchase FROM purchase_order_item WHERE create_time BETWEEN 2025-01-01 AND 2025-01-31 GROUP BY product_id ) pu ON p.id pu.product_id ORDER BY p.stock ASC;这里用LEFT JOIN而不用INNER JOIN是为了把没有销售也没有进货的商品也保留下来否则库存为 0 但没流水记录的商品会从报表里消失。聚合时间是闭区间BETWEEN默认包含两个端点你在自定义起始日期时要注意这一点否则月初和月末的流水会被重复计算。5. 避坑排查部署这套进销存源码常见的五个翻车现场5.1 数据库连接失败时区、字符集、驱动版本现象后端启动时报The server time zone value йʱ is unrecognized或者Public Key Retrieval is not allowed。原因MySQL 8.0 驱动要求连接串里明确声明时区开头的?参数顺序错了也会导致后续参数失效。解决把application.yml里的 URL 改成下面这种完整写法并且把时区参数放在最前面url: jdbc:mysql://localhost:3306/db_supermarket?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8useSSLfalseallowPublicKeyRetrievaltrue如果你的 MySQL 是 5.7驱动类可以继续用com.mysql.cj.jdbc.Driver但建表 SQL 要避免使用 8.0 独有的语法。连接成功后用SHOW VARIABLES LIKE character%;检查数据库字符集是否为utf8mb4不是的话重新改库。5.2 前端请求 404路由模式与代理路径现象登录页能打开但点击登录后一直转圈F12 看到请求/api/login返回 404 或Failed to load resource。原因前端请求路径与后端 Controller 的路径不匹配或者代理没把/api前缀剥掉。解决先看后端接口的RequestMapping值如果 Controller 里写的是/login那么前端请求应该是http://localhost:8080/login而不是/api/login。一个常见做法是在后端的application.yml里给所有接口加统一前缀或者在vue.config.js的代理里把/api重写为空路径proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } }这样前端用/api/login转发后到达后端就是/login。注意项目里可能已经有pathRewrite改动前先确认后端是否已经配置了 context-path。5.3 库存变成负数事务与锁现象并发测试时两个销售请求同时卖同一个商品库存更新后变成了负数。原因UPDATE product SET stock stock - #{quantity}自身是原子操作但如果提前用select stock做了判断且判断和更新之间没有加锁两个事务会同时读到旧库存。解决在判断库存之前用SELECT ... FOR UPDATE锁住商品行见 4.2 里的selectByIdForUpdate。如果源码里没有这个方法可以在 Mapper 接口自己加Select(SELECT * FROM product WHERE id #{id} FOR UPDATE) Product selectByIdForUpdate(Long id);这里要强调的是行锁必须发生在事务里如果Transactional没加锁在方法结束后立即释放等于没锁。另外注意FOR UPDATE要放在 SQL 语句末尾前面如果有GROUP BY会失效。5.4 图片上传失败静态资源映射现象上传商品图片报Failed to load resource: the server responded with a status of 404但本地路径下文件已经存在。原因Spring Boot 默认只处理classpath:/static/下的静态资源自定义上传目录不在这个范围内需要手动映射。解决在application.yml或配置类里加静态资源映射我是直接在配置类里写的Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:/your/path/upload/); } }/your/path/upload/改成你自己的绝对路径末尾的斜杠不能少。前端图片回显时URL 要用http://localhost:8080/upload/xxx.jpg而不是相对路径否则前端 8081 找不到这些图。5.5 乱码问题Java 和 MySQL 编码现象控制台日志中文正常但入库后商品名称变成???或者前端页面显示乱码。原因连接 URL 没声明characterEncodingutf8或者数据库表本身不是utf8mb4字符集。解决先确认数据库和表的字符集再确认连接串。我一般建表前直接给整个库指定字符集命令行执行ALTER DATABASE db_supermarket CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE product CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;后端代码里request.setCharacterEncoding(UTF-8)在 Spring Boot 下可以由配置完成server: servlet: encoding: charset: UTF-8 enabled: true force: true如果改完还是乱码重启后端和前端清浏览器缓存因为 Vue 的index.html可能被本地缓存了旧内容。6. 进阶改造成本核算从一张表开始的几个切入点先来说这套源码做成什么样才算「值」。它给你的不是一个只能交作业的演示项目而是一个可以往上叠业务的地基。我拿到手之后第一个改造是成本核算。基础版没有成本字段销售毛利只能拿销售价减进货价算但进货价会变同一种商品两次进货价格不同库存混在一起后卖出去的货到底该扣哪一批的进价就成了问题。我的做法是引入移动加权平均成本。需要新增一个字段avg_cost到product表然后在每次进货确认时用当前库存和新进货成本重新计算平均成本Transactional public void updateAverageCost(Long productId, int quantity, BigDecimal cost) { Product p productMapper.selectById(productId); BigDecimal oldTotal p.getAvgCost().multiply(BigDecimal.valueOf(p.getStock() - quantity)); BigDecimal newTotal cost.multiply(BigDecimal.valueOf(quantity)); BigDecimal newStock BigDecimal.valueOf(p.getStock()); p.setAvgCost(oldTotal.add(newTotal).divide(newStock, 2, RoundingMode.HALF_UP)); productMapper.updateById(p); }这段代码要在进货入库事务里和库存更新放在同一个事务中。这里容易翻车的是计算旧库存总值时必须从stock里先扣掉本次进货的数量否则oldTotal会把本次进货也算进去平均成本算错。第二个值得改的是多门店库存。进销存系统一旦从单店走向连锁product.stock这种单库存字段就不够用了。正确做法是新建一张store_product_stock表把stock从product表挪过去用store_id product_id做联合主键。销售扣库存时SQL 的WHERE条件要同时带上门店 ID这比在product表加store_id列更合理不然每个商品在每个门店一条记录商品表会被撑爆。如果你只是想要一套能快速做课设的后台管理模板这套源码的登录、权限拦截、CRUD 封装也够直接抄。MyBatis-Plus的BaseMapper让单表增删改查零 SQL你只需要把实体类和前端页面复制过去改一下字段名就能搭起一个图书管理或员工管理系统。这也是我一直觉得进销存源码比普通论坛源码有价值的原因它的业务复杂度刚好够展示事务、锁、聚合查询这些后端核心技能又不会复杂到看不懂。最后留一个习惯给想改造的人我每次拿到一份进销存源码都会先跑到成品状态、再做一次完整的进货-销售-退货最后才动手改代码。为什么因为只有先确认原样是好的才能确定后面出的问题是你自己改出来的。希望这套流程能帮你把源码真正跑通省下几个被坑到的晚上。本文还有配套的精品资源点击获取