Skip to content

支付回调

IMPORTANT

大部分渠道SDK支付都是异步通知的方式。注册游戏的时候,需要在渠道SDK后台配置一个支付通知地址,也有部分渠道SDK的支付通知地址,是在客户端调用渠道SDK支付接口的时候,通过参数传进去的。所有这些地址,都配置为U8Server中对应渠道SDK的支付回调处理接口的地址。

说明

每个渠道SDK支付回调通知的参数和请求方式,都不太一样,所以U8Server中,为每个渠道SDK都单独提供了一个支付回调处理接口。

对应的处理类在u8-x-server模块com.u8.server.web.controllers.partner.pay包名下,命名方式如下:

bash
UCPayController
BaiduPayController
DownjoyPayController
......

所有渠道SDK的支付通知地址,在配置的时候,格式都是固定的:{U8Server URL}/partner/pay/渠道名/callback/渠道号

假设当前U8Server的URL地址是http://localhost:8080/

当前A游戏的UC渠道,对应的渠道号是 10;那么,A游戏中需要将UC的支付回调通知地址配置为:

http://localhost:8080/partner/pay/uc/callback/10

当前B游戏的百度渠道,对应的渠道号是 50;那么,B游戏中需要将百度的支付回调通知地址配置为:

http://localhost:8080/partner/pay/baidu/callback/50

其他渠道,也是类似。

如果渠道SDK有管理后台,并且支付通知回调地址是在后台配置的。那么你直接配置对应渠道的地址即可。比如上面A游戏的UC渠道,你配置成 http://localhost:8080/partner/pay/uc/callback/10 即可。

对于那些需要客户端通过参数传支付回调地址的渠道SDK,在服务端实现创建渠道订单号接口的时候,将支付回调地址放在extension参数中,而不要将支付回调通知地址,写死在客户端。

流程

收到渠道SDK的支付回调通知时,我们需要在对应的渠道SDK支付回调处理接口类中,按照渠道SDK的要求,做一些处理。我们先看看U8Server中,整个支付流程:

1、游戏客户端,首先请求游戏服务器要充值

2、游戏服务器拿着该用户的id和一些支付成功之后需要原样返回的数据,去访问U8 Server申请订单号

3、U8 Server生成一个唯一的订单号,同时数据库中生成一条订单记录,状态是正在支付状态

4、游戏服务器将订单号返回给客户端

5、游戏客户端,拿到订单号之后,带着订单号以及游戏里充值相关的数据,调用SDK抽象接口的支付接口,调用对应的SDK支付界面,进行充值操作。

6、当前SDK的渠道实现在调用SDK支付界面之前,需要把刚刚的订单号,放到渠道SDK支付参数的自定义参数中。这个每个渠道都是一样的。

7、渠道SDK支付成功,立马返回一个状态

8、同时,渠道SDK服务器会异步通知游戏开发商设置的支付回调地址。这里,就是上面我们提供的U8Server的该渠道的通知回调地址

9、U8Server收到充值回调,根据当前渠道SDK的要求,验证合法,并修改订单状态,立马给渠道SDK服务器返回一个成功或者失败的状态。

10、然后U8Server根据自定义参数中的orderID,查询到对应的订单信息,再根据订单信息,获取到当前用户信息和对应的游戏信息,然后调用接入游戏之前,游戏服务器提供给U8Server的支付回调地址。这个回调地址,游戏服务器只需要提供一个给U8Server就可以了。因为游戏服务器只和U8Server交互。

11、游戏服务器收到回调,验证成功与否,立马返回给U8Server一个成功或者失败的信息。同时,给对应的玩家加游戏币。

NOTE:最新的U8SDK下单已经封装到了客户端框架内部,无需游戏层处理了。 所以使用最新的U8SDK框架,游戏服务器不需要再去U8Server下单了。

同时,我们通过支付流程的时序图,可以更加直观地看清整个流程:

通过这个流程,我们可以看到,所有渠道SDK支付头通知到U8Server,再由U8Server来通知到对应的游戏服务器。

渠道SDK支付回调

我们以UC渠道SDK的支付回调通知为例:

java
package com.u8.server.web.controllers.partner.pay;

