写下这篇原由:由于对接美团接口-需要提供回调接口,按指定方式传参并加密
https://peisong.meituan.com/open/doc#section1-1
******************************************************************************
API调用协议
调用流程
合作方调用配送开放平台API,需要按照以下步骤:填充参数 > 生成签名 > 拼装HTTP请求 > 发起HTTP请求> 得到HTTP响应 > 解释json结果
请求规则
规则名称 描述
请求地址 https://peisongopen.meituan.com/api
传输协议 采用HTTPS
请求方式 POST
参数格式 application/x-www-form-urlencoded
字符编码 统一采用UTF-8字符编码
返回值规则
规则名称 描述
数据格式 application/json
字符编码 统一采用UTF-8字符编码
数据结构
{
"code": 状态代码,
"message": 描述信息,
"data": 返回数据信息
}
接口参数介绍
配送开放平台API请求参数分为两种。访问API接口时需要同时提供两种参数。
系统参数:必传,包含appkey,签名,时间戳,接口版本信息
应用参数:请参照具体接口的参数定义 API列表
系统参数介绍
参数名称 参数类型 是 参数描述
appkey String 是 配送开放平台为每个合作方分配独立的appkey,作为合作方接入认证标识。每个appkey会绑定一个secret,用于计算签名。请妥善保管secret,避免泄密。如果secret意外泄露,可要求重新生成。
timestamp long 是 时间戳,格式为long,时区为GMT+8,即合作方调用接口时距离Epoch(1970年1月1日) 以秒计算的时间(unix-timestamp)。开放平台允许合作方请求最大时间误差为10分钟(配送开放平台接到请求的时间 - 合作方调用接口的时间 < 10分钟)。
version String 是 API协议版本,可选值:1.0。
sign String 是 API请求参数的签名计算结果。
业务参数介绍
具体请参照 API列表各接口的参数定义。
3 安全规范-签名算法
安全规范
签名算法
为了防止API调用过程中被黑客恶意篡改,调用任何一个API都需要携带签名,开放平台服务端会根据请求参数,对签名进行验证,签名不合法的请求将会被拒绝。
1)将所有系统参数及业务参数(其中sign,byte[]及值为空的参数除外)按照参数名的字典顺序排序
2)将参数以参数1值1参数2值2...的顺序拼接,例如a=&c=3&b=1,变为b1c3,参数使用utf-8编码
3)按照secret + 排序后的参数的顺序进行连接,得到加密前的字符串
4)对加密前的字符串进行sha1加密并转为小写字符串,得到签名
5)将得到的签名赋给sign作为请求的参数
假设请求参数如下
secret: test
系统参数:
appkey=test
timestamp=1477395862
version=1.0
应用参数:
number=123
string=测试
double=123.123
boolean=true
empty=
加密前的字符串为
testappkeytestbooleantruedouble123.123number123string测试timestamp1477395862version1.0
sha1计算所得sign为
8943ba698f4b009f80dc2fd69ff9b313381263bd
以java举例,签名算法代码如下
// 所有参数按参数名排序
Set keySet = paramMap.keySet();
List keyList = new ArrayList<>(keySet);
Collections.sort(keyList);
// 加密前字符串拼接
StringBuilder signStr = new StringBuilder();
for (String key : keyList) {
if (key.equals( "sign" )) {
continue ;
}
Object value = paramMap.get(key);
if (value == null || (value.getClass().isArray() && byte . class .isAssignableFrom(value.getClass().getComponentType()))) {
continue ;
}
String valueString = value.toString();
if (StringUtils.isEmpty(valueString)) {
continue ;
}
signStr.append(key).append(value);
}
// 计算SHA1签名
String sign = SHA1Util.Sha1( "test" + signStr.toString()).toLowerCase();
订单状态回调
******************************************************************************
订单状态回调
每次订单状态发生变化时,会对合作方提供的回调url进行回调。
注:回调url必须使用80或8080端口
签名算法
跟配送平台签名算法一致。使用配送平台的appkey跟secret做签名计算。
成功与重试
回调根据http响应码为200且返回{"code":0} 判断为成功,否则为失败
若第一次回调失败,会在10分钟内重试5次,且每次重试时间间隔逐步延长
若5次重试全部失败,会在之后每小时重试一次,直到当天结束。
回调接口说明
请求方式:post
请求格式:application/x-www-form-urlencoded
******************************************************************************
错误的使用,美团接收不到回调
/**
* 1.订单状态回调
* @param jsonParam
* @return
* @author wenjian,2019-05-22
* @see CallBack=CB=回调
*/
@ResponseBody
@RequestMapping(value="/orderStatusCallBack"
,method = RequestMethod.POST,produces = "application/x-www-form-urlencoded")
public String orderStatusCallBack(HttpServletRequest request
,String delivery_id ,String mt_peisong_id ,String order_id ,int status ,String courier_name
,String courier_phone ,int cancel_reason_id ,String cancel_reason
,String appkey ,long timestamp ,String sign ,String version) {
Date dateBeign = new Date();
String outData = "";//接口出参
String inData = "";//接口入参
LinkedHashMap<String, Object> returnDataMap = new LinkedHashMap<String, Object>();//返回出去的Map有序的
String returnStr = "";//返回出去的json字符串
String logStatus = CommonConstant.CODE_FAIL;
String apiInfo [] = {CommonConstant.API_SOURCE_MEITUAN,"orderStatusCallBack","[美团回调]1.订单状态回调"};//接口对接系统,接口编码,接口名字
//*******************************************************************************************
String clientIp = IPUtil.getIP(request);
String methodMsg = "1.订单状态回调";
String method = "orderStatusCallBack" + "[" + methodMsg + "]";
String uuid = UUIDUtil.getUUIDUpperCase();
String methodName = method + "[" + uuid + "]";
int codeValue = Integer.valueOf(CommonConstant.CODE_FAIL);//默认失败
inData = "{delivery_id:" + delivery_id + "}" + ",{mt_peisong_id:" + mt_peisong_id + "}" + ",{order_id:"+ order_id + "}"
+ ",{status:"+ status + "}"+ ",{courier_name:"+ courier_name + "}"
//****************************************************************************************
+ ",{courier_phone:"+ courier_phone + "}"+ ",{cancel_reason_id:"+ cancel_reason_id + "}"+ ",{cancel_reason:"+ cancel_reason + "}"
//****************************************************************************************
+ ",{appkey:"+ appkey + "}"+ ",{timestamp:"+ timestamp + "}"+ ",{sign:"+ sign + "}" + ",{version:"+ version + "}";
log.info(methodName + "begin[ip:" + clientIp + "]" + "入参:" + inData);
try {
/** delivery_id long 是 配送活动标识
mt_peisong_id String 是 美团配送内部订单id,最长不超过32个字符
order_id String 是 外部订单号,最长不超过32个字符
status int 是 状态代码,可选值为 0:待调度 20:已接单 30:已取货 50:已送达 99:已取消
回调接口的订单状态改变可能会跳过中间状态,比如从待调度状态直接变为已取货状态。
订单状态不会回流。即订单不会从已取货状态回到待调度状态。
订单状态为“已接单”和“已取货”时,如果当前骑手不能完成配送,会出现改派操作,例如:将订单从骑手A改派给骑手B,由骑手B完成后续配送,因此会出现同一订单多次返回同一状态不同骑手信息的情况”
courier_name String 否 配送员姓名(已接单,已取货状态的订单,配送员信息可能改变)
courier_phone String 否 配送员电话(已接单,已取货状态的订单,配送员信息可能改变)
cancel_reason_id int 否 取消原因id,详情参考 美团配送开放平台接口文档--门户页面-4.3,订单取消原因列表
cancel_reason String 否 取消原因详情,最长不超过256个字符
appkey String 是 开放平台分配的appkey,合作方唯一标识。
timestamp long 是 时间戳,格式为long,时区为GMT+8,当前距 离Epoch(1970年1月1日) 以秒计算的时间,即 unix-timestamp。
sign String 是 数据签名 */
Map<String,Object> dataMap = new HashMap<String,Object>();
if( null != delivery_id && delivery_id.trim().length() != 0) {
dataMap.put("delivery_id", delivery_id);
}
if( null != mt_peisong_id && mt_peisong_id.trim().length() != 0) {
dataMap.put("mt_peisong_id", mt_peisong_id);
}
if( null != order_id && order_id.trim().length() != 0) {
dataMap.put("order_id", order_id);
}
dataMap.put("status", status);
if( null != courier_name && courier_name.trim().length() != 0) {
dataMap.put("courier_name", courier_name);
}
//***************************************************************************
if( null != courier_phone && courier_phone.trim().length() != 0) {
dataMap.put("courier_phone", courier_phone);
}
dataMap.put("cancel_reason_id", cancel_reason_id);
if( null != cancel_reason && cancel_reason.trim().length() != 0) {
dataMap.put("cancel_reason", cancel_reason);
}
//*****************系统参数*****************************************
if( null != appkey && appkey.trim().length() != 0) {
dataMap.put("appkey", appkey);
}
dataMap.put("timestamp", timestamp);
if( null != version && version.trim().length() != 0) {
dataMap.put("version", version);
}
//*****************加密*****************************************
Map<String,Object> meiTuanConfigMap = getMeiTuanConfig(uuid);
String secretDB = (String) meiTuanConfigMap.get("secret");
String appkeyDB = (String) meiTuanConfigMap.get("appkey");
String phoneDB = (String) meiTuanConfigMap.get("phone");
log.info("注册手机[" + phoneDB + "]"+"本地存储key:[" + appkeyDB + "],传入appkey:" + "[" + appkey +"]," + (appkeyDB.equals(appkey)?"一样":"不一样"));
String signNew = MeiTuanSignHelper.generateSign(uuid,dataMap, secretDB );
boolean isSameSign = signNew.equals(sign);//签名加密串是否一样
log.info(methodName + "signNew{"+ signNew + "},sign{" + sign + "}," + isSameSign + "," + (isSameSign?"加密串一样":"加密串不一样"));
if(isSameSign) {
//加密串一样
codeValue = Integer.valueOf(CommonConstant.CODE_SUCCESS);
logStatus = CommonConstant.CODE_SUCCESS;
}else {
//加密串不一样
}
}catch (Exception e) {
e.printStackTrace();
log.error(methodName + ",异常",e);
if(e instanceof CommonCheckedParamException) {
}
}catch (Error e) {
e.printStackTrace();
log.error(methodName + "[不可恢复Error异常]",e);
}finally {
returnDataMap.put(CommonConstant.CODE, codeValue);
returnStr = JSON.toJSONString(returnDataMap);
outData = returnStr;
InterfaceLogsDto logDto = DtoUtil.getInterfaceLogsDto(apiInfo[0], clientIp, apiInfo[1], apiInfo[2], dateBeign, inData,outData, logStatus);
interfaceService.addLog(uuid, logDto);
}
log.info(methodName + ",end,响应:" + returnStr);
return returnStr;
}
1.普通的json串形式
/**
* 查询商品品类列表 [查本地数据库]
* @param jsonParam
* @return
* @author wenjian,2019-05-22
*/
@ResponseBody
@RequestMapping(value="/getGoodsCategoryList"
,method = RequestMethod.POST,produces = "application/json;charset=UTF-8")
public String getGoodsCategoryList(HttpServletRequest request,@RequestBody String jsonParam) {
String clientIp = IPUtil.getIP(request);
String methodMsg = "查询商品品类列表";
String method = "getGoodsCategoryList" + "[" + methodMsg + "]";
String uuid = UUIDUtil.getUUIDUpperCase();
String methodName = method + "[" + uuid + "]";
log.info(methodName + "begin[ip:" + clientIp + "]" + "入参:" + jsonParam);
String codeValue = CommonConstant.CODE_FAIL;//默认失败
String msgValue = CommonConstant.MSG_VALUE_FAIL;//默认失败
String msgDetailValue = "";
List<GoodsCategoryDto> dtoList = new ArrayList<GoodsCategoryDto>();
int total = 0;
try {
JSONObject jsonObject =JSONObject.parseObject(jsonParam);
//------检查tokenInfo头信息-------
ServiceUtil.checkTokenInfoHead(jsonObject);
JSONObject tokenInfo = (JSONObject) jsonObject.get("tokenInfo");//令牌信息
String channelId = String.valueOf(tokenInfo.get("channelId"));//渠道code
String tockenTimestamp = String.valueOf(tokenInfo.get("tockenTimestamp"));//tockenTimestamp;
String token = String.valueOf(tokenInfo.get("token"));//渠道号
//**************************************
//********************密钥********************
List<Map<String,Object>> channelKeyInfoList = interfaceService.getChannelKeyInfo(uuid);
String channelKey = ServiceUtil.getChannelKey(uuid,channelId,channelKeyInfoList);
String md5Input = channelId + tockenTimestamp + channelKey;
ServiceUtil.checkMD5(md5Input, token);
/**********查询日期-时间戳**********************/
String queryDateBegin = CommonStringUtil.getStringValueByFastjson(jsonObject.get("queryDateBegin"));//查询日期:开始
String queryDateEnd = CommonStringUtil.getStringValueByFastjson(jsonObject.get("queryDateEnd"));//查询日期:结束
ServiceUtil.checkedTimestamp(queryDateBegin);
ServiceUtil.checkedTimestamp(queryDateEnd);
Map<String,Object> paraMap = new HashMap<String,Object>();
paraMap.put("queryDateBegin", queryDateBegin);
paraMap.put("queryDateEnd", queryDateEnd);
dtoList = goodsCategoryService.getGoodsCategoryList(uuid, paraMap);
total = dtoList.size();
codeValue = CommonConstant.CODE_SUCCESS;
msgValue = CommonConstant.MSG_VALUE_SUCCESS;
msgDetailValue = methodMsg + ",成功";
}catch (CommonCheckedParamException e) {
e.printStackTrace();
msgDetailValue = methodMsg + ",失败!" + e.getMessage();
log.error(methodName + "异常",e);
}catch (Exception e) {
e.printStackTrace();
msgDetailValue = methodMsg + ",失败!" + e.getMessage();
log.error(methodName + "异常",e);
}catch (Error e) {
e.printStackTrace();
msgDetailValue = methodMsg + ",失败![不可恢复Error异常]" + e.getMessage();
log.error(methodName + "[不可恢复Error异常]",e);
}
LinkedHashMap<String, Object> dataMap = new LinkedHashMap<String, Object>();//有序的
dataMap.put(CommonConstant.CODE, codeValue);
dataMap.put(CommonConstant.MSG, msgValue);
dataMap.put(CommonConstant.MSG_DETAIL, msgDetailValue);
dataMap.put(CommonConstant.TOTAL, total);
dataMap.put(CommonConstant.DATA, dtoList);
String returnStr = JSON.toJSONString(dataMap);
log.info(methodName + ",end,响应:" + ServiceUtil.getResponseMsg(codeValue, total,dataMap));
return returnStr;
}
2.key-value形式-如美团要求的回调
/**
* 1.订单状态回调
* @param jsonParam
* @return
* @author wenjian,2019-05-22
* @see CallBack=CB=回调
*/
@ResponseBody
@PostMapping(value = "/orderStatusCallBack")
public String orderStatusCallBack(HttpServletRequest request ,@RequestParam Map<String, Object> paramMap) {
String clientIp = IPUtil.getIP(request);
String methodMsg = "1.订单状态回调";
String method = "orderStatusCallBack" + "[" + methodMsg + "]";
String uuid = UUIDUtil.getUUIDUpperCase();
String methodName = method + "[" + uuid + "]";
log.info(methodName + "begin[ip:" + clientIp + "]" + "入参:" + JSON.toJSONString(paramMap));
//*********************** 解析参数,begin **********************************************************************
String delivery_id = paramMap.containsKey("delivery_id") ? paramMap.get("delivery_id").toString():"";
String mt_peisong_id = paramMap.containsKey("mt_peisong_id") ? paramMap.get("mt_peisong_id").toString():"";
String order_id = paramMap.containsKey("order_id") ? paramMap.get("order_id").toString():"";
int status = paramMap.containsKey("status") ? Integer.valueOf(paramMap.get("status").toString()):INT_DEFAULT_NULL;
String courier_name = paramMap.containsKey("courier_name") ? paramMap.get("courier_name").toString():"";
String courier_phone = paramMap.containsKey("courier_phone") ? paramMap.get("courier_phone").toString():"";
int cancel_reason_id = paramMap.containsKey("cancel_reason_id") ? Integer.valueOf(paramMap.get("cancel_reason_id").toString()):INT_DEFAULT_NULL;
String cancel_reason = paramMap.containsKey("cancel_reason") ? paramMap.get("cancel_reason").toString():"";
String appkey = paramMap.containsKey("appkey") ? paramMap.get("appkey").toString():"";
long timestamp = paramMap.containsKey("timestamp") ? Long.valueOf(paramMap.get("timestamp").toString()):LONG_DEFAULT_NULL;
String sign = paramMap.containsKey("sign") ? paramMap.get("sign").toString():"";
String version = paramMap.containsKey("version") ? paramMap.get("version").toString():"";
//*********************** 解析参数,begin **********************************************************************
Date dateBeign = new Date();
String outData = "";//接口出参
String inData = "";//接口入参
LinkedHashMap<String, Object> returnDataMap = new LinkedHashMap<String, Object>();//返回出去的Map有序的
String returnStr = "";//返回出去的json字符串
String logStatus = CommonConstant.CODE_FAIL;
String apiInfo [] = {CommonConstant.API_SOURCE_MEITUAN,"orderStatusCallBack","[美团回调]1.订单状态回调"};//接口对接系统,接口编码,接口名字
//*******************************************************************************************
int codeValue = Integer.valueOf(CommonConstant.CODE_FAIL);//默认失败
inData = "{delivery_id:" + delivery_id + "}" + ",{mt_peisong_id:" + mt_peisong_id + "}" + ",{order_id:"+ order_id + "}"
+ ",{status:"+ status + "}"+ ",{courier_name:"+ courier_name + "}"
//****************************************************************************************
+ ",{courier_phone:"+ courier_phone + "}"+ ",{cancel_reason_id:"+ cancel_reason_id + "}"+ ",{cancel_reason:"+ cancel_reason + "}"
//****************************************************************************************
+ ",{appkey:"+ appkey + "}"+ ",{timestamp:"+ timestamp + "}"+ ",{sign:"+ sign + "}" + ",{version:"+ version + "}";
log.info(methodName + "begin[ip:" + clientIp + "]" + "入参:" + inData);
inData = JSON.toJSONString(paramMap);
try {
/** delivery_id long 是 配送活动标识
mt_peisong_id String 是 美团配送内部订单id,最长不超过32个字符
order_id String 是 外部订单号,最长不超过32个字符
status int 是 状态代码,可选值为 0:待调度 20:已接单 30:已取货 50:已送达 99:已取消
回调接口的订单状态改变可能会跳过中间状态,比如从待调度状态直接变为已取货状态。
订单状态不会回流。即订单不会从已取货状态回到待调度状态。
订单状态为“已接单”和“已取货”时,如果当前骑手不能完成配送,会出现改派操作,例如:将订单从骑手A改派给骑手B,由骑手B完成后续配送,因此会出现同一订单多次返回同一状态不同骑手信息的情况”
courier_name String 否 配送员姓名(已接单,已取货状态的订单,配送员信息可能改变)
courier_phone String 否 配送员电话(已接单,已取货状态的订单,配送员信息可能改变)
cancel_reason_id int 否 取消原因id,详情参考 美团配送开放平台接口文档--门户页面-4.3,订单取消原因列表
cancel_reason String 否 取消原因详情,最长不超过256个字符
appkey String 是 开放平台分配的appkey,合作方唯一标识。
timestamp long 是 时间戳,格式为long,时区为GMT+8,当前距 离Epoch(1970年1月1日) 以秒计算的时间,即 unix-timestamp。
sign String 是 数据签名 */
Map<String,Object> dataMap = new HashMap<String,Object>();
if( null != delivery_id && delivery_id.trim().length() != 0) {
dataMap.put("delivery_id", delivery_id);
}
if( null != mt_peisong_id && mt_peisong_id.trim().length() != 0) {
dataMap.put("mt_peisong_id", mt_peisong_id);
}
if( null != order_id && order_id.trim().length() != 0) {
dataMap.put("order_id", order_id);
}
if(status!= INT_DEFAULT_NULL) {
dataMap.put("status", status);
}
if( null != courier_name && courier_name.trim().length() != 0) {
dataMap.put("courier_name", courier_name);
}
//***************************************************************************
if( null != courier_phone && courier_phone.trim().length() != 0) {
dataMap.put("courier_phone", courier_phone);
}
if(cancel_reason_id != INT_DEFAULT_NULL) {
dataMap.put("cancel_reason_id", cancel_reason_id);
}
if( null != cancel_reason && cancel_reason.trim().length() != 0) {
dataMap.put("cancel_reason", cancel_reason);
}
//*****************系统参数*****************************************
if( null != appkey && appkey.trim().length() != 0) {
dataMap.put("appkey", appkey);
}
if(timestamp != LONG_DEFAULT_NULL) {
dataMap.put("timestamp", timestamp);
}
if( null != version && version.trim().length() != 0) {
dataMap.put("version", version);
}
//*****************加密*****************************************
Map<String,Object> meiTuanConfigMap = getMeiTuanConfig(uuid);
String secretDB = (String) meiTuanConfigMap.get("secret");
String appkeyDB = (String) meiTuanConfigMap.get("appkey");
String phoneDB = (String) meiTuanConfigMap.get("phone");
log.info("注册手机[" + phoneDB + "]"+"本地存储key:[" + appkeyDB + "],传入appkey:" + "[" + appkey +"]," + (appkeyDB.equals(appkey)?"一样":"不一样"));
String signNew = MeiTuanSignHelper.generateSign(uuid,dataMap, secretDB );
boolean isSameSign = signNew.equals(sign);//签名加密串是否一样
log.info(methodName + "signNew{"+ signNew + "},sign{" + sign + "}," + isSameSign + "," + (isSameSign?"加密串一样":"加密串不一样"));
if(isSameSign) {
//加密串一样
codeValue = Integer.valueOf(CommonConstant.CODE_SUCCESS);
logStatus = CommonConstant.CODE_SUCCESS;
}else {
//加密串不一样
}
}catch (Exception e) {
e.printStackTrace();
log.error(methodName + ",异常",e);
if(e instanceof CommonCheckedParamException) {
}
}catch (Error e) {
e.printStackTrace();
log.error(methodName + "[不可恢复Error异常]",e);
}finally {
returnDataMap.put(CommonConstant.CODE, codeValue);
returnStr = JSON.toJSONString(returnDataMap);
outData = returnStr;
InterfaceLogsDto logDto = DtoUtil.getInterfaceLogsDto(apiInfo[0], clientIp, apiInfo[1], apiInfo[2], dateBeign, inData,outData, logStatus);
interfaceService.addLog(uuid, logDto);
}
log.info(methodName + ",end,响应:" + returnStr);
return returnStr;
}