perf(admin): 优化观看记录列表并统一时间格式
- 新增 watched_at 索引,默认倒序翻页走 Merge Append 而不是全表排序 - 装配只取展示所需列,会话查询不再拉 refresh_token_hash - 关键词预检索加 2000 条上限 - admin-api 的 LocalDateTime 统一输出 yyyy-MM-dd HH:mm:ss
This commit is contained in:
1 parent
8999ff395d
commit
a41ab18636
6 files changed
+93
-13
No files matched your search
+28
@@ -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));
|
||||
}
|
||||
}
|
||||
+3
@@ -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));
|
||||
|
||||
+2
@@ -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))) {
|
||||
|
||||
+45
-10
@@ -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;
|
||||
|
||||
+4
-3
@@ -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;
|
||||
Reference in new issue
Block a user