Browse Source

feat(payment): 添加微信支付扩展

runphp 7 months ago
commit
ccde20c67c
10 changed files with 561 additions and 0 deletions
  1. 8 0
      .idea/.gitignore
  2. 2 0
      README.md
  3. 32 0
      composer.json
  4. 156 0
      config.php
  5. 17 0
      info.php
  6. 53 0
      src/Config.php
  7. 29 0
      src/Extension.php
  8. 25 0
      src/Hook/WechatpayHook.php
  9. 107 0
      src/PaymentProvider.php
  10. 132 0
      src/Trait/ApiTrait.php

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 2 - 0
README.md

@@ -0,0 +1,2 @@
+# 微信支付
+

+ 32 - 0
composer.json

@@ -0,0 +1,32 @@
+{
+  "name": "six-shop/wechatpay",
+  "description": "微信支付",
+  "type": "sixshop-extension",
+  "keywords": [
+    "sixshop",
+    "thinkphp"
+  ],
+  "require": {
+    "php": ">=8.3",
+    "six-shop/core": ">=0.4 <1.0",
+    "wechatpay/wechatpay": "^1.4.12"
+  },
+  "authors": [
+    {
+      "name": "hui he",
+      "email": "runphp@qq.com"
+    }
+  ],
+  "license": "MIT",
+  "autoload": {
+    "psr-4": {
+      "SixShop\\WechatPay\\": "src"
+    }
+  },
+  "extra": {
+    "sixshop": {
+      "id": "wechatpay",
+      "class": "SixShop\\WechatPay\\Extension"
+    }
+  }
+}

+ 156 - 0
config.php

@@ -0,0 +1,156 @@
+<?php
+declare(strict_types=1);
+
+return json_decode(<<<'JSON'
+[
+  {
+    "type": "input",
+    "field": "mchid",
+    "title": "mchid",
+    "info": "商户号",
+    "$required": true,
+    "_fc_id": "id_Fxo2md1dy5fjacc",
+    "name": "ref_Favmmd1dy5fjadc",
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "input"
+  },
+  {
+    "type": "input",
+    "field": "appid",
+    "title": "appid",
+    "info": "开发者ID",
+    "$required": false,
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "input"
+  },
+  {
+    "type": "upload",
+    "field": "apiclient_cert",
+    "title": "apiclient_cert",
+    "info": "商户API证书",
+    "$required": false,
+    "props": {
+      "action": "/admin/flysystem/file",
+      "name":"file",
+      "onSuccess": "$FNX:const res = $inject.args[0];\nconsole.log(res);if(res.code!=200 &&res.code!=0){alert(res.msg);return;}\nconst file = $inject.args[1];\n\nfile.url = res.data.file_url;",
+      "limit": 1
+    },
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "upload"
+  },
+  {
+    "type": "upload",
+    "field": "apiclient_key",
+    "title": "apiclient_key",
+    "info": "商户API私钥",
+    "$required": false,
+    "props": {
+      "action": "/admin/flysystem/file",
+      "name":"file",
+      "onSuccess": "$FNX:const res = $inject.args[0];\nconst file = $inject.args[1];\n\nfile.url = res.data.file_url;",
+      "limit": 1
+    },
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "upload"
+  },
+  {
+    "type": "input",
+    "field": "serial_no",
+    "title": "serial_no",
+    "info": "商户API证书序列号",
+    "$required": false,
+    "_fc_id": "id_Fho8md1e31edalc",
+    "name": "ref_F343md1e31edamc",
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "input"
+  },
+  {
+    "type": "upload",
+    "field": "public_key",
+    "title": "public_key",
+    "info": "微信支付公钥",
+    "$required": false,
+    "props": {
+      "action": "/admin/flysystem/file",
+      "name":"file",
+      "onSuccess": "$FNX:const res = $inject.args[0];\nconst file = $inject.args[1];\n\nfile.url = res.data.file_url;",
+      "limit": 1
+    },
+    "_fc_id": "id_Finbmd1e8rmmarc",
+    "name": "ref_Fb4jmd1e8rmmasc",
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "upload"
+  },
+  {
+    "type": "input",
+    "field": "wechatpay_serial",
+    "title": "wechatpay_serial",
+    "info": "微信支付公钥id",
+    "$required": false,
+    "_fc_id": "id_F8prmd1e9yx9auc",
+    "name": "ref_Fmmwmd1e9yx9avc",
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "input"
+  },
+  {
+    "type": "input",
+    "field": "api_v3_key",
+    "title": "api_v3_key",
+    "info": "APIv3密钥",
+    "$required": false,
+    "_fc_id": "id_Floymd1ech8faxc",
+    "name": "ref_Fdp2md1ech8fayc",
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "input"
+  },
+    {
+    "type": "switch",
+    "title": "启用沙箱环境",
+    "field": "sandbox",
+    "props": {
+      "activeText": "是",
+      "inactiveText": "否"
+    },
+    "_fc_id": "id_Fpxtmd1fz7stauc",
+    "name": "ref_Fihqmd1fz7stavc",
+    "_fc_drag_tag": "switch",
+    "display": true,
+    "hidden": false
+  },
+  {
+    "type": "input",
+    "title": "支付回调地址",
+    "field": "notify_url",
+    "props": {
+      "placeholder": "请输入支付结果通知回调地址"
+    },
+    "$required": true,
+    "_fc_id": "id_F0j5md1fz7stawc",
+    "name": "ref_Ffipmd1fz7staxc",
+    "_fc_drag_tag": "input",
+    "display": true,
+    "hidden": false
+  },
+  {
+    "type": "input",
+    "title": "退款回调地址",
+    "field": "refund_notify_url",
+    "props": {
+      "placeholder": "请输入退款结果通知回调地址"
+    },
+    "_fc_id": "id_Fcmkmd1fz7stayc",
+    "name": "ref_Fct5md1fz7stazc",
+    "_fc_drag_tag": "input",
+    "display": true,
+    "hidden": false
+  }
+]
+JSON, true);

