☰
基于RuoYi二次开发进销存系统:库存流水与表结构设计
2026/10/18 11:46:44 网站建设 项目流程

简介:基于Java和RuoYi框架二次开发的KS进销存系统源码,为小型经销商提供贴合自身业务流程的采购、销售、库存管理方案。系统特意弱化传统进销存软件的复杂逻辑,强调简洁直观,适合业务转型中的中小企业及希望学习实际项目结构的Java开发者。整个资源包共936个文件,其中Java源码304个、Vue组件101个、JavaScript脚本149个,另有SVG、PNG等素材及SQL、YML等配置,压缩包仅10.94MB,便于快速下载。当前已有837人学习使用。通过这套源码可完整了解RuoYi脚手架的改造方式、进销存核心功能的前后端实现,以及项目打包与启动脚本的使用方法;清晰的目录层级和多样化的文件类型,也为二次开发和业务扩展提供了便利。

1. 当业务负责人递来一份“三天上线”的进销存需求,我劝你先想清楚库存这张底牌

做过几个后台管理系统的人,多半会觉得基于Java和RuoYi二次开发一个进销存系统只是“加几张表、写几个CRUD、套个模板”的体力活。我第一次接手KS进销存系统的设计源码时也这么想,直到采购入库单保存成功、库存余额纹丝不动,销售出库把库存扣成了负数,我才意识到进销存和普通后台系统的本质区别:库存不是一张列表,而是一连串交易不断加减后留下的余额。你在界面上看到的是商品数,数据库里必须有一张流水账记录每一次变动的原因、单据号和时间。这份KS进销存系统设计源码的核心价值,不在于把界面做得多漂亮,而在于如何用RuoYi提供的权限、字典、代码生成器这些底座,把采购、销售、库存三个环节串成一条不丢数据、不产生负库存的完整链路。适合已经跑通过RuoYi框架、准备接手进销存或供应链类业务的人员来读。下面我从二次开发的边界、表结构、核心业务流程、高频踩坑四个维度完整拆一遍,附带能直接落地的代码片段。

2. 先认清RuoYi二次开发的边界:框架白送什么,进销存必须自己造什么

2.1 RuoYi工程骨架先跑通:这四层代码分别管什么事

RuoYi的典型单体工程结构从上到下分四块:admin是Web入口层,放Controller和启动类;framework是框架配置层,放Shiro安全配置、拦截器、数据源配置;system是系统模块层,放用户、角色、菜单、字典这类通用业务;common是公共工具层,放统一返回结果、分页对象、Redis工具、异常处理等。做二次开发时,如果你把进销存业务代码直接塞进system模块里,项目初期跑得很快,但随着采购单、销售单、客户、供应商这些表越来越多,system模块会变成一个塞满业务代码的大杂烩。我的习惯是在工程里新建一个业务模块目录(比如erp或biz),它依赖system和framework,但反过来不能被系统模块引用。代码如下:

ruoyi-admin/ # Web入口:Controller、启动类 ruoyi-framework/ # 框架配置:权限、拦截、数据源 ruoyi-system/ # 系统模块:用户角色菜单字典 ruoyi-common/ # 公共工具:分页、返回体、Redis ruoyi-erp/ # 进销存业务模块(二次开发新增) ├── controller/ # 采购/销售/库存控制器 ├── service/ # 业务逻辑与事务边界 ├── mapper/ # 数据访问接口与XML ├── domain/ # 实体对象 └── vo/ # 前端交互对象,比如采购明细列表

这里的关键逻辑是:RuoYi自带的system模块解决的是“谁能用这个系统、能看哪些菜单”的问题,而进销存模块解决的是“用户的业务单据怎么流转、库存怎么变动”的问题。两者通过登录用户的ID和部门ID关联,业务表里存create_by和create_dept字段,这样权限控制和数据归属才能接得上。把业务模块独立出来,后续如果要把进销存拆成独立的微服务,或者和电商订单系统对接,你只需要动ruoyi-erp这一个模块的代码。

2.2 框架白送的六项能力直接复用,别自己重写轮子

