解放双手!微信公众号自动推文全攻略

377 阅读6分钟

一、引言(痛点+价值)

  1. 为什么需要自动推文?
    • 手动发布低效、容易遗漏
    • 多平台同步需求(如博客→公众号)
  2. 本文能学到什么?
    • 通过API/工具实现定时/触发式发布
    • 代码示例(Java)

二、准备工作

  1. 微信公众号权限配置
    • 开通开发者权限
    • 获取AppID和AppSecret
  2. 准备工具依赖
    • Java:okhttp3

三、实现方案

  1. 增加配置项 application.yml或bootstrap.yml
weChat:
  api:
    appId: appId(需替换成自己的)
    appSecret: appSecret(需替换成自己的)
    token:
      url: https://api.weixin.qq.com/cgi-bin/token
    draft:
      url: https://api.weixin.qq.com/cgi-bin/draft/add
  1. 定时触发
@Slf4j
@Component
public class WeChatSchedule {

    @Resource
    DeepSeekDriver deepSeekDriver;
    @Resource
    WeChatDriver weChatDriver;
    @Resource
    IBoChaService boChaService;

    @Scheduled(cron = "0 30 8 * * ?")
    // 本地调试
    // @Scheduled(cron = "0/59 * * * * ?")
    public void syncWeChat() {
        this.sendWeChat();
    }


    /**
     * 通过deepseek以及博查api获取最新消息
     **/
    private void sendWeChat() {
        String searchPrompt = "查询本地的天气,全球最新的三条新闻(当前时间:" + TimeUtils.timestampToDate(new Date().getTime() / 1000) + ")";
        String searchResult = boChaService.getBoChaMessage(searchPrompt);
        log.info("联网查询结果:{}", searchResult);
        String prompt = "生成一篇微信公众号文章,要求:风趣幽默些,在文章末尾增加心灵鸡汤,本地当天的天气(以联网搜索内容为准,不得自创),以及全球最新的三条新闻(以联网搜索内容为准,不得自创)(【联网搜索结果】\n" + searchResult + ")\n\n回答要求: " + "\n- 基于搜索结果提供准确信息" + "\n- 禁止列出信息来源,禁止提供来源链接,禁止回复联网搜索的内容" + "\n- 保持回答简洁专业";
        Mono<String> aiContext = deepSeekDriver.chatCompletion(prompt, false, "deepseek")
                .filter(DeepSeekResponseEvent.class::isInstance) // 过滤出响应事件
                .map(DeepSeekResponseEvent.class::cast)
                .map(DeepSeekResponseEvent::getContent) // 提取内容
                .collectList() // 收集所有内容到List
                .map(list -> String.join("", list));// 拼接所有内容
        aiContext.subscribe(completeResponse -> {
            String completeMessage = completeResponse.replace("[DONE]", "");
            log.info("AI生成结果: {}", completeMessage);
            // 微信推文接口
            weChatDriver.sendWeChatMessages(completeMessage);
        });
    }
}
  1. 微信服务接口
// 公共
@Value("${weChat.api.token.url}")
private String tokenUrl;

@Value("${weChat.api.draft.url}")
private String draftUrl;

@Value("${weChat.api.appId}")
private String appId;

@Value("${weChat.api.appSecret}")
private String appSecret;

private final OkHttpClient client = new OkHttpClient();

@Resource
private ObjectMapper mapper;
  • 获取token
//定时每天调用一次,故并未缓存token
public String getWeChatToken() {
    String url = tokenUrl + "?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret;
    Request request = new Request.Builder().url(url).build();
    Response response = null;
    try {
        response = client.newCall(request).execute();
        if (null == response.body() || !response.isSuccessful()) {
            log.error("微信token接口请求失败,{}", response);
            throw new OperateException("微信token获取失败");
        }
        String json = response.body().string();
        log.info("微信token接口返回数据:{}", json);
        WeChatTokenResponse weChatTokenResponse = mapper.readValue(json, WeChatTokenResponse.class);
        return weChatTokenResponse.getToken();
    } catch (Exception e) {
        log.error("微信token接口调用失败,{}", Throwables.getStackTraceAsString(e));
        throw new OperateException("微信token获取失败");
    } finally {
        try {
            if (response != null && response.body() != null) {
                response.body().close();
            }
        } catch (Exception e) {
            log.error("关闭失败");
        }
    }
}
  • 创建草稿图文消息

