『博客开发日记』之升级记录访问日志接口

本文最后更新于 2026年5月17日 中午

升级记录访问日志接口


前言

在先前的访问记录中没有对操作人信息和地区信息进行收集

在数据表中新增 operator 和 region 字段用于记录

也方便后台记录


代码实现

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 访问日志表(VisitLog)表实体类
*
* @author makejava
* @since 2026-05-16 11:53:24
*/
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("visit_log")
public class VisitLog extends Model<VisitLog> {
@TableId
//日志ID
private Long id;
//访问者IP地址(不明文入库)
private String ip;
//操作人(游客/用户(显示用户名))
private String operator;
//访问的页面URL
private String pageUrl;
//页面标题
private String pageTitle;
//来源页面
private String referrer;
//设备类型(PC/Mobile/Tablet)
private String deviceType;
//地区(国内外)
private String region;
//浏览器类型
private String browser;
//操作系统
private String os;
//访问时间
private Date visitTime;
//页面执行时间(毫秒)
private Long executionTime;

}

在 SiteStatisticsServiceImpl 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
 //记录访问日志
@Override
public ResponseResult recordVisit(RecordVisitDto recordVisitDto, HttpServletRequest request)
{
long startTime = System.currentTimeMillis();
String ip = IpUtils.getIpAddr(request);
String userAgent = request.getHeader("User-Agent");
if (userAgent == null) {
userAgent = "";
}

// IP 限流:每个 IP 每分钟最多 30 次,超出直接拒绝
String rateLimitKey = "rate:recordVisit:" + ip + ":" + (System.currentTimeMillis() / 60000);
Long count = redisTemplate.opsForValue().increment(rateLimitKey);
if (count == 1) {
// 第一次写入,设置 70 秒过期
redisTemplate.expire(rateLimitKey, 70, java.util.concurrent.TimeUnit.SECONDS);
}
if (count > 30) {
throw new SystemException(AppHttpCodeEnum.IP_REQUEST_LIMIT);
}

// IP 脱敏:保留前三段,末段替换为 *(如 192.168.1.*)
String maskedIp = maskIp(ip);

// 判断操作人:登录用户显示用户名,否则显示“游客”
String operator = SystemConstants.OPERATOR_IS_VISITOR;
if (SecurityUtils.isLogin()) {
operator = Objects.requireNonNull(SecurityUtils.getLoginUser()).getUsername();
}

//根据ip获取地区信息
String region = IpLocationUtils.getCityByIp(ip);
if (region == null || region.isBlank()) {
region = SystemConstants.UNKNOWN;
}

// 访问日志写入 Redis 队列,由定时任务批量保存到数据库
VisitLog visitLog = new VisitLog();
visitLog.setIp(maskedIp);
visitLog.setOperator(operator);
visitLog.setRegion(region);

// 不存原始 UA,只保留解析后的设备信息
if (!userAgent.isEmpty()) {
visitLog.setDeviceType(parseDeviceType(userAgent));
visitLog.setBrowser(parseBrowser(userAgent));
visitLog.setOs(parseOs(userAgent));
}

// pageUrl 只保留路径部分,去除查询参数
String pageUrl = recordVisitDto.getPageUrl();
if (pageUrl != null && pageUrl.contains("?")) {
pageUrl = pageUrl.substring(0, pageUrl.indexOf('?'));
}
visitLog.setPageUrl(pageUrl);
visitLog.setPageTitle(recordVisitDto.getPageTitle());
visitLog.setReferrer(recordVisitDto.getReferrer());
visitLog.setMethod(request.getMethod());
visitLog.setVisitTime(new Date());
visitLog.setExecutionTime((int) (System.currentTimeMillis() - startTime));
redisCache.pushToList(SystemConstants.VISIT_LOG_QUEUE, visitLog);

// PV 计数递增
redisCache.incrementCacheMapValue(SystemConstants.SITE_TOTAL_VIEWS, SystemConstants.SITE_TOTAL_VIEWS_FIELD, 1);

// UV 去重:使用全站统一 key,保证 unique_visitor 统计的是全站唯一访客
String visitorHash = DigestUtils.md5DigestAsHex((ip + userAgent).getBytes(StandardCharsets.UTF_8));
String visitorKey = SystemConstants.SITE_VISITOR_SET_PREFIX;

// 如果是全站新访客就写入 Redis 和数据库
boolean isNewVisitor = redisCache.addToSet(visitorKey, visitorHash);
if (isNewVisitor) {
redisCache.incrementCacheMapValue(SystemConstants.SITE_TOTAL_VISITORS, SystemConstants.SITE_TOTAL_VISITORS_FIELD, 1);

UniqueVisitor uniqueVisitor = new UniqueVisitor();
uniqueVisitor.setVisitorHash(visitorHash);
uniqueVisitor.setIp(ip);
uniqueVisitor.setFirstVisitTime(new Date());
uniqueVisitor.setLastVisitTime(new Date());
uniqueVisitor.setVisitCount(1);
uniqueVisitorMapper.insert(uniqueVisitor);
} else {
// 已存在的全站唯一访客,更新最近访问时间和访问次数
UniqueVisitor uniqueVisitor = uniqueVisitorMapper.selectOne(new LambdaQueryWrapper<UniqueVisitor>()
.eq(UniqueVisitor::getVisitorHash, visitorHash)
.last("limit 1"));
if (uniqueVisitor != null) {
uniqueVisitor.setIp(ip);
uniqueVisitor.setLastVisitTime(new Date());
uniqueVisitor.setVisitCount(uniqueVisitor.getVisitCount() == null ? 1 : uniqueVisitor.getVisitCount() + 1);
uniqueVisitorMapper.updateById(uniqueVisitor);
}
}

return ResponseResult.okResult();
}

添加常量

1
2
3
4
5
6
7
8
/**
* 操作人为y游客
*/
public static final String OPERATOR_IS_VISITOR = "游客";
/**
* 未知 Unknown
*/
public static final String UNKNOWN = "unknown";




PS:该系列只做为作者学习开发项目做的笔记用

不一定符合读者来学习,仅供参考


预告

后续会记录博客的开发过程

每次学习会做一份笔记来进行发表

“一花一世界,一叶一菩提”


版权所有 © 2026 云梦泽
欢迎访问我的个人网站:https://hgt12.github.io/


『博客开发日记』之升级记录访问日志接口
http://example.com/2026/05/16/『博客开发日记』之升级记录访问日志接口/
作者
云梦泽
发布于
2026年5月16日
更新于
2026年5月17日
许可协议