通过Cookie和Redis实现网站新老用户访问量、平均浏览时长、跳出率等数据的统计

在cms中,站点的新老用户访问量、平均浏览时长、跳出率等数据统计是很常用的功能,在此仅记一次实现此功能的代码实操及部分代码和思路 ,已将核心数据统计功能封装成工具类

public class OperRedisUtil {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IStatisticsOperLogService statisticsOperLogService;

    public StatisticsOperNum getData(Long siteId) {
        StatisticsOperNum statisticsOperNum = StatisticsOperNum.builder().build();
        statisticsOperNum.setSiteId(siteId);
        statisticsOperNum.setNewUserNum((long) (Boolean.TRUE.equals(stringRedisTemplate.hasKey("newUserNumOfSite" + siteId)) ? Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.boundValueOps("newUserNumOfSite" + siteId).get())) : 0));
        statisticsOperNum.setOldUserNum((long) (Boolean.TRUE.equals(stringRedisTemplate.hasKey("oldUserNumOfSite" + siteId)) ? Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.boundValueOps("oldUserNumOfSite" + siteId).get())) : 0));
        statisticsOperNum.setNewUserViews((long) (Boolean.TRUE.equals(stringRedisTemplate.hasKey("newUserViewOfSite" + siteId)) ? Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.boundValueOps("newUserViewOfSite" + siteId).get())) : 0));
        statisticsOperNum.setOldUserViews((long) (Boolean.TRUE.equals(stringRedisTemplate.hasKey("oldUserViewOfSite" + siteId)) ? Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.boundValueOps("oldUserViewOfSite" + siteId).get())) : 0));
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey("newUserViewOfSite" + siteId)) && Boolean.TRUE.equals(stringRedisTemplate.hasKey("newUserDurationOfSite" + siteId))) {
            SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
            //这里很重要,如果不设置时区的话,输出结果就会是几点钟,而不是毫秒值对应的时分秒数量了。
            formatter.setTimeZone(TimeZone.getTimeZone("GMT+00:00"));
            //计算平均访问时长(ms),并转换为时分秒格式
            Integer newUserView = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("newUserViewOfSite" + siteId)));
            Integer newUserDuration = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("newUserDurationOfSite" + siteId)));
            statisticsOperNum.setNewUserAverageBrowsingTime(formatter.format(newUserDuration / newUserView));
        } else {
            statisticsOperNum.setNewUserAverageBrowsingTime("00:00:00");
        }
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey("oldUserViewOfSite" + siteId)) && Boolean.TRUE.equals(stringRedisTemplate.hasKey("oldUserDurationOfSite" + siteId))) {
            SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
            //这里很重要,如果不设置时区的话,输出结果就会是几点钟,而不是毫秒值对应的时分秒数量了。
            formatter.setTimeZone(TimeZone.getTimeZone("GMT+00:00"));
            //计算平均访问时长(ms),并转换为时分秒格式
            Integer oldUserView = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("oldUserViewOfSite" + siteId)));
            Integer oldUserDuration = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("oldUserDurationOfSite" + siteId)));
            statisticsOperNum.setOldUserAverageBrowsingTime(formatter.format(oldUserDuration / oldUserView));
        } else {
            statisticsOperNum.setOldUserAverageBrowsingTime("00:00:00");
        }
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey("newUserOperationOfSite" + siteId)) && Boolean.TRUE.equals(stringRedisTemplate.hasKey("newUserViewOfSite" + siteId))) {
            Map<Object, Object> newUserOperation = stringRedisTemplate.boundHashOps("newUserOperationOfSite" + siteId).entries();
            assert newUserOperation != null;
            //遍历map,查找有几个访问次数是1
            Double newUserOnceView = 0.0;
            for (Map.Entry<Object, Object> entry : newUserOperation.entrySet()) {
                if (entry.getValue().equals("1")) {
                    newUserOnceView++;
                }
            }
            NumberFormat percentInstance = NumberFormat.getPercentInstance();
            // 设置保留几位小数,这里设置的是保留两位小数
            percentInstance.setMinimumFractionDigits(2);
            statisticsOperNum.setNewUserBounceRate(percentInstance.format(newUserOnceView / Double.parseDouble(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("newUserViewOfSite" + siteId)))));
        } else {
            statisticsOperNum.setNewUserBounceRate("0.00%");
        }
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey("oldUserOperationOfSite" + siteId)) && Boolean.TRUE.equals(stringRedisTemplate.hasKey("oldUserViewOfSite" + siteId))) {
            Map<Object, Object> oldUserOperation = stringRedisTemplate.boundHashOps("oldUserOperationOfSite" + siteId).entries();
            assert oldUserOperation != null;
            //遍历map,查找有几个访问次数是1
            Double oldUserOnceView = 0.0;
            for (Map.Entry<Object, Object> entry : oldUserOperation.entrySet()) {
                if (entry.getValue().equals("1")) {
                    oldUserOnceView++;
                }
            }
            NumberFormat percentInstance = NumberFormat.getPercentInstance();
            // 设置保留几位小数,这里设置的是保留两位小数
            percentInstance.setMinimumFractionDigits(2);
            statisticsOperNum.setOldUserBounceRate(percentInstance.format(oldUserOnceView / Double.parseDouble(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("oldUserViewOfSite" + siteId)))));
        } else {
            statisticsOperNum.setOldUserBounceRate("0.00%");
        }
        return statisticsOperNum;
    }

    public String setData(HttpServletRequest request, HttpServletResponse response, Long siteId) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        LocalDate localDate = LocalDate.now();
        //获取cookie
        Cookie[] cookies = request.getCookies();
        String uuid = "";
        boolean cookieHaveUuid = false;
        //首先判断cookie中是否包含uuid
        if (cookies != null && cookies.length != 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("uuidOfSite" + siteId)) {
                    cookieHaveUuid = true;
                    uuid = cookie.getValue();
                }
            }
        }
        //如果没有uuid--新用户访问
        if (!cookieHaveUuid) {
            uuid = dtf.format(localDate) + "/" + UUID.randomUUID();
            Cookie cookie = new Cookie("uuidOfSite" + siteId, uuid);
            cookie.setMaxAge(60 * 60 * 24 * 365);//设置cookie有效期,不设置有效期的话默认有效期为本次会话
            cookie.setPath("/");//设置cookie保存路径
            response.addCookie(cookie);
            //在redis中今日新用户数量加一
            stringRedisTemplate.boundValueOps("newUserNumOfSite" + siteId).increment(1);
        }
        //判断uuid是否为今日生成的,如果是则为新用户访问,进行新用户关联数据更新,否则进行老用户关联数据更新
        if (uuid.split("/")[0].equals(dtf.format(localDate))) {
            //更新新用户访问map,将新用户的uuid和访问次数更新,后续计算跳出率用
            stringRedisTemplate.opsForHash().increment("newUserOperationOfSite" + siteId, uuid, 1);
            //新用户浏览量+1
            stringRedisTemplate.boundValueOps("newUserViewOfSite" + siteId).increment(1);
            //redis中更新新用户访问总时长
            QueryWrapper<StatisticsOperLog> statisticsOperLogQueryWrapper = new QueryWrapper<>();
            statisticsOperLogQueryWrapper.eq("cookie_uuid", uuid);
            statisticsOperLogQueryWrapper.eq("site_id", siteId);
            statisticsOperLogQueryWrapper.last("order by create_time desc limit 0,1");
            StatisticsOperLog statisticsOperLog1 = statisticsOperLogService.getOne(statisticsOperLogQueryWrapper);
            if (statisticsOperLog1 != null && Duration.between(statisticsOperLog1.getCreateTime(), LocalDateTime.now()).toMillis() < 1000 * 60 * 30) {
                stringRedisTemplate.boundValueOps("newUserDurationOfSite" + siteId).increment(Duration.between(statisticsOperLog1.getCreateTime(), LocalDateTime.now()).toMillis());
            }
        } else {
            //判断老用户是否今日第一次访问,是的话计入今日老用户数量+1否则不+1
            if (!stringRedisTemplate.opsForHash().hasKey("oldUserOperationOfSite" + siteId, uuid)) {
                stringRedisTemplate.boundValueOps("oldUserNumOfSite" + siteId).increment(1);
            }
            //更新老用户访问map,将老用户的uuid和访问次数更新,后续计算跳出率用
            stringRedisTemplate.opsForHash().increment("oldUserOperationOfSite" + siteId, uuid, 1);
            //老用户浏览量+1
            stringRedisTemplate.boundValueOps("oldUserViewOfSite" + siteId).increment(1);
            //redis中更新新用户访问总时长
            QueryWrapper<StatisticsOperLog> statisticsOperLogQueryWrapper1 = new QueryWrapper<>();
            statisticsOperLogQueryWrapper1.eq("cookie_uuid", uuid);
            statisticsOperLogQueryWrapper1.eq("site_id", siteId);
            statisticsOperLogQueryWrapper1.last("order by create_time desc limit 0,1");
            StatisticsOperLog statisticsOperLog2 = statisticsOperLogService.getOne(statisticsOperLogQueryWrapper1);
            if (statisticsOperLog2 != null && Duration.between(statisticsOperLog2.getCreateTime(), LocalDateTime.now()).toMillis() < 1000 * 60 * 30) {
                stringRedisTemplate.boundValueOps("oldUserDurationOfSite" + siteId).increment(Duration.between(statisticsOperLog2.getCreateTime(), LocalDateTime.now()).toMillis());
            }
        }
        return uuid;
    }

    public void removeData(Long siteId) {
        stringRedisTemplate.delete("newUserNumOfSite" + siteId);
        stringRedisTemplate.delete("oldUserNumOfSite" + siteId);
        stringRedisTemplate.delete("newUserViewOfSite" + siteId);
        stringRedisTemplate.delete("oldUserViewOfSite" + siteId);
        stringRedisTemplate.delete("newUserDurationOfSite" + siteId);
        stringRedisTemplate.delete("oldUserDurationOfSite" + siteId);
        stringRedisTemplate.delete("newUserOperationOfSite" + siteId);
        stringRedisTemplate.delete("oldUserOperationOfSite" + siteId);

    }

}

