Ver Fonte

feat(payment): 添加支付模块基础功能

runphp há 7 meses atrás
pai
commit
329d0bbc7f

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+.idea/

+ 14 - 0
README.md

@@ -0,0 +1,14 @@
+# payment
+
+所有支付模块扩展的基础扩展模块,是其他支付模块扩展的依赖扩展, 其他支付模块扩展必须实现`PaymentProviderInterface`
+
+
+## Hook
+
+### 支付服务信息上报事件
+`\SixShop\Extension\payment\Event\GatheringPaymentEvent` 监听该事件可获取到支付服务信息,否则无法获取到支付服务信息,后续可以扩展为支付服务注册中心,上报对象为`\SixShop\Extension\payment\PaymentInfo`
+
+## 实现支付扩展说明
+
+如果是支付模块,扩展的`Extension`类需要实现`\SixShop\Extension\payment\Contracts\PaymentExtensionInterface`接口,获取支付方式提供者`PaymentProviderInterface`完成支付业务
+

+ 34 - 0
composer.json

@@ -0,0 +1,34 @@
+{
+  "name": "six-shop/payment",
+  "description": "支付模块",
+  "type": "sixshop-extension",
+  "keywords": [
+    "sixshop",
+    "thinkphp"
+  ],
+  "require": {
+    "php": ">=8.3",
+    "six-shop/core": ">=0.4 <1.0"
+  },
+  "authors": [
+    {
+      "name": "hui he",
+      "email": "runphp@qq.com"
+    }
+  ],
+  "license": "MIT",
+  "autoload": {
+    "psr-4": {
+      "SixShop\\Payment\\": "src"
+    },
+    "files": [
+      "src/helper.php"
+    ]
+  },
+  "extra": {
+    "sixshop": {
+      "id": "payment",
+      "class": "SixShop\\Payment\\Extension"
+    }
+  }
+}

+ 38 - 0
config.php

@@ -0,0 +1,38 @@
+<?php
+declare(strict_types=1);
+
+return json_decode(<<<'JSON'
+[
+  {
+    "type": "checkbox",
+    "field": "pay_type",
+    "title": "支付方式",
+    "info": "这里可以开启关闭支付方式",
+    "effect": {
+      "fetch": {
+        "action": "{{API_BASE_URL}}/admin/payment",
+        "method": "GET",
+        "dataType": "json",
+        "headers": {
+          "Authorization": "Bearer {{API_TOKEN}}"
+        },
+        "query": {},
+        "data": {},
+        "parse": "[[FORM-CREATE-PREFIX-function (res, rule, api){\nreturn res.data.map(item => ({\n  value: item.id,\n  label: item.name\n}))\n}-FORM-CREATE-SUFFIX]]",
+        "beforeFetch": "",
+        "onError": "",
+        "to": "options"
+      }
+    },
+    "$required": false,
+    "props": {
+      "_optionType": 1
+    },
+    "_fc_id": "id_Fpr6mdk6u0kpafc",
+    "name": "ref_Forzmdk6u0kpagc",
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "checkbox"
+  }
+]
+JSON,true);

+ 52 - 0
database/migrations/20250720063342_extension_payment.php

@@ -0,0 +1,52 @@
+<?php
+
+use think\migration\Migrator;
+use think\migration\db\Column;
+
+class ExtensionPayment extends Migrator
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
+     *
+     * The following commands can be used in this method and Phinx will
+     * automatically reverse them when rolling back:
+     *
+     *    createTable
+     *    renameTable
+     *    addColumn
+     *    renameColumn
+     *    addIndex
+     *    addForeignKey
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change(): void
+    {
+        $this->table('extension_payment')
+            ->setComment('订单支付记录表')
+            ->addColumn(Column::integer('order_id')->setComment('关联订单ID'))
+            ->addColumn(Column::char('order_sn', 20)->setComment('订单编号'))
+            ->addColumn(Column::char('out_trade_no', 20)->setComment('商户支付订单号'))
+            ->addColumn(Column::tinyInteger('biz_type')->setComment('业务类型:1-商品订单支付'))
+            ->addColumn(Column::string('pay_type', 20)->setComment('支付方式:wechatpay-微信 xlpayment-信联支付'))
+            ->addColumn(Column::json('payment_param')->setComment('支付参数'))
+            ->addColumn(Column::decimal('amount', 10, 2)->setComment('支付金额(元)'))
+            ->addColumn(Column::tinyInteger('status')->setComment('支付状态:0-待支付/1-支付中/2-成功/3-失败/4-已关闭/5-退款中'))
+            ->addColumn(Column::char('transaction_id', 32)->setComment('三方支付订单号'))
+            ->addColumn(Column::integer('payment_time')->setComment('支付成功时间'))
+            ->addColumn(Column::integer('expire_time')->setComment('订单失效时间'))
+            ->addColumn(Column::json('payment_result')->setComment('查询支付结果信息'))
+            ->addColumn(Column::string('status_desc', 255)->setComment('支付状态说明'))
+            ->addTimestamps()
+            ->addIndex(['order_id', 'biz_type'])
+            ->addIndex(['transaction_id'])
+            ->addIndex(['out_trade_no'], ['unique' => true])
+            ->create();
+    }
+}

