PaymentProvider.php 10 KB

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