VideoConference 域统一了 5 家视频会议供应商的会议创建、查询、更新、取消、参会人管理和录制查询能力。它遵循标准三层包模式。所有供应商共享同一个 IVideoConferenceService 接口,但鉴权机制差异显著:腾讯会议/Zoom 用 OAuth2 Client Credentials,Microsoft Teams 用 Azure AD Client Credentials 调 Graph,飞书用 tenant_access_token Bearer,钉钉用 access_token query 参数。
域结构
Bitzsoft.Integrations.VideoConference ← 抽象层├── Bitzsoft.Integrations.VideoConference.TencentMeeting ← 腾讯会议(OAuth2 Client Credentials)├── Bitzsoft.Integrations.VideoConference.Zoom ← Zoom(Server-to-Server OAuth)├── Bitzsoft.Integrations.VideoConference.MicrosoftTeams ← Microsoft Teams(Graph onlineMeetings)├── Bitzsoft.Integrations.VideoConference.Feishu ← 飞书视频会议(tenant_access_token)├── Bitzsoft.Integrations.VideoConference.DingTalk ← 钉钉视频会议(access_token query)└── Bitzsoft.Integrations.VideoConference.All ← 聚合包统一接口(9 方法)
IVideoConferenceService 围绕”会议(Meeting)“这一核心概念组织,共 9 个成员:
public interface IVideoConferenceService{ string ProviderName { get; }
// 会议生命周期 Task<MeetingInfo> CreateMeetingAsync(CreateMeetingRequest request, CancellationToken cancellationToken = default); Task<MeetingInfo> GetMeetingAsync(string meetingId, CancellationToken cancellationToken = default); Task<VideoConferenceResult> UpdateMeetingAsync(UpdateMeetingRequest request, CancellationToken cancellationToken = default); Task<VideoConferenceResult> CancelMeetingAsync(string meetingId, CancellationToken cancellationToken = default); Task<List<MeetingInfo>> ListMeetingsAsync(ListMeetingsRequest request, CancellationToken cancellationToken = default);
// 参会人 Task<MeetingInviteResult> InviteAttendeesAsync(InviteAttendeesRequest request, CancellationToken cancellationToken = default); Task<MeetingInviteResult> RemoveAttendeesAsync(RemoveAttendeesRequest request, CancellationToken cancellationToken = default);
// 录制 Task<List<MeetingRecording>> ListRecordingsAsync(string meetingId, CancellationToken cancellationToken = default);}与 OutboundCall 的返回值约定一致:查询类方法(Create / Get / List / ListRecordings)抛异常表达失败,操作类方法(Update / Cancel / Invite / Remove)返回结果对象——VideoConferenceResult { Success, ErrorMessage?, RequestId? } 或 MeetingInviteResult { Success, ProcessedCount, ErrorMessage? }。
鉴权模型
5 家供应商使用 4 种不同的鉴权机制,这是本域最大的实现差异:
| 供应商 | 鉴权机制 | Token 注入 | Token 刷新 |
|---|---|---|---|
| 腾讯会议 | OAuth2 Client Credentials(Client Id + Secret + AppId) | Bearer 头 | POST /v1/open-token |
| Zoom | Server-to-Server OAuth(Account Id + Client Id + Secret) | Bearer 头 | POST /oauth/token(HTTP Basic Auth) |
| Microsoft Teams | Azure AD Client Credentials(Tenant + Client Id + Secret) | Bearer 头 | POST /{tenant}/oauth2/v2.0/token(form-urlencoded) |
| 飞书 | App Id + App Secret 换 tenant_access_token | Bearer 头 | POST /open-apis/auth/v3/tenant_access_token/internal |
| 钉钉 | App Key + App Secret 换 access_token | query 参数 ?access_token= | GET /gettoken |
TokenManager 并发安全
每个供应商的 Internal/*TokenManager 是一个注册为 Singleton 的令牌缓存管理器,采用双重检查锁模式:
- 快速路径:先无锁检查
_cached,命中且未过期直接返回。 - 双重检查:获取
SemaphoreSlim后再次检查缓存,防止并发刷新。 - 提前刷新:
effectiveExpire = Math.Max(expire - 300, 60),提前 5 分钟刷新,下限 60 秒。 - 失效自愈:HttpClient 检测到令牌失效码时调
Invalidate()清缓存,下次调用自动刷新。
public async Task<string> GetTokenAsync(CancellationToken ct){ if (TryGetValid(out var token)) // ① 快速路径无锁 return token;
await _lock.WaitAsync(ct); try { if (TryGetValid(out token)) // ② 进锁后 double-check return token; return await RefreshAsync(ct); // ③ 换 token } finally { _lock.Release(); }}双 HttpClient 隔离
每个供应商注册两个 HttpClient:业务 HttpClient(Typed Client,按请求注入令牌)和令牌刷新 HttpClient(命名 Client,不经业务管道,避免循环依赖)。
// 令牌刷新专用(不经业务管道,避免令牌注入循环)services.AddHttpClient(XxxConstants.TokenHttpClientName) .AddRequestLogging(XxxConstants.TokenHttpClientName);
// 业务请求(按请求注入令牌)services.AddHttpClient<XxxHttpClient>() .AddRequestLogging<XxxHttpClient>();错误处理
业务码校验
各供应商的成功判断方式不同:
| 供应商 | 成功判断 | 失败码字段 |
|---|---|---|
| 腾讯会议 | code == "0"(无 code 视为成功) | code / message |
| Zoom | HTTP 2xx 直接返回(无外层 code) | HTTP 非 2xx + {code, message} |
| Microsoft Teams | HTTP 2xx 直接返回 | HTTP 非 2xx + {error: {code, message}} |
| 飞书 | code == "0" | code / msg |
| 钉钉 | errcode == "0"(无 errcode 视为成功) | errcode / errmsg |
令牌失效自愈
HttpClient 检测到令牌失效类错误时,清除 TokenManager 缓存,下次调用自动刷新:
| 供应商 | 失效码 |
|---|---|
| 腾讯会议 | 9012(无权限)· 9013(过期)· 9014(无效) |
| Zoom / Teams | HTTP 401 Unauthorized |
| 飞书 | 99991661 · 99991663 · 99991664 |
| 钉钉 | 40014 · 42001 · 48002 |
供应商清单
| 供应商 | Provider ID | API 基地址 | Stability | DI 方法 |
|---|---|---|---|---|
| 腾讯会议 | tencent-meeting | api.meeting.qq.com | Preview | AddTencentMeetingVideoConference() |
| Zoom | zoom | api.zoom.us | Preview | AddZoomVideoConference() |
| Microsoft Teams | microsoft-teams | graph.microsoft.com | Preview | AddMicrosoftTeamsVideoConference() |
| 飞书视频会议 | feishu | open.feishu.cn | Preview | AddFeishuVideoConference() |
| 钉钉视频会议 | dingtalk | oapi.dingtalk.com | Preview | AddDingTalkVideoConference() |
所有供应商标记为 Preview——基本流程走通,边界场景可能未覆盖。
时间戳差异
不同供应商的时间表示方式不同,ModelMapper 负责统一为 DateTimeOffset(UTC):
| 供应商 | 时间格式 | 示例 |
|---|---|---|
| 腾讯会议 / 飞书 | Unix 秒级时间戳 | 1700000000 |
| 钉钉 | Unix 毫秒级时间戳 | 1700000000000 |
| Zoom / Teams | ISO 8601 | 2023-11-14T22:13:20Z |
注册
单厂商
// ① 腾讯会议(OAuth2 Client Credentials)builder.Services.AddTencentMeetingVideoConference(builder.Configuration.GetSection("VideoConference:TencentMeeting"));
// ② Zoom(Server-to-Server OAuth)builder.Services.AddZoomVideoConference(builder.Configuration.GetSection("VideoConference:Zoom"));
// ③ Microsoft Teams(Azure AD Client Credentials → Graph)builder.Services.AddMicrosoftTeamsVideoConference(builder.Configuration.GetSection("VideoConference:MicrosoftTeams"));
// ④ 飞书视频会议(tenant_access_token)builder.Services.AddFeishuVideoConference(builder.Configuration.GetSection("VideoConference:Feishu"));
// ⑤ 钉钉视频会议(access_token query)builder.Services.AddDingTalkVideoConference(builder.Configuration.GetSection("VideoConference:DingTalk"));全量聚合
builder.Services.AddBitzsoftVideoConferenceAll(builder.Configuration, "VideoConference");按配置节存在性自动注册,appsettings.json 只需写出启用的供应商:
{ "VideoConference": { "TencentMeeting": { "ClientId": "...", "ClientSecret": "...", "SdkAppId": "..." }, "Zoom": { "AccountId": "...", "ClientId": "...", "ClientSecret": "..." }, "MicrosoftTeams": { "TenantId": "...", "ClientId": "...", "ClientSecret": "...", "DefaultUserPrincipalName": "organizer@contoso.com" } }}消费
创建会议
public class MeetingManager(IIntegrationProviderResolver<IVideoConferenceService> providers){ public async Task<MeetingInfo> ScheduleAsync(string vendor, string subject, DateTimeOffset start, DateTimeOffset end) { var meeting = providers.GetRequired(vendor); // "tencent-meeting" / "zoom" / ... return await meeting.CreateMeetingAsync(new CreateMeetingRequest { Subject = subject, StartTime = start, EndTime = end, Attendees = [new() { Email = "alice@example.com", Name = "Alice" }], WaitingRoomEnabled = true, }); }}参会人管理
// 邀请var inviteResult = await meeting.InviteAttendeesAsync(new InviteAttendeesRequest{ MeetingId = info.MeetingId, Attendees = [new() { Email = "bob@example.com" }],});// inviteResult.Success / inviteResult.ProcessedCount
// 移除(腾讯/飞书 DELETE invitees,Zoom PUT registrants/status + action=cancel)var removeResult = await meeting.RemoveAttendeesAsync(new RemoveAttendeesRequest{ MeetingId = info.MeetingId, Attendees = [new() { UserId = "registrant-id" }],});录制查询
public async Task<List<MeetingRecording>> GetRecordingsAsync(IVideoConferenceService meeting, string meetingId) => await meeting.ListRecordingsAsync(meetingId);