Appearance
支付回调
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);方法完成订单,并通知游戏服务器给玩家发货。
5、return "SUCCESS" 或者 "FAILURE",按渠道要求给渠道服务器反馈
游戏服务器回调接口
当U8Server收到渠道SDK支付回调,并处理成功时,我们需要调用游戏服的支付回调地址,通知游戏支付成功,让游戏服给玩家发游戏币。
注意: 此协议详细信息,可以参考U8SDK接入文档中服务端部分的说明。