merge: 合并 K89 分集点赞与评论统计
This commit is contained in:
commit
dfc978c620
28 files changed
+683
-41
No files matched your search
+15
-8
@@ -175,9 +175,10 @@ app.register.max-per-device-per-day
|
|||||||
> 卡片(`AppDramaCardVO`)统一新增 `language` 字段(取值如 `zh-CN` / `id`),
|
> 卡片(`AppDramaCardVO`)统一新增 `language` 字段(取值如 `zh-CN` / `id`),
|
||||||
> 首页、发现页、分类、收藏、搜索等所有下发卡片的场景都带该字段,客户端可据此做语言筛选。
|
> 首页、发现页、分类、收藏、搜索等所有下发卡片的场景都带该字段,客户端可据此做语言筛选。
|
||||||
>
|
>
|
||||||
> 卡片同时下发 `likeCount` / `favoriteCount` / `commentCount`(均为 JSON 字符串),
|
> 卡片同时下发 `likeCount` / `favoriteCount` / `commentCount`(均为 JSON 字符串)。
|
||||||
> 点赞、收藏取 `drama` 冗余计数字段,评论数由 `content_comment` 按 `drama_id` 批量统计,
|
> `favoriteCount` 取整部剧;`likeCount` / `commentCount` 取卡片 `preview` 当前集的
|
||||||
> 排除逻辑删除与 `status = 'deleted'` 的记录。首页、发现、分类、收藏、搜索等列表卡片结构一致。
|
> `drama_episode.like_count` 与 `content_comment.episode_id` 统计(排除逻辑删除与 `status = 'deleted'`);
|
||||||
|
> `viewCount` 热度仍取整部剧。首页、发现、分类、收藏、搜索等列表卡片结构一致。
|
||||||
>
|
>
|
||||||
> 卡片新增 `dramaType`:`ANIME` 表示漫剧,`LIVE` 表示真人剧。
|
> 卡片新增 `dramaType`:`ANIME` 表示漫剧,`LIVE` 表示真人剧。
|
||||||
|
|
||||||
@@ -218,6 +219,8 @@ app.register.max-per-device-per-day
|
|||||||
"isFree": true,
|
"isFree": true,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"unlockCoin": 0,
|
"unlockCoin": 0,
|
||||||
|
"likeCount": "45",
|
||||||
|
"commentCount": "8",
|
||||||
"playUrl": "/files/episode/202610/xxx.mp4?e=1760000000&s=9f2c..."
|
"playUrl": "/files/episode/202610/xxx.mp4?e=1760000000&s=9f2c..."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -225,14 +228,16 @@ app.register.max-per-device-per-day
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
详情里的互动数量(K30 新增,供播放页右侧收藏/评论/点赞直接展示):
|
详情里的互动数量:
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `likeCount` | Long(JSON 字符串) | 取 `drama.like_count`,短剧累计点赞数 |
|
| `likeCount` | Long(JSON 字符串) | 剧级历史汇总,保留兼容;新客户端按集使用 `episodes[].likeCount` |
|
||||||
| `favoriteCount` | Long(JSON 字符串) | 取 `drama.favorite_count`,短剧累计收藏数 |
|
| `favoriteCount` | Long(JSON 字符串) | 取 `drama.favorite_count`,短剧累计收藏数 |
|
||||||
| `commentCount` | Long(JSON 字符串) | `content_comment` 中该剧未删除评论数:按 `drama_id` 统计,排除 `status = 'deleted'` 与逻辑删除记录 |
|
| `commentCount` | Long(JSON 字符串) | `content_comment` 中该剧未删除评论数:按 `drama_id` 统计,排除 `status = 'deleted'` 与逻辑删除记录 |
|
||||||
| `viewCount` | Long(JSON 字符串) | 取 `drama.view_count`,累计播放量,既有字段语义不变 |
|
| `viewCount` | Long(JSON 字符串) | 取 `drama.view_count`,累计播放量,既有字段语义不变 |
|
||||||
|
| `episodes[].likeCount` | Long(JSON 字符串) | 本集点赞数,取 `drama_episode.like_count` |
|
||||||
|
| `episodes[].commentCount` | Long(JSON 字符串) | 本集未删除评论数,按 `episode_id` 统计 |
|
||||||
|
|
||||||
计数是只读快照:互动操作完成后客户端重新拉取详情即可刷新;`Long` 字段一律按字符串下发,
|
计数是只读快照:互动操作完成后客户端重新拉取详情即可刷新;`Long` 字段一律按字符串下发,
|
||||||
客户端需要转成数字再展示(与 `viewCount` 相同)。
|
客户端需要转成数字再展示(与 `viewCount` 相同)。
|
||||||
@@ -253,7 +258,9 @@ app.register.max-per-device-per-day
|
|||||||
"playUrl": "/files/episode/202610/xxx.mp4?e=1760000000&s=9f2c...",
|
"playUrl": "/files/episode/202610/xxx.mp4?e=1760000000&s=9f2c...",
|
||||||
"qualities": ["AUTO"],
|
"qualities": ["AUTO"],
|
||||||
"unlockCoin": 0,
|
"unlockCoin": 0,
|
||||||
"previewSeconds": 0
|
"previewSeconds": 0,
|
||||||
|
"likeCount": "45",
|
||||||
|
"commentCount": "8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -307,9 +314,9 @@ app.register.max-per-device-per-day
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| POST | `/app/comments` | 登录 | 发表评论/回复:过敏感词后按策略发布或转待审 |
|
| POST | `/app/comments` | 登录 | 发表评论/回复:过敏感词后按策略发布或转待审 |
|
||||||
| GET | `/app/comments?dramaId=&cursor=&size=` | 匿名 | 游标翻页;不传 `rootId` 返回顶层评论,传了返回该楼层回复 |
|
| GET | `/app/comments?dramaId=&cursor=&size=` | 匿名 | 游标翻页;不传 `rootId` 返回顶层评论,传了返回该楼层回复 |
|
||||||
| GET | `/app/comments/count?dramaId=` | 匿名 | 返回该剧未删除评论总数,`Long` 按字符串下发 |
|
| GET | `/app/comments/count?dramaId=&episodeId=` | 匿名 | 传 `episodeId` 返回该集评论数;仅传 `dramaId` 返回全剧兼容汇总 |
|
||||||
| DELETE | `/app/comments/{id}` | 登录 | 删除自己的评论(逻辑置为 `deleted`) |
|
| DELETE | `/app/comments/{id}` | 登录 | 删除自己的评论(逻辑置为 `deleted`) |
|
||||||
| POST | `/app/likes/toggle` | 登录 | 点赞/取消点赞,`targetType` 支持 `DRAMA` / `COMMENT` |
|
| POST | `/app/likes/toggle` | 登录 | 点赞/取消点赞,`targetType` 支持 `DRAMA` / `EPISODE` / `COMMENT`;新客户端点赞按集 |
|
||||||
| POST | `/app/reports` | 登录 | 举报评论/用户/短剧;同一目标只保留一条待处理工单 |
|
| POST | `/app/reports` | 登录 | 举报评论/用户/短剧;同一目标只保留一条待处理工单 |
|
||||||
|
|
||||||
**敏感词三档处置**:`BLOCK` 直接拒绝并返回原因、`REPLACE` 替换后放行、`REVIEW` 转人工待审。
|
**敏感词三档处置**:`BLOCK` 直接拒绝并返回原因、`REPLACE` 替换后放行、`REVIEW` 转人工待审。
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# K89 点赞/评论按集、收藏/热度按剧实施计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
- 收藏、热度继续归属整部剧。
|
||||||
|
- 点赞、评论归属具体剧集:新增 `drama_episode.like_count`,`content_like` 使用 `EPISODE` 目标。
|
||||||
|
- App 卡片、详情、播放响应按当前集装配点赞/评论;后台短剧/剧集页提供对应汇总与只读展示。
|
||||||
|
|
||||||
|
## FruitServer A
|
||||||
|
|
||||||
|
1. 数据库
|
||||||
|
- 新增 `sql/migrate-k89-episode-like.sql`,只提交不执行。
|
||||||
|
- `sql/schema-phase1.sql` 的 drama_episode 增量字段补 like_count,默认 0。
|
||||||
|
|
||||||
|
2. 分集点赞
|
||||||
|
- `DramaEpisode` 增加 likeCount;`EpisodeService` 提供原子增减与批量 SUM。
|
||||||
|
- `LikeService` 增加 `TYPE_EPISODE`,校验已上线剧集的已上线分集,切换时同步分集计数,返回该集最新值。
|
||||||
|
- 保留 DRAMA/COMMENT 点赞兼容。
|
||||||
|
|
||||||
|
3. 分集评论
|
||||||
|
- `CommentService.countByEpisodes` 按 episode_id 批量分组统计未删除评论。
|
||||||
|
- `/app/comments/count` 增加可选 episodeId;传集数返回该集,只有 dramaId 时维持全剧兼容语义。
|
||||||
|
|
||||||
|
4. App 装配
|
||||||
|
- 卡片 preview 集的 likeCount/commentCount 写入卡片;favoriteCount/viewCount 仍取剧。
|
||||||
|
- 详情 episodes[] 每项增加 likeCount/commentCount。
|
||||||
|
- 播放响应增加 episodeId 对应的 likeCount/commentCount。
|
||||||
|
|
||||||
|
5. 后台
|
||||||
|
- EpisodeRespVO 增加本集 like/comment 与所属剧 favorite/heat;Controller.list 批量回填。
|
||||||
|
- EpisodeSaveReqVO 增加可空 likeCount,update 仅覆盖非 null,负数 BAD_REQUEST。
|
||||||
|
- DramaRespVO likeCount 改为分集 SUM,commentCount 保持全剧汇总;分页一次批量聚合避免 N+1。
|
||||||
|
- 更新 API 契约。
|
||||||
|
|
||||||
|
6. 测试
|
||||||
|
- EPISODE 点赞 +1/-1、不越界、目标不存在。
|
||||||
|
- App 卡片按 preview 集、剧级 favorite/heat。
|
||||||
|
- 后台分集列表计数与剧级收藏/热度。
|
||||||
|
- 短剧分页 likeCount 为分集汇总。
|
||||||
|
|
||||||
|
## ReelShortWeb B
|
||||||
|
|
||||||
|
1. EpisodeItem / EpisodeSaveData 增加对应互动字段。
|
||||||
|
2. 剧集表格增加本集点赞、评论,以及所属剧收藏、热度列。
|
||||||
|
3. 编辑弹窗增加本集点赞输入和评论只读;“按集实时统计”。
|
||||||
|
4. 短剧管理保持收藏/热度可编辑,点赞/评论展示为后端只读汇总。
|
||||||
|
|
||||||
|
## 风险与处理
|
||||||
|
|
||||||
|
- 存量 `drama.like_count` 保留不删,不回填分集;新展示以分集 SUM 为准。
|
||||||
|
- App 评论数按 preview 集统计,只有 episodeId 为空的历史评论仍计入全剧汇总。
|
||||||
|
- 评论/点赞接口保持匿名/登录既有安全语义,不新增第三方依赖。
|
||||||
|
- 迁移脚本由 PM 执行;本地门禁不访问真实数据库。
|
||||||
|
|
||||||
|
## 门禁与 Git
|
||||||
|
|
||||||
|
- FruitServer:`mvn test` 全过;提交 `feat(interaction): 点赞与评论按集统计`;`--no-ff` 合回 develop 并 push。
|
||||||
|
- ReelShortWeb:`pnpm ts:check`、`pnpm build:dev` 通过;提交 `feat(admin): 剧集列表展示分集互动数据`;`--no-ff` 合回 develop 并 push。
|
||||||
|
- 作者统一 `Zhou Weidong <502333560@qq.com>`;禁止 `git add -A`;各自 PROGRESS 追加 K89 留痕。
|
||||||
+11
-5
@@ -6,6 +6,7 @@ import com.fruitshorts.common.pojo.PageResult;
|
|||||||
import com.fruitshorts.common.pojo.Result;
|
import com.fruitshorts.common.pojo.Result;
|
||||||
import com.fruitshorts.content.dal.entity.DramaReviewRecord;
|
import com.fruitshorts.content.dal.entity.DramaReviewRecord;
|
||||||
import com.fruitshorts.content.service.DramaService;
|
import com.fruitshorts.content.service.DramaService;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
import com.fruitshorts.content.vo.DramaPageReqVO;
|
import com.fruitshorts.content.vo.DramaPageReqVO;
|
||||||
import com.fruitshorts.content.vo.DramaRespVO;
|
import com.fruitshorts.content.vo.DramaRespVO;
|
||||||
import com.fruitshorts.content.vo.DramaSaveReqVO;
|
import com.fruitshorts.content.vo.DramaSaveReqVO;
|
||||||
@@ -38,13 +39,14 @@ public class DramaController {
|
|||||||
|
|
||||||
private final DramaService dramaService;
|
private final DramaService dramaService;
|
||||||
private final CommentService commentService;
|
private final CommentService commentService;
|
||||||
|
private final EpisodeService episodeService;
|
||||||
|
|
||||||
@Operation(summary = "短剧分页查询")
|
@Operation(summary = "短剧分页查询")
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||||
public Result<PageResult<DramaRespVO>> page(DramaPageReqVO req) {
|
public Result<PageResult<DramaRespVO>> page(DramaPageReqVO req) {
|
||||||
PageResult<DramaRespVO> page = dramaService.page(req);
|
PageResult<DramaRespVO> page = dramaService.page(req);
|
||||||
fillCommentCounts(page.getList());
|
fillInteractionCounts(page.getList());
|
||||||
return Result.ok(page);
|
return Result.ok(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ public class DramaController {
|
|||||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||||
public Result<DramaRespVO> get(@PathVariable Long id) {
|
public Result<DramaRespVO> get(@PathVariable Long id) {
|
||||||
DramaRespVO drama = dramaService.get(id);
|
DramaRespVO drama = dramaService.get(id);
|
||||||
fillCommentCounts(List.of(drama));
|
fillInteractionCounts(List.of(drama));
|
||||||
return Result.ok(drama);
|
return Result.ok(drama);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,12 +143,16 @@ public class DramaController {
|
|||||||
SecurityUtils.getUserId(), SecurityUtils.getUsername());
|
SecurityUtils.getUserId(), SecurityUtils.getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillCommentCounts(List<DramaRespVO> dramas) {
|
private void fillInteractionCounts(List<DramaRespVO> dramas) {
|
||||||
if (dramas == null || dramas.isEmpty()) {
|
if (dramas == null || dramas.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<Long> ids = dramas.stream().map(DramaRespVO::getId).toList();
|
List<Long> ids = dramas.stream().map(DramaRespVO::getId).toList();
|
||||||
Map<Long, Long> counts = commentService.countByDramas(ids);
|
Map<Long, Long> likeCounts = episodeService.sumLikeCountsByDramas(ids);
|
||||||
dramas.forEach(drama -> drama.setCommentCount(counts.getOrDefault(drama.getId(), 0L)));
|
Map<Long, Long> commentCounts = commentService.countByDramas(ids);
|
||||||
|
dramas.forEach(drama -> {
|
||||||
|
drama.setLikeCount(likeCounts.getOrDefault(drama.getId(), 0L));
|
||||||
|
drama.setCommentCount(commentCounts.getOrDefault(drama.getId(), 0L));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+16
-1
@@ -3,11 +3,14 @@
|
|||||||
package com.fruitshorts.admin.controller;
|
package com.fruitshorts.admin.controller;
|
||||||
|
|
||||||
import com.fruitshorts.common.pojo.Result;
|
import com.fruitshorts.common.pojo.Result;
|
||||||
|
import com.fruitshorts.content.dal.entity.Drama;
|
||||||
|
import com.fruitshorts.content.service.DramaService;
|
||||||
import com.fruitshorts.content.service.EpisodeService;
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
import com.fruitshorts.content.vo.EpisodeRespVO;
|
import com.fruitshorts.content.vo.EpisodeRespVO;
|
||||||
import com.fruitshorts.content.vo.EpisodeSaveReqVO;
|
import com.fruitshorts.content.vo.EpisodeSaveReqVO;
|
||||||
import com.fruitshorts.content.vo.EpisodeSortReqVO;
|
import com.fruitshorts.content.vo.EpisodeSortReqVO;
|
||||||
import com.fruitshorts.framework.log.OperationLog;
|
import com.fruitshorts.framework.log.OperationLog;
|
||||||
|
import com.fruitshorts.interaction.service.CommentService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -24,6 +27,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "后台-剧集管理")
|
@Tag(name = "后台-剧集管理")
|
||||||
@RestController
|
@RestController
|
||||||
@@ -32,13 +36,24 @@ import java.util.List;
|
|||||||
public class EpisodeController {
|
public class EpisodeController {
|
||||||
|
|
||||||
private final EpisodeService episodeService;
|
private final EpisodeService episodeService;
|
||||||
|
private final CommentService commentService;
|
||||||
|
private final DramaService dramaService;
|
||||||
|
|
||||||
@Operation(summary = "剧集列表")
|
@Operation(summary = "剧集列表")
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("@ss.hasPermission('episode:list')")
|
@PreAuthorize("@ss.hasPermission('episode:list')")
|
||||||
public Result<List<EpisodeRespVO>> list(@RequestParam Long dramaId,
|
public Result<List<EpisodeRespVO>> list(@RequestParam Long dramaId,
|
||||||
@RequestParam(required = false) String status) {
|
@RequestParam(required = false) String status) {
|
||||||
return Result.ok(episodeService.listByDrama(dramaId, status));
|
List<EpisodeRespVO> episodes = episodeService.listByDrama(dramaId, status);
|
||||||
|
Map<Long, Long> commentCounts = commentService.countByEpisodes(
|
||||||
|
episodes.stream().map(EpisodeRespVO::getId).toList());
|
||||||
|
Drama drama = dramaService.getExisting(dramaId);
|
||||||
|
episodes.forEach(episode -> {
|
||||||
|
episode.setCommentCount(commentCounts.getOrDefault(episode.getId(), 0L));
|
||||||
|
episode.setFavoriteCount(drama.getFavoriteCount());
|
||||||
|
episode.setHeat(drama.getViewCount());
|
||||||
|
});
|
||||||
|
return Result.ok(episodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "新建剧集")
|
@Operation(summary = "新建剧集")
|
||||||
|
|||||||
+7
-1
@@ -5,6 +5,7 @@ package com.fruitshorts.admin.controller;
|
|||||||
import com.fruitshorts.admin.support.ControllerContractSupport;
|
import com.fruitshorts.admin.support.ControllerContractSupport;
|
||||||
import com.fruitshorts.common.pojo.PageResult;
|
import com.fruitshorts.common.pojo.PageResult;
|
||||||
import com.fruitshorts.content.service.DramaService;
|
import com.fruitshorts.content.service.DramaService;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
import com.fruitshorts.content.vo.DramaPageReqVO;
|
import com.fruitshorts.content.vo.DramaPageReqVO;
|
||||||
import com.fruitshorts.content.vo.DramaRespVO;
|
import com.fruitshorts.content.vo.DramaRespVO;
|
||||||
import com.fruitshorts.interaction.service.CommentService;
|
import com.fruitshorts.interaction.service.CommentService;
|
||||||
@@ -28,14 +29,16 @@ class DramaControllerContractTest {
|
|||||||
|
|
||||||
private DramaService dramaService;
|
private DramaService dramaService;
|
||||||
private CommentService commentService;
|
private CommentService commentService;
|
||||||
|
private EpisodeService episodeService;
|
||||||
private MockMvc mockMvc;
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
dramaService = mock(DramaService.class);
|
dramaService = mock(DramaService.class);
|
||||||
commentService = mock(CommentService.class);
|
commentService = mock(CommentService.class);
|
||||||
|
episodeService = mock(EpisodeService.class);
|
||||||
mockMvc = ControllerContractSupport.mockMvcOf(
|
mockMvc = ControllerContractSupport.mockMvcOf(
|
||||||
new DramaController(dramaService, commentService));
|
new DramaController(dramaService, commentService, episodeService));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -46,6 +49,8 @@ class DramaControllerContractTest {
|
|||||||
.willReturn(new PageResult<>(2L, List.of(first, second)));
|
.willReturn(new PageResult<>(2L, List.of(first, second)));
|
||||||
given(commentService.countByDramas(List.of(7L, 8L)))
|
given(commentService.countByDramas(List.of(7L, 8L)))
|
||||||
.willReturn(Map.of(7L, 11L));
|
.willReturn(Map.of(7L, 11L));
|
||||||
|
given(episodeService.sumLikeCountsByDramas(List.of(7L, 8L)))
|
||||||
|
.willReturn(Map.of(7L, 3200L, 8L, 120L));
|
||||||
|
|
||||||
mockMvc.perform(get("/admin/dramas/page"))
|
mockMvc.perform(get("/admin/dramas/page"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
@@ -65,6 +70,7 @@ class DramaControllerContractTest {
|
|||||||
void should_fill_comment_count_when_detail_is_requested() throws Exception {
|
void should_fill_comment_count_when_detail_is_requested() throws Exception {
|
||||||
given(dramaService.get(7L)).willReturn(drama(7L, 3200L, 1800L));
|
given(dramaService.get(7L)).willReturn(drama(7L, 3200L, 1800L));
|
||||||
given(commentService.countByDramas(List.of(7L))).willReturn(Map.of(7L, 11L));
|
given(commentService.countByDramas(List.of(7L))).willReturn(Map.of(7L, 11L));
|
||||||
|
given(episodeService.sumLikeCountsByDramas(List.of(7L))).willReturn(Map.of(7L, 3200L));
|
||||||
|
|
||||||
mockMvc.perform(get("/admin/dramas/7"))
|
mockMvc.perform(get("/admin/dramas/7"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
|
|||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
package com.fruitshorts.admin.controller;
|
||||||
|
|
||||||
|
import com.fruitshorts.admin.support.ControllerContractSupport;
|
||||||
|
import com.fruitshorts.content.dal.entity.Drama;
|
||||||
|
import com.fruitshorts.content.service.DramaService;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
|
import com.fruitshorts.content.vo.EpisodeRespVO;
|
||||||
|
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.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/** K89:后台剧集列表按集回填点赞/评论,并带所属剧收藏/热度。 */
|
||||||
|
class EpisodeControllerContractTest {
|
||||||
|
|
||||||
|
private EpisodeService episodeService;
|
||||||
|
private CommentService commentService;
|
||||||
|
private DramaService dramaService;
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
episodeService = mock(EpisodeService.class);
|
||||||
|
commentService = mock(CommentService.class);
|
||||||
|
dramaService = mock(DramaService.class);
|
||||||
|
mockMvc = ControllerContractSupport.mockMvcOf(
|
||||||
|
new EpisodeController(episodeService, commentService, dramaService));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_return_episode_and_drama_interaction_counts() throws Exception {
|
||||||
|
given(episodeService.listByDrama(5L, null)).willReturn(List.of(
|
||||||
|
episode(11L, 10L),
|
||||||
|
episode(12L, 20L)));
|
||||||
|
given(commentService.countByEpisodes(List.of(11L, 12L)))
|
||||||
|
.willReturn(Map.of(11L, 3L));
|
||||||
|
Drama drama = new Drama();
|
||||||
|
drama.setFavoriteCount(88L);
|
||||||
|
drama.setViewCount(1000L);
|
||||||
|
given(dramaService.getExisting(5L)).willReturn(drama);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/admin/episodes").param("dramaId", "5"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.data[0].likeCount").value("10"))
|
||||||
|
.andExpect(jsonPath("$.data[0].commentCount").value("3"))
|
||||||
|
.andExpect(jsonPath("$.data[0].favoriteCount").value("88"))
|
||||||
|
.andExpect(jsonPath("$.data[0].heat").value("1000"))
|
||||||
|
.andExpect(jsonPath("$.data[1].likeCount").value("20"))
|
||||||
|
.andExpect(jsonPath("$.data[1].commentCount").value("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private EpisodeRespVO episode(Long id, Long likeCount) {
|
||||||
|
EpisodeRespVO episode = new EpisodeRespVO();
|
||||||
|
episode.setId(id);
|
||||||
|
episode.setDramaId(5L);
|
||||||
|
episode.setLikeCount(likeCount);
|
||||||
|
return episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-2
@@ -3,6 +3,8 @@
|
|||||||
package com.fruitshorts.app.controller;
|
package com.fruitshorts.app.controller;
|
||||||
|
|
||||||
import com.fruitshorts.common.pojo.CursorResult;
|
import com.fruitshorts.common.pojo.CursorResult;
|
||||||
|
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
import com.fruitshorts.common.pojo.Result;
|
import com.fruitshorts.common.pojo.Result;
|
||||||
import com.fruitshorts.framework.security.SecurityUtils;
|
import com.fruitshorts.framework.security.SecurityUtils;
|
||||||
import com.fruitshorts.framework.web.WebUtils;
|
import com.fruitshorts.framework.web.WebUtils;
|
||||||
@@ -24,6 +26,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 评论。
|
* 评论。
|
||||||
*
|
*
|
||||||
@@ -38,6 +42,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
public class AppCommentController {
|
public class AppCommentController {
|
||||||
|
|
||||||
private final CommentService commentService;
|
private final CommentService commentService;
|
||||||
|
private final EpisodeService episodeService;
|
||||||
|
|
||||||
@Operation(summary = "发布评论")
|
@Operation(summary = "发布评论")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@@ -63,9 +68,18 @@ public class AppCommentController {
|
|||||||
return Result.ok(commentService.page(SecurityUtils.getUserIdOrNull(), query));
|
return Result.ok(commentService.page(SecurityUtils.getUserIdOrNull(), query));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "短剧评论总数(可匿名访问)")
|
@Operation(summary = "评论总数:传 episodeId 按集统计,仅 dramaId 时全剧统计(可匿名访问)")
|
||||||
@GetMapping("/count")
|
@GetMapping("/count")
|
||||||
public Result<Long> count(@RequestParam Long dramaId) {
|
public Result<Long> count(@RequestParam Long dramaId,
|
||||||
|
@RequestParam(required = false) Long episodeId) {
|
||||||
|
if (episodeId != null) {
|
||||||
|
DramaEpisode episode = episodeService.getPublished(episodeId);
|
||||||
|
if (!dramaId.equals(episode.getDramaId())) {
|
||||||
|
return Result.ok(0L);
|
||||||
|
}
|
||||||
|
return Result.ok(commentService.countByEpisodes(List.of(episodeId))
|
||||||
|
.getOrDefault(episodeId, 0L));
|
||||||
|
}
|
||||||
return Result.ok(commentService.countByDrama(dramaId));
|
return Result.ok(commentService.countByDrama(dramaId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-2
@@ -5,14 +5,20 @@ package com.fruitshorts.app.controller;
|
|||||||
import com.fruitshorts.app.service.AppCatalogService;
|
import com.fruitshorts.app.service.AppCatalogService;
|
||||||
import com.fruitshorts.app.vo.AppDramaDetailVO;
|
import com.fruitshorts.app.vo.AppDramaDetailVO;
|
||||||
import com.fruitshorts.common.pojo.Result;
|
import com.fruitshorts.common.pojo.Result;
|
||||||
|
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
import com.fruitshorts.framework.security.SecurityUtils;
|
import com.fruitshorts.framework.security.SecurityUtils;
|
||||||
import com.fruitshorts.framework.web.WebUtils;
|
import com.fruitshorts.framework.web.WebUtils;
|
||||||
|
import com.fruitshorts.interaction.service.CommentService;
|
||||||
import com.fruitshorts.playback.service.PlaybackService;
|
import com.fruitshorts.playback.service.PlaybackService;
|
||||||
import com.fruitshorts.playback.vo.PlayRespVO;
|
import com.fruitshorts.playback.vo.PlayRespVO;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -26,6 +32,8 @@ public class AppDramaController {
|
|||||||
|
|
||||||
private final AppCatalogService appCatalogService;
|
private final AppCatalogService appCatalogService;
|
||||||
private final PlaybackService playbackService;
|
private final PlaybackService playbackService;
|
||||||
|
private final EpisodeService episodeService;
|
||||||
|
private final CommentService commentService;
|
||||||
|
|
||||||
/** 详情:包含剧集列表,客户端进详情页一次请求即可渲染 */
|
/** 详情:包含剧集列表,客户端进详情页一次请求即可渲染 */
|
||||||
@Operation(summary = "短剧详情(含剧集列表,进详情页一次请求即可渲染)")
|
@Operation(summary = "短剧详情(含剧集列表,进详情页一次请求即可渲染)")
|
||||||
@@ -41,10 +49,14 @@ public class AppDramaController {
|
|||||||
@Operation(summary = "播放票据:判定权益后签发短时效播放地址")
|
@Operation(summary = "播放票据:判定权益后签发短时效播放地址")
|
||||||
@GetMapping("/episodes/{id}/play")
|
@GetMapping("/episodes/{id}/play")
|
||||||
public Result<PlayRespVO> play(@PathVariable Long id, HttpServletRequest request) {
|
public Result<PlayRespVO> play(@PathVariable Long id, HttpServletRequest request) {
|
||||||
return Result.ok(playbackService.play(
|
PlayRespVO vo = playbackService.play(
|
||||||
SecurityUtils.getUserIdOrNull(),
|
SecurityUtils.getUserIdOrNull(),
|
||||||
id,
|
id,
|
||||||
request.getHeader("X-Device-Id"),
|
request.getHeader("X-Device-Id"),
|
||||||
WebUtils.getClientIp(request)));
|
WebUtils.getClientIp(request));
|
||||||
|
DramaEpisode episode = episodeService.getExisting(id);
|
||||||
|
vo.setLikeCount(episode.getLikeCount() == null ? 0L : episode.getLikeCount());
|
||||||
|
vo.setCommentCount(commentService.countByEpisodes(List.of(id)).getOrDefault(id, 0L));
|
||||||
|
return Result.ok(vo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+15
-6
@@ -106,6 +106,8 @@ public class AppCatalogService {
|
|||||||
public AppDramaDetailVO dramaDetail(Long dramaId) {
|
public AppDramaDetailVO dramaDetail(Long dramaId) {
|
||||||
Drama drama = dramaService.getPublished(dramaId);
|
Drama drama = dramaService.getPublished(dramaId);
|
||||||
List<DramaEpisode> episodes = episodeService.listPublishedByDrama(dramaId);
|
List<DramaEpisode> episodes = episodeService.listPublishedByDrama(dramaId);
|
||||||
|
Map<Long, Long> episodeCommentCounts = commentService.countByEpisodes(
|
||||||
|
episodes.stream().map(DramaEpisode::getId).toList());
|
||||||
String categoryName = categoryNameMap().getOrDefault(drama.getCategoryId(), "");
|
String categoryName = categoryNameMap().getOrDefault(drama.getCategoryId(), "");
|
||||||
|
|
||||||
AppDramaDetailVO detail = new AppDramaDetailVO();
|
AppDramaDetailVO detail = new AppDramaDetailVO();
|
||||||
@@ -123,7 +125,9 @@ public class AppCatalogService {
|
|||||||
detail.setLikeCount(drama.getLikeCount());
|
detail.setLikeCount(drama.getLikeCount());
|
||||||
detail.setFavoriteCount(drama.getFavoriteCount());
|
detail.setFavoriteCount(drama.getFavoriteCount());
|
||||||
detail.setCommentCount(commentService.countByDrama(dramaId));
|
detail.setCommentCount(commentService.countByDrama(dramaId));
|
||||||
detail.setEpisodes(episodes.stream().map(this::toEpisode).toList());
|
detail.setEpisodes(episodes.stream()
|
||||||
|
.map(episode -> toEpisode(episode, episodeCommentCounts))
|
||||||
|
.toList());
|
||||||
return detail;
|
return detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +172,8 @@ public class AppCatalogService {
|
|||||||
private List<AppDramaCardVO> cards(List<Drama> dramas, Map<Long, String> categoryNames) {
|
private List<AppDramaCardVO> cards(List<Drama> dramas, Map<Long, String> categoryNames) {
|
||||||
List<Long> ids = dramas.stream().map(Drama::getId).toList();
|
List<Long> ids = dramas.stream().map(Drama::getId).toList();
|
||||||
Map<Long, DramaEpisode> previews = episodeService.firstPlayableByDramas(ids);
|
Map<Long, DramaEpisode> previews = episodeService.firstPlayableByDramas(ids);
|
||||||
Map<Long, Long> commentCounts = commentService.countByDramas(ids);
|
Map<Long, Long> commentCounts = commentService.countByEpisodes(
|
||||||
|
previews.values().stream().map(DramaEpisode::getId).toList());
|
||||||
return dramas.stream()
|
return dramas.stream()
|
||||||
.map(drama -> toCard(
|
.map(drama -> toCard(
|
||||||
drama, categoryNames, previews.get(drama.getId()), commentCounts))
|
drama, categoryNames, previews.get(drama.getId()), commentCounts))
|
||||||
@@ -187,16 +192,18 @@ public class AppCatalogService {
|
|||||||
card.setTotalEpisodes(drama.getTotalEpisodes());
|
card.setTotalEpisodes(drama.getTotalEpisodes());
|
||||||
card.setIsHot(drama.getIsHot() != null && drama.getIsHot() == 1);
|
card.setIsHot(drama.getIsHot() != null && drama.getIsHot() == 1);
|
||||||
card.setViewCount(drama.getViewCount());
|
card.setViewCount(drama.getViewCount());
|
||||||
card.setLikeCount(drama.getLikeCount());
|
card.setLikeCount(preview == null || preview.getLikeCount() == null
|
||||||
|
? 0L : preview.getLikeCount());
|
||||||
card.setFavoriteCount(drama.getFavoriteCount());
|
card.setFavoriteCount(drama.getFavoriteCount());
|
||||||
card.setCommentCount(commentCounts.getOrDefault(drama.getId(), 0L));
|
card.setCommentCount(preview == null
|
||||||
|
? 0L : commentCounts.getOrDefault(preview.getId(), 0L));
|
||||||
card.setLanguage(drama.getLanguage());
|
card.setLanguage(drama.getLanguage());
|
||||||
card.setDramaType(drama.getDramaType());
|
card.setDramaType(drama.getDramaType());
|
||||||
card.setPreview(preview == null ? null : toEpisode(preview));
|
card.setPreview(preview == null ? null : toEpisode(preview, commentCounts));
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppEpisodeVO toEpisode(DramaEpisode episode) {
|
private AppEpisodeVO toEpisode(DramaEpisode episode, Map<Long, Long> commentCounts) {
|
||||||
AppEpisodeVO vo = new AppEpisodeVO();
|
AppEpisodeVO vo = new AppEpisodeVO();
|
||||||
vo.setId(episode.getId());
|
vo.setId(episode.getId());
|
||||||
vo.setNumber(episode.getEpNo());
|
vo.setNumber(episode.getEpNo());
|
||||||
@@ -208,6 +215,8 @@ public class AppCatalogService {
|
|||||||
vo.setIsFree(free);
|
vo.setIsFree(free);
|
||||||
vo.setLocked(!free);
|
vo.setLocked(!free);
|
||||||
vo.setUnlockCoin(episode.getUnlockCoin());
|
vo.setUnlockCoin(episode.getUnlockCoin());
|
||||||
|
vo.setLikeCount(episode.getLikeCount() == null ? 0L : episode.getLikeCount());
|
||||||
|
vo.setCommentCount(commentCounts.getOrDefault(episode.getId(), 0L));
|
||||||
if (free) {
|
if (free) {
|
||||||
// 播放地址统一在这里签名:客户端拿不到可长期复用的裸地址
|
// 播放地址统一在这里签名:客户端拿不到可长期复用的裸地址
|
||||||
vo.setPlayUrl(playbackSigner.sign(playableSource(episode)));
|
vo.setPlayUrl(playbackSigner.sign(playableSource(episode)));
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public class AppEpisodeVO {
|
|||||||
/** true 表示需要解锁后才能播放 */
|
/** true 表示需要解锁后才能播放 */
|
||||||
private Boolean locked;
|
private Boolean locked;
|
||||||
private Integer unlockCoin;
|
private Integer unlockCoin;
|
||||||
|
private Long likeCount;
|
||||||
|
private Long commentCount;
|
||||||
/**
|
/**
|
||||||
* 可播放时的地址;锁定或不含片源时为空。
|
* 可播放时的地址;锁定或不含片源时为空。
|
||||||
* 详情页拿到就能直接播,不必按集再请求一次播放票据。
|
* 详情页拿到就能直接播,不必按集再请求一次播放票据。
|
||||||
|
|||||||
+4
-1
@@ -10,9 +10,11 @@ import com.fruitshorts.app.vo.AppDramaDetailVO;
|
|||||||
import com.fruitshorts.app.vo.AppFeedVO;
|
import com.fruitshorts.app.vo.AppFeedVO;
|
||||||
import com.fruitshorts.app.vo.AppSectionVO;
|
import com.fruitshorts.app.vo.AppSectionVO;
|
||||||
import com.fruitshorts.common.pojo.CursorResult;
|
import com.fruitshorts.common.pojo.CursorResult;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
import com.fruitshorts.common.exception.ErrorCode;
|
import com.fruitshorts.common.exception.ErrorCode;
|
||||||
import com.fruitshorts.common.exception.ServiceException;
|
import com.fruitshorts.common.exception.ServiceException;
|
||||||
import com.fruitshorts.playback.service.PlaybackService;
|
import com.fruitshorts.playback.service.PlaybackService;
|
||||||
|
import com.fruitshorts.interaction.service.CommentService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
@@ -43,7 +45,8 @@ class AppCatalogControllerContractTest {
|
|||||||
catalogService = mock(AppCatalogService.class);
|
catalogService = mock(AppCatalogService.class);
|
||||||
catalogMvc = ControllerContractSupport.mockMvcOf(new AppCatalogController(catalogService));
|
catalogMvc = ControllerContractSupport.mockMvcOf(new AppCatalogController(catalogService));
|
||||||
dramaMvc = ControllerContractSupport.mockMvcOf(
|
dramaMvc = ControllerContractSupport.mockMvcOf(
|
||||||
new AppDramaController(catalogService, mock(PlaybackService.class)));
|
new AppDramaController(catalogService, mock(PlaybackService.class),
|
||||||
|
mock(EpisodeService.class), mock(CommentService.class)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+25
-1
@@ -3,11 +3,16 @@
|
|||||||
package com.fruitshorts.app.controller;
|
package com.fruitshorts.app.controller;
|
||||||
|
|
||||||
import com.fruitshorts.app.support.ControllerContractSupport;
|
import com.fruitshorts.app.support.ControllerContractSupport;
|
||||||
|
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
import com.fruitshorts.interaction.service.CommentService;
|
import com.fruitshorts.interaction.service.CommentService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -19,13 +24,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
class AppCommentControllerContractTest {
|
class AppCommentControllerContractTest {
|
||||||
|
|
||||||
private CommentService commentService;
|
private CommentService commentService;
|
||||||
|
private EpisodeService episodeService;
|
||||||
private MockMvc mockMvc;
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
commentService = mock(CommentService.class);
|
commentService = mock(CommentService.class);
|
||||||
|
episodeService = mock(EpisodeService.class);
|
||||||
mockMvc = ControllerContractSupport.mockMvcOf(
|
mockMvc = ControllerContractSupport.mockMvcOf(
|
||||||
new AppCommentController(commentService));
|
new AppCommentController(commentService, episodeService));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -40,4 +47,21 @@ class AppCommentControllerContractTest {
|
|||||||
|
|
||||||
verify(commentService).countByDrama(7L);
|
verify(commentService).countByDrama(7L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_count_comments_by_episode_when_episode_id_is_given() throws Exception {
|
||||||
|
DramaEpisode episode = new DramaEpisode();
|
||||||
|
episode.setId(11L);
|
||||||
|
episode.setDramaId(7L);
|
||||||
|
given(episodeService.getPublished(11L)).willReturn(episode);
|
||||||
|
given(commentService.countByEpisodes(List.of(11L))).willReturn(Map.of(11L, 4L));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/app/comments/count")
|
||||||
|
.param("dramaId", "7")
|
||||||
|
.param("episodeId", "11"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.data").value("4"));
|
||||||
|
|
||||||
|
verify(commentService).countByEpisodes(List.of(11L));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -5,6 +5,7 @@ package com.fruitshorts.app.controller;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fruitshorts.app.service.AppCatalogService;
|
import com.fruitshorts.app.service.AppCatalogService;
|
||||||
import com.fruitshorts.common.pojo.CursorResult;
|
import com.fruitshorts.common.pojo.CursorResult;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
import com.fruitshorts.framework.log.AccessDeniedRecorder;
|
import com.fruitshorts.framework.log.AccessDeniedRecorder;
|
||||||
import com.fruitshorts.framework.security.JwtAuthenticationFilter;
|
import com.fruitshorts.framework.security.JwtAuthenticationFilter;
|
||||||
import com.fruitshorts.framework.security.JwtTokenProvider;
|
import com.fruitshorts.framework.security.JwtTokenProvider;
|
||||||
@@ -90,7 +91,7 @@ class AppCommentCountAnonymousSecurityTest {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
AppCommentController appCommentController(CommentService commentService) {
|
AppCommentController appCommentController(CommentService commentService) {
|
||||||
return new AppCommentController(commentService);
|
return new AppCommentController(commentService, mock(EpisodeService.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+17
-2
@@ -3,6 +3,9 @@ package com.fruitshorts.app.controller;
|
|||||||
|
|
||||||
import com.fruitshorts.app.service.AppCatalogService;
|
import com.fruitshorts.app.service.AppCatalogService;
|
||||||
import com.fruitshorts.app.support.ControllerContractSupport;
|
import com.fruitshorts.app.support.ControllerContractSupport;
|
||||||
|
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
|
import com.fruitshorts.interaction.service.CommentService;
|
||||||
import com.fruitshorts.playback.service.PlaybackService;
|
import com.fruitshorts.playback.service.PlaybackService;
|
||||||
import com.fruitshorts.playback.vo.PlayRespVO;
|
import com.fruitshorts.playback.vo.PlayRespVO;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -10,6 +13,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
@@ -30,13 +34,22 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
class AppPlaybackContractTest {
|
class AppPlaybackContractTest {
|
||||||
|
|
||||||
private PlaybackService playbackService;
|
private PlaybackService playbackService;
|
||||||
|
private EpisodeService episodeService;
|
||||||
|
private CommentService commentService;
|
||||||
private MockMvc mockMvc;
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
playbackService = mock(PlaybackService.class);
|
playbackService = mock(PlaybackService.class);
|
||||||
|
episodeService = mock(EpisodeService.class);
|
||||||
|
commentService = mock(CommentService.class);
|
||||||
|
DramaEpisode episode = new DramaEpisode();
|
||||||
|
episode.setLikeCount(88L);
|
||||||
|
given(episodeService.getExisting(any())).willReturn(episode);
|
||||||
|
given(commentService.countByEpisodes(any())).willReturn(Map.of());
|
||||||
mockMvc = ControllerContractSupport.mockMvcOf(
|
mockMvc = ControllerContractSupport.mockMvcOf(
|
||||||
new AppDramaController(mock(AppCatalogService.class), playbackService));
|
new AppDramaController(mock(AppCatalogService.class), playbackService,
|
||||||
|
episodeService, commentService));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -52,7 +65,9 @@ class AppPlaybackContractTest {
|
|||||||
.andExpect(jsonPath("$.data.episodeId").isString())
|
.andExpect(jsonPath("$.data.episodeId").isString())
|
||||||
.andExpect(jsonPath("$.data.accessible").value(true))
|
.andExpect(jsonPath("$.data.accessible").value(true))
|
||||||
.andExpect(jsonPath("$.data.accessType").value(PlaybackService.ACCESS_FREE))
|
.andExpect(jsonPath("$.data.accessType").value(PlaybackService.ACCESS_FREE))
|
||||||
.andExpect(jsonPath("$.data.playUrl").value("/files/ep.mp4?s=1"));
|
.andExpect(jsonPath("$.data.playUrl").value("/files/ep.mp4?s=1"))
|
||||||
|
.andExpect(jsonPath("$.data.likeCount").value("88"))
|
||||||
|
.andExpect(jsonPath("$.data.commentCount").value("0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+27
-8
@@ -5,6 +5,7 @@ import com.fruitshorts.app.vo.AppDramaCardVO;
|
|||||||
import com.fruitshorts.app.vo.AppDramaDetailVO;
|
import com.fruitshorts.app.vo.AppDramaDetailVO;
|
||||||
import com.fruitshorts.common.pojo.CursorResult;
|
import com.fruitshorts.common.pojo.CursorResult;
|
||||||
import com.fruitshorts.content.dal.entity.Drama;
|
import com.fruitshorts.content.dal.entity.Drama;
|
||||||
|
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||||
import com.fruitshorts.content.service.BannerService;
|
import com.fruitshorts.content.service.BannerService;
|
||||||
import com.fruitshorts.content.service.DramaCategoryService;
|
import com.fruitshorts.content.service.DramaCategoryService;
|
||||||
import com.fruitshorts.content.service.DramaService;
|
import com.fruitshorts.content.service.DramaService;
|
||||||
@@ -55,10 +56,12 @@ class AppCatalogServiceTest {
|
|||||||
drama.setLikeCount(3200L);
|
drama.setLikeCount(3200L);
|
||||||
drama.setFavoriteCount(1800L);
|
drama.setFavoriteCount(1800L);
|
||||||
drama.setViewCount(12400L);
|
drama.setViewCount(12400L);
|
||||||
|
DramaEpisode episode = episode(11L, 42L, 55L);
|
||||||
given(dramaService.getPublished(42L)).willReturn(drama);
|
given(dramaService.getPublished(42L)).willReturn(drama);
|
||||||
given(episodeService.listPublishedByDrama(42L)).willReturn(List.of());
|
given(episodeService.listPublishedByDrama(42L)).willReturn(List.of(episode));
|
||||||
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
||||||
given(commentService.countByDrama(42L)).willReturn(96L);
|
given(commentService.countByDrama(42L)).willReturn(96L);
|
||||||
|
given(commentService.countByEpisodes(List.of(11L))).willReturn(Map.of(11L, 2L));
|
||||||
|
|
||||||
AppDramaDetailVO detail = catalogService.dramaDetail(42L);
|
AppDramaDetailVO detail = catalogService.dramaDetail(42L);
|
||||||
|
|
||||||
@@ -66,6 +69,8 @@ class AppCatalogServiceTest {
|
|||||||
assertThat(detail.getLikeCount()).isEqualTo(3200L);
|
assertThat(detail.getLikeCount()).isEqualTo(3200L);
|
||||||
assertThat(detail.getFavoriteCount()).isEqualTo(1800L);
|
assertThat(detail.getFavoriteCount()).isEqualTo(1800L);
|
||||||
assertThat(detail.getCommentCount()).isEqualTo(96L);
|
assertThat(detail.getCommentCount()).isEqualTo(96L);
|
||||||
|
assertThat(detail.getEpisodes().get(0).getLikeCount()).isEqualTo(55L);
|
||||||
|
assertThat(detail.getEpisodes().get(0).getCommentCount()).isEqualTo(2L);
|
||||||
verify(commentService).countByDrama(42L);
|
verify(commentService).countByDrama(42L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +88,13 @@ class AppCatalogServiceTest {
|
|||||||
second.setViewCount(800L);
|
second.setViewCount(800L);
|
||||||
second.setLikeCount(120L);
|
second.setLikeCount(120L);
|
||||||
second.setFavoriteCount(80L);
|
second.setFavoriteCount(80L);
|
||||||
given(episodeService.firstPlayableByDramas(List.of(1L, 2L))).willReturn(Map.of());
|
DramaEpisode firstEpisode = episode(11L, 1L, 320L);
|
||||||
|
DramaEpisode secondEpisode = episode(22L, 2L, 120L);
|
||||||
|
given(episodeService.firstPlayableByDramas(List.of(1L, 2L)))
|
||||||
|
.willReturn(Map.of(1L, firstEpisode, 2L, secondEpisode));
|
||||||
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
||||||
given(commentService.countByDramas(List.of(1L, 2L)))
|
given(commentService.countByEpisodes(List.of(11L, 22L)))
|
||||||
.willReturn(Map.of(1L, 9L));
|
.willReturn(Map.of(11L, 9L));
|
||||||
|
|
||||||
List<AppDramaCardVO> cards = catalogService.toCards(List.of(first, second));
|
List<AppDramaCardVO> cards = catalogService.toCards(List.of(first, second));
|
||||||
|
|
||||||
@@ -97,7 +105,7 @@ class AppCatalogServiceTest {
|
|||||||
assertThat(cards.get(1).getLikeCount()).isEqualTo(120L);
|
assertThat(cards.get(1).getLikeCount()).isEqualTo(120L);
|
||||||
assertThat(cards.get(1).getFavoriteCount()).isEqualTo(80L);
|
assertThat(cards.get(1).getFavoriteCount()).isEqualTo(80L);
|
||||||
assertThat(cards.get(1).getCommentCount()).isZero();
|
assertThat(cards.get(1).getCommentCount()).isZero();
|
||||||
verify(commentService).countByDramas(List.of(1L, 2L));
|
verify(commentService).countByEpisodes(List.of(11L, 22L));
|
||||||
verify(commentService, never()).countByDrama(any());
|
verify(commentService, never()).countByDrama(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,14 +115,17 @@ class AppCatalogServiceTest {
|
|||||||
given(dramaService.listPublishedFeed(List.of("ANIME"), null, 21))
|
given(dramaService.listPublishedFeed(List.of("ANIME"), null, 21))
|
||||||
.willReturn(List.of(anime));
|
.willReturn(List.of(anime));
|
||||||
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
||||||
given(episodeService.firstPlayableByDramas(List.of(1L))).willReturn(Map.of());
|
DramaEpisode preview = episode(11L, 1L, 45L);
|
||||||
given(commentService.countByDramas(List.of(1L))).willReturn(Map.of(1L, 3L));
|
given(episodeService.firstPlayableByDramas(List.of(1L)))
|
||||||
|
.willReturn(Map.of(1L, preview));
|
||||||
|
given(commentService.countByEpisodes(List.of(11L))).willReturn(Map.of(11L, 3L));
|
||||||
|
|
||||||
CursorResult<AppDramaCardVO> result = catalogService.feed("ANIME", null, null);
|
CursorResult<AppDramaCardVO> result = catalogService.feed("ANIME", null, null);
|
||||||
|
|
||||||
assertThat(result.getList()).hasSize(1);
|
assertThat(result.getList()).hasSize(1);
|
||||||
assertThat(result.getList().get(0).getDramaType()).isEqualTo("ANIME");
|
assertThat(result.getList().get(0).getDramaType()).isEqualTo("ANIME");
|
||||||
assertThat(result.getList().get(0).getCommentCount()).isEqualTo(3L);
|
assertThat(result.getList().get(0).getCommentCount()).isEqualTo(3L);
|
||||||
|
assertThat(result.getList().get(0).getLikeCount()).isEqualTo(45L);
|
||||||
assertThat(result.isHasMore()).isFalse();
|
assertThat(result.isHasMore()).isFalse();
|
||||||
verify(dramaService).listPublishedFeed(List.of("ANIME"), null, 21);
|
verify(dramaService).listPublishedFeed(List.of("ANIME"), null, 21);
|
||||||
}
|
}
|
||||||
@@ -140,7 +151,7 @@ class AppCatalogServiceTest {
|
|||||||
.willReturn(List.of(second));
|
.willReturn(List.of(second));
|
||||||
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
||||||
given(episodeService.firstPlayableByDramas(any())).willReturn(Map.of());
|
given(episodeService.firstPlayableByDramas(any())).willReturn(Map.of());
|
||||||
given(commentService.countByDramas(any())).willReturn(Map.of());
|
given(commentService.countByEpisodes(any())).willReturn(Map.of());
|
||||||
|
|
||||||
CursorResult<AppDramaCardVO> firstPage = catalogService.feed(null, null, 1);
|
CursorResult<AppDramaCardVO> firstPage = catalogService.feed(null, null, 1);
|
||||||
CursorResult<AppDramaCardVO> secondPage = catalogService.feed(null, firstPage.getNextCursor(), 1);
|
CursorResult<AppDramaCardVO> secondPage = catalogService.feed(null, firstPage.getNextCursor(), 1);
|
||||||
@@ -160,4 +171,12 @@ class AppCatalogServiceTest {
|
|||||||
drama.setDramaType(dramaType);
|
drama.setDramaType(dramaType);
|
||||||
return drama;
|
return drama;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DramaEpisode episode(Long id, Long dramaId, Long likeCount) {
|
||||||
|
DramaEpisode episode = new DramaEpisode();
|
||||||
|
episode.setId(id);
|
||||||
|
episode.setDramaId(dramaId);
|
||||||
|
episode.setLikeCount(likeCount);
|
||||||
|
return episode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+19
@@ -109,4 +109,23 @@ class CommentServiceCountTest {
|
|||||||
assertThat(lambda.getParamNameValuePairs().values())
|
assertThat(lambda.getParamNameValuePairs().values())
|
||||||
.contains(7L, 8L, "deleted");
|
.contains(7L, 8L, "deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_group_comment_counts_by_episode() {
|
||||||
|
ArgumentCaptor<Wrapper<ContentComment>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||||
|
given(commentMapper.selectMaps(captor.capture())).willReturn(List.of(
|
||||||
|
Map.of("episode_id", 11L, "comment_count", 4L),
|
||||||
|
Map.of("episode_id", 12L, "comment_count", 2L)));
|
||||||
|
|
||||||
|
Map<Long, Long> counts = commentService.countByEpisodes(
|
||||||
|
Arrays.asList(11L, 12L, 12L, null));
|
||||||
|
|
||||||
|
assertThat(counts).containsEntry(11L, 4L).containsEntry(12L, 2L);
|
||||||
|
Wrapper<ContentComment> wrapper = captor.getValue();
|
||||||
|
assertThat(wrapper.getSqlSegment())
|
||||||
|
.contains("episode_id")
|
||||||
|
.contains("status")
|
||||||
|
.contains("deleted")
|
||||||
|
.contains("GROUP BY");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
package com.fruitshorts.app.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
import com.fruitshorts.common.exception.ErrorCode;
|
||||||
|
import com.fruitshorts.common.exception.ServiceException;
|
||||||
|
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||||
|
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
|
||||||
|
import com.fruitshorts.content.service.DramaService;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
|
import com.fruitshorts.content.vo.EpisodeSaveReqVO;
|
||||||
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
/** K89:剧集点赞编辑、原子增减与剧级批量汇总。 */
|
||||||
|
class EpisodeServiceK89Test {
|
||||||
|
|
||||||
|
private DramaEpisodeMapper episodeMapper;
|
||||||
|
private EpisodeService episodeService;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void initTableInfo() {
|
||||||
|
TableInfoHelper.initTableInfo(
|
||||||
|
new MapperBuilderAssistant(new MybatisConfiguration(), ""), DramaEpisode.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
episodeMapper = mock(DramaEpisodeMapper.class);
|
||||||
|
episodeService = new EpisodeService(episodeMapper, mock(DramaService.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void should_use_greatest_zero_when_episode_like_decrements() {
|
||||||
|
given(episodeMapper.update(isNull(), any())).willReturn(1);
|
||||||
|
ArgumentCaptor<Wrapper<DramaEpisode>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||||
|
|
||||||
|
episodeService.adjustLikeCount(11L, -1);
|
||||||
|
|
||||||
|
verify(episodeMapper).update(isNull(), captor.capture());
|
||||||
|
LambdaUpdateWrapper<DramaEpisode> update =
|
||||||
|
(LambdaUpdateWrapper<DramaEpisode>) captor.getValue();
|
||||||
|
assertThat(update.getSqlSet()).contains("GREATEST(like_count - 1, 0)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_update_episode_like_count_without_overwriting_when_null() {
|
||||||
|
DramaEpisode episode = new DramaEpisode();
|
||||||
|
episode.setId(11L);
|
||||||
|
episode.setDramaId(5L);
|
||||||
|
episode.setEpNo(1);
|
||||||
|
episode.setLikeCount(8L);
|
||||||
|
given(episodeMapper.selectById(11L)).willReturn(episode);
|
||||||
|
EpisodeSaveReqVO req = new EpisodeSaveReqVO();
|
||||||
|
req.setDramaId(5L);
|
||||||
|
req.setNumber(1);
|
||||||
|
req.setLikeCount(20L);
|
||||||
|
|
||||||
|
episodeService.update(11L, req);
|
||||||
|
|
||||||
|
assertThat(episode.getLikeCount()).isEqualTo(20L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_reject_negative_episode_like_count() {
|
||||||
|
DramaEpisode episode = new DramaEpisode();
|
||||||
|
episode.setId(11L);
|
||||||
|
episode.setDramaId(5L);
|
||||||
|
episode.setEpNo(1);
|
||||||
|
given(episodeMapper.selectById(11L)).willReturn(episode);
|
||||||
|
EpisodeSaveReqVO req = new EpisodeSaveReqVO();
|
||||||
|
req.setDramaId(5L);
|
||||||
|
req.setNumber(1);
|
||||||
|
req.setLikeCount(-1L);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> episodeService.update(11L, req))
|
||||||
|
.isInstanceOf(ServiceException.class)
|
||||||
|
.satisfies(error -> assertThat(((ServiceException) error).getCode())
|
||||||
|
.isEqualTo(ErrorCode.BAD_REQUEST.getCode()));
|
||||||
|
verify(episodeMapper, never()).updateById(any(DramaEpisode.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_sum_episode_likes_by_drama() {
|
||||||
|
given(episodeMapper.selectMaps(any())).willReturn(List.of(
|
||||||
|
Map.of("drama_id", 5L, "like_count", 120L),
|
||||||
|
Map.of("drama_id", 6L, "like_count", 30L)));
|
||||||
|
|
||||||
|
Map<Long, Long> sums = episodeService.sumLikeCountsByDramas(List.of(5L, 6L));
|
||||||
|
|
||||||
|
assertThat(sums).containsEntry(5L, 120L).containsEntry(6L, 30L);
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
package com.fruitshorts.interaction.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
import com.fruitshorts.common.exception.ErrorCode;
|
||||||
|
import com.fruitshorts.common.exception.ServiceException;
|
||||||
|
import com.fruitshorts.content.dal.entity.Drama;
|
||||||
|
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||||
|
import com.fruitshorts.content.service.DramaService;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
|
import com.fruitshorts.interaction.dal.entity.ContentLike;
|
||||||
|
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
|
||||||
|
import com.fruitshorts.interaction.dal.mapper.ContentLikeMapper;
|
||||||
|
import com.fruitshorts.interaction.vo.LikeToggleReqVO;
|
||||||
|
import com.fruitshorts.interaction.vo.LikeToggleRespVO;
|
||||||
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/** K89:点赞目标支持已上线的分集。 */
|
||||||
|
class LikeServiceEpisodeTest {
|
||||||
|
|
||||||
|
private ContentLikeMapper likeMapper;
|
||||||
|
private EpisodeService episodeService;
|
||||||
|
private DramaService dramaService;
|
||||||
|
private LikeService likeService;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void initTableInfo() {
|
||||||
|
TableInfoHelper.initTableInfo(
|
||||||
|
new MapperBuilderAssistant(new MybatisConfiguration(), ""), ContentLike.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
likeMapper = mock(ContentLikeMapper.class);
|
||||||
|
episodeService = mock(EpisodeService.class);
|
||||||
|
dramaService = mock(DramaService.class);
|
||||||
|
likeService = new LikeService(
|
||||||
|
likeMapper,
|
||||||
|
mock(ContentCommentMapper.class),
|
||||||
|
dramaService,
|
||||||
|
episodeService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_increment_episode_like_and_return_latest_count() {
|
||||||
|
DramaEpisode published = episode(6L);
|
||||||
|
given(episodeService.getPublished(11L)).willReturn(published);
|
||||||
|
given(dramaService.getPublished(5L)).willReturn(new Drama());
|
||||||
|
given(likeMapper.selectOne(any())).willReturn(null);
|
||||||
|
given(episodeService.getExisting(11L)).willReturn(published);
|
||||||
|
|
||||||
|
LikeToggleRespVO result = likeService.toggle(9L, request(11L));
|
||||||
|
|
||||||
|
assertThat(result.getLiked()).isTrue();
|
||||||
|
assertThat(result.getCount()).isEqualTo(6L);
|
||||||
|
verify(episodeService).adjustLikeCount(11L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_decrement_episode_like_when_cancelled() {
|
||||||
|
DramaEpisode published = episode(5L);
|
||||||
|
given(episodeService.getPublished(11L)).willReturn(published);
|
||||||
|
given(dramaService.getPublished(5L)).willReturn(new Drama());
|
||||||
|
given(likeMapper.selectOne(any())).willReturn(new ContentLike());
|
||||||
|
given(episodeService.getExisting(11L)).willReturn(published);
|
||||||
|
|
||||||
|
LikeToggleRespVO result = likeService.toggle(9L, request(11L));
|
||||||
|
|
||||||
|
assertThat(result.getLiked()).isFalse();
|
||||||
|
assertThat(result.getCount()).isEqualTo(5L);
|
||||||
|
verify(likeMapper).physicalDelete(9L, LikeService.TYPE_EPISODE, 11L);
|
||||||
|
verify(episodeService).adjustLikeCount(11L, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_reject_missing_or_unpublished_episode() {
|
||||||
|
given(episodeService.getPublished(11L))
|
||||||
|
.willThrow(new ServiceException(ErrorCode.DATA_NOT_FOUND, "剧集不存在或未上线"));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> likeService.toggle(9L, request(11L)))
|
||||||
|
.isInstanceOf(ServiceException.class);
|
||||||
|
verify(likeMapper, never()).insert(any(ContentLike.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private DramaEpisode episode(Long likeCount) {
|
||||||
|
DramaEpisode episode = new DramaEpisode();
|
||||||
|
episode.setId(11L);
|
||||||
|
episode.setDramaId(5L);
|
||||||
|
episode.setLikeCount(likeCount);
|
||||||
|
return episode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LikeToggleReqVO request(Long episodeId) {
|
||||||
|
LikeToggleReqVO req = new LikeToggleReqVO();
|
||||||
|
req.setTargetType(LikeService.TYPE_EPISODE);
|
||||||
|
req.setTargetId(episodeId);
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -40,6 +40,8 @@ public class DramaEpisode {
|
|||||||
private String status;
|
private String status;
|
||||||
private LocalDateTime releaseAt;
|
private LocalDateTime releaseAt;
|
||||||
private Long playCount;
|
private Long playCount;
|
||||||
|
/** 本集点赞数(点赞目标为 EPISODE) */
|
||||||
|
private Long likeCount;
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
|
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
|
||||||
|
|||||||
+69
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package com.fruitshorts.content.service;
|
package com.fruitshorts.content.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.fruitshorts.common.exception.ErrorCode;
|
import com.fruitshorts.common.exception.ErrorCode;
|
||||||
import com.fruitshorts.common.exception.ServiceException;
|
import com.fruitshorts.common.exception.ServiceException;
|
||||||
@@ -14,9 +15,12 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -103,6 +107,7 @@ public class EpisodeService {
|
|||||||
apply(episode, req);
|
apply(episode, req);
|
||||||
episode.setStatus(req.getStatus() == null ? ContentStatus.DRAFT.getCode() : req.getStatus());
|
episode.setStatus(req.getStatus() == null ? ContentStatus.DRAFT.getCode() : req.getStatus());
|
||||||
episode.setPlayCount(0L);
|
episode.setPlayCount(0L);
|
||||||
|
episode.setLikeCount(nonNegativeOrZero(req.getLikeCount()));
|
||||||
episode.setVideoAssetId(0L);
|
episode.setVideoAssetId(0L);
|
||||||
dramaEpisodeMapper.insert(episode);
|
dramaEpisodeMapper.insert(episode);
|
||||||
dramaService.syncTotalEpisodes(req.getDramaId());
|
dramaService.syncTotalEpisodes(req.getDramaId());
|
||||||
@@ -115,6 +120,9 @@ public class EpisodeService {
|
|||||||
dramaService.getExisting(req.getDramaId());
|
dramaService.getExisting(req.getDramaId());
|
||||||
ensureEpNoAvailable(req.getDramaId(), req.getNumber(), id);
|
ensureEpNoAvailable(req.getDramaId(), req.getNumber(), id);
|
||||||
apply(episode, req);
|
apply(episode, req);
|
||||||
|
if (req.getLikeCount() != null) {
|
||||||
|
episode.setLikeCount(nonNegative(req.getLikeCount()));
|
||||||
|
}
|
||||||
if (req.getStatus() != null) {
|
if (req.getStatus() != null) {
|
||||||
episode.setStatus(req.getStatus());
|
episode.setStatus(req.getStatus());
|
||||||
}
|
}
|
||||||
@@ -157,6 +165,46 @@ public class EpisodeService {
|
|||||||
.set(DramaEpisode::getStatus, ContentStatus.PUBLISHED.getCode()));
|
.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) {
|
private void ensureEpNoAvailable(Long dramaId, Integer epNo, Long excludeId) {
|
||||||
Long count = dramaEpisodeMapper.selectCount(Wrappers.<DramaEpisode>lambdaQuery()
|
Long count = dramaEpisodeMapper.selectCount(Wrappers.<DramaEpisode>lambdaQuery()
|
||||||
.eq(DramaEpisode::getDramaId, dramaId)
|
.eq(DramaEpisode::getDramaId, dramaId)
|
||||||
@@ -194,10 +242,31 @@ public class EpisodeService {
|
|||||||
vo.setStatus(episode.getStatus());
|
vo.setStatus(episode.getStatus());
|
||||||
vo.setStatusText(ContentStatus.textOf(episode.getStatus()));
|
vo.setStatusText(ContentStatus.textOf(episode.getStatus()));
|
||||||
vo.setViews(episode.getPlayCount());
|
vo.setViews(episode.getPlayCount());
|
||||||
|
vo.setLikeCount(episode.getLikeCount() == null ? 0L : episode.getLikeCount());
|
||||||
vo.setSort(episode.getSort());
|
vo.setSort(episode.getSort());
|
||||||
return vo;
|
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) {
|
private String formatDuration(Integer seconds) {
|
||||||
int value = seconds == null ? 0 : seconds;
|
int value = seconds == null ? 0 : seconds;
|
||||||
return String.format("%02d:%02d", value / 60, value % 60);
|
return String.format("%02d:%02d", value / 60, value % 60);
|
||||||
|
|||||||
@@ -21,5 +21,13 @@ public class EpisodeRespVO {
|
|||||||
private String status;
|
private String status;
|
||||||
private String statusText;
|
private String statusText;
|
||||||
private Long views;
|
private Long views;
|
||||||
|
/** 本集点赞数 */
|
||||||
|
private Long likeCount;
|
||||||
|
/** 本集评论数 */
|
||||||
|
private Long commentCount;
|
||||||
|
/** 所属剧收藏数,只读 */
|
||||||
|
private Long favoriteCount;
|
||||||
|
/** 所属剧热度(drama.view_count),只读 */
|
||||||
|
private Long heat;
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
}
|
}
|
||||||
+3
@@ -3,6 +3,7 @@
|
|||||||
package com.fruitshorts.content.vo;
|
package com.fruitshorts.content.vo;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.PositiveOrZero;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -20,6 +21,8 @@ public class EpisodeSaveReqVO {
|
|||||||
private String videoUrl;
|
private String videoUrl;
|
||||||
private Boolean isFree;
|
private Boolean isFree;
|
||||||
private Integer unlockCoin;
|
private Integer unlockCoin;
|
||||||
|
@PositiveOrZero(message = "本集点赞数不能小于 0")
|
||||||
|
private Long likeCount;
|
||||||
private String status;
|
private String status;
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
}
|
}
|
||||||
+30
@@ -52,6 +52,7 @@ import java.util.Set;
|
|||||||
public class CommentService {
|
public class CommentService {
|
||||||
|
|
||||||
private static final String DRAMA_ID_COLUMN = "drama_id";
|
private static final String DRAMA_ID_COLUMN = "drama_id";
|
||||||
|
private static final String EPISODE_ID_COLUMN = "episode_id";
|
||||||
private static final String COMMENT_COUNT_COLUMN = "comment_count";
|
private static final String COMMENT_COUNT_COLUMN = "comment_count";
|
||||||
|
|
||||||
public static final String STATUS_PENDING = "pending";
|
public static final String STATUS_PENDING = "pending";
|
||||||
@@ -222,6 +223,35 @@ public class CommentService {
|
|||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 批量统计多集未删除评论数,按 episode_id 分组。 */
|
||||||
|
public Map<Long, Long> countByEpisodes(Collection<Long> episodeIds) {
|
||||||
|
if (episodeIds == null || episodeIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
List<Long> ids = episodeIds.stream().filter(Objects::nonNull).distinct().toList();
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
QueryWrapper<ContentComment> wrapper = Wrappers.query();
|
||||||
|
wrapper.select(EPISODE_ID_COLUMN, "COUNT(*) AS " + COMMENT_COUNT_COLUMN)
|
||||||
|
.in(EPISODE_ID_COLUMN, ids)
|
||||||
|
.eq("deleted", 0)
|
||||||
|
.ne("status", STATUS_DELETED)
|
||||||
|
.groupBy(EPISODE_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 episodeId = asLong(row.get(EPISODE_ID_COLUMN));
|
||||||
|
if (episodeId != null) {
|
||||||
|
counts.put(episodeId, asLong(row.get(COMMENT_COUNT_COLUMN), 0L));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
private Long asLong(Object value) {
|
private Long asLong(Object value) {
|
||||||
return value instanceof Number number ? number.longValue() : null;
|
return value instanceof Number number ? number.longValue() : null;
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-2
@@ -5,7 +5,9 @@ package com.fruitshorts.interaction.service;
|
|||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.fruitshorts.common.exception.ErrorCode;
|
import com.fruitshorts.common.exception.ErrorCode;
|
||||||
import com.fruitshorts.common.exception.ServiceException;
|
import com.fruitshorts.common.exception.ServiceException;
|
||||||
|
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||||
import com.fruitshorts.content.service.DramaService;
|
import com.fruitshorts.content.service.DramaService;
|
||||||
|
import com.fruitshorts.content.service.EpisodeService;
|
||||||
import com.fruitshorts.interaction.dal.entity.ContentComment;
|
import com.fruitshorts.interaction.dal.entity.ContentComment;
|
||||||
import com.fruitshorts.interaction.dal.entity.ContentLike;
|
import com.fruitshorts.interaction.dal.entity.ContentLike;
|
||||||
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
|
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
|
||||||
@@ -29,17 +31,19 @@ import java.util.Locale;
|
|||||||
public class LikeService {
|
public class LikeService {
|
||||||
|
|
||||||
public static final String TYPE_DRAMA = "DRAMA";
|
public static final String TYPE_DRAMA = "DRAMA";
|
||||||
|
public static final String TYPE_EPISODE = "EPISODE";
|
||||||
public static final String TYPE_COMMENT = "COMMENT";
|
public static final String TYPE_COMMENT = "COMMENT";
|
||||||
|
|
||||||
private final ContentLikeMapper likeMapper;
|
private final ContentLikeMapper likeMapper;
|
||||||
private final ContentCommentMapper commentMapper;
|
private final ContentCommentMapper commentMapper;
|
||||||
private final DramaService dramaService;
|
private final DramaService dramaService;
|
||||||
|
private final EpisodeService episodeService;
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public LikeToggleRespVO toggle(Long userId, LikeToggleReqVO req) {
|
public LikeToggleRespVO toggle(Long userId, LikeToggleReqVO req) {
|
||||||
String type = req.getTargetType().trim().toUpperCase(Locale.ROOT);
|
String type = req.getTargetType().trim().toUpperCase(Locale.ROOT);
|
||||||
if (!TYPE_DRAMA.equals(type) && !TYPE_COMMENT.equals(type)) {
|
if (!TYPE_DRAMA.equals(type) && !TYPE_EPISODE.equals(type) && !TYPE_COMMENT.equals(type)) {
|
||||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "只支持对短剧或评论点赞");
|
throw new ServiceException(ErrorCode.BAD_REQUEST, "只支持对短剧、剧集或评论点赞");
|
||||||
}
|
}
|
||||||
Long targetId = req.getTargetId();
|
Long targetId = req.getTargetId();
|
||||||
validateTarget(type, targetId);
|
validateTarget(type, targetId);
|
||||||
@@ -74,6 +78,10 @@ public class LikeService {
|
|||||||
return dramaService.getExisting(targetId).getLikeCount() == null
|
return dramaService.getExisting(targetId).getLikeCount() == null
|
||||||
? 0L : dramaService.getExisting(targetId).getLikeCount();
|
? 0L : dramaService.getExisting(targetId).getLikeCount();
|
||||||
}
|
}
|
||||||
|
if (TYPE_EPISODE.equals(type)) {
|
||||||
|
return episodeService.getExisting(targetId).getLikeCount() == null
|
||||||
|
? 0L : episodeService.getExisting(targetId).getLikeCount();
|
||||||
|
}
|
||||||
ContentComment comment = commentMapper.selectById(targetId);
|
ContentComment comment = commentMapper.selectById(targetId);
|
||||||
return comment == null || comment.getLikeCount() == null ? 0L : comment.getLikeCount();
|
return comment == null || comment.getLikeCount() == null ? 0L : comment.getLikeCount();
|
||||||
}
|
}
|
||||||
@@ -83,6 +91,10 @@ public class LikeService {
|
|||||||
dramaService.adjustLikeCount(targetId, delta);
|
dramaService.adjustLikeCount(targetId, delta);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (TYPE_EPISODE.equals(type)) {
|
||||||
|
episodeService.adjustLikeCount(targetId, delta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
commentMapper.update(null, Wrappers.<ContentComment>lambdaUpdate()
|
commentMapper.update(null, Wrappers.<ContentComment>lambdaUpdate()
|
||||||
.eq(ContentComment::getId, targetId)
|
.eq(ContentComment::getId, targetId)
|
||||||
.setSql(delta > 0
|
.setSql(delta > 0
|
||||||
@@ -95,6 +107,11 @@ public class LikeService {
|
|||||||
dramaService.getPublished(targetId);
|
dramaService.getPublished(targetId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (TYPE_EPISODE.equals(type)) {
|
||||||
|
DramaEpisode episode = episodeService.getPublished(targetId);
|
||||||
|
dramaService.getPublished(episode.getDramaId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
ContentComment comment = commentMapper.selectById(targetId);
|
ContentComment comment = commentMapper.selectById(targetId);
|
||||||
if (comment == null || CommentService.STATUS_PUBLISHED.equals(comment.getStatus()) == false) {
|
if (comment == null || CommentService.STATUS_PUBLISHED.equals(comment.getStatus()) == false) {
|
||||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "评论不存在或未通过审核");
|
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "评论不存在或未通过审核");
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class LikeToggleReqVO {
|
public class LikeToggleReqVO {
|
||||||
|
|
||||||
/** DRAMA/COMMENT */
|
/** DRAMA/EPISODE/COMMENT */
|
||||||
@NotBlank(message = "缺少 targetType")
|
@NotBlank(message = "缺少 targetType")
|
||||||
private String targetType;
|
private String targetType;
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,8 @@ public class PlayRespVO {
|
|||||||
private Integer unlockCoin;
|
private Integer unlockCoin;
|
||||||
/** 试看秒数,0 表示不限制 */
|
/** 试看秒数,0 表示不限制 */
|
||||||
private Integer previewSeconds;
|
private Integer previewSeconds;
|
||||||
|
/** 本集点赞数 */
|
||||||
|
private Long likeCount;
|
||||||
|
/** 本集评论数 */
|
||||||
|
private Long commentCount;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS like_count BIGINT NOT NULL DEFAULT 0;
|
||||||
|
COMMENT ON COLUMN drama_episode.like_count IS '本集点赞数(点赞目标为 EPISODE)';
|
||||||
@@ -44,6 +44,7 @@ ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS preview_seconds INT
|
|||||||
ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS subtitle_url VARCHAR(512) NOT NULL DEFAULT '';
|
ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS subtitle_url VARCHAR(512) NOT NULL DEFAULT '';
|
||||||
ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS subtitle_lang VARCHAR(16) NOT NULL DEFAULT '';
|
ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS subtitle_lang VARCHAR(16) NOT NULL DEFAULT '';
|
||||||
ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS is_vip_only SMALLINT NOT NULL DEFAULT 0; -- 预留:会员专属(第二阶段)
|
ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS is_vip_only SMALLINT NOT NULL DEFAULT 0; -- 预留:会员专属(第二阶段)
|
||||||
|
ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS like_count BIGINT NOT NULL DEFAULT 0; -- 本集点赞数
|
||||||
|
|
||||||
-- 媒资:CDN 地址与转码状态
|
-- 媒资:CDN 地址与转码状态
|
||||||
ALTER TABLE media_asset ADD COLUMN IF NOT EXISTS cdn_url VARCHAR(512) NOT NULL DEFAULT '';
|
ALTER TABLE media_asset ADD COLUMN IF NOT EXISTS cdn_url VARCHAR(512) NOT NULL DEFAULT '';
|
||||||
|
|||||||
Reference in new issue
Block a user