133 lines
6.2 KiB
Go
133 lines
6.2 KiB
Go
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. 每个请求必须携带 Header:X-Agent-Account、X-Agent-Password、X-Agent-Timestamp、X-Agent-Nonce、X-Agent-Sign。
|
||
2. X-Agent-Timestamp 支持 Unix 秒、Unix 毫秒或 RFC3339 时间,服务端允许 5 分钟时间误差。
|
||
3. X-Agent-Nonce 为随机串,同一账号在 5 分钟内不可重复。
|
||
4. 签名原文按 7 行拼接,行尾使用 \n,最后一行后不追加换行:
|
||
METHOD
|
||
PATH
|
||
canonical_query
|
||
body_sha256
|
||
timestamp
|
||
nonce
|
||
account
|
||
5. METHOD 使用大写 HTTP 方法;PATH 只取请求路径,例如 /api/open/v1/cards/status,不包含域名和 query。
|
||
6. canonical_query 为 query 参数规范化结果:排除 sign 字段,参数名升序;同名多值按值升序;key 和 value 使用 URL QueryEscape 编码后用 key=value 拼接,多个参数用 & 连接;无 query 时为空字符串。
|
||
7. body_sha256 为原始请求 body 字节的 SHA256 小写十六进制值;GET 或空 body 使用空字符串的 SHA256;POST JSON 请求必须用最终发送的 body 字符串计算,签名和请求发送的 body 必须完全一致。
|
||
8. 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, "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,
|
||
})
|
||
}
|