WxMiniProgramHttpClient.java
8.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package com.infoloop.tianting.service.client;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageRequest;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
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;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
import static com.infoloop.tianting.constant.ConfigConstants.MINI_PROGRAM_APP_ID;
import static com.infoloop.tianting.constant.ConfigConstants.MINI_PROGRAM_APP_SECRET;
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class WxMiniProgramHttpClient {
private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appId}&secret={appSecret}";
private static final String PHONE_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={accessToken}";
private static final String SUBSCRIBE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={accessToken}";
private final RestTemplate restTemplate;
private final WxAccessTokenStore wxAccessTokenStore;
@Value(MINI_PROGRAM_APP_ID)
private String appId;
@Value(MINI_PROGRAM_APP_SECRET)
private String appSecret;
public WxUserPhoneResponseDto getUserPhoneInfoByCode(String code) {
try {
String accessToken = fetchStableAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> jsonDataMap = new HashMap<>();
jsonDataMap.put("code", code);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(jsonDataMap, headers);
return fetchPhoneNumber(accessToken, requestEntity);
} catch (Exception e) {
log.error("Error fetching user phone info", e);
throw new IllegalArgumentException("获取用户手机号失败");
}
}
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);
throw new IllegalArgumentException("获取 access_token 失败");
}
final var wxUserTokenDto = JsonUtil.readJsonAs(tokenResponse.getBody(), WxUserTokenDto.class);
log.info("WeChat access token received: {}", wxUserTokenDto);
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) {
log.info("Fetching phone number...");
ResponseEntity<WxUserPhoneResponseDto> phoneResponse = restTemplate.exchange(PHONE_URL, HttpMethod.POST, requestEntity, WxUserPhoneResponseDto.class, accessToken);
if (!phoneResponse.getStatusCode().is2xxSuccessful() || phoneResponse.getBody() == null) {
log.error("Failed to fetch phone number: {}", phoneResponse);
throw new IllegalArgumentException("获取手机号失败");
}
log.info("WeChat phone number response: {}", phoneResponse.getBody());
return phoneResponse.getBody();
}
public WxUserOpenIdDto getUserOpenIdByCode(String code) {
try {
final var url = "https://api.weixin.qq.com/sns/jscode2session" +
"?appid=" + appId + "&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code";
final var response = restTemplate.getForEntity(url, String.class);
return JsonUtil.readJsonAs(response.getBody(), WxUserOpenIdDto.class);
} catch (Exception e) {
log.error("Error fetching user openid", e);
throw new IllegalArgumentException("获取用户openid失败");
}
}
/**
* 发送订阅消息
* @param openId 用户 OpenID
* @param templateId 模板 ID
* @param page 点击跳转页面路径
* @param data 模板数据,格式为 Map<String, Map<String, String>>,例如:{"thing1": {"value": "内容"}}
* @return 发送结果
*/
public SendSubscriptionMessageResponse sendSubscriptionMessage(String openId, String templateId, String page, java.util.Map<String, Object> data) {
try {
String accessToken = fetchStableAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
SendSubscriptionMessageRequest request = SendSubscriptionMessageRequest.builder()
.touser(openId)
.template_id(templateId)
.page(page)
.data(data)
.build();
HttpEntity<SendSubscriptionMessageRequest> requestEntity = new HttpEntity<>(request, headers);
ResponseEntity<SendSubscriptionMessageResponse> response = restTemplate.postForEntity(
SUBSCRIBE_MESSAGE_URL, requestEntity, SendSubscriptionMessageResponse.class, accessToken);
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
log.error("Failed to send subscription message: {}", response);
throw new IllegalArgumentException("发送订阅消息失败");
}
SendSubscriptionMessageResponse result = response.getBody();
if (result.getErrcode() != null && result.getErrcode() != 0) {
log.error("WeChat subscription message error: errcode={}, errmsg={}", result.getErrcode(), result.getErrmsg());
}
return result;
} catch (Exception e) {
log.error("Error sending subscription message", e);
throw new IllegalArgumentException("发送订阅消息失败: " + e.getMessage());
}
}
}