TeamWork 域把钉钉、企业微信、飞书、泛微、致远、蓝凌六家协同办公平台的能力拆成一组细粒度接口。不同于 FileStorage 那种”一个胖接口塞所有操作”,这里按业务领域切分出 15 个能力接口,每家厂商只实现自己真正具备的能力。本页解释这套抽象的设计取舍;全部 15 个接口的精确签名见 TeamWork 抽象参考,钉钉实现细节见 TeamWork · 钉钉。
域结构
Bitzsoft.Integrations.TeamWork ← 抽象层(接口 + 异常 + 模型 + 枚举)├── Bitzsoft.Integrations.TeamWork.DingTalk ← 钉钉(最完整,6 个能力接口)├── Bitzsoft.Integrations.TeamWork.WeCom ← 企业微信(含客户联系人)├── Bitzsoft.Integrations.TeamWork.Feishu ← 飞书(含日历 + 文档)├── Bitzsoft.Integrations.TeamWork.Weaver ← 泛微(工作流 + 健康检查)├── Bitzsoft.Integrations.TeamWork.Seeyon ← 致远(目录占位)├── Bitzsoft.Integrations.TeamWork.Landray ← 蓝凌(目录占位)└── Bitzsoft.Integrations.TeamWork.All ← 聚合包为什么是细粒度接口
协同办公厂商的能力差异极大。钉钉有组织架构、消息、待办、审批、SSO、健康检查;企业微信在此基础上多了客户联系人;飞书独有日历和文档;泛微是传统 OA,核心价值在审批流。如果把它们塞进一个胖接口,每家厂商都得对不支持的方法抛 NotSupportedException,调用方还得记着哪些能用。
所以这里按业务领域拆接口,每家厂商只声明自己实现的接口。运行时注入的是 ITeamWorkApprovalProvider、ITeamWorkMessageProvider 这样的能力契约,而非一个庞大的 ITeamWork。
这与 FileStorage 的胖接口取舍正好相反——FileStorage 各家操作集合几乎一致(纵向差异),TeamWork 各家能力横向差异巨大。差异的方向决定了接口的形状。
15 个能力接口
| # | 接口 | 职责 | 典型方法 |
|---|---|---|---|
| 1 | ITeamWorkOrgProvider | 组织架构 | 部门查询、员工增删改查 |
| 2 | ITeamWorkMessageProvider | 消息推送 | 工作通知、卡片消息、撤回 |
| 3 | ITeamWorkSsoProvider | 单点登录 | 授权 URL、令牌交换/刷新、用户信息 |
| 4 | ITeamWorkTodoProvider | 统一待办 | 创建/更新状态/查询/撤回 |
| 5 | ITeamWorkApprovalProvider | 轻量审批 | 发起审批、查询状态、撤回 |
| 6 | ITeamWorkHealthProvider | 健康检查 | 连通性与 token 有效性探测 |
| 7 | ITeamWorkCalendarProvider | 日历 | 日程创建/查询/更新/删除(飞书) |
| 8 | ITeamWorkContactProvider | 客户联系人 | 外部联系人/客户群(企业微信) |
| 9 | ITeamWorkDocumentProvider | 文档 | 在线文档协作(飞书) |
| 10 | ITeamWorkFormProvider | OA 表单 | 表单创建/查询/更新(传统 OA) |
| 11 | ITeamWorkKnowledgeProvider | 知识库 | 知识库/上传/检索(蓝凌特色) |
| 12 | ITeamWorkMeetingProvider | 会议 | 会议预定/查询/取消(致远) |
| 13 | ITeamWorkOfficialDocumentProvider | 公文 | 收文/发文/归档(政务 OA) |
| 14 | ITeamWorkPortalProvider | 门户 | 门户配置/数据(蓝凌) |
| 15 | ITeamWorkWorkflowProvider | BPM 工作流 | 发起/查询/提交节点/撤回(泛微等) |
每个接口都有 ProviderName 属性(string),用于多厂商场景下的路由标识。ITeamWorkWorkflowProvider 面向复杂 BPM 流程,与轻量级的 ITeamWorkApprovalProvider 区分。
能力矩阵
| 厂商 | Provider ID | 实现的能力接口 |
|---|---|---|
| 钉钉 | dingtalk | Org、Message、Todo、Approval、Sso、Health |
| 企业微信 | wecom | Org、Message、Approval、Sso、Health、Contact |
| 飞书 | feishu | Org、Message、Todo、Approval、Sso、Health、Calendar、Document |
| 泛微 | weaver | Workflow、Health |
| 致远 | seeyon | 目录占位,尚未实现 |
| 蓝凌 | landray | 目录占位,尚未实现 |
能力探测模式
调用方常常持有一个”平台”对象,需要在运行时判断它是否具备某项能力。TeamWork 域的标准做法是 is 模式匹配:
public class CalendarSync(ITeamWorkOrgProvider org){ public async Task SyncCalendarIfSupportedAsync(CalendarEventRequest evt) { // ① 用 is 判断当前 provider 是否还实现了日历能力 if (org is ITeamWorkCalendarProvider calendar) { var eventId = await calendar.CreateEventAsync(evt); return; }
// ② 不具备则降级 Log.LogWarning("当前平台 {Provider} 不支持日历,跳过同步", org.ProviderName); }}为什么用 is 而不是 ITeamWorkCalendarProvider.TryGetEventAsync?因为同一个 DingTalkTeamWorkProvider 实例同时实现 6 个接口——DingTalkTeamWorkProvider : ITeamWorkOrgProvider, ITeamWorkMessageProvider, ITeamWorkTodoProvider, ITeamWorkApprovalProvider, ITeamWorkSsoProvider, ITeamWorkHealthProvider。注入任何一个能力接口,is 转型到其他能力接口都返回同一个对象。这让”能力探测”零成本、编译期类型安全。
飞书的 FeishuTeamWorkProvider 同一实例转型到 ITeamWorkCalendarProvider 成功,转型到 ITeamWorkContactProvider 失败——这正是能力差异在类型系统里的表达。
错误处理:异常风格
TeamWork 域用异常风格,不是 Result 模式。失败时抛 TeamWorkException:
public sealed class TeamWorkException : IntegrationException{ public string ProviderName => Provider ?? string.Empty; // 供应商名 public new string? ErrorCode => base.ErrorCode; // 供应商错误码 public new string? ProviderMessage => base.ProviderMessage; // 供应商原始消息
public TeamWorkException( string providerName, string message, string? errorCode = null, string? providerMessage = null, Exception? innerException = null) : base(domain: "TeamWork", message: $"[{providerName}] {message}", provider: providerName, errorCode: errorCode, providerMessage: providerMessage, innerException: innerException) { }}消息格式固定为 [ProviderName] message,调用方一眼能看出是哪家平台报错。错误码与原始消息由各厂商映射(如钉钉把 oapi 的 errcode / errmsg 填入)。
调用方需要 try/catch,或让异常冒泡到统一异常过滤器。这是历史选择,新域(如 EnterpriseInfo、ElectronicSignature)已转向 Result 模式,但 TeamWork 暂未迁移。
钉钉:参考实现
钉钉包是 TeamWork 域里工程化程度最高的实现,适合作为新建厂商包的模板。几个值得借鉴的做法(详见 TeamWork · 钉钉):
- 配置校验走
IValidateOptions<T>,而不是启动期Validate().ValidateOnIntegrationStart()。在IOptions<T>.Value首次访问时延迟校验,错误消息更精准。 - 双 HttpClient 分工:令牌刷新走专用 client(不经 AuthHandler,避免递归),业务请求走 typed client 并挂
DingTalkAuthHandler自动注入 access_token。 - 常量分区组织:
DingTalkConstants是partial类,按ApiPaths/Auth/FieldNames/StatusCodes拆成多个文件,API 路径和字段名不散落在业务代码里。 - Internal/ 分层:
HttpClient/AuthHandler/ModelMapper/OptionsValidator/JsonHelper全部放Internal/,只暴露 Provider 类和 Options。
注册模式
单厂商
// ① 钉钉(配置节绑定式)builder.Services.AddBitzsoftDingTalkTeamWork(builder.Configuration.GetSection("TeamWork:DingTalk"));
// ② 飞书builder.Services.AddBitzsoftFeishuTeamWork(builder.Configuration.GetSection("TeamWork:Feishu"));多厂商聚合
builder.Services.AddBitzsoftTeamWorkAll(builder.Configuration, "TeamWork");配置:
{ "TeamWork": { "DingTalk": { "AppKey": "...", "AppSecret": "...", "AgentId": 123456 }, "Feishu": { "AppId": "...", "AppSecret": "..." } }}每个能力接口都通过 AddIntegrationProviderCapability 绑定到厂商 Provider 实例。钉钉的注册把同一个 DingTalkTeamWorkProvider 绑定到 6 个能力接口:
services.TryAddTransient(sp => new DingTalkTeamWorkProvider(...));var descriptor = DingTalkProviderDescriptor.Instance;services.AddIntegrationProviderCapability<ITeamWorkOrgProvider, DingTalkTeamWorkProvider>(descriptor);services.AddIntegrationProviderCapability<ITeamWorkMessageProvider, DingTalkTeamWorkProvider>(descriptor);services.AddIntegrationProviderCapability<ITeamWorkTodoProvider, DingTalkTeamWorkProvider>(descriptor);services.AddIntegrationProviderCapability<ITeamWorkApprovalProvider, DingTalkTeamWorkProvider>(descriptor);services.AddIntegrationProviderCapability<ITeamWorkSsoProvider, DingTalkTeamWorkProvider>(descriptor);services.AddIntegrationProviderCapability<ITeamWorkHealthProvider, DingTalkTeamWorkProvider>(descriptor);Provider 自身为 Transient,避免 Singleton 捕获 typed HttpClient(HttpClient 不应被 Singleton 长期持有)。单厂商应用直接注入能力接口即可;多厂商应用通过 IIntegrationProviderResolver<ITeamWorkXxxProvider> 按厂商解析。
致远 / 蓝凌:目录占位
致远与蓝凌目前是目录占位,Descriptor 里 capabilityTypes 为空或仅声明,但未实现任何能力接口、不在 All 聚合包注册。需要时按钉钉模板自行实现对应能力接口并注册。
消费
// ① 注入需要的能力接口public class NotificationService(ITeamWorkMessageProvider message){ public async Task PushAsync(string userId, string content) { var result = await message.SendWorkNotificationAsync(new WorkNotificationRequest { ToUser = userId, Content = content, }); // 失败时会抛 TeamWorkException,不会返回失败 Result }}
// ② 能力探测:飞书有日历,钉钉没有public class CalendarService(ITeamWorkOrgProvider org){ public async Task<string?> CreateEventIfSupportedAsync(CalendarEventRequest evt) { if (org is ITeamWorkCalendarProvider calendar) return await calendar.CreateEventAsync(evt); return null; // 当前平台不支持日历 }}
// ③ 多厂商:按 Provider ID 解析public class OrgRouter(IIntegrationProviderResolver<ITeamWorkOrgProvider> orgs){ public async Task<IReadOnlyList<DepartmentInfo>> GetDepartmentsAsync(string platform) { var org = orgs.GetRequired(platform); // "dingtalk" / "wecom" / "feishu" return await org.GetDepartmentsAsync(); }}枚举
| 枚举 | 值 | 用途 |
|---|---|---|
ApprovalStatus | Unknown / Running / Agreed / Refused / Canceled | 审批流程状态 |
TodoStatus | Pending / Processing / Done / Canceled | 待办任务状态 |
MessageDeliveryState | Unknown / Success / Partial / Failed | 消息投递状态 |
TeamWorkHealthStatus | Unknown / Healthy / Degraded / Unhealthy | 健康检查状态 |
枚举值见 TeamWork 抽象参考。
已知边界
- 钉钉待办只支持标记完成:
UpdateTodoStatusAsync实际只支持设为Done,不支持撤回(Descriptorlimitations已记录)。 GetDepartmentsAsync递归整棵子树:钉钉/topapi/v2/department/listsub只返回下一级,实现内部递归并做环形防护(HashSet<long>去重)。GetEmployeesAsync分页上限:钉钉无”全公司用户”单接口,实现遍历各部门分页拉取,设安全上限MaxUserListPages = 1000防止异常数据死循环。- 致远 / 蓝凌未实现:仅目录占位,不参与
All聚合包注册。
相关
- TeamWork 抽象参考:全部 15 个接口的精确签名
- TeamWork · 钉钉:钉钉实现、双 HttpClient 与常量分区
- 三层包结构
- 结构变体
- 错误处理约定