.NET 传统信息系统无缝集成飞书审批流

周末深夜,你收到紧急审批通知------却发现只能在 PC 端处理,只能摸黑起床开电脑......

这样的场景,你是否也经历过?

传统 .NET 系统与现代移动协同之间的鸿沟,正在悄悄吞噬着企业的效率。审批卡在桌面端、通知滞后、数据孤岛------这些问题让工作体验大打折扣。

推倒重来?成本太高,风险太大。

本文将带你走一条渐进式改造之路:保持 .NET 系统作为业务核心,将飞书审批作为移动门户,通过 API 实现无缝协同。从原理、设计到编码,完整呈现如何让传统系统焕发新生,实现移动化、实时化的现代化升级。

无论你是开发者、架构师还是技术管理者,都能收获一套可落地、可扩展的集成方案和直接复用的代码实践。

当传统业务遇上现代协同,为何必须"破壁"?

我们正在解决什么?

传统 .NET 系统的局限

许多企业拥有多年累积的 .NET 业务系统,这些系统在企业运营中扮演着核心角色。然而,随着移动办公和现代协同工具的普及,这些传统系统正面临着严峻的挑战:

痛点 具体表现 业务影响
审批流程封闭 审批只能在桌面端完成,无法随时随地处理 移动办公受阻,响应迟缓
通知方式滞后 依赖邮件或站内消息推送 审批人及时性差,流程延误
数据孤岛严重 审批数据与业务数据分离 无法形成完整的业务闭环
用户体验陈旧 界面风格陈旧,交互体验差 用户满意度低,使用意愿下降

飞书审批的赋能价值

飞书审批作为企业级的审批协作平台,为我们提供了一个理想的"流程协作中心":
graph LR A[飞书审批平台] --> B[移动优先] A --> C[即时强通知] A --> D[流程可视化] A --> E[完善审计日志] B --> B1[随时随地处理审批] C --> C1[App推送/短信提醒] D --> D1[拖拽式流程配置] E --> E1[完整操作痕迹追溯]

我们的核心目标

通过本文的实践,我们将建立 ".NET 系统为业务核心,飞书审批为流程门户" 的现代化混合架构:

架构愿景:将飞书审批作为统一的移动审批门户,保持 .NET 系统作为业务逻辑和数据存储的核心,通过 API 实时同步,形成优势互补的协同体系。

你将收获什么?

  • 一套端到端的集成方法论,覆盖从原理、设计到部署的全流程
  • 清晰的 .NET 侧架构蓝图,包含关键的技术选型与设计决策
  • 可直接复用的 C# 核心代码与实践中总结的"避坑指南"
  • 一个完整的"请假审批"实战案例,助你从零到一完成验证

飞书审批开放平台如何与我们"对话"?

双向集成的关键流程

飞书审批与 .NET 系统的集成是一个双向数据流的过程:
sequenceDiagram participant User as 用户 participant NET as .NET系统 participant API as 飞书API participant FS as 飞书App Note over User,FS: 流程输出:发起审批 User->>NET: 1. 提交请假申请 NET->>NET: 2. 保存业务数据(状态:审批中) NET->>API: 3. 调用 CreateInstanceAsync API-->>NET: 4. 返回 instance_code NET->>NET: 5. 关联业务ID与instance_code Note over User,FS: 流程输入:回调通知 User->>FS: 6. 在飞书App中审批 FS->>API: 7. 审批完成 API->>NET: 8. Webhook回调事件 NET->>NET: 9. 根据instance_code更新业务状态

流程输出(发起阶段)

当用户在 .NET 系统发起审批时,系统会:

  1. 保存业务数据,状态标记为"审批中"
  2. 调用飞书 API CreateInstanceAsync 创建审批实例
  3. 接收返回的 instance_code,持久化到关联表

流程输入(回调阶段)

当审批人在飞书 App 完成审批后:

  1. 飞书服务器主动回调 .NET 系统的 Webhook 接口
  2. .NET 系统解析事件,提取 instance_codestatus
  3. 根据关联表查询对应的业务记录
  4. 更新业务状态,完成闭环

必须理解的三个核心概念

审批定义(approval_code)

审批定义是审批流程的"蓝图",在飞书管理后台配置:

csharp 复制代码
// 示例:请假审批的审批定义
var approvalCode = "7C468A54-8745-2245-9675-08B7C63E7A85";

定义包含

  • 表单结构(请假类型、开始时间、结束时间、请假事由等)
  • 审批流程(直属主管审批 → 人事审批)
  • 权限设置(谁可以发起、谁可以审批)

审批实例(instance_code)

审批实例是依据审批定义发起的一次具体审批任务:

csharp 复制代码
// 创建审批实例时返回
public record CreateInstancesResult
{
    /// <summary>
    /// 审批实例 Code
    /// </summary>
    [JsonPropertyName("instance_code")]
    public string InstanceCode { get; set; } = string.Empty;
}

