ソースを参照

feat(payment): 添加余额支付功能

runphp 7 ヶ月 前
コミット
f57ccb1e42

+ 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

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# 余额支付
+
+BalPay(Balance+Payment缩写)

+ 31 - 0
composer.json

@@ -0,0 +1,31 @@
+{
+  "name": "six-shop/balpay",
+  "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\\Balpay\\": "src"
+    }
+  },
+  "extra": {
+    "sixshop": {
+      "id": "balpay",
+      "class": "SixShop\\Balpay\\Extension"
+    }
+  }
+}

+ 4 - 0
config.php

@@ -0,0 +1,4 @@
+<?php
+declare(strict_types=1);
+
+return [];

+ 42 - 0
database/migrations/20250728173547_extension_balpay_log.php

@@ -0,0 +1,42 @@
+<?php
+declare(strict_types=1);
+
+use think\migration\db\Column;
+use think\migration\Migrator;
+
+class ExtensionBalpayLog 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
+    {
+        $table = $this->table('extension_balpay_log', ['commonts' => '余额支付日志']);
+        $table->addColumn(Column::integer('user_id')->setUnsigned()->setComment('用户ID'))
+            ->addColumn(Column::integer('order_id')->setUnsigned()->setDefault(0)->setComment('订单ID(可为0)'))
+            ->addColumn(Column::integer('type')->setUnsigned()->setDefault(0)->setComment('流水类型:1-充值 2-消费 3-退款 4-冻结 5-解冻'))
+            ->addColumn(Column::decimal('amount', 10, 2)->setComment('金额'))
+            ->addColumn(Column::decimal('balance', 10, 2)->setComment('当前余额'))
+            ->addColumn(Column::string('description')->setLimit(255)->setComment('描述'))
+            ->addTimestamps()
+            ->create();
+    }
+}

+ 16 - 0
info.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+return array(
+    'id' => 'balpay',
+    'name' => '余额支付',
+    'is_core' => false,
+    'category' => 'pay',
+    'description' => '余额支付服务,用于管理余额支付相关功能。',
+    'version' => '1.0.0',
+    'core_version' => '^1.0',
+    'author' => 'runphp',
+    'email' => 'runphp@qq.com',
+    'website' => '',
+    'image' => '',
+    'license' => 'MIT',
+);

+ 12 - 0
route/admin.php

@@ -0,0 +1,12 @@
+<?php
+declare(strict_types=1);
+
+
+use SixShop\Balpay\Controller\LogController;
+use think\facade\Route;
+
+// 后台管理API路由
+// 路由前缀: /admin/balpay
+
+Route::resource('log', LogController::class)
+    ->middleware(['auth']);

+ 59 - 0
src/Controller/LogController.php

@@ -0,0 +1,59 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Balpay\Controller;
+
+
+use SixShop\Core\Helper;
+use SixShop\Core\Request;
+use SixShop\Balpay\Entity\ExtensionBalpayLogEntity;
+use SixShop\Balpay\Enum\BalpayLogTypeEnum;
+use SixShop\System\Middleware\MacroPageMiddleware;
+use think\Response;
+
+class LogController
+{
+    protected array $middleware = [
+        MacroPageMiddleware::class
+    ];
+    public function index(Request $request, ExtensionBalpayLogEntity $entity): Response
+    {
+        $data = $request->get([
+            'user_id/d' => 0,
+            'type/d' =>  0,
+        ]);
+        return Helper::page_response($entity->getList($data['user_id'], BalpayLogTypeEnum::tryFrom($data['type']), $request->pageAndLimit()));
+    }
+
+    public function save(Request $request, ExtensionBalpayLogEntity $entity): Response
+    {
+        $data = $request->post([
+            'user_id/d',
+            'amount/f',
+            'type/d',
+            'description/s',
+        ]);
+        validate([
+            'user_id' => 'require|integer|gt:0',
+            'amount' => 'require|float|gt:0',
+            'type' => 'require|in:1,2',
+            'description' => 'require|string',
+        ], [
+            'user_id.require' => '用户ID不能为空',
+            'user_id.integer' => '用户ID必须为整数',
+            'user_id.gt' => '用户ID必须大于0',
+            'amount.require' => '金额不能为空',
+            'amount.float' => '金额必须为数字',
+            'amount.gt' => '金额必须大于0',
+            'type.require' => '类型不能为空',
+            'type.in' => '类型必须为1或2',
+            'description.require' => '描述不能为空',
+        ])->check($data);
+        $entity->add(
+            $data['user_id'],
+            $data['amount'],
+            BalpayLogTypeEnum::from($data['type']),
+            $data['description']
+        );
+        return Helper::success_response($entity);
+    }
+}

