PaymentProvider.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  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'], json_encode($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. // 验签有问题,暂时叫个支付查询处理 todo 待完善
  116. $queryResult = $this->query($payment['id']);
  117. return new PaymentNotifyResult(
  118. orderNo: $queryResult->orderNo,
  119. transactionId: $data['transaction_id'],
  120. amount: $queryResult->amount,
  121. status: $queryResult->status,
  122. raw: $data
  123. );
  124. } else if ($inBody['event_type'] == 'MCHTRANSFER.BILL.FINISHED') {
  125. // 转账完成
  126. $transferBill = $this->wechatpayTransferBillEntity->where('out_bill_no', $data['out_bill_no'])->findOrEmpty();
  127. if ($transferBill->isEmpty()) {
  128. throw new \RuntimeException('转账单不存在');
  129. }
  130. $this->wechatpayTransferBillEntity->refreshTransferBill($transferBill->id);
  131. return new PaymentNotifyResult(
  132. orderNo: $transferBill['out_bill_no'],
  133. transactionId: $transferBill['transfer_bill_no'],
  134. amount: round($transferBill['transfer_amount'] / 100, 2),
  135. status: PaymentStatusEnum::SUCCESS,
  136. raw: $data
  137. );
  138. }
  139. Log::warning('Not implemented: ' . $inBody['event_type']. ' ' . json_encode($data));
  140. throw_logic_exception('Not implemented: ' . $inBody['event_type']);
  141. }
  142. public function query(int $recordID): PaymentQueryResult
  143. {
  144. $payment = $this->extensionPaymentEntity->findOrEmpty($recordID);
  145. if ($payment->status === PaymentStatusEnum::PENDING) {
  146. try {
  147. $paymentResult = $this->queryByOutTradeNo($payment['out_trade_no']);
  148. } catch (ClientException $e) {
  149. if ($e->getCode() === 404) {
  150. throw new NotFoundException(sprintf('订单%s不存在', $payment['out_trade_no']));
  151. }
  152. throw $e;
  153. }
  154. $payment->payment_result = $paymentResult;
  155. if ($paymentResult->trade_state === 'SUCCESS') {
  156. $payment->transaction_id = $paymentResult->transaction_id;
  157. $payment->payment_time = strtotime($paymentResult->success_time);
  158. $payment->status = PaymentStatusEnum::SUCCESS;
  159. $payment->save();
  160. Event::trigger(new PaymentSuccessEvent($payment['order_sn'], self::PAYMENT_TYPE, $payment->toArray(), $payment->biz_type));
  161. } else {
  162. throw_logic_exception($paymentResult->trade_state_desc);
  163. }
  164. }
  165. return new PaymentQueryResult(
  166. orderNo: $payment['out_trade_no'],
  167. status: $payment['status'],
  168. amount: (float)$payment['amount'],
  169. raw: $payment->toArray()
  170. );
  171. }
  172. public function refund(int $recordID, PaymentRefundRequest $param): PaymentRefundResult
  173. {
  174. $payment = $this->extensionPaymentEntity->find($recordID);
  175. $refund = Db::transaction(function () use ($param, $payment) {
  176. $refund = $this->extensionRefundEntity->create([
  177. 'payment_id' => $payment->id,
  178. 'order_sn' => $payment->out_trade_no,
  179. 'reason' => $param->getReason(),
  180. 'amount' => $param->getAmount(),
  181. 'status' => RefundStatusEnum::REFUNDING,
  182. 'refund_param' => $param->getRaw(),
  183. 'status_desc' => '正在申请微信接口退款',
  184. ]);
  185. $result = $this->domesticRefunds(
  186. $refund->out_refund_no,
  187. $payment->out_trade_no,
  188. $param->getAmount(),
  189. $payment->amount,
  190. $param->getReason()
  191. );
  192. $refund->refund_id = $result->refund_id;
  193. $refund->refund_result = $result;
  194. $refund->save();
  195. if ($result->status === 'SUCCESS' || $result->status === 'PROCESSING') {
  196. QueryRefundJob::dispatch($refund->id)->delay(20);
  197. } else {
  198. throw new \RuntimeException(match ($result->status) {
  199. 'CLOSED' => '退款关闭',
  200. 'ABNORMAL' => '退款异常',
  201. default => '未知错误',
  202. });
  203. }
  204. return $refund;
  205. });
  206. return new PaymentRefundResult($refund);
  207. }
  208. public function refundQuery(int $refundID): PaymentRefundResult
  209. {
  210. $refund = $this->extensionRefundEntity->with('payment')->find($refundID);
  211. if ($refund->status === RefundStatusEnum::REFUNDING) {
  212. $result = $this->queryRefund($refund->out_refund_no);
  213. $refund->refund_result = $result;
  214. if ($result->status === 'SUCCESS') {
  215. $refund->status = RefundStatusEnum::SUCCESS;
  216. $refund->status_desc = '成功退款到' . $result->user_received_account;
  217. $refund->success_time = strtotime($result->success_time);
  218. Event::trigger(new RefundSuccessEvent(
  219. $refund->model(),
  220. $refund->payment,
  221. new PaymentRefundRequest($refund->amount, $refund->reason, $refund->refund_param))
  222. );
  223. } else if ($result->status === 'PROCESSING') {
  224. QueryRefundJob::dispatch($refund->id)->delay(10);
  225. }
  226. $refund->save();
  227. }
  228. return new PaymentRefundResult($refund->model());
  229. }
  230. /**
  231. * 发货信息录入
  232. * @param int $orderID 订单ID
  233. * @param string $itemDesc 商品描述
  234. * @param string $trackingNo 运单号
  235. * @param string $expressCompany 快递公司ID
  236. * @param string $receiverContact 收件人手机号码
  237. */
  238. public function uploadShippingInfo(
  239. int $orderID,
  240. string $itemDesc = '',
  241. string $trackingNo = '',
  242. string $expressCompany = '',
  243. string $receiverContact = '',
  244. int $logisticsType = 1,
  245. bool $failException = true,
  246. ): array
  247. {
  248. $order = $this->extensionPaymentEntity->where([
  249. 'order_id' => $orderID,
  250. 'status' => PaymentStatusEnum::SUCCESS
  251. ])->findOrEmpty();
  252. if ($order->isEmpty()) {
  253. throw new \RuntimeException('支付订单不存在或未支付');
  254. }
  255. return $this->uploadShippingInfoAPI($order->out_trade_no, $order->user_id, $itemDesc, $trackingNo, $expressCompany, $receiverContact, $logisticsType, $failException);
  256. }
  257. /**
  258. * 传运单接口 trace_waybill
  259. */
  260. public function waybillToken(
  261. int $orderID,
  262. string $receiverPhone,
  263. string $waybillID,
  264. string $deliveryID,
  265. array $detailList,
  266. bool $failException = true
  267. ): array
  268. {
  269. $order = $this->extensionPaymentEntity->where([
  270. 'order_id' => $orderID,
  271. 'status' => PaymentStatusEnum::SUCCESS
  272. ])->findOrEmpty();
  273. if ($order->isEmpty()) {
  274. throw new \RuntimeException('支付订单不存在或未支付');
  275. }
  276. return $this->waybillTokenAPI($order->user_id, $receiverPhone, $waybillID, $deliveryID, $detailList, $failException);
  277. }
  278. }