news 2026/9/13 18:01:26

Zoom 用量统计与报表分析实战:基于 zoom-rest-api 搭建会议、Webinar 与计费数据管道

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Zoom 用量统计与报表分析实战:基于 zoom-rest-api 搭建会议、Webinar 与计费数据管道

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 apireports2s oauth等信号时,pickPrimarySkill会路由到zoom-rest-api,并按需链式附加zoom-oauthzoom-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—— 每日用量报告,必填查询参数yearmonth
  • 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: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 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/v2https://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.meetingsd.meeting_minutesd.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 / 投票参与率均以到场人数为分母,衡量活跃度而非注册转化;
  • Setemail去重,避免同一人多次提问/投票被重复计数。

常见任务四:导出数据给 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 的权威提醒:

  1. me关键字规则:用户级 OAuth 应用必须me代替userId(否则报Invalid access token, does not contain scopes);S2S OAuth 应用禁止me,需提供真实userId或邮箱;账户级 OAuth 两者皆可。
  2. UUID 双重编码:以/开头或含//的 UUID 必须双重 URL 编码,否则报表端点 404。
  3. 时间格式:报表接口的时间参数使用YYYY-MM-DD日期格式;API 响应中的时间戳为 ISO 8601 UTC(带Z后缀)。部分 Report API 只接受 UTC 时间,务必逐端点核对。
  4. 速率限制按账户共享:同一 Zoom 账户下所有 App 共享配额,监控X-RateLimit-Remaining响应头,对 429 实现指数退避重试(见 backend-automation-s2s-oauth.md 的retryRequest示例)。
  5. 用 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 18:00:35

RoboMaster电控硬件实战讲义:从炸机到可靠设计

1. 项目概述:这本讲义不是“教材”,而是RoboMaster电控工程师的实战备忘录“Robomaster硬件基础讲义V0.2.1”——看到这个标题,我第一反应不是去翻目录,而是下意识摸了摸自己工装裤口袋里那枚被磨得发亮的STM32F407最小系统板。它…

作者头像 李华
网站建设 2026/9/13 17:59:48

DenseNet鸟类细粒度分类:121/161/169/201四版本选型与PyTorch实战

简介:本资源是一个基于DenseNet系列(121/161/169/201)的鸟类图像多类别分类实战项目,面向深度学习初学者与计算机视觉实践者,聚焦图像识别任务中的模型选型、迁移学习与评估体系构建。项目完整实现训练、验证与多维度性…

作者头像 李华
网站建设 2026/9/13 17:59:47

右心室MRI分割:PyTorch定制U-Net实战指南

简介:本资源是一套基于PyTorch框架与U-Net网络结构实现心脏右心室医学图像分割的完整Python项目,面向计算机、人工智能、生物医学工程等专业的本科生及初阶研究者,适用于毕业设计、课程设计、期末大作业及医学图像分析入门实践。项目代码经本…

作者头像 李华
网站建设 2026/9/13 17:58:32

MySQL知识体系梳理

过往对MySQL的认识比较零碎,东一块西一块,经常忘记。最近花了点时间重新梳理了下,试图从整体的视角理解它,形成体系化的知识,这样既有助于记忆,也能够提升技术水平。内容是基于过往的工作经验,结…

作者头像 李华
网站建设 2026/9/13 17:58:30

小体积高扭矩FOC方案难在哪?从散热、采样到MOS与MCU选型全拆解

做一体化关节、电动工具、无人机云台、水下推进器的朋友,应该都有这种经历:方案刚定型的时候很兴奋,等到要把控制板塞进一个更小的腔体、还想把峰值扭矩往上提一档的时候,突然发现哪哪都不对——不是MOS烫到不能摸,就是…

作者头像 李华