diff --git a/backend/application/api/controller/Order.php b/backend/application/api/controller/Order.php index 6e34b3a..b295465 100644 --- a/backend/application/api/controller/Order.php +++ b/backend/application/api/controller/Order.php @@ -250,6 +250,19 @@ class Order extends Api public function payNotify() { header('Content-Type: application/json; charset=utf-8'); + // 原始报文(用于排查微信是否真的回调到本地址) + $rawBody = file_get_contents('php://input'); + $notifyHeaders = []; + foreach ($_SERVER as $key => $value) { + if (stripos($key, 'HTTP_WECHATPAY') !== false || in_array($key, ['CONTENT_TYPE', 'REQUEST_METHOD'])) { + $notifyHeaders[$key] = $value; + } + } + Log::info('微信支付回调[进入] uri=' . ($_SERVER['REQUEST_URI'] ?? '') . ' ' . json_encode([ + 'headers' => $notifyHeaders, + 'body' => is_string($rawBody) ? $rawBody : '', + ], JSON_UNESCAPED_UNICODE)); + try { // PSR-7 响应(SDK 已完成验签/解密,业务失败返回 500 由微信重试) $response = OrderService::payNotify(); @@ -257,10 +270,16 @@ class Order extends Api foreach ($response->getHeaders() as $name => $values) { header($name . ': ' . implode(', ', $values)); } - echo $response->getBody()->getContents(); + $body = $response->getBody()->getContents(); + Log::info('微信支付回调[应答] http_code=' . $response->getStatusCode() . ' body=' . $body); + echo $body; exit; - } catch (\Exception $e) { - Log::info('微信回调信息信息_result_error:' . json_encode(['msg' => $e->getMessage()], JSON_UNESCAPED_UNICODE)); + } catch (\Throwable $e) { + // 注意:EasyWeChat 的 serve() 只捕获 Exception,TypeError/Error 会冒泡到这里 + Log::error('微信回调信息_result_error:' . json_encode([ + 'msg' => $e->getMessage(), + 'file' => $e->getFile() . ':' . $e->getLine(), + ], JSON_UNESCAPED_UNICODE)); http_response_code(500); echo json_encode(['code' => 'FAIL', 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE); exit; diff --git a/backend/application/common/service/EasyWeChatService.php b/backend/application/common/service/EasyWeChatService.php index 708db14..d84c8ac 100644 --- a/backend/application/common/service/EasyWeChatService.php +++ b/backend/application/common/service/EasyWeChatService.php @@ -232,15 +232,69 @@ class EasyWeChatService */ public function handlePayNotify(\Closure $paidHandler) { + $request = $this->payApp->getRequest(); + $traceId = date('YmdHis') . substr(md5(uniqid('', true)), 0, 6); + + // 【日志1】回调原始报文:确认微信有没有真的打过来、打到哪个地址 + Log::info('微信支付回调[收到请求] trace=' . $traceId . ' ' . json_encode([ + 'method' => $request->getMethod(), + 'uri' => (string)$request->getUri(), + 'headers' => self::getNotifyHeaders($request), + 'body' => (string)$request->getBody(), + ], JSON_UNESCAPED_UNICODE)); + $server = $this->payApp->getServer(); - $server->handlePaid(function (PayMessage $message, \Closure $next) use ($paidHandler) { + $server->handlePaid(function (PayMessage $message, \Closure $next) use ($paidHandler, $traceId) { + // 【日志2】解密后的业务数据(含 out_trade_no / trade_state / amount) + Log::info('微信支付回调[解密数据] trace=' . $traceId . ' ' . json_encode($message->toArray(), JSON_UNESCAPED_UNICODE)); + // 回调签名验证(官方推荐,验签失败抛异常 -> serve 返回失败,微信将重试) - $this->payApp->getValidator()->validate($this->payApp->getRequest()); - $paidHandler($message); + try { + $this->payApp->getValidator()->validate($this->payApp->getRequest()); + Log::info('微信支付回调[验签通过] trace=' . $traceId); + } catch (\Throwable $e) { + Log::error('微信支付回调[验签失败] trace=' . $traceId . ' msg=' . $e->getMessage()); + throw $e; + } + + try { + $paidHandler($message, $traceId); + } catch (\Throwable $e) { + Log::error('微信支付回调[业务处理异常] trace=' . $traceId . ' msg=' . $e->getMessage() + . ' file=' . $e->getFile() . ':' . $e->getLine()); + throw $e; + } + + Log::info('微信支付回调[业务处理完成] trace=' . $traceId); return $next($message); }); - return $server->serve(); + $response = $server->serve(); + // 注意:先转字符串(内部会 seek(0))并 rewind,保证控制器后续仍能读取 body 回给微信 + $responseBody = (string)$response->getBody(); + $response->getBody()->rewind(); + + // 【日志3】应答给微信的结果:status=200 且 code=SUCCESS 才算回调成功 + Log::info('微信支付回调[响应微信] trace=' . $traceId . ' status=' . $response->getStatusCode() . ' body=' . $responseBody); + + return $response; + } + + /** + * 提取回调请求中与验签相关的请求头(便于排查签名/证书问题) + * @param \Psr\Http\Message\ServerRequestInterface $request + * @return array + */ + private static function getNotifyHeaders($request): array + { + $headers = []; + foreach ($request->getHeaders() as $name => $values) { + if (stripos($name, 'wechatpay') !== false || in_array(strtolower($name), ['content-type', 'user-agent'])) { + $headers[$name] = implode(', ', $values); + } + } + + return $headers; } } diff --git a/backend/application/common/service/OrderService.php b/backend/application/common/service/OrderService.php index 62d83b9..1dc3848 100644 --- a/backend/application/common/service/OrderService.php +++ b/backend/application/common/service/OrderService.php @@ -5,6 +5,7 @@ namespace app\common\service; use think\Cache; use think\Db; use think\Log; +use EasyWeChat\Pay\Message as PayMessage; use app\common\model\Orders; use app\common\traits\ServiceTrait; use think\Exception; @@ -271,6 +272,14 @@ class OrderService throw new \Exception('该订单不存在'); } $orderData = $orderInfo->toArray(); + Log::info('微信下单[发起支付] ' . json_encode([ + 'order_no' => $orderNo, + 'user_id' => $userId, + 'order_status' => $orderData['order_status'] ?? null, + 'status_text' => self::ORDER_STATUS_MAP[$orderData['order_status'] ?? -1] ?? '未知', + 'payment_amount' => $orderData['payment_amount'] ?? 0, + 'total_amount' => $orderData['total_amount'] ?? 0, + ], JSON_UNESCAPED_UNICODE)); $orderData += [ 'channel' => 'wechat', 'tradeNo' => $orderData['order_no'], @@ -278,6 +287,10 @@ class OrderService 'transactionId' => 0, 'payInfo' => self::pay($orderInfo->order_no, $orderInfo->total_amount, $openid), //统一下单接口 ]; + Log::info('微信下单[下单成功] ' . json_encode([ + 'order_no' => $orderNo, + 'payInfo' => $orderData['payInfo'], + ], JSON_UNESCAPED_UNICODE)); return $orderData; @@ -393,46 +406,117 @@ class OrderService { $service = new EasyWeChatService(); - return $service->handlePayNotify(function (PayMessage $message) { - Log::info('微信回调信息_request:' . json_encode($message->getOriginalAttributes() ?? [], JSON_UNESCAPED_UNICODE)); + return $service->handlePayNotify(function (PayMessage $message, $traceId = '') { + $prefix = '微信支付回调[业务] trace=' . $traceId . ' '; $order_no = $message->out_trade_no ?? ''; + $total = $message->amount['total'] ?? 0; + + Log::info($prefix . '支付成功通知: ' . json_encode([ + 'order_no' => $order_no, + 'transaction_id' => $message->transaction_id ?? '', + 'trade_state' => $message->trade_state ?? '', + 'trade_type' => $message->trade_type ?? '', + 'amount_fen' => $total, + 'amount_yuan' => sprintf('%.2f', $total / 100), + 'success_time' => $message->success_time ?? '', + ], JSON_UNESCAPED_UNICODE)); + $orderInfo = OrderService::getOne(['order_no' => $order_no]); if (!$orderInfo) { + Log::error($prefix . '订单不存在: ' . json_encode(['order_no' => $order_no], JSON_UNESCAPED_UNICODE)); throw new \Exception('获取订单数据失败'); } + + Log::info($prefix . '订单当前状态: ' . json_encode([ + 'order_id' => $orderInfo->id, + 'order_no' => $order_no, + 'order_status' => $orderInfo->order_status, + 'status_text' => self::ORDER_STATUS_MAP[$orderInfo->order_status] ?? '未知', + 'order_type' => $orderInfo->order_type, + 'parent_no' => $orderInfo->parent_order_no ?? '', + 'payment_amount'=> $orderInfo->payment_amount, + ], JSON_UNESCAPED_UNICODE)); + //金额校验(v3 通知金额单位为分) - $total = $message->amount['total'] ?? 0; $checkMoney = sprintf('%.1f', $total / 100) == sprintf('%.1f', $orderInfo->payment_amount); if (!$checkMoney) { + Log::error($prefix . '金额校验失败: ' . json_encode([ + 'notify_amount' => sprintf('%.2f', $total / 100), + 'order_amount' => sprintf('%.2f', $orderInfo->payment_amount), + ], JSON_UNESCAPED_UNICODE)); throw new \Exception('金额错误:' . sprintf('%.2f', $total / 100) . '----' . sprintf('%.2f', $orderInfo->payment_amount)); } if (($message->trade_state ?? '') != 'SUCCESS') { + Log::error($prefix . '支付状态异常: trade_state=' . ($message->trade_state ?? '')); throw new \Exception('支付失败:' . ($message->trade_state ?? '')); } - Log::info('微信回调信息_result:' . json_encode(['order_no' => $order_no, 'trade_state' => $message->trade_state], JSON_UNESCAPED_UNICODE)); + //如果为待支付,才可更新为已支付。防止重复更新状态,导致重复发货。 - if ($orderInfo->order_status == self::ORDER_STATUS_WAIT_PAY) { - $paymentData = [ - 'order_no' => $order_no, - 'pay_status' => 'SUCCESS', - 'amount' => $total / 100, - 'currency' => $message->amount['currency'] ?? 'CNY', - 'pay_type' => '1', // 1微信 2支付宝 - 'pay_way' => $message->trade_type ?? 'JSAPI', - 'trace_no' => $message->transaction_id ?? '', - 'pay_time' => time(), - 'pay_success_time' => strtotime((string)($message->success_time ?? '')) ?: time(), - ]; - Db::transaction(function () use ($order_no, $paymentData, $orderInfo) { - self::where('order_no', $order_no)->update(['order_status' => self::ORDER_STATUS_WAIT_SHIP]); - OrderPaymentService::create($paymentData); - if ($orderInfo->order_type == 1) { + if ($orderInfo->order_status != self::ORDER_STATUS_WAIT_PAY) { + Log::info($prefix . '订单已处理过,跳过状态更新(幂等): ' . json_encode([ + 'order_no' => $order_no, + 'order_status' => $orderInfo->order_status, + 'status_text' => self::ORDER_STATUS_MAP[$orderInfo->order_status] ?? '未知', + ], JSON_UNESCAPED_UNICODE)); + + return true; + } + + $paymentData = [ + 'order_no' => $order_no, + 'pay_status' => 'SUCCESS', + 'amount' => $total / 100, + 'currency' => $message->amount['currency'] ?? 'CNY', + 'pay_type' => '1', // 1微信 2支付宝 + 'pay_way' => $message->trade_type ?? 'JSAPI', + 'trace_no' => $message->transaction_id ?? '', + 'pay_time' => time(), + 'pay_success_time' => strtotime((string)($message->success_time ?? '')) ?: time(), + ]; + Log::info($prefix . '开始更新订单状态: ' . json_encode($paymentData, JSON_UNESCAPED_UNICODE)); + + try { + Db::transaction(function () use ($order_no, $paymentData, $orderInfo, $prefix) { + $updateRows = self::where('order_no', $order_no)->update(['order_status' => self::ORDER_STATUS_WAIT_SHIP]); + Log::info($prefix . '订单状态更新: order_no=' . $order_no + . ' 10(待支付) -> ' . self::ORDER_STATUS_WAIT_SHIP . '(待发货) 影响行数=' . $updateRows); + + $payment = OrderPaymentService::create($paymentData); + Log::info($prefix . '支付流水写入: ' . json_encode([ + 'payment_id' => $payment->id ?? 0, + 'order_no' => $payment->order_no ?? '', + 'pay_status' => $payment->pay_status ?? '', + 'amount' => $payment->amount ?? 0, + 'trace_no' => $payment->trace_no ?? '', + ], JSON_UNESCAPED_UNICODE)); + + if ($orderInfo->order_type == self::ORDER_TYPE_FIRST_PAYMENT) { + Log::info($prefix . '需求首付款订单,处理合同成交: parent_order_no=' . $orderInfo->parent_order_no); DemandService::customerContractFinishedDeal($orderInfo->parent_order_no); - } elseif ($orderInfo->order_type == 2) { + } elseif ($orderInfo->order_type == self::ORDER_TYPE_FINAL_PAYMENT) { + Log::info($prefix . '需求尾款订单,处理验收结算: parent_order_no=' . $orderInfo->parent_order_no); DemandService::customerWorkAcceptFinishedDeal($orderInfo->parent_order_no); } }); + } catch (\Throwable $e) { + Log::error($prefix . '事务执行失败已回滚: msg=' . $e->getMessage() + . ' file=' . $e->getFile() . ':' . $e->getLine()); + throw $e; } + + // 复核:确认状态与支付流水真的落库了 + $newOrder = OrderService::getOne(['order_no' => $order_no]); + $payment = OrderPaymentService::getOne(['order_no' => $order_no]); + Log::info($prefix . '回调处理成功: ' . json_encode([ + 'order_no' => $order_no, + 'new_order_status'=> $newOrder->order_status ?? null, + 'new_status_text' => self::ORDER_STATUS_MAP[$newOrder->order_status ?? -1] ?? '未知', + 'payment_id' => $payment->id ?? 0, + 'payment_status' => $payment->pay_status ?? '', + 'trace_no' => $payment->trace_no ?? '', + ], JSON_UNESCAPED_UNICODE)); + + return true; }); } @@ -460,14 +544,22 @@ class OrderService public static function pay($order_sn, $total_fee, $openid, string $body = '软件产品服务') { if (!($body && $order_sn && $total_fee && $openid)) { + Log::error('微信下单[参数错误] ' . json_encode([ + 'body' => $body, + 'order_sn' => $order_sn, + 'total_fee' => $total_fee, + 'openid' => $openid, + ], JSON_UNESCAPED_UNICODE)); throw new \Exception('支付参数错误2'); } $EasyWeChatService = new EasyWeChatService(); $payRes = $EasyWeChatService->unify($body, $order_sn, $total_fee, $openid); if (!($payRes && !empty($payRes['prepay_id']))) { + Log::error('微信下单[统一下单失败] order_sn=' . $order_sn . ' res=' . json_encode($payRes, JSON_UNESCAPED_UNICODE)); throw new \Exception('统一下单接口调用失败:' . ($payRes['message'] ?? ($payRes['errmsg'] ?? '未知错误'))); } + Log::info('微信下单[prepay_id] order_sn=' . $order_sn . ' prepay_id=' . $payRes['prepay_id']); $payData = $EasyWeChatService->sdkConfig($payRes['prepay_id']); return $payData;