481 lines
18 KiB
PHP
481 lines
18 KiB
PHP
<?php
|
|
namespace app\admin\model\order;
|
|
use think\Model;
|
|
use app\admin\model\order\RefundLog;
|
|
class Refund extends Model
|
|
{
|
|
|
|
// 表名
|
|
protected $name = 'order_evaluate';
|
|
|
|
// 自动写入时间戳字段
|
|
protected $autoWriteTimestamp = 'integer';
|
|
|
|
// 定义时间戳字段名
|
|
protected $createTime = 'createtime';
|
|
protected $updateTime = false;
|
|
protected $deleteTime = false;
|
|
|
|
// 追加属性
|
|
protected $append = [
|
|
|
|
];
|
|
|
|
|
|
// 开始退款
|
|
public static function startRefund($order, $item, $refund_money, $remark = '',$saveEquity=1) {
|
|
if (is_object($order)) {
|
|
$order = $order->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();
|
|
}
|
|
}
|
|
}
|