接口入参请参考微信公众号官方文档)

private String createDraft(WeChatArticleRequest weChatArticleRequest) {
    String url = draftUrl + "?access_token=" + getWeChatToken();

    Response response = null;
    try {
        Map<String, Object> requestBodyMap = new HashMap<>(16);
        requestBodyMap.put("articles", List.of(weChatArticleRequest));
        String requestJson = mapper.writeValueAsString(requestBodyMap);
        log.info("微信创建草稿接口请求数据:{}", requestJson);
        RequestBody requestBody = RequestBody.create(requestJson, MediaType.parse("application/json; charset=utf-8"));
        Request request = new Request.Builder().url(url).post(requestBody).addHeader("Content-Type", "application/json").build();

        response = client.newCall(request).execute();
        if (null == response.body() || !response.isSuccessful()) {
            log.error("微信创建草稿接口请求失败,{}", response);
            throw new OperateException("微信创建草稿获取失败");
        }
        String json = response.body().string();
        log.info("微信创建草稿接口返回数据:{}", json);
        HashMap mediaIdMap = mapper.readValue(json, HashMap.class);
        return mediaIdMap.get("media_id").toString();
    } catch (Exception e) {
        log.error("微信创建草稿接口调用失败,{}", Throwables.getStackTraceAsString(e));
        throw new OperateException("微信创建草稿获取失败");
    } finally {
        try {
            if (response != null && response.body() != null) {
                response.body().close();
            }
        } catch (Exception e) {
            log.error("关闭失败");
        }
    }
}
  1. 微信公众号文章格式转换(markdown-->wx文章)
@Slf4j
@Component
public class MarkdownToHtmlUtil {

    private MarkdownToHtmlUtil() {
        // 初始化
    }

    // 微信公众号常用样式映射
    private static final Map<String, String> WECHAT_STYLES = new HashMap<>();

