# 支付回调

# 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
+orderTimestamp+itemId+channelId+appkey)
appkey的值跟登录的appkey是同一个。

举列:accountId:1350000001
areaId:1
orderPrice:600
orderId:13281108827665633280
orderTimestamp:1722590112
itemId:com.dianhun.test.a001
channelId:1010
appkey:12345678

加密前的值:135000000116001328110882766563328017225
90112com.dianhun.test.a001101012345678

加密后: 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; }
}
Last Updated: 2024/12/13 14:43:15