Prechádzať zdrojové kódy

New Report, Site Traffic Analysis

BrettonYe 1 rok pred
rodič
commit
d4cbcd4cb6

+ 1 - 1
app/Console/Commands/AutoClearLogs.php

@@ -39,7 +39,7 @@ class AutoClearLogs extends Command
     private function clearLog(): void
     {
         try {
-            NodeDailyDataFlow::where('created_at', '<=', date('Y-m-d H:i:s', strtotime(config('tasks.clean.node_daily_logs'))))->delete(); // 清除节点每天流量数据日志
+            NodeDailyDataFlow::whereNotNull('node_id')->where('created_at', '<=', date('Y-m-d H:i:s', strtotime(config('tasks.clean.node_daily_logs'))))->delete(); // 清除节点每天流量数据日志
 
             NodeHourlyDataFlow::where('created_at', '<=', date('Y-m-d H:i:s', strtotime(config('tasks.clean.node_hourly_logs'))))->delete(); // 清除节点每小时流量数据日志
 

+ 10 - 0
app/Console/Commands/TaskDaily.php

@@ -3,6 +3,7 @@
 namespace App\Console\Commands;
 
 use App\Models\Node;
+use App\Models\NodeDailyDataFlow;
 use App\Models\Ticket;
 use App\Models\User;
 use App\Notifications\TicketClosed;
@@ -183,5 +184,14 @@ class TaskDaily extends Command
                 ]);
             }
         });
+
+        $dailyTotal = NodeDailyDataFlow::whereNotNull('node_id')->whereCreatedAt($created_at)->selectRaw('SUM(u) as total_u, SUM(d) as total_d')->first();
+        if ($dailyTotal) {
+            NodeDailyDataFlow::create([
+                'u' => $dailyTotal->total_u,
+                'd' => $dailyTotal->total_d,
+                'created_at' => $created_at,
+            ]);
+        }
     }
 }

+ 119 - 71
app/Http/Controllers/Admin/ReportController.php

@@ -3,61 +3,47 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Http\Controllers\Controller;
+use App\Models\Node;
+use App\Models\NodeDailyDataFlow;
+use App\Models\NodeHourlyDataFlow;
 use App\Models\Order;
 use App\Models\User;
