想象一下這樣的場景:客户焦急地等待問題解決,而你的團隊卻在一堆郵件、Excel表格和零散的IM消息中手忙腳亂。這是不是很多企業每天都在上演的真實寫照?
在數字化轉型的浪潮中,我們不僅要讓系統"能用",更要讓團隊"好用"。飛書就像是協作世界的"超級英雄",它能讓原本各自為戰的業務系統手拉手,讓信息像流水一樣順暢流動。
今天,就讓我們一起踏上一段奇妙的旅程——藉助Mud.Feishu這個強大的開源工具,為我們的.NET業務系統裝上"協作翅膀",實現從傳統的工單處理到現代化的全鏈路任務協同的華麗轉身。
為什麼我們的系統需要"協作升級"?
當孤島遇上協作:那些年我們一起踩過的坑
還記得那個尷尬的下午嗎?客户在電話那頭焦急地詢問:"我的問題解決得怎麼樣了?" 而你卻在三個不同的系統之間來回切換,試圖拼湊出完整的答案。
隨着企業越來越大,業務越來越複雜,我們的傳統系統就像一個個獨立的"小島",雖然每個小島上都有寶藏(數據),但它們之間卻沒有橋樑:
-
信息都在各自的"保險櫃"裏:客服用一套系統,技術用另一套,產品還有自己的系統。想要看全局?那可真是個挑戰!
-
溝通還在"石器時代":郵件一來一回可能要等幾小時,重要消息可能淹沒在收件箱裏,IM聊天記錄又容易被刷屏遺忘。
-
任務進展像"盲人摸象":誰在負責什麼?進行到哪一步了?這些問題往往需要開會問一圈才能搞清楚。
-
跨部門協作像"跨越大海":技術説這是產品問題,產品説這是客服問題,客户的問題在部門之間"漂流",最後不了了之。
飛書API給傳統系統裝上"智能大腦"
如果説傳統系統是"單機版",那麼飛書API就是讓它們連入"互聯網"的魔法棒。飛書不僅僅是又一個辦公軟件,它的任務管理API就像是協作世界的"通用語言":
-
開放的"樂高積木":豐富的API接口就像樂高積木,你可以隨心所欲地搭建適合自己的協作場景。
-
實時"心跳感應":基於WebSocket的推送機制讓任務狀態變化像心跳一樣實時傳遞,告別"刷新查看"的等待時代。
-
移動"隨身助手":無論你在咖啡廳還是在路上,手機上的任務提醒和更新都不會錯過重要事項。
-
企業"安全衞士":完善的權限管理和數據加密,讓敏感信息在開放協作的同時依然安全可靠。
為什麼.NET是最佳選擇
在眾多技術棧中,.NET就像是那個穩重又有內涵的"理想伴侶",特別適合承擔企業級集成的重任:
-
穩如泰山的"老司機":.NET平台經過多年曆練,性能穩定可靠,就像一個經驗豐富的老司機,能在複雜的業務環境中穩健前行。
-
微軟"靠山"很給力:有微軟這樣的技術巨頭長期支持,不用擔心技術路線突然變卦,開發路上更有安全感。
-
與時俱進"新青年":從.NET 6.0開始,整個平台煥然一新,異步編程和併發處理能力讓複雜場景的處理變得遊刃有餘。
-
工具鏈"豪華套餐":Visual Studio就像是一把"瑞士軍刀",配合NuGet這個"百寶箱",開發效率自然節節攀升。
一個真實的應用場景:客服小王的一天
小王的"日常折磨":傳統工單系統的困境
讓我們跟隨客服小王,看看她是如何在傳統工單系統中"掙扎"的:
早上9點,小王剛坐下就收到了客户的緊急投訴。她迅速在系統中創建了工單,然後發送郵件給技術部門的老李。兩個小時過去了,老李才回復説這個問題需要產品部門的小張確認...
這聽起來是不是很熟悉?傳統工單系統就像是一個"信息傳遞遊戲",每個人都在等待,客户卻在焦慮。讓我們看看小王的工作流程圖:
看到那些紅色的步驟了嗎?每一步都是小王工作中的"痛點"。
三個讓小王"頭疼"的大難題
🌫️ 難題一:任務消失在"信息黑洞"裏
小王每天都在玩"捉迷藏"遊戲:
- 郵件森林:重要的任務分配郵件可能被淹沒在收件箱的數百封郵件中
- IM聊天刷屏:關鍵信息在羣聊裏被各種表情包和閒聊淹沒
- 管理層的"望遠鏡":想要了解整體進度?那就得一個個去問,就像用望遠鏡看星星
- 客户的"猜謎遊戲":客户打電話問進度,小王只能尷尬地説"我幫您問問"
🧩 難題二:跨部門協作像"拼圖遊戲"
工單在不同部門之間傳遞,就像是在玩拼圖,但總少了幾塊:
- 系統"方言"不同:客服系統、技術系統、產品系統各自説各自的"語言"
- 信息"接力賽"中的掉棒:工單在傳遞過程中,重要的背景信息"不翼而飛"
- 責任"皮球遊戲":這到底是技術問題還是產品問題?大家開始踢皮球
- 知識"孤島":解決方案和個人經驗都留在了各自的大腦裏,無法形成團隊財富
⏰ 難題三:優先級管理像"無頭蒼蠅"
傳統系統就像是沒有導航的司機:
- SLA"定時炸彈":重要的工單快要到期了,但系統不會自動提醒
- 進度"盲人摸象":哪個工單會超時?只能憑感覺猜測
- 管理層"霧裏看花":想要全局視圖?抱歉,系統只支持單點查看
- 資源調配"拍腦袋":誰該處理什麼任務?更多靠經驗而非數據
小王的"逆襲":當工單系統遇上飛書
現在,讓我們看看當飛書任務管理介入後,小王的工作發生了怎樣的神奇變化:
看到那些綠色步驟了嗎?每一個都代表着小王工作效率的飛躍提升!
小王的"幸福感提升清單":
-
🚀 從手工到自動化:原來要人工操作的步驟,現在系統自動搞定,小王終於有時間喝杯咖啡了
-
🔍 從黑盒到透明:任務進展一目瞭然,管理層再也不用追着她問進度,客户也能自己查看狀態
-
🌉 從孤島到通途:統一的協作平台讓跨部門合作變得像"左鄰右舍"一樣自然
-
😊 從被動到主動:客户收到實時更新通知,滿意度直線上升,小王的KPI也跟着水漲船高
搭積木的藝術:構建我們的協作橋樑
先看看我們的"家底":現有系統是什麼樣的
在企業級應用的世界裏,.NET系統就像是精心設計的"建築",主要有兩種常見的"建築風格":
經典的處理流程
微服務的現代佈局
使用飛書後優化的系統流程
現在到了最激動人心的部分!我們要在現有系統和飛書之間搭建一座"智能橋樑"。這個橋不是用磚塊,而是用代碼和智慧搭建的:
適配層組件交互流程
適配層(Mud.Feishu封裝)
Mud.Feishu提供了完整的飛書API封裝:
- HTTP API客户端:基於
IFeishuTenantV2Task接口,提供完整的任務管理能力 - WebSocket客户端:實時事件訂閲,支持自動重連和心跳檢測
- Webhook處理器:HTTP回調事件處理,支持事件路由和中間件模式
Mud.Feishu源碼倉庫:Gitee,Github
// 核心接口定義示例
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);
}
同步服務(雙向/事件驅動)
同步服務負責業務系統與飛書之間的數據同步:
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實現關鍵狀態的實時同步:
// 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;
}
}
}
批量補償(定時對賬)
定時執行批量對賬,確保數據一致性:
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的令牌管理機制:
// 配置示例
builder.Services.AddFeishuServices()
.ConfigureFrom(builder.Configuration)
.AddTaskApi()
.AddTokenManagers()
.Build();
API密鑰管理
- 應用密鑰安全存儲(環境變量/密鑰管理服務)
- 訪問令牌自動刷新和緩存
- 權限最小化原則,按需申請API權限
IP白名單
在飛書開放平台配置服務端IP白名單,增強安全性。
動手時間:一步步打造你的飛書集成模塊
1. 準備工作:讓一切就緒
第一步:和飛書"握手"——創建應用
讓我們先到飛書開放平台這個"遊樂場"註冊我們的"入場券":
-
打開大門:訪問 https://open.feishu.cn/,就像走進一個充滿可能性的新世界
-
領取身份卡:創建企業自建應用
- 給它起個響亮的名字:"客户支持工單集成助手"
- 寫個自我介紹:"我是來幫大家告別工單混亂的小能手"
-
申請通行證:配置應用權限
- 任務管理全權限:讀取、創建、更新、刪除(就像給了一把萬能鑰匙)
- 任務清單查看權:知道任務放在哪個"房間"
- 用户基本信息權:認識團隊裏的每個"小夥伴"
- 事件訂閲權:能夠監聽任務世界的"風吹草動"
-
設置專屬熱線:配置事件訂閲
- 提供你的"電話號碼"(請求網址):
https://your-domain.com/api/feishu/webhook - 準備"暗號"(驗證Token):只有你懂的驗證字符串
- 配置"保險箱"(數據加密密鑰):用AES-256保護敏感信息
- 提供你的"電話號碼"(請求網址):
第二步:搭建我們的"開發工作室"
現在讓我們創建一個乾淨的.NET項目,並邀請我們的"得力助手"們加入:
<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>
基礎配置文件
{
"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這些煩人的事情。我們只需要告訴它"密碼"在哪裏就行:
// 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
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處理器:飛書事件"快遞員"
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. 業務領域適配
對象映射設計:工單 ↔ 飛書任務字段對照表
使用AutoMapper進行對象映射配置:
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
};
}
}
同步策略實現
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);
}
}
}
容錯設計:異常分類、重試策略、死信隊列
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流程,我們只需要配置:
// appsettings.json中的配置
{
"Feishu": {
"AppId": "cli_xxxxxxxxxxxxxxxx",
"AppSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
// 服務註冊
builder.Services.AddFeishuApiService(builder.Configuration);
創建任務並關聯工單的完整示例
[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處理器與業務邏輯解耦設計
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);
}
}
}
應用場景落地詳解
🔧 場景一:讓工單"活"起來——自動飛書任務生成
工單到任務的"變身"過程
什麼時候該"變身"?——觸發時機揭秘
就像超級英雄有自己的"變身"條件,我們的工單也需要在合適的時機才生成飛書任務:
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);
}
}
}
實現步驟
第一步:監聽工單領域事件
[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);
}
}
第二步:構建飛書任務
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
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;
}
}
}
第四步:發送內部通知
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和數據庫唯一性約束
- 字段默認值處理:合理設置任務的默認優先級和截止時間
- 異常處理:網絡異常時的重試機制和本地緩存
- 性能優化:批量處理和異步操作
🔄 場景二:任務狀態雙向同步
狀態同步流程圖
飛書 → 系統:通過Webhook接收任務更新
狀態映射配置
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事件處理器
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;
}
}
系統 → 飛書:工單解決後自動關閉任務
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
});
}
}
}
👥 場景三:智能跨部門任務分配
智能分配規則引擎
規則引擎設計
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截止時間與提醒規則
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);
}
}
子任務依賴支持
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);
}
}
}
項目倉庫
Mud.Feishu Gitee源碼倉庫:Gitee
Mud.Feishu Github源碼倉庫:Github