fix(studio): 透传集扩容并入 1101001003 生成暂停降级 + 1101001006 入口速率闸,保留产品语义 (工单j)

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 <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-04 00:30:28 -07:00
parent 2e4dd56b76
commit 3afed479f8
2 changed files with 57 additions and 4 deletions

View File

@ -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_errorsafety 超时/失败的
* fail-closed 故障兜底语义为内部错误中性文案只作用户遮罩非控制面准入拒绝故仍按原逻辑包裹
*/
private static final Set<Integer> 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;

View File

@ -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<StudioTaskChainDO> 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);