Browse Source

Merge pull request #2213 from sspanel-uim/dev

Dev 20231022
M1Screw 2 years ago
parent
commit
1b2c591450

+ 1 - 1
README.md

@@ -27,7 +27,7 @@ SSPanel UIM 是一款专为 Shadowsocks / V2Ray / Trojan 协议设计的多用
 
 ## 特性
 
-- 集成 支付宝当面付,Stripe 银行卡,彩虹易支付 等多种支付系统
+- 集成 支付宝当面付,PayPal,Stripe 等多种支付系统
 - 支持多种邮件服务,内置队列功能,无需第三方组件即可使用
 - 内置基于 Bootstrap 5 的 tabler 主题,模板引擎支持
 - 支持 Shadowsocks 2022,TUIC 等最新代理协议

+ 3 - 0
app/routes.php

@@ -40,6 +40,9 @@ return static function (Slim\App $app): void {
         $group->get('/banned', App\Controllers\UserController::class . ':banned');
         // 节点
         $group->get('/server', App\Controllers\User\ServerController::class . ':server');
+        // 动态倍率
+        $group->get('/rate', App\Controllers\User\RateController::class . ':index');
+        $group->post('/rate', App\Controllers\User\RateController::class . ':ajax');
         // 审计
         $group->get('/detect', App\Controllers\User\DetectController::class . ':index');
         // 工单

+ 58 - 58
composer.lock

@@ -123,16 +123,16 @@
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.283.8",
+            "version": "3.283.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "0f73ec85852312a6e971cfe2eebfd7c8091cca34"
+                "reference": "0233b9f3f2155dac35c829ce4fc1b7cdb6ff8c0a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0f73ec85852312a6e971cfe2eebfd7c8091cca34",
-                "reference": "0f73ec85852312a6e971cfe2eebfd7c8091cca34",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0233b9f3f2155dac35c829ce4fc1b7cdb6ff8c0a",
+                "reference": "0233b9f3f2155dac35c829ce4fc1b7cdb6ff8c0a",
                 "shasum": ""
             },
             "require": {
@@ -212,9 +212,9 @@
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.283.8"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.283.9"
             },
