在医药零售与小型医疗机构日常运营中,药品管理始终是核心且复杂的环节。传统依赖手工台账的记录方式不仅效率低下,极易出现人为差错,更导致库存信息更新滞后,管理者难以实时掌握药品的进、销、存动态,从而引发药品积压浪费或临床短缺风险。为解决这一系列痛点,我们设计并实现了一套基于SpringBoot的现代化药品库存管理解决方案,该系统通过数字化的流程整合,为管理者提供了精准、实时、可视化的库存控制能力。
该系统采用经典的MVC分层架构,以SpringBoot为核心框架,极大地简化了项目的初始配置与部署流程。SpringBoot内嵌的Tomcat服务器使得应用可以打包成独立的JAR文件运行,无需依赖外部Servlet容器。在数据持久化层面,系统选择了稳定高效的MySQL数据库,并利用JPA(Java Persistence API)作为ORM框架,通过简洁的Repository接口定义,实现了对数据库的快速访问和操作,有效降低了SQL编写的复杂度与错误率。前端页面采用HTML、CSS和JavaScript构建,确保了界面的直观易用。整个项目由Maven进行依赖管理,保证了第三方库版本的一致性与项目构建的可重复性。
数据库架构设计与核心表分析
系统的数据模型由12张核心表构成,涵盖了药品、库存、供应商、客户、员工、采购、销售等所有业务实体。其设计遵循数据库第三范式,以减少数据冗余,并建立了完善的主外键约束以保障数据的引用完整性与一致性。
1. 药品信息表(medicine): 基础主数据表
此表是系统的基石,存储了所有药品的静态属性。其设计不仅包含了药品的基本标识(名称、编号),还通过分类ID(category_id)关联到药品分类表,实现了数据的层次化组织。特别值得注意的是对药品关键业务属性的定义,如purchase_price(采购价)、selling_price(销售价)和stock(库存数量),这些字段是后续进行采购、销售和利润核算的核心。status字段则用于软删除或标记药品状态,避免了物理删除可能造成的数据丢失。
CREATE TABLE `medicine` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`medicine_no` varchar(255) DEFAULT NULL COMMENT '药品编号',
`name` varchar(255) DEFAULT NULL COMMENT '药品名称',
`supplier_id` int(11) DEFAULT NULL COMMENT '供应商id',
`category_id` int(11) DEFAULT NULL COMMENT '分类id',
`purchase_price` decimal(10,2) DEFAULT NULL COMMENT '采购价',
`selling_price` decimal(10,2) DEFAULT NULL COMMENT '售价',
`stock` int(11) DEFAULT NULL COMMENT '库存',
`status` int(11) DEFAULT '1' COMMENT '0下架1上架',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='药品信息';
2. 销售单表(sale_info): 核心业务流水表
该表记录了每一笔销售交易的详细信息,是财务对账和销售分析的数据来源。其设计体现了业务操作的原子性:每笔销售单通过customer_id和employee_id分别关联到客户和操作员,明确了责任主体。sale_price(实收金额)和selling_price_sum(应收金额)的分离设计,为后续支持折扣、优惠等营销活动预留了扩展空间。status字段可用于跟踪订单状态(如待支付、已完成、已退款),sale_date则提供了时间维度的分析能力。
CREATE TABLE `sale_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`sale_no` varchar(255) DEFAULT NULL COMMENT '销售单号',
`medicine_id` int(11) DEFAULT NULL COMMENT '药品id',
`customer_id` int(11) DEFAULT NULL COMMENT '客户id',
`employee_id` int(11) DEFAULT NULL COMMENT '操作员id',
`sale_number` int(11) DEFAULT NULL COMMENT '销售数量',
`sale_date` datetime DEFAULT NULL COMMENT '销售日期',
`selling_price` decimal(10,2) DEFAULT NULL COMMENT '药品售价',
`selling_price_sum` decimal(10,2) DEFAULT NULL COMMENT '应收金额',
`sale_price` decimal(10,2) DEFAULT NULL COMMENT '实收金额',
`status` int(11) DEFAULT '1' COMMENT '0退货1正常',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='销售信息';
3. 采购信息表(procurement_info): 库存流入记录表
与销售单相对应,此表管理药品的入库流程。它通过supplier_id关联供应商,记录了采购单价(purchase_price)、数量(procurement_number)和总金额(purchase_price_sum)。procurement_date(采购日期)和status(状态)共同构成了采购订单的生命周期管理。该表与medicine表中的stock字段通过业务逻辑紧密关联,任何一笔采购入库操作都会实时更新对应药品的库存总量。
核心功能模块深度解析
1. 药品信息综合管理
此模块负责维护药品主数据,是系统所有流转操作的基础。管理员可以执行增、删、改、查等全套CRUD操作。新增药品时,系统会强制校验编号(medicine_no)的唯一性,并自动生成创建时间。查询功能支持多条件筛选,如按药品名称、分类或供应商进行快速检索,界面清晰展示了药品的当前库存与价格信息。

后端服务层通过MedicineService接口封装了核心业务逻辑,其实现类确保了数据操作的稳定性和事务性。
@Service
public class MedicineServiceImpl implements MedicineService {
@Autowired
private MedicineRepository medicineRepository;
@Override
@Transactional
public Medicine saveMedicine(Medicine medicine) {
// 设置默认状态和创建时间
if (medicine.getStatus() == null) {
medicine.setStatus(1);
}
if (medicine.getCreateTime() == null) {
medicine.setCreateTime(new Date());
}
medicine.setUpdateTime(new Date());
// 保存到数据库
return medicineRepository.save(medicine);
}
@Override
public Page<Medicine> findMedicinesWithPagination(String keyword, Pageable pageable) {
// 构建查询条件,支持按名称或编号模糊查询
Specification<Medicine> spec = (root, query, cb) -> {
if (keyword != null && !keyword.trim().isEmpty()) {
String pattern = "%" + keyword + "%";
return cb.or(
cb.like(root.get("name"), pattern),
cb.like(root.get("medicineNo"), pattern)
);
}
return cb.conjunction();
};
return medicineRepository.findAll(spec, pageable);
}
}
2. 采购入库与库存更新流程
采购管理模块实现了从创建订单到库存更新的完整闭环。员工创建采购单时,选择供应商和药品,填写采购数量和单价,系统会自动计算总金额。一旦采购单被确认提交,系统会通过一个事务性操作完成两项关键任务:首先在procurement_info表中插入新的采购记录,然后更新medicine表中对应药品的stock字段,确保库存数据的实时准确性。

库存更新的逻辑在服务层实现,保证了数据的一致性。
@Service
public class ProcurementServiceImpl implements ProcurementService {
@Autowired
private ProcurementRepository procurementRepository;
@Autowired
private MedicineRepository medicineRepository;
@Override
@Transactional // 声明式事务管理,确保两个操作同时成功或失败
public ProcurementInfo createProcurement(ProcurementInfo procurementInfo) {
// 1. 设置采购单基本信息
procurementInfo.setProcurementDate(new Date());
procurementInfo.setCreateTime(new Date());
procurementInfo.setStatus(1); // 标记为正常采购单
// 2. 保存采购记录
ProcurementInfo savedProcurement = procurementRepository.save(procurementInfo);
// 3. 更新药品库存
Medicine medicine = medicineRepository.findById(procurementInfo.getMedicineId())
.orElseThrow(() -> new RuntimeException("药品不存在"));
Integer newStock = medicine.getStock() + procurementInfo.getProcurementNumber();
medicine.setStock(newStock);
medicine.setUpdateTime(new Date());
medicineRepository.save(medicine);
return savedProcurement;
}
}
3. 销售出库与收银操作 销售模块是系统与客户交互的直接界面。在收银时,员工选择客户和要购买的药品,输入销售数量,系统会立即校验当前库存是否充足。校验通过后,界面会自动显示应收金额,员工录入实收金额后即可完成交易。此操作同样在一个事务内完成:生成销售单记录并扣减相应库存。

销售服务的核心代码如下,其中包含了关键的库存校验逻辑。
@Service
public class SaleInfoServiceImpl implements SaleInfoService {
@Autowired
private SaleInfoRepository saleInfoRepository;
@Autowired
private MedicineRepository medicineRepository;
@Override
@Transactional
public SaleInfo createSale(SaleInfo saleInfo) {
// 库存校验
Medicine medicine = medicineRepository.findById(saleInfo.getMedicineId())
.orElseThrow(() -> new RuntimeException("药品不存在"));
if (medicine.getStock() < saleInfo.getSaleNumber()) {
throw new RuntimeException("库存不足,当前库存:" + medicine.getStock());
}
// 设置销售单信息
saleInfo.setSaleDate(new Date());
saleInfo.setCreateTime(new Date());
saleInfo.setStatus(1);
// 计算应收金额
BigDecimal sellingPriceSum = medicine.getSellingPrice()
.multiply(BigDecimal.valueOf(saleInfo.getSaleNumber()));
saleInfo.setSellingPriceSum(sellingPriceSum);
// 保存销售记录
SaleInfo savedSale = saleInfoRepository.save(saleInfo);
// 扣减库存
Integer newStock = medicine.getStock() - saleInfo.getSaleNumber();
medicine.setStock(newStock);
medicine.setUpdateTime(new Date());
medicineRepository.save(medicine);
return savedSale;
}
}
4. 多角色权限与交互界面 系统设计了员工和客户两种角色,拥有不同的操作权限和视图。员工登录后可以管理药品、处理采购销售、查看报表。客户登录后则可以查询药品目录、查看购买记录、提交用药反馈或咨询。


角色权限的控制通过在Controller层进行拦截实现。
@RestController
@RequestMapping("/api/medicine")
public class MedicineController {
@GetMapping("/public/list")
public ResponseEntity<Page<Medicine>> getMedicineListForCustomer(Pageable pageable) {
// 客户可访问的药品列表接口,可能只返回上架药品
Page<Medicine> medicines = medicineService.findAvailableMedicines(pageable);
return ResponseEntity.ok(medicines);
}
@PostMapping("/admin")
@PreAuthorize("hasRole('EMPLOYEE')") // 权限注解,只有员工角色可访问
public ResponseEntity<Medicine> createMedicine(@RequestBody Medicine medicine) {
Medicine savedMedicine = medicineService.saveMedicine(medicine);
return ResponseEntity.ok(savedMedicine);
}
}
实体模型与数据映射
系统利用JPA的实体映射功能,将数据库表结构映射为Java对象,简化了数据操作。以下以Medicine实体为例,展示了如何使用JPA注解进行对象-关系映射。
@Entity
@Table(name = "medicine")
@Data
public class Medicine {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "medicine_no", unique = true)
private String medicineNo;
@Column(name = "name")
private String name;
@Column(name = "purchase_price", precision = 10, scale = 2)
private BigDecimal purchasePrice;
@Column(name = "selling_price", precision = 10, scale = 2)
private BigDecimal sellingPrice;
@Column(name = "stock")
private Integer stock;
@Column(name = "status")
private Integer status;
@Column(name = "create_time")
@CreationTimestamp
private Date createTime;
@Column(name = "update_time")
@UpdateTimestamp
private Date updateTime;
// 关联关系
@ManyToOne
@JoinColumn(name = "supplier_id")
private Supplier supplier;
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
}
数据访问层通过继承JpaRepository接口,无需编写实现代码即可获得强大的数据访问能力。
@Repository
public interface MedicineRepository extends JpaRepository<Medicine, Integer>, JpaSpecification<Medicine> {
// 根据编号查找药品
Optional<Medicine> findByMedicineNo(String medicineNo);
// 查找所有上架的药品
List<Medicine> findByStatus(Integer status);
}
系统优化与未来展望
尽管当前系统已能满足基本的药品进销存管理需求,但在实际生产环境中仍有多个维度可以深化和扩展。
库存预警与智能补货建议:系统可以增加库存预警阈值设置。当某种药品的库存量低于设定阈值时,自动触发预警通知(如站内信、邮件或短信)。更进一步,可以基于历史销售数据,构建时间序列预测模型,自动生成智能补货建议单,提示采购品种和数量。
- 实现思路:在
medicine表中增加min_stock(最低库存)和max_stock(最高库存)字段。开发一个定时任务(使用Spring的@Scheduled注解),定期扫描库存,生成预警列表。智能补货则可集成轻量级的机器学习库,如Weka或使用SQL进行移动平均计算。
- 实现思路:在
序列号与批次管理(GSP合规):为了满足《药品经营质量管理规范》(GSP)的要求,需要对药品进行更精细化的批次和有效期管理。
- 实现思路:重构库存模型。引入
batch(批次)表,记录药品的批号、生产日期、有效期至。stock不再直接存在于medicine表,而是作为batch表的一个字段。销售和采购时,需要指定具体的批次(可采用FIFO先进先出策略)。需在UI上增加批次选择界面和有效期预警功能。
- 实现思路:重构库存模型。引入
数据可视化与分析报表:当前系统主要以表格形式展示数据。增加丰富的图表化报表将极大提升数据的可读性和决策支持价值。
- 实现思路:集成图表库如ECharts或Chart.js。开发专门的报表Controller,提供JSON格式的统计数据(如月度销售额趋势、药品销量Top10、毛利率分析等)。前端通过AJAX请求数据并渲染成直观的折线图、柱状图、饼图。
系统集成与API开放:考虑与医保支付系统、电子发票平台或线上商城进行对接。
- 实现思路:将核心业务服务抽象为RESTful API,并完善API文档(使用Swagger/OpenAPI)。实现安全的认证授权机制(如OAuth 2.0)。这将使系统从一个孤立的信息系统转变为可融入更大数字化生态的业务中台。
性能优化与缓存策略:随着数据量的增长,频繁查询的静态数据(如药品分类、供应商列表)会成为性能瓶颈。
- 实现思路:引入Redis作为缓存层。对
CategoryService、SupplierService等查询方法使用Spring Cache注解(如@Cacheable)进行缓存,显著减少数据库访问次数。对于复杂的统计查询,可以考虑使用数据库的物化视图或单独的数据分析库。
- 实现思路:引入Redis作为缓存层。对
该药品库存管理系统的设计与实现,体现了现代企业级应用开发中对业务完整性、数据一致性和用户体验的综合考量。其清晰的架构、严谨的数据模型和模块化的代码,为在中小型医药流通领域快速部署一个稳定、可靠的数字化管理工具奠定了坚实的技术基础。未来的优化方向将紧紧围绕业务合规、数据智能和系统生态三个维度展开,持续提升系统的价值。