394 lines
18 KiB
PHP
394 lines
18 KiB
PHP
<?php
|
||
namespace addon\aliapp\model;
|
||
|
||
use app\model\BaseModel;
|
||
use app\model\system\Cron;
|
||
use think\Exception;
|
||
use think\facade\Db;
|
||
use think\facade\Log;
|
||
use addon\saasagent\model\SiteWebsite;
|
||
|
||
/**
|
||
* Common: 支付宝分账相关操作
|
||
* Author: wu-hui
|
||
* Time: 2023/01/29 14:46
|
||
* Class AliPaySubLedger
|
||
* @package addon\aliapp\model
|
||
*/
|
||
class AliPaySubLedger extends BaseModel{
|
||
|
||
protected int $site_id;
|
||
|
||
public function __construct($siteId){
|
||
$this->site_id = (int)$siteId;
|
||
}
|
||
|
||
/**
|
||
* Common: 分账关系绑定
|
||
* Author: wu-hui
|
||
* Time: 2023/01/29 16:06
|
||
* @param $websiteId
|
||
* @return array
|
||
* @throws Exception
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function relationBind($websiteId){
|
||
// 请求参数获取
|
||
$params = $this->relationParams($websiteId);
|
||
$textParams = [];
|
||
// 发起请求
|
||
$result = (new OpenPay($this->site_id))->requestApi('alipay.trade.royalty.relation.bind', $params,$textParams);
|
||
$result = $result['alipay_trade_royalty_relation_bind_response'];
|
||
|
||
if($result['code'] == 10000) return $this->success();
|
||
else throw new Exception($result['sub_msg']);
|
||
}
|
||
/**
|
||
* Common: 分账关系解绑
|
||
* Author: wu-hui
|
||
* Time: 2023/01/29 16:18
|
||
* @param $websiteId
|
||
* @return array
|
||
* @throws Exception
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function relationUnbind($websiteId){
|
||
// 请求参数获取
|
||
$params = $this->relationParams($websiteId);
|
||
$textParams = [];
|
||
// 发起请求
|
||
$result = (new OpenPay($this->site_id))->requestApi('alipay.trade.royalty.relation.unbind', $params,$textParams);
|
||
$result = $result['alipay_trade_royalty_relation_unbind_response'];
|
||
|
||
if($result['code'] == 10000) return $this->success();
|
||
else throw new Exception($result['sub_msg']);
|
||
}
|
||
/**
|
||
* Common: 分账关系查询
|
||
* Author: wu-hui
|
||
* Time: 2023/02/01 11:22
|
||
* @return array
|
||
* @throws Exception
|
||
*/
|
||
public function relationBatchQuery(){
|
||
// 请求参数获取
|
||
$params = [
|
||
'page_num' => 1,
|
||
'page_size' => 100,
|
||
'out_request_no' => $this->outRequestNo(0,'BDGX')
|
||
];
|
||
$textParams = [];
|
||
// 发起请求
|
||
$result = (new OpenPay($this->site_id))->requestApi('alipay.trade.royalty.relation.batchquery', $params,$textParams);
|
||
$result = $result['alipay_trade_royalty_relation_batchquery_response'];
|
||
|
||
if($result['code'] == 10000) return $this->success($result);
|
||
else throw new Exception($result['sub_msg']);
|
||
}
|
||
/**
|
||
* Common: 分账关系绑定|解绑 参数获取
|
||
* Author: wu-hui
|
||
* Time: 2023/01/29 15:47
|
||
* @param $websiteId
|
||
* @return array
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
private function relationParams($websiteId){
|
||
// 合伙人信息获取
|
||
$info = Db::name('website')->field('alipay_account,web_contacts')->where('id',$websiteId)->find();
|
||
// 店铺名称获取
|
||
$siteInfo = Db::name('site')->field('site_name,username')->where('site_id',$this->site_id)->find();
|
||
$siteName = $siteInfo['site_name'] ? : $siteInfo['username'];
|
||
return [
|
||
'receiver_list' => [
|
||
'type' => 'loginName',// 分账接收方方类型:loginName表示是支付宝登录号
|
||
'account' => $info['alipay_account'],// 分账接收方账号:当分账方类型是loginName时,本参数为用户的支付宝登录号
|
||
//'account_open_id' => '',// 分账接收方openId
|
||
'name' => $info['web_contacts'],// 当分账方类型是loginName时,本参数必传。
|
||
'memo' => "[{$siteName}]分账给[{$info['web_contacts']}]",// 分账关系描述
|
||
'login_name' => $info['alipay_account'],// 作为查询返回结果
|
||
'bind_login_name' => $info['alipay_account'],// 作为查询返回结果
|
||
],
|
||
'out_request_no' => $this->outRequestNo($websiteId)
|
||
];
|
||
}
|
||
/**
|
||
* Common: 生成外部唯一请求号
|
||
* Author: wu-hui
|
||
* Time: 2023/01/29 15:30
|
||
* @param $websiteId
|
||
* @param string $prefix
|
||
* @return string
|
||
*/
|
||
private function outRequestNo($websiteId,$prefix = 'SL'){
|
||
// 基础内容
|
||
$timeStr = $prefix.date('ymdHis').'S'.$this->site_id.'W'.$websiteId;
|
||
// 保证不重复 偏移操作
|
||
$cacheName = $this->site_id.'_'.$websiteId.'_'.$timeStr;
|
||
$maxNo = (int)cache($cacheName);
|
||
$order_no = $timeStr.'N'.$maxNo;
|
||
cache($cacheName,++$maxNo,5);
|
||
|
||
return $order_no;
|
||
}
|
||
/**
|
||
* Common: 分账 —— 确认结算
|
||
* Author: wu-hui
|
||
* Time: 2023/02/01 15:29
|
||
* @param int $orderId
|
||
* @return false|void
|
||
*/
|
||
public function settleConfirm(int $orderId){
|
||
try{
|
||
// 获取当前订单状态
|
||
$tradeQuery = $this->tradeQuery($orderId)['data'];
|
||
if($tradeQuery['trade_status'] != 'TRADE_SUCCESS') throw new Exception('当前订单不允许分账!订单状态:'.$tradeQuery['trade_status']);
|
||
if($orderId <= 0) throw new Exception("订单id错误 {$orderId}");
|
||
// 获取订单信息
|
||
$orderInfo = $this->getOrderInfo($orderId);
|
||
// 获取所有需要分红的合伙人信息
|
||
$siteWebsite = (new SiteWebsite())->getWholeSiteWebsiteList($orderInfo['site_id'])['data'];
|
||
if(!$siteWebsite) throw new Exception("没有合伙人;订单id: {$orderId}");
|
||
// 生成每个合伙人的分账信息
|
||
$insertData = [];
|
||
$time = time();
|
||
foreach($siteWebsite as $siteWebsiteInfo){
|
||
$insertData[] = [
|
||
'site_id' => $orderInfo['site_id'],
|
||
'order_id' => $orderInfo['order_id'],// 订单id
|
||
'trade_no' => $orderInfo['trade_no'],// 支付宝订单号
|
||
'trans_in' => $siteWebsiteInfo['alipay_account'],// 收入方账户
|
||
'amount' => sprintf("%.2f",($orderInfo['pay_money'] * ($siteWebsiteInfo['rate'] / 100))),// 添加时间
|
||
'website_id' => $siteWebsiteInfo['website_id'],
|
||
'create_time' => $time
|
||
];
|
||
}
|
||
Db::name('sub_ledger')->insertAll($insertData);
|
||
// 添加计划任务
|
||
$cron_model = new Cron();
|
||
$cron_model->addCron(1, 0, '分账确认结算', 'SettleConfirmSubLedger', strtotime('+1 minute', time()), $orderInfo['order_id']);
|
||
|
||
|
||
/*
|
||
// 配置信息
|
||
$params = [
|
||
'out_request_no' => $this->outRequestNo(0,'JS'),//确认结算请求流水号,开发者自行生成并保证唯一性,作为业务幂等性控制
|
||
'trade_no' => $orderInfo['trade_no'] ?? 0,// 支付宝交易号
|
||
'settle_info' => [
|
||
'settle_detail_infos' => [
|
||
[
|
||
// cardAliasNo:结算收款方的银行卡编号;userId:表示是支付宝账号对应的支付宝唯一用户号;loginName:表示是支付宝登录号;defaultSettle:表示结算到商户进件时设置的默认结算账号,结算主体为门店时不支持传defaultSettle;
|
||
'trans_in_type' => 'defaultSettle',
|
||
// 结算收款方。当结算收款方类型是cardAliasNo时,本参数为用户在支付宝绑定的卡编号;结算收款方类型是userId时,本参数为用户的支付宝账号对应的支付宝唯一用户号,以2088开头的纯16位数字;当结算收款方类型是loginName时,本参数为用户的支付宝登录号;当结算收款方类型是defaultSettle时,本参数不能传值,保持为空。
|
||
'trans_in' => '',
|
||
// 结算汇总维度,按照这个维度汇总成批次结算,由商户指定。目前需要和结算收款方账户类型为cardAliasNo配合使用
|
||
//'summary_dimension' => '',
|
||
// 结算主体标识。当结算主体类型为SecondMerchant时,为二级商户的SecondMerchantID;当结算主体类型为Store时,为门店的外标。
|
||
//'settle_entity_id' => $payShopInfo['zmalipay_value']['smid'],
|
||
// 结算主体类型。二级商户:SecondMerchant;商户或者直连商户门店:Store
|
||
//'settle_entity_type' => 'SecondMerchant',
|
||
// 结算的金额,单位为元。在创建订单和支付接口时必须和交易 金额相同。在结算确认接口时必须等于交易金额减去已退款金额。直付通账期模式下,如使用部分结算能力、传递了actual_amount字段,则忽略本字段的校验、可不传。
|
||
//'amount' => 0.1,//$orderInfo['pay_money'] ?? 0,
|
||
// 仅在直付通账期模式场景下,单笔交易需要分多次发起部分确认结算时使用,表示本次确认结算的实际结算金额。传递本字段后,原amount字段不再生效,结算金额以本字段为准。
|
||
'actual_amount' => $orderInfo['pay_money'] ?? 0,
|
||
]
|
||
],
|
||
],
|
||
'extend_params' => [
|
||
'royalty_freeze' => 'true',// 是否进行资金冻结,用于后续分账,true表示冻结,false或不传表示不冻结
|
||
],
|
||
];
|
||
// 发起请求
|
||
$result = (new OpenPay())->requestApi('alipay.trade.settle.confirm', $params, []);
|
||
|
||
$result = $result['alipay_trade_settle_confirm_response'];
|
||
if($result['code'] == 10000) return $this->success();
|
||
else throw new Exception($result['sub_msg']);
|
||
*/
|
||
}
|
||
catch(Exception $e){
|
||
Log::write("分账 - 确认结算:".$e->getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
/**
|
||
* Common: 分账 —— 分账请求
|
||
* Author: wu-hui
|
||
* Time: 2023/02/01 17:08
|
||
* @param $orderId
|
||
* @return array|false
|
||
* @throws \think\db\exception\DbException
|
||
*/
|
||
public function orderSettle($orderId){
|
||
$time = time();
|
||
try{
|
||
// 获取所有分账信息
|
||
$subLedgerList = Db::name('sub_ledger')
|
||
->field(['trade_no','trans_in','amount'])
|
||
->where('order_id',$orderId)
|
||
->select();
|
||
if($subLedgerList) $subLedgerList = $subLedgerList->toArray();
|
||
else throw new Exception('不存在分账信息!');
|
||
// 基本参数
|
||
$outRequestNo = $this->outRequestNo(0,'FZ');//结算请求流水号
|
||
$tradeNo = array_column($subLedgerList,'trade_no')[0];
|
||
// 循环生成分账信息
|
||
$royaltyParameters = [];
|
||
foreach($subLedgerList as $subLedgerInfo){
|
||
$royaltyParameters[] = [
|
||
//'royalty_type' => 'transfer',//分账类型:普通分账为:transfer;补差为:replenish;为空默认为分账transfer;
|
||
//'trans_out' => 'zoomtk@126.com',// 支出方账户
|
||
//'trans_out_type' => 'loginName',// 支出方账户类型
|
||
'trans_in_type' => 'loginName',// 收入方账户类型
|
||
'trans_in' => $subLedgerInfo['trans_in'],// 收入方账户
|
||
'amount' => $subLedgerInfo['amount'],// 分账的金额,单位为元
|
||
'desc' => "订单[{$tradeNo}]的分账金额",// 分账描述
|
||
//'royalty_scene' => '达人佣金',// 可选值:达人佣金、平台服务费、技术服务费、其他
|
||
//'trans_in_name' => '张志伟',// 分账收款方姓名,上送则进行姓名与支付宝账号的一致性校验,校验不一致则分账失败。不上送则不进行姓名校验
|
||
];
|
||
}
|
||
// 参数配置
|
||
$params = [
|
||
'out_request_no' => $outRequestNo,//结算请求流水号
|
||
'trade_no' => $tradeNo,// 支付宝交易订单号
|
||
'royalty_parameters' => $royaltyParameters,
|
||
'extend_params' => [
|
||
// 冻结分账场景下生效,其他场景传入无效。代表该交易分账是否完结,可选值:true/false,
|
||
// 不传默认为false。true:代表分账完结,则本次分账处理完成后会把该笔交易的剩余冻结金额全额解冻。false:代表分账未完结。
|
||
'royalty_finish' => 'true',
|
||
],
|
||
'royalty_mode' => 'sync',// 分账模式,目前有两种分账同步执行sync,分账异步执行async,不传默认同步执行
|
||
];
|
||
// 发起请求
|
||
$result = (new OpenPay())->requestApi('alipay.trade.order.settle',$params,[]);
|
||
Log::write('支付回调 - 分账 —— 分账请求:'.json_encode($result, JSON_UNESCAPED_UNICODE));
|
||
|
||
//debug([$params,$result]);
|
||
$result = $result['alipay_trade_order_settle_response'];
|
||
if($result['code'] == 10000) {
|
||
Db::name('sub_ledger')
|
||
->where('order_id',$orderId)
|
||
->update(['request_time'=>$time,'request_date'=>date('Y-m-d H:i:s',$time),'request_result'=>'成功']);
|
||
return $this->success();
|
||
}
|
||
else throw new Exception($result['sub_msg']);
|
||
}
|
||
catch(Exception $e){
|
||
// 记录失败信息
|
||
Db::name('sub_ledger')
|
||
->where('order_id',$orderId)
|
||
->update(['request_time'=>$time,'request_date'=>date('Y-m-d H:i:s',$time),'request_result'=>$e->getMessage()]);
|
||
|
||
Log::write("分账 - 分账请求:".$e->getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
/**
|
||
* Common: 支付宝交易查询
|
||
* Author: wu-hui
|
||
* Time: 2023/01/31 16:49
|
||
* @param $orderId
|
||
* @return array
|
||
* @throws Exception
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function tradeQuery($orderId){
|
||
$cacheName = 'trade_query_'.$orderId;
|
||
$cacheResult = cache($cacheName);
|
||
if($cacheResult){
|
||
|
||
return $this->success(json_decode($cacheResult,true));
|
||
}
|
||
else{
|
||
// 判断:订单id是否有效
|
||
if($orderId <= 0) throw new Exception("订单id错误 {$orderId}");
|
||
// 获取订单信息
|
||
$orderInfo = $this->getOrderInfo($orderId);
|
||
// 配置信息
|
||
$params = [
|
||
'out_trade_no' => $orderInfo['order_no'],//订单支付时传入的商户订单号,和支付宝交易号不能同时为空。
|
||
'trade_no' => $orderInfo['trade_no'] ?? 0,// 支付宝交易号
|
||
// 查询选项,商户传入该参数可定制本接口同步响应额外返回的信息字段,数组格式。支持枚举如下:
|
||
// trade_settle_info:返回的交易结算信息,包含分账、补差等信息;
|
||
// fund_bill_list:交易支付使用的资金渠道;
|
||
// voucher_detail_list:交易支付时使用的所有优惠券信息;
|
||
// discount_goods_detail:交易支付所使用的单品券优惠的商品优惠信息;
|
||
// mdiscount_amount:商家优惠金额;
|
||
//'query_options' => []
|
||
];
|
||
// 发起请求
|
||
$result = (new OpenPay())->requestApi('alipay.trade.query',$params,[]);
|
||
$result = $result['alipay_trade_query_response'];
|
||
if($result['code'] == 10000){
|
||
cache($cacheName,json_encode($result),3600);
|
||
return $this->success($result);
|
||
}
|
||
else throw new Exception($result['sub_msg']);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Common: 获取订单信息
|
||
* Author: wu-hui
|
||
* Time: 2023/01/30 9:59
|
||
* @param $orderId
|
||
* @return array
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function getOrderInfo($orderId){
|
||
$field = [
|
||
'o.order_id',
|
||
'o.order_no',
|
||
'p.pay_money',
|
||
'p.trade_no',
|
||
'p.pay_time',
|
||
'p.site_id',
|
||
];
|
||
$orderInfo = Db::name('order')
|
||
->alias('o')
|
||
->join('pay p','o.out_trade_no = p.out_trade_no','left')
|
||
->field($field)
|
||
->where('o.order_id',$orderId)
|
||
->find();
|
||
// 判断:订单状态
|
||
if(($orderInfo['pay_time'] ?? 0) <= 0) throw new Exception('订单未支付-'.$orderId);
|
||
// 修改site_id
|
||
if($orderInfo['site_id'] != $this->site_id) $this->site_id = (int)$orderInfo['site_id'];
|
||
|
||
return $orderInfo ?? [];
|
||
}
|
||
/**
|
||
* Common: 获取直通商户信息
|
||
* Author: wu-hui
|
||
* Time: 2023/01/30 11:45
|
||
* @return array|mixed|Db|\think\Model
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function getPayShopInfo(){
|
||
$info = (array)Db::name('pay_shop')
|
||
->field('shop_id,merchant_name,contacts_name,merchant_smid,zmalipay_value')
|
||
->where('site_id',$this->site_id)
|
||
->find();
|
||
if(!$info) throw new Exception('直通商户信息不存在!');
|
||
if($info) $info['zmalipay_value'] = json_decode($info['zmalipay_value'],true);
|
||
if(!$info['zmalipay_value'] || !$info['zmalipay_value']['smid']) throw new Exception('直通商户信息不全!');
|
||
|
||
return $info ?? [];
|
||
}
|
||
|
||
|
||
|
||
} |