第七章 支付系统

Pico支付,是基于Pico账户体系进行的游戏币支付系统,结算方式以现行的Pico公司下的游戏货币单位为准(P币)。如项目需要内购功能,请仔细阅读本章内容;如项目不需要内购功能,请忽略此章。

7.1 准备工作

7.1.1 获取支付所用字符串

开发者在接入支付SDK时,需要在开发者平台创建应用并获取相应字符串。申请流程如下:

开发者分为个人开发者和企业开发者,请根据实际情况进行申请。审核提交后,我们会在3个工作日内进行反馈,请及时查看开发者平台状态。

    1. 查看商户ID

申请成为开发者后,点击右上角昵称可以查看到开发者ID,该ID将作为支付系统中商户的唯一标志:

_images/7.1.png

图7.1 商户ID

    1. 获取相应字符串

开发者可以从管理中心进入到创建应用阶段。点击创建应用后,首先选择要发布的平台:

_images/7.2.png

图7.2 选择应用的发布平台

然后进入相应平台完善应用的相关信息:

_images/7.3.png

图7.3 完善应用的相关信息

请重点注意上图标红位置,请谨慎填写应用类型,一经填写是无法进行修改的!游戏类应用如果存在道具内付的情况,我们要求开发者必须采用开发者后台增加商品码的方式进行统一管理。

成功创建应用后,开发者平台会对其分配字符串,包括APP ID、APP KEY、APP Secret:

_images/7.4.png

图7.4 APP ID、APP KEY、APP Secret

再选择“游戏内支付配置”,配置游戏的内购信息:

_images/7.5.png

图7.5 游戏内购配置

注意,商品码的规则定义为‘首位为字母,仅允许输入字母及数字,不超过20位字符’。不同道具间的商品码不能重复。道具类型分为可消耗道具和不可消耗道具。可消耗道具为可重复购买的商品,如金币、血瓶等;不可消耗道具为一次性购买产品,如武器、解锁关卡。

7.1.2 使用支付所用字符串

本SDK已集成AndroidManifest文件,若您的工程中不包含AndroidManifest文件,直接使用本SDK中的AndroidManifest文件。若开发工程已有AndroidManifest文件,请务必把本SDK中的AndroidManifest相关项合并到已有AndroidManifest中。需合并项包含以下几点:

必要的权限:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Activity声明:

<activity android:name="com.pico.loginpaysdk.UnityAuthInterface">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
<activity android:name="com.pico.loginpaysdk.component.PicoSDKBrowser"
    android:configChanges="keyboardHidden|orientation"
    android:windowSoftInputMode="adjustResize"
    android:exported="false">
</activity>

开发者信息(国内)

<!--APPID-->
<meta-data
    android:name="pico_app_id"
    android:value="APP ID"/>
<!--APPKEY-->
<meta-data
    android:name="pico_app_key"
    android:value="APPK EY"/>
<!--授权范围-->
<meta-data
    android:name="pico_scope"
    android:value="SCOPE"/>
<!--开发者ID-->
<meta-data
    android:name="pico_merchant_id"
    android:value="DEVELOPER ID"/>
<!--支付Key-->
<meta-data
    android:name="pico_pay_key"
    android:value="APP SECRET"/>

开发者信息(国外)

<!--APPID-->
<meta-data
    android:name="pico_app_id_foreign"
    android:value="APP ID"/>
<!--APPKEY-->
<meta-data
    android:name="pico_app_key_foreign "
    android:value="APPK EY"/>
<!--授权范围-->
<meta-data
    android:name="pico_scope_foreign "
    android:value="SCOPE"/>
<!--开发者ID-->
<meta-data
    android:name="pico_merchant_id_foreign "
    android:value="DEVELOPER ID"/>
<!--支付Key-->
<meta-data
    android:name="pico_pay_key_foreign "
    android:value="APP SECRET"/>

注意,上述参数中的开发者信息(国内)、开发者信息(国外)根据实际工程需要只填入一组即可。其中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。

