feat(analytics): 数据分析页增加访问与受控设备的国家维度分布

参考受控版本分布的饼图实现,新增国家维度分布:
- AnalyticsDailyDim 增加 KIND_VISIT_COUNTRY / KIND_DEVICE_COUNTRY 常量
- AnalyticsReportService::aggregateDay() 按 country 聚合访问 UV 与受控设备数
- present() 汇总并返回 by_visit_country / by_device_country(ISO 代码转中文名展示)
- 视图新增两个国家饼图卡片与两个国家明细标签区

性能与正确性修复:
- ensureCached() 改为一次查询校验所有按日聚合 kind 的缓存完整性,
  避免旧缓存完整时新维度不被聚合的问题
- 新增迁移为 page_visits / devices 建立 (created_at, country) 复合索引,
  对齐 os_version 维度的索引做法,加速按日期范围 + GROUP BY country 的聚合
This commit is contained in:
hashbro
2026-09-26 19:49:25 +00:00
parent 2c974c8263
commit 8d5ec411f1
4 changed files with 177 additions and 5 deletions
@@ -146,6 +146,27 @@
</div>
</div>
<div class="layui-row layui-col-space15">
<div class="layui-col-md6">
<div class="layui-card an-pie-card">
<div class="layui-card-header">访问 · 国家</div>
<div class="layui-card-body">
<div id="an-visit-country-chart" class="an-pie"></div>
<p class="an-pie-hint" id="an-visit-country-hint"></p>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card an-pie-card">
<div class="layui-card-header">受控设备 · 国家</div>
<div class="layui-card-body">
<div id="an-device-country-chart" class="an-pie"></div>
<p class="an-pie-hint" id="an-device-country-hint"></p>
</div>
</div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">受控设备版本明细 <span class="layui-word-aux" id="an-device-count"></span></div>
<div class="layui-card-body">
@@ -158,6 +179,18 @@
<div class="an-tags" id="an-by-ios-version"><span class="an-tags-empty">—</span></div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">访问国家明细 <span class="layui-word-aux" id="an-visit-country-count"></span></div>
<div class="layui-card-body">
<div class="an-tags" id="an-by-visit-country"><span class="an-tags-empty">—</span></div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">受控设备国家明细 <span class="layui-word-aux" id="an-device-country-count"></span></div>
<div class="layui-card-body">
<div class="an-tags" id="an-by-device-country"><span class="an-tags-empty">—</span></div>
</div>
</div>
<div class="layui-word-aux" id="an-range" style="margin:0 0 12px;"></div>
@endsection
@@ -340,12 +373,20 @@ layui.use(['form', 'laydate', 'layer', 'echarts'], function () {
renderPie('an-os-chart', 'an-os-hint', data.by_os, 'uv', '系统');
renderPie('an-ios-chart', 'an-ios-hint', data.by_ios_version, 'uv', 'iOS 版本');
renderPie('an-device-chart', 'an-device-hint', data.by_device_ios_version, 'count', '受控版本');
renderPie('an-visit-country-chart', 'an-visit-country-hint', data.by_visit_country, 'uv', '访问国家');
renderPie('an-device-country-chart', 'an-device-country-hint', data.by_device_country, 'count', '受控国家');
fillTags('#an-by-ios-version', data.by_ios_version, 'uv');
fillTags('#an-by-device-ios', data.by_device_ios_version, 'count');
fillTags('#an-by-visit-country', data.by_visit_country, 'uv');
fillTags('#an-by-device-country', data.by_device_country, 'count');
var iosN = (data.by_ios_version || []).length;
var deviceN = (data.by_device_ios_version || []).length;
var visitCountryN = (data.by_visit_country || []).length;
var deviceCountryN = (data.by_device_country || []).length;
$('#an-ios-count').text(iosN ? ('· ' + iosN + ' 个') : '');
$('#an-device-count').text(deviceN ? ('· ' + deviceN + ' 个') : '');
$('#an-visit-country-count').text(visitCountryN ? ('· ' + visitCountryN + ' 个') : '');
$('#an-device-country-count').text(deviceCountryN ? ('· ' + deviceCountryN + ' 个') : '');
var computed = data.computed_at ? (' · 汇总于 ' + data.computed_at) : '';
$('#an-range').text('统计区间:' + data.from + ' ~ ' + data.to + '(按日去重后加总,今日约 15 分钟刷新)' + computed);
}