生态产品多个设计器现已发布候选版本

查看生态总览
跳转到内容

文件与存储 ​

文件能力分三块:文件库(元数据与多副本)、存储配置(后端在哪儿、怎么连)、导入导出中心(大批量数据的异步进出)。对应菜单目录 /file 与 /setting 下的存储配置页。

数据模型:元数据与存储位置分离 ​

这是理解整块能力的关键——一个文件(SysFile)可以有多个存储位置(SysFileStorage)。

text
SysFile(文件元数据,业务只认它)
  ├─ SysFileStorage  ← 主副本(IsPrimary = true):本地磁盘
  └─ SysFileStorage  ← 备副本:对象存储 / CDN

SysFile:文件是什么 ​

字段说明
FileName / OriginalName / FileExtension存储名 / 用户上传时的原名 / 扩展名
FileType / MimeType分类与 MIME
FileSize字节数(JSON 里是字符串,long 统一序列化为字符串)
FileHash / HashAlgorithm内容哈希——秒传探测与去重的依据
Width / Height / Duration图片尺寸 / 媒体时长
ThumbnailFileId缩略图指向另一条 SysFile
UploadIp / UploadSource上传来源审计

SysFileStorage:文件在哪儿 ​

字段说明
FileId归属文件
StorageType / StorageProvider / StorageConfigId后端类型、提供方、用的哪份存储配置
BucketName / StorageRegion / StoragePath / FullPath定位信息
InternalUrl / ExternalUrl / CdnUrl内网 / 外网 / CDN 访问地址
IsPrimary主副本标识,读取默认走它

为什么要拆两张表

迁移存储后端时(本地 → OSS)只需给同一个 SysFile 追加一条 SysFileStorage 并切换 IsPrimary,业务表里保存的 FileId 不用动。这也是下面「业务只存 fileId」这条约定成立的前提。

存储配置(/file/storage) ​

SysStorageConfig 是落库的存储后端配置,可以按租户隔离、运行期切换:

字段说明
ConfigCode / ConfigName配置编码与名称
StorageTypeLocal(0) / S3(1) / OSS(2) / COS(3) / MinIO(4)
Endpoint / Region / BucketName连接信息
AccessKeyId / SecretAccessKey凭据(加密存储)
IsDefault默认配置
IsEnabled / Sort / Remark常规

本地存储另有一份基础设施级配置在 appsettings 的 XiHan:ObjectStorage:Local(RootPath 默认 wwwroot/uploads、UrlPrefix 默认 /uploads)。

本地存储的 URL 是根相对路径

本地存储返回 /uploads/... 这样的根相对路径,静态文件中间件挂在鉴权之前所以能匿名直链。

前后端同源时直接可用;线上不同源时必须拼上 VITE_API_BASE_URL 的 origin,否则浏览器会拿前端域名去请求而 404。仓库里的 toAbsoluteFileUrl / useAvatarUrl 已经处理,自己拼 URL 时别忘了。

文件管理(/file/library) ​

上传、查看、下载、删除文件。几条约定:

业务表存 fileId,不存 URL

把 URL 写进业务表会在两件事上出问题:换存储后端时全表 URL 失效;带签名的私有对象 URL 会过期。

正确做法是业务表只存 FileId,读取时由服务端解析出当前有效的访问地址。

  • 秒传:上传前先按内容哈希探测,命中就直接复用已有 SysFile,不重复传输与落盘。注意探测未命中时响应的 data 字段会被 WhenWritingNull 整个省略——客户端要按 isSuccess 判定,不要判断 data 是否存在。
  • 上传走 multipart/form-data:前端不要手工设 Content-Type,让浏览器自己带 boundary(RequestClient 检测到 FormData 会主动删掉该头)。
  • 缩略图是独立的 SysFile,通过 ThumbnailFileId 关联,不是同一条记录的附属字段。

导出中心(/file/export-center) ​

大数据量导出不能占请求线程,走异步任务 + 队列消费:

text
页面点「导出」
   │ 提交 SysExportTask(Status=Pending,落查询快照与字段快照)
   │ 事务提交后入 IRedisDelayQueue<ExportTaskMessage>
   ▼
ExportTaskHostedService 拉取 → 执行导出 → 写文件 → 回写 FileId/进度/终态
   ▼
页面轮询任务状态 → 完成后下载

SysExportTask 的关键字段:

字段说明
BusinessType业务类型,取导出按钮所属的页面码(PageRegistry,如 log.access、identity.user),决定用哪个导出 Provider
Scope导出范围(当前搜索结果 / 全量等)
Format导出格式(CSV 等)
Status / Progress / TotalCount / ProcessedCount进度反馈
QuerySnapshot提交时的查询条件快照
FieldsSnapshot提交时的字段与列设置快照
FileId / FileName / FileSize产物
ErrorMessage / StartedTime / FinishedTime执行结果

为什么要存两份快照

导出是异步的——用户提交后可能立刻改了搜索条件、调了列设置甚至关掉页面。存快照保证导出的内容与点击那一刻看到的一致,而不是与执行那一刻的界面状态一致。

这也意味着:修复导出内容的 bug 要看快照里存了什么,而不是看现在页面上是什么。

查询快照解析不了时任务直接失败,原因写「查询条件无法解析」,不会落回空查询——空查询等于不加筛选,导出范围会悄悄变成全量。

前端 Schema 页只要在 PageSchema.resource.export 里声明 { businessType, buildQuery },导出按钮就会走这条异步链路;未声明时退化为本地 CSV 导出。导出按钮的显隐由 exportPermission 精准门控,未声明该字段则该页不显示导出。

崩溃恢复 ​

后台服务启动时会复位崩溃残留的「执行中」任务并重投,否则一次异常重启会让一批任务永久卡在中间态。新增类似链路时别漏这一步,见 缓存与异步。

导入 ​

SysImportHistory 记录导入批次。前端 Schema 页内置 SchemaImportDialog:下载模板 → 选文件 → CSV 解析 → 预校验 → 批量创建。

模板字段由页面的 ListFieldSchema 中 importable 为真的字段派生——和表格、导出共用同一份字段声明,不需要单独维护模板。

相关页面 ​

Released under The MIT License