zhuyifan

feat(service): 实现微信 access_token 缓存机制

......@@ -7,6 +7,7 @@ import com.infoloop.tianting.server.StorageService;
import com.infoloop.tianting.server.StorageServiceFactory;
import com.infoloop.tianting.store.LoginCodeStore;
import com.infoloop.tianting.store.WeeklyTaskStore;
import com.infoloop.tianting.store.WxAccessTokenStore;
import com.infoloop.tianting.utils.SmsUtil;
import io.lettuce.core.api.StatefulRedisConnection;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -33,6 +34,12 @@ public class AppConfig {
}
@Bean
public WxAccessTokenStore wxAccessTokenStore(@Autowired @Qualifier(REDIS_CONNECTION) final StatefulRedisConnection<String, String> connection) {
final var commands = connection.sync();
return new WxAccessTokenStore(commands);
}
@Bean
public WeeklyTaskStore weeklyTaskStore(@Autowired @Qualifier(REDIS_CONNECTION) final StatefulRedisConnection<String, String> connection) {
final var commands = connection.sync();
final var expireSeconds = Duration.ofDays(8).getSeconds();
......
......@@ -4,6 +4,8 @@ public class RedisKeyConstants {
public static final String REPEAT_SUBMIT = "REPEAT_SUBMIT";
public static final String WX_ACCESS_TOKEN = "WX_ACCESS_TOKEN";
public static String getSMSCodeKey(String phone) {
return "sms_code:" + phone;
}
......
......@@ -16,14 +16,7 @@ public class WxUserDto {
@NoArgsConstructor
@AllArgsConstructor
public static class WxUserOpenIdDto {
public String openid;
}
@Data
@Builder
@ApiModel(description = "WxUserOpenIdDto")
public static class WxUserOpenIdDto1 {
public String openId;
private String openid;
}
@Data
......@@ -32,7 +25,8 @@ public class WxUserDto {
@NoArgsConstructor
@AllArgsConstructor
public static class WxUserTokenDto {
public String access_token;
private String access_token;
private int expires_in;
}
@Data
......@@ -41,8 +35,8 @@ public class WxUserDto {
@NoArgsConstructor
@AllArgsConstructor
public static class WxUserPhoneInfoDto {
public String phoneNumber;
public String countryCode;
private String phoneNumber;
private String countryCode;
}
......@@ -52,19 +46,10 @@ public class WxUserDto {
@NoArgsConstructor
@AllArgsConstructor
public static class WxUserPhoneResponseDto {
public Integer errcode;
public String errmsg;
public String phone;
public WxUserPhoneInfoDto phone_info;
}
@Data
@Builder
@ApiModel(description = "WxUserCodeDto")
@NoArgsConstructor
@AllArgsConstructor
public static class WxUserCodeDto {
public String code;
private Integer errcode;
private String errmsg;
private String phone;
private WxUserPhoneInfoDto phone_info;
}
}
......
......@@ -3,9 +3,11 @@ package com.infoloop.tianting.service.client;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserOpenIdDto;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserPhoneResponseDto;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserTokenDto;
import com.infoloop.tianting.store.WxAccessTokenStore;
import com.infoloop.tianting.utils.JsonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
......@@ -32,6 +34,8 @@ public class WxMiniProgramHttpClient {
private final RestTemplate restTemplate;
private final WxAccessTokenStore wxAccessTokenStore;
@Value(MINI_PROGRAM_APP_ID)
private String appId;
......@@ -40,7 +44,7 @@ public class WxMiniProgramHttpClient {
public WxUserPhoneResponseDto getUserPhoneInfoByCode(String code) {
try {
String accessToken = fetchAccessToken();
String accessToken = fetchStableAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> jsonDataMap = new HashMap<>();
......@@ -55,6 +59,11 @@ public class WxMiniProgramHttpClient {
private String fetchAccessToken() {
log.info("Fetching WeChat access token...");
final var code = wxAccessTokenStore.getAccessToken();
if (StringUtils.isNotEmpty(code)) {
log.info("Using cached access token: {}", code);
return code;
}
ResponseEntity<String> tokenResponse = restTemplate.getForEntity(TOKEN_URL, String.class, appId, appSecret);
if (!tokenResponse.getStatusCode().is2xxSuccessful() || tokenResponse.getBody() == null) {
log.error("Failed to fetch access token: {}", tokenResponse);
......@@ -62,7 +71,45 @@ public class WxMiniProgramHttpClient {
}
final var wxUserTokenDto = JsonUtil.readJsonAs(tokenResponse.getBody(), WxUserTokenDto.class);
log.info("WeChat access token received: {}", wxUserTokenDto);
return wxUserTokenDto.getAccess_token();
final var accessToken = wxUserTokenDto.getAccess_token();
wxAccessTokenStore.setAccessToken(accessToken, wxUserTokenDto.getExpires_in());
return accessToken;
}
private final Object lock = new Object();
public String fetchStableAccessToken() {
log.info("Fetching WeChat Stable access token...");
String code = wxAccessTokenStore.getAccessToken();
if (StringUtils.isNotEmpty(code)) {
log.info("Using cached Stable access token: {}", code);
return code;
}
synchronized (lock) {
code = wxAccessTokenStore.getAccessToken();
if (StringUtils.isNotEmpty(code)) {
log.info("Using cached Stable access token after lock: {}", code);
return code;
}
String url = "https://api.weixin.qq.com/cgi-bin/stable_token";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, String> requestBody = new HashMap<>();
requestBody.put("grant_type", "client_credential");
requestBody.put("appid", appId);
requestBody.put("secret", appSecret);
HttpEntity<Map<String, String>> request = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
log.error("Failed to fetch Stable access token: {}", response);
throw new IllegalArgumentException("获取 stable access_token 失败");
}
WxUserTokenDto tokenDto = JsonUtil.readJsonAs(response.getBody(), WxUserTokenDto.class);
log.info("WeChat Stable access token received: {}", tokenDto);
final var accessToken = tokenDto.getAccess_token();
wxAccessTokenStore.setAccessToken(accessToken, tokenDto.getExpires_in());
return accessToken;
}
}
private WxUserPhoneResponseDto fetchPhoneNumber(String accessToken, HttpEntity<Map<String, Object>> requestEntity) {
......
package com.infoloop.tianting.store;
import com.infoloop.tianting.constant.RedisKeyConstants;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.sync.RedisCommands;
public class WxAccessTokenStore {
private final RedisCommands<String, String> commands;
public WxAccessTokenStore(final RedisCommands<String, String> commands) {
this.commands = commands;
}
public void setAccessToken(final String accessToken, final int expire) {
commands.set(RedisKeyConstants.WX_ACCESS_TOKEN, accessToken, new SetArgs().ex(expire));
}
public String getAccessToken() {
return commands.get((RedisKeyConstants.WX_ACCESS_TOKEN));
}
}