2026/10/19 6:42:15

SpringBoot2+Vue3进销存源码解析:单据流与库存事务设计

SpringBoot2+Vue3进销存源码解析:单据流与库存事务设计 这套源码我从头到尾读过一遍看的是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 那一套业务背景是某轴承制造企业的进销存。刚拿到的时候我也觉得“不就是增删改查”真正跑起来才发现进销存的价值全在单据与库存的联动上。所以这篇内容就围绕这套 Java Web 进销存源码展开把业务模块、表结构、核心接口和部署过程里容易被忽略的细节逐一说清楚给打算做毕业设计、中小型管理系统开发或者单纯想研究进销存源码的人做个参考。进销存系统看起来是管理系统的入门课采购单、销售单、库存表、往来账功能谁都说得出来。但真到落地每一环都有讲究一张销售单保存时库存怎么扣、流水怎么记、客户欠款怎么更新一张采购单审核后成本价要不要回写所有环节串起来才会发现“库存余额”不是简单加减而是一堆约束条件共同作用的结果。1. 先把业务账算清轴承进销存到底管哪几条“线”1.1 进销存最容易被低估的地方不是录入是台账一致性很多人拿到进销存项目第一反应是把采购、销售、库存三张表建出来然后写一套 CRUD 接口就收工。但真实业务里库存余额绝对不能只存在一张表里当“状态”用因为状态可以被改、可以被覆盖一旦出现脏数据整个系统就失去参考价值。这套源码给我的感觉是它的设计重心不在页面好不好看而在台账。所谓台账就是一张张单据按发生顺序排列后形成的一条可追溯链条。采购入库生成入库单销售出库生成出库单库存数量不是被“手动改”出来的而是被一张张单据“推”出来的。我后面会详细讲表结构怎么设计但这里先建立概念进销存的核心不是表单而是单据流。1.2 采购、销售、退货、调拨四条单据线的流转以轴承制造企业为例日常业务大致可以拆成四条线采购线向供应商买轴承毛坯、外圈、内圈、滚子、保持架到货后仓库验收入库。销售线客户下订单销售开单仓库出库财务按应收账款催款。退货线采购退给供应商、客户退回我们都涉及库存回补或扣减。调拨线厂区多个仓库之间调货跨仓移动但不改变总库存。这套源码在单据上做了主表加明细表的标准结构。一张采购单主表记录单号、供应商、单据日期、经手人、审核状态、备注明细表记录每一行商品、数量、单价、金额。为什么不能把商品直接塞在主表字段里因为一张单可能有几十种轴承型号每种型号数量、单价各不相同只有拆分明细才能支撑入库、出库、退货、对账、统计这一整套后续动作。我自己的体会是单据设计里最容易漏的是“单据状态”。新单、已提交、已审核、已入库、已作废如果不用状态字段管住业务人员就可能改已经出库的单据导致库存和账目对不上。这套源码里基本上每一张业务单据都带着审批流转字段这也是进销存和普通接口 Demo 拉开差距的地方。1.3 角色权限与操作留痕进销存系统还有一个麻烦点角色分工。销售员可以开销售单但不能改价格仓管员可以操作出入库但不能看客户欠款财务只看报表和应收应付管理员管基础资料和用户权限。这套源码的权限模型走的是主流的 RBAC也就是用户、角色、权限点三层。放在前端体现为菜单和按钮显隐后端则每个接口做权限点校验。只在前端藏菜单是不安全的因为别人可以直接调接口所以后端拦截器或注解必须兜底。除了权限还有操作日志谁在什么时间改了哪张单、原值是什么、现值是什么一套好的进销存系统不能没有这个否则出了账目纠纷很难追溯。2. 技术选型SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 是激进还是保守2.1 SpringBoot2.7 不是老是成熟现在 SpringBoot3 已经出来很长时间了JDK17 也是主流为什么这套源码还在用 SpringBoot2我自己做中小型企业项目也有类似偏好因为 SpringBoot2.7 正好卡在“生态完善”和“技术不算旧”之间。很多第三方工具、监控组件、老版本运维脚本还在用 SpringBoot2 的规范团队接手成本低部署也不需要频繁升级中间件。从源码上看项目用的还是 Servlet 体系下的 spring-boot-starter-web不是 WebFlux。进销存这类管理系统是典型的 IO 密集型加少量计算任务业务逻辑大多是事务性增删改查同步阻塞模型完全够用异步响应式反而会让代码更绕。SpringBoot2 对应的 Java 版本通常是 JDK8这也意味着开发机、服务器都不用装太高版本兼容面很广。2.2 MyBatis-Plus 解决 80% 的 CRUD但报表还得手写 SQLMyBatis-Plus 在这个项目里承担了几乎所有单表操作。BaseMapper 自带 insert、deleteById、selectById、updateById配合 LambdaQueryWrapper 可以少写大量 XML。比如按条件查询库存列表直接链式拼接条件就行。但它不代表万能。进销存里的库存汇总、销售统计、往来账余额这类报表查询一旦涉及多表 join 和分组聚合MyBatis-Plus 的 QueryWrapper 反而难维护。这套源码的做法是常规增删改查走 MyBatis-Plus复杂统计走自定义 XML SQL。这个边界划得清楚代码就不会变成一个堆满条件构造器的怪对象。这里补充一句很多人学 MyBatis-Plus 只记住了IService和ServiceImpl但我更推荐大的业务接口自己定义方法不硬套 IService。原因很简单进销存的保存动作往往不是一个单表插入而是要同时更新库存、流水、往来账多表事务放在一个“业务 Service”里更直观。如果所有逻辑都写到继承了 ServiceImpl 的类里后期会越写越重。2.3 Vue3 Element Plus 的后台组合很顺手前端这套用的是 Vue3 组合式 API 加 Element Plus。组件式开发管理后台体验明显比 Vue2 选项式写起来干净。页面按“列表页 表单弹窗 详情抽屉”拆分库存列表、销售开单、报表查询都可以复用同一套表格封装。组合式 API 对进销存这种表单密集型项目尤其友好需要把采购单主表和明细表放在同一个页面上维护用 ref、reactive 管理响应式数据比在 data 里堆嵌套属性更直观。Vue3 配合 Vite 的开发服务器启动速度也快改代码热更新几乎是秒级联调阶段会省很多时间。2.4 MySQL8.0 的窗口函数和杂项数据库用 MySQL8.0最值的部分是窗口函数和更好的优化器。进销存报表里经常要算“本月环比上月”“库存排名前多少”MySQL8.0 的ROW_NUMBER()、SUM() OVER (...)可以直接在一个 SQL 里写完。另外 MySQL8.0 默认字符集已经是 utf8mb4存中文、生僻字、表情符号都没有问题不需要像 MySQL5.7 那样单独改库。事务方面进销存对 ACID 的要求很高MySQL8.0 的 InnoDB 行锁、外键、崩溃恢复都比较成熟。下面讲数据库设计的时候我会专门说怎样利用唯一索引和事务约束来保证库存不超卖、不重号。3. 数据库设计库存流水怎样保证账实相符3.1 基础资料表商品、客户、供应商、仓库基础资料是进销存的静态主数据这套源码里包含商品信息表、客户表、供应商表、仓库表、用户表、角色表等。轴承这类商品有几个特殊属性型号、规格、材质、尺寸系列单纯放一个“商品名称”字段不够用。设计时至少要有商品编码、品名、规格型号、单位、默认采购价、默认销售价、库存上下限。商品编码一定要唯一因为后续采购单、销售单、盘点单全部要根据编码关联对应关系如果编码可以重复系统越用越乱。客户和供应商都属于“往来单位”有些企业客户和供应商可能是同一家但从业务严谨性出发这套源码分成了两张表。客户表维护联系人、电话、收货地址、信用额度、应收余额供应商表维护采购联系人、账期天数、应付余额。在界面上客户和供应商是分开管理的这也符合财务上应收、应付分账的习惯。需要注意的是像“信用额度”和“应收余额”这种字段虽然冗余在客户表里日常查询很方便但它应该由销售单、收款单自动累加或扣减不能开放给手工编辑。一旦变成可以手改的字段月底对账就会出现莫名其妙的差异。源码里这类汇总字段都放在 Service 层统一更新DAO 层没有直接 update 的暴露这是正确的做法。3.2 核心单据表主表 明细表到底怎么关联单据表是进销存的骨架。采购、销售、退货、调拨四类业务每一类基本都有一张主表加一张明细表。以销售出库为例销售主表字段有单据编号、客户 id、单据日期、销售类型、仓库 id、经手人、审核状态、单据总金额、已收款金额、备注。销售明细表字段有销售单主表 id、商品 id、商品编码、品名快照、数量、单价、金额、税率、库存批次如果有批次管理。流水表字段有出入库方向、商品 id、仓库 id、关联单据类型、关联单据编号、变动数量、变动前库存、变动后库存、操作人、操作时间。主表和明细表为什么要分开因为一张销售单可能有几十种商品分开后明细行数不受主表字段数限制查询汇总也方便。更重要的是明细表里要保留“品名快照”。哪怕以后某个商品的基础资料改名了历史单据里的商品名和单价也不能跟着变否则对账、审计全部失真。这一点是新人在设计进销存时常忽略的地方。3.3 库存表与流水表的“双写”约束库存怎么存最常见的是库存表 design仓库 id 商品 id 作为联合唯一索引一个仓库里一种商品只能有一行库存记录字段是当前库存量、冻结库存量、安全库存量、更新时间。这张表的数量永远是结果它只被两张东西驱动入库/出库单和库存流水。正确流程是插入或更新业务单据明细单据审核后才生效。在同一事务里更新库存表数量。在同一事务里写一条库存流水记录。库存流水记录不准修改、不准删除。这三步必须在一个事务里否则就出现“单据保存成功但库存没变”的严重 bug。这套源码的库存更新用了非常关键的一条 SQLUPDATE stock SET quantity quantity - #{quantity} WHERE warehouse_id #{warehouseId} AND product_id #{productId} AND quantity #{quantity}重点在最后的quantity #{quantity}。这相当于告诉数据库如果库存不够不允许减。MyBatis-Plus 返回受影响行数代码判断影响行数为 0直接抛业务异常“库存不足”。这个方案比“先 select 库存再用 JAVA 判断再 update”安全得多。因为 select 和 update 之间可能存在并发两个请求同时读到库存 5同时卖出 5 件后写的一方就可能把库存改成负数。直接在 update 条件里做判断依赖数据库行锁把并发问题挡在 SQL 执行这一层。3.4 库存流水为什么不能省有些初级项目只更新库存表不写流水表面看也能跑。但一旦用户手一抖改错单据或者某个商品突然对不上账没有流水就只能干瞪眼。流水表的价值有两个第一是追溯每一笔出入库都能知道是由哪张采购单、销售单、盘点单产生的第二是对账用流水表可以分段重算某段时间的库存变化比如月初库存加本月入库减本月出库算出来应该等于月末库存如果有偏差说明某张单据被修改过或漏记了。这套源码还把“变动前库存”“变动后库存”写进了流水这就让数据库层面能够自查连续性和一致性。比如某条流水显示变动前 100变动后 90那应该有一条相邻流水或者期初库存衔接上。有了这些细节系统才敢叫“进销存”而不是库存记录。3.5 报表统计表给 MySQL 减负进销存跑久了单据量会很大每次打开销售统计报表都实时 group by 几百万行明细压力不小。这套源码的做法是在日结或月结时生成汇总统计表。典型的是销售日汇总表字段包括统计日期、商品 id、销售数量、销售金额、成本金额、毛利。当天单据落库后系统可以执行一个定时任务把当日明细聚合到汇总表报表页直接查汇总表。这样即时查询速度很快也方便看趋势。代价是实时性会差一点一般后台管理系统的报表晚几分钟更新完全可以接受。定时任务可以用 Spring 自带的Scheduled晚上执行统计也可以在每次单据审核时同步更新汇总。像这种中小型进销存项目我建议先用Scheduled做每日汇总简单稳定后续量大再考虑用消息队列异步更新也不迟。4. 后端实现接口分层、库存扣减和报表 SQL 的实战细节4.1 Controller、Service、Mapper 怎么分层才不磨叽这套源码的分层很规矩Controller 只负责接收参数、调用 Service、返回统一结构Service 负责业务规则、事务、跨表操作Mapper 只负责 SQL。不要在 Controller 里直接写一大堆业务判断否则后续测试和复用都会很痛苦。统一返回结构是用一个ResultT类实现的里面有 code、message、data 三个字段。所有接口返回成功就是 code0失败则抛全局异常由RestControllerAdvice统一捕获返回。这样做最大的好处是前端不用每个接口都写各自的异常处理拦截 response 后只看 code 就行。分页上用的是 MyBatis-Plus 的分页插件配置一个PaginationInnerInterceptor即可。需要注意版本兼容性MyBatis-Plus 3.5 系列和 SpringBoot2 搭配要用匹配的 mybatis-plus-boot-starter选错版本会出现启动报错或者 SQL 解析失败。4.2 登录鉴权和权限点校验登录这块源码选的是 JWT 鉴权。用户密码一般会做 BCrypt 加密数据库里不存明文。登录成功后服务端返回 token前端存储在本地并在请求头里带上Authorization: Bearer xxx。后端过滤器负责解析 token把用户信息放到 ThreadLocal 或请求上下文中后续业务接口通过注解或拦截器获取当前用户。权限校验我建议分两级第一级校验是否登录第二级校验该角色是否有某个权限点。比如“销售开单”可能对应一个sale:add权限点只有销售角色拥有财务角色调用这个接口就会被拦截。权限点可以写死在注解里也可以用字典表存储这对进销存这类角色边界清晰的系统足够。这里有一个经常踩的坑修改密码、禁用用户、删除角色之后旧 token 可能仍然有效。解决思路是在 JWT 里保存一个登录版本号或修改时间后端每次鉴权时比对数据库里的值不一致就强制重新登录。如果不想频繁查库也可以把 token 有效期调短一点配合 token 刷新机制兼顾安全性和用户体验。4.3 销售出库的完整事务实现销售开单不只是插入一条销售单记录还要扣库存、记流水、更新客户应收余额。我简化一下核心代码思路Transactional(rollbackFor Exception.class) public void createSaleOrder(SaleOrderVO orderVO) { // 1. 保存销售单主表状态为待审核 SaleOrder order new SaleOrder(); order.setOrderNo(generateOrderNo(SO)); order.setCustomerId(orderVO.getCustomerId()); order.setStatus(0); saleOrderMapper.insert(order); // 2. 保存销售明细 for (SaleOrderItemVO item : orderVO.getItemList()) { SaleOrderItem detail new SaleOrderItem(); detail.setOrderId(order.getId()); detail.setProductId(item.getProductId()); detail.setQuantity(item.getQuantity()); detail.setPrice(item.getPrice()); saleOrderItemMapper.insert(detail); } }到了“审核”环节才真正动库存Transactional(rollbackFor Exception.class) public void auditSaleOrder(Long orderId) { SaleOrder order saleOrderMapper.selectById(orderId); // 校验状态必须为待审核 ListSaleOrderItem items saleOrderItemMapper.selectList( new LambdaQueryWrapperSaleOrderItem() .eq(SaleOrderItem::getOrderId, orderId)); for (SaleOrderItem item : items) { // 查出对应仓库商品的库存记录 Stock stock stockMapper.selectOne( new LambdaQueryWrapperStock() .eq(Stock::getWarehouseId, order.getWarehouseId()) .eq(Stock::getProductId, item.getProductId())); if (stock null || stock.getQuantity() item.getQuantity()) { throw new BizException(商品库存不足 item.getProductId()); } int rows stockMapper.deductStock( order.getWarehouseId(), item.getProductId(), item.getQuantity()); if (rows 0) { throw new BizException(库存扣减失败可能库存不足 item.getProductId()); } // 写库存流水 stockFlowMapper.insert(buildFlow(stock, item, 销售出库, order.getOrderNo())); } order.setStatus(1); saleOrderMapper.updateById(order); }看到没有库存扣减根本不先 select 再判断而是直接调deductStock这种带条件的 update。这是进销存后端代码里含金量最高的一小段。只要事务在三张表的更新要么全成要么全败。4.4 采购入库和采购退货的相反逻辑采购入库逻辑和销售出库相反是增加库存、增加应付余额、写正向流水。采购退货则是减少库存、冲减应付。方向反了没关系但有一个细节采购入库时要考虑成本价格是否回写商品资料。轴承采购价波动不算小同一种商品可能这次 8 元下次 8.5 元。如果不做批次核算财务会很难受。完整解决方案是把库存细分到批次比如每批入库生成一个批次号销售出库时按先进先出法选批次并出成本。但批次管理对系统复杂度是成倍增加的。如果只是中小型进销存可以采用“移动加权平均”法每入一笔货更新商品的成本均价。这套源码在这个位置做了一个可配置项普通企业选移动加权平均就够不必强行上批次。4.5 报表查询 SQL能一次查完就不要循环查进销存报表最坏的习惯是循环查数据库。比如统计客户销售额有人会在 Java 里遍历客户列表每个客户再执行一次 count 或 sum客户一多数据库直接就卡。正确写法是一条分组 SQLSELECT c.customer_name, SUM(d.amount) AS total_amount, COUNT(DISTINCT h.id) AS order_count FROM sale_order h INNER JOIN sale_order_item d ON d.order_id h.id INNER JOIN customer c ON c.id h.customer_id WHERE h.order_date BETWEEN #{startDate} AND #{endDate} AND h.status 1 GROUP BY c.id, c.customer_name ORDER BY total_amount DESC要注意给 h.order_date 和 h.status 建联合索引否则报表数据量大时查询会走全表扫描。我自己见过不少项目前期数据量小很流畅上线几个月报表就开始慢最后看执行计划十有八九是索引没建到位。4.6 事务边界不要把查询塞进事务里刚才核心逻辑都加了Transactional但有一种情况要小心如果你在事务里调用了远程接口、导出 Excel、发了通知短信这些操作会拖长事务时间甚至因为外部依赖导致数据库连接长时间被占用。事务里只应该放和数据库强相关、必须保证原子性的操作。导出 Excel 这种耗时操作应该先查出数据、释放事务再生成文件。另外事务不要随便加在 Controller 层方法上。Controller 层是请求入口一个接口可能只调一个 Service 方法但如果你直接在 Controller 上开事务所有子调用都挤在大事务里后续不好细化回滚粒度。正确做法是把事务加在 Service 实现方法上并且是具体负责跨表更新的方法上。5. 前端实现Vue3 页面路由、表单联动和权限按钮5.1 管理后台的目录与路由设计前端项目结构看过之后挺清楚的页面放在 src/views 下面按业务模块建目录比如 procurement、sales、inventory、report。公共组件放在 src/components接口请求统一放在 src/api状态管理用的是 Pinia。路由有两种组织方式一种是全部静态路由适合模块少、页面固定的系统另一种是动态路由登录后根据角色返回菜单再动态注册。这套源码采用的是动态路由加静态路由结合。像登录页、404 页是静态路由业务页面放到动态加载需要权限判断的角色菜单由后端下发。动态路由有个需要注意的坑刷新页面时路由可能会丢失。因为 token 还在但 Vue Router 里的动态路由是登录后 addRoute 进去的刷新后内存清空页面找不到组件。解决办法是在应用初始化时如果检测到本地有 token就重新拉取用户信息和菜单再 addRoute。这个逻辑必须赶在路由跳转之前执行完否则一刷新就跳到 404。5.2 路由守卫里不要只校验 token路由守卫是前端权限控制的第一道闸每次跳转前判断有没有 token没有就跳登录页。有 token 但用户信息为空就拉一次用户信息。这看起来简单但我见过很多项目只判断“有没有 token”结果用户被强制退出登录后重新拿了一个别人账号的 token也照样能进系统。更稳妥的做法是在路由守卫里校验当前用户是否有某个角色或权限码。既然后端动态菜单是根据角色生成的校验菜单其实就是校验角色。按钮级别的权限比如“审核”按钮只有仓管员可见可以在组件里封装一个v-permission指令传入权限码没有权限就在绑定阶段把按钮移除。但请记住前端的按钮隐藏只是用户体验不是安全措施。只要接口没做权限校验绕过前端照样可以提交请求所以前后端权限校验必须同时存在谁也不能替代谁。5.3 销售开单页面选商品、带价格、算金额进销存前端最核心的交互是销售开单和采购开单。页面通常是左侧一个商品选择弹窗右侧一个明细表格用户逐行添加商品、修改数量、查看金额。商品选择弹窗的基本逻辑是输入商品编码或名称关键字调后端搜索接口。接口返回商品列表包含商品编码、品名、规格、单位、销售价、当前库存。用户点击某一行前端判断当前明细表格里有没有这个商品。如果已有则数量加一如果没有则往明细数组 push 一行。在选择商品后默认带出销售价这个价格优先取商品基础资料里的销售价但允许销售人员手工改。每行价格、数量变化时前端实时计算该行金额和总金额。这里我建议用计算属性而不是在模板里塞复杂表达式。否则明细一多页面渲染很容易卡。数量字段还要做个交互细节用户输入数量后前端要和当前库存做一次轻校验库存不足直接标红提示不必等提交后端才报错。但前端校验不能阻止用户提交因为有的客户允许预留销售让仓管员后续分配实际库存。5.4 表格分页、搜索条件和导出 Excel列表页基本模式是顶部搜索区中间表格底部翻页。搜索条件常见的有日期范围、商品分类、客户名称、单据编号。这块的实现套路很固定前端维护一个 searchForm 对象查询按钮把对象传给后端翻页时也把对象带上保证换页不会丢条件。导出 Excel 是进销存项目的高频需求。常见做法是后端生成文件流前端用 blob 下载。后端用 EasyExcel 或 POI 都行。这里有个容易踩的坑导出接口的查询条件要和列表接口保持一致否则用户明明筛选了某个时间范围导出却把全量数据倒出来。比较稳妥的方案是导出接口接收和列表一模一样的查询条件对象后台复用同一个查询 Service。另外导出不要放在查询接口里让前端用“查询”和“导出”两个按钮分别触发。原因是导出通常很慢如果和普通查询混在一起接口超时可能会拖垮前端页面请求。前后端配合时可以在导出请求前弹一个“正在导出”遮罩完成后下载文件体验会好很多。6. 部署、联调与排错源码拿回来从哪看起6.1 环境版本清单先统一再动手拿到这套源码第一件事不是打开 IDE 直接跑而是先确认本地环境版本是不是和源码匹配。根据这套系统的技术栈我整理了一个比较稳妥的版本组合组件推荐版本说明JDK8 或 8SpringBoot2 系列常用 JDK8部分兼容 JDK11Maven3.6 及以上依赖管理版本太高一般也没问题MySQL8.0.x注意字符集选 utf8mb4Node.js16 或 18对应 Vite 4 / Vue3 比较稳妥npm/yarn/pnpm与 Node 匹配团队习惯pnpm 安装速度快这些版本只要不是偏差太大通常都能跑。最容易出的问题是 Node 版本过高比如 Node20 搭配某些老版本 Vite 会报错MySQL 版本如果是 5.7有些 SQL 语法比如窗口函数就可能有兼容问题。所以先看文档里的版本说明再决定要不要升级或降级。6.2 数据库初始化和后端启动步骤后端启动前要把 application.yml 里的数据库连接改成本地地址比如jdbc:mysql://localhost:3306/xxx?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。MySQL8 的驱动类名是com.mysql.cj.jdbc.Driver如果项目里还是老驱动com.mysql.jdbc.Driver启动时会有警告。初始化数据库时源码一般带了一个 xxx.sql 或 init.sql 脚本包括建库建表语句、初始菜单、初始角色、初始管理员账号密码。执行完脚本后记得检查 admin. 用户状态是否正常密码是 BCrypt 加密的。如果原项目数据库里已经有测试业务数据建议先清空或复制一份到测试库里避免后面联调时数据干扰。启动后端后先不要急着开前端先用接口测试工具或浏览器直接访问一下健康检查接口。确认后端能通再启动前端开发服务器。否则前后端同时有问题排查起来会互相干扰。6.3 联调阶段最常见的四类问题联调时我总结下来大多数问题集中在四个方面。第一个是跨域。前端端口在 5173后端在 8080直接请求必然跨域。解决方式是在后端加 CORS 配置或者前端开发服务器配置代理。个人更推荐后端统一加跨域过滤器生产环境前后端可能不同域名用一个配置项解决。第二个是时间字段显示不对。后端接口返回日期时间前端用new Date()解析很容易出现时区偏移 8 小时问题。解决办法是后端统一返回格式化字符串如yyyy-MM-dd HH:mm:ss或者前端封装一个日期解析函数全局处理时间类型。第三个是金额精度。价格、金额一定要用 BigDecimal不能直接存 double。如果数据库中某张表的金额字段使用的是 decimal(10, 2)但 Java 类里却是 Double查询结果在某些情况下可能出现精度损失或四舍五入不一致。建议全局搜索 Double 类型涉及金额全部替换成 BigDecimal。第四个是分页参数。MyBatis-Plus 分页需要先配置分页插件否则Page只会返回全部数据只有配置了拦截器总数和页数才正确。另外前端传 pageNum 是第几页pageSize 是每页数量后端的PageT构造参数是当前页和每页条数含义要对上否则第一页数据会变成第二页。6.4 文档怎么读才不算浪费这套源码带了一份项目文档如果一上来就从头到尾通读很容易看到一半就没兴趣。我的建议是倒着读或者按问题驱动读。第一步先看“环境搭建”或“快速开始”章节把项目跑起来。第二步看数据库设计文档中的 ER 图和数据字典重点关注库存表、流水表、单据主表和明细表。第三步看接口文档中的销售和采购相关接口对照后端代码理解关键逻辑。最后再看前端页面和菜单配置理解整个系统的用户操作路径。遇到看不懂的名词比如“期初库存”“移动加权平均”“应收核销”不要跳过这些往往是进销存业务的关键概念。建议先查一下通用含义再回到代码里看对应字段这样理解会快很多。文档里没有覆盖的部分多翻数据库的表注释和代码注释这套源码的表注释和字段注释比较全比单独的 Word 文档更贴近实际代码。个人在实际操作中的体会是进销存项目最值得反复研究的就是那张“销售审核库存扣减流水记录”的事务链路。把它弄懂相当于拿到了所有单据类功能的通用钥匙。如果你也想在这套源码上扩展新功能比如增加多仓调拨、批次保质期管理、供应商对账建议先在表设计上规划好流水字段和状态字段再考虑接口。最后再分享一个小技巧单据编号最好不要依赖数据库自增 id而是用日期加序列生成比如SO20250526001这样用户看图说话、售后查单都方便得多。