Tree 5 өдөр өмнө
parent
commit
562826bcca

+ 80 - 13
app/Http/Controllers/Admin/WeightConfigController.php

@@ -19,8 +19,15 @@ class WeightConfigController extends Controller
             $totalClicks[$id] = isset($totalRaw[$id]) ? intval($totalRaw[$id]) : 0;
         }
 
-        $days = 7;
+        $showRaw = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_SHOW);
+        $totalShows = [];
+        foreach (ApiWeightConfigController::VALID_IDS as $id) {
+            $totalShows[$id] = isset($showRaw[$id]) ? intval($showRaw[$id]) : 0;
+        }
+
+        $days = 3;
         $dailyClicks = [];
+        $dailyShows = [];
         for ($i = $days - 1; $i >= 0; $i--) {
             $date = date('Y-m-d', strtotime("-{$i} days"));
             $raw = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX . $date);
@@ -29,9 +36,34 @@ class WeightConfigController extends Controller
                 $row[$id] = isset($raw[$id]) ? intval($raw[$id]) : 0;
             }
             $dailyClicks[] = $row;
+
+            $rawShow = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_SHOW_DAILY_PREFIX . $date);
+            $rowShow = ['date' => $date];
+            foreach (ApiWeightConfigController::VALID_IDS as $id) {
+                $rowShow[$id] = isset($rawShow[$id]) ? intval($rawShow[$id]) : 0;
+            }
+            $dailyShows[] = $rowShow;
         }
 
-        return view('admin.weight_config.index', compact('config', 'totalClicks', 'dailyClicks'));
+        $today = date('Y-m-d');
+        $todayClickRaw = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX . $today);
+        $todayShowRaw = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_SHOW_DAILY_PREFIX . $today);
+        $todayClicks = [];
+        $todayShows = [];
+        foreach (ApiWeightConfigController::VALID_IDS as $id) {
+            $todayClicks[$id] = isset($todayClickRaw[$id]) ? intval($todayClickRaw[$id]) : 0;
+            $todayShows[$id] = isset($todayShowRaw[$id]) ? intval($todayShowRaw[$id]) : 0;
+        }
+
+        return view('admin.weight_config.index', compact(
+            'config',
+            'totalClicks',
+            'totalShows',
+            'todayClicks',
+            'todayShows',
+            'dailyClicks',
+            'dailyShows'
+        ));
     }
 
     public function update(Request $request)