具体思路如下:

首先用户访问网站都是无状态的,总不能要求用户去登录去保存状态,所以将标识保存到cookie中,有效期是一年。

用户访问网站的时候都会有一个统一的接口去监控获取用户的ip和站点的id、业务类型、参数、访问时间,所以在用户访问的时候对cookie进行操作,加一个字段uuid后期获取用户上一次访问时间用

在Redis中为每个站点维护八个数据:以站点X为例

  • 新用户数(string):newUserNumOfSite+siteid|数量

    • 怎么统计:用户访问本站点如果cookie中没有本站点相应的行则说明是新用户,自动生成一个key为userInfoOfSiteX,value为yyyy-MM-dd+uuid的行保存到cookie中,并为新用户数+1

  • 老用户数(string):oldUserNumOfSite+siteid|数量

    • 怎么统计:用户访问的时候读取cookie,如果value中的日期不是今天则说明是老用户,然后去看这个老用户是不是第一次访问,是的话老用户数+1,不是的话不加

  • 新用户访问量(string):newUserViewOfSite+siteid|数量

    • 读取日期,如果是今天生成的uuid访问量+1

  • 老用户访问量(string):oldUserViewOfSite+siteid|数量

    • 读取日期,如果不是今天生成的uuid访问量+1

  • 新用户总访问时长(string):newUserDurationOfSite+siteid|总时长

    • 在新用户访问过程中先去数据库查询上次访问时间,计算时间差,如果大于半小时则舍弃本次计算时长

  • 老用户总访问时长(string):oldUserDurationOfSite+siteid|总时长

    • 在老用户访问过程中去数据库查询上次访问时间,计算时间差,如果大于半小时则舍弃本次计算时长

  • 本站点每个新用户的访问次数(hash):newUserOperationOfSite+siteid|(用户uuid|访问次数)

    • 用户访问的时候将对应uuid的值+1

  • 本站点每个老用户的访问次数(hash):oldUserOperationOfSite+siteid|(用户uuid|访问次数)

    • 老用户访问的时候将对应的uuid值+1 

管理员进行数据查看时,新用户数就是新增用户数量,老用户数就是本日老用户数,新用户浏览量就是本日新用户对站点的访问量,老用户同理,跳出率计算为只访问一次网站的用户数量比上总用户数量,平均浏览时长为总时长除以用户数量,这样就实现了PV、UV的统计。 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodeXu_cyber

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值