Files
junhong_cmp_fiber/docs/agent-open-api/功能总结.md
Break 1f634eb465
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m3s
init
2026-06-16 15:15:50 +08:00

9.2 KiB
Raw Permalink Blame History

代理开放接口对接说明

1. 接入说明

代理开放接口挂载在 /api/open/v1,面向代理店铺第三方系统调用。接入方使用代理账号登录信息进行签名认证,通过认证后即可调用卡查询、套餐查询、预充值钱包查询和套餐购买接口。

请通过 HTTPS 调用开放接口。接入方不要在客户端日志、浏览器控制台或异常信息中记录账号密码、签名、随机串等敏感信息。

2. 认证 Header

每个请求必须携带以下 Header

Header 是什么 怎么来 示例 注意事项
X-Agent-Account 代理账号标识 填写代理登录平台使用的用户名或手机号,由平台开通代理账号时提供 agent00113800000000 必须与签名原文最后一行 account 完全一致
X-Agent-Password 代理账号当前登录密码 由代理账号持有人提供,和登录平台的密码一致 Passw0rd123 同时作为 HMAC-SHA256 签名密钥;密码变更后必须使用新密码签名;不要写入日志
X-Agent-Timestamp 请求发起时间 调用方在每次请求前生成 171540000017154000000002024-05-11T10:00:00+08:00 支持 Unix 秒、Unix 毫秒、RFC3339必须与签名原文 timestamp 行完全一致;客户端服务器时间需同步
X-Agent-Nonce 请求随机串 调用方每次请求自行生成,不需要平台提前分配 n-1715400000-001 或 UUID 同一账号 5 分钟内不可重复,重复会被判定为重放请求
X-Agent-Sign 请求签名 调用方按下方签名规则实时计算 5f2d... 不是平台分配的固定值;只要 method、path、query、body、timestamp、nonce、account 或 password 任意一个变化,签名都会变化

Header 生成顺序建议:

  1. 先确定 X-Agent-AccountX-Agent-Password
  2. 每次请求生成新的 X-Agent-TimestampX-Agent-Nonce
  3. 按下方规则拼出签名原文。
  4. X-Agent-Password 对签名原文做 HMAC-SHA256得到 X-Agent-Sign
  5. 带上 5 个 Header 发起请求。

签名原文:

METHOD
PATH
canonical_query
body_sha256
timestamp
nonce
account

拼接规则:

  • 使用换行符 \n 连接 7 行,最后一行后不追加换行。
  • METHOD 使用大写 HTTP 方法,例如 GETPOST
  • PATH 只取请求路径,例如 /api/open/v1/cards/status,不包含域名和 query。
  • canonical_query 为 query 参数规范化结果:排除 sign 字段参数名按升序排列同名多值按值升序排列key 和 value 使用 URL QueryEscape 编码后按 key=value 拼接;多个参数用 & 连接;无 query 时为空字符串。
  • body_sha256 为原始请求 body 字节的 SHA256 小写十六进制值。GET 或空 body 使用空字符串的 SHA256e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  • POST JSON 请求必须先确定最终发送的 body 字符串,再用同一个字符串计算 body_sha256 并发送请求;签名后不要再重新格式化 JSON、调整字段顺序或改变空格。
  • timestampnonceaccount 必须与 Header 中的值完全一致。

签名值为:

lowercase_hex(HMAC-SHA256(password, sign_payload))

其中 passwordX-Agent-Password 的原始值,sign_payload 为上面 7 行拼接后的字符串。

2.1 GET 签名示例

请求:

GET /api/open/v1/cards/status?card_no=89860000000000000001
X-Agent-Account: agent001
X-Agent-Password: Passw0rd123
X-Agent-Timestamp: 1715400000
X-Agent-Nonce: n-1715400000-001

签名原文:

GET
/api/open/v1/cards/status
card_no=89860000000000000001
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
1715400000
n-1715400000-001
agent001

Node.js 示例:

const crypto = require("crypto");

function sha256Hex(input) {
  return crypto.createHash("sha256").update(input).digest("hex");
}

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 sign({ method, path, query, body, timestamp, nonce, account, password }) {
  const payload = [
    method.toUpperCase(),
    path,
    canonicalQuery(query),
    sha256Hex(body || ""),
    timestamp,
    nonce,
    account,
  ].join("\n");

  return crypto.createHmac("sha256", password).update(payload).digest("hex");
}

