com.wanxiang
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/admission/GenAdmissionResource.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/admission/GenAdmissionResource.java
new file mode 100644
index 00000000..8f21fec3
--- /dev/null
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/admission/GenAdmissionResource.java
@@ -0,0 +1,71 @@
+package com.wanxiang.huijing.game.module.aigc.admission;
+
+import com.alibaba.csp.sentinel.annotation.SentinelResource;
+import com.alibaba.csp.sentinel.slots.block.BlockException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_ADMISSION_RATE_LIMITED;
+import static com.wanxiang.huijing.framework.common.exception.util.ServiceExceptionUtil.exception;
+
+/**
+ * 生成入队的 Sentinel 资源门 —— 入口 QPS 突发保护(FLOW_GRADE_QPS,防洪峰)。切片一 阶段〇 C2。
+ *
+ * 职责边界(经 Opus 评审纠正,别把空体 admit() 当缺陷):本门统计「每秒入队请求数」,不是「在跑生成并发」。
+ * 空体入口的 entry/exit 是纳秒级,thread-grade 统计不到真正在跑的生成——那发生在 admit() 返回之后的 RocketMQ
+ * 消费线程里。故「在跑生成并发≤15」由 C3 消费者 consumeThreadMax=15 保证,不由本门保证。
+ *
+ *
三层正交:
+ *
+ * - 入口速率(本门 · Sentinel FLOW_GRADE_QPS):每秒入队请求数上限,削洪峰;
+ * - 在跑并发(C3 · RocketMQ consumeThreadMax=15):同时在跑的生成数结构性上限;
+ * - 业务配额(既有 DB 三门):per-creator×level 日额度 + per-creator 并发 + 全局背压,在 admit() 之后执行。
+ *
+ *
+ * 越限行为:Sentinel 抛 {@link BlockException} → {@link #onAdmissionBlocked} 优雅转成业务异常
+ * {@code AIGC_ADMISSION_RATE_LIMITED}「生成服务繁忙…请稍后重试」(经全局 GlobalExceptionHandler 转 CommonResult.error,
+ * HTTP 仍 200、body.code 非 0),绝不 500、不静默丢。
+ *
+ *
规则源:Nacos dataId {@code sentinel-gen-flow-rules}(JSON),改规则即热生效、不重启;
+ * 其回滚杠杆(调大 count / 删 dataId)独立于控制平面 {@code aigc.control-plane.enabled} 开关。
+ *
+ * @author 绘境AI
+ */
+@Slf4j
+@Component
+public class GenAdmissionResource {
+
+ /** 生成准入资源名(须与 Nacos dataId sentinel-gen-flow-rules 里的 resource 字段字面对齐)。 */
+ public static final String RESOURCE = "aigc:gen:admission";
+
+ /**
+ * 入口速率闸:受 Sentinel {@code FLOW_GRADE_QPS} 规则约束(入队 QPS 超限即 block)。
+ * 通过则直接返回(调用方继续走 DB 三门 + 入队);被 block 则由 Sentinel 切面转调 {@link #onAdmissionBlocked}。
+ *
+ *
空体是设计,不是缺陷:{@code FLOW_GRADE_QPS} 只统计「每秒通过本资源的次数」(与方法耗时无关),
+ * 空体正合适——让 Sentinel 计入队 QPS 削洪峰。业务准入(配额)在调用方 DB 三门;在跑并发≤15 在 C3 consumeThreadMax,均不在此。
+ */
+ @SentinelResource(value = RESOURCE, blockHandler = "onAdmissionBlocked")
+ public void admit() {
+ // 有意留空:见方法注释——本资源只为 Sentinel 统计入队 QPS 服务,不承载任何业务逻辑(业务准入在调用方 DB 三门)。
+ }
+
+ /**
+ * 越入口 QPS 闸的优雅降级(Sentinel blockHandler)。
+ *
+ *
Sentinel 约定:blockHandler 签名 = 原方法参数列表 + 末尾追加一个 {@link BlockException},返回类型与原方法一致(void),
+ * 且可见性 public、与原方法同类——否则切面找不到 blockHandler、退化为直接向上抛 BlockException(会被当 500)。
+ * admit() 无参,故此处仅一个 BlockException 参。
+ *
+ * @param ex Sentinel 抛出的限流/降级异常(如 FlowException 等 BlockException 子类)
+ */
+ public void onAdmissionBlocked(BlockException ex) {
+ // 可追溯日志(创始人铁律:错误路径必须留痕)——记资源名 + block 类型(哪种 Sentinel 规则触发,如 FlowException=流控)。
+ // 刻意不调 ex.getRule():Sentinel 1.8.9 FlowException.getRule() 在 rule 为空时会 rule.as(..) 抛 NPE,
+ // 若该 NPE 冒泡会把「优雅限流」渲染成 500——与本门「绝不 500、不静默丢」相悖;exception 类名恒可取、已足够定位规则类型。
+ log.warn("[aigc-admission] Sentinel 入口 QPS 闸拦截:入队速率超限,拒绝新入队请求 resource={} blockType={}",
+ RESOURCE, ex.getClass().getSimpleName());
+ // 优雅转业务异常(错误码段 aigc 控制平面 1-101-001-006),前端得「繁忙,请稍后重试」提示;绝不 500、不静默丢。
+ throw exception(AIGC_ADMISSION_RATE_LIMITED);
+ }
+}
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcTaskServiceImpl.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcTaskServiceImpl.java
index c2451e51..7e1ac3e9 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcTaskServiceImpl.java
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcTaskServiceImpl.java
@@ -77,6 +77,15 @@ public class AigcTaskServiceImpl implements AigcTaskService {
@Resource
private ObjectProvider configApiProvider;
+ /**
+ * 入口 QPS 突发闸(Sentinel FLOW_GRADE_QPS,规则热源 Nacos;切片一 阶段〇 C2)。
+ * 与上方控制平面软注入件不同:GenAdmissionResource 是无条件 @Component(恒在席),故直接 @Resource 硬注入
+ * (同 aigcTaskMapper/playerApi),不走 ObjectProvider。职责 = 入队入口速率闸削洪峰,与 DB 三门(业务配额)
+ * / C3 consumeThreadMax(在跑并发≤15)三层正交,详见 {@link GenAdmissionResource} 类注释。
+ */
+ @Resource
+ private com.wanxiang.huijing.game.module.aigc.admission.GenAdmissionResource genAdmissionResource;
+
/**
* v0 会员档位占位值 = L1(执行版 §5.1/B3):AigcGenerateReqVO 无 level 字段、会员档数据模型未建,
* 提交侧统一落 L1,避免为未建会员体系造孤儿字段。分档逻辑 v0 即可用(§8 手塞 level=2 读 L2 档单测覆盖)。
@@ -289,7 +298,10 @@ public class AigcTaskServiceImpl implements AigcTaskService {
// ============================== D12 控制平面 + GP9 共享入队(W-G1 组A·执行版 §6.1)==============================
/**
- * 共享入队门:submitGenerate 与 retryTask 同走(决策D)。门序固定(最便宜→最贵):
+ * 共享入队门:submitGenerate 与 retryTask 同走(决策D)。
+ * 【C2 入口速率闸 · 先于下方全部 DB 门】方法体最前调 genAdmissionResource.admit():Sentinel FLOW_GRADE_QPS
+ * 统计每秒入队请求数削洪峰(规则热源 Nacos),越限由 blockHandler 优雅拒(非 500);独立于控制平面 enabled 开关。
+ * 门序固定(最便宜→最贵,均在 admit() 之后):
* ① 降级门(infra ConfigApi 读 aigc.generate.paused,无 LLM,new-api 挂了直接挡)
* → ② 配额+并发门(per-creator×level 当日非取消计数 + 在飞并发计数,纯计数)
* → ③ 背压门(全局在飞计数,纯计数)
@@ -308,6 +320,13 @@ public class AigcTaskServiceImpl implements AigcTaskService {
* @param task 已组装好(含 creatorUserId/level/prompt/status=queued/traceId)但尚未落库的任务
*/
private void enqueueWithControlPlane(AigcTaskDO task) {
+ // 入口 QPS 突发闸(Sentinel FLOW_GRADE_QPS,规则热源 Nacos dataId sentinel-gen-flow-rules)——先于下方全部 DB 门:
+ // 统计「每秒入队请求数」削洪峰,越限由 blockHandler 优雅转 AIGC_ADMISSION_RATE_LIMITED(非 500、不静默丢)。
+ // 与下方 DB 三门(per-creator 业务配额)分层正交;「在跑生成并发≤15」由 C3 RocketMQ consumeThreadMax 保证、不在此。
+ // 置于 props 判空之前 = 独立于控制平面 enabled 开关(基础设施级速率保护恒生效);无 Nacos 规则时为纳秒级 no-op,
+ // 其回滚杠杆是 Nacos 流控规则本身(调大 count / 删 dataId),与控制平面 enabled=false 各自独立。
+ genAdmissionResource.admit();
+
AigcControlPlaneProperties props = controlPlanePropertiesProvider.getIfAvailable();
// 控制平面关闭/未装配:全门 + GP9 旁路,直接落库(现行行为逐字不变,§10 回滚首选层)
if (props == null || !Boolean.TRUE.equals(props.getEnabled())) {
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/admission/GenAdmissionResourceTest.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/admission/GenAdmissionResourceTest.java
new file mode 100644
index 00000000..0b8a8a17
--- /dev/null
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/admission/GenAdmissionResourceTest.java
@@ -0,0 +1,43 @@
+package com.wanxiang.huijing.game.module.aigc.admission;
+
+import com.alibaba.csp.sentinel.slots.block.BlockException;
+import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
+import com.wanxiang.huijing.framework.common.exception.ServiceException;
+import org.junit.jupiter.api.Test;
+
+import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_ADMISSION_RATE_LIMITED;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * {@link GenAdmissionResource} 单元测试(切片一 阶段〇 C2)。
+ *
+ * 只锁 blockHandler 的优雅降级语义:越 QPS 闸 → 转可读业务异常(含「繁忙」、带业务错误码),绝不 500、不静默丢。
+ * admit() 空体的真 QPS 限流依赖 Sentinel 运行时统计 + Nacos 规则(集成态),不在单测范围——由后端窗口的集成验证覆盖。
+ *
+ * @author 绘境AI
+ */
+class GenAdmissionResourceTest {
+
+ /**
+ * 越入口 QPS 闸:blockHandler 必须抛业务 {@link ServiceException}(消息含「繁忙」、错误码 = aigc 控制平面段 1-101-001-006),
+ * 而非裸 500 服务器错误——锁死「优雅拒、不静默丢」这条降级契约不回退。
+ */
+ @Test
+ void onAdmissionBlocked_throwsBusinessServiceException_notServerError() {
+ GenAdmissionResource resource = new GenAdmissionResource();
+ // 模拟 Sentinel 流控拦截:FlowException 是 BlockException 子类,资源名对齐 RESOURCE
+ BlockException blockEx = new FlowException(GenAdmissionResource.RESOURCE);
+
+ ServiceException ex = assertThrows(ServiceException.class,
+ () -> resource.onAdmissionBlocked(blockEx));
+
+ // ① 优雅业务提示(含「繁忙」)——反 500、反静默丢
+ assertTrue(ex.getMessage().contains("繁忙"),
+ "blockHandler 应转成含「繁忙」的可读业务提示,实际=" + ex.getMessage());
+ // ② 携带 aigc 控制平面段业务码(经全局 handler 转 CommonResult.error、HTTP 200 body.code 非 0)
+ assertEquals(AIGC_ADMISSION_RATE_LIMITED.getCode(), ex.getCode(),
+ "错误码应 = AIGC_ADMISSION_RATE_LIMITED(1-101-001-006)");
+ }
+}
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcControlPlaneTest.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcControlPlaneTest.java
index bbaeca1b..3fead6ca 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcControlPlaneTest.java
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcControlPlaneTest.java
@@ -54,6 +54,10 @@ class AigcControlPlaneTest extends BaseMockitoUnitTest {
private ObjectProvider safetyCheckClientProvider;
@Mock
private ObjectProvider configApiProvider;
+ // C2 入口 QPS 闸(硬注入 @Component):@InjectMocks 会注入本 mock,其 void admit() 默认 no-op(不 block、不 NPE),
+ // 故控制平面各门用例行为不变;真限流降级语义由 GenAdmissionResourceTest 单独锁。
+ @Mock
+ private com.wanxiang.huijing.game.module.aigc.admission.GenAdmissionResource genAdmissionResource;
@Mock
private SafetyCheckClient safetyCheckClient;
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcTaskServiceImplTest.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcTaskServiceImplTest.java
index a12a9529..1cd7aff0 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcTaskServiceImplTest.java
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/service/task/AigcTaskServiceImplTest.java
@@ -55,6 +55,9 @@ class AigcTaskServiceImplTest extends BaseMockitoUnitTest {
private ObjectProvider safetyCheckClientProvider;
@Mock
private ObjectProvider configApiProvider;
+ // C2 入口 QPS 闸(硬注入 @Component):@InjectMocks 注入本 mock,void admit() 默认 no-op → 现行提交/重试基线行为不变。
+ @Mock
+ private com.wanxiang.huijing.game.module.aigc.admission.GenAdmissionResource genAdmissionResource;
// ============================== submitGenerate ==============================
diff --git a/game-cloud/huijing-server/src/main/resources/application.yaml b/game-cloud/huijing-server/src/main/resources/application.yaml
index 5609277b..bde623ae 100644
--- a/game-cloud/huijing-server/src/main/resources/application.yaml
+++ b/game-cloud/huijing-server/src/main/resources/application.yaml
@@ -69,6 +69,21 @@ spring:
enabled: true # 重启用 Nacos 配置中心(was false):从 Nacos 读外部化配置
namespace: public
file-extension: yaml # 配置文件格式
+ # Sentinel 入口 QPS 突发保护(切片一 阶段〇 C2):流控规则从 Nacos dataId sentinel-gen-flow-rules 热加载(改即生效、不重启)。
+ # 职责 = 生成入队入口速率闸(FLOW_GRADE_QPS 削洪峰),与 DB 三门(业务配额)/ C3 consumeThreadMax(在跑并发≤15)三层正交。
+ sentinel:
+ transport:
+ dashboard: ${SENTINEL_DASHBOARD:} # 留空 = 不接 Sentinel 控制台(mini-infra RAM 紧、不部面板);规则直存 Nacos
+ datasource:
+ gen-flow: # 生成入队流控规则数据源(名字任意,仅作 datasource 标识)
+ nacos:
+ server-addr: 100.64.0.8:8848 # 复用 C1 同一 mini-infra Nacos
+ username: nacos # 鉴权同 C1 nacos 段(见 docs/内网凭据与端点.md)
+ password: ${NACOS_PASSWORD} # 口令走 env 注入,严禁明文入仓
+ dataId: sentinel-gen-flow-rules # 流控规则 dataId(JSON;resource=aigc:gen:admission)
+ groupId: DEFAULT_GROUP
+ data-type: json
+ rule-type: flow # 流控规则(FLOW_GRADE_QPS 入口速率闸)
# openfeign:
# lazy-attributes-resolution: true
# client: