SaaS短链接系统-新手从零学习 9. 短链接监控(下

eve2333 发布于 16 天前 38 次阅读


- 第13节:如何开发访问单个短链接监控统计功能

package com.nageoffer.shortlink.project.controller;

import com.nageoffer.shortlink.project.common.convention.result.Result;
import com.nageoffer.shortlink.project.common.convention.result.Results;
import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsRespDTO;
import com.nageoffer.shortlink.project.service.ShortLinkStatsService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 短链接监控控制层
 */
@RestController
@RequiredArgsConstructor
public class ShortLinkStatsController {

    private final ShortLinkStatsService shortLinkStatsService;

    /**
     * 访问单个短链接指定时间内监控数据
     */
    @GetMapping("/api/short-link/v1/stats")
    public Result<ShortLinkStatsRespDTO> shortLinkStats(ShortLinkStatsReqDTO requestParam) {
        return Results.success(shortLinkStatsService.oneShortLinkStats(requestParam));
    }
}

服务器做出一些修改或者说新增才会post和put; 都用的是get方法, 一共四个参数

package com.nageoffer.shortlink.project.dto.req;

import lombok.Data;

/**
 * 短链接监控请求参数
 */
@Data
public class ShortLinkStatsReqDTO {

    /**
     * 完整短链接
     */
    private String fullShortUrl;

    /**
     * 分组标识
     */
    private String gid;

    /**
     * 开始日期
     */
    private String startDate;

    /**
     * 结束日期
     */
    private String endDate;
}

 从200多行的impl:首先第一步对吧?我们要先去判断一下他有没有访问记录,如果没有的话,这里面其实返回的就是我们这里面创建一个我想一想那就拿这个。然后创建我去你看如果说短链接没有访问过,他还是把这些数据这些给你看,他感受的有点多,其实正常的话我们直接展示个财务记录就行了,所以说如果没有访问记录,我们这里面直接有听到,那前端也会根据 now去判断是否展示展台查验。然后的话我们开始做技术访问,我给大家打开一个有的技术访问的话就指的是指的是它的对应日期的一个访问次数人数以及IP数,我们可以看到这个是它对应的反馈记录对吧?日期,然后访问量,然后ppuv以及uip,这个是比较好做的。然后假如说我们访问的是10月19号到11月18号,我们可以看到哪怕中间这些数据它没有记录返回了零的,这样的话其实在我们这边的话做了一步处理,为什么?是因为我们存到数据库里面,其实查的是查的是这张表t_link_access_stats。如果说你在对应没有对应时间没有去访问的话,它这里面是没有记录的,自然这里面也没有这对应访问记录了,所以说我们是怎么做的,我们是根据它的创建时间以及它的开始时间和结束时间去给它做了一个取范围的参数,什么意思?比如说你是10月19~11月18,我会把你这每一天的数据都给你创建出来一条记录,然后我们通过for each创建出来的时间字段进行for each去判断,和他查出来的记录进行一个比对,然后如果有值的话就赋值给对应的值,如果没值全部就是0,比较容易理解。

然后第二个的话就是地区,之前做地区的时候跟大家讲过,我们只做国内的一个地图访问,然后它这里面是Top5,应该是所有的地区都返回了,因为我们这里面没家里面的,所有的地区我们都返回了,我们是所有地区都return了,然后给它因为它有个啥它有一个百分比的参数,所以说我们这里面给它进行了这种进行除法运算,然后也给它取了一个余,就避免前端去做了。
然后再接下来是小时访问,就是在你所用的时间段内,它的24小时分布所占用的一个时间,然后基本上差不多,把它的小时数据统计出来,然后给它转换成我们对什么转换成我们对应的数值,然后新增进去其实一共你看一共24个小时对吧?0~23,这样的话其实它就是一个数组返回了0~23;
然后这边就是高频访问IP,然后我们这边的话可以看一下。就是这样的一个sql,我们这个是做了高频访问了,就是返回了前5条,然后这边对应返回两个参数,一个是IP,一个是count,然后一周详情的话一样的逻辑,然后访问浏览器记录,因为浏览器这边的话应该也是一个百分比的,它有一个浏览器是有一个百分比的,所以说我们这边一样进行了一个百分比的除法运算,然后操作系统这里的话有一点不太一样的是什么?它这里面其实就只需要有两个参数而已,对吧?所以说我们这里面给它把这个数据给它统计出来,然后做了一个类似于项目的这种操作,然后给它对应了一样的一个处罚运算。然后访客类型的话,这里可能就有一点稍微的有一点复杂,依赖我们之前的什么?这里依赖于我们的访客类型,所以说这里面的计算会稍微复杂一点,然后我们这里面一样的把 new都返回出去。可以看到我们这边返回的一个标识是UV type,一个new user,一个欧拉的user,然后都给它加到了 UV type state集合里面去。然后接下来就是访问设备和访问网络其实都是一样的,最终我们用构造者模式把这些数据全部给它构造成一个参数,也就是我们短链接监控的一个详细参数,可以看到这里面啥都有,基本上把那些参数都包括了,我给大家联调一下,因为太复杂了,确实不忍心耽误大家时间,我就自己写了,后续大家可以看着代码一起debug一下,比较容易理解

package com.nageoffer.shortlink.project.service.impl;


import cn.hutool.core.bean.BeanUtil;
import com.nageoffer.shortlink.project.dao.entiry.LinkAccessStatsDO;
import com.nageoffer.shortlink.project.dao.entiry.LinkDeviceStatsDO;
import com.nageoffer.shortlink.project.dao.entiry.LinkLocaleStatsDO;
import com.nageoffer.shortlink.project.dao.entiry.LinkNetworkStatsDO;
import com.nageoffer.shortlink.project.dao.mapper.LinkAccessLogsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkAccessStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkBrowserStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkDeviceStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkLocaleStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkNetworkStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkOsStatsMapper;
import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsAccessDailyRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsBrowserRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsDeviceRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsLocaleCNRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsNetworkRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsOsRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsTopIpRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsUvRespDTO;
import com.nageoffer.shortlink.project.service.ShortLinkStatsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 短链接监控接口实现层
 */
@Service
@RequiredArgsConstructor
public class ShortLinkStatsServiceImpl implements ShortLinkStatsService {

    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;

    @Override
    public ShortLinkStatsRespDTO oneShortLinkStats(ShortLinkStatsReqDTO requestParam) {
        // 基础访问详情
        List<LinkAccessStatsDO> listStatsByShortLink = linkAccessStatsMapper.listStatsByShortLink(requestParam);
        // 地区访问详情(仅国内)
        List<ShortLinkStatsLocaleCNRespDTO> localeCnStats = new ArrayList<>();
        List<LinkLocaleStatsDO> listedLocaleByShortLink = linkLocaleStatsMapper.listLocaleByShortLink(requestParam);
        int localeCnSum = listedLocaleByShortLink.stream()
                .mapToInt(LinkLocaleStatsDO::getCnt)
                .sum();
        listedLocaleByShortLink.forEach(each -> {
            double ratio = (double) each.getCnt() / localeCnSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsLocaleCNRespDTO localeCNRespDTO = ShortLinkStatsLocaleCNRespDTO.builder()
                    .cnt(each.getCnt())
                    .locale(each.getProvince())
                    .ratio(actualRatio)
                    .build();
            localeCnStats.add(localeCNRespDTO);
        });
        // 小时访问详情
        List<Integer> hourStats = new ArrayList<>();
        List<LinkAccessStatsDO> listHourStatsByShortLink = linkAccessStatsMapper.listHourStatsByShortLink(requestParam);
        for (int i = 0; i < 24; i++) {
            AtomicInteger hour = new AtomicInteger(i);
            int hourCnt = listHourStatsByShortLink.stream()
                    .filter(each -> Objects.equals(each.getHour(), hour.get()))
                    .findFirst()
                    .map(LinkAccessStatsDO::getPv)
                    .orElse(0);
            hourStats.add(hourCnt);
        }
        // 高频访问IP详情
        List<ShortLinkStatsTopIpRespDTO> topIpStats = new ArrayList<>();
        List<HashMap<String, Object>> listTopIpByShortLink = linkAccessLogsMapper.listTopIpByShortLink(requestParam);
        listTopIpByShortLink.forEach(each -> {
            ShortLinkStatsTopIpRespDTO statsTopIpRespDTO = ShortLinkStatsTopIpRespDTO.builder()
                    .ip(each.get("ip").toString())
                    .cnt(Integer.parseInt(each.get("count").toString()))
                    .build();
            topIpStats.add(statsTopIpRespDTO);
        });
        // 一周访问详情
        List<Integer> weekdayStats = new ArrayList<>();
        List<LinkAccessStatsDO> listWeekdayStatsByShortLink = linkAccessStatsMapper.listWeekdayStatsByShortLink(requestParam);
        for (int i = 1; i < 8; i++) {
            AtomicInteger weekday = new AtomicInteger(i);
            int weekdayCnt = listWeekdayStatsByShortLink.stream()
                    .filter(each -> Objects.equals(each.getWeekday(), weekday.get()))
                    .findFirst()
                    .map(LinkAccessStatsDO::getPv)
                    .orElse(0);
            weekdayStats.add(weekdayCnt);
        }
        // 浏览器访问详情
        List<ShortLinkStatsBrowserRespDTO> browserStats = new ArrayList<>();
        List<HashMap<String, Object>> listBrowserStatsByShortLink = linkBrowserStatsMapper.listBrowserStatsByShortLink(requestParam);
        int browserSum = listBrowserStatsByShortLink.stream()
                .mapToInt(each -> Integer.parseInt(each.get("count").toString()))
                .sum();
        listBrowserStatsByShortLink.forEach(each -> {
            double ratio = (double) Integer.parseInt(each.get("count").toString()) / browserSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsBrowserRespDTO browserRespDTO = ShortLinkStatsBrowserRespDTO.builder()
                    .cnt(Integer.parseInt(each.get("count").toString()))
                    .browser(each.get("browser").toString())
                    .ratio(actualRatio)
                    .build();
            browserStats.add(browserRespDTO);
        });
        // 操作系统访问详情
        List<ShortLinkStatsOsRespDTO> osStats = new ArrayList<>();
        List<HashMap<String, Object>> listOsStatsByShortLink = linkOsStatsMapper.listOsStatsByShortLink(requestParam);
        int osSum = listOsStatsByShortLink.stream()
                .mapToInt(each -> Integer.parseInt(each.get("count").toString()))
                .sum();
        listOsStatsByShortLink.forEach(each -> {
            double ratio = (double) Integer.parseInt(each.get("count").toString()) / osSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsOsRespDTO osRespDTO = ShortLinkStatsOsRespDTO.builder()
                    .cnt(Integer.parseInt(each.get("count").toString()))
                    .os(each.get("os").toString())
                    .ratio(actualRatio)
                    .build();
            osStats.add(osRespDTO);
        });
        // 访客访问类型详情
        List<ShortLinkStatsUvRespDTO> uvTypeStats = new ArrayList<>();
        HashMap<String, Object> findUvTypeByShortLink = linkAccessLogsMapper.findUvTypeCntByShortLink(requestParam);
        int oldUserCnt = Integer.parseInt(findUvTypeByShortLink.get("oldUserCnt").toString());
        int newUserCnt = Integer.parseInt(findUvTypeByShortLink.get("newUserCnt").toString());
        int uvSum = oldUserCnt + newUserCnt;
        double oldRatio = (double) oldUserCnt / uvSum;
        double actualOldRatio = Math.round(oldRatio * 100.0) / 100.0;
        double newRatio = (double) newUserCnt / uvSum;
        double actualNewRatio = Math.round(newRatio * 100.0) / 100.0;
        ShortLinkStatsUvRespDTO newUvRespDTO = ShortLinkStatsUvRespDTO.builder()
                .uvType("newUser")
                .cnt(newUserCnt)
                .ratio(actualNewRatio)
                .build();
        uvTypeStats.add(newUvRespDTO);
        ShortLinkStatsUvRespDTO oldUvRespDTO = ShortLinkStatsUvRespDTO.builder()
                .uvType("oldUser")
                .cnt(oldUserCnt)
                .ratio(actualOldRatio)
                .build();
        uvTypeStats.add(oldUvRespDTO);
        // 访问设备类型详情
        List<ShortLinkStatsDeviceRespDTO> deviceStats = new ArrayList<>();
        List<LinkDeviceStatsDO> listDeviceStatsByShortLink = linkDeviceStatsMapper.listDeviceStatsByShortLink(requestParam);
        int deviceSum = listDeviceStatsByShortLink.stream()
                .mapToInt(LinkDeviceStatsDO::getCnt)
                .sum();
        listDeviceStatsByShortLink.forEach(each -> {
            double ratio = (double) each.getCnt() / deviceSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsDeviceRespDTO deviceRespDTO = ShortLinkStatsDeviceRespDTO.builder()
                    .cnt(each.getCnt())
                    .device(each.getDevice())
                    .ratio(actualRatio)
                    .build();
            deviceStats.add(deviceRespDTO);
        });
        // 访问网络类型详情
        List<ShortLinkStatsNetworkRespDTO> networkStats = new ArrayList<>();
        List<LinkNetworkStatsDO> listNetworkStatsByShortLink = linkNetworkStatsMapper.listNetworkStatsByShortLink(requestParam);
        int networkSum = listNetworkStatsByShortLink.stream()
                .mapToInt(LinkNetworkStatsDO::getCnt)
                .sum();
        listNetworkStatsByShortLink.forEach(each -> {
            double ratio = (double) each.getCnt() / networkSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsNetworkRespDTO networkRespDTO = ShortLinkStatsNetworkRespDTO.builder()
                    .cnt(each.getCnt())
                    .network(each.getNetwork())
                    .ratio(actualRatio)
                    .build();
            networkStats.add(networkRespDTO);
        });
        return ShortLinkStatsRespDTO.builder()
                .daily(BeanUtil.copyToList(listStatsByShortLink, ShortLinkStatsAccessDailyRespDTO.class))
                .localeCnStats(localeCnStats)
                .hourStats(hourStats)
                .topIpStats(topIpStats)
                .weekdayStats(weekdayStats)
                .browserStats(browserStats)
                .osStats(osStats)
                .uvTypeStats(uvTypeStats)
                .deviceStats(deviceStats)
                .networkStats(networkStats)
                .build();
    }
}
package com.nageoffer.shortlink.project.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.LinkAccessLogsDO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.HashMap;
import java.util.List;

/**
 * 访问日志监控持久层
 */
public interface LinkAccessLogsMapper extends BaseMapper<LinkAccessLogsDO> {

    /**
     * 根据短链接获取指定日期内高频访问IP数据
     */
    @Select("SELECT " +
            "    ip, " +
            "    COUNT(ip) AS count " +
            "FROM " +
            "    t_link_access_logs " +
            "WHERE " +
            "    full_short_url = #{param.fullShortUrl} " +
            "    AND gid = #{param.gid} " +
            "    AND create_time BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    full_short_url, gid, ip " +
            "ORDER BY " +
            "    count DESC " +
            "LIMIT 5;")
    List<HashMap<String, Object>> listTopIpByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

    /**
     * 根据短链接获取指定日期内新旧访客数据
     */
    @Select("SELECT " +
            "    SUM(old_user) AS oldUserCnt, " +
            "    SUM(new_user) AS newUserCnt " +
            "FROM ( " +
            "    SELECT " +
            "        CASE WHEN COUNT(DISTINCT DATE(create_time)) > 1 THEN 1 ELSE 0 END AS old_user, " +
            "        CASE WHEN COUNT(DISTINCT DATE(create_time)) = 1 AND MAX(create_time) >= #{param.startDate} AND MAX(create_time) <= #{param.endDate} THEN 1 ELSE 0 END AS new_user " +
            "    FROM " +
            "        t_link_access_logs " +
            "    WHERE " +
            "        full_short_url = #{param.fullShortUrl} " +
            "        AND gid = #{param.gid} " +
            "    GROUP BY " +
            "        user " +
            ") AS user_counts;")
    HashMap<String, Object> findUvTypeCntByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

}
package com.nageoffer.shortlink.project.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.LinkAccessStatsDO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * 短链接基础访问监控持久层
 */
public interface LinkAccessStatsMapper extends BaseMapper<LinkAccessStatsDO> {

    /**
     * 记录基础访问监控数据
     */
    @Insert("INSERT INTO t_link_access_stats (full_short_url, gid, date, pv, uv, uip, hour, weekday, create_time, update_time, del_flag) " +
            "VALUES( #{linkAccessStats.fullShortUrl}, #{linkAccessStats.gid}, #{linkAccessStats.date}, #{linkAccessStats.pv}, #{linkAccessStats.uv}, #{linkAccessStats.uip}, #{linkAccessStats.hour}, #{linkAccessStats.weekday}, NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE pv = pv +  #{linkAccessStats.pv}, " +
            "uv = uv + #{linkAccessStats.uv}, " +
            " uip = uip + #{linkAccessStats.uip};")
    void shortLinkStats(@Param("linkAccessStats") LinkAccessStatsDO linkAccessStatsDO);



    /**
     * 根据短链接获取指定日期内基础监控数据
     */
    @Select("SELECT " +
            "    date, " +
            "    SUM(pv) AS pv, " +
            "    SUM(uv) AS uv, " +
            "    SUM(uip) AS uip " +
            "FROM " +
            "    t_link_access_stats " +
            "WHERE " +
            "    full_short_url = #{param.fullShortUrl} " +
            "    AND gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    full_short_url, gid, date;")
    List<LinkAccessStatsDO> listStatsByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

    /**
     * 根据短链接获取指定日期内小时基础监控数据
     */
    @Select("SELECT " +
            "    hour, " +
            "    SUM(pv) AS pv " +
            "FROM " +
            "    t_link_access_stats " +
            "WHERE " +
            "    full_short_url = #{param.fullShortUrl} " +
            "    AND gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    full_short_url, gid, hour;")
    List<LinkAccessStatsDO> listHourStatsByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

    /**
     * 根据短链接获取指定日期内小时基础监控数据
     */
    @Select("SELECT " +
            "    weekday, " +
            "    SUM(pv) AS pv " +
            "FROM " +
            "    t_link_access_stats " +
            "WHERE " +
            "    full_short_url = #{param.fullShortUrl} " +
            "    AND gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    full_short_url, gid, weekday;")
    List<LinkAccessStatsDO> listWeekdayStatsByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

}
package com.nageoffer.shortlink.project.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.LinkBrowserStatsDO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.HashMap;
import java.util.List;

/**
 * 浏览器统计访问持久层
 */
public interface LinkBrowserStatsMapper extends BaseMapper<LinkBrowserStatsDO> {

    /**
     * 记录浏览器访问监控数据
     */
    @Insert("INSERT INTO t_link_browser_stats (full_short_url, gid, date, cnt, browser, create_time, update_time, del_flag) " +
            "VALUES( #{linkBrowserStats.fullShortUrl}, #{linkBrowserStats.gid}, #{linkBrowserStats.date}, #{linkBrowserStats.cnt}, #{linkBrowserStats.browser}, NOW(), NOW(), 0) " +
            "ON DUPLICATE KEY UPDATE cnt = cnt +  #{linkBrowserStats.cnt};")
    void shortLinkBrowserState(@Param("linkBrowserStats") LinkBrowserStatsDO linkBrowserStatsDO);

    /**
     * 根据短链接获取指定日期内浏览器监控数据
     */
    @Select("SELECT " +
            "    browser, " +
            "    SUM(cnt) AS count " +
            "FROM " +
            "    t_link_browser_stats " +
            "WHERE " +
            "    full_short_url = #{param.fullShortUrl} " +
            "    AND gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    full_short_url, gid, date, browser;")
    List<HashMap<String, Object>> listBrowserStatsByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

}
package com.nageoffer.shortlink.project.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.LinkDeviceStatsDO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * 访问设备监控持久层
 */
public interface LinkDeviceStatsMapper extends BaseMapper<LinkDeviceStatsDO> {

    /**
     * 记录访问设备监控数据
     */
    @Insert("INSERT INTO t_link_device_stats (full_short_url, gid, date, cnt, device, create_time, update_time, del_flag) " +
            "VALUES( #{linkDeviceStats.fullShortUrl}, #{linkDeviceStats.gid}, #{linkDeviceStats.date}, #{linkDeviceStats.cnt}, #{linkDeviceStats.device}, NOW(), NOW(), 0) " +
            "ON DUPLICATE KEY UPDATE cnt = cnt +  #{linkDeviceStats.cnt};")
    void shortLinkDeviceState(@Param("linkDeviceStats") LinkDeviceStatsDO linkDeviceStatsDO);

    /**
     * 根据短链接获取指定日期内访问设备监控数据
     */
    @Select("SELECT " +
            "    device, " +
            "    SUM(cnt) AS cnt " +
            "FROM " +
            "    t_link_device_stats " +
            "WHERE " +
            "    full_short_url = #{param.fullShortUrl} " +
            "    AND gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    full_short_url, gid, device;")
    List<LinkDeviceStatsDO> listDeviceStatsByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

}
package com.nageoffer.shortlink.project.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.LinkLocaleStatsDO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * 地区统计访问持久层
 */
public interface LinkLocaleStatsMapper extends BaseMapper<LinkLocaleStatsDO> {

    /**
     * 记录地区访问监控数据
     */
    @Insert("INSERT INTO t_link_locale_stats (full_short_url, gid, date, cnt, country, province, city, adcode, create_time, update_time, del_flag) " +
            "VALUES( #{linkLocaleStats.fullShortUrl}, #{linkLocaleStats.gid}, #{linkLocaleStats.date}, #{linkLocaleStats.cnt}, #{linkLocaleStats.country}, #{linkLocaleStats.province}, #{linkLocaleStats.city}, #{linkLocaleStats.adcode}, NOW(), NOW(), 0) " +
            "ON DUPLICATE KEY UPDATE cnt = cnt +  #{linkLocaleStats.cnt};")
    void shortLinkLocaleState(@Param("linkLocaleStats") LinkLocaleStatsDO linkLocaleStatsDO);



    /**
     * 根据短链接获取指定日期内基础监控数据
     */
    @Select("SELECT " +
            "    province, " +
            "    SUM(cnt) AS cnt " +
            "FROM " +
            "    t_link_locale_stats " +
            "WHERE " +
            "    full_short_url = #{param.fullShortUrl} " +
            "    AND gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    full_short_url, gid, province;")
    List<LinkLocaleStatsDO> listLocaleByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

}
package com.nageoffer.shortlink.project.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.LinkNetworkStatsDO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * 访问网络监控持久层
 */
public interface LinkNetworkStatsMapper extends BaseMapper<LinkNetworkStatsDO> {

    /**
     * 记录访问设备监控数据
     */
    @Insert("INSERT INTO t_link_network_stats (full_short_url, gid, date, cnt, network, create_time, update_time, del_flag) " +
            "VALUES( #{linkNetworkStats.fullShortUrl}, #{linkNetworkStats.gid}, #{linkNetworkStats.date}, #{linkNetworkStats.cnt}, #{linkNetworkStats.network}, NOW(), NOW(), 0) " +
            "ON DUPLICATE KEY UPDATE cnt = cnt +  #{linkNetworkStats.cnt};")
    void shortLinkNetworkState(@Param("linkNetworkStats") LinkNetworkStatsDO linkNetworkStatsDO);


    /**
     * 根据短链接获取指定日期内访问网络监控数据
     */
    @Select("SELECT " +
            "    network, " +
            "    SUM(cnt) AS cnt " +
            "FROM " +
            "    t_link_network_stats " +
            "WHERE " +
            "    full_short_url = #{param.fullShortUrl} " +
            "    AND gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    full_short_url, gid, network;")
    List<LinkNetworkStatsDO> listNetworkStatsByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

}
package com.nageoffer.shortlink.project.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.LinkOsStatsDO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.HashMap;
import java.util.List;

/**
 * 操作系统统计访问持久层
 */
public interface LinkOsStatsMapper extends BaseMapper<LinkOsStatsDO> {

    /**
     * 记录地区访问监控数据
     */
    @Insert("INSERT INTO t_link_os_stats (full_short_url, gid, date, cnt, os, create_time, update_time, del_flag) " +
            "VALUES( #{linkOsStats.fullShortUrl}, #{linkOsStats.gid}, #{linkOsStats.date}, #{linkOsStats.cnt}, #{linkOsStats.os}, NOW(), NOW(), 0) " +
            "ON DUPLICATE KEY UPDATE cnt = cnt +  #{linkOsStats.cnt};")
    void shortLinkOsState(@Param("linkOsStats") LinkOsStatsDO linkOsStatsDO);

    /**
     * 根据短链接获取指定日期内操作系统监控数据
     */
    @Select("SELECT " +
            "    os, " +
            "    SUM(cnt) AS count " +
            "FROM " +
            "    t_link_os_stats " +
            "WHERE " +
            "    full_short_url = #{param.fullShortUrl} " +
            "    AND gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    full_short_url, gid,  os;")
    List<HashMap<String, Object>> listOsStatsByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

}
package com.nageoffer.shortlink.project.dto.resp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 短链接浏览器监控响应参数
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShortLinkStatsBrowserRespDTO {

    /**
     * 统计
     */
    private Integer cnt;

    /**
     * 浏览器
     */
    private String browser;

    /**
     * 占比
     */
    private Double ratio;
}
package com.nageoffer.shortlink.project.dto.resp;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;

/**
 * 短链接基础访问监控响应参数
 */
@Data
public class ShortLinkStatsAccessDailyRespDTO {

    /**
     * 日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date;

    /**
     * 访问量
     */
    private Integer pv;

    /**
     * 独立访客数
     */
    private Integer uv;

    /**
     * 独立IP数
     */
    private Integer uip;
}
package com.nageoffer.shortlink.project.dto.resp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 短链接访问设备监控响应参数
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShortLinkStatsDeviceRespDTO {

    /**
     * 统计
     */
    private Integer cnt;

    /**
     * 设备类型
     */
    private String device;

    /**
     * 占比
     */
    private Double ratio;
}
package com.nageoffer.shortlink.project.dto.resp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 短链接地区监控响应参数
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShortLinkStatsLocaleCNRespDTO {

    /**
     * 统计
     */
    private Integer cnt;

    /**
     * 地区
     */
    private String locale;

    /**
     * 占比
     */
    private Double ratio;
}
package com.nageoffer.shortlink.project.dto.resp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 短链接访问网络监控响应参数
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShortLinkStatsNetworkRespDTO {

    /**
     * 统计
     */
    private Integer cnt;

    /**
     * 访问网络
     */
    private String network;

    /**
     * 占比
     */
    private Double ratio;
}
package com.nageoffer.shortlink.project.dto.resp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 短链接操作系统监控响应参数
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShortLinkStatsOsRespDTO {

    /**
     * 统计
     */
    private Integer cnt;

    /**
     * 操作系统
     */
    private String os;

    /**
     * 占比
     */
    private Double ratio;
}
package com.nageoffer.shortlink.project.dto.resp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 短链接监控响应参数
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShortLinkStatsRespDTO {

    /**
     * 访问量
     */
    private Integer pv;

    /**
     * 独立访客数
     */
    private Integer uv;

    /**
     * 独立IP数
     */
    private Integer uip;

    /**
     * 基础访问详情
     */
    private List<ShortLinkStatsAccessDailyRespDTO> daily;

    /**
     * 地区访问详情(仅国内)
     */
    private List<ShortLinkStatsLocaleCNRespDTO> localeCnStats;

    /**
     * 小时访问详情
     */
    private List<Integer> hourStats;

    /**
     * 高频访问IP详情
     */
    private List<ShortLinkStatsTopIpRespDTO> topIpStats;

    /**
     * 一周访问详情
     */
    private List<Integer> weekdayStats;

    /**
     * 浏览器访问详情
     */
    private List<ShortLinkStatsBrowserRespDTO> browserStats;

    /**
     * 操作系统访问详情
     */
    private List<ShortLinkStatsOsRespDTO> osStats;

    /**
     * 访客访问类型详情
     */
    private List<ShortLinkStatsUvRespDTO> uvTypeStats;

    /**
     * 访问设备类型详情
     */
    private List<ShortLinkStatsDeviceRespDTO> deviceStats;

    /**
     * 访问网络类型详情
     */
    private List<ShortLinkStatsNetworkRespDTO> networkStats;
}
package com.nageoffer.shortlink.project.dto.resp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 短链接高频访问IP监控响应参数
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShortLinkStatsTopIpRespDTO {

    /**
     * 统计
     */
    private Integer cnt;

    /**
     * IP
     */
    private String ip;
}
package com.nageoffer.shortlink.project.dto.resp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 短链接访客监控响应参数
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShortLinkStatsUvRespDTO {

    /**
     * 统计
     */
    private Integer cnt;

    /**
     * 访客类型
     */
    private String uvType;

    /**
     * 占比
     */
    private Double ratio;
}
package com.nageoffer.shortlink.project.service;

import com.nageoffer.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkStatsRespDTO;

/**
 * 短链接监控接口层
 */
public interface ShortLinkStatsService {

    /**
     * 获取单个短链接监控数据
     *
     * @param requestParam 获取短链接监控数据入参
     * @return 短链接监控数据
     */
    ShortLinkStatsRespDTO oneShortLinkStats(ShortLinkStatsReqDTO requestParam);
}

“feature: 开发单个短链接访问监控详情功能”配合11.18的fix: 修复短链接监控相关数据

高频访问ip那段代码有点问题,传入参数的时间是日期,查询用的是create_time,会把日期转换为当天的0点,这样的话传入的结束日期当天的数据就查不到了。我用的比较笨的方法就是在mapper里面查询的时候再增加两个参数,一个是开始日期后面拼接00:00:00,一个是结束日期后面拼接23:59:59
t_link_access_logs 表中的sql和createTime有关的都得换成 CONCAT(#{param.startDate},' 00:00:00') 和 CONCAT(#{param.endDate},' 23:59:59') 不然查不到当天的数据

 为什么要使用AtomicInteger数据类型?因为lamda里不允许用 int,其实使用 Integer 也是可以的:AtomicInteger 是你说的这个效果,不过咱们这里仅为了遵守 lamda 表达式的规范

单个短链接以及分组监控中浏览器的统计 SQL 也应该将 date 分组条件删掉,详情查看下述提交。

- 第14节:如何记录短链接访问日志

它其实是有一个访问监控的:访问时间,访问IP,访客地区,设备信息,访客类型,访问来源,访问链接;

t_link_access_logs数据库添加2个,device,varchar(64)访问设备;network varchar(64)访问网络;

ALTER TABLE t_link_access_logs
ADD COLUMN `network` VARCHAR(64) DEFAULT NULL COMMENT '访问网络' AFTER `os`,
ADD COLUMN `device` VARCHAR(64) DEFAULT NULL COMMENT '访问设备' AFTER `network`,
ADD COLUMN `locale` VARCHAR(256) DEFAULT NULL COMMENT '访问地区' AFTER `device`;

ALTER TABLE t_link_access_logs
MODIFY COLUMN `ip` VARCHAR(64) DEFAULT NULL COMMENT 'IP' AFTER `user`;

LinkAccessLogsDO.java添加

    /**
     * 访问网络
     */
    private String network;

    /**
     * 访问设备
     */
    private String device;

    /**
     * 地区
     */
    private String locale;

- 第15节:分页查询短链接访问日志

    private void shortLinkStats(String fullShortUrl, String gid, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        try {
            AtomicReference<String> uv = new AtomicReference<>();
            Runnable addResponseCookieTask = () -> {
                uv.set(UUID.fastUUID().toString());
                Cookie uvCookie = new Cookie("uv", uv.get());
                uvCookie.setMaxAge(60 * 60 * 24 * 30);
                uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
                ((HttpServletResponse) response).addCookie(uvCookie);
                uvFirstFlag.set(Boolean.TRUE);
                stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, uv.get());
            };
            if (ArrayUtil.isNotEmpty(cookies)) {
                Arrays.stream(cookies)
                        .filter(each -> Objects.equals(each.getName(), "uv"))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(each -> {
                            uv.set(each);
                            Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                            uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                        }, addResponseCookieTask);
            } else {
                addResponseCookieTask.run();
            }
            String remoteAddr = LinkUtil.getActualIp(((HttpServletRequest) request));
            Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, remoteAddr);
            boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
            if (StrUtil.isBlank(gid)) {
                LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
                gid = shortLinkGotoDO.getGid();
            }
            int hour = DateUtil.hour(new Date(), true);
            Week week = DateUtil.dayOfWeekEnum(new Date());
            int weekValue = week.getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    //uv不再设置成1
                    .uv(uvFirstFlag.get() ? 1 : 0)
                    .uip(uipFirstFlag ? 1 : 0)
                    .hour(hour)
                    .weekday(weekValue)
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(new Date())
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
            Map<String, Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key", statsLocaleAmapKey);
            localeParamMap.put("ip", remoteAddr);
            String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            JSONObject localeResultObj = JSON.parseObject(localeResultStr);
            String infoCode = localeResultObj.getString("infocode");
            String actualProvince;
            String actualCity;

            if (StrUtil.isNotBlank(infoCode) && StrUtil.equals(infoCode, "10000")) {
                String province = localeResultObj.getString("province");
                boolean unknownFlag = StrUtil.equals(province, "[]");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        /*
                        .province(unknownFlag ? "未知" : province)
                        .city(unknownFlag ? "未知" : localeResultObj.getString("city"))
                        */
                        .province(actualProvince = unknownFlag ? "未知" : province)
                        .city(actualCity = unknownFlag ? "未知" : localeResultObj.getString("city"))

                        .adcode(unknownFlag ? "未知" : localeResultObj.getString("adcode"))
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .country("中国")
                        .gid(gid)
                        .date(new Date())
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStatsDO);
                String os = LinkUtil.getOs(((HttpServletRequest) request));

                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .os(os)
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);
                String browser = LinkUtil.getBrowser(((HttpServletRequest) request));

                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .browser(browser)
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
               /*
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .user(uv.get())
                        .ip(remoteAddr)
                        .browser(browser)
                        .os(os)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .build();
                linkAccessLogsMapper.insert(linkAccessLogsDO);
                */
                linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);
                String device = LinkUtil.getDevice(((HttpServletRequest) request));

                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        //.device(LinkUtil.getDevice(((HttpServletRequest) request)))
                        .device(device)
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
                String network = LinkUtil.getNetwork(((HttpServletRequest) request));

                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        //.network(LinkUtil.getNetwork(((HttpServletRequest) request)))
                        .network(network)
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .user(uv.get())
                        .ip(remoteAddr)
                        .browser(browser)
                        .os(os)
                        .network(network)
                        .device(device)
                        .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .build();
                linkAccessLogsMapper.insert(linkAccessLogsDO);

            }

        } catch (Throwable ex) {
            log.error("短链接访问量统计异常", ex);
        }
    }

