feat(feed): 出流卡片回填 project 本体元信息(title/cover/计数) — 修卡片 title/cover=null
缺口:feed 不持久化游戏本体元信息,buildStream 出流卡片 title/coverUrl/playCount/likeCount 一直为 null(跟踪项①),demo 不可演示。 修复:ProjectApi 新增 getFeedMeta(local @Primary, 契约 -api 路径) + ProjectFeedMetaDTO;ProjectServiceImpl 读 game_project 映射;FeedServiceImpl.buildStream 出流(可见态过滤后)按 gameId 去重回填本体字段,游戏不存在则留空不阻断。不碰 project -server 依赖、不动数据回路。 验证:feed18+project15 单测绿(含 title 回填断言);staging 重部署后 feed/stream 返回真标题且 B(q88)仍在 A(q28)前(数据回路状态存活)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
35f13ab7df
commit
ab0037cd4b
@ -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<FeedRankDO> 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<FeedCardRespVO> 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<FeedCardRespVO> cards) {
|
||||
if (cards.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 去重 gameId → 读 project 元信息(缓存 map,避免同一游戏多次 RPC)
|
||||
Map<Long, ProjectFeedMetaDTO> 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<ZoneRespVO> getZones() {
|
||||
// TODO 跨模块对接 project:读 game_zone(type=1官方精选 2UGC普通、status=1启用),按 sort 升序返回。
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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<Integer> 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<ProjectFeedMetaDTO> getFeedMeta(@RequestParam("gameId") Long gameId);
|
||||
|
||||
/**
|
||||
* 创建项目(studio 写类 RPC,建草稿;显式传 creatorUserId 作 owner)
|
||||
*
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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<ProjectFeedMetaDTO> getFeedMeta(Long gameId) {
|
||||
// feed 出流回填:委托 ProjectService 读本体元信息(title/cover/计数);游戏不存在返回 null(feed 侧留空不阻断出流)
|
||||
return success(projectService.getFeedMeta(gameId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Long> createProject(Long creatorUserId, String title, String templateId) {
|
||||
// studio 写类 RPC:RPC 无登录上下文,owner=显式 creatorUserId(调用方受信传入),不裸暴露给前端
|
||||
|
||||
@ -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 跨模块调用)
|
||||
*
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user