feat(device): 按 device_id 记录客户端系统与机型

- 新增 app_device_profile 表与 upsert,匿名请求同样建档
- app-api 设备画像过滤器,同设备 60 秒节流写一次
- 收藏/点赞/观看列表优先用设备画像,缺失回退最近会话
This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-27 13:53:47 +08:00
1 parent d9a1670a72
commit 2cf8f9a1c3
13 files changed
+362 -9

No files matched your search

@@ -0,0 +1,70 @@
// Author: fruit.com.cn
package com.fruitshorts.app.web;
import com.fruitshorts.user.service.DeviceProfileService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 收集客户端上报的设备画像(系统 / 机型 / 版本)。
*
* <p>客户端在请求头带 `X-Client-Platform`、`X-Client-Os`、`X-Client-Model`、`X-App-Version`,
* 这里按 device_id 建档,后台列表据此展示系统;游客请求同样入库,这是与旧会话方案的关键差异。
* 同一设备 60 秒内只写一次,避免每个请求都打库。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceProfileFilter extends OncePerRequestFilter {
private static final long THROTTLE_MS = 60_000L;
private static final int MAX_TRACKED_DEVICES = 20_000;
private final DeviceProfileService deviceProfileService;
private final Map<String, Long> lastWriteAt = new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
collect(request);
} catch (RuntimeException ex) {
// 画像只服务于后台展示,任何异常都不能影响业务请求
log.warn("device-profile collect failed reason={}", ex.getMessage());
}
filterChain.doFilter(request, response);
}
private void collect(HttpServletRequest request) {
String deviceId = request.getHeader("X-Device-Id");
if (!StringUtils.hasText(deviceId)) {
return;
}
long now = System.currentTimeMillis();
Long last = lastWriteAt.get(deviceId);
if (last != null && now - last < THROTTLE_MS) {
return;
}
if (lastWriteAt.size() > MAX_TRACKED_DEVICES) {
lastWriteAt.clear();
}
lastWriteAt.put(deviceId, now);
deviceProfileService.touch(deviceId,
request.getHeader("X-Client-Platform"),
request.getHeader("X-Client-Os"),
request.getHeader("X-Client-Model"),
request.getHeader("X-App-Version"));
}
}