+use Carbon\Carbon;
+use DB;
+use Illuminate\Contracts\View\View;
 use Illuminate\Http\Request;
 
 class ReportController extends Controller
 {
-    public function accounting()
+    public function accounting(): View
     {
-        $orders = Order::where('status', '>=', 2)->has('goods')->latest()->get(['created_at', 'amount']);
-        $ordersByDay = $orders->groupBy(function ($item) {
-            return $item->created_at->format('Y-m-d');
-        })->map(function ($row) {
-            return $row->sum('amount');
-        })->toArray();
-
-        $ordersByMonth = $orders->groupBy(function ($item) {
-            return $item->created_at->format('Y-m');
-        })->map(function ($row) {
-            return $row->sum('amount');
-        })->toArray();
-
-        $ordersByYear = $orders->groupBy(function ($item) {
-            return $item->created_at->format('Y');
-        })->map(function ($row) {
-            return $row->sum('amount');
-        })->sort()->toArray();
+        $completedOrders = Order::where('status', '>=', 2)->has('goods')->selectRaw('DATE(created_at) as date, sum(amount)/100 as total')->groupBy('date')->get();
 
-        $currentDays = date('j');
-        $lastDays = date('t', strtotime('-1 months'));
-        $data['days'] = range(1, max($currentDays, $lastDays));
-        $data['years'] = range(1, 12);
-
-        for ($i = 1; $i <= $currentDays; $i++) {
-            $data['currentMonth'][] = $ordersByDay[date(sprintf('Y-m-%02u', $i))] ?? 0;
-        }
+        $ordersByDay = $completedOrders->filter(fn ($order) => $order->date >= now()->subMonthNoOverflow()->startOfMonth()->format('Y-m-d'))->pluck('total', 'date');
 
-        for ($i = 1; $i <= $lastDays; $i++) {
-            $data['lastMonth'][] = $ordersByDay[date(sprintf('Y-m-%02u', $i), strtotime('-1 months'))] ?? 0;
-        }
+        $ordersByMonth = $completedOrders->filter(fn ($order) => $order->date >= now()->subYearNoOverflow()->startOfYear())->groupBy(fn ($order) => Carbon::parse($order->date)->format('Y-m'))->map(fn ($rows) => round($rows->sum('total'),
+            2))->toArray();
 
-        for ($i = 1; $i <= date('m'); $i++) {
-            $data['currentYear'][] = $ordersByMonth[date(sprintf('Y-%02u', $i))] ?? 0;
-        }
-
-        for ($i = 1; $i <= 12; $i++) {
-            $data['lastYear'][] = $ordersByMonth[date(sprintf('Y-%02u', $i), strtotime('-1 years'))] ?? 0;
-        }
+        $ordersByYear = $completedOrders->groupBy(fn ($order) => Carbon::parse($order->date)->format('Y'))->map(fn ($rows) => round($rows->sum('total'), 2))->toArray();
 
-        ksort($ordersByYear);
-        $data['ordersByYear'] = $ordersByYear;
+        $currentDays = date('j');
+        $lastMonth = strtotime('first day of last month');
+        $daysInLastMonth = date('t', $lastMonth);
+        $months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
+        $data = [
+            'days' => range(1, max($currentDays, $daysInLastMonth)),
+            'months' => array_map(static fn ($month) => Carbon::create(null, $month)->translatedFormat('F'), $months),
+            'currentMonth' => array_map(static fn ($i) => round($ordersByDay[date(sprintf('Y-m-%02u', $i))] ?? 0, 2), range(1, $currentDays)),
+            'lastMonth' => array_map(static fn ($i) => $ordersByDay[date(sprintf('Y-m-%02u', $i), $lastMonth)] ?? 0, range(1, $daysInLastMonth)),
+            'currentYear' => array_map(static fn ($i) => $ordersByMonth[date(sprintf('Y-%02u', $i))] ?? 0, range(1, date('m'))),
+            'lastYear' => array_map(static fn ($i) => $ordersByMonth[date(sprintf('Y-%02u', $i), strtotime('-1 years'))] ?? 0, $months),
+            'ordersByYear' => $ordersByYear,
+        ];
 
         return view('admin.report.accounting', compact('data'));
     }
 
-    public function userAnalysis(Request $request)
+    public function userAnalysis(Request $request): View
     {
         $uid = $request->input('uid');
         $username = $request->input('username');
@@ -69,40 +55,102 @@ class ReportController extends Controller
 
         $data = null;
         if (isset($user)) {
+            $currentTime = now();
+            $currentDay = $currentTime->day;
+            $currentHour = $currentTime->hour;
+
             // 用户当前小时在各线路消耗流量
-            $data['currentHourlyFlow'] = $user->dataFlowLogs()
-                ->where('log_time', '>=', now()->startOfHour()->timestamp)
-                ->groupBy('node_id')
-                ->selectRaw('node_id, sum(u + d) as total')
-                ->get()->toArray();
-
-            // 用户今天各小时在各线路消耗流量
-            $data['hours'] = range(0, 23);
-            $data['hourlyFlow'] = $user->hourlyDataFlows()->whereNotNull('node_id')
-                ->whereDate('created_at', now())
-                ->selectRaw('node_id, (DATE_FORMAT(user_hourly_data_flow.created_at, "%k")) as date, u + d as total')
-                ->get()->transform(function ($item) {
-                    return [
-                        'node_id' => $item->node_id,
-                        'date' => (int) $item->date,
-                        'total' => round($item->total / GiB, 2),
-                    ];
-                })->toArray();
-
-            // 用户本月每天在各线路消耗流量
-            $data['days'] = range(1, date('j'));
-            $data['dailyFlow'] = $user->dailyDataFlows()->whereNotNull('node_id')
-                ->whereMonth('created_at', date('n'))
-                ->selectRaw('node_id, (DATE_FORMAT(user_daily_data_flow.created_at, "%e")) as date, u + d as total')
-                ->get()->transform(function ($item) {
-                    return [
-                        'node_id' => $item->node_id,
-                        'date' => (int) $item->date,
-                        'total' => round($item->total / GiB, 2),
-                    ];
-                })->toArray();
+            $currentHourFlow = $user->dataFlowLogs()->where('log_time', '>=', $currentTime->startOfHour()->timestamp)->with('node:id,name')->groupBy('node_id')->selectRaw('node_id, log_time, sum(u + d) as total')->get()->map(fn ($item) => [
+                'id' => $item->node_id,
+                'name' => $item->node->name,
+                'time' => $currentHour,
+                'total' => round($item->total / MiB, 2),
+            ]);
+
+            $hoursFlow = $user->hourlyDataFlows()->whereNotNull('node_id')->whereDate('created_at', $currentTime)->with('node:id,name')->selectRaw('node_id, HOUR(created_at) as hour, u + d as total')->get()->map(fn ($item
+            ) => [
+                'id' => $item->node_id,
+                'name' => $item->node->name,
+                'time' => (int) $item->hour,
+                'total' => round($item->total / MiB, 2),
+            ]); // 用户今天各小时在各线路消耗流量
+
+            $daysFlow = $user->dailyDataFlows()->whereNotNull('node_id')->whereMonth('created_at', $currentTime)->with('node:id,name')->selectRaw('node_id, DAY(created_at) as day, u + d as total')->get()->map(fn ($item
+            ) => [
+                'id' => $item->node_id,
+                'name' => $item->node->name,
+                'time' => (int) $item->day,
+                'total' => round($item->total / MiB, 2),
+            ]);
+
+            $currentDayFlow = collect($currentHourFlow)
+                ->merge($hoursFlow)
+                ->groupBy('id')
+                ->map(fn ($items) => [
+                    'id' => $items->first()['id'],
+                    'name' => $items->first()['name'],
+                    'time' => $currentDay,
+                    'total' => round($items->sum('total'), 2),
+                ])->values();
+
+            $data = [
+                'hours' => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
+                'days' => range(1, $currentDay),
+                'nodes' => collect([$currentDayFlow, $daysFlow])->collapse()->pluck('name', 'id')->unique()->toArray(),
+                'hourlyFlows' => array_merge($hoursFlow->toArray(), $currentHourFlow->toArray()),
+                'dailyFlows' => array_merge($daysFlow->toArray(), $currentDayFlow->toArray()),
+            ];
         }
 
         return view('admin.report.userDataAnalysis', compact('data'));
     }
+
+    public function siteAnalysis(Request $request): View
+    {
+        $nodeId = $request->input('node_id');
+        $nodes = Node::orderBy('name')->pluck('name', 'id');
+
+        // Fetch flows
+        $flows = NodeDailyDataFlow::whereNodeId($nodeId)->selectRaw('DATE(created_at) as date, sum(u + d) as total')->groupBy('date')->get()->keyBy('date');
+
+        $dailyFlows = $flows->filter(fn ($flow) => $flow->date >= now()->subMonthNoOverflow()->startOfMonth()->toDateString())->pluck('total', 'date');
+
+        $monthlyFlows = $flows->groupBy(fn ($flow) => Carbon::parse($flow->date)->format('Y-m'))->map(fn ($rows) => round($rows->sum('total') / GiB, 2));
+
+        $yearlyFlows = $flows->groupBy(fn ($flow) => Carbon::parse($flow->date)->format('Y'))->map(fn ($rows) => round($rows->sum('total') / TiB, 2));
+
+        $currentDays = (int) date('j');
+        $lastDays = (int) date('t', strtotime('-1 months'));
+
+        $todayFlow = NodeHourlyDataFlow::whereDate('created_at', today())->when($nodeId, fn ($query) => $query->whereNodeId($nodeId))->sum(DB::raw('u + d')) / GiB;
+
+        $thirtyDaysAgo = now()->subDays(30);
+        $trafficData = NodeDailyDataFlow::where('node_id', $nodeId)->where('created_at', '>=', $thirtyDaysAgo)->selectRaw('SUM(u + d) as total, COUNT(*) as dataCounts')->first();
+
+        $total30Days = $trafficData->total ?? 0;
+
+        $daysWithData = max($trafficData->dataCounts ?? 0, 1);
+        $months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
+        $data = [
+            'days' => range(1, max($currentDays, $lastDays)),
+            'months' => array_map(static fn ($month) => Carbon::create(null, $month)->translatedFormat('F'), $months),
+            'currentMonth' => array_map(static fn ($i) => ($dailyFlows[date(sprintf('Y-m-%02u', $i))] ?? 0) / GiB, range(1, $currentDays)),
+            'lastMonth' => array_map(static fn ($i) => ($dailyFlows[date(sprintf('Y-m-%02u', $i), strtotime('first day of last month'))] ?? 0) / GiB, range(1, $lastDays)),
+            'currentYear' => array_map(static fn ($i) => $monthlyFlows[date(sprintf('Y-%02u', $i))] ?? 0, range(1, date('m'))),
+            'lastYear' => array_map(static fn ($i) => $monthlyFlows[date(sprintf('Y-%02u', $i), strtotime('-1 years'))] ?? 0, $months),
+            'yearlyFlows' => $yearlyFlows->toArray(),
+            'avgDaily30d' => round(($total30Days / GiB) / $daysWithData, 2),
+        ];
+
+        if ($nodeId) {
+            $totalAll30d = NodeDailyDataFlow::where('created_at', '>=', $thirtyDaysAgo)->whereNull('node_id')->sum(DB::raw('u + d'));
+
+            $data['nodePct30d'] = round(($total30Days / max($totalAll30d, 1)) * 100, 2);
+        }
+
+        $data['currentMonth'][$currentDays - 1] = $todayFlow;
+        $data['currentYear'][count($data['currentYear']) - 1] += $todayFlow;
+
+        return view('admin.report.siteDataAnalysis', compact('data', 'nodes'));
+    }
 }

