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