☰
Java C/S架构进销存系统:从需求文档到二次开发的完整实践
2026/10/16 1:51:25 网站建设 项目流程

简介:这是一份面向Java初、中级开发者及课程设计/毕业设计场景的进销存系统C/S架构完整资料包,覆盖采购、销售、库存、财务等核心业务模块,同时包含需求文档与详细设计文档,能帮助读者理解企业级业务流转与Java桌面应用开发全流程。压缩包共252个文件,约1.63MB,主要包含Java源码(.java)、编译后class文件、Swing界面类、数据库脚本(.sql/.db)、设计文档(.doc)以及工程配置(.properties/.project/.classpath)等,可按MVC结构查看controller/model/view分层实现。目前已有199人学习下载。资料的价值在于:既有可直接运行与改造的Java工程代码,也有配套的需求分析、用例图、ER图、表结构等设计文档,还包括开发日志与测试记录,适合作为课程设计、项目实训或自学进阶时的参考蓝本,便于对照业务模块梳理完整落地方案。

1. 先说结论:这套 Java C/S 进销存是拿来就能跑的完整闭环

某个做日用品批发的朋友,账上库存和实物差了三百多件,进销存全靠 Excel,采购单散落在三个人的电脑里。后来我接手排查,给他落地了这套 Java C/S 进销存——Java 写客户端,MySQL 做存储,C/S 架构一头客户端一头服务端,还齐整地附带了需求文档和详细设计文档。它不是一个只能演示登录的玩具,采购、销售、库存、报表、权限都有完整的单据流,SQL 脚本导入即用。

适合刚学完 Java SE、想拿真实管理系统练手的开发者,也适合小团队做内部工具时直接改造。文档能让你看得懂每一步设计,源码能让你改得动每一个模块。下面我会按「它是什么 → 文档怎么拆 → 代码长什么样 → 怎么跑 → 怎么改」一路讲透。

2. 项目定位与技术选型:为什么是 C/S 而不是 B/S

2.1 这套进销存到底做了什么

进销存三个字拆开:进是采购入库,销是销售出库,存是库存变化。围绕三个核心动作,系统把单据、往来单位、库存流水和报表串成一条完整的链。

  • 采购管理:维护供应商档案,做采购单、采购入库单,入库时自动增加库存并记录应付账款。
  • 销售管理:维护客户档案,做销售单、销售出库单,出库时自动扣减库存并记录应收账款。
  • 库存管理:库存查询、库存上下限预警、盘点单、报损报溢上报。
  • 报表统计:采购汇总、销售汇总、库存明细、往来账款对账。
  • 系统管理:用户账号、角色权限、操作日志。

数据流的规则很朴素:一切库存变动都产生库存流水,报表不直接查商品表的当前库存,而是按流水统计。需求文档把这条写成了硬性约束——禁止业务代码直接 UPDATE 商品的库存字段而不写流水。这个约束后来值了不少钱,它保证任何时候导出的库存明细都能追溯到源头单据。

C/S 和 B/S 的选择是这个项目最早定下来的。B/S 要部署 Web 服务器,要考虑浏览器兼容,对只在局域网里跑的中小仓库来说太重;C/S 客户端直连数据库,界面响应快,单据录入体验接近桌面软件。仓管员不用开浏览器输地址,双击图标就能开单。缺点是升级要每个客户端重新分发,但这套系统把客户端做成了解压即用,分发成本很低。

2.2 技术栈拆解:Java Swing + MySQL + JDBC 的经典组合

客户端用 Java Swing 基于 JDK 8,没有引入 Spring 全家桶。数据访问层用 JDBC 封装成工具类,连接信息放在配置文件里。界面层用 JFrame、JTable、JDialog 搭建后台管理界面,表格负责数据展示,弹窗负责录单。

数据库连接配置长这样:

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/erp_db?useSSL=false&characterEncoding=utf8 jdbc.username=root jdbc.password=123456

参数说明:characterEncoding=utf8 必须带上,否则中文乱码从登录页就开始;useSSL=false 是 MySQL 5.7 的常见配置,避免 SSL 告警刷屏。如果是 MySQL 8,驱动换成 com.mysql.cj.jdbc.Driver,URL 再加 serverTimezone=Asia/Shanghai。

数据库访问的核心类长这样:

public class DbUtil { private static final Properties props = new Properties(); private static ThreadLocal<Connection> holder = new ThreadLocal<>(); static { try (InputStream in = DbUtil.class.getResourceAsStream("/db.properties")) { props.load(in); Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { Connection conn = holder.get(); if (conn == null || conn.isClosed()) { conn = DriverManager.getConnection( props.getProperty("jdbc.url"), props.getProperty("jdbc.username"), props.getProperty("jdbc.password")); holder.set(conn); } return conn; } }

这段代码的作用:getConnection 第一次调用时从配置加载驱动并建立连接,之后同一线程复用同一条连接,事务提交或回滚由上层控制。ThreadLocal 是这个项目事务一致性的关键,少了它,采购单主表和明细表就可能出现一个成功一个失败。

选 JDBC 而不选 MyBatis,原因很现实:核心表就十来张,SQL 写在 DAO 里反而好排查。MyBatis 的映射配置在这种规模下是额外负担。如果你更熟 MyBatis,改成 XML 映射也顺手,后面二次开发章节会提改造思路。

Swing 界面有一个必须注意的点:所有数据库查询不能在事件分发线程里执行,否则窗口拖拽会卡死。这套系统的常见做法是启动一个线程池,把耗时的列表查询丢给后台线程,查询完成再通过 SwingUtilities.invokeLater 回到 EDT 刷新表格。改造时如果发现某个窗口一打开就转圈,先检查是不是直接在按钮事件里查了数据库。

2.3 目录结构与工程分层

拿到源码后先看目录结构,能少踩一半坑。整个工程是标准 Maven 布局:

erp-system/ ├── sql/ │ ├── erp_db.sql 建表脚本 │ └── erp_data.sql 演示数据 ├── doc/ │ ├── 需求文档.md │ ├── 详细设计文档.md │ └── 接口说明.md ├── src/main/ │ ├── java/com/erp/ │ │ ├── model/ 实体类 │ │ ├── dao/ 数据访问 │ │ ├── service/ 业务逻辑 │ │ ├── view/ Swing 界面 │ │ ├── controller/ 界面与服务桥接 │ │ └── util/ 工具类 │ └── resources/db.properties └── pom.xml

分层是经典的三层:view 只管界面绘制,controller 接收界面事件并调用 service,service 管事务和业务规则,dao 只做 SQL 读写。model 里的实体类对应数据库表,字段命名一一对应。

不少进销存源码会犯同一个毛病——把 SQL 直接写在界面类里。线程一多,界面卡死、连接泄漏全来了。这套工程把依赖方向压成单向,改动一个模块时不用在工程里到处搜索。doc 目录下的两份文档不是凑数的,需求文档写清了每个模块的业务规则,详细设计文档画了 ER 图和调用链,二次开发时先查文档再动手,效率比直接读源码高得多。

3. 需求文档拆解:七个核心模块的边界与数据流

3.1 采购入库与供应商对账

需求文档开篇是角色和权限,然后进入功能描述。采购入库是里面最完整的一条链路:仓管员打开采购入库界面,选择供应商,选择商品,填数量和单价,保存后库存增加,供应商的应付余额同步增加。

需求文档对采购入库的定义:一张入库单包含一个主表和一到多行明细,主表存供应商、单据号、日期、总金额、经手人,明细行存商品、数量、单价、金额,保存时主表明细一起落库,任何一个明细行校验失败整单失败。这个「整单事务」的要求直接决定了 service 层必须开手工事务。

这里有个容易被忽略的规则:暂估入库还是正式入库?这套需求文档采用了「入库即确认」的简单方案,不搞暂估冲回。财务如果需要应付账款,入库单确认时同步生成应付流水。对中小贸易公司来说,少一个暂估流程能降低大量使用成本,但代价是没有在途入库的概念,如果你要给客户做代销入库,这块得自己扩展。

供应商对账部分是一张「供应商对账单」报表,按供应商汇总期间内的采购金额、已付金额、应付余额。设计文档特别注明:余额不 join 采购入库表去算,而是从应付流水表累加。原因是采购单可能被红冲、被作废,直接汇总主表金额会得出错误的欠款数。

3.2 销售出库与客户信用管理

销售出库是采购入库的镜像,但多两个业务规则。第一,价格取价策略:商品档案里预设销售价,开单时自动带出,允许手工修改;第二,信用额度:客户档案里有 credit_limit 字段,出库单保存时计算该客户当前应收余额加本单金额,超过额度不允许保存。

信用额度在需求文档里是硬限制,写在 service 层而不是界面层。原因很简单:界面可以被绕过,service 才是最后一道关。很多进销存只在前端做了 toast 提醒,后端不校验,业务员换个端口连数据库直接就把单保存了。

销售单据还有一个红冲需求。出库单录错了不能直接删除,而是做红冲——生成一张负数金额的红字单据,库存反向变动,流水里留下审计痕迹。需求评审时「删除」和「红冲」讨论了很久,最后定的是只有未审核单据能删除,已审核单据只能红冲。这个 audit_status 字段贯穿所有核心单据,二次开发加新单时最好也沿用。

另外销售退货在需求文档里被设计成一张独立的退货单,而不是红冲的简写。退货单带原销售单号,可以只退部分数量,库存加回,应收余额同步减少。如果只依赖红冲做退货,客户退货原因没法记录,统计退货率时要自己从流水里猜。

3.3 库存盘点与预警

盘点功能是仓库用得最多的。流程是:新建盘点单、选择盘点仓库、录入实盘数量、保存后系统自动计算盘盈盘亏、生成库存调整记录。需求文档对盘点有一个硬约束:盘点期间该商品禁止出入库操作,防止并发修改把盘点结果搞乱。实现上用了一张临时盘点表配合行锁。

库存预警是另一个高频需求:商品档案设置 min_stock 和 max_stock,每次库存变动后 service 层检查是否越界,越界则写预警记录。需求文档要求预警在主界面快捷提醒区展示,点击跳转到对应商品。这套机制不复杂,但对商贸公司来说,可能比炫酷的数据大屏更实用。

3.4 需求文档里容易被忽略的约束条件

下面几条从需求文档里摘出来的业务约束,是实施时最容易漏的:

  • 单据号生成规则:前缀加日期加流水号,比如 RK202501150001,并发下必须靠唯一索引或数据库序列防重。
  • 负库存控制:默认不允许,但不强制。销售出库库存不足时给提示但允许超卖,具体看客户能不能接受负库存。
  • 软删除:基础资料不能物理删除,只能停用,因为历史单据还要关联它们。
  • 金额精度:单价金额统一用 BigDecimal,禁止 double 运算,否则月底对账会出现 0.01 的差异。

我见过最典型的翻车是没注意软删除规则,直接写 DELETE 把被历史单引用的商品删了,报表 join 出一堆空行,排查了两天才发现是物理删除惹的祸。所以我会建议二次开发前先通读一遍需求文档的「业务规则」段落,那部分字数不多但全是坑。读文档的正确姿势不是从第一章念到最后一章,而是先找「不得」「必须」「禁止」这些词,这些地方才是业务真正的边界。

4. 详细设计文档:从表结构到核心代码实现的完整链路

4.1 数据库设计:八张核心表的关系

详细设计文档从 ER 图开始。核心表可以归成四组:基础资料、单据主表、单据明细、流水账务。中等规模进销存有十几张表,这里挑八张核心的讲。

表名关键字段说明
sys_userid, username, password, role, status用户与角色
base_productid, code, name, category_id, spec, unit, min_stock, max_stock, status商品档案
base_supplierid, name, contact, phone, payable_balance供应商及应付余额
base_customerid, name, contact, credit_limit, receivable_balance客户及信用额度
purchase_docid, doc_no, supplier_id, total_amount, audit_status, creator采购单主表
purchase_itemid, doc_id, product_id, qty, price, amount采购单明细
stock_flowid, product_id, biz_type, biz_no, qty, balance_qty, create_time库存流水
payable_flowid, supplier_id, biz_no, amount, direction, create_time应付流水

stock_flow 的 balance_qty 是个关键设计:每次变动时计算该商品的最新结存并写入流水行,查当前库存直接取最后一条流水即可,省去反复 SUM 的开销。代价是并发下可能出现余额错位,所以库存变动操作都要用数据库行锁或服务层同步块保护。

表之间用的是逻辑外键而不是物理外键。设计文档里说明了原因:历史数据要归档迁移,物理外键在导入数据时容易卡住,逻辑外键让导出导入更灵活。实际查询全部用 JOIN 完成,影响不大。

4.2 核心代码:采购入库的完整调用链

从界面点保存开始,一次采购入库的调用链是:

PurchaseView.save -> PurchaseController.save -> PurchaseService.save -> PurchaseDao.insertDoc -> PurchaseDao.insertItems -> ProductDao.changeStock -> StockFlowDao.insert -> PayableFlowDao.insert

service 保存的简化代码:

public void savePurchase(PurchaseDoc doc, List<PurchaseItem> items) throws Exception { Connection conn = DbUtil.getConnection(); boolean oldAutoCommit = conn.getAutoCommit(); try { conn.setAutoCommit(false); String docNo = DocNoGenerator.next("RK"); doc.setDocNo(docNo); docDao.insert(doc); for (PurchaseItem item : items) { item.setDocId(doc.getId()); itemDao.insert(item); // 库存增加 productDao.changeStock(item.getProductId(), item.getQty()); // 写库存流水 stockFlowDao.insert(item.getProductId(), "PURCHASE", docNo, item.getQty()); } // 写应付流水 payableFlowDao.insert(doc.getSupplierId(), docNo, doc.getTotalAmount()); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(oldAutoCommit); DbUtil.close(conn); } }

逻辑说明:先关闭自动提交,保证主表、明细、库存、流水在同一个事务里;DocNoGenerator.next("RK") 生成唯一单据号;productDao.changeStock 执行的是 UPDATE base_product SET stock_qty = stock_qty + ? WHERE id = ?,影响行数为 0 说明商品已被逻辑停用,要抛异常回滚;stockFlowDao.insert 里会重新查询当前库存并写入 balance_qty,保证流水余额和商品库存一致。

参数说明:items 由界面表格逐行拼出来,qty 用 BigDecimal;totalAmount 在 service 里重新求和覆盖前端传值,防止人为改价。这条在详细设计文档里是重点标注,是从「能用」到「敢用」的差别。

红冲操作的代码和保存对称:生成新单据号,数量取负数,库存变动是负数方向。负数量的单据在报表里单独标记,月底对账时一正一负自然抵消。

4.3 登录与权限控制实现

sys_user 表带 role 字段,角色分管理员、仓管员、业务员。登录时校验用户名密码,密码用 MD5 加固定盐,登录成功把用户对象放进 SessionManager 内存容器。

public User login(String username, String password) { String sql = "SELECT * FROM sys_user WHERE username = ? AND password = ? AND status = 1"; User user = userDao.findByUsernameAndPwd(sql, username, md5WithSalt(password)); if (user != null) { SessionManager.put(user.getUsername(), user.getRole()); } return user; }

权限控制分成菜单级和按钮级。菜单级在 view 层根据角色隐藏窗口;按钮级在 controller 判断当前用户是否有操作权限。这样业务员登录后看不到系统管理菜单,能看入库单查询按钮,保存按钮是否可见由权限决定。

登录这块有一个遗留问题:MD5 加盐只是防同事看明文,不是防外部攻击。系统定位是内网工具,这个强度够用,但一旦要公网部署,密码哈希要换成 BCrypt,登录接口要做失败次数锁定。二次开发清单里我会把这条放在第一位。

5. 避坑指南:环境搭建与运行期最常翻车的 5 个点

5.1 环境准备:JDK 与数据库版本怎么配

建议组合是 JDK 8 + MySQL 5.7 + IDEA 2020 以上。JDK 8 是 Swing 时代最稳的组合,JDK 9 之后模块化对老代码偶有兼容问题。MySQL 8 也能用,但要改驱动类和 URL 参数,不换的话启动直接抛 ClassNotFoundException。

MySQL 5.7 环境下,建库时字符集选 utf8mb4。utf8mb4 和 utf8 的差别是前者能存 emoji 和生僻字,对进销存影响不大,但统一用 utf8mb4 可以避免后续导入商品名带特殊字符时报错。

版本选择这件事没有玄学,就是把组合固定住。我一般会先确认本机有没有多个 JDK 版本,IDEA 里 Project Structure 的 SDK 和 Modules 的 Language Level 都要指到 JDK 8。很多诡异报错其实是编译用的 JDK 版本和运行用的不一致造成的。

5.2 数据库初始化与配置文件修改

用 Navicat 新建数据库 erp_db,字符集选 utf8mb4,然后依次执行 sql 目录下的两个脚本。erp_data.sql 里带了演示账号和演示数据,登录进去就能看到商品、供应商和几张历史单据。

接着改 src/main/resources/db.properties,把密码换成自己的。改完后先跑一次 com.erp.util.DbUtilTest 的 main 方法测连接,打印「连接成功」再启动界面,能省下大量排查时间。这个测试入口不是每个进销存都有,有的话优先用。

执行完脚本可以顺手验证一下数据:SELECT COUNT(*) FROM base_product,如果返回几条演示商品说明导入成功。如果表存在但没数据,多半是只执行了建表脚本,演示数据脚本被漏了。演示数据对排查问题很有用,尤其是做报表联调时,空库看不出汇总逻辑对错。

注意:登录账号默认是 admin/123456,连真实局域网数据库前先去系统管理里改掉。

5.3 启动客户端与黑匣子排查

启动类在 com.erp.Main,运行 main 方法会弹出登录窗口。如果闪退,看控制台异常,九成是数据库连不上或驱动类不对。Swing 老项目在部分 Linux 上还会遇到字体显示不全,给 JVM 加 -Dfile.encoding=utf-8 参数一般能解决。

如果双击 jar 包没反应,不要反复点,用命令行跑一次:

java -jar erp-client.jar

命令行会直接打印异常堆栈,比双击黑匣子靠谱得多。这一步实际上是所有 Java 桌面项目排错的第一动作。

登录进去之后如果发现某些按钮点了没反应,先别怀疑代码坏了,检查当前账号的角色权限。管理员角色通常有全部权限,业务员角色被菜单级权限挡住是预期行为。这种「没反应」最坑,因为它不报错,看起来像死锁,实际上只是判断条件在 controller 里提前返回了。

5.4 踩坑记录:现象、原因、解决

第一条,导入 SQL 报错。现象是 Navicat 执行脚本到一半中断。原因通常是脚本头部带了 CREATE DATABASE 语句,和当前连接冲突。解决:删掉文件头部的 CREATE DATABASE 和 USE 语句,再在已建好的库里执行。

第二条,中文乱码。现象是登录后商品名称显示为问号。原因是连接 URL 没带字符集参数。解决:把 db.properties 的 URL 改成带 characterEncoding=utf8 的完整写法,重启客户端。

第三条,保存采购单卡住然后报锁等待。现象是两个窗口同时操作同一个商品,一个保存时另一个转圈超时。原因是 Swing 多窗口各持连接,MySQL 行锁竞争。解决:在 service 层对同商品加同步块,或者调大 innodb_lock_wait_timeout。

第四条,修改密码后登录失败。现象是管理员在数据库里直接改了用户表的密码字段,然后怎么都登不进去。原因是密码存的是 MD5 加盐后的值,手动更新的明文和登录校验逻辑不匹配。解决:用界面提供的修改密码功能,不要直接 UPDATE 数据库。

第五条,报表金额对不上。现象是采购汇总和应付余额差了几分钱。原因是某处用了 double 计算金额。解决:全局搜索 double 类型在金额字段的使用,全部替换成 BigDecimal,然后重新跑一遍流水表的数据校验。

这五条是我和同事在这类 C/S 项目上真实踩过的坑,前三条属于环境配置,后两条属于业务逻辑。如果你能在一开始就避开软删除和 BigDecimal 这两个雷,后面省下的时间足够你再做一个模块。

6. 二次开发实战:给系统加一个「借还货管理」模块

给客户借一批展示样货,过几天还回来,这是商贸公司很常见的业务,但标准进销存主流程里没有这一项。我拿这个模块演示一遍从需求到表到代码的完整改造路径,你会发现这套分层结构加模块比想象中顺手。

先加两张表,一张主表一张明细:

CREATE TABLE borrow_doc ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doc_no VARCHAR(32) NOT NULL UNIQUE, customer_id BIGINT NOT NULL, borrow_date DATE NOT NULL, return_date DATE, creator VARCHAR(32), status TINYINT DEFAULT 0 COMMENT '0 借出 1 已还', remark VARCHAR(200) ); CREATE TABLE borrow_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doc_id BIGINT NOT NULL, product_id BIGINT NOT NULL, qty DECIMAL(12,2) NOT NULL, return_qty DECIMAL(12,2) DEFAULT 0 );

第二步写 DAO 和 Service:借出时扣减库存,归还时加回库存,库存变动全部复用 StockFlowDao,流水类型新增「BORROW」「RETURN」两种。复用流水表的好处是库存报表依然能从流水追溯,不用单独维护一套借还统计。

第三步加界面:在 view 包新增 BorrowView,参考 PurchaseView 的「表格 + 弹窗」结构;在 MainFrame 的菜单里注册入口。Swing 改造不需要动框架,复制一个现有 view 改字段绑定即可,这也是当初选三层结构省下的时间。

验证方法:借出 10 个商品 A,库存查询里 A 减少 10;提交归还单 10 个,库存恢复原值,borrow_doc 的 status 变成 1,同时 stock_flow 表能看到两条方向相反、类型清晰的流水记录。这一步验证的不只是界面,而是整个事务链路是否闭合。

最后补一句月底对账用的「未归还清单」SQL,方便业务方催还:

SELECT bd.doc_no, c.name AS customer_name, bi.product_id, bi.qty FROM borrow_doc bd JOIN base_customer c ON bd.customer_id = c.id JOIN borrow_item bi ON bi.doc_id = bd.id WHERE bd.status = 0;

我自己的习惯是每加一个模块都走一遍这个流程:先读需求文档确认规则,再写表,再写 DAO,再写 Service,最后补视图。从那以后我每次动这类老系统,都会强制在文档里补一笔模块改动记录,还要把验证 SQL 一起存进 doc 目录。希望帮到你。

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

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

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

立即咨询