Echo/docs/280-网站数据统计.md

271 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 网站数据统计
---
使用 Redis 高级数据类型:
<img src="https://gitee.com/veal98/images/raw/master/img/20210131104903.png" style="zoom: 33%;" />
![](https://gitee.com/veal98/images/raw/master/img/20210131112124.png)
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<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); // 加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<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); // 加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;
}
}
```
别忘记在配置中添加此拦截器
![](https://gitee.com/veal98/images/raw/master/img/20210131130906.png)
## 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";
}
}
```
![](https://gitee.com/veal98/images/raw/master/img/20210131121136.png)
别忘记给管理员赋予权限:
![](https://gitee.com/veal98/images/raw/master/img/20210131130739.png)
## 前端
data.html
```html
<form method="post" th:action="@{/data/dau}">
<input type="date"name="start"
th:value="${#dates.format(dauStartDate, 'yyyy-MM-dd')}" />
<input type="date" required name="end"
th:value="${#dates.format(dauEndDate, 'yyyy-MM-dd')}" />
<button type="submit">开始统计</button>
</form>
统计结果 <span th:text="${dauResult}">
```