修改host的那个ip位127.0.0.1导向nageoffer.com ,在浏览器中访问nurl.ink:8001/1fN5rc,即可

- 第16节:分页查询短链接今日以及历史访问信息设计

 这里为什么不直接根据createtime between 0 - 现在时间 来查,然后sum一下 : 感觉直接Date creatime = CURDATE()就能查当日的
实现方法有很多,只不过我用了其中一种而已。如果按照你说的这种改改代码也能用,那就没问题 

 短链接监控之分页查询PV、UV、UIP

 短链接link表新增历史统计字段

`total_pv` int(11) DEFAULT '0' COMMENT '历史PV',
`total_uv` int(11) DEFAULT '0' COMMENT '历史UV',
`total_uip` int(11) DEFAULT '0' COMMENT '历史UIP',


针对原链接表的修改, 这里可以用测试文件快速生成
ALTER TABLE t_link_%d ADD COLUMN total_uv INT DEFAULT 0 COMMENT '历史uv' after `describe` , 
ADD COLUMN total_pv INT DEFAULT 0 COMMENT '历史pv' after total_uv , 
ADD COLUMN total_uip INT DEFAULT 0 COMMENT '历史uip' after total_pv ;

增加每日统计表

CREATE TABLE `t_link_stats_today` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `gid` varchar(32) DEFAULT 'default' COMMENT '分组标识',
  `full_short_url` varchar(128) DEFAULT NULL COMMENT '短链接',
  `date` date DEFAULT NULL COMMENT '日期',
  `today_pv` int(11) DEFAULT '0' COMMENT '今日PV',
  `today_uv` int(11) DEFAULT '0' COMMENT '今日UV',
  `today_uip` int(11) DEFAULT '0' COMMENT '今日IP数',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_full-short-url` (`full_short_url`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;;

 马丁选择重新创建16个表并又写入数据,

下面都是 feature:开发短链接分页监控相关功能;

optimize:完善短链接监控接口功能-------feature:开发短链接监控相关用户访问明细-------fix:修复短链接监控相关数据--------feature:开发短链接监控之分页访问记录接口这些就不再赘述了

 project\dao\entity添加如下代码

package com.nageoffer.shortlink.project.dao.entiry;

import com.baomidou.mybatisplus.annotation.TableName;
import com.nageoffer.shortlink.project.common.database.BaseDO;
import lombok.Data;

import java.util.Date;

/**
 * 短链接今日统计实体
 */
@TableName("t_link_stats_today")
@Data
public class LinkStatsTodayDO extends BaseDO {

    /**
     * id
     */
    private Long id;

    /**
     * 分组标识
     */
    private String gid;

    /**
     * 短链接
     */
    private String fullShortUrl;

    /**
     * 日期
     */
    private Date date;

    /**
     * 今日pv
     */
    private Integer todayPv;

    /**
     * 今日uv
     */
    private Integer todayUv;

    /**
     * 今日ip数
     */
    private Integer todayIpCount;
}

 LinkStatsTodayMapper.java如下

package com.nageoffer.shortlink.project.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.LinkStatsTodayDO;

/**
 * 短链接今日统计持久层
 * */
public interface LinkStatsTodayMapper extends BaseMapper<LinkStatsTodayDO> {
}

 ShortLinkDO添加如下内容

/**
 * 历史PV
 */
private Integer totalPv;

/**
 * 历史UV
 */
private Integer totalUv;

/**
 * 历史UIP
 */
private Integer totalUip;

分页查询出来的project\dto\resp的ShortLinkPageRespDTO.java添加如下 

/**
 * 历史PV
 */
private Integer totalPv;

/**
 * 今日PV
 */
private Integer toDayPv;

/**
 * 历史UV
 */
private Integer totalUv;

/**
 * 今日UV
 */
private Integer toDayUv;

/**
 * 历史UIP
 */
private Integer totalUIp;

/**
 * 今日UIP
 */
private Integer toDayUIp;

剩下的两个 admin\remote\dto\req添加ShortLinkPageReqDTO.java

/**
 * 排序标识
 */
private String orderTag;

 ShortLinkPageRespDTO.java添加admin\remote\dto\resp

/**
 * 历史PV
 */
private Integer totalPv;

/**
 * 今日PV
 */
private Integer todayPv;

/**
 * 历史UV
 */
private Integer totalUv;

/**
 * 今日UV
 */
private Integer todayUv;

/**
 * 历史UIP
 */
private Integer totalUip;

/**
 * 今日UIP
 */
private Integer todayUip;

新建 表

package com.nageoffer.shortlink.admin.test;

public class UserTableShardingTest {

    public static final String SQL = "CREATE TABLE `t_link_stats_today_%d` (\n"
            + "    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',\n"
            + "    `gid` varchar(32) DEFAULT 'default' COMMENT '分组标识',\n"
            + "    `full_short_url` varchar(128) DEFAULT NULL COMMENT '短链接',\n"
            + "    `date` date DEFAULT NULL COMMENT '日期',\n"
            + "    `today_pv` int(11) DEFAULT '0' COMMENT '今日PV',\n"
            + "    `today_uv` int(11) DEFAULT '0' COMMENT '今日UV',\n"
            + "    `today_uip` int(11) DEFAULT '0' COMMENT '今日IP数',\n"
            + "    `create_time` datetime DEFAULT NULL COMMENT '创建时间',\n"
            + "    `update_time` datetime DEFAULT NULL COMMENT '修改时间',\n"
            + "    `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',\n"
            + "    PRIMARY KEY (`id`),\n"
            + "    UNIQUE KEY `idx_unique_today_stats` (`full_short_url`,`gid`,`date`) USING BTREE\n"
            + ") ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;";

    public static void main(String[] args) {
        for (int i = 0; i < 16; i++) {
            System.out.printf((SQL) + "%n", i);
        }
    }
}

 我感觉其实没必要新建一张表,维护这张表感觉,每次访问的时候都要同时操作多张表,所以我觉得其实可以直接用t_link_access_stats这张表,因为这张表里根据Date有唯一索引,查询当天的访问数据的话直接查当天的,历史的就直接所有的sum加起来就可以

