mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-09 03:27:32 +08:00
用户访问统计使用缓存+定时任务
+ 重要:注释的地方如果没用到@nagisa77可以删除
This commit is contained in:
@@ -44,6 +44,8 @@ public class CachingConfig {
|
|||||||
public static final String VERIFY_CACHE_NAME="openisle_verify";
|
public static final String VERIFY_CACHE_NAME="openisle_verify";
|
||||||
// 发帖频率限制
|
// 发帖频率限制
|
||||||
public static final String LIMIT_CACHE_NAME="openisle_limit";
|
public static final String LIMIT_CACHE_NAME="openisle_limit";
|
||||||
|
// 用户访问统计
|
||||||
|
public static final String VISIT_CACHE_NAME="openisle_visit";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义Redis的序列化器
|
* 自定义Redis的序列化器
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.openisle.repository.UserRepository;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
@@ -26,6 +27,8 @@ import org.springframework.web.cors.CorsConfiguration;
|
|||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
@@ -44,6 +47,8 @@ public class SecurityConfig {
|
|||||||
@Value("${app.website-url}")
|
@Value("${app.website-url}")
|
||||||
private String websiteUrl;
|
private String websiteUrl;
|
||||||
|
|
||||||
|
private final RedisTemplate redisTemplate;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
@@ -208,7 +213,8 @@ public class SecurityConfig {
|
|||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||||
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (auth != null && auth.isAuthenticated() && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) {
|
if (auth != null && auth.isAuthenticated() && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) {
|
||||||
userVisitService.recordVisit(auth.getName());
|
String key = CachingConfig.VISIT_CACHE_NAME+":"+ LocalDate.now();
|
||||||
|
redisTemplate.opsForSet().add(key, auth.getName());
|
||||||
}
|
}
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ public class PostController {
|
|||||||
if (tagId != null) {
|
if (tagId != null) {
|
||||||
tids = java.util.List.of(tagId);
|
tids = java.util.List.of(tagId);
|
||||||
}
|
}
|
||||||
|
// 只需要在请求的一开始统计一次
|
||||||
if (auth != null) {
|
// if (auth != null) {
|
||||||
userVisitService.recordVisit(auth.getName());
|
// userVisitService.recordVisit(auth.getName());
|
||||||
}
|
// }
|
||||||
|
|
||||||
boolean hasCategories = ids != null && !ids.isEmpty();
|
boolean hasCategories = ids != null && !ids.isEmpty();
|
||||||
boolean hasTags = tids != null && !tids.isEmpty();
|
boolean hasTags = tids != null && !tids.isEmpty();
|
||||||
@@ -152,10 +152,10 @@ public class PostController {
|
|||||||
if (tagId != null) {
|
if (tagId != null) {
|
||||||
tids = java.util.List.of(tagId);
|
tids = java.util.List.of(tagId);
|
||||||
}
|
}
|
||||||
|
// 只需要在请求的一开始统计一次
|
||||||
if (auth != null) {
|
// if (auth != null) {
|
||||||
userVisitService.recordVisit(auth.getName());
|
// userVisitService.recordVisit(auth.getName());
|
||||||
}
|
// }
|
||||||
|
|
||||||
return postService.listPostsByViews(ids, tids, page, pageSize)
|
return postService.listPostsByViews(ids, tids, page, pageSize)
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
@@ -177,10 +177,10 @@ public class PostController {
|
|||||||
if (tagId != null) {
|
if (tagId != null) {
|
||||||
tids = java.util.List.of(tagId);
|
tids = java.util.List.of(tagId);
|
||||||
}
|
}
|
||||||
|
// 只需要在请求的一开始统计一次
|
||||||
if (auth != null) {
|
// if (auth != null) {
|
||||||
userVisitService.recordVisit(auth.getName());
|
// userVisitService.recordVisit(auth.getName());
|
||||||
}
|
// }
|
||||||
|
|
||||||
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
|
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
@@ -202,9 +202,10 @@ public class PostController {
|
|||||||
if (tagId != null) {
|
if (tagId != null) {
|
||||||
tids = java.util.List.of(tagId);
|
tids = java.util.List.of(tagId);
|
||||||
}
|
}
|
||||||
if (auth != null) {
|
// 只需要在请求的一开始统计一次
|
||||||
userVisitService.recordVisit(auth.getName());
|
// if (auth != null) {
|
||||||
}
|
// userVisitService.recordVisit(auth.getName());
|
||||||
|
// }
|
||||||
return postService.listFeaturedPosts(ids, tids, page, pageSize)
|
return postService.listFeaturedPosts(ids, tids, page, pageSize)
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ public class UserController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 这个方法似乎没有使用?
|
||||||
@PostMapping("/me/signin")
|
@PostMapping("/me/signin")
|
||||||
public Map<String, Integer> signIn(Authentication auth) {
|
public Map<String, Integer> signIn(Authentication auth) {
|
||||||
int reward = levelService.awardForSignin(auth.getName());
|
int reward = levelService.awardForSignin(auth.getName());
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.openisle.schdule;
|
||||||
|
|
||||||
|
import com.openisle.config.CachingConfig;
|
||||||
|
import com.openisle.model.User;
|
||||||
|
import com.openisle.model.UserVisit;
|
||||||
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.repository.UserVisitRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行计划
|
||||||
|
* 将每天用户访问落库
|
||||||
|
* @author smallclover
|
||||||
|
* @since 2025-09-09
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserVisitScheduler {
|
||||||
|
private final RedisTemplate redisTemplate;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final UserVisitRepository userVisitRepository;
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 5 0 * * ?")// 每天 00:05 执行
|
||||||
|
public void persistDailyVisits(){
|
||||||
|
LocalDate yesterday = LocalDate.now().minusDays(1);
|
||||||
|
String key = CachingConfig.VISIT_CACHE_NAME+":"+ yesterday;
|
||||||
|
Set<String> usernames = redisTemplate.opsForSet().members(key);
|
||||||
|
if(!CollectionUtils.isEmpty(usernames)){
|
||||||
|
for(String username: usernames){
|
||||||
|
User user = userRepository.findByUsername(username).orElse(null);
|
||||||
|
if(user != null){
|
||||||
|
UserVisit userVisit = new UserVisit();
|
||||||
|
userVisit.setUser(user);
|
||||||
|
userVisit.setVisitDate(yesterday);
|
||||||
|
userVisitRepository.save(userVisit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
package com.openisle.service;
|
package com.openisle.service;
|
||||||
|
|
||||||
|
import com.openisle.config.CachingConfig;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.model.UserVisit;
|
import com.openisle.model.UserVisit;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import com.openisle.repository.UserVisitRepository;
|
import com.openisle.repository.UserVisitRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.cache.annotation.CacheConfig;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -17,6 +24,8 @@ public class UserVisitService {
|
|||||||
private final UserVisitRepository userVisitRepository;
|
private final UserVisitRepository userVisitRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
private final RedisTemplate redisTemplate;
|
||||||
|
|
||||||
public boolean recordVisit(String username) {
|
public boolean recordVisit(String username) {
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
@@ -30,10 +39,36 @@ public class UserVisitService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计访问次数,改为从缓存获取/数据库获取
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
public long countVisits(String username) {
|
public long countVisits(String username) {
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
return userVisitRepository.countByUser(user);
|
|
||||||
|
// 如果缓存存在就返回
|
||||||
|
String key1 = CachingConfig.VISIT_CACHE_NAME + ":"+LocalDate.now()+":count:"+username;
|
||||||
|
Integer cached = (Integer) redisTemplate.opsForValue().get(key1);
|
||||||
|
if(cached != null){
|
||||||
|
return cached.longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis Set 检查今天是否访问
|
||||||
|
String todayKey = CachingConfig.VISIT_CACHE_NAME + ":" + LocalDate.now();
|
||||||
|
boolean todayVisited = redisTemplate.opsForSet().isMember(todayKey, username);
|
||||||
|
|
||||||
|
Long visitCount = userVisitRepository.countByUser(user);
|
||||||
|
if (todayVisited) visitCount += 1;
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
LocalDateTime endOfDay = now.toLocalDate().atTime(23, 59, 59);
|
||||||
|
long secondsUntilEndOfDay = Duration.between(now, endOfDay).getSeconds();
|
||||||
|
|
||||||
|
// 写入缓存,设置 TTL,当天剩余时间
|
||||||
|
redisTemplate.opsForValue().set(key1, visitCount, Duration.ofSeconds(secondsUntilEndOfDay));
|
||||||
|
return visitCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long countDau(LocalDate date) {
|
public long countDau(LocalDate date) {
|
||||||
|
|||||||
Reference in New Issue
Block a user