.. _9 支付系统:
9 支付系统
===============================
Pico支付,是基于Pico账户体系进行的游戏币支付系统,结算方式以现行的Pico公司下的游戏货币单位为准(P币)。如项目需要内购功能,请仔细阅读本章内容;如项目不需要内购功能,请忽略此章。
9.1 准备工作
-------------------
9.1.1 获取支付所用字符串
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
开发者在接入支付SDK时,需要在开发者平台创建应用并获取相应字符串。申请流程如下:
- 1. 登录开发者平台并注册Pico会员(http://dev.picovr.com/)
- 2. 申请成为开发者
开发者可申请为企业开发者,请根据实际情况进行申请。审核提交后,我们会在3个工作日内进行反馈,请及时查看开发者平台状态。
- 3. 查看商户ID
申请成为开发者后,点击右上角昵称可以查看到开发者ID,该ID将作为支付系统中商户的唯一标志:
.. image:: _static/9.1.png
图9.1 商户ID
- 4. 获取相应字符串
开发者可以从管理中心进入到创建应用阶段。点击创建应用后,首先选择要发布的平台:
.. image:: _static/9.2.png
图9.2 选择应用的发布平台
然后进入相应平台完善应用的相关信息
.. image:: _static/9.3.png
图9.3 完善应用的相关信息
请重点注意上图标红位置,请谨慎填写应用类型,一经填写是无法进行修改的!游戏类应用如果存在道具内付的情况,我们要求开发者必须采用开发者后台增加商品码的方式进行统一管理。
支付方式说明:
当前支付方式为两种,一种是P币支付(应用类型),一种是商品码支付(游戏类型),同一应用只能使用一种支付方式。
成功创建应用后,开发者平台会对其分配字符串,包括APP ID、APP KEY、APP Secret,还会给开发者分配一个developer ID:
.. image:: _static/9.4.png
图9.4 APP ID、APP KEY、APP Secret
再选择“游戏内支付配置”,配置游戏的内购信息:
.. image:: _static/9.5.png
图9.5 游戏内购配置
注意,商品码的规则定义为‘首位为字母,仅允许输入字母及数字,不超过20个字符’。不同道具间的商品码不能重复。道具类型分为可消耗道具和不可消耗道具。可消耗道具为可重复购买的商品,如金币、血瓶等;不可消耗道具为一次性购买产品,如武器、解锁关卡。
9.1.2 使用支付所用字符串
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
开发者需要注意,如果您需要使用SDK中的登录和支付功能或获取用户信息,则必须集成下面描述的配置,如果不进行如下配置或配置错误,则会造成登录和支付功能或者获取用户信息失败。如果您不需要这些功能,则可以跳过此部分。配置所需要的参数值,是在9.1.1中第四步获取到的值。
本SDK已集成AndroidManifes.xml,您的工程中不包含AndroidManifes.xml文件,直接使用本SDK中的AndroidManifes.xml文件。若开发工程已有AndroidManifes.xml文件,请务必把本SDK中的AndroidManifes.xml相关项合并到已有AndroidManifes.xml中。需合并项包含以下几点:
> 必要的权限:
.. code-block:: xml
> 开发者信息(国内)
.. code-block:: xml
// 将APP ID 换成应用申请的APPID
// 将APP KEY换成应用申请的APPKEY
//将 SCOPE换成固定值get_user_info
//将DEVELOPER ID换成开发者的DEVELOPER ID
//将APP SECRET换成应用申请的APPSECRET
> 开发者信息(国外)
.. code-block:: xml
注意,上述参数中的开发者信息(国内)、开发者信息(国外)根据实际工程需要只填入一组即可。
如果需要同时在国内和国外使用,则上述两套需要同时配置,相应的appid等参数以实际为准。
其中pico_app_id/pico_app_id_foreign与pico_app_key/ pico_app_key_foreign请填写从官方获得的字符串,pico_merchant_id/ pico_merchant_id_foreign和pico_pay_key/pico_pay_key_foreign在尚未从官方获得的情况下,请填写上述value值。
注:此配置文件中, pico_pay_key/ pico_pay_key_foreign对应开发者平台上AppSecret。
填写示例:
.. code-block:: xml
9.2 使用支付系统
------------------------------------
支付系统的流程比较复杂,我们特提供了一个Demo用以参考。展开Assets> Pvr_Payment>Demo>Scenes,打开Demo场景如下:
.. image:: _static/9.6.png
图9.6 支付Demo
上图中,每个按钮中左侧部分代表需调用的方法,右侧部分代表回调的方法。开发者使用支付系统时,请参照以上Demo实现。
注意:
1、Prefabs文件夹下有一“PicoPayment”的预制体,用于系统回调使用,请务必在工程中加入此物体。
2、绑定于PicoPayment上的“Callback”脚本中,预处理了一部分回调的数据,开发者若需处理其他数据,请修改相关函数中的内容, **但函数名不能修改**。
9.3 功能接口说明
------------------------------
接口回调函数均位于PicoPayment上的“Callback”脚本中。相关注意事项,请参考9.2节。
9.3.1 登陆
^^^^^^^^^^^^^^^^^^^^^
- 调用方法:void Login()
- 回调方法:void LoginCallback(string LoginInfo)
其中,LoginInfo为后台返回的登陆信息,登陆示范格式如下:
.. code-block:: java
{"isSuccess":"true或false",当值为true时,为成功登陆;值为false时,登陆失败 "msg":"对应说明信息"}
成功示例:
::
{"isSuccess":"true","msg":"SUCCESS"}
失败示例:
::
{"isSuccess":"false","msg":"Network exception"}
9.3.2 支付
^^^^^^^^^^^^^^^^^^^
注意,需要确认已调用登录接口9.3.1
- 调用方法:void Pay(string payOrderJson)
其中,payOrderJson为包含订单信息的Json字符串,以下参数为 必须 参数,包括:
========== =============================================
subject 订单标题,商品概述
body 订单描述,商品的详细描述
order_id 订单编号,由开发者生成的编号,不超过64个字符
total 商品总价,为大于0的整数,直接支付时输入,商品码支付时不输入
goods_tag 商品标签
pay_code 商品码,必须和开发者平台配置的相一致,直接支付时不输入,商品码支付时输入
========== =============================================
示例1:
.. code-block:: java
Pay("{'subject':'游戏',' body ':'购买完整游戏','order_id ':'10000','total ':'10','goods_tag ':'game' }");
示例2:
.. code-block:: java
Pay("{'subject':'游戏','body ':'购买完整游戏','order_id ':'10000',','goods_tag ':'game','pay_code':'123' }");
注意:示例1是直接支付的方式,示例2是使用商品码支付的方式,两者不可同时进行,直接支付时不可填写商品码属性,使用商品码时不可填写total属性,或将其设为0,且两种支付方式须与开发者平台账号中设置的支付种类保持一致。
注意:order_id在支付系统中是唯一的,所以为保证单个用户在同一个app上订单号唯一,需要使用 “openID+自定义订单号“的形式进行命名。
- 回调方法:void QueryOrPayCallback(string queryOrPayInfo)
其中,queryOrPayInfo为由后台返回支付信息,示范格式如下:
::
{"code ":"12000","msg":"PAY_SUCCESS"} //当code值为12000时,支付成功;其他为支付失败的异常码}
其中的支付回调code&msg一览:
====================== ==============================================
Code Msg
====================== ==============================================
11001 USER_NOT_LOGIN_OR_EXPIRED
11004 MISSING_APP_PARAMETERS
12000 PAY_SUCCESS
12002 ENTER_AMOUNT_ERROR
12003 PCOIN_NOT_ENOUGH
12006 NOT_ENTER_ORDER_INFO
12007 PAY_ORDER_EXIST
12008 PAY_CODE_NOT_EXIST
12009 PAY_CODE_ALERADY_CONSUMED
14001 SDK_LOCAL_ERROR
14004 NETWORK_ERROR
15001 SYSTEM_ERROR
15003 SERVICE_APP_PARAMETER_NOT_MATCH
====================== ==============================================
9.3.3 查询订单
^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 调用方法:void QueryOrder(string orderId)
其中,orderId为需要查询的订单编号
- 回调方法:void QueryOrPayCallback(string queryOrPayInfo)
其中,queryOrPayInfo为后台返回订单信息,格式如下:
- code: 当值为13000时,查询订单成功;其他为支付失败的异常码
- msg: code为13000时,msg返回订单信息的json串;code为其他值时,返回对应的说明信息
.. code-block:: java
{"code":"13000","msg":"用户信息json串"}
当code为 **13000** 时,msg的订单信息json串如下:
.. code-block:: java
{
"trade_no":"22016082314719505878171324",//Pico支付订单号
"open_id":"4f3148bdc34d9bca104927729a173b64",
"ret_msg":"",
"coupon_fee":0.00,
"fee_type":"PIC",
"pay_time":1471950587000,//支付完成时间
"nonce_str":"yiUzuv4VQO1OXBAzVyZSRztOmRgIOioT",
"out_trade_no":"12345678903", //商家订单号
"trade_status":"SUCCESS",//SUCCESS—支付成功
"trade_type":"EGG",
"result_code":"SUCCESS",
"mch_id":"company_id",
"ret_code":"SUCCESS",
"sub_msg":"OK",
"total_fee":100.00,//订单总金额
"app_id":"bf18ac2de375095d63428134e44d1867",
"sub_code":"SUCCESS",
"receipt_fee":100.00,//实收金额
"buyer_pay_fee":100.00//买家付款的金额
}
当code为其他值时,msg的订单信息json串如下:
::
{"code ":"13006","msg":"QUERY_ORDER_NOT_EXIST"}
查询的code和msg:
====================== ==============================================
Code Msg
====================== ==============================================
11001 USER_NOT_LOGIN_OR_EXPIRED
13000 QUERY_ORDER_SUCCESS
13003 NOT_ENTER_ORDER_ID
13006 QUERY_ORDER_NOT_EXIST
14001 SDK_LOCAL_ERROR
14004 NETWORK_ERROR
15001 SYSTEM_ERROR
15003 SERVICE_APP_PARAMETER_NOT_MATCH
====================== ==============================================
9.3.4 获取用户信息
^^^^^^^^^^^^^^^^^^^^^^^^^^^
注意,需要确认已调用登录接口9.3.1
- 调用方法:void GetUserAPI()
- 回调方法:void UserInfoCallback (string userInfo)
其中,userInfo为由后台返回用户信息,是一个未经处理的Json串, **其中openid为用户在当前应用的唯一标识(必有,应用更换appid时,会改变此值,请务必注意)** ,查询成功时示范格式如下
.. code-block:: java
{"ret_code":"0000",
"data":{
"aboutme":"",
"birthday":1460476800000, //long类型值,出生日期,默认时间是某年某月某日的零点
"phone":"13100000000", //手机号,使用手机注册用户必有
"username":"Admin", //用户名,可以被用户修改
"email":"", //邮箱,使用邮箱注册用户必有
"gender":"male",
"lastname":"",
"openid":"4f3148bdc34d9bca104927729a173b64", //用户唯一标识,必有
"firstname":"",
"avatar":"http://172.31.83.11/upload/6dd6ee103714e967846c3d38ae48d511",
"country":"China", //国家 可在官网进行设置
"city":"" //城市 可在官网进行设置
},
"ret_msg":"调用成功"
}
查询失败时,无data部分数据,示范格式如下:
.. code-block:: java
{
"ret_code":"5000",
"ret_msg":"SYSTEM_ERROR"
}
ret_code码及ret_msg一览如下:
========= =========================================
ret_code ret_msg
0000 REQUEST_SUCCESS
5000 服务端返回的错误信息
========= =========================================
9.4 开发者服务端交互
------------------------------
支付完成后,支付系统会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。
对后台通知交互时,如果支付系统收到商户的应答不是成功或超时,则认为通知失败,支付系统会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但不保证通知最终能成功。
同样的通知可能会多次发送给商户系统,商户系统必须能够正确处理重复的通知。推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
商户服务端需要实现下面的接口,用于接收来自Pico服务器请求,获取Pico支付系统的支付结果和用户信息:
================= ========================================================================
名称 支付结果回调接口
请求类型 POST
请求URL 支付,PayOrder传入的参数notify_url
请求格式 JSON
返回格式 JSON
是否需要登录 是
请求参数 详见下面“表9.1 支付结果通知中的通知参数”
请求参数示例
返回参数 +--------------+---------------+---------------+
| Parameter | Type | Description |
+--------------+---------------+---------------+
| ret_code | string | Error code |
+--------------+---------------+---------------+
| ret_msg | string | Error message |
+--------------+---------------+---------------+
详见下面“表9.2 返回结果”
返回参数实例 { "ret_code":"SUCCESS", "ret_msg":"OK" }
更新说明
================= ========================================================================
表9.1 支付结果通知中的通知参数
==================== =============== ====== ======== ===============================================================================
字段名 变量名 必填 类型 描述
==================== =============== ====== ======== ===============================================================================
返回状态码 ret_code 是 String SUCCESS/FAIL此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断
返回信息 ret_msg 否 String 返回信息,如非空,为错误原因:函数名失败参数格式校验错误
错误代码 sub_code 否 String 错误码
错误代码描述 sub_msg 否 String 错误返回的信息描述
Pico支付订单号 trade_no 是 String Pico支付订单号
商户订单号 out_trade_no 是 String 商户系统内部的订单号
应用ID app_id 是 String 平台审核通过的应用APP_ID
商户ID mch_id 是 String 支付分配的商户号
用户标识 open_id 是 String 用户在商户appid下的唯一标识
设备号 device_id 否 String 终端设备号
随机字符串 nonce_str 是 String 随机字符串,不长于32位。推荐随机数生成算法
函数名 signature 是 String 函数名,详见函数名生成算法
业务结果 result_code 是 String SUCCESS/FAIL
交易类型 trade_type 是 String 支付类型
货币类型 fee_type 是 String 货币类型
总金额 total_fee 是 String 订单总金额
实收金额 receipt_fee 是 String 实收金额
买家付款的金额 buyer_pay_fee 否 String 买家付款的金额
代金券或立减优惠金额 coupon_fee 否 String 代金券或立减优惠金额
商家数据包 attach 否 String 商家数据包,原样返回
支付完成时间 pay_time 是 String 支付完成时间,格式为yyyy-MM-dd HH:mm:ss
==================== =============== ====== ======== ===============================================================================
表9.2 返回结果
=================== =============== ====== ======== ================================================================================
字段名 变量名 必填 类型 描述
=================== =============== ====== ======== ================================================================================
返回状态码 ret_code 是 String SUCCESS/FAIL SUCCESS表示商户接收通知成功并校验成功
返回信息 ret_msg 否 String 返回信息,如非空,为错误原因:函数名失败参数格式校验错误
=================== =============== ====== ======== ================================================================================
特别提醒:商户系统对于支付结果通知的内容一定要做函数名验证,防止数据泄漏导致出现“假通知”,造成资金损失。
函数名校验规则是:
1.返回的参数列表,去掉signautre参数,同时添加key=“app_secret”,value=paykey,然后根据key值进行自然排序,多个参数之间用&隔开,最后进行MD5加密
2.用加密后的字符串和获取到的signature进行比较
相关函数如下:
.. code-block:: java
/**
* result :获取的数据的map集合
* paykey :就是开发者平台上的paykey
*/
public static String createSign(Map result, String paykey)
{
if (result == null || result.size() == 0)
return null;
result.put("app_secret", paykey); //1.添加key = “app_secret”,value=payke
String sign = result.get("signature");//2.保存signature的值,用于校验
result.remove("signature"); //3.移除signature参数
String[] tmp = new String[result.size()];
int i = 0;
for (String key : result.keySet())
{
tmp[i++] = key;
}
Arrays.sort(tmp); //4.自然排序
String signTemp = "";
for (String string : tmp)
{
if (result.get(string) == null)
continue;
signTemp += string + "=" + URLEncoder.encode(result.get(string).toString()
, "utf-8") + "&";
}
if (signTemp.endsWith("&"))
signTemp = signTemp.substring(0, signTemp.length() - 1);
Log.i(TAG, "createSign: " + signTemp);
String localSign = MD5.MD5(sign); //5.生成MD5加密后的字符串
return localSign.equal(sign);//6.和2中的sign进行校验
}