From 3afed479f88920080eadd6cacf8fefe4ac6332a9 Mon Sep 17 00:00:00 2001 From: lili Date: Sat, 4 Jul 2026 00:30:28 -0700 Subject: [PATCH] =?UTF-8?q?fix(studio):=20=E9=80=8F=E4=BC=A0=E9=9B=86?= =?UTF-8?q?=E6=89=A9=E5=AE=B9=E5=B9=B6=E5=85=A5=201101001003=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=9A=82=E5=81=9C=E9=99=8D=E7=BA=A7=20+=201101001006?= =?UTF-8?q?=20=E5=85=A5=E5=8F=A3=E9=80=9F=E7=8E=87=E9=97=B8,=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E4=BA=A7=E5=93=81=E8=AF=AD=E4=B9=89=20(=E5=B7=A5?= =?UTF-8?q?=E5=8D=95j)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AIGC_USER_REJECT_CODES 从三码扩到五码:并入 AIGC_GENERATE_PAUSED(1-101-001-003, 降级开关开启时创作者原只见笼统「生成委托失败」的体验税)与 AIGC_ADMISSION_RATE_LIMITED (1-101-001-006,Sentinel 入口速率闸,与背压/配额同族的控制面准入拒绝、用户可见)。委托 aigc 抛这两码时原样透传、保留产品语义,不再一刀切包成 STUDIO_AIGC_DELEGATE_FAIL。 未并入 AIGC_LLM_SAFETY_ERROR(1-101-001-005):其定义注释明记归因 tag 仍记 llm_error (safety 超时/失败的 fail-closed 故障兜底),语义为内部错误、中性文案只作用户遮罩,非控制 面准入拒绝,故仍按原逻辑包裹。 只扩集合 + 补测试:三处委托点 catch(ServiceException) 结构与 isControlPlaneUserReject helper 一字未动(引集合常量,扩集合即自动扩行为);文案不改;不动 aigc 模块本体。 验证:StudioServiceImplTest 37 测全绿(既有 35 不回归 + 1003/1006 各一透传断言), mvn -pl game-module-studio/game-module-studio-server -am test。 生效需随下轮 jar 轮换(仅改后端字节码,已在跑实例不热更)。 Co-Authored-By: Claude Fable 5 --- .../service/studio/StudioServiceImpl.java | 16 +++++-- .../service/studio/StudioServiceImplTest.java | 45 +++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) 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);