1. 项目概述与核心价值
这套"web药店管理系统"是一个典型的医药行业信息化解决方案,采用当前主流的前后端分离架构。后端基于SpringBoot 2.x构建,提供稳定的RESTful API;前端采用Vue 2/3框架实现动态交互;数据存储使用MySQL 5.7+关系型数据库。整套系统开箱即用,源码结构清晰,包含完整的药品管理、库存管理、销售统计等核心模块。
在实际药店运营中,传统手工记账方式存在三大痛点:一是药品效期管理困难,容易造成过期损失;二是销售数据统计滞后,无法实时掌握经营状况;三是处方药管理不规范,存在合规风险。本系统正是针对这些痛点设计,具有以下核心价值:
- 实现药品全生命周期数字化管理,支持批次号、生产日期、有效期等关键字段追踪
- 提供实时库存预警机制,当库存低于设定阈值或近效期药品达到提醒时限时自动触发告警
- 内置符合GSP规范的销售流程,支持处方药特殊管理要求(如必须关联医师处方)
- 可视化数据看板可实时展示销售额、毛利、畅销药品等关键经营指标
提示:系统默认采用前后端分离部署方式,这在现代Web开发中已成为主流方案。分离架构的优势在于前后端可以独立开发、测试和部署,同时也便于后续的横向扩展。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型解析
2.1 SpringBoot后端设计
后端采用SpringBoot 2.5.x版本构建,主要依赖包括:
- spring-boot-starter-web:提供Web MVC支持
- spring-boot-starter-data-jpa:数据库访问层
- spring-boot-starter-security:认证授权
- lombok:简化实体类编写
- hutool:Java工具包
数据库设计遵循第三范式,核心表包括:
- 药品主表(medicine):存储药品基本信息
- 库存表(inventory):记录各批次库存详情
- 用户表(user):系统账号管理
- 销售订单表(order):交易记录
java复制// 典型的药品实体类示例
@Entity
@Data
public class Medicine {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name; // 药品通用名
@Column(nullable = false)
private String specification; // 规格
@Column(nullable = false)
private String manufacturer; // 生产厂家
@Column(nullable = false)
private String approvalNumber; // 批准文号
@Enumerated(EnumType.STRING)
private MedicineType type; // 处方/非处方
// 其他字段及getter/setter...
}
2.2 Vue前端架构
前端采用Vue CLI搭建的项目骨架,主要技术组件包括:
- Vue Router:实现SPA路由跳转
- Vuex:状态集中管理
- Element UI:基础UI组件库
- ECharts:数据可视化
- Axios:HTTP请求封装
前端工程目录结构遵循最佳实践:
code复制src/
├── api/ # 接口定义
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── store/ # Vuex模块
├── utils/ # 工具函数
├── views/ # 页面组件
└── App.vue # 根组件
2.3 MySQL数据库配置
推荐使用MySQL 5.7+版本,关键配置参数建议:
ini复制[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
innodb_buffer_pool_size=1G # 根据服务器内存调整
innodb_log_file_size=256M
max_connections=200
创建数据库时需执行初始化SQL脚本,主要包含:
- 创建数据库和用户
- 建表语句
- 基础数据插入(如药品分类、角色权限等)
3. 系统部署指南
3.1 开发环境准备
后端环境:
- JDK 1.8+
- Maven 3.6+
- IDEA/Eclipse开发工具
前端环境:
- Node.js 14+
- npm/yarn包管理器
- VS Code/Vue专用IDE
数据库环境:
- MySQL 5.7+
- Navicat/Workbench管理工具
注意:在Windows环境下,MySQL安装后可能遇到服务启动失败问题,通常是由于之前安装残留导致的。可通过管理员权限运行
sc delete mysql清除旧服务,再重新初始化。
3.2 后端启动步骤
- 导入Maven项目
- 修改application.yml中的数据库配置
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/pharmacy?useSSL=false
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
- 执行Maven依赖下载
- 运行主启动类PharmacyApplication
3.3 前端启动流程
- 安装依赖
bash复制npm install
# 或
yarn install
- 配置API基础地址
javascript复制// src/utils/request.js
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // 在.env.development中配置
timeout: 5000
})
- 启动开发服务器
bash复制npm run serve
3.4 生产环境部署
推荐使用Docker容器化部署:
dockerfile复制# 后端Dockerfile示例
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD target/pharmacy-backend.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
Nginx前端部署配置:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
}
}
4. 核心功能实现解析
4.1 药品批次管理
系统采用"一物一码"管理策略,每个药品批次都有唯一编码。关键实现逻辑:
java复制// 批次入库逻辑
@Transactional
public void addInventory(InventoryDTO dto) {
// 校验药品基础信息
Medicine medicine = medicineRepository.findById(dto.getMedicineId())
.orElseThrow(() -> new BusinessException("药品不存在"));
// 生成批次号: 药品ID+入库日期+随机码
String batchNumber = String.format("%s-%tY%tm%d-%04d",
medicine.getApprovalNumber(),
dto.getProductionDate(),
dto.getProductionDate(),
new Random().nextInt(10000));
// 构建库存实体
Inventory inventory = new Inventory();
inventory.setBatchNumber(batchNumber);
inventory.setProductionDate(dto.getProductionDate());
inventory.setExpiryDate(dto.getExpiryDate());
inventory.setStock(dto.getQuantity());
inventory.setMedicine(medicine);
inventoryRepository.save(inventory);
// 记录操作日志
logService.addLog("入库操作", "新增批次: " + batchNumber);
}
4.2 智能预警机制
系统通过定时任务+事件监听实现双重预警:
- 每日凌晨执行效期检查
java复制@Scheduled(cron = "0 0 0 * * ?")
public void checkExpiryWarning() {
LocalDate warningDate = LocalDate.now().plusDays(30); // 提前30天预警
List<Inventory> expiring = inventoryRepository
.findByExpiryDateBetween(LocalDate.now(), warningDate);
expiring.forEach(inv -> {
String message = String.format("药品[%s]批次%s将在%s过期",
inv.getMedicine().getName(),
inv.getBatchNumber(),
inv.getExpiryDate());
// 发送站内信或邮件通知
notificationService.sendWarning(
"效期预警", message, "pharmacist");
});
}
- 库存操作时实时检查
java复制@EventListener
public void handleInventoryChange(InventoryEvent event) {
Inventory inventory = event.getInventory();
if (inventory.getStock() < inventory.getMedicine().getMinStock()) {
String message = String.format("药品[%s]库存不足! 当前:%d, 最低:%d",
inventory.getMedicine().getName(),
inventory.getStock(),
inventory.getMedicine().getMinStock());
notificationService.sendWarning(
"库存预警", message, "purchaser");
}
}
4.3 销售统计分析
采用ECharts实现可视化报表,后端提供聚合查询接口:
java复制@GetMapping("/stats/sales")
public ResponseEntity<SalesStatsVO> getSalesStats(
@RequestParam(required = false) String period) {
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusDays(
"week".equals(period) ? 7 :
"month".equals(period) ? 30 : 1);
// 查询销售数据
List<SalesDataDTO> salesData = orderRepository
.findSalesByDateRange(startDate, endDate);
// 按药品分类统计
Map<String, BigDecimal> categoryStats = salesData.stream()
.collect(Collectors.groupingBy(
dto -> dto.getMedicineType().getDisplayName(),
Collectors.reducing(
BigDecimal.ZERO,
SalesDataDTO::getAmount,
BigDecimal::add)));
// 构建返回对象
SalesStatsVO vo = new SalesStatsVO();
vo.setSalesData(salesData);
vo.setCategoryStats(categoryStats);
vo.setStartDate(startDate);
vo.setEndDate(endDate);
return ResponseEntity.ok(vo);
}
前端使用ECharts渲染:
vue复制<template>
<div class="chart-container">
<div ref="salesChart" style="width:100%;height:400px;"></div>
</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
mounted() {
this.fetchData()
},
methods: {
async fetchData() {
const { data } = await getSalesStats('month')
this.initChart(data)
},
initChart(stats) {
const chart = echarts.init(this.$refs.salesChart)
const option = {
tooltip: { trigger: 'axis' },
legend: { data: ['销售额'] },
xAxis: {
type: 'category',
data: stats.salesData.map(item => item.date)
},
yAxis: { type: 'value' },
series: [{
name: '销售额',
type: 'line',
data: stats.salesData.map(item => item.amount)
}]
}
chart.setOption(option)
}
}
}
</script>
5. 常见问题与解决方案
5.1 跨域问题处理
在前后端分离部署时,浏览器会因同源策略阻止跨域请求。解决方案:
后端配置(推荐):
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
前端代理配置(开发环境):
javascript复制// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
5.2 日期时间处理
前后端时间格式不一致是常见问题,推荐方案:
- 后端统一返回时间戳或ISO格式字符串
java复制@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
- 前端使用moment.js或day.js处理日期
javascript复制import dayjs from 'dayjs'
// 格式化显示
dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
5.3 权限控制实现
系统采用RBAC模型,关键实现点:
-
数据库设计五张表:
- 用户表(user)
- 角色表(role)
- 权限表(permission)
- 用户-角色关联表(user_role)
- 角色-权限关联表(role_permission)
-
Spring Security配置:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/medicine/**").hasAnyRole("ADMIN", "PHARMACIST")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable();
}
}
- 前端路由守卫:
javascript复制router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.getters.isAuthenticated) {
next('/login')
} else if (to.meta.roles && !store.getters.hasRole(to.meta.roles)) {
next('/403')
} else {
next()
}
})
6. 项目扩展与优化建议
6.1 微服务化改造
当单应用架构无法满足需求时,可考虑拆分为:
- 用户服务:处理认证授权
- 药品服务:核心业务逻辑
- 订单服务:交易处理
- 报表服务:数据分析
使用Spring Cloud Alibaba套件:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
6.2 缓存优化
引入Redis提升性能:
- 缓存热点数据
java复制@Cacheable(value = "medicine", key = "#id")
public Medicine getById(Long id) {
return medicineRepository.findById(id).orElse(null);
}
- 分布式锁控制库存扣减
java复制public boolean deductStock(Long medicineId, int quantity) {
String lockKey = "stock_lock:" + medicineId;
String requestId = UUID.randomUUID().toString();
try {
// 获取锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 执行库存扣减
Inventory inventory = inventoryRepository.findByMedicineId(medicineId);
if (inventory.getStock() >= quantity) {
inventory.setStock(inventory.getStock() - quantity);
inventoryRepository.save(inventory);
return true;
}
return false;
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
6.3 监控与日志
集成Prometheus + Grafana监控:
java复制@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> configureMetrics() {
return registry -> registry.config().commonTags("application", "pharmacy-system");
}
日志收集方案:
- 使用ELK Stack(Elasticsearch + Logstash + Kibana)
- 或直接使用阿里云SLS、腾讯云CLS等云服务
6.4 安全加固建议
- 接口防刷:使用Guava RateLimiter或Redis实现限流
java复制@Aspect
@Component
public class RateLimitAspect {
private final RateLimiter limiter = RateLimiter.create(100); // 100次/秒
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
if (limiter.tryAcquire()) {
return joinPoint.proceed();
}
throw new BusinessException("操作过于频繁,请稍后再试");
}
}
- SQL注入防护:
- 使用JPA/Hibernate等ORM框架
- 必须使用参数化查询
- 禁止直接拼接SQL
- XSS防护:
- 前端使用vue-sanitize过滤输入
- 后端对富文本内容使用JSoup清洗
java复制String safeHtml = Jsoup.clean(rawHtml, Whitelist.basic());
这套药店管理系统从技术选型到功能实现都考虑了实际业务场景需求,代码结构清晰,注释完整,非常适合作为学习SpringBoot+Vue全栈开发的实战项目,也可直接用于小型药店的信息化建设。根据实际需要,可以在此基础上继续扩展连锁门店管理、供应商协同、医保对接等高级功能。