@@ -61,23 +93,58 @@ class WeightConfigController extends Controller
 
     public function resetStats(Request $request)
     {
-        $type = $request->input('type', 'total');
+        $type = $request->input('type');
+        if (!$type) {
+            return response()->json(['status' => 'error', 'message' => '缺少操作类型']);
+        }
+        $prefix = config('database.redis.options.prefix', '');
+
+        $stripPrefix = function ($keys) use ($prefix) {
+            if (!$prefix || empty($keys)) {
+                return $keys;
+            }
+            return array_map(function ($k) use ($prefix) {
+                return strpos($k, $prefix) === 0 ? substr($k, strlen($prefix)) : $k;
+            }, $keys);
+        };
+
+        $hdelFieldFromDailyKeys = function (string $dailyPrefix, string $field) use ($stripPrefix) {
+            $keys = Redis::keys($dailyPrefix . '*');
+            foreach ($stripPrefix($keys) as $k) {
+                Redis::hdel($k, $field);
+            }
+        };
+
         if ($type === 'all') {
             Redis::del(ApiWeightConfigController::REDIS_KEY_CLICKS);
-            $keys = Redis::keys(ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX . '*');
-            if (!empty($keys)) {
-                $prefix = config('database.redis.options.prefix', '');
-                if ($prefix) {
-                    $keys = array_map(function ($k) use ($prefix) {
-                        return strpos($k, $prefix) === 0 ? substr($k, strlen($prefix)) : $k;
-                    }, $keys);
-                }
-                foreach ($keys as $k) {
+            Redis::del(ApiWeightConfigController::REDIS_KEY_SHOW);
+            foreach ([
+                         ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX,
+                         ApiWeightConfigController::REDIS_KEY_SHOW_DAILY_PREFIX,
+                     ] as $dailyPrefix) {
+                $keys = Redis::keys($dailyPrefix . '*');
+                foreach ($stripPrefix($keys) as $k) {
                     Redis::del($k);
                 }
             }
+        } elseif ($type === 'click_id') {
+            $id = intval($request->input('id'));
+            if (!in_array($id, ApiWeightConfigController::VALID_IDS, true)) {
+                return response()->json(['status' => 'error', 'message' => '无效 id']);
+            }
+            $field = (string) $id;
+            Redis::hdel(ApiWeightConfigController::REDIS_KEY_CLICKS, $field);
+            $hdelFieldFromDailyKeys(ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX, $field);
+        } elseif ($type === 'show_id') {
+            $id = intval($request->input('id'));
+            if (!in_array($id, ApiWeightConfigController::VALID_IDS, true)) {
+                return response()->json(['status' => 'error', 'message' => '无效 id']);
+            }
+            $field = (string) $id;
+            Redis::hdel(ApiWeightConfigController::REDIS_KEY_SHOW, $field);
+            $hdelFieldFromDailyKeys(ApiWeightConfigController::REDIS_KEY_SHOW_DAILY_PREFIX, $field);
         } else {
-            Redis::del(ApiWeightConfigController::REDIS_KEY_CLICKS);
+            return response()->json(['status' => 'error', 'message' => '未知操作类型']);
         }
         return response()->json(['status' => 'success', 'message' => '清除成功']);
     }

+ 19 - 2
app/Http/Controllers/Api/WeightConfigController.php

@@ -11,6 +11,10 @@ class WeightConfigController extends Controller
     const REDIS_KEY_CONFIG = 'WeightConfig1234:config';
     const REDIS_KEY_CLICKS = 'WeightConfig1234:clicks';
     const REDIS_KEY_CLICKS_DAILY_PREFIX = 'WeightConfig1234:clicks:daily:';
+    const REDIS_KEY_SHOW = 'WeightConfig1234:show';
+    const REDIS_KEY_SHOW_DAILY_PREFIX = 'WeightConfig1234:show:daily:';
+    /** 每日明细 Redis 过期时间(仅保留约 3 天) */
+    const DAILY_STATS_TTL_SECONDS = 86400 * 3;
     const VALID_IDS = [1, 2, 3, 4];
 
     public static function defaultConfig()
@@ -35,6 +39,9 @@ class WeightConfigController extends Controller
         return $result;
     }
 
+    /**
+     * 传 id;若请求中带参数 show(任意非 null 值,含空字符串或 0),只累计曝光统计 WeightConfig1234:show,不累计点击。
+     */
     public function clickRecord(Request $request)
     {
         $id = intval($request->input('id'));
@@ -42,12 +49,22 @@ class WeightConfigController extends Controller
             return apiReturnFail('invalid id');
         }
 
+        if ($request->exists('type')) {
+            Redis::hincrby(self::REDIS_KEY_SHOW, $id, 1);
+
+            $dailyShowKey = self::REDIS_KEY_SHOW_DAILY_PREFIX . date('Y-m-d');
+            Redis::hincrby($dailyShowKey, $id, 1);
+            Redis::expire($dailyShowKey, self::DAILY_STATS_TTL_SECONDS);
+
+            return apiReturnSuc(['id' => $id, 'type' => 'show']);
+        }
+
         Redis::hincrby(self::REDIS_KEY_CLICKS, $id, 1);
 
         $dailyKey = self::REDIS_KEY_CLICKS_DAILY_PREFIX . date('Y-m-d');
         Redis::hincrby($dailyKey, $id, 1);
-        Redis::expire($dailyKey, 60 * 60 * 24 * 90);
+        Redis::expire($dailyKey, self::DAILY_STATS_TTL_SECONDS);
 
-        return apiReturnSuc(['id' => $id]);
+        return apiReturnSuc(['id' => $id, 'type' => 'click']);
     }
 }

+ 222 - 39
resources/views/admin/weight_config/index.blade.php

@@ -6,8 +6,8 @@
         <div class="col-12">
             <div class="card">
                 <div class="card-header d-flex align-items-center justify-content-between">
-                    <h3 class="card-title mb-0">1234 权重配置 & 点击统计</h3>
-                    <small class="text-muted">配置 1/2/3/4 的权重(整数),前端按权重抽取,后台记录用户点击</small>
+                    <h3 class="card-title mb-0">1234 权重配置 & 数据统计</h3>
+                    <small class="text-muted">配置 1/2/3/4 权重;带 <code>show</code> 只记曝光;每日明细 Redis 保留约 3 天</small>
                 </div>
                 <div class="card-body">
                     <ul class="nav nav-tabs mb-3" role="tablist">
@@ -18,7 +18,7 @@
                         </li>
                         <li class="nav-item">
                             <a class="nav-link" data-toggle="tab" href="#tab-stats" role="tab">
-                                <i class="mdi mdi-chart-bar"></i> 点击统计
+                                <i class="mdi mdi-chart-bar"></i> 数据统计
                             </a>
                         </li>
                     </ul>
