添加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_EXPIRATION=86400000
|
||||||
JWT_REFRESH_EXPIRATION=604800000
|
JWT_REFRESH_EXPIRATION=604800000
|
||||||
|
|
||||||
|
# Redis(可选,不可用时自动跳过)
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
# 服务端口
|
# 服务端口
|
||||||
SERVER_PORT=8080
|
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>
|
<version>5.8.37</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>redis.clients</groupId>
|
||||||
|
<artifactId>jedis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-pool2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
<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.entity.*;
|
||||||
import com.kronecker.backend.mapper.*;
|
import com.kronecker.backend.mapper.*;
|
||||||
import com.kronecker.backend.service.DashboardService;
|
import com.kronecker.backend.service.DashboardService;
|
||||||
|
import com.kronecker.backend.utils.RedisCacheService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -21,16 +22,25 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
private final StockOutRecordMapper stockOutRecordMapper;
|
private final StockOutRecordMapper stockOutRecordMapper;
|
||||||
private final SupplierMapper supplierMapper;
|
private final SupplierMapper supplierMapper;
|
||||||
private final CustomerMapper customerMapper;
|
private final CustomerMapper customerMapper;
|
||||||
|
private final RedisCacheService redisCache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public Map<String, Object> getStatistics() {
|
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<>();
|
Map<String, Object> stats = new HashMap<>();
|
||||||
stats.put("medicineCount", medicineMapper.selectCount(null));
|
stats.put("medicineCount", medicineMapper.selectCount(null));
|
||||||
stats.put("stockBatchCount", stockMapper.selectCount(null));
|
stats.put("stockBatchCount", stockMapper.selectCount(null));
|
||||||
stats.put("pendingStockIn", stockInRecordMapper.selectCount(
|
stats.put("pendingStockIn", stockInRecordMapper.selectCount(
|
||||||
new LambdaQueryWrapper<StockInRecord>().eq(StockInRecord::getStatus, 0)));
|
new LambdaQueryWrapper<StockInRecord>().eq(StockInRecord::getStatus, 0)));
|
||||||
stats.put("expiryWarningCount", stockMapper.selectCount(
|
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;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.kronecker.backend.dto.StockInCreateDTO;
|
|||||||
import com.kronecker.backend.entity.*;
|
import com.kronecker.backend.entity.*;
|
||||||
import com.kronecker.backend.mapper.*;
|
import com.kronecker.backend.mapper.*;
|
||||||
import com.kronecker.backend.service.StockInService;
|
import com.kronecker.backend.service.StockInService;
|
||||||
|
import com.kronecker.backend.utils.RedisCacheService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -20,6 +21,7 @@ public class StockInServiceImpl implements StockInService {
|
|||||||
private final StockInRecordMapper recordMapper;
|
private final StockInRecordMapper recordMapper;
|
||||||
private final StockInDetailMapper detailMapper;
|
private final StockInDetailMapper detailMapper;
|
||||||
private final StockMapper stockMapper;
|
private final StockMapper stockMapper;
|
||||||
|
private final RedisCacheService redisCache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -65,6 +67,7 @@ public class StockInServiceImpl implements StockInService {
|
|||||||
}
|
}
|
||||||
record.setStatus(1);
|
record.setStatus(1);
|
||||||
recordMapper.updateById(record);
|
recordMapper.updateById(record);
|
||||||
|
redisCache.delete("dashboard:statistics");
|
||||||
|
|
||||||
// 更新库存
|
// 更新库存
|
||||||
var details = detailMapper.selectList(
|
var details = detailMapper.selectList(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.kronecker.backend.dto.StockOutCreateDTO;
|
|||||||
import com.kronecker.backend.entity.*;
|
import com.kronecker.backend.entity.*;
|
||||||
import com.kronecker.backend.mapper.*;
|
import com.kronecker.backend.mapper.*;
|
||||||
import com.kronecker.backend.service.StockOutService;
|
import com.kronecker.backend.service.StockOutService;
|
||||||
|
import com.kronecker.backend.utils.RedisCacheService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -20,6 +21,7 @@ public class StockOutServiceImpl implements StockOutService {
|
|||||||
private final StockOutRecordMapper recordMapper;
|
private final StockOutRecordMapper recordMapper;
|
||||||
private final StockOutDetailMapper detailMapper;
|
private final StockOutDetailMapper detailMapper;
|
||||||
private final StockMapper stockMapper;
|
private final StockMapper stockMapper;
|
||||||
|
private final RedisCacheService redisCache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -92,5 +94,6 @@ public class StockOutServiceImpl implements StockOutService {
|
|||||||
|
|
||||||
record.setStatus(1);
|
record.setStatus(1);
|
||||||
recordMapper.updateById(record);
|
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 |
|
| ORM | MyBatis-Plus | 3.5.11 |
|
||||||
| 安全认证 | Spring Security + JWT (jjwt 0.13.0) | — |
|
| 安全认证 | Spring Security + JWT (jjwt 0.13.0) | — |
|
||||||
| 数据库 | MySQL 8.0 (JDBC + HikariCP) | — |
|
| 数据库 | MySQL 8.0 (JDBC + HikariCP) | — |
|
||||||
|
| 缓存 | Redis 7 + Jedis(可选,无连接自动跳过) | — |
|
||||||
| 工具库 | Hutool 5.8 / Lombok | — |
|
| 工具库 | Hutool 5.8 / Lombok | — |
|
||||||
| 构建 | Maven 3.9+ | — |
|
| 构建 / 部署 | Maven 3.9+ / Docker + Docker Compose | — |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -37,8 +38,10 @@ graph TB
|
|||||||
end
|
end
|
||||||
|
|
||||||
DB["🐬 MySQL 8.0<br/>InnoDB · utf8mb4"]
|
DB["🐬 MySQL 8.0<br/>InnoDB · utf8mb4"]
|
||||||
|
Cache["⚡ Redis 7<br/>可选 · 自动降级"]
|
||||||
|
|
||||||
Client -->|"HTTPS + JWT Token"| Security
|
Client -->|"HTTPS + JWT Token"| Security
|
||||||
|
Service --> Cache
|
||||||
Security --> Controller
|
Security --> Controller
|
||||||
Controller --> Service
|
Controller --> Service
|
||||||
Service --> Mapper
|
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+
|
- JDK 21+ · Node.js 18+ · MySQL 8.0+ · Maven 3.9+
|
||||||
|
|
||||||
### 1. 初始化数据库
|
#### 1. 初始化数据库
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mysql -u root -p < backend/src/main/resources/database/init.sql
|
mysql -u root -p < backend/src/main/resources/database/init.sql
|
||||||
@@ -340,9 +360,12 @@ npm run dev
|
|||||||
MedicineManagerSystem/
|
MedicineManagerSystem/
|
||||||
├── README.md
|
├── README.md
|
||||||
├── .env.example
|
├── .env.example
|
||||||
|
├── docker-compose.yml # Docker 编排
|
||||||
├── projectDesigning.md # 详细架构设计文档
|
├── projectDesigning.md # 详细架构设计文档
|
||||||
│
|
│
|
||||||
├── frontend/ # Vue 3 SPA
|
├── frontend/ # Vue 3 SPA
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ ├── nginx.conf
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ ├── api/ (11) # API 模块
|
│ ├── api/ (11) # API 模块
|
||||||
│ ├── directives/ (1) # v-permission
|
│ ├── directives/ (1) # v-permission
|
||||||
@@ -350,10 +373,11 @@ MedicineManagerSystem/
|
|||||||
│ ├── router/ (1) # 路由 + 动态菜单
|
│ ├── router/ (1) # 路由 + 动态菜单
|
||||||
│ ├── stores/ (2) # Pinia
|
│ ├── stores/ (2) # Pinia
|
||||||
│ ├── styles/ (1) # SCSS
|
│ ├── styles/ (1) # SCSS
|
||||||
│ ├── utils/ (1) # Axios 封装
|
│ ├── utils/ (2) # Axios · Redis 缓存
|
||||||
│ └── views/ (16) # 页面组件
|
│ └── views/ (16) # 页面组件
|
||||||
│
|
│
|
||||||
└── backend/ # Spring Boot
|
└── backend/ # Spring Boot
|
||||||
|
├── Dockerfile
|
||||||
└── src/main/
|
└── src/main/
|
||||||
├── java/com/kronecker/backend/
|
├── java/com/kronecker/backend/
|
||||||
│ ├── common/ (4) # Result · 异常
|
│ ├── common/ (4) # Result · 异常
|
||||||
@@ -363,7 +387,8 @@ MedicineManagerSystem/
|
|||||||
│ ├── entity/ (15) # 数据库实体
|
│ ├── entity/ (15) # 数据库实体
|
||||||
│ ├── mapper/ (15) # MP Mapper
|
│ ├── mapper/ (15) # MP Mapper
|
||||||
│ ├── security/ (3) # JWT
|
│ ├── security/ (3) # JWT
|
||||||
│ └── service/ (23) # 业务逻辑
|
│ ├── service/ (23) # 业务逻辑
|
||||||
|
│ └── utils/ (1) # Redis 缓存
|
||||||
└── resources/
|
└── resources/
|
||||||
├── application.yml
|
├── application.yml
|
||||||
└── database/init.sql # 建表 + 种子数据
|
└── database/init.sql # 建表 + 种子数据
|
||||||
|
|||||||
Reference in New Issue
Block a user