+ 1 - 1
app/Http/Controllers/Admin/UserController.php

@@ -177,7 +177,7 @@ class UserController extends Controller
         try {
             for ($i = 0; $i < (int) request('amount', 1); $i++) {
                 $user = Helpers::addUser(Str::random(8).'@auto.generate', Str::random(), (int) sysConfig('default_traffic'), (int) sysConfig('default_days'));
-                Helpers::addUserTrafficModifyLog($user->id, 0, 1024 * GiB, trans('admin.user.massive.note'));
+                Helpers::addUserTrafficModifyLog($user->id, 0, TiB, trans('admin.user.massive.note'));
             }
 
             return Response::json(['status' => 'success', 'message' => trans('admin.user.massive.succeed')]);

+ 2 - 2
app/Http/Controllers/AdminController.php

@@ -42,9 +42,9 @@ class AdminController extends Controller
                 'expireWarningUserCount' => User::whereBetween('expired_at', [$today, today()->addDays(sysConfig('expire_days'))])->count(), // 临近过期用户数
                 'largeTrafficUserCount' => User::whereRaw('(u + d)/transfer_enable >= 0.9')->where('status', '<>', -1)->count(), // 流量使用超过90%的用户
                 'flowAbnormalUserCount' => count((new UserHourlyDataFlow)->trafficAbnormal()), // 1小时内流量异常用户
-                'monthlyTrafficUsage' => formatBytes(NodeDailyDataFlow::whereMonth('created_at', now()->month)->sum(DB::raw('u + d'))),
+                'monthlyTrafficUsage' => formatBytes(NodeDailyDataFlow::whereNull('node_id')->whereMonth('created_at', now()->month)->sum(DB::raw('u + d'))),
                 'dailyTrafficUsage' => $dailyTrafficUsage ? formatBytes($dailyTrafficUsage) : 0,
-                'totalTrafficUsage' => formatBytes(NodeDailyDataFlow::sum(DB::raw('u + d'))),
+                'totalTrafficUsage' => formatBytes(NodeDailyDataFlow::whereNull('node_id')->where('created_at', '>=', now()->subDays(30))->sum(DB::raw('u + d'))),
             ];
         });
 

+ 1 - 0
app/helpers.php

@@ -4,6 +4,7 @@ use Carbon\CarbonInterval;
 
 const MiB = 1048576;
 const GiB = 1073741824;
+const TiB = 1099511627776;
 
 const Minute = 60;
 const Hour = 3600;

+ 46 - 0
database/migrations/2024_05_24_234032_site_data_flow.php

@@ -0,0 +1,46 @@
+<?php
+
+use App\Models\NodeDailyDataFlow;
+use Carbon\Carbon;
+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('node_daily_data_flow', static function (Blueprint $table) {
+            $table->unsignedInteger('node_id')->nullable()->change();
+        });
+
+        // 使用查询构建器对数据进行分组并计算合计值
+        $dailyTotals = NodeDailyDataFlow::whereNotNull('node_id')->oldest()->selectRaw('DATE(created_at) as date, SUM(u) as total_u, SUM(d) as total_d')
+            ->groupBy('date')
+            ->get();
+
+        // 遍历查询结果,创建新的合计列
+        foreach ($dailyTotals as $dailyTotal) {
+            // 创建新记录,同时设置合计列的初始值
+            NodeDailyDataFlow::create([
+                'u' => $dailyTotal->total_u,
+                'd' => $dailyTotal->total_d,
+                'created_at' => Carbon::parse($dailyTotal->date)->endOfDay(),
+            ]);
+        }
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        NodeDailyDataFlow::whereNull('node_id')->delete();
+        Schema::table('node_daily_data_flow', static function (Blueprint $table) {
+            $table->unsignedInteger('node_id')->nullable(false)->change();
+        });
+    }
+};

+ 12 - 6
resources/lang/zh_CN/admin.php

@@ -81,9 +81,10 @@ return [
             'rebate_flow' => '返利流水',
         ],
         'analysis' => [
-            'attribute' => '分析报告',
-            'accounting' => '流水账簿',
-            'user_flow' => '用户分析',
+            'attribute' => '数据分析',
+            'accounting' => '流水账',
+            'user_flow' => '用户流量',
+            'site_flow' => '本站流量',
         ],
         'log' => [
             'attribute' => '日志系统',
@@ -361,9 +362,12 @@ return [
         'counts' => '共 <code>:num</code> 个权限角色',
     ],
     'report' => [
-        'monthly_accounting' => '月流水账簿',
-        'annually_accounting' => '年流水账簿',
-        'historic_accounting' => '历史流水账簿',
+        'daily_accounting' => '日流水',
+        'monthly_accounting' => '月流水',
+        'annually_accounting' => '年流水',
+        'daily_site_flow' => '日流量',
+        'monthly_site_flow' => '月流量',
+        'annually_site_flow' => '年流量',
         'current_month' => '本 月',
         'last_month' => '上 月',
         'current_year' => '今 年',
@@ -371,6 +375,8 @@ return [
         'hourly_traffic' => '每时流量',
         'daily_traffic' => '每天流量',
         'today' => '本 日',
+        'avg_traffic_30d' => '30天日均流量',
+        'sum_traffic_30d' => '30天占总流量比',
     ],
     'permission' => [
         'title' => '权限行为列表',

+ 75 - 75
resources/views/admin/index.blade.php

@@ -7,48 +7,48 @@
         <div class="row" data-by-row="true">
             @can('admin.user.index')
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.user.index')}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.user.index') }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-primary">
+                            <button class="btn btn-floating btn-sm btn-primary" type="button">
                                 <i class="icon md-account"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.users') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$totalUserCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $totalUserCount }}</span>
                                 @if ($todayRegister)
                                     <span class="badge badge-success badge-round up font-size-20 m-0" style="top:-20px">
-                                    <i class="icon wb-triangle-up" aria-hidden="true"></i> {{$todayRegister}}
-                                </span>
+                                        <i class="icon wb-triangle-up" aria-hidden="true"></i> {{ $todayRegister }}
+                                    </span>
                                 @endif
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.user.index', ['enable' => 1])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.user.index', ['enable' => 1]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-info">
+                            <button class="btn btn-floating btn-sm btn-info" type="button">
                                 <i class="icon md-account"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.available_users') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$enableUserCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $enableUserCount }}</span>
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.user.index', ['paying' => 1])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.user.index', ['paying' => 1]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-info">
+                            <button class="btn btn-floating btn-sm btn-info" type="button">
                                 <i class="icon md-money-box"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.paid_users') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$payingUserCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $payingUserCount }}</span>
                                 @if ($payingNewUserCount)
                                     <span class="badge badge-success badge-round up font-size-20 m-0" style="top:-20px">
-                                        <i class="icon wb-triangle-up" aria-hidden="true"></i> {{$payingNewUserCount}}
+                                        <i class="icon wb-triangle-up" aria-hidden="true"></i> {{ $payingNewUserCount }}
                                     </span>
                                 @endif
                             </div>
@@ -56,79 +56,79 @@
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.user.index', ['active' => 1])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.user.index', ['active' => 1]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-success">
+                            <button class="btn btn-floating btn-sm btn-success" type="button">
                                 <i class="icon md-account"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.active_days_users', ['days' => sysConfig('expire_days')]) }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$activeUserCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $activeUserCount }}</span>
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.user.index', ['unActive' => 1])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.user.index', ['unActive' => 1]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-warning">
+                            <button class="btn btn-floating btn-sm btn-warning" type="button">
                                 <i class="icon md-account"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.inactive_days_users', ['days' => sysConfig('expire_days')]) }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$inactiveUserCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $inactiveUserCount }}</span>
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.user.index', ['online' => 1])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.user.index', ['online' => 1]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-success">
+                            <button class="btn btn-floating btn-sm btn-success" type="button">
                                 <i class="icon md-account"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.online_users') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$onlineUserCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $onlineUserCount }}</span>
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.user.index', ['expireWarning' => 1])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.user.index', ['expireWarning' => 1]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-danger">
+                            <button class="btn btn-floating btn-sm btn-danger" type="button">
                                 <i class="icon md-account"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.expiring_users') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$expireWarningUserCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $expireWarningUserCount }}</span>
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.user.index', ['largeTraffic' => 1])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.user.index', ['largeTraffic' => 1]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-warning">
+                            <button class="btn btn-floating btn-sm btn-warning" type="button">
                                 <i class="icon md-account"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.overuse_users') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$largeTrafficUserCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $largeTrafficUserCount }}</span>
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.user.index', ['flowAbnormal' => 1])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.user.index', ['flowAbnormal' => 1]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-danger">
+                            <button class="btn btn-floating btn-sm btn-danger" type="button">
                                 <i class="icon md-account"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.abnormal_users') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$flowAbnormalUserCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $flowAbnormalUserCount }}</span>
                             </div>
                         </div>
                     </a>
@@ -136,27 +136,27 @@
             @endcan
             @can('admin.node.index')
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.node.index')}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.node.index') }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-primary">
+                            <button class="btn btn-floating btn-sm btn-primary" type="button">
                                 <i class="icon md-cloud"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.nodes') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$nodeCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $nodeCount }}</span>
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.node.index', ['status'=>0])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.node.index', ['status' => 0]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-info">
+                            <button class="btn btn-floating btn-sm btn-info" type="button">
                                 <i class="icon md-cloud-off"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.maintaining_nodes') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$abnormalNodeCount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $abnormalNodeCount }}</span>
                             </div>
                         </div>
                     </a>
