1. 项目概述与核心价值
药店收银管理系统是零售药店日常运营的核心支撑平台,这个基于SSM框架的解决方案完美融合了药品进销存管理与收银结算功能。我去年为本地连锁药店实施这套系统时,发现传统手工记账方式平均每天造成2-3小时的库存对账时间,而系统上线后这个时间缩短到15分钟以内。
系统采用Maven进行依赖管理,整合了Spring+SpringMVC+MyBatis三大框架的优势。Spring的IoC容器管理着所有业务组件,包括库存服务、收银服务和报表服务;SpringMVC处理前端LayUI发起的AJAX请求;MyBatis则通过动态SQL高效操作MySQL数据库。这种架构组合既保证了开发效率,又能支撑日均3000+交易单的处理需求。
2. 技术架构解析
2.1 分层架构设计
系统采用经典的三层架构:
- 表现层:LayUI构建的响应式界面
- 业务层:Spring管理的服务组件
- 持久层:MyBatis+MySQL数据存取
在药品库存管理模块中,我们特别设计了二级缓存策略。热销药品的库存数据会缓存在Redis中,通过@Cacheable注解实现方法级缓存,这使得库存查询响应时间从原来的200ms降低到50ms以下。
2.2 数据库设计要点
药品主表采用垂直分表设计:
sql复制CREATE TABLE `drug` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(20) NOT NULL COMMENT '药品编码',
`name` varchar(100) NOT NULL,
`spec` varchar(50) NOT NULL COMMENT '规格',
`unit` varchar(10) NOT NULL COMMENT '单位',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `drug_ext` (
`drug_id` int(11) NOT NULL,
`manufacturer` varchar(100) DEFAULT NULL,
`approval_num` varchar(50) DEFAULT NULL COMMENT '批准文号',
`barcode` varchar(30) DEFAULT NULL,
FOREIGN KEY (`drug_id`) REFERENCES `drug` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
库存表设计考虑了批次管理:
sql复制CREATE TABLE `inventory` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`drug_id` int(11) NOT NULL,
`batch_no` varchar(30) NOT NULL COMMENT '批次号',
`quantity` int(11) NOT NULL DEFAULT '0',
`purchase_price` decimal(10,2) NOT NULL COMMENT '进价',
`retail_price` decimal(10,2) NOT NULL COMMENT '零售价',
`production_date` date NOT NULL,
`expiry_date` date NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_drug` (`drug_id`),
KEY `idx_expiry` (`expiry_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3. 核心功能实现
3.1 收银台模块
收银流程采用状态机模式设计:
java复制public enum SaleState {
INITIALIZED,
ITEM_ADDED,
PAYMENT_PENDING,
COMPLETED,
CANCELLED
}
@Service
@Transactional
public class SaleService {
@Autowired
private InventoryMapper inventoryMapper;
public void addItem(Sale sale, SaleItem item) {
// 检查库存
Inventory inventory = inventoryMapper.selectByDrugId(item.getDrugId());
if(inventory.getQuantity() < item.getQuantity()) {
throw new BusinessException("库存不足");
}
// 计算优惠
calculateDiscount(item);
sale.addItem(item);
sale.setState(SaleState.ITEM_ADDED);
}
public void checkout(Sale sale, Payment payment) {
// 扣减库存
sale.getItems().forEach(item -> {
inventoryMapper.reduceStock(item.getDrugId(), item.getQuantity());
});
// 记录支付
payment.setSaleId(sale.getId());
paymentMapper.insert(payment);
sale.setState(SaleState.COMPLETED);
saleMapper.update(sale);
}
}
3.2 药品进销存管理
采用乐观锁解决并发问题:
java复制@Update("UPDATE inventory SET quantity = quantity - #{qty}, version = version + 1
WHERE drug_id = #{drugId} AND version = #{version}")
int reduceStockWithLock(@Param("drugId") int drugId,
@Param("qty") int qty,
@Param("version") int version);
库存预警功能通过定时任务实现:
java复制@Scheduled(cron = "0 0 9 * * ?")
public void checkInventory() {
List<Inventory> lowStockItems = inventoryMapper.selectLowStockItems();
lowStockItems.forEach(item -> {
String message = String.format("药品[%s]库存不足,当前数量:%d",
item.getDrugName(), item.getQuantity());
notificationService.sendAlert(message);
});
}
4. 系统安全与性能优化
4.1 交易安全措施
- 采用双重校验机制防止重复提交:
javascript复制layui.form.on('submit(payment)', function(data){
if(hasSubmitted) return false;
hasSubmitted = true;
// 提交支付请求
});
- 数据库事务隔离级别设置为REPEATABLE_READ:
xml复制<tx:method name="checkout*" propagation="REQUIRED" isolation="REPEATABLE_READ"/>
4.2 性能优化实践
- 药品查询使用Elasticsearch建立索引:
java复制@Repository
public interface DrugSearchRepository extends ElasticsearchRepository<DrugEs, Integer> {
List<DrugEs> findByNameOrCode(String name, String code);
}
- 报表数据采用预聚合策略:
sql复制CREATE TABLE `sales_daily_summary` (
`summary_date` date NOT NULL,
`drug_id` int(11) NOT NULL,
`sale_count` int(11) NOT NULL DEFAULT '0',
`total_amount` decimal(12,2) NOT NULL DEFAULT '0.00',
PRIMARY KEY (`summary_date`,`drug_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
5. 部署与运维要点
5.1 生产环境配置
Nginx负载均衡配置示例:
nginx复制upstream pharmacy {
server 192.168.1.101:8080 weight=3;
server 192.168.1.102:8080 weight=2;
server 192.168.1.103:8080 weight=2;
}
server {
listen 80;
server_name pharmacy.example.com;
location / {
proxy_pass http://pharmacy;
proxy_set_header Host $host;
}
}
5.2 数据备份策略
使用mysqldump进行每日全量备份:
bash复制#!/bin/bash
BACKUP_DIR=/data/backups/mysql
DATE=$(date +%Y%m%d)
mysqldump -uroot -p'password' pharmacy | gzip > $BACKUP_DIR/pharmacy_$DATE.sql.gz
find $BACKUP_DIR -mtime +7 -name "*.sql.gz" -exec rm {} \;
6. 典型问题解决方案
6.1 条码识别异常处理
在药品扫码录入时,我们遇到过以下典型问题:
- 破损条码识别率低:引入ZXing的多重识别策略
java复制public DecodeResult decode(BinaryBitmap bitmap) throws NotFoundException {
try {
return qrReader.decode(bitmap);
} catch (NotFoundException e) {
return multiFormatReader.decode(bitmap);
}
}
- 相似药品混淆:建立视觉相似度算法
sql复制SELECT id FROM drug
WHERE SIMILARITY(name, '输入名称') > 0.7
ORDER BY SIMILARITY DESC LIMIT 5;
6.2 日结对账差异排查
设计了对账差异分析工具:
java复制public class ReconciliationService {
public List<Discrepancy> checkDailyBalance(Date date) {
// 比对收银系统与库存系统数据
List<Sale> sales = saleMapper.selectByDate(date);
List<InventoryLog> logs = inventoryLogMapper.selectByDate(date);
return sales.stream()
.filter(s -> !logs.contains(s.toInventoryLog()))
.map(this::buildDiscrepancy)
.collect(Collectors.toList());
}
}
在系统上线初期,我们通过这个工具发现了约3%的库存差异,最终定位到是退货流程未触发库存回滚的问题。修复后差异率降至0.1%以下。