关键属性

  • 唯一标识一次审批流程
  • 包含该次审批的所有表单数据
  • 拥有独立的状态(审批中、通过、拒绝、撤回等)

身份映射(免登)

实现 .NET 系统用户与飞书用户的关联:
graph LR A[.NET系统用户<br/>UserId: 1001] -->|映射关系| B[飞书用户<br/>OpenId: ou_3cda9c...] B -->|通过飞书App审批| C[审批完成] C -->|回调instance_code| A

实现方式

  1. 在 .NET 系统的用户表中添加 FeishuOpenId 字段
  2. 用户首次登录时进行飞书免登录认证,获取并存储 open_id
  3. 发起审批时,使用 open_id 指定审批发起人

构建稳健、可扩展的 .NET 侧集成层

技术栈推荐

层次 技术选型 说明
应用框架 .NET 6/8/10 长期支持版本,性能优异
飞书 SDK Mud.Feishu 高度封装的飞书 API 客户端
Webhook 处理 Mud.Feishu.Webhook 飞书事件回调处理组件
认证授权 ASP.NET Core Identity / JWT 内部系统身份管理
异步解耦 RabbitMQ / Hangfire 回调消息队列处理,提升可靠性
数据存储 SQL Server / PostgreSQL 业务数据 + 审批关联表

分层架构图

graph TB subgraph "表示层 (UI)" A[Web 前端 / 移动端] end subgraph "应用层 (API)" B[LeaveController] C[FeishuWebhookController] end subgraph "领域层 (业务逻辑)" D[ILeaveService] E[ApprovalIntegrationService] F[IApprovalService] end subgraph "基础设施层" G[Mud.Feishu HTTP客户端] H[Mud.Feishu.Webhook处理器] I[数据仓储<br/>EF Core] end A --> B A --> C B --> D C --> E D --> F E --> F F --> G F --> H F --> I

关键设计:领域层抽象

在领域层引入 IApprovalService 接口,将飞书集成细节与核心业务逻辑解耦:

csharp 复制代码
/// <summary>
/// 审批服务抽象接口 - 解耦飞书实现细节
/// </summary>
public interface IApprovalService
{
    /// <summary>
    /// 发起审批
    /// </summary>
    Task<string> CreateApprovalAsync(ApprovalRequest request);

    /// <summary>
    /// 处理审批结果回调
    /// </summary>
    Task HandleApprovalCallbackAsync(ApprovalCallbackEvent callbackEvent);
}

/// <summary>
/// 飞书审批服务实现
/// </summary>
public class FeishuApprovalService : IApprovalService
{
    private readonly IFeishuTenantV4Approval _approvalApi;
    private readonly IApprovalRecordRepository _repository;

    public FeishuApprovalService(
        IFeishuTenantV4Approval approvalApi,
        IApprovalRecordRepository repository)
    {
        _approvalApi = approvalApi;
        _repository = repository;
    }

    public async Task<string> CreateApprovalAsync(ApprovalRequest request)
    {
        // 调用飞书 API
        var result = await _approvalApi.CreateInstanceAsync(...);
        return result.Data?.InstanceCode ?? string.Empty;
    }
}

优势

  • 业务逻辑不依赖具体飞书实现
  • 便于单元测试(可 Mock 接口)
  • 未来可轻松切换到其他审批平台

实战:手把手完成"请假审批"集成

第一步:飞书平台侧配置(审批流出口)

创建企业自建应用

