需要统计的事项:
UV每次访问都要把用户的ip(不管该用户有无登录)存到某个数据结构里,方便进行统计
DAU只统计登录的用户(根据用户id),对于那些没有登录的游客不进行统计。1表示访问过,为活跃用户;0表示没有访问过,为不活跃用户;
DAU更关注用户用户有效性
1.在RedisKeyUtil中
private static final String PREFIX_UV = "uv";//统计uv相关数据的前缀
private static final String PREFIX_DAU = "dau";//统计dau相关数据的前缀
// 单日UV,记录UV是以天为单位的
public static String getUVKey(String date) {
return PREFIX_UV + SPLIT + date;
}
// 区间UV,查看一周的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;
}
2.开发完key后,开发数据访问层,因为redis数据访问层可以直接写services
@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()));//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
calendar.setTime(start);
while (!calendar.getTime().after(end)) {//calendar.getTime()表明时间,after(end)表示 如果晚于end,有感叹号 表示 时间如果不晚于结束,则循环
String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));//每次遍历得到key
keyList.add(key);
calendar.add(Calendar.DATE, 1);//加一天
}
// 合并这些数据
String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));//redisKey是合并以后数据的key
redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());//keyList.toArray()表示把keyList 转换为数组
// 返回统计的结果
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,//第一个参数是RedisStringCommands.BitOperation.OR 即进行什么样的运算
redisKey.getBytes(), keyList.toArray(new byte[0][0]));//keyList.toArray()表示把keyList 转换为数组
return connection.bitCount(redisKey.getBytes());
}
});
}
}
3.在表现层中,新建拦截器
@Component
public class DataInterceptor implements HandlerInterceptor {
@Autowired
private DataService dataService;
@Autowired
private HostHolder hostHolder;//得到当前用户id
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//preHandle表示在请求之初统计,统计完后继续向下执行
// 统计UV
String ip = request.getRemoteHost();
dataService.recordUV(ip);
// 统计DAU
User user = hostHolder.getUser();
if (user != null) {//user不为空,才进行统计
dataService.recordDAU(user.getId());
}
return true;
}
}
之后在WebMvcConfig中,
@Autowired
private DataInterceptor dataInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(dataInterceptor)//对于静态资源无需拦截
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
3.新建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)
public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,//@DateTimeFormat(pattern = "yyyy-MM-dd") 来告诉服务器的日期格式。传入三个参数start,end,model
@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";
}
// 统计活跃用户
@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.html中,
<!-- 内容 -->
<div class="main">
<!-- 网站UV -->
<div class="container pl-5 pr-5 pt-3 pb-3 mt-3">
<h6 class="mt-3"><b class="square"></b> 网站 UV</h6>
<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><!-- 统计结果th:text="${uvResult}"-->
</li>
</ul>
</div>
<!-- 活跃用户 -->
<div class="container pl-5 pr-5 pt-3 pb-3 mt-4">
<h6 class="mt-3"><b class="square"></b> 活跃用户</h6>
<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>
</div>
</div>
测试:
只有管理员才能访问data
管理员登录后,直接敲这个路径访问data
UA只有一个,因为我们一直用自己的电脑
刚才我们用普通用户,版主,管理员三个账号进行了操作