@@ -164,12 +164,12 @@
             @endcan
             @can('admin.log.traffic')
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.log.traffic')}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.report.siteAnalysis') }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-primary">
+                            <button class="btn btn-floating btn-sm btn-primary" type="button">
                                 <i class="icon md-time-countdown"></i>
                             </button>
-                            <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.days_traffic_consumed', ['days' => now()->diffInDays((new DateTime())->modify(config('tasks.clean.node_daily_logs')))]) }}</span>
+                            <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.days_traffic_consumed', ['days' => 30]) }}</span>
                             <div class="content-text text-center mb-0">
                                 <span class="font-size-40 font-weight-100">{{ $totalTrafficUsage }}</span>
                             </div>
@@ -177,18 +177,18 @@
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{ route('admin.log.traffic') }}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.log.traffic') }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-primary">
+                            <button class="btn btn-floating btn-sm btn-primary" type="button">
                                 <i class="icon md-time-countdown"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.current_month_traffic_consumed') }}</span>
                             <div class="content-text text-center mb-0">
                                 <span class="font-size-40 font-weight-100">{{ $monthlyTrafficUsage }}</span>
-                                @if( $dailyTrafficUsage !== 0 )
+                                @if ($dailyTrafficUsage !== 0)
                                     <span class="badge badge-success badge-round up font-size-20 m-0" style="top:-20px">
-                                    <i class="icon wb-triangle-up" aria-hidden="true"></i> {{ $dailyTrafficUsage }}
-                                </span>
+                                        <i class="icon wb-triangle-up" aria-hidden="true"></i> {{ $dailyTrafficUsage }}
+                                    </span>
                                 @endif
                             </div>
                         </div>
@@ -197,54 +197,54 @@
             @endcan
             @can('admin.order')
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.order')}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.order') }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-primary">
+                            <button class="btn btn-floating btn-sm btn-primary" type="button">
                                 <i class="icon md-ticket-star"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.orders') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$totalOrder}}</span>
-                                @if($todayOrder)
+                                <span class="font-size-40 font-weight-100">{{ $totalOrder }}</span>
+                                @if ($todayOrder)
                                     <span class="badge badge-success badge-round up font-size-20 m-0" style="top:-20px">
-                                    <i class="icon wb-triangle-up" aria-hidden="true"></i> {{$todayOrder}}
-                                </span>
+                                        <i class="icon wb-triangle-up" aria-hidden="true"></i> {{ $todayOrder }}
+                                    </span>
                                 @endif
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.order')}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.order') }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-info">
+                            <button class="btn btn-floating btn-sm btn-info" type="button">
                                 <i class="icon md-ticket-star"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.online_orders') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$totalOnlinePayOrder}}</span>
-                                @if($todayOnlinePayOrder)
+                                <span class="font-size-40 font-weight-100">{{ $totalOnlinePayOrder }}</span>
+                                @if ($todayOnlinePayOrder)
                                     <span class="badge badge-success badge-round up font-size-20 m-0" style="top:-20px">
-                                    <i class="icon wb-triangle-up" aria-hidden="true"></i> {{$todayOnlinePayOrder}}
-                                </span>
+                                        <i class="icon wb-triangle-up" aria-hidden="true"></i> {{ $todayOnlinePayOrder }}
+                                    </span>
                                 @endif
                             </div>
                         </div>
                     </a>
                 </div>
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.order', ['status' => [1, 2]])}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.order', ['status' => [1, 2]]) }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-success">
+                            <button class="btn btn-floating btn-sm btn-success" type="button">
                                 <i class="icon md-ticket-star"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.succeed_orders') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$totalSuccessOrder}}</span>
-                                @if($todaySuccessOrder)
+                                <span class="font-size-40 font-weight-100">{{ $totalSuccessOrder }}</span>
+                                @if ($todaySuccessOrder)
                                     <span class="badge badge-success badge-round up font-size-20 m-0" style="top:-20px">
-                                    <i class="icon wb-triangle-up" aria-hidden="true"></i> {{$todaySuccessOrder}}
-                                </span>
+                                        <i class="icon wb-triangle-up" aria-hidden="true"></i> {{ $todaySuccessOrder }}
+                                    </span>
                                 @endif
                             </div>
                         </div>
@@ -255,12 +255,12 @@
                 <div class="col-xl-3 col-md-6 info-panel">
                     <div class="card card-shadow">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-primary">
