第七章 支付系统¶
Pico支付,是基于Pico账户体系进行的游戏币支付系统,结算方式以现行的Pico公司下的游戏货币单位为准(P币)。如项目需要内购功能,请仔细阅读本章内容;如项目不需要内购功能,请忽略此章。
7.1 准备工作¶
7.1.1 获取支付所用字符串¶
开发者在接入支付SDK时,需要在开发者平台创建应用并获取相应字符串。申请流程如下:
- 登录开发者平台并注册Pico会员(http://dev.picovr.com/)
- 申请成为开发者
开发者分为个人开发者和企业开发者,请根据实际情况进行申请。审核提交后,我们会在3个工作日内进行反馈,请及时查看开发者平台状态。
- 查看商户ID
申请成为开发者后,点击右上角昵称可以查看到开发者ID,该ID将作为支付系统中商户的唯一标志:
图7.1 商户ID
- 获取相应字符串
开发者可以从管理中心进入到创建应用阶段。点击创建应用后,首先选择要发布的平台:
图7.2 选择应用的发布平台
然后进入相应平台完善应用的相关信息:
图7.3 完善应用的相关信息
请重点注意上图标红位置,请谨慎填写应用类型,一经填写是无法进行修改的!游戏类应用如果存在道具内付的情况,我们要求开发者必须采用开发者后台增加商品码的方式进行统一管理。
成功创建应用后,开发者平台会对其分配字符串,包括APP ID、APP KEY、APP Secret:
图7.4 APP ID、APP KEY、APP Secret
再选择“游戏内支付配置”,配置游戏的内购信息:
图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场景如下:
图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进行校验
}