SOO 开放平台 API 对接文档

让您的软件 / 网站 / 脚本通过 HTTP 接口直接对接卡密验证、发卡、查询、封禁等能力 · 无需加密壳也能完成授权验证

00对接方式

两种方式共用同一套卡密/账号体系与签名密钥,登录成功拿到的是同一个 token,按需二选一。

纯 API 对接
HTTP 接口直连(本文档全部接口),界面与心跳由自己实现,适合有开发能力的团队。
查看本文档 ↓
登录组件 EXE
单 EXE 登录窗:公告 / 登录 / 注册 / 解绑 / 购卡全部窗口内完成,登录成功直接返回 token。
查看组件说明 ↓

组件登录组件(单 EXE 登录窗)

客户软件不用自己写登录界面和协议:启动这个 EXE,登录成功后输出 token + 到期时间 + 卡信息 到文件/命令行,你的软件拿到后自己跑心跳即可。

1. 是什么

2. 三种对接方式怎么选

对接方式登录界面协议心跳适用
纯 API 对接(本文档)客户自己做客户自己调客户自己跑有界面开发能力的团队
登录组件(本说明)组件内置模板组件内部完成客户自己跑只要登录窗、不想写界面/协议的客户
加密壳打包(加密工具)壳内置模板壳内置壳自动用加密工具打包的客户

3. 配置(soo-login-helper.ini)

# 客户只拿 exe + ini 就能用;改界面只需换模板,不用重新编译
[soo]
base_url   = https://sooyz.com      ; 服务端地址(主站或实例域名)
app_key    = 你的开放API应用Key      ; 用户中心 → 开放平台 生成
app_secret = 你的开放API应用密钥     ; 与 Key 配对,组件自动 HMAC 签名
tpl_url    =                        ; 云端模板地址(可空);优先于 tpl_file
tpl_file   = templates\index.html  ; 本地模板文件(相对 exe 目录)
product_name = 软件名称              ; 显示在窗口标题
out_file   = soo-login-result.json ; 登录成功结果输出文件(相对 exe 目录)
machine    =                        ; 留空=自动取本机机器码;可固定填写
width      = 1440                  ; 窗口宽(模板 meta soo-window 优先)
height     = 900                   ; 窗口高
topmost    = 0                     ; 1=置顶
exit_on_success = 1                ; 登录成功是否自动退出
stdout_mode    = 1                 ; 同时向标准输出打印结果(供父进程读取)
http_timeout   = 15                ; 网络请求超时(秒)

4. 模板:云端 or 本地

5. 客户软件怎么接收结果

登录成功后组件写 out_file(JSON)+ 打印到标准输出:

{"ok": true, "type": "card|user", "token": "登录返回的会话token", "expire": "2027-01-01 12:00:00", "expire_ts": 1752300000, "machine": "8430-915d-ed9e-ba14-95e8", "card": "卡号(卡密登录时有)", "user": "用户名(账号登录时有)", "plan_name": "套餐名", "notice": "登录成功提示"}

⚠ 心跳:组件不做心跳。你的软件拿到 token 后,按本文档 /api/open/user/login、/api/open/card/login 返回的 heartbeat_sec(后台「心跳时长」设置)定时调心跳接口续期/校验即可(心跳不计 API 日限额)。

6. 常见问题

获取:交付包「05-登录组件」(exe + ini 示例 + bridge.js + 对接说明.md + src),或联系管理员获取。

01快速开始

1. 开通与限额

2. 鉴权签名(所有接口必须携带)

每次请求需携带以下 4 个公共参数 + 业务参数:

参数说明
app_key应用的公开标识(可明文传输)
timestamp当前 Unix 秒级时间戳,允许误差 ±300 秒
nonce随机字符串(每次请求不同,防重放,600 秒内不可重复)
sign签名:HMAC-SHA256(api_secret, canon) 的十六进制小写串

canon 构造规则:取「全部参数(含公共参数与业务参数,不含 sign)」,剔除值为空的参数,按 key 字典序排序,拼接为 k1=v1&k2=v2&...。