+ 54 - 0
src/Entity/ExtensionBalpayLogEntity.php

@@ -0,0 +1,54 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Balpay\Entity;
+
+use app\model\User;
+use SixShop\Core\Helper;
+use SixShop\Balpay\Enum\BalpayLogTypeEnum;
+use SixShop\Core\Entity\BaseEntity;
+use think\Paginator;
+
+/**
+ * @mixin \SixShop\Balpay\Model\ExtensionBalpayLogModel
+ */
+class ExtensionBalpayLogEntity extends BaseEntity
+{
+
+    public function add(int $userID, float $amount, BalpayLogTypeEnum $type, string $description, int $orderID = 0)
+    {
+        $user = User::find($userID);
+        if (!$user) {
+            throw new \RuntimeException('用户不存在');
+        }
+        if ($type->negative()) {
+            if ($user->balance < $amount) {
+                Helper::throw_logic_exception('余额不足', status: 'not_enough_balance');
+            }
+            $user->dec('balance', $amount);
+            $amount = -$amount;
+        } else {
+            $user->inc('balance', $amount);
+        }
+        $this->transaction(function () use ($user, $amount, $type, $description, $orderID) {
+            $user->save();
+            $this->create([
+                'user_id' => $user->id,
+                'order_id' => $orderID,
+                'amount' => $amount,
+                'type' => $type,
+                'description' => $description,
+                'balance' => $user->balance,
+            ]);
+        });
+    }
+
+    public function getList(int $user_id, ?BalpayLogTypeEnum $type, array $pageAndLimit): Paginator
+    {
+        return $this->where('user_id', $user_id)
+            ->when($type, fn($query) => $query->where('type', $type))
+            ->append(['type_text'])
+            ->order('id', 'desc')
+            ->paginate($pageAndLimit);
+    }
+}

+ 50 - 0
src/Enum/BalpayLogTypeEnum.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace SixShop\Balpay\Enum;
+
+enum BalpayLogTypeEnum: int
+{
+    /**
+     * 充值
+     */
+    case RECHARGE = 1;
+
+    /**
+     * 消费
+     */
+    case CONSUMTION = 2;
+
+    /**
+     * 退款
+     */
+    case REFUND = 3;
+
+    /**
+     * 冻结
+     */
+    case FREEZE = 4;
+
+    /**
+     * 解冻
+     */
+    case UNFREEZE = 5;
+
+    public function toString(): string
+    {
+        return match ($this) {
+            self::RECHARGE => '充值',
+            self::CONSUMTION => '消费',
+            self::REFUND => '退款',
+            self::FREEZE => '冻结',
+            self::UNFREEZE => '解冻',
+        };
+    }
+
+    public function negative(): bool
+    {
+        return match ($this) {
+            self::CONSUMTION, self::FREEZE => true,
+            default => false,
+        };
+    }
+}

+ 22 - 0
src/Extension.php

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

+ 29 - 0
src/Hook/BalpayHook.php

@@ -0,0 +1,29 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Balpay\Hook;
+
+use app\model\User;
+use SixShop\Core\Attribute\Hook;
+use SixShop\Payment\Event\GatheringPaymentEvent;
+use SixShop\Payment\PaymentInfo;
+
+class BalpayHook
+{
+
+    /**
+     * 上报提供的支付服务信息
+     */
+    #[Hook(GatheringPaymentEvent::class)]
+    public function paymentInfoSubmission(GatheringPaymentEvent $event): PaymentInfo
+    {
+        return new PaymentInfo(
+            'balpay',
+            '余额支付',
+            '余额支付',
+            params: [
+                'balance' => (float)User::where('id', $event->request->userID)->value('balance'),
+            ]
+        );
+    }
+}