RuoYi里可以直接拿来用的能力,第一项是代码生成器。我在设计供应商、仓库这类基础档案的CRUD时,先在数据库建好表,再用生成器生成Java代码到ruoyi-erp模块,然后把Controller改成继承BaseController,分页查询用startPage()方法自动拦参数,返回体用AjaxResult.success()包装。第二项是字典管理,进销存里有大量“状态”“类型”字段,比如单据状态有草稿、已审核、已出库,商品分类有大类小类,这些值经常要改,千万别写死在代码里,用@DictDataSource注解或者SysDictData表管理,前端下拉框自动加载。第三项是参数配置,库存预警阈值、单据编号前缀、默认仓库等,存到sys_config表,用ConfigUtils.getConfigValue("erp.stock.warning")读取,改配置不用重新部署。第四项是定时任务,后面讲到的每日库存对账、过期订单自动关闭,都靠Quartz定时任务实现。第五项是操作日志,进销存里的审核、反审核操作必须留痕,直接调SysOperLogService记录操作人和操作内容。第六项是部门数据权限,但这项需要自己做增强,后面避坑章节会重点讲。

2.3 框架不提供的三件事:单据流水、库存快照、统计报表

RuoYi没有采购单、销售单、库存流水这些领域模型,这不算缺点,因为框架本来就不该绑定具体业务。真正需要你从零设计的,是三件事。

第一,单据编号生成。进销存的采购单、销售单、出入库单都得有业务编号,格式一般是“前缀+年月日+流水号”,比如CG20250117001,而且审核、打印、对账都要靠这个编号串联。RuoYi没有现成接口,我用的方案是建一张erp_bill_number表,每类单据占一行,每次生成编号时用SELECT ... FOR UPDATE锁行更新流水号,确保并发时不重号。第二,库存快照。进销存界面里显示的“当前库存”,在数据库里应该是一张erp_stock_balance表,记录每个仓库下每个SKU的实时余额。这张表不参与业务单据的录入,它只是库存流水的汇总结果,每次库存变动时在同一个事务里更新它。第三,统计报表。出库趋势、采购成本、销售毛利这些统计,靠RuoYi的代码生成器是变不出来的,要自己写SQL聚合查询。常见做法是在ruoyi-erp里单独建一个报表包,Controller里用startPage()支持分页,SQL里用GROUP BY按时间聚合,再用SUM()算出金额汇总。这里有个很重要的设计原则:报表查的是业务流水表的汇总,不是查库存余额表,否则时间维度一多就对不上账。

2.4 技术选型上的一点提醒:单体优先,别急着上微服务

如果你接手的是一套KS进销存设计源码,大概率是单体应用。对于中小规模业务的进销存,单体加Redis缓存加MySQL分区表完全够用,采购单日单量在一万单以内,单体应用的性能瓶颈根本不在应用层,而在SQL写法上。做二次开发时,我一般建议保持RuoYi原有的Shiro权限体系,不要为了“技术新颖”换掉Spring Security或者引入微服务网关。进销存的核心竞争力在于单据流程的正确性和数据一致性,而不是架构的复杂度。业务量真的上来后,先把报表查询从主库拆到只读库,再做订单模块的独立部署,这条路比一开始就搞服务拆分要稳得多。

3. 进销存表结构设计:从商品档案到库存流水,先把这六组表画出来

3.1 四张基础档案表:商品、供应商、客户、仓库

进销存的根基是四张基础档案表,设计它们时就得看长远,因为后面所有单据都引用它们的主键。商品表erp_product至少要有:product_code商品编码、product_name商品名称、category_id分类ID、unit计量单位、purchase_price采购价、sale_price销售价、prime_price成本价、status启用状态。供应商表erp_supplier和客户表erp_customer结构很相似,但一定不要合并成一张“往来单位表”,因为采购和销售的统计口径不同,后面做应付应收对账时分开更清楚。仓库表erp_warehouse要设计成支持多仓,warehouse_code仓库编码、warehouse_name仓库名称、address地址、manager负责人。这里一个容易犯的错是:商品表里放了“当前库存”字段。我见过有人这样做,界面查询是方便了,但每次出入库都要先查再改这个字段,并发高时数据铁定对不上。商品表只放档案信息,库存永远放在库存表里。

