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. 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\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, }) }