diff --git a/game-cloud/game-module-studio/game-module-studio-server/src/main/java/com/wanxiang/huijing/game/module/studio/service/studio/StudioServiceImpl.java b/game-cloud/game-module-studio/game-module-studio-server/src/main/java/com/wanxiang/huijing/game/module/studio/service/studio/StudioServiceImpl.java index f5650440..c0f11101 100644 --- a/game-cloud/game-module-studio/game-module-studio-server/src/main/java/com/wanxiang/huijing/game/module/studio/service/studio/StudioServiceImpl.java +++ b/game-cloud/game-module-studio/game-module-studio-server/src/main/java/com/wanxiang/huijing/game/module/studio/service/studio/StudioServiceImpl.java @@ -40,7 +40,9 @@ import java.util.Objects; import java.util.Set; import static com.wanxiang.huijing.game.module.studio.enums.ErrorCodeConstants.*; +import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_ADMISSION_RATE_LIMITED; import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_BACKPRESSURE_REJECTED; +import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_GENERATE_PAUSED; import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_QUOTA_EXCEEDED; import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_UNSAFE_PROMPT; import static com.wanxiang.huijing.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -64,14 +66,20 @@ public class StudioServiceImpl implements StudioService { private static final Logger log = LoggerFactory.getLogger(StudioServiceImpl.class); /** - * 控制面面向用户的类型化拒绝码集合(aigc 段 1-101-001-***:配额超限 / 背压拒绝 / Prompt 不安全)。 + * 控制面面向用户的类型化拒绝码集合(aigc 段 1-101-001-***:配额超限 / 背压拒绝 / 生成暂停降级 / Prompt 不安全 / 入口速率超限)。 * 委托 aigc 抛出这些码时原样透传给创作者、保留产品语义,不一刀切包成 STUDIO_AIGC_DELEGATE_FAIL * (2026-07-04 cutover S2 实证:GP9 拒绝/并发位满时创作者只见笼统「生成委托失败」,看不到真实原因)。 + * 扩容(工单j):并入 1-101-001-003 生成暂停/降级开关(降级门开启时创作者原只见「生成委托失败」,体验税)与 + * 1-101-001-006 入口速率闸(Sentinel FLOW_GRADE_QPS 削峰,与背压/配额同族的控制面准入拒绝,用户可见)。 + * 未并入 1-101-001-005 AIGC_LLM_SAFETY_ERROR:其定义注释明记「归因 tag 仍记 llm_error」(safety 超时/失败的 + * fail-closed 故障兜底),语义为内部错误、中性文案只作用户遮罩,非控制面准入拒绝,故仍按原逻辑包裹。 */ private static final Set AIGC_USER_REJECT_CODES = Set.of( - AIGC_QUOTA_EXCEEDED.getCode(), - AIGC_BACKPRESSURE_REJECTED.getCode(), - AIGC_UNSAFE_PROMPT.getCode()); + AIGC_QUOTA_EXCEEDED.getCode(), // 1-101-001-001 配额超限 + AIGC_BACKPRESSURE_REJECTED.getCode(), // 1-101-001-002 背压拒绝 + AIGC_GENERATE_PAUSED.getCode(), // 1-101-001-003 生成暂停/降级开关 + AIGC_UNSAFE_PROMPT.getCode(), // 1-101-001-004 Prompt 不安全 + AIGC_ADMISSION_RATE_LIMITED.getCode()); // 1-101-001-006 入口速率超限(Sentinel QPS 闸) @Resource private StudioSessionMapper studioSessionMapper; diff --git a/game-cloud/game-module-studio/game-module-studio-server/src/test/java/com/wanxiang/huijing/game/module/studio/service/studio/StudioServiceImplTest.java b/game-cloud/game-module-studio/game-module-studio-server/src/test/java/com/wanxiang/huijing/game/module/studio/service/studio/StudioServiceImplTest.java index e177d04d..d637d023 100644 --- a/game-cloud/game-module-studio/game-module-studio-server/src/test/java/com/wanxiang/huijing/game/module/studio/service/studio/StudioServiceImplTest.java +++ b/game-cloud/game-module-studio/game-module-studio-server/src/test/java/com/wanxiang/huijing/game/module/studio/service/studio/StudioServiceImplTest.java @@ -31,7 +31,9 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import static com.wanxiang.huijing.game.module.studio.enums.ErrorCodeConstants.*; +import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_ADMISSION_RATE_LIMITED; import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_BACKPRESSURE_REJECTED; +import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_GENERATE_PAUSED; import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_QUOTA_EXCEEDED; import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_UNSAFE_PROMPT; import static com.wanxiang.huijing.module.system.enums.ErrorCodeConstants.PLAYER_CREATE_NOT_IN_WHITELIST; @@ -279,6 +281,31 @@ class StudioServiceImplTest extends BaseMockitoUnitTest { assertEquals(STUDIO_AIGC_DELEGATE_FAIL.getCode(), ex.getCode()); } + @Test + void testGenerate_aigcGeneratePaused_passesThrough_andMarksFailed() { + // 透传集扩容(工单j):降级开关开启时 aigc submitGenerate 抛 1-101-001-003(生成暂停/降级门)→ + // studio 原样透传该码(非包成 1-112-002-000),创作者看到「生成服务繁忙维护中,请稍后再试」而非笼统「生成委托失败」; + // 链态先置失败(markChainFailed 保持链态语义不变,与既有控制面透传用例一致)。 + stubCreatorPass(); + StudioSessionDO session = session(512L, USER_ID, 2048L, "idle"); + when(studioSessionMapper.selectById(512L)).thenReturn(session); + doAnswer(inv -> { ((StudioTaskChainDO) inv.getArgument(0)).setId(1000L); return 1; }) + .when(studioTaskChainMapper).insert(any(StudioTaskChainDO.class)); + // aigc 控制面同步拒绝:降级开关开启抛类型化 ServiceException(降级门) + when(aigcApi.submitGenerate(any(AigcGenerateReqDTO.class))) + .thenThrow(new ServiceException(AIGC_GENERATE_PAUSED.getCode(), "生成服务繁忙维护中,请稍后再试")); + + ServiceException ex = assertThrows(ServiceException.class, + () -> studioService.generate(generateReq(512L, "p", null), USER_ID)); + + // 断言:原样透传控制面拒绝码 1-101-001-003(非 1-112-002-000) + assertEquals(AIGC_GENERATE_PAUSED.getCode(), ex.getCode()); + // 断言:链态被置失败(透传亦保持链态语义不变) + ArgumentCaptor chainCaptor = ArgumentCaptor.forClass(StudioTaskChainDO.class); + verify(studioTaskChainMapper).updateById(chainCaptor.capture()); + assertEquals(StudioSessionStatusEnum.FAILED.getStatus(), chainCaptor.getValue().getStatus()); + } + // ============================== getTaskChain 状态流转 ============================== @Test @@ -367,6 +394,24 @@ class StudioServiceImplTest extends BaseMockitoUnitTest { verify(studioTaskChainMapper, never()).updateById(any(StudioTaskChainDO.class)); } + @Test + void testRegenerate_aigcAdmissionRateLimited_passesThrough() { + // 透传集扩容(工单j):入口速率闸(Sentinel FLOW_GRADE_QPS)判入队速率超限 → regenerate 委托 retryTask 抛 + // 1-101-001-006 → 原样透传该码(非包成 1-112-002-000),创作者看到「生成服务繁忙(入队速率超限),请稍后重试」。 + // regenerate 原路径无链态清理,透传亦不引入清理(保持原语义,与既有 1-101-001-001 透传用例一致)。 + StudioTaskChainDO origin = taskChain(1000L, 512L, USER_ID, 8888L); + when(studioTaskChainMapper.selectById(1000L)).thenReturn(origin); + when(aigcApi.retryTask(8888L, USER_ID)) + .thenThrow(new ServiceException(AIGC_ADMISSION_RATE_LIMITED.getCode(), "生成服务繁忙(入队速率超限),请稍后重试")); + + ServiceException ex = assertThrows(ServiceException.class, + () -> studioService.regenerate(1000L, USER_ID)); + + assertEquals(AIGC_ADMISSION_RATE_LIMITED.getCode(), ex.getCode()); + // regenerate 无 markChainFailed:透传不应触碰任务链 updateById(保持原清理动作=无) + verify(studioTaskChainMapper, never()).updateById(any(StudioTaskChainDO.class)); + } + @Test void testGetTaskChain_taskChainNotExists_throws() { when(studioTaskChainMapper.selectById(1000L)).thenReturn(null);