服務號需主動向關注用戶推送交易提醒、發貨通知、預約確認等服務通知。微信規定:只能使用審核通過的模板,按模板 ID + 參數結構發送,不支持自定義內容隨意發送。
AppID/AppSecret。net.sf.json-lib 或 Jackson/Gson(推薦 Jackson)。POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
Content-Type: application/json
{
"touser": "OPENID",
"template_id": "TEMPLATE_ID",
"url": "https://example.com/detail",
"topcolor": "#FF0000",
"data": {
"first": {"value": "尊敬的用戶,您有一筆新訂單", "color": "#173177"},
"keyword1": {"value": "訂單號: 20240115001", "color": "#173177"},
"keyword2": {"value": "金額: ¥299.00", "color": "#173177"},
"remark": {"value": "請及時處理", "color": "#173177"}
}
}
public class TemplateMessage {
private String touser;
private String template_id;
private String url;
private String topcolor = "#FF0000";
private Map data = new LinkedHashMap<>(); // 保持順序
public static class DataItem {
private String value;
private String color;
// getter/setter
}
// getter/setter + 靜態構建器方法
}
public class AccessTokenManager {
private static final String APP_ID = "your_appid";
private static final String APP_SECRET = "your_secret";
private static volatile String accessToken;
private static volatile long expireTime = 0;
public static synchronized String getAccessToken() {
if (System.currentTimeMillis() < expireTime) return accessToken;
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + APP_ID + "&secret=" + APP_SECRET;
String resp = HttpClientUtil.get(url);
JSONObject json = JSONObject.fromObject(resp);
accessToken = json.getString("access_token");
expireTime = System.currentTimeMillis() + (json.getInt("expires_in") - 200) * 1000L; // 提前 200s 刷新
return accessToken;
}
}
public boolean sendTemplateMsg(TemplateMessage msg) {
String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + AccessTokenManager.getAccessToken();
String json = JSONObject.fromObject(msg).toString();
String resp = HttpClientUtil.postJson(url, json);
JSONObject result = JSONObject.fromObject(resp);
return result.getInt("errcode") == 0;
}
public List getAllOpenIds() {
List openIds = new ArrayList<>();
String nextOpenId = null;
do {
String url = "https://api.weixin.qq.com/cgi-bin/user/get?access_token=" + AccessTokenManager.getAccessToken();
if (nextOpenId != null) url += "&next_openid=" + nextOpenId;
String resp = HttpClientUtil.get(url);
JSONObject json = JSONObject.fromObject(resp);
if (json.getInt("errcode") != 0) break;
JSONArray arr = json.getJSONObject("data").getJSONArray("openid");
for (int i = 0; i < arr.size(); i++) openIds.add(arr.getString(i));
nextOpenId = json.getString("next_openid");
} while (nextOpenId != null && !nextOpenId.isEmpty());
return openIds;
}
public void broadcastTransactionNotice() {
List openIds = getAllOpenIds();
for (String openId : openIds) {
String nickname = getUserNickname(openId); // 可選,個性化
TemplateMessage msg = TemplateMessage.buildTransaction(openId, "TMPL_ID_123", "https://detail.url", nickname);
boolean ok = sendTemplateMsg(msg);
log.info("發送 {} -> {}", openId, ok ? "成功" : "失敗");
// 限流:每秒不超過 100 條,避免封禁
Thread.sleep(10);
}
}
| 坑點 | 解決 |
|---|---|
| AccessToken 過期/並發刷新 | 雙重檢查鎖 + volatile,或用 Redis 分布式鎖管理 |
| 模板參數名/順序不匹配 | 嚴格對照公眾平台模板詳情,用 LinkedHashMap 保證序列化順序 |
| data 字段 value 過長 | 單個 value ≤ 1024 字節,超長會報錯 40058 |
| 用戶未關注/取消關注 | 發送返回 43004,需捕獲並從本地庫移除 |
| 頻率限制 | 接口調用上限:10萬/天(認證服務號),並發建議 ≤ 100/s |
| HTTPS 證書驗證 | 生產環境必須啟用 SSL 驗證,勿關閉 |
手寫 HTTP/JSON 易出錯,推薦接入:
SDK 內置 AccessToken 自動刷新、模板消息構建器、異步發送、限流熔斷,生產級可靠。
模板消息是服務號觸達用戶的核心通道。掌握「模板審核 → JSON 結構構建 → AccessToken 管理 → 分頁獲取用戶 → 限流發送」全鏈路,並遷移到成熟 SDK,才能構建穩定、合規、可擴展的消息通知系統。