This commit is contained in:
@@ -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: 关联业务类型:recharge 或 order(可空)
|
||||
description: 关联业务类型:recharge、order 或 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_no);3) 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. 每个请求必须携带 Header:X-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 使用空字符串的 SHA256;POST 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. 每个请求必须携带 Header:X-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 使用空字符串的 SHA256;POST 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. 每个请求必须携带 Header:X-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 使用空字符串的 SHA256;POST 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. 每个请求必须携带 Header:X-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 使用空字符串的 SHA256;POST 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:
|
||||
|
||||
@@ -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. 错误码
|
||||
|
||||
|
||||
94
docs/polling-system/轮询配置二维条件重构计划.md
Normal file
94
docs/polling-system/轮询配置二维条件重构计划.md
Normal 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。
|
||||
- 迁移期间需要保持旧配置兼容,避免配置未更新时轮询队列为空。
|
||||
@@ -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_id(cards.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. 生成伪订单 SQL(order_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. 生成伪订单 SQL(order_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_transaction(type=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` | 与赠送/平台自营一致 |
|
||||
|
||||
Reference in New Issue
Block a user