文档
周期扣款支付后签约场景文档:https://opendocs.alipay.com/open/041bxs
业务流程
处理签约成功回调,添加到订阅表
定时任务自行请求订阅表,把达到扣款日期的订阅,然后请求支付宝扣款,并本地开通权限给用户,再计算下次扣款时间
处理签约解除回调,删除订阅表数据。(需要去设置网关回调地址,有退款的话支付宝会回调告诉我们)
包文件
golang包:https://github.com/phpgolangdeveloper/smartwalle
phpsdk包:https://opendocs.alipay.com/open/02np96
注意:上面的golang的包我是做过改进的,在 https://github.com/smartwalle/alipay 基础上改了源码
把改进的包下载下来之后,放在gopath路径中:/Users/twj/Documents/go_www/src/github.com/smartwalle/alipay,我这里的go是gopath环境
备注
因为我们支付系统是用golang写的,业务系统是用php写的,所以下面会有两个系统的代码,但go和php都大同小异。
接周期扣款要注意的点
支付宝的周期扣款,后续的扣款是商家自行请求扣款接口的,支付宝是不会帮你们做定时器然后回调接口提示你已经扣款的。需要你自己写定时任务计算好扣款日期,再去请求支付宝的,然后支付宝可以提前5天扣款。
周期扣款日期不能是28号到月底最后一天的,假设下次扣款日是9月28日,那么建议你设置扣款日期是下个月的1~3号,也就是这个字段:execute_time
周期扣款的后续,商家自行请求支付宝时候,每笔扣款是100元内,也就是你接入周期扣款的时候,后续的每笔自动扣款都必须是100元内,没得提升,想要提升额度就是要用商家代扣,具体问问alipay客服。
代码层
golang代码
结构体
type TradePay struct {Channel string `json:"channel"`TBody string `json:"t_body"`TotalFee float64 `json:"amount"`OutTradeNo string `json:"out_trade_no"`ProductName string `json:"product_name"`ProductDesc string `json:"product_desc"`OpenId string `json:"open_id"`TradeType string `json:"trade_type"`FrontUrl string `json:"front_url"`JsonStr stringAppId string `json:"app_id"`MchId string `json:"mch_id"`ApiKey string `json:"-"`NotifyUrl string `json:"notify_url"`NotifyType string `json:"notify_type"`ReturnUrl string `json:"return_url"`RsaPrivateKey string `json:"-"`AlipayrsaPublicKey string `json:"-"`PassbackParams url.Values `json:"body"`PayIp string `json:"bill_ip"`Tid int64 `json:"tid"`Charset string `json:"charset"`Returl string `json:"returl"`//add for UnionPay<通联支付专用>MerchantId int64 `json:"merchant_id"`UnionPayAppId string `json:"-"`CusId string `json:"-"`SignType string `json:"-"`ValidTime int `json:"-"`// benefitdetail 优惠信息Benefitdetail Benefitdetail `json:"benefitdetail"`ProductCode string `json:"product_code"`PeriodType string `json:"period_type,omitempty"`Period int `json:"period,omitempty"`ExternalAgreementNo string `json:"external_agreement_no,omitempty"`}
业务代码:
package paymentimport ("golang_payment/pkg/logging""golang_payment/pkg/setting""strconv""time")func demo(periodType string, period int) {var client, clientErr = alipay.New("appid", "AlipayrsaPublicKey","RsaPrivateKey", false)if clientErr != nil {logging.Info("Alipay UnifiedOrder New Client Error")panic(clientErr)}var p = alipay.TradeAppPay{}var signNotifyUrl = ""if setting.SanBox {p.NotifyURL = "沙盒支付成功回调地址"signNotifyUrl = "沙盒签约成功回调地址"} else {p.NotifyURL = "正式环境支付成功回调地址"signNotifyUrl = "正式环境签约成功回调地址"}p.ReturnURL = "同步回调地址"p.Body = "商品名字"p.Subject = "商品名字"p.OutTradeNo = "商家订单号"p.TotalAmount = strconv.FormatFloat(1, 'f', 2, 64)// 订单总价p.ProductCode = "QUICK_MSECURITY_PAY"// 请确定是周期扣款还是普通支付扣款if p.ProductCode == "CYCLE_PAY_AUTH" { // 周期购// 默认是按月var ExecuteTime = time.Now().AddDate(0, period, 0).Format("2006-01-02")if periodType == "DAY" {// 哪找天ExecuteTime = time.Now().AddDate(0, period, 1).Format("2006-01-02")}// TODO::注意:支付宝周期扣不能大于 28号, 如果周期扣款当天计算是大于本月28号的,建议设置到下个月的1~3号if time.Now().Format("2006-01-02") > time.Now().Format("2006-01")+"-28" {now := time.Now()currentYear, currentMonth, _ := now.Date()currentLocation := now.Location()//本月6号截止//fmt.Println(time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, currentLocation).Unix()+6*24*3600-1)//下个月6号截止//fmt.Println(time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, currentLocation).AddDate(0, 1, 0).Unix()+6*24*3600-1)tm := time.Unix(time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, currentLocation).AddDate(0, 1, 0).Unix()+1*24*3600-1, 0)ExecuteTime = tm.Format("2006-01-02")}// 签约参数var agreementSign = alipay.Trade{AgreementSignParams: &alipay.AgreementSignParams{PersonalProductCode: "CYCLE_PAY_AUTH_P",SignScene: "INDUSTRY|EDU",AccessParams: &alipay.Channel{Channel: "ALIPAYAPP"},ExternalAgreementNo: "随机生成的商家签约码,作用是商家数据库做唯一",PeriodRuleParams: &alipay.PeriodRuleParams{PeriodType: periodType,Period: period,ExecuteTime: ExecuteTime,SingleAmount: 100,// 周期扣款每笔限制扣款最大金额,目前支付宝最大是100元上限},SignNotifyUrl: signNotifyUrl,},}p.AgreementSignParams = agreementSign.AgreementSignParams}p.TimeoutExpress = "30m"var url, payErr = client.TradeAppPay(p)if payErr != nil {logging.Info(payErr)panic(payErr)} else {logging.Info("支付宝-tradePay:订单号支付宝支付串:" + url)}logging.Info(url)}
签约成功回调逻辑
public function alipayPayCallback(Request $request) { try { if (!CallbackService::alipayPayCallback($request->all())) { throw new \Exception('周期购回调有误'); } echo 'success'; }catch (\Exception $e){ Log::error('alipayPayCallback 订阅商品核查失败', [ 'msg' => $e->getMessage(), 'line' => $e->getLine(), 'file' => $e->getFile(), ]); echo 'error'; } }
public static function alipayPayCallback($all) { Log::info('周期扣签约回调', ['all' => $all]); $all = collect($all)->toArray(); if (!AlipaySigningCallbackModel::where('external_agreement_no', $all['external_agreement_no']) ->where('callback_status', $all['status']) ->first()) { // 添加回调表 AlipaySigningCallbackModel::insertGetId([ 'external_agreement_no' => $all['external_agreement_no'], 'info_data' => json_encode($all, true), 'callback_status' => $all['status'], ]); } if ($all['status'] != 'NORMAL') { Log::error('周期扣签约回调 支付宝周期扣异步回调status', ['all' => $all]); throw new \Exception('签约没成功'); } $aop = new AopClient(); //编码格式 $aop->postCharset = "UTF-8"; //支付宝公钥赋值 $aop->alipayrsaPublicKey = self::publicKey; $urlString = urldecode(http_build_query($all)); $data = explode('&', $urlString); $params = []; foreach ($data as $param) { $item = explode('=', $param, "2"); $params[$item[0]] = $item[1]; } $flag = $aop->rsaCheckV1($params, null, 'RSA2'); if (!$flag) { Log::error('周期扣签约回调 支付宝周期扣签约回调验证签名不通过', ['all' => $all]); throw new \Exception('支付宝周期扣签约回调验证签名不通过'); } $signingData = UserGoodSubscribeModel::from('m_user_good_subscribe as s') ->leftJoin('m_orders as o', 'o.transfer_no', '=', 's.out_trade_no') ->leftJoin('m_goods as g', 'o.sku', '=', 'g.sku') ->where('s.contract_no', $all['external_agreement_no']) ->select(['s.id', 'o.user_id', 'o.sku', 's.contract_no', 's.status', 's.out_trade_no', 'g.options']) ->first(); if (!$signingData) { throw new \Exception('该签约号有误'); } if ($signingData->status == 2) { throw new \Exception('该首次订单订单已经签约'); } try { DB::beginTransaction(); if (!AlipaySigningCallbackModel::where('external_agreement_no', $all['external_agreement_no'])->update([ 'status' => 1, ])) { throw new \Exception('回调表修改失败'); } $period_type = 'months'; $options = json_decode($signingData->options, true); switch ($options['unit']) { case 1: $period_type = 'day'; $period = (int)$options['unit_value']; break; case 2: $period = (int)$options['unit_value']; break; case 3: $period = 12 * (int)$options['unit_value']; break; default: throw new \Exception('签约回调商品有误'); } // 下次扣款时间 $next_pay = date('Y-m-d', strtotime("+$period $period_type", strtotime($all['sign_time']))); if (!UserGoodSubscribeModel::where('id', $signingData->id)->update([ 'status' => 2, 'contract_time' => date('Y-m-d H:i:s'), 'next_pay' => $next_pay, 'agreement_no' => $all['agreement_no'], ])) { throw new \Exception('回调添加订阅表失败'); } if (!OrdersModel::where('external_order', $all['external_agreement_no']) ->update(['agreement_no' => $all['agreement_no']])) { throw new \Exception('更新订单表失败'); } DB::commit(); } catch (\Exception $e) { Log::error('周期扣签约回调 周期购签约回调逻辑处理失败', [ 'msg' => $e->getMessage(), 'line' => $e->getLine(), 'file' => $e->getFile(), ]); DB::rollBack(); throw new \Exception($e->getMessage()); } return true; }
后续扣款逻辑
php使用的是阿里云提供的sdk包,上面有链接
写一个定时任务,每次扣款、首次签约,都计算好扣款时间,定时任务去查数据库,查当天是否达到了扣款时间,然后进行扣款逻辑。
public function aliPayCycle($agreement_no, $amount, $out_trade_no, $sku, $user_id, $contract_no) { $userInfo = User::where('id', $user_id)->where('status', 1)->select(['mobile', 'id'])->first(); $good = Goods::where('sku', $sku)->where('status', 1)->select(['name', 'sku', 'price', 'options'])->first(); $aop = new AopClient(); $aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do'; $aop->appId = '你的appid'; $aop->rsaPrivateKey = "私钥"; $aop->alipayrsaPublicKey = "公钥"; $aop->apiVersion = '1.0'; $aop->signType = 'RSA2'; $aop->postCharset = 'utf-8'; $aop->format = 'json'; $object = new \stdClass(); $object->out_trade_no = "订单号"; $object->total_amount = "订单价格"; $object->subject = "商品名字"; $object->product_code = 'CYCLE_PAY_AUTH'; $agreementParams = [ 'agreement_no' => $agreement_no, ]; $object->agreement_params = $agreementParams; $json = json_encode($object); $request = new AlipayTradePayRequest(); $request->setBizContent($json); $result = $aop->execute($request);//dd($result); $responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response"; $resultCode = $result->$responseNode->code; $resultSubMsg = $result->$responseNode->sub_msg; if (empty($resultCode) && $resultCode != 10000) { Log::error('支付宝周期扣失败', ['result' => $result]); throw new \Exception('支付宝周期扣失败' . $resultSubMsg); } // 下面是我们系统中的逻辑和计算下次周期扣的时间,不用管 $addPer = OrderService::onlineOrder( $sku, $user_id, $good->price, $amount, 0, $userInfo->mobile, date('Y-m-d H:i:s'), $contract_no, 2, $agreement_no ); //更新下次划扣时间 $period_type = 'months'; $options = json_decode($good->options, true); switch ($options['unit']) { case 1: $period_type = 'day'; $period = (int)$options['unit_value']; break; case 2: $period = (int)$options['unit_value']; break; case 3: $period = 12 * (int)$options['unit_value']; break; default: throw new \Exception('签约回调商品有误'); } // 下次扣款时间 $next_pay = date('Y-m-d', strtotime("+$period $period_type", strtotime(date('Y-m-d H:i:s')))); if (isset($addPer['makeResultID']) && $addPer['makeResultID']) { // 下次扣款时间 UserGoodSubscribe::where('agreement_no', $agreement_no)->update(['next_pay' => $next_pay]); } Log::info('支付宝周期扣扣款', ['addPer' => $addPer, 'agreement_no' => $agreement_no, 'user_id' => $user_id]); return true; }
解除签约
去支付宝的开放后台设置设置应用网关。用户解除签约的时候,是会回调到这个地址的
解除签约回调地址的逻辑
public function alipayGatewayCallback(Request $request) { try { if (!CallbackService::alipayGatewayCallback($request->all())) { throw new \Exception('周期购回调有误'); } echo 'success'; }catch (\Exception $e){ Log::error('订阅商品核查失败', [ 'msg' => $e->getMessage(), 'line' => $e->getLine(), 'file' => $e->getFile(), ]); echo 'error'; } }
public static function alipayGatewayCallback($all) { Log::info('支付宝网关回调', ['all' => $all]); $all = collect($all)->toArray(); // 添加回调表 $notifiId = AlipaySigningCallbackModel::insertGetId([ 'external_agreement_no' => $all['external_agreement_no'], 'info_data' => json_encode($all, true), 'callback_status' => $all['status'], ]); $aop = new AopClient(); //编码格式 $aop->postCharset = "UTF-8"; //支付宝公钥赋值 $aop->alipayrsaPublicKey = "公钥"; $urlString = urldecode(http_build_query($all)); $data = explode('&', $urlString); $params = []; foreach ($data as $param) { $item = explode('=', $param, "2"); $params[$item[0]] = $item[1]; } $flag = $aop->rsaCheckV1($params, null, 'RSA2'); if (!$flag) { Log::error('支付宝周期扣签约回调验证签名不通过', ['all' => $all]); throw new \Exception('支付宝周期扣签约回调验证签名不通过'); } // 状态 switch ($all['status']) { case 'UNSIGN':// 支付宝周期扣解约操作 break; default: break; } return true; }
数据表
CREATE TABLE `m_alipay_signing_callback` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '回调情况 0未处理 1处理已完成', `info_data` text NOT NULL COMMENT '整个订单数据序列化,后续需要再拿出来使用', `external_agreement_no` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '支付宝签约号', `callback_status` varchar(20) NOT NULL DEFAULT '' COMMENT '回调状态;正常:NORMAL,解约:UNSIGN,暂存,协议未生效过:TEMP,暂停:STOP', PRIMARY KEY (`id`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COMMENT='支付宝周期购签约回调表';CREATE TABLE `m_user_good_subscribe` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(11) unsigned NOT NULL DEFAULT '0', `type` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '1:支付宝扣款 2:苹果扣款 3:微信扣款', `out_trade_no` varchar(50) NOT NULL DEFAULT '' COMMENT '首次商户订单号', `status` tinyint(1) DEFAULT '0' COMMENT '0 未订阅 1签约中 2已订阅 -1已退订', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', `deleted_at` timestamp NULL DEFAULT NULL, `contract_no` varchar(255) NOT NULL DEFAULT '' COMMENT '支付宝商家本地唯一签约号/苹果transaction_id', `contract_time` timestamp NULL DEFAULT NULL COMMENT '签约成功时间', `sku` varchar(32) NOT NULL COMMENT '商品唯一ID', `next_pay` timestamp NULL DEFAULT NULL COMMENT '下次扣款时间', `amount` decimal(10,2) NOT NULL COMMENT '签约价格', `agreement_no` varchar(255) NOT NULL DEFAULT '' COMMENT '支付宝平台签约成功返回签约号/苹果原始original_transaction_id', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `contract_no` (`contract_no`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=76 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户商品签约表';
来源地址:https://blog.csdn.net/qq_41672878/article/details/126300858