简介一套基于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而不是自己搭SpringBootMyBatis脚手架我见过不少团队从零搭脚手架登录、权限、菜单三件套就耗掉两周还没碰到进销存的一个业务表。RuoYi-Vue这套技术栈把这个成本提前付掉了它内置了Spring Security JWT认证、用户角色权限、部门数据权限、操作日志、字典管理、定时任务和代码生成器拿来做进销存等于直接在毛坯房里拿到了水电和墙体。对比项自建SpringBootSecurity脚手架基于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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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, ListKsSaleOrderItem 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 ElementUIRedis承担会话和缓存。版本选择上我建议保守用社区验证过的主流版本不要一开始就去试最新的Java版本。组件推荐版本说明JDKJDK 8 或 JDK 11JDK 8最稳定JDK 11兼容性也没问题Maven3.6以上配阿里云镜像加速依赖下载MySQL5.7或8.0字符集必须utf8mb4Redis5.x或6.x默认无密码即可跑通Node.js16.x前端Vue CLI项目的老版本依赖需要它Nginx1.20以上生产部署才需要本地开发可跳过JDK版本是第一个容易出错的地方。如果本机默认装了JDK 17Spring 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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 换成你的数据库密码url里的characterEncodingutf8很重要否则中文商品名插入后变成问号。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 idselectStockForUpdate resultTypecom.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 /selectFOR 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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.classSpring默认只在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 LambdaQueryWrapperKsStock() .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用测试护住把并发扣减用行锁挡好这套系统才真正拿得出去。希望帮到你。本文还有配套的精品资源点击获取