feat(admin): 新增收藏与点赞只读查询接口
This commit is contained in:
1 parent
23321a0b0c
commit
7a2ff3f80e
14 files changed
+686
No files matched your search
+34
@@ -0,0 +1,34 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.admin.service.FavoriteAdminService;
|
||||
import com.fruitshorts.admin.vo.AdminFavoriteVO;
|
||||
import com.fruitshorts.admin.vo.FavoriteQueryReqVO;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 后台收藏只读查询入口。 */
|
||||
@Tag(name = "后台-收藏列表")
|
||||
@RestController
|
||||
@RequestMapping("/admin/favorites")
|
||||
@RequiredArgsConstructor
|
||||
public class FavoriteAdminController {
|
||||
private final FavoriteAdminService service;
|
||||
|
||||
/** 按用户、设备、内容和时间分页查询收藏。 */
|
||||
@Operation(summary = "收藏列表分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('favorite:list')")
|
||||
public Result<PageResult<AdminFavoriteVO>> page(@Valid FavoriteQueryReqVO req) {
|
||||
return Result.ok(service.page(req));
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.admin.service.LikeAdminService;
|
||||
import com.fruitshorts.admin.vo.AdminLikeVO;
|
||||
import com.fruitshorts.admin.vo.LikeQueryReqVO;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 后台点赞只读查询入口。 */
|
||||
@Tag(name = "后台-点赞列表")
|
||||
@RestController
|
||||
@RequestMapping("/admin/likes")
|
||||
@RequiredArgsConstructor
|
||||
public class LikeAdminController {
|
||||
private final LikeAdminService service;
|
||||
|
||||
/** 按用户、设备、内容和时间分页查询点赞。 */
|
||||
@Operation(summary = "点赞列表分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('like:list')")
|
||||
public Result<PageResult<AdminLikeVO>> page(@Valid LikeQueryReqVO req) {
|
||||
return Result.ok(service.page(req));
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
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.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.mapper.DramaMapper;
|
||||
import com.fruitshorts.user.dal.entity.AppUser;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 后台互动列表的分页、预检索及批量资料读取。 */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class EngagementAdminLookup {
|
||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||
private static final int MAX_PAGE_SIZE = 100;
|
||||
private final AppUserMapper userMapper;
|
||||
private final DramaMapper dramaMapper;
|
||||
|
||||
/** 规范化页码和页大小。 */
|
||||
public <T> Page<T> page(EngagementQueryReqVO req) {
|
||||
int index = req.getPageIndex() == null ? 1 : Math.max(1, req.getPageIndex());
|
||||
int size = req.getPageSize() == null || req.getPageSize() < 1
|
||||
? DEFAULT_PAGE_SIZE : Math.min(MAX_PAGE_SIZE, req.getPageSize());
|
||||
return new Page<>(index, size);
|
||||
}
|
||||
|
||||
/** 按 uid 或昵称预检索,未传关键词时不限制用户。 */
|
||||
public List<Long> userIds(String keyword) {
|
||||
if (!StringUtils.hasText(keyword)) {
|
||||
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)))
|
||||
.stream().map(AppUser::getId).toList();
|
||||
}
|
||||
|
||||
/** 按剧名预检索,未传剧名时不限制短剧。 */
|
||||
public List<Long> dramaIds(String title) {
|
||||
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();
|
||||
}
|
||||
|
||||
/** 区分无筛选和有筛选但无匹配。 */
|
||||
public boolean noMatch(List<Long> ids) {
|
||||
return ids != null && ids.isEmpty();
|
||||
}
|
||||
|
||||
/** 按非空去重 ID 批量读取用户,避免游客触发空 IN。 */
|
||||
public Map<Long, AppUser> users(Collection<Long> ids) {
|
||||
List<Long> keys = keys(ids);
|
||||
return keys.isEmpty() ? Map.of() : userMapper.selectBatchIds(keys).stream()
|
||||
.collect(Collectors.toMap(AppUser::getId, Function.identity()));
|
||||
}
|
||||
|
||||
/** 按非空去重 ID 批量读取短剧。 */
|
||||
public Map<Long, Drama> dramas(Collection<Long> ids) {
|
||||
List<Long> keys = keys(ids);
|
||||
return keys.isEmpty() ? Map.of() : dramaMapper.selectBatchIds(keys).stream()
|
||||
.collect(Collectors.toMap(Drama::getId, Function.identity()));
|
||||
}
|
||||
|
||||
private List<Long> keys(Collection<Long> ids) {
|
||||
return ids.stream().filter(Objects::nonNull).distinct().toList();
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fruitshorts.admin.vo.AdminFavoriteVO;
|
||||
import com.fruitshorts.admin.vo.FavoriteQueryReqVO;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.enums.ContentStatus;
|
||||
import com.fruitshorts.playback.dal.entity.UserFavorite;
|
||||
import com.fruitshorts.playback.dal.mapper.UserFavoriteMapper;
|
||||
import com.fruitshorts.user.dal.entity.AppUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** 后台收藏只读分页及跨域资料装配。 */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FavoriteAdminService {
|
||||
private final UserFavoriteMapper favoriteMapper;
|
||||
private final EngagementAdminLookup lookup;
|
||||
|
||||
/** 分页查询收藏,预检索无匹配时直接返回空页。 */
|
||||
public PageResult<AdminFavoriteVO> page(FavoriteQueryReqVO req) {
|
||||
List<Long> userIds = lookup.userIds(req.getKeyword());
|
||||
if (lookup.noMatch(userIds)) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
List<Long> dramaIds = lookup.dramaIds(req.getDramaTitle());
|
||||
if (lookup.noMatch(dramaIds)) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
var query = Wrappers.<UserFavorite>lambdaQuery()
|
||||
.eq(req.getUserId() != null, UserFavorite::getUserId, req.getUserId())
|
||||
.in(userIds != null, UserFavorite::getUserId, userIds)
|
||||
.eq(StringUtils.hasText(req.getDeviceId()), UserFavorite::getDeviceId, req.getDeviceId())
|
||||
.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);
|
||||
var result = favoriteMapper.selectPage(lookup.page(req), query);
|
||||
return new PageResult<>(result.getTotal(), assemble(result.getRecords()));
|
||||
}
|
||||
|
||||
private List<AdminFavoriteVO> assemble(List<UserFavorite> rows) {
|
||||
if (rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, AppUser> users = lookup.users(rows.stream().map(UserFavorite::getUserId).toList());
|
||||
Map<Long, Drama> dramas = lookup.dramas(rows.stream().map(UserFavorite::getDramaId).toList());
|
||||
return rows.stream().map(row -> toVo(row, row.getUserId() == null ? null : users.get(row.getUserId()),
|
||||
row.getDramaId() == null ? null : dramas.get(row.getDramaId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private AdminFavoriteVO toVo(UserFavorite row, AppUser user, Drama drama) {
|
||||
AdminFavoriteVO vo = new AdminFavoriteVO();
|
||||
vo.setId(row.getId());
|
||||
vo.setUserId(row.getUserId());
|
||||
vo.setDeviceId(row.getDeviceId());
|
||||
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());
|
||||
}
|
||||
if (drama != null) {
|
||||
vo.setDramaTitle(drama.getTitle());
|
||||
vo.setDramaCover(drama.getCoverUrl());
|
||||
vo.setDramaStatus(drama.getStatus());
|
||||
vo.setDramaPublished(ContentStatus.PUBLISHED.getCode().equals(drama.getStatus()));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fruitshorts.admin.vo.AdminLikeVO;
|
||||
import com.fruitshorts.admin.vo.LikeQueryReqVO;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
|
||||
import com.fruitshorts.interaction.dal.entity.ContentComment;
|
||||
import com.fruitshorts.interaction.dal.entity.ContentLike;
|
||||
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
|
||||
import com.fruitshorts.interaction.dal.mapper.ContentLikeMapper;
|
||||
import com.fruitshorts.user.dal.entity.AppUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 后台点赞只读分页及多态目标批量装配。 */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LikeAdminService {
|
||||
private static final String DRAMA = "DRAMA";
|
||||
private static final String EPISODE = "EPISODE";
|
||||
private static final String COMMENT = "COMMENT";
|
||||
private static final int SUMMARY_LENGTH = 30;
|
||||
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 "
|
||||
+ "WHERE e.id = target_id AND e.deleted = 0 AND e.drama_id = {0})) OR "
|
||||
+ "(target_type = 'COMMENT' AND EXISTS (SELECT 1 FROM content_comment c "
|
||||
+ "WHERE c.id = target_id AND c.deleted = 0 AND c.drama_id = {0}))";
|
||||
private static final String EPISODE_MATCH =
|
||||
"(target_type = 'EPISODE' AND target_id = {0}) OR "
|
||||
+ "(target_type = 'COMMENT' AND EXISTS (SELECT 1 FROM content_comment c "
|
||||
+ "WHERE c.id = target_id AND c.deleted = 0 AND c.episode_id = {0}))";
|
||||
|
||||
private final ContentLikeMapper likeMapper;
|
||||
private final ContentCommentMapper commentMapper;
|
||||
private final DramaEpisodeMapper episodeMapper;
|
||||
private final EngagementAdminLookup lookup;
|
||||
|
||||
/** 分页查询点赞;短剧及剧集条件在分页之前作用于各类目标。 */
|
||||
public PageResult<AdminLikeVO> page(LikeQueryReqVO req) {
|
||||
List<Long> users = lookup.userIds(req.getKeyword());
|
||||
if (lookup.noMatch(users)) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
List<Long> dramas = lookup.dramaIds(req.getDramaTitle());
|
||||
if (lookup.noMatch(dramas)) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
var query = buildQuery(req, users);
|
||||
if (req.getDramaId() != null) {
|
||||
query.and(q -> q.apply(DRAMA_MATCH, req.getDramaId()));
|
||||
}
|
||||
if (dramas != null) {
|
||||
query.and(q -> {
|
||||
for (Long id : dramas) {
|
||||
q.or().apply(DRAMA_MATCH, id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (req.getEpisodeId() != null) {
|
||||
query.and(q -> q.apply(EPISODE_MATCH, req.getEpisodeId()));
|
||||
}
|
||||
var result = likeMapper.selectPage(lookup.page(req), query);
|
||||
return new PageResult<>(result.getTotal(), assemble(result.getRecords()));
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ContentLike> buildQuery(LikeQueryReqVO req, List<Long> users) {
|
||||
return Wrappers.<ContentLike>lambdaQuery()
|
||||
.eq(req.getUserId() != null, ContentLike::getUserId, req.getUserId())
|
||||
.in(users != null, ContentLike::getUserId, users)
|
||||
.eq(StringUtils.hasText(req.getDeviceId()), ContentLike::getDeviceId, req.getDeviceId())
|
||||
.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);
|
||||
}
|
||||
|
||||
private List<AdminLikeVO> assemble(List<ContentLike> rows) {
|
||||
if (rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
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));
|
||||
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));
|
||||
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);
|
||||
return rows.stream().map(row -> toVo(row, users, comments, episodes, dramas)).toList();
|
||||
}
|
||||
|
||||
private List<Long> targetIds(List<ContentLike> rows, String type) {
|
||||
return rows.stream().filter(row -> type.equals(row.getTargetType())).map(ContentLike::getTargetId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
}
|
||||
|
||||
private Map<Long, ContentComment> comments(Collection<Long> ids) {
|
||||
return ids.isEmpty() ? Map.of() : commentMapper.selectBatchIds(ids).stream()
|
||||
.collect(Collectors.toMap(ContentComment::getId, Function.identity()));
|
||||
}
|
||||
|
||||
private Map<Long, DramaEpisode> episodes(Collection<Long> ids) {
|
||||
List<Long> keys = ids.stream().filter(Objects::nonNull).distinct().toList();
|
||||
return keys.isEmpty() ? Map.of() : episodeMapper.selectBatchIds(keys).stream()
|
||||
.collect(Collectors.toMap(DramaEpisode::getId, Function.identity()));
|
||||
}
|
||||
|
||||
private AdminLikeVO toVo(ContentLike row, Map<Long, AppUser> users, Map<Long, ContentComment> comments,
|
||||
Map<Long, DramaEpisode> episodes, Map<Long, Drama> dramas) {
|
||||
AdminLikeVO vo = new AdminLikeVO();
|
||||
vo.setId(row.getId());
|
||||
vo.setUserId(row.getUserId());
|
||||
vo.setDeviceId(row.getDeviceId());
|
||||
vo.setAnonymous(row.getUserId() == null);
|
||||
vo.setTargetType(row.getTargetType());
|
||||
vo.setTargetId(row.getTargetId());
|
||||
vo.setCreatedAt(row.getCreatedAt());
|
||||
AppUser user = row.getUserId() == null ? null : users.get(row.getUserId());
|
||||
if (user != null) {
|
||||
vo.setUid(user.getUid());
|
||||
vo.setNickname(user.getNickname());
|
||||
}
|
||||
ContentComment comment = COMMENT.equals(row.getTargetType()) ? comments.get(row.getTargetId()) : null;
|
||||
Long episodeId = EPISODE.equals(row.getTargetType()) ? row.getTargetId()
|
||||
: comment == null ? null : comment.getEpisodeId();
|
||||
DramaEpisode episode = episodeId == null ? null : episodes.get(episodeId);
|
||||
Long dramaId = DRAMA.equals(row.getTargetType()) ? row.getTargetId()
|
||||
: comment != null ? comment.getDramaId() : episode == null ? null : episode.getDramaId();
|
||||
vo.setEpisodeId(episodeId);
|
||||
vo.setDramaId(dramaId);
|
||||
if (comment != null && comment.getContent() != null) {
|
||||
String content = comment.getContent();
|
||||
int count = content.codePointCount(0, content.length());
|
||||
vo.setCommentSummary(content.substring(0, content.offsetByCodePoints(0, Math.min(count, SUMMARY_LENGTH))));
|
||||
}
|
||||
if (episode != null) {
|
||||
vo.setEpisodeNo(episode.getEpNo());
|
||||
vo.setEpisodeTitle(episode.getTitle());
|
||||
}
|
||||
Drama drama = dramaId == null ? null : dramas.get(dramaId);
|
||||
if (drama != null) {
|
||||
vo.setDramaTitle(drama.getTitle());
|
||||
vo.setDramaCover(drama.getCoverUrl());
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 后台收藏只读列表行,保留游客设备信息。 */
|
||||
@Data
|
||||
public class AdminFavoriteVO {
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String uid;
|
||||
private String nickname;
|
||||
private String deviceId;
|
||||
private Boolean anonymous;
|
||||
private Long dramaId;
|
||||
private String dramaTitle;
|
||||
private String dramaCover;
|
||||
private String dramaStatus;
|
||||
private Boolean dramaPublished;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 后台点赞只读列表行,保留游客设备信息。 */
|
||||
@Data
|
||||
public class AdminLikeVO {
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String uid;
|
||||
private String nickname;
|
||||
private String deviceId;
|
||||
private Boolean anonymous;
|
||||
private Long dramaId;
|
||||
private String dramaTitle;
|
||||
private String dramaCover;
|
||||
private String targetType;
|
||||
private Long targetId;
|
||||
private Long episodeId;
|
||||
private Integer episodeNo;
|
||||
private String episodeTitle;
|
||||
private String commentSummary;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 收藏与点赞列表的公共检索条件。 */
|
||||
@Data
|
||||
public class EngagementQueryReqVO {
|
||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||
private static final String TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
private Integer pageIndex = 1;
|
||||
private Integer pageSize = DEFAULT_PAGE_SIZE;
|
||||
private String keyword;
|
||||
private Long userId;
|
||||
private String deviceId;
|
||||
private String dramaTitle;
|
||||
private Long dramaId;
|
||||
@DateTimeFormat(pattern = TIME_PATTERN)
|
||||
private LocalDateTime startTime;
|
||||
@DateTimeFormat(pattern = TIME_PATTERN)
|
||||
private LocalDateTime endTime;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.vo;
|
||||
|
||||
/** 后台收藏分页检索条件。 */
|
||||
public class FavoriteQueryReqVO extends EngagementQueryReqVO {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.vo;
|
||||
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/** 后台点赞分页检索条件。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LikeQueryReqVO extends EngagementQueryReqVO {
|
||||
@Pattern(regexp = "DRAMA|EPISODE|COMMENT", message = "目标类型必须为 DRAMA、EPISODE 或 COMMENT")
|
||||
private String targetType;
|
||||
private Long targetId;
|
||||
private Long episodeId;
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.admin.service.FavoriteAdminService;
|
||||
import com.fruitshorts.admin.service.LikeAdminService;
|
||||
import com.fruitshorts.admin.support.ControllerContractSupport;
|
||||
import com.fruitshorts.admin.vo.AdminFavoriteVO;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/** 收藏和点赞分页路由及响应契约。 */
|
||||
class EngagementControllerContractTest {
|
||||
@Test
|
||||
void should_return_guest_fields_for_favorites() throws Exception {
|
||||
FavoriteAdminService service = mock(FavoriteAdminService.class);
|
||||
AdminFavoriteVO guest = new AdminFavoriteVO();
|
||||
guest.setId(1L);
|
||||
guest.setDeviceId("guest");
|
||||
guest.setAnonymous(true);
|
||||
when(service.page(any())).thenReturn(new PageResult<>(1L, List.of(guest)));
|
||||
ControllerContractSupport.mockMvcOf(new FavoriteAdminController(service))
|
||||
.perform(get("/admin/favorites/page"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.list[0].deviceId").value("guest"))
|
||||
.andExpect(jsonPath("$.data.list[0].anonymous").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_empty_like_page() throws Exception {
|
||||
LikeAdminService service = mock(LikeAdminService.class);
|
||||
when(service.page(any())).thenReturn(PageResult.empty());
|
||||
ControllerContractSupport.mockMvcOf(new LikeAdminController(service))
|
||||
.perform(get("/admin/likes/page").param("targetType", "COMMENT"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.total").value(0))
|
||||
.andExpect(jsonPath("$.data.list").isEmpty());
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fruitshorts.admin.vo.FavoriteQueryReqVO;
|
||||
import com.fruitshorts.admin.vo.LikeQueryReqVO;
|
||||
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
|
||||
import com.fruitshorts.interaction.dal.entity.ContentLike;
|
||||
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
|
||||
import com.fruitshorts.interaction.dal.mapper.ContentLikeMapper;
|
||||
import com.fruitshorts.playback.dal.entity.UserFavorite;
|
||||
import com.fruitshorts.playback.dal.mapper.UserFavoriteMapper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 收藏及点赞分页的游客、空结果和多态目标检验。 */
|
||||
class EngagementAdminServiceTest {
|
||||
@BeforeAll
|
||||
static void initMetadata() {
|
||||
MybatisConfiguration config = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(config, "");
|
||||
TableInfoHelper.initTableInfo(assistant, UserFavorite.class);
|
||||
TableInfoHelper.initTableInfo(assistant, ContentLike.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_enable_logic_delete_for_both_interaction_entities() {
|
||||
assertThat(TableInfoHelper.getTableInfo(UserFavorite.class).isWithLogicDelete()).isTrue();
|
||||
assertThat(TableInfoHelper.getTableInfo(ContentLike.class).isWithLogicDelete()).isTrue();
|
||||
assertThat(TableInfoHelper.getTableInfo(UserFavorite.class).getLogicDeleteSql(true, true))
|
||||
.contains("deleted").contains("0");
|
||||
assertThat(TableInfoHelper.getTableInfo(ContentLike.class).getLogicDeleteSql(true, true))
|
||||
.contains("deleted").contains("0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_empty_without_query_when_keyword_has_no_match() {
|
||||
EngagementAdminLookup lookup = mock(EngagementAdminLookup.class);
|
||||
UserFavoriteMapper mapper = mock(UserFavoriteMapper.class);
|
||||
when(lookup.userIds("missing")).thenReturn(List.of());
|
||||
when(lookup.noMatch(List.of())).thenReturn(true);
|
||||
FavoriteQueryReqVO req = new FavoriteQueryReqVO();
|
||||
req.setKeyword("missing");
|
||||
var result = new FavoriteAdminService(mapper, lookup).page(req);
|
||||
assertThat(result.getTotal()).isZero();
|
||||
assertThat(result.getList()).isEmpty();
|
||||
verifyNoInteractions(mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_preserve_guest_and_unpublished_drama_when_favorite_has_no_user() {
|
||||
EngagementAdminLookup lookup = mock(EngagementAdminLookup.class);
|
||||
UserFavoriteMapper mapper = mock(UserFavoriteMapper.class);
|
||||
when(lookup.page(any())).thenReturn(new Page<>(1, 10));
|
||||
UserFavorite row = new UserFavorite();
|
||||
row.setId(1L);
|
||||
row.setDeviceId("guest-device");
|
||||
row.setDramaId(2L);
|
||||
Page<UserFavorite> page = new Page<>(1, 10);
|
||||
page.setTotal(1);
|
||||
page.setRecords(List.of(row));
|
||||
when(mapper.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
|
||||
when(lookup.users(any())).thenReturn(Map.of());
|
||||
when(lookup.dramas(any())).thenReturn(Map.of());
|
||||
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();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_preserve_guest_drama_like_without_episode() {
|
||||
EngagementAdminLookup lookup = mock(EngagementAdminLookup.class);
|
||||
ContentLikeMapper mapper = mock(ContentLikeMapper.class);
|
||||
when(lookup.page(any())).thenReturn(new Page<>(1, 10));
|
||||
ContentLike row = new ContentLike();
|
||||
row.setId(1L);
|
||||
row.setTargetType("DRAMA");
|
||||
row.setTargetId(2L);
|
||||
row.setDeviceId("guest-device");
|
||||
Page<ContentLike> page = new Page<>(1, 10);
|
||||
page.setTotal(1);
|
||||
page.setRecords(List.of(row));
|
||||
when(mapper.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
|
||||
when(lookup.users(any())).thenReturn(Map.of());
|
||||
when(lookup.dramas(any())).thenReturn(Map.of());
|
||||
var service = new LikeAdminService(mapper, mock(ContentCommentMapper.class),
|
||||
mock(DramaEpisodeMapper.class), lookup);
|
||||
var result = service.page(new LikeQueryReqVO());
|
||||
assertThat(result.getList().get(0).getAnonymous()).isTrue();
|
||||
assertThat(result.getList().get(0).getDramaId()).isEqualTo(2L);
|
||||
assertThat(result.getList().get(0).getEpisodeId()).isNull();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user