1. 项目背景与需求分析
药店作为医疗健康服务的重要终端,其管理水平直接影响药品流通效率和患者用药安全。传统药店管理普遍存在以下痛点:
- 手工记录效率低下:药品入库、销售、盘点等环节依赖纸质记录,易出现数据丢失或错误
- 库存管理粗放:难以及时掌握库存动态,常发生断货或过期药品积压
- 销售分析缺失:缺乏数据支撑,无法精准把握热销药品和滞销品
- 处方管理混乱:处方药销售记录不规范,存在合规风险
本系统针对中小型药店设计,核心解决四大业务场景:
- 药品全生命周期管理(采购→入库→销售→效期监控)
- 实时库存预警与智能补货建议
- 多维度销售统计分析
- 处方药电子化审核流程
关键设计原则:操作界面简化(降低培训成本)、数据可视化(辅助决策)、权限精细化控制(保障数据安全)
2. 技术架构设计
2.1 整体架构方案
采用前后端分离架构,技术栈选型考量:
后端技术栈:
- Spring Boot 2.7:简化配置、快速启动(内嵌Tomcat)、自动依赖管理
- MyBatis-Plus 3.5:增强CRUD操作、动态SQL构建、分页插件
- MySQL 8.0:事务支持完善、JSON数据类型处理药品复杂属性
- Redis 6:缓存热点数据(如药品目录)、分布式锁控制库存并发
前端技术栈:
- Vue 3 + Composition API:响应式数据绑定、逻辑复用性强
- Element Plus:表单验证、表格分页等组件开箱即用
- ECharts 5:销售趋势、库存占比等可视化图表
- Vue Router + Pinia:路由守卫控制权限、集中式状态管理
2.2 数据库设计要点
药品基础表(drug_info)
sql复制CREATE TABLE `drug_info` (
`drug_code` VARCHAR(20) PRIMARY KEY COMMENT '药品编码规则:类别首字母+6位数字(如RX000001)',
`drug_name` VARCHAR(50) NOT NULL COMMENT '需关联国家药品编码库',
`spec` VARCHAR(30) NOT NULL COMMENT '格式:含量-剂型-包装(如10mg*24片/盒)',
`manufacturer` VARCHAR(60) INDEX COMMENT '建立厂商索引便于统计',
`category` ENUM('Rx','OTC','医疗器械') NOT NULL,
`stock_alert` INT DEFAULT 10 COMMENT '可配置不同药品的独立预警值',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
库存变更日志表(stock_log)
sql复制CREATE TABLE `stock_log` (
`log_id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`drug_code` VARCHAR(20) NOT NULL,
`change_type` ENUM('purchase','sale','return','adjust') NOT NULL,
`quantity` INT NOT NULL COMMENT '正数表示入库,负数表示出库',
`before_stock` INT NOT NULL,
`operator` VARCHAR(20) NOT NULL,
`related_order` VARCHAR(32) COMMENT '关联订单号',
`record_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`drug_code`) REFERENCES `drug_info`(`drug_code`)
) ENGINE=InnoDB;
设计技巧:通过触发器自动维护库存实时数量,避免应用层计算错误
sql复制DELIMITER //
CREATE TRIGGER after_stock_update
AFTER INSERT ON stock_log FOR EACH ROW
BEGIN
UPDATE drug_info
SET current_stock = current_stock + NEW.quantity
WHERE drug_code = NEW.drug_code;
END//
DELIMITER ;
3. 核心功能实现
3.1 药品库存管理
关键业务流程:
- 采购入库:扫描药品条形码自动填充基础信息
- 库存预警:定时任务检查库存量低于阈值药品
- 效期管理:近效期药品优先展示并提醒
代码示例 - 库存检查服务:
java复制@Service
@Slf4j
public class StockAlertService {
@Autowired
private DrugInfoMapper drugInfoMapper;
@Scheduled(cron = "0 0 9,17 * * ?") // 每天9点和17点执行
public void checkLowStock() {
List<DrugInfo> lowStockDrugs = drugInfoMapper.selectList(
new QueryWrapper<DrugInfo>()
.select("drug_code", "drug_name", "current_stock", "stock_alert")
.lt("current_stock", "stock_alert")
);
if (!lowStockDrugs.isEmpty()) {
String message = lowStockDrugs.stream()
.map(d -> d.getDrugName() + "剩余" + d.getCurrentStock())
.collect(Collectors.joining(","));
log.warn("库存预警:{}", message);
// 调用消息推送服务
}
}
}
3.2 销售管理模块
订单处理流程:
mermaid复制graph TD
A[创建订单] --> B{处方药?}
B -->|是| C[药师审核]
B -->|否| D[直接结算]
C -->|通过| D
C -->|拒绝| E[终止交易]
D --> F[库存扣减]
F --> G[生成销售记录]
并发控制方案:
java复制@Transactional
public Result submitOrder(OrderDTO orderDTO) {
// 使用Redis分布式锁防止超卖
String lockKey = "drug_stock:" + orderDTO.getDrugCode();
try {
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请重试");
}
// 检查库存
DrugInfo drug = drugInfoMapper.selectById(orderDTO.getDrugCode());
if (drug.getCurrentStock() < orderDTO.getQuantity()) {
throw new BusinessException("库存不足");
}
// 扣减库存
drugInfoMapper.updateStock(
orderDTO.getDrugCode(),
-orderDTO.getQuantity()
);
// 记录订单(省略其他逻辑)
return Result.success();
} finally {
redisTemplate.delete(lockKey);
}
}
4. 系统安全与优化
4.1 权限控制设计
RBAC模型实现:
java复制@PreAuthorize("hasRole('PHARMACIST') or hasRole('ADMIN')")
@PostMapping("/prescription/approve")
public Result approvePrescription(@RequestBody ApproveVO vo) {
// 处方审核逻辑
}
// 动态权限配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/drug/**").hasAnyRole("STAFF", "PHARMACIST", "ADMIN")
.antMatchers("/report/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()));
}
4.2 性能优化措施
-
缓存策略:
- 药品基础信息:Redis缓存 + 本地Caffeine二级缓存
- 使用@Cacheable注解简化缓存逻辑:
java复制@Cacheable(value = "drugInfo", key = "#drugCode") public DrugInfo getByCode(String drugCode) { return drugInfoMapper.selectById(drugCode); } -
SQL优化:
- 为高频查询字段建立复合索引
- 使用MyBatis-Plus性能分析插件拦截慢SQL
yaml复制mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl performance: max-time: 1000 format: true
5. 部署与运维
5.1 容器化部署方案
Docker Compose配置示例:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
ports:
- "3306:3306"
redis:
image: redis:6-alpine
ports:
- "6379:6379"
backend:
build: ./pharmacy-backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./pharmacy-frontend
ports:
- "80:80"
5.2 监控配置
-
Spring Boot Actuator:
yaml复制management: endpoints: web: exposure: include: health,metrics,prometheus metrics: export: prometheus: enabled: true -
Grafana监控看板:
- JVM内存使用
- 接口响应时间P99
- 数据库连接池状态
- Redis命中率
6. 开发经验与避坑指南
-
MyBatis-Plus使用技巧:
- 避免N+1查询问题:
java复制// 错误示范 List<Order> orders = orderMapper.selectList(null); orders.forEach(o -> { User user = userMapper.selectById(o.getUserId()); // 循环查询 }); // 正确做法 List<Order> orders = orderMapper.selectList( new QueryWrapper<Order>().in("id", orderIds) ); Map<Long, User> userMap = userMapper.selectBatchIds( orders.stream().map(Order::getUserId).collect(Collectors.toList()) ).stream().collect(Collectors.toMap(User::getId, u -> u)); -
Vue3性能优化:
- 对于大型表格使用虚拟滚动:
vue复制<el-table-v2 :columns="columns" :data="drugList" :width="1000" :height="500" :row-height="50" fixed /> -
常见问题排查:
- 跨域问题:确保后端配置正确的CORS策略
java复制@Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:8080") .allowedMethods("*"); } }; }- 日期序列化:统一前后端日期格式
yaml复制spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8
7. 项目扩展方向
-
移动端适配:
- 使用Uniapp打包生成微信小程序
- 实现扫码购药、电子处方预览功能
-
智能分析增强:
- 基于历史销售数据预测补货量
python复制# 示例:使用Prophet进行销量预测 from prophet import Prophet model = Prophet(seasonality_mode='multiplicative') model.fit(df) # df包含ds(日期)和y(销量)列 future = model.make_future_dataframe(periods=30) forecast = model.predict(future) -
第三方对接:
- 医保支付接口集成
- 电子监管码扫码验证
系统在实际部署中需特别注意药品数据准确性,建议定期与药监平台数据比对。对于处方药管理模块,务必保留完整的操作日志以满足合规审计要求。