目录
UV关注的是访问量,不关注有没有登录,只要访问过就都算,基于IP地址排重统计。DAU,基于用户ID排重统计(更关注用户的有效性),要求结果必须精确。
规划RedisKeyUtil
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;
}
DataService
对于HyperLogLog操作的是字符串
但是对于BitMap操作的是字节数组,利用or运算,而且需要使用到RedisConnection实现操作!
@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());
}
});
}
}
表现层
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();
dataService.recordUV(ip);
//统计DAU
User user = hostHolder.getUser();
if(user != null){
dataService.recordDAU(user.getId());
}
return true;
}
}
配置拦截器
registry.addInterceptor(dataInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.jpg", "/**/*.jpeg", "/**/*.png");
2. 查看值
DataController
import com.newcoder.community.service.DataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.Date;
@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";//表示我处理到一半,然后接着交给上面的方法进行处理,所以上面的方法要接收两种类型的请求方法方式
//请求在转发的过程中类型保持不变。
}
//统计活跃用户
@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>
</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>
配置权限:只有管理员能使用
.antMatchers(
"/discuss/delete",
"/data/**"
)
.hasAnyAuthority(
AUTHORITY_ADMIN
)
版权声明:本文为weixin_37841366原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。