1. 项目概述与架构设计
社区药房管理系统是一款基于SpringBoot+Vue.js前后端分离架构的药品零售管理解决方案。作为一名长期从事医疗信息化系统开发的工程师,我在实际项目中发现传统单体架构的药房管理系统普遍存在响应速度慢、维护成本高的问题。这套系统采用JDK17+SpringBoot3.x+Vue3的技术组合,正是针对这些痛点设计的现代化替代方案。
系统核心功能模块包括:
- 药品进销存管理(采购、库存、销售)
- 会员管理与积分系统
- 处方药审核流程
- 经营数据分析看板
- 多角色权限管理系统
技术栈选型上,后端采用SpringBoot3.x主要考虑到其自动配置特性可以快速搭建微服务架构,配合JDK17的ZGC垃圾回收器,实测在高并发药品查询场景下GC停顿时间控制在10ms以内。前端选用Vue3的组合式API写法,相比Options API更利于复杂业务组件(如药品批次管理)的代码组织。
2. 开发环境搭建
2.1 后端环境配置
推荐使用JDK17+IntelliJ IDEA的开发组合,以下是关键配置步骤:
- 安装Amazon Corretto 17 JDK:
bash复制# Ubuntu示例
wget https://corretto.aws/downloads/latest/amazon-corretto-17-x64-linux-jdk.deb
sudo apt install -y ./amazon-corretto-17-x64-linux-jdk.deb
- SpringBoot关键依赖配置(pom.xml节选):
xml复制<properties>
<java.version>17</java.version>
<spring-boot.version>3.1.5</spring-boot.version>
</properties>
<dependencies>
<!-- 数据持久化 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- API文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
注意:SpringBoot3.x默认使用Jakarta EE9+规范,与旧版javax包不兼容,迁移项目时需要特别注意包路径变更。
2.2 前端环境配置
Vue3开发环境建议使用Vite构建工具:
bash复制# 创建项目
npm create vite@latest pharmacy-management --template vue-ts
# 添加必要依赖
npm install pinia vue-router@4 axios element-plus
npm install --save-dev sass mockjs
推荐配置VSCode插件:
- Volar(Vue3官方推荐插件)
- ESLint(代码规范检查)
- Prettier(代码格式化)
3. 核心模块实现
3.1 药品库存管理
采用JPA实现多条件动态查询:
java复制@Entity
@Table(name = "medicine")
public class Medicine {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name; // 药品通用名
@Column(nullable = false)
private String specification; // 规格
@Enumerated(EnumType.STRING)
private MedicineType type; // 处方/非处方
@Column(precision = 10, scale = 2)
private BigDecimal purchasePrice; // 采购价
@Column(precision = 10, scale = 2)
private BigDecimal retailPrice; // 零售价
// 省略getter/setter
}
public interface MedicineRepository extends JpaRepository<Medicine, Long>,
JpaSpecificationExecutor<Medicine> {
@Query("SELECT m FROM Medicine m WHERE " +
"(:name IS NULL OR m.name LIKE %:name%) AND " +
"(:type IS NULL OR m.type = :type) AND " +
"(:minPrice IS NULL OR m.retailPrice >= :minPrice) AND " +
"(:maxPrice IS NULL OR m.retailPrice <= :maxPrice)")
Page<Medicine> searchMedicines(
@Param("name") String name,
@Param("type") MedicineType type,
@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice,
Pageable pageable);
}
前端使用Element Plus表格组件实现分页查询:
vue复制<template>
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="name" label="药品名称" width="180" />
<el-table-column prop="specification" label="规格" />
<el-table-column prop="type" label="类型" />
<el-table-column prop="retailPrice" label="零售价" sortable />
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
@current-change="fetchData"
layout="total, prev, pager, next"
/>
</template>
<script setup lang="ts">
const currentPage = ref(1)
const pageSize = 10
const total = ref(0)
const tableData = ref([])
const fetchData = async () => {
const params = {
page: currentPage.value - 1,
size: pageSize,
name: searchQuery.value
}
const res = await axios.get('/api/medicines', { params })
tableData.value = res.data.content
total.value = res.data.totalElements
}
</script>
3.2 处方药审核流程
采用状态机模式设计审核流程:
java复制public enum PrescriptionStatus {
DRAFT, // 草稿
SUBMITTED, // 已提交
PHARMACIST_REVIEW, // 药师审核
DOCTOR_REVIEW, // 医师审核
APPROVED, // 已批准
REJECTED, // 已拒绝
DISPENSED // 已配药
}
@Entity
public class Prescription {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private PrescriptionStatus status;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "prescription_id")
private List<PrescriptionItem> items = new ArrayList<>();
@ManyToOne
private User patient;
@ManyToOne
private User prescribingDoctor;
public void submit() {
if (this.status != PrescriptionStatus.DRAFT) {
throw new IllegalStateException("只有草稿状态的处方可以提交");
}
this.status = PrescriptionStatus.SUBMITTED;
}
// 其他状态转换方法...
}
4. 系统安全设计
4.1 JWT认证实现
Spring Security配置类:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/medicines").hasAnyRole("PHARMACIST", "ADMIN")
.requestMatchers("/api/prescriptions/**").hasRole("PHARMACIST")
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtFilter() {
return new JwtAuthenticationFilter();
}
}
JWT工具类核心方法:
java复制public class JwtUtils {
private static final String SECRET_KEY = "your-256-bit-secret";
private static final long EXPIRATION_TIME = 864_000_000; // 10天
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public static String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
5. 性能优化实践
5.1 药品库存缓存策略
使用Redis实现二级缓存:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
@Repository
@CacheConfig(cacheNames = "medicines")
public interface MedicineRepository extends JpaRepository<Medicine, Long> {
@Cacheable(key = "#id")
Optional<Medicine> findById(Long id);
@CacheEvict(allEntries = true)
<S extends Medicine> S save(S entity);
@CacheEvict(allEntries = true)
void deleteById(Long id);
}
5.2 前端性能优化
- 按需加载路由组件:
javascript复制const routes = [
{
path: '/medicines',
component: () => import('../views/MedicineListView.vue')
},
{
path: '/prescriptions',
component: () => import('../views/PrescriptionListView.vue')
}
]
- 使用Web Worker处理大数据量导出:
javascript复制// export.worker.js
self.onmessage = function(e) {
const { data, columns } = e.data
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.json_to_sheet(data, { header: columns })
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1")
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
self.postMessage(excelBuffer)
}
// 在组件中使用
const exportData = () => {
const worker = new Worker('@/workers/export.worker.js')
worker.postMessage({
data: tableData.value,
columns: exportColumns
})
worker.onmessage = (e) => {
const blob = new Blob([e.data], { type: 'application/octet-stream' })
saveAs(blob, '药品数据.xlsx')
worker.terminate()
}
}
6. 部署方案
6.1 Docker容器化部署
后端Dockerfile示例:
dockerfile复制FROM amazoncorretto:17-alpine
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
前端Dockerfile示例:
dockerfile复制FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
docker-compose.yml配置:
yaml复制version: '3.8'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/pharmacy?useSSL=false
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=password
depends_on:
- db
- redis
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=pharmacy
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
mysql_data:
7. 常见问题排查
- 跨域问题解决方案:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8081")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
- Vue3中Element Plus按需导入样式丢失问题:
javascript复制// vite.config.js
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
]
})
- SpringBoot3.x与Swagger兼容问题:
改用springdoc-openapi替代:
java复制@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI pharmacyOpenAPI() {
return new OpenAPI()
.info(new Info().title("药房管理系统API")
.description("社区药房管理后端API文档")
.version("v1.0.0"))
.externalDocs(new ExternalDocumentation()
.description("项目Wiki")
.url("https://github.com/yourrepo/wiki"));
}
}
在实际部署过程中,我遇到过Nginx配置不当导致前端路由404的问题,解决方案是在nginx.conf中添加try_files配置:
nginx复制location / {
try_files $uri $uri/ /index.html;
}
另一个常见问题是JDK17与某些旧版库的兼容性问题,特别是涉及反射操作的库。建议使用最新稳定版的依赖库,并在引入新依赖时先检查其是否支持JDK17。