| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- <?php
- declare(strict_types=1);
- namespace SixShop\WechatPay;
- use GuzzleHttp\Exception\ClientException;
- use SixShop\Core\Exception\NotFoundException;
- use SixShop\Payment\Contracts\PaymentNotifyResult;
- use SixShop\Payment\Contracts\PaymentProviderInterface;
- use SixShop\Payment\Contracts\PaymentQueryResult;
- use SixShop\Payment\Contracts\PaymentRefundQueryResult;
- use SixShop\Payment\Contracts\PaymentRefundRequest;
- use SixShop\Payment\Contracts\PaymentRefundResult;
- use SixShop\Payment\Contracts\PaymentResponse;
- use SixShop\Payment\Entity\ExtensionPaymentEntity;
- use SixShop\Payment\Entity\ExtensionRefundEntity;
- use SixShop\Payment\Enum\PaymentBizEnum;
- use SixShop\Payment\Enum\PaymentStatusEnum;
- use SixShop\Payment\Enum\RefundStatusEnum;
- use SixShop\Payment\Event\PaymentSuccessEvent;
- use SixShop\Payment\Event\RefundSuccessEvent;
- use SixShop\Wechat\Facade\WechatUser;
- use SixShop\WechatPay\Entity\WechatpayTransferBillEntity;
- use SixShop\WechatPay\Job\QueryRefundJob;
- use SixShop\WechatPay\Service\NotifyService;
- use SixShop\WechatPay\Trait\ApiTrait;
- use SixShop\WechatPay\Trait\MiniAppTrait;
- use SixShop\WechatPay\Trait\PaymentParamsTrait;
- use think\facade\Db;
- use think\facade\Event;
- use think\facade\Log;
- use function SixShop\Core\throw_logic_exception;
- class PaymentProvider implements PaymentProviderInterface
- {
- private const string PAYMENT_TYPE = 'wechatpay';
- use ApiTrait;
- use MiniAppTrait {
- uploadShippingInfo as private uploadShippingInfoAPI;
- waybillToken as private waybillTokenAPI;
- }
- use PaymentParamsTrait;
- public function __construct(
- private readonly ExtensionPaymentEntity $extensionPaymentEntity,
- private readonly ExtensionRefundEntity $extensionRefundEntity,
- private readonly WechatpayTransferBillEntity $wechatpayTransferBillEntity,
- private readonly NotifyService $notifyService,
- )
- {
- }
- public function create(array $order, PaymentBizEnum $bizType): PaymentResponse
- {
- $payment = $this->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<string, string>, inBody: string} $request
- * @return PaymentNotifyResult
- * @throws \Exception]
- */
- public function notify(array $request): PaymentNotifyResult
- {
- $inBody = $request['inBody'];
- $data = $this->notifyService->transactionSuccess($request['headers'], json_encode($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('订单不存在或已结束');
- }
- // 验签有问题,暂时叫个支付查询处理 todo 待完善
- $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 string $itemDesc 商品描述
- * @param string $trackingNo 运单号
- * @param string $expressCompany 快递公司ID
- * @param string $receiverContact 收件人手机号码
- */
- public function uploadShippingInfo(
- int $orderID,
- string $itemDesc = '',
- string $trackingNo = '',
- string $expressCompany = '',
- string $receiverContact = '',
- int $logisticsType = 1,
- bool $failException = true,
- ): array
- {
- $order = $this->extensionPaymentEntity->where([
- 'order_id' => $orderID,
- 'status' => PaymentStatusEnum::SUCCESS
- ])->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,
- 'status' => PaymentStatusEnum::SUCCESS
- ])->findOrEmpty();
- if ($order->isEmpty()) {
- throw new \RuntimeException('支付订单不存在或未支付');
- }
- return $this->waybillTokenAPI($order->user_id, $receiverPhone, $waybillID, $deliveryID, $detailList, $failException);
- }
- }
|