想象一下这样的场景:客户焦急地等待问题解决,而你的团队却在一堆邮件、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 --> D[IM沟通协调] D --> E[Excel跟踪进度] 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" A[ASP.NET Core Web API] B[前端应用 React/Vue] end subgraph "业务逻辑层 Business Logic Layer" C[工单管理服务] D[客户管理服务] E[通知服务] end subgraph "数据访问层 Data Access Layer" F[Entity Framework Core] G[SQL Server数据库] end A --> C A --> D B --> A C --> F D --> F E --> C F --> G
微服务的现代布局
graph TB subgraph "API网关" A[Ocelot/Gateway] end subgraph "微服务集群" B[工单服务] C[用户服务] D[通知服务] E[订单服务] end subgraph "基础设施" F[Redis缓存] G[RabbitMQ消息队列] H[SQL 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 "飞书集成适配层" D[Mud.Feishu HTTP API客户端] E[Mud.Feishu WebSocket客户端] F[Mud.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[飞书任务状态变更] --> B[WebSocket/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 --> C[WebSocket服务] A --> D[Webhook服务] A --> E[自定义业务服务] B --> F[Token管理器] B --> G[任务API客户端] C --> H[WebSocket管理器] C --> I[事件处理器1] C --> J[事件处理器2] D --> K[Webhook路由] 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 "工单系统" A[Ticket] --> B[Id] A --> C[Title] A --> D[Description] A --> E[Priority] A --> F[Assignee] A --> G[DueDate] end subgraph "映射转换" H[AutoMapper] I[业务规则引擎] J[数据转换器] end subgraph "飞书任务" K[CreateTaskRequest] --> L[Summary] K --> M[Description] K --> N[IsMilestone] K --> O[Members] K --> P[Due] K --> Q[Tasklists] 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[飞书任务状态变更] --> B[Webhook事件] B --> C[事件验证器] C --> D[状态映射器] D --> E[业务规则校验] E --> F[更新工单状态] F --> G[记录操作日志] G --> H[发送通知] end subgraph "系统 → 飞书" I[工单状态变更] --> J[状态映射器] J --> K[API调用] K --> L[更新飞书任务] L --> M[异常重试] M --> N[成功确认] end subgraph "双向映射规则" O[todo] --> P[InProgress] Q[done] --> R[Resolved] S[archived] --> T[Closed] 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 --> D[SLA规则] B --> E[工作负载规则] B --> F[可用性规则] C --> C1{技术问题?} C1 -->|是| C2[技术支持团队] C1 -->|否| C3{功能需求?} C3 -->|是| C4[产品团队] C1 -->|否| C5{Bug报告?} C5 -->|是| C6[开发团队] C3 -->|否| C7[客户服务团队] D --> D1[SLA时间计算] 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);
}
}
}