admin/addon/aliapp/model/AliPaySubLedger.php

394 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 ?? [];
}
}