# 伪代码:canon 示例
params = {"app_key": "a1b2...", "timestamp": "1792146000", "nonce": "x9f2...", "card": "ABCD-1234"}
canon  = "app_key=a1b2...&card=ABCD-1234&nonce=x9f2...&timestamp=1792146000"  # 字典序、排除空值、排除 sign
sign   = HMAC_SHA256(hex, api_secret, canon)

⚠ GET 请求:签名参数与业务参数全部放 URL query;⚠ POST 请求:全部放 JSON body(GET/POST 的签名键集必须一致,都含 4 个公共参数)。

3. 统一响应格式

{"code": 0, "msg": "ok", "data": {...}}   # code=0 成功;非 0 为业务错误;HTTP 401=鉴权失败,429=限流

4. 常见返回码

code含义
0成功
4001 / 4003 / 4004应用不存在 / 签名错误 / 卡密不存在
4010 / 4011 / 4012卡密不存在 / 已封禁 / 机器码不匹配(需解绑)
4015会话不存在或已下线(心跳需重登)
4101 / 4110 / 4111登录参数缺失 / 账号不存在或密码错误 / 账号已封禁
400 / 401 / 429 (HTTP)参数错误 / 鉴权失败(签名、时间窗、重放)/ QPS 或日调用超限

02接口列表

方法路径说明
POST/api/open/card/login卡密登录(自动激活/续期、绑定机器码)
POST/api/open/user/login账号密码模式登录
POST/api/open/heartbeat心跳续活(在线状态保活)
GET/api/open/card/info查询卡密状态(到期/次数/绑机/在线)
GET/api/open/sessions在线会话列表(kw/page/size)
POST/api/open/session/kick踢下线(按 token 或 card)
GET/api/open/info公告 / 最新版本 / 远程算法配置
POST/api/open/card/issue批量发卡(免费版拒绝;单次 1-500 张)
POST/api/open/card/{ban|unban|delete}卡密封禁 / 解封 / 删除
POST/api/open/user/{ban|unban|resetpwd}账号封禁 / 解封 / 重置密码
GET/POST/DELETE/api/open/vars全局变量读取 / 写入 / 删除(跨客户端同步)

03关键接口说明

POST/api/open/card/login卡密登录

参数:card 卡密号(必填)· machine 机器码(必填,首次登录即绑定)· ip 客户端 IP(可选)

返回 data:token(后续心跳/踢线用)· card_type · expire_at · remaining_count · status · heartbeat_sec(服务端下发的心跳间隔秒数,后台「软件设置/用户中心-应用设置」可调,缺省 60,范围 5~600)

POST/api/open/user/login账号登录

参数:username · password · machine · ip(可选)

返回 data:同卡密登录,token 直接使用。

POST/api/open/heartbeat心跳保活

参数:token(必填)· machine(可选,校验机器一致性)· ip(可选)

建议客户端按登录返回的 data.heartbeat_sec 作为心跳间隔调用(缺省 60 秒);连续心跳失败 / 被踢后返回 4015,客户端应重新登录。

GET/api/open/card/info卡密查询

参数:card 卡密号(必填)

返回 data:status · card_type · duration · expire_at · remaining_count · bound_machine · online 等。

GET/api/open/sessions在线列表

参数:kw(卡密/账号模糊,可选)· page · size

POST/api/open/session/kick踢下线

参数:token(按会话踢,可选)或 card(按卡密踢,可选)——至少传一个。

POST/api/open/card/issue批量发卡

参数:card_type(time 时间卡 / count 次数卡 / forever 永久卡 / points 积分卡,必填)· count(1-500,必填)· duration(时间卡天数,必填)· points(积分卡数值,可选)· prefix(卡密前缀,可选)· length(去前缀后长度,默认 14,可选)· note(备注,可选)

返回 data:cards 新卡列表。

POST/api/open/card/{ban|unban|delete}卡密管理

参数:card 卡密号(必填)。delete 会同时清除该卡会话。

POST/api/open/user/{ban|unban|resetpwd}账号管理

参数:username(必填)· password(仅 resetpwd 时必填,新密码)。

GET/api/open/vars变量读取

无业务参数,返回该应用全部全局变量(跨客户端实时同步,适合授权开关/配置下发)。

POST/api/open/vars变量写入

参数:vkey(必填)· vvalue(必填)· alias(别名,可选)。

