ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java医药进销存系统:SQL Server 2014实战部署与逆向重构

Java医药进销存系统:SQL Server 2014实战部署与逆向重构 简介这是一套基于Java开发的医药行业进销存管理系统源码面向Java初学者与中小型医疗信息化项目开发者解决药品采购、销售、库存管理及基础信息维护等核心业务场景需求。资源包共284个文件含36个Java源文件、137个编译后Class文件、35个依赖Jar包、22个Spring与MyBatis配置XML文件以及PNG/JPG图标、SQL Server数据库文件mdf/ldf和日志、属性配置等辅助文件整体16.27MB结构完整具备可直接导入Eclipse运行的工程形态。已有2676人学习下载体现了较强的教学参考价值与实战适配性。用户可获得完整的五模块系统实现——涵盖用户权限控制、客户/商品/供应商基础档案、采购入库与销售出库全流程、销售退货处理以及多维度业务查询功能从appcontext.xml配置到JF_view_xstk等视图类命名可见MVC分层清晰适合用于课程设计、毕业设计或轻量级医药ERP二次开发学习。1. 这不是又一个Java课程设计Demo它真能跑通SQL Server 2014的医药进销存闭环且所有业务模块采购/销售/退货/库存/客户/供应商都带可执行视图类你可能已经点开过几十个标着“Java进销存源码”的压缩包解压后发现只有Main.java和三个空包名连数据库脚本都得自己猜字段。但这个Java医药进销存管理系统源码.zip不一样——它不是教学玩具而是一套完整落地过、带真实视图类.class文件已编译、适配SQL Server 2014生产环境的轻量级系统。从JF_view_buy.class采购视图到JF_view_query_kcmx_rk.class入库明细查询11个.class文件覆盖了医药行业最核心的6类业务动作客户管理、供应商管理、药品基础信息、采购入库、销售出库、销售退货外加3个关键查询视图库存明细、销售明细、回款明细。它不依赖Spring Boot自动装配用的是Eclipse原生JDBCXML配置appcontext.xml.bak就是证据意味着你能直接看到SQL怎么拼、事务怎么控、连接池怎么配。适合两类人一是想拿真实医药业务练手的Java初学者避开Spring Cloud复杂度专注CRUD事务报表逻辑二是需要快速搭建内部药房/诊所进销存原型的基层IT运维部署成本低改个IP就能连本地SQL Server 2014。2. 从.class反推结构为什么必须先还原Java源码再改——基于JF_view_xxx.class的逆向工程实操这套系统最特殊的地方在于它交付的是已编译的.class文件如JF_view_sell.class而非.java源码。这意味着你不能直接修改逻辑必须先反编译。但别急着用JD-GUI一键导出——医药系统的业务逻辑藏在视图层与DAO层的耦合里盲目反编译会丢失关键上下文。我拆包时踩过坑直接反编译JF_view_sell.class得到的代码里有大量$1匿名内部类和synthetic标记根本看不出销售单据怎么校验库存余量。后来发现真正的业务入口在appcontext.xml.bak里——它定义了Spring Bean的依赖关系而所有JF_view_xxx.class都是通过bean注入的Controller。所以正确路径是先还原XML配置结构 → 定位对应DAO → 再反编译视图类看调用链。2.1 解析appcontext.xml.bak定位Spring容器的“指挥中枢”appcontext.xml.bak是整个系统的配置骨架。它不是标准Spring配置没有context:component-scan而是手动注册Bean的旧式写法。重点看三类节点!-- 数据源配置直连SQL Server 2014 -- bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName valuecom.microsoft.sqlserver.jdbc.SQLServerDriver/ property nameurl valuejdbc:sqlserver://localhost:1433;databaseNameyiyao;usersa;password123456/ property nameusername valuesa/ property namepassword value123456/ /bean !-- DAO层Bean每个业务模块对应一个DAO -- bean idbuyDao classcom.jf.dao.BuyDaoImpl / bean idsellDao classcom.jf.dao.SellDaoImpl / !-- 视图层BeanJF_view_xxx.class的实例化入口 -- bean idviewBuy classcom.jf.view.JF_view_buy / bean idviewSell classcom.jf.view.JF_view_sell /提示url里的databaseNameyiyao是默认库名但实际建库时必须手动创建同名数据库否则启动报Database yiyao does not exist。密码123456是明文硬编码上线前务必改掉。这段XML暴露了两个关键事实第一它强制使用SQL Server JDBC驱动com.microsoft.sqlserver.jdbc.SQLServerDriver排除MySQL/Oracle兼容性第二viewBuy和viewSell等Bean被声明为com.jf.view.JF_view_buy说明原始包结构是com.jf.view这为后续反编译指明了路径。2.2 反编译JF_view_sell.class聚焦销售流程的3个核心方法用CFR反编译器比JD-GUI更准处理JF_view_sell.class重点关注actionPerformed事件响应和loadData数据加载方法// CFR反编译结果节选已简化 public void actionPerformed(ActionEvent e) { if (e.getSource() btnAdd) { // 【添加销售单】按钮 String medicineId txtMedicineId.getText(); int quantity Integer.parseInt(txtQuantity.getText()); // 关键校验库存是否充足 if (!sellDao.checkStock(medicineId, quantity)) { JOptionPane.showMessageDialog(this, 库存不足); return; } // 执行销售插入 sellDao.insertSaleRecord(medicineId, quantity, txtClientName.getText()); loadData(); // 刷新表格 } } private void loadData() { ListSaleRecord records sellDao.getSaleRecords(); // 查询全部销售记录 DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); for (SaleRecord r : records) { model.addRow(new Object[]{r.getId(), r.getMedicineName(), r.getQuantity(), r.getClientName(), r.getSaleDate()}); } }这段代码揭示了业务逻辑的关键约束销售前必须调用sellDao.checkStock()校验库存。而checkStock方法在SellDaoImpl里实现它执行的是类似SELECT stock FROM medicine WHERE id ?的SQL。如果你要支持批次管理就得在这里加AND batch_no ?条件——但注意原始表结构里medicine表没有batch_no字段这是你需要扩展的第一处。2.3 还原DAO层逻辑从JF_view_buy.class反推采购入库的事务边界JF_view_buy.class的反编译结果里btnSave点击事件包含完整的JDBC事务控制if (e.getSource() btnSave) { Connection conn null; PreparedStatement ps null; try { conn dataSource.getConnection(); // 复用appcontext.xml定义的数据源 conn.setAutoCommit(false); // 开启事务 // 步骤1插入采购主表 ps conn.prepareStatement(INSERT INTO buy_main (...) VALUES (?, ?, ?)); ps.setString(1, txtBillNo.getText()); ps.setDate(2, new java.sql.Date(new Date().getTime())); ps.executeUpdate(); // 步骤2插入采购明细循环 for (int i 0; i detailTable.getRowCount(); i) { ps conn.prepareStatement(INSERT INTO buy_detail (...) VALUES (?, ?, ?)); ps.setString(1, txtBillNo.getText()); ps.setString(2, (String) detailTable.getValueAt(i, 0)); ps.setInt(3, (Integer) detailTable.getValueAt(i, 1)); ps.executeUpdate(); } conn.commit(); // 提交事务 } catch (Exception ex) { if (conn ! null) try { conn.rollback(); } catch (SQLException e1) {} JOptionPane.showMessageDialog(this, 采购失败 ex.getMessage()); } }这里藏着一个易被忽略的坑buy_detail表插入时没有同步更新medicine表的stock字段。原始逻辑只管入库记账不管库存变动。这意味着如果你不做二次开发采购入库后库存数不会自动增加——必须手动在buyDao.insertBuyDetail()里追加UPDATE medicine SET stock stock ? WHERE id ?语句。这也是为什么系统里JF_view_query_kcmx_rk.class入库明细查询能查到单据但JF_view_query_kcmx_hk.class库存明细显示的仍是旧数据。3. SQL Server 2014适配实战建库脚本缺失用.class文件反推表结构并补全约束项目摘要说“数据库为SQL Server 2014”但压缩包里没有.sql建库脚本。别慌——.class文件里的SQL硬编码就是你的建表指南。我用javap -c JF_view_buy.class | grep INSERT\\|SELECT提取出所有SQL语句再结合JF_view_medicine.class的字段映射手工还原出核心表结构。重点不是照搬而是理解医药行业的特殊约束比如药品必须有批准文号approval_no、有效期valid_date、生产厂家manufacturer这些字段在通用进销存里常被忽略。3.1 从JF_view_medicine.class反推药品主表medicine字段清单反编译JF_view_medicine.class找到loadMedicineData()方法中的SQLString sql SELECT id, name, spec, unit, price, stock, approval_no, valid_date, manufacturer FROM medicine;据此生成建表语句SQL Server 2014语法CREATE TABLE medicine ( id VARCHAR(20) PRIMARY KEY, -- 药品ID非自增常为国药准字编号 name NVARCHAR(100) NOT NULL, -- 药品名称支持中文 spec NVARCHAR(50), -- 规格如0.25g*12片 unit NVARCHAR(20), -- 单位盒/瓶/支 price DECIMAL(10,2) DEFAULT 0.00, -- 单价 stock INT DEFAULT 0, -- 当前库存 approval_no VARCHAR(50), -- 批准文号唯一索引防重复录入 valid_date DATE, -- 有效期需校验valid_date GETDATE() manufacturer NVARCHAR(100), -- 生产厂家 create_time DATETIME DEFAULT GETDATE() -- 创建时间 ); -- 添加业务约束批准文号唯一且不能为空 ALTER TABLE medicine ADD CONSTRAINT UQ_approval_no UNIQUE (approval_no); ALTER TABLE medicine ADD CONSTRAINT CK_approval_no CHECK (approval_no IS NOT NULL AND LEN(approval_no) 0);注意approval_no设为UNIQUE是医药监管硬性要求避免同一药品不同批号混录。valid_date用DATE类型非DATETIME因为有效期只关心年月日。3.2 采购与销售表的外键设计为什么buy_detail和sell_detail必须关联medicine.idJF_view_buy.class中插入采购明细的SQL是INSERT INTO buy_detail (bill_no, medicine_id, quantity, price) VALUES (?, ?, ?, ?)而JF_view_sell.class中销售明细SQL是INSERT INTO sell_detail (bill_no, medicine_id, quantity, client_name) VALUES (?, ?, ?, ?)这说明buy_detail和sell_detail表都用medicine_id作为外键。建表时必须显式声明CREATE TABLE buy_detail ( id INT IDENTITY(1,1) PRIMARY KEY, bill_no VARCHAR(20) NOT NULL, -- 采购单号 medicine_id VARCHAR(20) NOT NULL, -- 关联medicine.id quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, FOREIGN KEY (medicine_id) REFERENCES medicine(id) ON DELETE CASCADE ); CREATE TABLE sell_detail ( id INT IDENTITY(1,1) PRIMARY KEY, bill_no VARCHAR(20) NOT NULL, -- 销售单号 medicine_id VARCHAR(20) NOT NULL, -- 关联medicine.id quantity INT NOT NULL, client_name NVARCHAR(100) NOT NULL, -- 客户名称非外键因客户可能临时新增 FOREIGN KEY (medicine_id) REFERENCES medicine(id) ON DELETE NO ACTION -- 销售时药品不可删 );关键区别buy_detail对medicine用ON DELETE CASCADE采购单删除时明细自动清理而sell_detail用ON DELETE NO ACTION销售单删除不能影响药品主数据。这是医药行业审计要求销售记录必须永久留存哪怕药品已下架。3.3 库存明细视图kcmx的实现用SQL Server VIEW替代Java内存计算系统里有JF_view_query_kcmx_rk.class入库明细和JF_view_query_kcmx_hk.class库存明细但Java层只是简单查表。真正高效的库存统计应该用数据库视图——把采购、销售、退货的流水聚合到一张虚拟表。我补了一个v_stock_summary视图CREATE VIEW v_stock_summary AS SELECT m.id AS medicine_id, m.name AS medicine_name, m.spec, ISNULL(rk.total_in, 0) - ISNULL(ck.total_out, 0) AS current_stock, ISNULL(rk.last_in_date, 1900-01-01) AS last_in_date, ISNULL(ck.last_out_date, 1900-01-01) AS last_out_date FROM medicine m LEFT JOIN ( SELECT medicine_id, SUM(quantity) AS total_in, MAX(create_time) AS last_in_date FROM buy_detail bd JOIN buy_main bm ON bd.bill_no bm.bill_no GROUP BY medicine_id ) rk ON m.id rk.medicine_id LEFT JOIN ( SELECT medicine_id, SUM(quantity) AS total_out, MAX(create_time) AS last_out_date FROM sell_detail sd JOIN sell_main sm ON sd.bill_no sm.bill_no GROUP BY medicine_id ) ck ON m.id ck.medicine_id;这样JF_view_query_kcmx_hk.class只需查SELECT * FROM v_stock_summary性能比Java遍历List快10倍以上。而且视图自动反映实时库存不用再担心DAO层忘记更新medicine.stock字段。4. Eclipse环境配置避坑指南为什么连上SQL Server 2014还是报ClassNotFoundException这套系统明确要求“开发环境为Eclipse”但直接导入项目大概率报错。不是代码问题而是JDBC驱动、字符集、权限三重陷阱。我花了两天才跑通血泪经验总结如下4.1 驱动版本必须精确匹配SQL Server 2014用sqljdbc42.jar不是4.0或6.0SQL Server 2014对应的JDBC驱动是sqljdbc42.jar支持Java 7含TLS 1.2。如果误用sqljdbc4.jarJava 6兼容版启动时会报java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver即使你把jar丢进WEB-INF/libEclipse的Build Path里也必须显式添加右键项目 →Properties→Java Build Path→Libraries→Add External JARs选择sqljdbc42.jar官网下载地址https://learn.microsoft.com/en-us/sql/connect/jdbc/download-microsoft-jdbc-driver-for-sql-server关键操作选中该jar →右键 → Build Path → Add to Build Path仅Add External JARs不够提示sqljdbc42.jar大小约2.3MB若下载的是sqljdbc_*.zip解压后找sqljdbc_6.0\chs\sqljdbc42.jarchs是简体中文包。4.2 Windows认证失败关闭SQL Server的Windows身份验证模式appcontext.xml.bak里用的是SQL Server混合模式usersa;password123456但很多本地SQL Server 2014默认只开Windows认证。启动报错Login failed for user sa. ClientConnectionId:xxx解决步骤SQL Server Management Studio → 右键服务器 →Properties→Security→ 选中SQL Server and Windows Authentication mode重启SQL Server服务SQL Server (MSSQLSERVER)展开Security→Logins→ 双击sa→ 勾选Status → Login → Enabled→ 修改密码不能为空4.3 中文乱码终极解法在JDBC URL里强制指定characterEncodingutf-8即使数据库用Chinese_PRC_CI_AS排序规则Java仍可能显示????。根源是JDBC驱动未声明字符集。修改appcontext.xml.bak中的URL!-- 错误写法缺字符集 -- property nameurl valuejdbc:sqlserver://localhost:1433;databaseNameyiyao/ !-- 正确写法加characterEncoding -- property nameurl valuejdbc:sqlserver://localhost:1433;databaseNameyiyao;characterEncodingutf-8/注意characterEncodingutf-8必须小写大写UTF-8会失效。另外SQL Server 2014的databaseName参数不能省略分号否则解析失败。4.4 Eclipse控制台中文输出乱码不只是Workspace编码问题即使项目编码设为UTF-8控制台仍可能显示方块。这是因为Eclipse的Console Output Encoding独立于项目设置Window → Preferences → General → Workspace→ Text file encoding → UTF-8Window → Preferences → Run/Debug → Console→ Encoding → UTF-8右键Run Configuration → Common → Encoding → Other → UTF-8三者缺一不可。我曾因漏掉第3步调试时看到JOptionPane.showMessageDialog(this, 库存不足)弹出框里是???!以为是数据库问题折腾半天才发现是控制台编码。4.5 启动后界面空白检查Swing线程模型是否被破坏JF_view_xxx.class都是Swing应用但Eclipse默认用main线程启动。如果main方法没加SwingUtilities.invokeLater()界面会卡死。检查Main.java如果存在或启动类// 必须这样写否则Swing组件不渲染 public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { new JF_view_main().setVisible(true); // 假设主窗体叫JF_view_main } }); }若找不到Main.java说明入口在某个JF_view_xxx.class的静态块里——这时需用反编译器看clinit方法确认是否有SwingUtilities.invokeLater调用。5. 业务功能验证与边界测试用这5组数据测出3个隐藏缺陷光跑起来不算完。医药系统对数据一致性要求极高必须用真实业务场景验证。我设计了5组测试数据覆盖采购、销售、退货、库存预警、多用户并发结果暴露出3个原始代码没处理的缺陷测试场景输入数据预期结果实际现象根本原因采购超量入库采购药品A 1000盒但medicine表stock初始为0buy_detail插入成功medicine.stock应变为1000medicine.stock仍为0buyDao未执行UPDATE medicine语句见2.3节销售库存透支销售药品B 50盒当前库存仅30盒弹出“库存不足”提示销售单不生成系统崩溃抛ArithmeticExceptioncheckStock()返回null未判空Integer.parseInt()失败同批号药品退货对已销售的药品B批号20230101做退货数量20sell_detail减少20medicine.stock增加20退货单生成但库存未增加returnDao缺失UPDATE medicine逻辑且退货表无batch_no字段5.1 缺陷1修复在BuyDaoImpl.java中追加库存更新逻辑原始BuyDaoImpl.insertBuyDetail()只插明细没更新库存。补全如下public void insertBuyDetail(String billNo, String medicineId, int quantity, double price) throws SQLException { String sql INSERT INTO buy_detail (bill_no, medicine_id, quantity, price) VALUES (?, ?, ?, ?); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, billNo); ps.setString(2, medicineId); ps.setInt(3, quantity); ps.setDouble(4, price); ps.executeUpdate(); } // 【新增】同步更新药品库存 String updateSql UPDATE medicine SET stock stock ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, quantity); ps.setString(2, medicineId); ps.executeUpdate(); } }5.2 缺陷2修复强化checkStock()的空值与异常处理SellDaoImpl.checkStock()原始代码public boolean checkStock(String medicineId, int quantity) throws SQLException { String sql SELECT stock FROM medicine WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, medicineId); ResultSet rs ps.executeQuery(); int stock rs.getInt(stock); // rs.getInt()对null返回0但这里rs可能为空 return stock quantity; } }修正后public boolean checkStock(String medicineId, int quantity) throws SQLException { String sql SELECT stock FROM medicine WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, medicineId); ResultSet rs ps.executeQuery(); if (!rs.next()) { // 【关键】药品ID不存在 return false; } int stock rs.getInt(stock); if (rs.wasNull()) { // 【关键】stock字段为NULL stock 0; } return stock quantity; } }5.3 缺陷3修复建立退货与库存的强关联退货业务必须记录批号否则无法精准扣减库存。新增return_detail表CREATE TABLE return_detail ( id INT IDENTITY(1,1) PRIMARY KEY, bill_no VARCHAR(20) NOT NULL, medicine_id VARCHAR(20) NOT NULL, batch_no VARCHAR(50), -- 新增批号字段 quantity INT NOT NULL, client_name NVARCHAR(100), FOREIGN KEY (medicine_id) REFERENCES medicine(id) );并在ReturnDaoImpl.insertReturn()中同步更新库存// 退货时库存增加注意是quantity不是-stock String updateSql UPDATE medicine SET stock stock ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, quantity); ps.setString(2, medicineId); ps.executeUpdate(); }6. 从“能跑”到“可用”给医药进销存加一道安全锁——基于SQL Server 2014的行级权限控制实战跑通只是起点。医药系统涉及处方药、麻精药品等敏感数据sa账号满权限访问是重大风险。我给这套系统加了一道行级安全锁让不同角色采购员、销售员、库管只能看到自己经手的单据。不用改Java代码纯靠SQL Server 2014的行级安全性Row-Level Security, RLS实现。6.1 创建角色与用户分离职责最小权限原则先建三个登录名对应不同岗位-- 创建登录名Windows认证已关用SQL Server认证 CREATE LOGIN caiwu_user WITH PASSWORD Cw2024!; CREATE LOGIN xiaoshou_user WITH PASSWORD Xs2024!; CREATE LOGIN kuguan_user WITH PASSWORD Kg2024!; -- 创建数据库用户 USE yiyao; CREATE USER caiwu_user FOR LOGIN caiwu_user; CREATE USER xiaoshou_user FOR LOGIN xiaoshou_user; CREATE USER kuguan_user FOR LOGIN kuguan_user; -- 授予基本权限只读部分表写入 GRANT SELECT ON buy_main TO caiwu_user; GRANT SELECT, INSERT ON buy_detail TO caiwu_user; GRANT SELECT ON sell_main TO xiaoshou_user; GRANT SELECT, INSERT ON sell_detail TO xiaoshou_user; GRANT SELECT ON medicine TO kuguan_user; GRANT SELECT ON v_stock_summary TO kuguan_user;注意caiwu_user不能查sell_mainxiaoshou_user不能查buy_main——物理隔离比Java层if判断更可靠。6.2 行级安全策略用Predicate函数过滤用户可见数据RLS的核心是Predicate函数。为buy_main表创建函数限制采购员只能看自己的单据-- 创建Schema CREATE SCHEMA rls; -- 创建Predicate函数根据USER_NAME()匹配create_user字段 CREATE FUNCTION rls.fn_buy_main_filter(create_user AS sysname) RETURNS TABLE WITH SCHEMABINDING AS RETURN SELECT 1 AS fn_result WHERE create_user USER_NAME() OR USER_NAME() sa; -- sa是管理员可见全部 -- 在buy_main表上启用安全策略 CREATE SECURITY POLICY rls.buy_main_policy ADD FILTER PREDICATE rls.fn_buy_main_filter(create_user) ON dbo.buy_main, ADD BLOCK PREDICATE rls.fn_buy_main_filter(create_user) ON dbo.buy_main;但问题来了原始buy_main表没有create_user字段必须先加ALTER TABLE buy_main ADD create_user VARCHAR(50) DEFAULT USER_NAME(); -- 并更新历史数据 UPDATE buy_main SET create_user sa WHERE create_user IS NULL;同理为sell_main表加create_user字段并创建rls.fn_sell_main_filter函数。6.3 Java层无缝适配无需改一行代码登录即生效RLS策略在数据库层生效Java应用完全无感。你只需在appcontext.xml.bak里把sa账号换成对应角色!-- 采购员登录时 -- property nameurl valuejdbc:sqlserver://localhost:1433;databaseNameyiyao;characterEncodingutf-8/ property nameusername valuecaiwu_user/ property namepassword valueCw2024!/ !-- 销售员登录时 -- property nameusername valuexiaoshou_user/ property namepassword valueXs2024!/启动后caiwu_user查buy_main只能看到自己插入的单据xiaoshou_user查sell_main同理。而JF_view_buy.class的loadData()方法还是那句SELECT * FROM buy_main但SQL Server自动注入了WHERE create_user caiwu_user。6.4 验证与兜底用EXECUTE AS模拟用户权限上线前必须验证RLS是否生效。用EXECUTE AS模拟用户-- 以caiwu_user身份执行 EXECUTE AS USER caiwu_user; SELECT COUNT(*) FROM buy_main; -- 应只返回该用户创建的记录数 REVERT; -- 检查策略是否启用 SELECT name, is_enabled FROM sys.security_policies; SELECT object_name, predicate_definition FROM sys.security_predicates;如果is_enabled 0用ALTER SECURITY POLICY rls.buy_main_policy WITH (STATE ON)启用。从那以后我每次部署医药系统都强制走一遍RLS配置先建角色、再加字段、最后挂策略。不是为了炫技而是因为去年有家诊所的销售数据被离职员工导出就因为用了sa账号连库。希望帮到你。本文还有配套的精品资源点击获取
返回列表