问答平台(7),网站数据统计

网站数据统计-图示

UV

  • 独立访客(Unique Visitor),需通过用户IP去重统计数据。
  • 每次访问都要进行统计。
  • HyperLogLog,性能好,且存储空间小。

DAU

  • 日活跃用户(Daliy Active User),需通过用户ID去重统计数据。
  • 网站定义的规则:访问过一次,则认为其活跃。
  • Bitmap,性能好,且可以统计精确结果。

工具类

  • RedisKeyUtil: 增加内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private static final String PREFIX_UV = "uv";
    private static final String PREFIX_DAU = "dau";

    // 单日UV
    public static String getUVKey(String date) {
    return PREFIX_UV + SPLIT + date;
    }

    // 区间UV
    public static String getUVKey(String startDate, String endDate) {
    return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;
    }

    // 单日活跃用户
    public static String getDAUKey(String date) {
    return PREFIX_DAU + SPLIT + date;
    }

    // 区间活跃用户
    public static String getDAUKey(String startDate, String endDate) {
    return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate;
    }

业务层

  • DataService: 新增
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    @Service
    public class DataService {

    @Autowired
    private RedisTemplate redisTemplate;

    private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");

    // 将指定的IP计入UV
    public void recordUV(String ip) {
    String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
    redisTemplate.opsForHyperLogLog().add(redisKey, ip);
    }

    // 统计指定日期范围内的UV
    public long calculateUV(Date start, Date end) {
    if (start == null || end == null) {
    throw new IllegalArgumentException("参数不能为空!");
    }

    // 整理该日期范围内的key
    List<String> keyList = new ArrayList<>();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(start);
    while (!calendar.getTime().after(end)) {
    String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));
    keyList.add(key);
    calendar.add(Calendar.DATE, 1);
    }

    // 合并这些数据
    String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));
    redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());

    // 返回统计的结果
    return redisTemplate.opsForHyperLogLog().size(redisKey);
    }

    // 将指定用户计入DAU
    public void recordDAU(int userId) {
    String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));
    redisTemplate.opsForValue().setBit(redisKey, userId, true);
    }

    // 统计指定日期范围内的DAU
    public long calculateDAU(Date start, Date end) {
    if (start == null || end == null) {
    throw new IllegalArgumentException("参数不能为空!");
    }

    // 整理该日期范围内的key
    List<byte[]> keyList = new ArrayList<>();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(start);
    while (!calendar.getTime().after(end)) {
    String key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime()));
    keyList.add(key.getBytes());
    calendar.add(Calendar.DATE, 1);
    }

    // 进行OR运算
    return (long) redisTemplate.execute(new RedisCallback() {
    @Override
    public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
    String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end));
    redisConnection.bitOp(RedisStringCommands.BitOperation.OR,
    redisKey.getBytes(), keyList.toArray(new byte[0][0]));

    return redisConnection.bitCount(redisKey.getBytes());
    }
    });
    }
    }

拦截器

  • DataInterceptor: 新增
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Component
    public class DataInterceptor implements HandlerInterceptor {

    @Autowired
    private DataService dataService;

    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 统计UV
    String ip = request.getRemoteHost();
    dataService.recordUV(ip);

    // 统计DAU
    User user = hostHolder.getUser();
    if (user != null) {
    dataService.recordDAU(user.getId());
    }

    return true;
    }
    }

config

  • WebMvcConfig: 增加内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Autowired
    private DataInterceptor dataInterceptor;

    // 添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    //将拦截器加入registry对象
    registry.addInterceptor(dataInterceptor)
    .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }

表现层

  • DataController: 新增
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    @Controller
    public class DataController {

    @Autowired
    private DataService dataService;

    // 统计页面
    @RequestMapping(path = "/data", method = { RequestMethod.GET, RequestMethod.POST })
    public String getDataPage() {
    return "/site/admin/data";
    }

    // 统计网站UV
    @RequestMapping(path = "/data/uv", method = RequestMethod.POST)
    public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
    @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) {
    long uv = dataService.calculateUV(start, end);
    model.addAttribute("uvResult", uv);
    model.addAttribute("uvStartDate", start);
    model.addAttribute("uvEndDate", end);
    return "forward:/data";
    }

    // 统计活跃用户DAU
    @RequestMapping(path = "/data/dau", method = RequestMethod.POST)
    public String getDAU(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
    @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) {
    long dau = dataService.calculateDAU(start, end);
    model.addAttribute("dauResult", dau);
    model.addAttribute("dauStartDate", start);
    model.addAttribute("dauEndDate", end);
    return "forward:/data";
    }

    }

权限控制

  • SecurityConfig: 修改,"/data/**"
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    // 授权
    http.authorizeRequests()
    .antMatchers(
    "/discuss/delete",
    "/data/**"
    )
    .hasAnyAuthority(
    AUTHORITY_ADMIN
    )
    .anyRequest().permitAll()
    .and().csrf().disable();

    // 权限不够时的处理(略)
    }

页面

  • data.html: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <!-- 内容 -->
    <!-- 网站UV -->
    <form class="form-inline mt-3" method="post" th:action="@{/data/uv}">
    <input type="date" class="form-control" required name="start"
    th:value="${#dates.format(uvStartDate, 'yyyy-MM-dd')}"/>
    <input type="date" class="form-control ml-3" required name="end"
    th:value="${#dates.format(uvEndDate, 'yyyy-MM-dd')}"/>
    <button type="submit" class="btn btn-primary ml-3">开始统计</button>
    </form>
    <ul class="list-group mt-3 mb-3">
    <li class="list-group-item d-flex justify-content-between align-items-center">
    统计结果
    <span class="badge badge-primary badge-danger font-size-14" th:text="${uvResult}">0</span>
    </li>
    </ul>
    <!-- 活跃用户DAU -->
    <form class="form-inline mt-3" method="post" th:action="@{/data/dau}">
    <input type="date" class="form-control" required name="start"
    th:value="${#dates.format(dauStartDate, 'yyyy-MM-dd')}"/>
    <input type="date" class="form-control ml-3" required name="end"
    th:value="${#dates.format(dauEndDate, 'yyyy-MM-dd')}"/>
    <button type="submit" class="btn btn-primary ml-3">开始统计</button>
    </form>
    <ul class="list-group mt-3 mb-3">
    <li class="list-group-item d-flex justify-content-between align-items-center">
    统计结果
    <span class="badge badge-primary badge-danger font-size-14" th:text="${dauResult}">0</span>
    </li>
    </ul>

结果展示

网站数据统计结果-图示


问答平台(7),网站数据统计
https://lcf163.github.io/2020/06/15/问答平台(7),网站数据统计/
作者
乘风的小站
发布于
2020年6月15日
许可协议