添加Redis缓存支持和Docker部署配置

- 新增Redis缓存功能,用于优化仪表板统计数据查询性能
- 添加Redis连接池管理和自动降级机制,无连接时自动跳过缓存
- 配置Dockerfile支持前后端容器化部署
- 添加docker-compose编排文件实现一键部署
- 更新README文档包含新的部署方式和架构说明
- 在入库出库操作后清除相关缓存以保证数据一致性
This commit is contained in:
2026-07-07 10:08:33 +08:00
parent 48007f536d
commit b764f6fa59
12 changed files with 245 additions and 6 deletions

4
backend/.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
target/
.idea/
*.iml
.env

View File

@@ -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
View 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"]

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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");
}
}

View File

@@ -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());
}
}
}