# 服务云开放平台
> 服务云开放平台是西部机场集团面向全集团所有机场推出的在线旅客服务能力开放,自2018年推出以来,目前支持了各机场及成员企业共20多个SAAS化旅客服务在线应用,对外提供能力和数据接口服务,本接口文档包含了如何接入开放平台、现有的API接口分类和接口明细以及在对接中的常见问题,最大化的指导第三方平台和系统的接入与接出
本文主要提供给第三方软件服务商或有开发能力的成员机场公司将自有系统接入服务云使用
> 服务云API对于环境要求近乎没有,API的返回数据均使用的是JSON格式,PHP、C、C#、C++、ASP、Javascript等等只要支持JSON格式或XML的都可以调用!
本接口开放平台提供的都是测试环境的地址,在测试环境对接调试完成后,我们会提供具体接口的生产环境地址
测试环境地址:
*****
## 接入指南
![](https://img.kancloud.cn/d9/15/d915093a2e38f564ae7f79a7b301b87e_741x48.png)
#
> 1. 申请appcode和secretKey
*****
第三方需线下申请服务云应用参数,因为每个接口调用都需要应用代码(appcode)进行签名验证,所以在调用前需要申请两个参数,如果已经申请过了则不需要重复申请,分配的应用参数包括:
* appCode: 应用code
* secretKey: 应用key
申请appcode请联系服务云技术负责人任波,微信手机同号18309287010
#
> 2. 开通接口权限
*****
调用方有了appcode和secretKey后,需线下告诉服务云需要调用的接口是什么,服务云进行第三方应用和接口的授权,第三方需要提供以下信息
* 调用场景,用来做什么
* 调用频层,大概并发有多少
* 是否有同时多路调用的情况
* 分配的appcode
#
> 3、公共参数
*****
每个调用需要的请求公共参数包括以下几个(调用接口时务必带上以下参数):
* appCode:应用code (由服务云提供)
* timeStamp:时间戳(精度为毫秒,时间以标准北京时间为准,时间差不能大于服务器设置的值,当前设置为300秒)
* sign:签名(签名方式参考下面实例)
**注意:secretKey无需作为参数向后台传递,secretKey是为了生成sign使用,请第三方系统保存好,不要外泄**
每个调用需要返回参数有:
* code: 0 - 失败 1 - 成功
* message: 请求失败或者发生错误的具体描述,部分业务消息中携带接口编号
* timeStamp:请求的时间戳
#
> 4、请求方式
*****
* 所有接口请求方式均采用POST方式;
* Content-type 使用: application/x-www-form-urlencoded; charset=utf-8
#
> 5、签名方法
*****
* [ ] 生成签名步骤
有了安全凭证 appCode和 secretKey后,就可以生成签名串了。生成签名串的详细过程如下:
![](https://qqadapt.qpic.cn/txdocpic/0/e2ad2107ed2819e1c2ee00902438b0d0/0?_type=png)
* [ ] 签名举例
* 假如用户的appCode和secretKey如下
appCode:AS015
secretKey: \u9017\u6bd4
#
* 假如需要请求的参数为
name:admin
age:30
timestamp当前时间戳:1545927421045
appCode:U8Q5BKRT27BI
总结:公共请求参数有:appCode和timestamp;业务请求参数有:name和age
#
* 参数排序
首先对所有请求参数按参数名做字典序升序排列。(所谓字典序升序排列,直观上就如同在字典中排列单词一样排序,按照字母表或数字表里递增顺序的排列次序,即先考虑第一个“字母”,在相同的情况下考虑第二个“字母”,依此类推)。上述示例参数的排序结果如下:
```
"age" : 30,"appCode" : "U8Q5BKRT27BI","name" : "admin","timeStamp" : 1545927421045
```
**注意:参数排序时不需要加sign,我们看到age排在appCode前面是因为它们的第二个字母g在p前面**
* 拼接请求字符串
此步骤将生成请求字符串,将把上一步排序好的请求参数格式化成“参数名称”=“参数值”的形式,如对 age 参数,其参数名称为"age",参数值为"30",因此格式化后就为 age=30,然后将格式化后的各个参数用"&"拼接在一起,最终生成的请求字符串为:
```
age=30&appCode=U8Q5BKRT27BI&name=admin&timeStamp=1545927421045
```
* 生成签名串
签名使用HmacSHA1 算法进行签名
使用签名算法HmacSHA1和secretKey对上一步中获得的 请求字符串 进行签名,获得最终的签名串。
最终得到的签名串为:
3359CF98FE4BB6BDC99B157165E32B4E02651926
最后将生成的签名串作为参数sign的值拼接到请求中传到后台。
```
age=30&appCode=U8Q5BKRT27BI&name=admin&timeStamp=1545927421045&sign=3359CF98FE4BB6BDC99B157165E32B4E02651926
```
* [ ] 签名串生成java版代码示例
~~~
package com.cwag.pss.ipss.butt.provider.util;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
/**
* 签名
* 2018-12-25 20:21
*
* @author yangk
**/
@Component
@Slf4j
public class SignUtil {
@Value("${apiAuth.authKey}")
private String authKey;
// HMAC 加密算法名称
public static final String HMAC_MD5 = "HmacMD5";// 128位
public static final String HMAC_SHA1 = "HmacSHA1";// 126
//获取secretKey
public String getSecretKey(String appCode) {
TimeInterval time = DateUtil.timer();
authKey = authKey.toUpperCase();
appCode = appCode.toUpperCase();
String secretKey = hmacDigest(appCode, authKey, HMAC_MD5);
System.out.println("appCode:" + appCode);
System.out.println("secretKey:" + secretKey);
return secretKey;
}
//加密
public String getSign(SortedMap<Object, Object> parameters, String secretKey) {
StringBuffer sb = new StringBuffer();
StringBuffer sbkey = new StringBuffer();
Iterator it = parameters.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v)) {
sb.append(k + "=" + v + "&");
sbkey.append(k + "=" + v + "&");
}
}
String valStr = sb.toString().substring(0, sbkey.toString().length() - 1);
log.info("字符串:" + valStr);
String sign = sbkey.toString().substring(0, sbkey.toString().length() - 1);
sign = hmacDigest(sign, secretKey, HMAC_SHA1);
log.info("加密值:" + sign);
return sign;
}
/**
* 生成HMAC摘要
*
* @param plaintext 明文
* @param secretKey 安全秘钥
* @param algName 算法名称
* @return 摘要
*/
public static String hmacDigest(String plaintext, String secretKey, String algName) {
try {
Mac mac = Mac.getInstance(algName);
byte[] secretByte = secretKey.getBytes();
byte[] dataBytes = plaintext.getBytes();
SecretKey secret = new SecretKeySpec(secretByte, algName);
mac.init(secret);
byte[] doFinal = mac.doFinal(dataBytes);
return byte2HexStr(doFinal);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* 字节数组转字符串
*
* @param bytes 字节数组
* @return 字符串
*/
private static String byte2HexStr(byte[] bytes) {
StringBuilder hs = new StringBuilder();
String stmp;
for (int n = 0; bytes != null && n < bytes.length; n++) {
stmp = Integer.toHexString(bytes[n] & 0XFF);
if (stmp.length() == 1)
hs.append('0');
hs.append(stmp);
}
return hs.toString().toUpperCase();
}
/**
* 大巴开发票加密
*
* @param reqMap
* @param signKey
* @param charset
* @return
*/
public static String signBeforePost(Map<String, String> reqMap, String signKey, String charset) {
Predicate<Map.Entry<String, String>> predicate = new Predicate<Map.Entry<String, String>>() {
@Override
public boolean apply(Map.Entry<String, String> entry) {
return !"sign".equals(entry.getKey());
}
};
return mdsign(reqMap, signKey, charset, predicate);
}
public static String mdsign(Map<String, String> reqMap, String signKey, String charset, Predicate<Map.Entry<String, String>> predicate) {
if (StringUtils.isBlank(charset)) {
charset = "UTF-8";
}
Map<String, String> treeMap = Maps.newTreeMap();
treeMap.putAll(Maps.filterEntries(reqMap, predicate));
// 将null置空
String join = Joiner.on('&').withKeyValueSeparator("=").useForNull("").join(treeMap);
String signSrc = join + "&" + GetMessageDigest(signKey, "MD5", charset);
String sign = GetMessageDigest(signSrc, "MD5", charset);
return sign;
}
public static String GetMessageDigest(String strSrc, String encName, String charset) {
String charset_inner = StringUtils.isBlank(charset) ? "UTF-8" : StringUtils.trimToEmpty(charset);
MessageDigest md = null;
String strDes = null;
final String ALGO_DEFAULT = "SHA-1";
try {
if (StringUtils.isBlank(encName)) {
encName = ALGO_DEFAULT;
}
md = MessageDigest.getInstance(encName);
md.update(strSrc.getBytes(charset_inner));
strDes = bytes2Hex(md.digest()); // to HexString
} catch (NoSuchAlgorithmException e) {
e.getMessage();
} catch (UnsupportedEncodingException e) {
e.getMessage();
}
return strDes;
}
public static String bytes2Hex(byte[] bts) {
String des = StringUtils.EMPTY;
String tmp = null;
for (int i = 0; i < bts.length; i++) {
tmp = (Integer.toHexString(bts[i] & 0xFF));
if (tmp.length() == 1) {
des += "0";
}
des += tmp;
}
return des;
}
public static void main(String[] args) {
}
public static void strSort(String[] str) {
for (int i = 0; i < str.length; i++) {
for (int j = i + 1; j < str.length; j++) {
if (str[i].compareTo(str[j]) > 0) { //对象排序用camparTo方法
swap(str, i, j);
}
}
}
}
private static void swap(String[] strSort, int i, int j) {
String t = strSort[i];
strSort[i] = strSort[j];
strSort[j] = t;
}
private static void printArr(String[] str) {
for (int i = 0; i < str.length; i++) {
System.out.print(str[i] + "\t");
}
System.out.println();
}
}
~~~
获取sign方法
~~~
//secretKey appcode 替换成相应的 值
String secretKey = signUtil.getSecretKey("secretKey ","appcode");
String sign = signUtil.getSign(map, secretKey);
~~~
Java版AS128加密示例
~~~
package com.cwag.pss.ipss.butt.provider.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* @author Administrator
*AES128加密
*/
@Slf4j
public class AES128 {
// 加密
public static String Encrypt(String sSrc, String sKey) throws Exception {
if (sKey == null) {
log.error("Key为空null");
return null;
}
// 判断Key是否为16位
if (sKey.length() != 16) {
log.error("Key长度不是16位");
return null;
}
byte[] raw = sKey.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式"
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));
return new Base64().encodeToString(encrypted);//此处使用BASE64做转码功能,同时能起到2次加密的作用。
}
// 解密
public static String Decrypt(String sSrc, String sKey) throws Exception {
try {
// 判断Key是否正确
if (sKey == null) {
log.error("Key为空null");
return null;
}
// 判断Key是否为16位
if (sKey.length() != 16) {
log.error("Key长度不是16位");
return null;
}
byte[] raw = sKey.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] encrypted1 = new Base64().decode(sSrc);//先用base64解密
try {
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original,"utf-8");
return originalString;
} catch (Exception e) {
log.error(e.toString());
return null;
}
} catch (Exception ex) {
log.error(ex.toString());
return null;
}
}
public static byte[] AESDecrypt(byte[] encryptedBytes, byte[] keyBytes, String keyAlgorithm, String cipherAlgorithm, String IV)
throws Exception {
try {
// AES密钥长度为128bit、192bit、256bit,默认为128bit
if (keyBytes.length % 8 != 0 || keyBytes.length < 16 || keyBytes.length > 32) {
throw new Exception("AES密钥长度不合法");
}
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
SecretKey secretKey = new SecretKeySpec(keyBytes, keyAlgorithm);
if (IV != null && StringUtils.trimToNull(IV) != null) {
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivspec);
} else {
cipher.init(Cipher.DECRYPT_MODE, secretKey);
}
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return decryptedBytes;
} catch (NoSuchAlgorithmException e) {
throw new Exception(String.format("没有[%s]此类加密算法", cipherAlgorithm));
} catch (NoSuchPaddingException e) {
throw new Exception(String.format("没有[%s]此类填充模式", cipherAlgorithm));
} catch (InvalidKeyException e) {
throw new Exception("无效密钥");
} catch (InvalidAlgorithmParameterException e) {
throw new Exception("无效密钥参数");}
catch (BadPaddingException e) {
throw new Exception("错误填充模式");
} catch (IllegalBlockSizeException e) {
throw new Exception("解密块大小不合法");
}
}
public static String AESdecodeParam(String strKey,String param){
if(StringUtils.isEmpty(param)){
return null;
}
String resData = "";
try {
byte[] decodeBase64DataBytes = Base64.decodeBase64(param.getBytes("UTF-8"));
byte[] merchantXmlDataBytes = AESDecrypt(decodeBase64DataBytes,strKey.getBytes("UTF-8"), "AES", "AES/ECB/PKCS5Padding", null);
resData = new String(merchantXmlDataBytes,"UTF-8");
} catch (UnsupportedEncodingException e) {
//log.info(e.getMessage());
e.printStackTrace();
} catch (Exception e) {
//log.info(e.getMessage());
e.printStackTrace();
}
return resData;
}
public static void main(String[] args) throws Exception {
/*
* 此处使用AES-128-ECB加密模式,key需要为16位。
*/
String cKey = "1111111111111111";
// 需要加密的字串
String cSrc = "612724199006260030";
log.error(cSrc);
// 加密
String enString = AES128.Encrypt(cSrc, cKey);
log.error("加密后的字串是:" + enString);
// 解密
String DeString = AES128.Decrypt(enString, cKey);
// log.error("解密后的字串是:" + AES128.AESdecodeParam("7ol8fXTdzjosHxC8e6Y38iZFw%2B4ICG6oD1GIsxBieK8%3D"));
}
}
//源代码片段来自云代码http://yuncode.net
~~~
- 1、接入指南
- 2、接口列表
- 2.1、中转服务
- 2.1.1、行李免提订单生成修改
- 2.1.2、行李免提订单状态推送
- 2.1.3、中转礼包接口
- 2.2、用户中心
- 2.2.1、服务人员
- 2.2.2、车辆管理
- 2.2.3、商户管理
- 2.2.4、token相关
- 2.2.5、投诉对接
- 2.2.6、会员相关
- 2.3、消息中心
- 2.3.1、短信消息
- 2.3.2、语音消息
- 2.4、航班中心
- 2.4.1、获得指定离港航班信息
- 2.4.2、获得指定进港航班信息
- 2.4.3、获取离港航班信息列表
- 2.4.4、获取进港航班信息列表
- 2.4.5、获取离港目的地列表
- 2.4.6、获取进港出发地列表
- 2.4.7、获取热门城市列表
- 2.4.8、获得安检信息列表
- 2.4.9、获得值机信息列表
- 2.4.10、中转数据MQ接入
- 2.4.11、数据中心MQ数据(含安检)
- 2.5、订单中心
- 2.5.1、核销接口
- 2.6、对接中心
- 2.6.1大巴对接
- 2.6.2停车对接
- 2.6.3app对接
- 3、常见问题