feat(device): 按 device_id 记录客户端系统与机型
- 新增 app_device_profile 表与 upsert,匿名请求同样建档 - app-api 设备画像过滤器,同设备 60 秒节流写一次 - 收藏/点赞/观看列表优先用设备画像,缺失回退最近会话
This commit is contained in:
1 parent
d9a1670a72
commit
2cf8f9a1c3
13 files changed
+362
-9
No files matched your search
+35
@@ -0,0 +1,35 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.user.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备画像:按 device_id 记录客户端系统与机型。
|
||||
* 与会话不同,它不依赖用户身份,游客请求也会建档。
|
||||
*/
|
||||
@Data
|
||||
@TableName("app_device_profile")
|
||||
public class AppDeviceProfile {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String deviceId;
|
||||
private String platform;
|
||||
private String osVersion;
|
||||
private String deviceModel;
|
||||
private String appVersion;
|
||||
private LocalDateTime firstSeenAt;
|
||||
private LocalDateTime lastSeenAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.user.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.fruitshorts.user.dal.entity.AppDeviceProfile;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
public interface AppDeviceProfileMapper extends BaseMapper<AppDeviceProfile> {
|
||||
|
||||
/**
|
||||
* 按 device_id 建档或刷新。
|
||||
* 空字段不覆盖已有值:老版本客户端只带 deviceId,不能把已记录的系统抹掉。
|
||||
*/
|
||||
@Insert("""
|
||||
INSERT INTO app_device_profile (id, device_id, platform, os_version, device_model, app_version,
|
||||
first_seen_at, last_seen_at, created_at, updated_at, deleted)
|
||||
VALUES (#{p.id}, #{p.deviceId}, #{p.platform}, #{p.osVersion}, #{p.deviceModel}, #{p.appVersion},
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)
|
||||
ON CONFLICT (device_id) DO UPDATE SET
|
||||
platform = CASE WHEN EXCLUDED.platform <> '' THEN EXCLUDED.platform
|
||||
ELSE app_device_profile.platform END,
|
||||
os_version = CASE WHEN EXCLUDED.os_version <> '' THEN EXCLUDED.os_version
|
||||
ELSE app_device_profile.os_version END,
|
||||
device_model = CASE WHEN EXCLUDED.device_model <> '' THEN EXCLUDED.device_model
|
||||
ELSE app_device_profile.device_model END,
|
||||
app_version = CASE WHEN EXCLUDED.app_version <> '' THEN EXCLUDED.app_version
|
||||
ELSE app_device_profile.app_version END,
|
||||
last_seen_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""")
|
||||
int upsert(@Param("p") AppDeviceProfile profile);
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.user.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.fruitshorts.user.dal.entity.AppDeviceProfile;
|
||||
import com.fruitshorts.user.dal.mapper.AppDeviceProfileMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/** 设备画像写入:按 device_id 建档,匿名请求也记录。 */
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceProfileService {
|
||||
|
||||
private static final int DEVICE_ID_MAX = 128;
|
||||
private static final int PLATFORM_MAX = 16;
|
||||
private static final int OS_VERSION_MAX = 64;
|
||||
private static final int DEVICE_MODEL_MAX = 128;
|
||||
private static final int APP_VERSION_MAX = 32;
|
||||
|
||||
private final AppDeviceProfileMapper mapper;
|
||||
|
||||
/**
|
||||
* 记录或刷新设备画像。header 全部为空时直接跳过,
|
||||
* 避免老版本客户端只带 deviceId 时产生无意义的空记录。
|
||||
*/
|
||||
public void touch(String deviceId, String platform, String osVersion, String deviceModel, String appVersion) {
|
||||
if (!StringUtils.hasText(deviceId)) {
|
||||
return;
|
||||
}
|
||||
String cleanPlatform = sanitize(platform, PLATFORM_MAX).toUpperCase();
|
||||
String cleanOs = sanitize(osVersion, OS_VERSION_MAX);
|
||||
String cleanModel = sanitize(deviceModel, DEVICE_MODEL_MAX);
|
||||
String cleanApp = sanitize(appVersion, APP_VERSION_MAX);
|
||||
if (cleanPlatform.isEmpty() && cleanOs.isEmpty() && cleanModel.isEmpty() && cleanApp.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
AppDeviceProfile profile = new AppDeviceProfile();
|
||||
profile.setId(IdWorker.getId());
|
||||
profile.setDeviceId(sanitize(deviceId, DEVICE_ID_MAX));
|
||||
profile.setPlatform(cleanPlatform);
|
||||
profile.setOsVersion(cleanOs);
|
||||
profile.setDeviceModel(cleanModel);
|
||||
profile.setAppVersion(cleanApp);
|
||||
try {
|
||||
mapper.upsert(profile);
|
||||
} catch (RuntimeException ex) {
|
||||
// 画像只用于后台展示,写失败不能影响正常业务请求
|
||||
log.warn("device-profile upsert failed deviceId={} reason={}", profile.getDeviceId(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitize(String value, int maxLength) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return "";
|
||||
}
|
||||
String trimmed = value.trim().replaceAll("[\\p{Cntrl}]", "");
|
||||
return trimmed.length() <= maxLength ? trimmed : trimmed.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user