拓冰建站拓冰建站
首页 / 资讯中心 / 正文

JavaWeb药店管理系统:从Servlet到JSP分层实现与销售事务解析

简介基于Javaweb的药店管理系统完整项目源码与数据库文件专为Java Web课程设计、毕业设计以及初学Servlet/JSP的开发者准备。系统围绕药房日常运营设计了用户管理、药品管理、销售管理、库存管理等核心模块涵盖用户登录鉴权、药品增删改查、库存预警、订单生成与报表统计等典型业务场景。项目采用MVC分层架构前端包含HTML、CSS、JavaScript页面后端由Java、Servlet、JSP协同实现代码结构清晰注释明确适合作为综合练习或二次开发基线。压缩包大小22.51MB共2493个文件其中以JavaScript脚本1270个、PNG图片372个、CSS样式表164个、JSP视图44个、Java源码26个及SQL数据库脚本为主同时包含JAR依赖、XML配置和TXT说明文档文件类型覆盖一个完整Web项目所需的全部层面。目前已有881人浏览学习可对照真实项目掌握数据库表设计、增删改查、会话管理、权限控制与报表输出等关键技能。导入数据库并部署源码后即可运行能直观体验药品进销存全流程对理解Java Web开发规范和项目结构非常有帮助。1. 药店管理系统这个JavaWeb项目源码里真正值钱的是哪几层把一份基于 JavaWeb 的药店管理系统源码解压后你大概率会先看到 ckeditor.asp、events.asp、replaceall.asp 这类以富文本编辑器插件命名的遗留文件——它们属于前端编辑控件的历史资源跟药品、订单、库存这些核心业务没有关系。真正值得拆的是 src 目录下的 MVC 分层代码和配套的数据库脚本用户登录与权限控制、药品信息维护、销售出库、库存预警这几个模块把 JavaWeb 学习里最关键的 Servlet 请求处理、JSP 页面渲染、JDBC 预编译查询和事务提交回滚全串了起来。对正在赶课程设计或毕业设计的人这套源码可以直接对照着改对想给中小药店快速搭一套进销存后台的从业者它也远比 Spring Cloud 全家桶轻量一台 Tomcat 加一个 MySQL 实例就能跑。下面按「先看结构、再建库、后写代码、最后验证」的顺序拆。2. 从Servlet到JSP这套药店系统的MVC分层与目录结构2.1 为什么这个阶段选ServletJSP而不是直接上Spring BootServlet 是 Java 处理 HTTP 请求的基础组件负责接收参数、调用业务方法、决定跳转到哪个页面JSP 负责把业务数据渲染成 HTML允许在页面中嵌入 Java 代码JSTL 提供c:forEach这类标准标签库避免在 JSP 里写大量 scriptlet。药店管理系统的业务以增删改查为主请求频率不高并发压力集中在收银时段Servlet JSP JDBC 在 Tomcat 下运行稳定部署时一个 war 包就够不需要额外引入 Redis、Nginx 这些中间件。选这个技术栈还有一个学习层面的理由直接拿 Spring Boot MyBatis 写 CRUD 的确快但数据库连接何时开启、PreparedStatement 为什么能防注入、事务回滚怎么触发这些问题会被框架掩盖。这套源码把 Controller、Service、DAO 分层暴露得很清楚正好为后续迁移到 Spring MVC 打底。下面是三种常见方案的对比。方案学习成本运行依赖适用场景ServletJSPJDBC低请求链路一目了然仅 Tomcat MySQL课程设计、中小药店后台Spring BootMyBatis中高框架封装多需引入大量 jar团队协作、长期迭代系统PHPMySQL低但类型约束弱Apache/Nginx快速建站、原型验证这套项目里 Servlet 只做参数接收和页面跳转真正的库存扣减、金额计算放在 Service 层DAO 层只负责 SQL这就是典型的 MVC 分工。理解了这一点后面改代码时就不会把业务逻辑堆到 JSP 里也不会在 DAO 里写页面跳转。2.2 源码目录与一次登录请求的完整流转解压后建议先看目录结构判断一份源码能不能跑十分钟内就能得出结论pharmacy/ ├── src/com/pharmacy/ │ ├── controller/ # Servlet 控制器接收请求、跳转页面 │ ├── service/ # 业务逻辑订单金额计算、库存校验 │ ├── dao/ # JDBC 数据访问SQL 在这里执行 │ └── entity/ # 实体类对应 sys_user、drug、orders 表 ├── web/ │ ├── WEB-INF/web.xml # Servlet 映射、Filter 注册 │ ├── jsp/ # 登录页、药品列表、销售收银台等页面 │ └── static/ # CKEditor 等前端静态资源 └── db/pharmacy.sql # 建库建表及初始化数据脚本登录请求的流转链路是理解这套系统的最佳入口浏览器提交用户名和密码web.xml 里映射的 LoginServlet 接收请求LoginService 校验密码UserDao 通过 JDBC 查询 sys_user 表认证成功把用户对象写进 session跳转到 main.jsp认证失败则回到 login.jsp 并携带错误信息。session 里存的 loginUser 会被第 4 章的 Filter 用来判断是否已登录。看 Servlet 映射时重点看三个配置项servlet-class 指定处理类的全限定名写错会直接 404url-pattern 决定访问路径登录接口是 /login药品列表是 /drug/list路径冲突时 Tomcat 按最长匹配优先load-on-startup 如果配了正整数容器启动时就会初始化该 Servlet适合放系统参数加载。2.3 运行环境与JDBC连接参数这套项目建议的基线环境是 JDK 8 Tomcat 8.5/9 MySQL 5.7/8.0数据库驱动用 mysql-connector-java。连接参数一般集中在 classpath 下的 db.properties 里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/pharmacy_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456 jdbc.maxTotal20 jdbc.maxIdle8driver 要注意版本差异MySQL 5.x 时代是 com.mysql.jdbc.Driver8.x 驱动必须写成 com.mysql.cj.jdbc.Driver写错会在启动时报 ClassNotFoundException。url 里的 useUnicodetrue 和 characterEncodingUTF-8 必须一起出现否则 JSP 页面提交的中文药名写进数据库后全是问号serverTimezone 是 8.x 驱动强制要求的时间区参数不配会报时区异常。maxTotal 和 maxIdle 是连接池参数分别控制最大连接数和空闲连接数药店收银场景 20 个连接完全够用调太大反而浪费内存。注意项目跑在自己电脑上时db.properties 里的用户名密码要改成你本机 MySQL 的实际值同时确认数据库驱动 jar 放在 web/WEB-INF/lib 目录下而不是只在 IDE 的构建路径里。很多项目在 IDE 里能跑、部署到 Tomcat 就报 ClassNotFoundException就是这一步漏了。3. 药品、用户、订单三张核心表的数据库建模与SQL脚本3.1 先按业务流程拆表为什么订单要拆成主表和明细表药店日常流程是店员登录、按名称或编码查药品、选择药品结算、扣减库存、生成订单。围绕这条链路最少需要三张业务表加一张关系表sys_user 存操作人员及权限drug 存药品规格和库存orders 存每一笔销售的主信息order_item 存一笔订单里的每一条药品明细。把订单拆成主表和明细表不是冗余设计。一张收银小票可能包含三四种药如果把药品名、数量、价格都拼进 orders 的一个字段后续想统计「阿莫西林这个月卖了多少盒」就只能靠字符串解析连 SQL 聚合都做不了。拆开后 order_item 按 drug_id 关联药品销售排行、毛利统计都是标准 GROUP BY 查询。价格字段用 DECIMAL(10,2) 而不是 FLOAT因为浮点型在涉及金额累加时会产生精度误差药品单价只有两位小数月流水累加后误差会被放大。3.2 建库建表SQL与字段说明脚本文件 pharmacy.sql 是整个项目的根基先建库再建表顺序不能反CREATE DATABASE IF NOT EXISTS pharmacy_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE pharmacy_db; CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码, real_name VARCHAR(50) COMMENT 真实姓名, role TINYINT NOT NULL DEFAULT 2 COMMENT 1-管理员 2-店员, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-启用 0-停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT系统用户表; CREATE TABLE drug ( drug_id INT PRIMARY KEY AUTO_INCREMENT, drug_code VARCHAR(30) NOT NULL UNIQUE COMMENT 药品编码/条码, drug_name VARCHAR(100) NOT NULL COMMENT 药品通用名, spec VARCHAR(50) COMMENT 规格如0.25g*24片, unit VARCHAR(10) DEFAULT 盒 COMMENT 销售单位, sale_price DECIMAL(10,2) NOT NULL COMMENT 零售价, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, min_stock INT NOT NULL DEFAULT 20 COMMENT 库存预警线, category VARCHAR(30) COMMENT 分类感冒/消炎/心脑血管, supplier VARCHAR(100) COMMENT 供货商, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-在售 0-下架, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT药品表; CREATE TABLE orders ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号, user_id INT NOT NULL COMMENT 操作店员ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总额, pay_type TINYINT DEFAULT 1 COMMENT 1-现金 2-微信 3-支付宝, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT销售订单主表; CREATE TABLE order_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL COMMENT 关联orders.order_id, drug_id INT NOT NULL COMMENT 关联drug.drug_id, drug_name VARCHAR(100) COMMENT 冗余商品名防止药品改名后历史单失真, quantity INT NOT NULL COMMENT 销售数量, unit_price DECIMAL(10,2) NOT NULL COMMENT 成交单价, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计金额 ) ENGINEInnoDB COMMENT订单明细表;字段上有几个值得注意的约定。sys_user.password 长度设 64是给 MD5 或 SHA-256 的十六进制结果留的明文密码不仅长度不够也不应该出现在代码里。drug_code 加 UNIQUE 约束因为扫码销售时按编码精确匹配重复编码会导致查询返回多条记录。order_item 里冗余 drug_name 是刻意为之药品信息将来可能改名、下架历史订单的明细必须保留销售当天的快照直接 join drug 表拿名称会导致老单据显示成当前值。所有表用 ENGINEInnoDB是因为下一章的销售出库依赖事务的 commit 和 rollbackMyISAM 不支持事务和行级锁进销存场景不要用。销售统计最常用的查询条件是下单时间建议再补两个普通索引CREATE INDEX idx_orders_create_time ON orders(create_time); CREATE INDEX idx_drug_name ON drug(drug_name);3.3 库存预警、销售日报的SQL写法与触发器补充库存预警的逻辑直接落在业务层就是一条条件查询低于或等于预警线就认为需要补货SELECT drug_code, drug_name, spec, stock, min_stock FROM drug WHERE status 1 AND stock min_stock ORDER BY (stock - min_stock) ASC;ORDER BY (stock - min_stock) 让缺口最大的药排在最前面采购人员按这个列表下单就行。日报统计是管理层最常用的报表按天分组汇总SELECT DATE(create_time) AS sale_date, COUNT(DISTINCT order_id) AS order_count, COUNT(order_id) AS item_count, SUM(total_amount) AS total_amount FROM orders WHERE create_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(create_time) ORDER BY sale_date DESC;DATE(create_time) 把精确到秒的时间截断到日期GROUP BY 对截断后的结果分组COUNT(DISTINCT order_id) 统计订单笔数COUNT(order_id) 统计明细条数两者对比能看出每单平均包含几种药品。这段 SQL 在数据量到十万级时依然能秒出结果前提是 idx_orders_create_time 索引建了。如果还要追踪每次库存变化可以再建一张 drug_stock_log 表用触发器自动记录CREATE TABLE drug_stock_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, old_stock INT NOT NULL, new_stock INT NOT NULL, change_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; DELIMITER $$ CREATE TRIGGER trg_drug_stock_log AFTER UPDATE ON drug FOR EACH ROW BEGIN IF OLD.stock NEW.stock THEN INSERT INTO drug_stock_log(drug_id, old_stock, new_stock) VALUES (NEW.drug_id, OLD.stock, NEW.stock); END IF; END$$ DELIMITER ;触发器适合做审计日志但要注意它会影响 UPDATE 性能而且排错时不直观——如果哪天库存被莫名修改得先查触发器。我的习惯是进销存系统只在库存这类核心表上用触发器订单和用户的状态变更都在 Java 业务层显式记录方便跟日志和调试信息对上号。4. 登录拦截、药品查询与销售出库的关键代码实现4.1 用Filter拦截未登录请求统一做权限控制很多课程设计把权限判断写在每个 JSP 页面顶部页面一多就漏。更稳的常见做法是注册一个 Filter在请求进入 Servlet 之前统一检查 sessionpublic class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 登录页、登录接口、静态资源放行 if (uri.contains(/login) || uri.endsWith(.js) || uri.endsWith(.css) || uri.endsWith(.png) || uri.endsWith(.ico)) { chain.doFilter(req, resp); return; } // session 里没有 loginUser 说明未登录 Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }uri.contains(/login) 必须放在判断最前面否则登录请求自己也会被拦死形成死循环静态资源按后缀放行是为了让登录页的 CSS 和验证码图片能正常加载。sendRedirect 里必须带 request.getContextPath()它对应部署后的应用名比如 /pharmacy不写的话项目名一换跳转地址就失效。Filter 在 web.xml 里这样注册filter filter-nameloginFilter/filter-name filter-classcom.pharmacy.controller.LoginFilter/filter-class /filter filter-mapping filter-nameloginFilter/filter-name url-pattern/*/url-pattern /filter-mappingurl-pattern 用 /* 拦截全部路径放行逻辑放在 Java 代码里维护比在 XML 里堆一堆精确路径好改。管理员和店员的区别可以在 Filter 通过后继续判断 role 字段role2 的店员访问 /admin/ 开头的路径一律返回 403这就是基于角色的访问控制比单纯判断「是否登录」多一层安全边界。4.2 药品查询PreparedStatement预编译与分页参数药品列表页是系统使用频率最高的页面关键词模糊查询加分页是标配。DAO 层代码public ListDrug searchDrugs(String keyword, int page, int pageSize) { String sql SELECT drug_id, drug_code, drug_name, spec, unit, sale_price, stock, min_stock FROM drug WHERE drug_name LIKE ? OR drug_code LIKE ? ORDER BY drug_id DESC LIMIT ?, ?; ListDrug list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, (page - 1) * pageSize); // 计算偏移量 ps.setInt(4, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Drug d new Drug(); d.setDrugId(rs.getInt(drug_id)); d.setDrugCode(rs.getString(drug_code)); d.setDrugName(rs.getString(drug_name)); d.setSpec(rs.getString(spec)); d.setUnit(rs.getString(unit)); d.setSalePrice(rs.getBigDecimal(sale_price)); d.setStock(rs.getInt(stock)); d.setMinStock(rs.getInt(min_stock)); list.add(d); } } } catch (SQLException e) { throw new RuntimeException(药品查询失败, e); } return list; }关键在 PreparedStatement 的 ? 占位符。如果用字符串拼接把 keyword 直接拼进 SQL用户输入 OR 11这类内容时查询条件就会被改写轻则查出全部数据重则配合 UNION 注入拖库。setString 会把单引号等特殊字符转义从源头断掉这条路。分页参数里 LIMIT 的第一个值是偏移量第二页的 offset 是 (2-1)*pageSize所以第 3 个参数要写 (page - 1) * pageSize 而不是 page。keyword 传空字符串时%% 也能匹配全部记录业务层可以把它当作「查询所有」处理。try-with-resources 会在代码块结束后自动关闭 ResultSet、PreparedStatement、Connection比手写 finally 省事但连接池模式下关闭 Connection 只是归还连接不是真的断开。sale_price 用 getBigDecimal 读取对应数据库的 DECIMAL 类型用 getDouble 读会丢精度。4.3 销售出库一个事务同时保证订单、明细、库存一致最容易出错的模块是收银台。一次销售涉及三处写操作插入 orders 主表、插入多条 order_item、扣减 drug.stock。任何一步成功而另一步失败账就对不上——订单生成了库存没扣或者库存扣了订单没生成。解决方案是把三步包在同一个数据库事务里public boolean createSaleOrder(Order order, ListOrderItem items) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 生成业务订单号时间戳 三位随机数 order.setOrderNo(PO System.currentTimeMillis() String.format(%03d, new Random().nextInt(1000))); String insertOrder INSERT INTO orders(order_no, user_id, total_amount, pay_type) VALUES (?,?,?,?); int orderId; try (PreparedStatement ps conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, order.getOrderNo()); ps.setInt(2, order.getUserId()); ps.setBigDecimal(3, order.getTotalAmount()); ps.setInt(4, order.getPayType()); ps.executeUpdate(); try (ResultSet keys ps.getGeneratedKeys()) { keys.next(); orderId keys.getInt(1); // 自增主键供明细表做外键 } } for (OrderItem item : items) { // 条件更新只有库存充足才扣减成功 String updateStock UPDATE drug SET stock stock - ? WHERE drug_id ? AND stock ?; try (PreparedStatement ps conn.prepareStatement(updateStock)) { ps.setInt(1, item.getQuantity()); ps.setInt(2, item.getDrugId()); ps.setInt(3, item.getQuantity()); int rows ps.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足 item.getDrugName()); } } // 写入明细 String insertItem INSERT INTO order_item(order_id, drug_id, drug_name, quantity, unit_price, subtotal) VALUES (?,?,?,?,?,?); try (PreparedStatement ps conn.prepareStatement(insertItem)) { ps.setInt(1, orderId); ps.setInt(2, item.getDrugId()); ps.setString(3, item.getDrugName()); ps.setInt(4, item.getQuantity()); ps.setBigDecimal(5, item.getUnitPrice()); ps.setBigDecimal(6, item.getSubtotal()); ps.executeUpdate(); } } conn.commit(); // 三步都成功才提交 return true; } catch (Exception e) { if (conn ! null) { conn.rollback(); // 任何一步异常撤销全部写操作 } throw new RuntimeException(销售出库失败已回滚, e); } finally { if (conn ! null) { conn.setAutoCommit(true); // 还原连接状态再归还连接池 conn.close(); } } }这段代码有三个值得说透的点。第一是 setAutoCommit(false) 与 commit/rollback 的配合。MySQL 默认每条 SQL 自动提交关闭后所有写操作在当前事务里累积只有 commit 才真正落盘rollback 则全部撤销。catch 里抛出的 RuntimeException 会被 Servlet 层捕获并跳转到出错页面用户看到的是「库存不足」而不是一堆堆栈。第二是条件扣库存UPDATE drug SET stock stock - ? WHERE drug_id ? AND stock ?。先 SELECT 再判断库存再 UPDATE 的做法在并发收银时会出问题两个请求同时读到库存 5各卖 4 件两边都判断通过最后库存变成 -3。用 UPDATE 影响行数来判断数据库层面保证只有库存够扣才更新成功相当于一个轻量的乐观锁。这是这套源码里最值得抄走的一段代码。第三是 RETURN_GENERATED_KEYS。insert 执行后要拿到自增的 order_id靠的是 Statement.RETURN_GENERATED_KEYS 参数配合 getGeneratedKeys()。很多同学写到这里会用「先查 MAX(order_id) 1」的方式但并发下两个事务可能拿到同一个 ID外键就串了。finally 里把 autoCommit 还原成 true是因为连接来自连接池归还时如果还处在事务状态下一个请求拿到连接会行为异常。JSP 页面渲染订单明细时用 JSTL 而不是 scriptletc:forEach varitem items${orderItemList}配合${item.drugName}EL 表达式页面里不出现一行 Java 代码这是视图层推荐写法。5. 部署到Tomcat后的验证顺序与四个高频排错点5.1 从war包到能登录的冒烟验证链路把项目打成 war 放进 Tomcat 的 webapps 目录或者用 IDE 的 Run on Server 直接跑。启动前先确认 MySQL 里 pharmacy_db 已经建好db.properties 的用户名密码和本机一致。部署完成后按这个顺序做冒烟验证访问 http://localhost:8080/pharmacy/ 应该被 Filter 重定向到 login.jsp如果直接看到 404先检查应用名是否匹配 contextPath。用脚本里初始化的管理员账号登录成功进入 main.jsp故意输错密码应停留在登录页并提示错误。新增一个药品库存设 5、预警线设 10回列表页确认记录存在预警列表里能看到这一条。对这款药下一张销售单再去药品列表看库存从 5 变成 4orders 表和 order_item 表各多一条记录。用店员账号登录确认无法访问管理员路径。第五步本地验证时可以用两个浏览器各登一个账号避免 session 互相覆盖。5.2 四个高频报错与处理对照表报错或现象根因处理方式ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动 jar 没放到 WEB-INF/lib把 mysql-connector-java-8.x.jar 复制进 lib 并重启 Tomcat页面中文全变问号JSP 编码、请求编码、JDBC 编码不一致db.url 加 useUnicodetruecharacterEncodingUTF-8JSP 统一 pageEncodingUTF-8访问 /drug/list 返回 404Servlet 注解或 web.xml 的 url-pattern 与实际路径不一致检查映射路径大小写也要对上Access denied for user rootlocalhost密码错误或认证插件问题确认密码MySQL 8 默认 caching_sha2_password可执行 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码提示MySQL 8 的认证插件改动后无需重启数据库改完立即生效但已有连接要断开重连一次否则 navicat 或 JDBC 拿到的还是旧会话状态。5.3 顺着这套源码做二次开发的一个技巧批量导入从供应商 Excel 导入药品是药店系统最常见的扩展需求如果循环调用 4.2 节那种单条 insert五千条数据可能要几分钟。常见做法是改用 JDBC 批处理public void batchImport(ListDrug drugList) { String sql INSERT INTO drug(drug_code, drug_name, spec, sale_price, stock, min_stock) VALUES (?,?,?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); int batchSize 500; for (int i 0; i drugList.size(); i) { Drug d drugList.get(i); ps.setString(1, d.getDrugCode()); ps.setString(2, d.getDrugName()); ps.setString(3, d.getSpec()); ps.setBigDecimal(4, d.getSalePrice()); ps.setInt(5, d.getStock()); ps.setInt(6, d.getMinStock()); ps.addBatch(); if ((i 1) % batchSize 0) { ps.executeBatch(); // 每500条提交一批减少网络往返 } } ps.executeBatch(); conn.commit(); } catch (SQLException e) { throw new RuntimeException(批量导入失败, e); } }批处理的关键是 addBatch 攒一批再 executeBatch而不是每执行一条就发生一次网络往返。batchSize 取 500 是折中值太小批处理优势不明显太大则单次提交的 undo 日志膨胀出错回滚也慢。验证方式很简单导入前记录 drug 表行数导入后对比差值再抽查几条编码相同的记录确认 UNIQUE 约束让重复数据被拒绝而不是覆盖这两项都通过说明批量导入链路是干净的。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门