支付宝周期扣款

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);
    

支付宝要开通 周期设置好 证书 以及应用私钥  !!!!!!

mysql

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='订单后续扣款请求记录表'

以上是关于支付宝周期扣款的主要内容,如果未能解决你的问题,请参考以下文章

支付宝周期扣款Java逻辑代码

android 11 上配置微信授权、判断是不是安装微信或支付宝 、h5 拉起支付问题

如何关闭mycat花呐自动扣款

宝付提醒:被自动扣款只因你忽视了它

golang集成支付宝支付(沙箱环境)

支付宝 - 支付宝怎么解除自动续费?