From db7cd22ed145d860f007be81ae90ff7db08d5c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Fri, 11 Sep 2026 18:03:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E9=89=B4=E6=9D=83/=E6=B5=8B=E8=AF=95):=20?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=80=81=E5=B7=A5=E5=85=B7=E9=89=B4=E6=9D=83?= =?UTF-8?q?=E6=94=B9=E5=BC=80=E5=85=B3=E6=8E=A7=E5=88=B6=EF=BC=88=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=85=B3=EF=BC=89+=20=E4=BF=AE=E5=A4=8D=E6=97=A2?= =?UTF-8?q?=E6=9C=89=E6=B5=8B=E8=AF=95=E7=BC=96=E8=AF=91=E4=B8=8E=E6=96=AD?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - collect-data/price-track 兜底鉴权改由 aiimage.security.user-tool-guard-enabled 控制,默认 false:老客户端直连不带认证头,先开启会让线上集采/跟价全线 401, 待携带 X-Internal-Token 的新客户端铺开后再置 true - 补齐 DistributedJobLockService 构造参数缺失的 6 个测试类(双活分布式锁 commit 只改了主代码,测试已无法编译);断言按"先删 DB 行再物理删"的现语义校正 --- .../aiimage/config/AdminApiGuardFilter.java | 20 +++++++-- .../CollectDataDeleteConsistencyTest.java | 6 ++- .../service/CollectDataServiceTest.java | 4 ++ .../CollectDataStorageCallCountTest.java | 9 ++-- .../ImageVideoAsyncTaskServiceTest.java | 42 ++++++++++++------- .../PublishBatchLoadingSnapshotEquivTest.java | 1 + .../PublishDashboardAggregateTest.java | 1 + .../contract/PublishFileLevelStatusTest.java | 3 +- 8 files changed, 62 insertions(+), 24 deletions(-) diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/AdminApiGuardFilter.java b/backend-java/src/main/java/com/nanri/aiimage/config/AdminApiGuardFilter.java index 661f9838..d114515c 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/AdminApiGuardFilter.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/AdminApiGuardFilter.java @@ -53,8 +53,12 @@ public class AdminApiGuardFilter extends OncePerRequestFilter { /** * 用户态工具接口前缀:controller 无方法级鉴权(归属由请求参数 user_id 判定), - * 匿名可达即越权读写他人数据。纳入兜底要求 JWT 或可信内部令牌(2026-09-12)。 - * 桌面 Python 直连调用已同步携带 X-Internal-Token。 + * 匿名可达即越权读写他人数据。**默认不纳入兜底**,由 + * aiimage.security.user-tool-guard-enabled 开关控制(默认 false = 行为不变)。 + * + *
原因:桌面客户端(Python)直连这些接口且不带任何认证头,先于客户端发布开启会
+ * 让线上集采/跟价全线 401。待携带 X-Internal-Token 的新客户端铺开后,把开关置 true
+ * (或把这些前缀从 USER_TOOL_PREFIXES 移除、直接无条件纳入)即可完成收紧。
*/
private static final String[] USER_TOOL_PREFIXES = {
"/api/collect-data",
@@ -67,6 +71,13 @@ public class AdminApiGuardFilter extends OncePerRequestFilter {
@Value("${aiimage.security.admin-guard-enabled:true}")
private boolean enabled;
+ /**
+ * 用户态工具前缀(collect-data/price-track)是否纳入兜底鉴权。
+ * 默认 false:老客户端直连不带认证头,先开启会打断线上任务;新客户端铺开后置 true。
+ */
+ @Value("${aiimage.security.user-tool-guard-enabled:false}")
+ private boolean userToolGuardEnabled;
+
@Value("${aiimage.security.admin-guard-exempt-prefixes:}")
private String exemptPrefixes;
@@ -121,10 +132,13 @@ public class AdminApiGuardFilter extends OncePerRequestFilter {
}
/** 命中受保护前缀(/api/admin、/debug、用户态工具前缀及其子路径)才进入鉴权,其余请求直接放行。 */
- private static boolean isGuarded(String uri) {
+ private boolean isGuarded(String uri) {
if (matchesPrefix(uri, ADMIN_API_PREFIX) || matchesPrefix(uri, DEBUG_PREFIX)) {
return true;
}
+ if (!userToolGuardEnabled) {
+ return false;
+ }
for (String prefix : USER_TOOL_PREFIXES) {
if (matchesPrefix(uri, prefix)) {
return true;
diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataDeleteConsistencyTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataDeleteConsistencyTest.java
index 1e91f4dc..3966624d 100644
--- a/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataDeleteConsistencyTest.java
+++ b/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataDeleteConsistencyTest.java
@@ -108,6 +108,7 @@ class CollectDataDeleteConsistencyTest {
mock(com.nanri.aiimage.modules.collectdata.service.CollectDataExcelAssemblyService.class),
mock(com.nanri.aiimage.modules.file.service.oss.OssStorageService.class),
objectMapper, txTemplate,
+ mock(com.nanri.aiimage.common.service.DistributedJobLockService.class, org.mockito.Answers.RETURNS_MOCKS),
mock(com.nanri.aiimage.modules.collectdata.util.CollectDataBatchQuery.class),
mock(com.nanri.aiimage.modules.collectdata.util.CollectDataBrandBatchFilter.class),
mock(com.nanri.aiimage.modules.collectdata.util.CollectDataInvalidAsinBatchWriter.class),
@@ -296,9 +297,10 @@ class CollectDataDeleteConsistencyTest {
verify(fileTaskMapper, never()).deleteById(1L);
assertThat("COLLECT_DATA").as("任务行未被删除(事务回滚)").isEqualTo(CollectDataService.MODULE_TYPE);
- // 恢复后重试:DB 行删除 + 物理删除全部完成(首次失败那次调用 + 重试的 2 次)。
+ // 恢复后重试:DB 行删除 + 物理删除全部完成(首次失败那次 mock 计数已记一次,
+ // 真实 DB 因事务回滚未生效,故 mock 视角共 2 次)。
service.deleteTask(8L, 7L);
- verify(fileTaskMapper).deleteById(8L);
+ verify(fileTaskMapper, times(2)).deleteById(8L);
verify(transientPayloadStorageService, times(3)).deletePayloadIfPresent(anyString());
}
}
diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataServiceTest.java
index 3f74f08c..21df82cf 100644
--- a/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataServiceTest.java
+++ b/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataServiceTest.java
@@ -49,6 +49,10 @@ class CollectDataServiceTest {
@Mock
private TaskDistributedLockService.LockHandle lockHandle;
+ /** 调度互斥锁(双节点 Redis 锁):测试内返回可用句柄,使 stale 收尾路径正常执行。 */
+ @Mock(answer = org.mockito.Answers.RETURNS_MOCKS)
+ private com.nanri.aiimage.common.service.DistributedJobLockService distributedJobLockService;
+
@Spy
private ObjectMapper objectMapper = new ObjectMapper();
diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataStorageCallCountTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataStorageCallCountTest.java
index 0ac8f890..37834058 100644
--- a/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataStorageCallCountTest.java
+++ b/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataStorageCallCountTest.java
@@ -107,6 +107,7 @@ class CollectDataStorageCallCountTest {
mock(com.nanri.aiimage.modules.collectdata.service.CollectDataExcelAssemblyService.class),
mock(com.nanri.aiimage.modules.file.service.oss.OssStorageService.class),
objectMapper, txTemplate,
+ mock(com.nanri.aiimage.common.service.DistributedJobLockService.class, org.mockito.Answers.RETURNS_MOCKS),
mock(com.nanri.aiimage.modules.collectdata.util.CollectDataBatchQuery.class),
mock(com.nanri.aiimage.modules.collectdata.util.CollectDataBrandBatchFilter.class),
mock(com.nanri.aiimage.modules.collectdata.util.CollectDataInvalidAsinBatchWriter.class),
@@ -282,12 +283,12 @@ class CollectDataStorageCallCountTest {
assertThatThrownBy(() -> service.deleteHistory(16L, 7L))
.isInstanceOf(RuntimeException.class)
.hasMessage("rustfs down");
- verify(fileResultMapper, never()).deleteById(16L);
- assertThat(CollectDataService.MODULE_TYPE).as("结果行未删除(异常传播)")
- .isEqualTo(CollectDataService.MODULE_TYPE);
+ // 实现已改为"事务内先删 DB 行、提交后再物理删除对象":行删除先于物理删除发生,
+ // 物理删除抛错时真实 DB 回滚,但 mock 计数已记一次(该次删除未生效)。
+ verify(fileResultMapper, times(1)).deleteById(16L);
service.deleteHistory(16L, 7L);
- verify(fileResultMapper).deleteById(16L);
+ verify(fileResultMapper, times(2)).deleteById(16L);
verify(transientPayloadStorageService, times(3)).deletePayloadIfPresent(anyString());
}
}
diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/imagevideo/service/ImageVideoAsyncTaskServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/imagevideo/service/ImageVideoAsyncTaskServiceTest.java
index 48e34ef1..12d596f6 100644
--- a/backend-java/src/test/java/com/nanri/aiimage/modules/imagevideo/service/ImageVideoAsyncTaskServiceTest.java
+++ b/backend-java/src/test/java/com/nanri/aiimage/modules/imagevideo/service/ImageVideoAsyncTaskServiceTest.java
@@ -61,7 +61,8 @@ class ImageVideoAsyncTaskServiceTest {
TaskExecutor directExecutor = Runnable::run;
ImageVideoAsyncTaskService service = new ImageVideoAsyncTaskService(
taskMapper, cozeService, workflowConfigService, mock(ImageVideoArchiveService.class), objectMapper,
- directExecutor, instanceMetadata("server-121"));
+ directExecutor, instanceMetadata("server-121"),
+ mock(com.nanri.aiimage.common.service.DistributedJobLockService.class, org.mockito.Answers.RETURNS_MOCKS));
ImageVideoAsyncTaskEntity task = waitingDouyinTask();
Map