存储那么贵,何不白嫖飞书云文件空间

服务器硬盘又满了

下班前,监控系统给我发了条告警:NAS 存储空间不足 10%

打开后台一看,500G 的硬盘塞得满满当当,全是这两年积累的项目文档、设计稿、测试数据。清理了一波,也就腾出 20G,治标不治本。

买新硬盘?现在存储价格涨到天际,一块 2T 的企业级硬盘要 1000+。上云存储?阿里云 OSS 标准存储 500G 一年要 480 元,加上流量费、请求费,一年下来得好几百。

正发愁呢,同事发来一条飞书消息,附带一个云文档链接。我点开一看,突然意识到一个问题:

飞书企业版,每个用户有 100G 云空间,完全免费!

我们公司 50 个人,那就是 5T 的免费存储空间!

一个大胆的想法冒了出来:能不能把飞书云盘当成免费网盘用?


飞书云存储的"白嫖"姿势

先搞清楚飞书云盘的规则

在动手之前,先把飞书云盘的"家底"摸清楚:

项目 免费额度 说明
人均存储空间 100G 企业版标准配置
单文件大小 500MB 超过需要分片上传
文件类型限制 无明确限制 文档、图片、视频都行
API 调用频率 10次/秒 超了会返回 429

⚠️ 注意:以上是企业版(标准版)的配置,具体以你企业的实际套餐为准。

这个系统能帮你省多少钱

光说不练假把式,来算笔账:
graph LR A[500G 文件存储需求] --> B[自建NAS] A --> C[阿里云OSS] A --> D[飞书云盘] B --> B1[硬盘: 800元] B --> B2[电费: 200元/年] B --> B3[维护: 人力成本] C --> C1[存储费: 480元/年] C --> C2[流量费: 约200元/年] C --> C3[请求费: 约50元/年] D --> D1[💰 0元] style D1 fill:#90EE90 style B1 fill:#FFB6C1 style C1 fill:#FFB6C1

成本对比表

方案 首年成本 次年成本 优点 缺点
自建 NAS 800+ 元 200 元 完全掌控、速度快 硬件故障风险、需要维护
阿里云 OSS 730 元 730 元 稳定可靠、CDN加速 持续付费、流量费贵
腾讯云 COS 680 元 680 元 同上 同上
飞书云盘 0 元 0 元 免费、有协作功能 单文件500MB限制

一年省下 700+,这钱拿来喝奶茶不香吗?

核心功能一览

当然,直接用飞书 App 上传文件不是我们的目标。我们要做的是一个独立的文件管理系统,把飞书云盘当底层存储:
graph TB subgraph 用户界面 A1[文件上传] A2[文件下载] A3[文件夹管理] A4[分享链接] end subgraph 核心功能 B1[大文件分片上传] B2[飞书云盘同步] B3[版本管理] B4[权限隔离] end subgraph 底层存储 C1[飞书云盘 API] C2[本地 SQLite] end A1 --> B1 --> C1 A2 --> C1 A3 --> B2 --> C1 A4 --> B4 --> C2 B3 --> C2

先上成果:白嫖成功了!

口说无凭,先看看做出来的效果:

📸 系统登陆界面截图
📸 系统主界面截图

系统已经跑起来了,下面我就一步步讲怎么实现的。


上传:把文件"搬"到飞书云盘

普通上传 vs 分片上传

飞书对单文件有 500MB 的限制,但我们实际使用中经常遇到几百兆的设计稿、视频素材。怎么破?分片上传
sequenceDiagram participant 用户 participant 前端 participant 后端 participant 飞书API 用户->>前端: 选择文件(200MB) 前端->>前端: 切片(每片5MB) 前端->>后端: 初始化分片上传 后端->>飞书API: 申请upload_id 飞书API-->>后端: 返回upload_id loop 逐片上传 前端->>后端: 上传分片(seq=1,2,3...) 后端->>飞书API: 上传分片到云存储 end 前端->>后端: 完成上传 后端->>飞书API: 合并分片 飞书API-->>后端: 返回file_token 后端-->>前端: 上传成功

