问答平台(7),网站数据统计
UV
- 独立访客(Unique Visitor),需通过用户IP去重统计数据。
- 每次访问都要进行统计。
- HyperLogLog,性能好,且存储空间小。
DAU
- 日活跃用户(Daliy Active User),需通过用户ID去重统计数据。
- 网站定义的规则:访问过一次,则认为其活跃。
- Bitmap,性能好,且可以统计精确结果。
工具类
- RedisKeyUtil: 增加内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private 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;
}
业务层
- DataService: 新增
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73@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<>();
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);
}
// 合并这些数据
String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));
redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());
// 返回统计的结果
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 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());
}
});
}
}
拦截器
- DataInterceptor: 新增
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24@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;
}
}
config
- WebMvcConfig: 增加内容
1
2
3
4
5
6
7
8
9
10@Autowired
private DataInterceptor dataInterceptor;
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//将拦截器加入registry对象
registry.addInterceptor(dataInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
表现层
- DataController: 新增
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35@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") 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
@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";
}
}
权限控制
- SecurityConfig: 修改,
"/data/**"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@Override
protected void configure(HttpSecurity http) throws Exception {
// 授权
http.authorizeRequests()
.antMatchers(
"/discuss/delete",
"/data/**"
)
.hasAnyAuthority(
AUTHORITY_ADMIN
)
.anyRequest().permitAll()
.and().csrf().disable();
// 权限不够时的处理(略)
}
页面
- data.html: 修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<!-- 内容 -->
<!-- 网站UV -->
<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>
</li>
</ul>
<!-- 活跃用户DAU -->
<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>
结果展示
问答平台(7),网站数据统计
https://lcf163.github.io/2020/06/15/问答平台(7),网站数据统计/