
简介一套基于JavaWeb的药品存销信息管理系统源代码与数据库适合软件工程专业学生作为课程大作业、毕业设计或实训项目参考。系统围绕药品信息、入库、出库与库存统计等存销业务展开采用JSP、Servlet与JavaBean等经典JavaWeb分层结构配有数据库初始化脚本和项目配置文件导入开发环境后即可运行调试便于理解前后端数据流转。资源包内共包含623个文件其中Java源码108个、编译后的class文件202个方便直接部署或对照学习69个jar依赖库与65个XML配置项保障运行环境36个JSP页面与30个JS、22个CSS共同构成操作界面另有15个xls、12个xlsx数据表格及SQL脚本用于数据准备整个压缩包约46.67MB。内容预览中出现的多日访问日志和class文件说明项目经过实际编译运行。目前已有99人浏览学习借助完整的源码、页面、配置和数据库可以梳理请求处理、数据持久化以及页面渲染的完整链路对掌握传统JavaWeb开发思路很有帮助。1. 基于JavaWeb的药品存销信息管理系统一份能跑的大作业该长什么样软件工程大作业如果抽到“基于JavaWeb的药品存销信息管理系统”意味着你要交付的远不止一个能登录的页面一份运行不报错的源代码、一份能直接导入的MySQL数据库脚本以及能把这套存销流程讲清楚的设计文档。市面上很多“完整案例”要么缺表数据、要么JDBC连接写死、要么Servlet路径对不上第一次在IDEA里打开就翻车。这篇文章不替你做作业而是把从选型、建表、写代码到配置Tomcat跑通的完整路数拆给你看让新手跟着能复现让已经在改项目的人知道坑在哪、答辩时该说什么。2. JavaWeb 选型与业务设计先定技术栈再把存销流程拆清楚2.1 为什么用 Servlet JSP MySQL而不是直接上 Spring Boot很多同学一上来就想用 Spring Boot理由是“现在企业都用”。这句话对了一半。课程设计和毕业设计的评分重点在于你是否能把软件工程的方法论讲清楚需求分析、用例图、数据流图、模块划分、数据库设计、测试用例。用 Spring Boot 固然快但注解和自动装配对新手是个黑匣子——答辩时老师问“这个 Bean 是怎么注入的”“自动配置原理是什么”讲不清楚就容易扣分。相反Servlet JSP JDBC 这套 JavaWeb 经典栈每一行代码都在明面上请求怎么进来、参数怎么取、SQL 怎么执行、结果怎么渲染全程没有魔法。另一个现实原因是环境兼容性。课设一般要求本地能跑、老师能现场验收Tomcat 的版本、JDK 的版本、MySQL 的驱动版本都要可控。Servlet/JSP 工程对 IDEA 的依赖简单不需要 Maven 拉几百个依赖把 jar 放进 WEB-INF/lib 就能跑。我一般建议除非老师明确说“可以用 Spring Boot”否则老老实实用原生 JavaWeb。这不是技术倒退而是用最小的复杂度把软件工程这门课的得分点拿全。2.2 药品存销的五条主流程与模块边界药品存销系统听起来复杂拆开就是“进货”“卖货”“看库存”三件事。结合软件工程大作业的文档要求业务模块通常这样划分模块核心业务对应表涉及角色登录认证管理员和操作员登录、退出t_user管理员、操作员药品档案药品新增、修改、删除、按名称/编码查询t_drug管理员供应商管理维护供货商信息入库时选择t_supplier管理员入库管理填写入库单增加库存t_purchase、t_drug操作员销售出库填写销售单校验库存后扣减t_sale、t_drug操作员库存预警低于最低库存阈值时在前端高亮提示t_drug所有角色老师最看重的用例图、数据流图就是围绕这五条流程画的。入库和销售是系统的两个“动点”药品表和库存是“静点”权限只分两级——管理员能维护基础数据操作员只能做日常进出货这样用例图既有层次感又不复杂。如果你想加分可以在统计报表里加一张“按药品月度销量排行”的页面一个GROUP BY聚合查询就能实现但软件工程文档里能多画一张数据流图。2.3 数据流设计库存数是怎么在入库、销售之间流动的库存是整个系统的核心状态所有业务最终都落在t_drug.stock_quantity这一个字段上。入库时stock_quantity 入库数量销售出库时stock_quantity - 销售数量公式就是当前库存 历史入库总量 - 历史销售总量。这里有个设计选择课程设计常见做法是直接在药品表上维护库存字段而不是单独建一张库存流水表。原因很简单——查询列表时一条 SQL 就能带出库存不需要 JOIN 两张表。代价是每次增删操作都要保证同步更新这一步我会在源码实现里用事务兜底。需要注意的是药品还有批次和有效期问题。课设规模的系统可以简化处理t_drug表里直接放expire_date字段入库时手动更新不做批次自动拆分。如果老师追问“同一药品不同进货批次价格不一样怎么办”你要能答出“真实系统应该拆成药品批次表但课设范围把批次合并到主表用最新价格作为销售价”。这是评分点上很讨巧的一句话说明你懂边界在哪不是不会做而是范围控制。3. 数据库脚本建表、初始化数据与三个必改参数3.1 六张表的关系与字段设计数据库是这套系统最早能落地验证的部分。按上一章的模块划分我建议建六张表分别是t_user、t_supplier、t_drug、t_purchase、t_sale外加可选的数据字典表这里不做展开。t_purchase和t_sale只记录“单据头”层面的信息不拆明细表符合课设的体量。字段设计上有几个关键点金额一律用DECIMAL(10,2)不要用FLOAT否则精度问题会在结算时暴露库存和预警线用INT因为药品按盒、瓶、支计数不需要小数每个表都留create_time方便演示“最近几天进了多少货”这类 SQL 查询。外键在课程设计里很容易走极端。我一般会建外键约束但不推荐设置ON DELETE CASCADE因为药品一旦被销售单引用就应该禁止直接删除否则库存历史会对不上。实际操作中删除药品前先查t_sale和t_purchase有没有引用记录有则提示“该药品已有业务单据无法删除”建议改为下架状态。这种设计在答辩时能解释成“保证数据完整性”比裸删数据好得多。3.2 建库建表 SQL 脚本一次执行不出错拿到源代码后第一件事不是看代码而是先把数据库跑起来。下面是完整脚本整理文档时你也可以用 IDEA 的 Database 面板右键执行或直接导出这份脚本放进课程设计附件。-- 建库。字符集用 utf8mb4排序规则用 utf8mb4_general_ci CREATE DATABASE IF NOT EXISTS drug_store DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE drug_store; -- 用户表管理员和操作员放同一张表role 字段区分 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 课设演示用明文真实项目必须存散列值, real_name VARCHAR(50), role TINYINT NOT NULL DEFAULT 1 COMMENT 1管理员, 2操作员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 供应商表 CREATE TABLE t_supplier ( id INT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(100) NOT NULL, contact VARCHAR(50), phone VARCHAR(20), address VARCHAR(200) ); -- 药品表库存字段直接冗余在这里 CREATE TABLE t_drug ( id INT PRIMARY KEY AUTO_INCREMENT, drug_code VARCHAR(30) NOT NULL UNIQUE COMMENT 药品编码/条形码, drug_name VARCHAR(100) NOT NULL, spec VARCHAR(50) COMMENT 规格描述, unit VARCHAR(20) DEFAULT 盒, manufacturer VARCHAR(100), purchase_price DECIMAL(10,2) NOT NULL DEFAULT 0, sale_price DECIMAL(10,2) NOT NULL DEFAULT 0, expire_date DATE COMMENT 有效期截止, stock_min INT NOT NULL DEFAULT 10 COMMENT 库存预警下限, stock_quantity INT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 入库单记录每次补货 CREATE TABLE t_purchase ( id INT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, supplier_id INT, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, operator_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 销售单记录每次出库 CREATE TABLE t_sale ( id INT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, customer_name VARCHAR(50), operator_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 初始化一个管理员账号密码 123456登录以后可在页面里改 INSERT INTO t_user(username, password, real_name, role) VALUES (admin, 123456, 系统管理员, 1);这段脚本里有三个地方要专门说明。第一字符集用utf8mb4而不是utf8因为utf8mb4才能完整存储中文和生僻字符MySQL 8.0 默认就是utf8mb45.7 需要手动指定。第二DATETIME DEFAULT CURRENT_TIMESTAMP在 MySQL 5.6 及以上才支持如果你的 MySQL 是 5.5要么升级要么把create_time的默认值去掉在 Java 代码里手动new Date()再插入。第三药品表的stock_quantity直接冗余在主表这是刻意的查询列表时不用 JOIN性能更好。3.3 连接参数、字符集与三个必改配置数据库脚本能跑通不代表 Java 能连上真正的坑集中在连接参数。下面是 JDBC 连接串的常见写法String url jdbc:mysql://localhost:3306/drug_store? useUnicodetrue characterEncodingutf8 useSSLfalse serverTimezoneAsia/Shanghai;这三个配置必须改第一serverTimezoneAsia/Shanghai是给 MySQL 8.x 驱动用的不加会报时区错误第二个characterEncodingutf8决定了中文参数从页面到数据库不变成问号第三个useSSLfalse是为了避免本机连接时的 SSL 告警日志刷屏。如果你用的是 mysql-connector-java 5.1.x 老驱动类名是com.mysql.jdbc.Driver换成 8.x 驱动后类名要改成com.mysql.cj.jdbc.Driver有些项目从网上抄了 5.x 的驱动类名又配了 8.x 的 jar启动时直接ClassNotFoundException。另外如果你需要修改表结构不要直接在可视化工具里点了保存就完事。课程设计文档通常要求提交数据库脚本我建议每次改表都在 SQL 文件里同步更新用ALTER TABLE语句记录变更例如增加字段用ALTER TABLE t_drug ADD COLUMN stock_location VARCHAR(50);。这既是软件工程里的版本管理意识也是答辩时展示职业习惯的加分细节。4. 源码实现预编译查询打通增删改查库存扣减做成一个事务4.1 源码目录结构与分层职责拿到或自己写的源码包结构必须一眼能看出分层。我习惯用下面这种结构它对应了 MVC 的经典划分老师看代码时也能快速找到每一层drug-store/ ├── src/ │ └── com/drug/ │ ├── entity/ # 实体类Drug, User, Supplier, SaleOrder │ ├── dao/ # 数据访问层DrugDao, UserDao, SaleDao │ ├── service/ # 业务层DrugService, SaleService │ ├── servlet/ # 控制层LoginServlet, DrugServlet, SaleServlet │ └── util/ # 工具类DBUtil, StringUtil ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ # 存放 mysql-connector-java.jar │ ├── css/ # 样式文件 │ ├── jsp/ # 页面login.jsp, list_drug.jsp, add_drug.jsp │ └── index.jsp └── sql/ └── init.sql # 数据库初始化脚本分层的好处是答辩时好讲Servlet 只负责接收请求和转发页面不写 SQLDAO 只负责数据库操作不处理业务判断Service 夹在中间做库存校验和事务控制。如果代码全部堆在 Servlet 里虽然功能能跑但“软件工程”四个字的得分会大打折扣。一个小技巧在文档里画一张三层架构图标注每个类的职责这一页基本能撑起五分钟的讲解。4.2 DBUtil 与 PreparedStatement连接和防注入先做对所有 DAO 的起点是数据库连接工具类。课程设计阶段不需要上连接池DriverManager 直连最简单但代码要写得规范驱动注册放在静态块里、连接参数集中定义、提供统一的关闭资源方法。package com.drug.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class DBUtil { // 使用 8.x 驱动时类名是 com.mysql.cj.jdbc.Driver private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/drug_store ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (Exception ignore) {} } if (stmt ! null) { try { stmt.close(); } catch (Exception ignore) {} } if (conn ! null) { try { conn.close(); } catch (Exception ignore) {} } } }连接串里的四个参数在前面已经讲过这里不再重复。要强调的是PASSWORD这个位置我见过很多同学把密码硬编码在代码里最后交作业也不改老师如果连不上数据库第一反应就是看这里。正确做法是密码和用户名在课程设计说明文档里单独列一页代码里注释标明“部署前请先改成本机 MySQL 的账号密码”。这样既坦诚又避免答辩现场连库失败的尴尬。再说PreparedStatement。所有涉及参数的 SQL 都必须用预编译不要用Statement拼接字符串。原因不只是防 SQL 注入还有一层现实问题药品名可能包含单引号、百分号这些特殊字符用字符串拼接轻则 SQL 报错重则产生语法漏洞。预编译的?占位符天然规避了这类问题。4.3 药品管理增删改查从 Servlet 取参到 JSP 渲染的完整链路增删改查是系统的基本功以药品列表分页查询为例完整链路是浏览器请求drug/list→ Servlet 接收参数 → Service 调 DAO 查数据库 → 结果放进 request 域 → forward 到 JSP 渲染表格。先看 DAO 层的查询方法public ListDrug findByPage(String keyword, int start, int limit) throws Exception { String sql SELECT id, drug_code, drug_name, spec, unit, manufacturer, purchase_price, sale_price, expire_date, stock_min, stock_quantity FROM t_drug WHERE drug_name LIKE ? OR drug_code LIKE ? ORDER BY id DESC LIMIT ?, ?; ListDrug list new ArrayList(); Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, start); // 分页起点从0开始 ps.setInt(4, limit); // 每页条数 ResultSet rs ps.executeQuery(); while (rs.next()) { Drug d new Drug(); d.setId(rs.getInt(id)); d.setDrugCode(rs.getString(drug_code)); // 其余字段按相同方式填充 list.add(d); } DBUtil.close(rs, ps, conn); return list; }注意LIMIT ?, ?的参数类型必须用setInt如果用setStringMySQL 会隐式转换虽然多数情况下能跑但性能上会放弃索引。分页计算在 Servlet 里完成当前页码从page参数取默认 1每页显示 10 条总页数用(total limit - 1) / limit这种整数除法公式避免浮点误差。Servlet 层的代码是“薄”的只做参数解析和页面跳转WebServlet(/drug/list) public class DrugListServlet extends HttpServlet { private final DrugService drugService new DrugService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String pageStr req.getParameter(page); int page 1; if (pageStr ! null pageStr.matches(\\d)) { page Integer.parseInt(pageStr); } String keyword req.getParameter(keyword); if (keyword null) { keyword ; } int total drugService.count(keyword); int pageSize 10; int totalPage (total pageSize - 1) / pageSize; ListDrug list drugService.findByPage(keyword, (page - 1) * pageSize, pageSize); req.setAttribute(list, list); req.setAttribute(totalPage, totalPage); req.setAttribute(currentPage, page); req.setAttribute(keyword, keyword); req.getRequestDispatcher(/jsp/list_drug.jsp).forward(req, resp); } }这里有个容易被忽略的参数校验pageStr.matches(\\d)保证页码必须是纯数字否则非法输入会导致NumberFormatException。表单页面的action路径要和WebServlet里的路径完全一致这是 404 最常见的原因。JSP 端用 JSTL 遍历列表并在库存低于阈值时输出预警样式c:forEach items${list} vard tr td${d.drugCode}/td td${d.drugName}/td td${d.stockQuantity}/td td c:choose c:when test${d.stockQuantity d.stockMin} span stylecolor:red;库存偏低请补货/span /c:when c:otherwise正常/c:otherwise /c:choose /td /tr /c:forEach把查询条件里的keyword原样回显到搜索框是很多新手漏掉的细节——搜索后关键词消失用户以为系统出 bug 了。这个回显只需要在 JSP 里写${keyword}即可。增、删、改的流程和查询完全对称删除时注意先查业务单据有无引用这个逻辑写在 Service 层不要在 Servlet 里判断。4.4 销售出库校验、扣库存、写订单一个事务完成销售出库是整套系统最容易出 bug 的地方先查库存、判断足不足、扣减库存、写入销售单。如果这几步中间没有事务保护一旦第二步扣库存成功、第三步写单失败库存就凭空少了。我看到过的课设代码里80% 都漏了事务理由很简单——单条 SQL 的时候确实不需要事务但业务一复杂就原形毕露。public void sell(int drugId, int quantity, String customerName, int operatorId) throws Exception { Connection conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交后续手动控制 try { // 第一步查询当前库存加行锁防止并发超卖 String selectSql SELECT stock_quantity, sale_price FROM t_drug WHERE id ? FOR UPDATE; PreparedStatement ps1 conn.prepareStatement(selectSql); ps1.setInt(1, drugId); ResultSet rs ps1.executeQuery(); if (!rs.next()) { throw new RuntimeException(药品不存在); } int stock rs.getInt(stock_quantity); double salePrice rs.getDouble(sale_price); if (stock quantity) { throw new RuntimeException(库存不足当前剩余 stock); } // 第二步扣减库存 String updateSql UPDATE t_drug SET stock_quantity stock_quantity - ? WHERE id ?; PreparedStatement ps2 conn.prepareStatement(updateSql); ps2.setInt(1, quantity); ps2.setInt(2, drugId); ps2.executeUpdate(); // 第三步写入销售单 String insertSql INSERT INTO t_sale(drug_id, quantity, price, customer_name, operator_id) VALUES(?, ?, ?, ?, ?); PreparedStatement ps3 conn.prepareStatement(insertSql); ps3.setInt(1, drugId); ps3.setInt(2, quantity); ps3.setDouble(3, salePrice); ps3.setString(4, customerName); ps3.setInt(5, operatorId); ps3.executeUpdate(); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.close(); } }FOR UPDATE是关键中的关键它锁住的是药品表的这一行防止两个人同时买同一种药时都读到库存 10、同时扣成负数。课设本地单机可能永远触发不了并发但老师问起来能答出“行锁防并发超卖”这个回答比写十行 CRUD 都值钱。销售价格这里取了当前药品表里的sale_price不取前端传过来的价格原因很简单前端传的价格可以被篡改真实系统应该在服务端定价。另外所有executeUpdate都没用setAutoCommit(true)恢复因为conn.close()后连接就归还了单例复用连接的情形在课设里不存在不用画蛇添足。入库的业务和销售完全对称只是把扣库存变成加库存把写销售单变成写入库单。建议两个方法都放在SaleService和PurchaseService里共用一个事务工具方法不要在 Servlet 里写try-catch。事务控制放在 Service 层而不是 DAO 层是为了让 DAO 保持“一次数据库操作一个方法”的纯粹性这个分层原则也是答辩常考题。5. IDEA 运行 JavaWeb 项目配置跑通源码的五个步骤与常见问题排查5.1 推荐版本组合JDK 1.8 Tomcat 8.5 MySQL 5.7在网上找 javaweb 项目完整案例时最怕的就是版本不兼容。我推荐的组合是JDK 1.8、Tomcat 8.5、MySQL 5.7、mysql-connector-java 5.1.49。这套组合的兼容性最稳JDK 1.8 是 JavaWeb 课设的事实标准Tomcat 8.5 自带对 Servlet 3.1 的支持MySQL 5.7 既不老到缺功能也不会像 8.0 那样有多余的时区报错。如果你本机装的是 MySQL 8.0也没问题但驱动要换成 8.0.33连接串里必须带serverTimezoneAsia/Shanghai否则启动后第一次查询就抛The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。IDEA 版本不必强求最新。2019 到 2023 的任一版本都能跑但对新手来说2021 版以后自带的 Tomcat 集成更稳定。不要用 Eclipse虽然在 Eclipse 里也能配置但 IDEA 的 Artifact 部署机制有大量中文教程遇到问题好搜。5.2 导入源码与配置 Tomcat 的五个步骤IDE 运行 JavaWeb 项目的配置并不复杂但步骤顺序错了就会连锁报错。下面这套流程我每次都在用按顺序做基本一次过。第一步用 IDEA 打开源码根目录。如果项目没有 Maven 的pom.xml直接File - Open选中文件夹IDEA 会识别成普通 JavaWeb 工程如果有 pom.xml就选Trust Project等 Maven 导入。第二步检查 Project SDKFile - Project Structure - Project确认 Project SDK 是 1.8、Language Level 是 8。如果这里选错后面编译经常出现“java: 无效的源发行版”的报错。第三步把web/WEB-INF/lib下的 mysql 驱动 jar 加入依赖选中 jar右键Add as Library。这一步漏了启动时必报ClassNotFoundException: com.mysql.jdbc.Driver而且报错出现在运行时编译期完全不提示特别让人头疼。第四步配置 TomcatRun - Edit Configurations - 左上角 - Tomcat Server - Local在Application server里选择本机 Tomcat 目录然后切到Deployment标签点 - Artifact选带exploded的那个通常是drug-store:war explodedApplication context改成/drug-store。第五步先启动 MySQL 并执行 SQL 脚本建库再点 Tomcat 的绿色启动按钮看到Artifact is deployed successfully后浏览器访问http://localhost:8080/drug-store/login.jsp。这五个步骤里最容易翻车的是第四步的 Artifact 配置。很多网上教程直接点运行Tomcat 启动了但浏览器 404打开Run窗口一看根本没有Deployment内容这就是 Artifact 没加。另一个高频问题是Application context带了端口或多余的斜杠比如/drug-store/和/drug-store访问路径不一样前者在拼接页面跳转时很容易多出双斜杠。5.3 部署后最常见的五个报错现象、原因、解决我在帮人排查这个系统的过程里80% 的问题集中在下面五条按出现频率排序写给你。报错一浏览器看到 HTTP 404。现象是 Tomcat 正常启动了但访问不到登录页。原因一般有三个Artifact 没有部署到 Deployment 里访问的 URL 没带Application context页面放在/jsp/子目录但你访问的是根路径。解决方法是先看 Run 窗口的 Tomcat 日志确认Deployment有 artifact然后用http://localhost:8080/项目名/login.jsp访问如果项目名不确定在Deployment页签看Application context的值。报错二ClassNotFoundException: com.mysql.jdbc.Driver。现象是 Tomcat 启动不报错点登录才报 ClassNotFoundException。原因几乎只有一个mysql 驱动 jar 没有复制到web/WEB-INF/lib目录。IDEA 里的Add as Library只对编译期有效运行期的 classpath 是从WEB-INF/lib读取的。解决把 jar 复制到web/WEB-INF/lib下然后File - Sync刷新工程。这一步做完再重启 Tomcat。报错三中文全部变成问号或乱码。现象是药品名称、供应商名称在页面上显示成????或者写入 MySQL 后变成乱码。原因是编码在四个地方不一致JSP 页面编码、Servlet 取参编码、数据库表字符集、JDBC 连接串编码。解决方法是四端都对到 UTF-8JSP 文件头部加% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %每个 Servlet 的入口加req.setCharacterEncoding(UTF-8)建表时用utf8mb4JDBC URL 带characterEncodingutf8。字符集问题属于玄学范畴只要有一端漏了其他地方改再多都没用。报错四Communications link failure 或 Connection refused。现象是点击登录后在控制台看到连接失败异常堆栈里提到 localhost:3306。原因不外乎三种MySQL 服务没启动、root 密码和代码里不一致、连接串的端口写错。解决先在命令行执行mysql -u root -p测一下本机能否连接如果命令连不上去服务管理器里启动 MySQL如果能连上把代码里DBUtil的用户名密码改成你本机实际能用的那套。有些同学连的 MySQL 装在另一个端口比如 3307连接串也要对应改。报错五表单提交后报 500关键字里有Cannot invoke ... because ... is null。现象是列表页正常点新增或保存就 500。原因通常是前端表单的name属性和doPost里req.getParameter()的参数名对不上。比如 JSP 里写的是namedrugNameServlet 里取的是drug_name拿到 null。解决把 JSP 的 name 和 Servlet 的取值参数逐个核对一遍最好的办法是打开浏览器开发者工具看 Network 里 Payload 传了哪些字段名照着改 Servlet。这个习惯比肉眼排查快得多。6. 答辩演示与进阶技巧用三个动作证明系统能上线6.1 演示前先跑一遍完整业务闭环答辩现场最怕的不是代码写不好而是演示到一半数据对不上。我的习惯是演示前十分钟按“登录 → 新增药品 → 新增供应商 → 做一次入库 → 做一次销售 → 看库存变化 → 看预警提示”的顺序完整跑一遍并且所有输入都用真实业务数据而不是test、123这样的占位符。老师如果看到药品列表中库存从 10 变成 28 又变成 19这个系统的可信度立刻建立起来。建议提前录一屏完整操作视频放在 U 盘里万一现场网络或数据库抽风直接放视频保底。6.2 答辩最容易被追问的三个问题第一个问题“为什么不用连接池”回答思路课程设计重点在理解 JDBC 和事务控制DriverManager 足够演示但我知道生产环境会用 HikariCP 或 Druid如果老师需要我可以现场加配置。第二个问题“药品批次和有效期怎么处理”回答思路当前设计把有效期放在药品主表真实系统需拆批次表按先进先出原则发货。第三个问题“怎么保证并发下库存不超卖”回答思路销售方法里用了SELECT ... FOR UPDATE行锁加事务两个并发请求同时操作同一药品时后一个请求会等前一个提交从而读到最新库存。这三个问题能答好逻辑清晰一点比代码本身更能拿分。6.3 给系统加一个能省半小时的 Excel 导入如果你想演示“数据来源”这个环节最实用的技巧是让药品数据支持 Excel/CSV 导入。不需要引入 Apache POI简单场景直接用 CSV按编码、名称、规格、价格、库存量逐行解析。核心代码思路是用BufferedReader按行读split(,)切列逐行拼INSERT每 100 条批量执行一次。这个功能一般只需要一个 Servlet 和两个 JSP 方法但演示效果非常好——老师看到你导入上百条真实药品数据第一印象就是“这个学生做过真实项目”。整套系统做到这个程度技术栈虽然朴素但数据流、事务、权限、预警、部署文档每一项都能讲出设计理由。我也曾在这种大作业上吃过亏以为页面多就是功能全结果答辩时老师一句“库存扣减怎么保证一致性”就让我哑火。后来我把每个关键点都写成“为什么这么做”的笔记再答辩时反而被夸思路清楚。课设的价值不在炫技而在你能不能把每个决定背后的取舍讲明白。希望帮到你。本文还有配套的精品资源点击获取