在.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);
        }
    }
}
相关推荐
知守观1 小时前
ThreadLocal + 异步线程导致用户数据串号:一次跨请求数据泄漏的完整复盘
后端
前端冒菜师1 小时前
我为什么做了 Iris,又为什么停下了它
后端·ai编程
子一!!1 小时前
集成Spring家族的Spring论坛实战==一阶段
java·后端·spring
Ticnix1 小时前
你的 Agent 聊到第 20 轮就"失忆"?你管理的是历史,高手管理的是上下文
后端·python·agent
不合格的程序员1 小时前
Agent Memory架构设计与实现
后端·ai编程
用户93816912553601 小时前
分页查询 Out of sort memory 问题
后端
xyLJ1 小时前
为什么 Spring Boot 自动配置了 Redis,还要自己写 RedisTemplate?
后端
xixiaoyunya2 小时前
Spring Boot 3 统一响应与全局异常处理实战
java·spring boot·后端
JavaGuide2 小时前
DeepSeek Harness 官方桌面端终于有了!
前端·后端