+                            <button class="btn btn-floating btn-sm btn-primary" type="button">
                                 <i class="icon md-money"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.credit') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$totalCredit}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $totalCredit }}</span>
                             </div>
                         </div>
                     </div>
@@ -268,18 +268,18 @@
             @endcan
             @can('admin.aff.rebate')
                 <div class="col-xl-3 col-md-6 info-panel">
-                    <a href="{{route('admin.aff.rebate')}}" class="card card-shadow">
+                    <a class="card card-shadow" href="{{ route('admin.aff.rebate') }}">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-warning">
+                            <button class="btn btn-floating btn-sm btn-warning" type="button">
                                 <i class="icon md-money"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.withdrawing_commissions') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$totalWaitRefAmount}}</span>
-                                @if($todayWaitRefAmount)
+                                <span class="font-size-40 font-weight-100">{{ $totalWaitRefAmount }}</span>
+                                @if ($todayWaitRefAmount)
                                     <span class="badge badge-success badge-round up font-size-20 m-0" style="top:-20px">
-                                    <i class="icon wb-triangle-up" aria-hidden="true"></i> {{$todayWaitRefAmount}}
-                                </span>
+                                        <i class="icon wb-triangle-up" aria-hidden="true"></i> {{ $todayWaitRefAmount }}
+                                    </span>
                                 @endif
                             </div>
                         </div>
@@ -290,12 +290,12 @@
                 <div class="col-xl-3 col-md-6 info-panel">
                     <div class="card card-shadow">
                         <div class="card-block bg-white">
-                            <button type="button" class="btn btn-floating btn-sm btn-dark">
+                            <button class="btn btn-floating btn-sm btn-dark" type="button">
                                 <i class="icon md-money"></i>
                             </button>
                             <span class="ml-15 font-weight-400">{{ trans('admin.dashboard.withdrawn_commissions') }}</span>
                             <div class="content-text text-center mb-0">
-                                <span class="font-size-40 font-weight-100">{{$totalRefAmount}}</span>
+                                <span class="font-size-40 font-weight-100">{{ $totalRefAmount }}</span>
                             </div>
                         </div>
                     </div>
@@ -308,8 +308,8 @@
     <script src="/assets/global/vendor/matchheight/jquery.matchHeight-min.js"></script>
     <script src="/assets/global/js/Plugin/matchheight.js"></script>
     <script>
-      $(function() {
-        $('.card').matchHeight();
-      });
+        $(function() {
+            $('.card').matchHeight();
+        });
     </script>
 @endsection

+ 8 - 1
resources/views/admin/layouts.blade.php

@@ -355,7 +355,7 @@
                         </ul>
                     </li>
                 @endcanany
-                @canany(['admin.report.accounting', 'admin.report.userAnalysis'])
+                @canany(['admin.report.accounting', 'admin.report.userAnalysis', 'admin.report.siteAnalysis'])
                     <li class="site-menu-item has-sub {{ request()->routeIs('admin.report.*') ? 'active open' : '' }}">
                         <a href="javascript:void(0)">
                             <i class="site-menu-icon wb-stats-bars" aria-hidden="true"></i>
@@ -376,6 +376,13 @@
                                     </a>
                                 </li>
                             @endcan
+                            @can('admin.report.siteAnalysis')
+                                <li class="site-menu-item {{ request()->routeIs('admin.report.siteAnalysis') ? 'active open' : '' }}">
+                                    <a href="{{ route('admin.report.siteAnalysis') }}">
+                                        <span class="site-menu-title">{{ trans('admin.menu.analysis.site_flow') }}</span>
+                                    </a>
+                                </li>
+                            @endcan
                         </ul>
                     </li>
                 @endcanany

+ 120 - 136
resources/views/admin/report/accounting.blade.php

@@ -5,7 +5,7 @@
             <div class="card-block p-30">
                 <div class="row pb-20">
                     <div class="col-md-8 col-sm-6">
-                        <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.monthly_accounting') }}</div>
+                        <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.daily_accounting') }}</div>
                     </div>
                 </div>
                 <canvas id="days"></canvas>
@@ -15,7 +15,7 @@
             <div class="card-block p-30">
                 <div class="row pb-20">
                     <div class="col-md-8 col-sm-6">
-                        <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.annually_accounting') }}</div>
+                        <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.monthly_accounting') }}</div>
                     </div>
                 </div>
                 <canvas id="months"></canvas>
@@ -25,7 +25,7 @@
             <div class="card-block p-30">
                 <div class="row pb-20">
                     <div class="col-md-8 col-sm-6">
-                        <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.historic_accounting') }}</div>
+                        <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.annually_accounting') }}</div>
                     </div>
                 </div>
                 <canvas id="years"></canvas>
@@ -36,145 +36,129 @@
 @section('javascript')
     <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
     <script type="text/javascript">
-      function label_callbacks(tail) {
-        return {
-          mode: 'index',
-          intersect: false,
-          callbacks: {
-            title: function(context) {
-              return context[0].label + tail;
-            },
-            label: function(context) {
-              let label = context.dataset.label || '';
+        function label_callbacks(tail) {
+            return {
+                mode: 'index',
+                intersect: false,
+                callbacks: {
+                    title: function(context) {
+                        return context[0].label + tail;
+                    },
+                    label: function(context) {
+                        let label = context.dataset.label || '';
+                        if (label) {
+                            label += ': ';
+                        }
+                        if (context.parsed.y !== null) {
+                            label += new Intl.NumberFormat('{{ str_replace('_', '-', app()->getLocale()) }}', {
+                                style: 'currency',
+                                currency: '{{ sysConfig('standard_currency') }}',
+                            }).format(context.parsed.y);
+                        }
+                        return label;
+                    },
+                },
+            };
+        }
 
-              if (label) {
-                label += ': ';
-              }
-              if (context.parsed.y !== null) {
-                label += new Intl.NumberFormat('ch-CN', {
-                  style: 'currency',
-                  currency: '{{sysConfig('standard_currency')}}',
-                }).format(context.parsed.y);
-              }
-              return label;
-            },
-          },
-        };
-      }
+        function common_options(label) {
+            return {
+                responsive: true,
+                scales: {
+                    x: {
+                        grid: {
+                            display: false
+                        }
+                    },
+                    y: {
+                        grid: {
+                            display: false
+                        },
+                        min: 0
+                    },
+                },
+                plugins: {
+                    legend: {
+                        labels: {
+                            padding: 20,
+                            usePointStyle: true,
+                            pointStyle: 'circle',
+                            font: {
+                                size: 14
+                            },
+                        },
+                    },
+                    tooltip: label_callbacks(label || ''),
+                },
+            };
+        }
 