登录飞书开放平台(https://open.feishu.cn),进入应用管理:
graph LR A[创建自建应用] --> B[获取App ID] A --> C[获取App Secret] B --> D[配置到.NET系统] C --> D

关键配置

  • 记录 App IDApp Secret
  • 配置应用权限:审批相关权限(approval:approval:read, approval:instance:read, approval:instance:create

配置审批定义

在飞书管理后台创建"请假审批"模板:
graph LR A[审批定义配置] --> B[表单设置] A --> C[流程设置] A --> D[权限设置] B --> B1[请假类型<br/>开始时间<br/>结束时间<br/>请假天数<br/>请假事由] C --> C1[直属主管审批<br/>→ 人事审批] D --> D1[全员可发起]

记录关键信息

  • approval_code:审批定义的唯一标识
  • 表单控件的 id:用于程序填充表单数据

配置事件订阅

在飞书开放平台配置 Webhook:

配置项
请求网址 https://your-domain.com/api/feishu/webhook
验证 Token your_verification_token(自定义)
加密 Key your_encrypt_key(自定义)
订阅事件 approval_instance(审批实例状态变更)

第二步:.NET 侧基础搭建(集成基石)

封装飞书 API 客户端

基于 MudFeishu SDK 封装审批服务:

csharp 复制代码
/// <summary>
/// 飞书审批服务封装
/// </summary>
public class FeishuApprovalClient
{
    private readonly IFeishuTenantV4Approval _approvalApi;
    private readonly ILogger<FeishuApprovalClient> _logger;

    public FeishuApprovalClient(
        IFeishuTenantV4Approval approvalApi,
        ILogger<FeishuApprovalClient> logger)
    {
        _approvalApi = approvalApi;
        _logger = logger;
    }

    /// <summary>
    /// 创建审批实例
    /// </summary>
    public async Task<string> CreateInstanceAsync(CreateInstanceRequest request)
    {
        var result = await _approvalApi.CreateInstanceAsync(request);

        if (result == null || result.Code != 0)
        {
            _logger.LogError("创建审批实例失败: {Msg}", result?.Msg);
            throw new InvalidOperationException($"创建审批实例失败: {result?.Msg}");
        }

        _logger.LogInformation("创建审批实例成功: {InstanceCode}", result.Data?.InstanceCode);
        return result.Data?.InstanceCode ?? string.Empty;
    }

    /// <summary>
    /// 获取审批实例详情
    /// </summary>
    public async Task<GetApprovalInstanceResult?> GetInstanceAsync(string instanceCode)
    {
        var result = await _approvalApi.GetInstanceByIdAsync(instanceCode);

        if (result == null || result.Code != 0)
        {
            _logger.LogError("获取审批实例失败: {Msg}", result?.Msg);
            return null;
        }

        return result.Data;
    }
}

设计数据关联表

在业务数据库中添加审批关联表:

sql 复制代码
-- 审批关联表
CREATE TABLE ApprovalRecords (
    Id BIGINT PRIMARY KEY IDENTITY(1,1),
    BusinessType NVARCHAR(50) NOT NULL,          -- 业务类型:LeaveRequest, PurchaseRequest...
    BusinessId BIGINT NOT NULL,                   -- 业务ID
    InstanceCode NVARCHAR(64) NOT NULL,           -- 飞书审批实例Code
    ApprovalCode NVARCHAR(64) NOT NULL,            -- 审批定义Code
    Status NVARCHAR(20) NOT NULL,                  -- 状态:PENDING, APPROVED, REJECTED...
    CallbackData NVARCHAR(MAX),                   -- 回调数据(JSON)
    CreatedTime DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    UpdatedTime DATETIME2 NOT NULL DEFAULT GETUTCDATE(),

    CONSTRAINT UK_ApprovalRecords_Business UNIQUE(BusinessType, BusinessId)
);

CREATE INDEX IX_ApprovalRecords_InstanceCode ON ApprovalRecords(InstanceCode);
CREATE INDEX IX_ApprovalRecords_Status ON ApprovalRecords(Status);

对应的实体类:

csharp 复制代码
/// <summary>
/// 审批关联记录
/// </summary>
public class ApprovalRecord
{
    public long Id { get; set; }
    public string BusinessType { get; set; } = string.Empty;  // "LeaveRequest"
    public long BusinessId { get; set; }                       // 请假申请ID
    public string InstanceCode { get; set; } = string.Empty;   // 飞书实例Code
    public string ApprovalCode { get; set; } = string.Empty;    // 审批定义Code
    public string Status { get; set; } = string.Empty;         // PENDING/APPROVED/REJECTED
    public string? CallbackData { get; set; }                  // JSON格式
    public DateTime CreatedTime { get; set; }
    public DateTime UpdatedTime { get; set; }
}

第三步:核心业务流程编码(双向联通)

场景:用户提交请假单,发起审批

流程图
sequenceDiagram participant User as 用户 participant Controller as LeaveController participant Service as LeaveService participant DB as 数据库 participant FeishuAPI as 飞书API User->>Controller: 提交请假申请 Controller->>Service: SubmitLeaveRequest(request) Service->>DB: 保存请假记录(状态:审批中) Service->>Service: 构造表单数据 Service->>FeishuAPI: CreateInstanceAsync(approvalCode, form) FeishuAPI-->>Service: instance_code Service->>DB: 保存ApprovalRecord关联 Service-->>Controller: 提交成功 Controller-->>User: 等待审批

代码实现

csharp 复制代码
/// <summary>
/// 请假服务
/// </summary>
public class LeaveService
{
    private readonly ILeaveRequestRepository _leaveRepo;
    private readonly IApprovalRecordRepository _approvalRepo;
    private readonly FeishuApprovalClient _feishuClient;
    private readonly ILogger<LeaveService> _logger;

    public LeaveService(
        ILeaveRequestRepository leaveRepo,
        IApprovalRecordRepository approvalRepo,
        FeishuApprovalClient feishuClient,
        ILogger<LeaveService> logger)
    {
        _leaveRepo = leaveRepo;
        _approvalRepo = approvalRepo;
        _feishuClient = feishuClient;
        _logger = logger;
    }

    /// <summary>
    /// 提交请假申请并发起审批
    /// </summary>
    public async Task<long> SubmitLeaveRequestAsync(SubmitLeaveRequestDto dto)
    {
        // 1. 保存请假业务数据
        var leaveRequest = new LeaveRequest
        {
            UserId = dto.UserId,
            LeaveType = dto.LeaveType,
            StartTime = dto.StartTime,
            EndTime = dto.EndTime,
            Days = dto.Days,
            Reason = dto.Reason,
            Status = LeaveStatus.Pending,  // 审批中
            CreatedTime = DateTime.UtcNow
        };

        await _leaveRepo.AddAsync(leaveRequest);
        await _leaveRepo.SaveChangesAsync();

        // 2. 构造飞书审批表单数据
        var form = new List<object>
        {
            new { id = "leave_type", type = "select", value = dto.LeaveType },
            new { id = "start_time", type = "date", value = dto.StartTime.ToString("yyyy-MM-dd") },
            new { id = "end_time", type = "date", value = dto.EndTime.ToString("yyyy-MM-dd") },
            new { id = "days", type = "number", value = dto.Days.ToString() },
            new { id = "reason", type = "textarea", value = dto.Reason }
        };

        // 3. 调用飞书API创建审批实例
        var request = new CreateInstanceRequest
        {
            ApprovalCode = "7C468A54-8745-2245-9675-08B7C63E7A85",  // 请假审批定义Code
            UserId = dto.FeishuUserId,  // 飞书用户ID
            Form = JsonSerializer.Serialize(form),
            Uuid = Guid.NewGuid().ToString()  // 幂等ID
        };

        string instanceCode;
        try
        {
            instanceCode = await _feishuClient.CreateInstanceAsync(request);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "创建飞书审批实例失败");
            // 回滚业务数据
            leaveRequest.Status = LeaveStatus.Failed;
            await _leaveRepo.SaveChangesAsync();
            throw;
        }

        // 4. 保存审批关联记录
        var approvalRecord = new ApprovalRecord
        {
            BusinessType = "LeaveRequest",
            BusinessId = leaveRequest.Id,
            InstanceCode = instanceCode,
            ApprovalCode = request.ApprovalCode,
            Status = "PENDING",
            CreatedTime = DateTime.UtcNow,
            UpdatedTime = DateTime.UtcNow
        };

        await _approvalRepo.AddAsync(approvalRecord);
        await _approvalRepo.SaveChangesAsync();

        _logger.LogInformation("请假申请已提交并创建审批: LeaveId={LeaveId}, InstanceCode={InstanceCode}",
            leaveRequest.Id, instanceCode);

        return leaveRequest.Id;
    }
}

场景:审批完结,飞书回调通知结果

基于 Mud.Feishu.Webhook 实现安全的回调处理器

csharp 复制代码
using Mud.Feishu.Abstractions;
using Mud.Feishu.Abstractions.DataModels.Approval;
using Mud.Feishu.Abstractions.EventHandlers;

/// <summary>
/// 审批实例事件处理器
/// </summary>
public class ApprovalInstanceEventHandler : ApprovalInstanceEventHandler
{
    private readonly IApprovalRecordRepository _approvalRepo;
    private readonly ILeaveRequestRepository _leaveRepo;

    public ApprovalInstanceEventHandler(
        ILogger<ApprovalInstanceEventHandler> logger,
        IApprovalRecordRepository approvalRepo,
        ILeaveRequestRepository leaveRepo)
        : base(logger)
    {
        _approvalRepo = approvalRepo;
        _leaveRepo = leaveRepo;
    }

    /// <summary>
    /// 处理审批实例事件业务逻辑
    /// </summary>
    protected override async Task ProcessBusinessLogicAsync(
        EventData eventData,
        ObjectEventResult<ApprovalInstanceResult>? eventEntity,
        CancellationToken cancellationToken = default)
    {
        if (eventEntity?.Object == null)
        {
            _logger.LogWarning("审批实例事件数据无效");
            return;
        }

        var approvalEvent = eventEntity.Object;

        _logger.LogInformation("收到审批实例事件: InstanceCode={InstanceCode}, Status={Status}",
            approvalEvent.InstanceCode, approvalEvent.Status);

        // 幂等性处理:检查是否已处理过该事件
        var existingRecord = await _approvalRepo.GetByInstanceCodeAsync(approvalEvent.InstanceCode ?? string.Empty);
        if (existingRecord != null && existingRecord.Status == approvalEvent.Status)
        {
            _logger.LogInformation("该事件已处理过,跳过: EventId={EventId}", eventData.EventId);
            return;
        }

        // 根据业务类型处理审批结果
        await ProcessApprovalResultAsync(eventData, approvalEvent, cancellationToken);
    }

    /// <summary>
    /// 处理审批结果
    /// </summary>
    private async Task ProcessApprovalResultAsync(
        EventData eventData,
        ApprovalInstanceResult approvalEvent,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrEmpty(approvalEvent.InstanceCode))
        {
            _logger.LogWarning("审批实例Code为空,跳过处理");
            return;
        }

        // 查询审批关联记录
        var approvalRecord = await _approvalRepo.GetByInstanceCodeAsync(approvalEvent.InstanceCode);
        if (approvalRecord == null)
        {
            _logger.LogWarning("未找到审批关联记录: InstanceCode={InstanceCode}",
                approvalEvent.InstanceCode);
            return;
        }

        // 更新审批记录状态
        approvalRecord.Status = approvalEvent.Status ?? string.Empty;
        approvalRecord.CallbackData = JsonSerializer.Serialize(approvalEvent);
        approvalRecord.UpdatedTime = DateTime.UtcNow;
        await _approvalRepo.SaveChangesAsync();

        // 根据业务类型处理
        switch (approvalRecord.BusinessType)
        {
            case "LeaveRequest":
                await ProcessLeaveApprovalAsync(approvalRecord, approvalEvent.Status ?? string.Empty);
                break;

            // 可扩展其他业务类型
            default:
                _logger.LogWarning("未知的业务类型: {BusinessType}", approvalRecord.BusinessType);
                break;
        }
    }

    /// <summary>
    /// 处理请假审批结果
    /// </summary>
    private async Task ProcessLeaveApprovalAsync(ApprovalRecord approvalRecord, string status)
    {
        var leaveRequest = await _leaveRepo.GetByIdAsync(approvalRecord.BusinessId);
        if (leaveRequest == null)
        {
            _logger.LogWarning("未找到请假申请: BusinessId={BusinessId}", approvalRecord.BusinessId);
            return;
        }

        // 根据审批状态更新请假记录
        leaveRequest.Status = status switch
        {
            "APPROVED" => LeaveStatus.Approved,
            "REJECTED" => LeaveStatus.Rejected,
            "CANCELED" => LeaveStatus.Canceled,
            "DELETED" => LeaveStatus.Deleted,
            _ => LeaveStatus.Pending
        };

        leaveRequest.UpdatedTime = DateTime.UtcNow;
        await _leaveRepo.SaveChangesAsync();

        _logger.LogInformation("请假申请状态已更新: LeaveId={LeaveId}, Status={Status}",
            leaveRequest.Id, leaveRequest.Status);

        // TODO: 发送通知给申请人
        // TODO: 同步到考勤系统
    }
}

