PaymentProvider.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <?php
  2. declare(strict_types=1);
  3. namespace SixShop\WechatPay;
  4. use GuzzleHttp\Exception\ClientException;
  5. use SixShop\Core\Exception\NotFoundException;
  6. use SixShop\Payment\Contracts\PaymentNotifyResult;
  7. use SixShop\Payment\Contracts\PaymentProviderInterface;
  8. use SixShop\Payment\Contracts\PaymentQueryResult;
  9. use SixShop\Payment\Contracts\PaymentRefundQueryResult;
  10. use SixShop\Payment\Contracts\PaymentRefundRequest;
  11. use SixShop\Payment\Contracts\PaymentRefundResult;
  12. use SixShop\Payment\Contracts\PaymentResponse;
  13. use SixShop\Payment\Entity\ExtensionPaymentEntity;
  14. use SixShop\Payment\Entity\ExtensionRefundEntity;
  15. use SixShop\Payment\Enum\PaymentBizEnum;
  16. use SixShop\Payment\Enum\PaymentStatusEnum;
  17. use SixShop\Payment\Enum\RefundStatusEnum;
  18. use SixShop\Payment\Event\PaymentSuccessEvent;
  19. use SixShop\Payment\Event\RefundSuccessEvent;
  20. use SixShop\Wechat\Facade\WechatUser;
  21. use SixShop\Wechat\Service\MiniApp;
  22. use SixShop\WechatPay\Job\QueryRefundJob;
  23. use SixShop\WechatPay\Trait\ApiTrait;
  24. use SixShop\WechatPay\Trait\PaymentParamsTrait;
  25. use SixShop\WechatPay\Trait\UploadShippingInfoTrait;
  26. use think\facade\Db;
  27. use think\facade\Event;
  28. use function SixShop\Core\throw_logic_exception;
  29. class PaymentProvider implements PaymentProviderInterface
  30. {
  31. private const string PAYMENT_TYPE = 'wechatpay';
  32. use ApiTrait;
  33. use UploadShippingInfoTrait {
  34. uploadShippingInfo as private uploadShippingInfoAPI;
  35. }
  36. use PaymentParamsTrait;
  37. public function __construct(
  38. private readonly ExtensionPaymentEntity $extensionPaymentEntity,
  39. private readonly ExtensionRefundEntity $extensionRefundEntity,
  40. private readonly MiniApp $miniApp,
  41. )
  42. {
  43. }
  44. public function create(array $order, PaymentBizEnum $bizType): PaymentResponse
  45. {
  46. $payment = $this->extensionPaymentEntity->where([
  47. 'order_id' => $order['id'],
  48. 'pay_type' => self::PAYMENT_TYPE,
  49. 'biz_type' => $bizType,
  50. ])->findOrEmpty();
  51. if (!$payment->isEmpty()) {
  52. // todo 判断订单是否支付成功
  53. // 支付时间结束关闭订单
  54. // 订单未支付可重新支付
  55. // 交易已关闭请重新下单
  56. //订单已付款请勿重复操作
  57. // 订单已退款请重新下单
  58. //订单已撤销请重新下单
  59. // 订单支付中请稍后再试
  60. // 订单支付失败请重新下单
  61. throw new \RuntimeException('开发测试中,请稍后再试');
  62. }
  63. $payment->transaction(function () use ($bizType, $order, $payment) {
  64. $payment->save([
  65. 'user_id' => $order['user_id'],
  66. 'order_id' => $order['id'],
  67. 'order_sn' => $order['order_sn'],
  68. 'biz_type' => $bizType,
  69. 'pay_type' => self::PAYMENT_TYPE,
  70. 'amount' => $order['pay_amount'],
  71. 'status' => PaymentStatusEnum::PENDING
  72. ]);
  73. $expireTime = time() + 3600;
  74. $payment->expire_time = $expireTime;
  75. $openid = WechatUser::openid($order['user_id']);
  76. if ($openid === null) {
  77. throw_logic_exception('用户需要先使用微信登录绑定微信身份');
  78. }
  79. $payment->payment_param = $this->wechatPay(
  80. openid: $openid,
  81. outTradeNo: $payment['out_trade_no'],
  82. total: (int)($payment['amount'] * 100),
  83. description: '订单:' . $order['order_sn'],
  84. expireTime: $expireTime
  85. );
  86. $payment->payment_param = $this->paymentParams($payment->payment_param->prepay_id);
  87. $payment->save();
  88. });
  89. return new PaymentResponse(orderNo: $payment->out_trade_no, type: self::PAYMENT_TYPE, raw: $payment->toArray());
  90. }
  91. public function notify(array $request): PaymentNotifyResult
  92. {
  93. throw new \Exception('Not implemented');
  94. }
  95. public function query(int $recordID): PaymentQueryResult
  96. {
  97. $payment = $this->extensionPaymentEntity->findOrEmpty($recordID);
  98. if ($payment->status === PaymentStatusEnum::PENDING) {
  99. try {
  100. $paymentResult = $this->queryByOutTradeNo($payment['out_trade_no']);
  101. } catch (ClientException $e) {
  102. if ($e->getCode() === 404) {
  103. throw new NotFoundException(sprintf('订单%s不存在', $payment['out_trade_no']));
  104. }
  105. throw $e;
  106. }
  107. $payment->payment_result = $paymentResult;
  108. if ($paymentResult->trade_state === 'SUCCESS') {
  109. $payment->transaction_id = $paymentResult->transaction_id;
  110. $payment->status = PaymentStatusEnum::SUCCESS;
  111. $payment->save();
  112. Event::trigger(new PaymentSuccessEvent($payment['order_sn'], self::PAYMENT_TYPE, $payment->toArray(), $payment->biz_type));
  113. } else {
  114. throw_logic_exception($paymentResult->trade_state_desc);
  115. }
  116. }
  117. return new PaymentQueryResult(
  118. orderNo: $payment['out_trade_no'],
  119. status: $payment['status'],
  120. amount: (float)$payment['amount'],
  121. raw: $payment->toArray()
  122. );
  123. }
  124. public function refund(int $recordID, PaymentRefundRequest $param): PaymentRefundResult
  125. {
  126. $payment = $this->extensionPaymentEntity->find($recordID);
  127. $refund = Db::transaction(function () use ($param, $payment) {
  128. $refund = $this->extensionRefundEntity->create([
  129. 'payment_id' => $payment->id,
  130. 'order_sn' => $payment->out_trade_no,
  131. 'reason' => $param->getReason(),
  132. 'amount' => $param->getAmount(),
  133. 'status' => RefundStatusEnum::REFUNDING,
  134. 'refund_param' => $param->getRaw(),
  135. 'status_desc' => '正在申请微信接口退款',
  136. ]);
  137. $result = $this->domesticRefunds(
  138. $refund->out_refund_no,
  139. $payment->out_trade_no,
  140. $param->getAmount(),
  141. $payment->amount,
  142. $param->getReason()
  143. );
  144. $refund->refund_id = $result->refund_id;
  145. $refund->refund_result = $result;
  146. $refund->save();
  147. if ($result->status === 'SUCCESS' || $result->status === 'PROCESSING') {
  148. QueryRefundJob::dispatch($refund->id)->delay(20);
  149. } else {
  150. throw new \RuntimeException(match ($result->status) {
  151. 'CLOSED' => '退款关闭',
  152. 'ABNORMAL' => '退款异常',
  153. default => '未知错误',
  154. });
  155. }
  156. return $refund;
  157. });
  158. return new PaymentRefundResult($refund);
  159. }
  160. public function refundQuery(int $refundID): PaymentRefundResult
  161. {
  162. $refund = $this->extensionRefundEntity->with('payment')->find($refundID);
  163. if ($refund->status === RefundStatusEnum::REFUNDING) {
  164. $result = $this->queryRefund($refund->out_refund_no);
  165. $refund->refund_result = $result;
  166. if ($result->status === 'SUCCESS') {
  167. $refund->status = RefundStatusEnum::SUCCESS;
  168. $refund->status_desc = '成功退款到' . $result->user_received_account;
  169. $refund->success_time = strtotime($result->success_time);
  170. Event::trigger(new RefundSuccessEvent(
  171. $refund->model(),
  172. $refund->payment,
  173. new PaymentRefundRequest($refund->amount, $refund->reason, $refund->refund_param))
  174. );
  175. } else if ($result->status === 'PROCESSING') {
  176. QueryRefundJob::dispatch($refund->id)->delay(10);
  177. }
  178. $refund->save();
  179. }
  180. return new PaymentRefundResult($refund->model());
  181. }
  182. /**
  183. * 发货信息录入
  184. * @param int $orderID 订单ID
  185. * @param string $itemDesc 商品描述
  186. * @param string $trackingNo 运单号
  187. * @param string $expressCompany 快递公司ID
  188. * @param string $receiverContact 收件人手机号码
  189. */
  190. public function uploadShippingInfo(
  191. int $orderID,
  192. string $itemDesc,
  193. string $trackingNo,
  194. string $expressCompany,
  195. string $receiverContact,
  196. bool $failException = true
  197. ): array
  198. {
  199. $order = $this->extensionPaymentEntity->where([
  200. 'order_id' => $orderID,
  201. 'status' => PaymentStatusEnum::SUCCESS
  202. ])->findOrEmpty();
  203. if ($order->isEmpty()) {
  204. throw new \RuntimeException('支付订单不存在或未支付');
  205. }
  206. return $this->uploadShippingInfoAPI($order->out_trade_no, $order->user_id, $itemDesc, $trackingNo, $expressCompany, $receiverContact, $failException);
  207. }
  208. }