Skip to content

接入前必读


一、API调用基础说明

1. 网关地址

https://openapi.xyiot.link

2. 请求头参数

参数名是否必填说明
Content-Typeapplication/json;charset=UTF-8
x-open-client-id星寓开放平台 clientId
x-open-auth-mode固定为 SIGNATURE_V3
x-timestamp毫秒级时间戳
x-open-nonce随机 uuid
x-open-content-md5POST 必填,GET 不传请求体 MD5(Base64编码)
x-open-signature请求签名值(基于HmacSHA256加密计算)

3. 响应顶层参数

参数名说明
code默认 200 为成功,其他状态码参见「状态码说明」
dataAPI响应的业务数据
message状态码非200情况下返回的异常描述(建议优先使用code做异常识别)
requestId开放平台返回的当次请求的id,可用于部分异步回调场景匹配回调通知

示例:

json
{
  "code": "200",
  "data": {},
  "requestId": "0KLRm9DVcJjTVE3O"
}

二、访问鉴权说明

当前采用请求签名方式进行访问鉴权,具体操作步骤如下:

1.签名流程

txt
1.POST 请求:对按参数名字典序排序后的请求体(body)进行 MD5 摘要运算,即得到 contentMD5

2.组装签名字符串:
  String signPlaintext = 接口URI + "\n" + contentMD5 + "\n" + timestamp(毫秒时间戳) + "\n" + nonce(随机uuid);

3.对组装好的签名字符串进行 HmacSHA256 算法签名运算,即得到最终签名值

其他说明:
  1.当接口为 GET 请求时,URI 需要拼接按参数名字典序生成的 queryString,contentMD5 使用空字符串,且不传 x-open-content-md5 请求头
  2.签名、MD5、HTTP body 均使用 UTF-8 编码
  3.签名采用 HmacSHA256 算法,签名所使用的密钥为 clientId 匹配的 clientSecret

2.请求体(body) MD5摘要运算示例

注意:请求参数(body参数)须按照参数名的字典顺序排序后再进行MD5摘要计算

java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;

// 请求体参数先按参数名字典序排序,再序列化为紧凑 JSON。
// 注意:contentMD5 必须基于实际发送的 bodyJson 计算,不能签名一份 body、发送另一份 body。
Map<String, String> params = new TreeMap<>();
params.put("sn", "123");
String bodyJson = toSortedJson(params);

MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] md5Bytes = md5.digest(bodyJson.getBytes(StandardCharsets.UTF_8));
String contentMD5 = Base64.getEncoder().encodeToString(md5Bytes);

static String toSortedJson(Map<String, String> params) {
    Map<String, String> sorted = new TreeMap<>(params);
    StringBuilder builder = new StringBuilder("{");
    boolean first = true;
    for (Map.Entry<String, String> entry : sorted.entrySet()) {
        if (!first) {
            builder.append(',');
        }
        builder.append('"').append(entry.getKey()).append("\":\"")
                .append(entry.getValue()).append('"');
        first = false;
    }
    return builder.append('}').toString();
}

上述得到的 contentMD5 即为请求头 x-open-content-md5 的值

3.HmacSHA256 签名运算示例

java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

String uri = "/openapi/v2/lock/get_lock_info";
String contentMD5 = "上一步计算得到的contentMD5";
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = "随机uuid";
String secret = "your clientSecret";

String signPlaintext = uri + "\n" + contentMD5 + "\n" + timestamp + "\n" + nonce;

String algorithm = "HmacSHA256";
Mac hmacSha256 = Mac.getInstance(algorithm);
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
byte[] signPlaintextBytes = signPlaintext.getBytes(StandardCharsets.UTF_8);
hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, algorithm));
byte[] digestBytes = hmacSha256.doFinal(signPlaintextBytes);
String signature = Base64.getEncoder().encodeToString(digestBytes);

上述得到的 signature 即为最终签名值,也即为请求头 x-open-signature 的值

4.API签名完整示例

java
package com.xyiot.test;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;

public class SignTestV3 {

    private static final String SIGNATURE_PLAINTEXT_FMT = "%s\n%s\n%s\n%s";
    private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