-      function common_options(label) {
-        return {
-          responsive: true,
-          scales: {
-            x: {
-              grid: {
-                display: false,
-              },
-            },
-            y: {
-              grid: {
-                display: false,
-              },
-              min: 0,
-            },
-          },
-          plugins: {
-            legend: {
-              labels: {
-                padding: 20,
-                usePointStyle: true,
-                pointStyle: 'circle',
-                font: {size: 14},
-              },
-            },
-            tooltip: label_callbacks(label),
-          },
-        };
-      }
-
-      function area_a(label, data) {
-        return {
-          label: label,
-          backgroundColor: 'rgba(184, 215, 255)',
-          borderColor: 'rgba(184, 215, 255)',
-          data: data,
-          fill: {
-            target: 'origin',
-            above: 'rgba(184, 215, 255, 0.5)',
-          },
-          tension: 0.4,
-        };
-      }
+        function area(label, data, backgroundColor, aboveColor) {
+            return {
+                label: label,
+                backgroundColor: backgroundColor,
+                borderColor: backgroundColor,
+                data: data,
+                fill: {
+                    target: 'origin',
+                    above: aboveColor
+                },
+                tension: 0.4,
+            };
+        }
 
-      function area_b(label, data) {
-        return {
-          label: label,
-          backgroundColor: 'rgba(146, 240, 230)',
-          borderColor: 'rgba(146, 240, 230)',
-          data: data,
-          fill: {
-            target: 'origin',
-            above: 'rgba(146, 240, 230, 0.5)',
-          },
-          tension: 0.4,
-        };
-      }
-
-      new Chart(document.getElementById('days'), {
-        type: 'line',
-        data: {
-          labels: @json($data['days']),
-          datasets: [
-            area_a('{{ trans('admin.report.current_month') }}',@json($data['currentMonth'])),
-            area_b('{{ trans('admin.report.last_month') }} ',@json($data['lastMonth']))],
-        },
-        options: common_options(' {{ trans_choice('common.days.attribute', 1) }}'),
-      });
+        new Chart(document.getElementById('days'), {
+            type: 'line',
+            data: {
+                labels: @json($data['days']),
+                datasets: [
+                    area('{{ trans('admin.report.current_month') }}', @json($data['currentMonth']), 'rgba(184, 215, 255)', 'rgba(184, 215, 255, 0.5)'),
+                    area('{{ trans('admin.report.last_month') }} ', @json($data['lastMonth']), 'rgba(146, 240, 230)', 'rgba(146, 240, 230, 0.5)')
+                ],
+            },
+            options: common_options(' {{ trans_choice('common.days.attribute', 2) }}'),
+        });
 
-      new Chart(document.getElementById('months'), {
-        type: 'line',
-        data: {
-          labels: @json($data['years']),
-          datasets: [
-            area_a('{{ trans('admin.report.current_year') }}',@json($data['currentYear'])),
-            area_b('{{ trans('admin.report.last_year') }}',@json($data['lastYear']))],
-        },
-        options: common_options(' {{ trans('validation.attributes.month') }}'),
-      });
+        new Chart(document.getElementById('months'), {
+            type: 'line',
+            data: {
+                labels: @json($data['months']),
+                datasets: [
+                    area('{{ trans('admin.report.current_year') }}', @json($data['currentYear']), 'rgba(184, 215, 255)', 'rgba(184, 215, 255, 0.5)'),
+                    area('{{ trans('admin.report.last_year') }}', @json($data['lastYear']), 'rgba(146, 240, 230)', 'rgba(146, 240, 230, 0.5)')
+                ],
+            },
+            options: common_options(),
+        });
 
