ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java医药进销存系统基于SQL Server 2014的部署与二次开发

Java医药进销存系统基于SQL Server 2014的部署与二次开发

简介:面向医药进销存管理场景的Java源码项目,基于Eclipse开发,数据库使用SQL Server 2014,适合Java初学者巩固Swing界面编程与数据库操作,也适合有进销存开发需求的开发者参考。项目按基本信息、业务管理、业务查询、用户管理和系统管理五大模块组织,覆盖药品采购、销售、退货、客户与供应商维护、入库明细及销售明细查询等核心流程,业务逻辑完整清晰。资源共284个文件,除36个java源码和137个class编译文件外,还包含36张png界面图、35个jar依赖库、22个xml配置,以及数据库备份、日志和项目配置文件,压缩包大小16.27MB,整体结构规整,便于对照学习和二次开发。目前已有2676人学习下载,适合作为课程设计或毕业设计的入门模板,可直接导入Eclipse并配合SQL Server 2014运行调试,帮助读者快速理解医药进销存系统的前后端交互与数据库设计思路。

1. 医药进销存不是普通 CRUD:五模块、三链路和那份 appcontext.xml.bak

医药进销存管理系统这个 zip 表面看是个 Java 课程设计,实际上它把药房业务里的采购、销售、退货、客户往来、库存流水全串在了一条链上。解压之后你第一眼看到的不是一堆 .java,而是整整齐齐的 JF_view_*.class 文件,比如 JF_view_buy、JF_view_sell、JF_view_xstk,命名用的是拼音首字母,模块归属一眼能认出来。数据库锁定 SQL Server 2014,资源包里带 appcontext.xml.bak 这种 Spring 配置文件备份,说明它是可以编译运行、也可以拆开改的完整工程。适合两类人:准备交 Java 课程设计的人直接拿去跑通改界面,以及要接手维护老医药系统的工程师拿它当业务参照。至于它到底怎么连库、怎么启、哪几个环节最容易翻车,下面按实际拆包的顺序讲清楚。

2. SQL Server 2014 建库:核心表设计与初始化脚本

2.1 为什么选 SQL Server 2014 而不是 2008 或 2019

这份资源在摘要里明确写了数据库是 sqlserver2014,那环境就该尽量贴合。SQL Server 2008 R2 虽然也能跑,但语法上对 OFFSET-FETCH 分页、DATE 类型、STRING_SPLIT 这些特性支持不完整,老课程设计代码里如果用了 2012+ 才有的语法,08 上会直接报错。而 2019 或 2022 对硬件要求更高,安装包更大,对只想本地跑通项目的人来说没必要。

我的习惯是装 Developer 版或者 Express 版,功能上做单机开发完全够。安装时记住两个关键点:实例名用默认的 MSSQLSERVER,身份验证模式建议直接选“混合模式”,并给 sa 设置一个密码。如果你已经装完才发现是 Windows 身份验证,后面连接 JDBC 时会栽在“sa 登录失败”上,这一条在第五章避坑里展开说。

2.2 建库与五张核心表

在 SSMS 里新建查询,先建库再建表。这套建表脚本是我按进销存业务整理的,字段命名和这个项目的 class 命名风格一致,表结构尽量贴合它原生的设计习惯。

CREATE DATABASE DrugDB; GO USE DrugDB; GO -- 操作员表:对应“用户管理/增加用户” CREATE TABLE sys_user ( user_id INT IDENTITY(1,1) PRIMARY KEY, user_code VARCHAR(20) NOT NULL UNIQUE, -- 登录账号 password VARCHAR(50) NOT NULL, -- 登录密码,演示项目常用明文 user_role VARCHAR(20) DEFAULT '操作员' -- 管理员 / 操作员 ); GO

sys_user 表承担登录和权限判断。user_code 是登录账号,password 在真实项目里要做 MD5 加盐,但课程设计级别的代码基本都是明文存取,这里保持原样,别在生产环境这么干就行。