前端分片核心代码

切片逻辑很简单,用 JavaScript 的 Blob.slice() 方法:

javascript 复制代码
// ChunkUploader.vue - 核心切片逻辑
const CHUNK_SIZE = 5 * 1024 * 1024; // 每片 5MB

async function uploadFile(file) {
  // 1. 计算文件MD5(用于秒传和校验)
  const fileHash = await calculateMD5(file);
  
  // 2. 切片
  const chunks = [];
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
  
  for (let i = 0; i < totalChunks; i++) {
    const start = i * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, file.size);
    chunks.push({
      index: i,
      blob: file.slice(start, end),
      size: end - start
    });
  }
  
  // 3. 初始化分片上传
  const { uploadId } = await api.initChunkUpload({
    fileName: file.name,
    fileSize: file.size,
    fileHash,
    totalChunks
  });
  
  // 4. 逐片上传(支持并发)
  for (const chunk of chunks) {
    await api.uploadChunk(uploadId, chunk.index, chunk.blob);
    updateProgress(chunk.index / totalChunks * 100);
  }
  
  // 5. 完成上传
  await api.completeChunkUpload(uploadId);
}

后端合并逻辑

后端收到分片后,调用飞书 API 完成最终合并:

csharp 复制代码
// ChunkUploadService.cs - 分片合并
public async Task<CompleteResult> CompleteUploadAsync(string uploadId)
{
    // 1. 获取所有分片信息
    var chunks = await _dbContext.ChunkUploadRecords
        .Where(c => c.UploadId == uploadId)
        .OrderBy(c => c.ChunkIndex)
        .ToListAsync();
    
    // 2. 校验分片完整性
    if (chunks.Count != chunks.First().TotalChunks)
    {
        throw new InvalidOperationException("分片不完整");
    }
    
    // 3. 调用飞书API合并分片
    var response = await _driveFiles.UploadCompleteAsync(new UploadCompleteRequest
    {
        UploadId = uploadId,
        BlockNum = chunks.Count,
        BlockSize = CHUNK_SIZE
    });
    
    // 4. 创建文件记录
    var fileRecord = new FileRecord
    {
        FileToken = response.Data.FileToken,
        FileName = chunks.First().FileName,
        FileSize = chunks.Sum(c => c.ChunkSize),
        UploadTime = DateTime.UtcNow
    };
    
    _dbContext.FileRecords.Add(fileRecord);
    
    // 5. 清理临时分片记录
    _dbContext.ChunkUploadRecords.RemoveRange(chunks);
    await _dbContext.SaveChangesAsync();
    
    return new CompleteResult { Success = true, FileToken = fileRecord.FileToken };
}

一个隐藏坑:分片过期时间

开发过程中踩了个坑:飞书的分片上传有 7 天有效期

📌 场景:用户上传一个 300MB 文件,传到一半断网了,过了 8 天再来续传,结果报错"分片已过期"。

解决方案 :前端记录上传进度到 localStorage,每次打开页面检查是否有未完成的上传任务:

javascript 复制代码
// 检查断点续传
function checkPendingUploads() {
  const pending = localStorage.getItem('pending_uploads');
  if (pending) {
    const uploads = JSON.parse(pending);
    // 过滤掉超过6天的(留1天余量)
    const valid = uploads.filter(u => 
      Date.now() - u.startTime < 6 * 24 * 60 * 60 * 1000
    );
    return valid;
  }
  return [];
}

同步:让飞书云盘变成你的"第二硬盘"

为什么需要同步

用户可能会问:我直接在飞书 App 里上传文件不行吗?

