使用.NET 8+ 与飞书API构建组织架构同步服务

一、.NET生态下飞书API集成挑战

.NET企业应用场景

在现代企业数字化转型中,典型的.NET技术栈(如ASP.NET Core MVC/Web API, Entity Framework Core, SQL Server)构建的内部管理系统扮演着核心角色。这些系统承载着企业的关键业务流程,从人力资源管理到权限控制,从财务审批到业务数据分析。

然而,一个普遍存在的痛点是:员工信息在飞书和自建.NET系统间存在严重的数据不一致问题。当新员工入职时,HR在飞书中录入信息,但各个.NET系统仍需手动重复录入;当员工离职或转岗时,权限更新往往滞后,存在安全隐患;部门架构调整时,各系统的数据更新更是不同步,导致报表统计不准确。

同步的核心价值

统一身份认证: 通过建立可靠的组织架构同步机制,为后续实现飞书扫码登录(OAuth 2.0)打下坚实基础。当用户数据在飞书和本地系统保持一致时,才能基于飞书身份实现无缝的单点登录体验。

自动化运维: 利用.NET后台服务(如BackgroundService)实现全自动化的同步流程,替代人工操作,降低运维成本,提高数据准确性。

数据一致性: 确保企业内部所有.NET应用的组织数据与飞书源保持实时一致,为决策提供准确的数据支撑。

二、.NET技术选型

飞书开放平台配置

首先需要在飞书开放平台创建"企业自建应用":

  1. 登录飞书开放平台
  2. 创建企业自建应用,获取 AppIdAppSecret
  3. 申请必要的权限:
    • contact:contact:readonly (核心权限,用于读取联系人信息)
    • contact:user.employee_id:readonly (用于获取员工ID)
    • contact:department:readonly (用于读取部门信息)

.NET项目设置与技术栈

.NET版本: 推荐 .NET 8 (LTS长期支持版本),充分利用最新的性能优化和语言特性。

Mud.Feishu飞书服务SDK: 这是本次实践的核心组件。Mud.Feishu是一个现代化的.NET库,专门用于简化与飞书API的集成。相比原生SDK,它具有以下优势:

对比维度 原生SDK调用 Mud.Feishu组件
开发效率 需要手动构造HTTP请求、处理响应 只需调用简洁的接口方法,一行代码完成操作
类型安全 手动处理JSON序列化,容易出现类型错误 提供完整的强类型支持,编译时发现错误
令牌管理 需要手动获取、刷新和管理访问令牌 自动处理令牌获取和刷新机制
异常处理 需要手动处理各种网络异常和业务异常 提供统一的异常处理机制

安装Mud.Feishu:

bash 复制代码
dotnet add package Mud.Feishu --version 1.0.0

技术栈完整配置:

json 复制代码
// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Feishu": {
    "AppId": "your_app_id",
    "AppSecret": "your_app_secret",
    "BaseUrl": "https://open.feishu.cn"
  },
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=OrganizationSync;Trusted_Connection=true;"
  }
}

Program.cs 中注册服务:

csharp 复制代码
using Mud.Feishu;

var builder = WebApplication.CreateBuilder(args);

// 注册飞书 API 服务
builder.Services.AddFeishuApiService(builder.Configuration);

// 注册数据库上下文
builder.Services.AddDbContext<OrganizationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// 注册同步服务
builder.Services.AddScoped<IFeishuSyncService, FeishuSyncService>();
builder.Services.AddHostedService<FeishuFullSyncService>();

var app = builder.Build();

三、核心架构与同步模式设计

领域模型设计

设计本地数据库表结构,确保与飞书数据的有效映射:

csharp 复制代码
// 部门实体
public class Department
{
    public int Id { get; set; }
    public string FeishuDepartmentId { get; set; } // 飞书部门ID,用于关联
    public string Name { get; set; }
    public string? ParentFeishuDepartmentId { get; set; }
    public int SortOrder { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }

    // 导航属性
    public virtual Department? ParentDepartment { get; set; }
    public virtual ICollection<Department> SubDepartments { get; set; } = new List<Department>();
    public virtual ICollection<User> Users { get; set; } = new List<User>();
}

