ソースを参照

refactor(wechatpay): 重构微信支付模块

- 新增 HandleAsyncRequestTrait 和 PaymentParamsTrait
- 创建 WechatPayBuilder 类和对应的 Facade
- 重构 ApiTrait 中的微信支付、退款和转账逻辑
- 优化 PaymentProvider 中的支付流程
- 添加 WechatPayBuilder 的单元测试
runphp 6 ヶ月 前
コミット
3b245a0ce0

+ 1 - 0
composer.json

@@ -9,6 +9,7 @@
   "require": {
     "php": ">=8.3",
     "six-shop/core": ">=0.4 <1.0",
+    "six-shop/wechat": "^0.1",
     "wechatpay/wechatpay": "^1.4.12"
   },
   "authors": [

+ 19 - 0
src/Facade/WechatPayBuilder.php

@@ -0,0 +1,19 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\WechatPay\Facade;
+
+use SixShop\WechatPay\Config;
+use think\Facade;
+use WeChatPay\BuilderChainable;
+
+/**
+ * @method static BuilderChainable getBuilderChainable()
+ * @method static Config getConfig()
+ */
+class WechatPayBuilder extends Facade
+{
+    protected static function getFacadeClass(): string
+    {
+        return \SixShop\WechatPay\WechatPayBuilder::class;
+    }
+}

+ 8 - 16
src/PaymentProvider.php

@@ -15,6 +15,7 @@ use SixShop\Payment\Enum\PaymentBizEnum;
 use SixShop\Payment\Enum\PaymentStatusEnum;
 use SixShop\Wechat\Facade\WechatUser;
 use SixShop\WechatPay\Trait\ApiTrait;
+use SixShop\WechatPay\Trait\PaymentParamsTrait;
 use WeChatPay\Crypto\Rsa;
 use WeChatPay\Formatter;
 
@@ -22,6 +23,11 @@ class PaymentProvider implements PaymentProviderInterface
 {
     const string PAYMENT_TYPE = 'wechatpay';
     use ApiTrait;
+    use PaymentParamsTrait;
+
+    public function __construct(private readonly ExtensionPaymentEntity $extensionPaymentEntity)
+    {
+    }
 
     public function create(array $order, PaymentBizEnum $bizType): PaymentResponse
     {
@@ -42,20 +48,7 @@ class PaymentProvider implements PaymentProviderInterface
             // 订单支付失败请重新下单
             throw new  \RuntimeException('开发测试中,请稍后再试');
         }
-        $addSignFn = function (ExtensionPaymentEntity $payment) {
-            $params = [
-                'appId' => $this->config->appid,
-                'timeStamp' => (string)Formatter::timestamp(),
-                'nonceStr' => Formatter::nonce(),
-                'package' => 'prepay_id=' . $payment->payment_param->prepay_id,
-            ];
-            $params += ['paySign' => Rsa::sign(
-                Formatter::joinedByLineFeed(...array_values($params)),
-                $this->config->apiclient_key
-            ), 'signType' => 'RSA'];
-            $payment->payment_param = $params;
-        };
-        $payment->transaction(function () use ($bizType, $addSignFn, $order, $payment) {
+        $payment->transaction(function () use ($bizType, $order, $payment) {
             $payment->save([
                 'user_id' => $order['user_id'],
                 'order_id' => $order['id'],
@@ -76,10 +69,9 @@ class PaymentProvider implements PaymentProviderInterface
                 outTradeNo: $payment['out_trade_no'],
                 total: (int)($payment['amount'] * 100),
                 description: '订单:' . $order['order_sn'],
-                notifyUrl: $this->config->notify_url,
                 expireTime: $expireTime
             );
-            $addSignFn($payment);
+            $payment->payment_param = $this->paymentParams($payment->payment_param->prepay_id);
             $payment->save();
         });
         return new PaymentResponse(orderNo: $payment->out_trade_no, type: self::PAYMENT_TYPE, raw: $payment->toArray());

+ 78 - 90
src/Trait/ApiTrait.php

@@ -14,122 +14,110 @@ use WeChatPay\BuilderChainable;
 
 trait ApiTrait
 {
-    public function __construct(
-        private readonly Config                 $config,
-        private readonly ExtensionPaymentEntity $extensionPaymentEntity,
-        private ?BuilderChainable $builderChainable = null,
-    )
-    {
-    }
-    private function getBuilderChainable(): BuilderChainable
-    {
-
-        if ($this->builderChainable === null) {
-            $this->builderChainable = Builder::factory([
-                'mchid' => $this->config->mchid,
-                'serial' => $this->config->serial_no,
-                'privateKey' => $this->config->apiclient_key,
-                'certs' => [
-                    $this->config->platform_no => $this->config->platform_cert,
-                    $this->config->public_key_id => $this->config->public_key,
-                ],
-            ]);
-        }
-        return $this->builderChainable;
-    }
-
-    private function handleAsyncRequest(PromiseInterface $promise)
-    {
-        return $promise
-            ->then(function ($response) {
-                Log::info('微信支付异步回调返回数据:' . $response->getBody());
-                return json_decode((string)$response->getBody());
-            })
-            ->otherwise(function ($e) {
-                if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
-
-                    $errorBody = json_decode((string)$e->getResponse()->getBody());
-                    throw new ErrorException(message: trim($errorBody->message));
-                }
-                throw $e;
-            })
-            ->wait();
-    }
+    use HandleAsyncRequestTrait;
 
     /**
      * 微信支付
+     *
+     * @param string $openid 用户openid
+     * @param string $outTradeNo 商户订单号
+     * @param int $total 订单总金额,单位为分
+     * @param string $description 商品描述
+     * @param string $notifyUrl 通知地址
+     * @param int $expireTime 订单过期时间戳
+     * @return mixed
      */
-    private function wechatPay(string $openid, string $outTradeNo, int $total, string $description, string $notifyUrl, int $expireTime)
+    private function wechatPay(string $openid, string $outTradeNo, int $total, string $description, int $expireTime)
     {
 
         // https://pay.weixin.qq.com/doc/v3/merchant/4012791856
         // 【POST】/v3/pay/transactions/jsapi
-        return $this->handleAsyncRequest($this->getBuilderChainable()->v3->pay->transactions->jsapi->postAsync([
-            'json' => [
-                'mchid' => $this->config->mchid,
-                'out_trade_no' => $outTradeNo,
-                'appid' => $this->config->appid,
-                'description' => $description,
-                'notify_url' => $notifyUrl,
-                // yyyy-MM-DDTHH:mm:ss+TIMEZONE
-                'time_expire' => date('Y-m-d\TH:i:s+08:00', $expireTime),
-                'attach' => 'actor',
-                'amount' => [
-                    'total' => $total,
-                    'currency' => 'CNY'
+        return $this->handleAsyncRequest(function (
+            BuilderChainable $builder, Config $config, string $openid, string $outTradeNo, int $total, string $description, int $expireTime
+        ): PromiseInterface {
+            return $builder->v3->pay->transactions->jsapi->postAsync([
+                'json' => [
+                    'mchid' => $config->mchid,
+                    'out_trade_no' => $outTradeNo,
+                    'appid' => $config->appid,
+                    'description' => $description,
+                    'notify_url' => $config->notify_url,
+                    // yyyy-MM-DDTHH:mm:ss+TIMEZONE
+                    'time_expire' => date('Y-m-d\TH:i:s+08:00', $expireTime),
+                    'attach' => 'actor',
+                    'amount' => [
+                        'total' => $total,
+                        'currency' => 'CNY'
+                    ],
+                    'payer' => [
+                        'openid' => $openid
+                    ]
                 ],
-                'payer' => [
-                    'openid' => $openid
-                ]
-            ],]));
+            ]);
+        }, $openid, $outTradeNo, $total, $description, $expireTime);
 
     }
 
     /**
      * 退款申请
+     *
+     * @param string $outRefundNo 商户退款单号
+     * @param string $outTradeNo 商户订单号
+     * @param array $amount 退款金额信息
+     * @param string $reason 退款原因
+     * @return mixed
      */
     private function domesticRefunds(string $outRefundNo, string $outTradeNo, array $amount, string $reason)
     {
         // https://pay.weixin.qq.com/doc/v3/merchant/4012791862
         // 【POST】/v3/refund/domestic/refunds
-        return $this->handleAsyncRequest($this->getBuilderChainable()->v3->refund->domestic->refunds->postAsync([
-            'json' => [
-                'out_refund_no' => $outRefundNo,
-                'out_trade_no' => $outTradeNo,
-                'reason' => $reason,
-                'amount' => $amount,
-                'notify_url' => $this->notifyUrl
-            ]
-        ]));
+        return $this->handleAsyncRequest(function (BuilderChainable $builder, Config $config) use ($outRefundNo, $outTradeNo, $amount, $reason): PromiseInterface {
+            return $builder->v3->refund->domestic->refunds->postAsync([
+                'json' => [
+                    'out_refund_no' => $outRefundNo,
+                    'out_trade_no' => $outTradeNo,
+                    'reason' => $reason,
+                    'amount' => $amount,
+                    'notify_url' => $config->notifyUrl
+                ]
+            ]);
+        });
     }
 
     /**
      * 发起转账
+     *
+     * @param int $openid 用户openid
+     * @param string $orderSn 订单号
+     * @param string $amount 转账金额
+     * @return mixed
      */
-    private function transferBills(int $uid, string $orderSn, string $amount)
+    private function transferBills(int $openid, string $orderSn, string $amount)
     {
         // https://pay.weixin.qq.com/doc/v3/merchant/4012716434
         //【POST】/v3/fund-app/mch-transfer/transfer-bills
-        return $this->handleAsyncRequest($this->getBuilderChainable()->v3->fundApp->mchTransfer->transferBills->postAsync([
-            'json' => [
-                'appid' => $this->appId,
-                'openid' => $this->getOpenId($uid, 'routine'),
-                'out_bill_no' => $orderSn,
-                'transfer_amount' => (int)($amount * 100),
-                'notify_url' => $this->notifyUrl,
-                'transfer_scene_id' => '1005', // 佣金报酬
-                'transfer_scene_report_infos' => [
-                    [
-                        'info_type' => '岗位类型',
-                        'info_content' => '合作运营'
-                    ],
-                    [
-                        'info_type' => '报酬说明',
-                        'info_content' => sprintf('提现到账%.2f元', $amount)
+        return $this->handleAsyncRequest(function (BuilderChainable $builder, Config $config) use ($openid, $orderSn, $amount): PromiseInterface {
+            return $builder->v3->fundApp->mchTransfer->transferBills->postAsync([
+                'json' => [
+                    'appid' => $config->appId,
+                    'openid' => $openid,
+                    'out_bill_no' => $orderSn,
+                    'transfer_amount' => (int)($amount * 100),
+                    'notify_url' => $config->notifyUrl,
+                    'transfer_scene_id' => '1005', // 佣金报酬
+                    'transfer_scene_report_infos' => [
+                        [
+                            'info_type' => '岗位类型',
+                            'info_content' => '合作运营'
+                        ],
+                        [
+                            'info_type' => '报酬说明',
+                            'info_content' => sprintf('提现到账%.2f元', $amount)
+                        ],
                     ],
-                ],
-                'transfer_remark' => sprintf('提现%s元到零钱', $amount),
-            ]
-        ]));
+                    'transfer_remark' => sprintf('提现%s元到零钱', $amount),
+                ]
+            ]);
+        });
     }
-}
+}

+ 34 - 0
src/Trait/HandleAsyncRequestTrait.php

@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\WechatPay\Trait;
+
+use GuzzleHttp\Promise\PromiseInterface;
+use SixShop\WechatPay\Facade\WechatPayBuilder;
+use think\exception\ErrorException;
+use think\facade\Log;
+
+trait HandleAsyncRequestTrait
+{
+    private function handleAsyncRequest(PromiseInterface|\Closure $promiseOrCallback, ...$args)
+    {
+        if ($promiseOrCallback instanceof \Closure) {
+            $promise = $promiseOrCallback(WechatPayBuilder::getBuilderChainable(), WechatPayBuilder::getConfig(), ...$args);
+        } else {
+            $promise = $promiseOrCallback;
+        }
+        return $promise
+            ->then(function ($response) {
+                Log::info('微信支付异步回调返回数据:' . $response->getBody());
+                return json_decode((string)$response->getBody());
+            })
+            ->otherwise(function ($e) {
+                if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
+
+                    $errorBody = json_decode((string)$e->getResponse()->getBody());
+                    throw new ErrorException(message: trim($errorBody->message));
+                }
+                throw $e;
+            })
+            ->wait();
+    }
+}

+ 27 - 0
src/Trait/PaymentParamsTrait.php

@@ -0,0 +1,27 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\WechatPay\Trait;
+
+use SixShop\WechatPay\Facade\WechatPayBuilder;
+use WeChatPay\Crypto\Rsa;
+use WeChatPay\Formatter;
+
+trait PaymentParamsTrait
+{
+    private function paymentParams(string $prepayID): array
+    {
+        $config = WechatPayBuilder::getConfig();
+        $params = [
+            'appId' => $config->appid,
+            'timeStamp' => (string)Formatter::timestamp(),
+            'nonceStr' => Formatter::nonce(),
+            'package' => 'prepay_id=' . $prepayID,
+        ];
+        $params += ['paySign' => Rsa::sign(
+            Formatter::joinedByLineFeed(...array_values($params)),
+            $config->apiclient_key
+        ), 'signType' => 'RSA'];
+        return $params;
+    }
+}

+ 47 - 0
src/WechatPayBuilder.php

@@ -0,0 +1,47 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\WechatPay;
+
+use think\Facade;
+use WeChatPay\Builder;
+use WeChatPay\BuilderChainable;
+
+/**
+ * @method static BuilderChainable getBuilderChainable()
+ */
+class WechatPayBuilder extends Facade
+{
+    protected static function getFacadeAccessor(): string
+    {
+        return self::class;
+    }
+    public function __construct(
+        private readonly Config   $config,
+        private ?BuilderChainable $builderChainable = null,
+    )
+    {
+    }
+
+    public function getBuilderChainable(): BuilderChainable
+    {
+
+        if ($this->builderChainable === null) {
+            $this->builderChainable = Builder::factory([
+                'mchid' => $this->config->mchid,
+                'serial' => $this->config->serial_no,
+                'privateKey' => $this->config->apiclient_key,
+                'certs' => [
+                    $this->config->platform_no => $this->config->platform_cert,
+                    $this->config->public_key_id => $this->config->public_key,
+                ],
+            ]);
+        }
+        return $this->builderChainable;
+    }
+
+    public function getConfig(): Config
+    {
+        return $this->config;
+    }
+}

+ 16 - 0
test/WechatPayBuilderTest.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\WechatPay;
+
+use PHPUnit\Framework\TestCase;
+use WeChatPay\BuilderChainable;
+
+class WechatPayBuilderTest extends TestCase
+{
+    public function testCreate()
+    {
+        $result = \SixShop\WechatPay\Facade\WechatPayBuilder::getBuilderChainable();
+        $this->assertInstanceOf(BuilderChainable::class, $result);
+    }
+}