    /** 替换为实际的 API 网关地址 */
    private static final String API_GATEWAY = "https://openapi.xyiot.link";

    /** 替换为实际的 clientId / clientSecret */
    private static final String CLIENT_ID = "your clientId";
    private static final String CLIENT_SECRET = "your clientSecret";

    public static void main(String[] args) throws Exception {
        testPostSignature();
        testGetSignature();
    }

    // ================================================================
    //  POST 签名测试 (SIGNATURE_V3)
    // ================================================================
    static void testPostSignature() throws Exception {
        System.out.println("========== POST 签名测试 (SIGNATURE_V3) ==========");

        var timestamp = System.currentTimeMillis();
        var nonce = fastSimpleUUID();

        var data = new TreeMap<String, String>();
        data.put("sn", "123");
        var bodyJson = sortedJson(data);

        // contentMd5 = Base64(MD5(排序后的 JSON))
        var contentMd5 = md5Base64(bodyJson);

        var uri = "/openapi/v2/lock/get_lock_info";

        // 签名原文: uri + contentMd5 + timestamp + nonce
        var plaintext = String.format(SIGNATURE_PLAINTEXT_FMT, uri, contentMd5, timestamp, nonce);
        System.out.println("签名原文:\n" + plaintext);

        var signatureVal = hmacSha256Base64(CLIENT_SECRET, plaintext);

        var request = HttpRequest.newBuilder(URI.create(API_GATEWAY + uri))
                .header("Content-Type", "application/json;charset=UTF-8")
                .header("x-open-auth-mode", "SIGNATURE_V3")
                .header("x-open-client-id", CLIENT_ID)
                .header("x-open-nonce", nonce)
                .header("x-timestamp", String.valueOf(timestamp))
                .header("x-open-content-md5", contentMd5)
                .header("x-open-signature", signatureVal)
                .POST(HttpRequest.BodyPublishers.ofString(bodyJson, StandardCharsets.UTF_8))
                .build();

        var resp = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        System.out.println("响应: " + resp.body());
        System.out.println();
    }

    // ================================================================
    //  GET 签名: 无 contentMd5, queryString 拼接到 uri 再签名
    // ================================================================
    static void testGetSignature() throws Exception {
        System.out.println("========== GET 签名测试 (SIGNATURE_V3, 无 content-md5) ==========");

        var timestamp = System.currentTimeMillis();
        var nonce = fastSimpleUUID();

        var uri = "/openapi/v2/lock/list_page";
        // GET 的 queryString 参与签名: 参数按参数名字典顺序拼接
        var queryLine = queryString(Map.of("sn", "ABC123", "page", "1", "size", "10"));
        var signedUri = uri + queryLine; // GET 时将 queryString 拼入签名原文

        // GET 请求 contentMd5 为空字符串
        var contentMd5 = "";

        // 签名原文: signedUri + "" + timestamp + nonce
        var plaintext = String.format(SIGNATURE_PLAINTEXT_FMT, signedUri, contentMd5, timestamp, nonce);
        System.out.println("签名原文:\n" + plaintext);

        var signatureVal = hmacSha256Base64(CLIENT_SECRET, plaintext);

        // GET 请求不传 x-open-content-md5
        var request = HttpRequest.newBuilder(URI.create(API_GATEWAY + signedUri))
                .header("x-open-auth-mode", "SIGNATURE_V3")
                .header("x-open-client-id", CLIENT_ID)
                .header("x-open-nonce", nonce)
                .header("x-timestamp", String.valueOf(timestamp))
                .header("x-open-signature", signatureVal)
                .GET()
                .build();

        var resp = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        System.out.println("响应: " + resp.body());
        System.out.println();
    }

    static String sortedJson(Map<String, String> data) {
        var sorted = new TreeMap<>(data);
        var builder = new StringBuilder("{");
        var first = true;
        for (var entry : sorted.entrySet()) {
            if (!first) {
                builder.append(',');
            }
            builder.append('"').append(jsonEscape(entry.getKey())).append("\":\"")
                    .append(jsonEscape(entry.getValue())).append('"');
            first = false;
        }
        return builder.append('}').toString();
    }