当然可以,但有几个问题:

  1. 飞书 App 上传的文件,我们系统里看不到 ------ 因为我们维护了一份本地数据库记录
  2. 团队成员在飞书里改了文档,本地感知不到 ------ 没有实时通知机制
  3. 需要统一管理界面 ------ 把飞书云盘和其他来源的文件放在一起

所以需要一个同步机制,把飞书云盘的数据"拉"到我们的系统里。

同步策略:增量 + 递归

flowchart TD A[开始同步] --> B{是否首次同步?} B -->|是| C[全量同步] B -->|否| D[增量同步] C --> E[遍历根目录] E --> F{是文件夹?} F -->|是| G[递归遍历子目录] F -->|否| H[保存文件记录] G --> F D --> I[获取本地最后同步时间] I --> J[只查询该时间后修改的文件] J --> K[更新本地记录] H --> L[同步完成] K --> L

核心同步逻辑

csharp 复制代码
// FeishuSyncService.cs - 递归同步文件夹
private async Task<List<FolderRecord>> SyncFolderRecursiveAsync(
    string? folderToken, 
    int userId, 
    SyncResult result, 
    CancellationToken cancellationToken)
{
    var folders = new List<FolderRecord>();
    var allFiles = new List<FileInfo>();
    string? pageToken = null;

    // 分页获取文件夹内容
    do
    {
        var response = await _driveFolder.GetFilesPageListAsync(
            folderToken,
            page_token: pageToken,
            cancellationToken: cancellationToken);

        if (response?.Data?.Files == null || response.Data.Files.Length == 0)
            break;

        foreach (var file in response.Data.Files)
        {
            if (file.Type == "folder")
            {
                // 递归同步子文件夹
                var folder = await SyncFolderRecordAsync(file, folderToken, userId, result);
                if (folder != null)
                {
                    folders.Add(folder);
                    result.SyncedFolders++;
                    // 🔁 递归调用
                    await SyncFolderRecursiveAsync(file.Token, userId, result, cancellationToken);
                }
            }
            else
            {
                allFiles.Add(file);
            }
        }

        pageToken = response.Data.NextPageToken;
    }
    while (!string.IsNullOrEmpty(pageToken));

    // 批量同步文件
    if (allFiles.Count > 0)
    {
        await SyncFileRecordsWithMetadataAsync(allFiles, folderToken, userId, result, cancellationToken);
        result.SyncedFiles += allFiles.Count;
    }

    return folders;
}

同步性能优化

同步大量文件时,性能是个问题。我们做了几项优化:

优化项 问题 解决方案 效果
批量查询 逐个查询元数据太慢 一次查询 50 个文件的元数据 速度提升 50 倍
并发控制 多线程并发触发限流 限制同时 3 个同步任务 避免报错 429
本地缓存 每次同步都重新查询 SQLite 缓存文件记录 减少 API 调用

批量元数据查询代码

csharp 复制代码
// 批量查询文件元数据(一次最多50个)
private async Task SyncFileRecordsWithMetadataAsync(
    List<FileInfo> files, string? folderToken, int userId, SyncResult result)
{
    const int batchSize = 50; // 飞书API限制
    
    for (int i = 0; i < files.Count; i += batchSize)
    {
        var batch = files.Skip(i).Take(batchSize).ToList();
        
        // 构建批量查询请求
        var requestDocs = batch
            .Where(f => !string.IsNullOrEmpty(f.Token))
            .Select(f => new RequestDoc
            {
                DocToken = f.Token!,
                DocType = MapFileType(f.Type)
            })
            .ToArray();

        // 调用飞书批量元数据接口
        var metasResponse = await _driveFiles.BatchQueryMetasAsync(
            new MetasBatchQueryRequest { RequestDocs = requestDocs });

        // 构建元数据字典
        var metaDict = new Dictionary<string, FileMetaInfo>();
        if (metasResponse?.Data?.Metas != null)
        {
            foreach (var meta in metasResponse.Data.Metas)
            {
                if (!string.IsNullOrEmpty(meta.DocToken))
                    metaDict[meta.DocToken] = meta;
            }
        }

        // 逐个同步文件记录
        foreach (var file in batch)
        {
            metaDict.TryGetValue(file.Token!, out var metaInfo);
            await SyncFileRecordAsync(file, folderToken, userId, result, metaInfo);
        }
    }
}

