From 130d360556c66b9ca4a15a68716c59b83d0f387f Mon Sep 17 00:00:00 2001 From: siujamo Date: Fri, 15 May 2026 11:32:31 +0800 Subject: [PATCH] feat: add daily password endpoint with Redis caching --- CLAUDE.md | 2 +- .../config/SecurityConfig.java | 3 +- .../controller/DailyPasswordController.java | 27 +++++++ .../domain/dto/DailyPassword.java | 7 ++ .../domain/dto/DailyPasswordData.java | 14 ++++ .../domain/dto/DailyPasswordMetadata.java | 7 ++ .../domain/dto/DailyPasswordResponse.java | 9 +++ .../manager/DailyPasswordManager.java | 76 +++++++++++++++++++ .../service/DailyPasswordService.java | 19 +++++ 9 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/controller/DailyPasswordController.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPassword.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordData.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordMetadata.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordResponse.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/manager/DailyPasswordManager.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/service/DailyPasswordService.java diff --git a/CLAUDE.md b/CLAUDE.md index 5767357..479eb65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,7 +65,7 @@ com.onixbyte.deltaforceguide - **JPA + native queries**: Most CRUD uses Spring Data JPA. Native queries (in `ModificationRepository`) handle JSONB tag filtering with Postgres `@>` operator. - **Custom auth flow**: JWT tokens in httpOnly cookies (`AccessToken`). Spring Security with a custom `UsernamePasswordAuthenticationProvider` and `TokenAuthenticationFilter`. Tokens are auto-renewed within 5 min of expiry. - **JSONB storage**: `Modification.tags` and `Modification.accessories` (including nested `Tuning` objects) are stored as JSONB columns using Hypersistence Utils `JsonType`. -- **Manager layer**: `UserManager` and `UserCredentialManager` sit between service and repository, adding `@Transactional` boundaries without mixing concerns. +- **Strict layering**: The call chain must follow `Controller → Service → Manager → Repository/Mapper`. Skipping layers (e.g. Controller calling Manager directly, Service calling Repository directly) is not permitted. Each layer has a distinct responsibility: Controller handles HTTP concerns, Service contains business logic, Manager manages `@Transactional` boundaries and data access coordination, Repository/Mapper handles raw data access. - **DTOs as Java records**: All request/response objects are immutable records with static `from()` factory methods for entity→DTO conversion. - **Flyway migrations**: SQL migrations in `src/main/resources/db/migration/` — V2 (init), V3 (bullet/damage fields), V4 (user), V5 (accessories JSONB column). diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/SecurityConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/SecurityConfig.java index f77a522..ad9be0e 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/config/SecurityConfig.java +++ b/src/main/java/com/onixbyte/deltaforceguide/config/SecurityConfig.java @@ -57,7 +57,8 @@ public class SecurityConfig { ).permitAll() .requestMatchers(HttpMethod.GET, "/firearms", "/firearms/*", - "/modifications", "/modifications/*" + "/modifications", "/modifications/*", + "/daily-passwords", "/daily-passwords/*" ).permitAll() .anyRequest().authenticated() ) diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/DailyPasswordController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/DailyPasswordController.java new file mode 100644 index 0000000..f817f66 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/DailyPasswordController.java @@ -0,0 +1,27 @@ +package com.onixbyte.deltaforceguide.controller; + +import com.onixbyte.deltaforceguide.domain.dto.DailyPasswordResponse; +import com.onixbyte.deltaforceguide.service.DailyPasswordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "每日密码", description = "获取每日密码信息") +@RestController +@RequestMapping("/daily-passwords") +public class DailyPasswordController { + + private final DailyPasswordService dailyPasswordService; + + public DailyPasswordController(DailyPasswordService dailyPasswordService) { + this.dailyPasswordService = dailyPasswordService; + } + + @Operation(description = "获取当日的每日密码数据,该数据将被缓存一天") + @GetMapping + public DailyPasswordResponse getDailyPassword() { + return dailyPasswordService.getDailyPassword(); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPassword.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPassword.java new file mode 100644 index 0000000..2a381ad --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPassword.java @@ -0,0 +1,7 @@ +package com.onixbyte.deltaforceguide.domain.dto; + +public record DailyPassword( + String mapName, + String password +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordData.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordData.java new file mode 100644 index 0000000..c685542 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordData.java @@ -0,0 +1,14 @@ +package com.onixbyte.deltaforceguide.domain.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public record DailyPasswordData( + String updateDate, + Integer totalCount, + List passwords, + String source, + LocalDateTime lastUpdated, + Long timestamp +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordMetadata.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordMetadata.java new file mode 100644 index 0000000..a2fa36b --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordMetadata.java @@ -0,0 +1,7 @@ +package com.onixbyte.deltaforceguide.domain.dto; + +public record DailyPasswordMetadata( + String version, + String author +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordResponse.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordResponse.java new file mode 100644 index 0000000..7d29a27 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/DailyPasswordResponse.java @@ -0,0 +1,9 @@ +package com.onixbyte.deltaforceguide.domain.dto; + +public record DailyPasswordResponse( + String status, + String message, + DailyPasswordData data, + DailyPasswordMetadata metadata +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/DailyPasswordManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/DailyPasswordManager.java new file mode 100644 index 0000000..3ec28f9 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/DailyPasswordManager.java @@ -0,0 +1,76 @@ +package com.onixbyte.deltaforceguide.manager; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.onixbyte.deltaforceguide.domain.dto.DailyPasswordResponse; +import com.onixbyte.deltaforceguide.exeption.BizException; +import com.onixbyte.deltaforceguide.shared.JacksonModules; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.time.Duration; +import java.time.LocalDate; +import java.util.Objects; + +@Component +public class DailyPasswordManager { + + private static final String CACHE_KEY_PREFIX = "daily-password:"; + + private final RestClient restClient; + private final RedisTemplate redisTemplate; + + @Autowired + public DailyPasswordManager( + RestClient.Builder restClientBuilder, + RedisTemplate redisTemplate + ) { + var snakeCaseMapper = new ObjectMapper(); + snakeCaseMapper.setPropertyNamingStrategy( + PropertyNamingStrategies.SnakeCaseStrategy.INSTANCE); + snakeCaseMapper.configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + snakeCaseMapper.registerModule(JacksonModules.DATE_TIME_MODULE); + + this.restClient = restClientBuilder + .baseUrl("https://tmini.net/api") + .messageConverters(converters -> { + converters.removeIf( + MappingJackson2HttpMessageConverter.class::isInstance); + converters.add( + new MappingJackson2HttpMessageConverter(snakeCaseMapper)); + }) + .build(); + this.redisTemplate = redisTemplate; + } + + public DailyPasswordResponse getDailyPassword() { + var key = CACHE_KEY_PREFIX + LocalDate.now(); + + var cached = redisTemplate.opsForValue().get(key); + if (cached != null) { + return (DailyPasswordResponse) cached; + } + + var response = restClient.get() + .uri((uriBuilder) -> uriBuilder + .path("/sjzmm") + .queryParam("ckey", "") + .queryParam("type", "json") + .build()) + .retrieve() + .body(DailyPasswordResponse.class); + + if (Objects.isNull(response)) { + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, "暂无每日密码数据。"); + } + + redisTemplate.opsForValue().set(key, response, Duration.ofDays(1L)); + return response; + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/DailyPasswordService.java b/src/main/java/com/onixbyte/deltaforceguide/service/DailyPasswordService.java new file mode 100644 index 0000000..3ad3d4e --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/service/DailyPasswordService.java @@ -0,0 +1,19 @@ +package com.onixbyte.deltaforceguide.service; + +import com.onixbyte.deltaforceguide.domain.dto.DailyPasswordResponse; +import com.onixbyte.deltaforceguide.manager.DailyPasswordManager; +import org.springframework.stereotype.Service; + +@Service +public class DailyPasswordService { + + private final DailyPasswordManager dailyPasswordManager; + + public DailyPasswordService(DailyPasswordManager dailyPasswordManager) { + this.dailyPasswordManager = dailyPasswordManager; + } + + public DailyPasswordResponse getDailyPassword() { + return dailyPasswordManager.getDailyPassword(); + } +}