feat(admin): A1 管理后台收敛 Java 单后台完整实现并修复 guard 误拦内部令牌
Build Backend JAR / build (push) Has been cancelled

- Java 补齐后台全部迁移差集:shopduplicatecheck 店铺数据重复检查模块(V108 扫描表+查询/扫描服务)、
  PinyinAbbrUtil 拼音缩写、ImageHistory 接口调整为内部可用、AdminUser 支持内部令牌操作并放宽列表上限
- Flask 后台 admin_api.py 路由收敛转发 Java、admin.html/admin.js 适配新后台形态
- AdminApiGuardFilter 对可信 X-Internal-Token 放行(controller 自校验兜底),修复客户端仅凭
  内部令牌调用 /api/admin/shop-manages/credential 被误拦 401
- 测试:AdminApiGuardFilterTest 补可信/假令牌用例;AdminUserServiceTest 补菜单权限 mock;
  shopduplicatecheck 新增查询/聚合/CSV 单测
This commit is contained in:
2026-09-04 11:30:09 +08:00
parent 3420c72c1d
commit 8241cd704e
43 changed files with 3071 additions and 1318 deletions
@@ -62,6 +62,44 @@ class AdminApiGuardFilterTest {
assertThat(response.getContentAsString()).isEmpty();
}
@Test
void trustedInternalTokenBypassesGuardButReachesController() throws Exception {
// 仅凭 X-Internal-Token 的合法内部调用(如 shop-manages/credential 由 controller 自校验)
// 应被 guard 放行,而不是拦成 401 —— 修复客户端带令牌却被 guard 误拦的故障。
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
when(authSupport.isTrustedInternalToken(any())).thenReturn(true);
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-manages/credential");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
verify(authSupport, never()).requireUserOrInternal(any());
assertThat(chain.getRequest()).isNotNull();
assertThat(response.getContentAsString()).isEmpty();
}
@Test
void untrustedTokenStillRejected() throws Exception {
// 令牌不匹配(X-Internal-Token 错值/泄露)时 guard 仍然拦截
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
when(authSupport.isTrustedInternalToken(any())).thenReturn(false);
when(authSupport.requireUserOrInternal(any()))
.thenThrow(new BusinessException(401, "未登录"));
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-keys");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
JsonNode body = objectMapper.readTree(response.getContentAsString());
assertThat(body.path("success").asBoolean()).isFalse();
assertThat(body.path("code").asInt()).isEqualTo(401);
assertThat(chain.getRequest()).isNull();
}
@Test
void anonymousAdminApiRequestRejectedWith401Body() throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);