+ 35 - 0
database/migrations/20250731075545_extension_payment_user_id.php

@@ -0,0 +1,35 @@
+<?php
+
+use think\migration\Migrator;
+use think\migration\db\Column;
+
+class ExtensionPaymentUserId extends Migrator
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
+     *
+     * The following commands can be used in this method and Phinx will
+     * automatically reverse them when rolling back:
+     *
+     *    createTable
+     *    renameTable
+     *    addColumn
+     *    renameColumn
+     *    addIndex
+     *    addForeignKey
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change()
+    {
+        $this->table('extension_payment')
+            ->addColumn(Column::integer('user_id')->setUnsigned()->setDefault(0)->setAfter('id')->setComment('用户ID'))
+            ->update();
+    }
+}

+ 15 - 0
info.php

@@ -0,0 +1,15 @@
+<?php
+return [
+    'id' => 'payment',
+    'name' => '支付模块',
+    'is_core' => true,
+    'category' => 'pay',
+    'description' => '支付模块,是其他支付服务扩展的依赖扩展',
+    'version' => '1.0.0',
+    'core_version' => '^1.0',
+    'author' => 'runphp',
+    'email' => 'runphp@qq.com',
+    'website' => '',
+    'image' => '',
+    'license' => 'MIT',
+];

+ 10 - 0
route/admin.php

@@ -0,0 +1,10 @@
+<?php
+declare(strict_types=1);
+
+
+use SixShop\Payment\Controller\PaymentController;
+use think\facade\Route;
+
+Route::resource('', PaymentController::class)->middleware([
+    'auth'
+]);

+ 10 - 0
route/api.php

@@ -0,0 +1,10 @@
+<?php
+declare(strict_types=1);
+
+
+use SixShop\Payment\Controller\PaymentController;
+use think\facade\Route;
+
+Route::resource('', PaymentController::class)->middleware([
+    'auth'
+]);

+ 11 - 0
src/Contracts/PaymentExtensionInterface.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace SixShop\Payment\Contracts;
+
+interface PaymentExtensionInterface
+{
+    /**
+     * 获取支付方式提供者
+     */
+    public function getPaymentProvider(): PaymentProviderInterface;
+}

+ 84 - 0
src/Contracts/PaymentProviderInterface.php

@@ -0,0 +1,84 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment\Contracts;
+
+use SixShop\Payment\Enum\PaymentBizEnum;
+
+class PaymentResponse {
+    public function __construct(public string $orderNo, public string $type, public array $raw)
+    {
+    }
+}
+
+class PaymentNotifyResult {
+    public string $orderNo;
+    public string $transactionId;
+    public float  $amount;
+    public string $status;          // success/fail
+    public array  $raw;
+}
+
+class PaymentQueryResult {
+    public string $orderNo;
+    public string $status;          // paid/unpaid/closed
+    public float  $amount;
+    public array  $raw;
+}
+
+class PaymentRefundResult {
+    public string $refundNo;
+    public string $orderNo;
+    public float  $refundAmount;
+    public string $status;          // success/fail
+    public array  $raw;
+}
+
+class PaymentRefundQueryResult {
+    public string $refundNo;
+    public string $status;          // success/fail/processing
+    public array  $raw;
+}
+
+interface PaymentProviderInterface
+{
+    /**
+     * 创建支付订单(统一下单)
+     * @param array $order 订单信息(如订单号、金额、用户、商品等)
+     * @param PaymentBizEnum $bizType 业务类型
+     * @return PaymentResponse 下单结果(含支付跳转/二维码/表单/参数等)
+     * @throws PaymentException
+     */
+    public function create(array $order, PaymentBizEnum $bizType): PaymentResponse;
+
+    /**
+     * 处理支付回调(异步/同步通知)
+     * @param array $request 回调请求参数
+     * @return PaymentNotifyResult 处理结果(如订单号、金额、状态等)
+     * @throws PaymentException
+     */
+    public function notify(array $request): PaymentNotifyResult;
+
+    /**
+     * 查询支付订单状态
+     * @param string $orderNo 订单号
+     * @return PaymentQueryResult
+     * @throws PaymentException
+     */
+    public function query(string $orderNo): PaymentQueryResult;
+
+    /**
+     * 申请退款
+     * @param array $refund 退款信息(如原订单号、退款金额、原因等)
+     * @return PaymentRefundResult
+     * @throws PaymentException
+     */
+    public function refund(array $refund): PaymentRefundResult;
+
+    /**
+     * 查询退款状态
+     * @param string $refundNo 退款单号
+     * @return PaymentRefundQueryResult
+     * @throws PaymentException
+     */
+    public function refundQuery(string $refundNo): PaymentRefundQueryResult;
+}

