Compare commits

...

5 Commits

Author SHA1 Message Date
yootus
ec49ba3fd1 Loon和Surfboard适配anytls (#854)
* Loon适配anytls

* Surfboard适配anytls

Surfboard适配anytls
2026-04-02 15:47:41 +08:00
NFamou
b7c8b31a91 Merge pull request #856 from NFamou/master
支持Surfboard下发SS2022
2026-04-02 15:46:55 +08:00
xboard
f3fd40008b updata admin asset 2026-04-02 05:51:16 +08:00
Xboard
94fc5f6942 Merge pull request #841 from cedar2025/revert-755-feat/server-id-stat-user
Revert "feat: Track user traffic per node (server_id)"
2026-03-30 18:18:35 +08:00
Xboard
c5a8c836c0 Revert "feat: Track user traffic per node (server_id)" 2026-03-30 18:17:27 +08:00
9 changed files with 77 additions and 158 deletions

View File

@@ -14,29 +14,13 @@ class StatController extends Controller
public function getTrafficLog(Request $request)
{
$startDate = now()->startOfMonth()->timestamp;
// Aggregate per-node data into per-day entries for backward compatibility
$records = StatUser::query()
->select([
'user_id',
'server_rate',
'record_at',
'record_type',
DB::raw('SUM(u) as u'),
DB::raw('SUM(d) as d'),
])
->where('user_id', $request->user()->id)
->where('record_at', '>=', $startDate)
->groupBy(['user_id', 'server_rate', 'record_at', 'record_type'])
->orderBy('record_at', 'DESC')
->get()
->map(function ($item) {
$item->u = (int) $item->u;
$item->d = (int) $item->d;
return $item;
});
->get();
$data = TrafficLogResource::collection($records);
$data = TrafficLogResource::collection(collect($records));
return $this->success($data);
}
}

View File

@@ -13,7 +13,6 @@ use App\Models\Ticket;
use App\Models\User;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
@@ -235,38 +234,14 @@ class StatController extends Controller
]);
$pageSize = $request->input('pageSize', 10);
$page = $request->input('page', 1);
// Aggregate per-node data into per-day entries for backward compatibility
$query = StatUser::query()
->select([
'user_id',
'server_rate',
'record_at',
'record_type',
DB::raw('SUM(u) as u'),
DB::raw('SUM(d) as d'),
DB::raw('MAX(created_at) as created_at'),
DB::raw('MAX(updated_at) as updated_at'),
])
$records = StatUser::orderBy('record_at', 'DESC')
->where('user_id', $request->input('user_id'))
->groupBy(['user_id', 'server_rate', 'record_at', 'record_type'])
->orderBy('record_at', 'DESC');
// Manual pagination for grouped query
$total = (clone $query)->get()->count();
$data = $query->skip(($page - 1) * $pageSize)->take($pageSize)->get()
->map(function ($item) {
$item->u = (int) $item->u;
$item->d = (int) $item->d;
$item->created_at = (int) $item->created_at;
$item->updated_at = (int) $item->updated_at;
return $item;
});
->paginate($pageSize);
$data = $records->items();
return [
'data' => $data,
'total' => $total,
'total' => $records->total(),
];
}

View File