    static String queryString(Map<String, String> params) {
        if (params.isEmpty()) {
            return "";
        }
        var sorted = new TreeMap<>(params);
        var builder = new StringBuilder("?");
        var first = true;
        for (var entry : sorted.entrySet()) {
            if (!first) {
                builder.append('&');
            }
            builder.append(urlEncode(entry.getKey())).append('=').append(urlEncode(entry.getValue()));
            first = false;
        }
        return builder.toString();
    }

    static String md5Base64(String text) throws Exception {
        var md5 = MessageDigest.getInstance("MD5");
        return java.util.Base64.getEncoder()
                .encodeToString(md5.digest(text.getBytes(StandardCharsets.UTF_8)));
    }

    static String hmacSha256Base64(String secret, String text) throws Exception {
        var mac = Mac.getInstance("HmacSHA256");
        var key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(key);
        return java.util.Base64.getEncoder()
                .encodeToString(mac.doFinal(text.getBytes(StandardCharsets.UTF_8)));
    }

    static String fastSimpleUUID() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    private static String jsonEscape(String text) {
        var builder = new StringBuilder();
        for (var i = 0; i < text.length(); i++) {
            var ch = text.charAt(i);
            switch (ch) {
                case '"' -> builder.append("\\\"");
                case '\\' -> builder.append("\\\\");
                case '\b' -> builder.append("\\b");
                case '\f' -> builder.append("\\f");
                case '\n' -> builder.append("\\n");
                case '\r' -> builder.append("\\r");
                case '\t' -> builder.append("\\t");
                default -> {
                    if (ch < 0x20) {
                        builder.append(String.format("\\u%04x", (int) ch));
                    } else {
                        builder.append(ch);
                    }
                }
            }
        }
        return builder.toString();
    }

    private static String urlEncode(String text) {
        return URLEncoder.encode(text, StandardCharsets.UTF_8);
    }

}
go
package main

import (
	"crypto/hmac"
	"crypto/md5"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"
)

// ============================================================
// 配置 (替换为实际值)
// ============================================================
const (
	API_GATEWAY    = "https://openapi.xyiot.link"
	CLIENT_ID      = "your client_id"
	CLIENT_SECRET  = "your secret"

	SIGNATURE_PLAINTEXT_FMT = "%s\n%s\n%s\n%s"
)

// 对应 IdUtil.fastSimpleUUID(): 无横线的 UUID v4 (32 位 hex)
func fastSimpleUUID() string {
	b := make([]byte, 16)
	rand.Read(b)
	b[6] = (b[6] & 0x0f) | 0x40
	b[8] = (b[8] & 0x3f) | 0x80
	return fmt.Sprintf("%x", b)
}

