merge: 合并 K30-A 详情互动数量

This commit is contained in:
Zhou Weidong committed 2026-10-20 12:02:19 +08:00
commit f19d21ef96
7 files changed
+231 -1

No files matched your search

+49
View File
@@ -174,6 +174,55 @@ app.register.max-per-device-per-day
> 卡片(`AppDramaCardVO`)统一新增 `language` 字段(取值如 `zh-CN` / `id`),
> 首页、发现页、分类、收藏、搜索等所有下发卡片的场景都带该字段,客户端可据此做语言筛选。
#### 短剧详情响应(`GET /app/dramas/{id}`)
```json
{
"code": 0, "message": "ok",
"data": {
"id": "5",
"title": "命运的相遇",
"synopsis": "一次意外的相遇,让两个世界的人走到一起。",
"cover": "/files/drama/202610/cover.jpg",
"genre": "都市 · 情感",
"category": "都市",
"totalEpisodes": 80,
"freeEpisodes": 10,
"unlockCoin": 30,
"isHot": true,
"viewCount": "12400",
"likeCount": "3200",
"favoriteCount": "1800",
"commentCount": "96",
"episodes": [
{
"id": "2111012753262120962",
"number": 1,
"title": "第 1 集",
"duration": "03:00",
"durationSec": 180,
"isFree": true,
"locked": false,
"unlockCoin": 0,
"playUrl": "/files/episode/202610/xxx.mp4?e=1760000000&s=9f2c..."
}
]
}
}
```
详情里的互动数量(K30 新增,供播放页右侧收藏/评论/点赞直接展示):
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `likeCount` | Long(JSON 字符串) | 取 `drama.like_count`,短剧累计点赞数 |
| `favoriteCount` | Long(JSON 字符串) | 取 `drama.favorite_count`,短剧累计收藏数 |
| `commentCount` | Long(JSON 字符串) | `content_comment` 中该剧未删除评论数:按 `drama_id` 统计,排除 `status = 'deleted'` 与逻辑删除记录 |
| `viewCount` | Long(JSON 字符串) | 取 `drama.view_count`,累计播放量,既有字段语义不变 |
计数是只读快照:互动操作完成后客户端重新拉取详情即可刷新;`Long` 字段一律按字符串下发,
客户端需要转成数字再展示(与 `viewCount` 相同)。
### 1.4 播放(已实现,链路已重做)
| 方法 | 路径 | 鉴权 | 说明 |
@@ -17,6 +17,7 @@ import com.fruitshorts.content.service.BannerService;
import com.fruitshorts.content.service.DramaCategoryService;
import com.fruitshorts.content.service.DramaService;
import com.fruitshorts.content.service.EpisodeService;
import com.fruitshorts.interaction.service.CommentService;
import com.fruitshorts.media.service.PlaybackSigner;
import com.fruitshorts.content.vo.DramaCategoryRespVO;
import lombok.RequiredArgsConstructor;
@@ -44,6 +45,7 @@ public class AppCatalogService {
private final BannerService bannerService;
private final DramaCategoryService dramaCategoryService;
private final PlaybackSigner playbackSigner;
private final CommentService commentService;
public AppFeedVO homeFeed() {
Map<Long, String> categoryNames = categoryNameMap();
@@ -96,6 +98,9 @@ public class AppCatalogService {
detail.setUnlockCoin(drama.getUnlockCoin());
detail.setIsHot(drama.getIsHot() != null && drama.getIsHot() == 1);
detail.setViewCount(drama.getViewCount());
detail.setLikeCount(drama.getLikeCount());
detail.setFavoriteCount(drama.getFavoriteCount());
detail.setCommentCount(commentService.countByDrama(dramaId));
detail.setEpisodes(episodes.stream().map(this::toEpisode).toList());
return detail;
}
@@ -20,5 +20,11 @@ public class AppDramaDetailVO {
private Integer unlockCoin;
private Boolean isHot;
private Long viewCount;
/** 短剧累计点赞数:drama.like_count */
private Long likeCount;
/** 短剧累计收藏数:drama.favorite_count */
private Long favoriteCount;
/** 该剧未删除评论数:content_comment 按 drama_id 统计 */
private Long commentCount;
private List<AppEpisodeVO> episodes;
}
@@ -89,6 +89,10 @@ class AppCatalogControllerContractTest {
AppDramaDetailVO detail = new AppDramaDetailVO();
detail.setId(42L);
detail.setTitle("命运的相遇");
detail.setViewCount(12400L);
detail.setLikeCount(3200L);
detail.setFavoriteCount(1800L);
detail.setCommentCount(96L);
detail.setEpisodes(List.of());
given(catalogService.dramaDetail(42L)).willReturn(detail);
@@ -97,7 +101,11 @@ class AppCatalogControllerContractTest {
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.id").value("42"))
.andExpect(jsonPath("$.data.id").isString())
.andExpect(jsonPath("$.data.title").value("命运的相遇"));
.andExpect(jsonPath("$.data.title").value("命运的相遇"))
.andExpect(jsonPath("$.data.viewCount").value("12400"))
.andExpect(jsonPath("$.data.likeCount").value("3200"))
.andExpect(jsonPath("$.data.favoriteCount").value("1800"))
.andExpect(jsonPath("$.data.commentCount").value("96"));
verify(catalogService).dramaDetail(42L);
}
@@ -0,0 +1,66 @@
// Author: fruit.com.cn
package com.fruitshorts.app.service;
import com.fruitshorts.app.vo.AppDramaDetailVO;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.service.BannerService;
import com.fruitshorts.content.service.DramaCategoryService;
import com.fruitshorts.content.service.DramaService;
import com.fruitshorts.content.service.EpisodeService;
import com.fruitshorts.interaction.service.CommentService;
import com.fruitshorts.media.service.PlaybackSigner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* 目录聚合的互动数量装配:详情 VO 的点赞/收藏取短剧计数,评论数取互动域统计。
*/
class AppCatalogServiceTest {
private DramaService dramaService;
private EpisodeService episodeService;
private DramaCategoryService dramaCategoryService;
private CommentService commentService;
private AppCatalogService catalogService;
@BeforeEach
void setUp() {
dramaService = mock(DramaService.class);
episodeService = mock(EpisodeService.class);
BannerService bannerService = mock(BannerService.class);
dramaCategoryService = mock(DramaCategoryService.class);
PlaybackSigner playbackSigner = mock(PlaybackSigner.class);
commentService = mock(CommentService.class);
catalogService = new AppCatalogService(dramaService, episodeService, bannerService,
dramaCategoryService, playbackSigner, commentService);
}
@Test
void should_fill_interaction_counts_when_drama_detail_is_built() {
Drama drama = new Drama();
drama.setId(42L);
drama.setTitle("命运的相遇");
drama.setLikeCount(3200L);
drama.setFavoriteCount(1800L);
drama.setViewCount(12400L);
given(dramaService.getPublished(42L)).willReturn(drama);
given(episodeService.listPublishedByDrama(42L)).willReturn(List.of());
given(dramaCategoryService.listBasic()).willReturn(List.of());
given(commentService.countByDrama(42L)).willReturn(96L);
AppDramaDetailVO detail = catalogService.dramaDetail(42L);
assertThat(detail.getViewCount()).isEqualTo(12400L);
assertThat(detail.getLikeCount()).isEqualTo(3200L);
assertThat(detail.getFavoriteCount()).isEqualTo(1800L);
assertThat(detail.getCommentCount()).isEqualTo(96L);
verify(commentService).countByDrama(42L);
}
}
@@ -0,0 +1,80 @@
// Author: fruit.com.cn
package com.fruitshorts.app.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.fruitshorts.content.service.DramaService;
import com.fruitshorts.content.service.EpisodeService;
import com.fruitshorts.interaction.config.InteractionProperties;
import com.fruitshorts.interaction.dal.entity.ContentComment;
import com.fruitshorts.interaction.dal.mapper.ContentAuditRecordMapper;
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
import com.fruitshorts.interaction.dal.mapper.ContentLikeMapper;
import com.fruitshorts.interaction.service.AuditRecorder;
import com.fruitshorts.interaction.service.CommentService;
import com.fruitshorts.interaction.service.SensitiveWordService;
import com.fruitshorts.user.dal.mapper.AppUserMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* 评论计数口径:按剧统计,排除已删除评论。
*/
class CommentServiceCountTest {
private ContentCommentMapper commentMapper;
private CommentService commentService;
@BeforeAll
static void initTableInfo() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), ""), ContentComment.class);
}
@BeforeEach
void setUp() {
commentMapper = mock(ContentCommentMapper.class);
commentService = new CommentService(
commentMapper,
mock(ContentLikeMapper.class),
mock(ContentAuditRecordMapper.class),
mock(AppUserMapper.class),
mock(DramaService.class),
mock(EpisodeService.class),
mock(SensitiveWordService.class),
mock(AuditRecorder.class),
mock(InteractionProperties.class));
}
@Test
void should_count_comments_of_drama_and_exclude_deleted() {
ArgumentCaptor<Wrapper<ContentComment>> captor = ArgumentCaptor.forClass(Wrapper.class);
given(commentMapper.selectCount(captor.capture())).willReturn(5L);
long count = commentService.countByDrama(7L);
assertThat(count).isEqualTo(5L);
Wrapper<ContentComment> wrapper = captor.getValue();
assertThat(wrapper.getSqlSegment()).contains("drama_id").contains("status");
AbstractWrapper<ContentComment, ?, ?> lambda = (AbstractWrapper<ContentComment, ?, ?>) wrapper;
assertThat(lambda.getParamNameValuePairs().values()).contains(7L, "deleted");
}
@Test
void should_return_zero_without_query_when_drama_id_is_missing() {
assertThat(commentService.countByDrama(null)).isZero();
verify(commentMapper, never()).selectCount(any());
}
}
@@ -169,6 +169,22 @@ public class CommentService {
softDelete(comment);
}
/**
* 某部剧的评论总数(顶层评论与回复合计)。
*
* <p>只统计未删除评论:逻辑删除的记录由 MyBatis-Plus 自动过滤,
* 用户删除后状态置为 deleted 的记录也要排除。
*/
public long countByDrama(Long dramaId) {
if (dramaId == null) {
return 0L;
}
Long count = commentMapper.selectCount(Wrappers.<ContentComment>lambdaQuery()
.eq(ContentComment::getDramaId, dramaId)
.ne(ContentComment::getStatus, STATUS_DELETED));
return count == null ? 0L : count;
}
// ---------------- 后台 ----------------
public PageResult<CommentRespVO> adminPage(CommentQueryReqVO req) {