统一下单接口

请求URI https://jits.open.oppomobile.com/jitsopen/api/pay/v1.0/preOrder
HTTP Method POST
说明 CP在游戏中先调用该接口在 OPPO 小游戏平台生成预支付交易单,返回正确的预支付交易后调起支付。
请求
Header
Content-Type application/json
参数 类型 是否必需 说明
appId 字符串 CP游戏在开放平台申请的appId
openId 字符串 用户在oppo的身份标识(登录时传入的token)
timestamp 长整型 时间戳,当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数
sign 字符串 签名算法见下面的描述
deviceInfo 字符串 设备号
model 字符串 机型
ip 字符串 终端IP
productName 字符串 商品名称
productDesc 字符串 商品描述
count 长整型 商品数量
price 长整型 商品价格,以分为单位
currency 字符串 币种,人民币如:CNY
attach 字符串 附加信息
callBackUrl 字符串 回调地址
cpOrderId 字符串 CP自己的订单号
appVersion 字符串 游戏版本
engineVersion 字符串 快应用引擎版本(通过 getSystemInfo 获取 platformVersionCode)
成功响应
HTTP状态码 200
Header 说明
Content-Type application/json
Cache-Control no-store
Pragma no-cache
对象 字段 类型 是否必有 说明
code 字符串 必有
返回码,请求成功的话,返回“200” msg 字符串 可选 提示信息
data appId 字符串 必有 调用接口提交的appId
cpOrderId 字符串 必有 调用接口提交的CP订单号
orderNo 字符串 必有 下单生成的预付订单号
错误响应
HTTP状态码 200
Header 说明
Content-Type application/json
Cache-Control no-store
Pragma no-cache
字段 类型 是否必有 说明
code 字符串 必有 错误码
msg 字符串 必有 错误提示信息
data 对象 可选 返回的数据对象


错误码

code msg 说明
'501' token 失效 需要 CP 通知游戏客户端重新调用登录接口
'502' 参数错误 需要 CP 排查传过来的参数是否正确,响应中有具体的参数错误描述
'503' 签名错误 需要 CP 按照我们的签名规则检查签名生成是否正确
'504' 应用不存在 购买的游戏或应用不存在,需要 CP 校验传过来的 appId 是否正确

签名算法:

第一步 ,设所有发送或者接收到的数据为集合 M(sign 这个参数除外),将集合 M 内非空参数值的参数按照参数名 ASCII 码从小到大排序(字典序),使用 URL 键值对的格式(即 key1=value1&key2=value2)拼接成字符串 stringA。

特别注意以下重要规则:

  1. 参数名 ASCII 码从小到大排序(字典序);
  2. 如果参数的值为空不参与签名;
  3. 参数名区分大小写;
  4. 统一支付接口可能增加字段,验证签名时必须支持增加的扩展字段
  5. timestamp 的有效期是 15 分钟
  6. 私钥签名的时候使用 SHA256WithRSA 算法

参考代码 (Node.js 版):

const port = process.env.PORT || 3000
const ONLINE_API_URL = `http://127.0.0.1:${port}`
const TIMESTAMP = new Date().getTime()
/**
 * @description  1. 参数名ASCII码从小到大排序(字典序)
 * @param {object} obj
 * @returns {object} 返回排序好的对象
 */
function sortByASCII(obj) {
  var arr = new Array()
  var num = 0
  for (var i in obj) {
    arr[num] = i
    num++
  }
  var sortArr = arr.sort()
  var sortObj = {}
  for (var i in sortArr) {
    sortObj[sortArr[i]] = obj[sortArr[i]]
  }
  return sortObj
}

/**
 * @description 2. 使用 URL 键值对的格式(即key1=value1&key2=value2)拼接成字符串
 * @param {string|number|boolean} param
 * @param {*} key
 * @param {*} encode
 * @returns {string} 返回 URL 键值对字符串
 */
function urlEncode(param, key, encode) {
  if (param == null) return ''
  var paramStr = ''
  var t = typeof param
  if (t == 'string' || t == 'number' || t == 'boolean') {
    // paramStr += '&' + key + '=' + ((encode==null||encode) ? encodeURIComponent(param) : param)
    paramStr += '&' + key + '=' + param
  } else {
    for (var i in param) {
      var k =
        key == null
          ? i
          : key + (param instanceof Array ? '[' + i + ']' : '.' + i)
      paramStr += urlEncode(param[i], k, encode)
    }
  }
  return paramStr
}

