fix(studio): 透传控制面三拒绝码(1101001001/002/004)保留产品语义,其余异常仍包裹 1112002000 (cutover S2 工单d)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-03 18:51:08 -07:00
parent dbfe40906e
commit f9b85d429b
2 changed files with 149 additions and 0 deletions

View File

@ -25,6 +25,7 @@ import com.wanxiang.huijing.game.module.aigc.dto.AigcTemplateDTO;
import com.wanxiang.huijing.game.module.aigc.enums.AigcTaskStatusEnum;
import com.wanxiang.huijing.game.module.project.api.ProjectApi;
import com.wanxiang.huijing.module.system.api.passport.PlayerApi;
import com.wanxiang.huijing.framework.common.exception.ServiceException;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -39,6 +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_BACKPRESSURE_REJECTED;
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;
/**
@ -59,6 +63,16 @@ public class StudioServiceImpl implements StudioService {
private static final Logger log = LoggerFactory.getLogger(StudioServiceImpl.class);
/**
* 控制面面向用户的类型化拒绝码集合aigc 1-101-001-***配额超限 / 背压拒绝 / Prompt 不安全
* 委托 aigc 抛出这些码时原样透传给创作者保留产品语义不一刀切包成 STUDIO_AIGC_DELEGATE_FAIL
* 2026-07-04 cutover S2 实证GP9 拒绝/并发位满时创作者只见笼统生成委托失败看不到真实原因
*/
private static final Set<Integer> AIGC_USER_REJECT_CODES = Set.of(
AIGC_QUOTA_EXCEEDED.getCode(),
AIGC_BACKPRESSURE_REJECTED.getCode(),
AIGC_UNSAFE_PROMPT.getCode());
@Resource
private StudioSessionMapper studioSessionMapper;
@ -166,6 +180,19 @@ public class StudioServiceImpl implements StudioService {
AigcGenerateRespDTO resp;
try {
resp = aigcApi.submitGenerate(req).getCheckedData();
} catch (ServiceException se) {
// 委托 aigc 抛业务异常先置链态失败保持链态语义不变与原一刀切路径一致
markChainFailed(chain, session);
if (isControlPlaneUserReject(se)) {
// 控制面面向用户的类型化拒绝配额/背压/不安全 prompt原样透传保留产品语义
// 不包成 STUDIO_AIGC_DELEGATE_FAIL创作者可见真实拒绝原因
log.warn("[generate] aigc 控制面拒绝透传 code={} sessionId={} userId={}",
se.getCode(), session.getId(), userId);
throw se;
}
// 其余业务异常按原逻辑包成 studio 委托失败码行为一字不变
log.error("[generate] 生成委托 aigc 失败 sessionId={} userId={}", session.getId(), userId, se);
throw exception(STUDIO_AIGC_DELEGATE_FAIL);
} catch (Exception e) {
// aigc 调用异常 链态置失败 3 + 会话失败 studio 错误码中文日志traceId aigc 内部生成此处暂无
markChainFailed(chain, session);
@ -241,6 +268,18 @@ public class StudioServiceImpl implements StudioService {
AigcGenerateRespDTO resp;
try {
resp = aigcApi.retryTask(origin.getAigcTaskId(), userId).getCheckedData();
} catch (ServiceException se) {
if (isControlPlaneUserReject(se)) {
// 控制面面向用户的类型化拒绝配额/背压/不安全 prompt原样透传保留产品语义
// regenerate 原路径无链态清理 markChainFailed透传亦保持原清理动作=
log.warn("[regenerate] aigc 控制面拒绝透传 code={} sessionId={} taskChainId={} aigcTaskId={} userId={}",
se.getCode(), origin.getSessionId(), taskChainId, origin.getAigcTaskId(), userId);
throw se;
}
// 其余业务异常按原逻辑包成 studio 委托失败码行为一字不变
log.error("[regenerate] 重生成委托 aigc 失败 taskChainId={} aigcTaskId={} userId={}",
taskChainId, origin.getAigcTaskId(), userId, se);
throw exception(STUDIO_AIGC_DELEGATE_FAIL);
} catch (Exception e) {
log.error("[regenerate] 重生成委托 aigc 失败 taskChainId={} aigcTaskId={} userId={}",
taskChainId, origin.getAigcTaskId(), userId, e);
@ -406,6 +445,18 @@ public class StudioServiceImpl implements StudioService {
AigcGenerateRespDTO resp;
try {
resp = aigcApi.submitGenerate(req).getCheckedData();
} catch (ServiceException se) {
// 委托 aigc 抛业务异常先置链态失败保持链态语义不变
markChainFailed(chain, session);
if (isControlPlaneUserReject(se)) {
// 控制面面向用户的类型化拒绝配额/背压/不安全 prompt原样透传保留产品语义
// create/modify/extend 三路共用本 helper一致透传不包成 STUDIO_AIGC_DELEGATE_FAIL
log.warn("[{}] aigc 控制面拒绝透传 code={} sessionId={} userId={}",
opLabel, se.getCode(), session.getId(), req.getUserId());
throw se;
}
log.error("[{}] 生成委托 aigc 失败 sessionId={} userId={}", opLabel, session.getId(), req.getUserId(), se);
throw exception(STUDIO_AIGC_DELEGATE_FAIL);
} catch (Exception e) {
markChainFailed(chain, session);
log.error("[{}] 生成委托 aigc 失败 sessionId={} userId={}", opLabel, session.getId(), req.getUserId(), e);
@ -695,6 +746,17 @@ public class StudioServiceImpl implements StudioService {
updateSessionStatus(session.getId(), StudioSessionStatusEnum.FAILED.getStatus());
}
/**
* 判定 aigc 委托抛出的业务异常是否为控制面面向用户的类型化拒绝配额/背压/不安全 prompt
* 命中则由调用方原样透传保留产品语义否则按原逻辑包成 STUDIO_AIGC_DELEGATE_FAIL
*
* @param se aigc 委托抛出的业务异常
* @return true=需透传命中拒绝码集合false=按原逻辑包裹
*/
private boolean isControlPlaneUserReject(ServiceException se) {
return se.getCode() != null && AIGC_USER_REJECT_CODES.contains(se.getCode());
}
/**
* 回写会话状态仅状态位更新避免整对象覆盖
*

View File

@ -31,6 +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_BACKPRESSURE_REJECTED;
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;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
@ -234,6 +237,48 @@ class StudioServiceImplTest extends BaseMockitoUnitTest {
assertEquals(STUDIO_AIGC_DELEGATE_FAIL.getCode(), ex.getCode());
}
@Test
void testGenerate_aigcControlPlaneReject_passesThrough_andMarksFailed() {
// 控制面拒绝透传cutover S2 工单daigc submitGenerate 1-101-001-004Prompt 不安全
// 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 控制面同步拒绝抛类型化 ServiceExceptionGP9 不安全 prompt
when(aigcApi.submitGenerate(any(AigcGenerateReqDTO.class)))
.thenThrow(new ServiceException(AIGC_UNSAFE_PROMPT.getCode(), "Prompt 不安全"));
ServiceException ex = assertThrows(ServiceException.class,
() -> studioService.generate(generateReq(512L, "违规内容", null), USER_ID));
// 断言原样透传控制面拒绝码 1-101-001-004 1-112-002-000
assertEquals(AIGC_UNSAFE_PROMPT.getCode(), ex.getCode());
// 断言链态被置失败透传亦保持链态语义不变
ArgumentCaptor<StudioTaskChainDO> chainCaptor = ArgumentCaptor.forClass(StudioTaskChainDO.class);
verify(studioTaskChainMapper).updateById(chainCaptor.capture());
assertEquals(StudioSessionStatusEnum.FAILED.getStatus(), chainCaptor.getValue().getStatus());
}
@Test
void testGenerate_aigcOtherServiceException_stillWrapped() {
// 非透传集合的业务异常 1-101-000-000 任务不存在 仍按原逻辑包成 1-112-002-000行为不变
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));
when(aigcApi.submitGenerate(any(AigcGenerateReqDTO.class)))
.thenThrow(new ServiceException(1_101_000_000, "生成任务不存在"));
ServiceException ex = assertThrows(ServiceException.class,
() -> studioService.generate(generateReq(512L, "p", null), USER_ID));
// 断言非控制面拒绝码的业务异常仍被包成 studio 委托失败码
assertEquals(STUDIO_AIGC_DELEGATE_FAIL.getCode(), ex.getCode());
}
// ============================== getTaskChain 状态流转 ==============================
@Test
@ -305,6 +350,23 @@ class StudioServiceImplTest extends BaseMockitoUnitTest {
verify(aigcApi).retryTask(8888L, USER_ID);
}
@Test
void testRegenerate_aigcControlPlaneReject_passesThrough() {
// 控制面拒绝透传cutover S2 工单dregenerate 委托 retryTask 1-101-001-001配额/并发满
// 原样透传该码非包成 1-112-002-000regenerate 原路径无链态清理透传亦不引入清理保持原语义
StudioTaskChainDO origin = taskChain(1000L, 512L, USER_ID, 8888L);
when(studioTaskChainMapper.selectById(1000L)).thenReturn(origin);
when(aigcApi.retryTask(8888L, USER_ID))
.thenThrow(new ServiceException(AIGC_QUOTA_EXCEEDED.getCode(), "今日生成额度已用完或并发已满,请稍后再试"));
ServiceException ex = assertThrows(ServiceException.class,
() -> studioService.regenerate(1000L, USER_ID));
assertEquals(AIGC_QUOTA_EXCEEDED.getCode(), ex.getCode());
// regenerate markChainFailed透传不应触碰任务链 updateById保持原清理动作=
verify(studioTaskChainMapper, never()).updateById(any(StudioTaskChainDO.class));
}
@Test
void testGetTaskChain_taskChainNotExists_throws() {
when(studioTaskChainMapper.selectById(1000L)).thenReturn(null);
@ -398,6 +460,31 @@ class StudioServiceImplTest extends BaseMockitoUnitTest {
verify(aigcApi, never()).submitGenerate(any());
}
@Test
void testCreate_aigcControlPlaneReject_passesThrough_viaHelper() {
// 控制面拒绝透传共享 helper delegateGenerate create/modify/extend 一致create 委托 submitGenerate
// 1-101-001-002背压 原样透传该码非包成 1-112-002-000且链态先置失败
stubCreatorPass();
when(projectApi.createProject(eq(USER_ID), eq("我的小游戏"), eq("generic")))
.thenReturn(CommonResult.success(2048L));
doAnswer(inv -> { ((StudioSessionDO) inv.getArgument(0)).setId(512L); return 1; })
.when(studioSessionMapper).insert(any(StudioSessionDO.class));
doAnswer(inv -> { ((StudioTaskChainDO) inv.getArgument(0)).setId(1000L); return 1; })
.when(studioTaskChainMapper).insert(any(StudioTaskChainDO.class));
when(aigcApi.submitGenerate(any(AigcGenerateReqDTO.class)))
.thenThrow(new ServiceException(AIGC_BACKPRESSURE_REJECTED.getCode(), "生成排队过多,请稍后再试"));
ServiceException ex = assertThrows(ServiceException.class,
() -> studioService.create(createReq("我的小游戏", "做一个躲避陨石的小游戏", "generic"), USER_ID));
// 断言helper 路同样透传控制面拒绝码 1-101-001-002 1-112-002-000
assertEquals(AIGC_BACKPRESSURE_REJECTED.getCode(), ex.getCode());
// 断言链态被置失败delegateGenerate markChainFailed
ArgumentCaptor<StudioTaskChainDO> chainCaptor = ArgumentCaptor.forClass(StudioTaskChainDO.class);
verify(studioTaskChainMapper).updateById(chainCaptor.capture());
assertEquals(StudioSessionStatusEnum.FAILED.getStatus(), chainCaptor.getValue().getStatus());
}
// ============================== 生命周期编排 modifyC3==============================
@Test