DELETE/api/open/vars变量删除

参数:vkey(必填)。

04多语言对接示例

以下示例均为可直接运行的最小实现:签名 → 卡密登录 → 心跳(间隔跟随服务端 heartbeat_sec,含 4015 自动重登)。把 APP_KEY / API_SECRET / CARD_NO 换成你自己的即可。

# Python3 最小示例:签名 + 卡密登录 + 心跳(标准库,无第三方依赖)
import hashlib, hmac, json, time, secrets, urllib.request

BASE    = "https://sooyz.com"
APP_KEY = "你的app_key"
SECRET  = "你的api_secret"
CARD    = "你的卡密号"
MACHINE = "本机机器码"

def sign_params(params):
    params["app_key"] = APP_KEY
    params["timestamp"] = str(int(time.time()))
    params["nonce"] = secrets.token_hex(8)
    canon = "&".join(f"{k}={v}" for k, v in sorted(params.items()) if k != "sign" and v != "")
    params["sign"] = hmac.new(SECRET.encode(), canon.encode(), hashlib.sha256).hexdigest()
    return params

def post(path, params):
    body = json.dumps(sign_params(params)).encode()
    req = urllib.request.Request(BASE + path, data=body, headers={"Content-Type": "application/json"})
    return json.loads(urllib.request.urlopen(req).read())

def card_login():
    r = post("/api/open/card/login", {"card": CARD, "machine": MACHINE})
    if r["code"] != 0: raise SystemExit(r)
    d = r["data"]
    print("登录成功", d)
    # 心跳间隔(秒)以服务端下发 heartbeat_sec 为准,缺省 60
    return d["token"], int(d.get("heartbeat_sec") or 60)

# 1. 卡密登录(首次自动绑定机器码)
token, hb_sec = card_login()

# 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登)
while True:
    time.sleep(hb_sec)
    r = post("/api/open/heartbeat", {"token": token, "machine": MACHINE})
    print("心跳", r)
    if r["code"] == 4015:        # 会话失效/被踢
        print("会话失效,重新登录")
        token, hb_sec = card_login()
// C# (.NET 6+) 最小示例:签名 + 卡密登录 + 心跳(无第三方依赖)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

var appKey  = "你的app_key";
var secret  = "你的api_secret";
var card    = "你的卡密号";
var machine = "本机机器码";
var http    = new HttpClient();

Dictionary<string, string> Sign(Dictionary<string, string> p)
{
    p["app_key"]   = appKey;
    p["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
    p["nonce"]     = Guid.NewGuid().ToString("N");
    var canon = string.Join("&",
        p.Where(x => x.Key != "sign" && x.Value != "")
         .OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}"));
    using (var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
        p["sign"] = Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(canon))).ToLower();
    return p;
}

async Task<string> Post(string path, Dictionary<string, string> p)
{
    var body = new StringContent(JsonSerializer.Serialize(Sign(p)),
        Encoding.UTF8, "application/json");
    var resp = await http.PostAsync("https://sooyz.com" + path, body);
    return await resp.Content.ReadAsStringAsync();
}

async Task<(string token, int hbSec)> CardLogin()
{
    var r = await Post("/api/open/card/login",
        new Dictionary<string, string> { { "card", card }, { "machine", machine } });
    var data = JsonSerializer.Deserialize<JsonElement>(r);
    if (data.GetProperty("code").GetInt32() != 0) throw new Exception(r);
    var d = data.GetProperty("data");
    Console.WriteLine("登录成功 " + r);
    // 心跳间隔(秒)以服务端下发 heartbeat_sec 为准,缺省 60
    var hb = d.TryGetProperty("heartbeat_sec", out var h) ? h.GetInt32() : 60;
    return (d.GetProperty("token").GetString(), hb);
}

// 1. 卡密登录(首次自动绑定机器码)
var (token, hbSec) = await CardLogin();