-- 药品表:对应“基本信息”菜单下的商品信息 CREATE TABLE drug_medicine ( drug_id INT IDENTITY(1,1) PRIMARY KEY, drug_code VARCHAR(30) NOT NULL, -- 药品编码 drug_name NVARCHAR(60) NOT NULL, -- 药品名称,中文用 NVARCHAR spec NVARCHAR(40), -- 规格,比如 0.25g*24片 unit NVARCHAR(10), -- 单位:盒 / 瓶 / 袋 manufacturer NVARCHAR(80), -- 生产厂家 approval_no VARCHAR(40), -- 批准文号 purchase_price DECIMAL(10,2), -- 进价 sale_price DECIMAL(10,2), -- 售价 stock INT DEFAULT 0, -- 当前库存,联动字段 stock_low INT DEFAULT 10, -- 库存下限,预警用 valid_date DATE -- 效期,医药系统一定要有 ); GO

drug_medicine 是整条业务链的核心。stock 字段会被采购、销售、退货三个单据反复 UPDATE,stock_low 是库存预警的边界值。valid_date 是医药行业特有的字段,普通进销存不需要,但药品批次有效期管理靠它,老系统这个字段如果缺失,后面做 GSP 检查会非常被动。

-- 采购入库表:对应业务管理下的药品采购 CREATE TABLE buy_in ( id INT IDENTITY(1,1) PRIMARY KEY, buy_no VARCHAR(30) NOT NULL, -- 采购单号,业务编号 supplier_id INT NOT NULL, -- 供应商ID,对应JF_view_gys drug_id INT NOT NULL, -- 药品ID buy_num INT NOT NULL, -- 采购数量 buy_price DECIMAL(10,2), -- 采购单价,注意要记录单据价 buy_date DATETIME DEFAULT GETDATE(), -- 入库时间 operator VARCHAR(20), -- 操作员,从sys_user取 audit_flag CHAR(1) DEFAULT '0' -- 0未审核 1已审核 ); GO -- 销售表:对应JF_view_sell CREATE TABLE sell_out ( id INT IDENTITY(1,1) PRIMARY KEY, sell_no VARCHAR(30) NOT NULL, client_id INT NOT NULL, -- 客户ID,对应JF_view_client drug_id INT NOT NULL, sell_num INT NOT NULL, sell_price DECIMAL(10,2), -- 销售单价 sell_date DATETIME DEFAULT GETDATE(), operator VARCHAR(20) ); GO -- 销售退货表:对应JF_view_xstk CREATE TABLE return_rec ( id INT IDENTITY(1,1) PRIMARY KEY, return_no VARCHAR(30) NOT NULL, source_bill VARCHAR(30), -- 原销售单号,追踪退货来源 drug_id INT NOT NULL, return_num INT NOT NULL, return_date DATETIME DEFAULT GETDATE(), return_type VARCHAR(10) DEFAULT '销售退货' -- 销售退货 / 采购退货 ); GO

这三张表是业务单据的底表。注意 buy_in 里的 buy_price 是独立字段,不要 JOIN 药品表取 purchase_price,因为历史单据价格必须锁定,药品改价后旧单不能被影响。sell_out 同理,如果销售价实时 JOIN,报表里的历史毛利就会跟着价格漂移。

它们的表名和 JF_view 开头的 class 一一对应:JF_view_buy 操作 buy_in,JF_view_sell 操作 sell_out,JF_view_xstk 操作 return_rec,这个推理在后面第四章还会用到。

2.3 回款与库存明细

项目里还有一个很关键的查询 class:JF_view_query_kcmx_hk.class。kcmx 是“库存明细”拼音首字母,hk 是“回款”。这意味着系统不只记录销售,还跟踪客户回款情况。要支撑这个功能,需要一张客户往来账表。

CREATE TABLE client_account ( id INT IDENTITY(1,1) PRIMARY KEY, client_id INT NOT NULL, bill_type VARCHAR(10), -- 销售 / 收款 / 退货 bill_no VARCHAR(30), amount DECIMAL(10,2), account_date DATETIME DEFAULT GETDATE(), remark NVARCHAR(100) ); GO

client_account 是一张往来流水账。销售单生成时插一条正数,客户回款时插一条负数,JF_view_query_kcmx_hk 按 client_id 汇总就能算出客户欠款余额。这个设计不算复杂,但好处是每笔变动都有原始单据号可追踪,系统里“业务查询”模块的准确率就靠它。