7.2 使用支付系统

支付系统的流程比较复杂,我们特提供了一个Demo用以参考。展开Assets>Pvr_Payment>Demo>Scenes,打开Demo场景如下:

_images/7.6.png

图7.6 支付Demo

上图中,每个按钮中左侧部分代表需调用的方法,右侧部分代表回调的方法。开发者使用支付系统时,请参照以上Demo实现。

注意:

1、Prefabs文件夹下有一“PicoPayment”的预制体,用于系统回调使用,请务必在工程中加入此物体。

2、绑定于PicoPayment上的“Callback”脚本中,预处理了一部分回调的数据,开发者若需处理其他数据,请修改相关函数中的内容, 但函数名不能修改

7.3 功能接口说明

7.3.1 登陆

  • 调用方法:void Login()
  • 回调方法:void LoginOrUserInfoCallback(string LoginOrUserInfo)

其中,LoginOrUserInfo为后台返回的登陆信息,登陆成功示范格式如下:

{"access_token":"25ba00fb73343ff1ec32e1c152fff291",
"open_id":"2890d4a291108e73ef0e87340affe7a4",
"refresh_token":"5a189befeb3b33f7df101fbecffe4f98",
"expires_in":"1d6ef7f25a7b0ec3bbd5b6bf247adf71"}

登陆失败示范格式如下:

{"exception":"AccessToken is null"}

或:

