zhuyifan

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

...@@ -7,6 +7,7 @@ import com.infoloop.tianting.server.StorageService; ...@@ -7,6 +7,7 @@ import com.infoloop.tianting.server.StorageService;
7 import com.infoloop.tianting.server.StorageServiceFactory; 7 import com.infoloop.tianting.server.StorageServiceFactory;
8 import com.infoloop.tianting.store.LoginCodeStore; 8 import com.infoloop.tianting.store.LoginCodeStore;
9 import com.infoloop.tianting.store.WeeklyTaskStore; 9 import com.infoloop.tianting.store.WeeklyTaskStore;
10 +import com.infoloop.tianting.store.WxAccessTokenStore;
10 import com.infoloop.tianting.utils.SmsUtil; 11 import com.infoloop.tianting.utils.SmsUtil;
11 import io.lettuce.core.api.StatefulRedisConnection; 12 import io.lettuce.core.api.StatefulRedisConnection;
12 import org.springframework.beans.factory.annotation.Autowired; 13 import org.springframework.beans.factory.annotation.Autowired;
...@@ -33,6 +34,12 @@ public class AppConfig { ...@@ -33,6 +34,12 @@ public class AppConfig {
33 } 34 }
34 35
35 @Bean 36 @Bean
37 + public WxAccessTokenStore wxAccessTokenStore(@Autowired @Qualifier(REDIS_CONNECTION) final StatefulRedisConnection<String, String> connection) {
38 + final var commands = connection.sync();
39 + return new WxAccessTokenStore(commands, 7000);
40 + }
41 +
42 + @Bean
36 public WeeklyTaskStore weeklyTaskStore(@Autowired @Qualifier(REDIS_CONNECTION) final StatefulRedisConnection<String, String> connection) { 43 public WeeklyTaskStore weeklyTaskStore(@Autowired @Qualifier(REDIS_CONNECTION) final StatefulRedisConnection<String, String> connection) {
37 final var commands = connection.sync(); 44 final var commands = connection.sync();
38 final var expireSeconds = Duration.ofDays(8).getSeconds(); 45 final var expireSeconds = Duration.ofDays(8).getSeconds();
......
...@@ -4,6 +4,8 @@ public class RedisKeyConstants { ...@@ -4,6 +4,8 @@ public class RedisKeyConstants {
4 4
5 public static final String REPEAT_SUBMIT = "REPEAT_SUBMIT"; 5 public static final String REPEAT_SUBMIT = "REPEAT_SUBMIT";
6 6
7 + public static final String WX_ACCESS_TOKEN = "WX_ACCESS_TOKEN";
8 +
7 public static String getSMSCodeKey(String phone) { 9 public static String getSMSCodeKey(String phone) {
8 return "sms_code:" + phone; 10 return "sms_code:" + phone;
9 } 11 }
......
...@@ -3,9 +3,11 @@ package com.infoloop.tianting.service.client; ...@@ -3,9 +3,11 @@ package com.infoloop.tianting.service.client;
3 import com.infoloop.tianting.model.dto.WxUserDto.WxUserOpenIdDto; 3 import com.infoloop.tianting.model.dto.WxUserDto.WxUserOpenIdDto;
4 import com.infoloop.tianting.model.dto.WxUserDto.WxUserPhoneResponseDto; 4 import com.infoloop.tianting.model.dto.WxUserDto.WxUserPhoneResponseDto;
5 import com.infoloop.tianting.model.dto.WxUserDto.WxUserTokenDto; 5 import com.infoloop.tianting.model.dto.WxUserDto.WxUserTokenDto;
6 +import com.infoloop.tianting.store.WxAccessTokenStore;
6 import com.infoloop.tianting.utils.JsonUtil; 7 import com.infoloop.tianting.utils.JsonUtil;
7 import lombok.RequiredArgsConstructor; 8 import lombok.RequiredArgsConstructor;
8 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
10 +import org.apache.commons.lang3.StringUtils;
9 import org.springframework.beans.factory.annotation.Autowired; 11 import org.springframework.beans.factory.annotation.Autowired;
10 import org.springframework.beans.factory.annotation.Value; 12 import org.springframework.beans.factory.annotation.Value;
11 import org.springframework.http.HttpEntity; 13 import org.springframework.http.HttpEntity;
...@@ -32,6 +34,8 @@ public class WxMiniProgramHttpClient { ...@@ -32,6 +34,8 @@ public class WxMiniProgramHttpClient {
32 34
33 private final RestTemplate restTemplate; 35 private final RestTemplate restTemplate;
34 36
37 + private final WxAccessTokenStore wxAccessTokenStore;
38 +
35 @Value(MINI_PROGRAM_APP_ID) 39 @Value(MINI_PROGRAM_APP_ID)
36 private String appId; 40 private String appId;
37 41
...@@ -40,7 +44,7 @@ public class WxMiniProgramHttpClient { ...@@ -40,7 +44,7 @@ public class WxMiniProgramHttpClient {
40 44
41 public WxUserPhoneResponseDto getUserPhoneInfoByCode(String code) { 45 public WxUserPhoneResponseDto getUserPhoneInfoByCode(String code) {
42 try { 46 try {
43 - String accessToken = fetchAccessToken(); 47 + String accessToken = fetchStableAccessToken();
44 HttpHeaders headers = new HttpHeaders(); 48 HttpHeaders headers = new HttpHeaders();
45 headers.setContentType(MediaType.APPLICATION_JSON); 49 headers.setContentType(MediaType.APPLICATION_JSON);
46 Map<String, Object> jsonDataMap = new HashMap<>(); 50 Map<String, Object> jsonDataMap = new HashMap<>();
...@@ -55,6 +59,11 @@ public class WxMiniProgramHttpClient { ...@@ -55,6 +59,11 @@ public class WxMiniProgramHttpClient {
55 59
56 private String fetchAccessToken() { 60 private String fetchAccessToken() {
57 log.info("Fetching WeChat access token..."); 61 log.info("Fetching WeChat access token...");
62 + final var code = wxAccessTokenStore.getAccessToken();
63 + if (StringUtils.isNotEmpty(code)) {
64 + log.info("Using cached access token: {}", code);
65 + return code;
66 + }
58 ResponseEntity<String> tokenResponse = restTemplate.getForEntity(TOKEN_URL, String.class, appId, appSecret); 67 ResponseEntity<String> tokenResponse = restTemplate.getForEntity(TOKEN_URL, String.class, appId, appSecret);
59 if (!tokenResponse.getStatusCode().is2xxSuccessful() || tokenResponse.getBody() == null) { 68 if (!tokenResponse.getStatusCode().is2xxSuccessful() || tokenResponse.getBody() == null) {
60 log.error("Failed to fetch access token: {}", tokenResponse); 69 log.error("Failed to fetch access token: {}", tokenResponse);
...@@ -62,7 +71,36 @@ public class WxMiniProgramHttpClient { ...@@ -62,7 +71,36 @@ public class WxMiniProgramHttpClient {
62 } 71 }
63 final var wxUserTokenDto = JsonUtil.readJsonAs(tokenResponse.getBody(), WxUserTokenDto.class); 72 final var wxUserTokenDto = JsonUtil.readJsonAs(tokenResponse.getBody(), WxUserTokenDto.class);
64 log.info("WeChat access token received: {}", wxUserTokenDto); 73 log.info("WeChat access token received: {}", wxUserTokenDto);
65 - return wxUserTokenDto.getAccess_token(); 74 + final var accessToken = wxUserTokenDto.getAccess_token();
75 + wxAccessTokenStore.setAccessToken(accessToken);
76 + return accessToken;
77 + }
78 +
79 + public String fetchStableAccessToken() {
80 + log.info("Fetching WeChat access token...");
81 + final var code = wxAccessTokenStore.getAccessToken();
82 + if (StringUtils.isNotEmpty(code)) {
83 + log.info("Using cached access token: {}", code);
84 + return code;
85 + }
86 + String url = "https://api.weixin.qq.com/cgi-bin/stable_token";
87 + HttpHeaders headers = new HttpHeaders();
88 + headers.setContentType(MediaType.APPLICATION_JSON);
89 + Map<String, String> requestBody = new HashMap<>();
90 + requestBody.put("grant_type", "client_credential");
91 + requestBody.put("appid", appId);
92 + requestBody.put("secret", appSecret);
93 + HttpEntity<Map<String, String>> request = new HttpEntity<>(requestBody, headers);
94 + ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
95 + if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
96 + log.error("Failed to fetch access token: {}", response);
97 + throw new IllegalArgumentException("获取 access_token 失败");
98 + }
99 + WxUserTokenDto tokenDto = JsonUtil.readJsonAs(response.getBody(), WxUserTokenDto.class);
100 + log.info("WeChat access token received: {}", tokenDto);
101 + final var accessToken = tokenDto.getAccess_token();
102 + wxAccessTokenStore.setAccessToken(accessToken);
103 + return accessToken;
66 } 104 }
67 105
68 private WxUserPhoneResponseDto fetchPhoneNumber(String accessToken, HttpEntity<Map<String, Object>> requestEntity) { 106 private WxUserPhoneResponseDto fetchPhoneNumber(String accessToken, HttpEntity<Map<String, Object>> requestEntity) {
......
1 +package com.infoloop.tianting.store;
2 +
3 +import com.infoloop.tianting.constant.RedisKeyConstants;
4 +import io.lettuce.core.SetArgs;
5 +import io.lettuce.core.api.sync.RedisCommands;
6 +
7 +public class WxAccessTokenStore {
8 +
9 + private final RedisCommands<String, String> commands;
10 +
11 + private final long expire;
12 +
13 + public WxAccessTokenStore(final RedisCommands<String, String> commands, final long expire) {
14 + this.commands = commands;
15 + this.expire = expire;
16 + }
17 +
18 + public void setAccessToken(final String accessToken) {
19 + commands.set(RedisKeyConstants.WX_ACCESS_TOKEN, accessToken, new SetArgs().ex(expire));
20 + }
21 +
22 + public String getAccessToken() {
23 + return commands.get((RedisKeyConstants.WX_ACCESS_TOKEN));
24 + }
25 +}