toArray(); } if (is_object($item)) { $item = $item->toArray(); } \app\admin\model\order\Item::where([ 'id'=>$item['id'] ])->update([ 'refund_status'=>\app\common\constant\order\Status::REFUND_STATUS_OK, 'refund_price'=>$refund_money + $item['refund_price'], 'refund_time'=>time(), 'refund_msg'=>$remark ]); \app\common\model\order\Log::set($order['order_no'],"「{$item['item_name']}」发起退款,退款金额¥" . $refund_money); self::refund($order, $item, $refund_money, $remark,$saveEquity); // 订单退款后 // $data = ['order' => $order, 'item' => $item]; // \think\Hook::listen('order_refund_after', $data); } /** * 退款 * @param $order 所属订单 * @param $item 订单包含商品 * @param $refund_money 退款金额 * @param $remark 备注 * @param $saveEquity 退款后是否保留权益 * @return bool|void */ public static function refund($order, $item, $refund_money, $remark = '',$saveEquity=1) { if (is_object($order)) { $order = $order->toArray(); } if (is_object($item)) { $item = $item->toArray(); } // 生成退款单 $refundLog = new RefundLog(); $refundLog->order_no = $order['order_no']; $refundLog->refund_no = RefundLog::getNo($order['user_id']); $refundLog->order_item_id = $item['id']; $refundLog->pay_fee = $order['real_pay_price']; $refundLog->refund_fee = $refund_money; $refundLog->pay_type = $order['pay_type']; $refundLog->save_equity = $saveEquity; $notify_url = MODULE_URL .'/api/pay/notifyr/pay_type/' . $order['pay_type'] . '/platform/' . $order['source'] . "/uniacid/".UNIACID; if($refund_money <= 0){ $refundLog->save([ 'uniacid'=>UNIACID ]); self::refundFinish($order, $item, $refundLog); return true; } if (!empty($order['is_virtual_pay']) || $order['pay_type'] == 'virtual_pay') { return self::virtualPayRefund($order, $item, $refund_money, $remark, $saveEquity, $refundLog); } if ($order['pay_type'] == 'wechat' || $order['pay_type'] == 'alipay') { // 微信|支付宝退款 // 退款数据 $order_data = [ 'out_trade_no' => $order['order_no'] ]; if ($order['pay_type'] == 'wechat') { $total_fee = $order['real_pay_price'] * 100; $refund_fee = $refund_money * 100; $order_data = array_merge($order_data, [ 'out_refund_no' => $refundLog->refund_no, 'total_fee' => $total_fee, 'refund_fee' => $refund_fee, 'refund_desc' => $remark, ]); } else { $order_data = array_merge($order_data, [ 'out_request_no' => $refundLog->refund_no, 'refund_amount' => $refund_money, ]); } try{ // 接入支付账户池:若订单是通过账户池发起的,使用对应商户号退款 $forceAccountId = null; if (in_array($order['pay_type'], ['wechat', 'alipay'], true) && \app\admin\library\project\App::isInstall('paymentpool')) { $boundAccount = \app\common\library\app\paymentpool\Service::getAccountByOrder($order['order_no']); if ($boundAccount && !empty($boundAccount['id'])) { $forceAccountId = intval($boundAccount['id']); } } $pay = new \app\common\library\pay\PayService($order['pay_type'], $order['source'], $notify_url, $forceAccountId); $result = $pay->refund($order_data); }catch (\Exception $e){ throw new \app\common\exception\Exception($e->getMessage()); } if ($order['pay_type'] == 'wechat') { if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') { $refundLog->save([ 'uniacid'=>UNIACID ]); return true; } else { throw new \app\common\exception\Exception($result['return_msg']); } } else { if (isset($result['code']) && $result['code'] == '10000') { $refundLog->save([ 'uniacid'=>UNIACID ]); self::refundFinish($order, $item, $refundLog); return true; } else { $errMsg = isset($result['sub_msg']) ? $result['sub_msg'] : (isset($result['msg']) ? $result['msg'] : '退款失败'); throw new \app\common\exception\Exception($errMsg); } } } else if ($order['pay_type'] == 'balance') { // 余额退款 \app\common\model\User::money($refund_money, $order['user_id'], '订单退款,'.$order['order_no']); $refundLog->save([ 'uniacid'=>UNIACID ]); self::refundFinish($order, $item, $refundLog); return true; } else if ($order['pay_type'] == 'douyinpay') { $paymentConfig = \app\common\model\config\System::getConfig('douyinpay'); $douyinOrderData = \douyin\Byte::init($paymentConfig)->orderQuery($order['order_no']); if($douyinOrderData['err_no'] != 0){ throw new \app\common\exception\Exception("发起退款失败;" . ($douyinOrderData && isset($douyinOrderData['err_msg']) ? $douyinOrderData['err_msg'] : '')); } $item_order_id = ''; if(isset($douyinOrderData['data']) && !empty($douyinOrderData['data'])){ foreach ($douyinOrderData['data']['item_order_list'] as $skuItem){ if($skuItem['sku_id'] == $item['item_id']){ $item_order_id = $skuItem['item_order_id']; } } } if(!$item_order_id){ throw new \app\common\exception\Exception("发起退款失败;未查询到订单信息" ); } $params = []; $params['order_id'] = $order['transaction_id'];//交易系统侧订单号,长度 <= 64 byte $params['out_refund_no'] = $refundLog->refund_no;//开发者侧退款单号,长度 <= 64 byte $params['order_entry_schema'] = [ 'path'=>'pages/order/detail/detail', 'params'=>json_encode(['order_no'=>$params['out_refund_no']]) ]; $params['refund_total_amount'] = $refund_money * 100;//退款总金额,单位[分] // $params['notify_url'] = $notify_url; $params['refund_reason'] = [[ 'code'=>101, 'text'=>$remark ? $remark : '不想要了' ]];//退款原因,可填多个,不超过10个 $params['item_order_detail'] = [ [ 'item_order_id'=>$item_order_id, 'refund_amount'=>$refund_money * 100 ] ]; $data = \douyin\Byte::init($paymentConfig)->refund($params); if(isset($data['err_no']) && $data['err_no'] == 0){ $refundLog->refund_no = $data['data']['refund_id']; $refundLog->save([ 'uniacid'=>UNIACID ]); self::douyinRefundQuery($data['data']['refund_id'],$order, $item, $refundLog); return true; }else{ throw new \app\common\exception\Exception("发起退款失败;" . ($data && isset($data['err_msg']) ? $data['err_msg'] : '')); } } else if ($order->pay_type == 'score') { // 积分退款,暂不支持积分退款 } } /** * 虚拟支付退款处理 * 使用微信虚拟支付退款接口(refund_order)发起退款任务 * @param array $order 订单信息 * @param array $item 订单商品信息 * @param float $refund_money 退款金额(元) * @param string $remark 退款原因 * @param int $saveEquity 是否保留权益(1-保留 2-关闭) * @param RefundLog $refundLog 退款日志对象 * @return bool * @throws \Exception */ private static function virtualPayRefund($order, $item, $refund_money, $remark, $saveEquity, $refundLog) { try { if (is_object($order)) { $order = $order->toArray(); } if (!is_array($order)) { throw new \app\common\exception\Exception('订单数据格式异常'); } if (is_object($item)) { $item = $item->toArray(); } $virtualPayService = new \app\common\library\pay\VirtualPayService(); $orderNo = isset($order['order_no']) ? $order['order_no'] : ''; $userId = isset($order['user_id']) ? $order['user_id'] : 0; $openid = \app\common\model\order\VirtualPay::getOpenidByOrderNo($orderNo); \think\Log::info('[VirtualPayRefund] 开始退款 order_no=' . $orderNo . ', user_id=' . $userId . ', openid=' . ($openid ?: '空') . ', refund_money=' . $refund_money); if (empty($openid)) { \think\Log::warning('[VirtualPayRefund] 关联表无openid,尝试从oauth表获取 order_no=' . $orderNo . ', user_id=' . $userId); $oauth = \app\common\model\user\Oauth::where([ 'user_id' => $userId, 'provider' => 'Wechat', 'platform' => 'wxMiniProgram' ])->find(); if (!$oauth || empty($oauth->openid)) { \think\Log::error('[VirtualPayRefund] 无法获取用户openid order_no=' . $orderNo . ', user_id=' . $userId); throw new \app\common\exception\Exception('用户微信授权信息不存在,无法进行虚拟支付退款'); } $openid = $oauth->openid; \think\Log::info('[VirtualPayRefund] 从oauth表获取到openid order_no=' . $orderNo); } $realPayPrice = isset($order['real_pay_price']) ? $order['real_pay_price'] : 0; $orderResult = $virtualPayService->queryOrder($openid, $orderNo); if (isset($orderResult['errcode']) && $orderResult['errcode'] !== 0) { throw new \app\common\exception\Exception('查询虚拟支付订单失败:' . ($orderResult['errmsg'] ?? '未知错误')); } $leftFee = isset($orderResult['order']['left_fee']) ? intval($orderResult['order']['left_fee']) : intval($realPayPrice * 100); $refundFee = intval($refund_money * 100); if ($refundFee <= 0 || $refundFee > $leftFee) { throw new \app\common\exception\Exception('退款金额不合法,可退金额:' . ($leftFee / 100) . '元'); } $reasonMap = [ '产品问题' => '1', '售后问题' => '2', '用户主动退款' => '3', '价格问题' => '4', '其他' => '5', ]; $reasonCode = '0'; foreach ($reasonMap as $key => $code) { if (strpos($remark, $key) !== false) { $reasonCode = $code; break; } } $refundId = $refundLog->refund_no ?: ('VPR' . date('YmdHis') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT)); $result = $virtualPayService->refundOrder( $openid, $orderNo, $refundId, $leftFee, $refundFee, $reasonCode, '2' ); if (isset($result['errcode']) && $result['errcode'] !== 0) { throw new \app\common\exception\Exception('虚拟支付退款失败:' . ($result['errmsg'] ?? '未知错误')); } $refundOrderId = $result['refund_order_id'] ?? $refundId; $refundLog->refund_no = $refundOrderId; $refundLog->save([ 'uniacid' => UNIACID ]); $pollResult = $virtualPayService->pollRefundStatus($openid, $refundOrderId); \think\Log::info('[VirtualPayRefund] 退款轮询结果 order_no=' . $orderNo . ', refund_order_id=' . $refundOrderId . ', poll_result=' . json_encode($pollResult)); if (!$pollResult['success']) { $itemName = isset($item['item_name']) ? $item['item_name'] : '商品'; \app\common\model\order\Log::set($orderNo, "「{$itemName}」虚拟支付退款已发起,退款金额¥{$refund_money},退款单号:{$refundOrderId},状态:{$pollResult['message']}"); throw new \app\common\exception\Exception('虚拟支付退款已发起但未完成:' . $pollResult['message']); } self::refundFinish($order, $item, $refundLog); return true; } catch (\app\common\exception\Exception $e) { $refundLog->status = -1; $refundLog->save(); throw $e; } catch (\Exception $e) { $refundLog->status = -1; $refundLog->save(); throw new \app\common\exception\Exception('虚拟支付退款失败,请联系客服'); } catch (\Throwable $e) { $refundLog->status = -1; $refundLog->save(); throw new \app\common\exception\Exception('虚拟支付退款失败,请联系客服'); } } /** * 退款完成 * @param $order * @param $item * @param $refundLog * @return void */ public static function refundFinish($order, $item, $refundLog) { // 退款完成 $refundLog->status = 1; $refundLog->save(); //检查是否已全额退款,满足条件变更订单状态为已退款 $refundPrice = \app\admin\model\order\Item::where([ 'order_no'=>$order['order_no'] ])->sum('refund_price'); $orderItem = \app\admin\model\order\Item::get($item['id']); if($refundPrice >= $order['real_pay_price']){ \app\admin\model\order\Order::where([ 'order_no'=>$order['order_no'] ])->update([ 'status'=>\app\common\constant\order\Status::STATUS_REFUND ]); } if($refundLog->save_equity == 2 && $orderItem->refund_can_use != 2){ //关闭权益 $orderItem->refund_can_use = 2; if ($item['goods_type'] == 'composite') { // 组合商品:关闭所有子商品权限 if (\app\admin\library\project\App::isInstall('composite')) { $snapshoot = $item['snapshoot']; if (is_string($snapshoot)) { $snapshoot = json_decode($snapshoot, true); } if (!empty($snapshoot['sub_goods_list'])) { foreach ($snapshoot['sub_goods_list'] as $subGoods) { \app\common\model\user\Subscription::cancelUserSubscription( $item['user_id'], $subGoods['sub_goods_id'], $subGoods['sub_goods_type'], '' ); } } } } else { \app\common\model\user\Subscription::cancelUserSubscription($item['user_id'],$item['item_id'],$item['goods_type'],$item['extend_data']); } } // 全额退款时,组合商品返还销量 if ($item['goods_type'] == 'composite' && $refundPrice >= $order['real_pay_price']) { if (\app\admin\library\project\App::isInstall('composite')) { \app\common\behavior\order\Payed::compositeRefundHandle($item, $order, true); } } $orderItem->refund_status = \app\common\constant\order\Status::REFUND_STATUS_FINISH; $orderItem->save(); \app\common\model\order\Log::set($order['order_no'],"「{$item['item_name']}」退款成功"); } /** * * @param $refund_id * @param $order * @param $item * @param $refundLog * @param $num * @return void */ public static function douyinRefundQuery($refund_id,$order, $item, $refundLog,$num=0) { $paymentConfig = \app\common\model\config\System::getConfig('douyinpay'); $data = \douyin\Byte::init($paymentConfig)->refundQuery($refund_id); if(isset($data['err_no']) && $data['err_no'] == 0 && !empty($data['data']['refund_list'])){ switch ($data['data']['refund_list'][0]['refund_status']){ case 'SUCCESS': self::refundFinish($order, $item, $refundLog); break; case 'PROCESSING': sleep(2); if($num < 3){ self::douyinRefundQuery($refund_id,$order, $item, $refundLog,($num+1)); }else{ self::refundFinish($order, $item, $refundLog); } break; case 'FAIL': $refundLog->status = -1; $refundLog->save(); throw new \app\common\exception\Exception($data['data']['refund_list'][0]['message']); break; } }else{ $refundLog->status = -1; $refundLog->save(); } } }