feat: 添加环境变量管理工具和部署配置改版
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
主要改动: - 新增交互式环境配置脚本 (scripts/setup-env.sh) - 新增本地启动快捷脚本 (scripts/run-local.sh) - 新增环境变量模板文件 (.env.example) - 部署模式改版:使用嵌入式配置 + 环境变量覆盖 - 添加对象存储功能支持 - 改进 IoT 卡片导入任务 - 优化 OpenAPI 文档生成 - 删除旧的配置文件,改用嵌入式默认配置
This commit is contained in:
78
openspec/specs/bootstrap-init/spec.md
Normal file
78
openspec/specs/bootstrap-init/spec.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# bootstrap-init Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change deployment-self-init. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: 集中化目录初始化
|
||||
|
||||
系统 SHALL 在应用启动时通过 `bootstrap.EnsureDirectories()` 函数统一创建所有必需的运行时目录。
|
||||
|
||||
目录列表:
|
||||
- 临时文件目录(从 `config.Storage.TempDir` 读取)
|
||||
- 应用日志目录(从 `config.Logging.AppLog.Filename` 提取目录部分)
|
||||
- 访问日志目录(从 `config.Logging.AccessLog.Filename` 提取目录部分)
|
||||
|
||||
#### Scenario: 成功创建所有目录
|
||||
|
||||
- **WHEN** 应用启动且所有目录路径可写
|
||||
- **THEN** 系统创建所有必需目录,权限为 0755
|
||||
- **AND** 函数返回 nil
|
||||
|
||||
#### Scenario: 目录已存在
|
||||
|
||||
- **WHEN** 应用启动且目录已存在
|
||||
- **THEN** 系统跳过创建,不报错
|
||||
- **AND** 函数返回 nil
|
||||
|
||||
#### Scenario: 配置路径为空
|
||||
|
||||
- **WHEN** 某个目录配置为空字符串
|
||||
- **THEN** 系统跳过该目录的创建
|
||||
- **AND** 不影响其他目录的创建
|
||||
|
||||
### Requirement: 权限降级策略
|
||||
|
||||
系统 SHALL 在目录创建权限不足时自动降级到系统临时目录。
|
||||
|
||||
#### Scenario: 权限不足时降级
|
||||
|
||||
- **WHEN** 创建目录因权限不足失败(os.IsPermission 为 true)
|
||||
- **THEN** 系统使用 `os.TempDir()/junhong/<原目录名>` 作为降级路径
|
||||
- **AND** 记录 WARN 级别日志,包含原路径和降级路径
|
||||
- **AND** 函数返回降级后的路径
|
||||
|
||||
#### Scenario: 非权限错误
|
||||
|
||||
- **WHEN** 创建目录失败且不是权限问题
|
||||
- **THEN** 系统返回错误,应用启动失败
|
||||
- **AND** 错误信息包含目录路径和原始错误
|
||||
|
||||
### Requirement: 初始化顺序
|
||||
|
||||
系统 SHALL 确保目录初始化在所有组件初始化之前完成。
|
||||
|
||||
#### Scenario: 正确的初始化顺序
|
||||
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** 执行顺序为:
|
||||
1. config.Load() 加载配置
|
||||
2. bootstrap.EnsureDirectories() 创建目录
|
||||
3. logger.Init() 初始化日志
|
||||
4. 其他组件初始化
|
||||
|
||||
#### Scenario: 目录初始化失败
|
||||
|
||||
- **WHEN** `bootstrap.EnsureDirectories()` 返回错误
|
||||
- **THEN** 应用立即退出,不继续初始化其他组件
|
||||
- **AND** 错误信息输出到 stderr
|
||||
|
||||
### Requirement: 移除分散的目录创建逻辑
|
||||
|
||||
系统 SHALL 移除各组件中分散的目录创建代码。
|
||||
|
||||
#### Scenario: S3Provider 不再创建目录
|
||||
|
||||
- **WHEN** 初始化 S3Provider
|
||||
- **THEN** 不再调用 `os.MkdirAll` 创建临时目录
|
||||
- **AND** 假设目录已由 bootstrap 创建
|
||||
|
||||
133
openspec/specs/embedded-config/spec.md
Normal file
133
openspec/specs/embedded-config/spec.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# embedded-config Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change deployment-self-init. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: 配置嵌入
|
||||
|
||||
系统 SHALL 使用 Go 的 `go:embed` 指令将默认配置文件嵌入二进制文件。
|
||||
|
||||
嵌入文件位置:`pkg/config/defaults/config.yaml`
|
||||
|
||||
#### Scenario: 加载嵌入配置
|
||||
|
||||
- **WHEN** 调用 `config.Load()`
|
||||
- **THEN** 系统从嵌入的 `defaults/config.yaml` 读取默认配置
|
||||
- **AND** 无需外部配置文件即可启动
|
||||
|
||||
#### Scenario: 嵌入配置包含完整结构
|
||||
|
||||
- **WHEN** 读取嵌入配置
|
||||
- **THEN** 配置包含所有配置节:server、database、redis、storage、logging、queue、jwt、middleware
|
||||
|
||||
### Requirement: 环境变量覆盖
|
||||
|
||||
系统 SHALL 支持通过环境变量覆盖嵌入的默认配置值。
|
||||
|
||||
环境变量格式:`JUNHONG_{SECTION}_{KEY}`
|
||||
|
||||
#### Scenario: 环境变量覆盖配置
|
||||
|
||||
- **WHEN** 设置环境变量 `JUNHONG_DATABASE_HOST=myhost`
|
||||
- **THEN** `config.Database.Host` 的值为 "myhost"
|
||||
- **AND** 覆盖嵌入配置中的默认值
|
||||
|
||||
#### Scenario: 嵌套配置覆盖
|
||||
|
||||
- **WHEN** 设置环境变量 `JUNHONG_LOGGING_LEVEL=debug`
|
||||
- **THEN** `config.Logging.Level` 的值为 "debug"
|
||||
|
||||
#### Scenario: 未设置环境变量
|
||||
|
||||
- **WHEN** 未设置某个配置的环境变量
|
||||
- **THEN** 使用嵌入配置中的默认值
|
||||
|
||||
### Requirement: 配置优先级
|
||||
|
||||
系统 SHALL 按以下优先级应用配置(高到低):
|
||||
|
||||
1. 环境变量 (JUNHONG_*)
|
||||
2. 嵌入默认值 (go:embed)
|
||||
|
||||
#### Scenario: 优先级验证
|
||||
|
||||
- **WHEN** 嵌入配置中 `server.address` 为 ":3000"
|
||||
- **AND** 设置环境变量 `JUNHONG_SERVER_ADDRESS=:8080`
|
||||
- **THEN** 最终 `config.Server.Address` 为 ":8080"
|
||||
|
||||
### Requirement: 必填配置验证
|
||||
|
||||
系统 SHALL 在加载配置后验证必填配置项是否已设置。
|
||||
|
||||
必填配置项:
|
||||
- `database.host`
|
||||
- `database.user`
|
||||
- `database.password`
|
||||
- `database.dbname`
|
||||
- `redis.address`
|
||||
- `jwt.secret_key`
|
||||
|
||||
#### Scenario: 必填配置缺失
|
||||
|
||||
- **WHEN** 必填配置项为空且未通过环境变量设置
|
||||
- **THEN** `config.Load()` 返回错误
|
||||
- **AND** 错误信息明确指出缺失的配置项和对应的环境变量名
|
||||
|
||||
#### Scenario: 必填配置通过环境变量提供
|
||||
|
||||
- **WHEN** 所有必填配置通过环境变量设置
|
||||
- **THEN** `config.Load()` 成功返回配置
|
||||
|
||||
### Requirement: 删除外部配置文件支持
|
||||
|
||||
系统 SHALL 移除对外部配置文件的支持。
|
||||
|
||||
#### Scenario: 不读取 configs 目录
|
||||
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** 不读取 `configs/*.yaml` 文件
|
||||
- **AND** 不依赖 `CONFIG_PATH` 或 `CONFIG_ENV` 环境变量
|
||||
|
||||
### Requirement: 删除配置热重载
|
||||
|
||||
系统 SHALL 移除配置热重载功能。
|
||||
|
||||
#### Scenario: 不监听配置文件变化
|
||||
|
||||
- **WHEN** 应用运行中
|
||||
- **THEN** 不使用 fsnotify 监听文件变化
|
||||
- **AND** 删除 `pkg/config/watcher.go`
|
||||
|
||||
#### Scenario: 配置变更需重启
|
||||
|
||||
- **WHEN** 需要更改配置
|
||||
- **THEN** 必须重启应用使新配置生效
|
||||
|
||||
### Requirement: 环境变量前缀
|
||||
|
||||
系统 SHALL 使用 `JUNHONG_` 作为环境变量前缀。
|
||||
|
||||
#### Scenario: 前缀隔离
|
||||
|
||||
- **WHEN** 存在环境变量 `DATABASE_HOST=other`
|
||||
- **AND** 存在环境变量 `JUNHONG_DATABASE_HOST=correct`
|
||||
- **THEN** `config.Database.Host` 为 "correct"
|
||||
- **AND** 忽略无前缀的 `DATABASE_HOST`
|
||||
|
||||
### Requirement: 敏感配置处理
|
||||
|
||||
系统 SHALL 确保敏感配置不嵌入二进制文件。
|
||||
|
||||
敏感配置项(嵌入值为空):
|
||||
- `database.password`
|
||||
- `redis.password`
|
||||
- `jwt.secret_key`
|
||||
- `storage.s3.access_key_id`
|
||||
- `storage.s3.secret_access_key`
|
||||
|
||||
#### Scenario: 敏感配置默认为空
|
||||
|
||||
- **WHEN** 读取嵌入配置
|
||||
- **THEN** 敏感配置项的值为空字符串
|
||||
- **AND** 必须通过环境变量提供实际值
|
||||
|
||||
@@ -16,18 +16,22 @@ TBD - created by archiving change iot-card-standalone-management. Update Purpose
|
||||
|
||||
**导入参数**:
|
||||
- `carrier_id`: 运营商 ID(BIGINT,必填)
|
||||
- `carrier_type`: 运营商类型(VARCHAR(10),CMCC/CUCC/CTCC/CBN)
|
||||
- `batch_no`: 批次号(VARCHAR(100),可选)
|
||||
- `file_name`: 原始文件名(VARCHAR(255),可选)
|
||||
|
||||
**待导入数据**:
|
||||
- `card_list`: 待导入卡列表(JSONB,结构: [{iccid, msisdn}],替代原 iccid_list)
|
||||
|
||||
**进度统计**:
|
||||
- `total_count`: 总数(INT,CSV 文件总行数)
|
||||
- `success_count`: 成功数(INT,成功导入的 ICCID 数量)
|
||||
- `success_count`: 成功数(INT,成功导入的卡数量)
|
||||
- `skip_count`: 跳过数(INT,因重复等原因跳过的数量)
|
||||
- `fail_count`: 失败数(INT,因格式错误等原因失败的数量)
|
||||
|
||||
**结果详情**:
|
||||
- `skipped_items`: 跳过记录详情(JSONB,结构: [{line, iccid, reason}])
|
||||
- `failed_items`: 失败记录详情(JSONB,结构: [{line, iccid, reason}])
|
||||
- `skipped_items`: 跳过记录详情(JSONB,结构: [{line, iccid, msisdn, reason}])
|
||||
- `failed_items`: 失败记录详情(JSONB,结构: [{line, iccid, msisdn, reason}])
|
||||
|
||||
**时间和错误**:
|
||||
- `started_at`: 开始处理时间(TIMESTAMP,可空)
|
||||
@@ -43,23 +47,9 @@ TBD - created by archiving change iot-card-standalone-management. Update Purpose
|
||||
|
||||
#### Scenario: 创建导入任务
|
||||
|
||||
- **WHEN** 管理员上传 CSV 文件发起导入
|
||||
- **THEN** 系统创建导入任务记录,`status` 为 1(待处理),`total_count` 为 CSV 行数,返回任务 ID
|
||||
|
||||
#### Scenario: 导入任务开始处理
|
||||
|
||||
- **WHEN** Worker 开始处理导入任务
|
||||
- **THEN** 系统将任务 `status` 从 1(待处理) 变更为 2(处理中),`started_at` 记录当前时间
|
||||
|
||||
#### Scenario: 导入任务完成
|
||||
|
||||
- **WHEN** Worker 完成导入任务处理
|
||||
- **THEN** 系统将任务 `status` 变更为 3(已完成),`completed_at` 记录当前时间,更新 `success_count`、`skip_count`、`fail_count`
|
||||
|
||||
#### Scenario: 导入任务失败
|
||||
|
||||
- **WHEN** Worker 处理导入任务时发生严重错误(如文件损坏)
|
||||
- **THEN** 系统将任务 `status` 变更为 4(失败),`error_message` 记录错误信息
|
||||
- **GIVEN** 管理员上传包含 ICCID 和 MSISDN 两列的 CSV 文件
|
||||
- **WHEN** 系统解析 CSV 并创建导入任务
|
||||
- **THEN** 系统创建导入任务记录,`card_list` 包含 [{iccid, msisdn}] 结构,`status` 为 1(待处理)
|
||||
|
||||
---
|
||||
|
||||
@@ -174,3 +164,76 @@ TBD - created by archiving change iot-card-standalone-management. Update Purpose
|
||||
- **WHEN** 更新导入任务时 success_count + skip_count + fail_count > total_count
|
||||
- **THEN** 系统拒绝更新,返回错误信息"统计数量不一致"
|
||||
|
||||
### Requirement: CSV 文件格式规范
|
||||
|
||||
系统 SHALL 要求 CSV 文件必须包含 ICCID 和 MSISDN 两列。
|
||||
|
||||
**文件格式要求**:
|
||||
- 第一列: ICCID(必填,不能为空)
|
||||
- 第二列: MSISDN/接入号(必填,不能为空)
|
||||
- 支持表头行(自动识别并跳过)
|
||||
- 表头识别关键字: iccid/卡号 + msisdn/接入号/手机号
|
||||
|
||||
**解析规则**:
|
||||
- 自动去除首尾空格
|
||||
- 跳过空行
|
||||
- 第一行为表头时自动跳过
|
||||
- 列数不足 2 列的文件拒绝导入
|
||||
- ICCID 为空的行记录为失败
|
||||
- MSISDN 为空的行记录为失败
|
||||
|
||||
#### Scenario: 解析标准双列 CSV 文件
|
||||
|
||||
- **GIVEN** CSV 文件内容为:
|
||||
```
|
||||
iccid,msisdn
|
||||
89860012345678901234,13800000001
|
||||
89860012345678901235,13800000002
|
||||
```
|
||||
- **WHEN** 系统解析该 CSV 文件
|
||||
- **THEN** 解析结果包含 2 条有效记录,每条包含 ICCID 和 MSISDN
|
||||
|
||||
#### Scenario: 拒绝单列 CSV 文件
|
||||
|
||||
- **GIVEN** CSV 文件内容仅包含 ICCID 单列
|
||||
- **WHEN** 系统尝试解析该 CSV 文件
|
||||
- **THEN** 系统返回错误 "CSV 文件格式错误:缺少 MSISDN 列"
|
||||
|
||||
#### Scenario: MSISDN 为空的行记录失败
|
||||
|
||||
- **GIVEN** CSV 文件内容为:
|
||||
```
|
||||
iccid,msisdn
|
||||
89860012345678901234,13800000001
|
||||
89860012345678901235,
|
||||
```
|
||||
- **WHEN** 系统解析该 CSV 文件
|
||||
- **THEN** 第一条记录解析成功,第二条记录标记为失败,原因为 "MSISDN 不能为空"
|
||||
|
||||
#### Scenario: ICCID 为空的行记录失败
|
||||
|
||||
- **GIVEN** CSV 文件内容为:
|
||||
```
|
||||
iccid,msisdn
|
||||
89860012345678901234,13800000001
|
||||
,13800000002
|
||||
```
|
||||
- **WHEN** 系统解析该 CSV 文件
|
||||
- **THEN** 第一条记录解析成功,第二条记录标记为失败,原因为 "ICCID 不能为空"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 导入时填充 MSISDN 字段
|
||||
|
||||
系统 SHALL 在创建 IoT 卡记录时填充 MSISDN 字段。
|
||||
|
||||
**处理规则**:
|
||||
- 从 `card_list` 中获取 ICCID 和 MSISDN
|
||||
- 创建 `IotCard` 记录时同时设置 `iccid` 和 `msisdn` 字段
|
||||
|
||||
#### Scenario: 创建卡记录时填充 MSISDN
|
||||
|
||||
- **GIVEN** 导入任务包含卡数据 [{iccid: "898600...", msisdn: "13800000001"}]
|
||||
- **WHEN** Worker 处理导入任务创建卡记录
|
||||
- **THEN** 创建的 `IotCard` 记录 `iccid` 为 "898600...",`msisdn` 为 "13800000001"
|
||||
|
||||
|
||||
219
openspec/specs/object-storage/spec.md
Normal file
219
openspec/specs/object-storage/spec.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# 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 自动创建该目录
|
||||
|
||||
60
openspec/specs/openapi-markdown-description/spec.md
Normal file
60
openspec/specs/openapi-markdown-description/spec.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# openapi-markdown-description Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-openapi-markdown-description. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: RouteSpec 支持 Description 字段
|
||||
|
||||
RouteSpec 结构体 SHALL 包含 `Description` 字段,类型为 `string`,用于设置接口的详细 Markdown 说明。
|
||||
|
||||
#### Scenario: Description 字段为空时不影响生成
|
||||
|
||||
- **WHEN** RouteSpec.Description 为空字符串
|
||||
- **THEN** 生成的 OpenAPI 规范中该接口不包含 description 字段
|
||||
|
||||
#### Scenario: Description 字段有内容时写入 OpenAPI
|
||||
|
||||
- **WHEN** RouteSpec.Description 包含非空内容
|
||||
- **THEN** 生成的 OpenAPI 规范中该接口的 description 字段包含该内容
|
||||
|
||||
### Requirement: Description 支持 Markdown 语法
|
||||
|
||||
生成器 SHALL 原样保留 Description 字段的 Markdown 内容,不进行转义或处理,以便 OpenAPI 工具(如 Apifox)正确渲染。
|
||||
|
||||
#### Scenario: 支持基础 Markdown 格式
|
||||
|
||||
- **WHEN** Description 包含 Markdown 标题、列表、表格、代码块
|
||||
- **THEN** 生成的 OpenAPI YAML 文件中保留完整的 Markdown 格式
|
||||
|
||||
#### Scenario: 支持多行内容
|
||||
|
||||
- **WHEN** Description 包含多行文本
|
||||
- **THEN** 生成的 OpenAPI YAML 文件使用 YAML 多行字符串格式正确表示
|
||||
|
||||
### Requirement: AddOperation 方法处理 Description
|
||||
|
||||
AddOperation 方法 SHALL 接受 description 参数并设置到 openapi3.Operation.Description 字段。
|
||||
|
||||
#### Scenario: 普通接口设置 Description
|
||||
|
||||
- **WHEN** 调用 AddOperation 且 description 参数非空
|
||||
- **THEN** 生成的 Operation 对象包含 Description 字段
|
||||
|
||||
### Requirement: AddMultipartOperation 方法处理 Description
|
||||
|
||||
AddMultipartOperation 方法 SHALL 与 AddOperation 一致,支持 description 参数。
|
||||
|
||||
#### Scenario: 文件上传接口设置 Description
|
||||
|
||||
- **WHEN** 调用 AddMultipartOperation 且 description 参数非空
|
||||
- **THEN** 生成的 multipart/form-data 接口包含 Description 字段
|
||||
|
||||
### Requirement: Register 函数传递 Description
|
||||
|
||||
Register 函数 SHALL 从 RouteSpec 中提取 Description 字段并传递给文档生成器。
|
||||
|
||||
#### Scenario: Register 调用时传递 Description
|
||||
|
||||
- **WHEN** 调用 Register 函数注册路由
|
||||
- **THEN** RouteSpec.Description 被传递到对应的 AddOperation 或 AddMultipartOperation 调用
|
||||
|
||||
Reference in New Issue
Block a user