Files
junhong_cmp_fiber/internal/routes/open.go
2026-06-02 16:56:01 +08:00

181 lines
9.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package routes
import (
"github.com/gofiber/fiber/v2"
openapiHandler "github.com/break/junhong_cmp_fiber/internal/handler/openapi"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
// RegisterOpenAPIRoutes 注册代理开放接口路由
func RegisterOpenAPIRoutes(router fiber.Router, handler *openapiHandler.Handler, doc *openapi.Generator, basePath string) {
const tag = "代理开放接口"
const authDescription = `认证方式:
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\n" + `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");
}
` + "```"
Register(router, doc, basePath, "GET", "/cards/traffic", handler.GetCardTraffic, RouteSpec{
Summary: "查询单卡流量",
Description: authDescription + "\n\n按 card_no 查询单卡流量、当前套餐和待生效套餐。card_no 支持 ICCID、虚拟号、MSISDN。",
Input: new(dto.AgentOpenAPICardQueryRequest),
Output: new(dto.AgentOpenAPICardTrafficResponse),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "GET", "/cards/status", handler.GetCardStatus, RouteSpec{
Summary: "查询单卡状态",
Description: authDescription + "\n\n按 card_no 查询单卡状态,返回正常或停机以及停机原因。",
Input: new(dto.AgentOpenAPICardQueryRequest),
Output: new(dto.AgentOpenAPICardStatusResponse),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "GET", "/cards/realname-status", handler.GetRealnameStatus, RouteSpec{
Summary: "查询单卡实名状态",
Description: authDescription + "\n\n按 card_no 查询单卡实名状态。",
Input: new(dto.AgentOpenAPICardQueryRequest),
Output: new(dto.AgentOpenAPIRealnameStatusResponse),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "POST", "/cards/resume", handler.ResumeCard, RouteSpec{
Summary: "机卡分离卡复机",
Description: authDescription + "\n\n按 card_no 调用上游复机接口。调用前检查本地落库的 Gateway 卡状态扩展字段,仅当 gateway_extend 等于“机卡分离停机”时允许复机,否则返回“仅允许机卡分离状态下的卡复机”。",
Input: new(dto.AgentOpenAPICardResumeRequest),
Output: new(dto.AgentOpenAPICardResumeResponse),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "GET", "/packages", handler.ListPackages, RouteSpec{
Summary: "查询套餐列表",
Description: authDescription + "\n\n分页查询当前账号可购买的套餐返回套餐基础信息和价格。",
Input: new(dto.AgentOpenAPIPackageListRequest),
Output: new(dto.AgentOpenAPIPackageItem),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "POST", "/wallet/package-orders", handler.CreateWalletPackageOrders, RouteSpec{
Summary: "钱包套餐购买",
Description: authDescription + "\n\n使用预充值钱包为多张卡购买同一个套餐编码。每张卡独立下单允许部分成功。",
Input: new(dto.AgentOpenAPIWalletPackageOrderRequest),
Output: new(dto.AgentOpenAPIWalletPackageOrderResponse),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "GET", "/wallet/balance", handler.GetWalletBalance, RouteSpec{
Summary: "查询预充值钱包余额",
Description: authDescription + "\n\n查询当前账号预充值钱包余额。未开通钱包时返回 0。",
Output: new(dto.AgentOpenAPIWalletBalanceResponse),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "GET", "/wallet/transactions", handler.ListWalletTransactions, RouteSpec{
Summary: "查询预充值钱包流水",
Description: authDescription + "\n\n分页查询当前账号预充值钱包流水。",
Input: new(dto.AgentOpenAPIWalletTransactionListRequest),
Output: new(dto.MainWalletTransactionItem),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "GET", "/devices/traffic", handler.GetDeviceTraffic, RouteSpec{
Summary: "查询设备套餐内流量",
Description: authDescription + "\n\n按 device_no 查询设备套餐内流量、当前套餐和待生效套餐。device_no 支持虚拟号、IMEI。",
Input: new(dto.AgentOpenAPIDeviceQueryRequest),
Output: new(dto.AgentOpenAPIDeviceTrafficResponse),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "POST", "/devices/switch-card", handler.SwitchDeviceCard, RouteSpec{
Summary: "切网(多卡设备切换 ICCID",
Description: authDescription + "\n\n按 device_no 将多卡设备切换到指定 ICCID 的卡。",
Input: new(dto.AgentOpenAPIDeviceSwitchCardRequest),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "POST", "/devices/reboot", handler.RebootDevice, RouteSpec{
Summary: "重启设备",
Description: authDescription + "\n\n按 device_no 远程重启设备。",
Input: new(dto.AgentOpenAPIDeviceOperationRequest),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
Register(router, doc, basePath, "POST", "/devices/reset", handler.ResetDevice, RouteSpec{
Summary: "恢复出厂设置",
Description: authDescription + "\n\n按 device_no 远程恢复设备出厂设置。",
Input: new(dto.AgentOpenAPIDeviceOperationRequest),
Tags: []string{tag},
Auth: true,
SecurityScheme: openapi.SecuritySchemeAgentOpenAPI,
})
}