feat(user): 展示用户最近使用的系统

This commit is contained in:
Zhou Weidong committed 2026-10-23 12:34:13 +08:00
1 parent 45e3d84f59
commit ab58641bbe
12 files changed
+212 -8

No files matched your search

@@ -4,6 +4,7 @@ package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.admin.service.AppSessionSystemService;
import com.fruitshorts.admin.vo.AppUserWatchHistoryVO;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaEpisode;
@@ -13,6 +14,7 @@ import com.fruitshorts.content.service.EpisodeService;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.playback.dal.entity.WatchHistory;
import com.fruitshorts.user.dal.entity.AppUserSession;
import com.fruitshorts.playback.service.WatchProgressService;
import com.fruitshorts.user.dal.entity.AppUserBanRecord;
import com.fruitshorts.user.service.AppUserAdminService;
@@ -44,6 +46,7 @@ public class AppUserAdminController {
private final WatchProgressService watchProgressService;
private final DramaService dramaService;
private final EpisodeService episodeService;
private final AppSessionSystemService sessionSystem;
@Operation(summary = "App 用户分页查询")
@GetMapping("/page")
@@ -83,11 +86,15 @@ public class AppUserAdminController {
Map<Long, DramaEpisode> episodes = episodeService
.listByIds(rows.stream().map(WatchHistory::getEpisodeId).distinct().toList())
.stream().collect(Collectors.toMap(DramaEpisode::getId, item -> item, (a, b) -> a));
return Result.ok(rows.stream().map(row -> toVo(row, dramas, episodes)).toList());
List<AppUserSession> sessions = sessionSystem.groupByUser(List.of(id))
.getOrDefault(id, List.of());
return Result.ok(rows.stream()
.map(row -> toVo(row, dramas, episodes, sessionSystem.match(sessions, row.getDeviceId())))
.toList());
}
private AppUserWatchHistoryVO toVo(WatchHistory row, Map<Long, Drama> dramas,
Map<Long, DramaEpisode> episodes) {
Map<Long, DramaEpisode> episodes, AppUserSession session) {
AppUserWatchHistoryVO vo = new AppUserWatchHistoryVO();
vo.setId(row.getId());
vo.setDramaId(row.getDramaId());
@@ -100,6 +107,10 @@ public class AppUserAdminController {
long position = row.getPositionMs() == null ? 0L : row.getPositionMs();
long duration = row.getDurationMs() == null ? 0L : row.getDurationMs();
vo.setFinished(duration > 0 && position + 1000L >= duration);
if (session != null) {
vo.setPlatform(session.getPlatform());
vo.setOsVersion(session.getOsVersion());
}
Drama drama = dramas.get(row.getDramaId());
if (drama != null) {
vo.setDramaTitle(drama.getTitle());
@@ -0,0 +1,62 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fruitshorts.user.dal.entity.AppUserSession;
import com.fruitshorts.user.dal.mapper.AppUserSessionMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 按用户最近会话解析系统。同一设备优先,否则用仍有效的最近一次登录。
*/
@Service
@RequiredArgsConstructor
public class AppSessionSystemService {
private final AppUserSessionMapper sessionMapper;
/** 按用户分组,组内按最近活跃倒序。 */
public Map<Long, List<AppUserSession>> groupByUser(Collection<Long> userIds) {
Map<Long, List<AppUserSession>> grouped = new HashMap<>();
if (userIds == null || userIds.isEmpty()) {
return grouped;
}
List<AppUserSession> sessions = sessionMapper.selectList(Wrappers.<AppUserSession>lambdaQuery()
.in(AppUserSession::getUserId, userIds)
.orderByDesc(AppUserSession::getLastActiveAt)
.orderByDesc(AppUserSession::getId));
for (AppUserSession session : sessions) {
grouped.computeIfAbsent(session.getUserId(), key -> new ArrayList<>()).add(session);
}
return grouped;
}
/** 观看记录优先匹配同一设备,其次取未吊销会话。 */
public AppUserSession match(List<AppUserSession> sessions, String deviceId) {
if (sessions == null || sessions.isEmpty()) {
return null;
}
if (StringUtils.hasText(deviceId)) {
for (AppUserSession session : sessions) {
if (deviceId.equals(session.getDeviceId())) {
return session;
}
}
}
for (AppUserSession session : sessions) {
if (session.getRevokedAt() == null) {
return session;
}
}
return sessions.get(0);
}
}
@@ -17,6 +17,7 @@ import com.fruitshorts.content.enums.ContentStatus;
import com.fruitshorts.playback.dal.entity.WatchHistory;
import com.fruitshorts.playback.dal.mapper.WatchHistoryMapper;
import com.fruitshorts.user.dal.entity.AppUser;
import com.fruitshorts.user.dal.entity.AppUserSession;
import com.fruitshorts.user.dal.mapper.AppUserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -46,6 +47,7 @@ public class WatchHistoryAdminService {
private final AppUserMapper appUserMapper;
private final DramaMapper dramaMapper;
private final DramaEpisodeMapper dramaEpisodeMapper;
private final AppSessionSystemService sessionSystem;
/** 按后台筛选条件分页查询观看记录。 */
public PageResult<AdminWatchHistoryVO> page(WatchHistoryQueryReqVO req) {
@@ -125,10 +127,13 @@ public class WatchHistoryAdminService {
Map<Long, AppUser> users = loadUsers(rows);
Map<Long, Drama> dramas = loadDramas(rows);
Map<Long, DramaEpisode> episodes = loadEpisodes(rows);
Map<Long, List<AppUserSession>> sessions = sessionSystem.groupByUser(
rows.stream().map(WatchHistory::getUserId).distinct().toList());
List<AdminWatchHistoryVO> list = new ArrayList<>(rows.size());
for (WatchHistory row : rows) {
list.add(toVo(row, users.get(row.getUserId()), dramas.get(row.getDramaId()),
episodes.get(row.getEpisodeId())));
episodes.get(row.getEpisodeId()),
sessionSystem.match(sessions.get(row.getUserId()), row.getDeviceId())));
}
return list;
}
@@ -161,7 +166,7 @@ public class WatchHistoryAdminService {
}
private AdminWatchHistoryVO toVo(WatchHistory row, AppUser user, Drama drama,
DramaEpisode episode) {
DramaEpisode episode, AppUserSession session) {
AdminWatchHistoryVO vo = new AdminWatchHistoryVO();
vo.setId(row.getId());
vo.setUserId(row.getUserId());
@@ -172,6 +177,10 @@ public class WatchHistoryAdminService {
vo.setSource(row.getSource());
vo.setDeviceId(row.getDeviceId());
vo.setWatchedAt(row.getWatchedAt());
if (session != null) {
vo.setPlatform(session.getPlatform());
vo.setOsVersion(session.getOsVersion());
}
long position = row.getPositionMs() == null ? 0L : row.getPositionMs();
long duration = row.getDurationMs() == null ? 0L : row.getDurationMs();
vo.setFinished(duration > 0L && position + FINISHED_TOLERANCE_MS >= duration);
@@ -29,5 +29,9 @@ public class AdminWatchHistoryVO {
private Boolean finished;
private String source;
private String deviceId;
/** ANDROID / IOS / WEB,来自该次设备的登录会话。 */
private String platform;
/** 系统版本,例如 Android 16。 */
private String osVersion;
private LocalDateTime watchedAt;
}
@@ -28,5 +28,9 @@ public class AppUserWatchHistoryVO {
private Boolean finished;
private String source;
private String deviceId;
/** ANDROID / IOS / WEB,来自该次设备的登录会话。 */
private String platform;
/** 系统版本。 */
private String osVersion;
private LocalDateTime watchedAt;
}
@@ -0,0 +1,48 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.service;
import com.fruitshorts.user.dal.entity.AppUserSession;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/** 观看记录匹配登录系统。 */
class AppSessionSystemServiceTest {
private final AppSessionSystemService service = new AppSessionSystemService(null);
@Test
void should_use_same_device_session_when_watch_history_has_device() {
AppUserSession older = session("device-1", "ANDROID", "14", null);
AppUserSession newer = session("device-2", "IOS", "18", null);
AppUserSession matched = service.match(List.of(newer, older), "device-1");
assertThat(matched.getPlatform()).isEqualTo("ANDROID");
assertThat(matched.getOsVersion()).isEqualTo("14");
}
@Test
void should_use_active_session_when_device_does_not_match() {
AppUserSession revoked = session("device-9", "WEB", "1", LocalDateTime.now());
AppUserSession active = session("device-2", "IOS", "18", null);
AppUserSession matched = service.match(List.of(revoked, active), "missing");
assertThat(matched.getPlatform()).isEqualTo("IOS");
}
private AppUserSession session(String deviceId, String platform, String osVersion,
LocalDateTime revokedAt) {
AppUserSession session = new AppUserSession();
session.setDeviceId(deviceId);
session.setPlatform(platform);
session.setOsVersion(osVersion);
session.setRevokedAt(revokedAt);
return session;
}
}
@@ -58,7 +58,7 @@ class WatchHistoryAdminServiceTest {
dramaMapper = mock(DramaMapper.class);
dramaEpisodeMapper = mock(DramaEpisodeMapper.class);
service = new WatchHistoryAdminService(watchHistoryMapper, appUserMapper, dramaMapper,
dramaEpisodeMapper);
dramaEpisodeMapper, mock(AppSessionSystemService.class));
}
@Test