// 用户实体
public class User
{
    public int Id { get; set; }
    public string FeishuUserId { get; set; } // 飞书用户ID,用于关联
    public string Name { get; set; }
    public string? Email { get; set; }
    public string? Mobile { get; set; }
    public string? EmployeeNumber { get; set; }
    public int? DepartmentId { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }

    // 导航属性
    public virtual Department? Department { get; set; }
}

EF Core DbContext配置:

csharp 复制代码
public class OrganizationDbContext : DbContext
{
    public OrganizationDbContext(DbContextOptions<OrganizationDbContext> options)
        : base(options)
    {
    }

    public DbSet<Department> Departments { get; set; }
    public DbSet<User> Users { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 为飞书ID创建唯一索引
        modelBuilder.Entity<Department>()
            .HasIndex(d => d.FeishuDepartmentId)
            .IsUnique();

        modelBuilder.Entity<User>()
            .HasIndex(u => u.FeishuUserId)
            .IsUnique();

        // 配置部门层级关系
        modelBuilder.Entity<Department>()
            .HasOne(d => d.ParentDepartment)
            .WithMany(d => d.SubDepartments)
            .HasForeignKey(d => d.ParentFeishuDepartmentId)
            .HasPrincipalKey(d => d.FeishuDepartmentId);
    }
}

同步模式设计

模式一:全量同步(使用 BackgroundService)

适用于系统初始化或夜间批量同步的场景。通过递归方式获取完整的组织架构:

csharp 复制代码
public class FeishuFullSyncService : BackgroundService
{
    private readonly ILogger<FeishuFullSyncService> _logger;
    private readonly IFeishuSyncService _syncService;
    private readonly IServiceScopeFactory _scopeFactory;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _scopeFactory.CreateScope();
                _logger.LogInformation("开始执行全量同步任务");

                await _syncService.FullSyncDepartmentsAsync();
                await _syncService.FullSyncUsersAsync();

                _logger.LogInformation("全量同步任务完成");
                
                // 每天凌晨2点执行
                var nextRun = DateTime.Today.AddDays(1).AddHours(2);
                var delay = nextRun - DateTime.Now;
                await Task.Delay(delay, stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "全量同步任务执行失败");
                await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
            }
        }
    }
}

模式二:增量/事件同步(创建ASP.NET Core Web API 控制器)

适用于实时性要求高的场景,通过飞书事件订阅实现:

csharp 复制代码
[ApiController]
[Route("api/[controller]")]
public class FeishuEventController : ControllerBase
{
    private readonly ILogger<FeishuEventController> _logger;
    private readonly IFeishuSyncService _syncService;

    [HttpPost("webhook")]
    public async Task<IActionResult> HandleEvent([FromBody] FeishuEventRequest request)
    {
        try
        {
            // 验证事件签名
            if (!ValidateSignature(request))
                return Unauthorized();

            // 根据事件类型处理
            switch (request.Header.EventType)
            {
                case "contact.user.updated":
                    await _syncService.SyncUserAsync(request.Event.UserId);
                    break;
                case "contact.department.updated":
                    await _syncService.SyncDepartmentAsync(request.Event.DepartmentId);
                    break;
                // 其他事件类型...
            }

            return Ok(new { code = 0, msg: "success" });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "处理飞书事件失败");
            return BadRequest(new { code = -1, msg: ex.Message });
        }
    }

    private bool ValidateSignature(FeishuEventRequest request)
    {
        // 实现飞书事件签名验证逻辑
        // 参考:https://open.feishu.cn/document/server-docs/event-subscription-guides/event-verification
        return true;
    }
}

混合模式(推荐): 启动时全量同步 + 运行时事件同步 + 每日定时全量同步兜底。这种模式既保证了数据的一致性,又具备良好的实时性。

四、分步实现指南

步骤一:构建飞书API客户端

创建 FeishuApiService 类,封装飞书API调用:

csharp 复制代码
public interface IFeishuApiService
{
    Task<List<FeishuDepartment>> GetDepartmentsAsync();
    Task<List<FeishuUser>> GetUsersByDepartmentAsync(string departmentId);
    Task<FeishuUser?> GetUserByIdAsync(string userId);
}

public class FeishuApiService : IFeishuApiService
{
    private readonly IFeishuV3DepartmentsApi _departmentsApi;
    private readonly IFeishuV3UserApi _userApi;
    private readonly ILogger<FeishuApiService> _logger;

    public FeishuApiService(
        IFeishuV3DepartmentsApi departmentsApi,
        IFeishuV3UserApi userApi,
        ILogger<FeishuApiService> logger)
    {
        _departmentsApi = departmentsApi;
        _userApi = userApi;
        _logger = logger;
    }

    public async Task<List<FeishuDepartment>> GetDepartmentsAsync()
    {
        try
        {
            var result = await _departmentsApi.GetDepartmentByIdAsync("0"); // 获取根部门
            if (result.Code == 0 && result.Data != null)
            {
                var departments = new List<FeishuDepartment>();
                await ProcessDepartmentTree(result.Data, departments);
                return departments;
            }

            throw new FeishuException($"获取部门列表失败: {result.Msg}");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "获取部门列表异常");
            throw;
        }
    }

    public async Task<List<FeishuUser>> GetUsersByDepartmentAsync(string departmentId)
    {
        try
        {
            var users = new List<FeishuUser>();
            var pageToken = "";
            var pageSize = 50;

            do
            {
                var result = await _userApi.GetUserByDepartmentIdAsync(
                    departmentId: departmentId,
                    page_size: pageSize,
                    page_token: string.IsNullOrEmpty(pageToken) ? null : pageToken);

                if (result.Code == 0 && result.Data?.Items != null)
                {
                    users.AddRange(result.Data.Items.Select(item => MapToFeishuUser(item)));
                    pageToken = result.Data.PageToken ?? "";
                }
                else
                {
                    throw new FeishuException($"获取部门用户失败: {result.Msg}");
                }
            } while (!string.IsNullOrEmpty(pageToken));

            return users;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "获取部门用户异常,DepartmentId: {DepartmentId}", departmentId);
            throw;
        }
    }

    private async Task ProcessDepartmentTree(DepartmentData dept, List<FeishuDepartment> departments)
    {
        var feishuDept = MapToFeishuDepartment(dept);
        departments.Add(feishuDept);

        // 递归获取子部门
        if (dept.SubDepartments != null)
        {
            foreach (var subDept in dept.SubDepartments)
            {
                await ProcessDepartmentTree(subDept, departments);
            }
        }
    }

    private FeishuDepartment MapToFeishuDepartment(DepartmentData dept) => new()
    {
        DepartmentId = dept.DepartmentId,
        Name = dept.Name,
        ParentDepartmentId = dept.ParentDepartmentId,
        SortOrder = dept.Order
    };

    private FeishuUser MapToFeishuUser(UserData user) => new()
    {
        UserId = user.UserId,
        Name = user.Name,
        Email = user.Email,
        Mobile = user.Mobile,
        EmployeeNumber = user.EmployeeNumber,
        DepartmentIds = user.DepartmentIds?.ToList() ?? new List<string>(),
        IsActive = user.Status?.IsActive ?? false
    };
}

步骤二:实现数据映射与处理

创建DTO类用于数据转换,并使用AutoMapper进行映射:

csharp 复制代码
// 飞书数据传输对象
public class FeishuDepartment
{
    public string DepartmentId { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
    public string? ParentDepartmentId { get; set; }
    public int SortOrder { get; set; }
}

public class FeishuUser
{
    public string UserId { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
    public string? Email { get; set; }
    public string? Mobile { get; set; }
    public string? EmployeeNumber { get; set; }
    public List<string> DepartmentIds { get; set; } = new();
    public bool IsActive { get; set; }
}

// AutoMapper配置
public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<FeishuDepartment, Department>()
            .ForMember(dest => dest.FeishuDepartmentId, opt => opt.MapFrom(src => src.DepartmentId))
            .ForMember(dest => dest.Id, opt => opt.Ignore())
            .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow))
            .ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(src => DateTime.UtcNow))
            .ForMember(dest => dest.ParentDepartment, opt => opt.Ignore())
            .ForMember(dest => dest.SubDepartments, opt => opt.Ignore())
            .ForMember(dest => dest.Users, opt => opt.Ignore());

        CreateMap<FeishuUser, User>()
            .ForMember(dest => dest.FeishuUserId, opt => opt.MapFrom(src => src.UserId))
            .ForMember(dest => dest.Id, opt => opt.Ignore())
            .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow))
            .ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(src => DateTime.UtcNow))
            .ForMember(dest => dest.Department, opt => opt.Ignore());
    }
}

