需求:同步公众号的文章到第三方系统,然后可以编辑,并且显示
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;|\"|'|>|<|\\)$|\\}$)+$", "")
.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" +
" " + DateUtils.format(articleVO.getCreateTime(), DateUtils.PATTERN_DATETIME) + "\n" +
" </div>\n" +
"</div>" + articleVO.getContent() + "</body></html>";
}
7.然后又发现,公众号的视频无法播放!!!这个暂时没有解决,只能让运营重新上传了...
1139

被折叠的 条评论
为什么被折叠?