建表语句片段如下:

CREATE TABLE `erp_product` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID', `product_code` varchar(64) NOT NULL COMMENT '商品编码', `product_name` varchar(255) NOT NULL COMMENT '商品名称', `category_id` bigint DEFAULT NULL COMMENT '商品分类ID', `unit` varchar(20) DEFAULT '件' COMMENT '计量单位', `purchase_price` decimal(18,2) DEFAULT NULL COMMENT '采购价格', `sale_price` decimal(18,2) DEFAULT NULL COMMENT '销售价格', `prime_cost` decimal(18,2) DEFAULT NULL COMMENT '成本价', `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', `create_by` varchar(64) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_by` varchar(64) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `remark` varchar(500) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_product_code` (`product_code`) ) ENGINE=InnoDB COMMENT='商品档案表';

这段建表逻辑里的几个关键点:金额字段全部用decimal(18,2)而不用float/double,这是为了避免浮点计算误差导致对账不平;product_code加了唯一索引,防止同一编码录两遍;每个业务表都加了create_by/create_time这类审计字段,配合RuoYi的BaseEntity类可以直接复用创建人和创建时间的自动填充逻辑。

3.2 单据主表和明细表:一单多头,永远成对出现

采购单、销售单这类交易单据,在设计上必然拆成主表+明细表两张。主表存单据编号、往来单位ID、仓库ID、单据日期、整单金额、审核状态;明细表存商品ID、数量、单价、行金额、折扣。这两张表通过bill_id主外键关联。为什么必须拆?因为一张采购单可能包含几十种商品,如果所有商品和单据头全塞一张表里,重复信息冗余不说,修改其中一行商品的单价会波及整张单,数据维护非常痛苦。

采购单主表和明细表的建表骨架:

