1. 项目背景与核心需求
在线药店管理系统是医药行业数字化转型的关键基础设施。随着医药电商市场规模突破2000亿元,传统药店面临三大痛点:库存管理混乱导致近效期药品损耗率高达15%、处方药销售合规性难以追溯、线上线下业务割裂造成客户流失率超过30%。
这个基于MVC框架的系统设计,正是为了解决这些行业痛点。系统需要实现药品全生命周期管理、电子处方审核流转、智能库存预警、多终端订单同步等核心功能。选择MVC架构不仅因为它能清晰分离业务逻辑与展示层,更重要的是其组件化特性适合应对医药行业频繁的政策合规性更新——据统计,2023年医药电商领域法规变动达47次,模块化的MVC架构可使合规调整成本降低60%。
2. 技术栈选型与架构设计
2.1 为什么选择Spring Boot + Thymeleaf组合
在技术选型阶段,我们对比了三种主流方案:
- Spring Boot + JSP:开发效率高但维护性差
- Spring Boot + Vue.js:前后端分离但SEO不友好
- Spring Boot + Thymeleaf:兼具开发效率与搜索引擎优化
最终选择Spring Boot 2.7 + Thymeleaf 3.1的组合,主要基于三个实际考量:
- 医药产品的长尾搜索流量占比35%,需要天然支持SEO的模板引擎
- Thymeleaf的静态模板特性比JSP节省40%服务器资源
- Spring Boot的自动装配机制让合规性检查模块可以快速插拔
2.2 数据库设计的特殊考量
药品管理涉及多个特殊数据维度:
sql复制CREATE TABLE `medicine` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL COMMENT '通用名',
`spec` VARCHAR(50) NOT NULL COMMENT '规格',
`approval_no` VARCHAR(50) UNIQUE COMMENT '批准文号',
`category` ENUM('处方药','OTC','医疗器械') NOT NULL,
`require_prescription` BOOLEAN DEFAULT FALSE,
`cold_chain` BOOLEAN DEFAULT FALSE COMMENT '是否需要冷链',
`valid_months` SMALLINT COMMENT '有效期(月)',
`manufacturer` VARCHAR(200) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
特别注意:
- 使用utf8mb4字符集支持特殊药品名称(如中药名"䓬薴")
- 建立联合索引 (category, require_prescription) 加速处方药筛选
- 为近效期药品设计专用视图:
sql复制CREATE VIEW expiring_medicines AS
SELECT * FROM medicine
WHERE production_date < DATE_SUB(NOW(), INTERVAL (valid_months-3) MONTH);
3. 核心业务模块实现
3.1 处方审核的MVC实现
药品管理的核心难点在于处方药销售流程,我们采用三层验证机制:
Controller层处理电子处方图片上传:
java复制@PostMapping("/prescription/upload")
public String handleUpload(
@RequestParam MultipartFile file,
@Valid PrescriptionForm form,
BindingResult result) {
if (file.isEmpty() || result.hasErrors()) {
return "prescription/upload";
}
String fileId = storageService.store(file);
auditService.logUpload(form.getPatientId(), fileId);
return "redirect:/prescription/review/" + fileId;
}
Service层实现合规性校验:
java复制public PrescriptionReviewResult reviewPrescription(String fileId) {
Prescription prescription = ocrService.parse(fileId);
// 规则1:检查医师执业证书有效期
if (!licenseService.isValid(prescription.getDoctorLicense())) {
return PrescriptionReviewResult.reject("医师执业证书已过期");
}
// 规则2:检查药品配伍禁忌
List<Medicine> medicines = medicineRepository
.findAllById(prescription.getMedicineIds());
if (interactionChecker.hasConflict(medicines)) {
return PrescriptionReviewResult.reject("存在药品配伍禁忌");
}
return PrescriptionReviewResult.approve();
}
View层使用Thymeleaf动态生成审核页面:
html复制<div th:if="${result.approved}">
<h3 class="text-success">审核通过</h3>
<button class="btn btn-primary"
th:onclick="|addToCart(${prescription.id})|">
加入购物车
</button>
</div>
<div th:unless="${result.approved}">
<h3 class="text-danger">审核未通过</h3>
<p th:text="'原因:' + ${result.rejectReason}"></p>
</div>
3.2 库存预警的智能算法
药品库存管理需要平衡三个指标:
- 库存周转率(建议>4次/年)
- 缺货率(应<5%)
- 近效期损耗率(应<3%)
我们实现动态安全库存计算:
java复制public int calculateSafetyStock(Medicine medicine) {
// 基础因子:过去30天日均销量
double avgSales = salesService.getDailyAverage(medicine.getId(), 30);
// 调整因子1:季节系数(感冒药冬季需求高)
double seasonFactor = seasonService.getFactor(
medicine.getCategory(),
LocalDate.now().getMonth());
// 调整因子2:促销影响
double promotionFactor = promotionService.getCurrentFactor(
medicine.getId());
// 最终安全库存 = 基础需求 × 调整系数 × 供货周期
return (int) Math.ceil(
avgSales * seasonFactor * promotionFactor *
supplierService.getLeadDays(medicine.getSupplierId())
);
}
4. 安全与合规性设计
4.1 医药数据特殊保护
根据《药品网络销售监督管理办法》要求,系统实现:
- 处方药购买记录加密存储(采用AES-256)
- 审计日志保留至少5年
- 敏感操作二次验证
Spring Security配置示例:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/prescription/**").hasRole("PHARMACIST")
.antMatchers("/inventory/**").hasAnyRole("STOCK_MGR", "ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.and()
.logout()
.addLogoutHandler(new AuditLogoutHandler())
.permitAll();
}
}
4.2 防止XSS攻击的Thymeleaf配置
医药网站常成为恶意爬虫目标,我们在application.yml中配置:
yaml复制spring:
thymeleaf:
mode: HTML
encoding: UTF-8
cache: false
servlet:
content-type: text/html
# 关键安全配置
escape-html: true
reject-unresolved-expressions: true
同时自定义消毒处理器:
java复制public class MedicineXssProcessor extends AbstractProcessor {
@Override
protected String doProcess(String text) {
// 允许药品说明书需要的HTML标签
return HtmlUtils.htmlEscape(text, "UTF-8")
.replaceAll("<br>", "<br>")
.replaceAll("<ul>", "<ul>")
.replaceAll("<li>", "<li>");
}
}
5. 性能优化实践
5.1 药品搜索的Elasticsearch整合
为应对药品SKU超过10万的情况,我们采用:
java复制@Repository
public interface MedicineSearchRepository
extends ElasticsearchRepository<MedicineEs, Long> {
@Query("{\"bool\":{\"should\":["
+ "{\"match\":{\"name.pinyin\":\"?0\"}},"
+ "{\"match\":{\"name.std\":\"?0\"}},"
+ "{\"match\":{\"components\":\"?0\"}}"
+ "]}}")
Page<MedicineEs> search(String keyword, Pageable pageable);
}
配合JPA实现双写:
java复制@Transactional
public Medicine saveWithSearch(Medicine medicine) {
Medicine saved = medicineRepository.save(medicine);
medicineSearchRepository.save(convertToEsEntity(saved));
return saved;
}
5.2 缓存策略设计
采用三级缓存架构:
- 本地缓存:Caffeine缓存药品基础信息(TTL=5分钟)
- Redis缓存:存储库存实时数据(TTL=30秒)
- MySQL:持久化存储
配置示例:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.registerCustomCache("medicines",
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build());
return manager;
}
@Cacheable(value = "medicines", key = "#id")
public Medicine getById(Long id) {
return medicineRepository.findById(id).orElse(null);
}
}
6. 部署与监控方案
6.1 容器化部署
Docker-compose配置包含:
yaml复制version: '3.8'
services:
app:
image: pharmacy-system:${VERSION}
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_DATABASE: pharmacy
volumes:
- db_data:/var/lib/mysql
command: --default-authentication-plugin=mysql_native_password
6.2 监控指标设计
通过Spring Boot Actuator暴露关键指标:
- 处方审核平均耗时(预警阈值>3秒)
- 库存同步延迟(预警阈值>1分钟)
- 订单创建成功率(预警阈值<99%)
Prometheus配置示例:
yaml复制scrape_configs:
- job_name: 'pharmacy'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['app:8080']
relabel_configs:
- source_labels: [__address__]
target_label: instance
regex: '(.*):\d+'
replacement: '$1'
在项目开发过程中,我们发现三个关键经验:首先,药品批号校验需要兼容20多种不同格式;其次,处方审核流程必须保留完整的修改痕迹;最后,库存预警需要动态调整算法参数。这些细节往往决定系统在实际业务中的可用性。
