瀏覽代碼

feat: node dynamic rate configuration

M1Screw 2 年之前
父節點
當前提交
5b09d2ae98

+ 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');
         // 工单

+ 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) {

+ 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'}

+ 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;

+ 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);
+    }
+}

+ 8 - 2
src/Services/DynamicRate.php

@@ -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
+        );
     }
 }