StatisticsServiceImpl.java
package org.darkroomlibrary.service.impl;
import org.darkroomlibrary.infrastructure.cache.CacheService;
import org.darkroomlibrary.mapper.BookMapper;
import org.darkroomlibrary.mapper.BorrowRecordMapper;
import org.darkroomlibrary.mapper.UserMapper;
import org.darkroomlibrary.web.response.ApiResponse;
import org.darkroomlibrary.web.dto.query.BorrowRecordPageQuery;
import org.darkroomlibrary.domain.model.Book;
import org.darkroomlibrary.web.view.DailyCount;
import org.darkroomlibrary.service.FineService;
import org.darkroomlibrary.service.StatisticsService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 统计服务实现(所有聚合操作都在数据库端完成,避免全量加载到内存)
*/
@Slf4j
@Service
public class StatisticsServiceImpl implements StatisticsService {
private static final Duration STATISTICS_CACHE_TTL = Duration.ofMinutes(3);
@Resource
private BookMapper bookMapper;
@Resource
private BorrowRecordMapper borrowRecordMapper;
@Resource
private UserMapper userMapper;
@Resource
private FineService fineService;
@Resource
private CacheService cacheService;
@Resource
private ObjectMapper objectMapper;
/**
* 热门图书 — 数据库端 GROUP BY + LIMIT
*/
@Override
public ApiResponse<Map<String, Object>> hotBooks(Integer limit) {
int actualLimit = limit == null ? 10 : Math.max(1, Math.min(limit, 100));
String cacheKey = "cache:statistics:hot-books:" + actualLimit;
Map<String, Object> cached = getCachedMap(cacheKey);
if (cached != null) {
return ApiResponse.success(cached);
}
List<Map<String, Object>> books = borrowRecordMapper.hotBookStats(actualLimit);
Map<String, Object> result = new HashMap<>();
result.put("books", books);
result.put("total", books.size());
putCachedMap(cacheKey, result);
return ApiResponse.success(result);
}
/**
* 低库存图书 — 数据库端 WHERE 过滤
*/
@Override
public ApiResponse<Map<String, Object>> lowStock(Integer threshold) {
List<Book> books = bookMapper.queryLowStock(threshold);
Map<String, Object> result = new HashMap<>();
result.put("books", books);
result.put("total", books.size());
return ApiResponse.success(result);
}
/**
* 总览 — 使用 queryCount,不加载具体数据
*/
@Override
public ApiResponse<Map<String, Object>> overview() {
String cacheKey = "cache:statistics:overview";
Map<String, Object> cached = getCachedMap(cacheKey);
if (cached != null) {
return ApiResponse.success(cached);
}
Map<String, Object> result = new HashMap<>();
// queryCount 只执行 COUNT(*),不加载数据
result.put("totalBooks", bookMapper.queryCount(null));
result.put("totalUsers", userMapper.queryCount(null));
BorrowRecordPageQuery borrowDto = new BorrowRecordPageQuery();
borrowDto.setStatus(false);
result.put("activeBorrows", borrowRecordMapper.queryCount(borrowDto));
borrowDto.setStatus(true);
result.put("returnedBorrows", borrowRecordMapper.queryCount(borrowDto));
putCachedMap(cacheKey, result);
return ApiResponse.success(result);
}
/**
* 月度借阅统计 — 仅查询指定年月,不加载全表
*/
@Override
public ApiResponse<List<Map<String, Object>>> monthlyBorrowStats(Integer year, Integer month) {
if (year == null || year < 1970 || year > 2100 || month == null || month < 1 || month > 12) {
return ApiResponse.error("统计年月不正确");
}
YearMonth yearMonth = YearMonth.of(year, month);
List<DailyCount> dailyCounts = borrowRecordMapper.dailyBorrowStats(
yearMonth.atDay(1).atStartOfDay(),
yearMonth.plusMonths(1).atDay(1).atStartOfDay()
);
List<Map<String, Object>> result = new ArrayList<>();
for (DailyCount dailyCount : dailyCounts) {
Map<String, Object> map = new HashMap<>();
map.put("day", String.format("%02d", dailyCount.getDay().getDayOfMonth()));
map.put("count", dailyCount.getCount());
result.add(map);
}
return ApiResponse.success(result);
}
/**
* 逾期用户统计 — 复用 FineService 的数据库聚合
*/
@Override
public ApiResponse<List<Map<String, Object>>> overdueUserStats() {
return fineService.overdueUsers();
}
/**
* 馆藏分析 — 数据库端 GROUP BY 聚合
*/
@Override
public ApiResponse<Map<String, Object>> collectionAnalysis() {
String cacheKey = "cache:statistics:collection-analysis";
Map<String, Object> cached = getCachedMap(cacheKey);
if (cached != null) {
return ApiResponse.success(cached);
}
List<Map<String, Object>> stats = bookMapper.categoryStats();
Map<String, Object> result = new HashMap<>();
result.put("categories", stats);
result.put("categoryCount", stats.size());
putCachedMap(cacheKey, result);
return ApiResponse.success(result);
}
private Map<String, Object> getCachedMap(String key) {
try {
String cached = cacheService.getString(key).orElse(null);
if (cached != null) {
return objectMapper.readValue(cached, new TypeReference<Map<String, Object>>() {});
}
} catch (Exception e) {
log.warn("统计缓存读取失败,降级查询数据库: {}", e.getMessage());
}
return null;
}
private void putCachedMap(String key, Map<String, Object> value) {
try {
cacheService.setString(key, objectMapper.writeValueAsString(value), STATISTICS_CACHE_TTL);
} catch (Exception e) {
log.warn("统计缓存写入失败,忽略缓存: {}", e.getMessage());
}
}
}