+ 17 - 0
info.php

@@ -0,0 +1,17 @@
+<?php
+declare(strict_types=1);
+
+return [
+    'id' => 'wechatpay', # 扩展的唯一标识符
+    'name' => '微信支付服务', # 扩展的名称
+    'is_core' => false, # 是否核心扩展'
+    'category' => 'pay', # 扩展的分类 core:核心扩展,other:其他扩展
+    'description' => '这是用来管理微信微信支付服务相关功能的扩展。', # 扩展的描述
+    'version' => '1.0.0',  # 扩展的版本
+    'core_version' => '^1.0',  # 支持的核心版本
+    'author' => 'runphp', # 作者
+    'email' => 'runphp@qq.com', # 作者的邮箱
+    'website' => '', # 扩展的地址,可以是扩展的仓库地址,帮助用户寻找扩展,安装扩展等网络地址
+    'image' => '', # 扩展的图片,用于展示扩展的图标,或者是扩展的截图等图片地址
+    'license' => 'MIT', # 扩展的开源协议
+];

+ 53 - 0
src/Config.php

@@ -0,0 +1,53 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\WechatPay;
+
+use SixShop\System\ExtensionManager;
+use WeChatPay\Crypto\Rsa;
+
+/**
+ * 微信支付配置
+ * @package SixShop\WechatPay
+ * @property string $mchid 商户号
+ * @property string $appid 应用ID
+ * @property string $serial_no 商户证书序列号
+ * @property string $wechatpay_serial 微信支付证书序列号
+ * @property string $api_v3_key APIv3密钥
+ * @property string $notify_url 微信支付回调地址
+ * @property string $apiclient_cert 微信支付证书  用于服务端API请求报文签名
+ * @property string $apiclient_key 微信支付证书密钥 用于生成接口请求签名
+ * @property string $public_key 微信支付公钥
+ */
+class Config
+{
+
+
+    public function __construct(
+        private readonly ExtensionManager $extensionManager,
+        private ?array $options = null
+    )
+    {
+    }
+
+    public function __get(string $name)
+    {
+        if ($this->options === null) {
+            $this->options = $this->extensionManager->getExtensionConfig('wechatpay');
+            if (empty($this->options)) {
+                throw new \RuntimeException('微信支付配置不能为空');
+            }
+        }
+        return match ($name) {
+            'apiclient_cert' => $this->getKeyPath($this->options['apiclient_cert'][0]),
+            'apiclient_key' => Rsa::from($this->getKeyPath($this->options['apiclient_key'][0])),
+            'public_key' => Rsa::from($this->getKeyPath($this->options['public_key'][0]), Rsa::KEY_TYPE_PUBLIC),
+            default => $this->options[$name] ?? null,
+        };
+    }
+
+    public function getKeyPath(string $name): string
+    {
+        return 'file://' . public_path() . $name;
+    }
+}

+ 29 - 0
src/Extension.php

@@ -0,0 +1,29 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\WechatPay;
+
+use SixShop\Core\ExtensionAbstract;
+use SixShop\Payment\Contracts\PaymentExtensionInterface;
+use SixShop\Payment\Contracts\PaymentProviderInterface;
+use SixShop\WechatPay\Hook\WechatpayHook;
+
+class Extension extends ExtensionAbstract implements PaymentExtensionInterface
+{
+    public function getPaymentProvider(): PaymentProviderInterface
+    {
+        return app(PaymentProvider::class);
+    }
+
+    public function getHooks(): array
+    {
+        return [
+            WechatpayHook::class,
+        ];
+    }
+
+    protected function getBaseDir(): string
+    {
+        return dirname(__DIR__);
+    }
+}

+ 25 - 0
src/Hook/WechatpayHook.php