// 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登)
while (true)
{
    await Task.Delay(hbSec * 1000);
    var hr = await Post("/api/open/heartbeat",
        new Dictionary<string, string> { { "token", token }, { "machine", machine } });
    Console.WriteLine("心跳 " + hr);
    if (hr.Contains("\"4015\"")) { Console.WriteLine("会话失效,重新登录"); (token, hbSec) = await CardLogin(); }
}
// Java 11+ 最小示例:签名 + 卡密登录 + 心跳(JDK 自带 java.net.http,无第三方依赖)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class SooApi {
    static String appKey  = "你的app_key";
    static String secret  = "你的api_secret";
    static String card    = "你的卡密号";
    static String machine = "本机机器码";
    static HttpClient http = HttpClient.newHttpClient();

    static Map<String,String> sign(Map<String,String> p) {
        p.put("app_key", appKey);
        p.put("timestamp", String.valueOf(System.currentTimeMillis()/1000));
        p.put("nonce", UUID.randomUUID().toString().replace("-",""));
        List<String> keys = new ArrayList<>(p.keySet());
        Collections.sort(keys);
        StringBuilder canon = new StringBuilder();
        boolean first = true;
        for (String k : keys) {
            if (k.equals("sign") || p.get(k).isEmpty()) continue;
            if (!first) canon.append("&");
            first = false;
            canon.append(k).append("=").append(p.get(k));
        }
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] h = mac.doFinal(canon.toString().getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : h) sb.append(String.format("%02x", b));
            p.put("sign", sb.toString());
        } catch (Exception e) { throw new RuntimeException(e); }
        return p;
    }

    static String toJson(Map<String,String> p) {
        StringBuilder sb = new StringBuilder("{");
        boolean first = true;
        for (Map.Entry<String,String> e : p.entrySet()) {
            if (!first) sb.append(",");
            first = false;
            sb.append("\"").append(e.getKey()).append("\"")
              .append(":").append("\"").append(e.getValue()).append("\"");
        }
        return sb.append("}").toString();
    }

    static String post(String path, Map<String,String> p) throws Exception {
        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://sooyz.com" + path))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(toJson(sign(p))))
            .build();
        return http.send(req, HttpResponse.BodyHandlers.ofString()).body();
    }

    static int hbSec = 60;  // 心跳间隔(秒):登录时从 data.heartbeat_sec 读取,缺省 60

    static String cardLogin() throws Exception {
        Map<String,String> p = new HashMap<>();
        p.put("card", card); p.put("machine", machine);
        String r = post("/api/open/card/login", p);
        if (!r.contains("\"code\":0") && !r.contains("\"code\": 0")) throw new RuntimeException(r);
        System.out.println("登录成功 " + r);
        // 解析 data.heartbeat_sec(缺省 60)
        String[] hs = r.split("\"heartbeat_sec\":");
        if (hs.length > 1) hbSec = Integer.parseInt(hs[1].split(",")[0].trim());
        return r.split("\"token\":\"")[1].split("\"")[0];
    }

    public static void main(String[] args) throws Exception {
        // 1. 卡密登录(首次自动绑定机器码)
        String token = cardLogin();
        // 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登)
        while (true) {
            Thread.sleep(hbSec * 1000L);
            Map<String,String> h = new HashMap<>();
            h.put("token", token); h.put("machine", machine);
            String hr = post("/api/open/heartbeat", h);
            System.out.println("心跳 " + hr);
            if (hr.contains("4015")) { System.out.println("会话失效,重新登录"); token = cardLogin(); }
        }
    }
}
// Go 最小示例:签名 + 卡密登录 + 心跳(标准库,无第三方依赖)
package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "net/http"
    "sort"
    "strconv"
    "strings"
    "time"
)

var (
    base    = "https://sooyz.com"
    appKey  = "你的app_key"
    secret  = "你的api_secret"
    card    = "你的卡密号"
    machine = "本机机器码"
)

