☰
Spring Boot+Vue超市进销存源码实战:部署避坑与改造
2026/10/6 3:22:02 网站建设 项目流程

简介:面向超市零售场景的进销存管理系统完整源码,适合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 到 16,Vue 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 用的是正确的 JDK;node -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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true 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: true

URL 里的serverTimezone=Asia/Shanghai是很多人都踩过的地方,不写时 JDBC 驱动会拿不到正确的时区,连接直接报错。allowPublicKeyRetrieval=true是为了兼容 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?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true

如果你的 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 没声明characterEncoding=utf8,或者数据库表本身不是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,你只需要把实体类和前端页面复制过去,改一下字段名就能搭起一个图书管理或员工管理系统。这也是我一直觉得进销存源码比普通论坛源码有价值的原因:它的业务复杂度刚好够展示事务、锁、聚合查询这些后端核心技能,又不会复杂到看不懂。

最后留一个习惯给想改造的人,我每次拿到一份进销存源码,都会先跑到成品状态、再做一次完整的进货-销售-退货,最后才动手改代码。为什么?因为只有先确认原样是好的,才能确定后面出的问题是你自己改出来的。希望这套流程能帮你把源码真正跑通,省下几个被坑到的晚上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询