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 的出席率、QA 与投票互动数据评估活动效果将原始数据导出为 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.mdzoom-oauth可选补充S2S/User OAuth 令牌获取与刷新oauth/SKILL.mdzoom-webhooks可选补充实时用量事件跟踪webhooks/SKILL.mdgeneral/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出席者、QA、投票数据账单报表Billing reports用于计费目的的用量数据对应的 REST 端点在 rest-api/references/reports.md 中逐一声明GET /report/daily—— 每日用量报告必填查询参数year、monthGET /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:readreport:read:admin查看报表与分析数据report:masterreport:master:admin报表完整访问权限选型规则仅查询当前授权用户自己的数据时用report:read若后端服务S2S OAuth需要读取整个账户所有用户的用量必须使用report:read:admin。这与 S2S 应用无用户登录、账户级访问的定位一致见 backend-automation-s2s-oauth.md 中report:read:admin的配置示例。认证与快速开始获取访问令牌Server-to-Server OAuthReports 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_typeaccount_credentialsaccount_idACCOUNT_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?year2024month1 \ -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: userexample.com, join_time: 2024-01-15T10:00:00Z, leave_time: 2024-01-15T11:00:00Z, duration: 3600 } ] }注意duration单位为秒如 3600 秒 1 小时若需分钟数需除以 60。常见任务三Webinar 互动分析Webinar 报表比普通会议更丰富除参与者外还包含缺席者、QA 与投票数据。原文档用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字段QA / 投票参与率均以到场人数为分母衡量活跃度而非注册转化用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 个月QSSQuality 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 scopesS2S 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),仅供参考
