ARTICLE DETAIL

资讯详情

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

Javaweb药店管理系统源码与数据库落地实战:从建表到避坑

Javaweb药店管理系统源码与数据库落地实战:从建表到避坑 简介这是一套基于Java Web技术栈的药店管理系统完整项目包含可运行源码与配套数据库面向Java Web初学者、课程设计学生及需要实战练手的开发者帮助理解Servlet、JSP、JSTL与MVC分层开发在真实业务中的落地方式。系统覆盖用户管理、药品管理、销售管理、库存管理、报表统计与权限控制等模块数据库设计涉及药品表、用户表、订单表等核心结构并包含库存预警、销售报表、数据加密与用户认证等实用功能点。压缩包共2493个文件以js、png、gif、css等前端静态资源为主辅以jsp页面、java源码、class文件、jar依赖、xml配置及sql建库脚本整体约22.51MB目录按MVC模式组织便于按模块查阅与二次开发。目前已有882人学习下载适合对照源码梳理请求处理流程、数据库表关系与业务逻辑是掌握Java Web综合项目开发的实践素材。1. 药店管理系统从零到跑通一套 Javaweb 源码和数据库到底该怎么落地接手一个药店管理系统的 Javaweb 项目最怕的不是代码写不出来而是拿到源码和数据库文件之后环境跑不起来、表结构对不上、增删改查一改就崩。药店这个场景跟普通 CRUD 练手项目不一样它涉及药品批号、有效期、库存预警、处方药限购、销售流水追溯任何一张表设计歪了后面全是血泪经验。我见过太多人把「基于 Javaweb 的药店管理系统及源码和数据库」当成课程设计模板导入 IDEA 一跑就报 500最后连数据库都没连上。这篇笔记就按一线落地的顺序把技术选型、数据库设计、源码结构、环境配置、避坑排查和进阶技巧讲透适合正在做课程设计、毕业设计或者小团队接私活的开发者也适合想拿一个完整案例练手 Javaweb 连接 MySQL 数据库的人。读完你至少能自己把项目跑起来知道每个参数为什么这么设出了问题往哪查。2. 技术选型与数据库设计为什么药店场景不能照搬通用 CRUD 模板2.1 药店管理系统的核心业务对象决定了表结构普通管理系统通常就是用户表、角色表、日志表三件套但药店管理系统必须围绕「药品」这个核心实体展开。药品有通用名、商品名、规格、剂型、生产厂家、批准文号、批号、有效期、储存条件这些字段一个都不能省。尤其是批号和有效期直接决定库存能不能卖、要不要预警。销售环节还要记录处方药标记、购买人身份信息处方药限购、销售员、销售时间。如果只按「商品表 订单表」来设计后面做有效期预警和批号追溯时就得推倒重来。我一般会把核心表分成四组基础信息表药品、供应商、员工、会员、库存表按批号维度、销售表主表 明细、系统表用户、角色、日志。库存表一定要以「药品 ID 批号」作为联合唯一键而不是只按药品 ID 汇总数量。因为同一药品不同批号的有效期不同卖的时候要按先进先出扣减。这个设计点很多模板会忽略等到做近效期预警时才发现库存表根本查不出哪个批号快过期。2.2 数据库建表脚本与关键字段说明下面这段 SQL 是药店管理系统里最核心的药品表和库存表建表语句可以直接在 MySQL 5.7 或 8.0 里执行。注意字符集用 utf8mb4排序规则用 utf8mb4_general_ci避免药品名称里的生僻字乱码。-- 药品基础信息表 CREATE TABLE medicine ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 药品ID, generic_name varchar(100) NOT NULL COMMENT 通用名, trade_name varchar(100) DEFAULT NULL COMMENT 商品名, spec varchar(50) NOT NULL COMMENT 规格, dosage_form varchar(20) DEFAULT NULL COMMENT 剂型, manufacturer varchar(100) DEFAULT NULL COMMENT 生产厂家, approval_no varchar(50) DEFAULT NULL COMMENT 批准文号, is_prescription tinyint(1) DEFAULT 0 COMMENT 是否处方药 0否 1是, storage_condition varchar(50) DEFAULT NULL COMMENT 储存条件, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_generic_name (generic_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT药品基础信息; -- 库存表按批号维度 CREATE TABLE stock ( id int(11) NOT NULL AUTO_INCREMENT, medicine_id int(11) NOT NULL COMMENT 药品ID, batch_no varchar(50) NOT NULL COMMENT 批号, production_date date DEFAULT NULL COMMENT 生产日期, expiry_date date NOT NULL COMMENT 有效期至, quantity int(11) NOT NULL DEFAULT 0 COMMENT 库存数量, purchase_price decimal(10,2) DEFAULT NULL COMMENT 进价, sale_price decimal(10,2) DEFAULT NULL COMMENT 售价, supplier_id int(11) DEFAULT NULL COMMENT 供应商ID, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_medicine_batch (medicine_id,batch_no), KEY idx_expiry_date (expiry_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存批号表;逻辑说明medicine 表存药品静态信息stock 表存动态库存。uk_medicine_batch 联合唯一键保证同一药品同一批号只有一条记录入库时用 ON DUPLICATE KEY UPDATE 累加数量。idx_expiry_date 索引是为了近效期预警查询能走索引否则数据量上来后全表扫描会拖慢系统。参数方面quantity 用 int 足够药店单批号库存一般不会超过十万purchase_price 和 sale_price 用 decimal(10,2) 避免浮点误差。is_prescription 用 tinyint(1) 而不是 char方便 Java 端映射成 Boolean。2.3 销售主表与明细表的外键约束取舍销售表设计有个常见争议要不要加物理外键。我的做法是业务表之间不加物理外键只在应用层保证一致性。原因是药店系统后期可能分库或者做数据同步物理外键会带来迁移麻烦。但销售明细表必须冗余药品名称、规格、批号、售价因为历史订单不能因为药品信息修改而变样。CREATE TABLE sale_order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, member_id int(11) DEFAULT NULL COMMENT 会员ID, employee_id int(11) NOT NULL COMMENT 销售员ID, total_amount decimal(10,2) NOT NULL DEFAULT 0.00, pay_type tinyint(1) DEFAULT 1 COMMENT 支付方式 1现金 2医保 3移动支付, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售主表; CREATE TABLE sale_detail ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL, medicine_id int(11) NOT NULL, medicine_name varchar(100) NOT NULL COMMENT 冗余药品名, batch_no varchar(50) NOT NULL, quantity int(11) NOT NULL, price decimal(10,2) NOT NULL, subtotal decimal(10,2) NOT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售明细;order_no 用业务规则生成比如「年月日 随机数 序列」不要用自增 ID 直接暴露给前端。pay_type 预留医保类型方便后期扩展。sale_detail 里冗余 medicine_name 和 batch_no 是刻意为之历史数据不可变。如果做课程设计这套表结构足够覆盖增删改查、库存预警、销售统计三个核心模块。3. 源码结构与 IDEA 运行配置把 Javaweb 项目完整案例跑起来3.1 典型 Maven 项目目录与各层职责一套能跑的 Javaweb 药店管理系统源码目录结构通常长这样src/main/java 下分 controller、service、dao、entity、util 五个包src/main/resources 放 db.properties、mybatis-config.xml 或 spring 配置文件src/main/webapp 放 WEB-INF/web.xml、jsp 页面和 static 静态资源。如果是 SSM 项目controller 用 Controller 注解service 用 Servicedao 用 Repository 或者 MyBatis 的 Mapper 接口。新手最容易搞混的是 web.xml 里 DispatcherServlet 的配置和 spring 容器扫描路径配错了启动直接报 NoSuchBeanDefinitionException。我一般会先看 pom.xml 里的依赖版本重点确认 mysql-connector-java 版本和数据库版本匹配。MySQL 8.0 要用 8.0.x 的驱动连接 URL 要加时区参数否则报「The server time zone value is unrecognized」。下面是一个最小可用的 db.properties 配置。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/pharmacy_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码参数说明serverTimezone 必须设否则 MySQL 8 连接失败useSSLfalse 在本地开发关掉生产环境再开characterEncodingutf8 配合数据库 utf8mb4 使用。如果密码里有特殊字符记得做 URL 编码。3.2 在 IDEA 里配置 Tomcat 并部署项目步骤一File → Project Structure → Modules确认 src/main/java 被标记为 Sourcessrc/main/resources 被标记为 Resources。步骤二Facets 里添加 WebWeb Resource Directory 指向 src/main/webappweb.xml 路径指向 WEB-INF/web.xml。步骤三Artifacts 里添加 Web Application: Exploded输出目录默认就行。步骤四Run → Edit Configurations添加 Tomcat LocalDeployment 里选刚才的 ArtifactApplication context 设成 /pharmacy。启动后访问 http://localhost:8080/pharmacy 就能看到登录页。如果启动报 404先检查 Application context 是不是多了斜杠如果报 ClassNotFoundException检查 Artifact 里有没有把 Maven 依赖打进 WEB-INF/lib。这一步是「idea 运行 javaweb 项目配置」里最高频的翻车点很多人代码没问题就卡在 Artifact 没配全。3.3 数据库连接与 MyBatis 映射文件的关键配置如果项目用 MyBatismybatis-config.xml 里要配好 typeAliases 和 mappers。typeAliases 让实体类不用写全限定名mappers 指定 XML 映射文件位置。下面是一个药店管理系统的 MyBatis 核心配置片段。configuration typeAliases package namecom.pharmacy.entity/ /typeAliases mappers mapper resourcemapper/MedicineMapper.xml/ mapper resourcemapper/StockMapper.xml/ mapper resourcemapper/SaleOrderMapper.xml/ /mappers /configuration逻辑说明package 方式批量注册别名实体类名首字母小写就是别名。mappers 用 resource 方式加载 classpath 下的 XML。如果 Mapper 接口和 XML 放在同一包下也可以用 package 方式扫描但要求文件名一致。参数方面MyBatis 的 mapUnderscoreToCamelCase 建议设为 true这样数据库的 generic_name 能自动映射到 Java 的 genericName省掉大量 resultMap 配置。3.4 药品库存增删改查的 Service 层实现要点库存扣减是药店系统里最需要小心的地方。卖药时要按批号先进先出还要防止并发超卖。下面这段 Service 层代码演示了扣减库存的基本逻辑用了简单的 synchronized 锁适合课程设计级别生产环境建议用数据库行锁或者 Redis 分布式锁。Service public class StockService { Autowired private StockMapper stockMapper; // 按批号先进先出扣减库存 public synchronized boolean reduceStock(Integer medicineId, int quantity) { // 查出该药品所有有库存的批号按有效期升序 ListStock stockList stockMapper.selectAvailableByMedicineId(medicineId); int remain quantity; for (Stock stock : stockList) { if (remain 0) break; int deduct Math.min(stock.getQuantity(), remain); stockMapper.updateQuantity(stock.getId(), stock.getQuantity() - deduct); remain - deduct; } return remain 0; } }逻辑说明selectAvailableByMedicineId 对应的 SQL 要加WHERE quantity 0 AND expiry_date CURDATE() ORDER BY expiry_date ASC保证只扣有效库存且先过期先出。updateQuantity 要带条件WHERE id #{id} AND quantity #{deduct}防止并发时扣成负数。参数方面quantity 是本次销售数量remain 是还没扣完的数量循环结束后 remain 为 0 才算成功。如果返回 falseController 层要回滚事务并提示库存不足。4. 避坑与排查药店管理系统跑不起来时先查这 5 个地方4.1 现象启动报 Access denied for user rootlocalhost原因db.properties 里的密码不对或者 MySQL 用户没有远程/本地访问权限。解决先用命令行mysql -uroot -p确认密码能登录如果密码对但还报错检查 MySQL 的 user 表里 root 用户的 host 字段是不是 localhost。有时候用 Docker 跑 MySQL端口映射了但 root 只允许 % 访问本地连接反而被拒。改法ALTER USER rootlocalhost IDENTIFIED BY 新密码;或者新建一个专用用户。4.2 现象页面中文乱码药品名显示问号原因数据库、表、连接 URL、JSP 页面四层编码不一致。解决数据库和表用 utf8mb4连接 URL 加 characterEncodingutf8JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %Tomcat 的 server.xml 里 Connector 加 URIEncodingUTF-8。四层缺一层都可能乱码我一般会从数据库往回查先确认SHOW VARIABLES LIKE character%的结果。4.3 现象MyBatis 报 Invalid bound statement (not found)原因Mapper XML 没被编译到 classpath或者 namespace 和接口全限定名不一致或者方法名对不上。解决检查 target/classes 下有没有对应的 XML 文件如果没有在 pom.xml 的 build 里加 resources 配置把 src/main/java 下的 XML 也打包进去。然后核对 XML 的 namespace 是不是接口的全限定名select 的 id 是不是方法名。这个报错几乎每个 MyBatis 新手都会遇到属于必踩坑。4.4 现象销售时库存扣成负数原因并发扣减没有加锁或者 update 语句没带 quantity deduct 条件。解决Service 层加 synchronized 只能单机有效多节点要用数据库悲观锁SELECT ... FOR UPDATE或者乐观锁版本号。更简单的做法是 update 语句直接写SET quantity quantity - #{deduct} WHERE id #{id} AND quantity #{deduct}根据返回的影响行数判断是否成功。返回 0 就说明库存不足抛异常回滚。4.5 现象近效期预警查不出数据原因expiry_date 存成了字符串或者查询条件用了DATEDIFF(expiry_date, CURDATE()) 30但字段类型是 varchar导致隐式转换失效。解决建表时 expiry_date 必须用 date 类型查询时用expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY)。如果已经存成字符串先用 STR_TO_DATE 转换但最好还是改表结构。这个坑在课程设计里特别常见因为很多人图省事用 varchar 存日期。5. 进阶技巧用数据库同步和 SQL 优化让药店系统更稳5.1 用定时任务做近效期预警和库存同步药店系统跑起来之后真正体现价值的是自动化。我一般会加一个 Spring Task 或者 Quartz 定时任务每天凌晨跑一次近效期扫描把 30 天内过期的批号写进预警表前端登录后弹窗提示。同时可以做一个库存同步逻辑把销售明细汇总回库存表防止手工改库导致数据不一致。下面是一个简单的定时任务示例。Component public class ExpiryWarningTask { Autowired private StockMapper stockMapper; Autowired private WarningMapper warningMapper; // 每天凌晨2点执行 Scheduled(cron 0 0 2 * * ?) public void scanExpiry() { ListStock list stockMapper.selectExpiringSoon(30); for (Stock s : list) { warningMapper.insertWarning(s.getMedicineId(), s.getBatchNo(), s.getExpiryDate()); } } }逻辑说明cron 表达式0 0 2 * * ?表示每天 2 点执行。selectExpiringSoon 的 SQL 用expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL #{days} DAY)。参数 days 传 30可以根据药店实际管理要求调整成 60 或 90。预警表要加唯一键防止重复插入或者每次扫描前先清空当天预警。5.2 销售统计查询的 SQL 优化与索引调整药店老板最关心的是「今天卖了多少钱、哪个药卖得最好、哪个员工业绩高」。这些统计查询如果不加索引数据量到几万条就会明显变慢。我一般会在 sale_order 的 create_time 和 employee_id 上建联合索引在 sale_detail 的 medicine_id 上建索引。统计 SQL 尽量用 BETWEEN 而不是 YEAR()、MONTH() 函数因为函数会导致索引失效。-- 优化前索引失效 SELECT SUM(total_amount) FROM sale_order WHERE YEAR(create_time) 2025 AND MONTH(create_time) 6; -- 优化后走索引 SELECT SUM(total_amount) FROM sale_order WHERE create_time 2025-06-01 00:00:00 AND create_time 2025-07-01 00:00:00;参数说明日期范围用左闭右开避免月底最后一秒的边界问题。如果要做月度报表可以在应用层算好起止时间再传进来。另外统计结果如果实时性要求不高可以每天凌晨跑一次汇总表查询时直接读汇总表响应时间能从秒级降到毫秒级。5.3 数据库备份与结构变更的后悔药药店系统的数据比代码值钱销售流水丢了就是真金白银的损失。我习惯在项目里加一个简单的备份脚本用 mysqldump 每天导出一次保留最近 7 天。结构变更一定要用 SQL 脚本管理不要直接在客户端改表否则换台机器部署时结构对不上。下面是一个备份命令示例。mysqldump -uroot -p密码 --single-transaction --routines --triggers pharmacy_db /backup/pharmacy_$(date %Y%m%d).sql参数说明--single-transaction 保证 InnoDB 表导出时一致性不锁表--routines 导出存储过程和函数--triggers 导出触发器。备份文件按日期命名方便回滚。恢复时用mysql -uroot -p密码 pharmacy_db 备份文件.sql。这个习惯帮我省过好几次事有一次改表结构改错了直接回滚到前一天的数据只损失了几小时流水。5.4 从课程设计到真实可用的最后一步课程设计级别的药店管理系统和真实药店在用的系统差距往往不在功能多少而在边界处理。比如退货怎么退、拆零销售怎么算、医保对接怎么留接口、盘点差异怎么调账。我的建议是先把核心的进销存跑通再逐步加边界功能。每加一个功能先想清楚数据库怎么变、事务边界在哪、异常怎么回滚。这套思路比多写几个页面更有价值。我自己做这类项目时有个习惯每次改完代码先不急着点页面而是把关键 SQL 在数据库里手动跑一遍确认结果对了再联调。这个习惯让我少熬了很多夜。希望帮到你。本文还有配套的精品资源点击获取
返回列表