Files

406 lines
16 KiB
PHP
Raw Permalink 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 app\callback\library\app\wxapp;
use think\Log;
use app\common\model\order\Order;
use app\common\model\order\Log as OrderLog;
use app\common\constant\order\Status as OrderStatus;
use app\common\library\pay\VirtualPayService;
/**
* 微信小程序消息推送事件处理器
* 处理虚拟支付相关的4种推送事件:
* 1. xpay_goods_deliver_notify - 道具发货推送(现金购买道具支付成功)
* 2. xpay_coin_pay_notify - 代币支付推送(代币扣减成功)
* 3. xpay_refund_notify - 退款推送(退款完成后)
* 4. xpay_complaint_notify - 用户投诉推送(用户发起投诉)
*/
class Notify
{
/**
* 事件分发处理
* @param string $event 事件类型
* @param array $message 消息内容
* @return array|null 返回自定义响应(iOS退款问询需要),其他事件返回null
* @throws \Exception
*/
public function handle($event, $message)
{
switch ($event) {
case 'xpay_goods_deliver_notify':
$this->handleGoodsDeliver($message);
break;
case 'xpay_coin_pay_notify':
$this->handleCoinPay($message);
break;
case 'xpay_refund_notify':
$this->handleRefund($message);
break;
case 'xpay_complaint_notify':
$this->handleComplaint($message);
break;
case 'xpay_subscribe_ios_refund_query_notify':
return $this->handleIosRefundQuery($message);
default:
Log::warning("[WxappNotify] 未知事件类型: " . $event);
break;
}
return null;
}
/**
* 道具发货推送处理
* 用户通过现金购买道具且支付成功后触发
* 需要通知微信已发货完成(调用notifyProvideGoods
* @param array $message 消息内容
* @return void
*/
private function handleGoodsDeliver($message)
{
$outTradeNo = $message['OutTradeNo'] ?? '';
$openid = $message['OpenId'] ?? '';
$env = $message['Env'] ?? 0;
Log::info("[WxappNotify] 道具发货推送: order_no={$outTradeNo}, openid={$openid}, env={$env}");
if (empty($outTradeNo)) {
Log::error("[WxappNotify] 道具发货推送: 订单号为空");
throw new \Exception('订单号为空');
}
$order = Order::orderNoGetOrder($outTradeNo);
if (!$order) {
Log::error("[WxappNotify] 道具发货推送: 订单不存在 order_no={$outTradeNo}");
throw new \Exception('订单不存在');
}
if ($order['status'] !== OrderStatus::STATUS_UNPAID && $order['status'] !== OrderStatus::STATUS_PAID) {
Log::info("[WxappNotify] 道具发货推送: 订单状态无需处理 status={$order['status']}");
return;
}
if ($order['status'] === OrderStatus::STATUS_UNPAID) {
$notify = [
'order_no' => $outTradeNo,
'transaction_id' => $message['WeChatPayInfo']['TransactionId'] ?? '',
'notify_time' => date('Y-m-d H:i:s'),
'buyer_email' => $openid,
'pay_fee' => isset($message['GoodsInfo']['ActualPrice']) ? $message['GoodsInfo']['ActualPrice'] / 100 : $order['real_price'],
'pay_type' => 'virtual_pay',
];
$notify['payment_json'] = json_encode($message, JSON_UNESCAPED_UNICODE);
Order::paySuccess($order, $notify);
OrderLog::set($outTradeNo, "虚拟支付道具发货通知-订单已完成支付");
}
$this->notifyProvideGoods($outTradeNo, $env);
}
/**
* 代币支付推送处理
* 用户代币扣减成功后触发(代币支付场景,无需再调用notifyProvideGoods
* 参考Pay.php的successHandle方法实现统一的订单支付成功逻辑
* @param array $message 消息内容
* @return void
*/
private function handleCoinPay($message)
{
$outTradeNo = $message['OutTradeNo'] ?? '';
$openid = $message['OpenId'] ?? '';
$env = $message['Env'] ?? 0;
Log::info("[WxappNotify] 代币支付推送: order_no={$outTradeNo}, openid={$openid}, env={$env}");
if (empty($outTradeNo)) {
Log::error("[WxappNotify] 代币支付推送: 订单号为空");
throw new \Exception('订单号为空');
}
$order = Order::orderNoGetOrder($outTradeNo);
if (!$order) {
Log::error("[WxappNotify] 代币支付推送: 订单不存在 order_no={$outTradeNo}");
throw new \Exception('订单不存在');
}
if ($order['status'] !== OrderStatus::STATUS_UNPAID) {
Log::info("[WxappNotify] 代币支付推送: 订单已处理 status={$order['status']}");
return;
}
$coinInfo = $message['CoinInfo'] ?? [];
$wechatPayInfo = $message['WeChatPayInfo'] ?? [];
$payFee = isset($coinInfo['ActualPrice']) ? floatval($coinInfo['ActualPrice']) / 100 : floatval($order['real_price']);
$transactionId = $wechatPayInfo['TransactionId'] ?? '';
$coinQuantity = isset($coinInfo['Quantity']) ? intval($coinInfo['Quantity']) : 0;
$origPrice = isset($coinInfo['OrigPrice']) ? floatval($coinInfo['OrigPrice']) / 100 : 0;
$attach = $coinInfo['Attach'] ?? '';
$notify = [
'order_no' => $outTradeNo,
'transaction_id' => $transactionId,
'notify_time' => date('Y-m-d H:i:s'),
'buyer_email' => $openid,
'pay_fee' => $payFee,
'pay_type' => 'virtual_pay',
'payment_json' => json_encode($message, JSON_UNESCAPED_UNICODE),
'real_pay_price' => $payFee,
'pay_time' => time(),
'coin_quantity' => $coinQuantity,
'orig_price' => $origPrice,
'attach' => $attach,
'env' => intval($env),
];
try {
Order::paySuccess($order, $notify);
OrderLog::set($outTradeNo, "虚拟支付代币扣减通知-订单已完成支付, 代币数量:{$coinQuantity}, 实付金额:{$payFee}元");
Log::info("[WxappNotify] 代币支付处理成功: order_no={$outTradeNo}, pay_fee={$payFee}, coin_quantity={$coinQuantity}");
} catch (\Exception $e) {
Log::error("[WxappNotify] 代币支付处理异常: " . $e->getMessage() . " order_no={$outTradeNo}");
OrderLog::set($outTradeNo, "虚拟支付代币支付处理异常:{$e->getMessage()}");
throw $e;
}
}
/**
* 退款推送处理
* 退款完成后触发,根据退款结果更新订单状态和退款记录
* @param array $message 消息内容
* @return void
*/
private function handleRefund($message)
{
$mchOrderId = $message['MchOrderId'] ?? '';
$mchRefundId = $message['MchRefundId'] ?? '';
$wxRefundId = $message['WxRefundId'] ?? '';
$retCode = $message['RetCode'] ?? -1;
$retMsg = $message['RetMsg'] ?? '';
$refundFee = $message['RefundFee'] ?? 0;
$openid = $message['OpenId'] ?? '';
Log::info("[WxappNotify] 退款推送: order_no={$mchOrderId}, mch_refund_id={$mchRefundId}, wx_refund_id={$wxRefundId}, retCode={$retCode}, refundFee={$refundFee}");
if (empty($mchOrderId)) {
Log::error("[WxappNotify] 退款推送: 订单号为空");
throw new \Exception('订单号为空');
}
$order = Order::orderNoGetOrder($mchOrderId);
if (!$order) {
Log::error("[WxappNotify] 退款推送: 订单不存在 order_no={$mchOrderId}");
throw new \Exception('订单不存在');
}
$orderItems = \app\admin\model\order\Item::where(['order_no' => $mchOrderId])->select()->toArray();
if (empty($orderItems)) {
Log::error("[WxappNotify] 退款推送: 订单无商品项 order_no={$mchOrderId}");
throw new \Exception('订单商品不存在');
}
foreach ($orderItems as $item) {
$this->processRefundItem($order, $item, $message, $retCode, $retMsg, $refundFee, $mchRefundId, $wxRefundId, $openid);
}
}
/**
* 处理单个订单商品的退款逻辑
* @param array $order 订单信息
* @param array $item 订单商品信息
* @param array $message 微信推送的完整消息
* @param int $retCode 退款结果码(0-成功)
* @param string $retMsg 退款结果详情
* @param int $refundFee 退款金额(单位:分)
* @param string $mchRefundId 商户退款单号
* @param string $wxRefundId 微信退款单号
* @param string $openid 用户openid
* @return void
*/
private function processRefundItem($order, $item, $message, $retCode, $retMsg, $refundFee, $mchRefundId, $wxRefundId, $openid)
{
$refundLog = \app\admin\model\order\RefundLog::where([
'order_no' => $order['order_no'],
'order_item_id' => $item['id']
])->order('id', 'desc')->find();
if (!$refundLog) {
Log::warning("[WxappNotify] 退款推送: 未找到退款记录,创建新记录 order_no={$order['order_no']}, item_id={$item['id']}");
$refundLog = new \app\admin\model\order\RefundLog();
$refundLog->order_no = $order['order_no'];
$refundLog->order_item_id = $item['id'];
$refundLog->refund_no = $mchRefundId ?: ('WX' . date('YmdHis') . mt_rand(1000, 9999));
$refundLog->pay_fee = $order['real_pay_price'];
$refundLog->refund_fee = $refundFee / 100;
$refundLog->pay_type = $order['pay_type'];
$refundLog->save_equity = 2;
$refundLog->save();
}
if ($retCode === 0) {
try {
\app\admin\model\order\Refund::refundFinish($order, $item, $refundLog);
OrderLog::set($order['order_no'], "虚拟支付退款成功通知-商户退款单号:{$mchRefundId}, 微信退款单号:{$wxRefundId}, 退款金额:{$refundFee}分, 用户:{$openid}");
Log::info("[WxappNotify] 退款处理成功: order_no={$order['order_no']}, item_id={$item['id']}, refund_fee={$refundFee}分");
} catch (\Exception $e) {
Log::error("[WxappNotify] 退款处理异常: " . $e->getMessage() . " order_no={$order['order_no']}");
OrderLog::set($order['order_no'], "虚拟支付退款处理异常:{$e->getMessage()}");
}
} else {
$refundLog->status = -1;
$refundLog->save();
OrderLog::set($order['order_no'], "虚拟支付退款失败通知-错误码:{$retCode}, 原因:{$retMsg}, 商户退款单号:{$mchRefundId}");
Log::error("[WxappNotify] 退款失败: order_no={$order['order_no']}, retCode={$retCode}, retMsg={$retMsg}");
}
}
/**
* 用户投诉推送处理
* 用户发起投诉后触发
* @param array $message 消息内容
* @return void
*/
private function handleComplaint($message)
{
$mchOrderId = $message['MchOrderId'] ?? '';
$complaintId = $message['ComplaintId'] ?? '';
$complaintDetail = $message['ComplaintDetail'] ?? '';
Log::info("[WxappNotify] 用户投诉推送: order_no={$mchOrderId}, complaintId={$complaintId}");
$order = null;
if (!empty($mchOrderId)) {
$order = Order::orderNoGetOrder($mchOrderId);
}
Log::warning("[WxappNotify] 用户投诉详情: " . json_encode([
'order_no' => $mchOrderId,
'complaint_id' => $complaintId,
'complaint_detail' => $complaintDetail,
'openid' => $message['OpenId'] ?? '',
'wx_order_id' => $message['WxOrderId'] ?? '',
'complaint_time' => $message['ComplaintTime'] ?? 0,
], JSON_UNESCAPED_UNICODE));
if ($order) {
OrderLog::set($mchOrderId, "用户发起投诉-投诉单号:{$complaintId}, 投诉内容:{$complaintDetail}");
}
}
/**
* 通知微信已发货完成(现金订单)
* @param string $outTradeNo 订单号
* @param int $env 环境(0-正式 1-沙箱)
* @return void
*/
private function notifyProvideGoods($outTradeNo, $env = 0)
{
try {
$virtualPayService = new VirtualPayService();
$result = $virtualPayService->notifyProvideGoods($outTradeNo);
if (isset($result['errcode']) && $result['errcode'] === 0) {
Log::info("[WxappNotify] 通知发货成功: order_no={$outTradeNo}");
} else {
Log::error("[WxappNotify] 通知发货失败: " . json_encode($result));
throw new \Exception('通知发货失败: ' . ($result['errmsg'] ?? '未知错误'));
}
} catch (\Exception $e) {
Log::error("[WxappNotify] 通知发货异常: " . $e->getMessage());
throw $e;
}
}
/**
* iOS退款问询处理
* Apple支付用户申请退款时,微信会推送退款问询,开发者需在3秒内响应
* @param array $message 消息内容
* @return array 响应数据
*/
private function handleIosRefundQuery($message)
{
$refundTime = $message['refund_time'] ?? '';
$orderTime = $message['order_time'] ?? '';
$channelBill = $message['channel_bill'] ?? '';
$bundleId = $message['bundleid'] ?? '';
$productId = $message['product_id'] ?? '';
$pCount = $message['p_count'] ?? '';
$refundRequestReason = $message['refund_request_reason'] ?? '';
$provideStatus = $message['provide_status'] ?? '';
$payOrderId = $message['pay_order_id'] ?? '';
Log::info("[WxappNotify] iOS退款问询: pay_order_id={$payOrderId}, order_time={$orderTime}, refund_time={$refundTime}, channel_bill={$channelBill}, product_id={$productId}, reason={$refundRequestReason}, provide_status={$provideStatus}");
$config = \app\common\model\config\System::getConfig('virtual_pay');
$iosRefundPolicy = $config['ios_refund_policy'] ?? '2';
$resultCode = $this->calculateIosRefundResult($iosRefundPolicy, $orderTime, $payOrderId);
$resultInfo = $resultCode === 0 ? '同意退款' : '拒绝退款';
$evidence = $this->buildIosRefundEvidence($iosRefundPolicy, $orderTime, $payOrderId, $resultCode);
Log::info("[WxappNotify] iOS退款问询响应: pay_order_id={$payOrderId}, policy={$iosRefundPolicy}, result_code={$resultCode}, result_info={$resultInfo}");
return [
'result_code' => $resultCode,
'result_info' => $resultInfo,
'evidence' => $evidence,
];
}
/**
* 根据策略计算iOS退款问询结果
* @param string $policy 策略:1-任何时间拒绝退款,2-任何时间同意退款
* @param string $orderTime 订单时间(Unix时间戳)
* @param string $payOrderId 支付订单号
* @return int 0-建议退款,1-拒绝退款
*/
private function calculateIosRefundResult($policy, $orderTime, $payOrderId)
{
switch ($policy) {
case '1':
return 1;
case '2':
default:
return 0;
}
}
/**
* 构建iOS退款问询决策凭据
* @param string $policy 策略
* @param string $orderTime 订单时间
* @param string $payOrderId 支付订单号
* @param int $resultCode 结果码
* @return string
*/
private function buildIosRefundEvidence($policy, $orderTime, $payOrderId, $resultCode)
{
$policyDesc = [
'1' => '任何时间,建议苹果拒绝退款',
'2' => '任何时间,建议苹果同意退款',
];
$orderTimestamp = intval($orderTime);
$evidence = sprintf(
'策略:%s | 订单号:%s | 订单时间:%s | 决策:%s | 决策时间:%s',
$policyDesc[$policy] ?? '默认策略',
$payOrderId,
$orderTime ? date('Y-m-d H:i:s', $orderTimestamp) : '未知',
$resultCode === 0 ? '同意退款' : '拒绝退款',
date('Y-m-d H:i:s')
);
return $evidence;
}
}