步骤三:实现全量同步服务

创建同步服务接口和实现:

csharp 复制代码
public interface IFeishuSyncService
{
    Task FullSyncDepartmentsAsync();
    Task FullSyncUsersAsync();
    Task SyncDepartmentAsync(string departmentId);
    Task SyncUserAsync(string userId);
}

public class FeishuSyncService : IFeishuSyncService
{
    private readonly IFeishuApiService _feishuApiService;
    private readonly OrganizationDbContext _dbContext;
    private readonly IMapper _mapper;
    private readonly ILogger<FeishuSyncService> _logger;

    public async Task FullSyncDepartmentsAsync()
    {
        _logger.LogInformation("开始全量同步部门");
        
        try
        {
            // 获取飞书部门列表
            var feishuDepartments = await _feishuApiService.GetDepartmentsAsync();
            
            // 获取本地部门列表
            var localDepartments = await _dbContext.Departments
                .Where(d => d.IsActive)
                .ToDictionaryAsync(d => d.FeishuDepartmentId);

            var departmentsToAdd = new List<Department>();
            var departmentsToUpdate = new List<Department>();
            var departmentIdsToDeactivate = new HashSet<string>(localDepartments.Keys);

            foreach (var feishuDept in feishuDepartments)
            {
                departmentIdsToDeactivate.Remove(feishuDept.DepartmentId);

                if (localDepartments.TryGetValue(feishuDept.DepartmentId, out var localDept))
                {
                    // 更新现有部门
                    _mapper.Map(feishuDept, localDept);
                    localDept.UpdatedAt = DateTime.UtcNow;
                    departmentsToUpdate.Add(localDept);
                }
                else
                {
                    // 新增部门
                    var newDept = _mapper.Map<Department>(feishuDept);
                    newDept.IsActive = true;
                    departmentsToAdd.Add(newDept);
                }
            }

            // 执行数据库操作
            if (departmentsToAdd.Any())
            {
                _dbContext.Departments.AddRange(departmentsToAdd);
                _logger.LogInformation("新增部门数量: {Count}", departmentsToAdd.Count);
            }

            if (departmentsToUpdate.Any())
            {
                _dbContext.Departments.UpdateRange(departmentsToUpdate);
                _logger.LogInformation("更新部门数量: {Count}", departmentsToUpdate.Count);
            }

            // 逻辑删除不存在的部门
            if (departmentIdsToDeactivate.Any())
            {
                var departmentsToDeactivate = await _dbContext.Departments
                    .Where(d => departmentIdsToDeactivate.Contains(d.FeishuDepartmentId))
                    .ToListAsync();

                foreach (var dept in departmentsToDeactivate)
                {
                    dept.IsActive = false;
                    dept.UpdatedAt = DateTime.UtcNow;
                }

                _logger.LogInformation("停用部门数量: {Count}", departmentsToDeactivate.Count);
            }

            await _dbContext.SaveChangesAsync();
            _logger.LogInformation("部门全量同步完成");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "部门全量同步失败");
            throw;
        }
    }

    public async Task FullSyncUsersAsync()
    {
        _logger.LogInformation("开始全量同步用户");
        
        try
        {
            // 获取所有部门
            var departments = await _dbContext.Departments
                .Where(d => d.IsActive)
                .ToListAsync();

            var feishuUsers = new List<FeishuUser>();

            // 遍历每个部门获取用户
            foreach (var dept in departments)
            {
                try
                {
                    var deptUsers = await _feishuApiService.GetUsersByDepartmentAsync(dept.FeishuDepartmentId);
                    feishuUsers.AddRange(deptUsers);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "获取部门用户失败,DepartmentId: {DepartmentId}", dept.FeishuDepartmentId);
                    // 继续处理其他部门
                }
            }

            // 去重(用户可能属于多个部门)
            var uniqueUsers = feishuUsers.GroupBy(u => u.UserId).Select(g => g.First()).ToList();

            // 获取本地用户列表
            var localUsers = await _dbContext.Users
                .Where(u => u.IsActive)
                .ToDictionaryAsync(u => u.FeishuUserId);

            var usersToAdd = new List<User>();
            var usersToUpdate = new List<User>();
            var userIdsToDeactivate = new HashSet<string>(localUsers.Keys);

            foreach (var feishuUser in uniqueUsers)
            {
                userIdsToDeactivate.Remove(feishuUser.UserId);

                if (localUsers.TryGetValue(feishuUser.UserId, out var localUser))
                {
                    // 更新现有用户
                    _mapper.Map(feishuUser, localUser);
                    localUser.UpdatedAt = DateTime.UtcNow;
                    
                    // 设置主部门(取第一个有效部门)
                    var primaryDept = departments.FirstOrDefault(d => feishuUser.DepartmentIds.Contains(d.FeishuDepartmentId));
                    if (primaryDept != null)
                    {
                        localUser.DepartmentId = primaryDept.Id;
                    }
                    
                    usersToUpdate.Add(localUser);
                }
                else
                {
                    // 新增用户
                    var newUser = _mapper.Map<User>(feishuUser);
                    newUser.IsActive = true;
                    
                    // 设置主部门
                    var primaryDept = departments.FirstOrDefault(d => feishuUser.DepartmentIds.Contains(d.FeishuDepartmentId));
                    if (primaryDept != null)
                    {
                        newUser.DepartmentId = primaryDept.Id;
                    }
                    
                    usersToAdd.Add(newUser);
                }
            }

            // 执行数据库操作
            if (usersToAdd.Any())
            {
                _dbContext.Users.AddRange(usersToAdd);
                _logger.LogInformation("新增用户数量: {Count}", usersToAdd.Count);
            }

            if (usersToUpdate.Any())
            {
                _dbContext.Users.UpdateRange(usersToUpdate);
                _logger.LogInformation("更新用户数量: {Count}", usersToUpdate.Count);
            }

            // 逻辑删除不存在的用户
            if (userIdsToDeactivate.Any())
            {
                var usersToDeactivate = await _dbContext.Users
                    .Where(u => userIdsToDeactivate.Contains(u.FeishuUserId))
                    .ToListAsync();

                foreach (var user in usersToDeactivate)
                {
                    user.IsActive = false;
                    user.UpdatedAt = DateTime.UtcNow;
                }

                _logger.LogInformation("停用用户数量: {Count}", usersToDeactivate.Count);
            }

            await _dbContext.SaveChangesAsync();
            _logger.LogInformation("用户全量同步完成");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "用户全量同步失败");
            throw;
        }
    }

    public async Task SyncDepartmentAsync(string departmentId)
    {
        // 实现单个部门的增量同步
        // 类似于全量同步的逻辑,但只处理指定部门
        throw new NotImplementedException();
    }

    public async Task SyncUserAsync(string userId)
    {
        // 实现单个用户的增量同步
        // 类似于全量同步的逻辑,但只处理指定用户
        throw new NotImplementedException();
    }
}

