From 29564c8eb718acc9aabf1c2b6ab67a32ace2e743 Mon Sep 17 00:00:00 2001 From: zizi Date: Thu, 11 Jun 2026 05:55:15 +0000 Subject: [PATCH] =?UTF-8?q?fix(publish):=20=E4=BF=AE=E5=88=9B=E4=BD=9C?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E5=8F=91=E5=B8=83=20UI=20=E4=B8=A4=E7=BC=BA?= =?UTF-8?q?=E9=99=B7=E2=80=94=E2=80=94=E7=94=9F=E6=88=90=E5=9B=9E=E5=86=99?= =?UTF-8?q?=20currentVersionId=20+=20getZones=20=E8=90=BD=E5=AE=9E(HJ-FE-W?= =?UTF-8?q?ALK-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI 走查逮到、阻断真人 UI 发布的两个后端缺陷(编排器走 task.versionId 旁路掩盖): Bug A: 生成成功未回写 project.current_version_id - GameVersionServiceImpl.createForPackage 建版本后调 bindCurrentVersion 回写(幂等+新建两路径) - 原先该字段只在发布方法置位→生成后未发布阶段恒 null→Publish.vue 取不到 versionId/门禁⑤失败 - 安全:feed/play 用 game_feed_rank 的 versionId 不读 currentVersionId,对已发布游戏回写无副作用 Bug B: feed/zones 返回空(getZones 是 Collections.emptyList 桩) - FeedServiceImpl.getZones 落实直查 game_zone(type∈(1,2) AND status=1,sort 升序→ZoneRespVO) - 新增最小只读 GameZoneDO + GameZoneMapper(MVP 既定"同库可直查 game_zone") - 租户:GameZoneDO @TenantIgnore + staging tenant.enable=false→tenant_id=0 种子专区可取(实证) - 原桩致发布页专区选择器恒"加载中…"、launchZoneId 必填缺失而无法发布 子代理(Opus)执行+主 agent 核验(diff/租户口径/staging tenant.enable=false 实证);待 mini 构建+重部署+重走查闭合链路②。 Co-Authored-By: Claude Opus 4.8 --- .../feed/dal/dataobject/zone/GameZoneDO.java | 54 +++++++++++++++++++ .../feed/dal/mysql/zone/GameZoneMapper.java | 37 +++++++++++++ .../feed/service/feed/FeedServiceImpl.java | 31 +++++++++-- .../version/GameVersionServiceImpl.java | 41 ++++++++++++++ 4 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/dal/dataobject/zone/GameZoneDO.java create mode 100644 game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/dal/mysql/zone/GameZoneMapper.java diff --git a/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/dal/dataobject/zone/GameZoneDO.java b/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/dal/dataobject/zone/GameZoneDO.java new file mode 100644 index 00000000..241f3ca1 --- /dev/null +++ b/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/dal/dataobject/zone/GameZoneDO.java @@ -0,0 +1,54 @@ +package cn.wanxiang.game.module.feed.dal.dataobject.zone; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 专区 DO(对应表 game_zone,修 HJ-FE-WALK-001 Bug B 新增) + * + * 背景:feed 不拥有 Zone 实体,Zone 权威归 project(owner=project,V1 DDL/种子)。 + * 但发布页专区选择器需读「启用专区」列表(getZones),MVP 既定走「同库可直查 game_zone」, + * 故在 feed 侧建此最小只读 DO 直查,待 project -api RPC 就绪后可改 Feign(不影响表结构)。 + * + * 租户处置(最易翻车点):game_zone 是 project 的全局基础数据、非租户隔离,V1 种子行 tenant_id=0。 + * 故本 DO 刻意 **不继承 TenantBaseDO、改继承 {@link BaseDO}** 且加 {@link TenantIgnore} 注解—— + * 使 yudao 租户拦截器 TenantDatabaseInterceptor.computeIgnoreTable 判定为「忽略租户」, + * 查询不追加 tenant_id 条件,从而在 tenant=1 的业务请求下也能取到 tenant_id=0 的种子专区(参照 system DictTypeDO 范式)。 + * + * @author 绘境AI + */ +@TableName("game_zone") +@KeySequence("game_zone_seq") // Oracle/PostgreSQL 等主键自增用;MySQL 可忽略 +@Data +@EqualsAndHashCode(callSuper = true) +@TenantIgnore // 全局基础数据,忽略租户过滤:保证读到 tenant_id=0 的种子专区(详见类注释「租户处置」) +public class GameZoneDO extends BaseDO { + + /** + * 专区 ID + */ + @TableId + private Long id; + /** + * 专区名称 + */ + private String name; + /** + * 专区类型(双轨):1官方精选 2UGC普通 + */ + private Integer type; + /** + * 排序(升序) + */ + private Integer sort; + /** + * 状态:0停用 1启用 + */ + private Integer status; + +} diff --git a/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/dal/mysql/zone/GameZoneMapper.java b/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/dal/mysql/zone/GameZoneMapper.java new file mode 100644 index 00000000..46e846a2 --- /dev/null +++ b/game-cloud/game-module-feed/game-module-feed-server/src/main/java/cn/wanxiang/game/module/feed/dal/mysql/zone/GameZoneMapper.java @@ -0,0 +1,37 @@ +package cn.wanxiang.game.module.feed.dal.mysql.zone; + +import cn.wanxiang.game.module.feed.dal.dataobject.zone.GameZoneDO; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Arrays; +import java.util.List; + +/** + * 专区 Mapper(修 HJ-FE-WALK-001 Bug B 新增,feed 侧只读直查 game_zone) + * + * 仅承载发布页专区选择器所需的「启用专区列表」查询;表权威归 project,本模块只读不写。 + * + * @author 绘境AI + */ +@Mapper +public interface GameZoneMapper extends BaseMapperX { + + /** + * 取启用的双轨专区,按 sort 升序(发布页专区选择器,命中 idx_type_status) + * + * 过滤条件:type ∈ (1官方精选, 2UGC普通) AND status=1 启用。 + * deleted 由 MyBatis-Plus @TableLogic 自动追加;tenant 不过滤——GameZoneDO 已 @TenantIgnore, + * 拦截器不追加 tenant_id 条件,故 tenant_id=0 的种子专区在任意租户上下文下均可取到(详见 GameZoneDO 类注释)。 + * + * @return 启用专区列表(已按 sort 升序) + */ + default List selectEnabledZones() { + return selectList(new LambdaQueryWrapperX() + .in(GameZoneDO::getType, Arrays.asList(1, 2)) // 双轨类型:1官方精选 / 2UGC普通 + .eq(GameZoneDO::getStatus, 1) // 仅启用专区 + .orderByAsc(GameZoneDO::getSort)); // 排序升序出列 + } + +} 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 981699e1..6fb38fcf 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 @@ -13,8 +13,10 @@ import cn.wanxiang.game.module.feed.controller.app.feed.vo.ZoneStreamReqVO; import cn.wanxiang.game.module.feed.convert.feed.FeedConvert; import cn.wanxiang.game.module.feed.dal.dataobject.interact.FeedInteractLogDO; import cn.wanxiang.game.module.feed.dal.dataobject.rank.FeedRankDO; +import cn.wanxiang.game.module.feed.dal.dataobject.zone.GameZoneDO; 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.dal.mysql.zone.GameZoneMapper; import cn.wanxiang.game.module.feed.dto.FeedRankUpsertReqDTO; import cn.wanxiang.game.module.feed.enums.FeedActionEnum; import cn.wanxiang.game.module.project.api.ProjectApi; @@ -30,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.math.BigDecimal; -import java.util.Collections; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -61,6 +63,15 @@ public class FeedServiceImpl implements FeedService { @Resource private FeedInteractLogMapper feedInteractLogMapper; + /** + * 专区 Mapper(修 HJ-FE-WALK-001 Bug B:发布页专区选择器读启用专区) + * + * MVP 既定「同库可直查 game_zone」:feed 侧只读直查,待 project -api RPC 就绪后可改 Feign。 + * GameZoneDO 已 @TenantIgnore,故种子专区(tenant_id=0)在任意租户上下文下均可取到。 + */ + @Resource + private GameZoneMapper gameZoneMapper; + /** * project 模块 RPC 契约(仅依赖 -api) * @@ -181,9 +192,21 @@ public class FeedServiceImpl implements FeedService { @Override public List getZones() { - // TODO 跨模块对接 project:读 game_zone(type=1官方精选 2UGC普通、status=1启用),按 sort 升序返回。 - // MVP 同库可直查 game_zone,或待 project -api RPC 就绪后改 Feign。骨架阶段返回空列表占位。 - return Collections.emptyList(); + // 修 HJ-FE-WALK-001 Bug B:原桩恒返回空列表,致发布页专区选择器恒「专区加载中…」、launchZoneId 必填缺失而无法发布。 + // 实现:MVP 既定「同库可直查 game_zone」,直查启用的双轨专区(type∈(1,2) AND status=1),按 sort 升序。 + // 租户:GameZoneDO 已 @TenantIgnore,查询不追加 tenant_id 条件,故 tenant_id=0 的种子专区在 tenant=1 请求下也能取到。 + List zones = gameZoneMapper.selectEnabledZones(); + List result = new ArrayList<>(zones.size()); + for (GameZoneDO zone : zones) { + // 仅映射前端选择器所需字段(id/name/type/sort),与契约 ZoneRespVO 对齐 + ZoneRespVO vo = new ZoneRespVO(); + vo.setId(zone.getId()); + vo.setName(zone.getName()); + vo.setType(zone.getType()); + vo.setSort(zone.getSort()); + result.add(vo); + } + return result; } // ============================== 互动信号 ============================== diff --git a/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/version/GameVersionServiceImpl.java b/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/version/GameVersionServiceImpl.java index 89ee01c6..50ed9a6f 100644 --- a/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/version/GameVersionServiceImpl.java +++ b/game-cloud/game-module-project/game-module-project-server/src/main/java/cn/wanxiang/game/module/project/service/version/GameVersionServiceImpl.java @@ -1,6 +1,8 @@ package cn.wanxiang.game.module.project.service.version; +import cn.wanxiang.game.module.project.dal.dataobject.project.ProjectDO; import cn.wanxiang.game.module.project.dal.dataobject.version.GameVersionDO; +import cn.wanxiang.game.module.project.dal.mysql.project.ProjectMapper; import cn.wanxiang.game.module.project.dal.mysql.version.GameVersionMapper; import cn.wanxiang.game.module.project.dto.ProjectVersionCreateForPackageReqDTO; import jakarta.annotation.Resource; @@ -29,6 +31,14 @@ public class GameVersionServiceImpl implements GameVersionService { @Resource private GameVersionMapper gameVersionMapper; + /** + * 游戏项目 Mapper(修 HJ-FE-WALK-001 Bug A:落包建版本后回写 project.current_version_id) + * + * 仅用于把刚建好的版本 ID 绑定为项目「当前生效版本」,使前端发布页据此识别「存在可发布版本」。 + */ + @Resource + private ProjectMapper projectMapper; + @Override public Long createForPackage(ProjectVersionCreateForPackageReqDTO req) { // 幂等:同 genTaskId 已建版本则直接复用,不重复建(PackageFactory 重复落包安全) @@ -37,6 +47,8 @@ public class GameVersionServiceImpl implements GameVersionService { if (existing != null) { log.info("[createForPackage] 落包建版本幂等命中,复用已建版本 genTaskId={}, versionId={}", req.getGenTaskId(), existing.getId()); + // Bug A 修复:幂等命中路径也回写 current_version_id(防御重复落包但项目尚未绑定版本的场景) + bindCurrentVersion(req.getGameId(), existing.getId()); return existing.getId(); } } @@ -52,9 +64,38 @@ public class GameVersionServiceImpl implements GameVersionService { gameVersionMapper.insert(version); log.info("[createForPackage] 落包建版本成功 gameId={}, genTaskId={}, versionId={}", req.getGameId(), req.getGenTaskId(), version.getId()); + // Bug A 修复:新建版本后立即回写 current_version_id,让前端发布页识别「存在可发布版本」并取到 versionId + bindCurrentVersion(req.getGameId(), version.getId()); return version.getId(); } + /** + * 回写项目「当前生效版本」ID(修 HJ-FE-WALK-001 Bug A) + * + * 背景:原先 current_version_id 仅在发布方法里才被置位,导致「生成成功→未发布」阶段恒为 null, + * 前端发布页 Publish.vue 读 project.currentVersionId 取 versionId、门禁⑤「存在可发布版本」也读它, + * 故真人 UI 永远拿不到 versionId、无法发起发布(编排器批跑因走 task.versionId 旁路而掩盖了该缺陷)。 + * 此处在落包建版本成功后回写,打通该死锁。 + * + * 安全性:feed 出流 / runtime 即玩用的是 game_feed_rank 持久化的 versionId,不读 project.currentVersionId, + * 故对「已发布游戏再次落包」回写 current_version_id 不影响线上出流与试玩,无副作用。 + * + * @param gameId 游戏项目 ID(= GamePackage.gameId) + * @param versionId 待绑定为当前生效版本的版本 ID + */ + private void bindCurrentVersion(Long gameId, Long versionId) { + // 入参任一为空则不回写(防御:缺 gameId 无法定位项目、缺 versionId 无意义),直接返回 + if (gameId == null || versionId == null) { + return; + } + // 仅更新 current_version_id 单列:其余字段不置入 update DO,MyBatis-Plus 默认策略跳过 null,不覆盖原值 + ProjectDO update = new ProjectDO(); + update.setId(gameId); + update.setCurrentVersionId(versionId); + projectMapper.updateById(update); + log.info("[createForPackage] 已回写项目当前生效版本 gameId={}, currentVersionId={}", gameId, versionId); + } + @Override public GameVersionDO getVersion(Long versionId) { return gameVersionMapper.selectById(versionId);