注册 Webhook 服务(Program.cs)

csharp 复制代码
using Mud.Feishu.Webhook;
using Mud.Feishu;
using YourApp.Handlers;

var builder = WebApplication.CreateBuilder(args);

// 注册飞书 API 服务
builder.Services.AddFeishuServices()
    .ConfigureFrom(builder.Configuration)  // 从 "Feishu" 配置节读取
    .Build();

// 注册飞书 Webhook 事件订阅服务
builder.Services.AddFeishuWebhookServiceBuilder()
    .ConfigureFrom(builder.Configuration)  // 从 "FeishuWebhook" 配置节读取
    .AddHandler<ApprovalInstanceEventHandler>()  // 添加审批事件处理器
    .Build();

// 注册业务服务
builder.Services.AddScoped<ILeaveRequestRepository, LeaveRequestRepository>();
builder.Services.AddScoped<IApprovalRecordRepository, ApprovalRecordRepository>();

var app = builder.Build();

app.UseFeishuWebhook();  // 添加 Webhook 中间件

app.Run();

说明

  • ApprovalInstanceEventHandler 继承自 ApprovalInstanceEventHandler 基类
  • 基类已经实现了 HandleAsync 方法,会自动反序列化 ApprovalInstanceResult 类型的事件数据
  • 只需重写 ProcessBusinessLogicAsync 方法实现具体的业务逻辑即可
  • SDK 会根据 SupportedEventType 属性自动路由对应的事件到这个处理器
  • AddFeishuServices() 注册飞书 API 客户端服务,使用 Feishu 配置节
  • AddFeishuWebhookServiceBuilder() 注册 Webhook 事件订阅服务,使用 FeishuWebhook 配置节

