Java进销存系统开发实战:从数据库设计到并发扣库存的完整方案 简介适合有 Java SE 基础、正在做课程设计或毕业设计的开发者参考。资源围绕进销存系统的需求分析与详细设计展开覆盖采购、销售、库存、财务等核心业务模块并说明 Java Swing/JDBC 在 C/S 架构中的落地方式以及 MVC 分层、数据库表关系、接口设计等内容。压缩包共 252 个文件约 1.63MB以 Java 源码java/class为主体配合需求文档、详细设计文档doc、SQL 脚本和数据库文件便于结合文档阅读源码、梳理业务流程与表结构。目前已有 199 人学习适合用来快速理解一个 C/S 模式进销存项目的整体结构也可作为撰写需求文档、详细设计文档或搭建同类型系统的参考模板。1. 为什么一个批发商老板能把我问住这张采购单该不该入库最开始接到进销存 Java CS 项目时需求方的原话很简单“我们就是个批发部想要个管进货、卖货、查库存的软件别太贵。”听起来像是个课程设计就能搞定的东西可真坐到店里看他们走了一遍流程我才发现问题根本不在“进销存三个字怎么写”而在于一张采购单从“下单”到“入库”中间隔了一整夜货到了仓库才发现实际到货数和单子对不上这时候库管是先按单子入库还是按实数入库财务又要按哪个数跟供应商结算就这一个问题直接把单纯增删改查的思路打穿了。这套方案要做的不是做一个“能录入单据的系统”而是做一个能把“采购、销售、库存、往来账”串成一个闭环的 CS 结构的 Java 进销存系统。它适合两类人一类是正在做 Java 课设或毕设的学生需要一套能讲清楚“需求分析→数据库设计→模块实现→测试验收”的完整过程另一类是刚入行的开发者想找一个业务逻辑完整、技术栈不冷门的参考项目来改造。下文就从需求文档怎么拆、数据表怎么建、核心业务逻辑怎么写、最容易翻车的点在哪儿一直到答辩怎么讲按这个顺序把全套方案摊开。2. 总体架构设计CS 两层变三层Swing 客户端不直接碰数据库2.1 技术选型为什么走 CS 而不是 BS进销存这类系统有一个鲜明的业务特征操作频繁、单据录入量大、需要离线容忍而且使用场景集中在门店或仓库内网。客户自己也明确说了不想搞浏览器登录、不想依赖公网服务器就想要那种双击图标就能开的桌面程序。所以技术栈定为 Java SE 做客户端、MySQL 做存储是合理的但这不代表一个连 JDBC 的胖客户端就够用。我采用的是“客户端 业务服务层 数据库”的轻量三层结构客户端只负责界面和输入校验业务层单独抽出数据访问统一走 JDBC 封装或 MyBatis这样后面如果想换界面框架或加个 Web 管理端业务逻辑不用动。客户端界面用 Swing 而不是现在更“时髦”的 JavaFX主要考虑是客户机器配置老旧Swing 对低分辨率、小内存的兼容性更好而且大多数 Java 课设和毕设的教学资料仍然以 Swing 为主遇到布局或者表格模型的问题能查到的现成方案更多。当然如果项目允许JavaFX 的 TableView 和 CSS 样式在观感上确实更现代这部分我会在方案里注明替换方式但核心业务层保持不变。2.2 工程模块划分与客户端、服务端的职责边界整个系统按功能域拆成五个模块基础资料、采购管理、销售管理、库存管理、系统与报表。基础资料管商品分类、商品信息、供应商、客户、仓库采购和销售模块各自处理单据的录入、审核、退货库存模块负责出入库流水和盘点系统模块管用户、角色权限、操作日志和数据备份。每一个模块都对应一份详细设计文档里的用例图和类图模块之间只通过业务层方法调用不允许跨层直接操作数据库。一个我一直坚持的设计约束是客户端严禁写 SQL。哪怕是下拉框里查一个供应商列表也必须走服务层提供的getSupplierList()接口。这样做的好处让后来的排查极其方便——库存对不上账时只需要在服务层入口加日志就能定位是哪一笔操作把库存改错了不需要去翻 Swing 事件代码。另外一个好处是安全性客户端拿不到数据库账号密码数据库账号只配置在服务层连接池中避免开发时图省事把口令直接写在界面代码里导致泄露。com.wholesale.app ├── client // Swing界面登录、主窗体、各业务面板 ├── common // 公共类分页模型、导出工具、日期工具 ├── service // 业务接口与实现采购、销售、库存、报表 ├── dao // 数据访问JDBC封装、SQL语句、结果集映射 └── model // 实体类商品、采购单、采购明细、库存流水这个结构对应的实际工程就是上面这样一个标准分层各层之间依赖方向是从上到下的严禁出现 service 层反向依赖 client 层的情况。实际开发中考虑客户端如果用 Swing 做 MVC常常会把数据模型写在model包里同时被client和dao引用这没问题但model里的实体类必须保持纯粹的 POJO不带任何界面属性。很多学生在写这类系统时最容易犯的错是把业务逻辑写在窗体按钮的点击事件里结果一个按钮方法几百行后面想加个“审核后才能入库”的规则得满世界找。我这里强制要求每个模块的 Service 接口对应一个业务用例比如采购模块至少要有createPurchaseOrder、reviewPurchaseOrder、receiptPurchaseOrder、returnPurchaseGoods四个方法界面只负责收集数据和展示结果。3. 数据库设计不光是建表库存表和单据表之间有条看不见的约束链3.1 核心表结构与字段设计思路进销存系统的数据库设计是整套方案的灵魂评审导师或客户最看重的往往也是这一块。如果只建一张“商品表”加一张“出入库记录表”那需求文档里写明天就撑不住。我的方案里基础数据表有七张商品分类表、商品表、供应商表、客户表、仓库表、用户表、角色权限表业务表有六张采购订单表、采购订单明细表、销售订单表、销售订单明细表、库存流水表、盘点表。这里列出最核心的product和stock_flow的设计。CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, product_code VARCHAR(32) NOT NULL UNIQUE, product_name VARCHAR(64) NOT NULL, spec VARCHAR(64), unit VARCHAR(16) NOT NULL, purchase_price DECIMAL(10,2) NOT NULL, sale_price DECIMAL(10,2) NOT NULL, low_stock INT DEFAULT 0, status TINYINT DEFAULT 1, create_time DATETIME, update_time DATETIME ); CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, warehouse_id INT NOT NULL, flow_type TINYINT COMMENT 1采购入库 2销售出库 3退货入库 4销售退货出库 5盘点调整, ref_bill_no VARCHAR(32) COMMENT 来源单据号, change_qty INT NOT NULL COMMENT 正数入负数出, before_stock INT NOT NULL, after_stock INT NOT NULL, create_by INT, create_time DATETIME );商品表里的low_stock字段是库存预警用的低于这个值就在主界面弹提醒。价格字段用DECIMAL(10,2)而不是DOUBLE这是进销存第一原则——钱永远不用浮点数算否则账面上会冒出来 0.0001 元的差额。库存流水表是整个系统里数据量增长最快的一张表ref_bill_no一定要加索引因为每一笔单据冲正、对账、查历史都得靠这个字段关联回原始单据。flow_type用数字注释而不是字符串枚举为的是查询性能但在详细设计文档里会做一个枚举对照表。关于库存冗余字段我采用“库存流水为主、商品表冗余实时库存”的双写策略。商品表里加一个current_stock字段方便列表显示和预警判断但每次库存变动时必须先写流水再更新商品表的当前库存这两步必须放在同一个事务里。至于为什么不做成每次查询时用SUM(change_qty)现场算等你库存流水超过十万条再查一次商品列表就能感受到差距。3.2 采购单状态机表单、审核、入库之间的数据一致性采购单是整个流程里最容易出问题的环节。我设计方案时把订单表设计成一个状态字段来控制流转status取值依次为0 草稿、1 已提交、2 已审核、3 部分入库、4 已入库完成、5 已退货。为什么要有“部分入库”因为在真实批发场景中供应商分批发货太常见了一次进货 100 箱先到 60 箱隔天再到 40 箱。如果系统不支持部分入库库管就只能在到第一车货时把单子强行入库 100 箱账对不上是必然的。回想到最开始客户那个“按单子入库还是按实数入库”的问题我的答案是按实际到货数入库但入库前必须经过审核确认。整个流程是“采购员下单草稿→ 业务经理审核确认价格与数量→ 库管收货录入实际到货数量并生成入库流水 → 系统自动比对订单与入库差异差异部分生成待补收记录”。这样财务和库房看到的是同一张单子的两个维度谁也不用来回扯皮。-- 采购入库时判断当前已入库数量是否超过订单数量 UPDATE purchase_order SET status CASE WHEN received_qty #{qty} total_qty THEN 4 ELSE 3 END WHERE id #{orderId} AND received_qty #{qty} total_qty;上面这条 SQL 就是部分入库的核心逻辑。注意WHERE条件里带着 “已入库数量 本次数量不能超过订单数量” 的判断这笔数据如果返回更新行数为 0说明有人在并发操作或者录入有误业务层必须抛出异常而不是默默通过。这种“先比对再更新”的写法比先查一遍再更新的代码要安全得多能防住多窗口同时入库造成的超入。4. 核心模块实现销售出库、库存流水和往来账怎么在一个事务里收口4.1 销售模块的关键动作从下单到扣库存中间藏着个死锁陷阱销售模块的流程相对简单客户要货 → 开销售单 → 收款 → 出库。真正的复杂性在于一个销售单往往包含多个商品而每个商品都要更新库存、记流水、改客户欠款这些操作必须统一提交一个失败全部回滚。常规做法是给 Service 层方法加Transactional但这只是第一步真正容易翻车的是更新顺序。多个用户同时开单时如果线程 A 先锁商品 1 再锁商品 2线程 B 先锁商品 2 再锁商品 1两边就会互相等对方的锁数据库很快就报死锁错误。我一般会这样处理在销售出库这个服务方法里先把所有要扣减库存的商品按product_id升序排序然后依次执行库存更新保证所有线程都以同一个顺序拿锁。这样死锁概率会降到接近零。库存更新语句上还要带上“当前库存必须大于等于扣减数量”的条件防止超卖。// 按商品ID升序依次扣减库存避免交叉锁 ListOrderItem items order.getItems(); items.sort(Comparator.comparing(OrderItem::getProductId)); for (OrderItem item : items) { int rows stockDao.deductStock( item.getProductId(), item.getQty(), order.getOrderNo() ); if (rows 0) { throw new InsufficientStockException( 商品[ item.getProductName() ]库存不足当前可用库存 stockDao.getCurrentStock(item.getProductId())); } }这段代码里有几个值得注意的参数和设计点deductStock方法的 SQL 是UPDATE product SET current_stock current_stock - ? WHERE id ? AND current_stock ?它返回的rows是受影响的行数等于 0 就意味着当前库存不够根本不需要用查询先取出库存再在 Java 里做比较。另外业务层的异常一定要向上抛到控制层不能捕获后只弹一个提示就继续提交不然后面库存成负数了再查单子就晚了。4.2 报表与统计为什么说“库存流水表”是老板最认的功能功能做完能收钱的是报表。客户不懂什么三层架构什么事务隔离他就认一个数这个月赚了多少。销售统计报表需要按商品、按客户、按时间段三个维度汇总入库报表要让仓库知道每种商品进了多少、退了多少库存预警表则要列出所有低于low_stock的商品方便采购员安排补货。报表查询不建议用数据库视图因为参数一多变起来视图维护成本很高。我的方案是写一个专门的报表 DAO每个统计功能单独写 SQL比如销售汇总的 SQL 就按DATE(create_time)、product_id做分组外层再加日期区间和客户 ID 作为动态条件。这类报表查询量比较大印象里客户那边一个月的数据就十几万条所以在 SQL 里统一用pd.product_code关联商品表不仅查得清楚索引匹配也能用上。SELECT DATE(s.create_time) AS biz_date, p.product_code, p.product_name, SUM(sd.qty) AS total_qty, SUM(sd.qty * sd.price) AS total_money FROM sales_order s JOIN sales_order_detail sd ON s.id sd.order_id JOIN product p ON p.id sd.product_id WHERE s.create_time BETWEEN ? AND ? AND (? 0 OR s.customer_id ?) GROUP BY DATE(s.create_time), p.product_code, p.product_name ORDER BY biz_date DESC, total_money DESC;这个 SQL 的查询条件里? 0 OR customer_id ?是一个小技巧用来实现“客户下拉框选了就按客户查没选就查全部”。整套报表做完以后建议再导出一个 Excel 功能用的是 POI导出的列要和界面上显示的完全一致不然客户会说“这个报表什么都好就是导出来没法看”。操作日志表里也要把“谁、什么时候、导出了哪张报表”记录在案这是财务场景的基本合规要求。5. 进销存项目最容易翻车的五个细节从并发减库存到 Windows 打包字体模糊5.1 并发扣减库存导致库存变成负数现象是两台收银台同时卖同一个商品最后商品表的current_stock变成了负数流水账也是乱的。原因在于客户端执行“查询库存 → 判断足够 → 更新库存”这套流程没有加锁或没有条件约束两个窗口查询时库存都是 5各自判断可以卖先后减去 3 和 4最后就变成了 -2。解决方法是把扣减操作收敛成一条带条件的 UPDATE完全杜绝“先查再改”的写法同时业务层做乐观锁重试。这种问题在单机练习时永远测不出来但只要一上多客户端马上就现原形。5.2 单据编号在多人同时开单时重复现象是采购单和销售单的编号在第二天出现了两个相同的单号导致后续关联查询窜单。原因是直接用“日期 序号”拼接单号而且序号是用查询当日最大序号加 1 来生成的两个人同时查询时拿到的最大序号一样。正确的做法是用数据库表维护一个序号计数器或者直接用 UUID 作为业务编号但考虑到业务员要看着顺眼方案里倾向于用一张单号序列表每次生成单号时执行UPDATE sequence SET current_no current_no 1取回自增值这一取一换之间由数据库行锁确保唯一。5.3 日期时间筛选查不到当天的数据现象是录入一张单然后按“今天”查报表结果那张单没查出来。原因是界面上选择日期用的是java.util.Date传到 SQL 时带上了时分秒而查询条件只用了BETWEEN ? AND ?给了当天 00:00:00 到 23:59:59.999 之外的边界就可能把全天数据漏掉尤其在 MySQL 的 DATETIME 精度下。解决方式是统一使用LocalDate类型接收前端参数SQL 里对create_time ? AND create_time DATE_ADD(?, INTERVAL 1 DAY)这样的左闭右开区间来处理后端接口约定传日期即可不再传具体时刻。5.4 Swing 界面在高分屏下字体发虚模糊现象是同一套客户端在普通电脑上正常换到笔记本 2K 屏或外接高分屏上字体就糊成一片。原因是 Swing 在高 DPI 缩放下需要用系统属性启用缩放支持老代码没有配置时就会被 Windows 拉大位图来适配。在程序入口的main方法开头必须加上System.setProperty(sun.java2d.dpiaware, true)新版 JDK 则可以直接设jdk.gtk.version这样的属性。另外打包时候要明确主类不要用java -jar的默认清单我一般会在 pom 里配好maven-shade-plugin打一个包含全部依赖的可执行包。5.5 操作日志记录在业务代码里到处散落现象是需要追责一笔错误操作时查不到任何痕迹或者每个方法里手工写了一段日志入库代码导致业务代码里塞满了日志逻辑。解决方法是把日志记录放到 Service 层的方法执行前后统一处理利用代理拦截的方式自动记录“用户、时间、操作类型、核心参数摘要”业务代码里只留一个Log(审核采购单)这样的注解这样既不会漏记也更方便做查询归档。实际做的时候要注意日志和业务必须在一个事务里否则业务提交了日志回滚了就白记了。6. 验收与答辩怎么让导师或客户相信这套方案真的能交付到验收和答辩这一步最忌讳的是打开系统像无头苍蝇一样点来点去。我在交付时强制自己走一套固定的演示脚本按“基础资料 → 采购 → 销售 → 库存 → 报表”的顺序操作每一个环节都留下可查证的数据痕迹。比如演示采购时先录一张商品分类、加一个供应商再开一张采购单、审核、部分入库演示完立刻去库存流水里查这批货的入库流水。这一套流程走下来审核的人看到的不是“功能很多”而是“数据能闭环”这是进销存这种业务系统最重要的说服力。验收标准方面建议准备四份东西需求规格说明里的功能列表与验收结果对照表、核心业务流程的测试用例、多客户端并发测试记录、数据备份恢复的演练记录。这里有一个我在答辩现场被追问最多的问题“如果库存盘点发现账实不符系统怎么处理”回答思路一定不是含糊地说改库存而是讲清楚盘点的流程盘点单冻结库存 → 录入实际数量 → 系统计算差异 → 生成盘盈盘亏调整单 → 审核后生效并记录流水和操作日志。把这个闭环讲清楚比背十条功能列表都管用。关于演示数据有一条血泪经验登录账号的初始密码不要太复杂演示现场的紧张状态下输错三次密码被锁死是最尴尬的翻车。我习惯在演示环境预置好一套完整的业务数据包括近三个月的采购销售记录、库存预警商品、欠款客户这样打开报表页就能展示出有价值的统计结果而不是一个空列表。导师问到底层技术时重点讲事务边界、库存防超卖、权限控制三个点即可但前提是自己要能画清楚订单状态机的流转图。答辩时还有一个容易被问但平时不准备的问题是“这套系统如果用户量扩大十倍哪里最先撑不住”参考答案是先点出查询最频繁的库存流水表说明我们的分页查询加了索引也已经预留了按月分表的方案同时连接池的数量可以根据并发数调整接口层和数据层是分开的不会因为界面卡顿拖垮数据库。答到这个层次基本就证明这套方案不只是课程作业的水平了。从那以后我每次做这类系统都会把演示脚本和答辩预案当代码一样写进项目文档里强制自己提前演练一遍特别是那个最容易被追问的“账实不符”问题再也不空手上场。希望帮到你。本文还有配套的精品资源点击获取