完整 GET 调用示例:

const https = require("https");

const account = "agent001";
const password = "Passw0rd123";
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = `nonce-${Date.now()}`;
const method = "GET";
const path = "/api/open/v1/cards/status";
const query = { card_no: "89860000000000000001" };
const body = "";
const requestQuery = canonicalQuery(query);
const requestPath = `${path}?${requestQuery}`;
const signature = sign({ method, path, query, body, timestamp, nonce, account, password });

const req = https.request(
  {
    hostname: "open.example.com",
    path: requestPath,
    method,
    headers: {
      "X-Agent-Account": account,
      "X-Agent-Password": password,
      "X-Agent-Timestamp": timestamp,
      "X-Agent-Nonce": nonce,
      "X-Agent-Sign": signature,
    },
  },
  (res) => {
    let data = "";
    res.on("data", (chunk) => (data += chunk));
    res.on("end", () => console.log(data));
  }
);

req.end();

完整 POST 调用示例:

const https = require("https");

const account = "agent001";
const password = "Passw0rd123";
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = `nonce-${Date.now()}`;
const method = "POST";
const path = "/api/open/v1/wallet/package-orders";
const query = {};

const body = JSON.stringify({
  card_nos: ["89860000000000000001", "89860000000000000002"],
  package_code: "PKG001",
});

const signature = sign({ method, path, query, body, timestamp, nonce, account, password });

const req = https.request(
  {
    hostname: "open.example.com",
    path,
    method,
    headers: {
      "Content-Type": "application/json",
      "Content-Length": Buffer.byteLength(body),
      "X-Agent-Account": account,
      "X-Agent-Password": password,
      "X-Agent-Timestamp": timestamp,
      "X-Agent-Nonce": nonce,
      "X-Agent-Sign": signature,
    },
  },
  (res) => {
    let data = "";
    res.on("data", (chunk) => (data += chunk));
    res.on("end", () => console.log(data));
  }
);

req.write(body);
req.end();

curl 调用示例:

curl -G "https://open.example.com/api/open/v1/cards/status" \
  --data-urlencode "card_no=89860000000000000001" \
  -H "X-Agent-Account: agent001" \
  -H "X-Agent-Password: Passw0rd123" \
  -H "X-Agent-Timestamp: 1715400000" \
  -H "X-Agent-Nonce: n-1715400000-001" \
  -H "X-Agent-Sign: 上面代码计算出的签名"

3. 接口列表

方法 路径 说明
GET /api/open/v1/cards/traffic card_no 查询单卡流量和套餐
GET /api/open/v1/cards/status card_no 查询单卡正常/停机状态
GET /api/open/v1/cards/realname-status card_no 查询实名状态
GET /api/open/v1/packages 查询可购买套餐列表
POST /api/open/v1/wallet/package-orders 使用预充值钱包给多张卡购买同一个套餐
GET /api/open/v1/wallet/balance 查询预充值钱包余额
GET /api/open/v1/wallet/transactions 查询预充值钱包流水

card_no 支持 ICCID、虚拟号、MSISDN。开放接口只支持单卡设备标识会返回参数错误。

4. 字段口径

卡流量字段:

  • total_flow_mb:套餐总流量,单位 MB。
  • used_flow_mb:已用流量,单位 MB。
  • remaining_flow_mb:剩余流量,单位 MB。

套餐列表字段:

  • cost_price:套餐结算价,单位分。
  • retail_price:套餐销售价,单位分。
  • total_flow_mb:套餐总流量,单位 MB。
  • valid_days:套餐有效天数。

钱包购买接口一次只允许一个 package_codecard_nos 最多 100 张卡。每张卡独立创建钱包订单,允许部分成功,失败卡会返回独立错误码和原因。任一卡购买失败时,外层 code/msg 返回首条失败卡的错误码和原因,同时 data.failed_cards 保留失败明细,data.orders 保留已成功订单;接入方应以 success_countfailed_countordersfailed_cards 判断单卡结果,避免整批重试造成重复下单。

5. 错误码

错误码 说明
1048 开放接口认证失败
1049 开放接口签名无效
1190 开放接口时间戳无效
1191 重复请求,请勿重放
1005 无权限操作该资源或资源不存在

参数校验失败统一返回 1001。认证类失败只返回统一错误码和消息。