配置文件(appsettings.json)

json 复制代码
{
  // 飞书 Webhook 事件订阅配置
  "FeishuWebhook": {
    "VerificationToken": "your_verification_token",
    "EncryptKey": "your_encrypt_key",
    "RoutePrefix": "api/feishu/webhook",
    "AutoRegisterEndpoint": true,
    "EnableRequestLogging": true,
    "EnableExceptionHandling": true,
    "EventHandlingTimeoutMs": 30000,
    "MaxConcurrentEvents": 10
  },

  // 飞书 API 客户端配置
  "Feishu": {
    "AppId": "your_app_id",
    "AppSecret": "your_app_secret",
    "BaseUrl": "https://open.feishu.cn",
    "TimeOut": "30",
    "RetryCount": 3,
    "EnableLogging": true
  }
}

第四步:功能完善与联调测试

状态同步展示

在请假列表页展示审批状态:

csharp 复制代码
/// <summary>
/// 请假列表响应DTO
/// </summary>
public class LeaveRequestDto
{
    public long Id { get; set; }
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
    public int Days { get; set; }
    public string Status { get; set; } = string.Empty;      // 业务状态:Approved, Rejected
    public string ApprovalStatus { get; set; } = string.Empty; // 飞书审批状态:APPROVED, REJECTED, PENDING
    public string? FeishuInstanceUrl { get; set; }          // 飞书审批详情链接
    public DateTime CreatedTime { get; set; }
}

