在医药零售行业,高效精准的仓储与销售管理是保障药品供应、控制运营成本的核心环节。传统依赖手工台账和记忆的管理方式,不仅效率低下,且极易因人为疏忽导致库存数据不准确、药品过期或短缺等问题。针对这一痛点,我们设计并实现了一套基于JSP与Servlet技术的“药联智仓”管理系统,旨在通过数字化手段重塑药店的业务流程。
该系统采用经典的J2EE MVC架构,将业务逻辑、数据与界面呈现分离,确保了代码的良好结构和可维护性。Servlet作为系统的控制器中枢,负责拦截和处理所有客户端请求,进行业务逻辑调度与数据封装;JSP页面则专注于视图渲染,通过JSTL标签库和表达式语言动态展示数据,避免了在页面中混入Java代码;模型层由一系列封装了业务数据和行为的JavaBean构成;底层数据持久化则通过JDBC与MySQL数据库进行交互。这一技术选型成熟稳定,非常适合中小型药店快速部署和低成本运营。
数据库架构设计与核心表解析
数据库是整个系统的基石,其设计的合理性直接决定了系统的性能和数据一致性。“药联智仓”系统共设计了9张核心数据表,以下是其中几个关键表的设计亮点分析。
1. 药品信息表 (medicine)
此表是系统的核心实体,记录了药品的所有基础信息。其设计不仅考虑了通用属性,还针对医药行业的特殊要求进行了字段规划。
CREATE TABLE `medicine` (
`mid` int(11) NOT NULL AUTO_INCREMENT,
`medicineSn` varchar(64) NOT NULL,
`medicineName` varchar(64) NOT NULL,
`medicineSort` int(11) NOT NULL,
`medicineCost` double NOT NULL,
`medicinePrice` double NOT NULL,
`medicineUnit` varchar(32) NOT NULL,
`medicineIntro` varchar(512) DEFAULT NULL,
`medicinePro` varchar(32) NOT NULL,
`medicineProDate` date NOT NULL,
`medicineExpDate` date NOT NULL,
`medicineStock` int(11) NOT NULL,
`medicineStockDanger` int(11) NOT NULL,
`medicineSupplier` varchar(64) NOT NULL,
PRIMARY KEY (`mid`),
UNIQUE KEY `medicineSn` (`medicineSn`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
- 设计亮点:
- 唯一性约束:
medicineSn(药品编码)字段添加了唯一索引,确保了每一种药品在系统中的唯一标识,有效防止重复录入。 - 生命周期管理:专门设计了
medicineProDate(生产日期)和medicineExpDate(有效期至)字段,为后续实现近效期药品预警功能提供了数据支持。 - 库存安全预警:
medicineStock(当前库存)与medicineStockDanger(库存预警线)的搭配设计,使得系统可以轻松实现库存预警逻辑。当medicineStock低于medicineStockDanger时,系统可自动触发提醒,提示管理员进行采购。 - 成本与售价分离:明确区分
medicineCost(成本价)和medicinePrice(售价),便于精确计算毛利和进行财务分析。
- 唯一性约束:
2. 销售订单表 (sale)
该表记录了每一笔销售交易的详细信息,是进行销售统计和业绩分析的关键。
CREATE TABLE `sale` (
`saleId` int(11) NOT NULL AUTO_INCREMENT,
`saleNumber` varchar(64) NOT NULL,
`saleMedicine` int(11) NOT NULL,
`saleCount` int(11) NOT NULL,
`salePrice` double NOT NULL,
`saleTotal` double NOT NULL,
`saleCustomer` varchar(32) NOT NULL,
`saleOperator` varchar(32) NOT NULL,
`saleTime` datetime NOT NULL,
PRIMARY KEY (`saleId`),
UNIQUE KEY `saleNumber` (`saleNumber`),
KEY `saleMedicine` (`saleMedicine`),
CONSTRAINT `sale_ibfk_1` FOREIGN KEY (`saleMedicine`) REFERENCES `medicine` (`mid`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
- 设计亮点:
- 外键约束:通过
saleMedicine字段与medicine表的主键mid建立外键约束,保证了每一条销售记录都对应一个有效的药品,维护了数据的参照完整性。 - 交易可追溯性:记录了
saleOperator(操作员)和saleTime(销售时间),实现了操作的审计追踪。 - 冗余设计以提升性能:虽然可以通过
salePrice * saleCount计算出saleTotal(销售总额),但将其作为独立字段存储是一种合理的冗余。这避免了在需要进行大量统计查询时进行频繁的计算,用空间换取了时间,提升了报表生成的速度。
- 外键约束:通过
3. 采购订单表 (purchase)
与销售表相对应,采购表管理药品的入库流程。
CREATE TABLE `purchase` (
`purchaseId` int(11) NOT NULL AUTO_INCREMENT,
`purchaseNumber` varchar(64) NOT NULL,
`purchaseMedicine` int(11) NOT NULL,
`purchaseCount` int(11) NOT NULL,
`purchasePrice` double NOT NULL,
`purchaseTotal` double NOT NULL,
`purchaseSupplier` varchar(64) NOT NULL,
`purchaseOperator` varchar(32) NOT NULL,
`purchaseTime` datetime NOT NULL,
PRIMARY KEY (`purchaseId`),
UNIQUE KEY `purchaseNumber` (`purchaseNumber`),
KEY `purchaseMedicine` (`purchaseMedicine`),
CONSTRAINT `purchase_ibfk_1` FOREIGN KEY (`purchaseMedicine`) REFERENCES `medicine` (`mid`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
其设计理念与销售表类似,通过外键关联药品,并详细记录采购的各个环节,确保了入库流程的规范性和数据准确性。
核心功能模块深度解析
1. 药品信息综合管理 这是系统最基础也是最重要的模块,实现了对药品档案的全面管理。其核心Servlet控制器负责处理列表查询、新增、修改和删除等请求。
// MedicineListServlet.java 处理药品列表查询与分页
public class MedicineListServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int page = 1;
int limit = 10;
try {
page = Integer.parseInt(request.getParameter("page"));
limit = Integer.parseInt(request.getParameter("limit"));
} catch (NumberFormatException e) {
// 使用默认值
}
MedicineService medicineService = new MedicineService();
Page<Medicine> medicinePage = medicineService.getMedicineList(page, limit);
request.setAttribute("medicinePage", medicinePage);
request.getRequestDispatcher("/admin/medicine_list.jsp").forward(request, response);
}
}
在JSP页面中,使用JSTL循环展示药品列表,并利用EL表达式动态绑定数据。
<!-- medicine_list.jsp 药品列表展示片段 -->
<table class="table">
<thead>
<tr>
<th>药品编码</th>
<th>药品名称</th>
<th>生产厂家</th>
<th>当前库存</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<c:forEach items="${medicinePage.list}" var="medicine">
<tr>
<td>${medicine.medicineSn}</td>
<td>${medicine.medicineName}</td>
<td>${medicine.medicinePro}</td>
<td>
<span class="badge ${medicine.medicineStock lt medicine.medicineStockDanger ? 'badge-danger' : 'badge-success'}">
${medicine.medicineStock}
</span>
</td>
<td>
<a href="medicine_edit?id=${medicine.mid}" class="btn btn-sm btn-primary">编辑</a>
<a href="javascript:void(0)" onclick="confirmDelete(${medicine.mid})" class="btn btn-sm btn-danger">删除</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
图示:药品信息管理界面,清晰展示库存状态(正常/预警),并提供便捷的编辑入口。
2. 库存动态更新与销售出库 销售出库是系统业务流程的关键一环,其核心在于保证“事务性”:即生成销售记录的同时,必须原子性地更新药品库存。这项工作在Service层完成。
// SaleService.java 销售出库服务逻辑
public class SaleService {
public boolean addSale(Sale sale) {
Connection conn = DbUtil.getConnection();
try {
conn.setAutoCommit(false); // 开启事务
// 1. 插入销售记录
String sqlInsertSale = "INSERT INTO sale (saleNumber, saleMedicine, saleCount, salePrice, saleTotal, saleCustomer, saleOperator, saleTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
PreparedStatement pstmtSale = conn.prepareStatement(sqlInsertSale);
// ... 设置参数
pstmtSale.executeUpdate();
// 2. 更新药品库存
String sqlUpdateStock = "UPDATE medicine SET medicineStock = medicineStock - ? WHERE mid = ?";
PreparedStatement pstmtStock = conn.prepareStatement(sqlUpdateStock);
pstmtStock.setInt(1, sale.getSaleCount());
pstmtStock.setInt(2, sale.getSaleMedicine());
int affectedRows = pstmtStock.executeUpdate();
if (affectedRows == 0) {
throw new SQLException("更新库存失败,药品可能不存在。");
}
conn.commit(); // 提交事务
return true;
} catch (SQLException e) {
try {
conn.rollback(); // 回滚事务
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
return false;
} finally {
DbUtil.closeConnection(conn);
}
}
}
图示:销售出库操作界面,需填写客户信息、选择药品及数量,系统自动计算金额。
3. 采购入库与库存统计 采购入库是库存增加的入口,其逻辑与销售出库相反但同样需要事务保证。入库后,系统提供了多维度的统计查询功能。
// PurchaseService.java 采购入库服务逻辑
public class PurchaseService {
public boolean addPurchase(Purchase purchase) {
Connection conn = DbUtil.getConnection();
try {
conn.setAutoCommit(false);
// 1. 插入采购记录
String sqlInsert = "INSERT INTO purchase (purchaseNumber, purchaseMedicine, purchaseCount, purchasePrice, purchaseTotal, purchaseSupplier, purchaseOperator, purchaseTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
// ... 参数设置与执行
// 2. 增加药品库存
String sqlUpdate = "UPDATE medicine SET medicineStock = medicineStock + ? WHERE mid = ?";
// ... 参数设置与执行
conn.commit();
return true;
} catch (Exception e) {
// ... 异常处理与回滚
} finally {
DbUtil.closeConnection(conn);
}
}
// 统计某时间段内的入库总量
public double getTotalInbound(String startDate, String endDate) {
String sql = "SELECT SUM(purchaseTotal) FROM purchase WHERE purchaseTime BETWEEN ? AND ?";
// ... 执行查询并返回结果
}
}
图示:入库统计报表,可按时间范围筛选,直观展示采购金额趋势。
4. 实体模型(JavaBean) 模型层实体类严格遵循JavaBean规范,封装数据属性及其getter/setter方法,是MVC模型中数据流转的载体。
// Medicine.java 药品实体类
public class Medicine {
private int mid;
private String medicineSn;
private String medicineName;
private int medicineSort;
private double medicineCost;
private double medicinePrice;
private String medicineUnit;
private String medicineIntro;
private String medicinePro;
private Date medicineProDate;
private Date medicineExpDate;
private int medicineStock;
private int medicineStockDanger;
private String medicineSupplier;
// 无参构造器
public Medicine() {}
// 全参构造器
public Medicine(int mid, String medicineSn, String medicineName, ...) {
this.mid = mid;
this.medicineSn = medicineSn;
this.medicineName = medicineName;
// ... 其他属性赋值
}
// Getter and Setter 方法
public int getMid() { return mid; }
public void setMid(int mid) { this.mid = mid; }
public String getMedicineSn() { return medicineSn; }
public void setMedicineSn(String medicineSn) { this.medicineSn = medicineSn; }
// ... 其他属性的Getter和Setter
}
// Sale.java 销售订单实体类
public class Sale {
private int saleId;
private String saleNumber;
private int saleMedicine;
private int saleCount;
private double salePrice;
private double saleTotal;
private String saleCustomer;
private String saleOperator;
private Date saleTime;
// ... 构造器、Getter和Setter方法
}
图示:库存查询界面,支持按药品名称、编码等多条件筛选,实时反映库存健康状况。
功能展望与系统优化方向
尽管“药联智仓”系统已实现了核心的仓储与销售管理功能,但在实际企业级应用中仍有广阔的优化和扩展空间。
- 引入连接池与ORM框架:目前使用基础的JDBC操作数据库。未来可集成Druid等数据库连接池,有效管理数据库连接,提升系统性能。同时,可以考虑引入MyBatis或Hibernate等ORM框架,简化数据库操作代码,提高开发效率和数据访问层的可维护性。
- 实现分布式会话管理:当前系统可能依赖Servlet原生HttpSession。在需要横向扩展(如部署多台服务器)时,可将会话数据存储到Redis等分布式缓存中,实现应用服务器的无状态化,增强系统的伸缩性和高可用性。
- 构建RESTful API接口:为适应移动端应用(如店员APP、小程序)或与其他系统(如ERP、财务软件)集成,可将核心业务模块重构为RESTful API接口,实现前后端分离架构。
- 增强数据分析与BI看板:在现有统计功能基础上,引入ECharts等可视化库,构建管理者驾驶舱(Dashboard),提供销售趋势分析、毛利分析、库存周转率、畅销/滞销药品排名等深度商业智能分析。
- 完善权限控制模型:从简单的角色功能控制,升级为基于RBAC的精细化权限管理体系,支持功能权限、数据权限(如不同门店管理员只能管理本店数据)的动态配置,满足连锁药店的复杂管理需求。
该系统通过严谨的MVC架构和数据库设计,成功地将药品管理的核心业务流程数字化、自动化,为药店运营者提供了一个稳定、高效的管理工具。其清晰的代码结构为后续的功能增强和技术演进奠定了坚实的基础。随着技术的不断迭代,“药联智仓”有望发展成为更智能、更集成化的医药新零售解决方案。