merge: studio透传控制面三拒绝码(quota/backpressure/unsafe)保留产品语义 (cutover S2 工单d)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
d3ec652d82
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 回写会话状态(仅状态位更新,避免整对象覆盖)
|
||||
*
|
||||
|
||||
@ -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 工单d):aigc submitGenerate 抛 1-101-001-004(Prompt 不安全)→
|
||||
// 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(GP9 不安全 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 工单d):regenerate 委托 retryTask 抛 1-101-001-001(配额/并发满)→
|
||||
// 原样透传该码(非包成 1-112-002-000)。regenerate 原路径无链态清理,透传亦不引入清理(保持原语义)。
|
||||
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());
|
||||
}
|
||||
|
||||
// ============================== 生命周期编排 modify(C3)==============================
|
||||
|
||||
@Test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user