资讯动态

JavaWeb药品存销管理系统实战:三层架构+MySQL+连接池完整方案

发布时间:2026/9/28 12:56:39 来源:尧图企业网站定制
简介这是一套面向高校软件工程课程大作业的完整项目资料基于JavaWeb技术栈实现药品存销信息管理系统适合正在准备课程设计、毕业设计或需要JavaWeb实战练手的学生与开发者参考。压缩包共623个文件约46.67MB其中108个java源文件与202个class文件构成核心业务代码69个jar包提供框架与数据库驱动依赖65个xml与7个properties文件负责配置36个jsp、30个js与22个css搭建前端页面另有sql脚本、xls/xlsx数据表及日志文件辅助部署与调试。资源涵盖药品入库、销售、库存查询与后台管理等典型模块目录结构清晰可直接导入IDE运行便于理解分层设计与数据库交互流程。目前已有99人学习下载适合作为课程答辩参考或二次开发基础。1. 药品存销信息管理系统从课程大作业到能跑起来的 JavaWeb 项目很多计算机专业的学生到了大三下学期都会遇到一个绕不开的坎——软件工程大作业。题目发下来翻来覆去就是那么几个图书管理、学生选课、超市收银再就是药品存销信息管理系统。别急着嫌它土药品这个场景其实比图书管理复杂得多它天然带着批号、效期、库存预警、出入库流水这些真实业务约束做好了完全能写进简历。我见过太多人拿到题目第一反应是去搜“javaweb项目完整案例mysql”找到一个压缩包下载下来改个名字就交上去结果答辩时老师问一句“你的库存扣减是在哪一层做的”就当场卡壳。这篇东西就是写给那些不想只交个壳子、而是真想把这个系统跑通、讲明白的人。整套方案基于 JavaWeb 经典三层架构前端 JSP 后端 Servlet数据库用 MySQL开发工具 IDEA服务器 Tomcat。读完你能拿到一条从建库建表、写 DAO、配连接池到部署调试的完整路径也能看清哪些地方最容易翻车。适合正在做大作业的在校生也适合想拿一个完整案例练手 JavaWeb 的转行者。2. 先把架构和数据模型定下来别一上来就写 Servlet2.1 为什么药品存销系统必须分三层很多人做 JavaWeb 大作业的习惯是打开 IDEA新建一个 Servlet然后在 doGet 里从request.getParameter一路写到connection.prepareStatement最后把结果塞进request.setAttribute转发到 JSP。一个功能这么写没问题但药品存销系统至少有药品管理、供应商管理、入库、出库、库存查询、效期预警六七个模块如果全塞在 Servlet 里改一个字段名要翻十几个文件这就是典型的“能跑但没法维护”。三层架构不是老师为了凑字数编出来的概念它解决的是一个非常具体的问题职责分离。表现层Servlet JSP只负责接收请求和展示数据业务逻辑层Service负责校验和流程编排数据访问层DAO只负责和数据库打交道。药品入库这个动作表现层只管拿到药品 ID 和数量Service 层要判断这个药品是否存在、数量是否为正、入库后是否触发库存上限DAO 层只管执行 INSERT 和 UPDATE。这样拆开之后你答辩时被问到“库存扣减在哪一层”答案很明确在 Service 层因为出库时先查库存够不够、再扣减、再写流水这三步必须在一个事务里。提示大作业不要求你用 Spring 全家桶纯 Servlet JDBC 反而更能体现你对底层流程的理解。如果时间充裕可以用 Maven 管理依赖但不用也能跑。2.2 数据库表设计六张表撑起整个系统数据库设计是整个项目的地基地基歪了后面全是坑。药品存销系统的核心表不多但每张表的字段都要想清楚。下面是我一般会用的六张表结构直接给建表语句你可以照着在 MySQL 里执行。-- 药品信息表 CREATE TABLE drug ( drug_id INT PRIMARY KEY AUTO_INCREMENT, drug_name VARCHAR(100) NOT NULL, drug_code VARCHAR(50) UNIQUE NOT NULL, spec VARCHAR(50), unit VARCHAR(20), manufacturer VARCHAR(100), purchase_price DECIMAL(10,2), sale_price DECIMAL(10,2), stock_quantity INT DEFAULT 0, stock_warn INT DEFAULT 10, expire_date DATE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 供应商表 CREATE TABLE supplier ( supplier_id INT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(100) NOT NULL, contact_person VARCHAR(50), phone VARCHAR(20), address VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 入库记录表 CREATE TABLE stock_in ( in_id INT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, supplier_id INT, quantity INT NOT NULL, in_price DECIMAL(10,2), batch_no VARCHAR(50), operator VARCHAR(50), in_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (drug_id) REFERENCES drug(drug_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 出库记录表 CREATE TABLE stock_out ( out_id INT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, quantity INT NOT NULL, out_price DECIMAL(10,2), customer VARCHAR(100), operator VARCHAR(50), out_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (drug_id) REFERENCES drug(drug_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表 CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role VARCHAR(20) DEFAULT staff ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 库存预警日志表 CREATE TABLE stock_warn_log ( log_id INT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, warn_type VARCHAR(20), warn_msg VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这几张表的关系很清晰drug是主表stock_in和stock_out通过drug_id关联每次入库或出库都往对应表里插一条流水同时更新drug表的stock_quantity字段。stock_warn_log用来记录预警触发情况答辩时演示库存预警功能会用到。参数说明几个关键字段stock_warn是库存预警阈值默认 10意思是库存低于 10 就触发预警expire_date是效期查询时可以加一个条件筛选出 90 天内过期的药品batch_no是批号药品行业对批号追溯有硬性要求加上这个字段会让你的系统看起来更专业。字符集统一用utf8mb4不然药品名称里如果有生僻字会乱码这是血泪经验。2.3 项目目录结构IDEA 里怎么摆在 IDEA 里新建一个 Java Enterprise 项目勾选 Web Application目录结构按下面这样摆。这个结构不是唯一解但它是被无数 JavaWeb 项目验证过的、最不容易出问题的摆法。DrugMS/ ├── src/ │ └── com/drugms/ │ ├── dao/ -- 数据访问层 │ ├── service/ -- 业务逻辑层 │ ├── servlet/ -- 控制层 │ ├── entity/ -- 实体类 │ └── util/ -- 工具类DBUtil 等 ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ -- 放 mysql-connector-java.jar │ ├── css/ │ ├── js/ │ └── *.jsp └── pom.xml如果用 Mavenentity包里的类跟数据库表一一对应比如Drug.java里的字段就是drug表的列。util包里放DBUtil负责获取数据库连接。WEB-INF/lib目录下必须放 MySQL 驱动 jar 包否则 Tomcat 启动时会报ClassNotFoundException。如果用 Maven在pom.xml里加mysql-connector-java依赖就行版本选 8.0 以上5.x 的驱动类名和 URL 格式跟 8.x 不一样混用会翻车。3. 从 DBUtil 到 DAO把数据库连接池和增删改查写扎实3.1 数据库连接池为什么不能每次 new Connection新手最容易犯的错误是在每个 DAO 方法里写DriverManager.getConnection()用完close()。单次操作看不出问题但药品存销系统里一个入库操作可能要执行三四条 SQL每次新建连接的开销累积起来页面响应会明显变慢。更严重的是如果某次close()没执行到比如中间抛异常了连接就泄漏了跑一段时间数据库连接数打满整个系统卡死。常见做法是用 Druid 或者 C3P0 连接池。Druid 是阿里开源的配置简单监控功能强我一般首选它。在src下新建druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/drug_ms?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai usernameroot passwordyour_password initialSize5 maxActive20 maxWait3000然后写DBUtil工具类package com.drugms.util; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static DataSource dataSource; static { try { Properties props new Properties(); // 从类路径加载配置文件 InputStream is DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties); props.load(is); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new RuntimeException(数据库连接池初始化失败, e); } } // 获取连接 public static Connection getConnection() throws Exception { return dataSource.getConnection(); } // 释放资源注意关闭顺序ResultSet - Statement - Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (Exception e) {} try { if (stmt ! null) stmt.close(); } catch (Exception e) {} try { if (conn ! null) conn.close(); } catch (Exception e) {} } }这段代码的关键点static代码块保证连接池只初始化一次getConnection()从池里拿连接而不是新建close()方法把连接归还给池而不是真正关闭。参数方面initialSize5是初始连接数maxActive20是最大连接数maxWait3000是获取连接的超时时间毫秒。大作业场景下这些值够用了如果并发测试时发现拿不到连接把maxActive调大就行。注意url里的serverTimezoneAsia/Shanghai不能省MySQL 8.x 驱动如果不指定时区插入DATETIME字段时会报时区错误这是最常见的翻车点之一。3.2 DrugDAO一个完整的增删改查长什么样DAO 层的写法有固定套路定义接口写实现类每个方法对应一条 SQL。下面以DrugDAO为例把增删改查四个操作写全你照着改改就能用到其他表上。package com.drugms.dao; import com.drugms.entity.Drug; import com.drugms.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class DrugDAO { // 新增药品 public int insert(Drug drug) throws Exception { String sql INSERT INTO drug(drug_name, drug_code, spec, unit, manufacturer, purchase_price, sale_price, stock_quantity, stock_warn, expire_date) VALUES(?,?,?,?,?,?,?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, drug.getDrugName()); ps.setString(2, drug.getDrugCode()); ps.setString(3, drug.getSpec()); ps.setString(4, drug.getUnit()); ps.setString(5, drug.getManufacturer()); ps.setBigDecimal(6, drug.getPurchasePrice()); ps.setBigDecimal(7, drug.getSalePrice()); ps.setInt(8, drug.getStockQuantity()); ps.setInt(9, drug.getStockWarn()); ps.setDate(10, drug.getExpireDate()); return ps.executeUpdate(); } } // 根据 ID 删除 public int deleteById(int drugId) throws Exception { String sql DELETE FROM drug WHERE drug_id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, drugId); return ps.executeUpdate(); } } // 更新药品信息 public int update(Drug drug) throws Exception { String sql UPDATE drug SET drug_name?, spec?, unit?, purchase_price?, sale_price?, stock_warn?, expire_date? WHERE drug_id?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, drug.getDrugName()); ps.setString(2, drug.getSpec()); ps.setString(3, drug.getUnit()); ps.setBigDecimal(4, drug.getPurchasePrice()); ps.setBigDecimal(5, drug.getSalePrice()); ps.setInt(6, drug.getStockWarn()); ps.setDate(7, drug.getExpireDate()); ps.setInt(8, drug.getDrugId()); return ps.executeUpdate(); } } // 查询全部 public ListDrug findAll() throws Exception { String sql SELECT * FROM drug ORDER BY drug_id DESC; ListDrug list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } return list; } // 按名称模糊查询 public ListDrug findByName(String keyword) throws Exception { String sql SELECT * FROM drug WHERE drug_name LIKE ?; ListDrug list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } return list; } // 结果集映射为实体避免重复代码 private Drug mapRow(ResultSet rs) throws SQLException { Drug d new Drug(); d.setDrugId(rs.getInt(drug_id)); d.setDrugName(rs.getString(drug_name)); d.setDrugCode(rs.getString(drug_code)); d.setSpec(rs.getString(spec)); d.setUnit(rs.getString(unit)); d.setManufacturer(rs.getString(manufacturer)); d.setPurchasePrice(rs.getBigDecimal(purchase_price)); d.setSalePrice(rs.getBigDecimal(sale_price)); d.setStockQuantity(rs.getInt(stock_quantity)); d.setStockWarn(rs.getInt(stock_warn)); d.setExpireDate(rs.getDate(expire_date)); return d; } }逻辑说明每个方法都用try-with-resources自动关闭Connection和PreparedStatement比手动finally里 close 更安全。mapRow方法把ResultSet到Drug对象的映射抽出来findAll和findByName共用减少重复代码。参数方面PreparedStatement的?占位符从 1 开始编号setString、setInt、setBigDecimal、setDate分别对应不同字段类型别搞混。findByName里的LIKE查询用%keyword%实现模糊匹配如果要做精确匹配就去掉两边的%。3.3 Service 层入库出库的事务怎么写入库和出库是药品存销系统的核心业务必须保证原子性。以入库为例往stock_in表插一条记录同时更新drug表的stock_quantity。这两步要么都成功要么都回滚。package com.drugms.service; import com.drugms.dao.DrugDAO; import com.drugms.dao.StockInDAO; import com.drugms.util.DBUtil; import java.sql.Connection; public class StockService { private DrugDAO drugDAO new DrugDAO(); private StockInDAO stockInDAO new StockInDAO(); // 入库操作插入流水 更新库存同一事务 public boolean stockIn(int drugId, int quantity, String batchNo, int supplierId, String operator) throws Exception { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步插入入库记录 String inSql INSERT INTO stock_in(drug_id, supplier_id, quantity, batch_no, operator) VALUES(?,?,?,?,?); try (var ps conn.prepareStatement(inSql)) { ps.setInt(1, drugId); ps.setInt(2, supplierId); ps.setInt(3, quantity); ps.setString(4, batchNo); ps.setString(5, operator); ps.executeUpdate(); } // 第二步更新药品库存 String updateSql UPDATE drug SET stock_quantity stock_quantity ? WHERE drug_id ?; try (var ps conn.prepareStatement(updateSql)) { ps.setInt(1, quantity); ps.setInt(2, drugId); ps.executeUpdate(); } conn.commit(); // 两步都成功才提交 return true; } catch (Exception e) { if (conn ! null) conn.rollback(); // 任一步失败就回滚 throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } } }这段代码的核心是conn.setAutoCommit(false)和conn.commit()/conn.rollback()的配对使用。注意conn是从连接池拿的用完之后close()是归还给池不是真正关闭所以setAutoCommit(true)这步必须做否则下次从池里拿到这个连接时还是手动提交模式会出玄学问题。出库逻辑类似只是把stock_quantity ?改成stock_quantity - ?并且在更新前要先查一下当前库存够不够不够就抛异常回滚。4. 避坑与排查那些让大作业当场翻车的细节4.1 中文乱码从 JSP 到数据库一条链都要管现象页面上输入“阿莫西林”存进数据库变成“阿莫西林”或者查询时明明有数据却查不出来。原因乱码可能出现在三个环节——JSP 页面编码、Servlet 请求编码、数据库连接编码。任何一个环节没设对都会出问题。解决JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %Servlet 里在doGet/doPost第一行加request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)数据库连接 URL 里加useUnicodetruecharacterEncodingutf8mb4建表时指定DEFAULT CHARSETutf8mb4。四个地方都设对乱码问题基本绝迹。如果用的是 Tomcat 8 以上GET 请求的编码默认就是 UTF-8但 POST 请求必须手动设。4.2 连接池报错“Too many connections”现象系统跑一段时间后所有数据库操作都超时控制台报CommunicationsException或者Too many connections。原因DAO 方法里拿了连接但没关闭或者close()写在try块里而不是finally块里一旦中间抛异常就跳过了关闭逻辑。连接池的maxActive设成 20泄漏 20 次之后池就空了。解决统一用try-with-resources写法Connection、PreparedStatement、ResultSet都放在 try 的括号里Java 会自动关闭。如果必须手动关确保close()在finally块里。另外可以在 Druid 配置里加removeAbandonedtrue和removeAbandonedTimeout180让连接池自动回收长时间不用的连接但这只是补救根本还是要写对关闭逻辑。4.3 事务没生效自动提交没关掉现象入库时流水插进去了但库存没更新或者反过来数据对不上。原因conn.setAutoCommit(false)没写或者写了但conn不是同一个。比如在 Service 里开了事务但 DAO 方法内部又自己DBUtil.getConnection()拿了一个新连接两个连接互不相干事务自然管不到。解决事务涉及的所有 SQL 必须用同一个Connection对象。我一般会把Connection作为参数传给 DAO 方法而不是让 DAO 自己去拿。这样 Service 层控制事务边界DAO 层只管执行 SQL。如果觉得传参麻烦可以用 ThreadLocal 把连接绑定到当前线程但大作业场景下传参更直观。4.4 Tomcat 启动报 404web.xml 和注解二选一现象项目部署到 Tomcat 后访问http://localhost:8080/DrugMS/drugList报 404但 Tomcat 启动日志没有报错。原因Servlet 的 URL 映射没配。要么在web.xml里配servlet和servlet-mapping要么在 Servlet 类上加WebServlet(/drugList)注解。两个都配了会冲突两个都没配就 404。解决推荐用注解简洁不容易漏。在 Servlet 类名上面加WebServlet(/drugList)注意斜杠不能少。如果web.xml的metadata-completetrue注解会被忽略改成false或者删掉这个属性。另外检查 IDEA 的 Artifacts 配置确保WEB-INF/lib下的 jar 包被正确打包到输出目录否则运行时会报ClassNotFoundException。4.5 日期类型转换异常现象提交表单时填了“2025-12-31”后台报IllegalArgumentException或者日期变成 null。原因表单提交的日期是字符串request.getParameter()拿到的是String直接往java.sql.Date里塞会报错。或者用了SimpleDateFormat但格式串写成了yyyy/MM/dd跟输入格式不匹配。解决写一个日期转换工具方法用SimpleDateFormat(yyyy-MM-dd)解析解析失败时返回 null 而不是抛异常。实体类里的日期字段用java.sql.Date跟数据库DATE类型对应。如果数据库字段是DATETIME实体类用java.sql.Timestamp。JSP 页面上的日期输入框用input typedate浏览器会自带日期选择器提交格式就是yyyy-MM-dd省去手动格式化的麻烦。5. 库存预警与效期查询两个让答辩加分的小功能5.1 库存预警定时任务还是实时判断库存预警有两种实现思路。一种是定时任务每隔一段时间扫一遍drug表把stock_quantity stock_warn的记录写进stock_warn_log。另一种是实时判断每次入库出库后检查当前库存是否低于阈值是就写日志。大作业场景下我推荐实时判断因为不需要额外配 Quartz 或者 Timer代码量少演示效果也直观。// 在 StockService 的 stockIn 和 stockOut 方法末尾调用 private void checkWarn(Connection conn, int drugId) throws Exception { String sql SELECT stock_quantity, stock_warn, drug_name FROM drug WHERE drug_id ?; try (var ps conn.prepareStatement(sql)) { ps.setInt(1, drugId); try (var rs ps.executeQuery()) { if (rs.next()) { int stock rs.getInt(stock_quantity); int warn rs.getInt(stock_warn); String name rs.getString(drug_name); if (stock warn) { String logSql INSERT INTO stock_warn_log(drug_id, warn_type, warn_msg) VALUES(?,?,?); try (var logPs conn.prepareStatement(logSql)) { logPs.setInt(1, drugId); logPs.setString(2, 库存不足); logPs.setString(3, name 当前库存 stock 低于预警值 warn); logPs.executeUpdate(); } } } } } }这个方法放在同一个事务里入库出库成功后立即检查。参数stock_warn在药品新增时可以设置默认 10。演示的时候把某个药品的库存调到低于阈值再做出库操作预警日志立刻出现答辩老师一看就明白。5.2 效期查询用 SQL 的 DATEDIFF 筛出临期药品药品效期管理是真实药房系统的硬需求。查询 90 天内过期的药品SQL 一句话搞定SELECT drug_id, drug_name, batch_no, expire_date, DATEDIFF(expire_date, CURDATE()) AS days_left FROM drug WHERE DATEDIFF(expire_date, CURDATE()) BETWEEN 0 AND 90 ORDER BY days_left ASC;DATEDIFF返回两个日期之间的天数差CURDATE()是当前日期。BETWEEN 0 AND 90筛出还没过期但 90 天内会过期的药品ORDER BY days_left ASC让最紧急的排在最前面。如果days_left小于 0 说明已经过期可以单独写一个查询用WHERE DATEDIFF(expire_date, CURDATE()) 0。在 JSP 页面上把days_left小于 30 的行标红视觉效果拉满。5.3 分页查询别把全表数据一次塞进 JSP药品列表如果数据多了一次性findAll()查出几百条塞进 JSP页面会卡。分页查询用 MySQL 的LIMIT关键字SELECT * FROM drug ORDER BY drug_id DESC LIMIT ?, ?;第一个?是偏移量第二个?是每页条数。第 1 页偏移量 0第 2 页偏移量 10假设每页 10 条以此类推。偏移量 (当前页码 - 1) * 每页条数。DAO 方法接收page和pageSize两个参数Servlet 从request.getParameter(page)拿页码默认第 1 页。总页数用SELECT COUNT(*) FROM drug算出来传给 JSP 渲染分页链接。这个功能不复杂但有没有它系统的完成度差一个档次。5.4 用 IDEA 调试 JavaWeb 项目的三个习惯第一个习惯在 Servlet 的doGet第一行打断点用 Debug 模式启动 Tomcat浏览器访问时程序会停在断点处你可以一步步看参数怎么传、SQL 怎么拼。第二个习惯在DBUtil.getConnection()里打日志每次拿连接都输出一行跑一遍功能后看日志里连接拿了多少次、关了没有连接泄漏一眼就能发现。第三个习惯把 Tomcat 的日志级别调到 FINEconf/logging.properties里改SQL 异常和连接池报错都会打出来比只看控制台报错信息管用得多。这套东西做完你的药品存销信息管理系统就不是一个只能演示增删改查的空壳了。它有事务、有连接池、有预警、有效期管理、有分页答辩时随便挑一个点都能讲出为什么这么做。我当年第一次做大作业时把所有逻辑塞在一个 Servlet 里老师问“如果两个人同时出库同一批药品会怎样”我答不上来。后来补了事务和行锁的知识才明白并发扣库存要用UPDATE drug SET stock_quantity stock_quantity - ? WHERE drug_id ? AND stock_quantity ?靠WHERE条件保证不会扣成负数。这个细节写进论文里比堆十页需求分析都有说服力。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