2.4 初始化数据

建完表先塞一条管理员账号和几个基础药品,否则第一次启动界面是空的,很难判断程序是跑通了还是查不到数据。

INSERT INTO sys_user (user_code, password, user_role) VALUES ('admin', '123456', '管理员'); GO INSERT INTO drug_medicine (drug_code, drug_name, spec, unit, manufacturer, purchase_price, sale_price, stock, stock_low, valid_date) VALUES ('YP0001', N'阿莫西林胶囊', N'0.25g*24粒', N'盒', N'某制药厂', 8.00, 12.50, 200, 20, '2026-06-30'), ('YP0002', N'布洛芬缓释片', N'0.3g*20片', N'盒', N'某药业', 15.00, 22.00, 150, 15, '2025-12-31'); GO

初始化数据里最需要注意的是中文前面的 N 前缀。SQL Server 里 NVARCHAR 字段如果直接写 '阿莫西林' 而不加 N,在中文排序规则下可能没问题,但数据库排序规则一旦是 Latin1_General,就会存成乱码或者直接报错。写成 N'阿莫西林' 是让 SQL Server 明确按 Unicode 处理,这是跟 MySQL 完全不同的习惯,老手也常在这里翻车。

3. Eclipse 跑通项目:JDBC 连接参数、驱动 jar 和启动主类

3.1 解压结构与文件还原

解压以后你会看到两类东西:一类是 .class 编译文件,一类是 appcontext.xml.bak 配置备份。.class 文件应该放在项目的 bin 或 classes 目录里,如果用 Eclipse 新建工程导入,直接把解压后的整个目录作为工程根目录即可。Eclipse 会自动识别 .classpath 和 .project 文件,没有这两个文件就新建一个 Java Project,再把解压内容拷贝进 src 目录。

.appcontext.xml.bak 这个文件要注意,后缀 .bak 说明这是原始配置文件的一份备份。Spring 默认加载的是 appcontext.xml,所以第一步就是复制一份并改名:

cp appcontext.xml.bak appcontext.xml

改名后打开 appcontext.xml,里面是一个 Spring 管理的 DataSource 配置。老项目最常见的 DBCP 连接池写法如下,对应字段就是这个项目里会被改的三处。

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/> <property name="url" value="jdbc:sqlserver://localhost:1433;DatabaseName=DrugDB"/> <property name="username" value="sa"/> <property name="password" value="your_password"/> <property name="initialSize" value="3"/> <property name="maxActive" value="10"/> </bean>

三个必改参数:url 里的数据库实例名和端口、username、password。localhost 表示本机,如果你的 SQL Server 2014 装在远程机器或者虚拟机里,把 localhost 换成 IP。1433 是 SQL Server 默认端口,如果安装时改过端口,这里要同步改。

3.2 JDBC 驱动下载与适配

SQL Server 2014 的 JDBC 驱动选择有一点历史遗留问题。很多人拿到的老教程推荐 sqljdbc4.jar,这个 jar 对应 JDK 6+,在 JDK 8 下能正常用。微软后来把命名规则改成了 mssql-jdbc-6.2.2.jre8.jar,更直观。

驱动版本和 SQL Server 版本对照大致如下:

驱动 jar对应 SQL Server适用 JDK
sqljdbc4.jar2008 / 2008 R2 / 2012JDK 6+
mssql-jdbc-6.2.2.jre8.jar2012 / 2014 / 2016JDK 8
mssql-jdbc-7.2.2.jre8.jar2014 / 2016 / 2017JDK 8

所以锁定 SQL Server 2014 时,优先选 mssql-jdbc-6.2.2.jre8.jar,兼容性最稳。下载后把 jar 放到项目的 lib 目录,然后在 Eclipse 里 Build Path -> Add JARs 引入。这一步漏掉会出现 java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver,我在第五章会专门讲。

3.3 编译与启动入口

