简介:一套基于Java与RuoYi框架二次开发的KS进销存系统设计源码,面向需要从手工账目转向信息化管理的小型经销商,也适合Java学习者作为企业级项目参考。系统以采购、销售、库存三大核心模块为主线,流程设计贴近小公司日常操作,比传统专业进销存系统更简洁高效,能快速上手并落地使用。源码包共936个文件,包含304个Java源代码文件、149个JavaScript脚本、101个Vue组件,另有HTML页面、CSS样式、XML配置及SQL脚本等,压缩包约10.94MB。其中Vue与Java分层清晰,便于理解前后端交互和RuoYi的二次开发方式;GIF演示图与PNG/SVG资源也有助于快速掌握界面布局。目前已有837人浏览学习,适合正在规划系统选型或打算基于RuoYi定制进销存功能的开发人员。
1. 基于Java和RuoYi的KS进销存系统:这个二次开发项目解决什么问题
RuoYi框架的代码生成器十分钟能给你一套采购单、销售单的增删改查页面,但真正能上线的进销存系统要处理的是另一件事:每一次入库都要改变库存余额,每一次出库都要扣减可用量,单据、流水、库存三套数据必须对得上账。基于Java和RuoYi二次开发的KS进销存系统,正是要在RuoYi已有的用户权限、字典、日志和代码生成基础上,把商品档案、仓库、库存快照、采购与销售单据、库存流水这些业务层补出来。它适合两类人:刚接手进销存项目的Java后端,想看懂框架到业务怎么过渡;以及正在评估二次开发成本的团队,想明确在RuoYi上长出一个垂直业务模块到底要动哪些东西、哪里是深水区。下文按从设计到部署再到改业务代码的顺序,把这个项目的可复现路径完整讲一遍。
2. 基于RuoYi做进销存的设计:选型理由、四层数据模型、库存账一致性
把KS进销存当成一个从RuoYi派生出来的业务系统看待时,第一件事不是写代码,而是把框架与业务之间的边界划清楚。RuoYi能提供的是通用组织能力,进销存业务需要的是商品、仓库、单据和库存流水。两者各管一段,才是这个二次开发项目的核心价值。
2.1 为什么选RuoYi-Vue而不是自己搭SpringBoot+MyBatis脚手架
我见过不少团队从零搭脚手架,登录、权限、菜单三件套就耗掉两周,还没碰到进销存的一个业务表。RuoYi-Vue这套技术栈把这个成本提前付掉了,它内置了Spring Security + JWT认证、用户角色权限、部门数据权限、操作日志、字典管理、定时任务和代码生成器,拿来做进销存等于直接在毛坯房里拿到了水电和墙体。
| 对比项 | 自建SpringBoot+Security脚手架 | 基于RuoYi-Vue二次开发 |
|---|---|---|
| 登录认证 | JWT、密码加密、拦截器全部自己写 | 已内置,直接在Security配置上扩展 |
| 权限模型 | 用户-角色-菜单多表关系自己设计 | 已内置,菜单与权限标识绑定 |
| 部门数据权限 | 手写SQL条件拼接 | 注解配合MyBatis自动拼装 |
| 代码生成 | 无,单表CRUD重复劳动 | 内置生成器,单表单模块快速出壳 |
| 系统管理页面 | 需要自己开发 | 用户、角色、菜单、字典开箱即用 |
为什么会选这套组合而不是自己攒,还有一个更现实的理由:进销存的权限边界几乎天然和部门挂钩。一个门店只能看自己的库存,仓库管理员只能操作本仓单据,RuoYi的数据权限注解加上部门关联表就能解决,自己实现这些过滤条件很容易漏掉场景。
但选型不只看省事,还要看边界。RuoYi代码生成器只面向单表生成简单增删改查,进销存里的库存预占、可用量校验、单据主从表事务,生成器一概不负责。这也是标题里写明“二次开发”的原因:框架提供的是壳,库存事务这些业务核心必须自己在Service层重写。否则项目会停在“能录入”的阶段,而离“能记账”差得很远。
2.2 四层表结构:商品、仓库、库存快照、单据与流水
进销存的表设计可以拆成四层:档案层、单据层、流水层、库存层。档案层是商品、仓库、供应商和客户;单据层是采购单、销售单以及各自的明细子表;流水层记录每一次库存变动;库存层保存每个商品在每个仓库的当前余额。分清这四层,再看项目源码里的表就顺了。
-- 商品档案 CREATE TABLE ks_goods ( goods_id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID', goods_code varchar(32) NOT NULL COMMENT '商品编码', goods_name varchar(128) NOT NULL COMMENT '商品名称', category_id bigint(20) DEFAULT NULL COMMENT '商品分类ID', spec varchar(64) DEFAULT NULL COMMENT '规格型号', unit varchar(16) DEFAULT NULL COMMENT '计量单位', purchase_price decimal(10,2) DEFAULT 0.00 COMMENT '采购单价', sale_price decimal(10,2) DEFAULT 0.00 COMMENT '销售单价', status char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', create_by varchar(64) DEFAULT '', create_time datetime DEFAULT NULL, update_by varchar(64) DEFAULT '', update_time datetime DEFAULT NULL, PRIMARY KEY (goods_id), UNIQUE KEY uk_goods_code (goods_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品档案';goods_code加唯一索引是必须的,进销存里商品编码重复意味着两套货混成一笔账。purchase_price和sale_price用decimal(10,2)而不是double,避免浮点误差在月末对账时变成玄学问题。
-- 库存快照 CREATE TABLE ks_stock ( id bigint(20) NOT NULL AUTO_INCREMENT, goods_id bigint(20) NOT NULL COMMENT '商品ID', warehouse_id bigint(20) NOT NULL COMMENT '仓库ID', quantity decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT '可用库存', locked_quantity decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT '锁定库存', version int(11) NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_goods_warehouse (goods_id, warehouse_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存快照';ks_stock中goods_id和warehouse_id唯一,因为同一商品在同一仓库只能有一条余额记录。locked_quantity是给销售预占用的,下单先锁库存,出库再真正扣减,这套模型在进销存项目里最常见。
单据层一般是主从表,采购单主表存单号和结算信息,明细表存商品和数量;销售单同理。流水层的ks_stock_log至少要有goods_id、warehouse_id、变更前数量、变更后数量、业务类型、业务单号,这样每一笔库存变动都能追溯到源头。
2.3 库存余额一致性:快照表加流水表,事务边界在Service
库存计算有两种常见做法:一种是每次查询都去流水表SUM汇总,另一种是维护一张库存快照表。实时汇总在产品初期数据量小时还跑得动,一旦单据量上来,日报表接口就会变成灾难。KS项目使用的是快照表加流水表的组合,每次库存变动时同时更新余额并写流水,查询余额只需读一条记录。
一致性靠的是事务边界,不是靠运气。一张销售单可能包含五个商品,五条库存记录必须同时扣减成功,或者同时回滚。所以库存变更逻辑要收敛到一个Service方法里,不能在Controller里逐条调用,否则前面四条成功、第五条失败时没有统一回滚点。
@Service public class KsStockService { @Autowired private KsStockMapper ksStockMapper; @Autowired private KsStockLogMapper ksStockLogMapper; @Transactional(rollbackFor = Exception.class) public void saleOut(Long warehouseId, String orderNo, List<KsSaleOrderItem> items) { for (KsSaleOrderItem item : items) { KsStock stock = ksStockMapper.selectStockForUpdate(item.getGoodsId(), warehouseId); if (stock == null || stock.getQuantity().compareTo(item.getQuantity()) < 0) { throw new ServiceException("商品库存不足:" + item.getGoodsId()); } BigDecimal before = stock.getQuantity(); BigDecimal after = before.subtract(item.getQuantity()); stock.setQuantity(after); KsStockLog log = new KsStockLog(); log.setGoodsId(item.getGoodsId()); log.setWarehouseId(warehouseId); log.setChangeType("SALE_OUT"); log.setChangeQty(item.getQuantity().negate()); log.setBizNo(orderNo); log.setBeforeQty(before); log.setAfterQty(after); ksStockMapper.updateById(stock); ksStockLogMapper.insert(log); } } }selectStockForUpdate对应Mapper里的SELECT ... FOR UPDATE,目的是在当前事务内锁住这条库存记录,防止并发情况下的超卖。updateById之后马上写流水,两个操作在同一个事务中,任何一个异常都会整体回滚。参数中warehouseId和orderNo是必须的,流水表靠它们检索,如果缺少这两个字段,后续报表和审计都无从查起。
进销存业务里流水表往往要防重复,也就是同一个业务单号不能重复扣减两次。常见做法是在ks_stock_log上加业务单号和商品ID的联合唯一索引,提交时如果遇到DuplicateKey异常,说明这张单已经处理过,直接返回人工确认。
3. 本地跑通KS进销存:环境清单、数据库初始化与启动排查
拿到这套源码后,我不建议先翻业务代码,而是先把项目跑起来,再对照页面和日志去理解表结构。跑通的过程中会遇到环境版本、数据库脚本、前端依赖三关,每一关都有典型的翻车位置,下面按顺序展开。
3.1 环境清单与版本选择:JDK、MySQL、Redis、Node
这套基于Java和RuoYi的进销存项目,后端是Spring Boot + MyBatis,前端是Vue + ElementUI,Redis承担会话和缓存。版本选择上我建议保守,用社区验证过的主流版本,不要一开始就去试最新的Java版本。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | JDK 8 或 JDK 11 | JDK 8最稳定,JDK 11兼容性也没问题 |
| Maven | 3.6以上 | 配阿里云镜像加速依赖下载 |
| MySQL | 5.7或8.0 | 字符集必须utf8mb4 |
| Redis | 5.x或6.x | 默认无密码即可跑通 |
| Node.js | 16.x | 前端Vue CLI项目的老版本依赖需要它 |
| Nginx | 1.20以上 | 生产部署才需要,本地开发可跳过 |
JDK版本是第一个容易出错的地方。如果本机默认装了JDK 17,Spring Boot 2.x项目启动时可能出现“Illegal reflective access”警告甚至直接失败,所以优先切到JDK 8。Node版本同理,前端依赖中的node-sass对高版本Node不友好,安装失败时先用nvm切到16.x再执行npm install。本地调试阶段Nginx可以不装,Vue开发服务器自带代理转发能力。
3.2 数据库初始化:建库、执行基础脚本与业务脚本
源码包的sql目录里通常会按顺序存放初始化脚本,第一个是RuoYi框架的基础脚本,包含用户、角色、菜单、字典等系统表,后面是KS进销存的业务脚本,以ks_前缀的表为主。执行顺序不能反,业务表里会引用系统表的用户和字典数据,先建业务表后执行基础脚本会报外键或字典缺失。
mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS ks_erp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p ks_erp < sql/ruoyi_base.sql mysql -uroot -p ks_erp < sql/ks_business.sql执行时不要在命令行直接拼接数据库密码,使用-p参数后回车输入,避免密码留在shell history里。字符集务必保持utf8mb4,进销存商品名称里如果有生僻字或多字节字符,utf8字符集在排序和导入时会出现乱码。导入完成后,用mysql客户端查看sys_user表和ks_goods表数据是否存在,确认脚本不是空跑。
这一步常见的问题是脚本默认的admin账号密码不对。RuoYi基础脚本默认会创建admin用户,密码通常是加密串,登录页输入admin和默认密码即可。如果登录时报密码错误,先确认执行的是完整脚本,而不是只导入了部分业务表。
3.3 后端启动:改数据源、Redis、端口,看日志定位问题
后端是Maven多模块工程,主启动类在ruoyi-admin模块下。启动前必须修改两个配置文件,一个是数据源配置,一个是Redis配置。RuoYi的配置默认集中在application-druid.yml和application.yml里,先改数据源指向刚才建好的ks_erp库。
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql://localhost:3306/ks_erp?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: "换成你的数据库密码"url里的characterEncoding=utf8很重要,否则中文商品名插入后变成问号。serverTimezone指定为Asia/Shanghai,避免MySQL驱动把时间按UTC解释导致日志时间和本地区差八小时。
spring: redis: host: localhost port: 6379 password: database: 0Redis如果是本地默认安装且没设置密码,password留空即可。端口保持默认8080,如果本机8080被占用,在application.yml中改server.port。
cd ruoyi-admin mvn spring-boot:run启动过程中重点看控制台最后一段日志。看到“Started RuoYiApplication”说明后端起来正常;如果卡在数据源初始化,优先检查数据库账号密码和网络连通性;如果报“Unable to connect to Redis”,先确认本机redis-server进程是否在运行,用redis-cli ping验证。
3.4 前端启动:npm install、代理配置与登录验证
前端是一个Vue工程,通常目录名是ruoyi-ui。拿到源码后先安装依赖,再用开发服务器启动。Vue CLI项目中,我在暗框下(此处无涉敏感网络内容),但注意:依赖安装速度慢可以切换npm镜像源解决,不建议使用任何非官方代理工具。
cd ruoyi-ui npm install npm run dev启动后开发服务器会监听一个本地端口,例如8080或1024。Vue项目通过vue.config.js里的proxy字段把/api请求转发到后端8080端口,所以后端必须先启动,前端登录才能通。登录页输入admin和基础脚本里的默认密码,能跳转到首页且菜单正常渲染,就说明前后端联调已经打通。
如果npm install阶段一直报node-sass相关的编译错误,大概率是Node版本过高。先用nvm切换Node到16.x,删除node_modules目录重新安装。前端跑起来后菜单是空的,优先检查后端的菜单权限是否分配给admin角色,这通常不是前端问题,而是后端数据没配完整。
4. 进销存二次开发核心步骤:从代码生成器到库存事务重写
跑通存量代码只算开始,真正的二次开发工作量在业务模块。KS进销存的核心功能分三块:商品档案、采购入库、销售出库,以及库存报表。下面把这三块的落地路径拆开讲,每步都能直接复用。
4.1 先用代码生成器落地商品档案:表结构、生成配置、改造点
商品档案是最适合用RuoYi代码生成器处理的部分。只要MySQL里建好表,登录系统后在系统工具菜单下的代码生成页面点击导入,选择ks_goods这张表,RuoYi会读取表字段元数据,生成对应的Controller、Service、Mapper、实体类和前端Vue页面。
-- 代码生成器依赖的表结构,字段必须有注释 ALTER TABLE ks_goods ADD COLUMN remark varchar(500) DEFAULT NULL COMMENT '备注';生成器读取的是字段注释和类型,所以建表时comments必须写清楚;remark字段会被生成器识别为文本域,在表单页展示成多行输入框。生成之前把status字段的字典类型设置为sys_normal_disable,前端会自动渲染成启用停用开关,不用手写下拉框。
生成代码的包名和模块名要按项目规范设置。包名用com.ruoyi.ks,模块名用ks,业务名用goods,这样生成的类路径是controller/ks/KsGoodsController,前端页面落在views/ks/goods/index.vue。生成后不要直接用zip里的文件覆盖项目,把java文件放到ruoyi-admin模块对应的业务包,前端文件放到ruoyi-ui/src/views/ks目录。
生成器给出的Controller天然带权限标识,注解上会自动生成@PreAuthorize("@ss.hasPermi('ks:goods:list')")之类的内容。这部分可以直接复用,但Service层逻辑基本是空壳,selectKsGoodsList只是简单条件查询,没有库存联动,后续必须补。我的习惯是生成器只用来出壳,把查询逻辑和字段校验的手写部分留给业务实现,不要经过任何手工调整直接部署。
4.2 库存事务重写:Service层用FOR UPDATE锁住行、同事务写流水
商品档案能用生成器,库存变动不能。采购入库、销售出库这种操作同时涉及主表、明细表、库存表、流水表,必须用事务包起来。我在2.3节已经给出了saleOut方法的骨架,这里补全配套的Mapper语句和Controller调用方式。
<select id="selectStockForUpdate" resultType="com.ruoyi.ks.domain.KsStock"> SELECT id, goods_id, warehouse_id, quantity, locked_quantity, version FROM ks_stock WHERE goods_id = #{goodsId} AND warehouse_id = #{warehouseId} FOR UPDATE </select>FOR UPDATE加在库存查询上,锁的粒度是一条商品加仓库的记录。两个并发请求同时扣减同一个商品的库存时,第一个事务拿到锁,第二个事务必须等第一个提交后才能继续读取,这样就不会出现两个请求都读到100件、各扣50件、最后库存剩50的乌龙。要注意这个语句必须在事务内执行,如果Service方法没有加@Transactional,锁在语句执行完就释放了,锁不住并发。
Controller层只做参数组装和调用,不直接操作库存对象。常见错误是在Controller里先查库存判断再调Service扣减,两个操作被拆成两次独立调用,中间一旦有并发插入,判断就会失效。正确写法是把校验和扣减都收进saleOut方法内,Controller只需要把订单明细传进来,剩余问题在事务里解决。
采购入库的逻辑和saleOut对称,区别是把quantity加上去。需要注意stock为null的情况,也就是仓库里还没有这个商品的库存记录时,要新建一条并insert,而不是对空对象做add操作。流水表里changeQty用负数表示出库,正数表示入库,报表汇总时按正负值直接累加即可。
4.3 库存和报表的索引与分页优化:联合索引、PageHelper、汇总表
进销存系统跑半年后单据量上涨,最容易慢的接口是库存流水列表和销售报表。这两个查询共同的特点是条件固定、扫描范围大,优化思路也很明确:联合索引加上汇总表。
-- 库存流水查询条件:商品、仓库、时间范围 ALTER TABLE ks_stock_log ADD INDEX idx_goods_warehouse_time (goods_id, warehouse_id, create_time); -- 销售单据列表:状态加创建时间 ALTER TABLE ks_sale_order ADD INDEX idx_order_status_time (status, create_time);index_goods_warehouse_time的字段顺序是商品在前、仓库其次、时间最后,匹配查询条件中最常出现的三个字段。如果某个查询只按仓库和时间过滤,这个索引也能走,但效果会打折扣,因为查询优化器要从索引的第一个字段开始匹配。MyBatis分页插件用的startPage()会在执行前自动改写SQL,分页字段必须和order by顺序一致,否则会出现排序字段没有索引导致文件排序的情况。
单据量超过几十万条以后,实时聚合销售报表不合适。我的做法是增加一张销售日汇总表,每天用定时任务把前一天的单据按商品、仓库、日期汇总一次,报表查询直接读汇总表。月末结算时如果发现汇总数据和明细对不上,再查流水修正,这样日报告接口的响应时间能稳定在200毫秒内。
-- 日汇总表 CREATE TABLE ks_sale_daily_summary ( id bigint(20) NOT NULL AUTO_INCREMENT, summary_date date NOT NULL, goods_id bigint(20) NOT NULL, warehouse_id bigint(20) NOT NULL, sale_qty decimal(14,2) NOT NULL DEFAULT 0.00, sale_amount decimal(14,2) NOT NULL DEFAULT 0.00, PRIMARY KEY (id), UNIQUE KEY uk_date_goods_warehouse (summary_date, goods_id, warehouse_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售日汇总表';汇总表ID中unique key的三个字段就是定时任务写入时的去重依据,重复执行不会累计两份数据。采用汇总表后,日报、月报、商品维度分析都可以在一张表内完成,大幅减轻订单明细表的查询压力。
5. KS进销存二次开发避坑指南:超卖、数据越权、缓存不一致等5个现场
这一章整理的是实际开发中最容易翻车的五个场景,每条都按现象、原因、解决三个层次写。这些都是做完这套KS进销存后反复回踩过的点,希望你能直接跳过。
5.1 库存超卖:SELECT FOR UPDATE被漏掉,生成器代码扛不住并发
现象:销售出库接口在并发测试10个请求时,库存显示还剩,但实际录入了15个商品的出库单。
原因:库存扣减前用的是普通select查询,两条线程同时读到库存剩余10件,各自判断足够扣5件,然后都执行update,后一条把前一条的结果覆盖,库存最终变成0而不是期望的5。生成器生成的代码根本没有行锁概念,只有简单的CRUD替换。
解决:把库存查询改成FOR UPDATE锁定对应记录,并把扣减逻辑放进统一的@Transactional方法里。行锁生效的前提是这个查询必须建立在数据库连接上,如果用了MyBatis缓存或查询结果被覆盖,锁也不起作用。线上出现超卖时,第一排查清单是:Service方法有没有事务、Mapper有没有FOR UPDATE、事务隔离级别有没有被改。
5.2 数据权限失效:只看到本部门数据的系统变成了全量可见
现象:仓库管理员登录后,销售订单列表把其他门店的单据全部显示出来了,过滤条件形同虚设。
原因:RuoYi的数据权限是靠@DataSource注解和SQL中拼接的dept_id条件实现的。业务表如果没有保存dept_id字段,或者查询时没有join sys_dept表,数据权限过滤条件会被静默跳过。
解决:在业务主表增加dept_id字段,插入单据时用当前登录用户所属部门赋值。查询方法上标注@DataScope(deptAlias = "d"),Mapper的SQL必须带上系统默认的data_scope拼接片段。最稳妥的验证方式是开两个不同部门的测试账号,分别登录后确认看不到对方数据,而不是只看admin账号。
5.3 前端菜单404:动态路由名称不一致,代码生成后没生成菜单
现象:新建的商品档案菜单点击后跳转404,控制台提示找不到路由组件。
原因:RuoYi前端路由是动态生成的,菜单表里配的component路径必须和前端views目录下的文件路径严格一致。代码生成器默认生成到business/goods/index.vue,但菜单配置里写成了goods/index,路径对不上自然找不到组件。
解决:先在系统管理菜单里创建目录,component填业务模块的完整路径,例如business/goods/index,再去创建具体菜单项。菜单创建后清除浏览器缓存重新登录,让动态路由重新加载。如果修改过前端组件名,同时要检查Vue文件里的name属性是否和菜单表的路由名称一致,两处对不上也会渲染失败。
5.4 主单提交成功、明细插入失败:事务失效现场
现象:采购单接口返回成功,但查看明细表发现少了几条,库存却已经增加了对应数量。
原因:Service方法被拆分成了两次调用,Controller先调用insertPurchaseOrder保存主单,再循环调用insertPurchaseOrderItem保存明细。第二步异常时,第一步的事务已经提交,没有统一回滚点。
解决:把主单和明细的保存收敛到一个Service方法中,在这个方法上标注@Transactional(rollbackFor = Exception.class)。尤其注意rollbackFor要写Exception.class,Spring默认只在RuntimeException时回滚,如果业务抛的是检查异常,不加参数就会提交成功。事务失效的另一个常见触发点是同类内部方法调用,A方法调B方法,B的注解不生效,因为Spring代理拦截不到内部调用。
5.5 报表查询越来越慢:缺联合索引、实时聚合、分页字段不一致
现象:销售报表按日期过滤一个月的数据,接口响应时间从300毫秒涨到6秒。
原因:订单明细表数据量增长后,按create_time范围扫描需要遍历全表,同时还在查询中对商品表做了关联join,临时表计算量大增。
解决:第一步给明细表加联合索引,字段顺序按where条件中最常出现的组合设计;第二步把实时聚合改成日汇总表;第三步检查分页SQL里的order by字段是否走索引。执行计划里看到Using temporary或Using filesort就说明排序没走索引,需要调整分页字段或重新设计索引顺序。日报表查询慢时,优先排查的是汇总表是否被定时任务更新,而不是在查询层继续堆条件。
6. 收尾验证:把库存事务护住,压测才敢上线
6.1 用@Transactional测试保护核心Service逻辑
进销存系统的核心逻辑集中在库存变更方法里,这部分改动频率高、影响范围大,没有自动化测试兜底我不敢上线。我的习惯是给库存扣减方法写一个独立测试类,用@Transactional让测试数据在运行后自动回滚,不污染真实库存。
@RunWith(SpringRunner.class) @SpringBootTest @Transactional public class KsStockServiceTest { @Autowired private KsStockService ksStockService; @Autowired private KsStockMapper ksStockMapper; @Test public void testSaleOutShouldReduceStock() { // 准备一条库存记录 KsStock stock = new KsStock(); stock.setGoodsId(1L); stock.setWarehouseId(1L); stock.setQuantity(new BigDecimal("100")); ksStockMapper.insert(stock); // 构造销售明细,数量为10 KsSaleOrderItem item = new KsSaleOrderItem(); item.setGoodsId(1L); item.setQuantity(new BigDecimal("10")); ksStockService.saleOut(1L, "TEST20240001", Collections.singletonList(item)); // 验证扣减后为90 KsStock after = ksStockMapper.selectOne( new LambdaQueryWrapper<KsStock>() .eq(KsStock::getGoodsId, 1L) .eq(KsStock::getWarehouseId, 1L)); Assert.assertEquals(0, new BigDecimal("90").compareTo(after.getQuantity())); } }这个测试类里每个方法结束后事务回滚,所以插入的库存记录不会留在库里。测试用的商品ID和仓库ID可以任意指定,只要在方法内先插入即可,不用依赖环境数据。参数中TEST20240001是业务单号,测试过后无需关心流水是否存在,回滚时一并清掉。库存量的比较用compareTo而不是equals,避免BigDecimal的精度差异导致测试误报。
6.2 简单压测与上线前复核
单元测试之外,我至少做一轮并发压测再往外放。压测脚本不需要复杂,JMeter或Postman集合都可以,核心场景是销售出库接口并发20个线程,每个线程扣减同一个商品的库存,验证最终库存等于初始值减去扣减总量,不多扣不少扣。
上线前复核还有几个琐碎但关键的细节:把每天定时汇总任务的执行日志留全,把流水表里业务单号唯一索引加上,把数据权限测试账号过一遍。曾经在上线后三天才发现仓库管理员能看到别仓单据,就是因为测试只用了admin账号,验证模型不完整。现在我把这步固定成上线清单里的必检项。
这套基于Java和RuoYi的KS进销存系统,我做下来最大的感受是:框架能省的时间,它都省了;框架省不了的时间,全在库存事务和数据一致性上。把核心Service用测试护住,把并发扣减用行锁挡好,这套系统才真正拿得出去。希望帮到你。
本文还有配套的精品资源,点击获取