# 网站数据统计
---
使用 Redis 高级数据类型:

DAU 只统计登录用户,UV 兼并访客和登录用户
DAU:将用户 ID 作为 bitmap 的 key 存入,若该用户当天访问过,则置 value 为 1。一天用一个 bitmap
计算区间内的 DAU 的时候, 使用 or 运算,就是说这么多天只要这个用户登陆过一次,就算活跃用户
RedisKeyUtil:
```java
/**
* 单日 UV
* @param date
* @return
*/
public static String getUVKey(String date) {
return PREFIX_UV + SPLIT + date;
}
/**
* 区间 UV
* @param startDate
* @param endDate
* @return
*/
public static String getUVKey(String startDate, String endDate) {
return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;
}
/**
* 单日 DAU
* @param date
* @return
*/
public static String getDAUKey(String date) {
return PREFIX_DAU + SPLIT + date;
}
/**
* 区间 DAU
* @param startDate
* @param endDate
* @return
*/
public static String getDAUKey(String startDate, String endDate) {
return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate;
}
```
## Service
```java
/**
* 网站数据统计(UV / DAU)
*/
@Service
public class DataService {
@Autowired
private RedisTemplate redisTemplate;
private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
/**
* 将指定的 IP 计入当天的 UV
* @param ip
*/
public void recordUV(String ip) {
String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
redisTemplate.opsForHyperLogLog().add(redisKey, ip);
}
/**
* 统计指定日期范围内的 UV
* @param start
* @param end
* @return
*/
public long calculateUV(Date start, Date end) {
if (start == null || end == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 整理该日期范围内的 key
List 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); // 加1天
}
// 合并这些天的 UV
String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));
redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());
// 返回统计结果
return redisTemplate.opsForHyperLogLog().size(redisKey);
}
/**
* 将指定的 IP 计入当天的 DAU
* @param userId
*/
public void recordDAU(int userId) {
String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));
redisTemplate.opsForValue().setBit(redisKey, userId, true);
}
/**
* 统计指定日期范围内的 DAU
* @param start
* @param end
* @return
*/
public long calculateDAU(Date start, Date end) {
if (start == null || end == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 整理该日期范围内的 key
List 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); // 加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());
}
});
}
}
```
## 拦截器
在所有请求执行之前查询 uv 和 dau
```java
@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;
}
}
```
别忘记在配置中添加此拦截器

## Controller
```java
/**
* 网站数据
*/
@Controller
public class DataController {
@Autowired
private DataService dataService;
/**
* 进入统计界面
* @return
*/
@RequestMapping(value = "/data", method = {RequestMethod.GET, RequestMethod.POST})
public String getDataPage() {
return "/site/admin/data";
}
/**
* 统计网站 uv
* @param start
* @param end
* @return
*/
@PostMapping("/data/uv")
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
* @param start
* @param end
* @return
*/
@PostMapping("/data/dau")
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
```html
统计结果
```