import com.u8.server.common.contants.Consts;
import com.u8.server.common.logger.UGLogger;
import com.u8.server.common.utils.EncryptUtils;
import com.u8.server.common.utils.JsonUtils;
import com.u8.server.common.utils.StringUtils;
import com.u8.server.entities.U8Channel;
import com.u8.server.entities.U8Game;
import com.u8.server.entities.U8Order;
import com.u8.server.sdk.uc.PayCallbackResponse;
import com.u8.server.service.*;
import com.u8.server.service.common.I18nService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * UC/阿里游戏
 */
@Controller
@RequestMapping("/partner/pay/uc")
public class UCPayController {
    private final String TAG = "uc";
    private static final UGLogger logger = UGLogger.getLogger(UCPayController.class);

    private final UserService userService;

    private final OrderService orderService;

    private final ChannelService channelService;

    private final GameService gameService;

    private final OrderPlatformLogService platformLogService;

    private final I18nService i18nService;

    public UCPayController(UserService userService,
                           OrderService orderService,
                           ChannelService channelService,
                           GameService gameService,
                           OrderPlatformLogService platformLogService,
                           I18nService i18nService) {
        this.userService = userService;
        this.orderService = orderService;
        this.channelService = channelService;
        this.gameService = gameService;
        this.platformLogService = platformLogService;
        this.i18nService = i18nService;
    }

    private PayCallbackResponse parseData(HttpServletRequest request) throws IOException {
        BufferedReader br = request.getReader();
        String line;
        StringBuilder sb = new StringBuilder();
        while((line=br.readLine()) != null){
            sb.append(line).append("\r\n");
        }

        return JsonUtils.parse(sb.toString(), PayCallbackResponse.class);
    }

    @RequestMapping(value = "/callback", produces = "text/html;charset=UTF-8")
    @ResponseBody
    public String payCallback(HttpServletRequest request) throws IOException {

        PayCallbackResponse result = parseData(request);
        if(result == null) {
            logger.error("{} pay callback failed. data parse failed", TAG);
            return "FAILURE";
        }

        long orderID = Long.parseLong(result.getData().getCallbackInfo());
        U8Order order = orderService.getOrderByID(orderID);
        if (order == null) {
            // 订单不存在
            logger.error("{} pay callback failed:{}. order is not exists.", TAG, result.getData().getCallbackInfo());
            return "FAILURE";
        }

        if (order.getStatus() == Consts.OrderStatus.FINISH) {
            // 订单状态不是已支付
            logger.error("{} pay callback ignored:{}. status:{} is already finished", TAG, order.getId(), order.getStatus());
            platformLogService.addNotifyLog(order, false, i18nService.getMessage("err.pay.status.finish"));
            return "SUCCESS";
        }

        U8Channel channel = this.channelService.getChannel(order.getChannelID());
        if (channel == null) {
            // 渠道不存在
            logger.error("{} pay callback failed:{}. channel:{} is not exists", TAG, order.getId(), order.getChannelID());
            platformLogService.addNotifyLog(order, false, i18nService.getMessage("err.pay.channel.not.exists"));
            return "FAILURE";
        }

        final U8Game game = gameService.getGameByID(order.getAppID());
        if (game == null) {
            // 游戏不存在
            logger.error("{} pay callback failed:{}. game:{} is not exists", TAG, order.getId(), order.getAppID());
            platformLogService.addNotifyLog(order, false, i18nService.getMessage("err.pay.game.not.exists"));
            return "FAILURE";
        }

        if (order.getNotifyStatus() == Consts.NotifyStatus.NotifySuccess) {
            // 订单已经通知过游戏服务器了
            logger.error("{} pay callback ignored:{}. order is already notified to game server", TAG, order.getId());
            platformLogService.addNotifyLog(order, true, i18nService.getMessage("err.pay.already.notify"));
            return "SUCCESS";
        }

        if (!"S".equalsIgnoreCase(result.getData().getOrderStatus())) {
            // 订单状态错误
            logger.error("{} pay callback failed:{}. OrderStatus is not 1:{}", TAG, order.getId(), result.getData().getOrderStatus());
            platformLogService.addNotifyLog(order, false, i18nService.getMessage("err.pay.channel.status.failed"));
            return "FAILURE";
        }


        if (!isSignOK(order, channel, result)) {
            // 签名验证失败
            platformLogService.addNotifyLog(order, false, i18nService.getMessage("err.pay.sign.not.match"));
            return "FAILURE";
        }

        int realMoney = (int)(Float.parseFloat(result.getData().getAmount()) * 100f);
        if(realMoney < order.getPrice()){
            // 金额验证错误
            logger.error("{} pay callback failed:{}. price:{} is not matched with OrderMoney:{}", TAG, order.getId(), order.getPrice(), realMoney);
            platformLogService.addNotifyLog(order, false, i18nService.getMessage("err.pay.price.not.match"));
            return "FAILURE";
        }

        // 完成订单,并通知给游戏服务器
        orderService.completeOrder(game, order, result.getData().getOrderId(), realMoney);

        return "SUCCESS";

    }