// key 排序且紧凑输出
func sortJSON(data map[string]string) string {
	keys := make([]string, 0, len(data))
	for k := range data {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var sb strings.Builder
	sb.WriteByte('{')
	for i, k := range keys {
		if i > 0 {
			sb.WriteByte(',')
		}
		sb.WriteString(fmt.Sprintf("%q:%q", k, data[k]))
	}
	sb.WriteByte('}')
	return sb.String()
}

func queryString(params map[string]string) string {
	if len(params) == 0 {
		return ""
	}

	keys := make([]string, 0, len(params))
	for k := range params {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var sb strings.Builder
	sb.WriteByte('?')
	for i, k := range keys {
		if i > 0 {
			sb.WriteByte('&')
		}
		sb.WriteString(url.QueryEscape(k))
		sb.WriteByte('=')
		sb.WriteString(url.QueryEscape(params[k]))
	}
	return sb.String()
}

// 对应 Base64.encodeBase64(SecureUtil.md5().digest(...))
func md5Base64(text string) string {
	h := md5.Sum([]byte(text))
	return base64.StdEncoding.EncodeToString(h[:])
}

// 对应 Base64.encodeBase64(SecureUtil.hmac(HmacSHA256, key).digest(...))
func hmacSha256Base64(key, text string) string {
	mac := hmac.New(sha256.New, []byte(key))
	mac.Write([]byte(text))
	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

// 发起 HTTP 请求并输出响应
func doRequest(method, url string, headers map[string]string, body string) {
	client := &http.Client{Timeout: 30 * time.Second}

	var reqBody io.Reader
	if body != "" {
		reqBody = strings.NewReader(body)
	}

	req, err := http.NewRequest(method, url, reqBody)
	if err != nil {
		fmt.Printf("请求构造失败: %v\n\n", err)
		return
	}

	for k, v := range headers {
		req.Header.Set(k, v)
	}

	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("请求失败: %v\n\n", err)
		return
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	fmt.Printf("HTTP %d\n%s\n\n", resp.StatusCode, string(respBody))
}

// ============================================================
// POST 签名测试 (SIGNATURE_V3)
// ============================================================
func testPostSignature() {
	fmt.Println("========== POST 签名测试 (SIGNATURE_V3) ==========")

	timestamp := time.Now().UnixMilli()
	nonce := fastSimpleUUID()

	data := map[string]string{"sn": "abc123"}
	bodyJSON := sortJSON(data)

	// 签名计算使用 key 排序后的 JSON
	contentMD5 := md5Base64(bodyJSON)

	uri := "/openapi/v2/lock/get_lock_info"

	plaintext := fmt.Sprintf(SIGNATURE_PLAINTEXT_FMT, uri, contentMD5, timestamp, nonce)
	fmt.Printf("body:            %s\n", bodyJSON)
	fmt.Printf("content-md5:     %s\n", contentMD5)
	fmt.Printf("签名原文:\n%s\n\n", plaintext)

	signature := hmacSha256Base64(CLIENT_SECRET, plaintext)

	doRequest("POST", API_GATEWAY+uri, map[string]string{
		"Content-Type":         "application/json;charset=UTF-8",
		"x-open-auth-mode":    "SIGNATURE_V3",
		"x-open-client-id":   CLIENT_ID,
		"x-open-nonce":       nonce,
		"x-timestamp":        fmt.Sprintf("%d", timestamp),
		"x-open-content-md5": contentMD5,
		"x-open-signature":   signature,
	}, bodyJSON)
}

// ============================================================
// GET 签名: 无 contentMd5, queryString 拼接到 uri 再签名
// ============================================================
func testGetSignature() {
	fmt.Println("========== GET 签名测试 (SIGNATURE_V3, 无 content-md5) ==========")

	timestamp := time.Now().UnixMilli()
	nonce := fastSimpleUUID()

	uri := "/openapi/v2/lock/list_page"
	queryLine := queryString(map[string]string{"sn": "ABC123", "page": "1", "size": "10"})
	signedURI := uri + queryLine

	contentMD5 := ""

	plaintext := fmt.Sprintf(SIGNATURE_PLAINTEXT_FMT, signedURI, contentMD5, timestamp, nonce)
	fmt.Printf("signed_uri:      %s\n", signedURI)
	fmt.Printf("签名原文:\n%s\n\n", plaintext)

	signature := hmacSha256Base64(CLIENT_SECRET, plaintext)

	doRequest("GET", API_GATEWAY+signedURI, map[string]string{
		"x-open-auth-mode":  "SIGNATURE_V3",
		"x-open-client-id": CLIENT_ID,
		"x-open-nonce":     nonce,
		"x-timestamp":      fmt.Sprintf("%d", timestamp),
		"x-open-signature": signature,
	}, "")
}

func main() {
	testPostSignature()
	testGetSignature()
}
py
#!/usr/bin/env python3
import base64
import hashlib
import hmac
import json
import time
import urllib.parse
import uuid

import requests

# ============================================================
# 配置 (替换为实际值)
# ============================================================
API_GATEWAY = "https://openapi.xyiot.link"
CLIENT_ID = "your client_id"
CLIENT_SECRET = "your secret"

SIGNATURE_PLAINTEXT_FMT = "{}\n{}\n{}\n{}"


def fast_simple_uuid() -> str:
    """对应 IdUtil.fastSimpleUUID(): 无横线的 UUID (32 位 hex)"""
    return uuid.uuid4().hex


def sort_json(data: dict) -> str:
    """key 排序且紧凑输出"""
    return json.dumps(data, sort_keys=True, separators=(",", ":"))


def query_string(params: dict) -> str:
    """按参数名字典顺序生成 queryString"""
    if not params:
        return ""
    return "?" + urllib.parse.urlencode(sorted(params.items()))


def md5_base64(text: str) -> str:
    """对应 Base64.encodeBase64(SecureUtil.md5().digest(...))"""
    return base64.b64encode(hashlib.md5(text.encode("utf-8")).digest()).decode()


def hmac_sha256_base64(key: str, text: str) -> str:
    """对应 Base64.encodeBase64(SecureUtil.hmac(HmacSHA256, key).digest(...))"""
    return base64.b64encode(
        hmac.new(key.encode("utf-8"), text.encode("utf-8"), hashlib.sha256).digest()
    ).decode()


# ============================================================
# POST 签名测试 (SIGNATURE_V3)
# ============================================================
def test_post_signature():
    print("========== POST 签名测试 (SIGNATURE_V3) ==========")

    timestamp = int(time.time() * 1000)
    nonce = fast_simple_uuid()

    data = {"sn": "42B30119"}
    body_json = sort_json(data)
    content_md5 = md5_base64(body_json)

    uri = "/openapi/v2/lock/get_lock_info"

    # 签名原文: uri + contentMd5 + timestamp + nonce
    plaintext = SIGNATURE_PLAINTEXT_FMT.format(uri, content_md5, timestamp, nonce)
    print(f"签名原文:\n{plaintext}")

    signature = hmac_sha256_base64(CLIENT_SECRET, plaintext)

    resp = requests.post(
        API_GATEWAY + uri,
        headers={
            "Content-Type": "application/json;charset=UTF-8",
            "x-open-auth-mode": "SIGNATURE_V3",
            "x-open-client-id": CLIENT_ID,
            "x-open-nonce": nonce,
            "x-timestamp": str(timestamp),
            "x-open-content-md5": content_md5,
            "x-open-signature": signature,
        },
        data=body_json.encode("utf-8"),
    )

    print(f"响应: {resp.text}")
    print()


# ============================================================
# GET 签名: 无 contentMd5, queryString 拼接到 uri 再签名
# ============================================================
def test_get_signature():
    print("========== GET 签名测试 (SIGNATURE_V3, 无 content-md5) ==========")

    timestamp = int(time.time() * 1000)
    nonce = fast_simple_uuid()

    uri = "/openapi/v2/lock/list_page"
    query_line = query_string({"sn": "ABC123", "page": "1", "size": "10"})
    signed_uri = uri + query_line   # GET 时将 queryString 拼入签名原文

    content_md5 = ""                # GET 请求 contentMd5 为空字符串

    plaintext = SIGNATURE_PLAINTEXT_FMT.format(signed_uri, content_md5, timestamp, nonce)
    print(f"签名原文:\n{plaintext}")

    signature = hmac_sha256_base64(CLIENT_SECRET, plaintext)

    # GET 请求不传 x-open-content-md5
    resp = requests.get(
        API_GATEWAY + signed_uri,
        headers={
            "x-open-auth-mode": "SIGNATURE_V3",
            "x-open-client-id": CLIENT_ID,
            "x-open-nonce": nonce,
            "x-timestamp": str(timestamp),
            "x-open-signature": signature,
        },
    )

    print(f"响应: {resp.text}")
    print()


if __name__ == "__main__":
    test_post_signature()
    test_get_signature()
php
#!/usr/bin/env php
<?php
// ============================================================
// 配置 (替换为实际值)
// ============================================================
define('API_GATEWAY', 'https://openapi.xyiot.link');
define('CLIENT_ID',   'your client_id');
define('CLIENT_SECRET', 'your secret');

const SIGNATURE_PLAINTEXT_FMT = "%s\n%s\n%s\n%s";

/**
 * 对应 IdUtil.fastSimpleUUID(): 无横线的 UUID v4 (32 位 hex)
 */
function fastSimpleUUID(): string
{
    $data = random_bytes(16);
    $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
    return bin2hex($data);
}

/**
 * key 排序且紧凑输出
 */
function sortJson(array $data): string
{
    ksort($data);
    return json_encode($data, JSON_UNESCAPED_UNICODE);
}

/**
 * 按参数名字典顺序生成 queryString
 */
function queryString(array $params): string
{
    if (empty($params)) {
        return '';
    }

    ksort($params);
    return '?' . http_build_query($params);
}

/**
 * 对应 Base64.encodeBase64(SecureUtil.md5().digest(...))
 */
function md5Base64(string $text): string
{
    return base64_encode(md5($text, true));
}

/**
 * 对应 Base64.encodeBase64(SecureUtil.hmac(HmacSHA256, key).digest(...))
 */
function hmacSha256Base64(string $key, string $text): string
{
    return base64_encode(hash_hmac('sha256', $text, $key, true));
}

/**
 * 发起 HTTP 请求并输出响应
 */
function doRequest(string $method, string $url, array $headers, ?string $body = null): void
{
    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_HTTPHEADER     => array_map(fn($k, $v) => "$k: $v", array_keys($headers), $headers),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
    ]);

    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    }

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error    = curl_error($ch);
    curl_close($ch);

    echo "HTTP $httpCode\n";
    echo $response ?: $error;
    echo "\n\n";
}