-            "time": "2023-10-19T19:26:52+00:00"
+            "time": "2023-10-20T20:03:26+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
@@ -2660,39 +2660,39 @@
         },
         {
             "name": "openai-php/client",
-            "version": "v0.7.3",
+            "version": "v0.7.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/openai-php/client.git",
-                "reference": "86ffa2abe167d6192f01d4df90f83a97c8533303"
+                "reference": "bfa2f7e15909c01317064021cd3b15bc5bbd929b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/openai-php/client/zipball/86ffa2abe167d6192f01d4df90f83a97c8533303",
-                "reference": "86ffa2abe167d6192f01d4df90f83a97c8533303",
+                "url": "https://api.github.com/repos/openai-php/client/zipball/bfa2f7e15909c01317064021cd3b15bc5bbd929b",
+                "reference": "bfa2f7e15909c01317064021cd3b15bc5bbd929b",
                 "shasum": ""
             },
             "require": {
                 "php": "^8.1.0",
-                "php-http/discovery": "^1.19.0",
+                "php-http/discovery": "^1.19.1",
                 "php-http/multipart-stream-builder": "^1.3.0",
-                "psr/http-client": "^1.0.2",
+                "psr/http-client": "^1.0.3",
                 "psr/http-client-implementation": "^1.0.1",
                 "psr/http-factory-implementation": "*",
                 "psr/http-message": "^1.1.0|^2.0.0"
             },
             "require-dev": {
-                "guzzlehttp/guzzle": "^7.7.0",
-                "guzzlehttp/psr7": "^2.5.0",
-                "laravel/pint": "^1.10.3",
-                "nunomaduro/collision": "^7.7.0",
-                "pestphp/pest": "^2.16.0",
-                "pestphp/pest-plugin-arch": "^2.2.1",
+                "guzzlehttp/guzzle": "^7.8.0",
+                "guzzlehttp/psr7": "^2.6.1",
+                "laravel/pint": "^1.13.2",
+                "nunomaduro/collision": "^7.9.0",
+                "pestphp/pest": "^2.19.2",
+                "pestphp/pest-plugin-arch": "^2.3.3",
                 "pestphp/pest-plugin-mock": "^2.0.0",
-                "pestphp/pest-plugin-type-coverage": "^2.0.0",
-                "phpstan/phpstan": "^1.10.25",
+                "pestphp/pest-plugin-type-coverage": "^2.2.0",
+                "phpstan/phpstan": "^1.10.35",
                 "rector/rector": "^0.16.0",
-                "symfony/var-dumper": "^6.3.1"
+                "symfony/var-dumper": "^6.3.4"
             },
             "type": "library",
             "autoload": {
@@ -2732,7 +2732,7 @@
             ],
             "support": {
                 "issues": "https://github.com/openai-php/client/issues",
-                "source": "https://github.com/openai-php/client/tree/v0.7.3"
+                "source": "https://github.com/openai-php/client/tree/v0.7.4"
             },
             "funding": [
                 {
@@ -2748,7 +2748,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-09-08T08:48:59+00:00"
+            "time": "2023-10-21T09:00:06+00:00"
         },
         {
             "name": "ozdemir/datatables",
@@ -4966,16 +4966,16 @@
         },
         {
             "name": "symfony/http-client",
-            "version": "v6.3.5",
+            "version": "v6.3.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-client.git",
-                "reference": "213e564da4cbf61acc9728d97e666bcdb868c10d"
+                "reference": "ab8446f997efb9913627e9da10fa784d2182fe92"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-client/zipball/213e564da4cbf61acc9728d97e666bcdb868c10d",
-                "reference": "213e564da4cbf61acc9728d97e666bcdb868c10d",
+                "url": "https://api.github.com/repos/symfony/http-client/zipball/ab8446f997efb9913627e9da10fa784d2182fe92",
+                "reference": "ab8446f997efb9913627e9da10fa784d2182fe92",
                 "shasum": ""
             },
             "require": {
@@ -5038,7 +5038,7 @@
                 "http"
             ],
             "support": {
-                "source": "https://github.com/symfony/http-client/tree/v6.3.5"
+                "source": "https://github.com/symfony/http-client/tree/v6.3.6"
             },
             "funding": [
                 {
@@ -5054,7 +5054,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-09-29T15:57:12+00:00"
+            "time": "2023-10-06T10:08:56+00:00"
         },
         {
             "name": "symfony/http-client-contracts",
@@ -5136,16 +5136,16 @@
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v6.3.5",
+            "version": "v6.3.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "b50f5e281d722cb0f4c296f908bacc3e2b721957"
+                "reference": "c186627f52febe09c6d5270b04f8462687a250a6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b50f5e281d722cb0f4c296f908bacc3e2b721957",
-                "reference": "b50f5e281d722cb0f4c296f908bacc3e2b721957",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c186627f52febe09c6d5270b04f8462687a250a6",
+                "reference": "c186627f52febe09c6d5270b04f8462687a250a6",
                 "shasum": ""
             },
             "require": {
@@ -5155,12 +5155,12 @@
                 "symfony/polyfill-php83": "^1.27"
             },
             "conflict": {
-                "symfony/cache": "<6.2"
+                "symfony/cache": "<6.3"
             },
             "require-dev": {
-                "doctrine/dbal": "^2.13.1|^3.0",
+                "doctrine/dbal": "^2.13.1|^3|^4",
                 "predis/predis": "^1.1|^2.0",
-                "symfony/cache": "^5.4|^6.0",
+                "symfony/cache": "^6.3",
                 "symfony/dependency-injection": "^5.4|^6.0",
                 "symfony/expression-language": "^5.4|^6.0",
                 "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4",
@@ -5193,7 +5193,7 @@
             "description": "Defines an object-oriented layer for the HTTP specification",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-foundation/tree/v6.3.5"
+                "source": "https://github.com/symfony/http-foundation/tree/v6.3.6"
             },
             "funding": [
                 {
@@ -5209,7 +5209,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-09-04T21:33:54+00:00"
+            "time": "2023-10-17T11:32:53+00:00"
         },
         {
             "name": "symfony/options-resolver",
@@ -6014,16 +6014,16 @@
         },
         {
             "name": "symfony/translation",
-            "version": "v6.3.3",
+            "version": "v6.3.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "3ed078c54bc98bbe4414e1e9b2d5e85ed5a5c8bd"
+                "reference": "869b26c7a9d4b8a48afdd77ab36031909c87e3a2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/3ed078c54bc98bbe4414e1e9b2d5e85ed5a5c8bd",
-                "reference": "3ed078c54bc98bbe4414e1e9b2d5e85ed5a5c8bd",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/869b26c7a9d4b8a48afdd77ab36031909c87e3a2",
+                "reference": "869b26c7a9d4b8a48afdd77ab36031909c87e3a2",
                 "shasum": ""
             },
             "require": {
@@ -6089,7 +6089,7 @@
             "description": "Provides tools to internationalize your application",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/translation/tree/v6.3.3"
+                "source": "https://github.com/symfony/translation/tree/v6.3.6"
             },
             "funding": [
                 {
@@ -6105,7 +6105,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-31T07:08:24+00:00"
+            "time": "2023-10-17T11:32:53+00:00"
         },
         {
             "name": "symfony/translation-contracts",
@@ -9239,16 +9239,16 @@
         },
         {
             "name": "symfony/cache",
-            "version": "v6.3.5",
+            "version": "v6.3.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/cache.git",
-                "reference": "6c1a3ea078c4d88ee892530945df63a87981b2da"
+                "reference": "84aff8d948d6292d2b5a01ac622760be44dddc72"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/cache/zipball/6c1a3ea078c4d88ee892530945df63a87981b2da",
-                "reference": "6c1a3ea078c4d88ee892530945df63a87981b2da",
+                "url": "https://api.github.com/repos/symfony/cache/zipball/84aff8d948d6292d2b5a01ac622760be44dddc72",
+                "reference": "84aff8d948d6292d2b5a01ac622760be44dddc72",
                 "shasum": ""
             },
             "require": {
@@ -9257,7 +9257,7 @@
                 "psr/log": "^1.1|^2|^3",
                 "symfony/cache-contracts": "^2.5|^3",
                 "symfony/service-contracts": "^2.5|^3",
-                "symfony/var-exporter": "^6.2.10"
+                "symfony/var-exporter": "^6.3.6"
             },
             "conflict": {
                 "doctrine/dbal": "<2.13.1",
@@ -9272,7 +9272,7 @@
             },
             "require-dev": {
                 "cache/integration-tests": "dev-master",
-                "doctrine/dbal": "^2.13.1|^3.0",
+                "doctrine/dbal": "^2.13.1|^3|^4",
                 "predis/predis": "^1.1|^2.0",
                 "psr/simple-cache": "^1.0|^2.0|^3.0",
                 "symfony/config": "^5.4|^6.0",
@@ -9315,7 +9315,7 @@
                 "psr6"
             ],
             "support": {
-                "source": "https://github.com/symfony/cache/tree/v6.3.5"
+                "source": "https://github.com/symfony/cache/tree/v6.3.6"
             },
             "funding": [
                 {
@@ -9331,7 +9331,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-09-26T15:48:55+00:00"
+            "time": "2023-10-17T14:44:58+00:00"
         },
         {
             "name": "symfony/cache-contracts",
@@ -10072,16 +10072,16 @@
         },
         {
             "name": "symfony/var-exporter",
-            "version": "v6.3.4",
+            "version": "v6.3.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-exporter.git",
-                "reference": "df1f8aac5751871b83d30bf3e2c355770f8f0691"
+                "reference": "374d289c13cb989027274c86206ddc63b16a2441"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-exporter/zipball/df1f8aac5751871b83d30bf3e2c355770f8f0691",
-                "reference": "df1f8aac5751871b83d30bf3e2c355770f8f0691",
+                "url": "https://api.github.com/repos/symfony/var-exporter/zipball/374d289c13cb989027274c86206ddc63b16a2441",
+                "reference": "374d289c13cb989027274c86206ddc63b16a2441",
                 "shasum": ""
             },
             "require": {
@@ -10126,7 +10126,7 @@
                 "serialize"
             ],
             "support": {
-                "source": "https://github.com/symfony/var-exporter/tree/v6.3.4"
+                "source": "https://github.com/symfony/var-exporter/tree/v6.3.6"
             },
             "funding": [
                 {
@@ -10142,7 +10142,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-16T18:14:47+00:00"
+            "time": "2023-10-13T09:16:49+00:00"
         },
         {
             "name": "theseer/tokenizer",

+ 26 - 16
config/settings.json

@@ -109,6 +109,16 @@
         "default": "10",
         "mark": "最低充值限额"
     },
+    {
+        "id": null,
+        "item": "stripe_max_recharge",
+        "value": "1000",
+        "class": "billing",
+        "is_public": 1,
+        "type": "int",
+        "default": "1000",
+        "mark": "最高充值限额"
+    },
     {
         "id": null,
         "item": "stripe_card",
@@ -139,46 +149,46 @@
         "default": "0",
         "mark": "微信支付"
     },
-    {
-        "id": null,
-        "item": "stripe_max_recharge",
-        "value": "1000",
-        "class": "billing",
-        "is_public": 1,
-        "type": "int",
-        "default": "1000",
-        "mark": "最高充值限额"
-    },
     {
         "id": null,
         "item": "epay_url",
-        "value": "1000",
+        "value": "",
         "class": "billing",
         "is_public": 0,
         "type": "string",
-        "default": "1000",
+        "default": "",
         "mark": "易支付url"
     },
 	{
         "id": null,
         "item": "epay_pid",
-        "value": "1000",
+        "value": "",
         "class": "billing",
         "is_public": 0,
         "type": "string",
-        "default": "1000",
+        "default": "",
         "mark": "易支付商户ID"
     },
 	{
         "id": null,
         "item": "epay_key",
-        "value": "1000",
+        "value": "",
         "class": "billing",
         "is_public": 0,
         "type": "string",
-        "default": "1000",
+        "default": "",
         "mark": "易支付商户Key"
     },
+    {
+        "id": null,
+        "item": "epay_sign_type",
+        "value": "sha256",
+        "class": "billing",
+        "is_public": 0,
+        "type": "string",
+        "default": "sha256",
+        "mark": "易支付签名方式"
+    },
     {
         "id": null,
         "item": "epay_alipay",

+ 2 - 0
db/migrations/2023020100-init.php

@@ -142,6 +142,8 @@ return new class() implements MigrationInterface {
                 `status` varchar(255) NOT NULL DEFAULT '' COMMENT '节点状态',
                 `sort` tinyint(2) unsigned NOT NULL DEFAULT 14 COMMENT '节点类型',
                 `traffic_rate` float unsigned NOT NULL DEFAULT 1 COMMENT '流量倍率',
+                `is_dynamic_rate` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否启用动态流量倍率',
+                `dynamic_rate_config` longtext NOT NULL DEFAULT '{}' COMMENT '动态流量倍率配置' CHECK (json_valid(`custom_config`)),
                 `node_class` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT '节点等级',
                 `node_speedlimit` double unsigned NOT NULL DEFAULT 0 COMMENT '节点限速',
                 `node_bandwidth` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '节点流量',

+ 28 - 0
db/migrations/2023102200-add_node_dynamic_rate.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+use App\Interfaces\MigrationInterface;
+use App\Services\DB;
+
+return new class() implements MigrationInterface {
+    public function up(): int
+    {
+        DB::getPdo()->exec("
+            ALTER TABLE node ADD COLUMN IF NOT EXISTS `is_dynamic_rate` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否启用动态流量倍率';
+            ALTER TABLE node ADD COLUMN IF NOT EXISTS `dynamic_rate_config` longtext NOT NULL DEFAULT '{}' COMMENT '动态流量倍率配置' CHECK (json_valid(`custom_config`));
+        ");
+
+        return 2023102200;
+    }
+
+    public function down(): int
+    {
+        DB::getPdo()->exec("
+            ALTER TABLE node DROP COLUMN IF EXISTS `is_dynamic_rate`;
+            ALTER TABLE node DROP COLUMN IF EXISTS `dynamic_rate_config`;
+        ");
+
+        return 2023082000;
+    }
+};

+ 44 - 13
resources/views/tabler/admin/node/create.tpl

@@ -80,21 +80,52 @@
                                     修改节点自定义配置
                                 </label>
                             </div>
-                            <div class="mb-3">
-                                <div class="divide-y">
-                                    <div>
-                                        <label class="row">
-                                            <span class="col">显示此节点</span>
-                                            <span class="col-auto">
-                                                <label class="form-check form-check-single form-switch">
-                                                    <input id="type" class="form-check-input" type="checkbox"
-                                                           checked="">
-                                                </label>
-                                            </span>
-                                        </label>
-                                    </div>
+                            <div class="form-group mb-3 row">
+                                <span class="col">显示此节点</span>
+                                <span class="col-auto">
+                                      <label class="form-check form-check-single form-switch">
+                                          <input id="type" class="form-check-input" type="checkbox" {if $node->type}checked="" {/if}>
+                                      </label>
+                                  </span>
+                            </div>
+                            <div class="hr-text">
+                                <span>动态倍率</span>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <span class="col">启用动态流量倍率</span>
+                                <span class="col-auto">
+                                      <label class="form-check form-check-single form-switch">
+                                          <input id="is_dynamic_rate" class="form-check-input" type="checkbox" {if $node->is_dynamic_rate}checked="" {/if}>
+                                      </label>
+                                  </span>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">最大倍率</label>
+                                <div class="col">
+                                    <input id="max_rate" type="text" class="form-control" value="{$node->max_rate}">
+                                </div>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">最大倍率时间(时)</label>
+                                <div class="col">
+                                    <input id="max_rate_time" type="text" class="form-control" value="{$node->max_rate_time}">
+                                </div>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">最小倍率</label>
+                                <div class="col">
+                                    <input id="min_rate" type="text" class="form-control" value="{$node->min_rate}">
                                 </div>
                             </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">最小倍率时间(时)</label>
+                                <div class="col">
+                                    <input id="min_rate_time" type="text" class="form-control" value="{$node->min_rate_time}">
+                                </div>
+                                <label class="form-label col-form-label">
+                                    最大倍率时间必须大于最小倍率时间,否则将不会生效
+                                </label>
+                            </div>
                         </div>
                     </div>
                 </div>

+ 47 - 15
resources/views/tabler/admin/node/edit.tpl

@@ -80,21 +80,52 @@
                                     修改节点自定义配置
                                 </label>
                             </div>
-                            <div class="mb-3">
-                                <div class="divide-y">
-                                    <div>
-                                        <label class="row">
-                                            <span class="col">显示此节点</span>
-                                            <span class="col-auto">
-                                                <label class="form-check form-check-single form-switch">
-                                                    <input id="type" class="form-check-input" type="checkbox"
-                                                           {if $node->type === 1}checked="" {/if}>
-                                                </label>
-                                            </span>
-                                        </label>
-                                    </div>
+                            <div class="form-group mb-3 row">
+                                  <span class="col">显示此节点</span>
+                                  <span class="col-auto">
+                                      <label class="form-check form-check-single form-switch">
+                                          <input id="type" class="form-check-input" type="checkbox" {if $node->type}checked="" {/if}>
+                                      </label>
+                                  </span>
+                            </div>
+                            <div class="hr-text">
+                                <span>动态倍率</span>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <span class="col">启用动态流量倍率</span>
+                                <span class="col-auto">
+                                      <label class="form-check form-check-single form-switch">
+                                          <input id="is_dynamic_rate" class="form-check-input" type="checkbox" {if $node->is_dynamic_rate}checked="" {/if}>
+                                      </label>
+                                  </span>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">最大倍率</label>
+                                <div class="col">
+                                    <input id="max_rate" type="text" class="form-control" value="{$node->max_rate}">
                                 </div>
                             </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">最大倍率时间(时)</label>
+                                <div class="col">
+                                    <input id="max_rate_time" type="text" class="form-control" value="{$node->max_rate_time}">
+                                </div>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">最小倍率</label>
+                                <div class="col">
+                                    <input id="min_rate" type="text" class="form-control" value="{$node->min_rate}">
+                                </div>
+                            </div>
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">最小倍率时间(时)</label>
+                                <div class="col">
+                                    <input id="min_rate_time" type="text" class="form-control" value="{$node->min_rate_time}">
+                                </div>
+                                <label class="form-label col-form-label">
+                                    最大倍率时间必须大于最小倍率时间,否则将不会生效
+                                </label>
+                            </div>
                         </div>
                     </div>
                 </div>
@@ -129,14 +160,14 @@
                                 <label class="form-label col-3 col-form-label">已用流量 (GB)</label>
                                 <div class="col">
                                     <input id="node_bandwidth" type="text" class="form-control"
-                                           value="{round($node->node_bandwidth / 1073741824, 2)}" disabled="">
+                                           value="{$node->node_bandwidth}" disabled="">
                                 </div>
                             </div>
                             <div class="form-group mb-3 row">
                                 <label class="form-label col-3 col-form-label">可用流量 (GB)</label>
                                 <div class="col">
                                     <input id="node_bandwidth_limit" type="text" class="form-control"
-                                           value="{round($node->node_bandwidth_limit / 1073741824, 2)}">
+                                           value="{$node->node_bandwidth_limit}">
                                 </div>
                             </div>
                             <div class="form-group mb-3 row">
@@ -222,6 +253,7 @@
                 {$key}: $('#{$key}').val(),
                 {/foreach}
                 type: $("#type").is(":checked"),
+                is_dynamic_rate: $("#is_dynamic_rate").is(":checked"),
                 custom_config: JSON.stringify(editor.get()),
             },
             success: function (data) {

+ 8 - 1
resources/views/tabler/admin/setting/billing.tpl

@@ -199,12 +199,19 @@
                                             </div>
                                         </div>
                                         <div class="form-group mb-3 row">
-                                            <label class="form-label col-3 col-form-label">密钥</label>
+                                            <label class="form-label col-3 col-form-label">商户Key</label>
                                             <div class="col">
                                                 <input id="epay_key" type="text" class="form-control"
                                                        value="{$settings['epay_key']}">
                                             </div>
                                         </div>
+                                        <div class="form-group mb-3 row">
+                                            <label class="form-label col-3 col-form-label">签名方式</label>
+                                            <div class="col">
+                                                <input id="epay_sign_type" type="text" class="form-control"
+                                                       value="{$settings['epay_sign_type']}">
+                                            </div>
+                                        </div>
                                         <div class="form-group mb-3 row">
                                             <label class="form-label col-3 col-form-label">支付宝</label>
                                             <div class="col">

+ 2 - 4
resources/views/tabler/gateway/stripe.tpl

@@ -13,10 +13,8 @@
         等标识的信用卡或借记卡</p>
     <form action="/user/payment/purchase/stripe" method="post">
         <div class="form-group form-group-label">
-            <label class="floating-label" for="amount-stripe-card">金额</label>
-            <input class="form-control maxwidth-edit" id="price" name="price"
-                   min="{$public_setting['stripe_min_recharge']}" max="{$public_setting['stripe_max_recharge']}"
-                   step="0.1" type="number" required="required">
+            <input id="price" name="price" value="{$invoice->price}" hidden>
+            <input id="invoice_id" name="invoice_id" value="{$invoice->id}" hidden>
             <button class="btn btn-flat waves-attach" type="submit"><i class="icon ti ti-credit-card"></i></button>
         </div>
     </form>

+ 4 - 0
resources/views/tabler/user/header.tpl

@@ -126,6 +126,10 @@
                                     <i class="ti ti-server"></i>&nbsp;
                                     节点
                                 </a>
+                                <a class="dropdown-item" href="/user/rate">
+                                    <i class="ti ti-chart-bar"></i>&nbsp;
+                                    流量倍率
+                                </a>
                             </div>
                         </li>
                         <li class="nav-item dropdown">

+ 144 - 0
resources/views/tabler/user/rate.tpl

@@ -0,0 +1,144 @@
+{include file='user/header.tpl'}
+
+<script src="//{$config['jsdelivr_url']}/npm/htmx.org@latest/dist/htmx.min.js"></script>
+
+<div class="page-wrapper">
+    <div class="container-xl">
+        <div class="page-header d-print-none text-white">
+            <div class="row align-items-center">
+                <div class="col">
+                    <h2 class="page-title">
+                        <span class="home-title">流量倍率</span>
+                    </h2>
+                    <div class="page-pretitle my-3">
+                        <span class="home-subtitle">查看节点的每小时流量倍率</span>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="page-body">
+        <div class="container-xl">
+            <div class="row row-deck row-cards">
+                <div class="col-sm-12 col-lg-12">
+                    <div class="card">
+                        <div class="card-body">
+                            <div class="d-flex">
+                                <h3 class="card-title">流量倍率图表</h3>
+                                <div class="ms-auto">
+                                    <div class="dropdown">
+                                        <a id="dropdown-toggle" class="dropdown-toggle text-secondary" href="#" data-bs-toggle="dropdown"
+                                           aria-haspopup="true" aria-expanded="false">{$nodes[0]['name']}</a>
+                                        <div class="dropdown-menu dropdown-menu-end">
+                                            {foreach $nodes as $node}
+                                            <a class="dropdown-item" hx-post="/user/rate" hx-swap="none"
+                                               hx-vals='
+                                                {
+                                                   "node_id": "{$node['id']}"
+                                                }
+                                            '>{$node['name']}</a>
+                                            {/foreach}
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                            <div id="rate-chart"></div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        htmx.on("htmx:afterRequest", function(evt) {
+            var chart = window.ApexCharts && new ApexCharts(document.getElementById('rate-chart'), {
+                chart: {
+                    type: "bar",
+                    fontFamily: 'inherit',
+                    height: '250%',
+                    parentHeightOffset: 0,
+                    toolbar: {
+                        show: false,
+                    },
+                    animations: {
+                        enabled: false,
+                    },
+                },
+                plotOptions: {
+                    bar: {
+                        columnWidth: '70%',
+                        borderRadius: 5,
+                        dataLabels: {
+                            position: 'top'
+                        }
+                    }
+                },
+                dataLabels: {
+                    enabled: true,
+                    style: {
+                        fontSize: '13px',
+                    }
+                },
+                fill: {
+                    opacity: 1,
+                },
+                series: [{
+                    name: "倍率",
+                    data: []
+                }],
+                tooltip: {
+                    theme: 'dark'
+                },
+                grid: {
+                    padding: {
+                        top: -20,
+                        right: 0,
+                        left: -4,
+                        bottom: -4
+                    },
+                    strokeDashArray: 4,
+                },
+                xaxis: {
+                    title: {
+                        text: '小时',
+                    },
+                    labels: {
+                        padding: 0,
+                    },
+                    tooltip: {
+                        enabled: false
+                    },
+                    axisBorder: {
+                        show: false,
+                    },
+                    categories: ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'],
+                },
+                yaxis: {
+                    title: {
+                        text: '倍率',
+                        rotate: 0,
+                    },
+                    labels: {
+                        padding: 4,
+                    },
+                },
+                colors: [tabler.getColor("azure")],
+                legend: {
+                    show: false,
+                },
+            });
+            document.getElementById('dropdown-toggle').innerHTML = JSON.parse(evt.detail.xhr.response).msg;
+            chart.render();
+            chart.updateOptions({
+                series: [{
+                    name: "倍率",
+                    data: JSON.parse(evt.detail.xhr.response).data
+                }],
+            });
+        });
+    </script>
+
+    <script src="//{$config['jsdelivr_url']}/npm/@tabler/core@latest/dist/libs/apexcharts/dist/apexcharts.min.js"></script>
+
+{include file='user/footer.tpl'}

+ 5 - 1
resources/views/tabler/user/server.tpl

@@ -59,7 +59,11 @@
                                                                     </li>
                                                                     <li class="list-inline-item">
                                                                         <i class="ti ti-rocket"></i>&nbsp;
-                                                                        {$server["traffic_rate"]} 倍
+                                                                        {if $server["is_dynamic_rate"]}
+                                                                            动态倍率
+                                                                        {else}
+                                                                            {$server["traffic_rate"]} 倍
+                                                                        {/if}
                                                                     </li>
                                                                     <li class="list-inline-item">
                                                                         <i class="ti ti-server-2"></i>&nbsp;

+ 36 - 0
src/Controllers/Admin/NodeController.php

@@ -15,6 +15,7 @@ use Exception;
 use Psr\Http\Message\ResponseInterface;
 use Slim\Http\Response;
 use Slim\Http\ServerRequest;
+use function json_encode;
 use function trim;
 
 final class NodeController extends BaseController
@@ -28,6 +29,7 @@ final class NodeController extends BaseController
             'type' => '状态',
             'sort' => '类型',
             'traffic_rate' => '倍率',
+            'is_dynamic_rate' => '是否启用动态流量倍率',
             'node_class' => '等级',
             'node_group' => '组别',
             'node_bandwidth_limit' => '流量限制/GB',
@@ -40,6 +42,11 @@ final class NodeController extends BaseController
         'name',
         'server',
         'traffic_rate',
+        'is_dynamic_rate',
+        'max_rate',
+        'max_rate_time',
+        'min_rate',
+        'min_rate_time',
         'info',
         'node_group',
         'node_speedlimit',
@@ -88,6 +95,11 @@ final class NodeController extends BaseController
         $name = $request->getParam('name') ?? '';
         $server = trim($request->getParam('server'));
         $traffic_rate = $request->getParam('traffic_rate') ?? 1;
+        $is_dynamic_rate = $request->getParam('is_dynamic_rate') === 'true' ? 1 : 0;
+        $max_rate = $request->getParam('max_rate') ?? 1;
+        $max_rate_time = $request->getParam('max_rate_time') ?? 0;
+        $min_rate = $request->getParam('min_rate') ?? 1;
+        $min_rate_time = $request->getParam('min_rate_time') ?? 0;
         $custom_config = $request->getParam('custom_config') ?? '{}';
         $info = $request->getParam('info') ?? '';
         $type = $request->getParam('type') === 'true' ? 1 : 0;
@@ -117,7 +129,15 @@ final class NodeController extends BaseController
         $node = new Node();
         $node->name = $name;
         $node->server = $server;
+
         $node->traffic_rate = $traffic_rate;
+        $node->is_dynamic_rate = $is_dynamic_rate;
+        $node->dynamic_rate_config = json_encode([
+            'max_rate' => $max_rate,
+            'max_rate_time' => $max_rate_time,
+            'min_rate' => $min_rate,
+            'min_rate_time' => $min_rate_time,
+        ]);
 
         if ($custom_config !== '') {
             $node->custom_config = $custom_config;
@@ -191,6 +211,15 @@ final class NodeController extends BaseController
         $id = $args['id'];
         $node = Node::find($id);
 
+        $dynamic_rate_config = json_decode($node->dynamic_rate_config);
+        $node->max_rate = $dynamic_rate_config?->max_rate;
+        $node->max_rate_time = $dynamic_rate_config?->max_rate_time;
+        $node->min_rate = $dynamic_rate_config?->min_rate;
+        $node->min_rate_time = $dynamic_rate_config?->min_rate_time;
+
+        $node->node_bandwidth = Tools::flowToGB($node->node_bandwidth);
+        $node->node_bandwidth_limit = Tools::flowToGB($node->node_bandwidth_limit);
+
         return $response->write(
             $this->view()
                 ->assign('node', $node)
@@ -213,6 +242,13 @@ final class NodeController extends BaseController
         $node->node_group = $request->getParam('node_group');
         $node->server = trim($request->getParam('server'));
         $node->traffic_rate = $request->getParam('traffic_rate');
+        $node->is_dynamic_rate = $request->getParam('is_dynamic_rate') === 'true' ? 1 : 0;
+        $node->dynamic_rate_config = json_encode([
+            'max_rate' => $request->getParam('max_rate') ?? 1,
+            'max_rate_time' => $request->getParam('max_rate_time') ?? 0,
+            'min_rate' => $request->getParam('min_rate') ?? 1,
+            'min_rate_time' => $request->getParam('min_rate_time') ?? 0,
+        ]);
         $node->info = $request->getParam('info');
         $node->node_speedlimit = $request->getParam('node_speedlimit');
         $node->type = $request->getParam('type') === 'true' ? 1 : 0;

+ 1 - 0
src/Controllers/Admin/Setting/BillingController.php

@@ -34,6 +34,7 @@ final class BillingController extends BaseController
         'epay_url',
         'epay_pid',
         'epay_key',
+        'epay_sign_type',
         'epay_alipay',
         'epay_wechat',
         'epay_qq',

+ 79 - 0
src/Controllers/User/RateController.php

@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controllers\User;
+
+use App\Controllers\BaseController;
+use App\Models\Node;
+use App\Services\DynamicRate;
+use App\Utils\ResponseHelper;
+use Exception;
+use Psr\Http\Message\ResponseInterface;
+use Slim\Http\Response;
+use Slim\Http\ServerRequest;
+use voku\helper\AntiXSS;
+use function array_fill;
+
+final class RateController extends BaseController
+{
+    /**
+     * @throws Exception
+     */
+    public function index(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
+    {
+        $user = $this->user;
+        $query = Node::query();
+        $query->where('type', 1);
+
+        if (! $user->is_admin) {
+            $group = ($user->node_group !== 0 ? [0, $user->node_group] : [0]);
+            $query->whereIn('node_group', $group);
+        }
+
+        $nodes = $query->orderBy('node_class')->orderBy('name')->get();
+        $all_node = [];
+
+        foreach ($nodes as $node) {
+            if ($node->node_bandwidth_limit !== 0 && $node->node_bandwidth_limit <= $node->node_bandwidth) {
+                continue;
+            }
+
+            $array_node = [];
+            $array_node['id'] = $node->id;
+            $array_node['name'] = $node->name;
+
+            $all_node[] = $array_node;
+        }
+
+        return $response->write(
+            $this->view()
+                ->assign('nodes', $all_node)
+                ->fetch('user/rate.tpl')
+        );
+    }
+
+    public function ajax(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
+    {
+        $antiXss = new AntiXSS();
+        $node = Node::find($antiXss->xss_clean($request->getParam('node_id')));
+
+        if ($node === null) {
+            return ResponseHelper::error($response, '节点不存在');
+        }
+
+        if ($node->is_dynamic_rate) {
+            $dynamic_rate_config = json_decode($node->dynamic_rate_config);
+            $rates = DynamicRate::getFullDayRates(
+                (float) $dynamic_rate_config?->max_rate,
+                (int) $dynamic_rate_config?->max_rate_time,
+                (float) $dynamic_rate_config?->min_rate,
+                (int) $dynamic_rate_config?->min_rate_time,
+            );
+        } else {
+            $rates = array_fill(0, 24, $node->traffic_rate);
+        }
+
+        return ResponseHelper::successWithData($response, $node->name, $rates);
+    }
+}

+ 1 - 0
src/Controllers/User/ServerController.php

@@ -48,6 +48,7 @@ final class ServerController extends BaseController
             $array_node['online_user'] = $node->online_user;
             $array_node['online'] = $node->getNodeOnlineStatus();
             $array_node['traffic_rate'] = $node->traffic_rate;
+            $array_node['is_dynamic_rate'] = $node->is_dynamic_rate;
             $array_node['node_bandwidth'] = Tools::autoBytes($node->node_bandwidth);
             $array_node['node_bandwidth_limit'] = $node->node_bandwidth_limit === 0 ? '无限制' :
                 Tools::autoBytes($node->node_bandwidth_limit);

+ 15 - 1
src/Controllers/WebAPI/UserController.php

@@ -8,6 +8,7 @@ use App\Controllers\BaseController;
 use App\Models\DetectLog;
 use App\Models\Node;
 use App\Services\DB;
+use App\Services\DynamicRate;
 use App\Utils\ResponseHelper;
 use App\Utils\Tools;
 use Psr\Http\Message\ResponseInterface;
@@ -167,7 +168,20 @@ final class UserController extends BaseController
                 transfer_total = transfer_total + ?,
                 transfer_today = transfer_today + ? WHERE id = ?
         ');
-        $rate = (float) $node->traffic_rate;
+
+        if ($node->is_dynamic_rate) {
+            $dynamic_rate_config = json_decode($node->dynamic_rate_config);
+            $rate = DynamicRate::getRateByTime(
+                (float) $dynamic_rate_config?->max_rate,
+                (int) $dynamic_rate_config?->max_rate_time,
+                (float) $dynamic_rate_config?->min_rate,
+                (int) $dynamic_rate_config?->min_rate_time,
+                (int) date('H')
+            );
+        } else {
+            $rate = (float) $node->traffic_rate;
+        }
+
         $sum = 0;
 
         foreach ($data as $log) {

+ 44 - 44
src/Services/Captcha.php

@@ -5,6 +5,8 @@ declare(strict_types=1);
 namespace App\Services;
 
 use App\Models\Setting;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
 use function hash_hmac;
 use function json_decode;
 
@@ -29,38 +31,36 @@ final class Captcha
     public static function verify($param): bool
     {
         $result = false;
+        $client = new Client();
 
         switch (Setting::obtain('captcha_provider')) {
             case 'turnstile':
-                $turnstile = $param['turnstile'] ?? '';
-                if ($turnstile !== '') {
-                    $postdata = http_build_query(
-                        [
-                            'secret' => Setting::obtain('turnstile_secret'),
-                            'response' => $turnstile,
-                        ]
-                    );
+                if (isset($param['turnstile'])) {
+                    $turnstile_url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
 
-                    $opts = [
-                        'http' => [
-                            'method' => 'POST',
-                            'header' => 'Content-Type: application/x-www-form-urlencoded',
-                            'content' => $postdata,
-                        ],
+                    $turnstile_headers = [
+                        'Content-Type' => 'application/x-www-form-urlencoded',
                     ];
 
-                    $json = json_decode(file_get_contents(
-                        'https://challenges.cloudflare.com/turnstile/v0/siteverify',
-                        false,
-                        stream_context_create($opts)
-                    ));
+                    $turnstile_body = [
+                        'secret' => Setting::obtain('turnstile_secret'),
+                        'response' => $param['turnstile'],
+                    ];
 
-                    $result = $json->success;
+                    try {
+                        $result = json_decode($client->post($turnstile_url, [
+                            'headers' => $turnstile_headers,
+                            'form_params' => $turnstile_body,
+                            'timeout' => 3,
+                        ])->getBody()->getContents())->success;
+                    } catch (GuzzleException $e) {
+                        echo $e->getMessage();
+                    }
                 }
                 break;
             case 'geetest':
-                $geetest = $param['geetest'] ?? [];
-                if ($geetest !== []) {
+                if (isset($param['geetest'])) {
+                    $geetest = $param['geetest'];
                     $captcha_id = Setting::obtain('geetest_id');
                     $captcha_key = Setting::obtain('geetest_key');
                     $lot_number = $geetest['lot_number'];
@@ -69,32 +69,32 @@ final class Captcha
                     $gen_time = $geetest['gen_time'];
                     $sign_token = hash_hmac('sha256', $lot_number, $captcha_key);
 
-                    $postdata = http_build_query(
-                        [
-                            'lot_number' => $lot_number,
-                            'captcha_output' => $captcha_output,
-                            'pass_token' => $pass_token,
-                            'gen_time' => $gen_time,
-                            'sign_token' => $sign_token,
-                        ]
-                    );
+                    $geetest_headers = [
+                        'Content-Type' => 'application/x-www-form-urlencoded',
+                    ];
 
-                    $opts = [
-                        'http' => [
-                            'method' => 'POST',
-                            'header' => 'Content-type: application/x-www-form-urlencoded',
-                            'content' => $postdata,
-                            'timeout' => 5,
-                        ],
+                    $geetest_body = [
+                        'lot_number' => $lot_number,
+                        'captcha_output' => $captcha_output,
+                        'pass_token' => $pass_token,
+                        'gen_time' => $gen_time,
+                        'sign_token' => $sign_token,
                     ];
 
-                    $json = json_decode(file_get_contents(
-                        'https://gcaptcha4.geetest.com/validate?captcha_id=' . $captcha_id,
-                        false,
-                        stream_context_create($opts)
-                    ));
+                    $geetest_url = 'https://gcaptcha4.geetest.com/validate?captcha_id=' . $captcha_id;
+
+                    try {
+                        $json = json_decode($client->post($geetest_url, [
+                            'headers' => $geetest_headers,
+                            'form_params' => $geetest_body,
+                            'timeout' => 3,
+                        ])->getBody()->getContents());
+                    } catch (GuzzleException $e) {
+                        $json = null;
+                        echo $e->getMessage();
+                    }
 
-                    if ($json->result === 'success') {
+                    if ($json?->result === 'success') {
                         $result = true;
                     }
                 }

+ 12 - 6
src/Services/DynamicRate.php

@@ -12,9 +12,9 @@ final class DynamicRate
         float $min_rate,
         int $min_rate_time,
         string $method = 'logistic',
-    ): array|string {
+    ): array {
         if (! self::validateData($max_rate, $max_rate_time, $min_rate, $min_rate_time)) {
-            return 'Invalid data';
+            return [];
         }
 
         $rates = [];
@@ -33,9 +33,9 @@ final class DynamicRate
         int $min_rate_time,
         int $time,
         string $method = 'logistic',
-    ): float|string {
+    ): float {
         if (! self::validateData($max_rate, $max_rate_time, $min_rate, $min_rate_time)) {
-            return 'Invalid data';
+            return 1;
         }
 
         if ($time === $max_rate_time || $max_rate_time === $min_rate_time || $max_rate === $min_rate) {
@@ -85,7 +85,10 @@ final class DynamicRate
         $k = $time < $max_rate_time ? -0.7 : 1.3;
         $e = M_E;
 
-        return ($max_rate - $min_rate) / (1 + $e ** ($k * ($time - ($max_rate_time + $min_rate_time) / 2))) + $min_rate;
+        return round(
+            ($max_rate - $min_rate) / (1 + $e ** ($k * ($time - ($max_rate_time + $min_rate_time) / 2))) + $min_rate,
+            2
+        );
     }
 
     public static function linear(
@@ -98,6 +101,9 @@ final class DynamicRate
         $k = ($max_rate - $min_rate) / ($max_rate_time - $min_rate_time);
         $b = $max_rate - $k * $max_rate_time;
 
-        return $k * $time + $b;
+        return round(
+            $k * $time + $b,
+            2
+        );
     }
 }

+ 15 - 18
src/Services/Gateway/AbstractPayment.php

@@ -16,7 +16,6 @@ use Slim\Http\ServerRequest;
 use function get_called_class;
 use function in_array;
 use function json_decode;
-use function json_encode;
 use function time;
 
 abstract class AbstractPayment
@@ -57,32 +56,30 @@ abstract class AbstractPayment
 
     abstract public static function getPurchaseHTML(): string;
 
-    public function postPayment($tradeno): false|int|string
+    public function postPayment($trade_no): void
     {
-        $paylist = Paylist::where('tradeno', $tradeno)->first();
+        $paylist = Paylist::where('tradeno', $trade_no)->first();
 
-        if ($paylist->status === 1) {
-            return json_encode(['errcode' => 0]);
+        if ($paylist?->status === 0) {
+            $paylist->datetime = time();
+            $paylist->status = 1;
+            $paylist->save();
         }
 
-        $paylist->datetime = time();
-        $paylist->status = 1;
-        $paylist->save();
+        $invoice = Invoice::where('id', $paylist?->invoice_id)->first();
 
-        $user = User::find($paylist->userid);
-
-        $invoice = Invoice::where('id', $paylist->invoice_id)->first();
-        $invoice->status = 'paid_gateway';
-        $invoice->update_time = time();
-        $invoice->pay_time = time();
-        $invoice->save();
+        if ($invoice?->status === 'unpaid' && (int) $invoice?->price === (int) $paylist?->total) {
+            $invoice->status = 'paid_gateway';
+            $invoice->update_time = time();
+            $invoice->pay_time = time();
+            $invoice->save();
+        }
 
+        $user = User::find($paylist?->userid);
         // 返利
-        if ($user->ref_by > 0 && Setting::obtain('invitation_mode') === 'after_paid') {
+        if ($user !== null && $user->ref_by > 0 && Setting::obtain('invitation_mode') === 'after_paid') {
             (new Payback())->rebate($user->id, $paylist->total);
         }
-
-        return 0;
     }
 
     public static function generateGuid(): string

+ 7 - 6
src/Services/Gateway/AopF2F.php

@@ -40,6 +40,7 @@ final class AopF2F extends AbstractPayment
 
         $price = $antiXss->xss_clean($request->getParam('amount'));
         $invoice_id = $antiXss->xss_clean($request->getParam('invoice_id'));
+        $trade_no = self::generateGuid();
 
         if ($price <= 0) {
             return $response->withJson([
@@ -54,7 +55,7 @@ final class AopF2F extends AbstractPayment
         $pl->userid = $user->id;
         $pl->total = $price;
         $pl->invoice_id = $invoice_id;
-        $pl->tradeno = self::generateGuid();
+        $pl->tradeno = $trade_no;
         $pl->gateway = self::_readableName();
 
         $pl->save();
@@ -63,9 +64,9 @@ final class AopF2F extends AbstractPayment
 
         $request = $gateway->purchase();
         $request->setBizContent([
-            'subject' => $pl->tradeno,
-            'out_trade_no' => $pl->tradeno,
-            'total_amount' => $pl->total,
+            'subject' => $trade_no,
+            'out_trade_no' => $trade_no,
+            'total_amount' => $price,
         ]);
 
         $aliResponse = $request->send();
@@ -76,8 +77,8 @@ final class AopF2F extends AbstractPayment
         return $response->withJson([
             'ret' => 1,
             'qrcode' => $qrCodeContent,
-            'amount' => $pl->total,
-            'pid' => $pl->tradeno,
+            'amount' => $price,
+            'pid' => $trade_no,
         ]);
     }
 

+ 2 - 1
src/Services/Gateway/Epay.php

@@ -31,7 +31,7 @@ final class Epay extends AbstractPayment
         $this->epay['apiurl'] = Setting::obtain('epay_url');//易支付API地址
         $this->epay['partner'] = Setting::obtain('epay_pid');//易支付商户pid
         $this->epay['key'] = Setting::obtain('epay_key');//易支付商户Key
-        $this->epay['sign_type'] = strtoupper('SHA256'); //签名方式
+        $this->epay['sign_type'] = strtoupper(Setting::obtain('epay_sign_type')); //签名方式
         $this->epay['input_charset'] = strtolower('utf-8');//字符编码
         $this->epay['transport'] = 'https';//协议 http 或者https
     }
@@ -123,6 +123,7 @@ final class Epay extends AbstractPayment
 
             if ($trade_status === 'TRADE_SUCCESS') {
                 $this->postPayment($out_trade_no);
+
                 return $response->withJson(['state' => 'success', 'msg' => '支付成功']);
             }
 

+ 3 - 2
src/Services/Gateway/Epay/EpayTool.php

@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace App\Services\Gateway\Epay;
 
+use App\Models\Setting;
 use function hash;
 use function strlen;
 
@@ -13,13 +14,13 @@ final class EpayTool
     {
         $prestr .= $key;
 
-        return hash('sha256', $prestr);
+        return hash(Setting::obtain('epay_sign_type'), $prestr);
     }
 
     public static function verify($prestr, $sign, $key): bool
     {
         $prestr .= $key;
-        $correct_sign = hash('sha256', $prestr);
+        $correct_sign = hash(Setting::obtain('epay_sign_type'), $prestr);
 
         return $correct_sign === $sign;
     }

+ 1 - 2
src/Services/Gateway/PayPal.php

@@ -131,8 +131,7 @@ final class PayPal extends AbstractPayment
         $result = $pp->capturePaymentOrder($order_id);
 
         if (isset($result['status']) && $result['status'] === 'COMPLETED') {
-            $trade_no = $result['purchase_units'][0]['reference_id'];
-            $this->postPayment($trade_no);
+            $this->postPayment($result['purchase_units'][0]['reference_id']);
 
             return $response->withJson([
                 'ret' => 1,

+ 36 - 35
src/Services/Gateway/StripeCard.php

@@ -19,6 +19,7 @@ use Stripe\Checkout\Session;
 use Stripe\Exception\ApiErrorException;
 use Stripe\Stripe;
 use Stripe\StripeClient;
+use voku\helper\AntiXSS;
 
 final class StripeCard extends AbstractPayment
 {
@@ -43,50 +44,59 @@ final class StripeCard extends AbstractPayment
      */
     public function purchase(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
-        $user = Auth::getUser();
-        $configs = Setting::getClass('billing');
-        $price = $request->getParam('price');
+        $antiXss = new AntiXSS();
+
+        $price = $antiXss->xss_clean($request->getParam('price'));
+        $invoice_id = $antiXss->xss_clean($request->getParam('invoice_id'));
         $trade_no = self::generateGuid();
 
+        if ($price < Setting::obtain('stripe_min_recharge') ||
+            $price > Setting::obtain('stripe_max_recharge')
+        ) {
+            return $response->withJson([
+                'ret' => 0,
+                'msg' => '非法的金额',
+            ]);
+        }
+
+        $user = Auth::getUser();
         $pl = new Paylist();
+
         $pl->userid = $user->id;
         $pl->total = $price;
+        $pl->invoice_id = $invoice_id;
         $pl->tradeno = $trade_no;
         $pl->save();
 
-        $params = [
-            'trade_no' => $trade_no,
-            'sign' => hash('sha256', $trade_no . ':' . $configs['stripe_webhook_key']),
-        ];
-
-        $exchange_amount = Exchange::exchange($price, 'CNY', $configs['stripe_currency']);
+        $exchange_amount = Exchange::exchange($price, 'CNY', Setting::obtain('stripe_currency'));
 
-        Stripe::setApiKey($configs['stripe_sk']);
+        Stripe::setApiKey(Setting::obtain('stripe_sk'));
         $session = null;
 
         try {
             $session = Session::create([
                 'customer_email' => $user->email,
-                'line_items' => [[
-                    'price_data' => [
-                        'currency' => $configs['stripe_currency'],
-                        'product_data' => [
-                            'name' => 'Account Recharge',
+                'line_items' => [
+                    [
+                        'price_data' => [
+                            'currency' => Setting::obtain('stripe_currency'),
+                            'product_data' => [
+                                'name' => 'Account Recharge',
+                            ],
+                            'unit_amount' => (int) $exchange_amount,
                         ],
-                        'unit_amount' => (int) $exchange_amount,
+                        'quantity' => 1,
                     ],
-                    'quantity' => 1,
-                ],
                 ],
                 'mode' => 'payment',
-                'success_url' => self::getUserReturnUrl() .
-                    '?session_id={CHECKOUT_SESSION_ID}&' . http_build_query($params),
+                'client_reference_id' => $trade_no,
+                'success_url' => self::getUserReturnUrl() . '?session_id={CHECKOUT_SESSION_ID}',
                 'cancel_url' => $_ENV['baseUrl'] . '/user/invoice',
             ]);
         } catch (ApiErrorException $e) {
             return $response->withJson([
                 'ret' => 0,
-                'msg' => $e->getMessage(),
+                'msg' => 'Stripe API error',
             ]);
         }
 
@@ -108,18 +118,9 @@ final class StripeCard extends AbstractPayment
 
     public function getReturnHTML($request, $response, $args): ResponseInterface
     {
-        $sign = $request->getParam('sign');
-        $trade_no = $request->getParam('trade_no');
-        $session_id = $request->getParam('session_id');
-
-        $correct_sign = hash('sha256', $trade_no . ':' . Setting::obtain('stripe_webhook_key'));
+        $antiXss = new AntiXSS();
 
-        if ($correct_sign !== $sign) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Sign error',
-            ]);
-        }
+        $session_id = $antiXss->xss_clean($request->getParam('session_id'));
 
         $stripe = new StripeClient(Setting::obtain('stripe_sk'));
         $session = null;
@@ -129,12 +130,12 @@ final class StripeCard extends AbstractPayment
         } catch (ApiErrorException $e) {
             return $response->withJson([
                 'ret' => 0,
-                'msg' => $e->getMessage(),
+                'msg' => 'Stripe API error',
             ]);
         }
 
-        if ($session->payment_status === 'paid') {
-            $this->postPayment($trade_no);
+        if ($session !== null && $session->payment_status === 'paid') {
+            $this->postPayment($session->client_reference_id);
         }
 
         return $response->withRedirect($_ENV['baseUrl'] . '/user/invoice');