最近项目中需要使用支付宝的周期扣款,整理一下各种封装方法
APP支付(服务端)
/******************************************************
* 调用方法
******************************************************/
function test_pay()
{
$isSubscribe = 1;
$price = 0.01;
$detail = $body = "会员充值";
$orderSn = date("mdHis") . mt_rand(2000, 8000);
$hostApi = config('host_api');
if (!$isSubscribe) {
// 一次性支付
$bizSontent = [
"timeout_express" => "30m",
"product_code" => "QUICK_MSECURITY_PAY",
"total_amount" => $price,
"subject" => $detail,
"body" => $body,
"out_trade_no" => $orderSn,
];
} else {
// 订阅
// 参见下文sign_scene参数说明 https://opendocs.alipay.com/open/08bg92?pathHash=b655de17
$bizSontent = [
"out_trade_no" => $orderSn,
"total_amount" => $price, //订单总金额,首次支付的金额,不算在周期扣总金额里。
"subject" => $detail,
"body" => $body,
"product_code" => "CYCLE_PAY_AUTH", // CYCLE_PAY_AUTH
"timeout_express" => "90m",
//商家扣款协议信息
"agreement_sign_params" => [
"product_code" => "GENERAL_WITHHOLDING",//收单产品码固定为GENERAL_WITHHOLDING
"personal_product_code" => "CYCLE_PAY_AUTH_P", //个人签约产品码固定为CYCLE_PAY_AUTH_P
"sign_scene" => "INDUSTRY|DEFAULT_SCENE",//协议签约场景,参见下文sign_scene参数说明 数字传媒行业
"external_agreement_no" => $orderSn,//商户签约号,代扣协议中用户的唯一签约号
// "sign_notify_url" => $hostApi . "/v1/notify/alipay_sub",//签约成功异步通知地址
"access_params" => [ //签约接入的方式
"channel" => "ALIPAYAPP"
],
// 签约规则
"period_rule_params" => [
"period_type" => "DAY",//周期类型枚举值为 DAY 和 MONTH
"period" => 9999,//周期数,与 period_type 组合使用确定扣款周期 // 扣款周期类型period_type参数为DAY时,扣款周期period参数不得小于7。
"execute_time" => date('Y-m-d'),//用户签约后,下一次使用代扣支付扣款的时间
"single_amount" => $price,//单次扣款最大金额
// "total_amount" => "0.02",//周期内扣允许扣款的总金额,单位为元
// "total_payments" => "2"//总扣款次数。
]
],
];
}
$notiyUrl = $hostApi . '/notify/alipay';
list($result, $responseNode) = PayUtil::pay($bizSontent, $notiyUrl);
dump($result);
}
异步回调
// 支付宝异步通知
function alipay()
{
$verify = PayUtil::notifyVerify($_POST);
$orderSn = addslashes($_POST['out_trade_no'] ?? ''); //商户订单号
$trade_no = addslashes($_POST['trade_no'] ?? ''); //支付宝交易号
$trade_status = trim(addslashes($_POST['trade_status'] ?? ''));
if (!empty($trade_status)) {
// 支付回调
LogHelperUtil::outLog('alipay_notify_' . $trade_status, json_encode(['post' => $_POST, 'verify' => $verify]), 'alipay_notify');
if (empty($orderSn)) {
return "fail";
}
Db::name('order_log_alipay')->insert([
'order_sn' => $orderSn, 'trans_id' => $trade_no, 'create_time' => date('Y-m-d H:i:s'),
'content' => json_encode($_POST)
]);
$orderInfo = OrderModel::get_info(['order_sn' => $orderSn, 'status' => 0, 'pay_type' => 1]);
if (!empty($orderInfo) && $trade_status == 'TRADE_SUCCESS') {
$this->paySuccess($orderInfo, $trade_no);
return "success";
}
if ($trade_status == 'TRADE_CLOSED') {
// 退款
return "success";
}
return "fail";
}
/***************************************************************
* 订阅回调
* 重要参数说明
* status:协议状态,枚举支持。 NORMAL:正常 UNSIGN:解约。
* external_agreement_no:标示用户的唯一签约协议号,商家自定义。仅签约接口传入时返回
* agreement_no:支付宝系统中用以唯一标识用户签约记录的编号。
* notify_type:异步通知类型,枚举支持。 dut_user_sign:当 status = NORMAL 表示签约成功。 dut_user_unsign:当 status = UNSIGN 表示解约成功。
* sign_scene:签约协议场景。
* personal_product_code:协议产品码。
* alipay_user_id:用户的支付宝账号对应的支付宝唯一用户号。
**********************************************************************/
$status = $_POST['status'] ?? ''; // 协议状态,枚举支持。 NORMAL:正常 UNSIGN:解约。
$notifyType = $_POST['notify_type'] ?? ''; // 异步通知类型,枚举支持。 dut_user_sign:当 status = NORMAL 表示签约成功。 dut_user_unsign:当 status = UNSIGN 表示解约成功。
$orderSn = $_POST['external_agreement_no'] ?? ''; // 自定义
$agreementNo = $_POST['agreement_no'] ?? '';
Db::name('order_log_alipay_sub')->insert([
'order_sn' => $orderSn, 'trans_id' => $agreementNo, 'create_time' => date('Y-m-d H:i:s'),
'content' => json_encode($_POST)
]);
LogHelperUtil::outLog('alipay_notify_sub_' . $status, json_encode(['post' => $_POST, 'verify' => $verify]), 'alipay_notify_sub');
if (empty($agreementNo)) {
return "fail";
}
$orderInfo = OrderSubscribeModel::get_info(['order_sn' => $orderSn, 'pay_type' => 1]);
$oid = intval($orderInfo['id'] ?? 0); // 订单ID
if ($status == 'UNSIGN' && $notifyType == 'dut_user_unsign') {
// 解约
$response = OrderSubscribeModel::update_data(['id' => $oid], [
'status' => 2, // 0-签约中 1-已订阅 2-已退订
'contract_del_date' => date('Y-m-d H:i:s'), // 解约时间
]);
return "success";
}
// 0-签约中 1-已订阅 2-已退订
if (!empty($orderInfo) && $status == 'NORMAL' && $notifyType == 'dut_user_sign') {
// 签约成功
// 记录签约成功的订单
$response = OrderSubscribeModel::update_data(['id' => $oid], [
'status' => 1, // 0-签约中 1-已订阅 2-已退订
'agreement_no' => $agreementNo, // 签约号
'contract_date' => date('Y-m-d H:i:s'), // 签约时间
]);
if ($response) {
$toDay = date('Y-m-d');
$kontDay = date('Y-m-d', strtotime("+1 day"));
$nextPay = $orderInfo['next_pay'] ?? '';
if ($nextPay == $kontDay || $toDay == $nextPay) {
$notiyUrl = config('host_api') . '/notify/alipay_sub_knot?order_sn=' . $orderSn;
send_socket_time_task($notiyUrl, 300);
LogHelperUtil::outLog('alipay_notify_sub_' . $status, "已加入队列" . $notiyUrl, 'alipay_notify_sub');
}
}
return "success";
}
return "fail";
}
周期扣款操作(定时任务)
// 支付宝订阅自动扣款
function alipay_sub_knot()
{
$orderSnSub = trim(addslashes($_GET['order_sn'] ?? ''));
if (empty($orderSnSub)) {
return 'fail-sn';
}
// 0-签约中 1-已订阅 2-已退订
$orderInfo = OrderSubscribeModel::get_info(['order_sn' => $orderSnSub, 'status' => 1]);
if (empty($orderInfo)) {
return 'fail-order';
}
$type = $orderInfo['type'] ?? 0;
$uid = $orderInfo['uid'] ?? 0;
$priceRenew = $orderInfo['price_renew'] ?? 0; // 续订价格
$agreementNo = $orderInfo['agreement_no'] ?? ''; // 签约号
if (empty($agreementNo)) {
return 'fail-order';
}
LogHelperUtil::outLog('alipay_sub_knot', json_encode(['orderSnSub' => $orderSnSub, 'order' => $orderInfo]), 'alipay_sub_knot');
$detail = '周期扣款';
$orderSn = "DYK{
$type}U" . $uid . date("mdHis") . mt_rand(2000, 8000);
list($result) = PayUtil::pay_sub_knot($orderSn, $detail, $priceRenew, $agreementNo);
$resultCode = $result['code'] ?? 0;
if ($resultCode != 10000) {
LogHelperUtil::outLog('alipay_sub_knot_fail', json_encode(['orderSnSub' => $orderSnSub, 'order' => $orderInfo, 'result' => $result]), 'alipay_sub_knot');
return "fail-result";
}
// 扣款成功,写入订单逻辑
return "success";
}
其他一下方法
function alipay_test()
{
$agreementNo = '20235529965404462663';
list($res,$code) = PayUtil::agreementQuery($agreementNo); // 签约查询
dump($res);
// 周期性扣款协议执行计划修改
list($res,$code) = PayUtil::agreementModify($agreementNo,'2023-11-01','购买了半年包月');
dump($res);
// 解约
list($res,$code) = PayUtil::agreementUnsign($agreementNo);
dump($res);
return '';
}
PayUtil 封装方法文件
<?php
namespace pay\alipay;
class PayUtil
{
/**
* 发起支付
* 支付后签约场景:https://opendocs.alipay.com/pre-open/08bpuc?pathHash=a572b7a7
* @param $bizSontent
* @param $notiyUrl
* @return array|string[]
* @author wzb
* @date 2023/7/22 9:50
*/
static function pay($bizSontent = [], $notiyUrl = '')
{
$alipayConfig = config('alipay_config'); // 配置
if (empty($bizSontent) || empty($alipayConfig)) {
return ['', ''];
}
$aop = new AopClient();
$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = $alipayConfig['appid'] ?? '';
$aop->rsaPrivateKey = $alipayConfig['rsaPrivateKey'] ?? '';
$aop->alipayrsaPublicKey = $alipayConfig['alipayrsaPublicKey'] ?? '';
$aop->apiVersion = '1.0';
$aop->signType = 'RSA2';
$aop->postCharset = 'utf-8';
$aop->format = 'json';
$request = new \AlipayTradeAppPayRequest();
$request->setNotifyUrl($notiyUrl);
$request->setBizContent(json_encode($bizSontent));
$result = $aop->sdkExecute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
return [$result, $responseNode];
}
/**
* 查询交易信息
*
* @param $outTradeNo
* @return string[]|void
* @author wzb
* @date 2023/7/22 10:03
*/
static function queryOrder($outTradeNo = '')
{
$bizSontent = [
"out_trade_no" => $outTradeNo,
// "trade_no"=>"DJ4U2407211930124801",
// "query_options"=>[
// "trade_settle_info", // 交易结算信息
// ]
];
$alipayConfig = config('alipay_config'); // 配置
if (empty($alipayConfig)) {
return ['', ''];
}
$aop = new AopClient();
$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = $alipayConfig['appid'] ?? '';
$aop->rsaPrivateKey = $alipayConfig['rsaPrivateKey'] ?? '';
$aop->alipayrsaPublicKey = $alipayConfig['alipayrsaPublicKey'] ?? '';
$aop->apiVersion = '1.0';
$aop->signType = 'RSA2';
$aop->postCharset = 'utf-8';
$aop->format = 'json';
$request = new \AlipayTradeQueryRequest();
$request->setBizContent(json_encode($bizSontent));
$result = $aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$response = $result->$responseNode;
$response = json_decode(json_encode($response), true);
return [$response, $responseNode];
}
/**
* @param string $orderSn 订单号
* @param string $detail 说明
* @param int $totalAmount 扣款金额
* @param string $agreement_no 签约号
* @return array|string[]
* @author wzb
* @date 2023/7/29 10:50
*/
static function pay_sub_knot($orderSn, $detail, $totalAmount, $agreement_no)
{
$bizSontent = [
"out_trade_no" => $orderSn, //订单号
"total_amount" => $totalAmount,
"subject" => $detail,
"product_code" => "GENERAL_WITHHOLDING",
// 代扣信息。
'agreement_params' => [
"agreement_no" => $agreement_no,
],
];
$alipayConfig = config('alipay_config'); // 配置
if (empty($bizSontent) || empty($alipayConfig)) {
return ['', ''];
}
$aop = new AopClient();
$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = $alipayConfig['appid'] ?? '';
$aop->rsaPrivateKey = $alipayConfig['rsaPrivateKey'] ?? '';
$aop->alipayrsaPublicKey = $alipayConfig['alipayrsaPublicKey'] ?? '';
$aop->apiVersion = '1.0';
$aop->signType = 'RSA2';
$aop->postCharset = 'utf-8';
$aop->format = 'json';
$request = new \AlipayTradePayRequest();
$request->setBizContent(json_encode($bizSontent));
$result = $aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$response = $result->$responseNode;
$response = json_decode(json_encode($response), true);
return [$response, $responseNode];
}
/**
* 退款
* @param $outTradeNo
* @param $tradeNo
* @param $refundAmount
* @return array
* @author wzb
* @date 2023/7/25 17:14
*/
static function refundOrder($outTradeNo = '', $tradeNo = '', $refundAmount = 0)
{
$bizSontent = [
"out_trade_no" => $outTradeNo,
"trade_no" => $tradeNo,
"refund_amount" => $refundAmount,
];
$alipayConfig = config('alipay_config'); // 配置
if (empty($alipayConfig)) {
return ['', ''];
}
$aop = new AopClient();
$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = $alipayConfig['appid'] ?? '';
$aop->rsaPrivateKey = $alipayConfig['rsaPrivateKey'] ?? '';
$aop->alipayrsaPublicKey = $alipayConfig['alipayrsaPublicKey'] ?? '';
$aop->apiVersion = '1.0';
$aop->signType = 'RSA2';
$aop->postCharset = 'utf-8';
$aop->format = 'json';
$request = new \AlipayTradeRefundRequest();
$request->setBizContent(json_encode($bizSontent));
$result = $aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$response = $result->$responseNode;
$response = json_decode(json_encode($response), true);
return [$response, $responseNode];
}
/**
* 查询签约接口
* https://opendocs.alipay.com/open/3dab71bc_alipay.user.agreement.query?scene=8837b4183390497f84bb53783b488ecc&pathHash=9a0c5949
* @return array|string[]
* @author wzb
* @date 2023/7/29 13:35
*/
static function agreementQuery($agreementNo)
{
$bizSontent = [
"agreement_no" => $agreementNo,
];
$alipayConfig = config('alipay_config'); // 配置
if (empty($alipayConfig)) {
return ['', ''];
}
$aop = new AopClient();
$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = $alipayConfig['appid'] ?? '';
$aop->rsaPrivateKey = $alipayConfig['rsaPrivateKey'] ?? '';
$aop->alipayrsaPublicKey = $alipayConfig['alipayrsaPublicKey'] ?? '';
$aop->apiVersion = '1.0';
$aop->signType = 'RSA2';
$aop->postCharset = 'utf-8';
$aop->format = 'json';
$request = new \AlipayUserAgreementQueryRequest();
$request->setBizContent(json_encode($bizSontent));
$result = $aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$response = $result->$responseNode;
$response = json_decode(json_encode($response), true);
return [$response, $responseNode];
}
/**
* 解约
* https://opendocs.alipay.com/open/b841da1f_alipay.user.agreement.unsign?scene=90766afb41f74df6ae1676e89625ebac&pathHash=a3599432
* @param string $agreementNo 签约号(协议号)
* @param string $orderSn 订单号 代扣协议中标示用户的唯一签约号(确保在商户系统中唯一)。
* @return array|string[]
* @author wzb
* @date 2023/7/29 13:49
*/
static function agreementUnsign($agreementNo, $orderSn)
{
$bizSontent = [
"agreement_no" => $agreementNo,
"external_agreement_no" => $orderSn,
];
$alipayConfig = config('alipay_config'); // 配置
if (empty($alipayConfig)) {
return ['', ''];
}
$aop = new AopClient();
$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = $alipayConfig['appid'] ?? '';
$aop->rsaPrivateKey = $alipayConfig['rsaPrivateKey'] ?? '';
$aop->alipayrsaPublicKey = $alipayConfig['alipayrsaPublicKey'] ?? '';
$aop->apiVersion = '1.0';
$aop->signType = 'RSA2';
$aop->postCharset = 'utf-8';
$aop->format = 'json';
$request = new \AlipayUserAgreementUnsignRequest();
$request->setBizContent(json_encode($bizSontent));
$result = $aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$response = $result->$responseNode;
$response = json_decode(json_encode($response), true);
return [$response, $responseNode];
}
/**
* 周期性扣款协议执行计划修改
* https://opendocs.alipay.com/open/ed428330_alipay.user.agreement.executionplan.modify?pathHash=e019f106
* @param string $agreementNo 签约号(协议号)
* @param string $nextPay 商户下一次扣款时间 2023-01-01
* @param string $memo 具体修改原因 64个字符
* @return array|string[]
* @author wzb
* @date 2023/7/29 13:45
*/
static function agreementModify($agreementNo, $nextPay, $memo)
{
$bizSontent = [
"agreement_no" => $agreementNo,
"deduct_time" => $nextPay,
"memo" => $memo,
];
$alipayConfig = config('alipay_config'); // 配置
if (empty($alipayConfig)) {
return ['', ''];
}
$aop = new AopClient();
$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = $alipayConfig['appid'] ?? '';
$aop->rsaPrivateKey = $alipayConfig['rsaPrivateKey'] ?? '';
$aop->alipayrsaPublicKey = $alipayConfig['alipayrsaPublicKey'] ?? '';
$aop->apiVersion = '1.0';
$aop->signType = 'RSA2';
$aop->postCharset = 'utf-8';
$aop->format = 'json';
$request = new \AlipayUserAgreementExecutionplanModifyRequest();
$request->setBizContent(json_encode($bizSontent));
$result = $aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$response = $result->$responseNode;
$response = json_decode(json_encode($response), true);
return [$response, $responseNode];
}
/**
* 验证
* @param $arr
* @return bool|string[]|null
* @author wzb
* @date 2023/7/22 10:33
*/
static function notifyVerify($arr)
{
$alipayConfig = config('alipay_config'); // 配置
if (empty($alipayConfig)) {
return false;
}
$aop = new AopClient();
$aop->alipayrsaPublicKey = $alipayConfig['alipayrsaPublicKey'] ?? '';
$urlString = urldecode(http_build_query($arr));
$data = explode('&', $urlString);
$params = [];
foreach ($data as $param) {
$item = explode('=', $param, "2");
$params[$item[0]] = $item[1];
}
$result = $aop->rsaCheckV1($params, null, 'RSA2');
return $result;
}
}