Merge pull request #205 from nagisa77/codex/add-stat-module-for-dau-tracking

Add DAU stats filter and endpoint
This commit is contained in:
Tim
2025-07-14 21:22:19 +08:00
committed by GitHub
7 changed files with 110 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
package com.openisle.config;
import com.openisle.service.JwtService;
import com.openisle.service.UserVisitService;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
@@ -38,6 +39,7 @@ public class SecurityConfig {
private final JwtService jwtService;
private final UserRepository userRepository;
private final AccessDeniedHandler customAccessDeniedHandler;
private final UserVisitService userVisitService;
@Bean
public PasswordEncoder passwordEncoder() {
@@ -103,7 +105,8 @@ public class SecurityConfig {
.requestMatchers("/api/admin/**").hasAuthority("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(userVisitFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@@ -150,4 +153,18 @@ public class SecurityConfig {
}
};
}
@Bean
public OncePerRequestFilter userVisitFilter() {
return new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) {
userVisitService.recordVisit(auth.getName());
}
filterChain.doFilter(request, response);
}
};
}
}

View File

@@ -0,0 +1,26 @@
package com.openisle.controller;
import com.openisle.service.UserVisitService;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.Map;
@RestController
@RequestMapping("/api/stats")
@RequiredArgsConstructor
public class StatController {
private final UserVisitService userVisitService;
@GetMapping("/dau")
public Map<String, Long> dau(@RequestParam(value = "date", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
long count = userVisitService.countDau(date);
return Map.of("dau", count);
}
}

View File

@@ -10,4 +10,5 @@ import java.util.Optional;
public interface UserVisitRepository extends JpaRepository<UserVisit, Long> {
Optional<UserVisit> findByUserAndVisitDate(User user, LocalDate visitDate);
long countByUser(User user);
long countByVisitDate(LocalDate visitDate);
}

View File

@@ -32,4 +32,9 @@ public class UserVisitService {
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
return userVisitRepository.countByUser(user);
}
public long countDau(LocalDate date) {
LocalDate d = date != null ? date : LocalDate.now();
return userVisitRepository.countByVisitDate(d);
}
}

View File

@@ -11,6 +11,7 @@ import com.openisle.config.CustomAccessDeniedHandler;
import com.openisle.config.SecurityConfig;
import com.openisle.service.JwtService;
import com.openisle.repository.UserRepository;
import com.openisle.service.UserVisitService;
import com.openisle.model.Role;
import com.openisle.model.User;
import java.util.Optional;
@@ -31,6 +32,8 @@ class AdminControllerTest {
private JwtService jwtService;
@MockBean
private UserRepository userRepository;
@MockBean
private UserVisitService userVisitService;
@Test
void adminHelloReturnsMessage() throws Exception {

View File

@@ -11,6 +11,7 @@ import com.openisle.config.CustomAccessDeniedHandler;
import com.openisle.config.SecurityConfig;
import com.openisle.service.JwtService;
import com.openisle.repository.UserRepository;
import com.openisle.service.UserVisitService;
import com.openisle.model.Role;
import com.openisle.model.User;
import java.util.Optional;
@@ -31,6 +32,8 @@ class HelloControllerTest {
private JwtService jwtService;
@MockBean
private UserRepository userRepository;
@MockBean
private UserVisitService userVisitService;
@Test
void helloReturnsMessage() throws Exception {

View File

@@ -0,0 +1,54 @@
package com.openisle.controller;
import com.openisle.config.CustomAccessDeniedHandler;
import com.openisle.config.SecurityConfig;
import com.openisle.service.JwtService;
import com.openisle.repository.UserRepository;
import com.openisle.service.UserVisitService;
import com.openisle.model.Role;
import com.openisle.model.User;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Optional;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(StatController.class)
@AutoConfigureMockMvc
@Import({SecurityConfig.class, CustomAccessDeniedHandler.class})
class StatControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private JwtService jwtService;
@MockBean
private UserRepository userRepository;
@MockBean
private UserVisitService userVisitService;
@Test
void dauReturnsCount() throws Exception {
Mockito.when(jwtService.validateAndGetSubject("token")).thenReturn("user");
User user = new User();
user.setUsername("user");
user.setPassword("p");
user.setEmail("u@example.com");
user.setRole(Role.USER);
Mockito.when(userRepository.findByUsername("user")).thenReturn(Optional.of(user));
Mockito.when(userVisitService.countDau(Mockito.any())).thenReturn(3L);
mockMvc.perform(get("/api/stats/dau").header("Authorization", "Bearer token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.dau").value(3));
}
}