完全是可以的,后期要是怕频繁sum,直接把sum结果数据存redis。再走一层redis兜底就完美了 

- 第17节:如何统计短链接汇总访问数据


这里修改短链接部分的业务代码也需要修改: 后面重构了,修改短链接分组不需要变更监控表

 我们分表的操作修改shardingsphere-config-dev.yaml

# 数据源集合
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root

rules:
  - !SHARDING
    tables:
      t_link:
        actualDataNodes: ds_0.t_link_${0..15}
        tableStrategy:
          standard:
            shardingColumn: gid
            shardingAlgorithmName: link_table_hash_mod
      t_link_goto:
        actualDataNodes: ds_0.t_link_goto_${0..15}
        tableStrategy:
          standard:
            shardingColumn: full_short_url
            shardingAlgorithmName: link_goto_table_hash_mod
      t_link_stats_today:
        actualDataNodes: ds_0.t_link_stats_today_${0..15}
        tableStrategy:
          standard:
            shardingColumn: gid
            shardingAlgorithmName: link_stats_today_hash_mod
    shardingAlgorithms:
      link_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      link_goto_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      link_stats_today_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
props:
  sql-show: true

 那today_ip_count的话那这个分表的数量应该要要比这个t_link要多很多嘛,为什么还是要分16个?因为就是常规的分表行为对吧,还有就是短链接你创建出来他不一定有人访问

 我们修改了基础设施,现在修改代码来适配,修改project的ShortLinkServicelmpl.java,添加                .totalPv(0).totalUv(0).totalUip(0)     为了短链接新增时初始化访问统计为0

    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        String shortLinkSuffix = generateSuffix(requestParam);
        String fullShortUrl = StrBuilder.create(requestParam.getDomain())
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(requestParam.getDomain())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .fullShortUrl(fullShortUrl)
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .build();
        ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();

 开发短链接监控之汇总统计,project\dao\mapper的补充ShortLinkMapper.java

