Files
Xboard/IMPLEMENTATION_SUMMARY_per_node_user_traffic.md
2025-11-29 13:47:15 +01:00

5.4 KiB

Per-Node User Traffic Tracking - Implementation Summary

Changes Made

1. Database Migration

File: database/migrations/2025_11_29_000000_add_server_id_to_stat_user.php

  • Added server_id column to v2_stat_user table
  • Column is nullable for backward compatibility with existing data
  • Added composite index user_server_record_idx for efficient per-node queries

2. StatUserJob Updates

File: app/Jobs/StatUserJob.php

Updated all three database-specific methods to include server_id:

  • processUserStatForSqlite() - Added server_id to WHERE clause and CREATE
  • processUserStatForOtherDatabases() - Added server_id to upsert unique key
  • processUserStatForPostgres() - Added server_id to ON CONFLICT clause

3. StatUser Model

File: app/Models/StatUser.php

  • Added @property int|null $server_id documentation
  • Added server() relationship method to Server model

How It Works

Node Identification Flow

  1. Node sends traffic report:

    POST /api/v1/server/UniProxy/push?node_type=vmess&node_id=5&token=xxx
    
  2. Middleware extracts node info:

    • Server middleware validates node_id and token
    • Loads full server object from database
    • Injects into $request->attributes->set('node_info', $serverInfo)
  3. Controller passes to service:

    • $server = $request->attributes->get('node_info')
    • Contains $server->id, $server->rate, etc.
  4. StatUserJob now uses server_id:

    • Creates/updates records with composite key: (user_id, server_id, server_rate, record_at, record_type)
    • Traffic from different nodes is now stored separately

Record Creation Logic

NEW behavior with server_id:

Scenario Result
Same user, same node, same day UPDATE existing record (accumulate traffic)
Same user, different node, same day CREATE separate records per node
Same user, same node, next day CREATE new record (new day)

OLD behavior (without server_id):

  • Same user, same rate, same day → Single aggregated record (couldn't differentiate nodes)

Backward Compatibility

Existing Queries Continue to Work

All existing queries that aggregate user traffic will work unchanged:

// Example: User consumption ranking (aggregates across all nodes)
StatUser::select([
    'user_id',
    DB::raw('sum(u) as u'),
    DB::raw('sum(d) as d'),
    DB::raw('sum(u) + sum(d) as total')
])
->where('record_at', '>=', $startAt)
->where('record_at', '<', $endAt)
->groupBy('user_id')
->orderBy('total', 'DESC')
->get();

This query:

  • Works with old records (where server_id is NULL)
  • Works with new records (where server_id is populated)
  • Correctly sums traffic across all nodes per user

API Endpoints Unchanged

  • No changes to admin API endpoints
  • No changes to user API endpoints
  • No changes to node API endpoints (they already send node_id)

Legacy Data Preserved

  • Old records without server_id remain valid
  • Represent aggregated historical data
  • New records will have server_id populated

New Capabilities

Per-Node User Traffic Analysis

You can now query traffic per user per node:

// Get user's traffic breakdown by node
StatUser::where('user_id', $userId)
    ->where('record_at', '>=', $startDate)
    ->whereNotNull('server_id')  // Only new records
    ->groupBy('server_id')
    ->selectRaw('server_id, SUM(u) as upload, SUM(d) as download')
    ->get();

Example Use Cases

  1. Identify which nodes a user uses most
  2. Detect unusual traffic patterns per node
  3. Analyze node-specific user behavior
  4. Generate per-node billing reports

Migration Instructions

  1. Run the migration:

    php artisan migrate
    
  2. Deploy code changes - No downtime required

  3. Verify:

    • Old data remains queryable
    • New traffic reports populate server_id
    • Existing dashboards continue to work

Database Schema

Before

CREATE TABLE v2_stat_user (
    id INT PRIMARY KEY,
    user_id INT,
    server_rate DECIMAL(10),
    u BIGINT,
    d BIGINT,
    record_type CHAR(2),
    record_at INT,
    created_at INT,
    updated_at INT,
    UNIQUE KEY (server_rate, user_id, record_at)
);

After

CREATE TABLE v2_stat_user (
    id INT PRIMARY KEY,
    user_id INT,
    server_id INT NULL,  -- NEW
    server_rate DECIMAL(10),
    u BIGINT,
    d BIGINT,
    record_type CHAR(2),
    record_at INT,
    created_at INT,
    updated_at INT,
    UNIQUE KEY (server_rate, user_id, record_at),  -- Old unique key still exists
    INDEX user_server_record_idx (user_id, server_id, record_at)  -- NEW
);

Testing Checklist

  • Run migration successfully
  • Node reports traffic → server_id is populated
  • Same user on different nodes → separate records created
  • Same user on same node → traffic accumulates in single record
  • Existing admin dashboards show correct totals
  • User traffic logs display correctly
  • Old records (server_id=NULL) are still queryable
  • SUM queries aggregate correctly across nodes

Notes

  • The server_id is sourced from the node_id parameter that nodes already send
  • No changes needed to node software - they already provide this information
  • The composite unique key now effectively includes server_id in the WHERE clauses
  • PostgreSQL ON CONFLICT clause updated to match new unique constraint