U8SDK——U8Server的实现
U8Server是采用J2EE框架SSH2实现的。当然你也可以采用任何其他你熟悉的语言实现。深入分析U8Server
文章中,我们从类的抽象中,大致梳理了一下U8Server所要完成的工作。大的方向上,U8Server需要实现统一的用户登录认证,和支付中心两大功能。更深入的细节,U8Server需要完成以下功能:
 
1、游戏管理:查询游戏,创建游戏(同时生成游戏的唯一appID,appKey等),编辑游戏,删除游戏等功能。
2、渠道商管理:查询渠道商,创建渠道商(设置该渠道商对应的登陆认证地址,支付回调地址等操作。),编辑渠道商,删除渠道商等功能。
3、渠道管理:查询某个游戏的渠道,创建渠道(将指定的游戏和一个渠道商连接起来,同时生成渠道ID,配置该游戏对应该渠道的appID,appKey等信息),编辑渠道,删除渠道。
4、登陆认证:完成各个游戏各个渠道的统一登陆认证功能
5、支付回调:支付之前获取订单号。所有第三方SDK的支付回调,然后将第三方支付回调以统一的格式返回给游戏服务器
 
对于游戏管理,渠道商管理,渠道管理三个功能,可以做成一个后台管理界面,通过后台操作,实现各个功能的管理。就是简单的数据库查询和数据增删改查功能。本文主要介绍统一的登陆认证和支付两个部分。
 
对于登陆认证,通过之前的流程图我们知道。所有渠道SDK登陆成功之后,都会返回sid等信息,客户端紧接着拿着这个sid去U8Server进行登录认证,U8Server再去第三方SDK服务器认证,认证成功之后,U8Server会生成一条用户信息,然后将生成一个token给客户端。客户端拿着这个token,登陆游戏服务器,游戏服务器再拿着这个token去U8Server进行认证。如果合法,则用户成功登陆。
 
