All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
主要改动: - 新增交互式环境配置脚本 (scripts/setup-env.sh) - 新增本地启动快捷脚本 (scripts/run-local.sh) - 新增环境变量模板文件 (.env.example) - 部署模式改版:使用嵌入式配置 + 环境变量覆盖 - 添加对象存储功能支持 - 改进 IoT 卡片导入任务 - 优化 OpenAPI 文档生成 - 删除旧的配置文件,改用嵌入式默认配置
220 lines
6.3 KiB
Markdown
220 lines
6.3 KiB
Markdown
# object-storage Specification
|
||
|
||
## Purpose
|
||
TBD - created by archiving change add-object-storage. Update Purpose after archive.
|
||
## Requirements
|
||
### Requirement: Provider 接口
|
||
|
||
系统 SHALL 提供统一的对象存储 Provider 接口,支持 S3 兼容的对象存储服务。
|
||
|
||
接口定义:
|
||
```go
|
||
type Provider interface {
|
||
Upload(ctx context.Context, key string, reader io.Reader, contentType string) error
|
||
Download(ctx context.Context, key string, writer io.Writer) error
|
||
DownloadToTemp(ctx context.Context, key string) (localPath string, cleanup func(), err error)
|
||
Delete(ctx context.Context, key string) error
|
||
Exists(ctx context.Context, key string) (bool, error)
|
||
GetUploadURL(ctx context.Context, key string, contentType string, expires time.Duration) (string, error)
|
||
GetDownloadURL(ctx context.Context, key string, expires time.Duration) (string, error)
|
||
}
|
||
```
|
||
|
||
#### Scenario: 创建 S3 Provider
|
||
- **WHEN** 系统启动时读取 storage 配置
|
||
- **THEN** 系统 SHALL 创建 S3Provider 实例并验证连接
|
||
|
||
#### Scenario: 配置缺失
|
||
- **WHEN** storage 配置未设置或不完整
|
||
- **THEN** 系统 SHALL 记录警告日志并跳过初始化(不影响启动)
|
||
|
||
---
|
||
|
||
### Requirement: 文件上传
|
||
|
||
系统 SHALL 支持通过 Provider 接口上传文件到对象存储。
|
||
|
||
#### Scenario: 上传成功
|
||
- **WHEN** 调用 `Upload(ctx, "imports/test.csv", reader, "text/csv")`
|
||
- **THEN** 文件 SHALL 被上传到配置的 Bucket 中指定路径
|
||
- **THEN** 方法 SHALL 返回 nil
|
||
|
||
#### Scenario: 上传失败
|
||
- **WHEN** 对象存储服务不可用
|
||
- **THEN** 方法 SHALL 返回包含错误详情的 error
|
||
|
||
---
|
||
|
||
### Requirement: 文件下载
|
||
|
||
系统 SHALL 支持从对象存储下载文件。
|
||
|
||
#### Scenario: 下载到 Writer
|
||
- **WHEN** 调用 `Download(ctx, "imports/test.csv", writer)`
|
||
- **THEN** 文件内容 SHALL 被写入到提供的 writer
|
||
|
||
#### Scenario: 下载到临时文件
|
||
- **WHEN** 调用 `DownloadToTemp(ctx, "imports/test.csv")`
|
||
- **THEN** 系统 SHALL 下载文件到临时目录
|
||
- **THEN** 方法 SHALL 返回本地文件路径和 cleanup 函数
|
||
- **THEN** 调用 cleanup() 后临时文件 SHALL 被删除
|
||
|
||
#### Scenario: 文件不存在
|
||
- **WHEN** 下载的文件在对象存储中不存在
|
||
- **THEN** 方法 SHALL 返回 "文件不存在" 错误
|
||
|
||
---
|
||
|
||
### Requirement: 文件删除
|
||
|
||
系统 SHALL 支持从对象存储删除文件。
|
||
|
||
#### Scenario: 删除成功
|
||
- **WHEN** 调用 `Delete(ctx, "imports/test.csv")`
|
||
- **THEN** 文件 SHALL 从对象存储中删除
|
||
- **THEN** 方法 SHALL 返回 nil
|
||
|
||
#### Scenario: 删除不存在的文件
|
||
- **WHEN** 删除的文件不存在
|
||
- **THEN** 方法 SHALL 返回 nil(幂等操作)
|
||
|
||
---
|
||
|
||
### Requirement: 文件存在性检查
|
||
|
||
系统 SHALL 支持检查文件是否存在于对象存储。
|
||
|
||
#### Scenario: 文件存在
|
||
- **WHEN** 调用 `Exists(ctx, "imports/test.csv")` 且文件存在
|
||
- **THEN** 方法 SHALL 返回 (true, nil)
|
||
|
||
#### Scenario: 文件不存在
|
||
- **WHEN** 调用 `Exists(ctx, "imports/test.csv")` 且文件不存在
|
||
- **THEN** 方法 SHALL 返回 (false, nil)
|
||
|
||
---
|
||
|
||
### Requirement: 预签名上传 URL
|
||
|
||
系统 SHALL 支持生成预签名上传 URL,允许前端直接上传文件到对象存储。
|
||
|
||
#### Scenario: 生成上传 URL
|
||
- **WHEN** 调用 `GetUploadURL(ctx, "imports/test.csv", "text/csv", 15*time.Minute)`
|
||
- **THEN** 方法 SHALL 返回有效的预签名 URL
|
||
- **THEN** URL SHALL 在指定时间(15分钟)后过期
|
||
- **THEN** 使用该 URL 的 PUT 请求 SHALL 能成功上传文件
|
||
|
||
#### Scenario: URL 过期后
|
||
- **WHEN** 使用过期的预签名 URL 上传
|
||
- **THEN** 对象存储 SHALL 返回 403 Forbidden
|
||
|
||
---
|
||
|
||
### Requirement: 预签名下载 URL
|
||
|
||
系统 SHALL 支持生成预签名下载 URL,允许用户直接从对象存储下载文件。
|
||
|
||
#### Scenario: 生成下载 URL
|
||
- **WHEN** 调用 `GetDownloadURL(ctx, "exports/report.xlsx", 24*time.Hour)`
|
||
- **THEN** 方法 SHALL 返回有效的预签名 URL
|
||
- **THEN** URL SHALL 在指定时间(24小时)后过期
|
||
- **THEN** 使用该 URL 的 GET 请求 SHALL 能下载文件
|
||
|
||
---
|
||
|
||
### Requirement: 获取上传 URL API
|
||
|
||
系统 SHALL 提供 API 接口供前端获取预签名上传 URL。
|
||
|
||
接口定义:
|
||
```
|
||
POST /api/admin/storage/upload-url
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
|
||
Request:
|
||
{
|
||
"file_name": "cards.csv",
|
||
"content_type": "text/csv",
|
||
"purpose": "iot_import"
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 0,
|
||
"message": "success",
|
||
"data": {
|
||
"upload_url": "http://obs-helf.cucloud.cn/cmp/imports/2025/01/24/abc123.csv?X-Amz-...",
|
||
"file_key": "imports/2025/01/24/abc123.csv",
|
||
"expires_in": 900
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Scenario: 获取上传 URL 成功
|
||
- **WHEN** 已认证用户调用 POST /api/admin/storage/upload-url
|
||
- **AND** 请求包含有效的 file_name、content_type、purpose
|
||
- **THEN** 系统 SHALL 返回预签名上传 URL 和 file_key
|
||
- **THEN** file_key 格式 SHALL 为 `{purpose}/{year}/{month}/{day}/{uuid}.{ext}`
|
||
|
||
#### Scenario: 参数缺失
|
||
- **WHEN** 请求缺少必填参数
|
||
- **THEN** 系统 SHALL 返回 400 错误
|
||
|
||
#### Scenario: 未认证
|
||
- **WHEN** 请求未携带有效 Token
|
||
- **THEN** 系统 SHALL 返回 401 错误
|
||
|
||
---
|
||
|
||
### Requirement: 文件路径规范
|
||
|
||
系统 SHALL 按照规范生成文件路径。
|
||
|
||
路径格式:`{purpose}/{year}/{month}/{day}/{uuid}.{ext}`
|
||
|
||
支持的 purpose 值:
|
||
- `iot_import` → `imports/`
|
||
- `export` → `exports/`
|
||
- `attachment` → `attachments/`
|
||
|
||
#### Scenario: 生成导入文件路径
|
||
- **WHEN** purpose 为 "iot_import",file_name 为 "cards.csv"
|
||
- **THEN** 生成的 file_key SHALL 匹配 `imports/\d{4}/\d{2}/\d{2}/[a-f0-9-]+\.csv`
|
||
|
||
#### Scenario: 未知 purpose
|
||
- **WHEN** purpose 值不在支持列表中
|
||
- **THEN** 系统 SHALL 返回错误 "不支持的文件用途"
|
||
|
||
---
|
||
|
||
### Requirement: 配置结构
|
||
|
||
系统 SHALL 支持通过配置文件配置对象存储参数。
|
||
|
||
```yaml
|
||
storage:
|
||
provider: "s3"
|
||
s3:
|
||
endpoint: "http://obs-helf.cucloud.cn"
|
||
region: "cn-langfang-2"
|
||
bucket: "cmp"
|
||
access_key_id: "${OSS_ACCESS_KEY_ID}"
|
||
secret_access_key: "${OSS_SECRET_ACCESS_KEY}"
|
||
use_ssl: false
|
||
path_style: true
|
||
presign:
|
||
upload_expires: "15m"
|
||
download_expires: "24h"
|
||
temp_dir: "/tmp/junhong-storage"
|
||
```
|
||
|
||
#### Scenario: 环境变量替换
|
||
- **WHEN** 配置值为 `${ENV_VAR}` 格式
|
||
- **THEN** 系统 SHALL 从环境变量读取实际值
|
||
|
||
#### Scenario: 临时目录不存在
|
||
- **WHEN** temp_dir 目录不存在
|
||
- **THEN** 系统 SHALL 自动创建该目录
|
||
|