@@ -0,0 +1,25 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\WechatPay\Hook;
+
+use SixShop\Core\Attribute\Hook;
+use SixShop\Payment\Event\GatheringPaymentEvent;
+use SixShop\Payment\PaymentInfo;
+
+class WechatpayHook
+{
+
+    /**
+     * 上报提供的支付服务信息
+     */
+    #[Hook(GatheringPaymentEvent::class)]
+    public function paymentInfoSubmission(): PaymentInfo
+    {
+        return new PaymentInfo(
+            'wechatpay',
+            '微信支付',
+            '微信支付',
+           );
+    }
+}

+ 107 - 0
src/PaymentProvider.php

@@ -0,0 +1,107 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\WechatPay;
+
+use SixShop\Core\Helper;
+use SixShop\Payment\Contracts\PaymentNotifyResult;
+use SixShop\Payment\Contracts\PaymentProviderInterface;
+use SixShop\Payment\Contracts\PaymentQueryResult;
+use SixShop\Payment\Contracts\PaymentRefundQueryResult;
+use SixShop\Payment\Contracts\PaymentRefundResult;
+use SixShop\Payment\Contracts\PaymentResponse;
+use SixShop\Payment\Entity\ExtensionPaymentEntity;
+use SixShop\Payment\Enum\PaymentBizEnum;
+use SixShop\Payment\Enum\PaymentStatusEnum;
+use SixShop\Extension\wechat\Facade\WechatUser;
+use SixShop\WechatPay\Trait\ApiTrait;
+use WeChatPay\Crypto\Rsa;
+use WeChatPay\Formatter;
+
+class PaymentProvider implements PaymentProviderInterface
+{
+    const string PAYMENT_TYPE = 'wechatpay';
+    use ApiTrait;
+
+    public function create(array $order, PaymentBizEnum $bizType): PaymentResponse
+    {
+        $payment = $this->extensionPaymentEntity->where([
+            'order_id' => $order['id'],
+            'pay_type' => self::PAYMENT_TYPE,
+            'biz_type' => $bizType,
+        ])->findOrEmpty();
+        if (!$payment->isEmpty()) {
+            // todo 判断订单是否支付成功
+            // 支付时间结束关闭订单
+            // 订单未支付可重新支付
+            // 交易已关闭请重新下单
+            //订单已付款请勿重复操作
+            // 订单已退款请重新下单
+            //订单已撤销请重新下单
+            // 订单支付中请稍后再试
+            // 订单支付失败请重新下单
+            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->save([
+                'user_id' => $order['user_id'],
+                'order_id' => $order['id'],
+                'order_sn' => $order['order_sn'],
+                'biz_type' => $bizType,
+                'pay_type' => self::PAYMENT_TYPE,
+                'amount' => $order['pay_amount'],
+                'status' => PaymentStatusEnum::PENDING
+            ]);
+            $expireTime = time() + 3600;
+            $payment->expire_time = $expireTime;
+            $openid = WechatUser::openid($order['user_id']);
+            if ($openid === null) {
+                Helper::throw_logic_exception('用户需要先使用微信登录绑定微信身份');
+            }
+            $payment->payment_param = $this->wechatPay(
+                openid: $openid,
+                outTradeNo: $payment['out_trade_no'],
+                total: (int)($payment['amount'] * 100),
+                description: '订单:' . $order['order_sn'],
+                notifyUrl: $this->config->notify_url,
+                expireTime: $expireTime
+            );
+            $addSignFn($payment);
+            $payment->save();
+        });
+        return new PaymentResponse(orderNo: $payment->out_trade_no, type: self::PAYMENT_TYPE, raw: $payment->toArray());
+    }
+
+    public function notify(array $request): PaymentNotifyResult
+    {
+        throw new \Exception('Not implemented');
+    }
+
+    public function query(string $orderNo): PaymentQueryResult
+    {
+        throw new \Exception('Not implemented');
+    }
+
+    public function refund(array $refund): PaymentRefundResult
+    {
+        throw new \Exception('Not implemented');
+    }
+
+    public function refundQuery(string $refundNo): PaymentRefundQueryResult
+    {
+        throw new \Exception('Not implemented');
+    }
+}

+ 132 - 0
src/Trait/ApiTrait.php

@@ -0,0 +1,132 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\WechatPay\Trait;
+
+use app\xyz\exception\ApiException;
+use GuzzleHttp\Promise\PromiseInterface;
+use SixShop\Payment\Entity\ExtensionPaymentEntity;
+use SixShop\WechatPay\Config;
+use think\exception\ErrorException;
+use think\facade\Log;
+use WeChatPay\Builder;
+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->wechatpay_serial => $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();
+    }
+
+    /**
+     * 微信支付
+     */
+    private function wechatPay(string $openid, string $outTradeNo, int $total, string $description, string $notifyUrl, 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'
+                ],
+                'payer' => [
+                    'openid' => $openid
+                ]
+            ],]));
+
+    }
+
+    /**
+     * 退款申请
+     */
+    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
+            ]
+        ]));
+    }
+
+    /**
+     * 发起转账
+     */
+    private function transferBills(int $uid, 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)
+                    ],
+                ],
+                'transfer_remark' => sprintf('提现%s元到零钱', $amount),
+            ]
+        ]));
+    }
+}