CREATE TABLE `erp_purchase_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(64) NOT NULL COMMENT '采购单号', `supplier_id` bigint DEFAULT NULL COMMENT '供应商ID', `warehouse_id` bigint DEFAULT NULL COMMENT '入库仓库ID', `order_date` datetime DEFAULT NULL COMMENT '单据日期', `total_amount` decimal(18,2) DEFAULT NULL COMMENT '整单金额', `status` char(1) DEFAULT '0' COMMENT '状态(0草稿 1已审核 2已入库 3已关闭)', `audit_by` varchar(64) DEFAULT NULL, `audit_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB COMMENT='采购订单主表'; CREATE TABLE `erp_purchase_order_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `bill_id` bigint NOT NULL COMMENT '主表ID', `product_id` bigint DEFAULT NULL COMMENT '商品ID', `quantity` decimal(18,2) DEFAULT NULL COMMENT '采购数量', `price` decimal(18,2) DEFAULT NULL COMMENT '采购单价', `amount` decimal(18,2) DEFAULT NULL COMMENT '行金额', PRIMARY KEY (`id`), KEY `idx_bill_id` (`bill_id`) ) ENGINE=InnoDB COMMENT='采购订单明细表';

这个设计里两个容易被忽略的点:第一,order_date是业务日期,和create_time是两个概念,做月报统计时以order_date为准,不要以create_time为准。第二,明细表的数量为什么要用decimal而不是int?进销存里很多商品按重量、长度、面积计件,比如钢材按吨、布匹按米,数量永远存在小数点,所以这里千万别图省事用int,后期改表结构比现在用decimal痛苦得多。销售单的结构和采购单几乎一样,把supplier_id换成customer_id,状态改为草稿、已审核、已出库、已关闭即可。

3.3 库存流水表与库存余额表:记账录和快照的分工不能混

这两个表是整个进销存系统数据一致性的命门。库存流水表记录每一次数量变动:哪张单据、什么时间、哪个仓库、商品进还是出、变动前数量、变动后数量。它是只增不改的,任何审核、反审核操作都只是在这个表里追加记录,绝不更新历史行。库存余额表记录当前各仓库各商品的实时数量,它的作用只有一个——让界面查询库存时不用去SUM整张流水表,查得快。

库存流水表的建表语句:

CREATE TABLE `erp_stock_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `warehouse_id` bigint DEFAULT NULL COMMENT '仓库ID', `product_id` bigint DEFAULT NULL COMMENT '商品ID', `bill_type` char(1) DEFAULT NULL COMMENT '单据类型(1采购入库 2销售出库 3盘点 4调拨)', `bill_no` varchar(64) DEFAULT NULL COMMENT '关联单据号', `change_qty` decimal(18,2) DEFAULT NULL COMMENT '变动数量(正数入库 负数出库)', `before_qty` decimal(18,2) DEFAULT NULL COMMENT '变动前库存', `after_qty` decimal(18,2) DEFAULT NULL COMMENT '变动后库存', `remark` varchar(255) DEFAULT NULL, `create_by` varchar(64) DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_query` (`warehouse_id`, `product_id`, `create_time`) ) ENGINE=InnoDB COMMENT='库存流水表';

为什么流水表一定要记录变动前和变动后的数量?有一次我排查一个库存对不上的问题,发现某商品的余额是120,但把所有流水数量加起来却是150。就是因为早期设计时流水表没记before_qty和after_qty,业务员在审核时改了数量,程序直接改了余额表,流水表没有同步追加记录。后来的排查只能靠猜。如果你在建表时就强制每次变动把前后数量都记下来,任何一条业务的变动轨迹都一目了然——这张表的每一行就是一笔库存变动的完整快照,对账时按时间线重放就知道了哪一步出了问题。

3.4 期初建账:系统上线前必须解决的初始库存问题

系统上线时,仓库里往往已经有存量商品。这些存量怎么录入?有两条路:一是做一个“期初库存导入”功能,手动导入期初数量;二是生成一张“盘点入库单”,把期初数当成一次入库业务。我更推荐后者,因为期初库存本质上也是一次库存变动,用盘点单录入会自然生成库存流水,后续所有报表统计的口径就统一了。否则期初数只在余额表里,流水表里没有对应的记录,第一个月对账就发现期初数在报表里对不上。设计期初盘点单时,把单据类型设置为“盘点”,审核通过后调用库存变更服务,同时写流水表和余额表,和普通采购入库的入库逻辑复用同一套代码。

4. 用RuoYi代码生成器搭完骨架后,手写这三块核心业务代码

4.1 采购入库审核:一个事务里完成状态更新、库存流水、余额变更

采购单审核入库是整个系统里事务性最强的操作。它要在一个事务里完成四件事:把采购单状态从已审核更新为已入库,往流水表里插入入库记录,累加库存余额表的数量,把操作日志记下来。四件事里任何一件失败,整个事务必须回滚。很多新手最容易犯的错是:只更新了采购单状态和库存余额,漏了流水表,或者先写余额后写流水,顺序反了导致中间状态被别的线程读到。

采购入库的Service层核心代码:

@Override @Transactional(rollbackFor = Exception.class) public void auditPurchaseOrder(Long orderId) { // 1. 查询采购单主表和明细,校验状态必须为已审核且未入库 PurchaseOrder order = purchaseOrderMapper.selectById(orderId); if (order == null || !"1".equals(order.getStatus())) { throw new RuntimeException("采购单不存在或状态不允许入库"); } List<PurchaseOrderItem> items = purchaseOrderItemMapper.selectByBillId(orderId); if (items == null || items.isEmpty()) { throw new RuntimeException("采购单没有商品明细,无法入库"); } // 2. 遍历明细,逐行写入库存流水并累加余额 for (PurchaseOrderItem item : items) { // 查询当前库存余额,用于记录变动前数量 StockBalance balance = stockBalanceMapper.selectForUpdate( order.getWarehouseId(), item.getProductId()); BigDecimal beforeQty = (balance == null) ? BigDecimal.ZERO : balance.getQuantity(); BigDecimal afterQty = beforeQty.add(item.getQuantity()); // 写库存流水表 StockRecord record = new StockRecord(); record.setWarehouseId(order.getWarehouseId()); record.setProductId(item.getProductId()); record.setBillType("1"); // 采购入库 record.setBillNo(order.getOrderNo()); record.setChangeQty(item.getQuantity()); record.setBeforeQty(beforeQty); record.setAfterQty(afterQty); stockRecordMapper.insert(record); // 更新或插入库存余额表 if (balance == null) { balance = new StockBalance(); balance.setWarehouseId(order.getWarehouseId()); balance.setProductId(item.getProductId()); balance.setQuantity(afterQty); stockBalanceMapper.insert(balance); } else { balance.setQuantity(afterQty); stockBalanceMapper.updateById(balance); } } // 3. 更新采购单状态为已入库 order.setStatus("2"); purchaseOrderMapper.updateById(order); }

这段代码里有三个工程上很重要的细节。第一,@Transactional(rollbackFor = Exception.class)必须显式声明,否则遇到RuntimeException之外的异常不会回滚。第二,stockBalanceMapper.selectForUpdate是查询时带FOR UPDATE锁行,防止两个线程同时对同一个商品的库存做加减时出现超卖或负数,这个在并发出库场景下非常关键。第三,每次入库都在一个事务里同时写流水和余额,before_qty和after_qty保证了对账时可以回放整个库存变动轨迹。实际落地时,我会在事务提交后再记录操作日志,但为了简化演示,上面代码省略了日志这部分调用。

4.2 销售出库与库存扣减:防负库存的兜底校验

销售出库的逻辑和采购入库相反,从库存余额表里扣减数量。核心风险在于:并发场景下单品出库量可能超过当前库存,导致余额变成负数。虽然通过FOR UPDATE锁行能保证同一商品的扣减串行执行,但业务逻辑上仍然要加一层“扣减前校验”,防止业务员明知库存不够仍然强行出库。

销售出库扣减库存的校验逻辑:

// 在循环处理销售明细时,对每一行做防负校验 StockBalance balance = stockBalanceMapper.selectForUpdate( order.getWarehouseId(), item.getProductId()); if (balance == null || balance.getQuantity().compareTo(item.getQuantity()) < 0) { throw new RuntimeException("商品【" + item.getProductName() + "】库存不足,当前库存:" + (balance == null ? "0" : balance.getQuantity())); } BigDecimal beforeQty = balance.getQuantity(); BigDecimal afterQty = beforeQty.subtract(item.getQuantity()); // 写流水(负数量)并更新余额 StockRecord record = new StockRecord(); record.setChangeQty(item.getQuantity().negate()); // 出库记负数 record.setBeforeQty(beforeQty); record.setAfterQty(afterQty); stockRecordMapper.insert(record); balance.setQuantity(afterQty); stockBalanceMapper.updateById(balance);

这里容易忽略的一点是:库存不足的校验必须放在selectForUpdate之后,不能先查一次数量校验、再锁行更新。因为先查后锁存在竞态窗口——两个线程同时查到一个相同的充足库存,然后先后执行扣减,就可能把库存扣成负数。先锁行再校验,天然串行化了同一个商品的库存操作。我在批量出库和多位操作员同时开单的场景里,用这套逻辑跑了大半年没有出现过一笔负库存。库存不足时直接抛出运行时异常,事务整体回滚,界面层捕获后给操作员弹出一个明确提示。

4.3 库存查询与低库存预警:一条SQL解决效率问题

库存查询页要展示:商品编码、名称、分类、仓库、当前库存、预警状态。如果用Java程序逐个去查会很慢,直接让MySQL用LEFT JOIN一次把多表查出来,配合RuoYi自带的分页工具类就行。低库存预警的阈值放在RuoYi参数配置表里,查询时实时读取,这样改阈值不用改代码。

库存列表查询的自定义Mapper语句:

<select id="selectStockList" parameterType="StockQuery" resultType="StockVO"> SELECT p.product_code, p.product_name, p.unit, w.warehouse_name, b.quantity AS stock_qty, CASE WHEN b.quantity IS NULL OR b.quantity &lt; #{warningQty} THEN '预警' ELSE '正常' END AS stock_status FROM erp_product p LEFT JOIN erp_stock_balance b ON b.product_id = p.id AND b.warehouse_id = #{warehouseId} LEFT JOIN erp_warehouse w ON w.id = #{warehouseId} <where> <if test="productCode != null and productCode != ''"> AND p.product_code LIKE CONCAT('%', #{productCode}, '%') </if> <if test="productName != null and productName != ''"> AND p.product_name LIKE CONCAT('%', #{productName}, '%') </if> </where> ORDER BY p.product_code </select>

这里的LEFT JOIN有个细节:必须把erp_stock_balance的关联条件同时包含warehouse_id,否则同一商品在不同仓库有多条余额记录时,会查出一堆重复行。如果只需要看单一仓库的库存,直接在Mapper里传warehouseId并带上这个条件就对了。预警阈值warningQty通过#{warningQty}传入,调用方从RuoYi的ConfigUtils读取,比如预警阈值是10,库存为0的商品也会被标记为预警,正好覆盖断货场景。

5. 二次开发的4个高频坑:事务边界、金额精度、流水缺失、权限越权

5.1 一个事务里只写了主表,明细和流水没落库

现象:采购单在页面显示“已审核”,但库存没有增加,打开数据库发现erp_purchase_order状态是“2”,但erp_stock_record表里根本查不到对应记录。

原因:Service实现类里对auditPurchaseOrder方法加了@Transactional,但方法内部调用的是本类中的私有方法或非public方法,事务没有进入代理。或者事务只贴在了Controller层的某个方法上,Service层根本没加。RuoYi的工程已经扫描了Service包,但Spring的事务代理只对通过代理对象调用的public方法生效。

解决:事务注解统一放在Service层的public方法上,Controller层只做参数接收和返回封装,不在Controller上做任何涉及多表写入的操作。Service内部的所有流程都放在一个方法里,避免同类内部调用绕过代理。如果确实需要拆分方法,把被调用方法放到另一个Service类中,通过Spring注入调用,保证两个方法都在同一个事务上下文里。

5.2 金额字段从Double来,对账怎么都对不上

现象:采购单整单金额是1000.20元,但多个明细行金额加起来是1000.19元,相差一分钱。数据库字段明明是decimal(18,2),为什么还会出现这种问题?

原因:前端传参或Java实体里的金额属性用了Double或Float类型,浮点数在二进制里无法精确表示十进制小数,导致出现0.999999之类的尾差。常见做法是页面Vue里把金额转成了Number类型,后台直接接收成了Double,再转BigDecimal时调用了new BigDecimal(double)构造函数。

解决:前端金额用字符串传给后端;Java实体类金额字段全部声明为BigDecimal;数据库字段用decimal(18,2)。已有的Double类型字段在接收时用BigDecimal.valueOf(value)转换,不要用new BigDecimal(value)。代码规范上加一条,Controller接收参数用@RequestBody里的DTO,DTO内金额字段类型是BigDecimal,不让字符串或Double有机会混进来。这个坑在报表毛利统计时暴露得最快,差一分钱在汇总里被放大成几百块的差异。

5.3 库存余额在界面上改没了,流水表里却找不到这笔变动

现象:某商品库存从80突然变成了65,业务员说没有做过任何出库操作,流水表里也查不到数量为-15的记录。

原因:有人直接对erp_stock_balance表做了UPDATE操作,可能是为了临时修数据,也可能是在某个“库存调整”功能中直接改的余额,没有调用写流水的Service。库存余额表成了“黑匣子”,谁改过、为什么改、什么时候改的,完全不可追溯。

解决:任何库存变动都必须走统一的库存变更Service,这个Service里先写流水表、再更新余额表。开发阶段就在数据库层面禁止对erp_stock_balance表做手动的UPDATE/DELETE操作,DBA把表的更新权限收掉,只允许通过应用账号执行。如果确实需要人工修正余额,也必须生成一张“库存调整单”,说明调整原因,审核后走代码逻辑,而不是直接动数据库。给erp_stock_record表加一个create_time索引,定期抽查最近一周的变动记录,对不上时把问题消灭在早期。

5.4 RuoYi自带的权限只到菜单级,商品明细级没有数据隔离

现象:销售部的A操作员登录后,能看到采购部的进货成本;某业务员只负责华东区的客户,却可以查到华南区客户的订单明细。RuoYi的菜单权限配置正常,但数据没有任何隔离。

原因:RuoYi默认的权限模型是RBAC,控制的是菜单和按钮,到了数据行级(比如只看本部门数据、只看自己创建的单据)需要自己做数据范围控制。进销存里的采购成本、客户信息都属于敏感数据,不隔离迟早出问题。

解决:在业务查询Mapper的SQL里统一拼接数据权限条件。我在erp_purchase_order这类业务表上加了create_dept_id字段,在Service层调用查询前从SecurityUtils拿到当前登录用户的部门ID,把“只能查本部门的数据”条件拼进SQL。做法是定义一个数据权限工具类,在Mapper的XML里用${params.dataScope}拼条件,注意这里用${}而不是#{},因为它是权限规则拼接,不是参数值绑定。给部门管理员开放一个“可查看下级部门数据”的开关,让数据范围支持“本人/本部门/本部门及以下/全部”四级,配合系统模块里的部门管理,灵活度和安全性能兼顾。

6. 进阶技巧:用库存对账脚本和每日定时任务,把历史数据从“说不出话”变成“可追溯”

系统跑一段时间后,库存对账是对技术方案最严苛的验收。每个月月底,财务要求业务员签字的库存盘点表,和你系统里查询出的库存余额如果差一分钱,业务员不会认为是自己的Excel记错了,只会认为“系统有bug”。所以我在物料盘点前必做的一个动作用途很直接:写一个对账SQL,按商品和仓库维度,把流水表所有记录按时间重放一遍,算出理论库存,再和余额表对比。这一步不是给财务看的,而是给自己排查数据的一致性做底牌。

-- 按商品+仓库核对流水重算库存与余额表是否一致 SELECT w.id AS warehouse_id, w.warehouse_name, p.id AS product_id, p.product_code, COALESCE(SUM(r.change_qty), 0) AS sum_qty, COALESCE(b.quantity, 0) AS balance_qty, (COALESCE(SUM(r.change_qty), 0) - COALESCE(b.quantity, 0)) AS diff_qty FROM erp_warehouse w CROSS JOIN erp_product p LEFT JOIN erp_stock_record r ON r.warehouse_id = w.id AND r.product_id = p.id LEFT JOIN erp_stock_balance b ON b.warehouse_id = w.id AND b.product_id = p.id GROUP BY w.id, w.warehouse_name, p.id, p.product_code, b.quantity HAVING diff_qty != 0;

如果查出diff_qty不为0的行,说明问题要么是余额表被绕过流水表改过,要么是某张单据审核时流水和余额没有在同一个事务里提交。找到目标商品后,再按时间顺序查流水表就能锁定具体是哪一笔变动造成的。

RuoYi自带定时任务可以配合做每日对账。我通常配一个每天凌晨1点跑的Quartz任务,Java代码里执行上述SQL,把差异行写入一张erp_stock_check_log异常表,并在系统首页给管理员一个红点提醒。这个设计的好处是,异常在第二天一早就能暴露出来,而不是拖到月底盘点时对不上账。我习惯把定时任务里加一个限制:只检查昨天发生过库存变动的商品,避免每天全量扫描。一次完整排查的经验告诉我,库存对账异常发现得越早,原因越好定位,等到财务月结那天再报错就真的是灾难了。这套手段配合第2章到第5章的工程设计和避坑方案,正常业务量下能保证月月对账平得上。希望帮到你。

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

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

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

立即咨询