perf(admin): 优化观看记录列表并统一时间格式

- 新增 watched_at 索引,默认倒序翻页走 Merge Append 而不是全表排序
- 装配只取展示所需列,会话查询不再拉 refresh_token_hash
- 关键词预检索加 2000 条上限
- admin-api 的 LocalDateTime 统一输出 yyyy-MM-dd HH:mm:ss
This commit is contained in:
jianghaifeng authored and trae-solo-agent committed 2026-10-27 14:39:13 +08:00
1 parent 8999ff395d
commit a41ab18636
6 files changed
+93 -13

No files matched your search

@@ -0,0 +1,28 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.config;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.format.DateTimeFormatter;
/**
* 后台接口的时间格式统一成 `yyyy-MM-dd HH:mm:ss`。
*
* <p>application.yml 里的 `spring.jackson.date-format` 只作用于 java.util.Date,
* LocalDateTime 仍会输出 ISO 的 `2026-10-27T14:14:12.644`,后台表格里会露出一串带 T 的原文。
* 该配置只放在 admin-api,C 端保持 ISO,避免影响客户端解析。
*/
@Configuration
public class AdminJacksonTimeConfig {
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Bean
public Jackson2ObjectMapperBuilderCustomizer adminLocalDateTimeFormat() {
return builder -> builder.serializers(new LocalDateTimeSerializer(DATE_TIME));
}
}
@@ -36,6 +36,9 @@ public class AppSessionSystemService {
return grouped;
}
List<AppUserSession> sessions = sessionMapper.selectList(Wrappers.<AppUserSession>lambdaQuery()
.select(AppUserSession::getId, AppUserSession::getUserId, AppUserSession::getDeviceId,
AppUserSession::getPlatform, AppUserSession::getOsVersion,
AppUserSession::getRevokedAt, AppUserSession::getLastActiveAt)
.in(AppUserSession::getUserId, userIds)
.orderByDesc(AppUserSession::getLastActiveAt)
.orderByDesc(AppUserSession::getId));
@@ -126,6 +126,8 @@ public class EngagementAdminLookup {
}
Map<Long, AppUserSession> result = new HashMap<>();
for (AppUserSession session : sessionMapper.selectList(Wrappers.<AppUserSession>lambdaQuery()
.select(AppUserSession::getId, AppUserSession::getUserId,
AppUserSession::getPlatform)
.in(AppUserSession::getUserId, keys)
.orderByDesc(AppUserSession::getLastActiveAt)
.orderByDesc(AppUserSession::getId))) {
@@ -8,6 +8,8 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.admin.vo.AdminWatchHistoryVO;
import com.fruitshorts.admin.vo.WatchHistoryQueryReqVO;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaEpisode;
@@ -28,6 +30,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 后台观看记录分页与跨域装配。
@@ -43,6 +46,8 @@ public class WatchHistoryAdminService {
private static final int DEFAULT_PAGE_SIZE = 10;
private static final int MAX_PAGE_SIZE = 100;
private static final long FINISHED_TOLERANCE_MS = 1000L;
private static final int MAX_LOOKUP_SIZE = 2000;
private static final int LOOKUP_PROBE = MAX_LOOKUP_SIZE + 1;
private final WatchHistoryMapper watchHistoryMapper;
private final AppUserMapper appUserMapper;
@@ -119,24 +124,37 @@ public class WatchHistoryAdminService {
private List<Long> searchUserIds(String keyword) {
String value = keyword.trim();
return appUserMapper.selectList(Wrappers.<AppUser>lambdaQuery()
List<Long> ids = appUserMapper.selectList(Wrappers.<AppUser>lambdaQuery()
.select(AppUser::getId)
.and(query -> query.like(AppUser::getUid, value)
.or()
.like(AppUser::getNickname, value)))
.like(AppUser::getNickname, value))
.last("LIMIT " + LOOKUP_PROBE))
.stream()
.map(AppUser::getId)
.toList();
return ensureLookupSize(ids, "uid / 昵称");
}
private List<Long> searchDramaIds(String dramaTitle) {
String value = dramaTitle.trim();
return dramaMapper.selectList(Wrappers.<Drama>lambdaQuery()
List<Long> ids = dramaMapper.selectList(Wrappers.<Drama>lambdaQuery()
.select(Drama::getId)
.like(Drama::getTitle, value))
.like(Drama::getTitle, value)
.last("LIMIT " + LOOKUP_PROBE))
.stream()
.map(Drama::getId)
.toList();
return ensureLookupSize(ids, "短剧名称");
}
/** 关键词命中过多时直接提示收窄条件,避免把整张表塞进 IN。 */
private List<Long> ensureLookupSize(List<Long> ids, String field) {
if (ids.size() > MAX_LOOKUP_SIZE) {
throw new ServiceException(ErrorCode.BAD_REQUEST,
field + "命中超过 " + MAX_LOOKUP_SIZE + " 条,请使用更精确的筛选条件");
}
return ids;
}
private List<AdminWatchHistoryVO> toVoList(List<WatchHistory> rows) {
@@ -162,8 +180,14 @@ public class WatchHistoryAdminService {
private Map<Long, AppUser> loadUsers(List<WatchHistory> rows) {
Map<Long, AppUser> result = new HashMap<>();
List<Long> ids = rows.stream().map(WatchHistory::getUserId).distinct().toList();
for (AppUser user : appUserMapper.selectBatchIds(ids)) {
List<Long> ids = rows.stream().map(WatchHistory::getUserId).filter(Objects::nonNull).distinct().toList();
if (ids.isEmpty()) {
return result;
}
// 列表只需要身份展示字段,避免把整行用户资料拉回来
for (AppUser user : appUserMapper.selectList(Wrappers.<AppUser>lambdaQuery()
.select(AppUser::getId, AppUser::getUid, AppUser::getNickname)
.in(AppUser::getId, ids))) {
result.put(user.getId(), user);
}
return result;
@@ -171,8 +195,13 @@ public class WatchHistoryAdminService {
private Map<Long, Drama> loadDramas(List<WatchHistory> rows) {
Map<Long, Drama> result = new HashMap<>();
List<Long> ids = rows.stream().map(WatchHistory::getDramaId).distinct().toList();
for (Drama drama : dramaMapper.selectBatchIds(ids)) {
List<Long> ids = rows.stream().map(WatchHistory::getDramaId).filter(Objects::nonNull).distinct().toList();
if (ids.isEmpty()) {
return result;
}
for (Drama drama : dramaMapper.selectList(Wrappers.<Drama>lambdaQuery()
.select(Drama::getId, Drama::getTitle, Drama::getStatus)
.in(Drama::getId, ids))) {
result.put(drama.getId(), drama);
}
return result;
@@ -180,8 +209,14 @@ public class WatchHistoryAdminService {
private Map<Long, DramaEpisode> loadEpisodes(List<WatchHistory> rows) {
Map<Long, DramaEpisode> result = new HashMap<>();
List<Long> ids = rows.stream().map(WatchHistory::getEpisodeId).distinct().toList();
for (DramaEpisode episode : dramaEpisodeMapper.selectBatchIds(ids)) {
List<Long> ids = rows.stream().map(WatchHistory::getEpisodeId).filter(Objects::nonNull).distinct().toList();
if (ids.isEmpty()) {
return result;
}
for (DramaEpisode episode : dramaEpisodeMapper.selectList(Wrappers.<DramaEpisode>lambdaQuery()
.select(DramaEpisode::getId, DramaEpisode::getDramaId,
DramaEpisode::getEpNo, DramaEpisode::getTitle)
.in(DramaEpisode::getId, ids))) {
result.put(episode.getId(), episode);
}
return result;
@@ -199,9 +199,10 @@ class WatchHistoryAdminServiceTest {
}
private void stubBatchData() {
given(appUserMapper.selectBatchIds(any())).willReturn(List.of(user(7L, "Alice")));
given(dramaMapper.selectBatchIds(any())).willReturn(List.of(drama(5L, "Signal")));
given(dramaEpisodeMapper.selectBatchIds(any())).willReturn(List.of(episode(11L)));
// 装配阶段按需列查询,而不是 selectBatchIds,桩法要跟着改
given(appUserMapper.selectList(any())).willReturn(List.of(user(7L, "Alice")));
given(dramaMapper.selectList(any())).willReturn(List.of(drama(5L, "Signal")));
given(dramaEpisodeMapper.selectList(any())).willReturn(List.of(episode(11L)));
}
@SuppressWarnings("unchecked")
@@ -0,0 +1,11 @@
-- Author: fruit.com.cn
-- K112 观看记录默认列表的排序索引。
-- 后台按 watched_at DESC, id DESC 倒序翻页,原有索引都以 user_id/drama_id/device_id 开头,
-- 不带筛选条件时只能全表扫描再排序;补一条时间索引让翻页直接走索引。
-- watch_history 是分区表,在父表建索引会自动下发到各分区。
BEGIN;
CREATE INDEX IF NOT EXISTS idx_watch_history_watched_at
ON watch_history (watched_at DESC, id DESC);
COMMIT;