Java C/S架构进销存系统实战:从环境搭建到避坑指南

📅 发布时间:2026/10/17 2:52:00
Java C/S架构进销存系统实战:从环境搭建到避坑指南
简介一套完整的Java C/S进销存系统项目资料覆盖采购管理、销售管理、库存管理、财务对账等核心业务并附有需求分析、业务流程图、用例图、数据库设计和详细设计文档适合课程设计、毕业设计或初步接触企业级C/S架构的开发者参考。包体共252个文件、约1.63MB以Java源码68个、编译后的class文件166个为主另含doc需求/设计文档、SQL数据库脚本、properties配置、项目工程文件及界面截图可直接对照源码理解Swing界面、JDBC数据访问、MVC分层与数据库表关系设计。已有199人学习下载。其价值在于完整源码配合需求文档和设计文档能帮读者梳理采购、入库、销售、出库、库存预警的完整业务链路同时提供数据库脚本和工程配置便于快速搭建运行环境源码中数据访问层与界面层职责清晰适合作为进销存项目的入门参考和二次开发基础。1. 一套Java C/S进销存资源先搞清它解决什么问题进销存系统用Java C/S架构搭配需求文档和详细设计文档这套组合在校园项目和企业内部工具里都很常见。很多开发者下载后第一反应是代码能跑吗数据库怎么初始化文档和代码能对上吗我最近拆了一套典型的Java C/S进销存源码里面包含需求文档、详细设计文档、SQL脚本和完整Java工程。结论只要JDK和MySQL版本对齐半小时能跑通登录和进销存主流程。但坑也不少——中文乱码、事务不回滚、界面假死几乎每个环节都有人翻车。本文就把落地路径、核心模块和踩坑记录按顺序拆开讲适合刚接触C/S项目的学生也适合要给小公司做内部管理系统的开发者。2. 资源拆解需求文档、详细设计文档和代码怎么对照着看2.1 需求文档不是摆设功能清单先画出边界拿到资源包后我习惯先不看代码先看需求文档。这份进销存的需求文档大约30页覆盖了背景、角色定义、功能需求、非功能需求。角色分三类管理员、库管员、财务。功能需求围绕“进、销、存、报”四条线展开下面这张表是核心模块的提炼模块功能点角色基础资料商品分类维护、商品档案、供应商管理、客户管理管理员采购管理采购订单、入库审核、退货库管员销售管理销售订单、出库审核、退货库管员库存管理库存查询、库存预警、盘点库管员统计报表进销存汇总、利润统计管理员/财务需求文档里每个功能都有用户故事比如“作为库管员我希望在商品入库时自动更新库存以便实时掌握库存数量”。拿到代码后先看主窗体菜单栏对照这张表打勾。能跑通的功能打“Y”空壳写“N”。常见问题是文档里写了“库存盘点”代码里根本没有或者点击后弹“未实现”。这是课程设计资源最常见的问题因为作者为了凑文档把不存在的功能写进去了。我一般会按三遍法来核对需求文档第一遍通读了解业务全貌第二遍把每个功能点对应到菜单路径比如“采购管理-采购订单”对应主窗体的采购订单菜单第三遍针对每一个功能点实际操作一遍记录“预期行为”和“实际行为”的差异。这一步非常关键因为后面详细设计文档和代码都以需求文档为源头如果源头有虚后面全对不上。先画边界避免被文档带偏。很多同学拿到资源就直接打开代码结果越看越晕最后连数据库都建不起来就是少了这一步。2.2 详细设计文档表结构、模块划分和类图怎么落地详细设计文档一般包含E-R图、数据库表结构、模块类图、关键时序图。对于进销存核心表大致有这些表名说明关键字段t_user用户表id, username, password, rolet_goods商品表id, code, name, spec, unit, price, category_idt_supplier供应商表id, name, contact, phonet_customer客户表id, name, contact, phonet_purchase_in入库单主表id, order_no, supplier_id, total_amount, status, create_timet_purchase_item入库单明细id, order_id, goods_id, quantity, pricet_sale_out出库单主表id, order_no, customer_id, total_amount, status, create_timet_sale_item出库单明细id, order_id, goods_id, quantity, pricet_stock库存表id, goods_id, quantity, update_time详细设计文档里的表结构是数据库初始化脚本的“答案来源”。拿到SQL脚本后先用文本编辑器打开搜索CREATE TABLE核对字段是否与文档一致。常见差异文档里的t_stock有两个字段min_quantity和max_quantity脚本里没有或者类型从DECIMAL(10,2)变成DOUBLE。这种不一致会导致后面SQL语句运行报错或数据精度丢失。模块划分方面这份资源采用Swing客户端 JDBC直连MySQL的胖客户端模式严格说也是Client/Server架构中的一种——客户端负责界面和业务逻辑MySQL服务器负责数据存储。类包结构一般分view、service、dao、model、util。我一般会在IDE里展开包结构截图保存到笔记里然后对照详细设计文档的类图核对。文档里画了UserService、GoodsService、StockService代码里一般都有但文档里的ReportService可能被合并到StatisticsService里。这种小差异不影响运行但影响你理解“作者当时是怎么设计的”。核对表结构时我习惯按这个步骤走打开详细设计文档的“数据库设计”章节。打开SQL目录下的init.sql。用文本编辑器的搜索功能把每个核心表名过一遍。记录差异比如字段缺失、库存表没有预警值字段、外键约束可能没建。以SQL脚本为准因为代码里的SQL语句是照着脚本写的文档不是。为什么C/S模式值得学因为进销存是典型的数据录入密集、响应要求高的系统C/S模式下Swing界面操作流畅本地直连数据库延迟低适合局域网内小规模用户。缺点是需要安装客户端、升级麻烦。这份资源的价值在于完整展现了一个C/S系统的分层结构适合学习JDBC和Swing组合。3. 环境搭建与数据库初始化从零跑通这个C/S系统3.1 JDK版本选择与项目导入这份资源我拆的版本是基于JDK8开发编译级别是1.8。如果本机装了JDK17直接打开可能会报“无法访问或加载类”主要是Swing没变但有些老代码用了com.sun.*内部类或者某些第三方jar不再兼容。建议先装JDK8并配置JAVA_HOME。先在命令行确认当前版本java -version如果输出是1.8.0_xxx可以直接用。如果输出openjdk version 17或更高要么下载JDK8并切换要么把项目的编译级别改成你当前版本的兼容模式。但新手不建议适配C/S资源里的老代码往往依赖一些在更高版本中被限制的内部API强行适配可能要改十几处。导入项目的步骤用IDEA的话File - New - Project from Existing Sources选中资源包里的pom.xml或直接选文件夹。如果是EclipseFile - Import - General - Existing Projects into Workspace。多数资源是没有Maven的依赖包都在lib目录下。导入后右键lib下的jar包选Add as Library确保编译时不缺包。检查依赖时重点看mysql-connector-java.jar是否存在。有些资源包为了压缩体积把这个jar删了结果一跑就ClassNotFound。3.2 MySQL建库脚本与配置文件的四个必改参数资源包一般有一个sql目录里面是init.sql。执行前先查看脚本里的数据库名、字符集。mysql -u root -p sql/init.sql如果脚本包含CREATE DATABASE IF NOT EXISTS erp DEFAULT CHARACTER SET utf8;则会自动建库。如果脚本只建表不建库需要先手动创建数据库CREATE DATABASE erp DEFAULT CHARACTER SET utf8mb4; USE erp; SOURCE /path/to/init.sql;注意SOURCE后面的路径要用绝对路径Windows下是C:/...反斜杠要转义或改成斜杠。接着修改配置文件。常见配置文件是src/db.properties或config/db.properties。打开后里面应该是类似这样的内容jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/erp?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.password123456 jdbc.pool.maxActive5参数说明jdbc.driverMySQL 5.x用com.mysql.jdbc.DriverMySQL 8.x要用com.mysql.cj.jdbc.Driver否则报ClassNotFoundException。这个是最常见的启动失败原因。jdbc.url3306是MySQL端口erp是库名必须和脚本里的库名一致。useUnicodetruecharacterEncodingutf8解决中文乱码useSSLfalse是为了避免MySQL 8的SSL警告。jdbc.username/password改成自己本机的账号密码不要用123456这种大概率不对的占位符。jdbc.pool.maxActive如果代码里用的是连接池这个参数决定最大连接数。进销存操作频繁建议5以上但也别太大小服务器会被连接数打爆。改完配置后可以先用一个小工具验证连接避免在UI里反复试错。在IDE里写一个临时的Java类或者在命令行用MySQL客户端直接连一下库看能否正常访问。3.3 启动服务端与客户端先验证登录如果资源是带Socket服务端的流程是先启动Server.java再启动LoginUI.java。如果是直连数据库直接启动客户端即可。对于带服务端的版本服务端入口一般在server/ServerMain.java。在IDE里右键运行进度会停在那里等待客户端连接。也可以命令行启动java -cp .;lib/mysql-connector-java.jar server.ServerMain说明-cp指定类路径Windows下分隔符是;Linux/Mac是:。如果服务端端口被占用会报BindException需要改server.properties里的server.port默认10200。客户端启动入口java -cp .;lib/mysql-connector-java.jar view.LoginUI登录界面输入默认账号admin/admin123。如果登录失败优先看控制台报错——90%的情况是数据库连接问题解决方案见第5章避坑。提示如果脚本里预置的管理员密码是明文123456而代码里登录时做了MD5加密那么无论如何都登录不进去。常见做法是先在SQL里执行UPDATE t_user SET passwordMD5(admin123) WHERE usernameadmin;。4. 核心模块实战登录、商品管理、出入库和库存查询4.1 登录模块的密码校验与会话保持登录模块是C/S进销存的门面。常见的错误写法是对比明文密码正确做法是数据库存MD5值登录时把输入密码做同样的摘要再去比对。用PreparedStatement而不是Statement拼接否则SQL注入分分钟被答辩老师盯上。public User login(String username, String password) throws Exception { String sql SELECT id, username, role FROM t_user WHERE username? AND password?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, MD5Util.md5(password)); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setRole(rs.getString(role)); return u; } } } catch (SQLException e) { LogUtil.error(login, e); throw new BusinessException(登录查询失败请检查数据库连接); } return null; }逻辑说明?占位符先set参数再执行避免SQL拼接密码在数据库里存的是MD5摘要登录时同样做MD5比对是在SQL层完成所以查不到就返回null。try-with-resources保证连接、PreparedStatement、ResultSet自动关闭避免连接泄漏。参数说明如果MySQL版本是8DBUtil.getConnection()内部的Class.forName要写成com.mysql.cj.jdbc.Driver。MD5Util.md5在工具类里如果源码包没有可以自己实现算法固定。BusinessException是自定义异常在Service层统一抛出后由UI层捕获并弹窗不要直接让SQLException裸奔到界面。会话保持登录成功后把User对象放到一个全局的AppContext中界面上按钮的权限根据role值动态启用/禁用。比如管理员可见“用户管理”菜单库管员隐藏。有些资源包只是把id存下来没有权限区分这个后面进阶章节再补。4.2 商品档案管理C/S模式下的增删改查商品档案是进销存的基础数据。在C/S模式下界面常用表格JTable展示底层用DefaultTableModel。添加商品时先做前端校验再调用Service层。这里给出一个Service层的插入方法public void addGoods(Goods g) throws Exception { Connection conn DBUtil.getConnection(); String sql INSERT INTO t_goods (code, name, spec, unit, price, category_id) VALUES (?,?,?,?,?,?); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, g.getCode()); ps.setString(2, g.getName()); ps.setString(3, g.getSpec()); ps.setString(4, g.getUnit()); ps.setBigDecimal(5, g.getPrice()); ps.setInt(6, g.getCategoryId()); ps.executeUpdate(); } }逻辑说明这里没有手动加事务因为单表插入不涉及多条SQL但如果同时要更新分类表里的商品数量统计就必须包事务。注意price用BigDecimal而不是double否则后续算总金额会有精度玄学误差。参数说明code是商品编码建议唯一。如果数据库里没加唯一约束代码里在插入前先查一次是否存在否则会插入重复数据。spec是规格比如“500ml/瓶”这个字段在查询时经常要和名称拼接展示所以不要留空。删除和修改同理但删除前要检查t_stock里是否还有库存。如果库存大于0不能硬删否则历史单据对不上账。这个坑在避坑章节也会提到。在UI层表格刷新一般用tableModel.setRowCount(0)清空旧数据再逐行加入新数据。不要直接table.setModel(new DefaultTableModel())否则会丢列头。4.3 入库出库单流转事务和库存更新的边界这是整个系统最核心的地方。入库操作要同时插入入库单主表、入库单明细并更新库存表。三步必须在一个事务里任何一步失败都要回滚。很多新手写代码时只插了主表忘了更新库存或者更新库存时用了UPDATE t_stock SET quantity ?而不是quantity quantity ?导致覆盖。public void purchaseIn(PurchaseOrder order, ListPurchaseItem items) throws Exception { Connection conn DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement psOrder conn.prepareStatement( INSERT INTO t_purchase_in (order_no, supplier_id, total_amount, status, create_time) VALUES (?,?,?,?,?), Statement.RETURN_GENERATED_KEYS); PreparedStatement psItem conn.prepareStatement( INSERT INTO t_purchase_item (order_id, goods_id, quantity, price) VALUES (?,?,?,?)); PreparedStatement psStock conn.prepareStatement( UPDATE t_stock SET quantity quantity ?, update_time NOW() WHERE goods_id ?)) { psOrder.setString(1, order.getOrderNo()); psOrder.setInt(2, order.getSupplierId()); psOrder.setBigDecimal(3, order.getTotalAmount()); psOrder.setInt(4, order.getStatus()); psOrder.setTimestamp(5, new Timestamp(System.currentTimeMillis())); psOrder.executeUpdate(); ResultSet keys psOrder.getGeneratedKeys(); int orderId 0; if (keys.next()) { orderId keys.getInt(1); } for (PurchaseItem item : items) { psItem.setInt(1, orderId); psItem.setInt(2, item.getGoodsId()); psItem.setInt(3, item.getQuantity()); psItem.setBigDecimal(4, item.getPrice()); psItem.executeUpdate(); psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getGoodsId()); psStock.executeUpdate(); } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }逻辑说明setAutoCommit(false)后所有SQL在同一个事务里执行RETURN_GENERATED_KEYS拿到主表自动生成的ID用来插明细更新库存时用quantity quantity ?这样不会覆盖已有库存。参数说明PurchaseItem的quantity是本次入库数量必须大于0。如果库存表里还没有该商品记录需要先初始化一条库存记录否则UPDATE影响行数为0没人察觉。这里建议在更新后检查psStock.executeUpdate()返回值如果等于0则先执行INSERT INTO t_stock再更新。出库逻辑刚好相反库存更新写成quantity quantity - ?并且要加一个quantity ?的条件防止库存出成负数。如果UPDATE影响行数为0说明库存不足应该抛异常回滚整个出库单。4.4 库存查询与报表把数据变成可操作信息库存查询一般用多表关联t_stock联t_goods显示商品编码、名称、规格、当前库存。如果加预警还要查min_quantity。public ListStockView queryStock(String keyword, int page, int pageSize) throws Exception { String sql SELECT g.code, g.name, g.spec, s.quantity, g.min_quantity FROM t_stock s JOIN t_goods g ON s.goods_id g.id WHERE g.name LIKE ? OR g.code LIKE ? ORDER BY g.code LIMIT ? OFFSET ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, pageSize); ps.setInt(4, (page - 1) * pageSize); try (ResultSet rs ps.executeQuery()) { ListStockView list new ArrayList(); while (rs.next()) { StockView v new StockView(); v.setCode(rs.getString(code)); v.setName(rs.getString(name)); v.setSpec(rs.getString(spec)); v.setQuantity(rs.getInt(quantity)); v.setMinQuantity(rs.getInt(min_quantity)); list.add(v); } return list; } } }逻辑说明LIKE配合%做模糊匹配分页用LIMIT ? OFFSET ?注意MySQL的LIMIT参数不能直接拼在字符串里必须用PreparedStatement设置。OFFSET计算用(page - 1) * pageSize当page从1开始。参数说明pageSize建议20-50如果一次性加载几千条商品Swing的JTable会卡顿所以分页查询是进销存界面流畅的关键。报表统计通常用汇总SQLSELECT DATE_FORMAT(create_time, %Y-%m-%d) day, SUM(total_amount) amount FROM t_sale_out GROUP BY day ORDER BY day DESC LIMIT 30;这个SQL可以在DAO里执行返回数据后可以用JFreeChart画柱状图。如果资源包不带图表库至少用表格展示汇总结果否则“统计报表”功能就是一个摆设。5. 避坑指南进销存C/S项目最常见的五个坑5.1 数据库连接失败为什么提示ClassNotFoundException或Access denied现象启动客户端后点击登录界面弹出“数据库连接异常”控制台输出ClassNotFoundException: com.mysql.jdbc.Driver或Access denied for user rootlocalhost。原因前者是忘记把MySQL驱动jar放进lib目录或者驱动类名写错MySQL 8要写com.mysql.cj.jdbc.Driver。后者是密码错误或账号没有远程访问权限如果数据库在另一台机器上。解决先检查db.properties里的driver和URL。如果是MySQL5.x驱动类名是com.mysql.jdbc.DriverMySQL8.x是com.mysql.cj.jdbc.Driver。然后确认jar存在。Access denied就重置root密码或新建专用账号并赋予GRANT ALL PRIVILEGES ON erp.* TO erpuser%。我一般会在命令行mysql -u root -p先试连一次确认不是MySQL服务没启动。5.2 中文乱码JVM、SQL和Swing三方编码不一致现象商品名称显示成???或者插入到数据库里变成乱码。原因三个环节出问题都会这样。第一db.properties里URL没加characterEncodingutf8第二SQL脚本建库时字符集不是utf8第三IDE编译Swing文件时用了GBK编码导致字符串字面量本身是乱码。这是C/S项目最容易翻车的地方我遇到过折腾一整天最后发现是编译器默认编码问题。解决统一UTF-8。在mysql命令行执行SHOW VARIABLES LIKE character_set%;确认character_set_server是utf8。db.properties里加useUnicodetruecharacterEncodingutf8。IDE里把Project Encoding、File Encoding都改成UTF-8。如果已经插入了乱码数据库里先删掉重新插入。Swing的JTable渲染一般不用改只要数据源是UTF-8界面就正常。5.3 界面假死EDT里跑了网络IO或慢SQL现象点击“查询库存”后整个窗口变成白屏/无响应过几十秒才恢复或者永远不恢复。原因Swing的事件分发线程EDT被耗时操作阻塞。进销存里最常见的阻塞是直接在ActionListener里执行Thread.sleep、Socket通信、大表全量查询。比如查询10万条商品数据EDT卡死系统看起来像崩溃。解决所有可能超过100ms的操作放到SwingWorker或新线程中执行完成后用SwingUtilities.invokeLater更新UI。例如new SwingWorkerListStockView, Void() { Override protected ListStockView doInBackground() throws Exception { return queryStock(keyword, page, pageSize); } Override protected void done() { try { table.setModel(new StockTableModel(get())); } catch (Exception e) { e.printStackTrace(); } } }.execute();说明doInBackground在后台线程执行done在EDT执行。如果直接在doInBackground里往table里set模型会跨线程操作UI反而不安全。5.4 库存对不上事务没提交或更新语句写错现象入库单保存成功但库存表没变化或出库后库存不减反增。原因典型三种。第一插入主表和明细在同一个事务但更新库存的SQL写成了SET quantity ?如果传入的是本次数量而不是“原数量本次数量”就会把库存覆盖成本次数量。第二事务没有commit()或者中间方法吞掉了异常导致回滚没触发。第三UPDATE t_stock影响0行时没检查比如商品在库表没有记录。解决把库存更新统一写成quantity quantity ?在commit前打印日志看影响行数更新影响0行时补一条INSERT INTO t_stock。另外所有Service方法里不要用try { ... } catch (Exception e) { e.printStackTrace(); }至少要抛到Server层统一处理否则事务永远不自动回滚。具体排查时打开MySQL通用日志是最后的后悔药。临时开启SET GLOBAL general_log ON; SET GLOBAL general_log_file /tmp/mysql_general.log;跑一遍入库操作然后查看日志里实际执行的SQL顺序看是否少了UPDATE t_stock。定位到后再回头看事务边界基本就能找到问题。5.5 文档和代码版本不一致按图索骥反而翻车现象详细设计文档里画了“库存盘点”界面截图代码里根本没有需求文档里的菜单顺序和实际界面不同文档里写t_stock有min_quantity字段实际SQL里没有。原因很多资源包是作者在不同时期拼凑的文档更新到V2.0代码还停留在V1.0或者反过来。为了看起来完整把需求文档、设计文档、答辩PPT都塞进去了但没做一致性校验。解决先跑代码再读文档。把文档当作“背景知识”不是“验收标准”。跑通代码后用表格记录差异比如“需求文档靠山说支持盘点代码尚无建议后续实现”。如果要交课程设计或项目答辩这个差异表反而是加分项说明你真的动手验证过。我见过太多人照着文档写出和代码不匹配的“设计亮点”答辩时被现场演示打脸。6. 进阶技巧把单机C/S改造成可交付的小型系统如果只是跑通这套资源可以交差了。但真正要部署到小公司至少还要做三件事密码加盐、权限拦截、一键启动。密码加盐把4.1节的MD5Util.md5(password)改成MD5Util.md5(password salt)salt存用户表。虽然MD5不是安全算法但至少比明文强。加盐后的登录SQL要改成先按用户名查出用户和salt再在Java层计算比对不能直接在SQL里做拼接。权限拦截写一个简单的AuthUtil.check(role, goods:add)在Service层入口调用。不要依赖界面按钮是否隐藏因为C/S客户端被反编译后可以绕过界面直接调Service。如果不想引入Spring这种重框架可以写一个基础Service类在子类方法里手动调用AuthUtil.check或者用动态代理统一拦截。一键启动写两个bat脚本Windows下部署很实用。服务端脚本如果有服务端echo off cd /d %~dp0 set CLASSPATH.;lib\mysql-connector-java.jar start javaw -cp %CLASSPATH% server.ServerMain timeout /t 2 start javaw -cp %CLASSPATH% view.LoginUI说明start会开新窗口timeout让服务端先跑起来。如果没有服务端只启动LoginUI即可。这里的lib目录要和bat脚本放在同一个根目录下否则类路径找不到驱动。如果是Linux部署同样思路写start.sh用nohup java -cp方式后台运行。我拿到每一套C/S进销存资源都会强制自己走一遍“建库、改配置、登录、做一张入库单、查库存”的完整流程再回头去改文档差异记录。这个习惯帮我避掉了大部分“文档对不上代码”的坑也让交付给别人的时候心里有底。希望帮到你。本文还有配套的精品资源点击获取