我正在参加「掘金·启航计划」
网站数据统计
因为用到了redis做新的需求,所以打开 RedisKeyUtil,定义key
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;
}
使用redis的话命令式操作Redis的,比较简单,省去数据访问层,直接在业务层写即可
业务层(service)
新建一个 DataService
@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<>(); // 合并的话先搜集到一组key
Calendar calendar = Calendar.getInstance();
calendar.setTime(start); // 设置日期类为开始日期
while (!calendar.getTime().after(end)) { // 时间不晚于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()); // 将搜集到的key中数据合并
// 返回统计的结果
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,
redisKey.getBytes(), keyList.toArray(new byte[0][0])); //那一组key[]转成二维的byte数组new byte[0][0]
return connection.bitCount(redisKey.getBytes());
}
});
}
}
表现层
表现层的逻辑分成两部分,1. 什么时候去记录这个值 2. 查看这个值
- 记录值
记录这个值我们每次请求都得记,因为每次请求都有可能是一个新的访问,很显然我们在拦截器里写比较合适
新建一个拦截器: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(); // 得到ip
dataService.recordUV(ip); // 不管登不登录都统计UV
// 统计DAU
User user = hostHolder.getUser();
if (user != null) { // 登录了才统计DAU
dataService.recordDAU(user.getId());
}
return true;
}
}
然后配置拦截器:
@Autowired
private DataInterceptor dataInterceptor;
registry.addInterceptor(dataInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
//静态资源不拦截
- 展现数据
新建一个 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) // POST可以接收其他controller的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"; // 这个相当于转到上面那个路径为data的controller,然后那个controller跳转到页面
} // 直接写上面那个路径也可以
// 统计活跃用户
@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的controller,然后那个controller跳转到页面
} // 直接写上面那个路径也可以
}
最后就是处理模板:data.html
最后配置一下关于网站统计的功能只有管理员可以访问:
测试:
这个功能因为普通用户、版主是没有权限的,只有管理员是有权限的,所以就没有设置按钮,管理员如果想用的话,访问 /data 路径就可以跳转使用