博客 / 詳情

返回

如何在.NET系統中快速集成飛書任務分配能力

想象一下這樣的場景:客户焦急地等待問題解決,而你的團隊卻在一堆郵件、Excel表格和零散的IM消息中手忙腳亂。這是不是很多企業每天都在上演的真實寫照?

在數字化轉型的浪潮中,我們不僅要讓系統"能用",更要讓團隊"好用"。飛書就像是協作世界的"超級英雄",它能讓原本各自為戰的業務系統手拉手,讓信息像流水一樣順暢流動。

今天,就讓我們一起踏上一段奇妙的旅程——藉助Mud.Feishu這個強大的開源工具,為我們的.NET業務系統裝上"協作翅膀",實現從傳統的工單處理到現代化的全鏈路任務協同的華麗轉身。

為什麼我們的系統需要"協作升級"?

當孤島遇上協作:那些年我們一起踩過的坑

還記得那個尷尬的下午嗎?客户在電話那頭焦急地詢問:"我的問題解決得怎麼樣了?" 而你卻在三個不同的系統之間來回切換,試圖拼湊出完整的答案。

隨着企業越來越大,業務越來越複雜,我們的傳統系統就像一個個獨立的"小島",雖然每個小島上都有寶藏(數據),但它們之間卻沒有橋樑:

  1. 信息都在各自的"保險櫃"裏:客服用一套系統,技術用另一套,產品還有自己的系統。想要看全局?那可真是個挑戰!

  2. 溝通還在"石器時代":郵件一來一回可能要等幾小時,重要消息可能淹沒在收件箱裏,IM聊天記錄又容易被刷屏遺忘。

  3. 任務進展像"盲人摸象":誰在負責什麼?進行到哪一步了?這些問題往往需要開會問一圈才能搞清楚。

  4. 跨部門協作像"跨越大海":技術説這是產品問題,產品説這是客服問題,客户的問題在部門之間"漂流",最後不了了之。

飛書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回調事件處理,支持事件路由和中間件模式

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實現關鍵狀態的實時同步:

graph LR subgraph "實時事件流" A[飛書任務狀態變更] --> B[WebSocket/Webhook] B --> C[事件路由器] C --> D[狀態同步服務] D --> E[業務系統] end subgraph "批量補償流" F[定時任務觸發] --> G[數據對賬服務] G --> H[差異檢測] H --> I[數據修復] I --> E end
// 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. 準備工作:讓一切就緒

第一步:和飛書"握手"——創建應用

讓我們先到飛書開放平台這個"遊樂場"註冊我們的"入場券":

  1. 打開大門:訪問 https://open.feishu.cn/,就像走進一個充滿可能性的新世界

  2. 領取身份卡:創建企業自建應用

    • 給它起個響亮的名字:"客户支持工單集成助手"
    • 寫個自我介紹:"我是來幫大家告別工單混亂的小能手"
  3. 申請通行證:配置應用權限

    • 任務管理全權限:讀取、創建、更新、刪除(就像給了一把萬能鑰匙)
    • 任務清單查看權:知道任務放在哪個"房間"
    • 用户基本信息權:認識團隊裏的每個"小夥伴"
    • 事件訂閲權:能夠監聽任務世界的"風吹草動"
  4. 設置專屬熱線:配置事件訂閲

    • 提供你的"電話號碼"(請求網址):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這些煩人的事情。我們只需要告訴它"密碼"在哪裏就行:

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
// 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. 業務領域適配

對象映射設計:工單 ↔ 飛書任務字段對照表

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進行對象映射配置:

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);
        }
    }
}

應用場景落地詳解

🔧 場景一:讓工單"活"起來——自動飛書任務生成

工單到任務的"變身"過程

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

什麼時候該"變身"?——觸發時機揭秘

就像超級英雄有自己的"變身"條件,我們的工單也需要在合適的時機才生成飛書任務:

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 => "🟢",
            _ => "⚪"
        };
    }
}

注意事項

  1. 避免重複創建:使用冪等Token和數據庫唯一性約束
  2. 字段默認值處理:合理設置任務的默認優先級和截止時間
  3. 異常處理:網絡異常時的重試機制和本地緩存
  4. 性能優化:批量處理和異步操作

🔄 場景二:任務狀態雙向同步

狀態同步流程圖

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接收任務更新

狀態映射配置

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
            });
        }
    }
}

👥 場景三:智能跨部門任務分配

智能分配規則引擎

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[任務分配執行]

規則引擎設計

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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.