package com.nageoffer.shortlink.project.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.ShortLinkDO;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

public interface ShortLinkMapper extends BaseMapper<ShortLinkDO> {
    /**
     * 短链接访问统计自增
     */
    @Update("update t_link set total_pv = total_pv + #{totalPv}, total_uv = total_uv + #{totalUv}, total_uip = total_uip + #{totalUip} where gid = #{gid} and full_short_url = #{fullShortUrl}")
    void incrementStats(
            @Param("gid") String gid,
            @Param("fullShortUrl") String fullShortUrl,
            @Param("totalPv") Integer totalPv,
            @Param("totalUv") Integer totalUv,
            @Param("totalUip") Integer totalUip
    );
}

  ShortLinkServicelmpl.java里面添加,shortLinkStats方法里面前面已经对uv什么的处理了,

            LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                    .user(uv.get())
                    .ip(remoteAddr)
                    .browser(browser)
                    .os(os)
                    .network(network)
                    .device(device)
                    .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .build();
            linkAccessLogsMapper.insert(linkAccessLogsDO);
            baseMapper.incrementStats(gid, fullShortUrl, 1, uvFirstFlag.get() ? 1 : 0, uipFirstFlag ? 1 : 0);
        }

    } catch (Throwable ex) {
        log.error("短链接访问量统计异常", ex);
    }
}

 重启下,创建下新的短链接,得到了nurl.ink/2v5g6C,浏览器http://nurl.ink:8001/2v5g6C,看看数据库

 

- 第18节:如何统计短链接监控之今日数据访问

LinkStatsTodayMapper.java修改

package com.nageoffer.shortlink.project.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nageoffer.shortlink.project.dao.entiry.LinkStatsTodayDO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
/**
 * 短链接今日统计持久层
 * */
public interface LinkStatsTodayMapper extends BaseMapper<LinkStatsTodayDO> {

    /**
     * 记录今日统计监控数据
     */
    @Insert("INSERT INTO t_link_stats_today (full_short_url, gid, date,  today_uv, today_pv, today_uip, create_time, update_time, del_flag) " +
            "VALUES( #{linkTodayStats.fullShortUrl}, #{linkTodayStats.gid}, #{linkTodayStats.date}, #{linkTodayStats.todayUv}, #{linkTodayStats.todayPv}, #{linkTodayStats.todayUip}, NOW(), NOW(), 0) " +
            "ON DUPLICATE KEY UPDATE today_uv = today_uv +  #{linkTodayStats.todayUv}, today_pv = today_pv +  #{linkTodayStats.todayPv}, today_uip = today_uip +  #{linkTodayStats.todayUip};")
    void shortLinkTodayState(@Param("linkTodayStats") LinkStatsTodayDO linkStatsTodayDO);

}

 LinkStatsTodayDOjava修改为

package com.nageoffer.shortlink.project.dao.entiry;

import com.baomidou.mybatisplus.annotation.TableName;
import com.nageoffer.shortlink.project.common.database.BaseDO;
import lombok.*;
import lombok.NoArgsConstructor;
import java.util.Date;

/**
 * 短链接今日统计实体
 */
@TableName("t_link_stats_today")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LinkStatsTodayDO extends BaseDO {

    /**
     * id
     */
    private Long id;

    /**
     * 分组标识
     */
    private String gid;

    /**
     * 短链接
     */
    private String fullShortUrl;

    /**
     * 日期
     */
    private Date date;

    /**
     * 今日pv
     */
    private Integer todayPv;

    /**
     * 今日uv
     */
    private Integer todayUv;

    /**
     * 今日ip数
     */
    private Integer todayUip;
}

 ShortLinkServicelmpl.java

            linkAccessLogsMapper.insert(linkAccessLogsDO);
            baseMapper.incrementStats(gid, fullShortUrl, 1, uvFirstFlag.get() ? 1 : 0, uipFirstFlag ? 1 : 0);
            LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                    .todayPv(1)
                    .todayUv(uvFirstFlag.get() ? 1 : 0)
                    .todayUip(uipFirstFlag ? 1 : 0)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);
        }

    } catch (Throwable ex) {
        log.error("短链接访问量统计异常", ex);
    }
}

 我们第二次创建一下,

{
    "domain": "nurl.ink",
    "originUrl": "https://nageoffer.com",
    "gid": "1k91Uw",
    "createdType": 1,
    "validDateType": 0,
    "describe": "2025.6.29第二次创建"
}


得到nurl.ink/29OdGJ

 我访问了http://nurl.ink:8001/29OdGJ 3次,pv在sql为3

第一条创建的短链接的total_uv和uip为0.不应该也是1嘛?

第一条为0是不是因为cookie和redis里面已经有uv和uip了吧,他只是把数据库里存的删除了,cookie和redis的没有删,所以并不是第一次访问;创建出来又没有访问,为 0 是正常的

pv,uv换个顺序就解决了啊,insert语句数据库字段和对象里的值一一对应

 ClownMing:我觉得today插入的时候不能用之前的flag来判断了,我自己是这样写的: AtomicBoolean uvTodayFirstFlag = new AtomicBoolean(); Long uvTodayAdd = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + DateUtil.formatDate(date) + ":" + fullShortUrl, uv.get()); uvTodayFirstFlag.set(uvTodayAdd != null && uvTodayAdd > 0L); Long uipTodayAdd = stringRedisTemplate.opsForSet().add("short-link:stats:ip:" + DateUtil.formatDate(date) + ":" + fullShortUrl, ipAddress); boolean uipTodayFirstFlag = uipTodayAdd != null && uipTodayAdd > 0L; LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder() .id(SnowUtil.getSnowflakeNextId()) .gid(gid) .fullShortUrl(fullShortUrl) .date(date) .todayPv(1) .todayUv(uvTodayFirstFlag.get() ? 1 : 0) .todayUip(uipTodayFirstFlag ? 1 : 0) .build(); linkStatsTodayMapper.shortLinkTodayStats(linkStatsTodayDO);