+ 35 - 0
src/Model/ExtensionBalpayLogModel.php

@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Balpay\Model;
+
+use SixShop\Balpay\Enum\BalpayLogTypeEnum;
+use think\Model;
+
+/**
+ * Class SixShop\Balpay\Model\ExtensionBalpayLogModel
+ *
+ * @property float $amount 金额
+ * @property float $balance 当前余额
+ * @property int $id
+ * @property int $order_id 订单ID(可为0)
+ * @property int $type 流水类型:1-充值 2-消费 3-退款 4-冻结 5-解冻
+ * @property int $user_id 用户ID
+ * @property string $create_time
+ * @property string $description 描述
+ * @property string $update_time
+ */
+class ExtensionBalpayLogModel extends Model
+{
+    protected $name = 'extension_balpay_log';
+    protected $pk = 'id';
+
+    protected array $type = [
+        'type' => BalpayLogTypeEnum::class
+    ];
+
+    public function getTypeTextAttr($value, array $data)
+    {
+        return $data['type']->toString();
+    }
+}

+ 104 - 0
src/PaymentProvider.php

@@ -0,0 +1,104 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Balpay;
+
+use app\model\User;
+use SixShop\Core\Helper;
+use SixShop\Balpay\Entity\ExtensionBalpayLogEntity;
+use SixShop\Balpay\Enum\BalpayLogTypeEnum;
+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\Payment\Event\PaymentSuccessEvent;
+use think\facade\Event;
+
+class PaymentProvider implements PaymentProviderInterface
+{
+    const string PAYMENT_TYPE = 'balpay';
+
+    public function __construct(
+        private readonly ExtensionBalpayLogEntity $logEntity,
+        private readonly ExtensionPaymentEntity   $extensionPaymentEntity,
+    )
+    {
+    }
+
+    public function create(array $order, PaymentBizEnum $bizType): PaymentResponse
+    {
+        $this->checkPayPasswordAndBalance($order, $order['params']);
+        $payment = $this->extensionPaymentEntity->where([
+            'order_id' => $order['id'],
+            'pay_type' => self::PAYMENT_TYPE,
+            'biz_type' => $bizType,
+        ])->findOrEmpty();
+        if (!$payment->isEmpty()) {
+            Helper::throw_logic_exception('订单已支付', status: 'balpay.order_already_paid');
+        }
+        $payment->transaction(function () use ($bizType, $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::SUCCESS,
+            ]);
+            $this->logEntity->add(
+                $order['user_id'],
+                (float)$order['pay_amount'],
+                BalpayLogTypeEnum::CONSUMTION,
+                '支付订单:' . $order['order_sn'],
+                $order['id']
+            );
+
+        });
+        // 余额支付直接成功
+        Event::trigger(new PaymentSuccessEvent($order['order_sn'], self::PAYMENT_TYPE, $payment->toArray(), $bizType));
+        return new PaymentResponse($order['order_sn'], self::PAYMENT_TYPE, $payment->toArray());
+    }
+
+    public function notify(array $request): PaymentNotifyResult
+    {
+        throw new \Exception('未实现notify方法');
+
+    }
+
+    public function query(string $orderNo): PaymentQueryResult
+    {
+        throw new \Exception('未实现query方法');
+
+    }
+
+    public function refund(array $refund): PaymentRefundResult
+    {
+        throw new \Exception('未实现refund方法');
+    }
+
+    public function refundQuery(string $refundNo): PaymentRefundQueryResult
+    {
+        throw new \Exception('未实现refundQuery方法');
+    }
+
+    private function checkPayPasswordAndBalance(array $order, array $params): void
+    {
+        $password = $params['pay_password'] ?? '';
+        if (empty($password)) {
+            Helper::throw_logic_exception('请输入支付密码', status: 'balpay.pay_password_empty');
+        }
+        $user = User::find($order['user_id']);
+        if (empty($user->pay_password)) {
+            Helper::throw_logic_exception('请先设置支付密码', status: 'balpay.pay_password_not_set');
+        }
+        if (!password_verify($password, $user->pay_password)) {
+            Helper::throw_logic_exception('支付密码错误', status: 'balpay.pay_password_error');
+        }
+    }
+}