获取密钥生成签名
应用在调用孪生中台 API 之前,首先需要获取接口鉴权的签名。您需要使用应用所分配的 appId (应用 ID)和 AppKey,参考下方示例代码,生成 API 接口鉴权签名。
请求路径:/proxy/gateway/application/GenerateToken
请求方法:POST
请求参数:
nonce:随机字符串,不可重复,建议使用 uuid。
timestamp:当前时间戳(毫秒),前后五分钟有效。
注意:
tenantId:应用被下发到其他租户后,会生成新的 appId(应用 ID) 和 appSecret,所以请求时需要使用新生成的 appId(应用 ID)和 appSecret,或者使用原始 appId(应用 ID)+ 原始 appSecret + 对应租户的 tenantId,来为不同的租户生成 token。

应用签名生成规则
拼接字符串:
appId=10001&appSecret=xxxxxxxxxxxxxxxxxxxxxxxx&nonce=VlghmWSvnod7MvcC×tamp=1640783576118
对该字符串进行 md5 加密生成32位签名(小写)
sign = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
import org.apache.commons.lang3.RandomStringUtils;import java.nio.charset.StandardCharsets;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.util.Objects;public class SignUtil {/*** 生成签名** @param appId 应用ID* @param appSecret 应用密钥* @return 生成的签名*/public static String generateSign(Long appId, String appSecret) throws NoSuchAlgorithmException {Objects.requireNonNull(appId, "应用ID不能为空");Objects.requireNonNull(appSecret, "应用密钥不能为空");long timestamp = System.currentTimeMillis();String nonce = RandomStringUtils.randomAlphanumeric(32);String strToMD5 = "appId=" + appId + "&appSecret=" + appSecret + "&nonce=" + nonce + "×tamp=" + timestamp;return calculateMD5(strToMD5);}/*** 计算字符串的MD5哈希值** @param plainText 待计算的字符串* @return 字符串的MD5哈希值*/public static String calculateMD5(String plainText) throws NoSuchAlgorithmException {Objects.requireNonNull(plainText, "输入字符串不能为空");MessageDigest md = MessageDigest.getInstance("MD5");byte[] hashBytes = md.digest(plainText.getBytes(StandardCharsets.UTF_8));StringBuilder md5code = new StringBuilder();for (byte b : hashBytes) {String hex = Integer.toHexString(0xFF & b);if (hex.length() == 1) {md5code.append("0");}md5code.append(hex);}return md5code.toString();}}
message genTokenReq {string appId = 1;int64 timestamp = 2;string nonce = 3;string sign = 4;int32 tenantId = 5;}secret := "xxxx"req := &apigateway.GenTokenReq{AppId: "xxx",Nonce: uuid.New().String(),TenantId: xxxx,Timestamp: time.Now().UnixMilli(),}req.Sign = GenSign(secret, req.AppId, req.Nonce, req.Timestamp)func GenSign(secret, appid, nonce string, timestamp int64) string {// 创建一个mapparams := map[string]string{"appId": appid,"appSecret": secret,"nonce": nonce,"timestamp": strconv.FormatInt(timestamp, 10),}// 对map的键进行字典序排序keys := make([]string, 0, len(params))for k := range params {keys = append(keys, k)}sort.Strings(keys)// 拼接map的键值对var str stringfor i, k := range keys {str += k + "=" + params[k]if i != len(keys)-1 {str += "&"}}// 使用md5算法对字符串进行签名hash := md5.Sum([]byte(str))signature := hex.EncodeToString(hash[:])return signature}
HTTP/HTTPS
OPEN API Doc 声明定义包含接口名称、接口描述、接口定义、输入参数、请求示例、返回参数、错误码。
请求路径:/proxy/*
请求方法:*
请求头携带参数:
字段 | 数据类型 | 是否必须 | 释义 | 示例 |
Dtgw-Token | string(16) | 是 | 登录后由应用中心返回,之后访问需要携带至请求头中。用于验证身份,并确认该请求所属应用 | D6heVfeQT7RnINhM |
WebSocket
下面以孪生底座公共租户下的设备通知 API 和巡检应用为例,讲述应用在某个 WS API 下建立 WebSocket 链接的过程。
设备通知 API,API ID 为:bff0bda8-95ed-4034-b36b-0c533bf4a077。


巡检应用,应用 appid 是:10097。
1. 应用请求 WS API 建立 WS 连接:
示例如下:
wscat -c'ws://ws.dtwin.tencent.com/proxy/bff0bda8-95ed-4034-b36b-0c533bf4a077/weiling-pubsub/test?Dtgw-Token=gi9UyVNnSK3tpbTekBBryqnbmJ3Ta5PT'
或:
wscat -c'ws://ws.dtwin.tencent.com/proxy/bff0bda8-95ed-4034-b36b-0c533bf4a077/weiling-pubsub/test' -H "Dtgw-Token: gi9UyVNnSK3tpbTekBBryqnbmJ3Ta5PT"
其中:gi9UyVNnSK3tpbTekBBryqnbmJ3Ta5PT,是第0步获取的应用请求 API 的 token。token 可以放在请求头,也可以放在 url 的 query string 中。
说明:
(ws://ws.dtwin.tencent.com/proxy/bff0bda8-95ed-4034-b36b-0c533bf4a077/weiling-pubsub/test) WS API 要和资源中心看到的一样不能篡改。
2. WS 连接建立后,要进行具体的订阅操作:
WS 通道,消息传递格式是统一的 json 格式,格式定义如下:

参数说明:
1. ver:版本号,当前版本统一为1。
2. operation:操作,是一个枚举值,每个枚举值表达是一种类型的命令,不同的命令 body 是不同的 json 结构。
3. seq:请求响应序列,递增即可,请求和响应一一对应。
4. topic:只有在 operation 为9的时候有意义,代表 client 收到的消息是来自哪个 topic。有这个字段的原因是,订阅 topic 支持2级,通配订阅,详情请参见 订阅示例说明。
5. body:不同 operation 命令下是不同的 json 结构,可参见如下示例。
6. 具体推送的消息唯一 id,通常是 traceid,下行消息才有。
具体 operation 命令枚举定义如下,需要用到的是1 ~9和100:


其中 topic 支持两级目录结构的形式: /first/second,并支持*表示通配某一级,例如:
订阅 topic: /device/*,那么/device/add,/device/del,/device/xx 等 topic 上的消息都能收到,并且在推送的消息结构里面会有具体是哪个 topic 的消息字段。
订阅 topic:/*/*,则关于某个 ws api 的所有 topic 消息都能收到。
以下是
各个操作的示例
:订阅
请求:
{"ver":1,"operation":1,"seq_id":10001,"body":"{\\"sub_topic\\":[\\"/citybase/*\\",\\"/cim/add\\"]}"}
响应:
{"ver":1,"operation":2,"seq_id":10001,"body":"{\\"message\\":\\"ok\\"}"}
更新 topic
请求:
{"ver":1,"operation":3,"seq_id":10002,"body":"{\\"sub_topic\\":[\\"/cim/del\\",\\"/citybase/get\\"]}"}
响应:
{"ver":1,"operation":4,"seq_id":10001,"body":"{\\"message\\":\\"ok\\"}"}
取消订阅主题
请求:
{"ver":1,"operation":5,"seq_id":10003,"body":"{\\"sub_topic\\":[\\"/citybase/get\\"]}"}
响应:
{"ver":1,"operation":6,"seq_id":10003,"body":"{\\"message\\":\\"ok\\"}"}
心跳,维持 WS 连接,需要每隔45s发送一个心跳包,否则连接空闲,会关闭 WS 连接
请求:
{"ver":1,"operation":7,"seq_id":100011,"body":""}
响应:
{"ver":1,"operation":8,"seq_id":10001,"body":"{\\"gap\\":45}"}
接收业务推送的消息
{"ver":1,"operation":9,"topic":"/cim/add","body":"{\\"uid\\": 102002, \\"name\\": \\"dtwin test 4444\\"}"}
错误回复,如果发送错误,会发送一个错误的 WS 回复,错误回复的 operation 是100
{"ver":1,"operation":100,"seq_id":10001,"body":"{\\"code\\":110011,\\"message\\":\\"duplicate Subscribe operation\\"}"}
3. 示例:
以下是用 wscat 客户端,进行的一个完整流程的示例,白色是发送的请求,蓝色是收到的消息/响应。

说明:
WS 连接后的,第一个请求包一定要是订阅请求,operation 是1。
具体要订阅的 topic,应用根据具体的 WS API 的相关说明文档来进行订阅即可。
平台回调
考虑到适配的孪生应用支持多租户和多项目空间的产品形态,每个租户下的应用 ID 和 apiID 相对独立,孪生应用为了适配一次,达到万千租户开箱即用,注册应用时填写“消息通知服务回调地址”,并为应用授权“应用中心通知 API”这个消息通知服务接口,开通租户时,孪生底座通过调用应用所提供“消息通知服务回调地址”服务,通知孪生应用新租户下的应用 ID,达到不同租户下应用 ID 和 apiID 的应用权限系统相互隔离。
1. 孪生应用适配孪生底座的业务逻辑图:

2. 填写孪生应用“消息通知服务回调地址”。

3. 授权孪生应用消息通知服务接口“应用中心通知 API”

4. 应用创建回调消息:
business_type 为10001表示是来自应用中心的消息。
eventType 为1表示是应用创建的消息,不同 eventType 对应的 data 协议不同。
{"business_type": 10001,"data": "{\\"eventType\\":1,\\"data\\":\\"{\\\\\\"appId \\\\\\":26826,\\\\\\"appSecret\\\\\\":\\\\\\"***\\\\\\",\\\\\\"name\\\\\\":\\\\\\"智能资产管理\\\\\\",\\\\\\"originAppId\\\\\\":10041,\\\\\\"tenantId\\\\\\":100215}\\"}","event_id": "GBKHb4bbCFSSBlpicE4a37nban4bJeQt"}
上图代码示例协议内容说明:
字段名 | 数据类型 | 说明 |
tenantId | integer | 租户 id |
appId | integer | 新生成的 appId |
appSecret | string | 新生成的 appSecret |
name | string | 应用名称 |
originAppId | integer | 原始应用 id |
5. 应用下发到项目空间回调消息:
business_type 为10001表示是来自应用中心的消息。
eventType 为2表示是应用下发到项目空间的消息,不同 eventType 对应的 data 协议不同。
{"business_type": 10001,"data": "{\\"eventType\\":2,\\"data\\":\\"{\\\\\\"tenantName\\\\\\":\\\\\\"\\\\\\",\\\\\\"username\\\\\\":\\\\\\"dd\\\\\\",\\\\\\"tenantStatus\\\\\\":0,\\\\\\"tenantId\\\\\\":100101,\\\\\\"email\\\\\\":\\\\\\"22@2.cc\\\\\\",\\\\\\"workspaceId\\\\\\":1214,\\\\\\"workspaceName\\\\\\":\\\\\\"空间11\\\\\\",\\\\\\"workspaceDescription\\\\\\":\\\\\\"1\\\\\\",\\\\\\"workspaceStatus\\\\\\":0,\\\\\\"appId\\\\\\":20861,\\\\\\"appSecret\\\\\\":\\\\\\"43MGE0GN3Coc85KSotCn7BlkVHFs7Gf5\\\\\\",\\\\\\"name\\\\\\":\\\\\\"能源管理\\\\\\"}\\"}","event_id": "GBKHb4bbCFSSBlpicE4a37nban4bJeQt"}
上图代码示例协议内容说明:
字段名 | 数据类型 | 说明 |
tenantName | string | 租户名称 |
username | string | 租户负责人名称 |
email | string | 租户负责人 email |
tenantStatus | integer | 租户状态: 1:禁用 0:启用 -1:删除 |
tenantId | integer | 租户 id |
workspaceId | integer | 空间 id |
workspaceName | string | 空间名称 |
workspaceDescription | string | 空间描述 |
workspaceStatus | integer | 工作空间状态: 0:启用 1:停用 -1:已删除 |
appId | integer | 新生成的 appId |
appSecret | string | 新生成的 appSecret |
name | string | 应用名称 |
originAppId | integer | 原始应用 id |