Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Concept

TeamWork 协同办公

TeamWork 域的细粒度接口变体、15 个能力接口、能力探测模式、6 家协同办公厂商的能力矩阵与异常错误处理。

Last updated

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,调用方还得记着哪些能用。

所以这里按业务领域拆接口,每家厂商只声明自己实现的接口。运行时注入的是 ITeamWorkApprovalProviderITeamWorkMessageProvider 这样的能力契约,而非一个庞大的 ITeamWork

这与 FileStorage 的胖接口取舍正好相反——FileStorage 各家操作集合几乎一致(纵向差异),TeamWork 各家能力横向差异巨大。差异的方向决定了接口的形状。

15 个能力接口

#接口职责典型方法
1ITeamWorkOrgProvider组织架构部门查询、员工增删改查
2ITeamWorkMessageProvider消息推送工作通知、卡片消息、撤回
3ITeamWorkSsoProvider单点登录授权 URL、令牌交换/刷新、用户信息
4ITeamWorkTodoProvider统一待办创建/更新状态/查询/撤回
5ITeamWorkApprovalProvider轻量审批发起审批、查询状态、撤回
6ITeamWorkHealthProvider健康检查连通性与 token 有效性探测
7ITeamWorkCalendarProvider日历日程创建/查询/更新/删除(飞书)
8ITeamWorkContactProvider客户联系人外部联系人/客户群(企业微信)
9ITeamWorkDocumentProvider文档在线文档协作(飞书)
10ITeamWorkFormProviderOA 表单表单创建/查询/更新(传统 OA)
11ITeamWorkKnowledgeProvider知识库知识库/上传/检索(蓝凌特色)
12ITeamWorkMeetingProvider会议会议预定/查询/取消(致远)
13ITeamWorkOfficialDocumentProvider公文收文/发文/归档(政务 OA)
14ITeamWorkPortalProvider门户门户配置/数据(蓝凌)
15ITeamWorkWorkflowProviderBPM 工作流发起/查询/提交节点/撤回(泛微等)

每个接口都有 ProviderName 属性(string),用于多厂商场景下的路由标识。ITeamWorkWorkflowProvider 面向复杂 BPM 流程,与轻量级的 ITeamWorkApprovalProvider 区分。

能力矩阵

Weaver

Workflow

Health

Feishu

Org

Message

Todo

Approval

Sso

Health

Calendar

Document

WeCom

Org

Message

Approval

Sso

Health

Contact

DingTalk

Org

Message

Todo

Approval

Sso

Health

厂商Provider ID实现的能力接口
钉钉dingtalkOrg、Message、Todo、Approval、Sso、Health
企业微信wecomOrg、Message、Approval、Sso、Health、Contact
飞书feishuOrg、Message、Todo、Approval、Sso、Health、Calendar、Document
泛微weaverWorkflow、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 转型到其他能力接口都返回同一个对象。这让”能力探测”零成本、编译期类型安全。

isisisisisisisITeamWorkCalendarProvider

DingTalkTeamWorkProvider
(一个实例,6 个接口)

ITeamWorkOrgProvider

ITeamWorkMessageProvider

ITeamWorkTodoProvider

ITeamWorkApprovalProvider

ITeamWorkSsoProvider

ITeamWorkHealthProvider

转型失败(飞书才有)

飞书的 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。
  • 常量分区组织DingTalkConstantspartial 类,按 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();
}
}

枚举

枚举用途
ApprovalStatusUnknown / Running / Agreed / Refused / Canceled审批流程状态
TodoStatusPending / Processing / Done / Canceled待办任务状态
MessageDeliveryStateUnknown / Success / Partial / Failed消息投递状态
TeamWorkHealthStatusUnknown / Healthy / Degraded / Unhealthy健康检查状态

枚举值见 TeamWork 抽象参考

已知边界

  • 钉钉待办只支持标记完成UpdateTodoStatusAsync 实际只支持设为 Done,不支持撤回(Descriptor limitations 已记录)。
  • GetDepartmentsAsync 递归整棵子树:钉钉 /topapi/v2/department/listsub 只返回下一级,实现内部递归并做环形防护(HashSet<long> 去重)。
  • GetEmployeesAsync 分页上限:钉钉无”全公司用户”单接口,实现遍历各部门分页拉取,设安全上限 MaxUserListPages = 1000 防止异常数据死循环。
  • 致远 / 蓝凌未实现:仅目录占位,不参与 All 聚合包注册。

相关

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%