feat(admin): 收藏/点赞/观看记录支持按客户端平台筛选

- 设备画像按 device_id 命中,登录用户按最近一次登录会话的平台命中
- 修正 orderBy 先于动态条件导致 SQL 拼接错误的问题
This commit is contained in:
jianghaifeng authored and trae-solo-agent committed 2026-10-27 14:27:10 +08:00
1 parent 229f1d0198
commit 8999ff395d
8 files changed
+118 -6

No files matched your search

@@ -15,6 +15,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
@@ -62,6 +63,31 @@ public class AppSessionSystemService {
return result;
}
/**
* 平台筛选的命中范围:设备平台直接给 device_id;登录用户的行为行不带 device_id,
* 只能按该用户登录会话的平台判定,两者取并集。
*/
public record PlatformScope(List<String> deviceIds, List<Long> userIds) {
public boolean empty() {
return deviceIds.isEmpty() && userIds.isEmpty();
}
}
/** 按平台解析命中范围;未传平台返回 null 表示不限制。 */
public PlatformScope platformScope(String platform) {
if (!StringUtils.hasText(platform)) {
return null;
}
String value = platform.trim().toUpperCase(Locale.ROOT);
List<String> deviceIds = profileMapper.selectList(Wrappers.<AppDeviceProfile>lambdaQuery()
.select(AppDeviceProfile::getDeviceId)
.eq(AppDeviceProfile::getPlatform, value))
.stream().map(AppDeviceProfile::getDeviceId).toList();
// 只认「最近一次登录」的平台,与列表里展示的系统保持一致
List<Long> userIds = sessionMapper.selectUserIdsByLatestPlatform(value);
return new PlatformScope(deviceIds, userIds);
}
/** 观看记录优先匹配同一设备,其次取未吊销会话。 */
public AppUserSession match(List<AppUserSession> sessions, String deviceId) {
if (sessions == null || sessions.isEmpty()) {
@@ -21,6 +21,7 @@ import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
@@ -42,6 +43,7 @@ public class EngagementAdminLookup {
private final DramaMapper dramaMapper;
private final AppDeviceProfileMapper deviceProfileMapper;
private final AppUserSessionMapper sessionMapper;
private final AppSessionSystemService sessionSystem;
/** 规范化页码和页大小。 */
public <T> Page<T> page(EngagementQueryReqVO req) {
@@ -132,6 +134,11 @@ public class EngagementAdminLookup {
return result;
}
/** 按平台解析命中范围;未传平台返回 null 表示不限制。 */
public AppSessionSystemService.PlatformScope platformScope(String platform) {
return sessionSystem.platformScope(platform);
}
private List<Long> keys(Collection<Long> ids) {
return ids.stream().filter(Objects::nonNull).distinct().toList();
}
@@ -36,6 +36,10 @@ public class FavoriteAdminService {
if (lookup.noMatch(dramaIds)) {
return PageResult.empty();
}
var scope = lookup.platformScope(req.getPlatform());
if (scope != null && scope.empty()) {
return PageResult.empty();
}
var query = Wrappers.<UserFavorite>lambdaQuery()
.eq(req.getUserId() != null, UserFavorite::getUserId, req.getUserId())
.in(userIds != null, UserFavorite::getUserId, userIds)
@@ -43,8 +47,22 @@ public class FavoriteAdminService {
.eq(req.getDramaId() != null, UserFavorite::getDramaId, req.getDramaId())
.in(dramaIds != null, UserFavorite::getDramaId, dramaIds)
.ge(req.getStartTime() != null, UserFavorite::getCreatedAt, req.getStartTime())
.le(req.getEndTime() != null, UserFavorite::getCreatedAt, req.getEndTime())
.orderByDesc(UserFavorite::getCreatedAt, UserFavorite::getId);
.le(req.getEndTime() != null, UserFavorite::getCreatedAt, req.getEndTime());
// orderBy 必须最后追加,否则条件会被拼到 ORDER BY 后面
if (scope != null) {
query.and(inner -> {
if (!scope.deviceIds().isEmpty()) {
inner.in(UserFavorite::getDeviceId, scope.deviceIds());
}
if (!scope.userIds().isEmpty()) {
if (!scope.deviceIds().isEmpty()) {
inner.or();
}
inner.in(UserFavorite::getUserId, scope.userIds());
}
});
}
query.orderByDesc(UserFavorite::getCreatedAt, UserFavorite::getId);
var result = favoriteMapper.selectPage(lookup.page(req), query);
return new PageResult<>(result.getTotal(), assemble(result.getRecords()));
}
@@ -69,6 +69,23 @@ public class LikeAdminService {
return PageResult.empty();
}
var query = buildQuery(req, users);
var scope = lookup.platformScope(req.getPlatform());
if (scope != null && scope.empty()) {
return PageResult.empty();
}
if (scope != null) {
query.and(inner -> {
if (!scope.deviceIds().isEmpty()) {
inner.in(ContentLike::getDeviceId, scope.deviceIds());
}
if (!scope.userIds().isEmpty()) {
if (!scope.deviceIds().isEmpty()) {
inner.or();
}
inner.in(ContentLike::getUserId, scope.userIds());
}
});
}
if (req.getDramaId() != null) {
query.and(q -> q.apply(DRAMA_MATCH, req.getDramaId()));
}
@@ -82,6 +99,8 @@ public class LikeAdminService {
if (req.getEpisodeId() != null) {
query.and(q -> q.apply(EPISODE_MATCH, req.getEpisodeId()));
}
// orderBy 必须最后追加,否则条件会被拼到 ORDER BY 后面
query.orderByDesc(ContentLike::getCreatedAt, ContentLike::getId);
var result = likeMapper.selectPage(lookup.page(req), query);
return new PageResult<>(result.getTotal(), assemble(result.getRecords()));
}
@@ -94,8 +113,7 @@ public class LikeAdminService {
.eq(StringUtils.hasText(req.getTargetType()), ContentLike::getTargetType, req.getTargetType())
.eq(req.getTargetId() != null, ContentLike::getTargetId, req.getTargetId())
.ge(req.getStartTime() != null, ContentLike::getCreatedAt, req.getStartTime())
.le(req.getEndTime() != null, ContentLike::getCreatedAt, req.getEndTime())
.orderByDesc(ContentLike::getCreatedAt, ContentLike::getId);
.le(req.getEndTime() != null, ContentLike::getCreatedAt, req.getEndTime());
}
private List<AdminLikeVO> assemble(List<ContentLike> rows) {
@@ -66,14 +66,19 @@ public class WatchHistoryAdminService {
if (matchedDramaIds != null && matchedDramaIds.isEmpty()) {
return emptyPage();
}
var scope = sessionSystem.platformScope(req.getPlatform());
if (scope != null && scope.empty()) {
return emptyPage();
}
IPage<WatchHistory> result = watchHistoryMapper.selectPage(new Page<>(pageIndex, pageSize),
buildQuery(req, matchedUserIds, matchedDramaIds));
buildQuery(req, matchedUserIds, matchedDramaIds, scope));
return new PageResult<>(result.getTotal(), toVoList(result.getRecords()));
}
private LambdaQueryWrapper<WatchHistory> buildQuery(WatchHistoryQueryReqVO req,
List<Long> matchedUserIds,
List<Long> matchedDramaIds) {
List<Long> matchedDramaIds,
AppSessionSystemService.PlatformScope scope) {
LambdaQueryWrapper<WatchHistory> wrapper = Wrappers.<WatchHistory>lambdaQuery()
.eq(req.getUserId() != null, WatchHistory::getUserId, req.getUserId())
.in(matchedUserIds != null, WatchHistory::getUserId, matchedUserIds)
@@ -83,6 +88,19 @@ public class WatchHistoryAdminService {
.eq(StringUtils.hasText(req.getSource()), WatchHistory::getSource, req.getSource())
.ge(req.getStartTime() != null, WatchHistory::getWatchedAt, req.getStartTime())
.le(req.getEndTime() != null, WatchHistory::getWatchedAt, req.getEndTime());
if (scope != null) {
wrapper.and(inner -> {
if (!scope.deviceIds().isEmpty()) {
inner.in(WatchHistory::getDeviceId, scope.deviceIds());
}
if (!scope.userIds().isEmpty()) {
if (!scope.deviceIds().isEmpty()) {
inner.or();
}
inner.in(WatchHistory::getUserId, scope.userIds());
}
});
}
applyFinished(wrapper, req.getFinished());
return wrapper.orderByDesc(WatchHistory::getWatchedAt)
.orderByDesc(WatchHistory::getId);
@@ -16,6 +16,8 @@ public class EngagementQueryReqVO {
private String keyword;
private Long userId;
private String deviceId;
/** 客户端平台筛选:ANDROID / IOS / WEB,留空不限制。 */
private String platform;
private String dramaTitle;
private Long dramaId;
@DateTimeFormat(pattern = TIME_PATTERN)
@@ -20,6 +20,8 @@ public class WatchHistoryQueryReqVO {
private String dramaTitle;
private Long dramaId;
private Long episodeId;
/** 客户端平台筛选:ANDROID / IOS / WEB,留空不限制。 */
private String platform;
private String source;
private Boolean finished;
@@ -4,6 +4,27 @@ package com.fruitshorts.user.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fruitshorts.user.dal.entity.AppUserSession;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface AppUserSessionMapper extends BaseMapper<AppUserSession> {
/**
* 最近一次登录会话平台等于指定值的用户。
* 必须取「最近一次」而不是「存在过」:否则同一用户在 Android 登录过、
* 后来又换 iOS,会被两端都命中,筛选结果与实际展示的系统不一致。
*/
@Select("""
SELECT t.user_id
FROM (
SELECT DISTINCT ON (user_id) user_id, platform
FROM app_user_session
WHERE deleted = 0
ORDER BY user_id, last_active_at DESC NULLS LAST, id DESC
) t
WHERE t.platform = #{platform}
""")
List<Long> selectUserIdsByLatestPlatform(@Param("platform") String platform);
}