+ 16 - 0
src/Controller/PaymentController.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment\Controller;
+
+use SixShop\Core\Helper;
+use SixShop\Payment\PaymentManager;
+use think\Response;
+use think\response\Json;
+
+class PaymentController
+{
+    public function index(PaymentManager $paymentManager): Response
+    {
+        return Helper::success_response($paymentManager->getAllPayment());
+    }
+}

+ 14 - 0
src/Entity/ExtensionPaymentEntity.php

@@ -0,0 +1,14 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment\Entity;
+
+use SixShop\Core\Entity\BaseEntity;
+use SixShop\Payment\Model\ExtensionPaymentModel;
+
+/**
+ * @mixin ExtensionPaymentModel
+ */
+class ExtensionPaymentEntity extends BaseEntity
+{
+
+}

+ 27 - 0
src/Enum/NumberBizEnum.php

@@ -0,0 +1,27 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Payment\Enum;
+
+
+enum NumberBizEnum: int
+{
+    // 1 订单号 2 订单支付
+    case ORDER_NO = 1;
+    case ORDER_PAY = 2;
+
+    public function toString(): string
+    {
+        return match ($this) {
+            self::ORDER_NO => '订单号',
+            self::ORDER_PAY => '订单支付',
+        };
+    }
+
+    public static function fromPaymentBiz(PaymentBizEnum $biz): self
+    {
+        return match ($biz) {
+            PaymentBizEnum::ORDER_PAY => self::ORDER_PAY,
+        };
+    }
+}

+ 15 - 0
src/Enum/PaymentBizEnum.php

@@ -0,0 +1,15 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment\Enum;
+
+enum PaymentBizEnum:int
+{
+    case ORDER_PAY = 1;
+
+    public function toString(): string
+    {
+        return match ($this) {
+            self::ORDER_PAY => '订单支付',
+        };
+    }
+}

+ 25 - 0
src/Enum/PaymentStatusEnum.php

@@ -0,0 +1,25 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment\Enum;
+
+enum PaymentStatusEnum:int
+{
+    case PENDING = 0;
+    case PAYING = 1;
+    case SUCCESS = 2;
+    case FAIL = 3;
+    case CLOSED = 4;
+    case REFUNDING = 5;
+
+    public function toString(): string
+    {
+        return match ($this) {
+            self::PENDING => '待支付',
+            self::PAYING => '支付中',
+            self::SUCCESS => '成功',
+            self::FAIL => '失败',
+            self::CLOSED => '已关闭',
+            self::REFUNDING => '退款中',
+        };
+    }
+}

+ 12 - 0
src/Event/GatheringPaymentEvent.php

@@ -0,0 +1,12 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment\Event;
+
+use SixShop\Core\Request;
+
+class GatheringPaymentEvent
+{
+    public function __construct(public Request $request)
+    {
+    }
+}

+ 16 - 0
src/Event/PaymentSuccessEvent.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment\Event;
+
+use SixShop\Payment\Enum\PaymentBizEnum;
+
+class PaymentSuccessEvent
+{
+    public function __construct(
+        public string $orderNo,
+        public string $paymentType,
+        public array $payment,
+        public PaymentBizEnum $bizType,
+    ) {
+    }
+}

+ 22 - 0
src/Extension.php