func signParams(p map[string]string) map[string]string {
    p["app_key"] = appKey
    p["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
    p["nonce"] = fmt.Sprintf("%d", time.Now().UnixNano())
    keys := make([]string, 0, len(p))
    for k := range p { keys = append(keys, k) }
    sort.Strings(keys)
    var parts []string
    for _, k := range keys {
        if k == "sign" || p[k] == "" { continue }
        parts = append(parts, k+"="+p[k])
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(strings.Join(parts, "&")))
    p["sign"] = hex.EncodeToString(mac.Sum(nil))
    return p
}

func post(path string, p map[string]string) (map[string]interface{}, error) {
    body, _ := json.Marshal(signParams(p))
    resp, err := http.Post(base+path, "application/json", bytes.NewReader(body))
    if err != nil { return nil, err }
    defer resp.Body.Close()
    var out map[string]interface{}
    err = json.NewDecoder(resp.Body).Decode(&out)
    return out, err
}

func cardLogin() (string, int, error) {
    r, err := post("/api/open/card/login", map[string]string{"card": card, "machine": machine})
    if err != nil { return "", 60, err }
    if code, _ := r["code"].(float64); code != 0 { return "", 60, fmt.Errorf("%v", r) }
    data := r["data"].(map[string]interface{})
    token := data["token"].(string)
    hb := 60 // 心跳间隔(秒):以服务端下发 heartbeat_sec 为准,缺省 60
    if v, ok := data["heartbeat_sec"].(float64); ok && v > 0 { hb = int(v) }
    fmt.Println("登录成功", data)
    return token, hb, nil
}

func main() {
    // 1. 卡密登录(首次自动绑定机器码)
    token, hbSec, err := cardLogin()
    if err != nil { fmt.Println(err); return }
    // 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登)
    for {
        time.Sleep(time.Duration(hbSec) * time.Second)
        hr, err := post("/api/open/heartbeat", map[string]string{"token": token, "machine": machine})
        if err != nil { fmt.Println("心跳错误", err); continue }
        fmt.Println("心跳", hr)
        if code, _ := hr["code"].(float64); code == 4015 {
            fmt.Println("会话失效,重新登录")
            token, hbSec, _ = cardLogin()
        }
    }
}
<?php // PHP 最小示例:签名 + 卡密登录 + 心跳(标准库,无第三方依赖,CLI 运行)
<?php
$base   = 'https://sooyz.com';
$appKey = '你的app_key';
$secret = '你的api_secret';
$card   = '你的卡密号';
$machine = '本机机器码';

function sign_params($params) {
    global $appKey, $secret;
    $params['app_key']   = $appKey;
    $params['timestamp'] = (string)time();
    $params['nonce']     = bin2hex(random_bytes(8));
    ksort($params);                                          // 字典序
    $canon = '';
    foreach ($params as $k => $v) {
        if ($k === 'sign' || $v === '') continue;
        $canon .= $k . '=' . $v . '&';
    }
    $params['sign'] = hash_hmac('sha256', rtrim($canon, '&'), $secret);
    return $params;
}

function post($path, $params) {
    global $base;
    $params = sign_params($params);
    $ctx = stream_context_create(['http' => [
        'method'  => 'POST',
        'header'  => 'Content-Type: application/json',
        'content' => json_encode($params),
    ]]);
    return json_decode(file_get_contents($base . $path, false, $ctx), true);
}

function card_login() {
    global $card, $machine;
    $r = post('/api/open/card/login', ['card' => $card, 'machine' => $machine]);
    if ($r['code'] !== 0) { fwrite(STDERR, json_encode($r) . PHP_EOL); exit(1); }
    $d = $r['data'];
    echo "登录成功 " . json_encode($d) . PHP_EOL;
    // 心跳间隔(秒):以服务端下发 heartbeat_sec 为准,缺省 60
    $hb = isset($d['heartbeat_sec']) && $d['heartbeat_sec'] > 0 ? (int)$d['heartbeat_sec'] : 60;
    return ['token' => $d['token'], 'hb' => $hb];
}

// 1. 卡密登录(首次自动绑定机器码)
$login = card_login(); $token = $login['token']; $hbSec = $login['hb'];

// 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登)
while (true) {
    sleep($hbSec);
    $hr = post('/api/open/heartbeat', ['token' => $token, 'machine' => $machine]);
    echo "心跳 " . json_encode($hr) . PHP_EOL;
    if ($hr['code'] === 4015) { echo "会话失效,重新登录" . PHP_EOL; $login = card_login(); $token = $login['token']; $hbSec = $login['hb']; }
}
'================================================'
'  易语言 · 签名 + 卡密登录 + 心跳(精易模块 / 或直接 Winhttp)'
'================================================'
.版本 2
.程序集 窗口程序集_启动窗口

.子程序 取签名, 文本型
.参数 参数字典, 文本型   ' 已按字典序排好的 k=v&k=v
.参数 密钥, 文本型
.局部变量 结果, 字节集
结果 = 校验_取HMAC_SHA256 (到字节集 (参数字典), 到字节集 (密钥))
返回 (取数据摘要 (结果))   ' 十六进制小写

.子程序 卡密登录, 文本型
.局部变量 时间戳, 文本型
.局部变量 随机串, 文本型
.局部变量 签名, 文本型
.局部变量 请求体, 文本型
.局部变量 返回文本, 文本型
.局部变量 结果对象, 对象
.局部变量 token, 文本型
时间戳 = 到文本 (取现行时间戳 ())
随机串 = 文本_取随机字符 (16)
' 参数需按 key 字典序拼接:app_key,card,machine,nonce,timestamp
签名 = 取签名 (
    “app_key=” + “你的app_key” + “&card=” + “你的卡密号”
    + “&machine=” + “本机机器码” + “&nonce=” + 随机串
    + “&timestamp=” + 时间戳,
    “你的api_secret”)
请求体 = “{” + “” + “\”app_key\”:” + “\”你的app_key\”” + “,\”card\”:” + “\”你的卡密号\”” + “,\”machine\”:” + “\”本机机器码\”” + “,\”nonce\”:” + “\”” + 随机串 + “\”” + “,\”timestamp\”:” + “\”” + 时间戳 + “\”” + “,\”sign\”:” + “\”” + 签名 + “\”” + “}”
' 返回文本 = 网页_访问_对象 (“https://sooyz.com/api/open/card/login”, 1, 请求体, , , “application/json”)
' 用 编码_Utf8到Ansi / json 解析 返回文本,取 data.token 存入全局变量 token
' 同时取 data.heartbeat_sec 存入全局变量 心跳间隔(缺省 60,5~600)
' 若 code≠0 提示错误(4010 卡密不存在 / 4011 已封禁 / 4012 机器码不匹配)
返回 (token)

.子程序 心跳保活
.局部变量 时间戳, 文本型
.局部变量 随机串, 文本型
.局部变量 签名, 文本型
.局部变量 请求体, 文本型
.局部变量 返回文本, 文本型
' 每 心跳间隔 秒调用一次(间隔=登录返回的 data.heartbeat_sec,缺省 60);参数:token,machine + 公共参数,字典序拼接后签名
时间戳 = 到文本 (取现行时间戳 ())
随机串 = 文本_取随机字符 (16)
签名 = 取签名 (
    “app_key=” + “你的app_key” + “&machine=” + “本机机器码”
    + “&nonce=” + 随机串 + “&timestamp=” + 时间戳
    + “&token=” + 全局token,
    “你的api_secret”)
请求体 = “{” + “\”app_key\”:” + “\”你的app_key\”” + “,\”machine\”:” + “\”本机机器码\”” + “,\”nonce\”:” + “\”” + 随机串 + “\”” + “,\”timestamp\”:” + “\”” + 时间戳 + “\”” + “,\”token\”:” + “\”” + 全局token + “\”” + “,\”sign\”:” + “\”” + 签名 + “\”” + “}”
' 返回文本 = 网页_访问_对象 (“https://sooyz.com/api/open/heartbeat”, 1, 请求体, , , “application/json”)
' 解析返回 code:0=正常;4015=会话失效需重新 卡密登录()
'================================================'
'  火山PC · 签名 + 卡密登录 + 心跳'
'================================================'
' 关键步骤(火山视窗,需引用「火山模块」或手写 HMAC):'
' 1. 组装参数:app_key、card、machine、nonce、timestamp(心跳再加 token)'
' 2. 按键名首字母排序后拼 "k=v&k=v"(字典序,排除空值与 sign)'
' 3. 用 api_secret 做 HMAC-SHA256,转十六进制小写 = sign'
' 4. 把全部参数转 JSON 文本,POST https://sooyz.com/api/open/card/login'
' 5. 解析返回 JSON,code==0 取 data.token 保存(4012=机器码不匹配需解绑);并保存 data.heartbeat_sec 为心跳间隔(缺省 60,5~600)'
' 6. 每 心跳间隔 秒 POST /api/open/heartbeat(间隔=登录返回 heartbeat_sec,缺省 60);code==4015 重新登录'

' 火山视窗代码示意(伪码,方法名以你引用的模块为准):'
' 时间戳 = 取Unix时间戳 ()'
' 随机串 = 文本_生成随机字符串 (16)'
' 待签名文本 = "app_key=" + 你的app_key + "&card=" + 你的卡密号'
' 待签名文本 = 待签名文本 + "&machine=" + 本机机器码'
' 待签名文本 = 待签名文本 + "&nonce=" + 随机串 + "&timestamp=" + 时间戳'
' 签名 = HMAC_SHA256_十六进制 (待签名文本, 你的api_secret)'
' 发送POST请求(地址="https://sooyz.com/api/open/card/login", 数据=JSON化参数, 内容类型="application/json")'
' 返回 JSON 解析 code==0 后取 data.token 保存到全局变量'
' 心跳:每 心跳间隔 秒 发送POST请求(地址="https://sooyz.com/api/open/heartbeat", 数据=JSON化(token,machine), 内容类型="application/json")  (间隔=登录返回 data.heartbeat_sec,缺省 60)'
' 若 code==4015 → 重新执行登录流程
'================================================'
'  按键精灵(QUI / 脚本)· 签名 + 卡密登录 + 心跳'
'================================================'
' 需调用系统 COM 或使用第三方脚本插件实现 HMAC-SHA256;'
' 若脚本环境无 HMAC 库,可在本地程序内算好 sign 后由脚本调用(推荐)。'

' 按键精灵 9 / 2014 脚本示意:'
Dim appKey, secret, card, machine, ts, nonce, canon, sign, resp, token
appKey = "你的app_key" : secret = "你的api_secret"
card = "你的卡密号" : machine = "本机机器码"

' —— 卡密登录 ——'
ts = CStr(Round(GetSysTime()/1000))      ' Unix 秒级时间戳
nonce = GetTime() & Int(Rnd*100000)      ' 随机串
' 字典序拼接(app_key < card < machine < nonce < timestamp)
canon = "app_key=" & appKey & "&card=" & card & "&machine=" & machine
canon = canon & "&nonce=" & nonce & "&timestamp=" & ts
' sign = HMAC_SHA256(secret, canon) —— 用插件/COM 计算后填充
sign = 调用插件计算HMAC(canon, secret)
' 组装 JSON 并 POST https://sooyz.com/api/open/card/login,解析 code==0 取 data.token
' 并记录 data.heartbeat_sec 为心跳间隔(缺省 60,5~600)
' token = 解析返回JSON(data.token)

' —— 心跳保活:每 心跳间隔 秒执行一次(登录返回 heartbeat_sec,缺省 60) ——'
ts = CStr(Round(GetSysTime()/1000)) : nonce = GetTime() & Int(Rnd*100000)
canon = "app_key=" & appKey & "&machine=" & machine & "&nonce=" & nonce
canon = canon & "&timestamp=" & ts & "&token=" & token
sign = 调用插件计算HMAC(canon, secret)
' 组装 JSON POST https://sooyz.com/api/open/heartbeat'
' 返回 code==4015 → 回到上方重新执行卡密登录
' (计时可用 Plugin.Window 或延时循环控制,一轮 = 心跳间隔秒数,从登录返回 heartbeat_sec 读取)

05完整对接建议(客户端流程)

  1. 启动 → 调 POST /api/open/card/login(卡密模式)或 user/login(账号模式)→ 校验 code==0 且 expire_at 未过期。
  2. 登录成功拿到 token 后进入主流程;按 data.heartbeat_sec(缺省 60 秒)调 POST /api/open/heartbeat 保活。
  3. 心跳返回 4015(被踢/会话失效)→ 回到第 1 步重新登录。
  4. 需要在线人数/强制下线/公告版本:调 sessions、session/kick、info。
  5. 所有接口失败均按 code 判断业务错误,HTTP 401/429 判断鉴权与限流;建议对 401 检查时间校准,对 429 做退避重试。

更多问题 / 定制接口需求,联系 QQ:39460097