栗子ing 回复 ClownMing:我也有相同的想法,因为前面的flag是判断累计的,每一天需要刷新,感觉这里可以改进就是,缓存可以设置一下有效期,然后可以不用AtomicBoolean,直接用boolean就行,高并发的时候效率好点的感觉

Everglow 回复 ClownMing:给原来的uv uip集合 设一个辅助键,给辅助键设置个一天过期就可以了

Aleksib 回复 栗子ing:哥们这里为啥可以不用AtomicBoolean啊?

栗子ing 回复 Aleksib:没有什么因为啊,就是没有lambda的场景,用哪个都是一样的

李温候 回复 栗子ing:这和lambda表达式没关系吧,我记得boolean在多线程下是不安全的,AtomicBoolean是安全的

李温候 回复 Everglow:我觉得这个可行

栗子ing 回复 李温候:那你可以说一下为什么不能用boolean

栗子ing 回复 李温候:可以探讨一下,那我可以问你一下,为什么马哥 uvFirstFlag 用 ActomBoolean 然后 uipFirstFlag 用的是 boolean ,你可以回答一下。

李温候 回复 栗子ing:我是这样认为的哈:就楼主的案例为例在uvTodayFirstFlag.set的时候,多线程同时打到这里。在使用boolean的情况下,在同一时刻都设置了成true了,那这样最后统计uv的时候肯定多了一次(正常一个一个0,一个1)。但是使用AtomicBoolean的话就可以避免这个问题,AtomicBoolean底层是CAS的,提供了一些原子操作的方法,如get()和set(),可以保持多个线程同时打到这里时只会set一次(个人看法)

李温候 回复 栗子ing:这个我目前水平没咋看懂,也可能是马哥手误打错了?

栗子ing 回复 李温候:1. 局部变量,他的作用范围有没有超出方法,会不会存在多线程的问题呢? 2. 如果不会,那 cas 重试是不是造成一个性能损耗,线程过多,就会性能下降?

李温候 回复 栗子ing:局部变量,他们不都是在shortlinkstats方法里面的么,那岂不是都是局部变量了,你咋看马哥一个boolean,一个用actomboolean

栗子ing 回复 李温候:我不是说过,因为lambda里面不能使用boolean,需要使用不能发生修改的引用,所以使用了ActomBoolean这个不可变类,才能在lambda中使用

7 回复 栗子ing:我觉得可以重复使用,因为累计判断也需要设置每天过期,这样才能判断第二天是否登陆,所以在today逻辑应该也可以使用

马丁 回复 ClownMing:看了大家在讨论 Boolean 和 AtomicBoolean,这里总结回复下。在咱们这个场景里用两个都是可以的,因为不存在并发请求,所以两者作用一致,都是为了解决 lamda 的规范编译问题。只是我习惯用了 AtomicBoolean 所以在代码里写上了

TheTurtle 回复 7:累计判断也需要设置每天过期 是什么意思?

TheTurtle 回复 栗子ing:1. 感觉这种方案又会导致uv和uip的累计统计会有问题:对于累计uv的话cookies不能过期,不然过段时间对于同一个人其实会被看作两个人,而对于累计uip也是同理(因此如果用redis查重uip的话还要解决优化空间问题)。2. 我在想可不可以给每日uv单独设置一个cookie,每天零点过期;而对于每日ip,可以单独设一个today_ip_set,也是每日零点过期,此外在处理ip时还需要加到之前的total_ip_set。

TheTurtle 回复 栗子ing:用两种cookie以及两种redis-set来判断。

3 回复 ClownMing:感觉这里确实判断用之前的Flag有点问题

Venom. 回复 栗子ing:我也觉得设置有效期比较好

皮蛋瘦肉粥。 回复 Venom.:给cookie设置一个时间,反正没有用户的cookie就当作新用户。时间就设置为今日的23:59:59减去当前时间。

Venom. 回复 皮蛋瘦肉粥。:没太懂你什么意思 我觉得这块的问题就是redis里面统计uv和uip的set要设置有效期 要不然到后面统计逻辑就不对了

皮蛋瘦肉粥。 回复 Venom.:当 uv Cookie 不存在时,调用了 addResponseCookieTask 任务。这个任务负责生成一个新的 uv Cookie 并将其存储到 Redis 中,同时将 uvFirstFlag 设置为 true,表示这是首次访问。所以我把uv cookie的时间设置为23:59:59减去当前访问时间。那么就能做到cookie到了十二点就自动删除了,因为没有有效的 uv Cookie,系统会再次执行“首次访问”的逻辑,将该用户记录为新访客,并触发 uv + 1 的操作。

Cool 回复 李温候:实际上多线程下也不会有线程安全,因为boolean的读写都是原子性的,并且使用atomicBoolean也不会让多个线程打进来只set一次,cas在别的线程对变量修改后会再次执行

白日梦想家 回复 3:用之前的flag有啥问题嘛?没太懂


问题分析总结

1. ClownMing 的观点是否正确?

正确
原代码中 uvFirstFlaguipFirstFlag 是基于累计统计的判断(可能包含跨天数据),而 LinkStatsTodayDO 需要统计当日首次访问 。如果直接复用累计标志,会导致以下问题:

  • 逻辑错误 :若用户在前一天已访问过,则 uvFirstFlag 已为 false,即使当天首次访问也会被标记为非首次,导致今日 UV 统计遗漏。
  • 数据不准确 :今日统计应独立于历史数据,需通过 Redis 的 SET 操作重新判断当日唯一性(如 short-link:stats:uv:日期:fullShortUrl 的集合判断)。

2. 使用 AtomicBoolean 还是 boolean

栗子ing 正确,李温候错误

  • 局部变量线程安全问题 uvTodayFirstFlaguipTodayFirstFlag 是方法内的局部变量,属于线程私有栈内存,不存在多线程竞争问题。即使在 Lambda 表达式中使用,boolean 的不可变性已通过 final 保证(Java 8+ 对未显式声明 final 的局部变量隐式处理为不可变)。
  • 性能优化 AtomicBoolean 基于 CAS 操作,涉及 CPU 指令级锁,在无并发竞争场景下增加不必要的开销。boolean 更轻量。

3. Redis 键过期与统计独立性

Everglow 和 TheTurtle 正确

  • Key 设计 :为每日统计单独设置 Redis 键(如 short-link:stats:uv:20231001:fullShortUrl),并通过 TTL 设置过期时间为当日 23:59:59,确保每日数据隔离。
  • 累计统计与今日统计分离 :累计 UV/IP 应使用长期存在的 Key(如 short-link:stats:uv:total:fullShortUrl),而今日统计使用独立 Key,避免互相干扰。

4. Cookie 与 Redis 协同优化

皮蛋瘦肉粥 和 Venom 正确

  • Cookie 过期策略 :为客户端 Cookie 设置过期时间为当日 23:59:59,确保次日访问时触发重新统计逻辑。
  • Redis 过期策略 :通过 Key 的 TTL 自动清理历史数据,避免手动维护过期逻辑。

最终结论

ClownMing 的修正逻辑✅ 正确需重新判断当日首次访问,避免复用累计标志
栗子ing 的 boolean 选择✅ 正确局部变量无并发问题,boolean更高效
李温候的 AtomicBoolean 必要性❌ 错误无共享内存竞争,AtomicBoolean过度设计
Everglow 的 Redis 过期方案✅ 正确确保每日统计独立性
TheTurtle 的分层统计设计✅ 正确分离累计与当日统计,避免数据混淆

代码改进建议

  1. 重新判断当日 UV/UIP // 使用 Redis SET 判断当日首次访问 String uvKey = "short-link:stats:uv:" + DateUtil.formatDate(date, "yyyyMMdd") + ":" + fullShortUrl; Long uvTodayAdd = stringRedisTemplate.opsForSet().add(uvKey, uv.get()); boolean uvTodayFirstFlag = uvTodayAdd != null && uvTodayAdd > 0L; stringRedisTemplate.expire(uvKey, 1, TimeUnit.DAYS); // 设置过期时间 String ipKey = "short-link:stats:ip:" + DateUtil.formatDate(date, "yyyyMMdd") + ":" + fullShortUrl; Long uipTodayAdd = stringRedisTemplate.opsForSet().add(ipKey, ipAddress); boolean uipTodayFirstFlag = uipTodayAdd != null && uipTodayAdd > 0L; stringRedisTemplate.expire(ipKey, 1, TimeUnit.DAYS);
  2. 简化为 boolean // 因为是局部变量,无需 AtomicBoolean boolean uvTodayFirstFlag = uvTodayAdd != null && uvTodayAdd > 0L; boolean uipTodayFirstFlag = uipTodayAdd != null && uipTodayAdd > 0L;
  3. Redis Key 过期时间设置
    • 在插入 Redis 数据时,通过 expire 命令设置过期时间,确保每日数据自动清理。

总结

  • 核心问题 :今日统计需独立判断,不可复用累计标志。
  • 关键改进 :使用 Redis Key 过期机制实现每日统计隔离,局部变量用 boolean 更高效。
  • 设计原则 :分层存储(累计 vs 今日)、自动过期、线程安全最小化。

- 第19节:分页查询短链接监控数据排序功能

在shortlink\project\src\main\resources\mapper新建 LinkMapper.xml 

<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ Licensed to the Apache Software Foundation (ASF) under one or more
  ~ contributor license agreements.  See the NOTICE file distributed with
  ~ this work for additional information regarding copyright ownership.
  ~ The ASF licenses this file to You under the Apache License, Version 2.0
  ~ (the "License"); you may not use this file except in compliance with
  ~ the License.  You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nageoffer.shortlink.project.dao.mapper.ShortLinkMapper">

    <!-- 分页查询短链接 -->
    <select id="pageLink" parameterType="com.nageoffer.shortlink.project.dto.req.ShortLinkPageReqDTO"
            resultType="com.nageoffer.shortlink.project.dao.entiry.ShortLinkDO">
        SELECT t.*,
        COALESCE(s.today_pv, 0) AS todayPv,
        COALESCE(s.today_uv, 0) AS todayUv,
        COALESCE(s.today_uip, 0) AS todayUip
        FROM t_link t
        LEFT JOIN t_link_stats_today s ON t.gid = s.gid
        AND t.full_short_url = s.full_short_url
        AND s.date = CURDATE()
        WHERE t.gid = #{gid}
        AND t.enable_status = 0
        AND t.del_flag = 0
        <choose>
            <when test="orderTag == 'todayPv'">
                ORDER BY todayPv DESC
            </when>
            <when test="orderTag == 'todayUv'">
                ORDER BY todayUv DESC
            </when>
            <when test="orderTag == 'todayUip'">
                ORDER BY todayUip DESC
            </when>
            <when test="orderTag == 'totalPv'">
                ORDER BY t.total_pv DESC
            </when>
            <when test="orderTag == 'totalUv'">
                ORDER BY t.total_uv DESC
            </when>
            <when test="orderTag == 'totalUip'">
                ORDER BY t.total_uip DESC
            </when>
            <otherwise>
                ORDER BY t.create_time DESC
            </otherwise>
        </choose>
    </select>
</mapper>

在mabaisplus中进行扫描, 在yaml中也有mplus的mapper-location附带扫描地址,

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml

 shortlinkmapper中新加,你可以下载MyBatisX插件;这样子点击小红鸟可以直接跳转过来;

/**
 * 分页统计短链接
 */
IPage<ShortLinkDO> pageLink(ShortLinkPageReqDTO requestParam);

你看一下究极是干什么的。首先我们是要把那个分页,它会涉及到两个操作:一个就是查询出来短链接基础信息,也就是我们的这个T杠link,其次是我们要把今日统计给查出来;如果说今日统计等于空的话,我们要把它转换成0,比如都对pv to,对ub以及一个to duip,然后他们的连接条件是什么?一个是jad,一个是shut link,还有一个是比较重要的,就是今日统计的话,我们会在这里面生成非常多的今日统计的数据,可能是有19号到18号的20号的等等,所以说我们这里要把 today的张表要给他按当前日期计算,它当前日期我们可以直接去操作一下。我们10 10来个车一样,对吧?10月19这样就可以了。然后查询条件的话就是我们的分组标识要查安定宝,也就是生效的一个状态以及待flag等于0的。

