7.5 网站数据统计

101 阅读3分钟

我正在参加「掘金·启航计划」

网站数据统计

image-20220729103312080

因为用到了redis做新的需求,所以打开 RedisKeyUtil,定义key

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;
} 

image-20220729143121557

image-20220729143147107

使用redis的话命令式操作Redis的,比较简单,省去数据访问层,直接在业务层写即可

业务层(service)

新建一个 DataService

@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<>();       // 合并的话先搜集到一组key
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(start);                        // 设置日期类为开始日期
        while (!calendar.getTime().after(end)) {        // 时间不晚于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());     // 将搜集到的key中数据合并

        // 返回统计的结果
        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 connection) throws DataAccessException {
                String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end));
                connection.bitOp(RedisStringCommands.BitOperation.OR,
                        redisKey.getBytes(), keyList.toArray(new byte[0][0])); //那一组key[]转成二维的byte数组new byte[0][0]
                return connection.bitCount(redisKey.getBytes());
            }
        });
    }

}

image-20220729143240810

image-20220729143314908

表现层

表现层的逻辑分成两部分,1. 什么时候去记录这个值 2. 查看这个值

  1. 记录值

记录这个值我们每次请求都得记,因为每次请求都有可能是一个新的访问,很显然我们在拦截器里写比较合适

新建一个拦截器:DataInterceptor

@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();        // 得到ip
        dataService.recordUV(ip);                   // 不管登不登录都统计UV

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

        return true;
    }
}

image-20220729143545002

然后配置拦截器:

@Autowired
private DataInterceptor dataInterceptor;


registry.addInterceptor(dataInterceptor)
  .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
//静态资源不拦截

image-20220729143652434

  1. 展现数据

新建一个 DataController

@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) // POST可以接收其他controller的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";      // 这个相当于转到上面那个路径为data的controller,然后那个controller跳转到页面
    }                                // 直接写上面那个路径也可以

    // 统计活跃用户
    @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";      // 这个相当于转到上面那个路径为data的controller,然后那个controller跳转到页面
    }                                // 直接写上面那个路径也可以

}

image-20220729143809799

最后就是处理模板:data.html

image-20220729143921632

image-20220729144001047

image-20220729144134318

image-20220729144241843

最后配置一下关于网站统计的功能只有管理员可以访问:

image-20220729144411164

测试:

这个功能因为普通用户、版主是没有权限的,只有管理员是有权限的,所以就没有设置按钮,管理员如果想用的话,访问 /data 路径就可以跳转使用

image-20220729142609649