📸 文件同步运行效果
同步面板的运行截图,展示了同步进度、文件数量统计等信息_


分享:让文件流转起来

内部分享 vs 外部分享

文件分享有两种场景:

场景 对象 方式 特点
内部分享 组织架构内成员 飞书权限继承 无需额外配置,权限自动同步
外部分享 组织外用户 分享链接 + 密码 支持过期时间、访问次数限制

分享链接的实现

flowchart LR A[选择文件] --> B[点击分享] B --> C{分享类型} C -->|内部| D[选择成员/部门] C -->|外部| E[生成分享链接] D --> F[设置权限:可读/可编辑] E --> G[设置密码+过期时间] F --> H[发送飞书消息] G --> I[复制链接] H --> J[完成] I --> J

短链接生成算法

csharp 复制代码
// ShareService.cs - 生成短链接
public string GenerateShareCode()
{
    // 使用 Base62 编码,生成 6 位短码
    const string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    var random = new Random();
    var code = new char[6];
    
    for (int i = 0; i < 6; i++)
    {
        code[i] = chars[random.Next(chars.Length)];
    }
    
    return new string(code);
}

// 校验分享链接
public async Task<ShareValidationResult> ValidateShareAsync(string code, string? password)
{
    var share = await _dbContext.ShareRecords
        .FirstOrDefaultAsync(s => s.ShareCode == code);
    
    if (share == null)
        return ShareValidationResult.Fail("分享链接不存在");
    
    if (share.ExpireTime < DateTime.UtcNow)
        return ShareValidationResult.Fail("分享链接已过期");
    
    if (!string.IsNullOrEmpty(share.Password) && share.Password != password)
        return ShareValidationResult.Fail("密码错误");
    
    if (share.MaxAccessCount > 0 && share.AccessCount >= share.MaxAccessCount)
        return ShareValidationResult.Fail("访问次数已达上限");
    
    // 更新访问次数
    share.AccessCount++;
    await _dbContext.SaveChangesAsync();
    
    return ShareValidationResult.Success(share);
}

访问统计

每次访问分享链接,我们记录以下信息:

csharp 复制代码
// 记录访问日志
public class ShareAccessLog
{
    public int Id { get; set; }
    public int ShareId { get; set; }
    public string? IpAddress { get; set; }
    public string? UserAgent { get; set; }
    public string? Referer { get; set; }
    public DateTime AccessTime { get; set; }
    public string? Action { get; set; } // view, download
}

版本管理:文件改错了也不怕

版本号设计

上传同名文件时,我们不会直接覆盖,而是创建新版本:
gitGraph commit id: "v1 - 初稿" commit id: "v2 - 修改第一版" commit id: "v3 - 修改第二版" commit id: "v4 - 最终版" tag: "current"

版本表设计

sql 复制代码
CREATE TABLE FileVersions (
    Id INTEGER PRIMARY KEY,
    FileToken TEXT NOT NULL,
    Version INTEGER NOT NULL,
    StoragePath TEXT NOT NULL,      -- 本地存储路径
    FileSize INTEGER NOT NULL,
    UploadTime TEXT NOT NULL,
    Remark TEXT,
    UNIQUE(FileToken, Version)
);

版本存储策略

这里有个问题:版本文件存在哪里?

存储位置 优点 缺点
飞书云盘 统一管理 占用飞书空间额度
本地存储 不占飞书空间 需要额外备份

我们选择本地存储,原因很简单:飞书的免费空间有限,存历史版本太浪费了。

