成 都 狮 龙 书 廊 科 技 有 限 责 任 公 司
Chengdu Shilong Pearson Education technology Limited Liability Technology Group Co., Ltd.
头条号网站验证文件
客服电话:13904310313
公司总机:028-67876373
钉钉客服:17684321066
备案电话:15680712313
商标注册服务电话:15210354365
公安部备案号:22010602000144
google-site-verification: googlea5d4809e7c237a00.html
抖音开发接入指南狮龙书廊实例
¥100
组装参数 > 生成签名 > 拼装HTTP请求 > 发起HTTP请求 > 获得HTTP响应 > 解析json结果
抖店开放平台目前只提供正式环境给开发者,API调用地址为如下,请访问时拼接对应方法URL:
https://openapi-fxg.jinritemai.com
POST方式
注:由于历史原因,GET方式目前依旧支持,但后续可能会弃用
调用任何一个API都必须传入的参数,目前支持的公共参数有:
参数名称 | 参数类型 | 是否必须 | 示例值 | 参数描述 |
---|---|---|---|---|
method | string | 是 | product.getGoodsCategory | 调用的API接口名称 |
app_key | string | 是 | 3409409348479354011 | 应用创建完成后被分配的key |
access_token | string | 是 | c6f957da-1239-4343-84a1-c84e68915ff7 | 用于调用 API 的 access_token |
param_json | string | 是 | {"cid":"12","page":"1"} | 标准JSON类型 |
timestamp | string | 是 | 2020-09-15 14:48:13 | 时间戳,格式为yyyy-MM-dd HH:mm:ss,时区为GMT+8,例如:2016-01-01 12:00:00,和服务器时间相差超过10分钟会报错 |
v | string | 是 | 2 | API协议版本,当前版本为2 |
sign | string | 是 | 签名算法参照下面的介绍 | 输入参数签名结果 |
sign_method | String | 否 | hmac-sha256 | 签名算法类型(不传默认为md5算法签名) 1. hmac-sha256,采用hmac_sha_256算法签名(推荐); 2. md5,采用md5算法签名。 |
param_json字段应当放在POST body中传输,形式为「Content-Type: application/json」。里面是业务参数,按照Key的字典序排序,嵌套JSON也需要按Key排序。
剩余字段(包括method、app_key、access_token、timestamp、v、sign、sign_method)依旧采用url query方式传递。
参考curl命令
curl --location --request POST 'https://openapi-fxg.jinritemai.com/product/getGoodsCategory?method=product.getGoodsCategory&app_key=123456780×tamp=2011-06-16%2013:23:30&v=2&sign=ab3387e5&access_token=xxxxxxxx%0A' \ --header 'Content-Type: application/json' \ --data-raw '{"cid":"12","page":"1"}
注:timestamp也支持「2006-01-02 15:04:05」这个格式,但不推荐;sign_method也支持md5,但后续可能被弃用。
API调用除了必须包含公共参数外,如果API本身有业务级的参数也必须传入。
每个API的业务参数请参考 抖店开放平台API文档
为了防止API调用过程中被黑客恶意篡改,调用任何一个API都需要携带签名;服务端会根据请求参数,对签名进行验证,签名不合法的请求将会被拒绝。
目前支持的签名算法为hmac-sha256(推荐)、md5两种。
下面以PHP为例,详细解析一下签名算法:
STEP1:序列化参数
将paramJson参数序列化成JSON格式,并满足下面几个要求:
// 序列化前的对象 $a1 = array( "c" => 3, "b" => 2, "a" => array( "c" => 3, "b" => 2, "a" => 1, ), ); // 序列化后的结果 // {"a":{"a":1,"b":2,"c":3},"b":2,"c":3}
// 序列化前的对象 $a2 = array( "a" => 1.0, ); // 序列化后的结果 // {"a":1}
$a3 = array( 'a' => "&<>='/ô汉????",// 附加符号、中日韩、Emoji都不转义 ); // 序列化后的结果 // {"a":"&<>='/ô汉????"}
STEP2:计算签名
以app_key、method、param_json、timestamp、v这个顺序,把以上参数的键值对依次拼接在一起
$paramPattern = 'app_key'.$appKey.'method'.$method.'param_json'.$paramJson.'timestamp'.$timestamp.'v'.$v; // 拼接后的格式差不多是这样: // app_key***method***param_json***timestamp***v*
$signPattern = $appSecret.$paramPattern.$appSecret;
$sign = hash_hmac("sha256", $signPattern, $appSecret);
Java调用说明
PHP调用说明
C#调用说明
Golang调用说明
本文提供Java调用抖店开放平台API的示例代码给开发者参考。 另外,Java SDK当前已经在内测中,如有需要参与内测可提交工单联系抖店开放平台的工作人员。
Java:openjdk 16.0.2
Gson:2.8.7
以上为示例代码的测试环境,供参考。
// 序列化参数// 这一步看上去冗余,实际很重要。如果要自己实现,则必须保证这三点:// 1、保证JSON所有层级上Key的有序性// 2、保证JSON的所有数值不带多余的小数点// 3、保证转义逻辑与这段代码一致public static String marshal(Object o) { String raw = CustomGson.toJson(o); Map, ?> m = CustomGson.fromJson(raw, LinkedTreeMap.class); // 执行反序列化,把所有JSON对象转换成LinkedTreeMap return CustomGson.toJson(m); // 重新序列化,保证JSON所有层级上Key的有序性}private static final Gson CustomGson = new GsonBuilder() .registerTypeAdapter(LinkedTreeMap.class, newMapSerializer()) // 定制LinkedTreeMap序列化,确保所有key按字典序排序 .registerTypeAdapter(Integer.class, newNumberSerializer()) // 定制数值类型的序列化,确保整数输出不带小数点 .registerTypeAdapter(Long.class, newNumberSerializer()) // 同上 .registerTypeAdapter(Double.class, newNumberSerializer()) // 同上 .disableHtmlEscaping() // 禁用Html Escape,确保符号不转义:&<>=' .create(); // 为LinkedTreeMap定制的序列化器public static JsonSerializer> newMapSerializer() {return new JsonSerializer<>() {@Override public JsonElement serialize(LinkedTreeMap, ?> src, Type typeOfSrc, JsonSerializationContext context) { List keys = src.keySet().stream().map(Object::toString).sorted().toList(); JsonObject o = new JsonObject();for (String k : keys) { o.add(k, context.serialize(src.get(k))); }return o; } }; }// 为Number定制化的序列化器private static JsonSerializer newNumberSerializer() {return new JsonSerializer () {@Override public JsonElement serialize(T number, Type type, JsonSerializationContext context) {if (number instanceof Integer) {return new JsonPrimitive(number.intValue()); }if (number instanceof Long) {return new JsonPrimitive(number.longValue()); }if (number instanceof Double) {long longValue = number.longValue();double doubleValue = number.doubleValue();if (longValue == doubleValue) {return new JsonPrimitive(longValue); } }return new JsonPrimitive(number); } }; }
// 计算签名public static String sign(String appKey, String appSecret, String method, long timestamp, String paramJson) {// 按给定规则拼接参数 String paramPattern = "app_key" + appKey + "method" + method + "param_json" + paramJson + "timestamp" + timestamp + "v2"; String signPattern = appSecret + paramPattern + appSecret; System.out.println("sign_pattern:" + signPattern);return hmac(signPattern, appSecret); }// 计算hmacpublic static String hmac(String plainText, String appSecret) { Mac mac;try {byte[] secret = appSecret.getBytes(StandardCharsets.UTF_8); SecretKeySpec keySpec = new SecretKeySpec(secret, "HmacSHA256"); mac = Mac.getInstance("HmacSHA256"); mac.init(keySpec); } catch (NoSuchAlgorithmException | InvalidKeyException e) {return ""; }byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);byte[] digest = mac.doFinal(plainBytes); StringBuilder sb = new StringBuilder();for (byte b: digest) { sb.append(String.format("%02x", b)); }return sb.toString(); }
这里采用标准库的HttpURLConnection作为例子,建议开发者选用自己熟悉的库。
// 调用Open Api,取回数据public static String fetch(String appKey, String host, String method, long timestamp, String paramJson, String accessToken, String sign) throws IOException { String methodPath = method.replace('.', '/'); String u = host + "/" + methodPath +"?method=" + URLEncoder.encode(method, StandardCharsets.UTF_8) +"&app_key=" + URLEncoder.encode(appKey, StandardCharsets.UTF_8) +"&access_token=" + URLEncoder.encode(accessToken, StandardCharsets.UTF_8) +"×tamp=" + URLEncoder.encode(Long.toString(timestamp), StandardCharsets.UTF_8) +"&v=" + URLEncoder.encode("2", StandardCharsets.UTF_8) +"&sign=" + URLEncoder.encode(sign, StandardCharsets.UTF_8) +"&sign_method=" + URLEncoder.encode("hmac-sha256", StandardCharsets.UTF_8); URL url = new URL(u); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setDoOutput(true); conn.setDoInput(true); conn.setUseCaches(false); conn.setRequestMethod("POST"); conn.setRequestProperty("Accept", "*/*"); conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); OutputStream os = conn.getOutputStream(); os.write(paramJson.getBytes(StandardCharsets.UTF_8)); os.flush(); InputStream is = conn.getInputStream();byte[] all = is.readAllBytes();return new String(all, StandardCharsets.UTF_8); }
// 下面是一个批量加密接口的示例public static void main(String[] args) throws IOException {// 收集参数 String appKey = "*"; //替换成你的app_key String appSecret = "*"; // 替换成你的app_secret String accessToken = "*"; // 替换成你的access_token String host = "https://openapi-fxg.jinritemai.com"; String method = "order.batchEncrypt";long timestamp = System.currentTimeMillis() / 1000; Mapm2 = new HashMap<>(); m2.put("plain_text", "&<>='/ô汉????");//附加符号、中日韩、Emoji都不转义 m2.put("auth_id", "12345"); m2.put("is_support_index", false); m2.put("sensitive_type", 2); Map m = new HashMap<>(); m.put("batch_encrypt_list", new Object[]{m2});// 序列化参数 String paramJson = marshal(m); System.out.println("param_json:" + paramJson);// 计算签名 String signVal = sign(appKey, appSecret, method, timestamp, paramJson); System.out.println("sign_val:" + signVal);// 发起请求 String responseVal = fetch(appKey, host, method, timestamp, paramJson, accessToken, signVal); System.out.println("response_val:" + responseVal); }
本文提供Golang调用抖店开放平台API的示例代码给开发者作为参考。
// Marshal 序列化参数func Marshal(o interface{}) string {// 序列化一次 raw, _ := json.Marshal(o)// 反序列化为map m := make(map[string]interface{}) reader := bytes.NewReader(raw) decode := json.NewDecoder(reader) decode.UseNumber() _ = decode.Decode(&m)// 重新做一次序列化,并禁用Html Escape buffer := bytes.NewBufferString("") encoder := json.NewEncoder(buffer) encoder.SetEscapeHTML(false) _ = encoder.Encode(m) marshal := strings.TrimSpace(buffer.String()) // Trim掉末尾的换行符 return marshal }
// Sign 计算签名func Sign(appKey, appSecret, method string, timestamp int64, paramJson string) string {// 按给定规则拼接参数 paramPattern := "app_key" + appKey + "method" + method + "param_json" + paramJson + "timestamp" + strconv.FormatInt(timestamp, 10) + "v2" signPattern := appSecret + paramPattern + appSecret fmt.Println("sign_pattern:" + signPattern)return Hmac(signPattern, appSecret) }// Hmac 计算hmacfunc Hmac(s string, appSecret string) string { h := hmac.New(sha256.New, []byte(appSecret)) _, _ = h.Write([]byte(s))return hex.EncodeToString(h.Sum(nil)) }
// Fetch 调用Open Api,取回数据func Fetch(appKey, host, method string, timestamp int64, paramJson, accessToken, sign string) (string, error) { methodPath := strings.Replace(method, ".", "/", -1) params := url.Values{} params.Add("method", method) params.Add("app_key", appKey) params.Add("access_token", accessToken) params.Add("timestamp", strconv.FormatInt(timestamp, 10)) params.Add("v", "2") params.Add("sign", sign) params.Add("sign_method", "hmac-sha256") u := host + "/" + methodPath + "?" + params.Encode() reader := strings.NewReader(paramJson) req, err := http.NewRequest(http.MethodPost, u, reader)if err != nil {return "", nil } req.Header.Set("Accept", "*/*") req.Header.Set("Content-Type", "application/json;charset=UTF-8") do, err := http.DefaultClient.Do(req)if err != nil {return "", err } all, err := ioutil.ReadAll(do.Body)if err != nil {return "", err }return string(all), err }
func main() {// 收集参数 appKey := "*"// 替换成你的app_key appSecret := "*" // 替换成你的app_secret accessToken := "*" // 替换成你的access_token host := "https://openapi-fxg.jinritemai.com" method := "order.batchEncrypt" timestamp := time.Now().Unix() param := map[string]interface{}{"batch_encrypt_list": map[string]interface{}{"plain_text": "&<>='/ô汉????",//附加符号、中日韩、Emoji都不转义 "auth_id":"12345","is_support_index": false,"sensitive_type": 2, }, }// 序列化参数 paramJson := Marshal(param) fmt.Println("param_json:" + paramJson)// 计算签名 signVal := Sign(appKey, appSecret, method, timestamp, paramJson) fmt.Println("sign_val:" + signVal)// 发起请求 responseVal, err := Fetch(appKey, host, method, timestamp, paramJson, accessToken, signVal)if err != nil { fmt.Println("err:" + err.Error()) } fmt.Println("response_val:" + responseVal) }
本文提供C#调用抖店开放平台API的示例代码给开发者作为参考。
.NET 5.0.301
Newtonsoft.Json 13.0.1
以上为示例代码的测试环境,供参考。
// 序列化参数 // 这一步看上去冗余,实际很重要。如果要自己实现,则必须保证这三点: // 1、保证JSON所有层级上Key的有序性 // 2、保证JSON的所有数值不带多余的小数点 // 3、保证转义逻辑与这段代码一致 public static string Marshal(object o) { var raw = JsonConvert.SerializeObject(o); // 反序列化为JObject var dict = JsonConvert.DeserializeObject(raw); // 重新序列化 var settings = new JsonSerializerSettings(); settings.Converters = new List{new JObjectConverter(), new JValueConverter()}; return JsonConvert.SerializeObject(dict, Formatting.None, settings); } // 自定义JObject的序列化方法,确保对象的Key按字典序输出 class JObjectConverter : JsonConverter { public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { var val = value! as JObject; var props = val!.Properties().OrderBy(i => i.Name).ToList(); writer.WriteStartObject(); foreach (var p in props) { writer.WritePropertyName(p.Name); serializer.Serialize(writer, p.Value); } writer.WriteEndObject(); } public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override bool CanConvert(Type objectType) { return objectType == typeof(JObject); } } // 自定义JValue的序列化方法,确保浮点数输出时移除小数点后多余的零 class JValueConverter : JsonConverter { public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { var val = value! as JValue; if (val!.Type == JTokenType.Float) { var d = Convert.ToDouble(val.Value); var i = (long) d; if (Math.Abs(i - d) == 0) // 针对float,如果小数点后的零是多余的,那么按整数方式输出 { writer.WriteValue(i); return; } } writer.WriteValue(value); // 否则按原逻辑 } public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override bool CanConvert(Type objectType) { return objectType == typeof(JValue); } }
// 计算签名 public static string Sign(string appKey, string appSecret, string method, long timestamp, string paramJson) { // 按给定规则拼接参数 var paramPattern = "app_key" + appKey + "method" + method + "param_json" + paramJson + "timestamp" + timestamp + "v2"; var signPattern = appSecret + paramPattern + appSecret; Console.WriteLine("sign_pattern:" + signPattern); return Hmac(signPattern, appSecret); } // 计算hmac public static string Hmac(string plainText, string appSecret) { var h = new HMACSHA256(Encoding.UTF8.GetBytes(appSecret)); var sum = h.ComputeHash(Encoding.UTF8.GetBytes(plainText)); var sb = new StringBuilder(); foreach (byte b in sum) { sb.Append(b.ToString("x2")); } return sb.ToString(); }
// 调用Open Api,取回数据 public static string Fetch(string appKey, string host, string method, long timestamp, string paramJson, string accessToken, string sign) { var methodPath = method.Replace('.', '/'); var u = host + "/" + methodPath + "?method=" + HttpUtility.UrlEncode(method, Encoding.UTF8) + "&app_key=" + HttpUtility.UrlEncode(appKey, Encoding.UTF8) + "&access_token=" + HttpUtility.UrlEncode(accessToken, Encoding.UTF8) + "×tamp=" + HttpUtility.UrlEncode(timestamp.ToString(), Encoding.UTF8) + "&v=" + HttpUtility.UrlEncode("2", Encoding.UTF8) + "&sign=" + HttpUtility.UrlEncode(sign, Encoding.UTF8) + "&sign_method=" + HttpUtility.UrlEncode("hmac-sha256", Encoding.UTF8); var client = new HttpClient(); var req = new HttpRequestMessage(HttpMethod.Post, u); req.Headers.Add("Accept", "*/*"); var content = new ByteArrayContent(Encoding.UTF8.GetBytes(paramJson)); content.Headers.Add("Content-Type", "application/json;charset=UTF-8"); req.Content = content; var resp = client.Send(req); using var reader = new StreamReader(resp.Content.ReadAsStream(), Encoding.UTF8); return reader.ReadToEnd(); }
static void Main(string[] args) { TestCase(); return; // 收集参数 var appKey = "*"; //替换成你的app_key var appSecret = "*"; // 替换成你的app_secret var accessToken = "*"; // 替换成你的access_token var host = "https://openapi-fxg.jinritemai.com"; var method = "order.batchEncrypt"; var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); var m = new Dictionary{ { "batch_encrypt_list", new Dictionary { {"plain_text", "&<>='/ô汉????"},//附加符号、中日韩、Emoji都不转义 {"auth_id", "12345"}, {"is_support_index", false}, {"sensitive_type", 2.0}, } } }; // 序列化参数 var paramJson = Marshal(m); Console.WriteLine("param_json:" + paramJson); // 计算签名 var signVal = Sign(appKey, appSecret, method, timestamp, paramJson); Console.WriteLine("sign_val:" + signVal); // 发起请求 var responseVal = Fetch(appKey, host, method, timestamp, paramJson, accessToken, signVal); Console.WriteLine("response_val:" + responseVal); }
本文提供PHP调用抖店开放平台API的示例代码给开发者作为参考。
// 序列化参数,入参必须为关联数组 function marshal(array $param): string { rec_ksort($param); // 对关联数组中的kv,执行排序,需要递归 $s = json_encode($param, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); // 重新序列化,确保所有key按字典序排序 // 加入flag,确保斜杠不被escape,汉字不被escape return $s; } // 关联数组排序,递归 function rec_ksort(array &$arr) { $kstring = true; foreach ($arr as $k => &$v) { if (!is_string($k)) { $kstring = false; } if (is_array($v)) { rec_ksort($v); } } if ($kstring) { ksort($arr); } }
// 计算签名 function sign(string $appKey, string $appSecret, string $method, int $timestamp, string $paramJson): string { $paramPattern = 'app_key' . $appKey . 'method' . $method . 'param_json' . $paramJson . 'timestamp' . $timestamp . 'v2'; $signPattern = $appSecret . $paramPattern . $appSecret; print('sign_pattern:' . $signPattern . "\n"); return hash_hmac("sha256", $signPattern, $appSecret); }
// 调用Open Api,取回数据 function fetch(string $appKey, string $host, string $method, int $timestamp, string $paramJson, string $accessToken, string $sign): string { $methodPath = str_replace('.', '/', $method); $url = $host . '/' . $methodPath . '?method=' . urlencode($method) . '&app_key=' . urlencode($appKey) . '&access_token=' .urlencode($accessToken) . '×tamp=' . urlencode(strval($timestamp)) . '&v=' . urlencode('2') . '&sign=' . urlencode($sign) . '&sign_method=' . urlencode('hmac-sha256'); $opts = array('http' => array( 'method' => 'POST', 'header' => "Accept: */*\r\n" . "Content-type: application/json;charset=UTF-8\r\n", 'content' => $paramJson ) ); $context = stream_context_create($opts); $result = file_get_contents($url, false, $context); return $result; }
// 收集参数 $appKey = '*'; //替换成你的app_key $appSecret = '*'; // 替换成你的app_secret $accessToken = '*'; // 替换成你的access_token $host = 'https://openapi-fxg.jinritemai.com'; $method = 'order.batchEncrypt'; $timestamp = time(); $m = array( 'batch_encrypt_list' => array( 0 => array( 'plain_text' => "&<>='/ô汉????",//附加符号、中日韩、Emoji都不转义 'auth_id' => '12345', 'is_support_index' => false, 'sensitive_type' => 2, ) ) ); // 序列化参数 $paramJson = marshal($m); print('param_json:' . $paramJson . "\n"); // 计算签名 $signVal = sign($appKey, $appSecret, $method, $timestamp, $paramJson); print('sign_val:' . $signVal . "\n"); // 发起请求 $responseVal = fetch($appKey, $host, $method, $timestamp, $paramJson, $accessToken, $signVal); print('response_val:' . $responseVal . "\n");
对于ISV或者自研商家来说,只有官方同学添加相关场景的授权之后,开发者才可以在自己的控制台看到相关的场景及需要实现的接口及接口文档。
!
GET/POST
HTTPS
为了防止API调用过程中被黑客恶意篡改,抖店开放平台在发起调用任何一个API都会携带签名。ISV服务端在接收到请求后需要对请求做验签,确保请求是来自抖店开放平台的而不是其他的非法请求。目前从抖店开放平台发出的请求均采用MD5进行加密。
说明:验签只是为了验证参数有没有被篡改,所以服务商在接收到抖店开放平台的请求后**一定要把验签放在第一步、一定要把验签放在第一步、一定要把验签放在第一步。**验签测试阶段,我们构造的测试参数并不一定完全符合业务逻辑,但是这不会影响验签。一定要拿着我们原始的请求去做签名计算,而不是先反序列化或者解析成你们自己定义的结构体后用你们自己的结构体去做签名计算,这样有可能导致签名失败。即便现在不失败,后面我们新增参数的话,你们的接口可能会出兼容问题。
1、将param_json中参数按照key字母先后顺序排序,组成json
说明:如果是GET请求 param_json请从url中获取,如果是post请求,param_json的value为post请求的body
2、请求参数按照字母先后顺序排列
3、把参数名和参数值进行拼装
4、把app_secret分别拼接在上面得到的字符串的两端,假定app_secret的值为AppSecret
5、上述步骤获得的待加密字符串,使用 MD5签名算法后得到sign,与请求中的sign进行比较,如果不一致则验签失败。
注:计算签名逻辑请使用下面的示例代码!!!不要自己实现
SPISDK已经上线,推荐使用SDK对接,SDK说明文档:抖店开放平台SPI&消息网关SDK,使用SDK对接可跳过下面示例代码部分
以接口“demo/spi”为例
public Object spiDemo() { DemoSpiRequest request =new DemoSpiRequest(); //param中的参数从服务端调用你们发布的地址上取 //sign timestamp sign_method app_key sign_v2从URL参数取 //paramJson分为两种情况,如果是get请求,从URL参数取,如果post请求从请求体里取 //举个例子,发布的地址是http://www.abc.com //服务端实际调用时的请求地址是:http://www.abc.com?app_key=xxx&sign=xxx&sign_v2=xxx&sign_method=xxx×tamp=xxx¶m_json=xxx request.getParam().setSign( xxxx ); request.getParam().setTimestamp( xxxx ); request.getParam().setSignMethod( xxxx ); request.getParam().setAppKey( xxxx ); request.getParam().setParamJson( xxxx ); request.getParam().setSignV2( xxxx ); request.registerHandler(bizHandler); DemoSpiResponse response = request.execute(); //将response序列化成json返回 return response; } private DoudianOpSpiBizHandler bizHandler = new DoudianOpSpiBizHandler() { @Override public void handle(DoudianOpSpiContext context) { // 获取入参对象 DemoSpiParam param = context.getParam(); // 业务处理逻辑 // ... // 设置data数据 DemoSpiData data = context.getData(); // data.setXXX() // 返回成功 context.wrapSuccess(); // 返回失败 context.wrapError(100002L,系统错误 ); } };
import ( "code.byted.org/middleware/hertz" "crypto/md5" "encoding/hex" "encoding/json" "io" "sort" ) // TestSpi . // @router /api/spi/test [POST] func TestSpi(c *hertz.RequestContext) { //1.从请求中解析入参 appKey := c.Query("app_key") paramJson := c.Query("param_json") timestamp := c.Query("timestamp") sign := c.Query("sign") //2.参数校验,此处省略 //3.验签 //3.1.解析业务参数 paramJsonInterfaceMap := make(map[string]interface{}) err := json.Unmarshal([]byte(paramJson), ¶mJsonInterfaceMap) if err != nil { c.JSON(200, &DemoResult{ Code:100002, Message: "参数解析失败", }) return } //3.2.把param_json的key按照自然顺序排序 mm := getJsonString(paramJsonInterfaceMap) sortedParamJson, _ := json.Marshal(mm) sortedString := "app_key" + appKey + "param_json" + string(sortedParamJson) + "timestamp" + timestamp //3.3.拼接 appSecret 在头尾 appSecret := "63415a7a-de83-43ea-a522-cb616c47a4ef" preSignString := appSecret + sortedString + appSecret //3.4使用md5对参数加签 newSign := Md5(preSignString) //3.5.将入参中的sign和加签后的结果作比较,如果不一致则验签失败。 if newSign != sign { c.JSON(200, &DemoResult{ Code:100001, Message: "验签失败", }) return } //4.业务处理,此处省略 //5.组装响应结果 c.JSON(200, &DemoResult{ Code:0, Message: "success", Data: &TestResult{ Total: 100, Test:false, List: []*RefundOrder{ &RefundOrder{ RefundId: "11111", RefundReason: "七天无理由", }, }, }, }) } func Md5(s string) string { h := md5.New() _, _ = io.WriteString(h, s) return hex.EncodeToString(h.Sum(nil)) } func getJsonString(param map[string]interface{}) interface{} { sortedKeys := make([]string, 0) for k := range param { sortedKeys = append(sortedKeys, k) } sort.Strings(sortedKeys) sortedParamJsonMap := make(map[string]interface{}) for _, sk := range sortedKeys { switch value := param[sk].(type) { case string: sortedParamJsonMap[sk] = value case map[string]interface{}: sortedParamJsonMap[sk] = getJsonString(value) case []interface{}: if len(value) > 0 { if _, ok := value[0].(map[string]interface{}); ok { var sliceResult = make([]interface{}, 0) for _, interfaceValue := range value { switch mapInterfaceValue := interfaceValue.(type) { case map[string]interface{}: sliceResult = append(sliceResult, getJsonString(mapInterfaceValue)) } } sortedParamJsonMap[sk] = sliceResult } else { sortedParamJsonMap[sk] = value } } else { sortedParamJsonMap[sk] = make([]interface{}, 0) } default: sortedParamJsonMap[sk] = value } } return sortedParamJsonMap } type DemoResult struct { Codeint64 `json:"code"` Message string`json:"message"` Data*TestResult `json:"data"` } type TestResult struct { Total int64`json:"total"` Testbool `json:"test"` List[]*RefundOrder `json:"list"` } type RefundOrder struct { RefundId string `json:"refund_id"` RefundReason string `json:"refund_reason"` } import ( "code.byted.org/middleware/hertz" "crypto/md5" "encoding/hex" "encoding/json" "io" "sort" ) // TestSpi . // @router /api/spi/test [POST] func TestSpi(c *hertz.RequestContext) { //1.从请求中解析入参 appKey := c.Query("app_key") paramJson := c.Query("param_json") timestamp := c.Query("timestamp") sign := c.Query("sign") //2.参数校验,此处省略 //3.验签 //3.1.解析业务参数 paramJsonInterfaceMap := make(map[string]interface{}) err := json.Unmarshal([]byte(paramJson), ¶mJsonInterfaceMap) if err != nil { c.JSON(200, &DemoResult{ Code:100002, Message: "参数解析失败", }) return } //3.2.把param_json的key按照自然顺序排序 mm := getJsonString(paramJsonInterfaceMap) sortedParamJson, _ := json.Marshal(mm) sortedString := "app_key" + appKey + "param_json" + string(sortedParamJson) + "timestamp" + timestamp //3.3.拼接 appSecret 在头尾 appSecret := "63415a7a-de83-43ea-a522-cb616c47a4ef" preSignString := appSecret + sortedString + appSecret //3.4使用md5对参数加签 newSign := Md5(preSignString) //3.5.将入参中的sign和加签后的结果作比较,如果不一致则验签失败。 if newSign != sign { c.JSON(200, &DemoResult{ Code:100001, Message: "验签失败", }) return } //4.业务处理,此处省略 //5.组装响应结果 c.JSON(200, &DemoResult{ Code:0, Message: "success", Data: &TestResult{ Total: 100, Test:false, List: []*RefundOrder{ &RefundOrder{ RefundId: "11111", RefundReason: "七天无理由", }, }, }, }) } func Md5(s string) string { h := md5.New() _, _ = io.WriteString(h, s) return hex.EncodeToString(h.Sum(nil)) } func getJsonString(param map[string]interface{}) interface{} { sortedKeys := make([]string, 0) for k := range param { sortedKeys = append(sortedKeys, k) } sort.Strings(sortedKeys) sortedParamJsonMap := make(map[string]interface{}) for _, sk := range sortedKeys { switch value := param[sk].(type) { case string: sortedParamJsonMap[sk] = value case map[string]interface{}: sortedParamJsonMap[sk] = getJsonString(value) case []interface{}: if len(value) > 0 { if _, ok := value[0].(map[string]interface{}); ok { var sliceResult = make([]interface{}, 0) for _, interfaceValue := range value { switch mapInterfaceValue := interfaceValue.(type) { case map[string]interface{}: sliceResult = append(sliceResult, getJsonString(mapInterfaceValue)) } } sortedParamJsonMap[sk] = sliceResult } else { sortedParamJsonMap[sk] = value } } else { sortedParamJsonMap[sk] = make([]interface{}, 0) } default: sortedParamJsonMap[sk] = value } } return sortedParamJsonMap } type DemoResult struct { Codeint64 `json:"code"` Message string`json:"message"` Data*TestResult `json:"data"` } type TestResult struct { Total int64`json:"total"` Testbool `json:"test"` List[]*RefundOrder `json:"list"` } type RefundOrder struct { RefundId string `json:"refund_id"` RefundReason string `json:"refund_reason"` }
127.0.0.1:6789/shop/user/register?app_key=6900812651828348424¶m_json=%7B%22order_id%22%3A%221234%22%2C%22page%22%3A10%2C%22size%22%3A11%7D&sign=6c4447b0bf1898d38f78ab80f7d86e46×tamp=2021-06-01+21%3A49%3A17
{ "code": 0, "message": "success", "data": { "total": 100, "test": false, "list": [ { "refund_id": "11111", "refund_reason": "七天无理由" } ]
{ "code": 100001, "message": "验签失败", "data": null }
错误码 | 说明 |
---|---|
0 | 业务处理成功 |
100001 | 验签失败 |
100002 | 参数错误 |
100003 | 系统错误 |
更多业务错误码请参考各个SPI接口的接口文档说明。
ISV在完成代码研发后,可以在抖店开发者控制台发起接口自测,自测通过之后可以将服务端url去发布。
接口自测通过后可以将服务端地址发布到对应的接口上。接口发布成功之后字节内部的业务就可以发起相关的调用啦。
为了方便排查问题,抖店开放平台在向服务商的地址发起请求时会把平台的logId放到请求的hedaer中,建议开发者在接受到请求后,除了打印请求入参外把logId也打印或者记录下来。