支付宝周期扣款
Posted woshihaiyong168
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了支付宝周期扣款相关的知识,希望对你有一定的参考价值。
两种模式
1.签约扣款
2.扣款后签约
依据业务需求使用了 扣款后签约
1.每次扣款不能超过100元 每期每个签约只能扣款一次
2.应用审核必须通过审核才能走通
审核过程中会有sign错误问题出现 应用通过就没问题了(耽误了基本一天排查该问题)
应用私钥和证书一定要弄对 不然很麻烦
想看支付宝接口情况 https://opensupport.alipay.com/support/tools/cloudparse/interface?ant_source=antsupport
支付宝文档:周期扣款 | 网页&移动应用
composer包:支付宝更多方便的插件 | Pay
php框架 laravel
主要逻辑
1. 支付接口 - 返回str字符串 客户端利用sdk拉起
2.支付回调接口 - 支付成功会回调地址
3.签约通知接口 - 支付接口sign_notify_url参数填写地址 签约成功会回调该地址
4.解约通知接口 - 商户主动解约-没有用到 用户主动解约 会请求应用网关 设置支付宝应用网关为该地址
脚本
后续扣款脚本 时间为下次扣款的时间 最小维度为7天 提前5天可以扣款
重试脚本 建议重试两次
更新时间重试脚本 超过扣款时间 请求更改签约日期接口
1.app支付
支付接口 返回加密str给客户端客户端使用sdk拉起支付宝 、统一支付接口新增agreement_sign_params 参数 alipay/payment
```
/** * 支付宝支付 * * @return \\Illuminate\\Http\\JsonResponse * @throws \\Throwable */ public function payment() $user = getApiUser(); $type = request("type", 'setmeal');//setmeal 包时套餐 eachcost单次套餐 $setmeal_id = request("setmeal_id");// 套餐id $setmeal = db('vip_setmeal')->find($setmeal_id); if (!isset($setmeal)) return json(4001, '请选择套餐'); if (strpos($setmeal->channel, '1') === false) return json(4001, '类型不正确'); $price = $setmeal->money; $title = $setmeal->title; $days = 0; switch ($setmeal->date_type) case 1: $vip = 'week'; $days = 7; break; case 2: $vip = 'onemonth'; $days = 30; break; case 3: $vip = 'month'; $days = 90; break; case 4: $vip = 'year'; break; case 5: $vip = 'oneyear'; break; case 6: $vip = 'perpetual'; break; default: $vip = ''; break; switch ($type) case 'setmeal': $title1 = "购买会员时长" . $title; $title = $user['name'] . "购买会员时长" . $title; break; case 'eachcost': $title1 = "购买次数" . $title; $title = $user['name'] . "购买次数" . $title; break; default: $title1 = "购买会员时长"; $title = $user['name'] . "购买会员时长"; break; // 将返回字符串,供后续 APP 调用,调用方式不在本文档讨论范围内,请参考官方文档。 $orderno = Order::getOrderNum(); $other['num'] = $setmeal->num; $other['price'] = $price; $other['type'] = $type; $other['date'] = $vip; $other['is_new'] = 1; $other['setmeal_id'] = $setmeal_id; // 生成支付宝支付参数 $params = [ 'subject' => $title1, 'out_trade_no' => $orderno, 'total_amount' => $price, 'agreement_sign_params' => [ 'personal_product_code' => 'CYCLE_PAY_AUTH_P', 'sign_scene' => 'INDUSTRY|DIGITAL_MEDIA', 'external_agreement_no' => $orderno, 'access_params' => [ 'channel' => 'ALIPAYAPP' ], 'period_rule_params' => [ 'period_type' => 'DAY', 'period' => $days, 'execute_time' => Carbon::now()->addDays($days)->toDateString(), 'single_amount' => $price, ], 'sign_notify_url' => config('app.url') . '/api/alipay/agreement' ], ]; Log::channel('orders')->info($orderno . '-拉起支付-data:' . json_encode($params, JSON_UNESCAPED_UNICODE)); try DB::beginTransaction(); // 获取支付宝支付信息 Pay::config(config('ypay.alipay_config')); $order_str = Pay::alipay()->app($params)->getBody()->getContents(); Log::channel('orders')->info($orderno . '-拉起成功-data:' . $order_str); // 保存订单信息 $order = Order::query()->create([ "user_id" => $user['id'], "title" => $title, "ordernum" => $orderno, "prepay_id" => '', "remark" => request('remark'), "money" => $price, "channel" => 1, "status" => 1, "other" => $other, ]); // 创建签约 OrderAliPayAgreement::query()->create([ 'user_id' => $user['id'], 'order_id' => $order->id, 'ordernum' => $orderno, 'order_price' => $price, 'vip_setmeal_id' => $setmeal_id, 'period_price' => $price, 'period_day' => $days, 'agreement_execute_time' => Carbon::now()->addDays($days)->toDateTimeString(), ]); DB::commit(); // 创建签约 catch (\\Throwable $t) Log::channel('orders')->error($orderno . '-拉起失败-' . $t->getMessage()); DB::rollBack(); return json(4001, '支付拉起异常'); return json(1001, '请求成功', ['order_str' => $order_str, 'orderId' => $orderno]);
2. 支付回调接口
/** * 支付回调 * @throws \\EasyWeChat\\Kernel\\Exceptions\\Exception */ public function notify() $alipay = Pay::alipay(config('ypay.alipay_config')); $res = request()->all(); $orderno = $res['out_trade_no'] ?? 0; Log::channel('orders')->info('alipay_notify:' . $orderno . ':data:' . json_encode($res, JSON_UNESCAPED_UNICODE)); try $data = $alipay->callback(); // 是的,验签就这么简单! $data = $data->all(); // 请自行对 trade_status 进行判断及其它逻辑进行判断,在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。 // 1、商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号; // 2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额); // 3、校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email); // 4、验证app_id是否为该商户本身。 // 5、其它业务逻辑情况 if (in_array($data['trade_status'], ['TRADE_CLOSED', 'TRADE_FINISHED'])) return $alipay->success(); $order = Order::query() ->with('user') ->where('ordernum', $data['out_trade_no'])->first(); if (!$order || $order->status == 2) // 如果订单不存在 或者 订单已经支付过了 return $alipay->success(); if ($data['trade_status'] == 'TRADE_SUCCESS') $order->pay_time = now()->toDateTimeString(); // 更新支付时间为当前时间 $order->transaction_id = $data['trade_no']; $order->status = 2; $order->save(); // 保存订单 if (isset($order->other['is_new']) && $order->other['is_new'] == 2) //更新用户到期时间,剩余次数 User::saveNewVip($order); else //更新用户到期时间,剩余次数 User::saveVip($order); Log::channel('orders')->info('alipay_notify:' . $orderno . ':success'); return $alipay->success(); catch (\\Exception $e) Log::channel('orders')->error('alipay_notify:' . $orderno . ':error:' . $e->getMessage()); $error_msg = "【支付宝-回调异常】\\n\\n【订单】:$orderno\\n【原因】:$e->getMessage()\\n"; DingTalk::ding(1, $error_msg, 'text', []);
3.签约回调接口
/** * 签约回调 * * @return \\Psr\\Http\\Message\\ResponseInterface */ public function agreement() $alipay = Pay::alipay(config('ypay.alipay_config')); $res = request()->all(); $agreement_no = $res['external_agreement_no'] ?? 0; Log::channel('orders')->info('alipay_agreement_notify:' . $agreement_no . ':data:' . json_encode($res, JSON_UNESCAPED_UNICODE)); try $data = $alipay->callback(); $data = $data->all(); $agreement = OrderAliPayAgreement::query() ->where('ordernum', $data['external_agreement_no'])->first(); if (!$agreement || $agreement->agreement_status == 2) // 如果签约不存在 或者 已经签约过了 return $alipay->success(); if ($data['status'] == 'NORMAL') $agreement->agreement_success_time = now()->toDateTimeString(); // 更新签约时间为当前时间 $agreement->agreement_no = $data['agreement_no']; $agreement->agreement_status = 2; $agreement->save(); Log::channel('orders')->info('alipay_agreement_notify:' . $agreement_no . ':success'); return $alipay->success(); catch (\\Exception $e) Log::channel('orders')->error('alipay_agreement_notify:' . $agreement_no . ':error:' . $e->getMessage()); $error_msg = "【支付宝签约-回调异常】\\n\\n【订单】:$agreement_no\\n【原因】:$e->getMessage()\\n"; DingTalk::ding(1, $error_msg, 'text', []);
4.解约回调接口
/** * 网关通知 */ public function gateway() $alipay = Pay::alipay(config('ypay.alipay_config')); $res = request()->all(); Log::channel('orders')->info('alipay_gateway_notify:data:' . json_encode($res, JSON_UNESCAPED_UNICODE)); try $data = $alipay->callback(); $data = $data->all(); switch ($data['notify_type']) // 签约 case 'dut_user_sign': break; // 解约 case 'dut_user_unsign': if ($data['status'] == 'UNSIGN') OrderAliPayAgreement::query() ->where('agreement_no', '=', $data['agreement_no']) ->update([ 'agreement_close_time' => now()->toDateTimeString(), 'agreement_status' => 3 ]); break; default: break; return $alipay->success(); catch (\\Exception $e) Log::channel('orders')->error('alipay_gateway_notify:error:' . $e->getMessage()); // todo dingding
脚本
public function handle() switch ($this->argument('operation')) // 扣款 case 'alipay_check': $this->alipayCheck(); break; // 重试 case 'alipay_retry' : $this->alipayRetry(); break; // 延期 case 'alipay_change' : $this->alipayChange(); break; default: break; /** * * 周期扣款 * */ public function alipayCheck() // https://pay.yansongda.cn/docs/v3/alipay/more.html // 当前时间处于扣款时间段内(提前 5 天+扣款日当天)则直接发起重试,如:约定扣款日为 20 号,支持商家在 15 至 20 号内可直接重试。 // 当前时间即将超过扣款时间段,可以通过 alipay.user.agreement.executionplan.modify(周期性扣款协议执行计划修改接口)推迟下一次扣款时间继续重试。 $now = now()->addDays(5)->toDateString(); $to_pay_agreement = OrderAliPayAgreement::query() ->where(['agreement_status' => 2]) ->whereBetween('agreement_execute_time', [$now . ' 00:00:00', $now . ' 23:59:59']) ->get() ->toArray(); if (!$to_pay_agreement) return true; $config = config('ypay.alipay_config'); foreach ($to_pay_agreement as $item) // 是否存在关联订单 $is_exist = OrderAliPayAgreementRelation::query() ->where('agreement_id', '=', $item['agreement_id']) ->where('agreement_execute_time', '=', $item['agreement_execute_time']) ->first(); // 获取原始订单数据 $order_info = Order::query()->where('id', '=', $item['order_id'])->first(); if (empty($is_exist['id'])) $orderno = Order::getOrderNum(); $order = Order::query()->create([ "user_id" => $item['user_id'], "title" => $order_info->title, "ordernum" => $orderno, "prepay_id" => '', "remark" => '签约续费', "money" => $item['period_price'], "channel" => 1, "status" => 1, "other" => $order_info->other, ]); // 创建订单 OrderAliPayAgreementRelation::query()->create([ 'agreement_id' => $item['agreement_id'], 'order_id' => $order->id, 'ordernum' => $orderno, 'agreement_execute_time' => $item['agreement_execute_time'], ]); $order_id = $order->id; else $order_id = $is_exist->order_id; $orderno = $is_exist->ordernum; $agreement_params = [ 'period_now_order_id' => $order_id, 'period_now_status' => 2, 'period_now_time' => now()->toDateTimeString(), 'period_execute_time' => $item['agreement_execute_time'], 'period_now_log' => '', ]; // 开始执行扣款 $params = [ 'subject' => $order_info->title, 'out_trade_no' => $orderno, 'total_amount' => $item['period_price'], 'product_code' => 'CYCLE_PAY_AUTH', 'agreement_params' => [ 'agreement_no' => $item['agreement_no'], ], ]; $this->_doPeriod($config, $params, $item, $agreement_params, $order_id, $orderno); /** * 周期重试 * * @return bool */ public function alipayRetry() $today = now()->toDateString(); $retry_agreement = OrderAliPayAgreement::query() ->where(['agreement_status' => 2, 'period_now_status' => 2]) ->where('period_retry', '<', 3) ->where('period_execute_time', '>=', $today . ' 00:00:00') ->get() ->toArray(); if (!$retry_agreement) return true; $config = config('ypay.alipay_config'); foreach ($retry_agreement as $item) // 获取扣款订单数据 $order_info = Order::query()->where('id', '=', $item['period_now_order_id'])->first(); $agreement_params = [ 'period_now_status' => 2, 'period_now_time' => now()->toDateTimeString(), 'period_now_log' => '', 'period_retry' => $item['period_retry'] + 1, ]; // 开始执行扣款 $params = [ 'subject' => $order_info->title, 'out_trade_no' => $order_info->ordernum, 'total_amount' => $item['period_price'], 'product_code' => 'CYCLE_PAY_AUTH', 'agreement_params' => [ 'agreement_no' => $item['agreement_no'], ], ]; $this->_doPeriod($config, $params, $item, $agreement_params, $order_info['id'], $order_info['ordernum']); /** * 扣款 * * @param $config * @param $params * @param $agreement_info * @param $agreement_params * @param $order_id * @param $orderno */ private function _doPeriod($config, $params, $agreement_info, $agreement_params, $order_id, $orderno) try // 开始执行扣款 pay::config($config); $allPlugins = Pay::alipay()->mergeCommonPlugins([PayPlugin::class]); $result = Pay::alipay()->pay($allPlugins, $params)->toArray(); $agreement_params['period_now_log'] = $result; if ($result['code'] == '10000' && !empty($result['trade_no'])) $agreement_params['period_now_status'] = 1; $agreement_params['period_success'] = $agreement_info['period_success'] + 1; // 更新下次扣款时间 $agreement_params['agreement_execute_time'] = Carbon::parse($agreement_info['agreement_execute_time'])->addDays($agreement_info['period_day'])->toDateTimeString(); catch (Exception $exception) $agreement_params['period_now_log'] = $exception->getMessage(); OrderAliPayAgreement::query() ->where('agreement_id', '=', $agreement_info['agreement_id']) ->update($agreement_params); // 日志入库 OrderAliPayAgreementLog::query()->create([ 'agreement_id' => $agreement_info['agreement_id'], 'order_id' => $order_id, 'ordernum' => $orderno, 'agreement_execute_time' => $agreement_info['agreement_execute_time'], 'params' => json_encode($params), 'response' => json_encode($agreement_params['period_now_log']), ]); /** * 周期改签 * * * @return bool */ public function alipayChange() $change_agreement = OrderAliPayAgreement::query() ->where(['agreement_status' => 2, 'period_now_status' => 2]) ->where('period_retry', '=', 3) ->where('agreement_execute_time', '<', now()->toDateString() . ' 00:00:00') ->whereNull('period_change') ->get() ->toArray(); if (!$change_agreement) return true; $config = config('ypay.alipay_config'); foreach ($change_agreement as $item) $agreement_params = [ 'period_now_order_id' => 0, 'period_now_status' => 0, 'period_execute_time' => null, 'period_now_time' => null, 'period_change' => 1, 'period_retry' => 0, 'period_now_log' => null, ]; try $params = [ 'agreement_no' => $item['agreement_no'], 'deduct_time' => Carbon::parse($item['agreement_execute_time'])->addDays($item['period_day'])->toDateString(), 'memo' => '失败重试-延期', ]; // 开始执行改签 pay::config($config); $allPlugins = Pay::alipay()->mergeCommonPlugins([AgreementExecutionPlanModifyPlugin::class]); $result = Pay::alipay()->pay($allPlugins, $params)->toArray(); $agreement_params['period_now_log'] = $result; if ($result['code'] == '10000' && !empty($result['agreement_no'])) // 更新下次扣款时间 $agreement_params['agreement_execute_time'] = Carbon::parse($item['agreement_execute_time'])->addDays($item['period_day'])->toDateTimeString(); catch (Exception $exception) $agreement_params['period_now_log'] = $exception->getMessage(); OrderAliPayAgreement::query() ->where('agreement_id', '=', $item['agreement_id']) ->update($agreement_params);
支付宝要开通 周期设置好 证书 以及应用私钥 !!!!!!
1.
CREATE TABLE `order_alipay_agreement` (
`agreement_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户id',
`order_id` int(11) NOT NULL DEFAULT '0' COMMENT '签约时订单ID',
`ordernum` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '订单编号',
`order_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '签约时价格',
`vip_setmeal_id` int(11) NOT NULL DEFAULT '0' COMMENT '套餐ID',
`agreement_no` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '支付宝协议号',
`agreement_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1待签约 2已签约 3关闭签约',
`agreement_success_time` datetime DEFAULT NULL COMMENT '签约时间',
`agreement_close_time` datetime DEFAULT NULL COMMENT '关闭签约时间',
`agreement_execute_time` datetime DEFAULT NULL COMMENT '下次扣款时间',
`period_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '周期扣款价格',
`period_day` int(11) NOT NULL DEFAULT '0' COMMENT '周期天数',
`period_success` int(11) NOT NULL DEFAULT '0' COMMENT '成功扣款次数',
`period_now_order_id` int(11) DEFAULT '0' COMMENT '周期-最新扣款订单ID',
`period_now_status` tinyint(4) DEFAULT NULL COMMENT '周期-最新执行状态 1-扣款成功 2-扣款失败',
`period_now_time` datetime DEFAULT NULL COMMENT '周期-最新执行时间',
`period_execute_time` datetime DEFAULT NULL COMMENT '周期-扣款时间',
`period_now_log` text COLLATE utf8mb4_unicode_ci COMMENT '周期-最新执行日志',
`period_retry` int(11) NOT NULL DEFAULT '0' COMMENT '周期-重试次数',
`period_change` tinyint(1) DEFAULT NULL COMMENT '周期-是否延期 1-是 ',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`agreement_id`) USING BTREE,
UNIQUE KEY `unidx_order_num` (`ordernum`) USING BTREE,
KEY `idx_status_channel` (`agreement_status`),
KEY `idx_user_id_status` (`user_id`,`agreement_status`) USING BTREE,
KEY `idx_time` (`agreement_execute_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT COMMENT='支付宝签约表'
2.
CREATE TABLE `order_alipay_agreement_relation` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`agreement_id` int(11) NOT NULL DEFAULT '0' COMMENT '签约ID',
`order_id` int(11) NOT NULL DEFAULT '0' COMMENT '后续扣款订单ID',
`ordernum` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '后续扣款订单号',
`agreement_execute_time` datetime DEFAULT NULL COMMENT '计划扣款时间',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单后续扣款关系表'
3.
CREATE TABLE `order_alipay_agreement_log` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`agreement_id` int(11) NOT NULL DEFAULT '0' COMMENT '签约ID',
`order_id` int(11) NOT NULL DEFAULT '0' COMMENT '后续扣款订单ID',
`ordernum` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '后续扣款订单号',
`agreement_execute_time` datetime DEFAULT NULL COMMENT '计划扣款时间',
`params` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '请求参数',
`response` text COLLATE utf8mb4_unicode_ci COMMENT '返回值',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单后续扣款请求记录表'
以上是关于支付宝周期扣款的主要内容,如果未能解决你的问题,请参考以下文章