周末深夜,你收到紧急审批通知------却发现只能在 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 系统发起审批时,系统会:
- 保存业务数据,状态标记为"审批中"
- 调用飞书 API
CreateInstanceAsync创建审批实例 - 接收返回的
instance_code,持久化到关联表
流程输入(回调阶段)
当审批人在飞书 App 完成审批后:
- 飞书服务器主动回调 .NET 系统的 Webhook 接口
- .NET 系统解析事件,提取
instance_code和status - 根据关联表查询对应的业务记录
- 更新业务状态,完成闭环
必须理解的三个核心概念
审批定义(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
实现方式:
- 在 .NET 系统的用户表中添加
FeishuOpenId字段 - 用户首次登录时进行飞书免登录认证,获取并存储
open_id - 发起审批时,使用
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 ID和App 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 系统无需推倒重来,通过合理的架构设计与飞书审批的深度集成,同样可以焕发新的活力。希望本文的实践能够为你的数字化转型之路提供有价值的参考。
让我们一起告别信息孤岛,拥抱现代化的协同办公体验!🚀