两种模式
1.签约扣款
2.扣款后签约
依据业务需求使用了 扣款后签约
1.每次扣款不能超过100元 每期每个签约只能扣款一次
2.应用审核必须通过审核才能走通
审核过程中会有sign错误问题出现 应用通过就没问题了(耽误了基本一天排查该问题)
主要逻辑
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='订单后续扣款请求记录表'