简介:金翔云WEB进销存系统是一套面向中小企业及零售、批发、生产型企业的云端管理工具,基于Web架构,无需安装客户端,通过浏览器即可完成库存、销售、采购与账务的日常管理,适合希望低成本实现业务数字化的经营者与开发者参考。压缩包共1841个文件,约4.59MB,以1192个gif界面素材、346个asp服务端脚本、101个jpg图片、60个css样式、43个js脚本及39个htm页面为主,另含22个db与3个mdb数据库文件、少量doc说明与txt配置文本,构成完整的Web应用目录结构。系统覆盖库存预警与调拨、销售订单与客户跟进、采购订单与供应商维护、应收应付及财务报表、多维度经营分析,并支持按角色分配操作权限,默认管理账号为admin。目前已有132人学习下载,可帮助读者快速理解进销存业务模块划分、页面组织方式与数据库设计思路,适合作为二次开发或课程设计的基础参考。
1. 金翔云WEB进销存系统:从采购入库到销售出库,一套系统怎么把账实不符压下去
做商贸批发或者小型制造的团队,最头疼的往往不是没订单,而是订单一多,库存数字就开始飘。采购说货到了,仓库说没收到;销售说开单了,财务说账上没这笔应收。金翔云WEB进销存系统这类基于浏览器访问的进销存方案,核心价值就是把这些散落在 Excel、聊天记录和纸质单据里的动作,收敛到一条可追溯的数据链上。它适合的是没有专职 IT 团队、但又被手工记账拖得难受的中小团队,尤其是需要多人同时开单、查库存、对账的场景。这篇文章不讲虚的,就按采购、销售、库存、权限这几条线,把落地路径和参数配置拆开讲,让你看完能判断这套东西能不能接进自己的业务里。
2. 采购与入库:单据流转怎么设计才不堵
采购入库是进销存系统的入口,也是最容易出乱子的地方。很多团队上线系统后,采购单和入库单还是两张皮,采购员填了单,仓库不点确认,库存就不动,结果系统里的数字永远比实际慢半拍。金翔云WEB进销存系统这类方案通常把采购流程拆成请购、采购订单、收货入库、采购退货几个节点,每个节点对应一张单据,单据之间靠关联字段串起来。设计得好,采购员、仓管、财务各看各的界面,但底层数据是一条线。
2.1 采购订单到入库单的字段映射
先看一张采购订单表需要哪些字段才能撑起后续的入库和结算。下面是一个简化的建表语句,用 MySQL 举例,字段命名尽量贴近实际业务,方便你对照自己的系统改。
CREATE TABLE purchase_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '采购订单号,唯一', supplier_id BIGINT NOT NULL COMMENT '供应商ID,关联供应商表', warehouse_id BIGINT NOT NULL COMMENT '收货仓库ID', order_date DATE NOT NULL COMMENT '下单日期', expect_date DATE COMMENT '预计到货日期', status TINYINT DEFAULT 0 COMMENT '0待审核 1已审核 2部分入库 3全部入库 4已关闭', total_amount DECIMAL(12,2) DEFAULT 0.00 COMMENT '订单总金额', created_by BIGINT NOT NULL COMMENT '制单人', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_supplier (supplier_id), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购订单主表';这张表的关键在status字段和expect_date。status决定了这张单还能不能继续入库,很多系统翻车就翻在状态机没设计好,订单已经全部入库了还能再点入库,库存直接翻倍。expect_date则是给采购跟单用的,超过预计日期还没入库的单子,系统应该能筛出来提醒,而不是等人去翻。
明细表同样重要,采购订单明细要记录商品、数量、单价、税率,还要有一个received_qty字段记录已入库数量。这个字段是入库单回写的结果,不能手动改。
CREATE TABLE purchase_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT '关联采购订单主表', product_id BIGINT NOT NULL COMMENT '商品ID', qty DECIMAL(12,3) NOT NULL COMMENT '采购数量', received_qty DECIMAL(12,3) DEFAULT 0.000 COMMENT '已入库数量', price DECIMAL(12,4) NOT NULL COMMENT '含税单价', tax_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '税率百分比', KEY idx_order (order_id), KEY idx_product (product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购订单明细';received_qty和qty的比较决定了订单是部分入库还是全部入库。每次入库单审核通过,就用入库数量去累加received_qty,然后判断是否等于qty,等于就把主表状态推到 3,小于就推到 2。这个逻辑必须放在事务里,否则并发入库会把数量写乱。
2.2 入库单审核后库存怎么变
入库单审核是库存真正增加的触发点。常见做法是入库单本身不直接改库存,而是生成一条库存流水,再由流水汇总到库存余额表。这样做的好处是任何库存变动都有据可查,对账的时候能顺着流水倒推。
-- 入库单审核通过后,写入库存流水 INSERT INTO stock_flow ( flow_type, product_id, warehouse_id, qty, ref_order_no, ref_type, created_by, created_at ) VALUES ( 1, -- 1表示入库 #{productId}, #{warehouseId}, #{qty}, #{inboundNo}, 'PURCHASE_IN', #{operatorId}, NOW() ); -- 更新库存余额,存在则累加,不存在则插入 INSERT INTO stock_balance (product_id, warehouse_id, qty) VALUES (#{productId}, #{warehouseId}, #{qty}) ON DUPLICATE KEY UPDATE qty = qty + #{qty};这里用ON DUPLICATE KEY UPDATE是为了避免先查后插的并发问题。库存余额表上要建(product_id, warehouse_id)的唯一索引,否则同一商品同一仓库会出现多条余额记录,查库存的时候就得做聚合,性能差还容易错。
参数上要注意qty的精度。进销存里数量字段建议用DECIMAL(12,3),不要用FLOAT或DOUBLE,浮点数在累加时会出现 0.1+0.2 不等于 0.3 的经典问题,库存对不上很多时候就是精度丢的。单价用DECIMAL(12,4),金额用DECIMAL(12,2),这是比较稳妥的搭配。
提示:入库单审核接口一定要加幂等控制,用入库单号做唯一键,防止前端重复提交导致库存重复增加。
3. 销售与出库:开单快和账目准怎么同时要
销售出库是进销存里压力最大的环节,因为开单速度直接影响客户体验,但库存扣减又必须准确。很多系统在这里翻车,要么是开单时查库存慢,要么是并发下单导致超卖。金翔云WEB进销存系统这类 Web 方案,通常把销售订单和销售出库单分开,订单只管意向,出库才真正扣库存。这个设计给了仓库一个缓冲,但也要求订单和出库之间的数量关系必须卡死。
3.1 销售订单的库存占用逻辑
销售订单审核后要不要占用库存,是个需要想清楚的问题。不占用,可能同一个库存被多个订单同时卖出,最后发不出货;占用,又可能因为订单取消不及时导致库存被虚占。常见做法是订单审核后做预占,出库时把预占转成实扣,订单关闭时释放预占。
-- 库存预占表,记录每个订单占用了多少库存 CREATE TABLE stock_reserve ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, order_no VARCHAR(32) NOT NULL COMMENT '销售订单号', reserve_qty DECIMAL(12,3) NOT NULL COMMENT '预占数量', status TINYINT DEFAULT 1 COMMENT '1预占中 2已转出库 3已释放', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_product_warehouse (product_id, warehouse_id), KEY idx_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存预占表';可用库存的计算公式是:可用库存 = 库存余额 - 预占中数量。开单时查的是可用库存,不是账面库存。这样即使账面有 100 件,已经被其他订单占了 80 件,新订单最多只能卖 20 件,避免超卖。
预占的粒度要控制好。如果按订单整单预占,一个订单里有多个商品,取消时释放逻辑会复杂;如果按明细预占,数据量会大一些,但释放和转出都更清晰。我一般建议按明细预占,order_no加product_id做联合索引,查起来不慢。
3.2 出库单扣减库存的事务边界
出库单审核是库存真正减少的动作,这里的事务边界必须画清楚。一个出库单可能有多行明细,每行都要扣库存、写流水、更新预占状态,这些操作要么全成功,要么全回滚。
// 伪代码示意出库审核的事务逻辑 @Transactional(rollbackFor = Exception.class) public void confirmOutbound(OutboundDTO dto) { // 1. 校验出库单状态,防止重复审核 Outbound order = outboundMapper.selectForUpdate(dto.getOutboundNo()); if (order.getStatus() != 0) { throw new BizException("出库单已审核,请勿重复操作"); } // 2. 逐行扣减库存 for (OutboundItem item : dto.getItems()) { // 扣减库存余额,带条件更新防止负库存 int affected = stockBalanceMapper.deductQty( item.getProductId(), item.getWarehouseId(), item.getQty() ); if (affected == 0) { throw new BizException("库存不足:" + item.getProductName()); } // 3. 写入库存流水 stockFlowMapper.insertOutboundFlow(item, dto.getOutboundNo()); // 4. 更新预占状态为已转出库 stockReserveMapper.updateStatus( dto.getSalesOrderNo(), item.getProductId(), 2 ); } // 5. 更新出库单状态 outboundMapper.updateStatus(dto.getOutboundNo(), 1); }扣减库存的 SQL 要带条件,不能直接UPDATE stock_balance SET qty = qty - #{qty},而是UPDATE stock_balance SET qty = qty - #{qty} WHERE product_id = ? AND warehouse_id = ? AND qty >= #{qty}。这样即使并发进来,库存不够的那条更新会影响 0 行,事务回滚,不会出现负库存。
selectForUpdate是为了锁住出库单记录,防止两个请求同时审核同一张单。这个锁的粒度是单号级别,不会影响其他出库单,性能可以接受。
注意:出库单审核接口的响应时间要控制好,如果明细行数很多,逐行扣库存可能超过前端超时时间。可以考虑批量扣减,但事务边界和幂等控制不能省。
4. 库存盘点与调拨:账实不符怎么查、怎么修
库存对不上是进销存系统最常见的投诉。用户说系统里显示 100,实际仓库只有 95,差了 5 个。这时候不能直接改库存数字,得先查流水,看这 5 个是入库少录了、出库多扣了,还是盘点漏了。金翔云WEB进销存系统这类方案一般会提供库存流水查询和盘点单功能,前者用来追溯,后者用来修正。
4.1 用库存流水定位差异来源
库存流水表是排查差异的核心。每条流水记录变动类型、变动数量、关联单据号、操作人和时间。查差异的时候,按商品和仓库筛选,把期初、入库、出库、盘点调整几条线拉出来,看哪个环节的数字对不上。
-- 查询某商品某仓库的库存流水,按时间倒序 SELECT flow_type, CASE flow_type WHEN 1 THEN '采购入库' WHEN 2 THEN '销售出库' WHEN 3 THEN '盘点调整' WHEN 4 THEN '调拨入库' WHEN 5 THEN '调拨出库' ELSE '其他' END AS flow_type_name, qty, ref_order_no, created_by, created_at FROM stock_flow WHERE product_id = #{productId} AND warehouse_id = #{warehouseId} AND created_at BETWEEN #{startTime} AND #{endTime} ORDER BY created_at DESC;查的时候要注意flow_type的正负号。入库类流水qty是正数,出库类是负数,盘点调整根据盈亏方向决定正负。如果流水表里存的都是正数,靠flow_type判断方向,那汇总的时候就要用CASE WHEN转换,容易写错。我一般建议流水表里直接存带符号的数量,入库正、出库负,汇总的时候直接SUM(qty)就是余额,少一层转换少一个坑。
4.2 盘点单的生成与差异处理
盘点单的生成有两种方式:一种是全盘,把仓库里所有商品都拉出来;一种是抽盘,只盘指定商品或分类。全盘适合月末结账,抽盘适合日常抽查。盘点单生成后,系统会带出账面数量,盘点人填入实盘数量,系统自动算差异。
-- 盘点单主表 CREATE TABLE stock_check ( id BIGINT PRIMARY KEY AUTO_INCREMENT, check_no VARCHAR(32) NOT NULL COMMENT '盘点单号', warehouse_id BIGINT NOT NULL, check_type TINYINT DEFAULT 1 COMMENT '1全盘 2抽盘', status TINYINT DEFAULT 0 COMMENT '0盘点中 1已审核', created_by BIGINT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_check_no (check_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='盘点单主表'; -- 盘点单明细 CREATE TABLE stock_check_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, check_id BIGINT NOT NULL, product_id BIGINT NOT NULL, book_qty DECIMAL(12,3) NOT NULL COMMENT '账面数量', actual_qty DECIMAL(12,3) DEFAULT NULL COMMENT '实盘数量', diff_qty DECIMAL(12,3) DEFAULT NULL COMMENT '差异数量', KEY idx_check (check_id), KEY idx_product (product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='盘点单明细';盘点单审核后,差异数量要写回库存余额,同时生成一条flow_type = 3的盘点调整流水。这里的关键是盘点单审核也要幂等,审核过的单子不能再审,否则差异会被重复调整。
差异处理有个原则:盘盈要查原因,盘亏也要查原因。不能直接调平了事。盘盈可能是入库漏录,盘亏可能是出库多扣或者丢失。系统能帮你把差异算出来,但原因得人去查。我见过一个团队,盘点差异直接调平,连续三个月盘亏,最后发现是出库时把赠品也当正品扣了库存,但赠品根本没入库。
提示:盘点期间建议锁定仓库,不允许新的出入库操作,否则盘点结果永远对不上。如果业务不允许锁仓,至少要把盘点基准时间点记下来,后续流水按时间点切分。
5. 避坑与排查:进销存上线后最容易翻车的 5 个地方
进销存系统上线只是开始,真正的问题往往在运行一两周后才暴露。下面这 5 个坑是我见过频率最高的,每个都按现象、原因、解决来写,你对照自己的系统排查。
5.1 库存出现负数
现象:库存余额表里某个商品某个仓库的qty小于 0,但系统没有拦截。
原因:扣减库存的 SQL 没有加qty >= #{qty}条件,或者加了条件但事务隔离级别不够,并发时两个请求都读到足够的库存,然后都执行了扣减。
解决:扣减 SQL 必须带条件更新,并且检查影响行数。UPDATE stock_balance SET qty = qty - #{qty} WHERE product_id = ? AND warehouse_id = ? AND qty >= #{qty},影响行数为 0 就抛异常回滚。另外把事务隔离级别调到READ COMMITTED以上,配合行锁使用。
5.2 同一张单重复审核导致库存翻倍
现象:采购入库单审核一次,库存加了一次;用户刷新页面又点了一次审核,库存又加了一次。
原因:审核接口没有幂等控制,或者幂等键设计有问题。前端按钮没有防重复点击,后端也没有校验单据状态。
解决:审核接口先查单据状态,已审核的直接返回错误。同时用单据号做唯一键,在流水表上建唯一索引,重复插入会报错回滚。前端按钮点击后置灰,但后端不能依赖前端。
5.3 库存流水和余额对不上
现象:把流水表里某个商品的SUM(qty)和余额表里的qty对比,发现差了几十。
原因:流水写入和余额更新不在同一个事务里,或者余额更新失败但流水写成功了。也可能是历史数据迁移时只迁了余额没迁流水。
解决:流水和余额必须在同一个事务里更新,任何一步失败都回滚。上线前做一次全量对账,用流水汇总去核对余额,差异记录逐条查。日常也可以跑定时任务,每天凌晨对账,发现差异及时告警。
5.4 销售开单时查不到库存,但仓库明明有货
现象:销售员开单,系统提示库存不足,但仓库说货就在架子上。
原因:库存被其他订单预占了,可用库存 = 余额 - 预占,销售员看到的是可用库存。或者商品和仓库的对应关系搞错了,货在 A 仓库,开单选的 B 仓库。
解决:开单界面要同时显示账面库存、预占数量和可用库存,让销售员知道为什么不够。仓库选择要默认带出商品的主仓库,减少选错。预占释放要及时,订单取消或超时未出库的要自动释放。
5.5 盘点后库存调整了,但成本没跟着变
现象:盘点盘盈了 10 个,库存数量加了,但库存金额没变,导致成本核算时单价被拉低。
原因:盘点调整只更新了数量,没有更新金额。或者成本核算用的是移动加权平均,但盘点调整没有触发成本重算。
解决:盘点调整要同时更新数量和金额,盘盈按当前移动加权平均成本入库,盘亏按当前成本出库。如果系统支持,盘点审核后触发一次成本重算,把调整后的数量和金额一起更新到成本表。
6. 权限与数据隔离:多人协作时怎么不互相踩脚
进销存系统一旦多人使用,权限和数据隔离就是绕不开的。采购员只能看自己的采购单,销售员只能看自己的客户,仓库管理员只能操作自己仓库的出入库,财务能看所有单据但不能改库存。这些规则如果不在系统里卡死,靠人自觉,迟早出问题。
6.1 基于角色的功能权限配置
功能权限控制的是「能不能点这个菜单、能不能调这个接口」。常见做法是角色绑菜单,用户绑角色。金翔云WEB进销存系统这类方案一般会预置几个角色:管理员、采购员、销售员、仓管员、财务。每个角色对应的菜单和按钮权限不同。
-- 角色表 CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(32) NOT NULL COMMENT '角色编码', role_name VARCHAR(64) NOT NULL COMMENT '角色名称', UNIQUE KEY uk_role_code (role_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; -- 角色菜单关联表 CREATE TABLE sys_role_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_id BIGINT NOT NULL, menu_id BIGINT NOT NULL, KEY idx_role (role_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联'; -- 用户角色关联表 CREATE TABLE sys_user_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, KEY idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联';后端接口鉴权的时候,不能只判断用户有没有登录,还要判断用户有没有这个接口对应的权限码。常见做法是用注解或者拦截器,在方法执行前校验权限码。
// 权限校验注解示例 @RequirePermission("purchase:order:audit") @PostMapping("/purchase/order/audit") public Result auditPurchaseOrder(@RequestBody AuditDTO dto) { // 只有拥有采购订单审核权限的用户才能执行 purchaseService.auditOrder(dto); return Result.success(); }权限码的粒度要控制好。太粗了,采购员能审核自己的单,等于没控;太细了,每个按钮一个权限码,配置起来累死人。我一般按「模块:单据:动作」三级来分,比如purchase:order:create、purchase:order:audit、purchase:order:view,够用又不至于太碎。
6.2 数据权限隔离的三种实现方式
功能权限解决的是「能不能操作」,数据权限解决的是「能操作哪些数据」。采购员 A 和采购员 B 都有创建采购单的权限,但 A 只能看自己的单,B 只能看自己的单,互相看不到。数据权限的实现方式主要有三种。
第一种是在 SQL 里拼条件。每次查询都带上created_by = #{currentUserId}或者warehouse_id IN (用户有权限的仓库)。这种方式简单直接,但每个查询都要记得拼,漏一个就出数据泄露。
第二种是用 MyBatis 拦截器统一处理。在拦截器里解析 SQL,自动加上数据权限条件。这种方式对业务代码侵入小,但拦截器写起来复杂,尤其是 SQL 里有子查询、联合查询的时候,解析容易出错。
第三种是在数据库层做视图或者行级安全策略。比如给每个用户建一个数据库账号,用视图过滤数据。这种方式最彻底,但维护成本高,不适合中小团队。
我一般推荐第一种加第二种结合:核心查询用拦截器统一加条件,特殊查询手动拼。拦截器里维护一个配置,标明哪些表需要按created_by过滤,哪些表需要按warehouse_id过滤。
// MyBatis 拦截器示意,在 SQL 执行前追加数据权限条件 @Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class DataPermissionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; // 获取当前用户的数据权限范围 DataScope scope = SecurityUtils.getCurrentDataScope(); if (scope == null || scope.isAll()) { return invocation.proceed(); } // 根据 MappedStatement 的 id 判断是否需要加数据权限 String msId = ms.getId(); if (needDataPermission(msId)) { // 改写 SQL,追加条件 BoundSql boundSql = ms.getBoundSql(parameter); String originalSql = boundSql.getSql(); String newSql = appendDataScope(originalSql, scope); // 用新 SQL 替换原 SQL,继续执行 // 具体实现略,核心是反射修改 BoundSql 的 sql 字段 } return invocation.proceed(); } }数据权限的配置要能动态调整。比如某个销售员升职成销售主管,要能看整个团队的单子,这时候改的是数据权限范围,不是功能权限。数据权限范围可以按用户、按角色、按部门来配,灵活度越高,配置越复杂,要根据团队规模权衡。
注意:数据权限和功能权限要分开校验。有功能权限不代表有数据权限,有数据权限也不代表有功能权限。两个都通过才能执行操作。
6.3 操作日志与审计追踪
多人协作的系统,操作日志是后悔药。谁在什么时候改了哪张单的哪个字段,改前是什么、改后是什么,都要记下来。出了问题能查到人,也能查到改了什么。
CREATE TABLE sys_oper_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, user_name VARCHAR(64) NOT NULL, module VARCHAR(64) NOT NULL COMMENT '模块名', operation VARCHAR(128) NOT NULL COMMENT '操作描述', method VARCHAR(256) COMMENT '请求方法', params TEXT COMMENT '请求参数', result TEXT COMMENT '返回结果', ip VARCHAR(64) COMMENT '操作IP', cost_time BIGINT COMMENT '耗时毫秒', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_created (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';日志记录用 AOP 切面来做,在方法执行前后记录参数和结果。参数里的敏感信息比如密码要脱敏,大字段比如图片 base64 不要记,否则日志表膨胀很快。日志保留时间根据合规要求定,一般至少保留 6 个月。
审计追踪不只是记日志,还要能查。按用户查、按时间查、按模块查,最好能按单据号查。比如查某个采购单的所有操作记录,从创建到审核到入库,一条时间线拉出来,谁改了什么一目了然。
7. 上线前的一次全链路对账:把库存、单据、权限串起来验一遍
系统开发完,别急着全员推广。先做一次全链路对账,把采购入库、销售出库、库存调拨、盘点调整、权限隔离这几条线串起来跑一遍,看数据能不能对上。这一步做好了,上线后能省掉大量救火时间。
对账的步骤我一般这么安排。先准备基础数据:两个仓库、十个商品、三个供应商、三个客户、五个用户分别绑不同角色。然后按业务流程走一遍:采购员创建采购订单,仓管审核入库,销售员开单,仓管出库,财务查流水和余额。每一步都记录预期结果和实际结果。
# 对账脚本示意,用 curl 调接口模拟业务操作 # 1. 采购员登录,创建采购订单 curl -X POST http://localhost:8080/api/purchase/order/create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${PURCHASE_TOKEN}" \ -d '{"supplierId":1,"warehouseId":1,"items":[{"productId":1,"qty":100,"price":10.00}]}' # 2. 仓管登录,审核入库单 curl -X POST http://localhost:8080/api/inbound/confirm \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${WAREHOUSE_TOKEN}" \ -d '{"inboundNo":"IN20240101001"}' # 3. 查库存余额,预期 productId=1, warehouseId=1 的 qty 为 100 curl -X GET "http://localhost:8080/api/stock/balance?productId=1&warehouseId=1" \ -H "Authorization: Bearer ${WAREHOUSE_TOKEN}" # 4. 销售员登录,创建销售订单并出库 curl -X POST http://localhost:8080/api/sales/order/create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${SALES_TOKEN}" \ -d '{"customerId":1,"warehouseId":1,"items":[{"productId":1,"qty":30,"price":15.00}]}' # 5. 出库后查库存,预期 qty 为 70 curl -X GET "http://localhost:8080/api/stock/balance?productId=1&warehouseId=1" \ -H "Authorization: Bearer ${WAREHOUSE_TOKEN}" # 6. 用采购员 token 查销售订单,预期返回 403 无权限 curl -X GET "http://localhost:8080/api/sales/order/list" \ -H "Authorization: Bearer ${PURCHASE_TOKEN}"这个脚本跑完,重点看三个地方。一是库存数字对不对,采购入库 100,销售出库 30,余额应该是 70,流水汇总也应该是 70。二是权限隔离有没有生效,采购员查销售订单应该被拒绝,销售员查采购订单也应该被拒绝。三是单据状态流转对不对,采购订单从待审核到已审核到全部入库,销售订单从待审核到已出库,状态不能跳。
对账的时候最容易发现的问题不是功能没实现,而是边界没处理。比如采购订单部分入库后,剩余数量还能不能继续入库;销售订单出库一部分后,剩余数量能不能取消;盘点单审核后,库存流水和余额是不是同时更新了。这些问题在开发阶段往往被忽略,一对账就暴露。
我自己的习惯是,每次改完库存相关的代码,都把这个对账脚本跑一遍。脚本里的 token 和单号用变量,每次跑之前重置数据库,保证结果可复现。这个习惯帮我拦住了好几次差点上线的 bug,有一次是并发扣库存没加锁,单线程跑没问题,脚本里加了并发请求就出现了负库存。
对账通过后,别急着把老数据全迁进来。先迁一小部分,让真实用户用一周,观察库存流水和余额有没有偏差。偏差在可接受范围内,再全量迁移。迁移的时候注意期初库存的录入,期初库存要对应到期初流水,否则第一天的流水汇总就对不上余额。
希望帮到你。
本文还有配套的精品资源,点击获取