@@ -0,0 +1,22 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Payment;
+
+use SixShop\Core\ExtensionAbstract;
+use SixShop\Payment\Hook\OrderHook;
+
+class Extension extends ExtensionAbstract
+{
+    public function getHooks(): array
+    {
+        return [
+            OrderHook::class
+        ];
+    }
+
+    protected function getBaseDir(): string
+    {
+        return dirname(__DIR__);
+    }
+}

+ 27 - 0
src/Hook/OrderHook.php

@@ -0,0 +1,27 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Payment\Hook;
+
+use app\model\Order;
+use SixShop\Core\Attribute\Hook;
+use SixShop\Payment\Enum\PaymentBizEnum;
+use SixShop\Payment\Event\PaymentSuccessEvent;
+
+class OrderHook
+{
+
+    #[Hook(PaymentSuccessEvent::class)]
+    public function onPaymentSuccess(PaymentSuccessEvent $event): void
+    {
+        if ($event->bizType != PaymentBizEnum::ORDER_PAY) {
+            return;
+        }
+        $order = Order::find($event->payment['order_id']);
+        $order->save([
+            'pay_time' => date('Y-m-d H:i:s'),
+            'pay_status' => 1, //已支付
+            'order_status' => 20, //待发货
+        ]);
+    }
+}

+ 54 - 0
src/Model/ExtensionPaymentModel.php

@@ -0,0 +1,54 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment\Model;
+
+use SixShop\Payment\Enum\NumberBizEnum;
+use SixShop\Payment\Enum\PaymentBizEnum;
+use SixShop\Payment\Enum\PaymentStatusEnum;
+use think\Model;
+
+/**
+ * Class SixShop\Payment\Model\ExtensionPaymentModel
+ *
+ * @property \SixShop\Payment\Enum\PaymentBizEnum $biz_type 业务类型:1-商品订单支付
+ * @property \SixShop\Payment\Enum\PaymentStatusEnum $status 支付状态:0-待支付/1-支付中/2-成功/3-失败/4-已关闭/5-退款中
+ * @property array $payment_param 支付参数
+ * @property array $payment_result 查询支付结果信息
+ * @property float $amount 支付金额(元)
+ * @property int $expire_time 订单失效时间
+ * @property int $id
+ * @property int $order_id 关联订单ID
+ * @property int $payment_time 支付成功时间
+ * @property int $user_id 用户ID
+ * @property string $create_time
+ * @property string $order_sn 订单编号
+ * @property string $out_trade_no 商户支付订单号
+ * @property string $pay_type 支付方式:wechatpay-微信 xlpayment-信联支付
+ * @property string $status_desc 支付状态说明
+ * @property string $transaction_id 三方支付订单号
+ * @property string $update_time
+ */
+class ExtensionPaymentModel extends Model
+{
+    protected $name = 'extension_payment';
+    protected $pk = 'id';
+
+    protected array $type = [
+        'biz_type' => PaymentBizEnum::class,
+        'status' => PaymentStatusEnum::class,
+        'payment_param' => 'json',
+        'payment_result' => 'json',
+    ];
+    protected function setOutTradeNoAttr($value, $data): string
+    {
+        return generate_number(NumberBizEnum::fromPaymentBiz($data['biz_type']));
+    }
+
+    protected function getOptions(): array
+    {
+        return array_merge(parent::getOptions(),[
+            'insert' => ['out_trade_no'],
+            'readonly' => ['order_id', 'order_sn', 'out_trade_no', 'biz_type', 'pay_type', 'amount'],
+        ]);
+    }
+}

+ 20 - 0
src/PaymentInfo.php

@@ -0,0 +1,20 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment;
+
+use SixShop\System\Enum\ExtensionStatusEnum;
+
+class PaymentInfo
+{
+    public function __construct(
+        public readonly string $id,
+        public readonly string $name,
+        public readonly string $description,
+        public ExtensionStatusEnum $status = ExtensionStatusEnum::UNINSTALLED,
+        public bool $enabled = false, // 是否启用
+        public bool $hidden = false, // 是否隐藏
+        public array $params = [],
+    ) {
+
+    }
+}

+ 79 - 0
src/PaymentManager.php

