目录
前提条件
字节跳动:
- API 支付支持版本(不拉起收银台,直接拉起微信/支付宝):今日头条(iOS & Android)7.4.3+版本;抖音 iOS 9.1.0+版本 / Android 8.7.0+版本
- 在字节跳动开放平台 -> 基础设置 -> 企业认证完成 -> 将所有信息填写好
- 开发管理->开发设置 -> 小程序Key&服务器域名配置好
- 在字节跳动开放平台 -> 功能管理 -> 支付 -> 收银台支付 完成信息填写(图片真的找不到原图抱歉) 填写之后 这个样儿
- 提交一个测试版(用于支付宝APP签约)
支付宝:
- 注册支付宝开放平台账号并完成企业认证
- 在开发者中心控制台->我的应用中创建网页&移动应用->支付接入; 填入名称与抖音小程序相同->选择网页应用->url无所谓
- 在能力列表中添加能力选择APP支付; 选择确认后需要签约, 此处签约的APP名称一定要填写测试版字节跳动小程序的名称(必须), 其他的看着填写(最好有截图放到word中上传)
- 签约完成后需要在应用信息中设置接口加签方式(选RSA2接口加密方式自己百度很简单) 保存下载好任何可以下载的文件; 授权回调地址用来接收支付宝验签用暂时不管他
正文
工作中用到抖音小程序与支付宝开发 记录一次摸(cai)索(keng)过程 !
首先要梳理下思路: 要区分为两个部分{ 支付宝 , 字节跳动 } 本文需要通过 字节跳动的 tt.pay 前端方法 吊起支付宝支付
一定要看好前提条件, 不然很容易乱
代码流程:
大概过程: 前端请求服务端接口, 服务端返回orderInfo
服务端生成的orderInfo过程:
1、获取字节跳动签名(sign)
2、获取支付宝的alipay_url
3、组合参数 orderInfo 给前端
详细代码
一、获取字节跳动订单
- 抖音用户授权登录(需要Openid)
- 获取字节跳动订单 , 此处需要创建自定义订单信息 (随便写除了openid) 我的代码 -----------获取字节跳动订单-----------
//------------------自定义订单信息-------------------------- $data = [ 'out_order_no' => date('Ymd') . $time, //随便搞个订单号 'openid' => $userinfo['openid'], //抖音用户openid 'fee' => '1', //金额 单位:分!分!分! 'cid' => $cid, 'time' => $time, 'body' => '123', //支付的内容(支付宝) 'subject' => '456', //支付的标题(支付宝) //body 和 subject 刚开始先用数字(中文会有其他问题) ]; //------------------自定义订单信息-------------------------- //TP5框架 fastadmin //------------------组合请求sign信息-------------------------- //↓↓↓获取用户真实IP $risk_info = request()->ip(); //↓↓↓头条支付分配给业务方的ID(不是头条小程序的appid) $payload['app_id'] = $this->config['tt_pay_app_id']; //↓↓↓头条支付分配给业务方的支付秘钥 $app_secret = $this->config['tt_pay_app_secret']; //↓↓↓请求使用的编码格式 $payload['charset'] = "utf-8"; //↓↓↓接口名称 $payload['method'] = "tp.trade.create"; //↓↓↓发送请求的时间 $payload['timestamp'] = $time; // 请求参数的集合 json $biz_content = [ //商户订单号 "out_order_no" => $data['out_order_no'], //唯一标识用户open_id "uid" => $data['openid'], //金额,分为单位,应传整型 "total_amount" => $data['fee'], //商户订单名称 "subject" => $data['subject'], //商户订单详情 "body" => $data['body'], //头条支付分配给业务方的商户号 "merchant_id" => $this->config['merchant_id'], //货币种类 "currency" => "CNY", //下单时间戳 "trade_time" => $time, //订单有效时间(此处测试 时间留的长) 单位:秒 "valid_time" => "3000", //服务器异步通知地址尽量https 没试过http "notify_url" => 'https://**********.com/api/pay/verificationSign', //用户的真实ip 一定要json序列化 "risk_info" => json_encode(['ip' => $risk_info]), ]; $payload['biz_content'] = json_encode($biz_content); //字节跳动采用的是MD5加密 $payload['sign_type'] = "MD5"; $payload['format'] = "json"; $payload['version'] = "1.0"; //这里写了一个签名的方法, 千万别乱, 此处签名用来请求的, 与其他签名没有任何关联;(共3个签名) $stringToBeSigned = $this->getSignContent($payload, $payload['charset']); $payload["sign"] = md5($stringToBeSigned . $app_secret); $url = "https://tp-pay.snssdk.com/gateway"; // 请求地址正式环境 $result = $this->curl($url, $payload); /*返回示例: {"response":{"code":"10000","msg":"Success","trade_no":"SP2020081509594510822988870791"},"sign":"E7RRJSJCVAhA4DMyMPr/Q1IOc1RKpyQNikl9l8b3ObW6dAYzep7rK6wY5YVjSubhmINsI0iWb/cu+YCqp1D+amifkXh4nX2JG3D0xgi2eWJrTv3Ou27zuEPbEb5y10SBG1f4QCYoa7r2upmOL5xbjY6kG5iDPjiS4JIthsojR5Q="}*/ $result = json_decode($result, true); //------------------组合请求sign信息--------------------------
------------------------------------------------------------------获取字节跳动签名------------------------------------------------------------------
-
返回值{"response":{"code":"10000","msg":"Success","trade_no":"SP2020081509594510822988870791"},"sign":"E7RRJSJCVAhA4DMyMPr/Q1IOc1RKpyQNikl9l8b3ObW6dAYzep7rK6wY5YVjSubhmINsI0iWb/cu+YCqp1D+amifkXh4nX2JG3D0xgi2eWJrTv3Ou27zuEPbEb5y10SBG1f4QCYoa7r2upmOL5xbjY6kG5iDPjiS4JIthsojR5Q="} trade_no(订单号)留着用哈
二、获取支付宝alipay_url参数
- 我用的是支付宝sdk中证书方法生成 alipay_url
- 先下载支付宝sdk 点我下载 tp5将aop文件夹放到vendor ; 生成支付宝alipay_url 上代码:
//阿里url证书 public function aliUrlZhengshu($data) { //需要在AopCertClient.php文件中加入 //namespace app\api\controller; //use think\Exception; //引入文件 用来实例化 require_once VENDOR_PATH . 'aop/AopCertClient.php'; $c = new AopCertClient; $appCertPath = VENDOR_PATH . 'aop/crt/appCertPublicKey.crt';//应用证书路径(要确保证书文件可读) $alipayCertPath = VENDOR_PATH . 'aop/crt/alipayCertPublicKey_RSA2.crt';//支付宝公钥证书路径(要确保证书文件可读) $rootCertPath = VENDOR_PATH . 'aop/crt/alipayRootCert.crt';//支付宝根证书路径(要确保证书文件可读) $c->gatewayUrl = "https://openapi.alipay.com/gateway.do"; $c->appId = $this->config['ali_app_app_id']; $c->rsaPrivateKey = $this->config['ali_app_rsa_pri_key']; $c->format = "json"; $c->charset = "UTF-8"; $c->signType = "RSA2"; //调用getPublicKey从支付宝公钥证书中提取公钥 $c->alipayrsaPublicKey = $c->getPublicKey($alipayCertPath); //是否校验自动下载的支付宝公钥证书,如果开启校验要保证支付宝根证书在有效期内 $c->isCheckAlipayPublicCert = true; //调用getCertSN获取证书序列号 $c->appCertSN = $c->getCertSN($appCertPath); //调用getRootCertSN获取支付宝根证书序列号 $c->alipayRootCertSN = $c->getRootCertSN($rootCertPath); //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.open.public.template.message.industry.modify //文件中加入 namespace app\api\controller; 即可 require_once VENDOR_PATH . 'aop/request/AlipayTradeAppPayRequest.php'; $request = new AlipayTradeAppPayRequest(); //此次只是参数展示,未进行字符串转义,实际情况下请转义 $request->setBizContent($this->getcontent($data)); $response = $c->sdkExecute($request); return $response; }
-
一定有人发现 getcontent 方法不存在 嘿嘿 上代码:
//业务数据
public function getcontent($data)
{
$biz_content = array(
'out_trade_no' => $data['out_order_no'], //之前咱们自定义的订单号 out_trade_no
'product_code' => 'QUICK_MSECURITY_PAY', //定死了 别动
'total_amount' => $data['fee'] / 100, //单位换算
'subject' => $data['subject'], //之前定好的 标题
'method' => 'alipay.trade.app.pay', //定死了 别动
'notify_url' => 'https://******.com/api/pay/verificationSign',//回调接口需要配置到支付宝
'body' => $data['body'], //之前定好的 内容
'timeout_express' => '1m', //支付超时时间 文档去支付宝搜索咯 1m-15d
);
return json_encode($biz_content);
}
三、开始生成orderInfo 给前端吊起支付宝咯
$res = 获取字节跳动的订单号(一、获取字节跳动订单)
if ($res['result']['response']['code'] != 10000) {
$this->error('错误', $res['result']['response']['code']);
} elseif ($res) {
$out_order_no = $res['out_order_no'];//自定义的订单号
$aliurl = $this->aliUrlZhengshu($res);//获取 alipay_url
$arr = [
'merchant_id' => $this->config['merchant_id'],//字节跳动商户号 前提条件->字节跳动->4 完成填写后
'app_id' => $this->config['tt_pay_app_id'],//字节跳动APPID 前提条件->字节跳动->4 完成填写后
'sign_type' => 'MD5',//定死的别动!!!
'timestamp' => strval($res['time']),//需要为字符串类型的时间戳
'version' => '2.0',//定死的别动!!!
'trade_type' => 'H5',//定死的别动!!!
'product_code' => 'pay',//定死的别动!!!
'payment_type' => 'direct',//定死的别动!!!
'out_order_no' => strval($out_order_no),//自定义的订单号
'uid' => $this->openid,// 用户的openid 登录后可以获取到
'total_amount' => $res['fee'],//金额 这里单位:分
'currency' => 'CNY',//定死的别动!
'trade_no' => $res['trade_no'],//刚刚获取的字节跳动订单 忘了往上找找
'subject' => $res['subject'],//之前定好的标题
'body' => $res['body'],//之前定好的内容
'trade_time' => strval($res['time']),//一定要和 上面的 timestamp 字段相同
'valid_time' => '3000',//测试留的时间长
'notify_url' => 'https://tp-pay.snssdk.com/paycallback',//定死的别动!!!
'alipay_url' => $aliurl,//刚刚生成的 记得不
];
$stringToBeSigned = $this->getSignContent($arr);//这里待签名处理.方法下面
$sign = md5($stringToBeSigned . $this->config['tt_pay_app_secret']);//这生成签名咯, 不要乱, 签名好多的
//这两个字段的写入原因: 在待签名字符串 getSignContent 方法中不能有 sign和risk_info 所以在生成签名($sign)之后写入到里面
$arr['sign'] = $sign;
$arr['risk_info'] = json_encode(['ip' => request()->ip()]);
//这两个字段的写入原因: 在待签名字符串 getSignContent 方法中不能有 sign和risk_info 所以在生成签名($sign)之后写入到里面
$res = htmlspecialchars_decode(json_encode($arr));//这里html的编译解析, 防止html编译
$this->success('返回orderinfo', $res);
}
这里是处理待签名的方法咯 一共三个别落下:
/**
* 签名处理
* @param $params
* @param $charset
* @return string
*/
public function getSignContent($params, $charset = 'utf-8')
{
ksort($params);
$stringToBeSigned = "";
$i = 0;
foreach ($params as $k => $v) {
if (false === $this->checkEmpty($v) && "@" != substr($v, 0, 1)) {
// 转换成目标字符集
$v = $this->characet($v, $charset);
if ($i == 0) {
$stringToBeSigned .= "$k" . "=" . "$v";
} else {
$stringToBeSigned .= "&" . "$k" . "=" . "$v";
}
$i++;
}
}
unset ($k, $v);
return $stringToBeSigned;
}
/**
* 校验$value是否非空
* @param $value
* @return boolean;
* if not set ,return true;
* if is null , return true;
**/
public function checkEmpty($value)
{
if (!isset($value))
return true;
if ($value === null)
return true;
if (trim($value) === "")
return true;
return false;
}
/**
* 转换字符集编码
* @param $data
* @param $targetCharset
* @return string
*/
public function characet($data, $targetCharset)
{
if (!empty($data)) {
$fileType = "UTF-8";
if (strcasecmp($fileType, $targetCharset) != 0) {
$data = mb_convert_encoding($data, $targetCharset, $fileType);
}
}
return $data;
}
总结哈 keke
$res 就是我们要的orderInfo 处理好之后是个json串 所以前端取到数据后需要json反序列化一下.
前端uniapp写的.
可以分享下我们前端 重要的是 错误代码(错误代码:CD0015 CD0025 这个是字节跳动的错误)
_this.getOrderPayInfo().then(e => { //调用后端接口得到orderInfo
let currenttime = Math.round(new Date() / 1000);
let order = JSON.parse(e.data.data);
if(e.data){
uni.requestPayment({
provider: 'toutiao',
service: 4, // 不拉起字节跳动小程序收银台
_debug: 1,
payChannel: {
default_pay_channel: 'alipay' // wx || alipay
},
orderInfo: order, // 订单信息
getOrderStatus(res) {
let { out_order_no } = res;
return new Promise(function (resolve, reject) {
})
},
success: (res) => {
console.log("成功");
console.log(res);
},
fail: (res) => {
console.log("失败");
console.log(res); // 错误代码:CD0015 CD0025
}
})
// _this.loadModal = false;
}
})