文章目录
- 前言
- 一、IOS内购支付(ios订单生成自己写逻辑即可)
- 1.支付回调票据校验controller
- 1.支付回调票据校验server
- 二、安卓APP支付宝支付
- 1.生成订单返回支付宝字符串(用于app拉起支付宝,这里用的是证书模式)
- 2.生成订单返回支付宝字符串server方法
- 3.支付宝回调处理
- 三、支付宝APP提现(没有授权,直接填用户账户信息)
- 1.支付宝APP提现controller方法
- 2.支付宝APP提现server方法
前言
这里用的是thinkphp5,但是支付这个其他框架或版本也可以参考,只需要改下写法即可
一、IOS内购支付(ios订单生成自己写逻辑即可)
在苹果内购支付中第一步先生成一个内部订单,然后把订单信息给到IOS客户端,内购支付只要是回调校验票据出路,这个票据会返给IOS客户端,让客户端把票据及订单信息一起传过来,注意订单号唯一即可
1.支付回调票据校验controller
public function payResult()
{
//苹果内购的验证收据 orders模型换成自己的订单模型,字段换成自己的订单字段即可
$receipt_data = $this->request->post('receipt-data');//票据
$orderSn = $this->request->post('order_sn');//订单号
// 验证支付状态
$result = (new IosCheckServer())->payMoneyCheck($receipt_data, $orderSn);
if($result['status']){
$this->success('支付成功');
}else{
$this->error($result['message']);
}
}
1.支付回调票据校验server
/**
* @param $receipt_data
* @param $orderSn
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
* @throws \think\exception\PDOException
* ios充值消费燃币支付回调票据校验
*/
public function payMoneyCheck($receipt_data, $orderSn)
{
//苹果内购的验证收据 orders模型换成自己的订单模型,字段换成自己的订单字段即可
$orderinfo = Orders::where(['order_sn' => $orderSn])->find();
if($orderinfo){
if($orderinfo['pay_status'] == Orders::PAY_STATUS_PAID){
return ['status' => false,'message' => '订单已成功支付,请勿重复发起申请'];
}
}else{
return ['status' => false,'message'=>'订单异常'];
}
// 验证支付状态
$result = $this->validate_apple_pay($receipt_data);
if($result['status']){
/*业务逻辑*/
Db::startTrans();
try {
$orderinfo->pay_status = Orders::PAY_STATUS_PAID;
$orderinfo->paid_at = time();
$orderinfo->save();
/*处理自己内部业务逻辑*/
/*处理自己内部业务逻辑*/
/*处理自己内部业务逻辑*/
Db::commit();
} catch (\Exception $e) {
Db::rollback();
return ['status' => false,'message' => '支付失败,请联系管理员'];
}
return ['status' => true,'message' => '支付成功'];
}else{
return ['status' => false,'message' => $result['message']];
}
}
/**
* @param $receipt_data
* @param $orderinfo
* @return array
* @throws \think\exception\PDOException
* 校验票据
*/
public function validate_apple_pay($receipt_data)
{
// 验证参数
if (strlen($receipt_data) < 20){
return ['status' => false, 'message' => '非法参数'];
}
// 请求验证
$html = $this->acurl($receipt_data);
$data = json_decode($html,true);
// 如果是沙盒数据 则验证沙盒模式
if($data['status'] == '21007'){
// 请求验证
$html = $this->acurl($receipt_data, 1);
$data = json_decode($html,true);
$data['sandbox'] = '1';
}
//找出时间最大的数组
$receiptitem = $data['receipt']['in_app'];
//这个是排序的方法 下面回帖出来 苹果会把这个会员往期的订单信息全部返回,需要找出来最近的那一组信息
$item = $this->arraySort($receiptitem,'purchase_date','desc');
//判断一下过期时间 延长会员时间
$orderThird = $item['transaction_id']; //本次订阅的订单号
$orderThirdFirst = $item['original_transaction_id']; //这个是原始订单号
if($orderThird == $orderThirdFirst){ //首次订阅 两个相等
return ['status' => true,'message' => '支付成功'];
}else{
//判断过期时间和当前时间比较 expires_date_ms是毫秒单位
if($item['expires_date_ms']/1000 > time()){
return ['status' => true,'message' => '支付成功'];
}else{
//过期处理 票据失效
return ['code' => false,'message' => '票据失效,请联系管理员'];
}
}
}
/**
* @param $receipt_data
* @param int $sandbox
* @return bool|string
* 数据校验
*/
public function acurl($receipt_data, $sandbox = 0)
{
/**
* 21000 App Store不能读取你提供的JSON对象
* 21002 receipt-data域的数据有问题
* 21003 receipt无法通过验证
* 21004 提供的shared secret不匹配你账号中的shared secret
* 21005 receipt服务器当前不可用
* 21006 receipt合法,但是订阅已过期。服务器接收到这个状态码时,receipt数据仍然会解码并一起发送
* 21007 receipt是Sandbox receipt,但却发送至生产系统的验证服务
* 21008 receipt是生产receipt,但却发送至Sandbox环境的验证服务
*/
//小票信息
$POSTFIELDS = array("receipt-data" => $receipt_data);
$POSTFIELDS = json_encode($POSTFIELDS);
$url_buy = "https://buy.itunes.apple.com/verifyReceipt";//正式购买地址
$url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";//沙盒购买地址
$url = $sandbox ? $url_sandbox : $url_buy;
//curl - post
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $POSTFIELDS);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
/**
* @param $arr
* @param $key
* @param string $type
* @return mixed
* 查找最新数据
*/
public static function arraySort($arr,$key,$type='asc')
{
$keyArr = []; // 初始化存放数组将要排序的字段值
foreach ($arr as $k => $v) {
$keyArr[$k] = $v[$key]; // 循环获取到将要排序的字段值
}
if ($type == 'asc') {
asort($keyArr); // 排序方式,将一维数组进行相应排序
} else {
arsort($keyArr);
}
foreach ($keyArr as $k => $v) {
$newArray[$k] = $arr[$k]; // 循环将配置的值放入响应的下标下
}
$newArray = array_merge($newArray); // 重置下标
return $newArray[0]; // 数据返回
}
二、安卓APP支付宝支付
前期准备工作:
- 获取 App ID 和 商户私钥
- 申请并下载支付宝公钥证书和应用证书
- 如果站点有ssl更好,没有的话可以下载一个
cacert.pem
证书放到配置文件对应的地址里面,https://curl.se/docs/caextract.html - 完成之后修改一下
php.ini
的配置curl.cainfo = /etc/pki/tls/certs/cacert.pem
这里的地址时我按照原配置文件直接用的,放其他地方的话只要保证配置文件里面一致即可,支付宝的证书我是直接放项目里面的,引用的时候保证路径没问题即可
1.生成订单返回支付宝字符串(用于app拉起支付宝,这里用的是证书模式)
controller方法(订单逻辑部分可以自己处理)
/**
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
* 会员订单创建 - android
*/
public function memberOrderByAndroid()
{
/**
* 生成订单数据
*/
$result = [];
Db::startTrans();
try {
$model = new Orders($arr);
$res = $model->save($arr);
if ($res === false){
Db::rollback();
$this->error('订单创建失败');
}
$server = new AlipayServer();
//订单号,金额,回调地址
$res = $server->payMemberOrder($model->order_sn, number_format($model->amount, 2), '****/api/v1/member/notify');
if ($res['code']){
$result['orderStr'] = $res['data'];
}
Db::commit();
}catch (\Exception $exception){
Db::rollback();
$this->error('订单创建失败');
}
$this->success('操作成功', $result);
}
2.生成订单返回支付宝字符串server方法
/**
* @param $out_trade_no 订单号
* @param $total_amount 金额
* @param $notify_url 回调地址
* @return array
* 燃币充值 - 安卓 - 支付宝
*/
public function payMemberOrder($out_trade_no, $total_amount, $notify_url)
{
require_once __PUBLIC__ . '/../vendor/alipaysdk/openapi/v2/aop/AopCertClient.php';
require_once __PUBLIC__ . '/../vendor/alipaysdk/openapi/v2/aop/request/AlipayTradeAppPayRequest.php';
/** 初始化 **/
$aop = new \AopCertClient;
/** 支付宝网关 **/
$aop->gatewayUrl = "https://openapi.alipay.com/gateway.do";
/** 应用id,如何获取请参考:https://opensupport.alipay.com/support/helpcenter/190/201602493024 **/
$aop->appId = config('alipay.app_id');
/** 密钥格式为pkcs1,如何获取私钥请参考:https://opensupport.alipay.com/support/helpcenter/207/201602469554 **/
$aop->rsaPrivateKey = trim(config('alipay.private_key'));
/** 应用公钥证书路径,下载后保存位置的绝对路径 **/
$appCertPath = __PUBLIC__ . '/cert/appCertPublicKey_2021004170664201.crt';
/** 支付宝公钥证书路径,下载后保存位置的绝对路径 **/
$alipayCertPath = __PUBLIC__ . '/cert/alipayCertPublicKey_RSA2.crt';
/** 支付宝根证书路径,下载后保存位置的绝对路径 **/
$rootCertPath = __PUBLIC__ . '/cert/alipayRootCert.crt';
/** 设置签名类型 **/
$aop->signType= "RSA2";
/** 设置请求格式,固定值json **/
$aop->format = "json";
/** 设置编码格式 **/
$aop->charset= "utf-8";
/** 调用getPublicKey从支付宝公钥证书中提取公钥 **/
$aop->alipayrsaPublicKey = $aop->getPublicKey($alipayCertPath);
/** 是否校验自动下载的支付宝公钥证书,如果开启校验要保证支付宝根证书在有效期内 **/
$aop->isCheckAlipayPublicCert = true;
/** 调用getCertSN获取证书序列号 **/
$aop->appCertSN = $aop->getCertSN($appCertPath);
/** 调用getRootCertSN获取支付宝根证书序列号 **/
$aop->alipayRootCertSN = $aop->getRootCertSN($rootCertPath);
/** 实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.trade.app.pay **/
$request = new \AlipayTradeAppPayRequest();
/** 设置业务参数 **/
$request->setBizContent("{" .
/** 商户订单号,商户自定义,需保证在商户端不重复,如:20200612000001 **/
"\"out_trade_no\":\"{$out_trade_no}\"," .
/** 销售产品码,固定值:QUICK_MSECURITY_PAY **/
"\"product_code\":\"QUICK_MSECURITY_PAY\"," .
/** 订单金额,精确到小数点后两位 **/
"\"total_amount\":\"{$total_amount}\"," .
/** 订单标题(可自定义) **/
"\"subject\":\"会员充值\"," .
/** 订单描述(可自定义) **/
"\"body\":\"会员充值\"" .
"}");
/** 异步通知地址,以http或者https开头的,商户外网可以post访问的异步地址,用于接收支付宝返回的支付结果,如果未收到该通知可参考该文档进行确认:https://opensupport.alipay.com/support/helpcenter/193/201602475759 **/
$request->setNotifyUrl($notify_url);
try {
/** 调用SDK生成支付链接,可在浏览器打开链接进入支付页面 **/
$result = $aop->sdkExecute($request);
$result = str_replace('&', '&', htmlspecialchars($result));
/** response.getBody()打印结果就是orderString,可以直接给客户端请求,无需再做处理。如果传值客户端失败,可根据返回错误信息到该文档寻找排查方案:https://opensupport.alipay.com/support/helpcenter/89 **/
return ['code' => 1, 'data' => $result, 'msg' => ''];
} catch (\Exception $e) {
return ['code' => 0, 'data' => $e->getMessage(), 'msg' => ''];
}
}
返回APP客户端示例:
3.支付宝回调处理
/**
* @return string
* 安卓充值支付回调
*/
public function notify()
{
if(isset($_POST['out_trade_no']) && empty($order_sn)){
$order_sn = $_POST['out_trade_no'];
}
Db::startTrans();
try {
$order = Orders::where('order_sn', $order_sn)->find();
if(!$order){
Db::rollback();
return 'false';
}
$order->save(['pay_status' => Orders::PAY_STATUS_PAID]);
//处理自己的业务逻辑
Db::commit();
}catch (\Exception $exception){
Db::rollback();
return 'false';
}
return 'success';
}
三、支付宝APP提现(没有授权,直接填用户账户信息)
这里设计的是让用户输入支付宝账户和绑定的用户名,授权的话可以参考支付宝文档https://opendocs.alipay.com/common/02nk10?pathHash=a7475006
1.支付宝APP提现controller方法
/**
* 提现到支付宝
*/
public function alipayToUser()
{
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('Hd20240306');
$lockKey = 'box_lock_' . $this->user->id;
$lockValue = uniqid(); // 生成唯一值,用于解锁验证
$expireTime = 10; // 锁的超时时间,秒
// 尝试获取锁
$isLocked = $redis->setnx($lockKey, $lockValue);
if ($isLocked) {
$redis->expire($lockKey, $expireTime); // 设置锁的过期时间,以避免死锁
Db::startTrans();
try {
$out_biz_no = $this->request->post('out_biz_no');//商户端的唯一订单号
$payee_type = $this->request->post('payee_type');//收款方账户类型(支付宝账户)
$payee_account = $this->request->post('payee_account');//收款方支付宝账号
$name = $this->request->post('name');//收款方支付宝账号
if (!$name || $name == ''){
throw new Exception('请输入支付宝绑定的真实用户名');
}
$cash = (new Cash())->where('order_sn', $out_biz_no)->find();
if (!$cash){
throw new Exception('订单有误');
}
$amount = number_format($cash->amount, 2);//转账金额
//用户 -
$num = number_format($amount * config('money.other'), 2);
$memo = '提现'.$num.'燃币';
$user_info = \app\common\model\User::where('id', $this->user->id)->find();
if ($user_info->money < number_format($cash->amount * config('money.other'), 2)){
throw new Exception('余额不足');
}
//业务逻辑处理
//提现
(new AlipayServer())->payToUser($out_biz_no, $payee_account, $amount, $payee_type, $name);
//订单状态变更
$cash->payee_account = $payee_account;
$cash->real_name = $name;
$cash->pay_status = 2;
$cash->save();
Db::commit();
}catch (\Exception $exception){
Db::rollback();
Log::info('alipayToUser-to' . $exception->getMessage());
$this->error($exception->getMessage());
}
$this->success('提现成功');
} else {
$this->error('操作过于频繁,请稍后再试');
}
}
2.支付宝APP提现server方法
/**
* @param $outBizNo - 订单号
* @param $payeeAccount - 账户
* @param $amount - 金额
* @param $payeeType - 账户类型
* @param $name - 账户绑定的实名信息
* @param string $remark - 备注
*/
public function payToUser($outBizNo, $payeeAccount, $amount, $payeeType, $name, $remark = '提现')
{
require_once __PUBLIC__ . '/../vendor/alipaysdk/openapi/v3/src/Util/AlipayConfigUtil.php';
require_once __PUBLIC__ . '/../vendor/alipaysdk/openapi/v3/src/Util/AlipayLogger.php';
require_once __PUBLIC__ . '/../vendor/alipaysdk/openapi/v3/src/Api/AlipayFundTransUniApi.php';
require_once __PUBLIC__ . '/../vendor/alipaysdk/openapi/v3/src/Model/AlipayFundTransUniTransferModel.php';
require_once __PUBLIC__ . '/../vendor/alipaysdk/openapi/v3/src/Model/Participant.php';
// 初始化SDK
$alipayConfigUtil = new AlipayConfigUtil($this->getPayConfig());
// 构造请求参数以调用接口
$apiInstance = new AlipayFundTransUniApi(
new Client()
);
// 设置AlipayConfigUtil
$apiInstance->setAlipayConfigUtil($alipayConfigUtil);
$data = new AlipayFundTransUniTransferModel();
// 设置商家侧唯一订单号
$data->setOutBizNo($outBizNo);//自己定义的转账外部单号
// 设置订单总金额
$data->setTransAmount($amount);
// 设置描述特定的业务场景
$data->setBizScene("DIRECT_TRANSFER");
// 设置业务产品码
$data->setProductCode("TRANS_ACCOUNT_NO_PWD");
// 设置转账业务的标题
$data->setOrderTitle($remark);
// 设置收款方信息
$payeeInfo = new Participant();
$payeeInfo->setIdentity($payeeAccount);
$payeeInfo->setIdentityType($payeeType);
$payeeInfo->setName($name);
$data->setPayeeInfo($payeeInfo);
// 设置业务备注
$data->setRemark($remark);
// 设置转账业务请求的扩展参数
$data->setBusinessParams("{\"payer_show_name_use_alias\":\"true\"}");
try {
//屏蔽日志打印
AlipayLogger::setNeedEnableLogger(false);
$result = $apiInstance->transfer($data);
$result = json_decode($result, true);
if ($result['status'] == 'SUCCESS'){
return true;
}else{
throw new Exception($result['message']);
}
} catch (ApiException $e) {
throw new Exception('操作失败' . $e->getMessage());
}
}