feat(app-api): 详情补充互动数量字段
This commit is contained in:
1 parent
678652cb20
commit
671953bff3
7 files changed
+231
-1
No files matched your search
@@ -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;
|
||||
}
|
||||
+9
-1
@@ -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);
|
||||
}
|
||||
|
||||
+66
@@ -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);
|
||||
}
|
||||
}
|
||||
+80
@@ -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());
|
||||
}
|
||||
}
|
||||
+16
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user