PaymentProvider.php 13 KB

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