如何在.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

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. 准备工作:让一切就绪

第一步:和飞书"握手"------创建应用

让我们先到飞书开放平台这个"游乐场"注册我们的"入场券":

  1. 打开大门 :访问 https://open.feishu.cn/,就像走进一个充满可能性的新世界

  2. 领取身份卡:创建企业自建应用

    • 给它起个响亮的名字:"客户支持工单集成助手"
    • 写个自我介绍:"我是来帮大家告别工单混乱的小能手"
  3. 申请通行证:配置应用权限

    • 任务管理全权限:读取、创建、更新、删除(就像给了一把万能钥匙)
    • 任务清单查看权:知道任务放在哪个"房间"
    • 用户基本信息权:认识团队里的每个"小伙伴"
    • 事件订阅权:能够监听任务世界的"风吹草动"
  4. 设置专属热线:配置事件订阅

    • 提供你的"电话号码"(请求网址):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 => "🟢",
            _ => "⚪"
        };
    }
}

注意事项

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

状态映射配置

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

项目仓库

Mud.Feishu Gitee源码仓库Gitee
Mud.Feishu Github源码仓库Github

相关推荐
bugcome_com2 小时前
深入理解 C# 特性(Attribute):概念、实现与实战
c#·.net
WebRuntime3 小时前
所有64位WinForm应用都是Chromium浏览器(2)
javascript·c#·.net·web
Sunsets_Red4 小时前
待修改莫队与普通莫队优化
java·c++·python·学习·算法·数学建模·c#
时光追逐者5 小时前
一款基于 .NET 9 构建的企业级 Web RBAC 快速开发框架
前端·c#·.net·.net core
想你依然心痛5 小时前
【TextIn大模型加速器+火山引擎】打造智能文档处理流水线:从跨国药企手册到金融单据核验的全链路实战
金融·c#·火山引擎
kingwebo'sZone5 小时前
win11智能应用控制已阻止此应用
c#
baivfhpwxf20236 小时前
c# 删除文件夹里的所有文件
c#
niucloud-admin7 小时前
二次开发指导
二次开发
easyboot7 小时前
python获取C#WEBAPI的数据
开发语言·python·c#