fix(admin): 加固收藏与点赞列表查询

- 关键词/剧名预检索加 2000 条上限,避免 IN 列表超限并给出可操作提示
- 新增 created_at 分页索引迁移,避免无条件列表退化为全表排序
- dramaPublished 改为三态,短剧已不存在时不再误报未发布
This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-27 13:23:21 +08:00
1 parent 226dc275bb
commit c2ba3df47c
7 files changed
+59 -9

No files matched your search

@@ -5,6 +5,8 @@ package com.fruitshorts.admin.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.admin.vo.EngagementQueryReqVO;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.mapper.DramaMapper;
import com.fruitshorts.user.dal.entity.AppUser;
@@ -25,6 +27,12 @@ import java.util.stream.Collectors;
public class EngagementAdminLookup {
private static final int DEFAULT_PAGE_SIZE = 10;
private static final int MAX_PAGE_SIZE = 100;
/**
* 关键词预检索的 ID 上限:命中过多时 IN 列表会超出数据库参数上限,
* 也说明关键词过于宽泛,直接提示使用者补充条件而不是截断结果。
*/
private static final int MAX_LOOKUP_SIZE = 2000;
private static final int LOOKUP_PROBE = MAX_LOOKUP_SIZE + 1;
private final AppUserMapper userMapper;
private final DramaMapper dramaMapper;
@@ -42,9 +50,11 @@ public class EngagementAdminLookup {
return null;
}
String value = keyword.trim();
return userMapper.selectList(Wrappers.<AppUser>lambdaQuery().select(AppUser::getId)
.and(q -> q.like(AppUser::getUid, value).or().like(AppUser::getNickname, value)))
List<Long> ids = userMapper.selectList(Wrappers.<AppUser>lambdaQuery().select(AppUser::getId)
.and(q -> q.like(AppUser::getUid, value).or().like(AppUser::getNickname, value))
.last("LIMIT " + LOOKUP_PROBE))
.stream().map(AppUser::getId).toList();
return ensureLookupSize(ids, "uid / 昵称");
}
/** 按剧名预检索,未传剧名时不限制短剧。 */
@@ -52,8 +62,19 @@ public class EngagementAdminLookup {
if (!StringUtils.hasText(title)) {
return null;
}
return dramaMapper.selectList(Wrappers.<Drama>lambdaQuery().select(Drama::getId)
.like(Drama::getTitle, title.trim())).stream().map(Drama::getId).toList();
List<Long> ids = dramaMapper.selectList(Wrappers.<Drama>lambdaQuery().select(Drama::getId)
.like(Drama::getTitle, title.trim())
.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;
}
/** 区分无筛选和有筛选但无匹配。 */
@@ -66,7 +66,6 @@ public class FavoriteAdminService {
vo.setAnonymous(row.getUserId() == null);
vo.setDramaId(row.getDramaId());
vo.setCreatedAt(row.getCreatedAt());
vo.setDramaPublished(false);
if (user != null) {
vo.setUid(user.getUid());
vo.setNickname(user.getNickname());
@@ -77,6 +76,8 @@ public class FavoriteAdminService {
vo.setDramaStatus(drama.getStatus());
vo.setDramaPublished(ContentStatus.PUBLISHED.getCode().equals(drama.getStatus()));
}
// 短剧读不到(已删除或数据异常)时保持 dramaPublished 为 null,
// 与「未发布」区分开,前端只在明确 false 时才展示未发布标记。
return vo;
}
}
@@ -18,6 +18,7 @@ import com.fruitshorts.user.dal.entity.AppUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -33,6 +34,12 @@ public class LikeAdminService {
private static final String EPISODE = "EPISODE";
private static final String COMMENT = "COMMENT";
private static final int SUMMARY_LENGTH = 30;
/**
* 点赞目标是多态列(target_type + target_id),按短剧/剧集筛选必须跨表回溯,
* 无法用 LambdaQueryWrapper 表达,只能写原生片段。
* 维护约定:表名、逻辑删除列与 {@code drama_episode} / {@code content_comment}
* 实体上的 @TableName / @TableLogic 保持一致,改表结构时需要同步修改这里。
*/
private static final String DRAMA_MATCH =
"(target_type = 'DRAMA' AND target_id = {0}) OR "
+ "(target_type = 'EPISODE' AND EXISTS (SELECT 1 FROM drama_episode e "
@@ -95,11 +102,11 @@ public class LikeAdminService {
}
Map<Long, AppUser> users = lookup.users(rows.stream().map(ContentLike::getUserId).toList());
Map<Long, ContentComment> comments = comments(targetIds(rows, COMMENT));
List<Long> episodeIds = new java.util.ArrayList<>(targetIds(rows, EPISODE));
List<Long> episodeIds = new ArrayList<>(targetIds(rows, EPISODE));
comments.values().stream().map(ContentComment::getEpisodeId).filter(Objects::nonNull)
.forEach(episodeIds::add);
Map<Long, DramaEpisode> episodes = episodes(episodeIds);
List<Long> dramaIds = new java.util.ArrayList<>(targetIds(rows, DRAMA));
List<Long> dramaIds = new ArrayList<>(targetIds(rows, DRAMA));
comments.values().stream().map(ContentComment::getDramaId).filter(Objects::nonNull).forEach(dramaIds::add);
episodes.values().stream().map(DramaEpisode::getDramaId).filter(Objects::nonNull).forEach(dramaIds::add);
Map<Long, Drama> dramas = lookup.dramas(dramaIds);
@@ -18,6 +18,7 @@ public class AdminFavoriteVO {
private String dramaTitle;
private String dramaCover;
private String dramaStatus;
/** true=已发布,false=未发布,null=短剧已不存在(不代表未发布)。 */
private Boolean dramaPublished;
private LocalDateTime createdAt;
}
@@ -2,6 +2,11 @@
package com.fruitshorts.admin.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** 后台收藏分页检索条件。 */
@Data
@EqualsAndHashCode(callSuper = true)
public class FavoriteQueryReqVO extends EngagementQueryReqVO {
}
@@ -58,7 +58,7 @@ class EngagementAdminServiceTest {
}
@Test
void should_preserve_guest_and_unpublished_drama_when_favorite_has_no_user() {
void should_keep_drama_published_unknown_when_drama_missing() {
EngagementAdminLookup lookup = mock(EngagementAdminLookup.class);
UserFavoriteMapper mapper = mock(UserFavoriteMapper.class);
when(lookup.page(any())).thenReturn(new Page<>(1, 10));
@@ -75,7 +75,8 @@ class EngagementAdminServiceTest {
var result = new FavoriteAdminService(mapper, lookup).page(new FavoriteQueryReqVO());
assertThat(result.getList().get(0).getAnonymous()).isTrue();
assertThat(result.getList().get(0).getDeviceId()).isEqualTo("guest-device");
assertThat(result.getList().get(0).getDramaPublished()).isFalse();
// 短剧已不存在时不能误报成「未发布」
assertThat(result.getList().get(0).getDramaPublished()).isNull();
}
@Test