在.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 Layel 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. 打开大门 :访问 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.1.0" />
    <PackageReference Include="Mud.Feishu.WebSocket" Version="1.1.0" />
    <PackageReference Include="Mud.Feishu.Webhook" Version="1.1.0" />
    <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客户端:我们的"翻译官"

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);
        }
    }
}
相关推荐
考虑考虑7 小时前
JDK26中的Map.ofLazy
java·后端·java ee
云浪9 小时前
Go 并发实战:从零实现一个高并发安全的任务调度器
javascript·后端·go
苏三的开发日记9 小时前
Docker 启动顺序优化(CentOS / Ubuntu)
后端
高频因子挖掘机11 小时前
同一交易日出现重复 K 线:去重前先确认时间、来源和粒度
后端·github·api
feely020811 小时前
《我写了 31 个静态扫描插件,踩了 7 个「假绿」》
后端
橙子家12 小时前
参数校验类异常(Argument Exception)【C# Exception 详解系列一】
后端
掘金挖土12 小时前
前端手摸手跑路之 AI 应用开发(九)
前端·后端
全栈弄潮儿13 小时前
Python实战第8期:函数
后端·python·编程语言
码事漫谈13 小时前
你让 AI “尽力做”,它反而可能做不好
后端