# 支付成功通知
- 玩家下单支付成功后,美团开放平台会回调游戏开发者服务端的回调接口(开发者提供的回调接口的端口号,必须是80或者443),完成支付结果通知。
- 如果游戏开发者服务端返回异常,美团开放平台会进行重试,重试间隔采取递增的策略。
- 回调说明
: 接口回调采取对称加密的方式,假设开发者在游戏开放平台注册的支付回调接口为http://url.callback;则回调时形式如:
method:POST header:application/json;charset=utf-8 url:http://url.callback postdata: { data:加密后的数据, sign:签名 } - 开发者的服务端在接收到回调时,可以使用sign的算法进行签名验证。如果匹配了,说明来自于美团游戏开放平台的接口回调是正确的,开发者可以进一步解密数据包和执行业务逻辑。
# 支付成功回调开发者接口
通知接口参数:
参数名 类型 默认值 是否必填 说明 sign String 是 签名,签名方法见附录 data String 是 加密后的数据,通知内容(JSON)加密后的生成的字符串,解密方法见附录 data 解密后的内容:
参数名 类型 默认值 是否必填 说明 bizOrderId String 是 游戏厂商下单传入的订单号 mgcOrderId String 是 美团侧订单号 mgcId String 是 美团侧玩家角色号 price String 是 美团侧收到的游戏下单金额,单位:分. payStatus String 是 OK:支付成功 开发者服务端接口收到回调通知后,接口响应(code:0代表成功,非0代表失败):
{ "code": 0, "msg": "ok" }
# 附录
- Sign签名的计算算法为
String secretKey = AESUtil.createKey(appId + "&" + appSecret); //开发者可以在开发者后台查询得到开放平台为其分配的appId和appSecret signature = SHAUtil.encryptSHA1Str(data + secretKey) //data :美团开发平台回调返回的报文中data字段对应的value - AESUtil工具类
import java.security.MessageDigest; import javax.crypto.SecretKey; public static String createKey(String password) { try { byte[] key = password.getBytes("UTF-8"); MessageDigest sha = MessageDigest.getInstance("SHA-1"); key = sha.digest(key); key = Arrays.copyOf(key, 32); SecretKey secretKey = new SecretKeySpec(key, "AES"); return Base64.getEncoder().encodeToString(secretKey.getEncoded()); } catch (Exception var3) { throw new RuntimeException(var3); } } - SHAUtil工具类
import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import org.apache.commons.codec.digest.DigestUtils; public static String encryptSHA1Str(String text) { byte[] bytes = encryptSHA(text, "SHA-1"); return byte2hex(bytes); } private static byte[] encryptSHA(String text, String algorithm) { MessageDigest md; try { md = MessageDigest.getInstance(algorithm); } catch (NoSuchAlgorithmException var4) { throw new IllegalArgumentException(var4); } byte[] infoBytes = text.getBytes(StandardCharsets.UTF_8); md.update(infoBytes); return md.digest(); } private static String byte2hex(byte[] bytes) { StringBuilder stringBuilder = new StringBuilder(); byte[] var2 = bytes; int var3 = bytes.length; for(int var4 = 0; var4 < var3; ++var4) { byte b = var2[var4]; String hex = Integer.toHexString(b & 255); if (hex.length() == 1) { stringBuilder.append("0"); } stringBuilder.append(hex); } return stringBuilder.toString(); } - data字段的加密使用AES_256_GCM算法加密,解密工具类如下
- 解密工具类
以下代码为AES_256_GCM的解密java示例代码,仅供参考 import javax.crypto.Cipher; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; private static final String ALGORITHM = "AES"; private static final String ALGORITHM_PADDING = "AES/GCM/NoPadding"; private static final int GCM_NONCE_LENGTH_BIT = 128; private static final int GCM_TAG_LENGTH_BIT = 128; /** * 解密方法 * @param key 密钥(AESUtil.createKey(appId + "&" + appSecret)) * @param encryptData 密文数据 * @return 明文数据 */ public static String decryptWithAESGCM256(String key, String encryptData) { try { byte[] decodeData = Base64.getUrlDecoder().decode(encryptData); byte[] nonceBytes = Arrays.copyOfRange(decodeData, 0, GCM_NONCE_LENGTH_BIT / 8); byte[] contentBytes = Arrays.copyOfRange(decodeData, GCM_NONCE_LENGTH_BIT / 8, decodeData.length); byte[] keyBytes = Base64.getDecoder().decode(key.getBytes(StandardCharsets.UTF_8)); SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM); GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH_BIT, nonceBytes); Cipher cipher = Cipher.getInstance(ALGORITHM_PADDING, "SunJCE"); cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); return new String(cipher.doFinal(contentBytes), StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); } return null; } - PHP demo(版本使用7.1以上)
/**
* 生成密钥
* $appId:开发者后台看
* $appSecret:开发者后台看
*/
function createKey($appId, $appSecret) {
$aaa = base64_encode(str_pad(sha1($appId.'&'.$appSecret,true), 32,pack('V', 0)));
return $aaa;
}
/**
* 签名sha1算法
* $secretKey: 由上面的 createKey方法生成
* $encryptData 通知报文的data密文内容
*/
function encryptSHA1Str($secretKey, $encryptData) {
$aaa = $encryptData.$secretKey;
$sign = sha1($aaa);
return $sign;
}
/**
* AES-GCM-256解密算法
* $secretKey: 由上面的 createKey方法生成
* $encryptData 通知报文的data密文内容
*/
function decryptWithAESGCM256($secretKey, $encryptData) {
$decodeEncrypt = urlsafe_b64decode($encryptData);
$decodeSecret = base64_decode($secretKey);
$data = openssl_decrypt(substr($decodeEncrypt, 16, -16), 'aes-256-gcm', $decodeSecret, 1, substr($decodeEncrypt, 0, 16), substr($decodeEncrypt, -16, 16));
return $data;
}
function urlsafe_b64decode($string) {
$data = str_replace(array('-','_'),array('+','/'),$string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
return base64_decode($data);
}
- JS demo
// 使用示例:获取签名和解密报文
const password = `${appid}&${appsecret}`;
const secretKey = getSecretKey(password);
const encodeSignature = getEncryptSha1Str(encodeData, secretKey);
const decode = getDecryptRes(secretKey, encodeData);
import crypto from 'crypto';
/**
* 生成密钥
* $password: appId + "&" + appSecret 拼接生成
*/
export function getSecretKey(password) {
let key = crypto.createHash('sha1').update(password)
.digest();
key = Buffer.concat([Buffer.from(key, 0, 32), Buffer.alloc(32, 0)]).slice(0, 32);
const base64String = Buffer.from(key).toString('base64');
return base64String;
}
/**
* 签名sha1算法
* $encryptData:密文内容
* $secretKey:密钥,由上面的createKey方法生成
*/
export function getEncryptSha1Str(encrpyData, secretKey) {
const res = crypto.createHash('sha1').update(encrpyData + secretKey)
.digest('hex');
return res;
}
/**
* aes-256-gcm 解密
* @param {String} serectKey 密钥 base64
* @param {String} encrpyData 密文 base64
* @returns {string}
*/
export function getDecryptRes(serectKey, encrpyData) {
try {
const decodeSecretKey = Buffer.from(serectKey, 'base64');
const decodeEncrpyData = Buffer.from(encrpyData, 'base64');
const iv = decodeEncrpyData.slice(0, 16);
const contentData = decodeEncrpyData.slice(16, -16);
const decipher = crypto.createDecipheriv('aes-256-gcm', decodeSecretKey, iv);
const decrypted = decipher.update(contentData, 'base64', 'utf8');
return decrypted;
} catch (e) {
console.log('decrypt error', e);
}
}