// ============================================================
// POST 签名测试 (SIGNATURE_V3)
// ============================================================
function testPostSignature(): void
{
    echo "========== POST 签名测试 (SIGNATURE_V3) ==========\n";

    $timestamp = intval(microtime(true) * 1000);
    $nonce = fastSimpleUUID();

    $data = ["sn" => "abc123"];
    $bodyJson = sortJson($data);
    $contentMd5 = md5Base64($bodyJson);

    $uri = "/openapi/v2/lock/get_lock_info";

    $plaintext = sprintf(SIGNATURE_PLAINTEXT_FMT, $uri, $contentMd5, $timestamp, $nonce);
    echo "body:            $bodyJson\n";
    echo "content-md5:     $contentMd5\n";
    echo "签名原文:\n$plaintext\n\n";

    $signature = hmacSha256Base64(CLIENT_SECRET, $plaintext);

    doRequest('POST', API_GATEWAY . $uri, [
        'Content-Type'          => 'application/json;charset=UTF-8',
        'x-open-auth-mode'     => 'SIGNATURE_V3',
        'x-open-client-id'    => CLIENT_ID,
        'x-open-nonce'        => $nonce,
        'x-timestamp'         => (string) $timestamp,
        'x-open-content-md5'  => $contentMd5,
        'x-open-signature'    => $signature,
    ], $bodyJson);
}

