Zoom 用量统计与报表分析实战:基于 zoom-rest-api 搭建会议、Webinar 与计费数据管道
【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins
本文以 knowledge-work-plugins 仓库中 usage-reporting-analytics.md 为核心指南,系统讲解如何通过 Zoom Reporting 系列 API 获取会议统计、参与者明细与账单数据,并结合仓库内 zoom-rest-api 技能源码给出可运行的前后端代码。读完本文,你将掌握日/月用量汇总、单场会议参与者分析、Webinar 互动指标计算、BI 数据导出以及数据保留策略的完整落地方案。
概述:Reporting API 能做什么
Zoom 的 Reporting 系列接口(/v2/report/*)面向管理员与开发者提供账户级、用户级、会议级三个粒度的用量数据。典型用途包括:
- 追踪会议使用量(每天多少场会议、消耗多少分钟);
- 生成按用户的参与统计与活跃度报表,用于计费分摊或业务洞察;
- 提取 Webinar 的出席率、Q&A 与投票互动数据,评估活动效果;
- 将原始数据导出为 CSV/JSON,喂给 BigQuery、Snowflake 等数据仓库做 BI 分析。
在 general/SKILL.md 的用例索引中,本场景被归入「Usage Reporting and Analytics」,明确标注主导技能为zoom-rest-api——这属于"确定性后端自动化、报表、定时任务"路由,走 REST API 而非 MCP 动态工具层。
技能依赖与路由定位
根据原文档的 Skills Needed 说明,实现本场景的核心技能是:
| 技能 | 角色 | 仓库位置 |
|---|---|---|
| zoom-rest-api | 主技能:端点选择、OAuth 要求、速率限制、错误调试 | rest-api/SKILL.md |
| zoom-oauth(可选) | 补充:S2S/User OAuth 令牌获取与刷新 | oauth/SKILL.md |
| zoom-webhooks(可选) | 补充:实时用量事件跟踪 | webhooks/SKILL.md |
general/SKILL.md 中给出了判定逻辑:当查询包含rest api、report、s2s oauth等信号时,pickPrimarySkill会路由到zoom-rest-api,并按需链式附加zoom-oauth与zoom-webhooks。这意味着一个完整的报表系统通常是 REST 拉取为主、Webhook 实时事件为辅的混合架构。
报表类型总览
原文档将 Reporting API 覆盖的报表归纳为四类:
| 报表类型 | 说明 |
|---|---|
| 每日用量(Daily usage) | 每天的会议场次、消耗分钟数 |
| 会议明细(Meeting details) | 参与者列表、加入/离开时间 |
| Webinar 报表(Webinar reports) | 出席者、Q&A、投票数据 |
| 账单报表(Billing reports) | 用于计费目的的用量数据 |
对应的 REST 端点在 rest-api/references/reports.md 中逐一声明:
GET /report/daily—— 每日用量报告,必填查询参数year、month;GET /report/meetings/{meetingId}—— 单场会议明细;GET /report/meetings/{meetingId}/participants—— 会议参与者报告;GET /report/webinars/{webinarId}/participants—— Webinar 参与者报告;GET /report/users—— 活跃/非活跃主持人报告。
前置条件与权限范围
账户要求
- 管理员(Admin)或所有者(Owner)账户:Reports API 返回的是账户级用量数据,普通成员权限不足;
report:read权限范围(scope):所有报表端点均依赖该 scope。
Scope 选型细节
仓库 general/references/scopes.md 对 Reports 类 scope 做了完整梳理:
| 用户级 Scope | 管理级 Scope | 访问范围 |
|---|---|---|
report:read | report:read:admin | 查看报表与分析数据 |
report:master | report:master:admin | 报表完整访问权限 |
选型规则:仅查询当前授权用户自己的数据时用report:read;若后端服务(S2S OAuth)需要读取整个账户所有用户的用量,必须使用report:read:admin。这与 S2S 应用"无用户登录、账户级访问"的定位一致,见 backend-automation-s2s-oauth.md 中report:read:admin的配置示例。
认证与快速开始
获取访问令牌(Server-to-Server OAuth)
Reports API 走 Bearer Token 认证。以 rest-api/SKILL.md 提供的 S2S 令牌获取方式为例:
curl -X POST "https://zoom.us/oauth/token" \ -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=account_credentials&account_id=ACCOUNT_ID"响应中包含access_token(有效期expires_in通常为 3600 秒)与scope字段,例如"scope": "report:read:admin meeting:read user:read"。
快速开始:两条核心 curl 命令
原文档给出的最小可用示例:
# 获取每日用量报告 curl -X GET "https://api.zoom.us/v2/report/daily?year=2024&month=1" \ -H "Authorization: Bearer {accessToken}" # 获取某场会议的参与者列表 curl -X GET "https://api.zoom.us/v2/report/meetings/{meetingId}/participants" \ -H "Authorization: Bearer {accessToken}"Base URL 与区域端点
所有请求使用 HTTPS 与/v2版本前缀,默认基址为https://api.zoom.us/v2。仓库 rest-api/concepts/api-architecture.md 强调:OAuth 令牌响应中的api_url字段标明用户所在数据区域,若需满足数据驻留合规要求,可改用区域端点(如https://api-eu.zoom.us/v2、https://api-sg.zoom.us/v2);而全局 URLhttps://api.zoom.us在任何区域都可用,非强制。
常见任务一:日/月用量汇总
原文档提供了聚合每日报表的完整 Node.js 实现,这里完整继承并补充注释:
const axios = require('axios'); // 获取每日用量报告 async function getDailyUsage(year, month) { const response = await axios.get( `https://api.zoom.us/v2/report/daily`, { params: { year, month }, headers: { 'Authorization': `Bearer ${accessToken}` } } ); // 返回字段:dates[]、total_meeting_minutes、total_meetings、total_participants return response.data; } // 聚合月度统计 async function getMonthlyStats(year, month) { const daily = await getDailyUsage(year, month); return { totalMeetings: daily.dates.reduce((sum, d) => sum + d.meetings, 0), totalMinutes: daily.dates.reduce((sum, d) => sum + d.meeting_minutes, 0), totalParticipants: daily.dates.reduce((sum, d) => sum + d.participants, 0), averageMeetingDuration: daily.dates.length > 0 ? daily.total_meeting_minutes / daily.total_meetings : 0, peakDay: daily.dates.reduce((max, d) => d.meetings > max.meetings ? d : max, { meetings: 0 } ) }; } // 获取用户级活动数据 async function getUserActivity(userId, fromDate, toDate) { const response = await axios.get( `https://api.zoom.us/v2/report/users/${userId}/meetings`, { params: { from: fromDate, to: toDate, page_size: 300 }, headers: { 'Authorization': `Bearer ${accessToken}` } } ); return response.data.meetings; }响应结构印证
rest-api/references/reports.md 给出了/report/daily的真实 JSON 结构,便于核对字段命名:
{ "dates": [ { "date": "2024-01-15", "new_users": 5, "meetings": 25, "participants": 150, "meeting_minutes": 3600 } ] }注意聚合代码里访问的字段是d.meetings、d.meeting_minutes、d.participants,与响应体逐一对齐。
常见任务二:单场会议参与者报告
关键坑:UUID 双重编码
原文档特别提示:meetingId既可以是数字型会议 ID,也可以是 UUID。当 UUID 包含/或//时,必须双重 URL 编码,否则请求会 404。仓库 rest-api/concepts/api-architecture.md 完整解释了原因与处理函数:
function encodeUUID(uuid) { // 以 / 开头或包含 // 的 UUID 需要双重编码 if (uuid.startsWith('/') || uuid.includes('//')) { return encodeURIComponent(encodeURIComponent(uuid)); } return encodeURIComponent(uuid); } // 示例:UUID /abcABC123== // 单次编码 %2FabcABC123%3D%3D // 双重编码 %252FabcABC123%253D%253D ← 必须使用参与者拉取与会议指标计算
// 获取会议参与者 async function getMeetingParticipants(meetingId) { // 注意:meetingId 可以是会议 ID 或 UUID // 若 UUID 含 / 或 //,需双重编码 const encodedId = meetingId.includes('/') ? encodeURIComponent(encodeURIComponent(meetingId)) : meetingId; const response = await axios.get( `https://api.zoom.us/v2/report/meetings/${encodedId}/participants`, { params: { page_size: 300 }, headers: { 'Authorization': `Bearer ${accessToken}` } } ); return response.data.participants; } // 计算会议指标 function calculateMeetingMetrics(participants) { const uniqueParticipants = new Set(participants.map(p => p.user_email || p.name)); // 计算每个参与者的在线时长 const durations = participants.map(p => { const join = new Date(p.join_time); const leave = new Date(p.leave_time); return (leave - join) / 1000 / 60; // 分钟 }); return { totalParticipants: uniqueParticipants.size, peakConcurrent: calculatePeakConcurrent(participants), averageAttendanceDuration: average(durations), lateJoiners: participants.filter(p => /* 迟到逻辑 */).length, earlyLeavers: participants.filter(p => /* 早退逻辑 */).length }; } function calculatePeakConcurrent(participants) { const events = []; participants.forEach(p => { events.push({ time: new Date(p.join_time), delta: 1 }); events.push({ time: new Date(p.leave_time), delta: -1 }); }); events.sort((a, b) => a.time - b.time); let current = 0; let peak = 0; events.forEach(e => { current += e.delta; peak = Math.max(peak, current); }); return peak; }calculatePeakConcurrent采用经典的"扫描线算法":把每个参与者的加入/离开转为时间轴上的 +1/-1 事件,排序后线性扫描即可得到历史并发峰值,无需对参与者两两比较。
参与者响应结构
{ "participants": [ { "id": "user_id", "name": "User Name", "user_email": "user@example.com", "join_time": "2024-01-15T10:00:00Z", "leave_time": "2024-01-15T11:00:00Z", "duration": 3600 } ] }注意:duration单位为秒(如 3600 秒 = 1 小时),若需分钟数需除以 60。
常见任务三:Webinar 互动分析
Webinar 报表比普通会议更丰富,除参与者外还包含缺席者、Q&A 与投票数据。原文档用Promise.all并发拉取四路数据,并基于此计算互动得分:
// 获取 webinar 参与者(panelists + attendees) async function getWebinarReport(webinarId) { const [participants, absentees, qa, polls] = await Promise.all([ getWebinarParticipants(webinarId), getWebinarAbsentees(webinarId), getWebinarQA(webinarId), getWebinarPolls(webinarId) ]); return { participants, absentees, qa, polls }; } async function getWebinarParticipants(webinarId) { const response = await axios.get( `https://api.zoom.us/v2/report/webinars/${webinarId}/participants`, { headers: { 'Authorization': `Bearer ${accessToken}` }} ); return response.data.participants; } async function getWebinarAbsentees(webinarId) { const response = await axios.get( `https://api.zoom.us/v2/report/webinars/${webinarId}/absentees`, { headers: { 'Authorization': `Bearer ${accessToken}` }} ); return response.data.registrants; } async function getWebinarQA(webinarId) { const response = await axios.get( `https://api.zoom.us/v2/report/webinars/${webinarId}/qa`, { headers: { 'Authorization': `Bearer ${accessToken}` }} ); return response.data.questions; } async function getWebinarPolls(webinarId) { const response = await axios.get( `https://api.zoom.us/v2/report/webinars/${webinarId}/polls`, { headers: { 'Authorization': `Bearer ${accessToken}` }} ); return response.data.questions; } // 计算 webinar 互动得分 function calculateEngagementScore(report) { const { participants, absentees, qa, polls } = report; const registeredCount = participants.length + absentees.length; const attendedCount = participants.length; const participatedInQA = new Set(qa.map(q => q.email)).size; const participatedInPolls = new Set(polls.flatMap(p => p.email)).size; return { attendanceRate: (attendedCount / registeredCount * 100).toFixed(1), qaParticipation: (participatedInQA / attendedCount * 100).toFixed(1), pollParticipation: (participatedInPolls / attendedCount * 100).toFixed(1), totalQuestions: qa.length, averageAttendanceDuration: average(participants.map(p => p.duration)) }; }互动得分模型的逻辑要点:
- 出席率= 实际参会人数 / 注册人数(注册数 = 参与者 + 缺席者,缺席者接口返回的是
registrants字段); - Q&A / 投票参与率均以到场人数为分母,衡量活跃度而非注册转化;
- 用
Set对email去重,避免同一人多次提问/投票被重复计数。
常见任务四:导出数据给 BI 工具
报表系统的终点通常是数据仓库。原文档给出了三条导出路径:CSV、JSON 数据仓库直写、定时任务。
const { Parser } = require('json2csv'); const fs = require('fs'); // 导出为 CSV 供 BI 工具使用 async function exportMeetingsToCSV(fromDate, toDate, outputPath) { // 拉取日期范围内的所有会议(自动翻页) const meetings = []; let nextPageToken = null; do { const response = await axios.get( 'https://api.zoom.us/v2/report/users/me/meetings', { params: { from: fromDate, to: toDate, page_size: 300, next_page_token: nextPageToken }, headers: { 'Authorization': `Bearer ${accessToken}` } } ); meetings.push(...response.data.meetings); nextPageToken = response.data.next_page_token; } while (nextPageToken); // 扁平化为 CSV 行 const flatMeetings = meetings.map(m => ({ id: m.id, uuid: m.uuid, topic: m.topic, start_time: m.start_time, end_time: m.end_time, duration_minutes: m.duration, participants_count: m.participants_count, host_email: m.host_email, has_recording: m.has_recording ? 'yes' : 'no' })); const parser = new Parser(); const csv = parser.parse(flatMeetings); fs.writeFileSync(outputPath, csv); return outputPath; } // 导出为 JSON 写入数据仓库 async function exportToDataWarehouse(fromDate, toDate) { const meetings = await getAllMeetings(fromDate, toDate); // 适配 BigQuery/Snowflake 的结构化记录 const records = meetings.map(m => ({ ...m, _ingested_at: new Date().toISOString(), _source: 'zoom_api' })); // 写入仓库 await bigquery.dataset('zoom').table('meetings').insert(records); } // 定时导出任务 const cron = require('node-cron'); cron.schedule('0 1 * * *', async () => { // 每天凌晨 1 点执行 const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000); const from = yesterday.toISOString().split('T')[0]; const to = from; await exportToDataWarehouse(from, to); console.log(`Exported data for ${from}`); });这段代码中值得注意的分页规范:使用next_page_token而非过时的page_number翻页——rest-api/SKILL.md 明确将「分页使用next_page_token」列为关键最佳实践,并说明page_number属遗留方案正被逐步淘汰。page_size上限取 300 是 Zoom 列表接口的通用约定。
另外,report/users/me/meetings中的me关键字仅在用户级 OAuth 应用下合法;若使用 S2S OAuth,必须替换为真实userId或邮箱(见下文"常见坑")。
数据保留说明
原文档给出了三类报表的保留期限,这是设计定时抓取与归档策略时必须遵守的边界:
| 数据类型 | 保留期限 |
|---|---|
| 会议/Webinar 报表 | 结束后可用12 个月 |
| 参与者报表 | 会议结束后可用1 个月 |
| QSS(Quality of Service)质量数据 | 可用30 天 |
仓库内其他文档对保留策略做了交叉印证与扩展:
- minutes-calculation.md 的数据保留表一致地记录:Session Quality API 30 天、Reports API(会议)12 个月、Reports API(参与者)1 个月,并额外提示Webhook 事件需要自建存储——这是实时计费管道需要自持历史的原因;
- qss-monitoring.md 说明 QSS 数据按「每参与者约每分钟 1 条」的频率通过 Webhook 下发,且仅通过 Webhook Logs API 保留 7 天。
实践建议:由于参与者报表 1 个月即过期,务必在会议结束后尽快拉取并落库;面向合规审计的系统应把原始报表定期归档到自有存储,不能依赖 Zoom 侧的长期保留。
常见坑与最佳实践汇总
结合原文档与 rest-api/SKILL.md 的权威提醒:
me关键字规则:用户级 OAuth 应用必须用me代替userId(否则报Invalid access token, does not contain scopes);S2S OAuth 应用禁止用me,需提供真实userId或邮箱;账户级 OAuth 两者皆可。- UUID 双重编码:以
/开头或含//的 UUID 必须双重 URL 编码,否则报表端点 404。 - 时间格式:报表接口的时间参数使用
YYYY-MM-DD日期格式;API 响应中的时间戳为 ISO 8601 UTC(带Z后缀)。部分 Report API 只接受 UTC 时间,务必逐端点核对。 - 速率限制按账户共享:同一 Zoom 账户下所有 App 共享配额,监控
X-RateLimit-Remaining响应头,对 429 实现指数退避重试(见 backend-automation-s2s-oauth.md 的retryRequest示例)。 - 用 Webhook 替代轮询:高实时性场景优先订阅
meeting.ended等事件,避免高频轮询浪费配额;Reports API 更适合作为日/月级对账与补数的权威数据源。
延伸阅读
- usage-reporting-analytics.md —— 本文主题文档原文;
- rest-api/references/reports.md —— Reports 端点、参数与响应结构速查;
- rest-api/concepts/api-architecture.md —— Base URL、
me关键字、UUID 编码、时间格式; - general/references/scopes.md ——
report:read系列 scope 权限矩阵; - general/use-cases/minutes-calculation.md —— 基于 participant-minutes 的计费计算与成本预估;
- general/use-cases/qss-monitoring.md —— 实时 QoS 监控与 Webhook 数据管道;
- general/use-cases/backend-automation-s2s-oauth.md —— 报表系统的服务端认证与部署骨架。
【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考