init
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m3s

This commit is contained in:
Break
2026-06-16 15:15:50 +08:00
parent 84ab3bad99
commit 1f634eb465
21 changed files with 2776 additions and 41 deletions

View File

@@ -204,7 +204,68 @@ components:
description: 当前生效套餐已用流量(MB)
type: integer
card_no:
description: 卡标识
description: 卡标识(原始传入值)
type: string
device_no:
description: 设备虚拟号(当入参被识别为设备维度时返回,表示该卡/标识关联的设备)
type: string
pending_packages:
description: 待生效套餐列表
items:
$ref: '#/components/schemas/DtoAgentOpenAPIPackageTrafficItem'
nullable: true
type: array
type: object
DtoAgentOpenAPIDeviceOperationRequest:
properties:
device_no:
description: 设备标识支持虚拟号、IMEI
maxLength: 100
minLength: 1
type: string
required:
- device_no
type: object
DtoAgentOpenAPIDeviceSwitchCardRequest:
properties:
device_no:
description: 设备标识支持虚拟号、IMEI
maxLength: 100
minLength: 1
type: string
iccid:
description: 目标卡 ICCID
maxLength: 100
minLength: 1
type: string
required:
- device_no
- iccid
type: object
DtoAgentOpenAPIDeviceTrafficResponse:
properties:
active_expires_at:
description: 当前生效套餐最晚过期时间
format: date-time
nullable: true
type: string
active_packages:
description: 当前生效套餐列表
items:
$ref: '#/components/schemas/DtoAgentOpenAPIPackageTrafficItem'
nullable: true
type: array
active_remaining_flow_mb:
description: 当前生效套餐剩余流量(MB)
type: integer
active_total_flow_mb:
description: 当前生效套餐总流量(MB)
type: integer
active_used_flow_mb:
description: 当前生效套餐已用流量(MB)
type: integer
device_no:
description: 设备标识
type: string
pending_packages:
description: 待生效套餐列表
@@ -1269,7 +1330,7 @@ components:
minimum: 0
type: integer
paid_amount:
description: 购买实付金额(分),线下支付或无订单分配时为空
description: 购买成本价(分),仅平台账号可见,非平台账号不返回此字段
nullable: true
type: integer
priority:
@@ -1292,6 +1353,10 @@ components:
refund_no:
description: 退款单号快照(未退款时为空)
type: string
retail_amount:
description: 购买零售价(分),无订单关联时为空
nullable: true
type: integer
status:
description: 状态0待生效 1生效中 2已用完 3已过期 4已失效
type: integer
@@ -1679,7 +1744,7 @@ components:
nullable: true
type: string
reference_type:
description: 关联业务类型rechargeorder可空
description: 关联业务类型rechargeorder 或 exchange(可空)
nullable: true
type: string
remark:
@@ -1687,10 +1752,10 @@ components:
nullable: true
type: string
transaction_type:
description: 交易类型recharge/deduct/refund
description: 交易类型recharge/deduct/refund/exchange
type: string
transaction_type_text:
description: 交易类型文本:充值/扣款/退款
description: 交易类型文本:充值/扣款/退款/换货迁移
type: string
type: object
DtoAssetWalletTransactionListResponse:
@@ -2468,6 +2533,9 @@ components:
exchange_reason:
description: 换货原因
type: string
flow_type:
description: 换货流程类型 (shipping:物流换货, direct:直接换货)
type: string
id:
description: 换货单ID
minimum: 0
@@ -2475,6 +2543,9 @@ components:
status:
description: 换货状态 (1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)
type: integer
status_name:
description: 换货状态名称(中文)
type: string
status_text:
description: 换货状态文本
type: string
@@ -3148,6 +3219,21 @@ components:
maxLength: 100
minLength: 1
type: string
flow_type:
description: 换货流程类型 (shipping:物流换货, direct:直接换货)
enum:
- shipping
- direct
type: string
migrate_data:
description: 是否执行全量迁移direct 流程未传按 false 处理
nullable: true
type: boolean
new_identifier:
description: 新资产标识符direct 流程必填(ICCID/虚拟号/IMEI/SN)
maxLength: 100
minLength: 1
type: string
old_asset_type:
description: 旧资产类型 (iot_card:物联网卡, device:设备)
type: string
@@ -4895,6 +4981,11 @@ components:
type: object
DtoExchangeOrderResponse:
properties:
completed_at:
description: 换货完成时间
format: date-time
nullable: true
type: string
created_at:
description: 创建时间
format: date-time
@@ -4920,6 +5011,12 @@ components:
express_no:
description: 快递单号
type: string
flow_type:
description: 换货流程类型 (shipping:物流换货, direct:直接换货)
type: string
flow_type_name:
description: 换货流程类型名称
type: string
id:
description: 换货单ID
minimum: 0
@@ -4967,6 +5064,11 @@ components:
description: 备注
nullable: true
type: string
shipped_at:
description: 发货时间
format: date-time
nullable: true
type: string
shop_id:
description: 所属店铺ID
minimum: 0
@@ -4975,6 +5077,9 @@ components:
status:
description: 换货状态 (1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)
type: integer
status_name:
description: 换货状态名称(中文)
type: string
status_text:
description: 换货状态文本
type: string
@@ -8118,6 +8223,24 @@ components:
nullable: true
type: string
type: object
DtoUpdateAssetPackageExpiresAtRequest:
properties:
expires_at:
description: 新的套餐过期时间格式YYYY-MM-DD HH:MM:SS 或 RFC3339
type: string
required:
- expires_at
type: object
DtoUpdateAssetPackageUsageRequest:
properties:
data_usage_mb:
description: 新的套餐真实已用量(MB)
minimum: 0
nullable: true
type: integer
required:
- data_usage_mb
type: object
DtoUpdateAssetPollingStatusRequest:
properties:
enable_polling:
@@ -10975,6 +11098,166 @@ paths:
summary: 资产套餐列表
tags:
- 资产管理
/api/admin/assets/{identifier}/packages/{package_usage_id}/expires-at:
patch:
description: 修改指定资产下某条套餐使用记录的过期时间。仅账号ID为2的账号可调用。
parameters:
- description: 资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN
in: path
name: identifier
required: true
schema:
description: 资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN
type: string
- description: 套餐使用记录ID
in: path
name: package_usage_id
required: true
schema:
description: 套餐使用记录ID
minimum: 0
type: integer
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DtoUpdateAssetPackageExpiresAtRequest'
responses:
"200":
content:
application/json:
schema:
properties:
code:
description: 响应码
example: 0
type: integer
data:
$ref: '#/components/schemas/DtoAssetPackageResponse'
msg:
description: 响应消息
example: success
type: string
timestamp:
description: 时间戳
format: date-time
type: string
required:
- code
- msg
- data
- timestamp
type: object
description: 成功
"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/assets/{identifier}/packages/{package_usage_id}/used-data:
patch:
description: 修改指定资产下某条套餐使用记录的真实已用量(MB)。仅账号ID为2的账号可调用。
parameters:
- description: 资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN
in: path
name: identifier
required: true
schema:
description: 资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN
type: string
- description: 套餐使用记录ID
in: path
name: package_usage_id
required: true
schema:
description: 套餐使用记录ID
minimum: 0
type: integer
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DtoUpdateAssetPackageUsageRequest'
responses:
"200":
content:
application/json:
schema:
properties:
code:
description: 响应码
example: 0
type: integer
data:
$ref: '#/components/schemas/DtoAssetPackageResponse'
msg:
description: 响应消息
example: success
type: string
timestamp:
description: 时间戳
format: date-time
type: string
required:
- code
- msg
- data
- timestamp
type: object
description: 成功
"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/assets/{identifier}/polling-status:
patch:
description: 启用或禁用指定资产IoT卡或设备的定期状态轮询。
@@ -11490,11 +11773,11 @@ paths:
schema:
description: 每页数量默认20最大100
type: integer
- description: 交易类型过滤recharge/deduct/refund
- description: 交易类型过滤recharge/deduct/refund/exchange
in: query
name: transaction_type
schema:
description: 交易类型过滤recharge/deduct/refund
description: 交易类型过滤recharge/deduct/refund/exchange
nullable: true
type: string
- description: 开始时间RFC3339
@@ -13399,6 +13682,13 @@ paths:
description: 是否有生效中的套餐true:有生效中主套餐, false:无生效中主套餐)
nullable: true
type: boolean
- description: 关键字搜索匹配虚拟号或IMEI(模糊查询与virtual_no/imei独立)
in: query
name: keyword
schema:
description: 关键字搜索匹配虚拟号或IMEI(模糊查询与virtual_no/imei独立)
maxLength: 100
type: string
responses:
"200":
content:
@@ -15499,6 +15789,15 @@ paths:
minimum: 1
nullable: true
type: integer
- description: 换货流程类型 (shipping:物流换货, direct:直接换货)
in: query
name: flow_type
schema:
description: 换货流程类型 (shipping:物流换货, direct:直接换货)
enum:
- shipping
- direct
type: string
- description: 资产标识符搜索(旧资产/新资产标识符模糊匹配)
in: query
name: identifier
@@ -16772,6 +17071,13 @@ paths:
description: 运营商名称(模糊查询)
maxLength: 100
type: string
- description: 关键字搜索匹配ICCID或卡虚拟号(模糊查询与iccid/virtual_no独立)
in: query
name: keyword
schema:
description: 关键字搜索匹配ICCID或卡虚拟号(模糊查询与iccid/virtual_no独立)
maxLength: 100
type: string
responses:
"200":
content:
@@ -26812,7 +27118,7 @@ paths:
}
```
按 card_no 查询单卡流量、当前套餐和待生效套餐。card_no 支持 ICCID虚拟号MSISDN。
按 card_no 查询流量、当前套餐和待生效套餐。card_no 支持三种解析路径1) ICCID/虚拟号/MSISDN 对应独立卡查卡维度流量2) 已绑定设备的卡,自动反查绑定设备,查设备维度流量(响应含 device_no3) IMEI 或设备虚拟号,直接查设备维度流量(响应含 device_no
parameters:
- description: 卡标识(支持 ICCID、虚拟号、MSISDN
in: query
@@ -26880,7 +27186,462 @@ paths:
AgentOpenAPIPassword: []
AgentOpenAPISign: []
AgentOpenAPITimestamp: []
summary: 查询单卡流量
summary: 查询单卡或设备流量
tags:
- 代理开放接口
/api/open/v1/devices/reboot:
post:
description: |-
认证方式:
1. 每个请求必须携带 HeaderX-Agent-Account、X-Agent-Password、X-Agent-Timestamp、X-Agent-Nonce、X-Agent-Sign。
2. Header 字段说明:
- X-Agent-Account代理账号标识填写代理登录平台使用的用户名或手机号例如 agent001该值由平台开通代理账号时提供必须与签名原文 account 行完全一致。
- X-Agent-Password代理账号当前登录密码由代理账号持有人提供该值同时作为 HMAC-SHA256 签名密钥,密码变更后必须使用新密码签名。
- X-Agent-Timestamp请求发起时间由调用方生成支持 Unix 秒、Unix 毫秒或 RFC3339 时间,例如 1715400000、1715400000000 或 2024-05-11T10:00:00+08:00必须与签名原文 timestamp 行完全一致。
- X-Agent-Nonce请求随机串由调用方每次请求自行生成例如 UUID、雪花 ID 或“时间戳+随机数”;不需要平台提前分配,同一账号在 5 分钟内不可重复。
- X-Agent-Sign请求签名由调用方按下方规则实时计算得到不是平台分配的固定值只要 method、path、query、body、timestamp、nonce、account 或 password 任意一个变化,签名都会变化。
3. X-Agent-Timestamp 超出允许时间窗口会认证失败;客户端服务器时间应保持同步。
4. X-Agent-Nonce 重复会被判定为重放请求。
5. 签名原文按 7 行拼接,行尾使用 \n最后一行后不追加换行
METHOD
PATH
canonical_query
body_sha256
timestamp
nonce
account
6. METHOD 使用大写 HTTP 方法PATH 只取请求路径,例如 /api/open/v1/cards/status不包含域名和 query。
7. canonical_query 为 query 参数规范化结果:排除 sign 字段参数名升序同名多值按值升序key 和 value 使用 URL QueryEscape 编码后用 key=value 拼接,多个参数用 & 连接;无 query 时为空字符串。
8. body_sha256 为原始请求 body 字节的 SHA256 小写十六进制值GET 或空 body 使用空字符串的 SHA256POST JSON 请求必须用最终发送的 body 字符串计算,签名和请求发送的 body 必须完全一致。
9. X-Agent-Sign = hex(HMAC-SHA256(password, sign_payload))password 为 X-Agent-Password 的原始值,结果使用小写十六进制。
Node.js 签名示例:
```javascript
const crypto = require("crypto");
function queryEscape(value) {
return new URLSearchParams({ v: String(value) }).toString().slice(2);
}
function canonicalQuery(params) {
return Object.entries(params || {})
.filter(([key]) => key.toLowerCase() !== "sign")
.flatMap(([key, value]) => {
const values = Array.isArray(value) ? value : [value];
return values.sort().map((item) => [key, item]);
})
.sort(([aKey, aValue], [bKey, bValue]) => {
if (aKey === bKey) return String(aValue).localeCompare(String(bValue));
return aKey.localeCompare(bKey);
})
.map(([key, value]) => queryEscape(key) + "=" + queryEscape(value))
.join("&");
}
function buildAgentSign({ method, path, query, body, timestamp, nonce, account, password }) {
const bodyText = body || "";
const bodyHash = crypto.createHash("sha256").update(bodyText).digest("hex");
const signPayload = [
method.toUpperCase(),
path,
canonicalQuery(query),
bodyHash,
timestamp,
nonce,
account,
].join("\n");
return crypto.createHmac("sha256", password).update(signPayload).digest("hex");
}
```
按 device_no 远程重启设备。
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DtoAgentOpenAPIDeviceOperationRequest'
responses:
"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:
- AgentOpenAPIAccount: []
AgentOpenAPINonce: []
AgentOpenAPIPassword: []
AgentOpenAPISign: []
AgentOpenAPITimestamp: []
summary: 重启设备
tags:
- 代理开放接口
/api/open/v1/devices/reset:
post:
description: |-
认证方式:
1. 每个请求必须携带 HeaderX-Agent-Account、X-Agent-Password、X-Agent-Timestamp、X-Agent-Nonce、X-Agent-Sign。
2. Header 字段说明:
- X-Agent-Account代理账号标识填写代理登录平台使用的用户名或手机号例如 agent001该值由平台开通代理账号时提供必须与签名原文 account 行完全一致。
- X-Agent-Password代理账号当前登录密码由代理账号持有人提供该值同时作为 HMAC-SHA256 签名密钥,密码变更后必须使用新密码签名。
- X-Agent-Timestamp请求发起时间由调用方生成支持 Unix 秒、Unix 毫秒或 RFC3339 时间,例如 1715400000、1715400000000 或 2024-05-11T10:00:00+08:00必须与签名原文 timestamp 行完全一致。
- X-Agent-Nonce请求随机串由调用方每次请求自行生成例如 UUID、雪花 ID 或“时间戳+随机数”;不需要平台提前分配,同一账号在 5 分钟内不可重复。
- X-Agent-Sign请求签名由调用方按下方规则实时计算得到不是平台分配的固定值只要 method、path、query、body、timestamp、nonce、account 或 password 任意一个变化,签名都会变化。
3. X-Agent-Timestamp 超出允许时间窗口会认证失败;客户端服务器时间应保持同步。
4. X-Agent-Nonce 重复会被判定为重放请求。
5. 签名原文按 7 行拼接,行尾使用 \n最后一行后不追加换行
METHOD
PATH
canonical_query
body_sha256
timestamp
nonce
account
6. METHOD 使用大写 HTTP 方法PATH 只取请求路径,例如 /api/open/v1/cards/status不包含域名和 query。
7. canonical_query 为 query 参数规范化结果:排除 sign 字段参数名升序同名多值按值升序key 和 value 使用 URL QueryEscape 编码后用 key=value 拼接,多个参数用 & 连接;无 query 时为空字符串。
8. body_sha256 为原始请求 body 字节的 SHA256 小写十六进制值GET 或空 body 使用空字符串的 SHA256POST JSON 请求必须用最终发送的 body 字符串计算,签名和请求发送的 body 必须完全一致。
9. X-Agent-Sign = hex(HMAC-SHA256(password, sign_payload))password 为 X-Agent-Password 的原始值,结果使用小写十六进制。
Node.js 签名示例:
```javascript
const crypto = require("crypto");
function queryEscape(value) {
return new URLSearchParams({ v: String(value) }).toString().slice(2);
}
function canonicalQuery(params) {
return Object.entries(params || {})
.filter(([key]) => key.toLowerCase() !== "sign")
.flatMap(([key, value]) => {
const values = Array.isArray(value) ? value : [value];
return values.sort().map((item) => [key, item]);
})
.sort(([aKey, aValue], [bKey, bValue]) => {
if (aKey === bKey) return String(aValue).localeCompare(String(bValue));
return aKey.localeCompare(bKey);
})
.map(([key, value]) => queryEscape(key) + "=" + queryEscape(value))
.join("&");
}
function buildAgentSign({ method, path, query, body, timestamp, nonce, account, password }) {
const bodyText = body || "";
const bodyHash = crypto.createHash("sha256").update(bodyText).digest("hex");
const signPayload = [
method.toUpperCase(),
path,
canonicalQuery(query),
bodyHash,
timestamp,
nonce,
account,
].join("\n");
return crypto.createHmac("sha256", password).update(signPayload).digest("hex");
}
```
按 device_no 远程恢复设备出厂设置。
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DtoAgentOpenAPIDeviceOperationRequest'
responses:
"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:
- AgentOpenAPIAccount: []
AgentOpenAPINonce: []
AgentOpenAPIPassword: []
AgentOpenAPISign: []
AgentOpenAPITimestamp: []
summary: 恢复出厂设置
tags:
- 代理开放接口
/api/open/v1/devices/switch-card:
post:
description: |-
认证方式:
1. 每个请求必须携带 HeaderX-Agent-Account、X-Agent-Password、X-Agent-Timestamp、X-Agent-Nonce、X-Agent-Sign。
2. Header 字段说明:
- X-Agent-Account代理账号标识填写代理登录平台使用的用户名或手机号例如 agent001该值由平台开通代理账号时提供必须与签名原文 account 行完全一致。
- X-Agent-Password代理账号当前登录密码由代理账号持有人提供该值同时作为 HMAC-SHA256 签名密钥,密码变更后必须使用新密码签名。
- X-Agent-Timestamp请求发起时间由调用方生成支持 Unix 秒、Unix 毫秒或 RFC3339 时间,例如 1715400000、1715400000000 或 2024-05-11T10:00:00+08:00必须与签名原文 timestamp 行完全一致。
- X-Agent-Nonce请求随机串由调用方每次请求自行生成例如 UUID、雪花 ID 或“时间戳+随机数”;不需要平台提前分配,同一账号在 5 分钟内不可重复。
- X-Agent-Sign请求签名由调用方按下方规则实时计算得到不是平台分配的固定值只要 method、path、query、body、timestamp、nonce、account 或 password 任意一个变化,签名都会变化。
3. X-Agent-Timestamp 超出允许时间窗口会认证失败;客户端服务器时间应保持同步。
4. X-Agent-Nonce 重复会被判定为重放请求。
5. 签名原文按 7 行拼接,行尾使用 \n最后一行后不追加换行
METHOD
PATH
canonical_query
body_sha256
timestamp
nonce
account
6. METHOD 使用大写 HTTP 方法PATH 只取请求路径,例如 /api/open/v1/cards/status不包含域名和 query。
7. canonical_query 为 query 参数规范化结果:排除 sign 字段参数名升序同名多值按值升序key 和 value 使用 URL QueryEscape 编码后用 key=value 拼接,多个参数用 & 连接;无 query 时为空字符串。
8. body_sha256 为原始请求 body 字节的 SHA256 小写十六进制值GET 或空 body 使用空字符串的 SHA256POST JSON 请求必须用最终发送的 body 字符串计算,签名和请求发送的 body 必须完全一致。
9. X-Agent-Sign = hex(HMAC-SHA256(password, sign_payload))password 为 X-Agent-Password 的原始值,结果使用小写十六进制。
Node.js 签名示例:
```javascript
const crypto = require("crypto");
function queryEscape(value) {
return new URLSearchParams({ v: String(value) }).toString().slice(2);
}
function canonicalQuery(params) {
return Object.entries(params || {})
.filter(([key]) => key.toLowerCase() !== "sign")
.flatMap(([key, value]) => {
const values = Array.isArray(value) ? value : [value];
return values.sort().map((item) => [key, item]);
})
.sort(([aKey, aValue], [bKey, bValue]) => {
if (aKey === bKey) return String(aValue).localeCompare(String(bValue));
return aKey.localeCompare(bKey);
})
.map(([key, value]) => queryEscape(key) + "=" + queryEscape(value))
.join("&");
}
function buildAgentSign({ method, path, query, body, timestamp, nonce, account, password }) {
const bodyText = body || "";
const bodyHash = crypto.createHash("sha256").update(bodyText).digest("hex");
const signPayload = [
method.toUpperCase(),
path,
canonicalQuery(query),
bodyHash,
timestamp,
nonce,
account,
].join("\n");
return crypto.createHmac("sha256", password).update(signPayload).digest("hex");
}
```
按 device_no 将多卡设备切换到指定 ICCID 的卡。
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DtoAgentOpenAPIDeviceSwitchCardRequest'
responses:
"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:
- AgentOpenAPIAccount: []
AgentOpenAPINonce: []
AgentOpenAPIPassword: []
AgentOpenAPISign: []
AgentOpenAPITimestamp: []
summary: 切网(多卡设备切换 ICCID
tags:
- 代理开放接口
/api/open/v1/devices/traffic:
get:
description: |-
认证方式:
1. 每个请求必须携带 HeaderX-Agent-Account、X-Agent-Password、X-Agent-Timestamp、X-Agent-Nonce、X-Agent-Sign。
2. Header 字段说明:
- X-Agent-Account代理账号标识填写代理登录平台使用的用户名或手机号例如 agent001该值由平台开通代理账号时提供必须与签名原文 account 行完全一致。
- X-Agent-Password代理账号当前登录密码由代理账号持有人提供该值同时作为 HMAC-SHA256 签名密钥,密码变更后必须使用新密码签名。
- X-Agent-Timestamp请求发起时间由调用方生成支持 Unix 秒、Unix 毫秒或 RFC3339 时间,例如 1715400000、1715400000000 或 2024-05-11T10:00:00+08:00必须与签名原文 timestamp 行完全一致。
- X-Agent-Nonce请求随机串由调用方每次请求自行生成例如 UUID、雪花 ID 或“时间戳+随机数”;不需要平台提前分配,同一账号在 5 分钟内不可重复。
- X-Agent-Sign请求签名由调用方按下方规则实时计算得到不是平台分配的固定值只要 method、path、query、body、timestamp、nonce、account 或 password 任意一个变化,签名都会变化。
3. X-Agent-Timestamp 超出允许时间窗口会认证失败;客户端服务器时间应保持同步。
4. X-Agent-Nonce 重复会被判定为重放请求。
5. 签名原文按 7 行拼接,行尾使用 \n最后一行后不追加换行
METHOD
PATH
canonical_query
body_sha256
timestamp
nonce
account
6. METHOD 使用大写 HTTP 方法PATH 只取请求路径,例如 /api/open/v1/cards/status不包含域名和 query。
7. canonical_query 为 query 参数规范化结果:排除 sign 字段参数名升序同名多值按值升序key 和 value 使用 URL QueryEscape 编码后用 key=value 拼接,多个参数用 & 连接;无 query 时为空字符串。
8. body_sha256 为原始请求 body 字节的 SHA256 小写十六进制值GET 或空 body 使用空字符串的 SHA256POST JSON 请求必须用最终发送的 body 字符串计算,签名和请求发送的 body 必须完全一致。
9. X-Agent-Sign = hex(HMAC-SHA256(password, sign_payload))password 为 X-Agent-Password 的原始值,结果使用小写十六进制。
Node.js 签名示例:
```javascript
const crypto = require("crypto");
function queryEscape(value) {
return new URLSearchParams({ v: String(value) }).toString().slice(2);
}
function canonicalQuery(params) {
return Object.entries(params || {})
.filter(([key]) => key.toLowerCase() !== "sign")
.flatMap(([key, value]) => {
const values = Array.isArray(value) ? value : [value];
return values.sort().map((item) => [key, item]);
})
.sort(([aKey, aValue], [bKey, bValue]) => {
if (aKey === bKey) return String(aValue).localeCompare(String(bValue));
return aKey.localeCompare(bKey);
})
.map(([key, value]) => queryEscape(key) + "=" + queryEscape(value))
.join("&");
}
function buildAgentSign({ method, path, query, body, timestamp, nonce, account, password }) {
const bodyText = body || "";
const bodyHash = crypto.createHash("sha256").update(bodyText).digest("hex");
const signPayload = [
method.toUpperCase(),
path,
canonicalQuery(query),
bodyHash,
timestamp,
nonce,
account,
].join("\n");
return crypto.createHmac("sha256", password).update(signPayload).digest("hex");
}
```
按 device_no 查询设备套餐内流量、当前套餐和待生效套餐。device_no 支持虚拟号、IMEI。
parameters:
- description: 设备标识支持虚拟号、IMEI
in: query
name: device_no
required: true
schema:
description: 设备标识支持虚拟号、IMEI
maxLength: 100
minLength: 1
type: string
responses:
"200":
content:
application/json:
schema:
properties:
code:
description: 响应码
example: 0
type: integer
data:
$ref: '#/components/schemas/DtoAgentOpenAPIDeviceTrafficResponse'
msg:
description: 响应消息
example: success
type: string
timestamp:
description: 时间戳
format: date-time
type: string
required:
- code
- msg
- data
- timestamp
type: object
description: 成功
"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:
- AgentOpenAPIAccount: []
AgentOpenAPINonce: []
AgentOpenAPIPassword: []
AgentOpenAPISign: []
AgentOpenAPITimestamp: []
summary: 查询设备套餐内流量
tags:
- 代理开放接口
/api/open/v1/packages:

View File

@@ -250,7 +250,7 @@ curl -G "https://open.example.com/api/open/v1/cards/status" \
- `total_flow_mb`:套餐总流量,单位 MB。
- `valid_days`:套餐有效天数。
钱包购买接口一次只允许一个 `package_code``card_nos` 最多 100 张卡。每张卡独立创建钱包订单,允许部分成功,失败卡会返回独立错误码和原因。
钱包购买接口一次只允许一个 `package_code``card_nos` 最多 100 张卡。每张卡独立创建钱包订单,允许部分成功,失败卡会返回独立错误码和原因。任一卡购买失败时,外层 `code`/`msg` 返回首条失败卡的错误码和原因,同时 `data.failed_cards` 保留失败明细,`data.orders` 保留已成功订单;接入方应以 `success_count``failed_count``orders``failed_cards` 判断单卡结果,避免整批重试造成重复下单。
## 5. 错误码

View File

@@ -0,0 +1,94 @@
# 轮询配置二维条件重构计划
## 背景
当前轮询配置使用 `card_condition` 单字段表达卡的轮询条件,但业务上实际存在两个独立维度:
- 实名状态:未实名、已实名
- 卡状态:开机、停机
单字段枚举会把两个维度压成互斥状态,导致“未实名 + 停机”这类卡只能命中一个条件,配置语义不直观,也容易出现高低频策略互相覆盖。
## 目标
将轮询配置从单一 `card_condition` 改为二维条件匹配:
- `realname_condition``any``not_real_name``real_name`
- `network_condition``any``online``offline`
配置匹配规则调整为:
```text
实名条件匹配 AND 卡状态条件匹配 AND 卡类型/运营商等条件匹配
```
一张卡允许同时命中多条配置;每个任务类型仍按现有 `priority ASC` 选择第一条非空 interval。
## 推荐语义
| 条件字段 | 值 | 语义 |
| --- | --- | --- |
| `realname_condition` | `any` | 不限制实名状态 |
| `realname_condition` | `not_real_name` | `real_name_status != 1` |
| `realname_condition` | `real_name` | `real_name_status = 1` |
| `network_condition` | `any` | 不限制开停机状态 |
| `network_condition` | `online` | `network_status = 1` |
| `network_condition` | `offline` | `network_status = 0` |
## 迁移映射
为了平滑迁移旧配置,可按以下规则回填新字段:
| 旧 `card_condition` | 新 `realname_condition` | 新 `network_condition` |
| --- | --- | --- |
| 空 | `any` | `any` |
| `not_real_name` | `not_real_name` | `online` |
| `real_name` | `real_name` | `any` |
| `suspended` | `any` | `offline` |
| `activated` | `real_name` | `online` |
## 示例配置
未实名卡高频实名:
```text
realname_condition = not_real_name
network_condition = any
realname_check_interval = 120
其他 interval = NULL
```
停机卡高频套餐和卡状态:
```text
realname_condition = any
network_condition = offline
package_check_interval = 120
card_status_check_interval = 120
其他 interval = NULL
```
已实名开机卡低频实名、常规套餐和卡状态:
```text
realname_condition = real_name
network_condition = online
realname_check_interval = 86400
package_check_interval = 300
card_status_check_interval = 3600
```
## 实施步骤
1. 新增数据库字段 `realname_condition``network_condition`,保留旧字段用于迁移兼容。
2. 回填历史配置的新字段值。
3. 调整 DTO、Handler、Service、文档生成器和 OpenAPI 描述。
4. 调整 `PollingConfigManager` 的匹配逻辑,按二维条件筛选配置。
5. 验证典型组合:未实名开机、未实名停机、已实名开机、已实名停机。
6. 确认线上配置完成迁移后,再考虑废弃旧 `card_condition` 字段。
## 风险
- `real_name` 条件生效后,现有灰度配置可能覆盖大量已实名卡,需要上线前确认优先级和 interval。
- 多条配置同时命中是预期行为,但需要用 priority 明确每个任务类型的最终 interval。
- 迁移期间需要保持旧配置兼容,避免配置未更新时轮询队列为空。

View File

@@ -1,7 +1,7 @@
# 奇成数据迁移方案
> 目标:把奇成(旧 MySQL `kyhl` 库的存量卡和设备迁移到新系统PostgreSQL输出 SQL 脚本供线上手动执行。
> 范围:当前态资产 + 当前生效套餐 + 累计已用流量 + 代理佣金总值。**不迁移历史明细**。
> 范围:当前态资产 + 当前生效正式套餐 + 未生效待生效正式套餐 + 累计已用流量 + 代理佣金总值。**不迁移历史明细、已过期套餐和加油包**。
---
@@ -14,12 +14,10 @@
│ resources/devices.csv│
└──────────┬───────────┘
我们维护映射配置
我们维护唯一决策配置
┌──────────▼───────────┐
│ config/carrier.yaml │
config/series.yaml
│ config/package.yaml │
│ config/shop.yaml │
│ config/mapping.yaml │
归属/槽位/套餐映射
└──────────┬───────────┘
┌──────────▼───────────┐
@@ -45,9 +43,11 @@
```
**关键约束**
- 两个脚本都**不直接写入**线上库,只生成 `.sql` 文件
- 两个脚本都**不直接写入**线上库,只生成 `.sql` 文件和审核 CSV
- `config/mapping.yaml` 是归属、设备当前槽位、套餐来源槽位和套餐迁移范围的决策源;奇成只提供历史事实
- 脚本对奇成线上库**只 SELECT**DB 连接层强制只读事务(`SET TRANSACTION READ ONLY`
- 所有 INSERT 使用 `ON CONFLICT DO NOTHING`,支持重跑
- `errors.csv` 非空时不执行 SQL先修正配置/输入后重跑
---
@@ -84,13 +84,14 @@
| `sim_iccid_2` | 否 | 插槽2的 iccid | `89860623...` |
| `sim_iccid_3` | 否 | 插槽3的 iccid | |
| `sim_iccid_4` | 否 | 插槽4的 iccid | |
| `current_slot` | 否 | 当前使用的插槽位置1-4用于 `tb_device_sim_binding.is_current` | `1` |
| `target_shop_code` | 否 | 目标店铺编码。**留空 = 进平台库存** | `SHOP20260428103424CZZQ` |
| `current_slot` | 否 | 当前使用的插槽位置1-4用于 `tb_device_sim_binding.is_current`。空值按 `mapping.yaml` 批量规则或覆盖项填充 | `2` |
| `package_source_slot` | 否 | 设备套餐来源槽位1-4。只迁该槽位卡的奇成正式套餐为设备套餐 | `2` |
**注意**
- 设备上的卡,其 `cards.csv``target_shop_code` 必须留空(一致性校验由脚本检查)
- `cards.csv` 必须先到位,`devices.csv``sim_iccid_*` 才能被校验
- 业务方分别准备这两份是合理的,奇成 `tbl_card_relate`(主卡+副卡1+副卡2的关系业务方应已掌握
- 槽位解析优先级:`devices.csv` 行级配置 > `mapping.yaml.overrides.devices` > `mapping.yaml.ownership_rules.device` 批量默认值
- `current_slot``package_source_slot` 对应槽位无卡时写入 `errors.csv` 并阻断该设备相关 SQL
---
@@ -123,9 +124,37 @@
## 四、我们维护的映射配置
迁移前**必须**在新系统建好店铺、运营商、套餐系列、套餐,然后填好以下映射
迁移前**必须**在新系统建好店铺、运营商、套餐系列、套餐,然后填好 `config/mapping.yaml`。该文件是本次迁移的唯一决策源
### 4.1 `config/carrier_mapping.yaml`
### 4.1 归属、槽位和套餐规则
```yaml
ownership_rules:
default_target_shop_code: KWTX
device:
mode: default_shop
current_slot: 2
package_source_slot: 2
standalone_card:
mode: default_shop
package_rules:
migrate_statuses: [active, pending]
overrides:
devices:
- virtual_no: "862639073940258"
target_shop_code: OTHER
current_slot: 1
package_source_slot: 1
cards:
- iccid: "89861590172420360956"
target_shop_code: OTHER
```
归属规则:独立卡按 `overrides.cards``ownership_rules.standalone_card` 决定;设备按 `overrides.devices``ownership_rules.device` 决定;设备上的卡跟随设备。奇成 `agent_id` 不再作为默认归属来源。
### 4.2 `config/mapping.yaml` 中的 carriers
```yaml
# 奇成的运营商名称(业务方在 cards.csv 中填的)→ 新系统 carrier_id + type
@@ -141,7 +170,7 @@ carriers:
# ... 业务方提供的所有运营商都要列出
```
### 4.2 `config/series_mapping.yaml`
### 4.3 `config/mapping.yaml` 中的 series
```yaml
# 奇成套餐系列名 → 新系统 series_id
@@ -153,7 +182,7 @@ series:
target_series_id: 6
```
### 4.3 `config/package_mapping.yaml`
### 4.4 `config/mapping.yaml` 中的 packages
```yaml
# 奇成套餐名 → 新系统 package_idcards.csv 的 package_name 直接对照这里)
@@ -166,7 +195,7 @@ packages:
> 注:奇成的 `tbl_set_meal` 套餐表非常大,**不需要全部映射**,只映射本次迁移涉及的套餐(脚本预扫描 cards.csv 收集 package_name 集合,缺失的列出来让我们补)。
### 4.4 `config/shop_mapping.yaml`(可选别名)
### 4.5 店铺编码
```yaml
# 如果业务方愿意填 shop_code本文件可以省略
@@ -318,16 +347,15 @@ FROM c;
```
1. 读 cards.csv 得到 iccid 列表
2. 连奇成(只读事务)批量查:
- tbl_card_life WHERE iccid_mark IN (...) AND status=1 ORDER BY expire_date DESC
→ 每张卡取最新生效的套餐meal_id, meal_name, start_date, expire_date
- tbl_card WHERE iccid_mark IN (...) → total_bytes_cnt (GB)
- tbl_agent_commission_account → 按 agent_id 聚合 can_draw_amount, total_commision_amount
- tbl_agent根据 cards.csv 中 agent 信息映射出 shop_id
3. 对每张卡:
a. 套餐映射legacy_meal_name → package_id从 yaml 或业务方 csv
b. 生成伪订单 SQLorder_no = "MIG-CARD-" + iccid 后8位 + 时间戳
c. 生成 tb_package_usage SQL关联伪订单 + 计算 snapshot 字段
d. 生成 UPDATE tb_iot_card SET data_usage_mb = ... (把 GB 转 MB
- `tbl_card_life` 当前生效正式套餐和 `tbl_next_month_card_life` 次月待生效套餐,保留 legacy 套餐 ID、套餐名、类型、状态、开始时间、到期时间和稳定排序键
- `tbl_card.total_bytes_cnt` → 反算新系统真用量 MB
- `tbl_agent_commission_account` / `tbl_agent` → 仅用于代理钱包初始化
3. 对每个独立卡或设备套餐来源槽位:
a. 套餐映射:`legacy_meal_id` → `target_package_id`(来自 `mapping.yaml.packages`
b. active 套餐写 `tb_package_usage.status=1`pending 套餐写 `status=0` 并按稳定顺序写 `priority`
c. 生成伪订单 SQLorder_no = `MIG-<ICCID>-<priority>`
d. 生成 `tb_package_usage` SQL关联伪订单 + 计算 snapshot 字段
e. 生成 `UPDATE tb_iot_card SET data_usage_mb = ...`(真用量 MB
4. 对每个 shop按代理映射出来的
a. 生成 INSERT/UPDATE tb_agent_wallet总值
b. 生成一条 tb_agent_wallet_transactiontype=initial_migration, amount=总值, remark=源奇成代理ID
@@ -343,7 +371,10 @@ output/
step2_03_card_data_usage_update.sql # 卡累计流量 UPDATE
step2_04_agent_wallet_init.sql # 代理钱包总值
step2_05_agent_wallet_transactions.sql # 迁移流水
warnings.csv # 警告:找不到当前生效套餐、无法映射的代理等
step2_06_asset_series_update.sql # 资产套餐系列回填
package_resolution.csv # 套餐迁移审核文件
errors.csv # 套餐映射缺失、多 active、槽位缺失等阻断错误
warnings.csv # 用量反算和代理钱包警告
summary.txt
```
@@ -357,7 +388,7 @@ WITH new_order AS (
total_amount, payment_method, payment_status, paid_at,
source, generation, creator, updater, created_at, updated_at
)
SELECT 'MIG-CARD-' || RIGHT('89852000263338772439', 8) || '-' || TO_CHAR(NOW(), 'YYYYMMDDHH24MISS'),
SELECT 'MIG-89852000263338772439-1',
'single_card', 'personal', 0, c.id,
0, 'offline', 2, NOW(),
'migration', 1, 0, 0, NOW(), NOW()
@@ -489,9 +520,9 @@ scripts/migration/
| 1 | 业务方填的 carrier_name / package_name 与奇成不一致 → 映射失败 | 脚本预扫描,把所有未匹配项写到 unmatched.csv 让业务方修正 |
| 2 | 奇成的 iccid 有 18/21/22 位脏数据 | 脚本第三节规则剔除,进 errors.csv |
| 3 | 同一 iccid 被业务方填多次 | 脚本去重 + 警告 |
| 4 | 设备上的卡在 cards.csv 里又写了 target_shop_code | 一致性校验,进 errors.csv |
| 5 | 奇成 tbl_card_life 一张卡有多条 status=1 记录 | 取 expire_date 最大的那条warnings.csv 记录 |
| 6 | 套餐 ID 映射不全 | 脚本预扫描,缺的列出来,业务方/我们补全 yaml |
| 4 | 设备 `current_slot``package_source_slot` 指向空槽位 | 写入 errors.csv阻断该设备资产或套餐 SQL |
| 5 | 同一资产存在多个 active 正式套餐 | 写入 errors.csv阻断该资产套餐 SQL人工裁决后修正 |
| 6 | 套餐 ID 映射不全 | `package_resolution.csv` 记录errors.csv 阻断对应套餐 SQL |
| 7 | tb_iot_card.is_standalone 由触发器维护 | 脚本写入时设 true触发器会因为后续 binding 自动转 false |
| 8 | 代理→shop 映射不明确 | 需要业务方提供 agent_id → shop_code 映射表,作为 config/agent_shop_mapping.yaml |
| 9 | 线上库 sequence 与测试库不同 | 用 CTE + RETURNING 模式,不硬编码 id |
@@ -526,7 +557,7 @@ mappings:
| 字段 | 值 | 说明 |
|------|-----|------|
| `order_no` | `MIG-CARD-<iccid后8位>-<时间戳>` / `MIG-DEV-<vno>-<时间戳>` | 前缀就是迁移数据的唯一识别标记 |
| `order_no` | `MIG-<ICCID>-<priority>` | 前缀就是迁移数据的唯一识别标记 |
| `source` | `'admin'`(复用现有) | 不新增 `migration` 取值 |
| `buyer_type` | `'personal'` | 与 buyer_id=0 配对 |
| `buyer_id` | `0` | 与赠送/平台自营一致 |