开发记录:同步公众号文章

GPT-oss:20b

GPT-oss:20b

图文对话
Gpt-oss

GPT OSS 是OpenAI 推出的重量级开放模型,面向强推理、智能体任务以及多样化开发场景

需求:同步公众号的文章到第三方系统,然后可以编辑,并且显示

1、开始想通过获取永久素材来实现(https://developers.weixin.qq.com/doc/subscription/api/material/permanent/api_batchgetmaterial.html),但是发现公众号现在没有保存图文素材了,新的文章就获取不到了,所以放弃了这个方案。

2.然后想通过获取已发布的消息列表来实现(https://developers.weixin.qq.com/doc/subscription/api/public/api_freepublish_batchget.html),但是发现这个这接口只能获取到使用发布能力发布的文章,而运营一般使用群发消息来确保所有人能收到消息,所以也放弃了这个方案。

3.至于群发消息,在群发消息的接口里没有获取历史群发消息的接口,所以想通过群发消息来实现也不行。

4.最后经过商讨,决定使用草稿的接口来实现(https://developers.weixin.qq.com/doc/subscription/api/draftbox/draftmanage/api_draft_batchget.html),让运营在发送文章时保存一份草稿(也可以在新增文章草稿时选择已有内容),然后再第三方系统点击同步草稿的内容。

5.但是虽然内容可以在微信小程序上显示,却因为防盗链在第三方显示不了微信公众号的图片,所以决定先下载到本地上传到阿里云OSS之后再使用,包括封面图以及内容中的图片。

package com.winkeytech.util;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springblade.core.oss.model.BladeFile;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springframework.stereotype.Component;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @Description: 微信公众号文章工具类
 */
@Slf4j
@Component
@AllArgsConstructor
public class WxMpArticleUtil {

	// 匹配所有 http/https 图片 URL(通用),微信图片常用域名
	private static final Pattern IMAGE_URL_PATTERN = Pattern.compile(
		"https?://(?:[a-zA-Z0-9.-]*\\.)?(?:mmbiz\\.qpic\\.cn|mmbiz\\.qlogo\\.cn|wxpic\\.com|wx\\.qlogo\\.cn)[^\"'\\s>)}]*",
		Pattern.CASE_INSENSITIVE
	);

	private final ResourceFeign resourceFeign;

	public String uploadImage(String imageUrl) {
		if (imageUrl == null || !imageUrl.startsWith("http")) {
			return null;
		}

		try {
			// 1. 提取文件扩展名(根据 URL 或 Content-Type)
			String ext = extractFileExtension(imageUrl);

			// 2. 生成安全的唯一文件名(不含特殊字符)
			String safeFileName = UUID.randomUUID().toString().replace("-", "") + ext;

			// 3. 获取图片字节流
			URL url = new URL(imageUrl);
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; WeChatImageBot/1.0)");
			conn.setConnectTimeout(5000);
			conn.setReadTimeout(10000);

			// 可选:从响应头获取真实类型(更准确)
			String contentType = conn.getContentType();
			if (contentType != null && !ext.equals(getExtensionFromMimeType(contentType))) {
				// 如果 URL 扩展名和实际类型不符,以实际类型为准
				ext = getExtensionFromMimeType(contentType);

				// 如果不是图片的地址,则跳过
				if (Func.isBlank(ext)) {
					return null;
				}

				safeFileName = safeFileName.substring(0, safeFileName.lastIndexOf('.')) + ext;
			}

			// 4. 上传到阿里云OSS,这里是上传到阿里云OSS,可以使用自己存储图片的方法
			R<File> fileR = this.resourceFeign.putFile(MultipartFileUtils.createMultipartFile("file", safeFileName, contentType, conn.getInputStream()));
			if (!fileR.isSuccess()) {
				log.error("❌ 图片上传阿里云OSS失败:{}=====结果:{}", imageUrl, Func.toJson(fileR));
				return null;
			}

			log.info("✅ 图片获取成功:{}", imageUrl);

			return fileR.getData().getLink();
		} catch (Exception e) {
			log.info("❌ 图片获取成功:{}", imageUrl);
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 处理公众号内容
	 */
	public String getContent(String content) {
		if (content == null || content.isEmpty()) {
			return content;
		}

		// 1. 提取所有疑似图片 URL
		Set<String> allUrls = extractAllImageUrls(content);

		// 3. 上传并构建映射
		Map<String, String> urlMapping = new HashMap<>();
		for (String url : allUrls) {
			try {
				String ossUrl = this.uploadImage(url);
				if (Func.isBlank(ossUrl)) {
					continue;
				}
				urlMapping.put(url, ossUrl);
			} catch (Exception e) {
				System.err.println("上传失败: " + url + " | " + e.getMessage());
				log.error("公众号文章内容图片上传失败:{}=====结果:{}", url, e.getMessage());
			}
		}

		// 先按 key 长度排序(长的在前)
		List<Map.Entry<String, String>> sortedEntries = urlMapping.entrySet()
			.stream()
			.sorted((e1, e2) -> Integer.compare(e2.getKey().length(), e1.getKey().length()))
			.collect(Collectors.toList());

		// 用普通 for 循环替换
		for (Map.Entry<String, String> entry : sortedEntries) {
			content = content.replace(entry.getKey(), entry.getValue());
		}

		// 处理html标签信息
		Document doc = Jsoup.parse(content);
		Elements imgTags = doc.select("img");

		for (Element img : imgTags) {
			String src = img.attr("src");

			// 判断src是否有值,如果没有则使用data-src的值
			if (Func.isBlank(src)) {
				src = img.attr("data-src");
				img.attr("src", src);
			}

		}

		// 设置输出时不 pretty print
		doc.outputSettings(new Document.OutputSettings().prettyPrint(false));
		// 更新后的 HTML 内容
		return doc.body().html();
	}

	// 从 URL 提取扩展名
	private String extractFileExtension(String url) {
		// 移除查询参数(?后面的内容)
		String cleanUrl = url.split("\\?")[0].toLowerCase();

		if (cleanUrl.endsWith(".jpg") || cleanUrl.endsWith(".jpeg")) {
			return ".jpg";
		} else if (cleanUrl.endsWith(".png")) {
			return ".png";
		} else if (cleanUrl.endsWith(".gif")) {
			return ".gif";
		} else if (cleanUrl.endsWith(".webp")) {
			return ".webp";
		} else {
			// 默认为 jpg
			return ".jpg";
		}
	}

	// 从 MIME 类型获取扩展名(更可靠)
	private String getExtensionFromMimeType(String mimeType) {
		if (mimeType == null) return ".jpg";
		switch (mimeType.toLowerCase()) {
			case "image/jpeg":
			case "image/jpg":
				return ".jpg";
			case "image/png":
				return ".png";
			case "image/gif":
				return ".gif";
			case "image/webp":
				return ".webp";
			default:
				return "";
		}
	}

	/**
	 * 提取文本中所有可能的图片 URL
	 */
	private Set<String> extractAllImageUrls(String text) {
		Set<String> urls = new LinkedHashSet<>();
		Matcher matcher = IMAGE_URL_PATTERN.matcher(text);
		while (matcher.find()) {
			String url = matcher.group().trim();
			// 清理可能的尾部干扰字符(如引号、括号)
			url = url.replaceAll("[\"'\\)>]+$", "")
				// 去除尾部的 HTML 实体或引号类干扰
				.replaceAll("(&quot;|&#34;|&gt;|&lt;|&amp;quot;|\"|'|>|<|\\)$|\\}$)+$", "")
				.trim();
			if (!url.isEmpty()) {
				urls.add(url);
			}
		}
		return urls;
	}

}

6.后续再小程序的测试时发现,在rich-text标签中svg标签显示不了,所以改用web-view标签进行显示了,所以需要一个返回HTML的接口。

<web-view style="width: calc(100% - 30rpx * 2);margin: 0 auto calc(55rpx + env(safe-area-inset-bottom));"
  src="{{BASE_API}}/article/mp-show/{{article.id}}">
</web-view>
    @GetMapping(value = "mp-show/{id}", produces = MediaType.TEXT_HTML_VALUE)
	@ApiOperation(value = "公众号文章显示", notes = "公众号文章显示")
	@ApiOperationSupport(order = 7)
	@IgnoreSecurity
	public String mpShow(@PathVariable Long id) {
		ArticleVO articleVO = articleService.detail(id);
		return "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>" + articleVO.getTitle() + "</title></head><body style=\"padding-bottom: calc(55rpx + env(safe-area-inset-bottom));\"><div class=\"title\" style=\"\n" +
			"  font-size: 24px;\n" +
			"  color: #333;\n" +
			"  font-weight: 600;\n" +
			"  margin-bottom: 10px;\n" +
			"  margin-top: 20px;\n" +
			"  box-sizing: border-box;\n" +
			"  padding: 0 10rpx;\n" +
			"  text-align: center;\n" +
			"\">\n" +
			"  " + articleVO.getTitle() + "\n" +
			"</div>\n" +
			"\n" +
			"<div class=\"info\" style=\"\n" +
			"  box-sizing: border-box;\n" +
			"  padding: 0 10px;\n" +
			"  display: flex;\n" +
			"  justify-content: space-between;\n" +
			"  font-size: 14px;\n" +
			"  font-weight: 500;\n" +
			"  color: #999;\n" +
			"  margin-bottom: 20px;\n" +
			"  position: relative;\n" +
			"  z-index: 1;\n" +
			"\">\n" +
			"  <div class=\"author\" style=\"\">\n" +
			"    " + articleVO.getAuthor() + "\n" +
			"  </div>\n" +
			"  <div class=\"time\" style=\"flex-grow: 1;\">\n" +
			"    &nbsp;" + DateUtils.format(articleVO.getCreateTime(), DateUtils.PATTERN_DATETIME) + "\n" +
			"  </div>\n" +
			"</div>" + articleVO.getContent() + "</body></html>";
	}

7.然后又发现,公众号的视频无法播放!!!这个暂时没有解决,只能让运营重新上传了...

您可能感兴趣的与本文相关的镜像

GPT-oss:20b

GPT-oss:20b

图文对话
Gpt-oss

GPT OSS 是OpenAI 推出的重量级开放模型,面向强推理、智能体任务以及多样化开发场景

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值