@@ -71,74 +71,138 @@
                         </div>
 
                         <div class="tab-pane fade" id="tab-stats" role="tabpanel">
-                            <div class="d-flex align-items-center justify-content-between mb-3">
-                                <h5 class="mb-0">总点击统计</h5>
-                                <button class="btn btn-sm btn-outline-danger" id="reset-stats">
-                                    <i class="mdi mdi-delete"></i> 清除总统计
-                                </button>
+                            @php
+                                $chartIds = [1, 2, 3, 4];
+                                $chartTodayShows = array_map(fn ($id) => (int) ($todayShows[$id] ?? 0), $chartIds);
+                                $chartTodayClicks = array_map(fn ($id) => (int) ($todayClicks[$id] ?? 0), $chartIds);
+                                $chartTotalShows = array_map(fn ($id) => (int) ($totalShows[$id] ?? 0), $chartIds);
+                                $chartTotalClicks = array_map(fn ($id) => (int) ($totalClicks[$id] ?? 0), $chartIds);
+                            @endphp
+                            <div class="card border mb-4 shadow-sm">
+                                <div class="card-body">
+                                    <div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
+                                        <h5 class="card-title mb-0">曝光 & 点击(按 ID)</h5>
+                                        <div class="btn-group btn-group-sm" role="group" aria-label="统计维度">
+                                            <button type="button" class="btn btn-outline-primary active weight-chart-mode" data-mode="today">当天</button>
+                                            <button type="button" class="btn btn-outline-primary weight-chart-mode" data-mode="total">总计</button>
+                                        </div>
+                                    </div>
+                                    <p class="text-muted small mb-3 mb-md-2">
+                                        <span id="weight-chart-subtitle">当天数据 · {{ date('Y-m-d') }}</span>
+                                    </p>
+                                    <div id="weight-stats-chart" style="width:100%;height:340px;"></div>
+                                    <div class="d-flex justify-content-center align-items-center mt-2 small text-muted">
+                                        <span class="mr-3"><span class="weight-chart-legend-swatch" style="background:#c8bdb0;"></span> 曝光(show)</span>
+                                        <span><span class="weight-chart-legend-swatch" style="background:#2563eb;"></span> 点击</span>
+                                    </div>
+                                </div>
                             </div>
+
+                            <h5 class="mb-3">总计(Redis 累计)</h5>
                             @php
                                 $totalClicksAll = array_sum($totalClicks);
+                                $totalShowsAll = array_sum($totalShows);
                             @endphp
                             <div class="table-responsive mb-4">
-                                <table class="table table-bordered align-middle" style="max-width: 600px;">
+                                <table class="table table-bordered align-middle weight-stats-merge-table" style="max-width: 920px;">
                                     <thead class="thead-light">
                                         <tr>
                                             <th>ID</th>
-                                            <th>点击次数</th>
-                                            <th>占比</th>
+                                            <th class="text-right">曝光</th>
+                                            <th class="text-right">曝光%</th>
+                                            <th class="text-right">点击</th>
+                                            <th class="text-right">点击%</th>
+                                            <th class="text-center" style="min-width:140px;">操作</th>
                                         </tr>
                                     </thead>
                                     <tbody>
                                         @foreach([1,2,3,4] as $id)
                                             @php
                                                 $c = intval($totalClicks[$id] ?? 0);
-                                                $p = $totalClicksAll > 0 ? round($c / $totalClicksAll * 100, 2) : 0;
+                                                $s = intval($totalShows[$id] ?? 0);
+                                                $pClick = $totalClicksAll > 0 ? round($c / $totalClicksAll * 100, 2) : 0;
+                                                $pShow = $totalShowsAll > 0 ? round($s / $totalShowsAll * 100, 2) : 0;
                                             @endphp
                                             <tr>
-                                                <td><span class="badge badge-info">{{ $id }}</span></td>
-                                                <td>{{ $c }}</td>
-                                                <td>{{ $p }}%</td>
+                                                <td><span class="badge badge-primary">{{ $id }}</span></td>
+                                                <td class="text-right">{{ $s }}</td>
+                                                <td class="text-right">{{ $pShow }}%</td>
+                                                <td class="text-right">{{ $c }}</td>
+                                                <td class="text-right">{{ $pClick }}%</td>
+                                                <td class="text-center">
+                                                    <button type="button" class="btn btn-outline-secondary btn-sm py-0 px-2 mr-1 reset-stats-btn"
+                                                            data-type="show_id" data-id="{{ $id }}"
+                                                            data-confirm="确认清除 ID {{ $id }} 的曝光总计及每日明细中该 ID?">
+                                                        清曝光
+                                                    </button>
+                                                    <button type="button" class="btn btn-outline-danger btn-sm py-0 px-2 reset-stats-btn"
+                                                            data-type="click_id" data-id="{{ $id }}"
+                                                            data-confirm="确认清除 ID {{ $id }} 的点击总计及每日明细中该 ID?">
+                                                        清点击
+                                                    </button>
+                                                </td>
                                             </tr>
                                         @endforeach
                                         <tr class="table-secondary">
                                             <td><strong>合计</strong></td>
