Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

A Practical Guide to Calling WeChat Official Account APIs from Spring Boot

Tech 2

Configuration

Add the following properties to application.yml to store WeChat credentials:

wechat:
  appid: your_app_id
  secret: your_app_secret
  token: your_verification_token
  refreshExpire: 120

The token is used during server verification, and refreshExpire controls how long the access token is kept in the cache.

Server Verification

When you configure the server URL in the WeChat official account dashboard, WeChat sends a GET request to verify ownership. You need an endpoint that checks the signature and returns the echostr parameter.

@RestController
@RequestMapping("/wechat")
public class WechatCallbackController {

    @Value("${wechat.token}")
    private String verifyToken;

    @GetMapping("/verify")
    public String verifySignature(@RequestParam("signature") String signature,
                                  @RequestParam("timestamp") String timestamp,
                                  @RequestParam("nonce") String nonce,
                                  @RequestParam("echostr") String echostr) {
        if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
            return "";
        }
        String[] items = {verifyToken, timestamp, nonce};
        Arrays.sort(items);
        String raw = String.join("", items);
        String hashed = DigestUtils.sha1Hex(raw);
        if (hashed.equals(signature)) {
            return echostr;
        }
        return "";
    }
}

Use any SHA-1 utility (e.g., DigestUtils.sha1Hex from Apache Commons Codec) to compute the hash.

Access Token Management

The access token is required for most API calls. Since it expires after 2 hours, we cache it in Redis.

Define a service that retrieves the token from Redis or fetches a new one from WeChat:

@Service
public class AccessTokenService {

    private static final String TOKEN_URL = 
        "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
    private static final String CACHE_KEY_PREFIX = "wechat:token:";

    @Value("${wechat.appid}")
    private String appId;
    @Value("${wechat.secret}")
    private String secret;
    @Value("${wechat.refreshExpire:120}")
    private long cacheExpireMinutes;

    private final RedisService redisService;

    public String obtainAccessToken() {
        String cacheKey = CACHE_KEY_PREFIX + appId;
        String token = redisService.get(cacheKey);
        if (token != null) {
            return token;
        }
        token = fetchFreshToken();
        redisService.set(cacheKey, token, cacheExpireMinutes * 60);
        return token;
    }

    private String fetchFreshToken() {
        String url = String.format(TOKEN_URL, appId, secret);
        String response = HttpClientUtil.get(url);
        JSONObject json = JSON.parseObject(response);
        String token = json.getString("access_token");
        if (StringUtils.isBlank(token)) {
            throw new RuntimeException("Failed to obtain access token");
        }
        return token;
    }
}

The RedisService is a simple abstraction for Redis operations. You can replace it with RedisTemplate<String, String>.

Material Management

Uploading Permanent Images (URL Only)

Use the following endpoint to upload an image and receive only the URL:

public String uploadImageAndGetUrl(String localFilePath) {
    String ext = FilenameUtils.getExtension(localFilePath).toUpperCase();
    if (!Arrays.asList("JPG","PNG","JPEG","GIF","BMP").contains(ext)) {
        throw new IllegalArgumentException("Unsupported image format");
    }
    String uploadUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s",
        accessTokenService.obtainAccessToken());
    String response = HttpClientUtil.uploadFile(uploadUrl, localFilePath, null);
    JSONObject json = JSON.parseObject(response);
    return json.getString("url");
}

Adding Permanent Material

For general material (image, voice, video, thumb), use the add_material API. The response includes media_id and, for images, a url.

public JSONObject addPermanentMaterial(String filePath, String type, 
                                       String videoTitle, String videoIntro) {
    String addUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=%s&type=%s",
        accessTokenService.obtainAccessToken(), type);
    JSONObject extraFields = null;
    if ("video".equals(type)) {
        extraFields = new JSONObject();
        extraFields.put("title", videoTitle);
        extraFields.put("introduction", videoIntro);
    }
    String response = HttpClientUtil.uploadFile(addUrl, filePath, extraFields);
    return JSON.parseObject(response);
}

Deleting Material

public void deleteMaterial(String mediaId) {
    String deleteUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/material/del_material?access_token=%s",
        accessTokenService.obtainAccessToken());
    JSONObject body = new JSONObject();
    body.put("media_id", mediaId);
    String response = HttpClientUtil.postJson(deleteUrl, body);
    JSONObject result = JSON.parseObject(response);
    if (result.getInteger("errcode") != 0) {
        throw new RuntimeException("Material deletion failed");
    }
}

Draft and Rich‑Text Content

To create a draft or a news article, you need an HTML body with images uploaded to WeChat's servers.

Creating a Draft

public String createDraft(ArticleDraft draft) {
    String draftUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/draft/add?access_token=%s",
        accessTokenService.obtainAccessToken());
    JSONObject payload = new JSONObject();
    JSONArray articles = new JSONArray();
    JSONObject art = (JSONObject) JSON.toJSON(draft);
    articles.add(art);
    payload.put("articles", articles);
    String response = HttpClientUtil.postJson(draftUrl, payload);
    JSONObject json = JSON.parseObject(response);
    return json.getString("media_id");
}

Creating a Permanent News Article