-      new Chart(document.getElementById('years'), {
-        type: 'line',
-        data: {
-          labels: @json(array_keys($data['ordersByYear'])),
-          datasets: [
-            {
-              backgroundColor: 'rgba(184, 215, 255)',
-              borderColor: 'rgba(184, 215, 255)',
-              data: @json(array_values($data['ordersByYear'])),
-              fill: {target: 'origin'},
-              tension: 0.4,
-            }],
-        },
-        options: {
-          responsive: true,
-          scales: {
-            x: {
-              grid: {
-                display: false,
-              },
+        new Chart(document.getElementById('years'), {
+            type: 'line',
+            data: {
+                labels: @json(array_keys($data['ordersByYear'])),
+                datasets: [
+                    area('', @json(array_values($data['ordersByYear'])), 'rgba(184, 215, 255)')
+                ],
             },
-            y: {
-              grid: {
-                display: false,
-              },
-              min: 0,
+            options: {
+                responsive: true,
+                scales: {
+                    x: {
+                        grid: {
+                            display: false
+                        }
+                    },
+                    y: {
+                        grid: {
+                            display: false
+                        },
+                        min: 0
+                    }
+                },
+                plugins: {
+                    legend: false,
+                    tooltip: label_callbacks(' {{ trans('validation.attributes.year') }}')
+                },
             },
-          },
-          plugins: {
-            legend: false,
-            tooltip: label_callbacks(' {{ trans('validation.attributes.year') }}'),
-          },
-        },
-      });
+        });
     </script>
 @endsection

+ 214 - 0
resources/views/admin/report/siteDataAnalysis.blade.php

@@ -0,0 +1,214 @@
+@extends('admin.layouts')
+@section('css')
+    <link href="/assets/global/vendor/bootstrap-select/bootstrap-select.min.css" rel="stylesheet">
+@endsection
+@section('content')
+    <div class="page-content container">
+        <div class="card card-shadow">
+            <div class="card-block p-30">
+                <form class="form-row">
+                    <div class="form-group col-xxl-2 col-lg-3 col-md-3 col-sm-4">
+                        <select class="form-control" id="node_id" name="node_id" data-plugin="selectpicker" data-style="btn-outline btn-primary"
+                                title="{{ trans('admin.logs.user_traffic.choose_node') }}" onchange="this.form.submit()">
+                            @foreach ($nodes as $id => $name)
+                                <option value="{{ $id }}">{{ $name }}</option>
+                            @endforeach
+                        </select>
+                    </div>
+                    <div class="form-group col-xxl-1 col-lg-3 col-md-3 col-4 btn-group">
+                        <button class="btn btn-primary" type="submit">{{ trans('common.search') }}</button>
+                        <a class="btn btn-danger" href="{{ route('admin.report.siteAnalysis') }}">{{ trans('common.reset') }}</a>
+                    </div>
+                </form>
+            </div>
+        </div>
+        <div class="card card-shadow">
+            <div class="card-block p-md-30">
+                <div class="row pb-20" style="height:calc(100% - 322px);">
+                    <div class="col-md-8 col-sm-6">
+                        <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.daily_site_flow') }}</div>
+                    </div>
+                    <div class="col-md-4 col-sm-6">
+                        <div class="row">
+                            @isset($data['avgDaily30d'])
+                                <div class="col-sm-6">
+                                    <div class="counter counter-md">
+                                        <div class="counter-number-group text-nowrap">
+                                            <span class="counter-number">{{ $data['avgDaily30d'] }}</span>
+                                            <span class="counter-number-related">GiB</span>
+                                        </div>
+                                        <div class="counter-label blue-grey-400">{{ trans('admin.report.avg_traffic_30d') }}</div>
+                                    </div>
+                                </div>
+                            @endisset
+                            @isset($data['nodePct30d'])
+                                <div class="col-sm-6">
+                                    <div class="counter counter-md">
+                                        <div class="counter-number-group text-nowrap">
+                                            <span class="counter-number">{{ $data['nodePct30d'] }}</span>
+                                            <span class="counter-number-related">%</span>
+                                        </div>
+                                        <div class="counter-label blue-grey-400">{{ trans('admin.report.sum_traffic_30d') }}</div>
+                                    </div>
+                                </div>
+                            @endisset
+                        </div>
+                    </div>
+                </div>
+                <canvas id="days"></canvas>
+            </div>
+        </div>
+        <div class="card card-shadow">
+            <div class="card-block p-30">
+                <div class="row pb-20">
+                    <div class="col-md-8 col-sm-6">
+                        <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.monthly_site_flow') }}</div>
+                    </div>
+                </div>
+                <canvas id="months"></canvas>
+            </div>
+        </div>
+        <div class="card card-shadow">
+            <div class="card-block p-30">
+                <div class="row pb-20">
+                    <div class="col-md-8 col-sm-6">
+                        <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.annually_site_flow') }}</div>
+                    </div>
+                </div>
+                <canvas id="years"></canvas>
+            </div>
+        </div>
+    </div>
+@endsection
+@section('javascript')
+    <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
+    <script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
+    <script src="/assets/global/js/Plugin/bootstrap-select.js"></script>
+    <script type="text/javascript">
+        function label_callbacks(titleLabel, valueLabel = ' GiB') {
+            return {
+                mode: 'index',
+                intersect: false,
+                callbacks: {
+                    title: function(context) {
+                        return context[0].label + titleLabel;
+                    },
+                    label: function(context) {
+                        let label = context.dataset.label || '';
+                        if (label) {
+                            label += ': ';
+                        }
+                        if (context.parsed.y !== null) {
+                            label += new Intl.NumberFormat('{{ str_replace('_', '-', app()->getLocale()) }}').format(
+                                context.parsed.y) + valueLabel;
+                        }
+                        return label;
+                    },
+                },
+            };
+        }
+
+        function common_options(label) {
+            return {
+                responsive: true,
+                scales: {
+                    x: {
+                        grid: {
+                            display: false,
+                        },
+                    },
+                    y: {
+                        grid: {
+                            display: false,
+                        },
+                        min: 0,
+                    },
+                },
+                plugins: {
+                    legend: {
+                        labels: {
+                            padding: 20,
+                            usePointStyle: true,
+                            pointStyle: 'circle',
+                            font: {
+                                size: 14
+                            },
+                        },
+                    },
+                    tooltip: label_callbacks(label || ''),
+                },
+            };
+        }
+
+        function create_area_dataset(label, data, color) {
+            return {
+                label: label,
+                backgroundColor: color,
+                borderColor: color,
+                data: data,
+                fill: {
+                    target: 'origin',
+                    above: color.replace(')', ', 0.5)'), // Change opacity
+                },
+                tension: 0.4,
+            };
+        }
+
+        function create_chart(elementId, type, labels, datasets, options) {
+            new Chart(document.getElementById(elementId), {
+                type: type,
+                data: {
+                    labels: labels,
+                    datasets: datasets,
+                },
+                options: options,
+            });
+        }
+
+        const daysLabels = @json($data['days']);
+        const currentMonthData = @json($data['currentMonth']);
+        const lastMonthData = @json($data['lastMonth']);
+        const yearsLabels = @json($data['months']);
+        const currentYearData = @json($data['currentYear']);
+        const lastYearData = @json($data['lastYear']);
+        const yearlyFlowsLabels = @json(array_keys($data['yearlyFlows']));
+        const yearlyFlowsData = @json(array_values($data['yearlyFlows']));
+
+        create_chart('days', 'line', daysLabels, [
+            create_area_dataset('{{ trans('admin.report.current_month') }}', currentMonthData, 'rgba(184, 215, 255)'),
+            create_area_dataset('{{ trans('admin.report.last_month') }}', lastMonthData, 'rgba(146, 240, 230)'),
+        ], common_options(' {{ trans_choice('common.days.attribute', 2) }}'));
+
+        create_chart('months', 'line', yearsLabels, [
+            create_area_dataset('{{ trans('admin.report.current_year') }}', currentYearData, 'rgba(184, 215, 255)'),
+            create_area_dataset('{{ trans('admin.report.last_year') }}', lastYearData, 'rgba(146, 240, 230)'),
+        ], common_options());
+
+        create_chart('years', 'line', yearlyFlowsLabels, [
+            create_area_dataset('{{ trans('validation.attributes.year') }}', yearlyFlowsData, 'rgba(184, 215, 255)'),
+        ], {
+            responsive: true,
+            scales: {
+                x: {
+                    grid: {
+                        display: false,
+                    },
+                },
+                y: {
+                    grid: {
+                        display: false,
+                    },
+                    min: 0,
+                },
+            },
+            plugins: {
+                legend: false,
+                tooltip: label_callbacks(' {{ trans('validation.attributes.year') }}', ' TiB'),
+            },
+        });
+
+        $(document).ready(function() {
+            $('#node_id').val({{ Request::query('node_id') }});
+        });
+    </script>
+@endsection

+ 134 - 107
resources/views/admin/report/userDataAnalysis.blade.php

@@ -1,18 +1,19 @@
 @extends('admin.layouts')
 @section('content')
-    <div class="page-content container-fluid">
+    <div class="page-content container">
         <div class="card card-shadow">
             <div class="card-block p-30">
                 <form class="form-row">
                     <div class="form-group col-xxl-1 col-lg-1 col-md-1 col-sm-4">
-                        <input type="number" class="form-control" name="uid" value="{{Request::query('uid')}}" placeholder="{{ trans('model.user.id') }}"/>
+                        <input class="form-control" name="uid" type="number" value="{{ Request::query('uid') }}" placeholder="{{ trans('model.user.id') }}" />
                     </div>
                     <div class="form-group col-xxl-2 col-lg-3 col-md-3 col-sm-4">
-                        <input type="text" class="form-control" name="username" value="{{Request::query('username')}}" placeholder="{{ trans('model.user.username') }}"/>
+                        <input class="form-control" name="username" type="text" value="{{ Request::query('username') }}"
+                               placeholder="{{ trans('model.user.username') }}" />
                     </div>
                     <div class="form-group col-xxl-1 col-lg-3 col-md-3 col-4 btn-group">
-                        <button type="submit" class="btn btn-primary">{{ trans('common.search') }}</button>
-                        <a href="{{route('admin.report.userAnalysis')}}" class="btn btn-danger">{{ trans('common.reset') }}</a>
+                        <button class="btn btn-primary" type="submit">{{ trans('common.search') }}</button>
+                        <a class="btn btn-danger" href="{{ route('admin.report.userAnalysis') }}">{{ trans('common.reset') }}</a>
                     </div>
                 </form>
             </div>
@@ -47,112 +48,138 @@
     @isset($data)
         <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
         <script type="text/javascript">
-          function label_callbacks(tail) {
-            return {
-              mode: 'index',
-              intersect: false,
-              callbacks: {
-                title: function(context) {
-                  return context[0].label + ' ' + tail;
-                },
-                label: function(context) {
-                  let label = context.dataset.label || '';
+            function createBarChart(elementId, labels, datasets, labelTail) {
+                new Chart(document.getElementById(elementId), {
+                    type: 'bar',
+                    data: {
+                        labels: labels,
+                        datasets: datasets,
+                    },
+                    options: {
+                        parsing: {
+                            xAxisKey: 'time',
+                            yAxisKey: 'total',
+                        },
+                        scales: {
+                            x: {
+                                stacked: true,
+                            },
+                            y: {
+                                stacked: true,
+                            },
+                        },
+                        responsive: true,
+                        plugins: {
+                            legend: {
+                                labels: {
+                                    padding: 20,
+                                    usePointStyle: true,
+                                    pointStyle: 'circle',
+                                    font: {
+                                        size: 14
+                                    },
+                                },
+                            },
+                            tooltip: label_callbacks(labelTail),
+                        },
+                    },
+                });
+            }
 
-                  if (label) {
-                    label += ': ';
-                  }
-                  if (context.parsed.y !== null) {
-                    label += context.parsed.y + ' GB';
-                  }
-                  return label;
-                },
-              },
-            };
-          }
+            function label_callbacks(tail) {
+                return {
+                    mode: 'index',
+                    intersect: false,
+                    callbacks: {
+                        title: function(context) {
+                            return context[0].label + ' ' + tail;
+                        },
+                        label: function(context) {
+                            let label = context.dataset.label || '';
+                            if (label) {
+                                label += ': ';
+                            }
+                            if (context.parsed.y !== null) {
+                                label += context.parsed.y + ' MiB';
+                            }
+                            return label;
+                        },
+                    },
+                };
+            }
 
-          function area_a(label, data) {
-            return {
-              label: label,
-              backgroundColor: 'rgba(184, 215, 255)',
-              borderColor: 'rgba(184, 215, 255)',
-              data: data,
-              fill: {
-                target: 'origin',
-                above: 'rgba(184, 215, 255, 0.5)',
-              },
-              tension: 0.4,
-            };
-          }
+            const userData = @json($data);
+            const nodeColorMap = generateNodeColorMap(userData.nodes); // 获取所有节点名称并生成颜色映射
 
-          new Chart(document.getElementById('hourlyBar'), {
-            type: 'bar',
-            data: {
-              labels: @json($data['hours']),
-              datasets: [area_a('{{ trans('admin.report.today') }}',@json($data['hourlyFlow']))],
-            },
-            options: {
-              // stack: 'node_id',
-              parsing: {
-                xAxisKey: 'date',
-                yAxisKey: 'total',
-              },
-              scales: {
-                x: {
-                  stacked: true,
-                },
-                y: {
-                  stacked: true,
-                },
-              },
-              responsive: true,
-              plugins: {
-                legend: {
-                  labels: {
-                    padding: 20,
-                    usePointStyle: true,
-                    pointStyle: 'circle',
-                    font: {size: 14},
-                  },
-                },
-                tooltip: label_callbacks(@json(trans_choice('common.hour', 2))),
-              },
-            },
-          });
+            function generateNodeColorMap(nodeNames) {
+                const colorMap = {};
+                Object.entries(nodeNames).forEach(([id, name]) => {
+                    colorMap[id] = getRandomColor(name);
+                });
+                return colorMap;
+            }
 
-          new Chart(document.getElementById('dailyBar'), {
-            type: 'bar',
-            data: {
-              labels: @json($data['days']),
-              datasets: [area_a('{{ trans('admin.report.current_month') }}',@json($data['dailyFlow']))],
-            },
-            options: {
-              // stack: 'node_id',
-              parsing: {
-                xAxisKey: 'date',
-                yAxisKey: 'total',
-              },
-              scales: {
-                x: {
-                  stacked: true,
-                },
-                y: {
-                  stacked: true,
-                },
-              },
-              responsive: true,
-              plugins: {
-                legend: {
-                  labels: {
-                    padding: 20,
-                    usePointStyle: true,
-                    pointStyle: 'circle',
-                    font: {size: 14},
-                  },
-                },
-                tooltip: label_callbacks(@json(trans_choice('common.days.attribute', 2))),
-              },
-            },
-          });
+            // 生成随机颜色
+            function getRandomColor(name) {
+                // 将字符串转换为哈希值
+                let hash = 0;
+                for (let i = 0; i < name.length; i++) {
+                    hash = name.charCodeAt(i) + ((hash << 5) - hash);
+                }
+
+                // 定义不同色调的范围
+                const hueOffset = hash % 360;
+                const hueRange = 20; // 色调范围
+
+                // 计算最终色调
+                const hue = (hueOffset + Math.random() * hueRange) % 360; // 确保 hue 在 0-359 之间
+
+                // 保持饱和度和亮度固定
+                const saturation = 70; // 保持饱和度较高
+                const lightness = 50; // 保持亮度适中
+
+                // 添加透明度
+                const alpha = 0.55; // 50% 透明度
+
+                return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
+            }
+
+            // 生成数据集
+            // 生成数据集
+            function generateDatasets(flows) {
+                const dataByNode = {};
+
+                // 按节点 ID 分组数据
+                flows.forEach(flow => {
+                    if (!dataByNode[flow.id]) {
+                        dataByNode[flow.id] = [];
+                    }
+                    dataByNode[flow.id].push({
+                        time: flow.time,
+                        total: flow.total,
+                        name: flow.name,
+                    });
+                });
+
+                // 创建 datasets 数组
+                let datasets = [];
+                for (const nodeId in dataByNode) {
+                    if (dataByNode.hasOwnProperty(nodeId)) {
+                        datasets.push({
+                            label: dataByNode[nodeId][0].name, // 使用 name 作为标签
+                            backgroundColor: nodeColorMap[nodeId],
+                            borderColor: nodeColorMap[nodeId],
+                            data: dataByNode[nodeId],
+                            fill: true,
+                        });
+                    }
+                }
+                return datasets;
+            }
+
+            // 创建图表
+            createBarChart('hourlyBar', userData.hours, generateDatasets(userData.hourlyFlows), @json(trans_choice('common.hour', 2)));
+            createBarChart('dailyBar', userData.days, generateDatasets(userData.dailyFlows), @json(trans_choice('common.days.attribute', 2)));
         </script>
     @endisset
 @endsection

+ 1 - 0
routes/admin.php

@@ -106,6 +106,7 @@ Route::prefix('admin')->name('admin.')->group(function () {
     Route::prefix('report')->name('report.')->controller(ReportController::class)->group(function () {
         Route::get('accounting', 'accounting')->name('accounting'); // 流水账簿
         Route::get('user/analysis', 'userAnalysis')->name('userAnalysis'); // 用户流量分析
+        Route::get('site/analysis', 'siteAnalysis')->name('siteAnalysis'); // 网站流量分析
     });
 
     Route::prefix('log')->name('log.')->controller(LogsController::class)->group(function () {