csharp 复制代码
// VersionService.cs - 创建新版本
public async Task<FileVersion> CreateVersionAsync(string fileToken, IFormFile file)
{
    // 1. 获取当前最大版本号
    var maxVersion = await _dbContext.FileVersions
        .Where(v => v.FileToken == fileToken)
        .MaxAsync(v => (int?)v.Version) ?? 0;
    
    // 2. 保存新版本文件到本地
    var storagePath = $"versions/{fileToken}_v{maxVersion + 1}";
    using var stream = File.Create(storagePath);
    await file.CopyToAsync(stream);
    
    // 3. 创建版本记录
    var version = new FileVersion
    {
        FileToken = fileToken,
        Version = maxVersion + 1,
        StoragePath = storagePath,
        FileSize = file.Length,
        UploadTime = DateTime.UtcNow
    };
    
    _dbContext.FileVersions.Add(version);
    await _dbContext.SaveChangesAsync();
    
    return version;
}

// 版本回滚
public async Task RestoreVersionAsync(string fileToken, int version)
{
    var targetVersion = await _dbContext.FileVersions
        .FirstOrDefaultAsync(v => v.FileToken == fileToken && v.Version == version)
        ?? throw new NotFoundException("版本不存在");
    
    // 将目标版本文件上传到飞书,覆盖当前版本
    var fileBytes = await File.ReadAllBytesAsync(targetVersion.StoragePath);
    await _driveFiles.UploadAsync(fileToken, fileBytes);
}

版本清理策略

历史版本也不能无限保留,我们设置了清理规则:

  • 保留最近 10 个版本
  • 清理超过 30 天的历史版本
csharp 复制代码
// 定时清理任务(每天凌晨执行)
public async Task CleanupOldVersionsAsync()
{
    var cutoffDate = DateTime.UtcNow.AddDays(-30);
    
    var oldVersions = await _dbContext.FileVersions
        .Where(v => v.UploadTime < cutoffDate)
        .ToListAsync();
    
    foreach (var version in oldVersions)
    {
        // 删除本地文件
        if (File.Exists(version.StoragePath))
            File.Delete(version.StoragePath);
    }
    
    _dbContext.FileVersions.RemoveRange(oldVersions);
    await _dbContext.SaveChangesAsync();
}

权限体系:自己的文件自己管

用户隔离设计