这个项目入口是一个 Java Swing 主窗口。从文件名规律看,JF 是 Java Frame 的缩写,view 表示界面层,那么启动类大概率是 JF_view_login 或者某个 MainFrame。你可以直接在 Eclipse 里右键项目,Run As -> Java Application,在弹出的列表里找带 main 方法的类。

如果 Eclipse 没有自动编译出目标文件,手动编译也可以,命令如下:

javac -encoding UTF-8 -cp "lib/mssql-jdbc-6.2.2.jre8.jar;lib/*" -d bin src/com/drug/*.java java -cp "bin;lib/mssql-jdbc-6.2.2.jre8.jar;lib/*" com.drug.frame.LoginFrame

第一行是编译源码到 bin 目录,-encoding UTF-8 是为了在 Windows 中文环境下不乱码,Windows 默认 GBK 编码,很多老工程里中文注释编译不过去就是因为没指定编码。第二行是运行,-cp 里必须包含 bin 目录和驱动 jar,缺一个都起不来。如果你导入后发现源码缺失,只有 class 文件,可以直接通过反编译找回逻辑,第四章给出操作命令。

3.4 登录与功能验证

程序启动后,用 admin / 123456 登录。登录成功进入主界面后,先到“基本信息”里添加供应商、客户、药品,再去“业务管理”录一张采购单,最后到“业务查询”里看库存明细和回款记录。我一般会按这个顺序走一遍:新增供应商 -> 新增药品 -> 采购入库 -> 销售出库 -> 销售退货 -> 查库存。六个步骤走完,这套进销存的主链路就算闭环验证了。

4. 从 class 反推业务逻辑:采购入库、销售出库与退货回冲的联动关系

4.1 class 命名规律拆解

拿到一个只有 .class 没有 .java 的源码包,第一步不是慌,而是读命名。JF_view_medicine.class 里的 medicine 是药品,JF_view_gys.class 的 gys 是供应商的拼音首字母,JF_view_client.class 是客户,JF_view_xstk.class 的 xstk 是“销售退款”或者“销售退货”,JF_view_query_kcmx_hk.class 的 kcmx 是“库存明细”,hk 是“回款”。

把这些命名映射到摘要里描述的五个模块:基本信息对应 medicine、client、gys;业务管理对应 buy、sell、xstk;业务查询对应 query_kcmx_rk(入库)和 query_kcmx_hk(回款);用户管理对应 sys_user 界面。由此可见,这套系统的界面类 = 业务实体的视图层,一个实体一个窗口,业务逻辑散落在按钮事件里。

4.2 采购入库到库存增加的联动

最值得看的就是采购入库的逻辑。JF_view_buy 里录一张采购单,界面上选择供应商、药品、填数量,点保存后发生的事情是这样的:

-- 伪代码形式还原采购保存逻辑 INSERT INTO buy_in (buy_no, supplier_id, drug_id, buy_num, buy_price, operator) VALUES ('CG20250101001', 1, 1, 100, 8.00, 'admin'); UPDATE drug_medicine SET stock = stock + 100 WHERE drug_id = 1;

这两条语句是一个事务里的。先落采购单,再更新药品库存。如果只 INSERT 不 UPDATE,库存永远是 0,后面销售会直接变成负库存。老工程师接手这类系统第一件事就是检查这个事务有没有漏,漏了就是“系统里单子很多但库存对不上”的疑难杂症。

4.3 销售出库到库存扣减和客户往来

销售模块和采购正好相反。JF_view_sell 保存时,sell_out 插一条销售记录,drug_medicine 的 stock 减去销售数量,同时 client_account 插一笔客户欠款:

BEGIN TRANSACTION; INSERT INTO sell_out (sell_no, client_id, drug_id, sell_num, sell_price, operator) VALUES ('XS20250101001', 2, 1, 10, 12.50, 'admin'); UPDATE drug_medicine SET stock = stock - 10 WHERE drug_id = 1; INSERT INTO client_account (client_id, bill_type, bill_no, amount) VALUES (2, '销售', 'XS20250101001', 125.00); COMMIT;

销售价与进价的差,就是毛利的来源。如果你在“业务查询”里看到的毛利数据对不上,最可能是回款表的金额用了含税价或者漏减了退货,后面第五章会讲。

4.4 销售退货的回冲逻辑

JF_view_xstk 处理退货时有个细节:退货必须回冲库存,而且回冲的是原销售单,不是简单再插一条负数销售。正确做法是 return_rec 记录原销售单号 source_bill,同时 stock 加回退货数量,client_account 插入一笔负数金额冲掉客户欠款。

INSERT INTO return_rec (return_no, source_bill, drug_id, return_num) VALUES ('TH20250101001', 'XS20250101001', 1, 2); UPDATE drug_medicine SET stock = stock + 2 WHERE drug_id = 1; INSERT INTO client_account (client_id, bill_type, bill_no, amount) VALUES (2, '退货', 'TH20250101001', -25.00);

注意退货回冲的金额要用负数,这样汇总客户余额时不用区分借贷方向,直接 SUM 就是当前欠款。这个设计简单但实用。

4.5 用 CFR 反编译 class 看实际代码

如果 zip 里没有 .java,用 CFR 反编译即可恢复可读的源码:

java -jar cfr-0.152.jar JF_view_buy.class --outputdir src_restore

反编译后的文件在 src_restore 目录里,可以直接用 Eclipse 打开。CFR 对 Switch、Lambda、匿名内部类的还原度比较高,这个 Swing 老项目没有复杂语法,恢复出来的代码基本能读。用它打开 JF_view_buy.class,你能看到采购按钮的 ActionListener 里就是 4.2 节那两段 SQL 的 JDBC 实现。

提示:反编译恢复的是逻辑,不是原始排版和注释。如果打算在反编译代码上做二次开发,建议先跑通再改,不要直接大改结构。

5. 避坑:连接失败、中文乱码与 class 版本不匹配的现场记录

5.1 通过端口 1433 连接 localhost 失败

现象:启动程序报“通过端口 1433 连接到主机 localhost 失败”,或者“Connection refused: connect”。

原因:SQL Server 2014 安装后默认可能没有开启 TCP/IP 协议。SSMS 里能连上,但 JDBC 走的是 TCP 1433 端口,协议没启用自然连不上。

解决:打开“SQL Server 配置管理器”,找到“SQL Server 网络配置”->“MSSQLSERVER 的协议”,把 TCP/IP 启用,然后右键“SQL Server 服务”重启 MSSQLSERVER。这一步是 SQL Server 连接里最常见的坑,80% 的“连不上数据库”都是这个原因。

5.2 sa 登录失败

现象:报错“用户 'sa' 登录失败”,明明密码是对的就进不去。

原因:安装时选了 Windows 身份验证模式,SQL Server 不认 sa 这个账号。

解决:在 SSMS 里用 Windows 身份连接,右键服务器属性 -> 安全性 -> 服务器身份验证改为“SQL Server 和 Windows 身份验证模式”,重启服务。然后到“安全性”->“登录名”里找到 sa,设置密码并启用登录。这个坑你要是踩过一次,下次装 SQL Server 时一定会选混合模式。

5.3 ClassNotFoundException: SQLServerDriver

现象:启动直接抛 java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver。

原因:JDBC 驱动 jar 没有真正进入 classpath。你明明把 jar 放进 lib 目录了,但 Eclipse 不会自动把 lib 下的 jar 加入 Build Path。

解决:右键项目 -> Build Path -> Configure Build Path -> Add JARs,手动把 mssql-jdbc-6.2.2.jre8.jar 加进去。命令行运行的话,-cp 参数里别漏掉 lib 下的 jar。

5.4 UnsupportedClassVersionError

现象:运行时报 java.lang.UnsupportedClassVersionError,后面跟着一串 class file version 数字。

原因:class 文件是高版本 JDK 编译的,当前 JRE 版本太低。比如 class file version 52.0 对应 JDK 8,你用 JDK 7 的 JRE 去跑就会报这个错。

解决:安装 JDK 8,或者用 Eclipse 里更高版本的 JRE 运行项目。反编译修改后重新编译时,可以指定编译版本:

javac -source 1.8 -target 1.8 -encoding UTF-8 -cp "lib/*" -d bin src/com/drug/*.java

5.5 中文乱码或粗体字变成问号

现象:药品名称在 Swing 界面上显示成问号,或者入库单上中文全变乱码。

原因:两个源头。一是 SQL Server 排序规则不认中文,二是 Java 编译/运行编码不一致。老项目在 Windows 上默认 GBK,如果编译时指定了 UTF-8,运行读取数据就会乱。

解决:数据库层面用排序规则 Chinese_PRC_CI_AS,字段用 NVARCHAR;Java 层面编译用 -encoding UTF-8,或者干脆统一用 GBK 编译运行,保证编译编码和源码编码一致即可。别混用,混用必乱。

5.6 说是源码包,打开全是 .class

现象:说好的源码下载下来一解压,src 目录里全是 .class,没有 .java。

原因:开发者交付时只导出了编译产物,或者打包工具默认只带了 bin 目录。这是老资源包里非常常见的情况,不代表资源不能用。

解决:用 CFR 反编译成 .java 放回 src 即可,命令在 4.5 节。如果只是简单改个数据库密码、改个界面文字,甚至可以直接用 JD-GUI 打开 class 查看逻辑,不一定要还原完整工程。从那以后我拿到任何 zip 资源都会先看目录结构,不再默认它一定带源码。

6. 二次开发:库存预警触发器与毛利日报查询

6.1 给药品表加一个库存预警触发器

原系统的库存下限字段 stock_low 已经有了,但预警逻辑可能靠程序轮询,也可能根本没写。最简单的补强方式是在数据库层加一个触发器,让采购入库、销售出库之后自动把低于下限的药品写进一张预警表。

CREATE TABLE stock_warning ( warn_id INT IDENTITY(1,1) PRIMARY KEY, drug_id INT NOT NULL, warn_date DATETIME DEFAULT GETDATE(), current_stock INT, low_limit INT, is_handled CHAR(1) DEFAULT '0' ); GO CREATE TRIGGER trg_stock_warning ON drug_medicine AFTER UPDATE AS BEGIN INSERT INTO stock_warning (drug_id, current_stock, low_limit) SELECT i.drug_id, i.stock, i.stock_low FROM inserted i WHERE i.stock < i.stock_low; END; GO

注意这里不能用游标逐行处理,要用基于集合的写法,update 大批量时性能才稳。AFTER UPDATE 触发器天然覆盖了采购入库(stock 增加)和销售出库(stock 减少)两种场景,比在 Java 代码里加判断更省事,还不容易漏。

6.2 加一张毛利日报视图

业务查询模块里的销售明细,最常被吐槽的是“看不到今天赚了多少”。补一个按天汇总的视图,直接拉当天销售额、成本和毛利。

CREATE VIEW v_sales_daily AS SELECT CONVERT(VARCHAR(10), s.sell_date, 120) AS biz_date, COUNT(DISTINCT s.sell_no) AS bill_count, SUM(s.sell_price * s.sell_num) AS sale_amount, SUM(m.purchase_price * s.sell_num) AS cost_amount, SUM((s.sell_price - m.purchase_price) * s.sell_num) AS gross_profit FROM sell_out s JOIN drug_medicine m ON s.drug_id = m.drug_id GROUP BY CONVERT(VARCHAR(10), s.sell_date, 120); GO

视图建立在 sell_out 和 drug_medicine 的 JOIN 上,销售额用单据价,成本用药品表当前进价。这里有个局限:如果药品进价历史有变动,成本会按当前进价算,不够精确。真要精确就得在 sell_out 里冗余一个 cost_price 字段,录单时把当时进价存进去,这是生产环境的做法。课程设计水平用视图完全够,而且把这段 SQL 直接贴在业务查询的代码里替换原来的明细查询,界面就能多出一个日报按钮。

做完这两个改造,这个老系统在功能上就不只是“能跑”了,而是具备了库存预警和经营日报两个真正会在药房日常管理里用到的能力。我自己的习惯是每拆一个老项目,先加这两个补丁再验收,因为它们能快速验证数据库设计是否合理——触发器要跑得动说明表结构干净,视图能查出数说明业务链路通了。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进