// ============================================================
// GET 签名: 无 contentMd5, queryString 拼接到 uri 再签名
// ============================================================
function testGetSignature(): void
{
    echo "========== GET 签名测试 (SIGNATURE_V3, 无 content-md5) ==========\n";

    $timestamp = intval(microtime(true) * 1000);
    $nonce = fastSimpleUUID();

    $uri = "/openapi/v2/lock/list_page";
    $queryLine = queryString(["sn" => "ABC123", "page" => "1", "size" => "10"]);
    $signedUri = $uri . $queryLine;

    $contentMd5 = "";

    $plaintext = sprintf(SIGNATURE_PLAINTEXT_FMT, $signedUri, $contentMd5, $timestamp, $nonce);
    echo "signed_uri:      $signedUri\n";
    echo "签名原文:\n$plaintext\n\n";

    $signature = hmacSha256Base64(CLIENT_SECRET, $plaintext);

    doRequest('GET', API_GATEWAY . $signedUri, [
        'x-open-auth-mode'  => 'SIGNATURE_V3',
        'x-open-client-id' => CLIENT_ID,
        'x-open-nonce'     => $nonce,
        'x-timestamp'      => (string) $timestamp,
        'x-open-signature' => $signature,
    ]);
}

// ---- main ----
testPostSignature();
testGetSignature();

星寓智能开放平台 OpenAPI 文档