    /***
     * 验证支付
     * @param channel
     * @param rsp
     * @return
     */
    public boolean isSignOK(U8Order order, U8Channel channel, PayCallbackResponse rsp){

        String signSource= "accountId="+rsp.getData().getAccountId()+"amount="+rsp.getData().getAmount()+"callbackInfo="+rsp.getData().getCallbackInfo();
        if(rsp.getData().getCpOrderId() != null && rsp.getData().getCpOrderId().length() > 0){
            signSource += "cpOrderId="+rsp.getData().getCpOrderId();
        }
        signSource = signSource+"creator="+rsp.getData().getCreator()+"failedDesc="+rsp.getData().getFailedDesc()+"gameId="+rsp.getData().getGameId()
                +"orderId="+rsp.getData().getOrderId()+"orderStatus="+rsp.getData().getOrderStatus()
                +"payWay="+rsp.getData().getPayWay()
                +channel.getCpAppKey();

        String sign = EncryptUtils.md5(signSource).toLowerCase();
        boolean valid = sign.equals(rsp.getSign());
        if (!valid) {
            logger.error("{} pay callback failed:{}. sign not match. sign:{}; sign local:{}; sign string:{}", TAG, order.getId(), rsp.getSign(), sign, signSource);
        }
        return valid;

    }


    private static class PayResult {
        private String out_order_no;        //:cp订单编号
        private String order_no;        //:订单编号
        private String amount;      //:支付金额 单位元
        private String role_id;     //:玩家角色id
        private String pay_time;        //:支付时间(YY-mm-dd HH:ii:ss) 2017-12-31 12:22:22
        private String cp_game_id;      //:游戏id(对接群获取)
        private String sign;        //:签名

        public String getOut_order_no() {
            return out_order_no;
        }

        public void setOut_order_no(String out_order_no) {
            this.out_order_no = out_order_no;
        }

        public String getOrder_no() {
            return order_no;
        }

        public void setOrder_no(String order_no) {
            this.order_no = order_no;
        }

        public String getAmount() {
            return amount;
        }

        public void setAmount(String amount) {
            this.amount = amount;
        }

        public String getRole_id() {
            return role_id;
        }

        public void setRole_id(String role_id) {
            this.role_id = role_id;
        }

        public String getPay_time() {
            return pay_time;
        }

        public void setPay_time(String pay_time) {
            this.pay_time = pay_time;
        }

        public String getCp_game_id() {
            return cp_game_id;
        }

        public void setCp_game_id(String cp_game_id) {
            this.cp_game_id = cp_game_id;
        }

        public String getSign() {
            return sign;
        }

        public void setSign(String sign) {
            this.sign = sign;
        }
    }
}


1、@RequestMapping("/partner/pay/uc") :类上面加此注解,指定该渠道SDK的支付回调处理地址的相对路径。 注意: 回调格式必须以【/partner/pay/渠道SDK标识】命名。
2、@RequestMapping(value = "/callback") :方法上加此注解,一般固定名称/callback。 这样UC渠道完整的回调地址就是: {{u8server地址}}/partner/pay/uc/callback
3、isSignOK:此方法中进行签名校验
4、处理成功,调用orderService.completeOrder(game, order, result.getData().getOrderId(), realMoney);方法完成订单,并通知游戏服务器给玩家发货。
5return "SUCCESS" 或者 "FAILURE",按渠道要求给渠道服务器反馈

游戏服务器回调接口

当U8Server收到渠道SDK支付回调,并处理成功时,我们需要调用游戏服的支付回调地址,通知游戏支付成功,让游戏服给玩家发游戏币。

注意: 此协议详细信息,可以参考U8SDK接入文档中服务端部分的说明。

版权所有© 2021-2030 上海丞诺网络科技有限公司