// 统一下单必填的数据(除了sign),以下数据为测试数据
let dataObject = {
   appId: '', // 游戏上架后分配的 appId
   openId: '', // token
   timestamp: TIMESTAMP, // 建议作为常量存储时间戳,因为后续签名会对时间戳做校验,如:const TIMESTAMP = new Date().getTime()
   productName: '测试',
   productDesc: 'testpay',
   count: 1,
   price: 1,
   currency: 'CNY',
   cpOrderId: '1',
   appVersion: '1.0.0',
   engineVersion: '1045',
   callBackUrl: `${ONLINE_API_URL}/payResult` // 服务器接收平台返回数据的接口回调地址
}
// 获得第一步结果,URL 键值对,如: appId=&appVersion=1.0.0&callBackUrl=http://127.0.0.1:3000/payResult&count=1&cpOrderId=1&currency=CNY&engineVersion=1045&openId=TN_dmhkN3BNWWQ3T0RCNHdIUnlzc2JVc0ljRnZxUit5TWNhODJhWlM3MXpyeUN4ekdaZmhoTXVlU3FyZ0l0c05ieA&price=1&productDesc=testpay&productName=测试&timestamp=1592557604326
let dataString = urlEncode(sortByASCII(payData))

第二步 ,将源串 stringA 使用 RSA 算法(SHA256WithRSA),用私钥进行签名,将签名后的字符数组经过 Base64 编码,最后得到签名字段 sign 的值

签名参考代码(Node.js 版):

const crypto = require('crypto') // node 自带的加密模块

// 3. 将源串stringA使用RSA算法(SHA256WithRSA),用私钥进行签名,将签名后的字符数组经过Base64编码,最后得到签名字段sign的值
function signDataWithCrypto(msg) {
   const privateKey = '' // 输入密钥
   let sign = crypto.createSign('RSA-SHA256')
   sign.update(msg)
   let res = sign.sign(privateKey, 'base64')

   // 以下用公钥认证私钥的签名
   const verify = crypto.createVerify('RSA-SHA256')

   verify.write(msg)
   verify.end()

   // 以下是验证签名操作
   //   const publicKey = '' // 输入公钥
   //   console.log(`验证签名: ${verify.verify(publicKey, res, 'base64')}\n`)
  return res
}

签名参考代码(java 版):

public class Example {

    private static final Logger logger = LoggerFactory.getLogger(Example.class);

    public static String getSignContent() {

        TreeMap<String, String> treeMap = new TreeMap<>();
        treeMap.put("appId", "123456");
        treeMap.put("cpOrderId", "abcde");
        treeMap.put("callBackUrl", "http://xxx.aa.bb");
        treeMap.put("productName", "游戏点卡");
        treeMap.put("price", String.valueOf(200));
        treeMap.put("timestamp", "233123123131");

        Set<String> keys = treeMap.keySet();
        StringBuilder sb = new StringBuilder();
        Iterator<String> iterator = keys.iterator();
        while (iterator.hasNext()) {
            String key = iterator.next();
            sb.append(key).append("=").append(treeMap.get(key)).append("&");
        }

        String signContent = sb.toString().substring(0, sb.length() - 1);

        return signContent;

    }

    public static String sign(String content, String privateKey) {
        String charset = "utf-8";
        try {
            PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(Base64.base64Decode(privateKey));
            KeyFactory keyf = KeyFactory.getInstance("RSA");
            PrivateKey priKey = keyf.generatePrivate(priPKCS8);
            java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");

            signature.initSign(priKey);
            signature.update(content.getBytes(charset));

            byte[] signed = signature.sign();

            return Base64.base64Encode(signed);
        } catch (Exception e) {
            logger.error("签名出错.", e);
        }

        return null;
    }

    public static void main(String[] args) {
        String signContent = getSignContent();
        String priKey = "abcdefghijk";
        String sign = sign(signContent, priKey);
        System.out.println(sign);
    }
}
© 2020 OPPO. All rights reserved.

results matching ""

    No results matching ""