PaymentProvider.php 12 KB

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