feat(interaction): 点赞与评论按集统计

This commit is contained in:
Zhou Weidong committed 2026-10-21 15:56:26 +08:00
1 parent c7ff729f84
commit c6f729fb27
28 files changed
+683 -41

No files matched your search

@@ -40,6 +40,8 @@ public class DramaEpisode {
private String status;
private LocalDateTime releaseAt;
private Long playCount;
/** 本集点赞数(点赞目标为 EPISODE) */
private Long likeCount;
private Integer sort;
private LocalDateTime createdAt;
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
@@ -2,6 +2,7 @@
package com.fruitshorts.content.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
@@ -14,9 +15,12 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Service
@RequiredArgsConstructor
@@ -103,6 +107,7 @@ public class EpisodeService {
apply(episode, req);
episode.setStatus(req.getStatus() == null ? ContentStatus.DRAFT.getCode() : req.getStatus());
episode.setPlayCount(0L);
episode.setLikeCount(nonNegativeOrZero(req.getLikeCount()));
episode.setVideoAssetId(0L);
dramaEpisodeMapper.insert(episode);
dramaService.syncTotalEpisodes(req.getDramaId());
@@ -115,6 +120,9 @@ public class EpisodeService {
dramaService.getExisting(req.getDramaId());
ensureEpNoAvailable(req.getDramaId(), req.getNumber(), id);
apply(episode, req);
if (req.getLikeCount() != null) {
episode.setLikeCount(nonNegative(req.getLikeCount()));
}
if (req.getStatus() != null) {
episode.setStatus(req.getStatus());
}
@@ -157,6 +165,46 @@ public class EpisodeService {
.set(DramaEpisode::getStatus, ContentStatus.PUBLISHED.getCode()));
}
/** 本集点赞数原子增减,避免并发读改写丢计数。 */
public void adjustLikeCount(Long episodeId, int delta) {
if (episodeId == null || delta == 0) {
return;
}
dramaEpisodeMapper.update(null, Wrappers.<DramaEpisode>lambdaUpdate()
.eq(DramaEpisode::getId, episodeId)
.setSql(delta > 0
? "like_count = like_count + 1"
: "like_count = GREATEST(like_count - 1, 0)"));
}
/** 批量汇总多部剧的分集点赞数,避免后台逐剧 N+1 查询。 */
public Map<Long, Long> sumLikeCountsByDramas(Collection<Long> dramaIds) {
if (dramaIds == null || dramaIds.isEmpty()) {
return Map.of();
}
List<Long> ids = dramaIds.stream().filter(Objects::nonNull).distinct().toList();
if (ids.isEmpty()) {
return Map.of();
}
QueryWrapper<DramaEpisode> wrapper = Wrappers.query();
wrapper.select("drama_id AS drama_id", "COALESCE(SUM(like_count), 0) AS like_count")
.in("drama_id", ids)
.eq("deleted", 0)
.groupBy("drama_id");
List<Map<String, Object>> rows = dramaEpisodeMapper.selectMaps(wrapper);
if (rows == null || rows.isEmpty()) {
return Map.of();
}
Map<Long, Long> counts = new HashMap<>();
for (Map<String, Object> row : rows) {
Long dramaId = asLong(row.get("drama_id"));
if (dramaId != null) {
counts.put(dramaId, asLong(row.get("like_count"), 0L));
}
}
return counts;
}
private void ensureEpNoAvailable(Long dramaId, Integer epNo, Long excludeId) {
Long count = dramaEpisodeMapper.selectCount(Wrappers.<DramaEpisode>lambdaQuery()
.eq(DramaEpisode::getDramaId, dramaId)
@@ -194,10 +242,31 @@ public class EpisodeService {
vo.setStatus(episode.getStatus());
vo.setStatusText(ContentStatus.textOf(episode.getStatus()));
vo.setViews(episode.getPlayCount());
vo.setLikeCount(episode.getLikeCount() == null ? 0L : episode.getLikeCount());
vo.setSort(episode.getSort());
return vo;
}
private Long nonNegativeOrZero(Long value) {
return value == null ? 0L : nonNegative(value);
}
private Long nonNegative(Long value) {
if (value < 0) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "本集点赞数不能小于 0");
}
return value;
}
private Long asLong(Object value) {
return value instanceof Number number ? number.longValue() : null;
}
private long asLong(Object value, long defaultValue) {
Long number = asLong(value);
return number == null ? defaultValue : number;
}
private String formatDuration(Integer seconds) {
int value = seconds == null ? 0 : seconds;
return String.format("%02d:%02d", value / 60, value % 60);
@@ -21,5 +21,13 @@ public class EpisodeRespVO {
private String status;
private String statusText;
private Long views;
/** 本集点赞数 */
private Long likeCount;
/** 本集评论数 */
private Long commentCount;
/** 所属剧收藏数,只读 */
private Long favoriteCount;
/** 所属剧热度(drama.view_count),只读 */
private Long heat;
private Integer sort;
}
@@ -3,6 +3,7 @@
package com.fruitshorts.content.vo;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;
import lombok.Data;
@Data
@@ -20,6 +21,8 @@ public class EpisodeSaveReqVO {
private String videoUrl;
private Boolean isFree;
private Integer unlockCoin;
@PositiveOrZero(message = "本集点赞数不能小于 0")
private Long likeCount;
private String status;
private Integer sort;
}