业务流程
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;
}
解除签约
去支付宝的开放后台设置设置应用网关。用户解除签约的时候,是会回调到这个地址的
解除签约回调地址的逻辑
/***
* 支付宝网关回调接口
* @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='用户商品签约表';