merge: 合并 K84-A 卡片互动计数

This commit is contained in:
Zhou Weidong committed 2026-10-21 11:14:35 +08:00
commit 88794b816e
13 files changed
+324 -50

No files matched your search

+4
View File
@@ -173,6 +173,10 @@ app.register.max-per-device-per-day
> 卡片(`AppDramaCardVO`)统一新增 `language` 字段(取值如 `zh-CN` / `id`),
> 首页、发现页、分类、收藏、搜索等所有下发卡片的场景都带该字段,客户端可据此做语言筛选。
>
> 卡片同时下发 `likeCount` / `favoriteCount` / `commentCount`(均为 JSON 字符串),
> 点赞、收藏取 `drama` 冗余计数字段,评论数由 `content_comment` 按 `drama_id` 批量统计,
> 排除逻辑删除与 `status = 'deleted'` 的记录。首页、发现、分类、收藏、搜索等列表卡片结构一致。
#### 短剧详情响应(`GET /app/dramas/{id}`)
+40 -41
View File
@@ -1,59 +1,58 @@
# INBOX
## [已处理] K83-A 右侧互动栏改用参考图同款图标资源
## [已处理] K84-A 列表卡片与后台补充收藏 / 点赞 / 评论数
### 背景
K82-A 用 Material 图标替换后,形状与参考图仍不一致(星形/气泡/心形/分享箭头细节不同),
且 Material 字形自带内边距,视觉上比参考图小。
右侧互动栏要显示收藏、评论、喜欢的数量,但首页 feed 等列表卡片拿不到这三个数。
已从产品提供的参考图中提取出四个图标(白色 + 透明通道,保留原始形状与相对大小),
放在仓库 `assets/icon/rail/`:
已确认:
- `rail_save.png`(收藏,实心星)
- `rail_comment.png`(评论,带三个点的气泡)
- `rail_like.png`(喜欢,实心心形)
- `rail_share.png`(分享,弯向右的箭头)
- 数据库 `drama` 表有 `favorite_count`、`like_count`(无 comment_count 列,评论数按
`content_comment` 统计,见 `AppDramaDetailVO.commentCount`)
- `AppDramaDetailVO` 已返回 `likeCount / favoriteCount / commentCount`
- **`AppDramaCardVO`(列表卡片)只有 `viewCount`,缺这三个字段** → 客户端列表阶段拿不到
- 后台 `DramaRespVO` 只有 `views`,也缺这三个数
- 客户端 `Drama.fromJson` 已经解析 `likeCount / favoriteCount / commentCount`,**客户端无需改**
### 改动
### 任务(FruitServer 仓库)
1. `pubspec.yaml`
- `flutter.assets` 增加 `assets/icon/rail/`(目录)
2. `lib/pages/player/widgets/interaction_rail.dart`
- `InteractionRailItem` 的 `IconData icon / activeIcon` 改为 `String asset`
(`selected` 只决定颜色,不再切换图标)
- 渲染改为:
```dart
Image.asset(
item.asset,
width: _iconSize,
height: _iconSize,
fit: BoxFit.contain,
color: item.selected ? AppColors.blue : Colors.white,
colorBlendMode: BlendMode.srcIn,
)
```
- `_iconSize`:`30` → `32`;`_buttonSize` 保持 `44`;`_itemGap`:`22` → `24`
- 删除 K82-A 引入的 `mirrorIcon`(新资源本身已朝右)
3. `lib/pages/player/widgets/home_feed_overlay.dart` 与 `player_overlay.dart`
- 四项分别传:`assets/icon/rail/rail_save.png`、`rail_comment.png`、`rail_like.png`、`rail_share.png`
4. 测试
- 把 `find.byIcon(Icons.star_rounded / favorite_rounded / reply_rounded / ios_share_rounded …)` 类断言
改为查找对应 `Image.asset`(或用 Key 定位后断言 `Image.image` 的 asset 名)
- Key(`rail-save`、`rail-comments`、`rail-like`、`rail-share`、`player-rail-*`、计数 Key)保持不变
1. `fruitshorts-app-api/.../vo/AppDramaCardVO.java`
- 增加 `Long likeCount`、`Long favoriteCount`、`Long commentCount`
2. `fruitshorts-app-api/.../service/AppCatalogService.java`
- `cards()` 里一次性批量取评论数:`Map<Long, Long> commentCounts = commentService.countByDramas(ids)`
- `toCard()` 填充:
- `likeCount` ← `drama.getLikeCount()`
- `favoriteCount` ← `drama.getFavoriteCount()`
- `commentCount` ← `commentCounts.getOrDefault(drama.getId(), 0L)`
- 不要逐条 `countByDrama`(避免 N+1)
3. `fruitshorts-module-interaction/.../service/CommentService.java`
- 新增 `Map<Long, Long> countByDramas(Collection<Long> dramaIds)`:
`deleted = 0` 且 `drama_id in (...)`,按 `drama_id` 分组计数;空集合返回空 Map
- 现有 `countByDrama(Long)` 可复用该方法的单元素结果或保持不动
4. 后台管理端(注意模块依赖方向:`module-interaction` 依赖 `module-content`,不能反向依赖)
- `fruitshorts-module-content/.../vo/DramaRespVO.java` 增加 `likeCount / favoriteCount / commentCount`
- `DramaService`(module-content)只填充 `likeCount / favoriteCount`(取实体字段),
`commentCount` 先留空
- `fruitshorts-admin-api` 的 Drama 接口(`DramaController` 或 admin 侧小服务)在分页/详情返回前,
用 `CommentService.countByDramas(ids)` 批量回填 `commentCount`
(分页一次查询;详情单条)
5. 测试
- `AppCatalogControllerContractTest`:列表卡片接口增加 `likeCount / favoriteCount / commentCount` 断言
- 后台分页/详情如有测试,补充三个字段断言
- `CommentService.countByDramas` 补单测(空集合 / 多剧集分组)
### 验收
- 门禁:`dart analyze lib test` 零告警、`flutter test` 全过、`flutter build apk --debug` 成功。
- 真机自查:首页 feed 与全屏播放页右侧四个图标与参考图形状一致,大小一致,无深色圆底。
- 门禁:`mvn test` 全过。
- 接口自测:`GET /app/home/feed`(或现有首页聚合接口)返回的每个卡片都带三个数字段。
### Git
- 从 `develop` 拉 `feat/rail-icon-assets`。
- 提交:`feat(player): 互动栏改用参考图同款图标资源`,作者 `Zhou Weidong <502333560@qq.com>`。
- `--no-ff` 合回 `develop` 并 push;`FruitServer/docs/plans/PROGRESS.md` 追加 K83-A 留痕。
- 从 `develop` 拉 `feat/drama-card-counts`。
- 提交:`feat(content): 列表卡片与后台返回收藏点赞评论数`,作者 `Zhou Weidong <502333560@qq.com>`。
- `--no-ff` 合回 `develop` 并 push;`FruitServer/docs/plans/PROGRESS.md` 追加 K84-A 留痕。
### 约束
- 全程中文;禁止 `git add -A`;图标资源已放到 `assets/icon/rail/`,不要重新绘制或改动形状;
不改互动逻辑与 Key;完成后停住等待验收。
- 全程中文;禁止 `git add -A`;不改客户端代码、不改 specs 仓库;完成后停住等待验收。
+28
View File
@@ -22884,3 +22884,31 @@ Running Gradle task 'assembleDebug'...
```
解读:静态检查零告警,490 条测试全部通过,debug APK 构建成功。真机验收由用户执行。
## 2026-10-21 FruitServer:K84-A 列表卡片与后台补充互动计数
- 前置状态:K83-A 文档留痕完成,FruitServer develop 提交 `f903369`。
- 分支:`feat/drama-card-counts`
- `AppDramaCardVO` 新增 `likeCount` / `favoriteCount` / `commentCount`,首页、发现、分类、收藏、搜索等复用卡片的接口统一返回三个字段。
- `AppCatalogService.cards()` 对每批卡片只调用一次 `CommentService.countByDramas(ids)`,点赞/收藏取 `Drama` 实体冗余字段,评论缺失时回填 0,避免逐条查询。
- `CommentService` 新增按 `drama_id` 分组批量统计,过滤逻辑删除与 `status = 'deleted'`,空集合不查询数据库。
- 后台 `DramaRespVO` 新增三个字段;`module-content` 只填点赞/收藏,`admin-api` 的分页和详情接口从 `module-interaction` 批量回填评论数,模块依赖方向保持不变。
- `docs/api-phase1.md` 已同步卡片互动计数契约;未修改 Flutter 客户端、specs 仓库或其它业务逻辑。
- 测试覆盖列表卡片三个字段契约、单次批量查询、空集合、多剧分组统计,以及后台分页/详情三个字段回填。
门禁说明:本机默认 JDK 27 不兼容既有 Mockito inline 测试,首次尝试在既有 commerce 模块失败;按项目要求固定 JDK 21 后完整重跑通过。
门禁原始输出:
```text
$ JAVA_HOME=/opt/homebrew/Cellar/openjdk@21/21.0.12.1/libexec/openjdk.jdk/Contents/Home mvn test
[INFO] Reactor Summary for fruitshorts 1.0.0-SNAPSHOT:
[INFO] fruitshorts-module-interaction ..................... SUCCESS
[INFO] fruitshorts-admin-api ............................... SUCCESS
[INFO] fruitshorts-app-api ................................. SUCCESS
[INFO] fruitshorts-transcode-worker ........................ SUCCESS
[INFO] BUILD SUCCESS
[INFO] Total time: 14.294 s
```
解读:JDK 21 下完整 Maven 测试通过,App/Admin 互动计数契约与既有模块测试均保持绿色。接口联调验收由用户执行。
@@ -10,8 +10,9 @@ import com.fruitshorts.content.vo.DramaPageReqVO;
import com.fruitshorts.content.vo.DramaRespVO;
import com.fruitshorts.content.vo.DramaSaveReqVO;
import com.fruitshorts.content.vo.DramaStatusReqVO;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.interaction.service.CommentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@@ -27,6 +28,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@Tag(name = "后台-短剧管理")
@RestController
@@ -35,19 +37,24 @@ import java.util.List;
public class DramaController {
private final DramaService dramaService;
private final CommentService commentService;
@Operation(summary = "短剧分页查询")
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<PageResult<DramaRespVO>> page(DramaPageReqVO req) {
return Result.ok(dramaService.page(req));
PageResult<DramaRespVO> page = dramaService.page(req);
fillCommentCounts(page.getList());
return Result.ok(page);
}
@Operation(summary = "短剧详情")
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<DramaRespVO> get(@PathVariable Long id) {
return Result.ok(dramaService.get(id));
DramaRespVO drama = dramaService.get(id);
fillCommentCounts(List.of(drama));
return Result.ok(drama);
}
@Operation(summary = "新建短剧")
@@ -133,4 +140,13 @@ public class DramaController {
dramaService.changeStatus(id, action, req == null ? null : req.getReason(),
SecurityUtils.getUserId(), SecurityUtils.getUsername());
}
private void fillCommentCounts(List<DramaRespVO> dramas) {
if (dramas == null || dramas.isEmpty()) {
return;
}
List<Long> ids = dramas.stream().map(DramaRespVO::getId).toList();
Map<Long, Long> counts = commentService.countByDramas(ids);
dramas.forEach(drama -> drama.setCommentCount(counts.getOrDefault(drama.getId(), 0L)));
}
}
@@ -0,0 +1,83 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller;
import com.fruitshorts.admin.support.ControllerContractSupport;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.content.service.DramaService;
import com.fruitshorts.content.vo.DramaPageReqVO;
import com.fruitshorts.content.vo.DramaRespVO;
import com.fruitshorts.interaction.service.CommentService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** 后台短剧列表与详情的互动数量回填契约。 */
class DramaControllerContractTest {
private DramaService dramaService;
private CommentService commentService;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
dramaService = mock(DramaService.class);
commentService = mock(CommentService.class);
mockMvc = ControllerContractSupport.mockMvcOf(
new DramaController(dramaService, commentService));
}
@Test
void should_fill_comment_counts_in_batch_when_page_is_requested() throws Exception {
DramaRespVO first = drama(7L, 3200L, 1800L);
DramaRespVO second = drama(8L, 120L, 80L);
given(dramaService.page(any(DramaPageReqVO.class)))
.willReturn(new PageResult<>(2L, List.of(first, second)));
given(commentService.countByDramas(List.of(7L, 8L)))
.willReturn(Map.of(7L, 11L));
mockMvc.perform(get("/admin/dramas/page"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.list[0].likeCount").value("3200"))
.andExpect(jsonPath("$.data.list[0].favoriteCount").value("1800"))
.andExpect(jsonPath("$.data.list[0].commentCount").value("11"))
.andExpect(jsonPath("$.data.list[1].likeCount").value("120"))
.andExpect(jsonPath("$.data.list[1].favoriteCount").value("80"))
.andExpect(jsonPath("$.data.list[1].commentCount").value("0"));
verify(commentService).countByDramas(List.of(7L, 8L));
}
@Test
void should_fill_comment_count_when_detail_is_requested() throws Exception {
given(dramaService.get(7L)).willReturn(drama(7L, 3200L, 1800L));
given(commentService.countByDramas(List.of(7L))).willReturn(Map.of(7L, 11L));
mockMvc.perform(get("/admin/dramas/7"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.likeCount").value("3200"))
.andExpect(jsonPath("$.data.favoriteCount").value("1800"))
.andExpect(jsonPath("$.data.commentCount").value("11"));
}
private DramaRespVO drama(Long id, Long likeCount, Long favoriteCount) {
DramaRespVO drama = new DramaRespVO();
drama.setId(id);
drama.setTitle("短剧" + id);
drama.setLikeCount(likeCount);
drama.setFavoriteCount(favoriteCount);
return drama;
}
}
@@ -144,15 +144,17 @@ public class AppCatalogService {
/** 整屏卡片共用一次剧集查询,取每部剧的首个可播免费集作为预览。 */
private List<AppDramaCardVO> cards(List<Drama> dramas, Map<Long, String> categoryNames) {
Map<Long, DramaEpisode> previews = episodeService.firstPlayableByDramas(
dramas.stream().map(Drama::getId).toList());
List<Long> ids = dramas.stream().map(Drama::getId).toList();
Map<Long, DramaEpisode> previews = episodeService.firstPlayableByDramas(ids);
Map<Long, Long> commentCounts = commentService.countByDramas(ids);
return dramas.stream()
.map(drama -> toCard(drama, categoryNames, previews.get(drama.getId())))
.map(drama -> toCard(
drama, categoryNames, previews.get(drama.getId()), commentCounts))
.toList();
}
private AppDramaCardVO toCard(Drama drama, Map<Long, String> categoryNames,
DramaEpisode preview) {
DramaEpisode preview, Map<Long, Long> commentCounts) {
AppDramaCardVO card = new AppDramaCardVO();
card.setId(drama.getId());
card.setTitle(drama.getTitle());
@@ -163,6 +165,9 @@ public class AppCatalogService {
card.setTotalEpisodes(drama.getTotalEpisodes());
card.setIsHot(drama.getIsHot() != null && drama.getIsHot() == 1);
card.setViewCount(drama.getViewCount());
card.setLikeCount(drama.getLikeCount());
card.setFavoriteCount(drama.getFavoriteCount());
card.setCommentCount(commentCounts.getOrDefault(drama.getId(), 0L));
card.setLanguage(drama.getLanguage());
card.setPreview(preview == null ? null : toEpisode(preview));
return card;
@@ -18,6 +18,12 @@ public class AppDramaCardVO {
private Integer totalEpisodes;
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;
/** 语种,供客户端按语言筛选(如 zh-CN / id)。 */
@Schema(description = "短剧语种,如 zh-CN / id")
private String language;
@@ -5,8 +5,10 @@ import com.fruitshorts.app.service.AppCatalogService;
import com.fruitshorts.app.support.ControllerContractSupport;
import com.fruitshorts.app.vo.AppBannerVO;
import com.fruitshorts.app.vo.AppCategoryVO;
import com.fruitshorts.app.vo.AppDramaCardVO;
import com.fruitshorts.app.vo.AppDramaDetailVO;
import com.fruitshorts.app.vo.AppFeedVO;
import com.fruitshorts.app.vo.AppSectionVO;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import com.fruitshorts.playback.service.PlaybackService;
@@ -50,7 +52,15 @@ class AppCatalogControllerContractTest {
banner.setImageUrl("/files/banner/9.png");
banner.setLinkType("DRAMA");
banner.setLinkValue("12");
AppFeedVO feed = new AppFeedVO(List.of(banner), List.of());
AppDramaCardVO card = new AppDramaCardVO();
card.setId(12L);
card.setTitle("命运的相遇");
card.setViewCount(12400L);
card.setLikeCount(3200L);
card.setFavoriteCount(1800L);
card.setCommentCount(96L);
AppSectionVO section = new AppSectionVO("hot", "热门推荐", List.of(card));
AppFeedVO feed = new AppFeedVO(List.of(banner), List.of(section));
given(catalogService.homeFeed()).willReturn(feed);
catalogMvc.perform(get("/app/home/feed"))
@@ -58,7 +68,11 @@ class AppCatalogControllerContractTest {
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.banners[0].id").value(9))
.andExpect(jsonPath("$.data.banners[0].imageUrl").value("/files/banner/9.png"))
.andExpect(jsonPath("$.data.banners[0].linkType").value("DRAMA"));
.andExpect(jsonPath("$.data.banners[0].linkType").value("DRAMA"))
.andExpect(jsonPath("$.data.sections[0].dramas[0].viewCount").value("12400"))
.andExpect(jsonPath("$.data.sections[0].dramas[0].likeCount").value("3200"))
.andExpect(jsonPath("$.data.sections[0].dramas[0].favoriteCount").value("1800"))
.andExpect(jsonPath("$.data.sections[0].dramas[0].commentCount").value("96"));
}
@Test
@@ -1,6 +1,7 @@
// Author: fruit.com.cn
package com.fruitshorts.app.service;
import com.fruitshorts.app.vo.AppDramaCardVO;
import com.fruitshorts.app.vo.AppDramaDetailVO;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.service.BannerService;
@@ -13,10 +14,13 @@ import org.junit.jupiter.api.BeforeEach;
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.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
@@ -63,4 +67,36 @@ class AppCatalogServiceTest {
assertThat(detail.getCommentCount()).isEqualTo(96L);
verify(commentService).countByDrama(42L);
}
@Test
void should_fill_card_counts_with_one_batch_comment_query() {
Drama first = new Drama();
first.setId(1L);
first.setTitle("第一部");
first.setViewCount(1200L);
first.setLikeCount(320L);
first.setFavoriteCount(180L);
Drama second = new Drama();
second.setId(2L);
second.setTitle("第二部");
second.setViewCount(800L);
second.setLikeCount(120L);
second.setFavoriteCount(80L);
given(episodeService.firstPlayableByDramas(List.of(1L, 2L))).willReturn(Map.of());
given(dramaCategoryService.listBasic()).willReturn(List.of());
given(commentService.countByDramas(List.of(1L, 2L)))
.willReturn(Map.of(1L, 9L));
List<AppDramaCardVO> cards = catalogService.toCards(List.of(first, second));
assertThat(cards).hasSize(2);
assertThat(cards.get(0).getLikeCount()).isEqualTo(320L);
assertThat(cards.get(0).getFavoriteCount()).isEqualTo(180L);
assertThat(cards.get(0).getCommentCount()).isEqualTo(9L);
assertThat(cards.get(1).getLikeCount()).isEqualTo(120L);
assertThat(cards.get(1).getFavoriteCount()).isEqualTo(80L);
assertThat(cards.get(1).getCommentCount()).isZero();
verify(commentService).countByDramas(List.of(1L, 2L));
verify(commentService, never()).countByDrama(any());
}
}
@@ -22,6 +22,10 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import java.util.Arrays;
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.BDDMockito.given;
@@ -77,4 +81,32 @@ class CommentServiceCountTest {
assertThat(commentService.countByDrama(null)).isZero();
verify(commentMapper, never()).selectCount(any());
}
@Test
void should_return_empty_map_without_query_when_drama_ids_are_empty() {
assertThat(commentService.countByDramas(List.of())).isEmpty();
verify(commentMapper, never()).selectMaps(any());
}
@Test
void should_group_comment_counts_when_multiple_dramas_are_requested() {
ArgumentCaptor<Wrapper<ContentComment>> captor = ArgumentCaptor.forClass(Wrapper.class);
given(commentMapper.selectMaps(captor.capture())).willReturn(List.of(
Map.of("drama_id", 7L, "comment_count", 5L),
Map.of("drama_id", 8L, "comment_count", 2L)));
Map<Long, Long> counts = commentService.countByDramas(
Arrays.asList(7L, 8L, 8L, null));
assertThat(counts).containsEntry(7L, 5L).containsEntry(8L, 2L);
Wrapper<ContentComment> wrapper = captor.getValue();
assertThat(wrapper.getSqlSegment())
.contains("drama_id")
.contains("status")
.contains("deleted")
.contains("GROUP BY");
AbstractWrapper<ContentComment, ?, ?> lambda = (AbstractWrapper<ContentComment, ?, ?>) wrapper;
assertThat(lambda.getParamNameValuePairs().values())
.contains(7L, 8L, "deleted");
}
}
@@ -325,6 +325,8 @@ public class DramaService {
vo.setStatus(drama.getStatus());
vo.setStatusText(ContentStatus.textOf(drama.getStatus()));
vo.setViews(drama.getViewCount());
vo.setLikeCount(drama.getLikeCount());
vo.setFavoriteCount(drama.getFavoriteCount());
vo.setSort(drama.getSort());
vo.setPublishAt(drama.getPublishedAt());
vo.setCreatedAt(drama.getCreatedAt());
@@ -32,6 +32,9 @@ public class DramaRespVO {
private String status;
private String statusText;
private Long views;
private Long likeCount;
private Long favoriteCount;
private Long commentCount;
private Integer sort;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -3,6 +3,7 @@
package com.fruitshorts.interaction.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -32,10 +33,12 @@ import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
@@ -48,6 +51,9 @@ import java.util.Set;
@RequiredArgsConstructor
public class CommentService {
private static final String DRAMA_ID_COLUMN = "drama_id";
private static final String COMMENT_COUNT_COLUMN = "comment_count";
public static final String STATUS_PENDING = "pending";
public static final String STATUS_PUBLISHED = "published";
public static final String STATUS_REJECTED = "rejected";
@@ -185,6 +191,46 @@ public class CommentService {
return count == null ? 0L : count;
}
/**
* 批量统计多部剧的未删除评论数,按 drama_id 分组,避免逐剧查询。
*/
public Map<Long, Long> countByDramas(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<ContentComment> wrapper = Wrappers.query();
wrapper.select(DRAMA_ID_COLUMN, "COUNT(*) AS " + COMMENT_COUNT_COLUMN)
.in(DRAMA_ID_COLUMN, ids)
.eq("deleted", 0)
.ne("status", STATUS_DELETED)
.groupBy(DRAMA_ID_COLUMN);
List<Map<String, Object>> rows = commentMapper.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_COLUMN));
if (dramaId != null) {
counts.put(dramaId, asLong(row.get(COMMENT_COUNT_COLUMN), 0L));
}
}
return counts;
}
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;
}
// ---------------- 后台 ----------------
public PageResult<CommentRespVO> adminPage(CommentQueryReqVO req) {