@@ -0,0 +1,79 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Payment;
+
+use SixShop\Payment\Contracts\PaymentResponse;
+use SixShop\Payment\Entity\ExtensionPaymentEntity;
+use SixShop\Payment\Enum\PaymentBizEnum;
+use SixShop\Payment\Event\GatheringPaymentEvent;
+use SixShop\System\ExtensionManager;
+use SixShop\System\Model\ExtensionModel;
+use think\facade\Event;
+use think\Request;
+
+readonly class PaymentManager
+{
+
+    public function __construct(
+        private ExtensionManager $extensionManager,
+        private ExtensionPaymentEntity $extensionPaymentEntity,
+    )
+    {
+    }
+
+    /**
+     * 获取所有支付方式
+     *
+     * @return PaymentInfo[]
+     */
+    public function getAllPayment():array
+    {
+        /* @var PaymentInfo[] $paymentList */
+        $paymentList = Event::trigger(GatheringPaymentEvent::class);
+        $paymentIds = array_column($paymentList, 'id');
+        $statusMap = ExtensionModel::whereIn('id', $paymentIds)->column(['status','id'], 'id', true);
+        $payTypeList = extension_config('payment', 'pay_type')??[];
+        foreach ($paymentList as $payment) {
+            $payment->status = $statusMap[$payment->id]['status'];
+            $payment->enabled = in_array($payment->id, $payTypeList, true);
+        }
+        return $paymentList;
+    }
+
+    /**
+     *  创建支付订单
+     */
+    public function create($paymentId,  array $order, PaymentBizEnum $bizType = PaymentBizEnum::ORDER_PAY):  PaymentResponse
+    {
+        $extension = $this->extensionManager->getExtension($paymentId);
+        return $extension->getPaymentProvider()->create($order, $bizType);
+    }
+
+    /**
+     * 获取指定支付方式
+     */
+    public function getPayment($paymentId):array
+    {
+        return [
+            // todo
+        ];
+    }
+
+    /**
+     * 开启支付方式
+     */
+    public function enablePayment($paymentId):bool
+    {
+        // todo
+        return true;
+    }
+
+    /**
+     * 关闭支付方式
+     */
+    public function disablePayment($paymentId):bool
+    {
+        // todo
+        return true;
+    }
+}

+ 63 - 0
src/helper.php

@@ -0,0 +1,63 @@
+<?php
+declare(strict_types=1);
+
+
+use SixShop\Payment\Enum\NumberBizEnum;
+
+if (!function_exists('generate_number')) {
+    /**
+     * 生成20位数字编号
+     * 格式:14位日期时间(YmdHis) + 2位业务码(00-99) + 3位随机数 + 1位校验位
+     *
+     * @param NumberBizEnum $bizCode 业务编码(0-99),1 订单号 2 订单支付
+     * @return string 20位数字编号
+     * @throws InvalidArgumentException 当业务码不在0-99范围时抛出
+     */
+    function generate_number(NumberBizEnum $bizCode): string
+    {
+        $bizCode = $bizCode->value;
+        // 1. 校验业务码范围
+        if ($bizCode < 0 || $bizCode > 99) {
+            throw new InvalidArgumentException('业务码必须是0-99之间的整数');
+        }
+        // 2. 14位日期时间(年月日时分秒)
+        $datetime = date('YmdHis');
+        // 3. 2位业务码(自动补零)
+        $bizPart = str_pad((string)$bizCode, 2, '0', STR_PAD_LEFT);
+        // 4. 3位安全随机数
+        $random = str_pad((string)random_int(0, 999), 3, '0', STR_PAD_LEFT);
+        // 5. 组合前19位
+        $prefix = $datetime . $bizPart . $random;
+        // 6. 计算校验位(简单加权求和)
+        $sum = 0;
+        for ($i = 0; $i < 19; $i++) {
+            $sum += $prefix[$i] * (($i % 2) ? 3 : 1);
+        }
+        $checkDigit = (10 - ($sum % 10)) % 10;
+
+        return $prefix . $checkDigit;
+    }
+}
+
+if (!function_exists('validate_order_number')) {
+    /**
+     * 验证订单号有效性
+     */
+    function validate_order_number(string $orderNo): bool
+    {
+        // 基础格式检查
+        if (!preg_match('/^\d{20}$/', $orderNo)) {
+            return false;
+        }
+
+        // 校验位验证
+        $prefix = substr($orderNo, 0, 19);
+        $check = substr($orderNo, -1);
+
+        $sum = 0;
+        for ($i = 0; $i < 19; $i++) {
+            $sum += $prefix[$i] * (($i % 2) ? 3 : 1);
+        }
+        return ((10 - ($sum % 10)) % 10) == $check;
+    }
+}