当前项目已经具备后端 NestJS 服务、前端 React 页面以及基础的认证能力,但尚未实现统一的文件上传和对象存储能力。现阶段后续多个业务 case 都会依赖文件上传,例如:
因此,这个 case 需要提前建设一个“可复用的通用上传能力”,让后续业务不必各自重新设计文件上传流程。
本 case 的目标存储后端为 Google Cloud Storage,当前已明确以下配置:
STORAGE_DRIVER=gcsGCS_BUCKET=aitori-publicGCS_PROJECT_ID=app-aitori-jpGCS_CLIENT_EMAIL=oss-227@app-aitori-jp.iam.gserviceaccount.comGCS_PRIVATE_KEY=...用户还明确要求:
stylist/{type}/{filename}type 采用自由传字符串的方式,后续由具体业务 case 决定取值stylist/{type}/{filename}。url,减少前端和业务层自行拼接访问地址。.env、配置层和校验层。type 保持自由输入,但必须经过安全校验,避免路径穿越和命名污染。objectKey 作为辅助信息保留。url,减少访问拼接逻辑和前后端重复认知。本 case 最终采用“通用上传接口 + 可复用服务层 + GCS 驱动”方案。
POST /api/v1/storage/uploadStorageService这个方案的优点:
POST /api/v1/storage/uploadmultipart/form-data请求字段:
file: 必填,上传文件type: 必填,业务类型字符串说明:
type 由具体业务调用方决定,例如未来可以是 template、customer-photo、simulation-resulttype 做业务枚举约束上传成功后返回统一响应结构:
{
"ok": true,
"data": {
"url": "https://storage.googleapis.com/aitori-public/stylist/template/20260628T014500_ab12cd34.png",
"objectKey": "stylist/template/20260628T014500_ab12cd34.png",
"bucket": "aitori-public",
"contentType": "image/png",
"size": 123456
},
"error": null
}
字段说明:
url: 完整可访问 URL,后续业务表默认存这个字段objectKey: GCS 对象 key,供调试、清理、迁移或未来删除能力使用bucket: bucket 名称contentType: 文件内容类型size: 文件大小失败场景统一走 { ok, data, error } 结构。
示例:
{
"ok": false,
"data": null,
"error": {
"code": "STORAGE_INVALID_TYPE",
"message": "上传类型不合法"
}
}
GCS 对象 key 固定为:
stylist/{type}/{filename}
其中:
stylist 为固定前缀type 由调用方提供filename 由服务端生成本 case 中 type 采用自由字符串传入,但必须满足安全校验:
-_..//\这样既保留业务灵活性,又避免目录穿越和不可控路径结构。
最终文件名不保留原始文件名,而由服务端生成唯一文件名。
规则:
20260628T014500_ab12cd34.png这样可以避免:
这是本 case 的关键约束之一。
上传完成后,服务端不仅要生成 objectKey,还要同步生成完整可访问 URL:
https://storage.googleapis.com/{bucket}/{objectKey}
例如:
https://storage.googleapis.com/aitori-public/stylist/template/20260628T014500_ab12cd34.png
后续业务表默认存:
url不推荐只存:
objectKey原因:
objectKey 仍建议作为接口返回值保留,用于:
但在当前阶段,业务表不强制额外存 objectKey。
建议新增独立 storage 模块。
职责:
POST /storage/uploadtypeStorageService不负责:
职责:
typeobjectKey同时提供服务层方法,例如:
uploadFile(...)uploadBuffer(...)以便后续后端内部其他模块直接复用,而不必通过 HTTP 再调一次自己。
职责:
不负责:
需要接入以下字段:
STORAGE_DRIVERGCS_BUCKETGCS_PROJECT_IDGCS_CLIENT_EMAILGCS_PRIVATE_KEYbackend/.envbackend/.env.example需要同步更新:
backend/src/config/env.schema.tsbackend/src/config/app.config.ts目标是对外暴露一个统一 storage 配置对象,例如:
storage.driverstorage.gcs.bucketstorage.gcs.projectIdstorage.gcs.clientEmailstorage.gcs.privateKey说明:
GCS_PRIVATE_KEY 需要正确处理 \n 换行,保证最终传入 SDK 的是合法私钥内容以下情况返回 400:
typetype 不合法推荐错误码:
STORAGE_FILE_REQUIREDSTORAGE_TYPE_REQUIREDSTORAGE_INVALID_TYPE以下情况返回 500:
STORAGE_DRIVER 未配置推荐错误码:
STORAGE_DRIVER_UNSUPPORTEDSTORAGE_CONFIG_INVALIDGCS SDK 上传失败时返回统一上传失败错误。
推荐错误码:
STORAGE_UPLOAD_FAILED要求:
本 case 不只服务于 HTTP 上传。
后续其他业务模块应可以直接依赖服务层,例如:
因此,服务层设计必须脱离 Express multipart 语义,支持接收:
这会让 HTTP 接口只是一个外壳,而不是唯一入口。
建议覆盖:
type 合法性校验GCS_PRIVATE_KEY 换行处理StorageService 调用驱动上传的行为建议覆盖:
POST /api/v1/storage/upload 成功上传400type 返回 400说明:
POST /api/v1/storage/uploadobjectKey 满足 stylist/{type}/{serverGeneratedFilename} 规则urltype、缺文件等错误能返回明确错误.env、.env.example、配置层和校验层在本设计确认后,下一步进入实现计划阶段,拆分为:
storage 模块、服务层和 GCS 驱动。POST /api/v1/storage/upload。