# 支付回调
# 1、支付流程图
# 2、充值回调接口必须提供
接口描述:用于向游戏添加游戏币。(如果有充值失败的情况,会用同一个订单号来进行补单请求)
接口提供方:由项目(游戏)开发人员提供,建议提供正式和测试的两个接口地址。
输入参数:JSON
输入参数举例:
{
"accountId": "1350000001",
"areaId": "1",
"orderId": "13281108827665633280",
"orderTimestamp": "1722590112",
"orderPrice": 600,
"channelId": 1010,
"itemId": "com.dianhun.test.a001",
"itemName": "com.dianhun.test.a001",
"memo": "",
"remark": "",
"region": "1",
"currency": "CNY",
"sign": "7990c320348f1dbff47152ae96d04351"
}
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
1 | orderId | String | 20 | SDK平台支付订单ID(唯一),SDK服务端生成的订单号 |
2 | accountId | String | 20 | 账号ID,对应游戏客户端接入SDK支付的[accountId]参数 |
3 | areaId | String | 4 | 大区ID,对应客户端接入SDK支付的[areaId]参数具体请参考下面 【6、说明】 |
4 | orderTimestamp | String | 14 | 订单充值成功时间,时间戳格式精确到秒 |
5 | orderPrice | Int | 4 | 订单支付总金额,单位为分,建议必须校对商品Id跟金额是否一致,对应游戏客户端接入SDK支付的[orderPrice]参数 |
6 | channelId | Int | 4 | SDK充值渠道ID |
7 | itemId | String | 30 | 商品ID,对应游戏客户端接入SDK支付的[itemId]参数, 建议校对商品Id跟金额是否一致 |
8 | itemName | String | 30 | 商品名称,对应游戏客户端接入SDK支付的itemName]参数 |
9 | memo | String | 200 | 透传参数,对应游戏客户端接入SDK支付的[memo]参数,根据下单的值原样返回,请使用字符串或者JsonString |
10 | remark | String | 200 | 透传参数,对应游戏客户端接入SDK支付的[remark]参数,游戏自定,param 不足时使用【历史字段,已经废弃使用】 |
11 | region | String | 1 | 所属地区。1:中国大陆;0:港澳台及海外 |
12 | currency | String | 3 | 货币类型,国际货币标准单位,比如:CNY |
13 | sandbox | Int | 4 | 是否测试订单,1:测试订单;0:正式 |
14 | sign | String | 32 | 订单签名 加密规则:MD5( accountId+areaId+orderPrice+orderId 举列:accountId:1350000001 加密前的值:135000000116001328110882766563328017225 加密后: 7990c320348f1dbff47152ae96d04351 |
15 | itemNum | Int | 4 | 商品数量,目前为固定 1 |
输出参数:JSON
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
1 | status | String | ok查询成功fail查询失败,paramerror参数错误,repeat 订单号重复 (如游戏返回重复,我们认为成功,其他的错误继续提交) othererror 其他错误 |
# 3、角色信息查询接口 主体发行与海外网页充值必须提供
接口描述:用来充值时判断此大区有无相关角色信息
接口提供方:由项目(游戏)开发人员提供,建议提供正式和测试的两个接口地址。
TIP
一个账号同一大区有多角色的情况下,角色ID
可以通过透传来处理
输入参数:JSON
输入参数举例:
{"areaId":"2", "accountId":"123456", "memo":"memo", "region":"1"}
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
1 | accountId | String | 20 | 账号ID,对应游戏客户端接入SDK支付的[accountId]参数 |
2 | areaId | String | 4 | 大区ID,对应客户端接入SDK支付的[areaId]参数具体请参考下面 【6、说明】 |
3 | memo | String | 200 | 透传参数,对应游戏客户端接入SDK支付的[memo]参数,根据下单的值原样返回,请使用字符串或者JsonString |
4 | region | String | 1 | 地区。1:中国大陆;0:港澳台及海外 |
输出参数:JSON
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
1 | status | String | 必须输出。ok:成功;fail:角色不存在 | |
2 | accountId | String | 账号ID | |
3 | areaId | String | 大区ID | |
4 | roleName | String | 角色名字 |
# 4、账号信息查询接口 海外网页充值必须提供
接口描述:用来海外网页充值时查询相关的账号ID
和大区ID
信息
接口提供方:由项目(游戏)开发人员提供,建议提供正式和测试的两个接口地址。
输入参数:JSON
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
1 | roleId | String | 20 | 游戏角色ID |
2 | areaId | String | 4 | 大区ID,如果角色ID能够确认账号ID,则这个参数传0。 |
输出参数:JSON
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
1 | status | String | 必须输出。ok:成功;fail:角色不存在 | |
2 | accountId | String | 账号ID | |
3 | areaId | String | 大区ID | |
4 | roleName | String | 角色名字 | |
5 | areaName | string | 大区名称 |
# 5、游戏所有的角色信息查询接口 微信小游戏道具商城必接
接口描述:微信小游戏道具商城需要拉取游戏所有角色信息(最多10个)
接口提供方:由项目(游戏)开发人员提供
输入参数:JSON
输入参数举例:
{"accountId":"1234567", "itemId":"pro.123", "orderPrice":1, "needRoleNum":20}
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
1 | accountId | String | 20 | 账号ID,对应游戏客户端接入SDK支付的[accountId]参数 |
2 | itemId | String | 30 | 商品ID |
3 | orderPrice | Int | 4 | 订单支付总金额,单位为分 |
4 | needRoleNum | Int | 4 | 最多返回角色数量,如果角色少可以忽略,数量不超过20个 |
输出参数:JSON
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
1 | status | String | 必须输出。ok:成功;fail:角色不存在 | |
2 | data | JsonArray | 角色信息List | |
3 | data->roleId | String | 角色ID | |
4 | data->roleName | String | 角色名称 | |
4 | data->areaId | String | 大区ID | |
4 | data->areaName | String | 大区名称 | |
4 | data->memo | String | 透传参数 |
# 6、游戏大区查询接口废弃
接口描述:用于获取大区信息(不需要开线下充值的不需要提供此接口)
接口提供方:由项目(游戏)开发人员提供,建议提供正式和测试的两个接口地址。
输入参数:JSON
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
输出参数:JSON
序号 | 变量名 | 类型 | 长度 | 说明 |
---|---|---|---|---|
1 | areaId | String | 大区ID | |
2 | areaName | String | 大区名称 | |
3 | status | String | 大区状态(1新开,2爆满,3推荐,4维护,5测试) | |
4 | channel | String | 大区渠道标识AppStore:就是正版iOS all:所有渠道 other:除了AppStore,dh,应用宝的其他渠道yyb:应用宝 例如:[{"areaId":"1","name":"游戏1区","status":"ok","channel":"AppStore"}] |
注意
开区频次较高的游戏,大区信息更新和添加大区的时候需要请求接口地址,来刷新缓存,具体接口地址请联系相关开放人员。
# 7、说明
WARNING
请求方式统一为:POST
;数据格式统一用:JSON
WARNING
以上接口请分别提供正式环境、测试环境两个接口地址便于后续不同环境测试 一开始对接正式和测试地址可以使用同一个。
注意
限购道具,需游戏自己增加逻辑处理(如果在回调的时候返回限制结果,
但是实际用户已经支付成功了
注意
充值回调必须校验金额
和商品Id
是否匹配,根据传入的商品Id
来进行发货处理。
如果透传字段放了游戏的订单号
,也必须校验订单号
跟传入的金额
是否匹配。
TIP
大区ID
(充值回调,角色查询中的 areaId
)SDK有固定了几个值来做特殊处理
100:测试 ,会回调提供的测试地址 9999:iOS提审服,会回调提供的提审地址
目前平台默认回调一个地址(除测试提审之外),如果需要分发不同回调地址,默认是游戏进行分发逻辑;平台侧也可以支持3种方式分发逻辑处理
1、根据 areaId
(游戏客户端调用SDK充值接口给的 areaId
)来进行回调分发不同的地址。
2、根据游戏,在提供的回调地址上增加 areaId
路由:callback/{areaId}
,可以游戏根据这个规则进行不同地址分发。
3、根据 source
(充值来源)回调分发不同的地址。
TIP
一个账号同一大区有多角色的情况下,角色ID
可以通过透传来处理
# 8、代码示例
# 8.1 支付回调接口
# JAVA实现
//基于SpringBoot框架的java实现
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.dh.wxmg.model.vo.PayCallbackVO;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
/**
* @author Dream
* @create 2024/10/28/17:50
*/
@RestController
@RequestMapping(value = "/demo")
public class DemoController {
@RequestMapping(value = "/callback", method = {RequestMethod.POST})
public ResponseEntity<Map<String, String>> callback(@RequestBody PayCallbackVO callbackVO) {
Map<String, String> rtMap = new HashMap<>(1);
try {
//根据需要可以适当增加参数校验
if (ObjectUtil.isNull(callbackVO) || StrUtil.isBlank(callbackVO.getSign()) || StrUtil.isBlank(callbackVO.getAccountId()) || StrUtil.isBlank(callbackVO.getOrderId())) {
rtMap.put("status", "paramerror");
return new ResponseEntity<>(rtMap, HttpStatus.OK);
}
//此处改成SDK平台提供的appKey
String appKey = "12345678";
//生成签名串
String currentSign = generateSign(callbackVO, appKey);
if (!StrUtil.equals(currentSign, callbackVO.getSign())) {
rtMap.put("status", "othererror");
return new ResponseEntity<>(rtMap, HttpStatus.OK);
}
//校验订单号、商品id、金额是否匹配,发货等业务逻辑此处省略...
//最后返回成功
rtMap.put("status", "ok");
return new ResponseEntity<>(rtMap, HttpStatus.OK);
} catch (Exception e) {
//记录日志和业务告警等异常处理...
rtMap.put("status", "fail");
return new ResponseEntity<>(rtMap, HttpStatus.OK);
}
}
/**
* 生成MD5签名
* @param callbackVO sdk支付回调参数对象
* @param appKey 签名密钥
* @return string 签名字符串
* @throws NoSuchAlgorithmException
*/
public String generateSign(PayCallbackVO callbackVO, String appKey) throws NoSuchAlgorithmException {
//明文拼接,注意拼接顺序:accountId+areaId+orderPrice+orderId+orderTimestamp+itemId+channelId+appkey
String text = callbackVO.getAccountId() + callbackVO.getAreaId() + callbackVO.getOrderPrice() +
callbackVO.getOrderId() + callbackVO.getOrderTimestamp() + callbackVO.getItemId() +
callbackVO.getChannelId() + appKey;
//获取MD5消息摘要实例
MessageDigest md = MessageDigest.getInstance("MD5");
//将输入字符串转换为utf8编码的字节数组并更新消息摘要
byte[] hash = md.digest(text.getBytes(StandardCharsets.UTF_8));
//使用DatatypeConverter将字节数组转换为十六进制字符串
return DatatypeConverter.printHexBinary(hash).toLowerCase();
}
}
/**
* PayCallbackVO 实体POJO类
* @author Dream
* @create 2024/10/28/18:01
*/
@Getter
@Setter
public class PayCallbackVO {
/**
* 参数释义链接
* https://docs.open.17m3.com/v2/server/paynotify.html#_2%E3%80%81%E5%85%85%E5%80%BC%E5%9B%9E%E8%B0%83%E6%8E%A5%E5%8F%A3
*/
private String accountId;
private String areaId;
private String orderId;
private String orderTimestamp;
private Integer orderPrice;
private Integer channelId;
private String itemId;
private String itemName;
private String memo;
private String remark;
private String region;
private String currency;
private String sign;
}
# PHP实现
<?php
namespace app\index\controller;
//基于ThinkPHP5的php实现
use think\Controller;
use think\Request;
use think\Exception;
class DemoController extends Controller
{
/**
* 支付回调接口
* 处理支付平台的回调请求,验证签名并返回处理结果
*
* @param Request $request HTTP请求对象
* @return \think\response\Json JSON格式的响应
*/
public function callback(Request $request)
{
header('Content-Type: application/json');
// 此处为SDK平台提供的appKey
$appKey = "12345678";
// 获取POST请求数据
$data = $request->post();
// 检查请求数据是否为空,以及必要字段是否存在
if (empty($data) || empty($data['sign']) || empty($data['accountId']) || empty($data['orderId'])) {
return json(['status' => 'paramerror']);
}
try {
// 将请求数据转换为对象,便于后续处理
$callbackVO = (object) $data;
// 生成当前签名
$currentSign = $this->generateSign($callbackVO, $appKey);
// 验证签名是否一致
if ($currentSign !== $callbackVO->sign) {
return json(['status' => 'othererror']);
}
// 校验订单号、商品id、金额是否匹配,及其他业务逻辑(此处省略)
// 返回成功状态
return json(['status' => 'ok']);
} catch (Exception $e) {
// 异常处理:记录日志和发送告警(此处省略具体实现)
return json(['status' => 'fail']);
}
}
/**
* 生成MD5签名
*
* @param object $callbackVO 支付回调参数对象
* @param string $appKey 签名密钥
* @return string MD5签名字符串
*/
private function generateSign($callbackVO, $appKey)
{
// 明文拼接,注意拼接顺序:accountId+areaId+orderPrice+orderId+orderTimestamp+itemId+channelId+appkey
$text = $callbackVO->accountId . $callbackVO->areaId . $callbackVO->orderPrice .
$callbackVO->orderId . $callbackVO->orderTimestamp . $callbackVO->itemId .
$callbackVO->channelId . $appKey;
// 使用MD5算法生成哈希值,并转换为小写
return strtolower(md5($text));
}
}
# Go实现
package controller
//基于Gin框架的golang实现
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func LoadDemoController(router *gin.Engine) {
index := router.Group("/demo")
{
index.POST("/callback", paymentCallback)
}
}
// PayCallbackVO 定义支付回调数据结构
type PayCallbackVO struct {
AccountId string `json:"accountId"`
AreaId string `json:"areaId"`
OrderId string `json:"orderId"`
OrderTimestamp string `json:"orderTimestamp"`
OrderPrice int `json:"orderPrice"`
ChannelId int `json:"channelId"`
ItemId string `json:"itemId"`
ItemName string `json:"itemName"`
Memo string `json:"memo"`
Remark string `json:"remark"`
Region string `json:"region"`
Currency string `json:"currency"`
Sign string `json:"sign"`
}
// 支付回调接口
func paymentCallback(c *gin.Context) {
var callbackVO PayCallbackVO
// 绑定JSON请求体到PayCallbackVO结构
if err := c.ShouldBindJSON(&callbackVO); err != nil {
c.JSON(http.StatusOK, gin.H{"status": "paramerror"})
return
}
// 检查必要字段是否为空
if callbackVO.Sign == "" || callbackVO.AccountId == "" || callbackVO.OrderId == "" {
c.JSON(http.StatusOK, gin.H{"status": "paramerror"})
return
}
// 此处为SDK平台提供的appKey
appKey := "12345678"
currentSign := generateSign(callbackVO, appKey)
// 验证签名是否一致
if currentSign != callbackVO.Sign {
c.JSON(http.StatusOK, gin.H{"status": "othererror"})
return
}
// 校验订单号、商品id、金额是否匹配,及其他业务逻辑(此处省略)
// 返回成功状态
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
// generateSign 生成MD5签名
func generateSign(vo PayCallbackVO, appKey string) string {
// 明文拼接
text := fmt.Sprintf("%s%s%d%s%s%s%d%s",
vo.AccountId, vo.AreaId, vo.OrderPrice,
vo.OrderId, vo.OrderTimestamp, vo.ItemId,
vo.ChannelId, appKey)
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}
# C#实现
//基于.net framework的实现
// 改成SDK平台提供的应用密钥
private const string AppKey = "12345678";
/// <summary>
/// 处理支付回调的POST请求
/// </summary>
/// <param name="callbackVO">包含支付回调信息的对象</param>
/// <returns>操作结果的JSON响应</returns>
[HttpPost]
public IHttpActionResult Callback([FromBody] PayCallbackVO callbackVO)
{
// 检查请求体是否为空,以及必要字段是否存在
if (callbackVO == null || string.IsNullOrEmpty(callbackVO.Sign) ||
string.IsNullOrEmpty(callbackVO.AccountId) ||
string.IsNullOrEmpty(callbackVO.OrderId))
{
return Content(HttpStatusCode.BadRequest, new { status = "paramerror" });
}
try
{
// 生成当前签名并进行验证
var currentSign = GenerateSign(callbackVO);
if (!currentSign.Equals(callbackVO.Sign, StringComparison.OrdinalIgnoreCase))
{
return Content(HttpStatusCode.BadRequest, new { status = "othererror" });
}
// 此处可以添加其他业务逻辑,如校验订单号、商品ID和金额是否匹配等
// 返回成功状态
return Ok(new { status = "ok" });
}
catch (Exception ex)
{
// 出现异常时返回服务器错误,并记录日志(如果需要)
return InternalServerError(ex);
}
}
/// <summary>
/// 生成MD5签名
/// </summary>
/// <param name="vo">支付回调参数对象</param>
/// <returns>生成的MD5签名字符串</returns>
private string GenerateSign(PayCallbackVO vo)
{
// 按指定顺序拼接字符串以生成签名输入
var text = $"{vo.AccountId}{vo.AreaId}{vo.OrderPrice}" +
$"{vo.OrderId}{vo.OrderTimestamp}{vo.ItemId}" +
$"{vo.ChannelId}{AppKey}";
// 使用MD5算法生成哈希值并转换为小写
using (var md5 = System.Security.Cryptography.MD5.Create())
{
var inputBytes = Encoding.UTF8.GetBytes(text);
var hashBytes = md5.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
}
/// <summary>
/// 支付回调的数据结构定义
/// </summary>
public class PayCallbackVO
{
public string AccountId { get; set; }
public string AreaId { get; set; }
public string OrderId { get; set; }
public string OrderTimestamp { get; set; }
public int OrderPrice { get; set; }
public int ChannelId { get; set; }
public string ItemId { get; set; }
public string ItemName { get; set; }
public string Memo { get; set; }
public string Remark { get; set; }
public string Region { get; set; }
public string Currency { get; set; }
public string Sign { get; set; }
}