然后接下来的话排序的话,我们跟前端约定好了,让他一个字段,奥德泰克他可能会有以下几个类型 today,pv to duv to,对uip以及我们的历史总量的ppuv以及uip,它在每个case下面有对应的什么?就是对应的语句相当于是它的动态标签如果说,奥特塔克谁都不等于,ok,它会用我们的创建时间就是去做一个倒叙,这是一个它分页的一个解读,有一点要跟大家说的是什么?我们这里的话他们的态度就是我们在这里它的一个配置对象要通过配置对象去做,然后其次的话他态度我们不能用什么,我们是a配置开头,,但是我们不能把iPad值当做result type,我们要把它里面的发型去当做它的绿色的态度这是要重点跟大家说的。

 shardingsphere-config.yaml里面添加一个这个

bindingTables:
  - t_link, t_link_stats_today

之前我使用的时候是没有用过shardingsphere的绑定表的,所以说我刚才在写代码的时候就遇到了比较难搞的一个问题,就是它出现了笛卡尔积,什么意思?我给大家示范一下。首先我们可以看到正常是两条记录对吧?因为我们表里面其实也就两条记录对不对?但是我给大家演示一下如果说我不加这个绑定表他会查出多少条记录,它会查询出来非常多的记录,虽然说它最终返回的是两条但是你看到了它返回了N多的记录,这是因为它这里面有个绑定表的概念,如果说使用绑定表它去查询的话,你没有去给它做绑定表的规则限制,这样的话其实它就会产生一个笛卡尔积这样的一个行为。

当然其实这是shardingsphere本身内部判断的一个就是内部执行的一个逻辑,我们把这个放到我们的数据库客户端里面去执行,一样是返回两条记录,也是返回两条记录,但是你通过shardingsphere它一聚合一做这种笛卡尔积对吧?产生笛卡尔积之后它就相当于返回了10条记录,这种肯定是不对的。我们加了绑定表之后,这个问题就解决了。绑定表很简单,你这种对吧?有可能会产生笛卡尔积的东西,给他把绑定表里面给他绑定一下就可以了,记住要用同样的分片键对吧?比如link和stats这两个表,把它们用同一个分片键进行关联,都是gid对吧?然后这样的话其实把这个放开就能满足我们的需求。

给大家扩展一下,相当于shardingsphere的知识又加了一层,因为很多场景下你用了这种分库分表之后,绑定表一般是很少去做关联查询的,起码我在工作当中是基本上分库分表的表我们是不做关联查询的,这也是第一次遇到这种情况,但是因为我之前了解过shardingsphere有一个绑定表的概念,所以说我去官网搜了一下,然后对吧这个问题就解决了

然后给大家提一点就是有个补充PR,就是一个免费的补充,看大家做不做,这里面的话其实它绑定表的概念核心概念里面其实讲的不是很清晰,它只是告诉你去做了这种基本的分片行为,说实话在这里我是没有看到绑定表的解决方案的,但是因为我之前对shardingsphere的文档研究的挺多的,就是我做项目初期也做了很多关于shardingsphere文档的建设,所以说我去看了一下用户手册,然后在这里数据分片,应该是绑定。然后它这边有个绑定表配置项,然后通过他的配置项,然后知道了这是一个可有可无的配置,就是可选项,然后下面你去写它对应的绑定表的一些规则,我们甚至可以把 funding词加到刚才的里面去,我建议是这样,但是到底你提了PR之后别人会不会合并,这是一个未知的事情。

 shortlinkDO里面添加

    /**
     * 今日PV
     */
    @TableField(exist = false)
    private Integer todayPv;

    /**
     * 今日UV
     */
    @TableField(exist = false)
    private Integer todayUv;

    /**
     * 今日UIP
     */
    @TableField(exist = false)
    private Integer todayUip;

 ShortLinkPageRegDTO.java加上分页的

    /**
     * 排序标识
     */
    private String orderTag;

 ShortLinkServicelmpl.java 注释掉

@Override
public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
    /*
    LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
            .eq(ShortLinkDO::getGid, requestParam.getGid())
            .eq(ShortLinkDO::getEnableStatus, 0)
            .eq(ShortLinkDO::getDelFlag, 0)
            .orderByDesc(ShortLinkDO::getCreateTime);
    IPage<ShortLinkDO> resultPage = baseMapper.selectPage(requestParam, queryWrapper);
    */
    IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);

    //return resultPage.convert(each -> BeanUtil.toBean(each, ShortLinkPageRespDTO.class));
    return resultPage.convert(each -> {
        ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
        result.setDomain("http://" + result.getDomain());
        return result;
    });
}

ShortLinkRemoteService.java 新增分页部分;之前我们这里是正常没有这个数据的

default Result<IPage<ShortLinkPageRespDTO>> pageShortLink(ShortLinkPageReqDTO requestParam) {
    Map<String, Object> requestMap = new HashMap<>();
    requestMap.put("gid", requestParam.getGid());
    requestMap.put("orderTag", requestParam.getOrderTag());
    requestMap.put("current", requestParam.getCurrent());
    requestMap.put("size", requestParam.getSize());
    String resultPageStr = HttpUtil.get("http://127.0.0.1:8001/api/short-link/v1/page", requestMap);
    return JSON.parseObject(resultPageStr, new TypeReference<>() {
    });
}

 feature:开发短链接监控之分页排序功能

 为啥不分开查t_link和t_link_stats_today,感觉这样就不会ShortLinkServicelmpl.java出现错误了;
分开查做不到分页效果

 当我们在 ShardingSphere 中设置了绑定表,ShardingSphere 就会基于分片键的哈希值或分片规则将两个表的分片进行对应绑定。这样,它只会查询那些分片键相同的分片,避免了无效的跨分片组合查询,从而提高了性能。

诀别:这里为什么不把 s.date = CURDATE() 加到 WHERE 的条件里,我试了下如果短链接当天没有访问的话会把所有gid与传入的gid相同的短链接查询出来,还是我们要做的功能就是这个样子

全糖很多冰 回复 诀别:我也是会这样

呦呼~ 回复 诀别:因为如果这个条件放在where里面的话,当天如果没有数据,你shortlink的任何数据都查不到了,而我们希望的是如果当天没有数据,当天的统计是0,而不是查不出来这条数据。然后我们本来就是要查出所有该分组下面的短链接,就是应该要查出所有的,你可以再看看多表联查时候的条件顺序。

- 第20节:如何统计短链接监控之指定时间内PV、UV、UIP数据

 补充了一个用于配置 Java 代码格式化规则的文件,它基于 Eclipse 代码格式化器 的配置规范。这种配置文件通常与 Spotless 插件结合使用,后者是一个流行的代码格式化工具,可以集成到 Maven、Gradle 等构建系统中,确保团队成员的代码风格一致。

short-link_spotless_formatter.xml 

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
-->

<profiles version="13">
    <profile kind="CodeFormatterProfile" name="'ShortLink Current'" version="13">
        <setting id="org.eclipse.jdt.core.compiler.source" value="17"/>
        <setting id="org.eclipse.jdt.core.compiler.compliance" value="17"/>
        <setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="17"/>
        <setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
        <setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
        <setting id="org.eclipse.jdt.core.formatter.lineSplit" value="200"/>
        <setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="200"/>
        <setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
        <setting id="org.eclipse.jdt.core.formatter.indentation.size" value="1"/>
        <setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="false"/>
        <setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="false"/>
        <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
        <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
        <setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="16"/>
        <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
        <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
        <setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
        <setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="16"/>
        <setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
        <setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
        <setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="160"/>
        <setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="10"/>
        <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="106"/>
        <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="106"/>
        <setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="106"/>
        <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="1"/>
        <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call.count_dependent" value="16|5|80"/>
    </profile>
</profiles>

pom.xml里面 

        <spotless-maven-plugin.version>2.22.1</spotless-maven-plugin.version>





            <plugin>
                <groupId>com.diffplug.spotless</groupId>
                <artifactId>spotless-maven-plugin</artifactId>
                <version>${spotless-maven-plugin.version}</version>
                <configuration>
                    <java>
                        <!--<eclipse>
                            <file>${maven.multiModuleProjectDirectory}/format/short-link_spotless_formatter.xml</file>
                        </eclipse>-->
                        <licenseHeader>
                            <!-- ${maven.multiModuleProjectDirectory} 爆红属于正常,并不影响编译或者运行,忽略就好 -->
                            <file>${maven.multiModuleProjectDirectory}/format/license-header</file>
                        </licenseHeader>
                    </java>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>apply</goal>
                        </goals>
                        <phase>compile</phase>
                    </execution>
                </executions>
            </plugin>

 linkAccessLogsmapper里面不好弄;

/**
 * 根据短链接获取指定日期内PV、UV、UIP数据
 */
@Select("SELECT " +
        "    COUNT(user) AS pv, " +
        "    COUNT(DISTINCT user) AS uv, " +
        "    COUNT(DISTINCT ip) AS uip " +
        "FROM " +
        "    t_link_access_logs " +
        "WHERE " +
        "    full_short_url = #{param.fullShortUrl} " +
        "    AND gid = #{param.gid} " +
        "    AND create_time BETWEEN #{param.startDate} and #{param.endDate} " +
        "GROUP BY " +
        "    full_short_url, gid;")
LinkAccessStatsDO findPvUvUidStatsByShortLink(@Param("param") ShortLinkStatsReqDTO requestParam);

ShortLinkStatsServicelmpl.java 添加基础访问数据,return 中补充pv,uv,uip等

       if (CollUtil.isEmpty(listStatsByShortLink)) {
            return null;
        }
        // 基础访问数据
        LinkAccessStatsDO pvUvUidStatsByShortLink = linkAccessLogsMapper.findPvUvUidStatsByShortLink(requestParam);
        // 基础访问详情
        List<ShortLinkStatsAccessDailyRespDTO> daily = new ArrayList<>();
        List<String> rangeDates = DateUtil.rangeToList(DateUtil.parse(requestParam.getStartDate()), DateUtil.parse(requestParam.getEndDate()), DateField.DAY_OF_MONTH).stream()








        return ShortLinkStatsRespDTO.builder()
                .pv(pvUvUidStatsByShortLink.getPv())
                .uv(pvUvUidStatsByShortLink.getUv())
                .uip(pvUvUidStatsByShortLink.getUip())
                .daily(daily)
                .localeCnStats(localeCnStats)
                .hourStats(hourStats)

查询的时候一定要输入正确,我查的是2025.6.4-2023.6.30,我就说为什么一直查不到 应该是2025.6.30;大概是可以看出这些信息

pv总访问量(Page View)= 4
uv独立访客数(User Visitor)= 1
uip独立IP数(Unique IP)= 1
timeStats按小时分组的访问量:16点3次,22点1次
weekday星期几统计(7=星期日)
browser浏览器类型:Microsoft Edge
os操作系统:Windows
device设备类型:PC
network网络类型:Mobile(移动网络)
userBehavior用户行为分析:新用户1人,无旧用户
ipList访问IP列表:127.0.0.1 共4次访问
location地域分布:未知地区4次访问(可能需要补充具体地理信息)

 。:List<LinkAccessStatsDO> listStatsByShortLink = linkAccessStatsMapper.listStatsByShortLink(requestParam); if (CollUtil.isEmpty(listStatsByShortLink)) { return null; } // 基础访问数据 LinkAccessStatsDO pvUvUidStatsByShortLink = linkAccessLogsMapper.findPvUvUidStatsByShortLink(requestParam); 这里虽然做了判空,但listStatsByShortLink和findPvUvUidStatsByShortLink中的select 语句不完全一致,between and 那里一个是date,一个是create time,查询的时候右侧闭包不一致,会导致报错

I'm sure 回复 。:对的,把findPvUvUidStatsByShortLink方法的时间条件那行代码替换为" AND create_time BETWEEN CONCAT(#{param.startDate},' 00:00:00') and CONCAT(#{param.endDate},' 23:59:59') " + 就可以了

