extensionPaymentEntity->where([ 'order_id' => $order['id'], 'pay_type' => self::PAYMENT_TYPE, 'biz_type' => $bizType, ])->findOrEmpty(); if (!$payment->isEmpty()) { // todo 判断订单是否支付成功 // 支付时间结束关闭订单 // 订单未支付可重新支付 // 交易已关闭请重新下单 //订单已付款请勿重复操作 // 订单已退款请重新下单 //订单已撤销请重新下单 // 订单支付中请稍后再试 // 订单支付失败请重新下单 throw new \RuntimeException('开发测试中,请稍后再试'); } $payment->transaction(function () use ($bizType, $order, $payment) { $payment->save([ 'user_id' => $order['user_id'], 'order_id' => $order['id'], 'order_sn' => $order['order_sn'], 'biz_type' => $bizType, 'pay_type' => self::PAYMENT_TYPE, 'amount' => $order['pay_amount'], 'status' => PaymentStatusEnum::PENDING ]); $expireTime = time() + 3600; $payment->expire_time = $expireTime; $openid = WechatUser::openid($order['user_id']); if ($openid === null) { throw_logic_exception('用户需要先使用微信登录绑定微信身份'); } $payment->payment_param = $this->wechatPay( openid: $openid, outTradeNo: $payment['out_trade_no'], total: (int)($payment['amount'] * 100), description: ($order['description'] ?? '') ?: '订单:' . $order['order_sn'], expireTime: $expireTime ); $payment->payment_param = $this->paymentParams($payment->payment_param->prepay_id); $payment->save(); }); return new PaymentResponse(orderNo: $payment->out_trade_no, type: self::PAYMENT_TYPE, raw: $payment->toArray()); } /** * 支付成功通知 * * @param array{headers: array, inBody: string} $request * @return PaymentNotifyResult * @throws \Exception] */ public function notify(array $request): PaymentNotifyResult { $inBody = $request['inBody']; $data = $this->notifyService->transactionSuccess($request['headers'], $request['inBody']); if ($inBody['event_type'] == 'TRANSACTION.SUCCESS') { // 交易成功 Log::debug(__METHOD__ . json_encode($data)); $payment = $this->extensionPaymentEntity->where([ 'out_trade_no' => $data['out_trade_no'], ])->findOrEmpty(); if ($payment->isEmpty()) { throw new \RuntimeException('订单不存在或已结束'); } $queryResult = $this->query($payment['id']); return new PaymentNotifyResult( orderNo: $queryResult->orderNo, transactionId: $data['transaction_id'], amount: $queryResult->amount, status: $queryResult->status, raw: $data ); } else if ($inBody['event_type'] == 'MCHTRANSFER.BILL.FINISHED') { // 转账完成 $transferBill = $this->wechatpayTransferBillEntity->where('out_bill_no', $data['out_bill_no'])->findOrEmpty(); if ($transferBill->isEmpty()) { throw new \RuntimeException('转账单不存在'); } $this->wechatpayTransferBillEntity->refreshTransferBill($transferBill->id); return new PaymentNotifyResult( orderNo: $transferBill['out_bill_no'], transactionId: $transferBill['transfer_bill_no'], amount: round($transferBill['transfer_amount'] / 100, 2), status: PaymentStatusEnum::SUCCESS, raw: $data ); } Log::warning('Not implemented: ' . $inBody['event_type']. ' ' . json_encode($data)); throw_logic_exception('Not implemented: ' . $inBody['event_type']); } public function query(int $recordID): PaymentQueryResult { $payment = $this->extensionPaymentEntity->findOrEmpty($recordID); if ($payment->status === PaymentStatusEnum::PENDING) { try { $paymentResult = $this->queryByOutTradeNo($payment['out_trade_no']); } catch (ClientException $e) { if ($e->getCode() === 404) { throw new NotFoundException(sprintf('订单%s不存在', $payment['out_trade_no'])); } throw $e; } $payment->payment_result = $paymentResult; if ($paymentResult->trade_state === 'SUCCESS') { $payment->transaction_id = $paymentResult->transaction_id; $payment->payment_time = strtotime($paymentResult->success_time); $payment->status = PaymentStatusEnum::SUCCESS; $payment->save(); Event::trigger(new PaymentSuccessEvent($payment['order_sn'], self::PAYMENT_TYPE, $payment->toArray(), $payment->biz_type)); } else { throw_logic_exception($paymentResult->trade_state_desc); } } return new PaymentQueryResult( orderNo: $payment['out_trade_no'], status: $payment['status'], amount: (float)$payment['amount'], raw: $payment->toArray() ); } public function refund(int $recordID, PaymentRefundRequest $param): PaymentRefundResult { $payment = $this->extensionPaymentEntity->find($recordID); $refund = Db::transaction(function () use ($param, $payment) { $refund = $this->extensionRefundEntity->create([ 'payment_id' => $payment->id, 'order_sn' => $payment->out_trade_no, 'reason' => $param->getReason(), 'amount' => $param->getAmount(), 'status' => RefundStatusEnum::REFUNDING, 'refund_param' => $param->getRaw(), 'status_desc' => '正在申请微信接口退款', ]); $result = $this->domesticRefunds( $refund->out_refund_no, $payment->out_trade_no, $param->getAmount(), $payment->amount, $param->getReason() ); $refund->refund_id = $result->refund_id; $refund->refund_result = $result; $refund->save(); if ($result->status === 'SUCCESS' || $result->status === 'PROCESSING') { QueryRefundJob::dispatch($refund->id)->delay(20); } else { throw new \RuntimeException(match ($result->status) { 'CLOSED' => '退款关闭', 'ABNORMAL' => '退款异常', default => '未知错误', }); } return $refund; }); return new PaymentRefundResult($refund); } public function refundQuery(int $refundID): PaymentRefundResult { $refund = $this->extensionRefundEntity->with('payment')->find($refundID); if ($refund->status === RefundStatusEnum::REFUNDING) { $result = $this->queryRefund($refund->out_refund_no); $refund->refund_result = $result; if ($result->status === 'SUCCESS') { $refund->status = RefundStatusEnum::SUCCESS; $refund->status_desc = '成功退款到' . $result->user_received_account; $refund->success_time = strtotime($result->success_time); Event::trigger(new RefundSuccessEvent( $refund->model(), $refund->payment, new PaymentRefundRequest($refund->amount, $refund->reason, $refund->refund_param)) ); } else if ($result->status === 'PROCESSING') { QueryRefundJob::dispatch($refund->id)->delay(10); } $refund->save(); } return new PaymentRefundResult($refund->model()); } /** * 发货信息录入 * @param int $orderID 订单ID * @param int $bizType 业务类型 * @param string $itemDesc 商品描述 * @param string $trackingNo 运单号 * @param string $expressCompany 快递公司ID * @param string $receiverContact 收件人手机号码 * @param int $logisticsType 配送方式 * @param bool $failException 是否抛出异常 */ public function uploadShippingInfo( int $orderID, int $bizType = 1, string $itemDesc = '', string $trackingNo = '', string $expressCompany = '', string $receiverContact = '', int $logisticsType = 1, bool $failException = true, ): array { $order = $this->extensionPaymentEntity->where([ 'order_id' => $orderID, 'biz_type' => $bizType, 'status' => PaymentStatusEnum::SUCCESS, 'pay_type' => self::PAYMENT_TYPE, ])->findOrEmpty(); if ($order->isEmpty()) { throw new \RuntimeException('支付订单不存在或未支付'); } return $this->uploadShippingInfoAPI($order->out_trade_no, $order->user_id, $itemDesc, $trackingNo, $expressCompany, $receiverContact, $logisticsType, $failException); } /** * 传运单接口 trace_waybill */ public function waybillToken( int $orderID, string $receiverPhone, string $waybillID, string $deliveryID, array $detailList, bool $failException = true ): array { $order = $this->extensionPaymentEntity->where([ 'order_id' => $orderID, 'biz_type' => 1, 'status' => PaymentStatusEnum::SUCCESS, 'pay_type' => self::PAYMENT_TYPE ])->findOrEmpty(); if ($order->isEmpty()) { throw new \RuntimeException('支付订单不存在或未支付'); } return $this->waybillTokenAPI($order->user_id, $receiverPhone, $waybillID, $deliveryID, $detailList, $failException); } }