/cgi-bin/token 獲取 access_token(有效期 7200 秒,建議緩存)/cgi-bin/message/mass/sendall 或 /cgi-bin/message/mass/send 發送errcode 判斷結果,處理重試與錯誤碼<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
public class WechatApiClient {
private static final Logger log = LoggerFactory.getLogger(WechatApiClient.class);
private static final String API_BASE = "https://api.weixin.qq.com/cgi-bin";
private static final String TOKEN_URL = API_BASE + "/token?grant_type=client_credential&appid=%s&secret=%s";
private static final String MASS_SEND_ALL_URL = API_BASE + "/message/mass/sendall?access_token=%s";
private static final String MASS_SEND_URL = API_BASE + "/message/mass/send?access_token=%s";
private final CloseableHttpClient httpClient;
private final ObjectMapper mapper = new ObjectMapper(); // 或 FastJson
public WechatApiClient() {
// 配置連接池、SSL 信任所有 (生產環境請配置正規憑證)
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registry);
cm.setMaxTotal(20);
cm.setDefaultMaxPerRoute(10);
this.httpClient = HttpClients.custom().setConnectionManager(cm).build();
}
/** 獲取 access_token,建議外層緩存 7000 秒 */
public String getAccessToken(String appId, String appSecret) throws IOException {
String url = String.format(TOKEN_URL, appId, appSecret);
HttpGet get = new HttpGet(url);
try (CloseableHttpResponse resp = httpClient.execute(get)) {
String body = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
JsonNode node = mapper.readTree(body);
if (node.has("errcode")) {
throw new WechatApiException(node.get("errcode").asInt(), node.get("errmsg").asText());
}
return node.get("access_token").asText();
}
}
/** 群發給所有關注者 */
public String massSendAll(String accessToken, String msgType, Object content) throws IOException {
String url = String.format(MASS_SEND_ALL_URL, accessToken);
return executeMassSend(url, buildMassMessage(true, null, msgType, content));
}
/** 群發給指定 OpenID 列表 */
public String massSendToUsers(String accessToken, List<String> openIds, String msgType, Object content) throws IOException {
String url = String.format(MASS_SEND_URL, accessToken);
return executeMassSend(url, buildMassMessage(false, openIds, msgType, content));
}
private ObjectNode buildMassMessage(boolean toAll, List<String> openIds, String msgType, Object content) {
ObjectNode root = mapper.createObjectNode();
if (toAll) {
root.putObject("filter").put("is_to_all", true);
} else {
ObjectNode filter = root.putObject("filter");
filter.put("is_to_all", false);
ArrayNode arr = filter.putArray("touser");
openIds.forEach(arr::add);
}
root.put("msgtype", msgType);
root.set(msgType, mapper.valueToTree(content));
return root;
}
private String executeMassSend(String url, ObjectNode payload) throws IOException {
HttpPost post = new HttpPost(url);
post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
post.setEntity(new StringEntity(mapper.writeValueAsString(payload), StandardCharsets.UTF_8));
try (CloseableHttpResponse resp = httpClient.execute(post)) {
String body = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
log.info("Wechat mass send response: {}", body);
return body;
}
}
public void close() throws IOException { httpClient.close(); }
}
// 發送給所有人
String textContent = "{\"content\":\"歡迎關注我們的公眾號!\"}";
client.massSendAll(token, "text", textContent);
// 發送給指定用戶
List<String> openIds = Arrays.asList("oUser1", "oUser2");
client.massSendToUsers(token, openIds, "text", textContent);
String newsContent = "{\"articles\":[{\"title\":\"標題\",\"description\":\"描述\",\"url\":\"https://xxx.com\",\"picurl\":\"https://xxx.com/img.jpg\"}]}";
client.massSendAll(token, "news", newsContent);
String tmplContent = "{\"touser\":\"OPENID\",\"template_id\":\"TMPL_ID\",\"url\":\"https://xxx.com\",\"data\":{\"first\":{\"value\":\"您好\"},\"keyword1\":{\"value\":\"訂單號\"}}}";
// 模板消息使用 /cgi-bin/message/template/send 接口,非群發接口
@RestController
@RequestMapping("/wechat")
public class WechatController {
@Autowired private WechatApiClient wechatClient;
@Value("${wechat.appId}") private String appId;
@Value("${wechat.appSecret}") private String appSecret;
@GetMapping("/sendAllText")
public Result sendAllText(@RequestParam String content) {
try {
String token = wechatClient.getAccessToken(appId, appSecret);
String resp = wechatClient.massSendAll(token, "text", "{\"content\":\"" + content + "\"}");
return Result.ok(resp);
} catch (Exception e) {
log.error("群發失敗", e);
return Result.fail(e.getMessage());
}
}
}
45015 (回覆超時)、45047 (群發頻率超限)、40003 (OpenID 非法) 等需具體業務重試或告警。TrustAll 策略。MaxTotal、MaxPerRoute,配置 RequestConfig 超時時間。基於 HttpClient 4.5+ 實現微信群發核心在於:標準化 HTTP 客端配置、JSON 序列化與微信接口協議對齊、Access Token 生命週期管理、錯誤碼語義化處理。封裝為可復用 Client 組件,配合 Spring Boot 外部化配置,即可快速接入微信消息觸達能力。