一如既往 回复 I'm sure:感觉不如直接为t_link_access_logs添加多一个date字段,就像其他监控数据表那样判断date就行

- 第21节:如何统计分组短链接监控数据

监控数据我们上面已经知道了,这边已经有一个单个短链接的指定时间内访问监控数据 - 分组短链接是什么意思?这里看到。三个参数的话它是分组标识下有一个短链接,完整的一个标识,查的只是这一个短链接在指定时间内的监控数据,然后我们分组这里的话其实已经没有单独的标识了,他其实查的是整个分组,这也就意味着分组链接要查询的数据量更大,以及它所消耗的性能会更多,所以说功能一定要比单个链接的限流策略要更严格。

我们开发完分组链接监控的话就是在原有的单个链接监控方法后面加个@Group注解然后名字前面加个group,然后把这些数据给搞过来。然后的话我们和上面监控单个链接的代码基本上结构一样,为了避免耽误大家更多的时间,我直接复用了大部分代码。相当于我们单个链接的话其实是可以通过一些if判断对吧?是可以满足分组需求的,相当于我们大概做个判断,如果发现gid不为空的话,我们就把这个条件加进去,但是因为我个人的一个开发习惯,我不是很喜欢去复用这些代码,除非在百分之八九十的可能性的情况下,已知不会产生后续的代码变更,所以我那样会选择复用,我不能保证的情况下更习惯的是单独写一个方法。

我们现在这一步的话,就是拆方法的过程,可以看到listStatsByShortLink和listStatsByGroup这两个方法,然后我们下面就有一个相同的方法叫listStatsByGroupWithDetails。Google的话就相当于我们把fullShortUrl参数给删掉,然后只有这里刚才那个没删掉,ok。我们要把公司的fullShortUrl参数要删掉,然后这边的话只有一个gid和date这样一个数据,然后这样的去访问。后面下面的下面都类似,我们把所有的方式都要字段全部通过查单个的方法请求里删掉,然后查询的不就是对应的一个分组信息对吧?

下面这些都是可以看到,我们之前都是掰着手头稍微说的,现在都是针对分组维度,比如说这里listTopIpByGroup,然后listBrowserStatsByGroup等等,我们重启一下服务,然后试一试这个代码功能。这个就是我们刚才那个group方法,这个是分组标识,然后这里的话基本上和我们之前预期是一致的,pv/uv/uip以及它的一个时间访问对吧?整体是没有任何问题的,大家到时候也可以按照我们本次提交的一些信息去做一些变更。

 admin的ShortLinkStatsController.java

    /**
     * 访问分组短链接指定时间内监控数据
     */
    @GetMapping("/api/short-link/admin/v1/stats/group")
    public Result<ShortLinkStatsRespDTO> groupShortLinkStats(ShortLinkGroupStatsReqDTO requestParam) {
        return shortLinkRemoteService.groupShortLinkStats(requestParam);
    }

 ShortLinkRemoteService.java

    /**
     * 访问分组短链接指定时间内监控数据
     *
     * @param requestParam 访分组问短链接监控请求参数
     * @return 分组短链接监控信息
     */
    default Result<ShortLinkStatsRespDTO> groupShortLinkStats(ShortLinkGroupStatsReqDTO requestParam) {
        String resultBodyStr = HttpUtil.get("http://127.0.0.1:8001/api/short-link/v1/stats/group", BeanUtil.beanToMap(requestParam));
        return JSON.parseObject(resultBodyStr, new TypeReference<>() {
        });
    }

admin的req新建ShortLinkGroupStatsReqDTO.java 

package com.nageoffer.shortlink.admin.remote.dto.req;
import lombok.Data;

/**
 * 分组短链接监控请求参数
 */
@Data
public class ShortLinkGroupStatsReqDTO {

    /**
     * 分组标识
     */
    private String gid;

    /**
     * 开始日期
     */
    private String startDate;

    /**
     * 结束日期
     */
    private String endDate;
}

 project的ShortLinkStatsController.java

    /**
     * 访问分组短链接指定时间内监控数据
     */
    @GetMapping("/api/short-link/v1/stats/group")
    public Result<ShortLinkStatsRespDTO> groupShortLinkStats(ShortLinkGroupStatsReqDTO requestParam) {
        return Results.success(shortLinkStatsService.groupShortLinkStats(requestParam));
    }

 LinkAccessLogsMapper.java加两个方法


    /**
     * 根据分组获取指定日期内高频访问IP数据
     */
    @Select("SELECT " +
            "    ip, " +
            "    COUNT(ip) AS count " +
            "FROM " +
            "    t_link_access_logs " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND create_time BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid, ip " +
            "ORDER BY " +
            "    count DESC " +
            "LIMIT 5;")
    List<HashMap<String, Object>> listTopIpByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);



   /**
     * 根据分组获取指定日期内PV、UV、UIP数据
     */
    @Select("SELECT " +
            "    COUNT(user) AS pv, " +
            "    COUNT(DISTINCT user) AS uv, " +
            "    COUNT(DISTINCT ip) AS uip " +
            "FROM " +
            "    t_link_access_logs " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND create_time BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid;")
    LinkAccessStatsDO findPvUvUidStatsByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);

 LinkAccessStatsMapper.java同理

   /**
     * 根据分组获取指定日期内基础监控数据
     */
    @Select("SELECT " +
            "    date, " +
            "    SUM(pv) AS pv, " +
            "    SUM(uv) AS uv, " +
            "    SUM(uip) AS uip " +
            "FROM " +
            "    t_link_access_stats " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid, date;")
    List<LinkAccessStatsDO> listStatsByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);





    /**
     * 根据分组获取指定日期内小时基础监控数据
     */
    @Select("SELECT " +
            "    hour, " +
            "    SUM(pv) AS pv " +
            "FROM " +
            "    t_link_access_stats " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid, hour;")
    List<LinkAccessStatsDO> listHourStatsByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);





    /**
     * 根据分组获取指定日期内week基础监控数据
     */
    @Select("SELECT " +
            "    weekday, " +
            "    SUM(pv) AS pv " +
            "FROM " +
            "    t_link_access_stats " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid, weekday;")
    List<LinkAccessStatsDO> listWeekdayStatsByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);

LinkBrowserStatsMapper.java

    /**
     * 根据分组获取指定日期内浏览器监控数据
     */
    @Select("SELECT " +
            "    browser, " +
            "    SUM(cnt) AS count " +
            "FROM " +
            "    t_link_browser_stats " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid, date, browser;")
    List<HashMap<String, Object>> listBrowserStatsByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);

 LinkDeviceStatsMapper.java


    /**
     * 根据分组获取指定日期内访问设备监控数据
     */
    @Select("SELECT " +
            "    device, " +
            "    SUM(cnt) AS cnt " +
            "FROM " +
            "    t_link_device_stats " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid, device;")
    List<LinkDeviceStatsDO> listDeviceStatsByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);

LinkLocaleStatsMapperjava 


    /**
     * 根据分组获取指定日期内地区监控数据
     */
    @Select("SELECT " +
            "    province, " +
            "    SUM(cnt) AS cnt " +
            "FROM " +
            "    t_link_locale_stats " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid, province;")
    List<LinkLocaleStatsDO> listLocaleByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);

LinkNetworkStatsMapper.java

  /**
     * 根据分组获取指定日期内访问网络监控数据
     */
    @Select("SELECT " +
            "    network, " +
            "    SUM(cnt) AS cnt " +
            "FROM " +
            "    t_link_network_stats " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid, network;")
    List<LinkNetworkStatsDO> listNetworkStatsByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);

 LinkOsStatsMapperjava

   /**
     * 根据分组获取指定日期内操作系统监控数据
     */
    @Select("SELECT " +
            "    os, " +
            "    SUM(cnt) AS count " +
            "FROM " +
            "    t_link_os_stats " +
            "WHERE " +
            "    gid = #{param.gid} " +
            "    AND date BETWEEN #{param.startDate} and #{param.endDate} " +
            "GROUP BY " +
            "    gid, os;")
    List<HashMap<String, Object>> listOsStatsByGroup(@Param("param") ShortLinkGroupStatsReqDTO requestParam);

project里面也建一个 

package com.nageoffer.shortlink.project.dto.req;


import lombok.Data;

/**
 * 分组短链接监控请求参数
 */
@Data
public class ShortLinkGroupStatsReqDTO {

    /**
     * 分组标识
     */
    private String gid;

    /**
     * 开始日期
     */
    private String startDate;

    /**
     * 结束日期
     */
    private String endDate;
}

 ShortLinkStatsService.java添加方法

    /**
     * 获取分组短链接监控数据
     *
     * @param requestParam 获取分组短链接监控数据入参
     * @return 分组短链接监控数据
     */
    ShortLinkStatsRespDTO groupShortLinkStats(ShortLinkGroupStatsReqDTO requestParam);

