添加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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user