-                                            <td colspan="2"><strong>{{ $totalClicksAll }}</strong></td>
+                                            <td class="text-right"><strong>{{ $totalShowsAll }}</strong></td>
+                                            <td class="text-right">—</td>
+                                            <td class="text-right"><strong>{{ $totalClicksAll }}</strong></td>
+                                            <td class="text-right">—</td>
+                                            <td class="text-center text-muted small">—</td>
                                         </tr>
                                     </tbody>
                                 </table>
                             </div>
 
-                            <h5 class="mb-3">最近 7 天每日点击</h5>
-                            <div class="table-responsive">
-                                <table class="table table-bordered align-middle" style="max-width: 800px;">
+                            <h5 class="mb-3">最近 3 天每日(曝光 / 点击)</h5>
+                            <div class="table-responsive mb-4">
+                                <table class="table table-bordered align-middle table-sm weight-stats-merge-table" style="max-width: 920px;">
                                     <thead class="thead-light">
                                         <tr>
-                                            <th>日期</th>
-                                            <th>1</th>
-                                            <th>2</th>
-                                            <th>3</th>
-                                            <th>4</th>
-                                            <th>合计</th>
+                                            <th class="align-middle">日期</th>
+                                            @foreach([1,2,3,4] as $hid)
+                                                <th class="text-center">{{ $hid }}</th>
+                                            @endforeach
+                                            <th class="text-right">曝光∑</th>
+                                            <th class="text-right">点击∑</th>
                                         </tr>
                                     </thead>
                                     <tbody>
-                                        @foreach($dailyClicks as $row)
+                                        @foreach($dailyClicks as $idx => $row)
                                             @php
-                                                $sum = intval($row[1]) + intval($row[2]) + intval($row[3]) + intval($row[4]);
+                                                $rowS = $dailyShows[$idx] ?? ['date' => $row['date'], 1=>0,2=>0,3=>0,4=>0];
+                                                $sumShow = 0;
+                                                $sumClick = 0;
                                             @endphp
                                             <tr>
                                                 <td>{{ $row['date'] }}</td>
-                                                <td>{{ $row[1] }}</td>
-                                                <td>{{ $row[2] }}</td>
-                                                <td>{{ $row[3] }}</td>
-                                                <td>{{ $row[4] }}</td>
-                                                <td><strong>{{ $sum }}</strong></td>
+                                                @foreach([1,2,3,4] as $hid)
+                                                    @php
+                                                        $sv = intval($rowS[$hid] ?? 0);
+                                                        $cv = intval($row[$hid] ?? 0);
+                                                        $sumShow += $sv;
+                                                        $sumClick += $cv;
+                                                    @endphp
+                                                    <td class="text-center cell-exp-click">
+                                                        <span class="text-muted small">曝</span> {{ $sv }}
+                                                        <span class="mx-1 text-muted">/</span>
+                                                        <span class="text-muted small">点</span> {{ $cv }}
+                                                    </td>
+                                                @endforeach
+                                                <td class="text-right"><strong>{{ $sumShow }}</strong></td>
+                                                <td class="text-right"><strong>{{ $sumClick }}</strong></td>
                                             </tr>
                                         @endforeach
                                     </tbody>
                                 </table>
                             </div>
+
+                            <div class="alert alert-secondary mb-0">
+                                <button type="button" class="btn btn-sm btn-danger reset-stats-btn" data-type="all" data-confirm="确认清除点击与曝光的全部总计及每日明细?此操作不可恢复。">
+                                    <i class="mdi mdi-delete-forever"></i> 清除全部统计(点击+曝光含明细)
+                                </button>
+                            </div>
                         </div>
                     </div>
                 </div>
@@ -147,8 +211,115 @@
     </div>
 </div>
 
