业务流程

1、处理签约成功回调,添加到订阅表

2、定时任务自行请求订阅表,把达到扣款日期的订阅,然后请求支付宝扣款,并本地开通权限给用户,再计算下次扣款时间

3、处理签约解除回调,删除订阅表数据。(需要去设置网关回调地址,有退款的话支付宝会回调告诉我们)

包文件


注意:上面的golang的包我是做过改进的,在 https://github.com/smartwalle/alipay 基础上改了源码

把改进的包下载下来之后,放在gopath路径中:/Users/twj/Documents/go_www/src/github.com/smartwalle/alipay,我这里的go是gopath环境

备注

因为我们支付系统是用golang写的,业务系统是用php写的,所以下面会有两个系统的代码,但go和php都大同小异。

接周期扣款要注意的点

1、支付宝的周期扣款,后续的扣款是商家自行请求扣款接口的,支付宝是不会帮你们做定时器然后回调接口提示你已经扣款的。需要你自己写定时任务计算好扣款日期,再去请求支付宝的,然后支付宝可以提前5天扣款。

2、周期扣款日期不能是28号到月底最后一天的,假设下次扣款日是9月28日,那么建议你设置扣款日期是下个月的1~3号,也就是这个字段:execute_time

3、周期扣款的后续,商家自行请求支付宝时候,每笔扣款是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            string
	AppId              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 payment

import (
	
	"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)
}

签约成功回调逻辑

/***
     * 支付宝周期购回调逻辑处理
     * @param Request $request
     */
    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';
        }
    }
/***
     * 周期购回调逻辑处理
     * 这个回调是签约成功的回调
     * 我们用的是支付后签约,会有两个回调,一个是支付成功回调,一个是签约成功回调
     * @param $all
     * @return bool
     * @throws \Exception
     */
    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;
    }

后续扣款逻辑

1、php使用的是阿里云提供的sdk包,上面有链接

2、写一个定时任务,每次扣款、首次签约,都计算好扣款时间,定时任务去查数据库,查当天是否达到了扣款时间,然后进行扣款逻辑。

/***
     * 支付宝周期扣扣款
     * @param $agreement_no
     * @param $amount
     * @param $out_trade_no
     * @param $sku
     * @param $user_id
     * @param $contract_no
     * @return bool
     * @throws \Exception
     */
    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;
    }

解除签约

去支付宝的开放后台设置设置应用网关。用户解除签约的时候,是会回调到这个地址的

java支付宝发送订阅消息 支付宝订阅消息在哪_golang

解除签约回调地址的逻辑

/***
     * 支付宝网关回调接口
     * @param Request $request
     * @throws \Exception
     */
    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';
        }
    }
/***
     * 支付宝网关回调接口
     * 目前存在回调:1:用户取消周期购签约
     * @param $all
     * @return bool
     * @throws \Exception
     */
    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='用户商品签约表';