多用户环境下,数据隔离是基本要求。我们采用简单的用户级隔离:
erDiagram USER ||--o{ FILE_RECORD : owns USER ||--o{ FOLDER_RECORD : owns USER ||--o{ SHARE_RECORD : creates USER ||--o{ OPERATION_LOG : generates USER { int Id PK string Username string PasswordHash string Role } FILE_RECORD { int Id PK int UserId FK string FileToken string FileName string FolderToken bool IsDeleted } FOLDER_RECORD { int Id PK int UserId FK string FolderToken string FolderName string ParentFolderToken }

查询自动过滤用户

csharp 复制代码
// 在 DbContext 中重写 SaveChanges,自动注入 UserId
public override int SaveChanges()
{
    var entries = ChangeTracker.Entries()
        .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
    
    foreach (var entry in entries)
    {
        if (entry.Entity is IHasUser entity && entry.State == EntityState.Added)
        {
            entity.UserId = _currentUserService.UserId;
        }
    }
    
    return base.SaveChanges();
}

JWT 认证流程

sequenceDiagram participant 用户 participant 前端 participant 后端 participant 数据库 用户->>前端: 输入用户名密码 前端->>后端: POST /api/auth/login 后端->>数据库: 验证用户 数据库-->>后端: 用户信息 后端->>后端: 生成 JWT Token 后端-->>前端: { token, refreshToken } 前端->>前端: 存储 token 到 localStorage Note over 前端: 后续请求带上 token 前端->>后端: GET /api/files<br/>Authorization: Bearer {token} 后端->>后端: 验证 token 后端-->>前端: 文件列表 Note over 前端: Token 过期时自动刷新 前端->>后端: POST /api/auth/refresh<br/>{ refreshToken } 后端-->>前端: { newToken, newRefreshToken }

Token 生成与验证

csharp 复制代码
// AuthService.cs
public string GenerateToken(User user)
{
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JwtSettings:SecretKey"]!));
    var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    
    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
        new Claim(ClaimTypes.Name, user.Username),
        new Claim(ClaimTypes.Role, user.Role)
    };
    
    var token = new JwtSecurityToken(
        issuer: _config["JwtSettings:Issuer"],
        audience: _config["JwtSettings:Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddHours(1),
        signingCredentials: credentials
    );
    
    return new JwtSecurityTokenHandler().WriteToken(token);
}

操作日志审计

所有关键操作都记录日志:

csharp 复制代码
// OperationLogService.cs
public enum OperationType
{
    Login,
    Upload,
    Download,
    Delete,
    Share,
    Sync
}

public async Task LogAsync(int userId, OperationType type, string target, string? detail = null)
{
    var log = new OperationLog
    {
        UserId = userId,
        OperationType = type.ToString(),
        Target = target,
        Detail = detail,
        OperationTime = DateTime.UtcNow,
        IpAddress = _httpContext.Connection.RemoteIpAddress?.ToString()
    };
    
    _dbContext.OperationLogs.Add(log);
    await _dbContext.SaveChangesAsync();
}

部署:从零开始搭一套

创建飞书应用

第一步,去飞书开放平台创建应用:

  1. 打开 飞书开放平台
  2. 点击"开发者后台" → "创建企业自建应用"
  3. 填写应用名称、描述
  4. 进入应用详情,记录 App IDApp Secret

配置权限(重要!):

权限名称 权限点 用途
云空间 drive:drive 访问云空间
文件 drive:file 文件操作
文件夹 drive:folder 文件夹操作
文档 docs:doc 文档操作

⚠️ 坑点:权限配置后需要发布应用才能生效,发布后还要管理员审批。

后端部署(.NET 8)

bash 复制代码
# 1. 克隆项目
git clone https://gitee.com/mudtools/MudFeishu.git
cd Demos/FeishuFileServer/backend

# 2. 创建本地配置文件
# 注意:appsettings.local.json 不要提交到 git
cat > appsettings.local.json << 'EOF'
{
  "FeishuApps": {
    "Default": {
      "AppId": "cli_xxxxxxxxxxxxx",
      "AppSecret": "xxxxxxxxxxxxxxxxxxxxxxxx",
      "IsDefault": true
    }
  },
  "JwtSettings": {
    "SecretKey": "your-secret-key-at-least-32-characters-long-for-security"
  }
}
EOF

# 3. 安装依赖并运行
dotnet restore
dotnet run

# 后端服务将在 http://localhost:5000 启动
# Swagger 文档:http://localhost:5000/swagger

前端部署(Vue 3)

bash 复制代码
# 1. 进入前端目录
cd ../frontend

# 2. 配置后端地址
cat > .env.development << 'EOF'
VITE_API_BASE_URL=http://localhost:5000
EOF

# 3. 安装依赖
npm install
# 或使用 pnpm(更快)
# pnpm install

# 4. 启动开发服务器
npm run dev

# 前端服务将在 http://localhost:3000 启动

生产环境部署建议

Docker 部署(推荐):

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

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "FeishuFileServer.dll"]
yaml 复制代码
# docker-compose.yml
version: '3.8'
services:
  backend:
    build: ./backend
    ports:
      - "5000:5000"
    volumes:
      - ./data:/app/data
      - ./uploads:/app/uploads
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
  
  frontend:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./frontend/dist:/usr/share/nginx/html
      - ./nginx.conf:/etc/nginx/nginx.conf

Nginx 反向代理配置

nginx 复制代码
server {
    listen 80;
    server_name your-domain.com;
    
    # 前端静态资源
    location / {
        root /var/www/frontend/dist;
        try_files $uri $uri/ /index.html;
    }
    
    # 后端 API
    location /api {
        proxy_pass http://localhost:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # 文件上传大小限制
    client_max_body_size 500M;
}

踩坑记录:那些官方文档没告诉你的

开发过程中踩了不少坑,这里记录下来,希望能帮你少走弯路。

API 限流坑

现象 :同步到一半突然报错 429 Too Many Requests

原因:飞书 API 有调用频率限制,每个应用每秒最多 10 次请求。

解决

csharp 复制代码
// 使用信号量控制并发
private static readonly SemaphoreSlim _apiLimiter = new(10, 10);

public async Task<T> CallApiAsync<T>(Func<Task<T>> apiCall)
{
    await _apiLimiter.WaitAsync();
    try
    {
        return await apiCall();
    }
    finally
    {
        _apiLimiter.Release();
    }
}

文件类型坑

现象 :上传 .docx 文件后,在飞书 App 里打开报错

原因 :飞书对不同文件类型有不同的处理方式,需要明确指定 doc_type

解决

csharp 复制代码
// 根据文件扩展名映射 doc_type
private string MapFileType(string extension)
{
    return extension.ToLower() switch
    {
        ".docx" => "docx",
        ".xlsx" => "xlsx",
        ".pptx" => "pptx",
        ".pdf" => "pdf",
        _ => "file"  // 通用文件类型
    };
}

权限坑

现象:上传成功,但获取文件列表返回空

原因 :应用权限配置不完整,缺少 drive:drive:readonly 权限

解决:去开放平台检查权限配置,确保以下权限都已开启:

  • drive:drive
  • drive:drive:readonly
  • drive:file
  • drive:file:readonly

9.4 分片上传坑

现象:大文件上传失败,报"分片已过期"

原因:飞书分片上传有效期 7 天,中断后超时

解决:前端记录上传进度,支持断点续传(详见第 3.4 节)


后续玩法:还能怎么"薅"

这个系统还有很大的扩展空间:

功能 实现思路 收益
文件在线预览 调用飞书预览 API 不用自己实现 Office 渲染
文档协同编辑 接入飞书文档 API 免费在线协作功能
定时同步任务 每天凌晨自动同步 数据始终最新
多租户支持 支持多企业配置 给其他公司用

在线预览示例

javascript 复制代码
// 获取飞书预览链接
async function getPreviewUrl(fileToken) {
  const response = await api.get(`/files/${fileToken}/preview-url`);
  // 在 iframe 中打开预览
  window.open(response.data.previewUrl, '_blank');
}
相关推荐
K姐研究社7 小时前
阿里JVS Claw实测 – 手机一键部署 OpenClaw,开箱即用
人工智能·智能手机·aigc·飞书
Snasph7 小时前
Openclaw 插件突然要跑特定端口,导致无法连接的问题处理
飞书·qq·openclaw
夏树同学10 小时前
Newtonsoft技巧/与System.Text.Json的对比
.net
竹之却11 小时前
OpenClaw 接入QQ-Bot + 接入Feishu(飞书)
运维·服务器·飞书·openclaw·qq-bot·opencalw接入qq+飞书
脑电信号要分类12 小时前
将多张图片拼接成一个pdf文件输出
pdf·c#·apache
一马平川的大草原12 小时前
OpenClaw实现多agent协作-针对飞书插件
飞书·多agent·openclaw
njsgcs12 小时前
c# solidworks 折弯系数检查
开发语言·c#
格林威14 小时前
工业相机图像采集:Grab Timeout 设置建议——拒绝“假死”与“丢帧”的黄金法则
开发语言·人工智能·数码相机·计算机视觉·c#·机器视觉·工业相机
唐青枫14 小时前
C#.NET SignalR + Redis Backplane 深入解析:多节点部署与跨实例消息同步
c#·.net