/// <summary>
/// 查询请假列表
/// </summary>
public async Task<List<LeaveRequestDto>> GetLeaveListAsync(long userId)
{
    var leaves = await _leaveRepo.GetByUserIdAsync(userId);
    var instanceCodes = leaves.Select(l => l.InstanceCode).ToList();

    // 批量查询审批记录
    var approvals = await _approvalRepo.GetByInstanceCodesAsync(instanceCodes);

    var result = leaves.Select(leave =>
    {
        var approval = approvals.FirstOrDefault(a => a.InstanceCode == leave.InstanceCode);

        return new LeaveRequestDto
        {
            Id = leave.Id,
            StartTime = leave.StartTime,
            EndTime = leave.EndTime,
            Days = leave.Days,
            Status = leave.Status.ToString(),
            ApprovalStatus = approval?.Status ?? "UNKNOWN",
            FeishuInstanceUrl = !string.IsNullOrEmpty(approval?.InstanceCode)
                ? $"https://www.feishu.cn/approval/approval/view/{approval.InstanceCode}"
                : null,
            CreatedTime = leave.CreatedTime
        };
    }).ToList();

    return result;
}

添加"在飞书中查看"链接

在列表页添加操作按钮:

html 复制代码
<!-- 前端页面示例 -->
<table class="leave-list">
    <thead>
        <tr>
            <th>开始时间</th>
            <th>结束时间</th>
            <th>天数</th>
            <th>审批状态</th>
            <th>操作</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var leave in Model.Leaves)
        {
            <tr>
                <td>@leave.StartTime.ToString("yyyy-MM-dd")</td>
                <td>@leave.EndTime.ToString("yyyy-MM-dd")</td>
                <td>@leave.Days</td>
                <td>
                    <span class="status @leave.ApprovalStatus">
                        @GetStatusText(leave.ApprovalStatus)
                    </span>
                </td>
                <td>
                    @if (!string.IsNullOrEmpty(leave.FeishuInstanceUrl))
                    {
                        <a href="@leave.FeishuInstanceUrl" target="_blank" class="btn">
                            在飞书中查看
                        </a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

联调测试

测试场景 验证要点 测试工具
发起审批 飞书是否收到审批通知、表单数据是否正确 直接在系统发起
审批流程 各审批节点是否正确流转 飞书管理后台
回调接收 Webhook是否正确接收事件、数据是否完整 飞书"模拟事件推送"工具
状态同步 业务状态是否正确更新、通知是否发送 数据库查询、日志查看
异常处理 网络异常、签名验证失败等边界情况 模拟异常场景

飞书模拟事件推送工具

在飞书开放平台的"事件订阅"页面,可以使用"模拟事件推送"功能测试 Webhook 接口:
graph LR A[飞书管理后台] -->|模拟事件推送| B[Webhook接口] B -->|日志输出| C[检查处理结果] C -->|成功| D[验证完成] C -->|失败| E[查看错误日志]


生产级注意事项

安全与可靠性

机密管理

切勿将敏感信息硬编码在代码中!

csharp 复制代码
// ❌ 错误示例
var appSecret = "cli_xxxxxxxxxxxxxxx";  // 危险!

// ✅ 正确示例
builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{vaultName}.vault.azure.net/"),
    new DefaultAzureCredential());

var appSecret = builder.Configuration["Feishu:AppSecret"];

推荐方案

  • Azure Key Vault / AWS Secrets Manager
  • HashiCorp Vault
  • Docker Secrets(容器化部署)

幂等性处理

飞书可能会重复推送同一个事件(网络重试等),必须保证业务逻辑的幂等性:

csharp 复制代码
public async Task HandleAsync(EventData eventData, CancellationToken cancellationToken = default)
{
    // 使用 EventId 或 instance_code + status 组合作为幂等键
    var idempotencyKey = $"{approvalEvent.InstanceCode}_{approvalEvent.Status}";

    // 检查是否已处理过
    if (await _cache.ExistsAsync(idempotencyKey))
    {
        _logger.LogInformation("事件已处理过,跳过: Key={IdempotencyKey}", idempotencyKey);
        return;
    }

    // 标记为已处理(设置过期时间,如24小时)
    await _cache.SetAsync(idempotencyKey, "1", TimeSpan.FromHours(24));

    // 执行业务逻辑
    await ProcessEventAsync(approvalEvent, cancellationToken);
}

API 容错

使用 Polly 为飞书 API 调用添加重试和熔断机制:

csharp 复制代码
// 注册 HttpClient 时添加 Polly 策略
builder.Services.AddHttpClient("Feishu")
    .AddTransientHttpErrorPolicy(p =>
        p.WaitAndRetryAsync(3, retryAttempt =>
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
    .AddPolicyHandler(Policy<HttpResponseMessage>
        .Handle<HttpRequestException>()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

边界情况与优雅降级

审批人失效处理

csharp 复制代码
// 飞书审批定义中配置默认审批人
var request = new CreateInstanceRequest
{
    ApprovalCode = "xxxx",
    // 如果自选审批人为空,使用默认审批人
    NodeApproverUserIdLists = dto.ApproverUserId.HasValue
        ? new[] { new NodeApprover { NodeId = "node1", ApproverUserIds = new[] { dto.ApproverUserId.Value } } }
        : null  // 走默认审批人流程
};

网络超时与异步处理

回调处理应快速响应飞书(建议在 3 秒内),复杂逻辑移至后台作业:

csharp 复制代码
public async Task HandleAsync(EventData eventData, CancellationToken cancellationToken = default)
{
    // 1. 快速保存事件到队列
    await _eventQueue.EnqueueAsync(eventData);

    // 2. 立即返回,由后台作业处理
    // Hangfire、RabbitMQ 等会异步消费队列
    await Task.CompletedTask;
}

// 后台作业处理
[Queue("approval-callback")]
public async Task ProcessApprovalEventAsync(EventData eventData)
{
    // 复杂的业务逻辑处理
    await _approvalService.ProcessCallbackAsync(eventData);
}

监控与告警

建立关键节点的监控:

csharp 复制代码
// 监控指标
public class ApprovalMetrics
{
    private readonly Counter _approvalCreatedCounter;
    private readonly Counter _callbackReceivedCounter;
    private readonly Histogram _processingTimeHistogram;

    public void RecordApprovalCreated(string approvalType)
    {
        _approvalCreatedCounter.WithLabels(approvalType).Inc();
    }

    public void RecordCallbackReceived(string status)
    {
        _callbackReceivedCounter.WithLabels(status).Inc();
    }

    public void RecordProcessingTime(TimeSpan duration)
    {
        _processingTimeHistogram.Observe(duration.TotalSeconds);
    }
}

// 告警规则(Prometheus 示例)
# 审批发起失败率超过 5% 触发告警
alert: ApprovalCreationFailureRate
expr: rate(approval_creation_failed_total[5m]) / rate(approval_creation_total[5m]) > 0.05
for: 5m
annotations:
  summary: "审批创建失败率过高"

扩展性与维护性

策略模式支持多平台

csharp 复制代码
/// <summary>
/// 审批平台策略接口
/// </summary>
public interface IApprovalPlatformStrategy
{
    string PlatformName { get; }
    Task<string> CreateInstanceAsync(ApprovalRequest request);
}

/// <summary>
/// 飞书审批策略
/// </summary>
public class FeishuApprovalStrategy : IApprovalPlatformStrategy
{
    public string PlatformName => "Feishu";
    // ... 实现
}

/// <summary>
/// 钉钉审批策略
/// </summary>
public class DingTalkApprovalStrategy : IApprovalPlatformStrategy
{
    public string PlatformName => "DingTalk";
    // ... 实现
}

/// <summary>
/// 审批策略工厂
/// </summary>
public class ApprovalStrategyFactory
{
    private readonly IEnumerable<IApprovalPlatformStrategy> _strategies;

    public IApprovalPlatformStrategy GetStrategy(string platformName)
    {
        return _strategies.FirstOrDefault(s => s.PlatformName == platformName)
            ?? throw new NotSupportedException($"不支持的审批平台: {platformName}");
    }
}

审计日志

详细记录审批流转换的关键日志:

csharp 复制代码
public class ApprovalAuditService
{
    private readonly IApprovalAuditRepository _auditRepo;

    public async Task LogAsync(ApprovalAuditLog log)
    {
        log.Timestamp = DateTime.UtcNow;
        await _auditRepo.AddAsync(log);
        await _auditRepo.SaveChangesAsync();

        // 结构化日志输出
        _logger.LogInformation("审批审计: {AuditType}, InstanceCode={InstanceCode}, BusinessId={BusinessId}",
            log.AuditType, log.InstanceCode, log.BusinessId);
    }
}

// 使用示例
await _auditService.LogAsync(new ApprovalAuditLog
{
    AuditType = "ApprovalStarted",
    InstanceCode = instanceCode,
    BusinessId = leaveRequest.Id,
    OperatorId = userId,
    Details = new { leaveType, days, reason }
});

最后一点内容

核心价值

通过本文的实践,我们成功实现了:

价值点 实现方式 收益
移动化审批 飞书 App 作为审批门户 随时随地处理审批
即时通知 飞书强通知机制 审批人及时响应
数据闭环 .NET 业务库 + 审批关联表 完整的业务流程追踪
解耦设计 领域层抽象 + 策略模式 便于扩展和维护

全文总结

本文提供了一个从理念、设计到编码落地的完整闭环:
mindmap root((飞书审批集成)) 理念 双向集成 .NET为业务核心 飞书为流程门户 设计 分层架构 领域抽象 安全机制 实现 发起审批 回调处理 状态同步 最佳实践 机密管理 幂等性 异步处理 监控告警

扩展

场景扩展

将此模式快速复用于其他业务场景:

业务场景 审批流程 复杂度
报销审批 发起 → 直属主管 → 财务审核
采购申请 发起 → 部门主管 → 采购部 → 总经理
合同审批 法务审核 → 财务审核 → 总经理
加班申请 直属主管审批

深度集成

利用飞书更多能力,打造更丰富的协同体验:
graph TB A[飞书审批] --> B[消息卡片] A --> C[智能机器人] A --> D[知识库] B --> B1[审批详情展示] B --> B2[操作按钮] C --> C1[智能提醒] C --> C2[自动补全] D --> D1[历史记录查询] D --> D2[审批规范] E[.NET系统] --> A B --> E C --> E D --> E

功能扩展示例

  • 在飞书群聊中通过消息卡片直接查看审批详情
  • 通过机器人智能回复,引导用户填写审批表单
  • 将审批记录同步到飞书知识库,方便查阅

平台化

将审批集成能力抽象为中台服务,供企业内部所有系统统一调用:

csharp 复制代码
/// <summary>
/// 统一审批服务中台
/// </summary>
public interface IApprovalCenterService
{
    /// <summary>
    /// 统一发起审批(支持多平台)
    /// </summary>
    Task<string> CreateApprovalAsync(UnifiedApprovalRequest request);

    /// <summary>
    /// 查询审批状态
    /// </summary>
    Task<ApprovalStatus> GetStatusAsync(string instanceId);

    /// <summary>
    /// 审批统计报表
    /// </summary>
    Task<ApprovalStatistics> GetStatisticsAsync(DateTime from, DateTime to);
}

// 多个系统统一调用
await _approvalCenter.CreateApprovalAsync(new UnifiedApprovalRequest
{
    BusinessSystem = "HR",
    BusinessType = "LeaveRequest",
    BusinessId = leaveId,
    Platform = "Feishu"  // 可切换到其他平台
});

结语

传统 .NET 系统无需推倒重来,通过合理的架构设计与飞书审批的深度集成,同样可以焕发新的活力。希望本文的实践能够为你的数字化转型之路提供有价值的参考。

让我们一起告别信息孤岛,拥抱现代化的协同办公体验!🚀


相关资源

项目地址

相关推荐
烛阴16 小时前
C# 正则表达式(5):前瞻/后顾(Lookaround)——零宽断言做“条件校验”和“精确提取”
前端·正则表达式·c#
为自己_带盐19 小时前
从零开始玩转 Microsoft Agent Framework:我的 MAF 实践之旅-第二篇
后端·microsoft·ai·.net
m5655bj21 小时前
使用 C# 实现 Excel 工作表拆分
windows·c#·excel·visual studio
fengfuyao9851 天前
基于C#实现的支持五笔和拼音输入的输入法
开发语言·c#
xiaowu0801 天前
C# 多返回值写法
java·前端·c#
步步为营DotNet1 天前
深度剖析.NET 中IConfiguration:灵活配置管理的核心枢纽
前端·网络·.net
Crazy Struggle1 天前
C# 不依赖 OpenCV 的图像处理算法:滤波、锐化与边缘检测
.net·开源项目·winform
前端慢慢其修远1 天前
利用signalR实现简单通信(Vue2+C#)
c#·vue
微小冷1 天前
C#异步编程详解
开发语言·c#·async·await·异步编程
qq_316165291 天前
C#委托和事件的区别
开发语言·c#