视频号下载视频思路
要实现「个微登录企微后转发视频号 → 企微回调获取参数 → 解析视频链接 → 小程序推送」,核心是 适配企微外部联系人(个微登录场景)+ 回调消息解析 + 视频链接拼接 + 小程序消息推送,全程遵循企微官方接口规范,以下是 完整实现方案(含配置 + 代码 + 测试),聚焦「个微登录(外部联系人)」场景适配:
一、核心逻辑与前提说明
1. 完整流程(个微登录场景)
plaintext
个微用户 → 添加企微外部联系人 → 转发视频号到企微聊天框 → 企微触发回调 → 后端解密消息提取 object_id/object_nonce_id → 调用视频号详情接口拼接播放/下载链接 → 后端调用企微发送小程序接口 → 个微用户收到小程序通知 → 点击小程序直接播放/下载视频
2. 关键前提(必须满足)
- 企微已创建应用(需完成企业认证,个人未认证企业无法使用外部联系人功能);
- 应用已开启「外部联系人」权限(允许接收个微用户的消息);
- 企微已关联目标小程序(用于发送小程序消息);
- 后端服务器支持 HTTPS(企微回调强制要求),公网可访问;
- 个微用户已添加企微外部联系人(通过企微对外名片添加)。
二、第一步:企微后台核心配置(适配个微登录场景)
1. 应用基础配置(接收外部联系人消息)
登录企微管理后台 → 应用管理 → 选择你的应用(如「视频下载助手」):
(1)配置「接收消息」(回调核心)
- 开启「接收消息」开关;
- 选择「加密模式」(推荐,安全且兼容外部联系人消息);
- 填写配置:
- 回调 URL:
https://你的公网域名/wechat/callback(HTTPS + 公网可达,路径与后端接口一致); - Token:自定义字符串(如
wxcp_video_2025),需与后端application.yml一致; - EncodingAESKey:点击「随机生成」,复制到后端配置(43 位字符,需完全一致);
- 回调 URL:
- 勾选「接收外部联系人消息」(关键!适配个微登录场景,否则收不到个微用户的转发消息)。
(2)配置「外部联系人权限」
- 应用详情 → 权限管理 → 找到「外部联系人」相关权限:
- 勾选「获取外部联系人基本信息」「接收外部联系人消息」「发送外部联系人消息」;
- 应用可见范围:
- 内部成员:添加企微内部成员(用于接收个微用户的转发消息);
- 外部联系人:无需额外设置,个微用户添加企微外部联系人后即可互动。
(3)关联小程序(发送小程序消息必备)
- 企微管理后台 → 应用管理 → 小程序 → 点击「关联小程序」;
- 输入你的小程序 AppID → 小程序管理员在微信上确认授权;
- 关联后,在「可见范围」中勾选「外部联系人」(允许个微用户接收该小程序消息)。
2. 验证回调有效性
- 配置完成后,点击「回调验证」按钮,提示「验证成功」说明企微能正常访问你的后端;
- 若验证失败:检查 HTTPS 证书是否有效、URL 是否公网可达、Token/AESKey 是否一致。
三、第二步:服务器 / 网络配置(确保企微能回调)
1. 本地开发(用 ngrok 穿透,无需云服务器)
- 下载 ngrok:https://ngrok.com/;
- 执行命令:
ngrok http 8080(生成临时 HTTPS 域名,如https://abc123.ngrok.io); - 同步更新企微回调 URL 为
https://abc123.ngrok.io/wechat/callback; - 保持 ngrok 终端开启(关闭后穿透失效)。
2. 线上部署(云服务器,如阿里云 / 腾讯云)
- 配置安全组:放行 8080 端口(后端端口)和 443 端口(HTTPS);
- 部署 HTTPS 证书:用 Let's Encrypt 免费证书或云厂商 SSL 证书,通过 Nginx 反向代理:
nginx
server {listen 443 ssl;server_name yourdomain.com; # 你的域名ssl_certificate /usr/local/nginx/conf/cert/yourdomain.pem; # 证书公钥ssl_certificate_key /usr/local/nginx/conf/cert/yourdomain.key; # 证书私钥location /wechat/callback {proxy_pass http://127.0.0.1:8080; # 转发到后端proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;} }
三、第三步:后端代码实现(核心逻辑)
基于 Spring Boot + 企微 SDK 4.7.8-20251105.104605,适配外部联系人(个微登录)场景,核心实现「回调接收→参数提取→视频解析→小程序推送」。
1. 依赖配置(pom.xml)
xml
<dependencies><!-- Spring Boot核心 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 企微SDK(支持外部联系人+小程序消息) --><dependency><groupId>com.github.binarywang</groupId><artifactId>weixin-java-cp</artifactId><version>4.7.8-20251105.104605</version></dependency><!-- HTTP客户端 --><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.11.0</version></dependency><!-- JSON解析 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.32</version></dependency><!-- 日志 --><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></dependency>
</dependencies><!-- 快照仓库(拉取企微SDK快照版) -->
<repositories><repository><id>sonatype-snapshots</id><url>https://oss.sonatype.org/content/repositories/snapshots/</url><snapshots><enabled>true</enabled></snapshots></repository>
</repositories>
2. 配置文件(application.yml)
yaml
wechat:cp:corp-id: 你的企微CorpID # 我的企业→企业信息corp-secret: 你的应用Secret # 应用管理→你的应用→Secretagent-id: 你的应用AgentID # 应用管理→你的应用→AgentID(整数)callback-token: 你的回调Token # 与企微后台一致aes-key: 你的EncodingAESKey # 与企微后台一致(43位)miniapp:appid: 你的小程序AppID # 小程序后台→开发→开发设置page-path: /pages/play/index # 小程序播放/下载页面路径channels:detail-api-url: "https://channels.weixin.qq.com/web/api/feed/detail?eid=export%{objectId}_%{nonceId}"server:port: 8080servlet:context-path: /
3. 企微配置类(初始化 SDK)
java
运行
import com.github.binarywang.weixin.cp.config.WxCpConfigStorage;
import com.github.binarywang.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import com.github.binarywang.weixin.cp.service.WxCpService;
import com.github.binarywang.weixin.cp.service.impl.WxCpServiceImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class WxCpConfig {@Value("${wechat.cp.corp-id}")private String corpId;@Value("${wechat.cp.corp-secret}")private String corpSecret;@Value("${wechat.cp.agent-id}")private Integer agentId;@Value("${wechat.cp.callback-token}")private String callbackToken;@Value("${wechat.cp.aes-key}")private String aesKey;@Beanpublic WxCpConfigStorage wxCpConfigStorage() {WxCpDefaultConfigImpl config = new WxCpDefaultConfigImpl();config.setCorpId(corpId);config.setCorpSecret(corpSecret);config.setAgentId(agentId);config.setToken(callbackToken);config.setAesKey(aesKey);config.setHttpConnectTimeoutMs(10000);config.setHttpReadTimeoutMs(30000);return config;}@Beanpublic WxCpService wxCpService(WxCpConfigStorage configStorage) {WxCpServiceImpl service = new WxCpServiceImpl();service.setWxCpConfigStorage(configStorage);// 启用外部联系人消息支持(适配个微登录场景)service.getExternalContactService();return service;}
}
4. 回调接口(接收企微消息,提取核心参数)
java
运行
import com.github.binarywang.weixin.cp.bean.message.WxCpXmlMessage;
import com.github.binarywang.weixin.cp.bean.message.WxCpXmlOutMessage;
import com.github.binarywang.weixin.cp.service.WxCpService;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/wechat/callback")
public class WxCpCallbackController {private static final Logger log = LoggerFactory.getLogger(WxCpCallbackController.class);@Autowiredprivate WxCpService wxCpService;@Autowiredprivate VideoService videoService;/*** 企微回调验证(GET请求)*/@GetMappingpublic String verifyCallback(@RequestParam("msg_signature") String msgSignature,@RequestParam("timestamp") String timestamp,@RequestParam("nonce") String nonce,@RequestParam("echostr") String echostr) {try {String decryptEchostr = wxCpService.decryptEchoStr(msgSignature, timestamp, nonce, echostr);log.info("回调验证成功:{}", decryptEchostr);return decryptEchostr;} catch (Exception e) {log.error("回调验证失败", e);return "验证失败";}}/*** 接收企微消息(POST请求,适配内部/外部联系人)*/@PostMapping(produces = "application/xml;charset=UTF-8")public String receiveMessage(@RequestParam("msg_signature") String msgSignature,@RequestParam("timestamp") String timestamp,@RequestParam("nonce") String nonce,@RequestBody String requestBody) {try {log.info("接收企微消息:{}", requestBody);// 1. 解密消息WxCpXmlMessage inMessage = WxCpXmlMessage.fromXml(requestBody);inMessage = wxCpService.decryptMessage(inMessage, msgSignature, timestamp, nonce);log.info("解密后消息:{}", inMessage);// 2. 筛选视频号视频消息(msgType=sphfeed,feedType=4)if ("sphfeed".equals(inMessage.getMsgType()) && "4".equals(inMessage.getFeedType())) {String objectId = inMessage.getObjectId();String nonceId = inMessage.getObjectNonceId();String receiverId = inMessage.getToUser(); // 接收人ID(内部成员userId/外部联系人openId)String senderId = inMessage.getFromUser(); // 发送人ID(个微用户=openId,企微用户=userId)// 校验参数if (Strings.isNullOrEmpty(objectId) || Strings.isNullOrEmpty(nonceId)) {log.error("参数缺失:objectId={}, nonceId={}", objectId, nonceId);return buildReply("获取视频信息失败");}// 3. 解析视频链接并发送小程序消息String videoUrl = videoService.parseVideoUrl(objectId, nonceId);if (videoUrl != null) {// 适配外部联系人:发送给个微用户(senderId是外部联系人openId)videoService.sendMiniProgramMessage(senderId, videoUrl);return buildReply("视频解析成功,小程序通知已发送~");} else {return buildReply("视频解析失败,请稍后重试");}}return buildReply("暂不支持该类型消息");} catch (Exception e) {log.error("处理消息异常", e);return buildReply("处理失败");}}/*** 构建企微回复消息*/private String buildReply(String content) {WxCpXmlOutMessage outMessage = WxCpXmlOutMessage.TEXT().content(content).fromUser(wxCpService.getWxCpConfigStorage().getCorpId()).toUser("").build();return outMessage.toXml();}
}
5. 视频解析与小程序推送服务
java
运行
import com.alibaba.fastjson.JSONObject;
import com.github.binarywang.weixin.cp.bean.WxCpMessage;
import com.github.binarywang.weixin.cp.service.WxCpService;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;import java.net.URLEncoder;
import java.util.concurrent.TimeUnit;@Service
public class VideoService {private static final Logger log = LoggerFactory.getLogger(VideoService.class);private final OkHttpClient okHttpClient;@Autowiredprivate WxCpService wxCpService;@Value("${wechat.channels.detail-api-url}")private String videoDetailApiUrl;@Value("${wechat.miniapp.appid}")private String miniAppId;@Value("${wechat.miniapp.page-path}")private String miniPagePath;public VideoService() {this.okHttpClient = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).retryOnConnectionFailure(true).build();}/*** 调用视频号详情接口,拼接播放/下载链接(url + urlToken)*/public String parseVideoUrl(String objectId, String nonceId) {try {// 1. 拼接视频号详情接口URLString apiUrl = videoDetailApiUrl.replace("{objectId}", objectId).replace("{nonceId}", nonceId);log.info("调用视频号详情接口:{}", apiUrl);// 2. 模拟浏览器请求头(避免被拦截)Request request = new Request.Builder().url(apiUrl).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36").header("Referer", "https://channels.weixin.qq.com/").header("Accept", "application/json, text/plain, */*").build();Response response = okHttpClient.newCall(request).execute();if (!response.isSuccessful()) {log.error("接口调用失败,状态码:{}", response.code());return null;}// 3. 解析响应,提取url和urlTokenString responseBody = response.body().string();JSONObject result = JSONObject.parseObject(responseBody);JSONObject data = result.getJSONObject("data");if (data == null) {log.error("响应无data字段:{}", responseBody);return null;}JSONObject objectDesc = data.getJSONObject("object_desc");JSONObject media = objectDesc.getJSONArray("media").getJSONObject(0);String baseUrl = media.getString("url");String urlToken = media.getString("url_token");// 4. 拼接完整播放/下载链接(直接访问可播放/下载)String videoUrl = baseUrl + (baseUrl.contains("?") ? "&token=" : "?token=") + urlToken;log.info("视频链接拼接成功:{}", videoUrl);return videoUrl;} catch (Exception e) {log.error("解析视频链接失败", e);return null;}}/*** 调用企微接口,发送小程序消息(适配内部/外部联系人)*/public void sendMiniProgramMessage(String receiverId, String videoUrl) {try {// URL编码视频链接(避免特殊字符截断)String encodedVideoUrl = URLEncoder.encode(videoUrl, "UTF-8");// 小程序页面路径(携带视频链接参数)String finalPagePath = miniPagePath + "?url=" + encodedVideoUrl;// 构建企微小程序消息WxCpMessage message = WxCpMessage.MINIPROGRAM_NOTICE().agentId(wxCpService.getWxCpConfigStorage().getAgentId()).toUser(receiverId) // 接收人ID:内部成员=userId,外部联系人=openId.title("视频播放通知").appId(miniAppId) // 关联的小程序AppID.page(finalPagePath) // 小程序页面(含视频链接).description("你转发的视频已解析完成,点击直接播放/下载").build();// 发送消息(企微SDK自动处理access_token,支持外部联系人)wxCpService.messageSend(message);log.info("已向{}发送小程序消息,页面路径:{}", receiverId, finalPagePath);} catch (Exception e) {log.error("发送小程序消息失败", e);}}
}
四、第四步:小程序页面开发(播放 / 下载视频)
小程序需创建「播放 / 下载页面」,接收后端传递的视频链接,实现直接播放和下载功能,示例代码如下:
1. 页面结构(pages/play/index.wxml)
xml
<view class="container"><view class="title">视频播放/下载</view><!-- 视频播放组件(直接播放拼接后的链接) --><video src="{{videoUrl}}" class="video-player"controlsenable-play-gesture></video><!-- 下载按钮 --><button bindtap="downloadVideo" class="download-btn">保存视频到手机</button>
</view>
2. 页面样式(pages/play/index.wxss)
css
.container {padding: 20rpx;box-sizing: border-box;
}.title {font-size: 32rpx;font-weight: bold;margin-bottom: 20rpx;text-align: center;
}.video-player {width: 100%;height: 400rpx;background-color: #f5f5f5;border-radius: 16rpx;margin-bottom: 30rpx;
}.download-btn {background-color: #2f54eb;color: white;font-size: 30rpx;border-radius: 8rpx;
}
3. 页面逻辑(pages/play/index.js)
javascript
运行
Page({data: {videoUrl: '' // 接收后端传递的视频链接},onLoad(options) {// 接收并解码视频链接(后端已URL编码)const videoUrl = decodeURIComponent(options.url);this.setData({ videoUrl });console.log("视频链接:", videoUrl);},// 下载视频到手机downloadVideo() {const { videoUrl } = this.data;wx.showLoading({ title: '下载中...' });// 调用微信下载API(需配置小程序download合法域名,或用web-view跳转)wx.downloadFile({url: videoUrl,filePath: `${wx.env.USER_DATA_PATH}/video_${Date.now()}.mp4`,success: (res) => {if (res.statusCode === 200) {// 下载成功后保存到相册wx.saveVideoToPhotosAlbum({filePath: res.filePath,success: () => {wx.hideLoading();wx.showToast({ title: '下载成功!已保存到相册' });},fail: (err) => {wx.hideLoading();wx.showToast({ title: '保存失败,请授权相册权限', icon: 'none' });console.error("保存失败:", err);}});}},fail: (err) => {wx.hideLoading();wx.showToast({ title: '下载失败,请重试', icon: 'none' });console.error("下载失败:", err);}});}
});
4. 小程序配置(app.json)
json
{"pages": ["pages/play/index" // 新增播放页面路径],"window": {"backgroundTextStyle": "light","navigationBarBackgroundColor": "#fff","navigationBarTitleText": "视频助手","navigationBarTextStyle": "black"},"sitemapLocation": "sitemap.json"
}
5. 小程序发布
- 用微信开发者工具上传代码(版本号如 1.0.0);
- 小程序后台 → 版本管理 → 提交审核(个人主体 1-2 小时通过);
- 审核通过后发布「线上版本」(用户可正常访问)。
五、第五步:测试流程(验证完整功能)
-
准备工作:
- 个微用户添加企微外部联系人(通过企微对外名片);
- 启动后端应用,确保 ngrok 穿透正常(本地开发)或云服务器部署成功;
- 企微后台确认应用回调验证成功。
-
测试步骤:
- 个微用户打开视频号,选择任意视频 → 转发 → 选择企微外部联系人;
- 后端日志打印「接收企微消息」「解密后消息」「视频链接拼接成功」;
- 个微用户收到企微的「小程序通知」(标题 “视频播放通知”);
- 点击通知进入小程序,视频自动加载播放,点击「保存视频到手机」可下载到相册。
六、关键适配与问题排查
1. 个微登录(外部联系人)场景适配
- 确保企微应用已开启「外部联系人」权限(应用管理→权限管理);
- 发送小程序消息时,
receiverId是外部联系人的 openId(回调消息的fromUser),企微 API 支持给外部联系人发送小程序消息; - 若发送失败:检查应用「可见范围」是否包含外部联系人,或外部联系人是否已授权消息接收。
2. 常见问题排查
| 问题现象 | 原因 | 解决方案 |
|---|---|---|
| 收不到回调消息 | 回调 URL 不是 HTTPS / 公网不可达 | 用 ngrok 穿透或配置云服务器 HTTPS |
| 解析不到 objectId/nonceId | 消息类型不是 sphfeed 或 feedType≠4 | 确认转发的是视频号视频(非其他类型) |
| 视频链接无法播放 | 链接拼接错误 / 视频号接口调整 | 打印视频号接口响应,检查 url 和 urlToken 字段 |
| 小程序消息发送失败 | 企微未关联小程序 | 企微后台→应用管理→小程序→关联目标小程序 |
| 小程序无法下载视频 | 未配置 download 合法域名 | 小程序后台→开发→服务器域名→添加视频链接的域名(或用 web-view 跳转) |
总结
核心实现逻辑:企微回调接收外部联系人(个微)的视频号消息 → 提取参数解析视频链接 → 调用企微小程序消息接口推送 → 小程序承载播放 / 下载。关键配置是企微应用的回调、外部联系人权限、小程序关联,代码核心是回调消息解密、视频链接拼接、小程序消息推送,全程适配个微登录场景,确保用户操作流程简洁(转发→收通知→播放 / 下载)。
按以上步骤配置后,即可实现 “个微转发视频号到企微 → 自动推送小程序播放 / 下载链接” 的完整功能,无需用户额外操作,体验流畅。