    static {
        WECHAT_STYLES.put("h1", "style="font-size:22px; font-weight:bold; color:#333; margin-top:20px; margin-bottom:10px;"");
        WECHAT_STYLES.put("h2", "style="font-size:20px; font-weight:bold; color:#333; margin-top:18px; margin-bottom:8px;"");
        WECHAT_STYLES.put("h3", "style="font-size:18px; font-weight:bold; color:#333; margin-top:16px; margin-bottom:6px;"");
        WECHAT_STYLES.put("p", "style="font-size:16px; line-height:1.8; color:#666; margin-bottom:15px;"");
        WECHAT_STYLES.put("pre", "style="background-color:#f8f8f8; border-radius:4px; padding:10px; margin-bottom:15px; overflow-x:auto;"");
        WECHAT_STYLES.put("code", "style="font-family:Consolas,Monaco,'Andale Mono',monospace; font-size:14px; color:#c7254e; background-color:#f9f2f4; border-radius:3px; padding:0 3px;"");
        WECHAT_STYLES.put("blockquote", "style="border-left:4px solid #ddd; padding-left:15px; color:#777; margin:10px 0;"");
        WECHAT_STYLES.put("ul", "style="margin:10px 0; padding-left:30px;"");
        WECHAT_STYLES.put("ol", "style="margin:10px 0; padding-left:30px;"");
        WECHAT_STYLES.put("li", "style="margin-bottom:5px;"");
        WECHAT_STYLES.put("img", "style="max-width:100%; height:auto; display:block; margin:10px auto;"");
        WECHAT_STYLES.put("table", "style="border-collapse:collapse; width:100%; margin-bottom:15px;"");
        WECHAT_STYLES.put("th", "style="border:1px solid #ddd; padding:8px; background-color:#f2f2f2;"");
        WECHAT_STYLES.put("td", "style="border:1px solid #ddd; padding:8px;"");
    }

    public static String convert(String markdown) {
        // 预处理Markdown文本,移除列表中的空行
        markdown = markdown.replace("\n", "\n");
        markdown = preprocessMarkdown(markdown);

        // 配置 Markdown 解析器,支持表格和标题锚点
        List<Extension> extensions = Arrays.asList(TablesExtension.create(), HeadingAnchorExtension.create());

        // 创建解析器
        Parser parser = Parser.builder().extensions(extensions).build();

        // 解析 Markdown 文本
        Node document = parser.parse(markdown);

        // 创建 HTML 渲染器,并添加自定义属性
        HtmlRenderer renderer = HtmlRenderer.builder().extensions(extensions).attributeProviderFactory(context -> new CustomAttributeProvider()).build();

        // 渲染为 HTML
        String html = renderer.render(document);

        // 应用微信公众号样式
        return applyWechatStyles(html);
    }

    private static String preprocessMarkdown(String markdown) {
        StringBuilder processed = new StringBuilder();
        String[] lines = markdown.split("\n");

        boolean inOrderedList = false;
        boolean inUnorderedList = false;

        for (String line : lines) {
            line = line.trim();

            // 检查是否是有序列表项
            boolean isOrderedListItem = line.matches("^\d+\.\s.*");

            // 检查是否是无序列表项
            boolean isUnorderedListItem = line.matches("^[*+-]\s.*");

            // 处理有序列表
            if (isOrderedListItem) {
                inOrderedList = true;
                inUnorderedList = false;
                processed.append(line).append("\n");
            }
            // 处理无序列表
            else if (isUnorderedListItem) {
                inUnorderedList = true;
                inOrderedList = false;
                processed.append(line).append("\n");
            }
            // 处理列表中的空行
            else if ((inOrderedList || inUnorderedList) && line.isEmpty()) {
                // 跳过列表中的空行
                continue;
            } else {
                // 处理非列表内容
                inOrderedList = false;
                inUnorderedList = false;
                processed.append(line).append("\n");
            }
        }

        return processed.toString();
    }

    private static String applyWechatStyles(String html) {
        StringBuilder styledHtml = new StringBuilder();
        styledHtml.append("<div style="font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue','PingFang SC','Hiragino Sans GB','Droid Sans Fallback','Microsoft YaHei',sans-serif;">");

        for (Map.Entry<String, String> entry : WECHAT_STYLES.entrySet()) {
            String tag = entry.getKey();
            String style = entry.getValue();

            // 处理开始标签
            html = html.replaceAll("<" + tag + "(?!\s*style=)(\s|>)", "<" + tag + " " + style + "$1");

            // 处理自闭合标签
            html = html.replaceAll("<" + tag + "\s*" + style + "\s*/>", "<" + tag + " " + style + " />");
        }

        styledHtml.append(html.replace("\n", "").replace("\n", ""));
        styledHtml.append("</div>");
        return styledHtml.toString();
    }

    // 自定义属性提供器,用于添加额外的 HTML 属性
    private static class CustomAttributeProvider implements AttributeProvider {
        @Override
        public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
            // 为链接添加 target="_blank"
            if ("a".equals(tagName)) {
                attributes.put("target", "_blank");
            }
        }
    }
}
  1. 手动发布

因还未进行公众号认证,现只能在app或者官网手动发布

四、避坑指南

  • API每日有调用限额,请参考微信公众号官方文档
  • IP白名单未配置,接口无法调用。配置路径:公众号官网/设置与开发/开发接口管理/IP白名单

五、总结

  1. 自动化的优点
  • 内容生产效率提升3-5倍(常规选题)
  • 推送时效性从小时级压缩至分钟级
  • 人力成本降低40-60%(标准运营团队)
  1. 自动化的缺点
  • 内容风险:易出现敏感词(建立动态库,发布前过滤)
  • 法律风险:AI生成的内容权限认定

六、结语

通过本文的介绍,详细探讨了如何利用Java和微信公众号API实现自动推文功能。从权限配置到代码实现,再到避坑指南,我们一步步拆解了自动化推文的全流程。虽然当前方案仍需手动发布草稿(受限于公众号认证状态),但核心的自动化内容生成和草稿创建已能显著提升效率。

未来优化方向

  1. 结合OAuth2.0实现全自动发布(需认证服务号)
  2. 增加多账号管理能力
  3. 集成敏感词过滤系统

技术永远是为场景服务的工具,期待看到大家更有创意的实现方式。如果你有更巧妙的方案(比如用Python简化流程/低代码工具搭配),欢迎在评论区分享交流!

保持迭代,持续自动化 🚀

(注:本文代码已脱敏,实际使用时请替换为自己的AppID和密钥)

未完结。。。