@@ -78,7 +78,6 @@ class StatUserJob implements ShouldQueue
DB::transaction(function () use ($uid, $v, $recordAt) {
$existingRecord = StatUser::where([
'user_id' => $uid,
'server_id' => $this->server['id'],
'server_rate' => $this->server['rate'],
'record_at' => $recordAt,
'record_type' => $this->recordType,
@@ -93,7 +92,6 @@ class StatUserJob implements ShouldQueue
} else {
StatUser::create([
'user_id' => $uid,
'server_id' => $this->server['id'],
'server_rate' => $this->server['rate'],
'record_at' => $recordAt,
'record_type' => $this->recordType,
@@ -111,7 +109,6 @@ class StatUserJob implements ShouldQueue
StatUser::upsert(
[
'user_id' => $uid,
'server_id' => $this->server['id'],
'server_rate' => $this->server['rate'],
'record_at' => $recordAt,
'record_type' => $this->recordType,
@@ -120,7 +117,7 @@ class StatUserJob implements ShouldQueue
'created_at' => time(),
'updated_at' => time(),
],
['user_id', 'server_id', 'server_rate', 'record_at', 'record_type'],
['user_id', 'server_rate', 'record_at', 'record_type'],
[
'u' => DB::raw("u + VALUES(u)"),
'd' => DB::raw("d + VALUES(d)"),
@@ -139,9 +136,9 @@ class StatUserJob implements ShouldQueue
$u = intval($v[0] * $this->server['rate']);
$d = intval($v[1] * $this->server['rate']);
$sql = "INSERT INTO {$table} (user_id, server_id, server_rate, record_at, record_type, u, d, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (user_id, server_id, server_rate, record_at)
$sql = "INSERT INTO {$table} (user_id, server_rate, record_at, record_type, u, d, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (user_id, server_rate, record_at)
DO UPDATE SET
u = {$table}.u + EXCLUDED.u,
d = {$table}.d + EXCLUDED.d,
@@ -149,7 +146,6 @@ class StatUserJob implements ShouldQueue
DB::statement($sql, [
$uid,
$this->server['id'],
$this->server['rate'],
$recordAt,
$this->recordType,

View File

@@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Model;
*
* @property int $id
* @property int $user_id 用户ID
* @property int|null $server_id 节点ID (nullable for legacy data)
* @property int $u 上行流量
* @property int $d 下行流量
* @property int $record_at 记录时间
@@ -26,12 +25,4 @@ class StatUser extends Model
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
/**
* Get the server that this traffic stat belongs to
*/
public function server()
{
return $this->belongsTo(Server::class, 'server_id');
}
}

View File

@@ -15,6 +15,7 @@ class Loon extends AbstractProtocol
Server::TYPE_TROJAN,
Server::TYPE_HYSTERIA,
Server::TYPE_VLESS,
Server::TYPE_ANYTLS,
];
protected $protocolRequirements = [
@@ -47,6 +48,9 @@ class Loon extends AbstractProtocol
if ($item['type'] === Server::TYPE_VLESS) {
$uri .= self::buildVless($item['password'], $item);
}
if ($item['type'] === Server::TYPE_ANYTLS) {
$uri .= self::buildAnyTLS($item['password'], $item);
}
}
return response($uri)
->header('content-type', 'text/plain')
@@ -325,4 +329,29 @@ class Loon extends AbstractProtocol
$uri .= "\r\n";
return $uri;
}
public static function buildAnyTLS($password, $server)
{
$protocol_settings = data_get($server, 'protocol_settings', []);
$config = [
"{$server['name']}=anytls",
"{$server['host']}",
"{$server['port']}",
"{$password}",
"udp=true"
];
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
$config[] = "sni={$serverName}";
}
// ✅ 跳过证书校验
if (data_get($protocol_settings, 'tls.allow_insecure')) {
$config[] = 'skip-cert-verify=true';
}
$config = array_filter($config);
return implode(',', $config) . "\r\n";
}
}

View File

@@ -14,6 +14,7 @@ class Surfboard extends AbstractProtocol
Server::TYPE_SHADOWSOCKS,
Server::TYPE_VMESS,
Server::TYPE_TROJAN,
Server::TYPE_ANYTLS,
];
const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.surfboard.conf';
const DEFAULT_TEMPLATE_FILE = 'resources/rules/default.surfboard.conf';
@@ -36,7 +37,10 @@ class Surfboard extends AbstractProtocol
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305'
'chacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305'
])
) {
// [Proxy]
@@ -56,6 +60,10 @@ class Surfboard extends AbstractProtocol
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === Server::TYPE_ANYTLS) {
$proxies .= self::buildAnyTLS($item['password'], $item);
$proxyGroup .= $item['name'] . ', ';
}
}
$config = subscribe_template('surfboard');
@@ -190,4 +198,32 @@ class Surfboard extends AbstractProtocol
$uri .= "\r\n";
return $uri;
}
public static function buildAnyTLS($password, $server)
{
$protocol_settings = data_get($server, 'protocol_settings', []);
$config = [
"{$server['name']}=anytls",
"{$server['host']}",
"{$server['port']}",
"password={$password}",
"tfo=true",
"udp-relay=true"
];
// SNI
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
$config[] = "sni={$serverName}";
}
// 跳过证书校验
if (data_get($protocol_settings, 'tls.allow_insecure')) {
$config[] = "skip-cert-verify=true";
}
$config = array_filter($config);
return implode(',', $config) . "\r\n";
}
}

View File

@@ -1,37 +0,0 @@
<?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_stat_user', function (Blueprint $table) {
// Add server_id column as nullable for backward compatibility
$table->integer('server_id')->nullable()->after('user_id')->comment('节点ID (nullable for legacy data)');
// Add index for per-node queries
if (config('database.default') !== 'sqlite') {
$table->index(['user_id', 'server_id', 'record_at'], 'user_server_record_idx');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('v2_stat_user', function (Blueprint $table) {
if (config('database.default') !== 'sqlite') {
$table->dropIndex('user_server_record_idx');
}
$table->dropColumn('server_id');
});
}
};

View File

@@ -1,55 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Updates the unique constraint on v2_stat_user to include server_id,
* allowing per-node user traffic tracking.
*/
public function up(): void
{
if (config('database.default') === 'sqlite') {
// SQLite uses explicit WHERE queries in code, no constraint changes needed
return;
}
Schema::table('v2_stat_user', function (Blueprint $table) {
// Drop the old unique constraint that doesn't include server_id
$table->dropUnique('server_rate_user_id_record_at');
// Add new unique constraint including server_id
// Note: NULL server_id values (legacy) are treated as distinct in MySQL
$table->unique(
['user_id', 'server_id', 'server_rate', 'record_at', 'record_type'],
'stat_user_unique_idx'
);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (config('database.default') === 'sqlite') {
return;
}
Schema::table('v2_stat_user', function (Blueprint $table) {
// Drop new constraint
$table->dropUnique('stat_user_unique_idx');
// Restore original constraint
$table->unique(
['server_rate', 'user_id', 'record_at'],
'server_rate_user_id_record_at'
);
});
}
};