要实现「个微登录企微后转发视频号 → 企微回调获取参数 → 解析视频链接 → 小程序推送」,核心是 适配企微外部联系人(个微登录场景)+ 回调消息解析 + 视频链接拼接 + 小程序消息推送 ,全程遵循企微官方接口规范,以下是 完整实现方案(含配置 + 代码 + 测试),聚焦「个微登录(外部联系人)」场景适配:
一、核心逻辑与前提说明
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 # 应用管理→你的应用→Secret
agent-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: 8080
servlet:
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;
@Bean
public 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;
}
@Bean
public 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);
@Autowired
private WxCpService wxCpService;
@Autowired
private VideoService videoService;
/**
* 企微回调验证(GET请求)
*/
@GetMapping
public 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;
@Autowired
private 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. 拼接视频号详情接口URL
String 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和urlToken
String 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"
controls
enable-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 跳转) |
总结
核心实现逻辑:企微回调接收外部联系人(个微)的视频号消息 → 提取参数解析视频链接 → 调用企微小程序消息接口推送 → 小程序承载播放 / 下载。关键配置是企微应用的回调、外部联系人权限、小程序关联,代码核心是回调消息解密、视频链接拼接、小程序消息推送,全程适配个微登录场景,确保用户操作流程简洁(转发→收通知→播放 / 下载)。
按以上步骤配置后,即可实现 "个微转发视频号到企微 → 自动推送小程序播放 / 下载链接" 的完整功能,无需用户额外操作,体验流畅。