添加Redis缓存支持和Docker部署配置
- 新增Redis缓存功能,用于优化仪表板统计数据查询性能 - 添加Redis连接池管理和自动降级机制,无连接时自动跳过缓存 - 配置Dockerfile支持前后端容器化部署 - 添加docker-compose编排文件实现一键部署 - 更新README文档包含新的部署方式和架构说明 - 在入库出库操作后清除相关缓存以保证数据一致性
This commit is contained in:
4
backend/.dockerignore
Normal file
4
backend/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
target/
|
||||
.idea/
|
||||
*.iml
|
||||
.env
|
||||
@@ -15,5 +15,10 @@ JWT_SECRET=change_me_to_a_random_base64_string_at_least_256_bits
|
||||
JWT_EXPIRATION=86400000
|
||||
JWT_REFRESH_EXPIRATION=604800000
|
||||
|
||||
# Redis(可选,不可用时自动跳过)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# 服务端口
|
||||
SERVER_PORT=8080
|
||||
|
||||
25
backend/Dockerfile
Normal file
25
backend/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
# ============================================================
|
||||
# 药品管理系统 — 后端 Dockerfile(多阶段构建)
|
||||
# ============================================================
|
||||
|
||||
# ---- 构建阶段 ----
|
||||
FROM eclipse-temurin:21-jdk-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY pom.xml mvnw mvnw.cmd ./
|
||||
COPY .mvn .mvn
|
||||
# 缓存依赖
|
||||
RUN chmod +x mvnw && ./mvnw dependency:go-offline -q
|
||||
COPY src ./src
|
||||
RUN ./mvnw package -DskipTests -q
|
||||
|
||||
# ---- 运行阶段 ----
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/target/*.jar app.jar
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV SERVER_PORT=8080
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
@@ -81,6 +81,16 @@
|
||||
<version>5.8.37</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.kronecker.backend.entity.*;
|
||||
import com.kronecker.backend.mapper.*;
|
||||
import com.kronecker.backend.service.DashboardService;
|
||||
import com.kronecker.backend.utils.RedisCacheService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -21,16 +22,25 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
private final StockOutRecordMapper stockOutRecordMapper;
|
||||
private final SupplierMapper supplierMapper;
|
||||
private final CustomerMapper customerMapper;
|
||||
private final RedisCacheService redisCache;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> getStatistics() {
|
||||
// 尝试从缓存读取
|
||||
Map<String, Object> cached = redisCache.getObject("dashboard:statistics", Map.class);
|
||||
if (cached != null) return cached;
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("medicineCount", medicineMapper.selectCount(null));
|
||||
stats.put("stockBatchCount", stockMapper.selectCount(null));
|
||||
stats.put("pendingStockIn", stockInRecordMapper.selectCount(
|
||||
new LambdaQueryWrapper<StockInRecord>().eq(StockInRecord::getStatus, 0)));
|
||||
stats.put("expiryWarningCount", stockMapper.selectCount(
|
||||
new LambdaQueryWrapper<Stock>().le(Stock::getExpiryDate, LocalDate.now().plusDays(90))));
|
||||
new LambdaQueryWrapper<Stock>().gt(Stock::getQuantity, 0)
|
||||
.le(Stock::getExpiryDate, LocalDate.now().plusDays(90))));
|
||||
// 缓存 60 秒
|
||||
redisCache.setObject("dashboard:statistics", stats, 60);
|
||||
return stats;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.kronecker.backend.dto.StockInCreateDTO;
|
||||
import com.kronecker.backend.entity.*;
|
||||
import com.kronecker.backend.mapper.*;
|
||||
import com.kronecker.backend.service.StockInService;
|
||||
import com.kronecker.backend.utils.RedisCacheService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -20,6 +21,7 @@ public class StockInServiceImpl implements StockInService {
|
||||
private final StockInRecordMapper recordMapper;
|
||||
private final StockInDetailMapper detailMapper;
|
||||
private final StockMapper stockMapper;
|
||||
private final RedisCacheService redisCache;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@@ -65,6 +67,7 @@ public class StockInServiceImpl implements StockInService {
|
||||
}
|
||||
record.setStatus(1);
|
||||
recordMapper.updateById(record);
|
||||
redisCache.delete("dashboard:statistics");
|
||||
|
||||
// 更新库存
|
||||
var details = detailMapper.selectList(
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.kronecker.backend.dto.StockOutCreateDTO;
|
||||
import com.kronecker.backend.entity.*;
|
||||
import com.kronecker.backend.mapper.*;
|
||||
import com.kronecker.backend.service.StockOutService;
|
||||
import com.kronecker.backend.utils.RedisCacheService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -20,6 +21,7 @@ public class StockOutServiceImpl implements StockOutService {
|
||||
private final StockOutRecordMapper recordMapper;
|
||||
private final StockOutDetailMapper detailMapper;
|
||||
private final StockMapper stockMapper;
|
||||
private final RedisCacheService redisCache;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@@ -92,5 +94,6 @@ public class StockOutServiceImpl implements StockOutService {
|
||||
|
||||
record.setStatus(1);
|
||||
recordMapper.updateById(record);
|
||||
redisCache.delete("dashboard:statistics");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.kronecker.backend.utils;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RedisCacheService {
|
||||
|
||||
@Value("${REDIS_HOST:localhost}")
|
||||
private String host;
|
||||
|
||||
@Value("${REDIS_PORT:6379}")
|
||||
private int port;
|
||||
|
||||
@Value("${REDIS_PASSWORD:}")
|
||||
private String password;
|
||||
|
||||
@Value("${REDIS_TIMEOUT:2000}")
|
||||
private int timeout;
|
||||
|
||||
private JedisPool pool;
|
||||
private boolean available = false;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
JedisPoolConfig config = new JedisPoolConfig();
|
||||
config.setMaxTotal(8);
|
||||
config.setMaxIdle(4);
|
||||
config.setMinIdle(1);
|
||||
|
||||
pool = (password != null && !password.isEmpty())
|
||||
? new JedisPool(config, host, port, timeout, password)
|
||||
: new JedisPool(config, host, port, timeout);
|
||||
|
||||
// 测试连接
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
jedis.ping();
|
||||
}
|
||||
available = true;
|
||||
log.info("Redis 连接成功: {}:{}", host, port);
|
||||
} catch (Exception e) {
|
||||
available = false;
|
||||
pool = null;
|
||||
log.warn("Redis 不可用 ({}:{}),跳过缓存: {}", host, port, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
if (pool != null) pool.close();
|
||||
}
|
||||
|
||||
public boolean enabled() {
|
||||
return available && pool != null;
|
||||
}
|
||||
|
||||
public void set(String key, String value, long seconds) {
|
||||
if (!enabled()) return;
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
jedis.setex(key, seconds, value);
|
||||
} catch (Exception e) {
|
||||
log.warn("Redis set {} 失败: {}", key, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public String get(String key) {
|
||||
if (!enabled()) return null;
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
return jedis.get(key);
|
||||
} catch (Exception e) {
|
||||
log.warn("Redis get {} 失败: {}", key, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// === Object 序列化方法 ===
|
||||
|
||||
public void setObject(String key, Object value, long seconds) {
|
||||
if (value == null) return;
|
||||
set(key, JSONUtil.toJsonStr(value), seconds);
|
||||
}
|
||||
|
||||
public <T> T getObject(String key, Class<T> clazz) {
|
||||
String json = get(key);
|
||||
if (json == null) return null;
|
||||
try {
|
||||
return JSONUtil.toBean(json, clazz);
|
||||
} catch (Exception e) {
|
||||
log.warn("Redis 反序列化 {} 失败: {}", key, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(String key) {
|
||||
if (!enabled()) return;
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
jedis.del(key);
|
||||
} catch (Exception e) {
|
||||
log.warn("Redis del {} 失败: {}", key, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
4
frontend/.dockerignore
Normal file
4
frontend/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.local
|
||||
20
frontend/Dockerfile
Normal file
20
frontend/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# ============================================================
|
||||
# 药品管理系统 — 前端 Dockerfile(多阶段构建)
|
||||
# ============================================================
|
||||
|
||||
# ---- 构建阶段 ----
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ---- 运行阶段 ----
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
19
frontend/nginx.conf
Normal file
19
frontend/nginx.conf
Normal file
@@ -0,0 +1,19 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# API 代理到后端
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# Vue SPA 路由
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
35
readme.md
35
readme.md
@@ -18,8 +18,9 @@
|
||||
| ORM | MyBatis-Plus | 3.5.11 |
|
||||
| 安全认证 | Spring Security + JWT (jjwt 0.13.0) | — |
|
||||
| 数据库 | MySQL 8.0 (JDBC + HikariCP) | — |
|
||||
| 缓存 | Redis 7 + Jedis(可选,无连接自动跳过) | — |
|
||||
| 工具库 | Hutool 5.8 / Lombok | — |
|
||||
| 构建 | Maven 3.9+ | — |
|
||||
| 构建 / 部署 | Maven 3.9+ / Docker + Docker Compose | — |
|
||||
|
||||
---
|
||||
|
||||
@@ -37,8 +38,10 @@ graph TB
|
||||
end
|
||||
|
||||
DB["🐬 MySQL 8.0<br/>InnoDB · utf8mb4"]
|
||||
Cache["⚡ Redis 7<br/>可选 · 自动降级"]
|
||||
|
||||
Client -->|"HTTPS + JWT Token"| Security
|
||||
Service --> Cache
|
||||
Security --> Controller
|
||||
Controller --> Service
|
||||
Service --> Mapper
|
||||
@@ -278,11 +281,28 @@ sequenceDiagram
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
### 方式一:Docker Compose(推荐)
|
||||
|
||||
```bash
|
||||
# 1. 配置环境变量
|
||||
cp .env.example .env
|
||||
# 编辑 .env 设置 DB_PASSWORD 和 JWT_SECRET
|
||||
|
||||
# 2. 一键启动(MySQL + Redis + 后端 + 前端)
|
||||
docker-compose up -d
|
||||
|
||||
# 3. 访问
|
||||
open http://localhost
|
||||
```
|
||||
|
||||
> 首次启动 MySQL 会自动执行 `init.sql` 建表和数据。Redis 不可用时后端自动降级跳过缓存。
|
||||
|
||||
### 方式二:手动启动
|
||||
|
||||
**环境要求**
|
||||
- JDK 21+ · Node.js 18+ · MySQL 8.0+ · Maven 3.9+
|
||||
|
||||
### 1. 初始化数据库
|
||||
#### 1. 初始化数据库
|
||||
|
||||
```bash
|
||||
mysql -u root -p < backend/src/main/resources/database/init.sql
|
||||
@@ -340,9 +360,12 @@ npm run dev
|
||||
MedicineManagerSystem/
|
||||
├── README.md
|
||||
├── .env.example
|
||||
├── docker-compose.yml # Docker 编排
|
||||
├── projectDesigning.md # 详细架构设计文档
|
||||
│
|
||||
├── frontend/ # Vue 3 SPA
|
||||
│ ├── Dockerfile
|
||||
│ ├── nginx.conf
|
||||
│ └── src/
|
||||
│ ├── api/ (11) # API 模块
|
||||
│ ├── directives/ (1) # v-permission
|
||||
@@ -350,10 +373,11 @@ MedicineManagerSystem/
|
||||
│ ├── router/ (1) # 路由 + 动态菜单
|
||||
│ ├── stores/ (2) # Pinia
|
||||
│ ├── styles/ (1) # SCSS
|
||||
│ ├── utils/ (1) # Axios 封装
|
||||
│ ├── utils/ (2) # Axios · Redis 缓存
|
||||
│ └── views/ (16) # 页面组件
|
||||
│
|
||||
└── backend/ # Spring Boot
|
||||
├── Dockerfile
|
||||
└── src/main/
|
||||
├── java/com/kronecker/backend/
|
||||
│ ├── common/ (4) # Result · 异常
|
||||
@@ -363,7 +387,8 @@ MedicineManagerSystem/
|
||||
│ ├── entity/ (15) # 数据库实体
|
||||
│ ├── mapper/ (15) # MP Mapper
|
||||
│ ├── security/ (3) # JWT
|
||||
│ └── service/ (23) # 业务逻辑
|
||||
│ ├── service/ (23) # 业务逻辑
|
||||
│ └── utils/ (1) # Redis 缓存
|
||||
└── resources/
|
||||
├── application.yml
|
||||
└── database/init.sql # 建表 + 种子数据
|
||||
|
||||
Reference in New Issue
Block a user