diff --git a/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/service/feed/FeedServiceImpl.java b/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/service/feed/FeedServiceImpl.java index 92ba84e6..338c1102 100644 --- a/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/service/feed/FeedServiceImpl.java +++ b/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/service/feed/FeedServiceImpl.java @@ -2,6 +2,7 @@ package cn.wanxiang.game.module.feed.service.feed; import cn.wanxiang.game.module.feed.controller.admin.feed.vo.FeaturedPageReqVO; import cn.wanxiang.game.module.feed.controller.admin.feed.vo.FeaturedReqVO; +import cn.wanxiang.game.module.feed.controller.app.feed.vo.FeedCardRespVO; import cn.wanxiang.game.module.feed.controller.app.feed.vo.FeedInteractReqVO; import cn.wanxiang.game.module.feed.controller.app.feed.vo.FeedStreamReqVO; import cn.wanxiang.game.module.feed.controller.app.feed.vo.FeedStreamRespVO; @@ -17,6 +18,7 @@ import cn.wanxiang.game.module.feed.dal.mysql.rank.FeedRankMapper; import cn.wanxiang.game.module.feed.dto.FeedRankUpsertReqDTO; import cn.wanxiang.game.module.feed.enums.FeedActionEnum; import cn.wanxiang.game.module.project.api.ProjectApi; +import cn.wanxiang.game.module.project.dto.ProjectFeedMetaDTO; import cn.wanxiang.game.module.project.enums.ProjectStatusEnum; import cn.wanxiang.game.module.runtime.api.RuntimePackageApi; import cn.wanxiang.game.module.runtime.enums.PackageStatusEnum; @@ -109,8 +111,11 @@ public class FeedServiceImpl implements FeedService { // 故对 rank 结果批查 project.status 二次过滤——仅 project.status==PUBLISHED(4) 的游戏出流,未发布/降级(下架/封禁/审核中)不出流。 List visibleRanks = filterByProjectPublished(ranks); FeedStreamRespVO resp = new FeedStreamRespVO(); - resp.setList(FeedConvert.toCardList(visibleRanks)); - // TODO 跨模块回填:本体元信息读 project(同库直查或 RPC);登录态用户互动 liked/favorited 读 game_feed_interact_log。 + // 出流卡片:先填本模块自有字段(gameId/versionId/zone/quality),再跨模块回填 project 本体元信息(title/cover/计数) + List cards = FeedConvert.toCardList(visibleRanks); + backfillCardMeta(cards); + resp.setList(cards); + // TODO 登录态用户互动 liked/favorited 读 game_feed_interact_log 回填(当前默认 false)。 // TODO cursor 续接:以末条 sortScore 编码 nextCursor;不足 size 条则 hasMore=false。骨架先返回空串占位。 resp.setNextCursor(""); // hasMore 以二次过滤后的真实出流条数判定(避免被过滤掉的行误判还有更多) @@ -143,6 +148,37 @@ public class FeedServiceImpl implements FeedService { .collect(Collectors.toList()); } + /** + * 跨模块回填卡片本体元信息(§3.1:feed 不持久化本体,经 ProjectApi 本地 bean 读 project 回填) + * + * 按 gameId 去重读 project 元信息(title/coverUrl/playCount/likeCount)回填到卡片; + * 游戏不存在(getFeedMeta data=null)则保留该卡本体字段为空,不阻断出流。 + * + * @param cards 待回填的卡片列表(已按可见态过滤为已发布游戏) + */ + private void backfillCardMeta(List cards) { + if (cards.isEmpty()) { + return; + } + // 去重 gameId → 读 project 元信息(缓存 map,避免同一游戏多次 RPC) + Map metaByGameId = new LinkedHashMap<>(); + for (FeedCardRespVO card : cards) { + metaByGameId.computeIfAbsent(card.getGameId(), + gid -> projectApi.getFeedMeta(gid).getCheckedData()); + } + // 回填本体字段(title/cover/计数);元信息缺失(游戏不存在)则跳过该卡,留空由前端缺省兜底 + for (FeedCardRespVO card : cards) { + ProjectFeedMetaDTO meta = metaByGameId.get(card.getGameId()); + if (meta == null) { + continue; + } + card.setTitle(meta.getTitle()); + card.setCoverUrl(meta.getCoverUrl()); + card.setPlayCount(meta.getPlayCount()); + card.setLikeCount(meta.getLikeCount()); + } + } + @Override public List getZones() { // TODO 跨模块对接 project:读 game_zone(type=1官方精选 2UGC普通、status=1启用),按 sort 升序返回。 diff --git a/game-cloud/game-module-feed/game-module-feed-server/src/test/java/cn/wanxiang/game/module/feed/service/feed/FeedServiceImplTest.java b/game-cloud/game-module-feed/game-module-feed-server/src/test/java/cn/wanxiang/game/module/feed/service/feed/FeedServiceImplTest.java index be7cfb81..b2bee40d 100644 --- a/game-cloud/game-module-feed/game-module-feed-server/src/test/java/cn/wanxiang/game/module/feed/service/feed/FeedServiceImplTest.java +++ b/game-cloud/game-module-feed/game-module-feed-server/src/test/java/cn/wanxiang/game/module/feed/service/feed/FeedServiceImplTest.java @@ -12,6 +12,7 @@ import cn.wanxiang.game.module.feed.dal.mysql.interact.FeedInteractLogMapper; import cn.wanxiang.game.module.feed.dal.mysql.rank.FeedRankMapper; import cn.wanxiang.game.module.feed.dto.FeedRankUpsertReqDTO; import cn.wanxiang.game.module.project.api.ProjectApi; +import cn.wanxiang.game.module.project.dto.ProjectFeedMetaDTO; import cn.wanxiang.game.module.runtime.api.RuntimePackageApi; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -66,6 +67,12 @@ class FeedServiceImplTest extends BaseMockitoUnitTest { // §3.1 C1 读侧可见态二次过滤:两游戏均 project.status=PUBLISHED(4) 才出流(不 stub 则 getStatus 返回 null 被剔除) when(projectApi.getStatus(1L)).thenReturn(CommonResult.success(4)); when(projectApi.getStatus(2L)).thenReturn(CommonResult.success(4)); + // 出流后跨模块回填本体元信息:game1 回填真元信息(断言生效)、game2 无元信息(留空不阻断) + ProjectFeedMetaDTO meta1 = new ProjectFeedMetaDTO(); + meta1.setGameId(1L); + meta1.setTitle("甲游戏"); + when(projectApi.getFeedMeta(1L)).thenReturn(CommonResult.success(meta1)); + when(projectApi.getFeedMeta(2L)).thenReturn(CommonResult.success(null)); FeedStreamReqVO reqVO = new FeedStreamReqVO(); reqVO.setSize(2); @@ -73,6 +80,7 @@ class FeedServiceImplTest extends BaseMockitoUnitTest { assertEquals(2, resp.getList().size()); assertEquals(1L, resp.getList().get(0).getGameId()); + assertEquals("甲游戏", resp.getList().get(0).getTitle()); // 本体元信息已回填 assertTrue(resp.getHasMore()); // 取满 size → 还有下一页 assertEquals("", resp.getNextCursor()); // 骨架阶段 cursor 占位空串 } @@ -85,6 +93,8 @@ class FeedServiceImplTest extends BaseMockitoUnitTest { rank(2L, 11L, 0L, new BigDecimal("80")))); // project 已下架(5) → 剔除 when(projectApi.getStatus(1L)).thenReturn(CommonResult.success(4)); when(projectApi.getStatus(2L)).thenReturn(CommonResult.success(5)); + // game1 出流后回填本体元信息(game2 已被可见态过滤,不参与回填) + when(projectApi.getFeedMeta(1L)).thenReturn(CommonResult.success(null)); FeedStreamReqVO reqVO = new FeedStreamReqVO(); reqVO.setSize(2); diff --git a/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/api/ProjectApi.java b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/api/ProjectApi.java index b090491a..fd9b1596 100644 --- a/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/api/ProjectApi.java +++ b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/api/ProjectApi.java @@ -1,6 +1,7 @@ package cn.wanxiang.game.module.project.api; import cn.wanxiang.game.module.project.enums.ApiConstants; +import cn.wanxiang.game.module.project.dto.ProjectFeedMetaDTO; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -66,6 +67,20 @@ public interface ProjectApi { @Parameter(name = "gameId", description = "游戏项目 ID", required = true, example = "1024") CommonResult getStatus(@RequestParam("gameId") Long gameId); + /** + * 按游戏 ID 取游戏流卡片本体元信息(title/coverUrl/playCount/likeCount,feed 回填用) + * + * feed 不持久化游戏本体元信息,buildStream 出流(已按可见态过滤为已发布游戏)后据此回填卡片标题/封面/计数。 + * 显式列、命中主键查询;游戏不存在时 data 返回 null(feed 侧留空不回填,不阻断出流)。 + * + * @param gameId 游戏项目 ID(game_project.id) + * @return 卡片本体元信息(CommonResult 包裹;游戏不存在为 null) + */ + @GetMapping(PREFIX + "/get-feed-meta") + @Operation(summary = "按游戏 ID 取游戏流卡片本体元信息(feed 回填)") + @Parameter(name = "gameId", description = "游戏项目 ID", required = true, example = "1024") + CommonResult getFeedMeta(@RequestParam("gameId") Long gameId); + /** * 创建项目(studio 写类 RPC,建草稿;显式传 creatorUserId 作 owner) * diff --git a/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/dto/ProjectFeedMetaDTO.java b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/dto/ProjectFeedMetaDTO.java new file mode 100644 index 00000000..2bbde20b --- /dev/null +++ b/game-cloud/game-module-project/game-module-project-api/src/main/java/cn/wanxiang/game/module/project/dto/ProjectFeedMetaDTO.java @@ -0,0 +1,30 @@ +package cn.wanxiang.game.module.project.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 游戏流卡片本体元信息 DTO(feed 回填用,project 对外契约 -api) + * + * feed 不持久化游戏本体元信息(标题/封面/累计计数),由 buildStream 出流(已按可见态过滤为已发布游戏)后, + * 经 {@link cn.wanxiang.game.module.project.api.ProjectApi#getFeedMeta} 回填到 FeedCard。 + * 跨模块共享:消费方(feed)只引用本 -api,不依赖 project 的 -server。 + * + * @author 造梦AI + */ +@Data +public class ProjectFeedMetaDTO implements Serializable { + + /** 游戏 ID(game_project.id) */ + private Long gameId; + /** 标题 */ + private String title; + /** 封面图 URL(首屏卡片展示;可能为空字符串,前端做缺省兜底) */ + private String coverUrl; + /** 累计试玩数(telemetry 回写到 project) */ + private Long playCount; + /** 累计点赞数 */ + private Long likeCount; + +} diff --git a/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/api/ProjectApiImpl.java b/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/api/ProjectApiImpl.java index 373a3bd5..9046f39b 100644 --- a/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/api/ProjectApiImpl.java +++ b/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/api/ProjectApiImpl.java @@ -1,6 +1,7 @@ package cn.wanxiang.game.module.project.api; import cn.wanxiang.game.module.project.controller.app.project.vo.ProjectCreateReqVO; +import cn.wanxiang.game.module.project.dto.ProjectFeedMetaDTO; import cn.wanxiang.game.module.project.service.project.ProjectService; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import jakarta.annotation.Resource; @@ -44,6 +45,12 @@ public class ProjectApiImpl implements ProjectApi { return success(projectService.getStatus(gameId)); } + @Override + public CommonResult getFeedMeta(Long gameId) { + // feed 出流回填:委托 ProjectService 读本体元信息(title/cover/计数);游戏不存在返回 null(feed 侧留空不阻断出流) + return success(projectService.getFeedMeta(gameId)); + } + @Override public CommonResult createProject(Long creatorUserId, String title, String templateId) { // studio 写类 RPC:RPC 无登录上下文,owner=显式 creatorUserId(调用方受信传入),不裸暴露给前端 diff --git a/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/project/ProjectService.java b/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/project/ProjectService.java index 3938b9c5..89caabb3 100644 --- a/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/project/ProjectService.java +++ b/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/project/ProjectService.java @@ -8,6 +8,7 @@ import cn.wanxiang.game.module.project.controller.app.project.vo.ProjectPageReqV import cn.wanxiang.game.module.project.controller.app.project.vo.ProjectPublishReqVO; import cn.wanxiang.game.module.project.controller.app.project.vo.ProjectPublishRespVO; import cn.wanxiang.game.module.project.dal.dataobject.project.ProjectDO; +import cn.wanxiang.game.module.project.dto.ProjectFeedMetaDTO; import cn.iocoder.yudao.framework.common.pojo.PageResult; /** @@ -113,6 +114,16 @@ public interface ProjectService { */ Integer getStatus(Long gameId); + /** + * 取游戏流卡片本体元信息(title/coverUrl/playCount/likeCount,供 feed -api 回填) + * + * 只读、命中主键查询;游戏不存在返回 null(由 feed 侧留空不回填,不阻断出流)。 + * + * @param gameId 游戏项目 ID(game_project.id) + * @return 卡片本体元信息 DTO;游戏不存在返回 null + */ + ProjectFeedMetaDTO getFeedMeta(Long gameId); + /** * 封禁联动下架(R3,供 compliance 跨模块调用) * diff --git a/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/project/ProjectServiceImpl.java b/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/project/ProjectServiceImpl.java index e898db37..60f8b731 100644 --- a/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/project/ProjectServiceImpl.java +++ b/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/project/ProjectServiceImpl.java @@ -11,6 +11,7 @@ import cn.wanxiang.game.module.project.controller.app.project.vo.GateResultVO; import cn.wanxiang.game.module.project.convert.project.ProjectConvert; import cn.wanxiang.game.module.project.dal.dataobject.project.ProjectDO; import cn.wanxiang.game.module.project.dal.dataobject.review.ReviewRecordDO; +import cn.wanxiang.game.module.project.dto.ProjectFeedMetaDTO; import cn.wanxiang.game.module.project.dal.mysql.project.ProjectMapper; import cn.wanxiang.game.module.project.dal.mysql.review.ReviewRecordMapper; import cn.wanxiang.game.module.project.enums.ProjectStatusEnum; @@ -243,6 +244,22 @@ public class ProjectServiceImpl implements ProjectService { return project == null ? null : project.getStatus(); } + @Override + public ProjectFeedMetaDTO getFeedMeta(Long gameId) { + // feed 出流回填:读 game_project 本体元信息映射为对外 DTO;游戏不存在返回 null(feed 侧留空不阻断出流) + ProjectDO project = projectMapper.selectById(gameId); + if (project == null) { + return null; + } + ProjectFeedMetaDTO meta = new ProjectFeedMetaDTO(); + meta.setGameId(project.getId()); + meta.setTitle(project.getTitle()); + meta.setCoverUrl(project.getCoverUrl()); + meta.setPlayCount(project.getPlayCount()); + meta.setLikeCount(project.getLikeCount()); + return meta; + } + @Override public boolean unlistIfPublished(Long gameId) { // R3 封禁联动下架:状态查询 + 下架决策均收敛在 project 权威侧,复用既有 reviewProject 状态机(不重写=D2)