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:
@@ -140,17 +140,16 @@ if err := initDefaultAdmin(deps, services); err != nil {
|
||||
|
||||
### 自定义配置
|
||||
|
||||
在 `configs/config.yaml` 中添加:
|
||||
通过环境变量自定义:
|
||||
|
||||
```yaml
|
||||
default_admin:
|
||||
username: "自定义用户名"
|
||||
password: "自定义密码"
|
||||
phone: "自定义手机号"
|
||||
```bash
|
||||
export JUNHONG_DEFAULT_ADMIN_USERNAME="自定义用户名"
|
||||
export JUNHONG_DEFAULT_ADMIN_PASSWORD="自定义密码"
|
||||
export JUNHONG_DEFAULT_ADMIN_PHONE="自定义手机号"
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- 配置项为可选,不参与 `Validate()` 验证
|
||||
- 配置项为可选,不参与 `ValidateRequired()` 验证
|
||||
- 任何字段留空则使用代码默认值
|
||||
- 密码必须足够复杂(建议包含大小写字母、数字、特殊字符)
|
||||
|
||||
@@ -192,12 +191,11 @@ go run cmd/api/main.go
|
||||
|
||||
### 场景3:使用自定义配置
|
||||
|
||||
**配置文件** (`configs/config.yaml`):
|
||||
```yaml
|
||||
default_admin:
|
||||
username: "myadmin"
|
||||
password: "MySecurePass@2024"
|
||||
phone: "13900000000"
|
||||
**设置环境变量**:
|
||||
```bash
|
||||
export JUNHONG_DEFAULT_ADMIN_USERNAME="myadmin"
|
||||
export JUNHONG_DEFAULT_ADMIN_PASSWORD="MySecurePass@2024"
|
||||
export JUNHONG_DEFAULT_ADMIN_PHONE="13900000000"
|
||||
```
|
||||
|
||||
**启动服务**:
|
||||
@@ -230,11 +228,11 @@ go run cmd/api/main.go
|
||||
- ✅ 包含时间戳、用户名、手机号
|
||||
- ⚠️ 日志中不会记录明文密码
|
||||
|
||||
### 4. 配置文件安全
|
||||
### 4. 配置安全
|
||||
|
||||
- ⚠️ `config.yaml` 中的密码是明文存储
|
||||
- ⚠️ 确保配置文件访问权限受限(不要提交到公开仓库)
|
||||
- ⚠️ 生产环境建议使用环境变量或密钥管理服务
|
||||
- ✅ 配置通过环境变量设置,不存储在代码仓库中
|
||||
- ⚠️ 确保环境变量安全(使用密钥管理服务或加密存储)
|
||||
- ⚠️ 生产环境务必修改默认密码
|
||||
|
||||
## 手动创建管理员(备用方案)
|
||||
|
||||
@@ -285,7 +283,8 @@ func main() {
|
||||
|
||||
- `pkg/constants/constants.go` - 默认值常量定义
|
||||
- `pkg/config/config.go` - 配置结构定义
|
||||
- `configs/config.yaml` - 配置示例
|
||||
- `pkg/config/defaults/config.yaml` - 嵌入式默认配置
|
||||
- `docs/environment-variables.md` - 环境变量配置文档
|
||||
- `internal/service/account/service.go` - CreateSystemAccount 方法
|
||||
- `internal/bootstrap/admin.go` - initDefaultAdmin 函数
|
||||
- `internal/bootstrap/bootstrap.go` - Bootstrap 主流程
|
||||
|
||||
@@ -148,6 +148,77 @@ components:
|
||||
description: 总卡数量
|
||||
type: integer
|
||||
type: object
|
||||
DtoAllocateStandaloneCardsRequest:
|
||||
properties:
|
||||
batch_no:
|
||||
description: 批次号(selection_type=filter时可选)
|
||||
maxLength: 100
|
||||
type: string
|
||||
carrier_id:
|
||||
description: 运营商ID(selection_type=filter时可选)
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
iccid_end:
|
||||
description: 结束ICCID(selection_type=range时必填)
|
||||
maxLength: 20
|
||||
type: string
|
||||
iccid_start:
|
||||
description: 起始ICCID(selection_type=range时必填)
|
||||
maxLength: 20
|
||||
type: string
|
||||
iccids:
|
||||
description: ICCID列表(selection_type=list时必填,最多1000个)
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
remark:
|
||||
description: 备注
|
||||
maxLength: 500
|
||||
type: string
|
||||
selection_type:
|
||||
description: 选卡方式 (list:ICCID列表, range:号段范围, filter:筛选条件)
|
||||
enum:
|
||||
- list
|
||||
- range
|
||||
- filter
|
||||
type: string
|
||||
status:
|
||||
description: 卡状态 (1:在库, 2:已分销)(selection_type=filter时可选)
|
||||
maximum: 4
|
||||
minimum: 1
|
||||
nullable: true
|
||||
type: integer
|
||||
to_shop_id:
|
||||
description: 目标店铺ID
|
||||
minimum: 1
|
||||
type: integer
|
||||
required:
|
||||
- to_shop_id
|
||||
- selection_type
|
||||
type: object
|
||||
DtoAllocateStandaloneCardsResponse:
|
||||
properties:
|
||||
allocation_no:
|
||||
description: 分配单号
|
||||
type: string
|
||||
fail_count:
|
||||
description: 失败数
|
||||
type: integer
|
||||
failed_items:
|
||||
description: 失败项列表
|
||||
items:
|
||||
$ref: '#/components/schemas/DtoAllocationFailedItem'
|
||||
nullable: true
|
||||
type: array
|
||||
success_count:
|
||||
description: 成功数
|
||||
type: integer
|
||||
total_count:
|
||||
description: 待分配总数
|
||||
type: integer
|
||||
type: object
|
||||
DtoAllocatedDevice:
|
||||
properties:
|
||||
card_count:
|
||||
@@ -167,6 +238,15 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
DtoAllocationFailedItem:
|
||||
properties:
|
||||
iccid:
|
||||
description: ICCID
|
||||
type: string
|
||||
reason:
|
||||
description: 失败原因
|
||||
type: string
|
||||
type: object
|
||||
DtoApproveWithdrawalReq:
|
||||
properties:
|
||||
account_name:
|
||||
@@ -198,6 +278,156 @@ components:
|
||||
required:
|
||||
- payment_type
|
||||
type: object
|
||||
DtoAssetAllocationRecordDetailResponse:
|
||||
properties:
|
||||
allocation_name:
|
||||
description: 分配类型名称
|
||||
type: string
|
||||
allocation_no:
|
||||
description: 分配单号
|
||||
type: string
|
||||
allocation_type:
|
||||
description: 分配类型 (allocate:分配, recall:回收)
|
||||
type: string
|
||||
asset_id:
|
||||
description: 资产ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
asset_identifier:
|
||||
description: 资产标识符(ICCID或设备号)
|
||||
type: string
|
||||
asset_type:
|
||||
description: 资产类型 (iot_card:物联网卡, device:设备)
|
||||
type: string
|
||||
asset_type_name:
|
||||
description: 资产类型名称
|
||||
type: string
|
||||
created_at:
|
||||
description: 创建时间
|
||||
format: date-time
|
||||
type: string
|
||||
from_owner_id:
|
||||
description: 来源所有者ID
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
from_owner_name:
|
||||
description: 来源所有者名称
|
||||
type: string
|
||||
from_owner_type:
|
||||
description: 来源所有者类型
|
||||
type: string
|
||||
id:
|
||||
description: 记录ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
operator_id:
|
||||
description: 操作人ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
operator_name:
|
||||
description: 操作人名称
|
||||
type: string
|
||||
related_card_count:
|
||||
description: 关联卡数量
|
||||
type: integer
|
||||
related_card_ids:
|
||||
description: 关联卡ID列表
|
||||
items:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: array
|
||||
related_device_id:
|
||||
description: 关联设备ID
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
remark:
|
||||
description: 备注
|
||||
type: string
|
||||
to_owner_id:
|
||||
description: 目标所有者ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
to_owner_name:
|
||||
description: 目标所有者名称
|
||||
type: string
|
||||
to_owner_type:
|
||||
description: 目标所有者类型
|
||||
type: string
|
||||
type: object
|
||||
DtoAssetAllocationRecordResponse:
|
||||
properties:
|
||||
allocation_name:
|
||||
description: 分配类型名称
|
||||
type: string
|
||||
allocation_no:
|
||||
description: 分配单号
|
||||
type: string
|
||||
allocation_type:
|
||||
description: 分配类型 (allocate:分配, recall:回收)
|
||||
type: string
|
||||
asset_id:
|
||||
description: 资产ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
asset_identifier:
|
||||
description: 资产标识符(ICCID或设备号)
|
||||
type: string
|
||||
asset_type:
|
||||
description: 资产类型 (iot_card:物联网卡, device:设备)
|
||||
type: string
|
||||
asset_type_name:
|
||||
description: 资产类型名称
|
||||
type: string
|
||||
created_at:
|
||||
description: 创建时间
|
||||
format: date-time
|
||||
type: string
|
||||
from_owner_id:
|
||||
description: 来源所有者ID
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
from_owner_name:
|
||||
description: 来源所有者名称
|
||||
type: string
|
||||
from_owner_type:
|
||||
description: 来源所有者类型
|
||||
type: string
|
||||
id:
|
||||
description: 记录ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
operator_id:
|
||||
description: 操作人ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
operator_name:
|
||||
description: 操作人名称
|
||||
type: string
|
||||
related_card_count:
|
||||
description: 关联卡数量
|
||||
type: integer
|
||||
related_device_id:
|
||||
description: 关联设备ID
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
remark:
|
||||
description: 备注
|
||||
type: string
|
||||
to_owner_id:
|
||||
description: 目标所有者ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
to_owner_name:
|
||||
description: 目标所有者名称
|
||||
type: string
|
||||
to_owner_type:
|
||||
description: 目标所有者类型
|
||||
type: string
|
||||
type: object
|
||||
DtoAssignPermissionsParams:
|
||||
properties:
|
||||
perm_ids:
|
||||
@@ -832,6 +1062,55 @@ components:
|
||||
description: 失败原因
|
||||
type: string
|
||||
type: object
|
||||
DtoGetUploadURLRequest:
|
||||
properties:
|
||||
content_type:
|
||||
description: 文件 MIME 类型(如:text/csv),留空则自动推断
|
||||
maxLength: 100
|
||||
type: string
|
||||
file_name:
|
||||
description: 文件名(如:cards.csv)
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
type: string
|
||||
purpose:
|
||||
description: 文件用途 (iot_import:ICCID导入, export:数据导出, attachment:附件)
|
||||
type: string
|
||||
required:
|
||||
- file_name
|
||||
- purpose
|
||||
type: object
|
||||
DtoGetUploadURLResponse:
|
||||
properties:
|
||||
expires_in:
|
||||
description: URL 有效期(秒)
|
||||
type: integer
|
||||
file_key:
|
||||
description: 文件路径标识,上传成功后用于调用业务接口
|
||||
type: string
|
||||
upload_url:
|
||||
description: 预签名上传 URL,使用 PUT 方法上传文件
|
||||
type: string
|
||||
type: object
|
||||
DtoImportIotCardRequest:
|
||||
properties:
|
||||
batch_no:
|
||||
description: 批次号
|
||||
maxLength: 100
|
||||
type: string
|
||||
carrier_id:
|
||||
description: 运营商ID
|
||||
minimum: 1
|
||||
type: integer
|
||||
file_key:
|
||||
description: 对象存储文件路径(通过 /storage/upload-url 获取)
|
||||
maxLength: 500
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- carrier_id
|
||||
- file_key
|
||||
type: object
|
||||
DtoImportIotCardResponse:
|
||||
properties:
|
||||
message:
|
||||
@@ -853,6 +1132,9 @@ components:
|
||||
line:
|
||||
description: 行号
|
||||
type: integer
|
||||
msisdn:
|
||||
description: 接入号
|
||||
type: string
|
||||
reason:
|
||||
description: 原因
|
||||
type: string
|
||||
@@ -985,6 +1267,27 @@ components:
|
||||
description: 总数
|
||||
type: integer
|
||||
type: object
|
||||
DtoListAssetAllocationRecordResponse:
|
||||
properties:
|
||||
list:
|
||||
description: 分配记录列表
|
||||
items:
|
||||
$ref: '#/components/schemas/DtoAssetAllocationRecordResponse'
|
||||
nullable: true
|
||||
type: array
|
||||
page:
|
||||
description: 当前页码
|
||||
type: integer
|
||||
page_size:
|
||||
description: 每页数量
|
||||
type: integer
|
||||
total:
|
||||
description: 总数
|
||||
type: integer
|
||||
total_pages:
|
||||
description: 总页数
|
||||
type: integer
|
||||
type: object
|
||||
DtoListImportTaskResponse:
|
||||
properties:
|
||||
list:
|
||||
@@ -1261,6 +1564,71 @@ components:
|
||||
description: 成功数量
|
||||
type: integer
|
||||
type: object
|
||||
DtoRecallStandaloneCardsRequest:
|
||||
properties:
|
||||
batch_no:
|
||||
description: 批次号(selection_type=filter时可选)
|
||||
maxLength: 100
|
||||
type: string
|
||||
carrier_id:
|
||||
description: 运营商ID(selection_type=filter时可选)
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
from_shop_id:
|
||||
description: 来源店铺ID(被回收方)
|
||||
minimum: 1
|
||||
type: integer
|
||||
iccid_end:
|
||||
description: 结束ICCID(selection_type=range时必填)
|
||||
maxLength: 20
|
||||
type: string
|
||||
iccid_start:
|
||||
description: 起始ICCID(selection_type=range时必填)
|
||||
maxLength: 20
|
||||
type: string
|
||||
iccids:
|
||||
description: ICCID列表(selection_type=list时必填,最多1000个)
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
remark:
|
||||
description: 备注
|
||||
maxLength: 500
|
||||
type: string
|
||||
selection_type:
|
||||
description: 选卡方式 (list:ICCID列表, range:号段范围, filter:筛选条件)
|
||||
enum:
|
||||
- list
|
||||
- range
|
||||
- filter
|
||||
type: string
|
||||
required:
|
||||
- from_shop_id
|
||||
- selection_type
|
||||
type: object
|
||||
DtoRecallStandaloneCardsResponse:
|
||||
properties:
|
||||
allocation_no:
|
||||
description: 回收单号
|
||||
type: string
|
||||
fail_count:
|
||||
description: 失败数
|
||||
type: integer
|
||||
failed_items:
|
||||
description: 失败项列表
|
||||
items:
|
||||
$ref: '#/components/schemas/DtoAllocationFailedItem'
|
||||
nullable: true
|
||||
type: array
|
||||
success_count:
|
||||
description: 成功数
|
||||
type: integer
|
||||
total_count:
|
||||
description: 待回收总数
|
||||
type: integer
|
||||
type: object
|
||||
DtoRecalledDevice:
|
||||
properties:
|
||||
card_count:
|
||||
@@ -2297,19 +2665,6 @@ components:
|
||||
- message
|
||||
- timestamp
|
||||
type: object
|
||||
FormDataDtoImportIotCardRequest:
|
||||
properties:
|
||||
batch_no:
|
||||
description: 批次号
|
||||
maxLength: 100
|
||||
type: string
|
||||
carrier_id:
|
||||
description: 运营商ID
|
||||
minimum: 1
|
||||
type: integer
|
||||
required:
|
||||
- carrier_id
|
||||
type: object
|
||||
ModelPermission:
|
||||
properties:
|
||||
available_for_role_types:
|
||||
@@ -2779,6 +3134,179 @@ paths:
|
||||
summary: 分配角色
|
||||
tags:
|
||||
- 账号相关
|
||||
/api/admin/asset-allocation-records:
|
||||
get:
|
||||
parameters:
|
||||
- description: 页码
|
||||
in: query
|
||||
name: page
|
||||
schema:
|
||||
description: 页码
|
||||
minimum: 1
|
||||
type: integer
|
||||
- description: 每页数量
|
||||
in: query
|
||||
name: page_size
|
||||
schema:
|
||||
description: 每页数量
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
type: integer
|
||||
- description: 分配类型 (allocate:分配, recall:回收)
|
||||
in: query
|
||||
name: allocation_type
|
||||
schema:
|
||||
description: 分配类型 (allocate:分配, recall:回收)
|
||||
enum:
|
||||
- allocate
|
||||
- recall
|
||||
type: string
|
||||
- description: 资产类型 (iot_card:物联网卡, device:设备)
|
||||
in: query
|
||||
name: asset_type
|
||||
schema:
|
||||
description: 资产类型 (iot_card:物联网卡, device:设备)
|
||||
enum:
|
||||
- iot_card
|
||||
- device
|
||||
type: string
|
||||
- description: 资产标识符(ICCID或设备号,模糊查询)
|
||||
in: query
|
||||
name: asset_identifier
|
||||
schema:
|
||||
description: 资产标识符(ICCID或设备号,模糊查询)
|
||||
maxLength: 50
|
||||
type: string
|
||||
- description: 分配单号(精确匹配)
|
||||
in: query
|
||||
name: allocation_no
|
||||
schema:
|
||||
description: 分配单号(精确匹配)
|
||||
maxLength: 50
|
||||
type: string
|
||||
- description: 来源店铺ID
|
||||
in: query
|
||||
name: from_shop_id
|
||||
schema:
|
||||
description: 来源店铺ID
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
- description: 目标店铺ID
|
||||
in: query
|
||||
name: to_shop_id
|
||||
schema:
|
||||
description: 目标店铺ID
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
- description: 操作人ID
|
||||
in: query
|
||||
name: operator_id
|
||||
schema:
|
||||
description: 操作人ID
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
- description: 创建时间起始
|
||||
in: query
|
||||
name: created_at_start
|
||||
schema:
|
||||
description: 创建时间起始
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
- description: 创建时间结束
|
||||
in: query
|
||||
name: created_at_end
|
||||
schema:
|
||||
description: 创建时间结束
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoListAssetAllocationRecordResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 分配记录列表
|
||||
tags:
|
||||
- 资产分配记录
|
||||
/api/admin/asset-allocation-records/{id}:
|
||||
get:
|
||||
parameters:
|
||||
- description: 记录ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
description: 记录ID
|
||||
minimum: 1
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoAssetAllocationRecordDetailResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 分配记录详情
|
||||
tags:
|
||||
- 资产分配记录
|
||||
/api/admin/commission/withdrawal-requests:
|
||||
get:
|
||||
parameters:
|
||||
@@ -4006,27 +4534,37 @@ paths:
|
||||
- 企业客户管理
|
||||
/api/admin/iot-cards/import:
|
||||
post:
|
||||
parameters:
|
||||
- description: 运营商ID
|
||||
in: query
|
||||
name: carrier_id
|
||||
required: true
|
||||
schema:
|
||||
description: 运营商ID
|
||||
minimum: 1
|
||||
type: integer
|
||||
- description: 批次号
|
||||
in: query
|
||||
name: batch_no
|
||||
schema:
|
||||
description: 批次号
|
||||
maxLength: 100
|
||||
type: string
|
||||
description: |-
|
||||
## ⚠️ 接口变更说明(BREAKING CHANGE)
|
||||
|
||||
本接口已从 `multipart/form-data` 改为 `application/json`。
|
||||
|
||||
### 完整导入流程
|
||||
|
||||
1. **获取上传 URL**: 调用 `POST /api/admin/storage/upload-url`
|
||||
2. **上传 CSV 文件**: 使用预签名 URL 上传文件到对象存储
|
||||
3. **调用本接口**: 使用返回的 `file_key` 提交导入任务
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"carrier_id": 1,
|
||||
"batch_no": "BATCH-2025-01",
|
||||
"file_key": "imports/2025/01/24/abc123.csv"
|
||||
}
|
||||
```
|
||||
|
||||
### CSV 文件格式
|
||||
|
||||
- 必须包含两列:`iccid`, `msisdn`
|
||||
- 首行为表头
|
||||
- 编码:UTF-8
|
||||
requestBody:
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FormDataDtoImportIotCardRequest'
|
||||
$ref: '#/components/schemas/DtoImportIotCardRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
@@ -4060,7 +4598,7 @@ paths:
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 批量导入ICCID
|
||||
summary: 批量导入IoT卡(ICCID+MSISDN)
|
||||
tags:
|
||||
- IoT卡管理
|
||||
/api/admin/iot-cards/import-tasks:
|
||||
@@ -4340,6 +4878,92 @@ paths:
|
||||
summary: 单卡列表(未绑定设备)
|
||||
tags:
|
||||
- IoT卡管理
|
||||
/api/admin/iot-cards/standalone/allocate:
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoAllocateStandaloneCardsRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoAllocateStandaloneCardsResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 批量分配单卡
|
||||
tags:
|
||||
- IoT卡管理
|
||||
/api/admin/iot-cards/standalone/recall:
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoRecallStandaloneCardsRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoRecallStandaloneCardsResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 批量回收单卡
|
||||
tags:
|
||||
- IoT卡管理
|
||||
/api/admin/login:
|
||||
post:
|
||||
requestBody:
|
||||
@@ -6740,6 +7364,99 @@ paths:
|
||||
summary: 代理商佣金列表
|
||||
tags:
|
||||
- 代理商佣金管理
|
||||
/api/admin/storage/upload-url:
|
||||
post:
|
||||
description: |-
|
||||
## 文件上传流程
|
||||
|
||||
本接口用于获取对象存储的预签名上传 URL,实现前端直传文件到对象存储。
|
||||
|
||||
### 完整流程
|
||||
|
||||
1. **调用本接口** 获取预签名 URL 和 file_key
|
||||
2. **使用预签名 URL 上传文件** 发起 PUT 请求直接上传到对象存储
|
||||
3. **调用业务接口** 使用 file_key 调用相关业务接口(如 ICCID 导入)
|
||||
|
||||
### 前端上传示例
|
||||
|
||||
```javascript
|
||||
// 1. 获取预签名 URL
|
||||
const { data } = await api.post('/storage/upload-url', {
|
||||
file_name: 'cards.csv',
|
||||
content_type: 'text/csv',
|
||||
purpose: 'iot_import'
|
||||
});
|
||||
|
||||
// 2. 上传文件到对象存储
|
||||
await fetch(data.upload_url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'text/csv' },
|
||||
body: file
|
||||
});
|
||||
|
||||
// 3. 调用业务接口
|
||||
await api.post('/iot-cards/import', {
|
||||
carrier_id: 1,
|
||||
batch_no: 'BATCH-2025-01',
|
||||
file_key: data.file_key
|
||||
});
|
||||
```
|
||||
|
||||
### purpose 可选值
|
||||
|
||||
| 值 | 说明 | 生成路径格式 |
|
||||
|---|------|-------------|
|
||||
| iot_import | ICCID 导入 | imports/YYYY/MM/DD/uuid.csv |
|
||||
| export | 数据导出 | exports/YYYY/MM/DD/uuid.xlsx |
|
||||
| attachment | 附件上传 | attachments/YYYY/MM/DD/uuid.ext |
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 预签名 URL 有效期 **15 分钟**,请及时使用
|
||||
- 上传时 Content-Type 需与请求时一致
|
||||
- file_key 在上传成功后永久有效,用于后续业务接口调用
|
||||
- 上传失败时可重新调用本接口获取新的 URL
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoGetUploadURLRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoGetUploadURLResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取文件上传预签名 URL
|
||||
tags:
|
||||
- 对象存储
|
||||
/api/admin/tasks/{id}:
|
||||
get:
|
||||
parameters:
|
||||
|
||||
@@ -173,15 +173,65 @@ func registerXxxRoutes(
|
||||
|
||||
```go
|
||||
type RouteSpec struct {
|
||||
Summary string // 操作摘要(中文,简短)
|
||||
Input interface{} // 请求参数 DTO
|
||||
Output interface{} // 响应结果 DTO
|
||||
Tags []string // 分类标签(用于文档分组)
|
||||
Auth bool // 是否需要认证
|
||||
Summary string // 操作摘要(中文,简短,一行)
|
||||
Description string // 详细说明,支持 Markdown 语法(可选)
|
||||
Input interface{} // 请求参数 DTO
|
||||
Output interface{} // 响应结果 DTO
|
||||
Tags []string // 分类标签(用于文档分组)
|
||||
Auth bool // 是否需要认证
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 完整示例
|
||||
### 4. Description 字段(Markdown 说明)
|
||||
|
||||
`Description` 字段用于添加接口的详细说明,支持 **CommonMark Markdown** 语法。Apifox 等 OpenAPI 工具会正确渲染这些 Markdown 内容。
|
||||
|
||||
**使用场景**:
|
||||
- 业务规则说明
|
||||
- 请求频率限制
|
||||
- 注意事项
|
||||
- 错误码说明
|
||||
- 数据格式说明
|
||||
|
||||
**示例**:
|
||||
```go
|
||||
Register(router, doc, basePath, "POST", "/login", handler.Login, RouteSpec{
|
||||
Summary: "后台登录",
|
||||
Description: `## 登录说明
|
||||
|
||||
**请求频率限制**:每分钟最多 10 次
|
||||
|
||||
### 注意事项
|
||||
1. 密码错误 5 次后账号将被锁定 30 分钟
|
||||
2. Token 有效期为 24 小时
|
||||
|
||||
### 返回码说明
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 1001 | 用户名或密码错误 |
|
||||
| 1002 | 账号已被锁定 |
|
||||
`,
|
||||
Tags: []string{"认证"},
|
||||
Input: new(dto.LoginRequest),
|
||||
Output: new(dto.LoginResponse),
|
||||
Auth: false,
|
||||
})
|
||||
```
|
||||
|
||||
**支持的 Markdown 语法**:
|
||||
- 标题:`#`、`##`、`###`
|
||||
- 列表:`-`、`1.`
|
||||
- 表格:`| 列1 | 列2 |`
|
||||
- 代码:`` `code` `` 和 ` ```code block``` `
|
||||
- 强调:`**粗体**`、`*斜体*`
|
||||
- 链接:`[文本](url)`
|
||||
|
||||
**最佳实践**:
|
||||
- 保持简洁,控制在 500 字以内
|
||||
- 使用结构化的 Markdown(标题、列表、表格)提高可读性
|
||||
- 避免使用 HTML 标签(兼容性较差)
|
||||
|
||||
### 5. 完整示例
|
||||
|
||||
```go
|
||||
func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *openapi.Generator, basePath string) {
|
||||
|
||||
@@ -30,16 +30,18 @@
|
||||
|
||||
### 配置项
|
||||
|
||||
在 `configs/config.yaml` 中配置 Token 有效期:
|
||||
通过环境变量配置 Token 有效期:
|
||||
|
||||
```yaml
|
||||
jwt:
|
||||
secret_key: "your-secret-key-here"
|
||||
token_duration: 3600 # JWT 有效期(个人客户,秒)
|
||||
access_token_ttl: 86400 # Access Token 有效期(B端,秒)
|
||||
refresh_token_ttl: 604800 # Refresh Token 有效期(B端,秒)
|
||||
```bash
|
||||
# JWT 配置
|
||||
export JUNHONG_JWT_SECRET_KEY="your-secret-key-here"
|
||||
export JUNHONG_JWT_TOKEN_DURATION="24h" # JWT 有效期(个人客户)
|
||||
export JUNHONG_JWT_ACCESS_TOKEN_TTL="24h" # Access Token 有效期(B端)
|
||||
export JUNHONG_JWT_REFRESH_TOKEN_TTL="168h" # Refresh Token 有效期(B端,7天)
|
||||
```
|
||||
|
||||
详细配置说明见 [环境变量配置文档](environment-variables.md)
|
||||
|
||||
---
|
||||
|
||||
## 在路由中集成认证
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
│ │ │
|
||||
│ ┌───────────▼─────────────────────┐ │
|
||||
│ │ 服务运行中 │ │
|
||||
│ │ - API: 0.0.0.0:8088 │ │
|
||||
│ │ - API: 0.0.0.0:3000 │ │
|
||||
│ │ - Worker: 后台任务处理 │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└───────────────────────────────────────┘
|
||||
@@ -98,81 +98,34 @@ docker ps | grep runner
|
||||
mkdir -p /home/qycard001/app/junhong_cmp
|
||||
cd /home/qycard001/app/junhong_cmp
|
||||
|
||||
# 创建必要的子目录
|
||||
mkdir -p configs logs
|
||||
# 创建日志目录(配置已嵌入二进制文件,无需 configs 目录)
|
||||
mkdir -p logs
|
||||
```
|
||||
|
||||
### 1.3 准备配置文件
|
||||
### 1.3 配置说明
|
||||
|
||||
#### 创建 `.env` 文件(数据库迁移配置)
|
||||
系统使用**嵌入式配置 + 环境变量覆盖**机制:
|
||||
|
||||
- 默认配置已编译在二进制文件中
|
||||
- 通过 `docker-compose.prod.yml` 中的环境变量覆盖配置
|
||||
- 环境变量前缀:`JUNHONG_`
|
||||
- 格式:`JUNHONG_{配置路径}`,路径分隔符用下划线替代点号
|
||||
|
||||
**无需手动创建配置文件**,所有配置在 `docker-compose.prod.yml` 的 `environment` 中管理。
|
||||
|
||||
### 1.4 部署文件
|
||||
|
||||
`docker-compose.prod.yml` 由 CI/CD 自动从代码仓库复制到部署目录,无需手动操作。
|
||||
|
||||
如需手动部署,可从代码仓库复制:
|
||||
|
||||
```bash
|
||||
cat > /home/qycard001/app/junhong_cmp/.env << 'EOF'
|
||||
MIGRATIONS_DIR=migrations
|
||||
DB_HOST=cxd.whcxd.cn
|
||||
DB_PORT=16159
|
||||
DB_USER=erp_pgsql
|
||||
DB_PASSWORD=erp_2025
|
||||
DB_NAME=junhong_cmp_test
|
||||
DB_SSLMODE=disable
|
||||
EOF
|
||||
```
|
||||
|
||||
#### 创建 `configs/config.yaml`(应用配置)
|
||||
|
||||
```bash
|
||||
cat > /home/qycard001/app/junhong_cmp/configs/config.yaml << 'EOF'
|
||||
server:
|
||||
port: 8088
|
||||
read_timeout: 60
|
||||
write_timeout: 60
|
||||
|
||||
database:
|
||||
host: cxd.whcxd.cn
|
||||
port: 16159
|
||||
user: erp_pgsql
|
||||
password: erp_2025
|
||||
dbname: junhong_cmp_test
|
||||
sslmode: disable
|
||||
max_open_conns: 100
|
||||
max_idle_conns: 10
|
||||
|
||||
redis:
|
||||
host: 你的Redis地址
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
logging:
|
||||
level: info
|
||||
output: logs/app.log
|
||||
max_size: 100
|
||||
max_backups: 7
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
middleware:
|
||||
enable_rate_limiter: false
|
||||
EOF
|
||||
```
|
||||
|
||||
**重要**:将 `你的Redis地址` 替换为实际的 Redis 地址。
|
||||
|
||||
### 1.4 复制部署文件
|
||||
|
||||
从代码仓库复制 `docker-compose.prod.yml` 到服务器:
|
||||
|
||||
```bash
|
||||
# 在服务器上执行
|
||||
cd /home/qycard001/app/junhong_cmp
|
||||
|
||||
# 方式1: 使用 Git(推荐)
|
||||
# 方式1: 使用 Git
|
||||
git clone <你的仓库地址> temp
|
||||
cp temp/docker-compose.prod.yml ./docker-compose.prod.yml
|
||||
rm -rf temp
|
||||
|
||||
# 方式2: 从本地上传
|
||||
# 在本地执行:
|
||||
# scp -P 52022 docker-compose.prod.yml qycard001@47.111.166.169:/home/qycard001/app/junhong_cmp/
|
||||
```
|
||||
|
||||
@@ -251,7 +204,7 @@ docker-compose -f docker-compose.prod.yml logs -f
|
||||
|
||||
```bash
|
||||
# 测试 API 健康检查
|
||||
curl http://localhost:8088/health
|
||||
curl http://localhost:3000/health
|
||||
|
||||
# 预期输出:
|
||||
# {"code":0,"msg":"ok","data":{"status":"healthy"},"timestamp":1234567890}
|
||||
@@ -397,9 +350,9 @@ docker system prune -a -f --volumes
|
||||
|
||||
**排查步骤**:
|
||||
1. 查看容器日志:`docker-compose -f docker-compose.prod.yml logs api`
|
||||
2. 检查配置文件是否正确(数据库连接、Redis 连接)
|
||||
2. 检查 `docker-compose.prod.yml` 中的环境变量配置是否正确(数据库连接、Redis 连接)
|
||||
3. 确认外部依赖(PostgreSQL、Redis)是否可访问
|
||||
4. 手动测试健康检查:`curl http://localhost:8088/health`
|
||||
4. 手动测试健康检查:`curl http://localhost:3000/health`
|
||||
|
||||
### Q2: 数据库迁移失败
|
||||
|
||||
@@ -470,7 +423,7 @@ docker restart docker-runner-01
|
||||
```bash
|
||||
# 仅开放必要端口
|
||||
sudo ufw allow 52022/tcp # SSH
|
||||
sudo ufw allow 8088/tcp # API(如果需要外部访问)
|
||||
sudo ufw allow 3000/tcp # API(如果需要外部访问)
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
@@ -480,7 +433,7 @@ docker restart docker-runner-01
|
||||
|
||||
4. **备份策略**:
|
||||
- 定期备份数据库
|
||||
- 定期备份配置文件(`.env`、`config.yaml`)
|
||||
- 定期备份 `docker-compose.prod.yml`(包含所有配置)
|
||||
|
||||
---
|
||||
|
||||
|
||||
212
docs/environment-variables.md
Normal file
212
docs/environment-variables.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# 环境变量配置文档
|
||||
|
||||
## 概述
|
||||
|
||||
君鸿卡管系统使用嵌入式配置机制,默认配置编译在二进制文件中,通过环境变量进行覆盖。
|
||||
|
||||
**环境变量前缀**: `JUNHONG_`
|
||||
**格式规则**: 配置路径中的 `.` 替换为 `_`,全部大写
|
||||
|
||||
## 必填配置
|
||||
|
||||
以下配置没有合理的默认值,必须通过环境变量设置:
|
||||
|
||||
### 数据库配置
|
||||
|
||||
| 环境变量 | 说明 | 示例 |
|
||||
|---------|------|------|
|
||||
| `JUNHONG_DATABASE_HOST` | 数据库主机地址 | `localhost` |
|
||||
| `JUNHONG_DATABASE_PORT` | 数据库端口 | `5432` |
|
||||
| `JUNHONG_DATABASE_USER` | 数据库用户名 | `postgres` |
|
||||
| `JUNHONG_DATABASE_PASSWORD` | 数据库密码 | `your_password` |
|
||||
| `JUNHONG_DATABASE_DBNAME` | 数据库名称 | `junhong_cmp` |
|
||||
|
||||
### Redis 配置
|
||||
|
||||
| 环境变量 | 说明 | 示例 |
|
||||
|---------|------|------|
|
||||
| `JUNHONG_REDIS_ADDRESS` | Redis 主机地址 | `localhost` |
|
||||
|
||||
### JWT 配置
|
||||
|
||||
| 环境变量 | 说明 | 示例 |
|
||||
|---------|------|------|
|
||||
| `JUNHONG_JWT_SECRET_KEY` | JWT 签名密钥(生产环境必须修改) | `your-secret-key` |
|
||||
|
||||
## 可选配置
|
||||
|
||||
以下配置有合理的默认值,可按需覆盖:
|
||||
|
||||
### 服务器配置
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_SERVER_ADDRESS` | `:3000` | 服务监听地址 |
|
||||
| `JUNHONG_SERVER_READ_TIMEOUT` | `30s` | 读取超时时间 |
|
||||
| `JUNHONG_SERVER_WRITE_TIMEOUT` | `30s` | 写入超时时间 |
|
||||
| `JUNHONG_SERVER_SHUTDOWN_TIMEOUT` | `30s` | 优雅关闭超时 |
|
||||
| `JUNHONG_SERVER_PREFORK` | `false` | 是否启用预分叉模式 |
|
||||
|
||||
### 数据库连接池
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_DATABASE_SSLMODE` | `disable` | SSL 模式 |
|
||||
| `JUNHONG_DATABASE_MAX_OPEN_CONNS` | `25` | 最大打开连接数 |
|
||||
| `JUNHONG_DATABASE_MAX_IDLE_CONNS` | `10` | 最大空闲连接数 |
|
||||
| `JUNHONG_DATABASE_CONN_MAX_LIFETIME` | `1h` | 连接最大生命周期 |
|
||||
|
||||
### Redis 配置
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_REDIS_PORT` | `6379` | Redis 端口 |
|
||||
| `JUNHONG_REDIS_PASSWORD` | `""` | Redis 密码 |
|
||||
| `JUNHONG_REDIS_DB` | `0` | Redis 数据库编号 |
|
||||
| `JUNHONG_REDIS_POOL_SIZE` | `100` | 连接池大小 |
|
||||
| `JUNHONG_REDIS_MIN_IDLE_CONNS` | `10` | 最小空闲连接数 |
|
||||
| `JUNHONG_REDIS_DIAL_TIMEOUT` | `5s` | 连接超时 |
|
||||
| `JUNHONG_REDIS_READ_TIMEOUT` | `3s` | 读取超时 |
|
||||
| `JUNHONG_REDIS_WRITE_TIMEOUT` | `3s` | 写入超时 |
|
||||
|
||||
### 日志配置
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_LOGGING_LEVEL` | `info` | 日志级别 (debug/info/warn/error) |
|
||||
| `JUNHONG_LOGGING_DEVELOPMENT` | `false` | 开发模式(启用彩色输出) |
|
||||
| `JUNHONG_LOGGING_APP_LOG_FILENAME` | `logs/app.log` | 应用日志文件路径 |
|
||||
| `JUNHONG_LOGGING_APP_LOG_MAX_SIZE` | `100` | 日志文件最大大小 (MB) |
|
||||
| `JUNHONG_LOGGING_APP_LOG_MAX_BACKUPS` | `7` | 最大备份文件数 |
|
||||
| `JUNHONG_LOGGING_APP_LOG_MAX_AGE` | `30` | 日志保留天数 |
|
||||
| `JUNHONG_LOGGING_APP_LOG_COMPRESS` | `true` | 是否压缩旧日志 |
|
||||
| `JUNHONG_LOGGING_ACCESS_LOG_FILENAME` | `logs/access.log` | 访问日志文件路径 |
|
||||
|
||||
### JWT 配置
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_JWT_TOKEN_DURATION` | `24h` | Token 有效期 |
|
||||
| `JUNHONG_JWT_ACCESS_TOKEN_TTL` | `24h` | Access Token TTL |
|
||||
| `JUNHONG_JWT_REFRESH_TOKEN_TTL` | `168h` | Refresh Token TTL (7天) |
|
||||
|
||||
### 队列配置
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_QUEUE_CONCURRENCY` | `10` | 并发 Worker 数量 |
|
||||
| `JUNHONG_QUEUE_RETRY_MAX` | `3` | 最大重试次数 |
|
||||
| `JUNHONG_QUEUE_TIMEOUT` | `30m` | 任务超时时间 |
|
||||
|
||||
### 限流中间件
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_MIDDLEWARE_ENABLE_RATE_LIMITER` | `false` | 启用限流 |
|
||||
| `JUNHONG_MIDDLEWARE_RATE_LIMITER_MAX` | `100` | 最大请求数 |
|
||||
| `JUNHONG_MIDDLEWARE_RATE_LIMITER_EXPIRATION` | `1m` | 时间窗口 |
|
||||
| `JUNHONG_MIDDLEWARE_RATE_LIMITER_STORAGE` | `memory` | 存储后端 (memory/redis) |
|
||||
|
||||
### 对象存储配置
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_STORAGE_PROVIDER` | `""` | 存储提供商 (s3) |
|
||||
| `JUNHONG_STORAGE_TEMP_DIR` | `/tmp/junhong` | 临时文件目录 |
|
||||
| `JUNHONG_STORAGE_S3_ENDPOINT` | `""` | S3 端点 |
|
||||
| `JUNHONG_STORAGE_S3_REGION` | `""` | S3 区域 |
|
||||
| `JUNHONG_STORAGE_S3_BUCKET` | `""` | S3 存储桶 |
|
||||
| `JUNHONG_STORAGE_S3_ACCESS_KEY_ID` | `""` | S3 访问密钥 ID |
|
||||
| `JUNHONG_STORAGE_S3_SECRET_ACCESS_KEY` | `""` | S3 访问密钥 |
|
||||
| `JUNHONG_STORAGE_S3_USE_SSL` | `true` | 是否使用 SSL |
|
||||
| `JUNHONG_STORAGE_S3_PATH_STYLE` | `true` | 是否使用路径风格 |
|
||||
| `JUNHONG_STORAGE_PRESIGN_UPLOAD_EXPIRES` | `1h` | 预签名上传 URL 有效期 |
|
||||
| `JUNHONG_STORAGE_PRESIGN_DOWNLOAD_EXPIRES` | `1h` | 预签名下载 URL 有效期 |
|
||||
|
||||
### 短信配置
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_SMS_GATEWAY_URL` | `""` | 短信网关 URL |
|
||||
| `JUNHONG_SMS_USERNAME` | `""` | 短信账号 |
|
||||
| `JUNHONG_SMS_PASSWORD` | `""` | 短信密码 |
|
||||
| `JUNHONG_SMS_SIGNATURE` | `""` | 短信签名 |
|
||||
| `JUNHONG_SMS_TIMEOUT` | `10s` | 请求超时 |
|
||||
|
||||
### 默认管理员
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `JUNHONG_DEFAULT_ADMIN_USERNAME` | `admin` | 默认管理员用户名 |
|
||||
| `JUNHONG_DEFAULT_ADMIN_PASSWORD` | `Admin@123456` | 默认管理员密码 |
|
||||
| `JUNHONG_DEFAULT_ADMIN_PHONE` | `13800000000` | 默认管理员手机号 |
|
||||
|
||||
## Docker Compose 示例
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
api:
|
||||
image: registry.boss160.cn/junhong/cmp-fiber-api:latest
|
||||
environment:
|
||||
- JUNHONG_DATABASE_HOST=postgres
|
||||
- JUNHONG_DATABASE_PORT=5432
|
||||
- JUNHONG_DATABASE_USER=junhong
|
||||
- JUNHONG_DATABASE_PASSWORD=secret123
|
||||
- JUNHONG_DATABASE_DBNAME=junhong_cmp
|
||||
- JUNHONG_REDIS_ADDRESS=redis
|
||||
- JUNHONG_JWT_SECRET_KEY=your-production-secret-key
|
||||
- JUNHONG_LOGGING_LEVEL=info
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
worker:
|
||||
image: registry.boss160.cn/junhong/cmp-fiber-worker:latest
|
||||
environment:
|
||||
- JUNHONG_DATABASE_HOST=postgres
|
||||
- JUNHONG_DATABASE_PORT=5432
|
||||
- JUNHONG_DATABASE_USER=junhong
|
||||
- JUNHONG_DATABASE_PASSWORD=secret123
|
||||
- JUNHONG_DATABASE_DBNAME=junhong_cmp
|
||||
- JUNHONG_REDIS_ADDRESS=redis
|
||||
- JUNHONG_JWT_SECRET_KEY=your-production-secret-key
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
|
||||
postgres:
|
||||
image: postgres:14
|
||||
environment:
|
||||
- POSTGRES_USER=junhong
|
||||
- POSTGRES_PASSWORD=secret123
|
||||
- POSTGRES_DB=junhong_cmp
|
||||
|
||||
redis:
|
||||
image: redis:6
|
||||
```
|
||||
|
||||
## 本地开发
|
||||
|
||||
本地开发可以创建 `.env` 文件(不要提交到 Git):
|
||||
|
||||
```bash
|
||||
# .env
|
||||
JUNHONG_DATABASE_HOST=localhost
|
||||
JUNHONG_DATABASE_PORT=5432
|
||||
JUNHONG_DATABASE_USER=postgres
|
||||
JUNHONG_DATABASE_PASSWORD=postgres
|
||||
JUNHONG_DATABASE_DBNAME=junhong_cmp_dev
|
||||
JUNHONG_REDIS_ADDRESS=localhost
|
||||
JUNHONG_JWT_SECRET_KEY=dev-secret-key
|
||||
JUNHONG_LOGGING_LEVEL=debug
|
||||
JUNHONG_LOGGING_DEVELOPMENT=true
|
||||
```
|
||||
|
||||
然后使用 `source .env` 加载环境变量后运行:
|
||||
|
||||
```bash
|
||||
source .env
|
||||
go run cmd/api/main.go
|
||||
```
|
||||
163
docs/object-storage/使用指南.md
Normal file
163
docs/object-storage/使用指南.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 对象存储使用指南
|
||||
|
||||
本文档介绍如何在后端代码中使用对象存储服务。
|
||||
|
||||
## 配置
|
||||
|
||||
通过环境变量配置对象存储:
|
||||
|
||||
```bash
|
||||
# 存储提供商
|
||||
export JUNHONG_STORAGE_PROVIDER="s3"
|
||||
|
||||
# S3 配置
|
||||
export JUNHONG_STORAGE_S3_ENDPOINT="http://obs-helf.cucloud.cn"
|
||||
export JUNHONG_STORAGE_S3_REGION="cn-langfang-2"
|
||||
export JUNHONG_STORAGE_S3_BUCKET="cmp"
|
||||
export JUNHONG_STORAGE_S3_ACCESS_KEY_ID="YOUR_ACCESS_KEY"
|
||||
export JUNHONG_STORAGE_S3_SECRET_ACCESS_KEY="YOUR_SECRET_KEY"
|
||||
export JUNHONG_STORAGE_S3_USE_SSL="false"
|
||||
export JUNHONG_STORAGE_S3_PATH_STYLE="true"
|
||||
|
||||
# 预签名 URL 配置
|
||||
export JUNHONG_STORAGE_PRESIGN_UPLOAD_EXPIRES="15m"
|
||||
export JUNHONG_STORAGE_PRESIGN_DOWNLOAD_EXPIRES="24h"
|
||||
|
||||
# 临时文件目录
|
||||
export JUNHONG_STORAGE_TEMP_DIR="/tmp/junhong-storage"
|
||||
```
|
||||
|
||||
详细配置说明见 [环境变量配置文档](../environment-variables.md)
|
||||
|
||||
## StorageService 使用
|
||||
|
||||
### 获取预签名上传 URL
|
||||
|
||||
```go
|
||||
result, err := storageService.GetUploadURL(ctx, "iot_import", "cards.csv", "text/csv")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// result.URL - 预签名上传 URL
|
||||
// result.FileKey - 文件路径(用于后续业务接口)
|
||||
// result.ExpiresIn - URL 有效期(秒)
|
||||
```
|
||||
|
||||
### 下载文件到临时目录
|
||||
|
||||
```go
|
||||
localPath, cleanup, err := storageService.DownloadToTemp(ctx, fileKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup() // 处理完成后自动删除临时文件
|
||||
|
||||
// 使用 localPath 读取文件内容
|
||||
f, _ := os.Open(localPath)
|
||||
defer f.Close()
|
||||
```
|
||||
|
||||
### 直接上传文件
|
||||
|
||||
```go
|
||||
reader := bytes.NewReader(content)
|
||||
err := storageService.Provider().Upload(ctx, fileKey, reader, "text/csv")
|
||||
```
|
||||
|
||||
### 检查文件是否存在
|
||||
|
||||
```go
|
||||
exists, err := storageService.Provider().Exists(ctx, fileKey)
|
||||
```
|
||||
|
||||
### 删除文件
|
||||
|
||||
```go
|
||||
err := storageService.Provider().Delete(ctx, fileKey)
|
||||
```
|
||||
|
||||
## Purpose 类型
|
||||
|
||||
| Purpose | 说明 | 生成路径 | ContentType |
|
||||
|---------|------|---------|-------------|
|
||||
| iot_import | ICCID 导入 | imports/YYYY/MM/DD/uuid.csv | text/csv |
|
||||
| export | 数据导出 | exports/YYYY/MM/DD/uuid.xlsx | application/vnd.openxmlformats... |
|
||||
| attachment | 附件上传 | attachments/YYYY/MM/DD/uuid.ext | 自动检测 |
|
||||
|
||||
## 错误处理
|
||||
|
||||
存储相关错误码定义在 `pkg/errors/codes.go`:
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|-------|------|
|
||||
| 1090 | 对象存储服务未配置 |
|
||||
| 1091 | 文件上传失败 |
|
||||
| 1092 | 文件下载失败 |
|
||||
| 1093 | 文件不存在 |
|
||||
| 1094 | 不支持的文件用途 |
|
||||
| 1095 | 不支持的文件类型 |
|
||||
|
||||
## 在 Handler 中使用
|
||||
|
||||
```go
|
||||
type MyHandler struct {
|
||||
storageService *storage.Service
|
||||
}
|
||||
|
||||
func (h *MyHandler) Upload(c *fiber.Ctx) error {
|
||||
var req dto.GetUploadURLRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "参数解析失败")
|
||||
}
|
||||
|
||||
result, err := h.storageService.GetUploadURL(
|
||||
c.UserContext(),
|
||||
req.Purpose,
|
||||
req.FileName,
|
||||
req.ContentType,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeStorageUploadFailed, err.Error())
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
```
|
||||
|
||||
## 在 Worker 中使用
|
||||
|
||||
```go
|
||||
func (h *TaskHandler) HandleTask(ctx context.Context, task *asynq.Task) error {
|
||||
// 从任务记录获取文件路径
|
||||
fileKey := importTask.StorageKey
|
||||
|
||||
// 下载到临时文件
|
||||
localPath, cleanup, err := h.storageService.DownloadToTemp(ctx, fileKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// 解析文件
|
||||
f, _ := os.Open(localPath)
|
||||
defer f.Close()
|
||||
|
||||
// 处理文件内容...
|
||||
}
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
|
||||
运行对象存储功能测试:
|
||||
|
||||
```bash
|
||||
go run scripts/test_storage.go
|
||||
```
|
||||
|
||||
测试内容包括:
|
||||
1. 生成预签名上传 URL
|
||||
2. 上传测试文件
|
||||
3. 检查文件是否存在
|
||||
4. 下载到临时文件
|
||||
5. 删除测试文件
|
||||
250
docs/object-storage/前端接入指南.md
Normal file
250
docs/object-storage/前端接入指南.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# 对象存储前端接入指南
|
||||
|
||||
## 文件上传流程
|
||||
|
||||
```
|
||||
前端 后端 API 对象存储
|
||||
│ │ │
|
||||
│ 1. POST /storage/upload-url │
|
||||
│ {file_name, content_type, purpose} │
|
||||
│ ─────────────────────────► │
|
||||
│ │ │
|
||||
│ 2. 返回 {upload_url, file_key, expires_in} │
|
||||
│ ◄───────────────────────── │
|
||||
│ │ │
|
||||
│ 3. PUT upload_url (文件内容) │
|
||||
│ ─────────────────────────────────────────────────► │
|
||||
│ │ │
|
||||
│ 4. 上传成功 (200 OK) │
|
||||
│ ◄───────────────────────────────────────────────── │
|
||||
│ │ │
|
||||
│ 5. POST /iot-cards/import │
|
||||
│ {carrier_id, batch_no, file_key} │
|
||||
│ ─────────────────────────► │
|
||||
│ │ │
|
||||
│ 6. 返回任务创建成功 │
|
||||
│ ◄───────────────────────── │
|
||||
```
|
||||
|
||||
## 获取预签名 URL 接口
|
||||
|
||||
### 请求
|
||||
|
||||
```http
|
||||
POST /api/admin/storage/upload-url
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
|
||||
{
|
||||
"file_name": "cards.csv",
|
||||
"content_type": "text/csv",
|
||||
"purpose": "iot_import"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"upload_url": "http://obs-helf.cucloud.cn/cmp/imports/2025/01/24/abc123.csv?X-Amz-Algorithm=...",
|
||||
"file_key": "imports/2025/01/24/abc123.csv",
|
||||
"expires_in": 900
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### purpose 可选值
|
||||
|
||||
| 值 | 说明 | 生成路径 |
|
||||
|---|------|---------|
|
||||
| iot_import | ICCID 导入 | imports/YYYY/MM/DD/uuid.csv |
|
||||
| export | 数据导出 | exports/YYYY/MM/DD/uuid.xlsx |
|
||||
| attachment | 附件上传 | attachments/YYYY/MM/DD/uuid.ext |
|
||||
|
||||
## 使用预签名 URL 上传文件
|
||||
|
||||
获取到 `upload_url` 后,直接使用 PUT 请求上传文件到对象存储:
|
||||
|
||||
```javascript
|
||||
const response = await fetch(upload_url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': content_type
|
||||
},
|
||||
body: file
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('上传成功');
|
||||
} else {
|
||||
console.error('上传失败:', response.status);
|
||||
}
|
||||
```
|
||||
|
||||
## ICCID 导入接口变更(BREAKING CHANGE)
|
||||
|
||||
### 变更前
|
||||
|
||||
```http
|
||||
POST /api/admin/iot-cards/import
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
carrier_id=1
|
||||
batch_no=BATCH-2025-01
|
||||
file=@cards.csv
|
||||
```
|
||||
|
||||
### 变更后
|
||||
|
||||
```http
|
||||
POST /api/admin/iot-cards/import
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
|
||||
{
|
||||
"carrier_id": 1,
|
||||
"batch_no": "BATCH-2025-01",
|
||||
"file_key": "imports/2025/01/24/abc123.csv"
|
||||
}
|
||||
```
|
||||
|
||||
## 完整代码示例(TypeScript)
|
||||
|
||||
```typescript
|
||||
interface UploadURLResponse {
|
||||
upload_url: string;
|
||||
file_key: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
async function uploadAndImportCards(
|
||||
file: File,
|
||||
carrierId: number,
|
||||
batchNo: string
|
||||
): Promise<void> {
|
||||
// 1. 获取预签名上传 URL
|
||||
const urlResponse = await fetch('/api/admin/storage/upload-url', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${getToken()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_name: file.name,
|
||||
content_type: file.type || 'text/csv',
|
||||
purpose: 'iot_import'
|
||||
})
|
||||
});
|
||||
|
||||
if (!urlResponse.ok) {
|
||||
throw new Error('获取上传 URL 失败');
|
||||
}
|
||||
|
||||
const { data } = await urlResponse.json();
|
||||
const { upload_url, file_key } = data as UploadURLResponse;
|
||||
|
||||
// 2. 上传文件到对象存储
|
||||
const uploadResponse = await fetch(upload_url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': file.type || 'text/csv'
|
||||
},
|
||||
body: file
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error('文件上传失败');
|
||||
}
|
||||
|
||||
// 3. 调用导入接口
|
||||
const importResponse = await fetch('/api/admin/iot-cards/import', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${getToken()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
carrier_id: carrierId,
|
||||
batch_no: batchNo,
|
||||
file_key: file_key
|
||||
})
|
||||
});
|
||||
|
||||
if (!importResponse.ok) {
|
||||
throw new Error('导入任务创建失败');
|
||||
}
|
||||
|
||||
console.log('导入任务已创建');
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理和重试策略
|
||||
|
||||
### 预签名 URL 过期
|
||||
|
||||
预签名 URL 有效期为 15 分钟。如果上传时 URL 已过期,需要重新获取:
|
||||
|
||||
```typescript
|
||||
async function uploadWithRetry(file: File, purpose: string, maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
const { upload_url, file_key } = await getUploadURL(file.name, file.type, purpose);
|
||||
|
||||
try {
|
||||
await uploadFile(upload_url, file);
|
||||
return file_key;
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) throw error;
|
||||
console.warn(`上传失败,重试 ${i + 1}/${maxRetries}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 网络错误
|
||||
|
||||
对象存储上传可能因网络问题失败,建议实现重试机制:
|
||||
|
||||
```typescript
|
||||
async function uploadFile(url: string, file: File, retries = 3) {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': file.type },
|
||||
body: file
|
||||
});
|
||||
|
||||
if (response.ok) return;
|
||||
|
||||
if (response.status >= 500) {
|
||||
// 服务端错误,可重试
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`上传失败: ${response.status}`);
|
||||
} catch (error) {
|
||||
if (i === retries - 1) throw error;
|
||||
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 上传时报 CORS 错误
|
||||
|
||||
确保对象存储已配置 CORS 规则允许前端域名访问。
|
||||
|
||||
### Q: 预签名 URL 无法使用
|
||||
|
||||
1. 检查 URL 是否过期(15 分钟有效期)
|
||||
2. 确保 Content-Type 与获取 URL 时指定的一致
|
||||
3. 检查文件大小是否超过限制
|
||||
|
||||
### Q: file_key 可以重复使用吗
|
||||
|
||||
可以。file_key 一旦上传成功就永久有效,可以在多个业务接口中使用同一个 file_key。
|
||||
Reference in New Issue
Block a user