ShortLinkStatsServicelmpl.java中补充对此方法的实现 


    @Override
    public ShortLinkStatsRespDTO groupShortLinkStats(ShortLinkGroupStatsReqDTO requestParam) {
        List<LinkAccessStatsDO> listStatsByGroup = linkAccessStatsMapper.listStatsByGroup(requestParam);
        if (CollUtil.isEmpty(listStatsByGroup)) {
            return null;
        }
        // 基础访问数据
        LinkAccessStatsDO pvUvUidStatsByGroup = linkAccessLogsMapper.findPvUvUidStatsByGroup(requestParam);
        // 基础访问详情
        List<ShortLinkStatsAccessDailyRespDTO> daily = new ArrayList<>();
        List<String> rangeDates = DateUtil.rangeToList(DateUtil.parse(requestParam.getStartDate()), DateUtil.parse(requestParam.getEndDate()), DateField.DAY_OF_MONTH).stream()
                .map(DateUtil::formatDate)
                .toList();
        rangeDates.forEach(each -> listStatsByGroup.stream()
                .filter(item -> Objects.equals(each, DateUtil.formatDate(item.getDate())))
                .findFirst()
                .ifPresentOrElse(item -> {
                    ShortLinkStatsAccessDailyRespDTO accessDailyRespDTO = ShortLinkStatsAccessDailyRespDTO.builder()
                            .date(each)
                            .pv(item.getPv())
                            .uv(item.getUv())
                            .uip(item.getUip())
                            .build();
                    daily.add(accessDailyRespDTO);
                }, () -> {
                    ShortLinkStatsAccessDailyRespDTO accessDailyRespDTO = ShortLinkStatsAccessDailyRespDTO.builder()
                            .date(each)
                            .pv(0)
                            .uv(0)
                            .uip(0)
                            .build();
                    daily.add(accessDailyRespDTO);
                }));
        // 地区访问详情(仅国内)
        List<ShortLinkStatsLocaleCNRespDTO> localeCnStats = new ArrayList<>();
        List<LinkLocaleStatsDO> listedLocaleByGroup = linkLocaleStatsMapper.listLocaleByGroup(requestParam);
        int localeCnSum = listedLocaleByGroup.stream()
                .mapToInt(LinkLocaleStatsDO::getCnt)
                .sum();
        listedLocaleByGroup.forEach(each -> {
            double ratio = (double) each.getCnt() / localeCnSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsLocaleCNRespDTO localeCNRespDTO = ShortLinkStatsLocaleCNRespDTO.builder()
                    .cnt(each.getCnt())
                    .locale(each.getProvince())
                    .ratio(actualRatio)
                    .build();
            localeCnStats.add(localeCNRespDTO);
        });
        // 小时访问详情
        List<Integer> hourStats = new ArrayList<>();
        List<LinkAccessStatsDO> listHourStatsByGroup = linkAccessStatsMapper.listHourStatsByGroup(requestParam);
        for (int i = 0; i < 24; i++) {
            AtomicInteger hour = new AtomicInteger(i);
            int hourCnt = listHourStatsByGroup.stream()
                    .filter(each -> Objects.equals(each.getHour(), hour.get()))
                    .findFirst()
                    .map(LinkAccessStatsDO::getPv)
                    .orElse(0);
            hourStats.add(hourCnt);
        }
        // 高频访问IP详情
        List<ShortLinkStatsTopIpRespDTO> topIpStats = new ArrayList<>();
        List<HashMap<String, Object>> listTopIpByGroup = linkAccessLogsMapper.listTopIpByGroup(requestParam);
        listTopIpByGroup.forEach(each -> {
            ShortLinkStatsTopIpRespDTO statsTopIpRespDTO = ShortLinkStatsTopIpRespDTO.builder()
                    .ip(each.get("ip").toString())
                    .cnt(Integer.parseInt(each.get("count").toString()))
                    .build();
            topIpStats.add(statsTopIpRespDTO);
        });
        // 一周访问详情
        List<Integer> weekdayStats = new ArrayList<>();
        List<LinkAccessStatsDO> listWeekdayStatsByGroup = linkAccessStatsMapper.listWeekdayStatsByGroup(requestParam);
        for (int i = 1; i < 8; i++) {
            AtomicInteger weekday = new AtomicInteger(i);
            int weekdayCnt = listWeekdayStatsByGroup.stream()
                    .filter(each -> Objects.equals(each.getWeekday(), weekday.get()))
                    .findFirst()
                    .map(LinkAccessStatsDO::getPv)
                    .orElse(0);
            weekdayStats.add(weekdayCnt);
        }
        // 浏览器访问详情
        List<ShortLinkStatsBrowserRespDTO> browserStats = new ArrayList<>();
        List<HashMap<String, Object>> listBrowserStatsByGroup = linkBrowserStatsMapper.listBrowserStatsByGroup(requestParam);
        int browserSum = listBrowserStatsByGroup.stream()
                .mapToInt(each -> Integer.parseInt(each.get("count").toString()))
                .sum();
        listBrowserStatsByGroup.forEach(each -> {
            double ratio = (double) Integer.parseInt(each.get("count").toString()) / browserSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsBrowserRespDTO browserRespDTO = ShortLinkStatsBrowserRespDTO.builder()
                    .cnt(Integer.parseInt(each.get("count").toString()))
                    .browser(each.get("browser").toString())
                    .ratio(actualRatio)
                    .build();
            browserStats.add(browserRespDTO);
        });
        // 操作系统访问详情
        List<ShortLinkStatsOsRespDTO> osStats = new ArrayList<>();
        List<HashMap<String, Object>> listOsStatsByGroup = linkOsStatsMapper.listOsStatsByGroup(requestParam);
        int osSum = listOsStatsByGroup.stream()
                .mapToInt(each -> Integer.parseInt(each.get("count").toString()))
                .sum();
        listOsStatsByGroup.forEach(each -> {
            double ratio = (double) Integer.parseInt(each.get("count").toString()) / osSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsOsRespDTO osRespDTO = ShortLinkStatsOsRespDTO.builder()
                    .cnt(Integer.parseInt(each.get("count").toString()))
                    .os(each.get("os").toString())
                    .ratio(actualRatio)
                    .build();
            osStats.add(osRespDTO);
        });
        // 访问设备类型详情
        List<ShortLinkStatsDeviceRespDTO> deviceStats = new ArrayList<>();
        List<LinkDeviceStatsDO> listDeviceStatsByGroup = linkDeviceStatsMapper.listDeviceStatsByGroup(requestParam);
        int deviceSum = listDeviceStatsByGroup.stream()
                .mapToInt(LinkDeviceStatsDO::getCnt)
                .sum();
        listDeviceStatsByGroup.forEach(each -> {
            double ratio = (double) each.getCnt() / deviceSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsDeviceRespDTO deviceRespDTO = ShortLinkStatsDeviceRespDTO.builder()
                    .cnt(each.getCnt())
                    .device(each.getDevice())
                    .ratio(actualRatio)
                    .build();
            deviceStats.add(deviceRespDTO);
        });
        // 访问网络类型详情
        List<ShortLinkStatsNetworkRespDTO> networkStats = new ArrayList<>();
        List<LinkNetworkStatsDO> listNetworkStatsByGroup = linkNetworkStatsMapper.listNetworkStatsByGroup(requestParam);
        int networkSum = listNetworkStatsByGroup.stream()
                .mapToInt(LinkNetworkStatsDO::getCnt)
                .sum();
        listNetworkStatsByGroup.forEach(each -> {
            double ratio = (double) each.getCnt() / networkSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsNetworkRespDTO networkRespDTO = ShortLinkStatsNetworkRespDTO.builder()
                    .cnt(each.getCnt())
                    .network(each.getNetwork())
                    .ratio(actualRatio)
                    .build();
            networkStats.add(networkRespDTO);
        });
        return ShortLinkStatsRespDTO.builder()
                .pv(pvUvUidStatsByGroup.getPv())
                .uv(pvUvUidStatsByGroup.getUv())
                .uip(pvUvUidStatsByGroup.getUip())
                .daily(daily)
                .localeCnStats(localeCnStats)
                .hourStats(hourStats)
                .topIpStats(topIpStats)
                .weekdayStats(weekdayStats)
                .browserStats(browserStats)
                .osStats(osStats)
                .deviceStats(deviceStats)
                .networkStats(networkStats)
                .build();
    }

分组监控中浏览器的统计 SQL 应该将 date 分组条件删掉,详情查看下述提交。 fix: 修复短链接获取浏览器监控数据中的日期问题 nageoffer/shortlink - Gitee.com

- 第22节:分页查询分组短链接访问日记

 之前做的我们只需要跟改上节课的结尾,我们讲到了这节课要做短链接,就是分组监控的一个访问日志功能,我们可以看一下访问日志其实就是之前做的,我们只需要跟改单个监控一样给它改成一个group就可以了。这边访问分组关链接指定时间内,到时候然后这边的话老规矩复用。admin的ShortLinkStatsController.java添加

/**
 * 访问分组短链接指定时间内访问记录监控数据
 */
@GetMapping("/api/short-link/admin/v1/stats/access-record/group")
public Result<IPage<ShortLinkStatsAccessRecordRespDTO>> groupShortLinkStatsAccessRecord(ShortLinkGroupStatsAccessRecordReqDTO requestParam) {
    return shortLinkRemoteService.groupShortLinkStatsAccessRecord(requestParam);
}

remote里面req添加 

package com.nageoffer.shortlink.admin.remote.dto.req;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;

/**
 * 分组短链接监控访问记录请求参数
 */
@Data
public class ShortLinkGroupStatsAccessRecordReqDTO extends Page {

    /**
     * 分组标识
     */
    private String gid;

    /**
     * 开始日期
     */
    private String startDate;

    /**
     * 结束日期
     */
    private String endDate;
}

remoteservice层生成。

/**
 * 访问分组短链接指定时间内监控访问记录数据
 *
 * @param requestParam 访问分组短链接监控访问记录请求参数
 * @return 分组短链接监控访问记录信息
 */
default Result<IPage<ShortLinkStatsAccessRecordRespDTO>> groupShortLinkStatsAccessRecord(ShortLinkGroupStatsAccessRecordReqDTO requestParam) {
    Map<String, Object> stringObjectMap = BeanUtil.beanToMap(requestParam, false, true);
    stringObjectMap.remove("orders");
    stringObjectMap.remove("records");
    String resultBodyStr = HttpUtil.get("http://127.0.0.1:8001/api/short-link/v1/stats/access-record/group", stringObjectMap);
    return JSON.parseObject(resultBodyStr, new TypeReference<>() {
    });
}

这个人也够长的了。四五百行代码了。project的ShortLinkStatsServicelmpl.java里面生成,还有project的dto

package com.nageoffer.shortlink.project.dto.req;


import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nageoffer.shortlink.project.dao.entiry.LinkAccessLogsDO;
import lombok.Data;

/**
 * 分组短链接监控访问记录请求参数
 */
@Data
public class ShortLinkGroupStatsAccessRecordReqDTO extends Page<LinkAccessLogsDO> {

    /**
     * 分组标识
     */
    private String gid;

    /**
     * 开始日期
     */
    private String startDate;

    /**
     * 结束日期
     */
    private String endDate;
}

 project的service如下

/**
 * 访问分组短链接指定时间内访问记录监控数据
 *
 * @param requestParam 获取分组短链接监控访问记录数据入参
 * @return 分组访问记录监控数据
 */
IPage<ShortLinkStatsAccessRecordRespDTO> groupShortLinkStatsAccessRecord(ShortLinkGroupStatsAccessRecordReqDTO requestParam);
@Override
public IPage<ShortLinkStatsAccessRecordRespDTO> groupShortLinkStatsAccessRecord(ShortLinkGroupStatsAccessRecordReqDTO requestParam) {
    LambdaQueryWrapper<LinkAccessLogsDO> queryWrapper = Wrappers.lambdaQuery(LinkAccessLogsDO.class)
            .eq(LinkAccessLogsDO::getGid, requestParam.getGid())
            .between(LinkAccessLogsDO::getCreateTime, requestParam.getStartDate(), requestParam.getEndDate())
            .eq(LinkAccessLogsDO::getDelFlag, 0)
            .orderByDesc(LinkAccessLogsDO::getCreateTime);
    IPage<LinkAccessLogsDO> linkAccessLogsDOIPage = linkAccessLogsMapper.selectPage(requestParam, queryWrapper);
    IPage<ShortLinkStatsAccessRecordRespDTO> actualResult = linkAccessLogsDOIPage.convert(each -> BeanUtil.toBean(each, ShortLinkStatsAccessRecordRespDTO.class));
    List<String> userAccessLogsList = actualResult.getRecords().stream()
            .map(ShortLinkStatsAccessRecordRespDTO::getUser)
            .toList();
    List<Map<String, Object>> uvTypeList = linkAccessLogsMapper.selectGroupUvTypeByUsers(
            requestParam.getGid(),
            requestParam.getStartDate(),
            requestParam.getEndDate(),
            userAccessLogsList
    );
    actualResult.getRecords().forEach(each -> {
        String uvType = uvTypeList.stream()
                .filter(item -> Objects.equals(each.getUser(), item.get("user")))
                .findFirst()
                .map(item -> item.get("uvType"))
                .map(Object::toString)
                .orElse("旧访客");
        each.setUvType(uvType);
    });
    return actualResult;
}

project的LinkAccessLogsMapper.java 

/**
 * 获取分组用户信息是否新老访客
 */
@Select("<script> " +
        "SELECT " +
        "    user, " +
        "    CASE " +
        "        WHEN MIN(create_time) BETWEEN #{startDate} AND #{endDate} THEN '新访客' " +
        "        ELSE '老访客' " +
        "    END AS uvType " +
        "FROM " +
        "    t_link_access_logs " +
        "WHERE " +
        "    gid = #{gid} " +
        "    AND user IN " +
        "    <foreach item='item' index='index' collection='userAccessLogsList' open='(' separator=',' close=')'> " +
        "        #{item} " +
        "    </foreach> " +
        "GROUP BY " +
        "    user;" +
        "    </script>"
)
List<Map<String, Object>> selectGroupUvTypeByUsers(
        @Param("gid") String gid,
        @Param("startDate") String startDate,
        @Param("endDate") String endDate,
        @Param("userAccessLogsList") List<String> userAccessLogsList
);

然后我们把给他搞到这里来,然后从Controller里面去复制对应的方法,

 project 的ShortLinkStatsController.java

/**
 * 访问分组短链接指定时间内访问记录监控数据
 */
@GetMapping("/api/short-link/v1/stats/access-record/group")
public Result<IPage<ShortLinkStatsAccessRecordRespDTO>> groupShortLinkStatsAccessRecord(ShortLinkGroupStatsAccessRecordReqDTO requestParam) {
    return Results.success(shortLinkStatsService.groupShortLinkStatsAccessRecord(requestParam));
}

如果是直接复制LinkAccessLogsMapper的selectGroupPage代码,要注意马哥不小心把表名写错了(拉取的最新代码还没改过来),要把t_link_1改成t_link :已Fix

 按照gitee拉取这个版本代码的同学 如果访客类型有问题 是project/src/main/java/com/nageoffer/shortlink/project/service/impl/ShortLinkStatsServiceImpl类的groupShortLinkStatsAccessRecord方法里最下面.map(item -> item.get("UvType")) 里面的"UvType"应该是"uvType" ​