Compare commits

...

3 Commits

Author SHA1 Message Date
xboard
d6a3614d98 update 2026_03_28_161536_add_traffic_fields_to_servers.php 2026-03-30 02:58:09 +08:00
xboard
a58d66d72e feat: node traffic limit & batch operations
- Traffic monitoring with transfer_enable limit
- Batch delete nodes
- Reset traffic (single/batch)
2026-03-30 02:50:56 +08:00
xboard
daf3055b42 fix: escape Telegram Markdown special characters 2026-03-30 01:46:56 +08:00
10 changed files with 187 additions and 12 deletions

View File

@@ -111,6 +111,94 @@ class ManageController extends Controller
return $this->success(true);
}
/**
* 批量删除节点
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function batchDelete(Request $request)
{
$request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
]);
$ids = $request->input('ids');
if (empty($ids)) {
return $this->fail([400, '请选择要删除的节点']);
}
try {
$deleted = Server::whereIn('id', $ids)->delete();
if ($deleted === false) {
return $this->fail([500, '批量删除失败']);
}
return $this->success(true);
} catch (\Exception $e) {
Log::error($e);
return $this->fail([500, '批量删除失败']);
}
}
/**
* 重置节点流量
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function resetTraffic(Request $request)
{
$request->validate([
'id' => 'required|integer',
]);
$server = Server::find($request->id);
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->u = 0;
$server->d = 0;
$server->save();
Log::info("Server {$server->id} ({$server->name}) traffic reset by admin");
return $this->success(true);
} catch (\Exception $e) {
Log::error($e);
return $this->fail([500, '重置失败']);
}
}
/**
* 批量重置节点流量
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function batchResetTraffic(Request $request)
{
$request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
]);
$ids = $request->input('ids');
if (empty($ids)) {
return $this->fail([400, '请选择要重置的节点']);
}
try {
Server::whereIn('id', $ids)->update([
'u' => 0,
'd' => 0,
]);
Log::info("Servers " . implode(',', $ids) . " traffic reset by admin");
return $this->success(true);
} catch (\Exception $e) {
Log::error($e);
return $this->fail([500, '批量重置失败']);
}
}
/**
* 复制节点
@@ -120,11 +208,11 @@ class ManageController extends Controller
public function copy(Request $request)
{
$server = Server::find($request->input('id'));
$server->show = 0;
$server->code = null;
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
$server->show = 0;
$server->code = null;
Server::create($server->toArray());
return $this->success(true);
}

View File

@@ -128,6 +128,7 @@ class ServerSave extends FormRequest
'rate_time_ranges.*.end' => 'required_with:rate_time_ranges|string|date_format:H:i',
'rate_time_ranges.*.rate' => 'required_with:rate_time_ranges|numeric|min:0',
'protocol_settings' => 'array',
'transfer_enable' => 'nullable|integer|min:0',
];
}
@@ -200,6 +201,8 @@ class ServerSave extends FormRequest
'protocol_settings.*.string' => ':attribute 必须是字符串',
'protocol_settings.*.integer' => ':attribute 必须是整数',
'protocol_settings.*.in' => ':attribute 的值不合法',
'transfer_enable.integer' => '流量上限必须是整数',
'transfer_enable.min' => '流量上限不能小于0',
];
}
}

View File

@@ -82,6 +82,9 @@ class AdminRoute
$router->post('/drop', [ManageController::class, 'drop']);
$router->post('/copy', [ManageController::class, 'copy']);
$router->post('/sort', [ManageController::class, 'sort']);
$router->post('/batchDelete', [ManageController::class, 'batchDelete']);
$router->post('/resetTraffic', [ManageController::class, 'resetTraffic']);
$router->post('/batchResetTraffic', [ManageController::class, 'batchResetTraffic']);
});
// Order

View File

@@ -3,12 +3,14 @@
namespace App\Jobs;
use App\Models\Server;
use App\Models\StatServer;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -59,12 +61,23 @@ class StatServerJob implements ShouldQueue
try {
$this->processServerStat($u, $d, $recordAt);
$this->updateServerTraffic($u, $d);
} catch (\Exception $e) {
Log::error('StatServerJob failed for server ' . $this->server['id'] . ': ' . $e->getMessage());
throw $e;
}
}
protected function updateServerTraffic(int $u, int $d): void
{
DB::table('v2_server')
->where('id', $this->server['id'])
->incrementEach(
['u' => $u, 'd' => $d],
['updated_at' => Carbon::now()]
);
}
protected function processServerStat(int $u, int $d, int $recordAt): void
{
$driver = config('database.default');

View File

@@ -52,6 +52,10 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
* @property int|null $d 下行流量
* @property int|null $total 总流量
* @property-read array|null $load_status 负载状态包含CPU、内存、交换区、磁盘信息
*
* @property int $transfer_enable 流量上限0或者null表示不限制
* @property int $u 当前上传流量
* @property int $d 当前下载流量
*/
class Server extends Model
{
@@ -124,6 +128,9 @@ class Server extends Model
'updated_at' => 'timestamp',
'rate_time_ranges' => 'array',
'rate_time_enable' => 'boolean',
'transfer_enable' => 'integer',
'u' => 'integer',
'd' => 'integer',
];
private const MULTIPLEX_CONFIGURATION = [

View File

@@ -42,6 +42,11 @@ class ServerService
{
$servers = Server::whereJsonContains('group_ids', (string) $user->group_id)
->where('show', true)
->where(function ($query) {
$query->whereNull('transfer_enable')
->orWhere('transfer_enable', 0)
->orWhereRaw('u + d < transfer_enable');
})
->orderBy('sort', 'ASC')
->get()
->append(['last_check_at', 'last_push_at', 'online', 'is_online', 'available_status', 'cache_key', 'server_key']);
@@ -244,10 +249,10 @@ class ServerService
default => [],
};
$response = array_filter(
$response,
static fn ($value) => $value !== null
);
// $response = array_filter(
// $response,
// static fn ($value) => $value !== null
// );
if (!empty($node['route_ids'])) {
$response['routes'] = self::getRoutes($node['route_ids']);

View File

@@ -229,4 +229,14 @@ class Helper
{
return $transfer_enable / 1073741824;
}
/**
* 转义 Telegram Markdown 特殊字符
* @param string $text
* @return string
*/
public static function escapeMarkdown(string $text): string
{
return str_replace(['_', '*', '`', '['], ['\_', '\*', '\`', '\['], $text);
}
}

View File

@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('v2_server', function (Blueprint $table) {
if (!Schema::hasColumn('v2_server', 'transfer_enable')) {
$table->bigInteger('transfer_enable')
->default(null)
->nullable()
->after('rate')
->comment('Traffic limit , 0 or null=no limit');
}
if (!Schema::hasColumn('v2_server', 'u')) {
$table->bigInteger('u')
->default(0)
->after('transfer_enable')
->comment('upload traffic');
}
if (!Schema::hasColumn('v2_server', 'd')) {
$table->bigInteger('d')
->default(0)
->after('u')
->comment('donwload traffic');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('v2_server', function (Blueprint $table) {
$table->dropColumn(['transfer_enable', 'u', 'd']);
});
}
};

View File

@@ -58,8 +58,8 @@ class Plugin extends AbstractPlugin
"支付渠道:%s\n" .
"本站订单:`%s`",
$order->total_amount / 100,
$payment->payment,
$payment->name,
Helper::escapeMarkdown($payment->payment),
Helper::escapeMarkdown($payment->name),
$order->trade_no
);
$this->telegramService->sendMessageWithAdmin($message, true);
@@ -92,7 +92,7 @@ class Plugin extends AbstractPlugin
$TGmessage .= "📍 位置: `{$region}`\n";
if ($plan) {
$TGmessage .= "📦 套餐: `{$plan->name}`\n";
$TGmessage .= "📦 套餐: `" . Helper::escapeMarkdown($plan->name) . "`\n";
$TGmessage .= "📊 流量: `{$remaining_traffic}G / {$transfer_enable}G` (剩余/总计)\n";
$TGmessage .= "⬆️⬇️ 已用: `{$u}G / {$d}G`\n";
$TGmessage .= "⏰ 到期: `{$expired_at}`\n";
@@ -103,8 +103,8 @@ class Plugin extends AbstractPlugin
$TGmessage .= "💰 余额: `{$money}元`\n";
$TGmessage .= "💸 佣金: `{$affmoney}元`\n";
$TGmessage .= "━━━━━━━━━━━━━━━━━━━━\n";
$TGmessage .= "📝 *主题*: `{$ticket->subject}`\n";
$TGmessage .= "💬 *内容*: `{$message->message}`";
$TGmessage .= "📝 *主题*: `" . Helper::escapeMarkdown($ticket->subject) . "`\n";
$TGmessage .= "💬 *内容*: `" . Helper::escapeMarkdown($message->message) . "`";
$this->telegramService->sendMessageWithAdmin($TGmessage, true);
}