一键导入
file-upload
Wavelet 项目专用:当业务需要上传文件、读取已上传文件、在 Worker/任务中程序化摄取字节流、选择存储引擎能力、或排查 w_uploads / 文件统计异常时必须使用。本技能指导 storage 与 upload 分层、upload.Ingest 策略选型、前后端接入与禁止旁路写表。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Wavelet 项目专用:当业务需要上传文件、读取已上传文件、在 Worker/任务中程序化摄取字节流、选择存储引擎能力、或排查 w_uploads / 文件统计异常时必须使用。本技能指导 storage 与 upload 分层、upload.Ingest 策略选型、前后端接入与禁止旁路写表。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Wavelet 项目专用:当新增或修改业务缓存(RAM/Redis/DB 三层读路径)、缓存失效、多节点 pub/sub 同步、或评估高频读是否应接入缓存时必须使用。本技能说明系统标准缓存框架、参考实现、禁止写法与分布式一致性要求。
Wavelet 项目专用:当新增或修改 ClickHouse 批量写入、接入 internal/db/batchwriter、将业务域异步 flush 到分析表、迁移 risk_control/节点访问日志/可观测时序写入、或评估 async_insert 与背压策略时必须使用。本技能指导分层职责、各域独立 Writer 实例、repository 批量 API 与禁止写法。
Wavelet 项目专用:当新增或修改数据库表结构、索引、初始化数据、系统配置 seed、模板 seed、默认管理员、goose SQL 迁移、internal/db/migrator、ClickHouse 分析库 DDL 或数据库升级流程时必须使用。本技能指导在 internal/db/migrator/goose 下编写 PostgreSQL/SQLite 双方言 SQL 迁移,以及在 goose/clickhouse 下编写 ClickHouse 单方言分析表迁移,并完成验证。
Provides comprehensive code review guidance for React 19, Vue 3, Angular 17+, Svelte 5, Rust, TypeScript, Java, PHP, Python, Django, Go, C#/.NET, Kotlin, Swift, NestJS, C/C++, and more. Helps catch bugs, improve code quality, and give constructive feedback. Use when: reviewing pull requests, conducting PR reviews, code review, reviewing code changes, establishing review standards, mentoring developers, architecture reviews, security audits, checking code quality, finding bugs, giving feedback on code.
Use when reviewing Go code or checking code against community style standards. Also use proactively before submitting a Go PR or when reviewing any Go code changes, even if the user doesn't explicitly request a style review. Does not cover language-specific syntax — delegates to specialized skills.
Use when writing concurrent Go code — goroutines, channels, mutexes, or thread-safety guarantees. Also use when parallelizing work, fixing data races, or protecting shared state, even if the user doesn't explicitly mention concurrency primitives. Does not cover context.Context patterns (see go-context).
| name | file-upload |
| description | Wavelet 项目专用:当业务需要上传文件、读取已上传文件、在 Worker/任务中程序化摄取字节流、选择存储引擎能力、或排查 w_uploads / 文件统计异常时必须使用。本技能指导 storage 与 upload 分层、upload.Ingest 策略选型、前后端接入与禁止旁路写表。 |
本技能是 Wavelet 文件上传与对象存储的唯一开发指导。开始开发前先阅读仓库根目录 AGENTS.md,遵守项目级核心规则。
Wavelet 将「对象存储」与「上传业务」分为两层,禁止混用职责:
| 层级 | 包路径 | 职责 | 业务是否直接调用 |
|---|---|---|---|
| 对象存储引擎 | internal/storage | Backend 接口:Put / Get / Delete / Test;按配置切换 Local / S3 / R2 / OSS / WebDAV | 禁止(仅 upload 域内部使用) |
| 上传域服务 | internal/apps/upload | w_uploads 记录、权限、秒传、统计、文件服务、upload.Ingest | 必须 |
| 上传 HTTP 入口 | internal/apps/upload/handler | POST /api/v1/upload 等 multipart 接口 | 前端 / 用户侧上传 |
| 文件访问 | internal/apps/upload/filesrv | GET /f/:id 流式响应、访问控制、图片 WebP 压缩 | 展示 / 下载 |
业务模块 ──► upload.Ingest / upload.Remove(唯一写入门禁)
├── storage.Backend.Put/Get/Delete
├── repository.CreateUpload(仅 upload 内部)
└── RecordUploadStatsAdd/Remove(ingest 内置,禁止业务直调)
以下写法一律禁止:
// ❌ 业务包直接写 blob
storage.Active(ctx); backend.Put(...)
// ❌ 旁路写 w_uploads
db.DB(ctx).Create(&model.Upload{})
repository.CreateUpload(ctx, upload) // 仅 internal/apps/upload 允许
// ❌ 手动维护统计
upload.ApplyUploadStatsAdd(ctx, upload) // 已 Deprecated
// ❌ 业务表存物理路径
invoice.FilePath = "uploads/2026/01/02/123.pdf"
正确做法:业务表只存 upload_id(uint64 / JSON string),通过 /f/{id} 或 upload.OpenStoredObject 访问。
根据场景选择 upload.Ingest 的 Policy:
| 场景 | Policy | 哈希命中时 | 未命中时 | 典型调用方 |
|---|---|---|---|---|
| 用户 HTTP 上传(含秒传) | PolicyDedupNewRecord | 复用 path,新建记录 + 统计 | 写 blob + 新建记录 + 统计 | handler.UploadFile(已内置) |
| Worker 生成全新文件 | PolicyCreate | 不查重,始终写 blob + 记录 | 同左 | 报表导出、定时生成 |
| 镜像 / 去重摄取(Pixez) | PolicyResolveExisting | 直接返回已有记录,不建新记录、不加统计 | 写 blob + 新建记录 + 统计 | 异步镜像任务 |
| 业务只需引用已有文件 | 不调 Ingest | — | — | 业务 API 校验 upload_id 即可 |
| 字段 | 含义 |
|---|---|
Created | 是否新建了 w_uploads 记录 |
Stored | 是否写入了新 blob |
Resolved | 是否通过哈希解析到已有记录(仅 PolicyResolveExisting) |
在 logics.go(接受 context.Context,不依赖 *gin.Context)中调用:
import (
"bytes"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/Rain-kl/Wavelet/internal/model"
)
func ingestMirrorFile(ctx context.Context, userID uint64, data []byte, hash, filename, mime, ext string) (model.Upload, error) {
accessMode := 1
result, err := upload.Ingest(ctx, upload.IngestRequest{
UserID: userID,
Reader: bytes.NewReader(data),
Size: int64(len(data)),
FileName: filename,
MimeType: mime,
Extension: ext,
Hash: hash, // 必填:SHA-256 hex
Type: "your_biz_type",
AccessMode: &accessMode,
Metadata: model.UploadMetadata{
Extra: map[string]any{"source": "worker"},
},
Policy: upload.PolicyResolveExisting,
})
if err != nil {
return model.Upload{}, err
}
return result.Upload, nil
}
| 字段 | 说明 |
|---|---|
Hash | 必填,推荐 SHA-256 hex;用于秒传 / 镜像去重 |
Type | 业务分类(如 avatar、invoice、pixez_mirror),用于筛选与统计 |
AccessMode | nil 时按 type 默认:avatar → 公开(1),其余 → 私有(0) |
SkipExtensionCheck | Worker 场景若已自行校验扩展名,可设为 true |
ObjectKeyFn | 可选自定义存储路径;默认 uploads/YYYY/MM/DD/{id}.{ext} |
| 错误 | 含义 | Handler 映射建议 |
|---|---|---|
upload.ErrIngestStorageReadOnly | 存储迁移维护中 | response.AbortConflict |
ingest.ErrForbidden | 无权删除他人文件 | HTTP 403 |
shared.ErrUnsupportedFormat | 扩展名不在白名单 | response.AbortBadRequest |
// 管理员 / 系统删除
_, err := upload.Remove(ctx, uploadID)
// 用户删除自己的文件
_, err := upload.RemoveOwned(ctx, userID, uploadID)
uploadRec, err := repository.GetActiveUploadByID(ctx, uploadID)
obj, err := uploadstorage.OpenStoredObject(ctx, &uploadRec)
defer obj.Body.Close()
或通过门面(若已从 exports 暴露 OpenStoredObject)读取。HTTP 对外访问统一走 GET /f/:id。
推荐 两步流程(先上传、后提交业务):
POST /api/v1/upload → 获得 upload.idupload_id,用 repository.GetActiveUploadByID 校验存在且 status 为 activeupload.Type 是否为预期业务类型upload_id 写入业务表字段(如 cover_file_id)禁止在业务 Handler 中重复实现 multipart 解析,除非有极强的特殊协议需求。
使用 frontend/lib/services/upload/:
import { services } from '@/lib/services'
import { getFileUrl } from '@/lib/services/upload'
// 上传
const upload = await services.upload.uploadFile(file, 'invoice', { orderId: '123' })
// 展示
const url = getFileUrl(upload.id) // → /f/{id}
// Base64 图片(头像等)
const res = await services.upload.uploadBase64Image(croppedBase64, 'avatar', 'avatar.png')
UploadService / AdminUploadService,在 frontend/lib/services/index.ts 注册getFileUrl(id, quality?) 或 FileImagePreview 组件upload_id,不要提交 blob URL 或 file_pathw_upload_stats 由 upload.Ingest / upload.Remove 自动维护,业务不得手动增量。
若发现 trend / total 与 w_uploads 不一致(常见于历史旁路写表):
upload.RebuildUploadStats(ctx) // 从 w_uploads 全量重建统计
排查清单:
upload.Ingest 直接 db.Create(&model.Upload{})?ApplyUploadStatsAdd?upload.Remove(须在软删前扣减统计)?testhelper.SetupTestEnvironment(t) 初始化 DBstorage.MockStorage(...) + storage.IsEnabledFunc = func() bool { return true }uploads/test 路径;本地文件测试用 t.TempDir() 或 mock backend参考:internal/apps/upload/ingest/ingest_test.go
修改 upload handler 后运行:
go test ./internal/apps/upload/...
make code-check
若变更 HTTP 接口,运行 make swagger。
将以下模式:
storage.Active(ctx)
backend.Put(ctx, key, reader, size, mime)
db.DB(ctx).Create(&upload)
替换为:
upload.Ingest(ctx, upload.IngestRequest{ Policy: upload.PolicyResolveExisting, ... })
迁移完成后执行一次 upload.RebuildUploadStats(ctx) 修复历史统计偏差。
完成文件上传相关开发后,确认:
repository.CreateUpload / SoftDeleteUpload 调用storage.Active + Put 直接写文件upload_id,不存 file_pathPolicymake code-check 通过make swagger