那么,这样我们的登陆认证接口就有了,请看代码:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | @Action("getToken")     public void getLoginToken(){         try{             Log.i("The appID is "+this.appID);             UGame game = gameManager.queryGame(this.appID);             if(game == null){                 renderState(StateCode.CODE_GAME_NONE, null);                 return;             }             UChannel channel = channelManager.queryChannel(this.channelID);             if(channel == null){                 renderState(StateCode.CODE_CHANNEL_NONE, null);                 return;             }             UChannelMaster master = channel.getMaster();             if(master == null){                 renderState(StateCode.CODE_CHANNEL_NONE, null);                 return;             }             ISDKVerifier verifier = Class.forName(master.verifyClass)             SDKVerifyResult sdkResult = verifier.verify(channel, sid, extension);             if(sdkResult.isSuccess()){                 UUser user = userManager.getUserByCpID(appID, channelID, sdkResult.getUserID());                 if(user == null){                     user = userManager.generateUser(channel, sdkResult);                 }else{                     user.setLastLoginTime(new Date());                 }                 user.setToken(UGenerator.generateToken(user));                 userManager.saveUser(user);                 JSONObject data = new JSONObject();                 data.put("userID", user.getId());                 data.put("token", user.getToken());                 renderState(StateCode.CODE_AUTH_SUCCESS, data);             }else{                 renderState(StateCode.CODE_AUTH_FAILED, null);             }         }catch (Exception e){             Log.e(e.getMessage());             renderState(StateCode.CODE_AUTH_FAILED, null);         }     } | 
这个请求处理方法getToken就是第一步,客户端拿到sid时,发起getToken请求。访问getToken时,客户端带上,appId,channelId,sid等数据。
注意这里的appID,channelId是U8Server之前通过后台分配该该游戏的。
在这个方法中,通过appID,查询到当前是哪个游戏,通过channelID查询到当前是哪个渠道。因为各个渠道的登陆认证方式不同,所以,我们抽象出来一个接口:ISDKVerifier 。各个渠道SDK的登陆认证类实现该接口。然后将实现类的完整类名,存储在UChannelMaster对象中。
这样,通过渠道信息,我们就知道当前的SDK登陆认证实现类的完整类名,并进行实例化。然后登陆认证之后,返回一个认证结果。
如果认证成功,我们生成一条用户信息,同时生成一个token。然后将userID和该token返回给客户端。
这样,整个登陆认证过程中,第三方SDK的认证部分就结束了。接下来的流程仅仅是在U8Server和游戏服务器之间了。这就是当客户端拿到这个userID和token信息时,客户端就开始登陆游戏服务器。游戏服务器需要拿着这个userID和token去U8Server进行二次认证。看接口:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |     @Action("verifyAccount")     public void loginVerify(){         try{             UUser user = userManager.getUser(this.userID);             if(user == null){                 renderState(StateCode.CODE_USER_NONE, null);                 return;             }             if(StringUtils.isEmpty(this.token)){                 renderState(StateCode.CODE_VERIFY_FAILED, null);                 return;             }             long now = System.currentTimeMillis();             if(!this.userManager.isTokenValid(user, token)){                 renderState(StateCode.CODE_TOKEN_ERROR, null);                 return;             }             JSONObject data = new JSONObject();             data.put("userID", user.getId());             data.put("username", user.getName());             renderState(StateCode.CODE_AUTH_SUCCESS, data);             return;         }catch (Exception e){             Log.e(e.getMessage());         }         renderState(StateCode.CODE_VERIFY_FAILED, null);     } | 
服务器拿着userID和token来U8Server进行认证。U8Server验证token是否合法和是否超时,如果验证通过,则返回userID,username等用户信息给游戏服务器。否则,其他情况,均返回失败信息。
整个登陆认证,也就这两个接口。当接其他第三方SDK时,比如当乐,直接定义一个DLSDK同时实现ISDKVerifier接口,同时将该实现类,设置在当乐对应的UChannelMaster对象中。
接下来,我们再来看支付中心。支付中心之前也说了,也包括两个部分。首先是获取订单号。然后是支付通知回调。先看获取订单号接口:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |     @Action("getOrderID")     public void getOrderID(){         try{             UUser user = userManager.getUser(this.userID);             if(user == null){                 renderState(StateCode.CODE_USER_NONE, null);                 return;             }             if(money < 0 ){                 renderState(StateCode.CODE_MONEY_ERROR, null);                 return;             }             UOrder order = orderManager.generateOrder(user, money, extension);             if(order != null){                 JSONObject data = new JSONObject();                 data.put("orderID", order.getOrderID());                 renderState(StateCode.CODE_AUTH_SUCCESS, data);                 return;             }         }catch (Exception e){             Log.e(e.getMessage());         }         renderState(StateCode.CODE_ORDER_ERROR, null);     } | 
该接口很简单,就是客户端传过来当前的userID,充值金额,已经需要在充值通知回调原封不动返回的extension数据。然后,U8Server生成一条UOrder,同时将该order的状态设置为“正在充值状态”。然后,将这个订单号返回给客户端。
客户端拿到这个订单号时,就开始调用调用第三方SDK的充值接口,同时将该订单号作为第三方SDK的自定义参数传递到第三方SDK。然后第三方SDK充值完成时,会通知到U8Server.(这个就要游戏在申请接入第三方SDK的时候,将通知回调设置为U8Server对应该渠道的支付通知回调地址),U8Server验证后再讲结果通知给游戏服务器。
那么,U8Server承担了第三方SDK通知回调的功能,因为各个渠道的通知参数和请求方式等都不太一样,所以我们选择将各个渠道的通知回调实现分开。下面以UC渠道的支付通知回调为例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | @Controller @Namespace("/uc") public class UCPayCallbackAction extends UActionSupport{     @Autowired     private UOrderManager orderManager;     @Action("payCallback")     public void payCallback(){         try{             //BufferedReader br = new BufferedReader(new InputStreamReader(this.request.getInputStream(), "UTF-8"));             BufferedReader br = this.request.getReader();             String line;             StringBuilder sb = new StringBuilder();             while((line=br.readLine()) != null){                 sb.append(line).append("\r\n");             }             Log.d("UC Pay Callback . request params:" + sb.toString());             PayCallbackResponse rsp = (PayCallbackResponse) JsonUtils.decodeJson(sb.toString(), PayCallbackResponse.class);             if(rsp == null){                 this.renderState(false);                 return;             }             long orderID = Long.parseLong(rsp.getData().getCallbackInfo());             UOrder order = orderManager.getOrder(orderID);             if(order == null || order.getChannel() == null){                 Log.d("The order is null or the channel is null.");                 this.renderState(false);                 return;             }             UCSDK sdk = new UCSDK();             if(!sdk.verifyPay(order.getChannel(), rsp)){                 Log.d("The sign is not matched.");                 this.renderState(false);                 return;             }             if("S".equals(rsp.getData().getOrderStatus())){                 float money = Float.parseFloat(rsp.getData().getAmount());                 int moneyInt = (int)(money * 100);  //以分为单位                 order.setMoney(moneyInt);                 order.setChannelOrderID(rsp.getData().getOrderId());                 order.setState(PayState.STATE_SUC);                 orderManager.saveOrder(order);                 Log.d("The channnel order is ok. sendToServer.");                 sendToServer(order);             }else{                 order.setChannelOrderID(rsp.getData().getOrderId());                 order.setState(PayState.STATE_FAILED);                 orderManager.saveOrder(order);             }             renderState(true);         }catch (Exception e){             try{                 this.renderState(false);             }catch (Exception e2){                 Log.e(e2.getMessage());             }             Log.e(e.getMessage());         }     }     private boolean sendToServer(UOrder order){         UGame game = order.getGame();         if(game == null){             return false;         }         if(StringUtils.isEmpty(game.getPayCallback())){             return false;         }         JSONObject data = new JSONObject();         data.put("orderID", order.getOrderID());         data.put("userID", order.getUserID());         data.put("gameID", order.getAppID());         data.put("money", order.getMoney());         data.put("currency", order.getCurrency());         data.put("extension", order.getExtension());         JSONObject response = new JSONObject();         response.put("state", StateCode.CODE_AUTH_SUCCESS);         response.put("data", data);         response.put("sign", UGenerator.generateSign(order));         UHttpAgent httpAgent = new UHttpAgent("text/html");         String serverRes = httpAgent.post(game.getPayCallback(), response.toString());         if(serverRes.equals("SUCCESS")){             order.setState(PayState.STATE_COMPLETE);             orderManager.saveOrder(order);             return true;         }         return false;     }     private void renderState(boolean suc) throws IOException{         String res = "SUCCESS";         if(!suc){             res = "FAILURE";         }         this.response.getWriter().write(res);     } } | 
可以看到,在payCallback方法中,我们解析了UC渠道支付回调的参数,然后进行验证,同时,通过取出自定义参数中的OrderID,从数据库中取出之前获取订单号时,生成的那条订单数据。
如果,验证合法,我们就会将该订单的状态置为成功状态。同时,将第三方渠道订单号保存到该订单中。
然后,我们就将该订单的数据和以该订单数据某种结合生成的md5校验码一起通知给该游戏的游戏服务器。(游戏服务器在接入U8Server时,就要求他们提供一个支付回调地址)如果,收到游戏服务器成功的通知,则将该订单的状态设置为“完成”。
到此,说明一次支付就完成了。
那这只是正常情况,如果出现错误或者异常情况,我们需要及时返回错误的信息。如果支付回调没有成功,U8Server可能还需要每隔一定得时间,再次尝试请求游戏服务器的回调地址。
那么,到这里,一个统一的登陆认证中心和支付中心的雏形已经有了。剩下来的事情,就是在实践中,不断改善不断重构不断稳定。。。
本文出自 U8SDK技术博客,转载时请注明出处及相应链接。
本文永久链接: http://www.uustory.com/?p=1453









