想象一下这样的场景:客户焦急地等待问题解决,而你的团队却在一堆邮件、Excel表格和零散的IM消息中手忙脚乱。这是不是很多企业每天都在上演的真实写照?
在数字化转型的浪潮中,我们不仅要让系统"能用",更要让团队"好用"。飞书就像是协作世界的"超级英雄",它能让原本各自为战的业务系统手拉手,让信息像流水一样顺畅流动。
今天,就让我们一起踏上一段奇妙的旅程------借助Mud.Feishu这个强大的开源工具,为我们的.NET业务系统装上"协作翅膀",实现从传统的工单处理到现代化的全链路任务协同的华丽转身。
为什么我们的系统需要"协作升级"?
当孤岛遇上协作:那些年我们一起踩过的坑
还记得那个尴尬的下午吗?客户在电话那头焦急地询问:"我的问题解决得怎么样了?" 而你却在三个不同的系统之间来回切换,试图拼凑出完整的答案。
随着企业越来越大,业务越来越复杂,我们的传统系统就像一个个独立的"小岛",虽然每个小岛上都有宝藏(数据),但它们之间却没有桥梁:
-
信息都在各自的"保险柜"里:客服用一套系统,技术用另一套,产品还有自己的系统。想要看全局?那可真是个挑战!
-
沟通还在"石器时代":邮件一来一回可能要等几小时,重要消息可能淹没在收件箱里,IM聊天记录又容易被刷屏遗忘。
-
任务进展像"盲人摸象":谁在负责什么?进行到哪一步了?这些问题往往需要开会问一圈才能搞清楚。
-
跨部门协作像"跨越大海":技术说这是产品问题,产品说这是客服问题,客户的问题在部门之间"漂流",最后不了了之。
飞书API给传统系统装上"智能大脑"
如果说传统系统是"单机版",那么飞书API就是让它们连入"互联网"的魔法棒。飞书不仅仅是又一个办公软件,它的任务管理API就像是协作世界的"通用语言":
-
开放的"乐高积木":丰富的API接口就像乐高积木,你可以随心所欲地搭建适合自己的协作场景。
-
实时"心跳感应":基于WebSocket的推送机制让任务状态变化像心跳一样实时传递,告别"刷新查看"的等待时代。
-
移动"随身助手":无论你在咖啡厅还是在路上,手机上的任务提醒和更新都不会错过重要事项。
-
企业"安全卫士":完善的权限管理和数据加密,让敏感信息在开放协作的同时依然安全可靠。
为什么.NET是最佳选择
在众多技术栈中,.NET就像是那个稳重又有内涵的"理想伴侣",特别适合承担企业级集成的重任:
-
稳如泰山的"老司机":.NET平台经过多年历练,性能稳定可靠,就像一个经验丰富的老司机,能在复杂的业务环境中稳健前行。
-
微软"靠山"很给力:有微软这样的技术巨头长期支持,不用担心技术路线突然变卦,开发路上更有安全感。
-
与时俱进"新青年":从.NET 6.0开始,整个平台焕然一新,异步编程和并发处理能力让复杂场景的处理变得游刃有余。
-
工具链"豪华套餐":Visual Studio就像是一把"瑞士军刀",配合NuGet这个"百宝箱",开发效率自然节节攀升。
一个真实的应用场景:客服小王的一天
小王的"日常折磨":传统工单系统的困境
让我们跟随客服小王,看看她是如何在传统工单系统中"挣扎"的:
早上9点,小王刚坐下就收到了客户的紧急投诉。她迅速在系统中创建了工单,然后发送邮件给技术部门的老李。两个小时过去了,老李才回复说这个问题需要产品部门的小张确认...
这听起来是不是很熟悉?传统工单系统就像是一个"信息传递游戏",每个人都在等待,客户却在焦虑。让我们看看小王的工作流程图:
graph TD A客户提交工单 --> B邮件通知客服 B --> C手动分配处理人 C --> DIM沟通协调 D --> EExcel跟踪进度 E --> F手动更新状态 F --> G邮件回复客户 style B fill:#ffcccc style D fill:#ffcccc style E fill:#ffcccc style G fill:#ffcccc
看到那些红色的步骤了吗?每一步都是小王工作中的"痛点"。
三个让小王"头疼"的大难题
🌫️ 难题一:任务消失在"信息黑洞"里
小王每天都在玩"捉迷藏"游戏:
- 邮件森林:重要的任务分配邮件可能被淹没在收件箱的数百封邮件中
- IM聊天刷屏:关键信息在群聊里被各种表情包和闲聊淹没
- 管理层的"望远镜":想要了解整体进度?那就得一个个去问,就像用望远镜看星星
- 客户的"猜谜游戏":客户打电话问进度,小王只能尴尬地说"我帮您问问"
🧩 难题二:跨部门协作像"拼图游戏"
工单在不同部门之间传递,就像是在玩拼图,但总少了几块:
- 系统"方言"不同:客服系统、技术系统、产品系统各自说各自的"语言"
- 信息"接力赛"中的掉棒:工单在传递过程中,重要的背景信息"不翼而飞"
- 责任"皮球游戏":这到底是技术问题还是产品问题?大家开始踢皮球
- 知识"孤岛":解决方案和个人经验都留在了各自的大脑里,无法形成团队财富
⏰ 难题三:优先级管理像"无头苍蝇"
传统系统就像是没有导航的司机:
- SLA"定时炸弹":重要的工单快要到期了,但系统不会自动提醒
- 进度"盲人摸象":哪个工单会超时?只能凭感觉猜测
- 管理层"雾里看花":想要全局视图?抱歉,系统只支持单点查看
- 资源调配"拍脑袋":谁该处理什么任务?更多靠经验而非数据
小王的"逆袭":当工单系统遇上飞书
现在,让我们看看当飞书任务管理介入后,小王的工作发生了怎样的神奇变化:
graph TD A客户提交工单 --> B✨ 自动创建飞书任务 B --> C🎯 智能分配责任人 C --> D📱 实时状态同步 D --> E🤝 多方协同处理 E --> F⏰ 自动预警提醒 F --> G🎉 完成自动通知 style B fill:#ccffcc style C fill:#ccffcc style D fill:#ccffcc style E fill:#ccffcc style F fill:#ccffcc style G fill:#ccffcc
看到那些绿色步骤了吗?每一个都代表着小王工作效率的飞跃提升!
小王的"幸福感提升清单":
-
🚀 从手工到自动化:原来要人工操作的步骤,现在系统自动搞定,小王终于有时间喝杯咖啡了
-
🔍 从黑盒到透明:任务进展一目了然,管理层再也不用追着她问进度,客户也能自己查看状态
-
🌉 从孤岛到通途:统一的协作平台让跨部门合作变得像"左邻右舍"一样自然
-
😊 从被动到主动:客户收到实时更新通知,满意度直线上升,小王的KPI也跟着水涨船高
搭积木的艺术:构建我们的协作桥梁
先看看我们的"家底":现有系统是什么样的
在企业级应用的世界里,.NET系统就像是精心设计的"建筑",主要有两种常见的"建筑风格":
经典的处理流程
graph TB subgraph "表现层 Presentation Layer" AASP.NET Core Web API B前端应用 React/Vue end subgraph "业务逻辑层 Business Logic Layer" C工单管理服务 D客户管理服务 E通知服务 end subgraph "数据访问层 Data Access Layer" FEntity Framework Core GSQL Server数据库 end A --> C A --> D B --> A C --> F D --> F E --> C F --> G
微服务的现代布局
graph TB subgraph "API网关" AOcelot/Gateway end subgraph "微服务集群" B工单服务 C用户服务 D通知服务 E订单服务 end subgraph "基础设施" FRedis缓存 GRabbitMQ消息队列 HSQL Server集群 end A --> B A --> C A --> D A --> E B --> F B --> G C --> F D --> F E --> H
使用飞书后优化的系统流程
现在到了最激动人心的部分!我们要在现有系统和飞书之间搭建一座"智能桥梁"。这个桥不是用砖块,而是用代码和智慧搭建的:
graph TB subgraph "业务应用层" A工单管理系统 B客户支持系统 C项目管理系统 end subgraph "飞书集成适配层" DMud.Feishu HTTP API客户端 EMud.Feishu WebSocket客户端 FMud.Feishu Webhook处理器 end subgraph "同步服务层" G任务同步服务 H事件路由服务 I状态同步服务 end subgraph "监控与回退机制" J健康监控 K重试机制 L降级策略 end subgraph "飞书平台" M飞书任务API N飞书WebSocket O飞书事件订阅 end A --> G B --> G C --> G G --> D G --> E G --> F D --> M E --> N F --> O G --> H H --> I G --> J J --> K K --> L
适配层组件交互流程
sequenceDiagram participant B as 业务系统 participant S as 同步服务 participant A as 适配层 participant F as 飞书平台 participant W as WebSocket/Webhook Note over B,W: 双向数据同步流程 B->>S: 创建/更新工单 S->>A: 调用任务API A->>F: HTTP API调用 F-->>A: 返回结果 A-->>S: 同步完成 S-->>B: 更新关联ID Note over F,W: 实时事件推送 F->>W: 任务状态变更 W->>A: 事件通知 A->>S: 处理业务逻辑 S->>B: 更新工单状态
适配层(Mud.Feishu封装)
Mud.Feishu提供了完整的飞书API封装:
- HTTP API客户端 :基于
IFeishuTenantV2Task接口,提供完整的任务管理能力 - WebSocket客户端:实时事件订阅,支持自动重连和心跳检测
- Webhook处理器:HTTP回调事件处理,支持事件路由和中间件模式
csharp
// 核心接口定义示例
public interface IFeishuTaskAdapter
{
Task<string> CreateTaskAsync(CreateTaskRequest request);
Task<TaskInfo> GetTaskAsync(string taskGuid);
Task<bool> UpdateTaskAsync(string taskGuid, UpdateTaskRequest request);
Task<bool> DeleteTaskAsync(string taskGuid);
}
同步服务(双向/事件驱动)
同步服务负责业务系统与飞书之间的数据同步:
csharp
public class TaskSyncService
{
private readonly IFeishuTaskAdapter _feishuAdapter;
private readonly ITicketRepository _ticketRepository;
private readonly ILogger<TaskSyncService> _logger;
// 双向同步逻辑
public async Task SyncTicketToTaskAsync(string ticketId)
{
var ticket = await _ticketRepository.GetByIdAsync(ticketId);
if (ticket?.FeishuTaskId == null)
{
var taskRequest = MapTicketToTask(ticket);
var result = await _feishuAdapter.CreateTaskAsync(taskRequest);
ticket.FeishuTaskId = result;
await _ticketRepository.UpdateAsync(ticket);
}
}
}
监控与回退机制
确保集成系统的稳定性和可靠性:
- 健康监控:定期检查API可用性和连接状态
- 重试机制:网络异常时自动重试,支持指数退避策略
- 降级策略:飞书服务不可用时,系统可降级到本地任务管理
数据同步策略
实时同步(关键状态变更)
通过WebSocket和Webhook实现关键状态的实时同步:
graph LR subgraph "实时事件流" A飞书任务状态变更 --> BWebSocket/Webhook B --> C事件路由器 C --> D状态同步服务 D --> E业务系统 end subgraph "批量补偿流" F定时任务触发 --> G数据对账服务 G --> H差异检测 H --> I数据修复 I --> E end
csharp
// WebSocket事件处理示例
public class TaskEventHandler : IFeishuEventHandler
{
public string SupportedEventType => "task.status_changed";
public async Task HandleAsync(EventData eventData, CancellationToken cancellationToken = default)
{
switch (eventData.EventType)
{
case "task.status_changed":
await HandleTaskStatusChanged(eventData);
break;
case "task.assignee_updated":
await HandleAssigneeUpdated(eventData);
break;
}
}
}
批量补偿(定时对账)
定时执行批量对账,确保数据一致性:
csharp
public class TaskReconciliationService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await ReconcileTasksAsync();
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
安全方案
OAuth 2.0流程
基于Mud.Feishu的令牌管理机制:
csharp
// 配置示例
builder.Services.AddFeishuServices()
.ConfigureFrom(builder.Configuration)
.AddTaskApi()
.AddTokenManagers()
.Build();
API密钥管理
- 应用密钥安全存储(环境变量/密钥管理服务)
- 访问令牌自动刷新和缓存
- 权限最小化原则,按需申请API权限
IP白名单
在飞书开放平台配置服务端IP白名单,增强安全性。
动手时间:一步步打造你的飞书集成模块
1. 准备工作:让一切就绪
第一步:和飞书"握手"------创建应用
让我们先到飞书开放平台这个"游乐场"注册我们的"入场券":
-
领取身份卡:创建企业自建应用
- 给它起个响亮的名字:"客户支持工单集成助手"
- 写个自我介绍:"我是来帮大家告别工单混乱的小能手"
-
申请通行证:配置应用权限
- 任务管理全权限:读取、创建、更新、删除(就像给了一把万能钥匙)
- 任务清单查看权:知道任务放在哪个"房间"
- 用户基本信息权:认识团队里的每个"小伙伴"
- 事件订阅权:能够监听任务世界的"风吹草动"
-
设置专属热线:配置事件订阅
- 提供你的"电话号码"(请求网址):
https://your-domain.com/api/feishu/webhook - 准备"暗号"(验证Token):只有你懂的验证字符串
- 配置"保险箱"(数据加密密钥):用AES-256保护敏感信息
- 提供你的"电话号码"(请求网址):
第二步:搭建我们的"开发工作室"
现在让我们创建一个干净的.NET项目,并邀请我们的"得力助手"们加入:
xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mud.Feishu" Version="1.0.9" />
<PackageReference Include="Mud.Feishu.WebSocket" Version="1.0.9" />
<PackageReference Include="Mud.Feishu.Webhook" Version="1.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
</ItemGroup>
</Project>
基础配置文件
json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Feishu": {
"AppId": "cli_xxxxxxxxxxxxxxxx",
"AppSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"BaseUrl": "https://open.feishu.cn",
"TimeOut": "30",
"RetryCount": 3
},
"Feishu:WebSocket": {
"AutoReconnect": true,
"MaxReconnectAttempts": 5,
"ReconnectDelayMs": 5000,
"HeartbeatIntervalMs": 30000,
"EnableLogging": true
},
"FeishuWebhook": {
"RoutePrefix": "api/feishu/webhook",
"VerificationToken": "your_verification_token",
"EncryptKey": "your_encrypt_key",
"EnableRequestLogging": true
},
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=TicketSystem;Trusted_Connection=true;"
}
}
2. 核心魔法:让代码活起来
认证服务:让系统"记住"我是谁
想象一下,每次调用飞书API都要重新登录是多么繁琐。幸运的是,Mud.Feishu已经为我们准备了一个"智能门卫",它会自动处理认证和刷新token这些烦人的事情。我们只需要告诉它"密码"在哪里就行:
graph TB subgraph "服务注册架构" A.NET Host Builder --> B飞书API服务 A --> CWebSocket服务 A --> DWebhook服务 A --> E自定义业务服务 B --> FToken管理器 B --> G任务API客户端 C --> HWebSocket管理器 C --> I事件处理器1 C --> J事件处理器2 D --> KWebhook路由 D --> L事件处理器 E --> M任务同步服务 E --> N通知服务 end
csharp
// Program.cs - 服务注册
var builder = WebApplication.CreateBuilder(args);
// 注册飞书服务
builder.Services.AddFeishuServices()
.ConfigureFrom(builder.Configuration)
.AddTaskApi()
.AddTokenManagers()
.Build();
// 注册WebSocket服务
builder.Services.AddFeishuWebSocketServiceBuilder()
.ConfigureFrom(builder.Configuration)
.UseMultiHandler()
.AddHandler<TaskEventHandler>()
.AddHandler<TicketEventHandler>()
.Build();
// 注册Webhook服务
builder.Services.AddFeishuWebhookServiceBuilder(builder.Configuration)
.AddHandler<TaskWebhookHandler>()
.Build();
// 自定义服务注册
builder.Services.AddScoped<IFeishuTaskService, FeishuTaskService>();
builder.Services.AddScoped<ITicketSyncService, TicketSyncService>();
任务API客户端:开箱即用的飞书 .net SDK
csharp
public interface IFeishuTaskService
{
Task<string> CreateTaskFromTicketAsync(Ticket ticket);
Task<bool> UpdateTaskAsync(string taskGuid, UpdateTaskRequest request);
Task<TaskInfo?> GetTaskAsync(string taskGuid);
Task<bool> DeleteTaskAsync(string taskGuid);
Task<bool> AddTaskMemberAsync(string taskGuid, string userId, string role);
Task<List<TaskInfo>> GetTasksByProjectAsync(string projectKey);
}
public class FeishuTaskService : IFeishuTaskService
{
private readonly IFeishuTenantV2Task _taskApi;
private readonly ILogger<FeishuTaskService> _logger;
private readonly IMapper _mapper;
public FeishuTaskService(
IFeishuTenantV2Task taskApi,
ILogger<FeishuTaskService> logger,
IMapper mapper)
{
_taskApi = taskApi ?? throw new ArgumentNullException(nameof(taskApi));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
public async Task<string> CreateTaskFromTicketAsync(Ticket ticket)
{
try
{
var createRequest = _mapper.Map<CreateTaskRequest>(ticket);
// 设置任务清单
createRequest.Tasklists = new[]
{
new TaskInTaskListInfo
{
TasklistGuid = GetTaskListByPriority(ticket.Priority),
CustomFields = GetCustomFieldsForTicket(ticket)
}
};
// 设置任务成员
var assignees = await GetAssigneesForTicket(ticket);
createRequest.Members = assignees.Select(u => new TaskMemberInfo
{
UserId = u.FeishuUserId,
UserType = UserType.User,
TaskRole = TaskRole.Assignee
}).ToArray();
// 设置截止时间
if (ticket.DueDate.HasValue)
{
createRequest.Due = new TaskTime
{
Timestamp = ((DateTimeOffset)ticket.DueDate.Value).ToUnixTimeMilliseconds().ToString()
};
}
// 设置提醒
if (ticket.Priority == TicketPriority.High)
{
createRequest.Reminders = new[]
{
new TaskReminder
{
MinutesBefore = 60, // 1小时前提醒
ReminderType = ReminderType.Push
}
};
}
var result = await _taskApi.CreateTaskAsync(createRequest);
if (result?.Code == 0 && result.Data != null)
{
_logger.LogInformation("成功创建飞书任务,工单ID: {TicketId}, 任务ID: {TaskGuid}",
ticket.Id, result.Data.Task.Guid);
return result.Data.Task.Guid;
}
_logger.LogError("创建飞书任务失败,工单ID: {TicketId}, 错误: {Error}",
ticket.Id, result?.Msg);
throw new FeishuException($"创建飞书任务失败: {result?.Msg}");
}
catch (Exception ex)
{
_logger.LogError(ex, "创建飞书任务时发生异常,工单ID: {TicketId}", ticket.Id);
throw;
}
}
public async Task<bool> UpdateTaskAsync(string taskGuid, UpdateTaskRequest request)
{
try
{
var result = await _taskApi.UpdateTaskAsync(taskGuid, request);
var success = result?.Code == 0;
if (success)
{
_logger.LogInformation("成功更新飞书任务,任务ID: {TaskGuid}", taskGuid);
}
else
{
_logger.LogWarning("更新飞书任务失败,任务ID: {TaskGuid}, 错误: {Error}",
taskGuid, result?.Msg);
}
return success;
}
catch (Exception ex)
{
_logger.LogError(ex, "更新飞书任务时发生异常,任务ID: {TaskGuid}", taskGuid);
return false;
}
}
public async Task<TaskInfo?> GetTaskAsync(string taskGuid)
{
try
{
var result = await _taskApi.GetTaskByIdAsync(taskGuid);
return result?.Code == 0 ? result.Data?.Task : null;
}
catch (Exception ex)
{
_logger.LogError(ex, "获取飞书任务详情时发生异常,任务ID: {TaskGuid}", taskGuid);
return null;
}
}
public async Task<bool> DeleteTaskAsync(string taskGuid)
{
try
{
var result = await _taskApi.DeleteTaskByIdAsync(taskGuid);
var success = result?.Code == 0;
if (success)
{
_logger.LogInformation("成功删除飞书任务,任务ID: {TaskGuid}", taskGuid);
}
return success;
}
catch (Exception ex)
{
_logger.LogError(ex, "删除飞书任务时发生异常,任务ID: {TaskGuid}", taskGuid);
return false;
}
}
private string GetTaskListByPriority(TicketPriority priority)
{
return priority switch
{
TicketPriority.High => "high_priority_tasklist_guid",
TicketPriority.Medium => "medium_priority_tasklist_guid",
TicketPriority.Low => "low_priority_tasklist_guid",
_ => "default_tasklist_guid"
};
}
private CustomFieldValue[] GetCustomFieldsForTicket(Ticket ticket)
{
return new[]
{
new CustomFieldValue
{
FieldId = "ticket_id_field",
TextValue = ticket.Id
},
new CustomFieldValue
{
FieldId = "customer_field",
TextValue = ticket.CustomerName
},
new CustomFieldValue
{
FieldId = "project_field",
SingleSelectValue = new EnumValue
{
Id = ticket.ProjectId.ToString()
}
}
};
}
}
WebSocket处理器:飞书事件"快递员"
csharp
public class TaskEventHandler : IFeishuEventHandler
{
private readonly IFeishuTaskService _taskService;
private readonly ITicketSyncService _syncService;
private readonly ILogger<TaskEventHandler> _logger;
public TaskEventHandler(
IFeishuTaskService taskService,
ITicketSyncService syncService,
ILogger<TaskEventHandler> logger)
{
_taskService = taskService;
_syncService = syncService;
_logger = logger;
}
public string SupportedEventType => "task.status_changed";
public async Task HandleAsync(EventData eventData, CancellationToken cancellationToken = default)
{
try
{
switch (eventData.EventType)
{
case "task.status_changed":
await HandleTaskStatusChangedAsync(eventData);
break;
case "task.assignee_updated":
await HandleAssigneeUpdatedAsync(eventData);
break;
case "task.comment_added":
await HandleCommentAddedAsync(eventData);
break;
default:
_logger.LogDebug("收到未处理的任务事件类型: {EventType}", eventData.EventType);
break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "处理飞书任务事件时发生异常,事件类型: {EventType}", eventData.EventType);
}
}
private async Task HandleTaskStatusChangedAsync(EventData eventData)
{
var taskEvent = JsonSerializer.Deserialize<TaskStatusChangedEvent>(eventData.Data.ToString());
_logger.LogInformation("任务状态变更,任务ID: {TaskGuid}, 新状态: {Status}",
taskEvent.Task.Guid, taskEvent.Task.Status);
// 同步到工单系统
await _syncService.SyncTaskStatusToTicketAsync(taskEvent.Task.Guid, taskEvent.Task.Status);
// 发送通知
if (taskEvent.Task.Status == "done")
{
await NotifyTaskCompletedAsync(taskEvent.Task);
}
}
private async Task HandleAssigneeUpdatedAsync(EventData eventData)
{
var taskEvent = JsonSerializer.Deserialize<AssigneeUpdatedEvent>(eventData.Data.ToString());
_logger.LogInformation("任务负责人更新,任务ID: {TaskGuid}", taskEvent.Task.Guid);
// 同步分配人信息到工单
await _syncService.SyncTaskAssigneeToTicketAsync(taskEvent.Task.Guid, taskEvent.Task.Members);
}
private async Task NotifyTaskCompletedAsync(TaskInfo task)
{
// 发送飞书通知到相关群组
// 这里可以调用飞书消息API发送通知
}
}
3. 业务领域适配
对象映射设计:工单 ↔ 飞书任务字段对照表
graph LR subgraph "工单系统" ATicket --> BId A --> CTitle A --> DDescription A --> EPriority A --> FAssignee A --> GDueDate end subgraph "映射转换" HAutoMapper I业务规则引擎 J数据转换器 end subgraph "飞书任务" KCreateTaskRequest --> LSummary K --> MDescription K --> NIsMilestone K --> OMembers K --> PDue K --> QTasklists end B --> H C --> H D --> I E --> I F --> J G --> J H --> L I --> N J --> O J --> P style H fill:#e1f5fe style I fill:#e8f5e8 style J fill:#fff3e0
使用AutoMapper进行对象映射配置:
csharp
public class FeishuMappingProfile : Profile
{
public FeishuMappingProfile()
{
// 工单 -> 飞书任务创建请求
CreateMap<Ticket, CreateTaskRequest>()
.ForMember(dest => dest.Summary, opt => opt.MapFrom(src => $"[工单#{src.Id}] {src.Title}"))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => BuildTaskDescription(src)))
.ForMember(dest => dest.Start, opt => opt.MapFrom(src => src.CreatedDate.HasValue
? new TasksStartTime { Timestamp = ((DateTimeOffset)src.CreatedDate.Value).ToUnixTimeMilliseconds().ToString() }
: null))
.ForMember(dest => dest.Mode, opt => opt.MapFrom(src => src.AssigneeCount > 1 ? 2 : 1)) // 或签/会签
.ForMember(dest => dest.IsMilestone, opt => opt.MapFrom(src => src.Priority == TicketPriority.High));
// 飞书任务 -> 工单状态更新
CreateMap<TaskInfo, TicketStatusUpdate>()
.ForMember(dest => dest.TicketId, opt => opt.MapFrom(src => ExtractTicketId(src.Extra)))
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => MapTaskStatusToTicketStatus(src.Status)))
.ForMember(dest => dest.CompletedAt, opt => opt.MapFrom(src => ParseCompletedAt(src.CompletedAt)));
}
private string BuildTaskDescription(Ticket ticket)
{
var description = new StringBuilder();
description.AppendLine($"**客户:** {ticket.CustomerName}");
description.AppendLine($"**项目:** {ticket.ProjectName}");
description.AppendLine($"**优先级:** {ticket.Priority}");
description.AppendLine();
description.AppendLine("**问题描述:**");
description.AppendLine(ticket.Description);
if (!string.IsNullOrEmpty(ticket.Attachments))
{
description.AppendLine();
description.AppendLine("**附件:**");
description.AppendLine(ticket.Attachments);
}
return description.ToString();
}
private TicketStatus MapTaskStatusToTicketStatus(string? taskStatus)
{
return taskStatus switch
{
"todo" => TicketStatus.InProgress,
"done" => TicketStatus.Resolved,
_ => TicketStatus.Open
};
}
}
同步策略实现
csharp
public class TicketSyncService : ITicketSyncService
{
private readonly IFeishuTaskService _feishuTaskService;
private readonly ITicketRepository _ticketRepository;
private readonly INotificationService _notificationService;
private readonly ILogger<TicketSyncService> _logger;
// 实时事件监听(工单创建/更新)
public async Task HandleTicketCreatedAsync(Ticket ticket)
{
try
{
// 只为高优先级工单创建飞书任务
if (ticket.Priority >= TicketPriority.Medium)
{
var taskGuid = await _feishuTaskService.CreateTaskFromTicketAsync(ticket);
ticket.FeishuTaskId = taskGuid;
await _ticketRepository.UpdateAsync(ticket);
_logger.LogInformation("为新工单创建飞书任务,工单ID: {TicketId}, 任务ID: {TaskGuid}",
ticket.Id, taskGuid);
// 发送通知
await _notificationService.NotifyTaskCreatedAsync(ticket, taskGuid);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "处理工单创建事件时发生异常,工单ID: {TicketId}", ticket.Id);
}
}
// 定时全量同步(防丢失)
public async Task ReconcileTasksAsync()
{
_logger.LogInformation("开始执行任务对账...");
try
{
// 获取所有关联了飞书任务的工单
var ticketsWithTasks = await _ticketRepository.GetTicketsWithFeishuTasksAsync();
foreach (var ticket in ticketsWithTasks)
{
if (string.IsNullOrEmpty(ticket.FeishuTaskId))
continue;
// 检查飞书任务是否存在且状态一致
var task = await _feishuTaskService.GetTaskAsync(ticket.FeishuTaskId);
if (task == null)
{
_logger.LogWarning("飞书任务不存在,工单ID: {TicketId}, 任务ID: {TaskGuid}",
ticket.Id, ticket.FeishuTaskId);
// 重新创建任务
var newTaskGuid = await _feishuTaskService.CreateTaskFromTicketAsync(ticket);
ticket.FeishuTaskId = newTaskGuid;
await _ticketRepository.UpdateAsync(ticket);
}
else if (MapTaskStatusToTicketStatus(task.Status) != ticket.Status)
{
// 状态不一致,同步飞书任务状态到工单
await SyncTaskStatusToTicketAsync(ticket.FeishuTaskId, task.Status);
}
}
_logger.LogInformation("任务对账完成");
}
catch (Exception ex)
{
_logger.LogError(ex, "执行任务对账时发生异常");
}
}
public async Task SyncTaskStatusToTicketAsync(string taskGuid, string taskStatus)
{
try
{
var ticketId = ExtractTicketIdFromTask(taskGuid);
if (string.IsNullOrEmpty(ticketId))
return;
var ticket = await _ticketRepository.GetByIdAsync(ticketId);
if (ticket == null)
return;
var newStatus = MapTaskStatusToTicketStatus(taskStatus);
if (ticket.Status != newStatus)
{
ticket.Status = newStatus;
ticket.StatusUpdatedBy = "FeishuSync";
ticket.StatusUpdatedAt = DateTime.UtcNow;
await _ticketRepository.UpdateAsync(ticket);
_logger.LogInformation("同步飞书任务状态到工单,任务ID: {TaskGuid}, 工单ID: {TicketId}, 新状态: {Status}",
taskGuid, ticketId, newStatus);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "同步飞书任务状态到工单时发生异常,任务ID: {TaskGuid}", taskGuid);
}
}
}
容错设计:异常分类、重试策略、死信队列
csharp
public class FaultTolerantTaskService : IFeishuTaskService
{
private readonly IFeishuTaskService _innerService;
private readonly IAsyncPolicy _retryPolicy;
private readonly ILogger<FaultTolerantTaskService> _logger;
private readonly IDeadLetterQueue _deadLetterQueue;
public FaultTolerantTaskService(
IFeishuTaskService innerService,
ILogger<FaultTolerantTaskService> logger,
IDeadLetterQueue deadLetterQueue)
{
_innerService = innerService;
_logger = logger;
_deadLetterQueue = deadLetterQueue;
// 配置重试策略
_retryPolicy = Policy
.Handle<FeishuApiException>(ex => ex.IsTransient)
.Or<HttpRequestException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryAttempt, context) =>
{
_logger.LogWarning("操作失败,准备第{RetryAttempt}次重试,延迟{Delay}ms",
retryAttempt, timespan.TotalMilliseconds);
});
}
public async Task<string> CreateTaskFromTicketAsync(Ticket ticket)
{
try
{
return await _retryPolicy.ExecuteAsync(() => _innerService.CreateTaskFromTicketAsync(ticket));
}
catch (Exception ex)
{
// 非临时性异常或重试次数耗尽,加入死信队列
await _deadLetterQueue.EnqueueAsync(new DeadLetterMessage
{
Operation = "CreateTask",
Data = ticket,
Exception = ex,
Timestamp = DateTime.UtcNow
});
_logger.LogError(ex, "创建飞书任务失败并加入死信队列,工单ID: {TicketId}", ticket.Id);
throw;
}
}
}
4. 关键代码片段
OAuth 2.0授权流程实现(含刷新逻辑)
Mud.Feishu已经内置了完整的OAuth流程,我们只需要配置:
csharp
// appsettings.json中的配置
{
"Feishu": {
"AppId": "cli_xxxxxxxxxxxxxxxx",
"AppSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
// 服务注册
builder.Services.AddFeishuApiService(builder.Configuration);
创建任务并关联工单的完整示例
csharp
[ApiController]
[Route("api/[controller]")]
public class TicketController : ControllerBase
{
private readonly ITicketService _ticketService;
private readonly IFeishuTaskService _feishuTaskService;
private readonly ITicketSyncService _syncService;
[HttpPost]
public async Task<IActionResult> CreateTicket([FromBody] CreateTicketRequest request)
{
try
{
// 1. 创建工单
var ticket = await _ticketService.CreateTicketAsync(request);
// 2. 如果是高优先级工单,自动创建飞书任务
if (ticket.Priority >= TicketPriority.Medium)
{
var taskGuid = await _feishuTaskService.CreateTaskFromTicketAsync(ticket);
ticket.FeishuTaskId = taskGuid;
await _ticketService.UpdateTicketAsync(ticket);
}
return Ok(new { TicketId = ticket.Id, FeishuTaskId = ticket.FeishuTaskId });
}
catch (Exception ex)
{
_logger.LogError(ex, "创建工单时发生异常");
return StatusCode(500, new { Error = "创建工单失败" });
}
}
[HttpPut("{id}/status")]
public async Task<IActionResult> UpdateTicketStatus(string id, [FromBody] UpdateStatusRequest request)
{
try
{
var ticket = await _ticketService.GetTicketAsync(id);
if (ticket == null)
return NotFound();
// 更新工单状态
ticket.Status = request.Status;
await _ticketService.UpdateTicketAsync(ticket);
// 同步到飞书任务
if (!string.IsNullOrEmpty(ticket.FeishuTaskId))
{
var updateRequest = new UpdateTaskRequest
{
Status = MapTicketStatusToTaskStatus(request.Status),
CompletedAt = request.Status == TicketStatus.Resolved ?
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString() : null
};
await _feishuTaskService.UpdateTaskAsync(ticket.FeishuTaskId, updateRequest);
}
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "更新工单状态时发生异常,工单ID: {TicketId}", id);
return StatusCode(500, new { Error = "更新状态失败" });
}
}
}
WebSocket处理器与业务逻辑解耦设计
csharp
public class FeishuWebSocketBackgroundService : BackgroundService
{
private readonly IFeishuWebSocketManager _webSocketManager;
private readonly IEnumerable<IFeishuWebSocketEventHandler> _handlers;
private readonly ILogger<FeishuWebSocketBackgroundService> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// 启动WebSocket连接
await _webSocketManager.StartAsync(stoppingToken);
// 订阅事件
_webSocketManager.MessageReceived += async (sender, e) =>
{
await HandleMessageAsync(e.Message);
};
_webSocketManager.Error += async (sender, e) =>
{
_logger.LogError(e.Exception, "WebSocket连接发生错误");
await HandleErrorAsync(e.Exception);
};
_webSocketManager.Disconnected += async (sender, e) =>
{
_logger.LogWarning("WebSocket连接断开,原因: {Reason}", e.CloseStatusDescription);
await HandleDisconnectionAsync();
};
// 保持服务运行
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000, stoppingToken);
}
}
private async Task HandleMessageAsync(FeishuWebSocketMessage message)
{
var tasks = _handlers.Select(handler =>
SafeHandleAsync(handler, message));
await Task.WhenAll(tasks);
}
private async Task SafeHandleAsync(IFeishuWebSocketEventHandler handler, FeishuWebSocketMessage message)
{
try
{
await handler.HandleAsync(message);
}
catch (Exception ex)
{
_logger.LogError(ex, "事件处理器处理消息时发生异常,处理器类型: {HandlerType}",
handler.GetType().Name);
}
}
}
应用场景落地详解
🔧 场景一:让工单"活"起来------自动飞书任务生成
工单到任务的"变身"过程
sequenceDiagram participant C as 客户/系统 participant T as 工单系统 participant E as 事件总线 participant S as 同步服务 participant F as 飞书API participant N as 通知服务 Note over C,N: 工单自动生成飞书任务流程 C->>T: 提交工单 T->>T: 验证工单信息 T->>T: 保存工单到数据库 alt 高优先级工单 T->>E: 发布工单创建事件 E->>S: 订阅事件处理 S->>S: 检查是否需要创建任务 S->>F: 调用飞书任务API F-->>S: 返回任务ID S->>T: 更新工单关联ID S->>N: 发送创建通知 end alt 客户标记紧急 T->>E: 发布紧急标记事件 E->>S: 处理紧急事件 S->>F: 创建或更新任务优先级 S->>N: 发送紧急通知 end alt SLA预警 T->>E: 发布SLA预警事件 E->>S: 处理SLA预警 S->>F: 创建任务并设置提醒 end
什么时候该"变身"?------触发时机揭秘
就像超级英雄有自己的"变身"条件,我们的工单也需要在合适的时机才生成飞书任务:
csharp
public class TaskCreationTrigger
{
// 1. 高优先级工单创建时自动触发
public async Task HandleTicketCreatedAsync(TicketCreatedEvent @event)
{
if (@event.Ticket.Priority >= TicketPriority.High)
{
await CreateFeishuTaskAsync(@event.Ticket);
}
}
// 2. 客户标记紧急时触发
public async Task HandleTicketMarkedUrgentAsync(TicketMarkedUrgentEvent @event)
{
// 如果还没有飞书任务,立即创建
if (string.IsNullOrEmpty(@event.Ticket.FeishuTaskId))
{
await CreateFeishuTaskAsync(@event.Ticket);
}
else
{
// 更新现有任务优先级
await UpdateTaskPriorityAsync(@event.Ticket.FeishuTaskId, TicketPriority.High);
}
}
// 3. SLA即将超时预警时触发
public async Task HandleSLAWarningAsync(SLAWarningEvent @event)
{
if (@event.Ticket.Priority >= TicketPriority.Medium)
{
await CreateFeishuTaskAsync(@event.Ticket);
}
}
}
实现步骤
第一步:监听工单领域事件
csharp
[ApiController]
[Route("api/tickets")]
public class TicketsController : ControllerBase
{
private readonly ITicketService _ticketService;
private readonly IEventBus _eventBus;
private readonly ILogger<TicketsController> _logger;
[HttpPost]
public async Task<IActionResult> CreateTicket([FromBody] CreateTicketRequest request)
{
var ticket = await _ticketService.CreateTicketAsync(request);
// 发布领域事件
await _eventBus.PublishAsync(new TicketCreatedEvent { Ticket = ticket });
return CreatedAtAction(nameof(GetTicket), new { id = ticket.Id }, ticket);
}
[HttpPut("{id}/urgent")]
public async Task<IActionResult> MarkAsUrgent(string id)
{
var ticket = await _ticketService.MarkAsUrgentAsync(id);
// 发布紧急标记事件
await _eventBus.PublishAsync(new TicketMarkedUrgentEvent { Ticket = ticket });
return Ok(ticket);
}
}
第二步:构建飞书任务
csharp
public class FeishuTaskBuilder
{
private readonly IUserService _userService;
private readonly IProjectService _projectService;
public async Task<CreateTaskRequest> BuildTaskAsync(Ticket ticket)
{
var task = new CreateTaskRequest
{
Summary = $"[工单#{ticket.Id}] {ticket.Title}",
Description = await BuildDescriptionAsync(ticket),
Extra = JsonSerializer.Serialize(new { TicketId = ticket.Id }),
ClientToken = Guid.NewGuid().ToString() // 幂等Token
};
// 设置截止时间
if (ticket.SlaDeadline.HasValue)
{
task.Due = new TaskTime
{
Timestamp = ((DateTimeOffset)ticket.SlaDeadline.Value).ToUnixTimeMilliseconds().ToString()
};
}
// 设置开始时间
task.Start = new TasksStartTime
{
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()
};
// 设置任务模式
task.Mode = ticket.Assignees?.Count > 1 ? 2 : 1; // 多人时用或签
// 设置里程碑标识
task.IsMilestone = ticket.Priority == TicketPriority.High;
// 设置提醒规则
task.Reminders = BuildReminders(ticket);
// 设置任务成员
task.Members = await BuildTaskMembersAsync(ticket);
// 设置所属清单
task.Tasklists = new[]
{
new TaskInTaskListInfo
{
TasklistGuid = await GetTaskListGuidAsync(ticket),
CustomFields = await BuildCustomFieldsAsync(ticket)
}
};
return task;
}
private async Task<string> BuildDescriptionAsync(Ticket ticket)
{
var description = new StringBuilder();
// 基本信息
description.AppendLine($"**客户:** {ticket.CustomerName}");
description.AppendLine($"**联系电话:** {ticket.CustomerPhone}");
description.AppendLine($"**项目:** {ticket.ProjectName}");
description.AppendLine($"**优先级:** {GetPriorityDisplay(ticket.Priority)}");
description.AppendLine($"**创建时间:** {ticket.CreatedAt:yyyy-MM-dd HH:mm:ss}");
if (ticket.SlaDeadline.HasValue)
{
description.AppendLine($"**SLA截止时间:** {ticket.SlaDeadline:yyyy-MM-dd HH:mm:ss}");
}
description.AppendLine();
// 问题描述
description.AppendLine("## 问题描述");
description.AppendLine(ticket.Description);
// 附件信息
if (!string.IsNullOrEmpty(ticket.Attachments))
{
description.AppendLine();
description.AppendLine("## 相关附件");
description.AppendLine(ticket.Attachments);
}
// 处理历史
if (ticket.History?.Any() == true)
{
description.AppendLine();
description.AppendLine("## 处理历史");
foreach (var history in ticket.History.Take(3))
{
description.AppendLine($"- {history.CreatedAt:MM-dd HH:mm} {history.Operator}:{history.Action}");
}
}
return description.ToString();
}
private TaskReminder[] BuildReminders(Ticket ticket)
{
var reminders = new List<TaskReminder>();
// 高优先级任务设置多个提醒
if (ticket.Priority == TicketPriority.High)
{
reminders.Add(new TaskReminder
{
MinutesBefore = 120, // 2小时前提醒
ReminderType = ReminderType.Push
});
reminders.Add(new TaskReminder
{
MinutesBefore = 60, // 1小时前提醒
ReminderType = ReminderType.Push
});
}
else if (ticket.Priority == TicketPriority.Medium)
{
reminders.Add(new TaskReminder
{
MinutesBefore = 240, // 4小时前提醒
ReminderType = ReminderType.Push
});
}
return reminders.ToArray();
}
private async Task<TaskMemberInfo[]> BuildTaskMembersAsync(Ticket ticket)
{
var members = new List<TaskMemberInfo>();
// 添加负责人
if (!string.IsNullOrEmpty(ticket.AssigneeId))
{
var assignee = await _userService.GetByIdAsync(ticket.AssigneeId);
if (assignee?.FeishuUserId != null)
{
members.Add(new TaskMemberInfo
{
UserId = assignee.FeishuUserId,
UserType = UserType.User,
TaskRole = TaskRole.Assignee
});
}
}
// 添加关注人
if (ticket.Watchers?.Any() == true)
{
foreach (var watcherId in ticket.Watchers)
{
var watcher = await _userService.GetByIdAsync(watcherId);
if (watcher?.FeishuUserId != null)
{
members.Add(new TaskMemberInfo
{
UserId = watcher.FeishuUserId,
UserType = UserType.User,
TaskRole = TaskRole.Follower
});
}
}
}
// 添加项目经理作为关注人
var project = await _projectService.GetByIdAsync(ticket.ProjectId);
if (project?.ManagerFeishuUserId != null)
{
members.Add(new TaskMemberInfo
{
UserId = project.ManagerFeishuUserId,
UserType = UserType.User,
TaskRole = TaskRole.Follower
});
}
return members.ToArray();
}
}
第三步:调用API并保存关联ID
csharp
public class TaskCreationService
{
private readonly IFeishuTaskService _feishuTaskService;
private readonly ITicketRepository _ticketRepository;
private readonly ILogger<TaskCreationService> _logger;
public async Task<string> CreateFeishuTaskAsync(Ticket ticket)
{
try
{
// 检查是否已存在任务
if (!string.IsNullOrEmpty(ticket.FeishuTaskId))
{
_logger.LogWarning("工单已存在飞书任务,工单ID: {TicketId}, 任务ID: {TaskGuid}",
ticket.Id, ticket.FeishuTaskId);
return ticket.FeishuTaskId;
}
// 创建飞书任务
var taskRequest = await _taskBuilder.BuildTaskAsync(ticket);
var taskGuid = await _feishuTaskService.CreateTaskAsync(taskRequest);
// 保存关联关系
ticket.FeishuTaskId = taskGuid;
ticket.FeishuTaskCreatedAt = DateTime.UtcNow;
await _ticketRepository.UpdateAsync(ticket);
_logger.LogInformation("成功为工单创建飞书任务,工单ID: {TicketId}, 任务ID: {TaskGuid}",
ticket.Id, taskGuid);
return taskGuid;
}
catch (Exception ex)
{
_logger.LogError(ex, "为工单创建飞书任务失败,工单ID: {TicketId}", ticket.Id);
throw;
}
}
}
第四步:发送内部通知
csharp
public class NotificationService
{
private readonly IFeishuMessageService _messageService;
private readonly IProjectService _projectService;
public async Task NotifyTaskCreatedAsync(Ticket ticket, string taskGuid)
{
try
{
var project = await _projectService.GetByIdAsync(ticket.ProjectId);
var message = new CardMessageRequest
{
ReceiveIdType = "chat_id",
ReceiveId = project.FeishuGroupId,
Card = new InteractiveCard
{
Header = new CardHeader
{
Title = "🎯 新工单转飞书任务",
Template = "blue"
},
Elements = new List<ICardElement>
{
new CardMarkdown
{
Content = $"**工单编号:** #{ticket.Id}\n" +
$"**客户:** {ticket.CustomerName}\n" +
$"**优先级:** {GetPriorityEmoji(ticket.Priority)} {ticket.Priority}\n" +
$"**负责人:** {ticket.AssigneeName}"
},
new CardButton
{
Text = new CardText
{
Content = "查看任务详情",
Tag = "plain_text"
},
Type = "template",
Url = $"https://.feishu.cn/messenger/collection/task/detail/{taskGuid}"
}
}
}
};
await _messageService.SendCardMessageAsync(message);
}
catch (Exception ex)
{
_logger.LogError(ex, "发送任务创建通知失败,工单ID: {TicketId}", ticket.Id);
}
}
private string GetPriorityEmoji(TicketPriority priority)
{
return priority switch
{
TicketPriority.High => "🔴",
TicketPriority.Medium => "🟡",
TicketPriority.Low => "🟢",
_ => "⚪"
};
}
}
注意事项
- 避免重复创建:使用幂等Token和数据库唯一性约束
- 字段默认值处理:合理设置任务的默认优先级和截止时间
- 异常处理:网络异常时的重试机制和本地缓存
- 性能优化:批量处理和异步操作
🔄 场景二:任务状态双向同步
状态同步流程图
graph TB subgraph "飞书 → 系统" A飞书任务状态变更 --> BWebhook事件 B --> C事件验证器 C --> D状态映射器 D --> E业务规则校验 E --> F更新工单状态 F --> G记录操作日志 G --> H发送通知 end subgraph "系统 → 飞书" I工单状态变更 --> J状态映射器 J --> KAPI调用 K --> L更新飞书任务 L --> M异常重试 M --> N成功确认 end subgraph "双向映射规则" Otodo --> PInProgress Qdone --> RResolved Sarchived --> TClosed P --> O R --> Q T --> S end
飞书 → 系统:通过Webhook接收任务更新
状态映射配置
csharp
public class TaskStatusMapper
{
private static readonly Dictionary<string, TicketStatus> StatusMapping = new()
{
{ "todo", TicketStatus.InProgress },
{ "done", TicketStatus.Resolved },
{ "archived", TicketStatus.Closed }
};
public TicketStatus MapTaskStatusToTicketStatus(string taskStatus)
{
return StatusMapping.TryGetValue(taskStatus, out var ticketStatus)
? ticketStatus
: TicketStatus.Open;
}
public string MapTicketStatusToTaskStatus(TicketStatus ticketStatus)
{
return ticketStatus switch
{
TicketStatus.Open => "todo",
TicketStatus.InProgress => "todo",
TicketStatus.Resolved => "done",
TicketStatus.Closed => "archived",
_ => "todo"
};
}
}
Webhook事件处理器
csharp
public class TaskWebhookHandler : IFeishuEventHandler
{
private readonly ITicketSyncService _syncService;
private readonly IOperationLogService _logService;
private readonly ILogger<TaskWebhookHandler> _logger;
public string SupportedEventType => "task.status_changed";
public async Task HandleAsync(EventData eventData, CancellationToken cancellationToken = default)
{
try
{
switch (eventData.EventType)
{
case "task.status_changed":
await HandleStatusChangedAsync(eventData);
break;
case "task.assignee_updated":
await HandleAssigneeUpdatedAsync(eventData);
break;
case "task.deleted":
await HandleTaskDeletedAsync(eventData);
break;
default:
_logger.LogDebug("未处理的任务事件类型: {EventType}", eventData.EventType);
break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "处理飞书任务Webhook事件失败");
throw;
}
}
private async Task HandleStatusChangedAsync(EventData eventData)
{
var taskEvent = JsonSerializer.Deserialize<TaskStatusChangedEvent>(eventData.Event.ToString());
// 校验业务规则
if (!await ValidateStatusTransitionAsync(taskEvent))
{
_logger.LogWarning("任务状态转换不符合业务规则,任务ID: {TaskGuid}", taskEvent.Task.Guid);
return;
}
// 更新工单状态
await _syncService.SyncTaskStatusToTicketAsync(taskEvent.Task.Guid, taskEvent.Task.Status);
// 记录操作日志
await _logService.LogAsync(new OperationLog
{
TicketId = ExtractTicketId(taskEvent.Task.Extra),
Action = "StatusChanged",
OldValue = taskEvent.OldStatus,
NewValue = taskEvent.Task.Status,
Operator = "FeishuSync",
Timestamp = DateTime.UtcNow,
Source = "FeishuTask"
});
}
private async Task<bool> ValidateStatusTransitionAsync(TaskStatusChangedEvent taskEvent)
{
// 业务规则:不允许从"完成"状态回退到其他状态
if (taskEvent.OldStatus == "done" && taskEvent.Task.Status != "done")
{
// 检查是否有特殊权限
var hasSpecialPermission = await HasSpecialPermissionAsync(taskEvent.Operator.UserId);
return hasSpecialPermission;
}
// 业务规则:只有负责人可以修改任务状态
var isAssignee = taskEvent.Task.Members?.Any(m =>
m.UserId == taskEvent.Operator.UserId && m.TaskRole == TaskRole.Assignee) ?? false;
return isAssignee;
}
}
系统 → 飞书:工单解决后自动关闭任务
csharp
public class TicketStatusHandler
{
private readonly IFeishuTaskService _feishuTaskService;
private readonly IEventBus _eventBus;
public async Task HandleTicketStatusChangedAsync(TicketStatusChangedEvent @event)
{
try
{
if (!string.IsNullOrEmpty(@event.Ticket.FeishuTaskId))
{
var updateRequest = new UpdateTaskRequest
{
Status = _statusMapper.MapTicketStatusToTaskStatus(@event.NewStatus),
CompletedAt = @event.NewStatus == TicketStatus.Resolved
? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()
: null
};
var success = await _feishuTaskService.UpdateTaskAsync(@event.Ticket.FeishuTaskId, updateRequest);
if (success)
{
_logger.LogInformation("成功同步工单状态到飞书任务,工单ID: {TicketId}, 任务ID: {TaskGuid}",
@event.Ticket.Id, @event.Ticket.FeishuTaskId);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "同步工单状态到飞书任务失败,工单ID: {TicketId}", @event.Ticket.Id);
// 发送到重试队列
await _retryQueue.EnqueueAsync(new RetryMessage
{
Data = @event,
Timestamp = DateTime.UtcNow,
RetryCount = 0
});
}
}
}
👥 场景三:智能跨部门任务分配
智能分配规则引擎
graph TD A工单输入 --> B规则引擎 B --> C工单类型规则 B --> DSLA规则 B --> E工作负载规则 B --> F可用性规则 C --> C1{技术问题?} C1 -->|是| C2技术支持团队 C1 -->|否| C3{功能需求?} C3 -->|是| C4产品团队 C1 -->|否| C5{Bug报告?} C5 -->|是| C6开发团队 C3 -->|否| C7客户服务团队 D --> D1SLA时间计算 D1 --> D2优先级调整 E --> E1当前工作量评估 E1 --> E2负载均衡分配 F --> F1在线状态检查 F1 --> F2技能匹配 C2 --> G候选人池 C4 --> G C6 --> G C7 --> G D2 --> G E2 --> G F2 --> G G --> H最终分配决策 H --> I任务分配执行
规则引擎设计
csharp
public class TaskAssignmentRuleEngine
{
private readonly IUserService _userService;
private readonly IProjectService _projectService;
private readonly ISLAService _slaService;
public async Task<AssignmentResult> AssignTaskAsync(Ticket ticket)
{
var rules = new List<IAssignmentRule>
{
new TicketTypeAssignmentRule(_userService),
new SLAAssignmentRule(_slaService),
new WorkloadAssignmentRule(_userService),
new AvailabilityAssignmentRule(_userService)
};
foreach (var rule in rules)
{
var result = await rule.EvaluateAsync(ticket);
if (result.IsMatch)
{
return result;
}
}
// 默认分配规则
return await GetDefaultAssignmentAsync(ticket);
}
}
public class TicketTypeAssignmentRule : IAssignmentRule
{
public async Task<AssignmentResult> EvaluateAsync(Ticket ticket)
{
return ticket.Type switch
{
TicketType.TechnicalIssue => await AssignToTechnicalSupportAsync(ticket),
TicketType.FeatureRequest => await AssignToProductAsync(ticket),
TicketType.BugReport => await AssignToDevelopmentAsync(ticket),
TicketType.CustomerComplaint => await AssignToCustomerServiceAsync(ticket),
_ => AssignmentResult.NoMatch()
};
}
private async Task<AssignmentResult> AssignToTechnicalSupportAsync(Ticket ticket)
{
var techSupportTeam = await _userService.GetUsersByRoleAsync("TechnicalSupport");
var availableMember = techSupportTeam
.Where(u => u.IsAvailable && u.CurrentWorkload < u.MaxWorkload)
.OrderBy(u => u.CurrentWorkload)
.FirstOrDefault();
return availableMember != null
? AssignmentResult.Success(availableMember.Id, "按工单类型分配到技术支持")
: AssignmentResult.NoMatch();
}
}
SLA截止时间与提醒规则
csharp
public class SLAService
{
public async Task<DateTime?> CalculateSLADeadlineAsync(Ticket ticket)
{
var slaConfig = await GetSLAConfigAsync(ticket.Priority, ticket.Type);
var businessHours = await GetBusinessHoursAsync(ticket.CustomerId);
var deadline = CalculateBusinessDeadline(DateTime.UtcNow, slaConfig.ResponseHours, businessHours);
return deadline;
}
public async Task SetupSLARemindersAsync(string ticketId, DateTime deadline)
{
var reminders = new List<SLAReminder>
{
new SLAReminder
{
TicketId = ticketId,
TriggerTime = deadline.AddHours(-2), // 提前2小时
Type = ReminderType.Warning,
Message = "工单即将超过SLA时间,请尽快处理"
},
new SLAReminder
{
TicketId = ticketId,
TriggerTime = deadline.AddMinutes(-30), // 提前30分钟
Type = ReminderType.Urgent,
Message = "工单SLA即将超时,紧急处理"
}
};
await SaveRemindersAsync(reminders);
}
}
子任务依赖支持
csharp
public class SubTaskManager
{
public async Task<string> CreateSubTaskAsync(string parentTaskGuid, SubTaskRequest request)
{
var subTaskRequest = new CreateSubTaskRequest
{
Summary = request.Title,
Description = request.Description,
Due = request.DueDate.HasValue
? new TaskTime { Timestamp = ((DateTimeOffset)request.DueDate.Value).ToUnixTimeMilliseconds().ToString() }
: null,
Members = request.AssigneeId != null
? new[] { new TaskMemberInfo { UserId = request.AssigneeId, TaskRole = TaskRole.Assignee } }
: null
};
var result = await _taskApi.CreateSubTaskAsync(parentTaskGuid, subTaskRequest);
return result?.Data?.SubTask?.Guid;
}
public async Task SetupTaskDependenciesAsync(string taskGuid, List<TaskDependency> dependencies)
{
foreach (var dependency in dependencies)
{
var addRequest = new AddTaskDependenciesRequest
{
Dependencies = new[]
{
new TaskDependencyInfo
{
TaskGuid = dependency.DependentTaskGuid,
DependencyType = dependency.Type
}
}
};
await _taskApi.AddTaskDependenciesByIdAsync(taskGuid, addRequest);
}
}
}