public String createNewsArticle(ArticleDraft article) {
    String newsUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=%s",
        accessTokenService.obtainAccessToken());
    JSONObject payload = new JSONObject();
    JSONArray articles = new JSONArray();
    articles.add(JSON.toJSON(article));
    payload.put("articles", articles);
    String response = HttpClientUtil.postJson(newsUrl, payload);
    return JSON.parseObject(response).getString("media_id");
}

Querying and Deleting Drafts

public JSONObject fetchDraft(String mediaId) {
    String getUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/draft/get?access_token=%s",
        accessTokenService.obtainAccessToken());
    JSONObject body = new JSONObject();
    body.put("media_id", mediaId);
    String response = HttpClientUtil.postJson(getUrl, body);
    return JSON.parseObject(response);
}

public void removeDraft(String mediaId) {
    String deleteUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/draft/delete?access_token=%s",
        accessTokenService.obtainAccessToken());
    JSONObject body = new JSONObject();
    body.put("media_id", mediaId);
    String response = HttpClientUtil.postJson(deleteUrl, body);
    if (JSON.parseObject(response).getInteger("errcode") != 0) {
        throw new RuntimeException("Draft deletion failed");
    }
}

Publishing and Sending Messages

Submit a Draft for Free Publish

public String publishDraft(String mediaId) {
    String publishUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/freepublish/submit?access_token=%s",
        accessTokenService.obtainAccessToken());
    JSONObject body = new JSONObject();
    body.put("media_id", mediaId);
    String response = HttpClientUtil.postJson(publishUrl, body);
    JSONObject json = JSON.parseObject(response);
    if (json.getInteger("errcode") != 0) {
        throw new RuntimeException("Publish submission failed");
    }
    return json.getString("publish_id");
}

Query Publish Status

public JSONObject queryPublishStatus(String publishId) {
    String statusUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/freepublish/get?access_token=%s",
        accessTokenService.obtainAccessToken());
    JSONObject body = new JSONObject();
    body.put("publish_id", publishId);
    String response = HttpClientUtil.postJson(statusUrl, body);
    return JSON.parseObject(response);
}

Delete a Published Article

public void deletePublishedArticle(String articleId) {
    String deleteUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/freepublish/delete?access_token=%s",
        accessTokenService.obtainAccessToken());
    JSONObject body = new JSONObject();
    body.put("article_id", articleId);
    body.put("index", 1);
    String response = HttpClientUtil.postJson(deleteUrl, body);
    if (JSON.parseObject(response).getInteger("errcode") != 0) {
        throw new RuntimeException("Article deletion failed");
    }
}

Mass‑Send to All Followers

public void sendToAllFollowers(String mediaId) {
    String sendUrl = String.format(
        "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=%s",
        accessTokenService.obtainAccessToken());
    JSONObject payload = new JSONObject();
    JSONObject filter = new JSONObject();
    filter.put("is_to_all", true);
    payload.put("filter", filter);
    JSONObject mpnews = new JSONObject();
    mpnews.put("media_id", mediaId);
    payload.put("mpnews", mpnews);
    payload.put("msgtype", "mpnews");
    payload.put("send_ignore_reprint", 1);
    String response = HttpClientUtil.postJson(sendUrl, payload);
    if (JSON.parseObject(response).getInteger("errcode") != 0) {
        throw new RuntimeException("Mass send failed");
    }
}

HTTP Client Utilities

The HttpClientUtil class must handle both JSON payloads and multipart file uploads. Below is a simplified version using HttpURLConnection:

public class HttpClientUtil {

    public static String get(String urlString) {
        // standard GET implementation returning response body
    }

    public static String postJson(String urlString, JSONObject json) {
        // POST with Content-Type: application/json
    }

    public static String uploadFile(String urlString, String filePath, JSONObject videoMeta) {
        // multipart/form-data upload as shown in the original HttpUtils code
        // The boundary and format must match WeChat's requirements exactly.
        // Pay special attention to line breaks (\r\n) and the final boundary.
    }
}

Data Models

@Data
public class ArticleDraft {
    private String title;
    private String author;
    private String digest;
    private String content;           // HTML body, images must use WeChat URLs
    private String content_source_url;
    private String thumb_media_id;
    private int need_open_comment;
    private int only_fans_can_comment;
    // optional crop coordinates for draft (pic_crop_235_1, pic_crop_1_1)
}

Important Workflow Tips

  1. Image handling: Before creating a draft, scan the HTML content for external image URLs, upload each to WeChat (using the uploadimg endpoint), and replace them with the returned WeChat URLs.
  2. Cover image: The thumb_media_id must come from a permanent material (use add_material with type=image).
  3. Video material: When uploading a video, always pass title and introduction as the second form field in the multipart request.
  4. Character limits: Title ≤ 64 charactres, author ≤ 8 characters, digest ≤ 120 characters, content ≤ 20,000 characters.
  5. Unsupported image formats: Even if you rename a .webp file to .jpg, WeChat will reject it. Make sure the actual image format matches the extension.
  6. Multipart formatting: The upload body must include precise \r\n newlines and a trailing boundary with -- ending; otherwise the API returns a generic error.

By following these guidelines you can fully automate content creation and distribution through the WeChat Official Account platform.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.