feat(admin): 收藏/点赞/观看记录支持按客户端平台筛选
- 设备画像按 device_id 命中,登录用户按最近一次登录会话的平台命中 - 修正 orderBy 先于动态条件导致 SQL 拼接错误的问题
This commit is contained in:
1 parent
229f1d0198
commit
8999ff395d
8 files changed
+118
-6
No files matched your search
+26
@@ -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()) {
|
||||
|
||||
+7
@@ -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();
|
||||
}
|
||||
|
||||
+20
-2
@@ -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()));
|
||||
}
|
||||
|
||||
+20
-2
@@ -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) {
|
||||
|
||||
+20
-2
@@ -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)
|
||||
|
||||
+2
@@ -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;
|
||||
|
||||
|
||||
+21
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user