{"exception":"auth code of response is null}

取消登陆时示范格式如下:

{"cancel":"cancel"}

7.3.2 支付

  • 调用方法:void Pay(string payOrderJson)

其中,payOrderJson为包含订单信息的Json字符串,包括:

subject 订单标题
body 订单描述
order_id 订单编号
total 商品总价
goods_tag 商品标签
notify_url 欲通知的URL
pay_code 商品码

示例1:

Pay("{'subject':'游戏',' body ':'购买完整游戏','order_id ':'10000','total   ':'10','goods_tag ':'game' }");

示例2:

Pay("{'subject':'游戏','body ':'购买完整游戏','order_id ':'10000',','goods_tag ':'game','pay_code':'123' }");

注意:示例1是直接支付的方式,示例2是使用商品码支付的方式,两者不可同时进行,直接支付时不可填写商品码属性,使用商品码时不可填写total属性,或将其设为0,且两种支付方式须与开发者平台账号中设置的支付种类保持一致。

  • 回调方法:void QueryOrPayCallback(string queryOrPayInfo)

其中,queryOrPayInfo为由后台返回支付信息,示范格式如下:

{"code ":"12000","msg":"支付成功"}

其中的支付回调code&msg一览:

Code Msg
00000 网络异常
10000 登陆成功
10001 用户未登录
10002 请输入正确金额
10003 登陆过期,请重新登陆
11000 商户验证成功
11001 商户验证失败
11002 用户验证参数错误或请求过期
11003 商户未验证
12000 支付成功
12001 支付失败
12003 P币不足
12004 余额可用
13000 生成订单
13001 获取数据失败
13002 生成订单失败
14000 查询订单成功
14001 订单不存在/有误
14002 用户取消支付操作
15000 未输入商品信息
15001 未输入预付ID
15002 请输入Pico支付订单号或商户订单号
NOAUTH 商户无此接口权限
SYSTEMERROR 系统错误
APP_ID_NOT_EXIST APP_ID不存在
MCHID_NOT_EXIST MCHID 不存在
APP_ID_MCHID_NOT_MATCH app_id和mch_id不匹配
LACK_PARAMS 缺少参数
SIGNERROR 签名错误
NO_DATA 没有查询到数据

7.3.3 查询订单

  • 调用方法:void QueryOrder(string orderId)

其中,order为需要查询的订单编号

  • 回调方法:void QueryOrPayCallback(string queryOrPayInfo)

其中,queryOrPayInfo为后台返回订单信息,为一段未经处理的Json串,查询成功时范例如下:

{
"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,//实收金额
"signature":"be3fae4d68fec9c444fde821659bce69",
"buyer_pay_fee":100.00//买家付款的金额
}

查询失败时示范格式如下:

{"code ":"14001","msg":"订单不存在"}

7.3.4 获取用户信息

  • 调用方法:void GetUserAPI()
  • 回调方法:void UserInfoCallback (string userInfo)

其中,userInfo为由后台返回用户信息,是一个未经处理的Json串,查询成功时示范格式如下

{"ret_code":"0000",
"data":{
"aboutme":"",
"birthday":1460476800000,
"phone":"13100000000",
"username":"Admin",
"email":"",
"gender":"male",
"lastname":"",
"openid":"4f3148bdc34d9bca104927729a173b64",
"firstname":"",
"avatar":"http://172.31.83.11/upload/6dd6ee103714e967846c3d38ae48d511",
"signature":"14a25d7219d8dfc91e55f63286ae5c0a",
"country":"China",
"city":""
},
"ret_msg":"调用成功"
}

查询失败示范格式如下:

{
"ret_code":"00003000",
"ret_msg":"签名验证失败"
}

其他ret_code码及ret_msg一览如下:

0000 请求成功
00020000 数据库操作失败
9999 系统错误
00001000 参数错误
00002000 数据解析失败
00003000 签名验证失败
00003001 时间验证失败
00060000 用户未找到
00060001 用户密码错误
00060002 用户登录未知错误
00061000 用户token未找到失败
00061001 用户token验证失败
00061002 用户token未知错误
00070001 应用验证失败
00071001 应用密钥验证失败
00080001 OAUTH_CODE验证失败
00090001 REFRESH_TOKEN验证失败
00100001 ACCESS_TOKEN验证失败
00110001 SCOPE验证失败

7.4 开发者服务端交互

支付完成后,支付系统会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。

对后台通知交互时,如果支付系统收到商户的应答不是成功或超时,则认为通知失败,支付系统会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但不保证通知最终能成功。

同样的通知可能会多次发送给商户系统,商户系统必须能够正确处理重复的通知。推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。

商户服务端需要实现下面的接口,用于接收来自Pico服务器请求,获取Pico支付系统的支付结果和用户信息:

名称 支付结果回调接口
请求类型 POST
请求URL 支付,PayOrder传入的参数notify_url
请求格式 JSON
返回格式 JSON
是否需要登录
请求参数 详见下面“表7.1 支付结果通知中的通知参数”
请求参数示例  
更新说明  

返回参数

参数名 类型及范围 说明
ret_code string 错误代码
ret_msg string 错误信息字符串

返回参数示例

{
"ret_code":"SUCCESS",
"ret_msg":"OK"
}

表7.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

表7.2 返回结果

字段名 变量名 必填 类型 描述
返回状态码 ret_code String SUCCESS/FAIL SUCCESS表示商户接收通知成功并校验成功
返回信息 ret_msg String 返回信息,如非空,为错误原因:签名失败参数格式校验错误

特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,防止数据泄漏导致出现“假通知”,造成资金损失。

签名校验规则是:

1.返回的参数列表,去掉signautre参数,同时添加key=“app_secret”,value=paykey,然后根据key值进行自然排序,多个参数之间用&隔开,最后进行MD5加密

2.用加密后的字符串和获取到的signature进行比较

签名函数如下:

/**
* result :获取的数据的map集合
* paykey :就是开发者平台上的paykey
*/
public static String createSign(Map<String, Object> 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 sign = "";
    for (String string : tmp)
    {
        if (m.get(string) == null)
            continue;
        sign += string + "=" + URLEncoder.encode(m.get(string).toString()
            , "utf-8") + "&";
    }
    if (sign.endsWith("&"))
        sign = sign.substring(0, sign.length() - 1);
    Log.i(TAG, "createSign: " + sign);
    String localSign = MD5.MD5(sign); //5.生成MD5加密后的字符串
    return localSign.equal(sign);//6.和2中的sign进行校验
}