步骤四:实现增量事件同步

创建事件处理器来处理飞书的实时事件:

csharp 复制代码
public class FeishuEventHandler
{
    private readonly IFeishuSyncService _syncService;
    private readonly ILogger<FeishuEventHandler> _logger;

    public async Task HandleUserUpdatedEvent(FeishuEventPayload payload)
    {
        try
        {
            var userId = payload.UserId;
            _logger.LogInformation("处理用户更新事件,UserId: {UserId}", userId);
            
            await _syncService.SyncUserAsync(userId);
            
            _logger.LogInformation("用户更新事件处理完成,UserId: {UserId}", userId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "处理用户更新事件失败,UserId: {UserId}", payload.UserId);
            throw;
        }
    }

    public async Task HandleDepartmentUpdatedEvent(FeishuEventPayload payload)
    {
        try
        {
            var departmentId = payload.DepartmentId;
            _logger.LogInformation("处理部门更新事件,DepartmentId: {DepartmentId}", departmentId);
            
            await _syncService.SyncDepartmentAsync(departmentId);
            
            _logger.LogInformation("部门更新事件处理完成,DepartmentId: {DepartmentId}", departmentId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "处理部门更新事件失败,DepartmentId: {DepartmentId}", payload.DepartmentId);
            throw;
        }
    }
}