+<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
 <script>
 $(function() {
+    var weightChartData = {
+        today: {
+            shows: @json($chartTodayShows),
+            clicks: @json($chartTodayClicks),
+            subtitle: @json('当天数据 · ' . date('Y-m-d'))
+        },
+        total: {
+            shows: @json($chartTotalShows),
+            clicks: @json($chartTotalClicks),
+            subtitle: @json('总计(Redis 累计)')
+        }
+    };
+
+    function initWeightStatsChart() {
+        var chartDom = document.getElementById('weight-stats-chart');
+        if (!chartDom || typeof echarts === 'undefined') {
+            return null;
+        }
+        var chart = echarts.init(chartDom);
+
+        function buildOption(pack) {
+            return {
+                backgroundColor: '#ffffff',
+                tooltip: {
+                    trigger: 'axis',
+                    axisPointer: { type: 'shadow' }
+                },
+                legend: {
+                    data: ['曝光', '点击'],
+                    bottom: 6,
+                    itemGap: 28,
+                    textStyle: { color: '#555' }
+                },
+                grid: {
+                    left: '2%',
+                    right: '2%',
+                    bottom: '16%',
+                    top: '10%',
+                    containLabel: true
+                },
+                xAxis: {
+                    type: 'category',
+                    data: ['1', '2', '3', '4'],
+                    axisLine: { lineStyle: { color: '#ddd' } },
+                    axisTick: { alignWithLabel: true },
+                    axisLabel: { color: '#666', fontSize: 12 }
+                },
+                yAxis: {
+                    type: 'value',
+                    minInterval: 1,
+                    splitLine: { lineStyle: { color: '#eeeeee', width: 1 } },
+                    axisLabel: { color: '#666' }
+                },
+                series: [
+                    {
+                        name: '曝光',
+                        type: 'bar',
+                        barMaxWidth: 36,
+                        barGap: '18%',
+                        barCategoryGap: '40%',
+                        data: pack.shows,
+                        itemStyle: {
+                            color: '#c8bdb0',
+                            borderRadius: [4, 4, 0, 0]
+                        }
+                    },
+                    {
+                        name: '点击',
+                        type: 'bar',
+                        barMaxWidth: 36,
+                        data: pack.clicks,
+                        itemStyle: {
+                            color: '#2563eb',
+                            borderRadius: [4, 4, 0, 0]
+                        }
+                    }
+                ]
+            };
+        }
+
+        function applyMode(mode) {
+            var pack = mode === 'today' ? weightChartData.today : weightChartData.total;
+            chart.setOption(buildOption(pack), true);
+            $('#weight-chart-subtitle').text(pack.subtitle);
+            $('.weight-chart-mode').removeClass('active');
+            $('.weight-chart-mode[data-mode="' + mode + '"]').addClass('active');
+        }
+
+        $('.weight-chart-mode').on('click', function () {
+            applyMode($(this).data('mode'));
+        });
+
+        $('a[data-toggle="tab"][href="#tab-stats"]').on('shown.bs.tab', function () {
+            chart.resize();
+        });
+
+        $(window).on('resize', function () {
+            chart.resize();
+        });
+
+        applyMode('today');
+        return chart;
+    }
+
+    initWeightStatsChart();
+
     function refreshPercents() {
         let total = 0;
         $('.weight-input').each(function(){
@@ -196,12 +367,16 @@ $(function() {
         });
     });
 
-    $('#reset-stats').click(function(){
-        if (!confirm('确认清除总点击统计?(每日明细仍保留)')) return;
-        $.post("{{ url('/admin/weight-config/reset-stats') }}", {
-            type: 'total',
-            _token: "{{ csrf_token() }}"
-        }).done(function(res){
+    $('.reset-stats-btn').click(function(){
+        const type = $(this).data('type');
+        const id = $(this).data('id');
+        const msg = $(this).data('confirm') || '确认清除?';
+        if (!confirm(msg)) return;
+        const payload = { type: type, _token: "{{ csrf_token() }}" };
+        if (id !== undefined && id !== '') {
+            payload.id = id;
+        }
+        $.post("{{ url('/admin/weight-config/reset-stats') }}", payload).done(function(res){
             if (res.status === 'success') {
                 location.reload();
             } else {
@@ -217,6 +392,14 @@ $(function() {
 <style>
 .table thead th { white-space: nowrap; }
 .table tbody td { vertical-align: middle; }
-.badge { font-size: .85rem; padding: .5em .6em; }
+.cell-exp-click { white-space: nowrap; font-size: 0.875rem; }
+.weight-stats-merge-table tbody td { vertical-align: middle; }
+    display: inline-block;
+    width: 12px;
+    height: 12px;
+    border-radius: 3px;
+    vertical-align: -2px;
+    margin-right: 6px;
+}
 </style>
 @endsection