feat: add daily password endpoint with Redis caching
This commit is contained in:
@@ -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.
|
- **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.
|
- **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`.
|
- **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.
|
- **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).
|
- **Flyway migrations**: SQL migrations in `src/main/resources/db/migration/` — V2 (init), V3 (bullet/damage fields), V4 (user), V5 (accessories JSONB column).
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ public class SecurityConfig {
|
|||||||
).permitAll()
|
).permitAll()
|
||||||
.requestMatchers(HttpMethod.GET,
|
.requestMatchers(HttpMethod.GET,
|
||||||
"/firearms", "/firearms/*",
|
"/firearms", "/firearms/*",
|
||||||
"/modifications", "/modifications/*"
|
"/modifications", "/modifications/*",
|
||||||
|
"/daily-passwords", "/daily-passwords/*"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.onixbyte.deltaforceguide.domain.dto;
|
||||||
|
|
||||||
|
public record DailyPassword(
|
||||||
|
String mapName,
|
||||||
|
String password
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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<DailyPassword> passwords,
|
||||||
|
String source,
|
||||||
|
LocalDateTime lastUpdated,
|
||||||
|
Long timestamp
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.onixbyte.deltaforceguide.domain.dto;
|
||||||
|
|
||||||
|
public record DailyPasswordMetadata(
|
||||||
|
String version,
|
||||||
|
String author
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.onixbyte.deltaforceguide.domain.dto;
|
||||||
|
|
||||||
|
public record DailyPasswordResponse(
|
||||||
|
String status,
|
||||||
|
String message,
|
||||||
|
DailyPasswordData data,
|
||||||
|
DailyPasswordMetadata metadata
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public DailyPasswordManager(
|
||||||
|
RestClient.Builder restClientBuilder,
|
||||||
|
RedisTemplate<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user