步骤五:处理边界情况与异常

使用Polly库实现重试和熔断策略:

csharp 复制代码
public class ResilientFeishuApiService : IFeishuApiService
{
    private readonly IFeishuApiService _innerService;
    private readonly IAsyncPolicy _retryPolicy;
    private readonly IAsyncPolicy _circuitBreakerPolicy;

    public ResilientFeishuApiService(IFeishuApiService innerService)
    {
        _innerService = innerService;
        
        _retryPolicy = Policy
            .Handle<FeishuException>(ex => ex.ErrorCode >= 500) // 服务器错误重试
            .Or<HttpRequestException>() // 网络错误重试
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timespan, retryAttempt, context) =>
                {
                    Console.WriteLine($"重试第 {retryAttempt} 次,延迟 {timespan.TotalSeconds} 秒");
                });

        _circuitBreakerPolicy = Policy
            .Handle<FeishuException>()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromMinutes(1),
                onBreak: (ex, breakDelay) =>
                {
                    Console.WriteLine($"熔断器开启,延迟 {breakDelay.TotalMinutes} 分钟");
                },
                onReset: () =>
                {
                    Console.WriteLine("熔断器重置");
                });
    }

    public async Task<List<FeishuDepartment>> GetDepartmentsAsync()
    {
        return await _retryPolicy.ExecuteAsync(() => 
            _circuitBreakerPolicy.ExecuteAsync(() => _innerService.GetDepartmentsAsync()));
    }

    // 其他方法类似实现...
}

五、进阶功能与.NET最佳实践

依赖注入与配置

使用选项模式管理配置,实现配置的强类型化和验证:

csharp 复制代码
public class FeishuSyncOptions
{
    public int PageSize { get; set; } = 50;
    public TimeSpan SyncInterval { get; set; } = TimeSpan.FromHours(24);
    public int MaxRetryAttempts { get; set; } = 3;
    public bool EnableEventSync { get; set; } = true;
}

// 在Program.cs中注册
builder.Services.Configure<FeishuSyncOptions>(builder.Configuration.GetSection("FeishuSync"));
builder.Services.AddScoped<IFeishuSyncService, FeishuSyncService>();

日志与监控

集成结构化日志和监控:

csharp 复制代码
public class FeishuSyncService : IFeishuSyncService
{
    private readonly ILogger<FeishuSyncService> _logger;
    private readonly IMetrics _metrics;

    public async Task FullSyncDepartmentsAsync()
    {
        using var activity = Activity.StartActivity("feishu_sync_departments_full");
        
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            _logger.LogInformation("开始全量同步部门,ActivityId: {ActivityId}", Activity.Current?.Id);
            
            // 同步逻辑...
            
            stopwatch.Stop();
            _metrics.Counter("feishu_sync_departments_success").Add(1);
            _metrics.Histogram("feishu_sync_departments_duration").Record(stopwatch.ElapsedMilliseconds);
            
            _logger.LogInformation("部门全量同步完成,耗时: {ElapsedMs}ms", stopwatch.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            _metrics.Counter("feishu_sync_departments_failed").Add(1);
            _logger.LogError(ex, "部门全量同步失败");
            throw;
        }
    }
}

实现飞书扫码登录(SSO)

基于已有的同步数据,实现飞书OAuth登录:

csharp 复制代码
public class FeishuAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private readonly IFeishuV3AuthenticationApi _authApi;
    private readonly IUserService _userService;

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.ContainsKey("Authorization"))
            return AuthenticateResult.Fail("Missing Authorization Header");

        try
        {
            var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
            
            // 获取用户信息
            var userInfo = await _authApi.GetUserInfoAsync(token);
            if (userInfo.Code != 0)
                return AuthenticateResult.Fail("Invalid token");

            // 在本地数据库中查找用户
            var user = await _userService.GetByFeishuUserIdAsync(userInfo.Data.UserId);
            if (user == null || !user.IsActive)
                return AuthenticateResult.Fail("User not found or inactive");

            // 创建身份票据
            var claims = new[]
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Name),
                new Claim(ClaimTypes.Email, user.Email ?? ""),
                new Claim("feishu_user_id", user.FeishuUserId)
            };

            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }
        catch (Exception ex)
        {
            return AuthenticateResult.Fail($"Authentication failed: {ex.Message}");
        }
    }
}

部署与运维

Docker化部署:

dockerfile 复制代码
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["OrganizationSync/OrganizationSync.csproj", "OrganizationSync/"]
RUN dotnet restore "OrganizationSync/OrganizationSync.csproj"
COPY . .
WORKDIR "/src/OrganizationSync"
RUN dotnet build "OrganizationSync.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "OrganizationSync.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OrganizationSync.dll"]

健康检查:

csharp 复制代码
public class FeishuSyncHealthCheck : IHealthCheck
{
    private readonly IFeishuApiService _feishuApiService;
    private readonly OrganizationDbContext _dbContext;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, 
        CancellationToken cancellationToken = default)
    {
        try
        {
            // 检查飞书API连接
            var departments = await _feishuApiService.GetDepartmentsAsync();
            
            // 检查数据库连接
            var userCount = await _dbContext.Users.CountAsync(cancellationToken);
            
            var data = new Dictionary<string, object>
            {
                { "department_count", departments.Count },
                { "local_user_count", userCount },
                { "last_sync", DateTime.UtcNow }
            };

            return HealthCheckResult.Healthy("飞书同步服务运行正常", data);
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("飞书同步服务异常", ex);
        }
    }
}

六、总结

通过本次实践,我们成功构建了一个基于.NET 8+和飞书API的企业级组织架构同步服务。整个解决方案采用了现代化的.NET技术栈:

  • Mud.Feishu SDK:提供了类型安全的飞书API调用,大幅简化了开发工作
  • BackgroundService:实现了可靠的后台定时同步
  • Entity Framework Core:提供了高效的数据持久化和关系映射
  • System.Text.Json:确保了高性能的JSON序列化
  • ASP.NET Core:构建了事件接收和SSO登录的Web API

后续改进方向

基于当前的同步基础,后续还可以进一步实现:

  • 飞书消息推送集成
  • 审批流程对接
  • 数据分析和报表
  • 多租户支持

这个实践充分展示了.NET生态系统在企业集成场景中的强大能力,通过合理的技术选型和架构设计,能够构建出高性能、高可靠性的企业级应用。


Mud.Feishu项目源码: 可以参考 Mud.Feishu 项目 获取更多详细的实现代码和最佳实践。

相关资源:

相关推荐
道一232 小时前
C# 读取文件方法介绍
开发语言·c#
Charles_go6 小时前
C#中级8、什么是缓存
开发语言·缓存·c#
用户83562907805117 小时前
如何在 C# 中自动化生成 PDF 表格
后端·c#
DolphinScheduler社区18 小时前
图解 Apache DolphinScheduler 如何配置飞书告警
java·大数据·开源·飞书·告警·任务调度·海豚调度
mudtools18 小时前
.NET如何快速集成飞书API的最佳实践
c#·.net·飞书
ThreePointsHeat19 小时前
Unity 关于打包WebGL + jslib录制RenderTexture画面
unity·c#·webgl
a***976820 小时前
如何使用C#与SQL Server数据库进行交互
数据库·c#·交互
乘乘凉21 小时前
C#中的值传递和引用传递
java·开发语言·c#