feat(密钥管理): 代理配置后台明文展示 + 按次统计各密钥调用损耗
- 后台密钥管理「代理设置」列改为完整明文展示(含账号密码),便于运维核对; 新增 full 字段仅对代理模块下发,密钥两列保持脱敏 - 新增 biz_user_secret_usage_daily(V117):用户 × 模块 × 天累计真实对外请求次数 - 计次口径:LLM 每次真实 HTTP 请求(含重试)计 1 次;代理每次成功提取计 1 次 - LLM 埋点走 SecretUsageContext 上下文(批次外设置、线程池内快照恢复) - 新增内部上报接口 /api/internal/user-secret-usage(X-Internal-Token) - 新增后台页「密钥用量统计」:日期范围 + 用户名 + 分组筛选,含范围内汇总
This commit is contained in:
+2
-1
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import com.nanri.aiimage.modules.usersecret.service.UserSecretUsageService;
|
||||
|
||||
/**
|
||||
* Task 77:统一 LLM、品牌检查和紫鸟 HTTP 客户端的连接复用策略。
|
||||
@@ -98,7 +99,7 @@ class HttpClientConnectionReuseTest {
|
||||
void test_task_077_brand_normal_multiple_items() throws Exception {
|
||||
// 批量场景:Coze/品牌/紫鸟三个客户端各自持有独立 RestClient,
|
||||
// 但底层连接池共用同一 HttpClient 实例,不重复创建。
|
||||
SimilarAsinLlmClient llm = new SimilarAsinLlmClient(new SimilarAsinProperties(), new ObjectMapper(), null);
|
||||
SimilarAsinLlmClient llm = new SimilarAsinLlmClient(new SimilarAsinProperties(), new ObjectMapper(), null, null);
|
||||
BrandCheckClient brand = new BrandCheckClient(new BrandCheckProperties(), null);
|
||||
ZiniaoClientImpl ziniao = new ZiniaoClientImpl(new ZiniaoProperties(), new ObjectMapper());
|
||||
|
||||
|
||||
+10
-4
@@ -34,6 +34,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import com.nanri.aiimage.modules.usersecret.service.UserSecretUsageService;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Task 78:为所有外部调用(LLM / 品牌检查 / 紫鸟)统一增加耗时、重试、
|
||||
@@ -159,7 +161,8 @@ class ExternalCallMetricsRecorderTest {
|
||||
void test_task_078_payload_metrics_normal_default_path() throws Exception {
|
||||
SimilarAsinProperties props = llmProps();
|
||||
SimilarAsinLlmClient client =
|
||||
new SimilarAsinLlmClient(props, objectMapper, new ExternalCallMetricsRecorder(registry));
|
||||
new SimilarAsinLlmClient(props, objectMapper, new ExternalCallMetricsRecorder(registry),
|
||||
mock(UserSecretUsageService.class));
|
||||
|
||||
String content = client.invokeChat("test-model", "system", "hello", "test-key");
|
||||
|
||||
@@ -177,7 +180,8 @@ class ExternalCallMetricsRecorderTest {
|
||||
void test_task_078_payload_metrics_normal_multiple_items() throws Exception {
|
||||
SimilarAsinProperties props = llmProps();
|
||||
SimilarAsinLlmClient client =
|
||||
new SimilarAsinLlmClient(props, objectMapper, new ExternalCallMetricsRecorder(registry));
|
||||
new SimilarAsinLlmClient(props, objectMapper, new ExternalCallMetricsRecorder(registry),
|
||||
mock(UserSecretUsageService.class));
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
client.invokeChat("test-model", "system", "prompt-" + i, "test-key");
|
||||
@@ -259,7 +263,8 @@ class ExternalCallMetricsRecorderTest {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
int index = i;
|
||||
pool.submit(() -> {
|
||||
SimilarAsinLlmClient client = new SimilarAsinLlmClient(props, objectMapper, recorder);
|
||||
SimilarAsinLlmClient client = new SimilarAsinLlmClient(props, objectMapper, recorder,
|
||||
mock(UserSecretUsageService.class));
|
||||
try {
|
||||
client.invokeChat("test-model", "system", "prompt-" + index, "test-key");
|
||||
} catch (Exception ignored) {
|
||||
@@ -309,7 +314,8 @@ class ExternalCallMetricsRecorderTest {
|
||||
ExternalCallMetricsRecorder recorder = new ExternalCallMetricsRecorder(registry);
|
||||
SimilarAsinProperties props = llmProps();
|
||||
SimilarAsinLlmClient client =
|
||||
new SimilarAsinLlmClient(props, objectMapper, recorder);
|
||||
new SimilarAsinLlmClient(props, objectMapper, recorder,
|
||||
mock(UserSecretUsageService.class));
|
||||
|
||||
// 第一次调用走 500 失败路径,第二次调用恢复成功:错误可恢复
|
||||
llmFailNext.set(true);
|
||||
|
||||
+4
-1
@@ -22,6 +22,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import com.nanri.aiimage.modules.usersecret.service.UserSecretUsageService;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class AppearancePatentLlmClientHttpTest {
|
||||
|
||||
@@ -45,7 +47,8 @@ class AppearancePatentLlmClientHttpTest {
|
||||
properties,
|
||||
objectMapper,
|
||||
null,
|
||||
new BrandCheckClient(new BrandCheckProperties(), null)
|
||||
new BrandCheckClient(new BrandCheckProperties(), null),
|
||||
mock(UserSecretUsageService.class)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+11
-5
@@ -15,6 +15,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import com.nanri.aiimage.modules.usersecret.service.UserSecretUsageService;
|
||||
|
||||
class AppearancePatentLlmClientTest {
|
||||
|
||||
@@ -22,7 +23,8 @@ class AppearancePatentLlmClientTest {
|
||||
new AppearancePatentProperties(),
|
||||
new ObjectMapper(),
|
||||
null,
|
||||
new BrandCheckClient(new BrandCheckProperties(), null)
|
||||
new BrandCheckClient(new BrandCheckProperties(), null),
|
||||
mock(UserSecretUsageService.class)
|
||||
);
|
||||
|
||||
@Test
|
||||
@@ -65,7 +67,8 @@ class AppearancePatentLlmClientTest {
|
||||
new AppearancePatentProperties(),
|
||||
new ObjectMapper(),
|
||||
null,
|
||||
brandCheckClient
|
||||
brandCheckClient,
|
||||
mock(UserSecretUsageService.class)
|
||||
);
|
||||
AppearancePatentResultRowDto row = new AppearancePatentResultRowDto();
|
||||
row.setId("1");
|
||||
@@ -90,7 +93,8 @@ class AppearancePatentLlmClientTest {
|
||||
new AppearancePatentProperties(),
|
||||
new ObjectMapper(),
|
||||
null,
|
||||
brandCheckClient
|
||||
brandCheckClient,
|
||||
mock(UserSecretUsageService.class)
|
||||
);
|
||||
AppearancePatentResultRowDto row = new AppearancePatentResultRowDto();
|
||||
row.setId("1");
|
||||
@@ -112,7 +116,8 @@ class AppearancePatentLlmClientTest {
|
||||
new AppearancePatentProperties(),
|
||||
new ObjectMapper(),
|
||||
null,
|
||||
brandCheckClient
|
||||
brandCheckClient,
|
||||
mock(UserSecretUsageService.class)
|
||||
);
|
||||
AppearancePatentResultRowDto row = new AppearancePatentResultRowDto();
|
||||
row.setId("1");
|
||||
@@ -134,7 +139,8 @@ class AppearancePatentLlmClientTest {
|
||||
new AppearancePatentProperties(),
|
||||
new ObjectMapper(),
|
||||
null,
|
||||
brandCheckClient
|
||||
brandCheckClient,
|
||||
mock(UserSecretUsageService.class)
|
||||
);
|
||||
AppearancePatentResultRowDto row = new AppearancePatentResultRowDto();
|
||||
row.setId("1");
|
||||
|
||||
+3
-1
@@ -16,6 +16,8 @@ import org.mockito.Mockito;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.nanri.aiimage.modules.usersecret.service.UserSecretUsageService;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* 本地验证入口:用生产真实批次数据 + 生产 LLM 网关跑 SimilarAsinLlmService 完整链路。
|
||||
@@ -60,7 +62,7 @@ public class SimilarAsinLlmLocalVerify {
|
||||
props.setLlmRowConcurrency(2);
|
||||
props.setLlmImageDownloadTimeoutSeconds(10);
|
||||
|
||||
SimilarAsinLlmClient client = new SimilarAsinLlmClient(props, objectMapper, null);
|
||||
SimilarAsinLlmClient client = new SimilarAsinLlmClient(props, objectMapper, null, null);
|
||||
OssProperties ossProps = new OssProperties();
|
||||
ossProps.setEndpoint("https://oss.aishufu.top");
|
||||
ossProps.setPublicEndpoint("https://oss.aishufu.top");
|
||||
|
||||
+47
-3
@@ -1,9 +1,12 @@
|
||||
package com.nanri.aiimage.modules.usersecret.service;
|
||||
|
||||
import com.nanri.aiimage.common.security.ShopCredentialCryptoService;
|
||||
import com.nanri.aiimage.config.UserSecretProperties;
|
||||
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||
import com.nanri.aiimage.modules.notification.service.NotificationDispatchService;
|
||||
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||
import com.nanri.aiimage.modules.permission.support.UserDataScopeSupport;
|
||||
import com.nanri.aiimage.modules.shopkey.model.dto.UserGroupRef;
|
||||
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
|
||||
import com.nanri.aiimage.modules.usersecret.client.JikipProxyClient;
|
||||
@@ -36,6 +39,9 @@ class UserApiSecretServiceTest {
|
||||
private final AdminAuthSupport adminAuthSupport = mock(AdminAuthSupport.class);
|
||||
private final com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper adminGroupMapper =
|
||||
mock(com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper.class);
|
||||
private final UserDataScopeSupport userDataScopeSupport = mock(UserDataScopeSupport.class);
|
||||
private final NotificationDispatchService notificationDispatchService = mock(NotificationDispatchService.class);
|
||||
private final UserSecretProperties userSecretProperties = new UserSecretProperties();
|
||||
|
||||
private UserApiSecretService newService() {
|
||||
when(crypto.encrypt(anyString())).thenAnswer(inv -> "enc:" + inv.getArgument(0, String.class));
|
||||
@@ -46,7 +52,8 @@ class UserApiSecretServiceTest {
|
||||
// 默认按主管(admin)判定;超管用例里单独改打桩。
|
||||
when(adminAuthSupport.currentRole(any())).thenReturn("admin");
|
||||
return new UserApiSecretService(
|
||||
mapper, crypto, checkService, jikipProxyClient, adminUserMapper, adminAuthSupport, adminGroupMapper);
|
||||
mapper, crypto, checkService, jikipProxyClient, adminUserMapper, adminAuthSupport, adminGroupMapper,
|
||||
userDataScopeSupport, notificationDispatchService, userSecretProperties);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -240,8 +247,10 @@ class UserApiSecretServiceTest {
|
||||
assertThat(rowVo.getGroups()).containsExactly("一组");
|
||||
assertThat(rowVo.getStatus()).isEqualTo("failed");
|
||||
assertThat(rowVo.getSimilarAsin().getMasked()).isEqualTo("sk-s****1234");
|
||||
assertThat(rowVo.getSimilarAsin().getFull()).isNull();
|
||||
assertThat(rowVo.getProxy().getExists()).isTrue();
|
||||
assertThat(rowVo.getProxy().getMasked()).isEqualTo("http://***@1.2.3.4:8080");
|
||||
assertThat(rowVo.getProxy().getFull()).isEqualTo("http://user:pass@1.2.3.4:8080");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -268,7 +277,7 @@ class UserApiSecretServiceTest {
|
||||
group.setId(5L);
|
||||
group.setGroupName("一组");
|
||||
when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of(group));
|
||||
when(adminGroupMapper.selectUserIdsByGroupId(5L)).thenReturn(List.of(1L, 88L));
|
||||
when(userDataScopeSupport.resolveVisibleUserIds(88L)).thenReturn(List.of(1L, 88L));
|
||||
when(mapper.selectList(any())).thenReturn(List.of(row(1L, "similar-asin", "enc:sk-1", "passed")));
|
||||
AdminUserEntity user = new AdminUserEntity();
|
||||
user.setId(1L);
|
||||
@@ -285,7 +294,42 @@ class UserApiSecretServiceTest {
|
||||
assertThat(page.getItems().get(0).getGroups()).containsExactly("一组");
|
||||
assertThat(page.getGroupOptions()).extracting(com.nanri.aiimage.modules.usersecret.model.vo.AdminUserSecretPageVo.GroupOptionVo::groupName)
|
||||
.containsExactly("一组");
|
||||
verify(adminGroupMapper).selectUserIdsByGroupId(5L);
|
||||
verify(userDataScopeSupport).resolveVisibleUserIds(88L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void decideSecretAlertMapsBalanceAndInvalidKeyOnly() {
|
||||
// 欠费 → error 级 secret_balance
|
||||
var balance = UserApiSecretService.decideSecretAlert(new UserApiSecretCheckService.CheckOutcome(
|
||||
UserApiSecretCheckService.STATUS_FAILED,
|
||||
UserApiSecretCheckService.CODE_INSUFFICIENT_BALANCE,
|
||||
"余额不足", 120, false));
|
||||
assertThat(balance).isNotNull();
|
||||
assertThat(balance.scene()).isEqualTo("secret_balance");
|
||||
assertThat(balance.level()).isEqualTo("error");
|
||||
|
||||
// 密钥无效 / 被拒 → warning 级 secret_invalid
|
||||
for (String code : List.of(UserApiSecretCheckService.CODE_INVALID_KEY, UserApiSecretCheckService.CODE_FORBIDDEN)) {
|
||||
var invalid = UserApiSecretService.decideSecretAlert(new UserApiSecretCheckService.CheckOutcome(
|
||||
UserApiSecretCheckService.STATUS_FAILED, code, "失败", 100, false));
|
||||
assertThat(invalid).isNotNull();
|
||||
assertThat(invalid.scene()).isEqualTo("secret_invalid");
|
||||
assertThat(invalid.level()).isEqualTo("warning");
|
||||
}
|
||||
|
||||
// 网络抖动 / 限流 / 上游异常 → 不通知
|
||||
for (String code : List.of(UserApiSecretCheckService.CODE_NETWORK_ERROR,
|
||||
UserApiSecretCheckService.CODE_RATE_LIMITED, UserApiSecretCheckService.CODE_SERVER_ERROR)) {
|
||||
assertThat(UserApiSecretService.decideSecretAlert(new UserApiSecretCheckService.CheckOutcome(
|
||||
UserApiSecretCheckService.STATUS_ERROR, code, "抖动", 90, false))).isNull();
|
||||
assertThat(UserApiSecretService.decideSecretAlert(new UserApiSecretCheckService.CheckOutcome(
|
||||
UserApiSecretCheckService.STATUS_FAILED, code, "抖动", 90, false))).isNull();
|
||||
}
|
||||
|
||||
// 通过 / 空 outcome → 不通知
|
||||
assertThat(UserApiSecretService.decideSecretAlert(new UserApiSecretCheckService.CheckOutcome(
|
||||
UserApiSecretCheckService.STATUS_PASSED, UserApiSecretCheckService.CODE_OK, "正常", 50, false))).isNull();
|
||||
assertThat(UserApiSecretService.decideSecretAlert(null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package com.nanri.aiimage.modules.usersecret.service;
|
||||
|
||||
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||
import com.nanri.aiimage.modules.permission.support.UserDataScopeSupport;
|
||||
import com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper;
|
||||
import com.nanri.aiimage.modules.shopkey.model.dto.UserGroupRef;
|
||||
import com.nanri.aiimage.modules.usersecret.mapper.UserSecretUsageMapper;
|
||||
import com.nanri.aiimage.modules.usersecret.model.dto.AdminUserSecretUsageQuery;
|
||||
import com.nanri.aiimage.modules.usersecret.model.entity.UserSecretUsageEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class UserSecretUsageServiceTest {
|
||||
|
||||
private final UserSecretUsageMapper usageMapper = mock(UserSecretUsageMapper.class);
|
||||
private final AdminUserMapper adminUserMapper = mock(AdminUserMapper.class);
|
||||
private final AdminAuthSupport adminAuthSupport = mock(AdminAuthSupport.class);
|
||||
private final ShopManageGroupMapper adminGroupMapper = mock(ShopManageGroupMapper.class);
|
||||
private final UserDataScopeSupport userDataScopeSupport = mock(UserDataScopeSupport.class);
|
||||
|
||||
private UserSecretUsageService newService() {
|
||||
// 默认按主管(admin)判定;超管用例里单独改打桩。
|
||||
when(adminAuthSupport.currentRole(any())).thenReturn("admin");
|
||||
return new UserSecretUsageService(
|
||||
usageMapper, adminUserMapper, adminAuthSupport, adminGroupMapper, userDataScopeSupport);
|
||||
}
|
||||
|
||||
private UserSecretUsageEntity usageRow(Long userId, String moduleKey, int count) {
|
||||
UserSecretUsageEntity row = new UserSecretUsageEntity();
|
||||
row.setUserId(userId);
|
||||
row.setModuleKey(moduleKey);
|
||||
row.setBusinessDate(LocalDate.now());
|
||||
row.setCallCount(count);
|
||||
return row;
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordIncrementsByModuleWithTodayDate() {
|
||||
UserSecretUsageService service = newService();
|
||||
when(usageMapper.increment(any(), any(), any(), eq(1))).thenReturn(1);
|
||||
|
||||
service.record(7L, "similar-asin", 1);
|
||||
|
||||
verify(usageMapper).increment(eq(7L), eq("similar-asin"), eq(LocalDate.now()), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordSkipsInvalidUserOrUnknownModule() {
|
||||
UserSecretUsageService service = newService();
|
||||
|
||||
service.record(null, "similar-asin", 1);
|
||||
service.record(0L, "similar-asin", 1);
|
||||
service.record(7L, "not-a-module", 1);
|
||||
service.record(7L, "similar-asin", 0);
|
||||
|
||||
verify(usageMapper, never()).increment(any(), any(), any(), any(Integer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordSwallowsMapperFailure() {
|
||||
UserSecretUsageService service = newService();
|
||||
when(usageMapper.increment(any(), any(), any(), eq(1)))
|
||||
.thenThrow(new IllegalStateException("db down"));
|
||||
|
||||
// 计次失败绝不影响任务主流程
|
||||
service.record(7L, "proxy", 1);
|
||||
|
||||
verify(usageMapper).increment(eq(7L), eq("proxy"), eq(LocalDate.now()), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordReportedCapsCountAndRejectsUnknownModule() {
|
||||
UserSecretUsageService service = newService();
|
||||
when(usageMapper.increment(any(), any(), any(), any(Integer.class))).thenReturn(1);
|
||||
|
||||
assertThat(service.recordReported(7L, "proxy", 50_000)).isTrue();
|
||||
assertThat(service.recordReported(7L, "not-a-module", 3)).isFalse();
|
||||
assertThat(service.recordReported(7L, "proxy", 0)).isFalse();
|
||||
|
||||
// 超出上限按 10000 截断,防止异常客户端写爆计数
|
||||
verify(usageMapper).increment(eq(7L), eq("proxy"), eq(LocalDate.now()), eq(10_000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminPageAggregatesByUserAndSortsByTotal() {
|
||||
UserSecretUsageService service = newService();
|
||||
when(adminAuthSupport.currentRole(any())).thenReturn("super_admin");
|
||||
when(usageMapper.selectList(any())).thenReturn(List.of(
|
||||
usageRow(1L, "similar-asin", 10),
|
||||
usageRow(1L, "proxy", 2),
|
||||
usageRow(2L, "appearance-patent", 7)));
|
||||
|
||||
AdminUserEntity user1 = new AdminUserEntity();
|
||||
user1.setId(1L);
|
||||
user1.setUsername("张三");
|
||||
AdminUserEntity user2 = new AdminUserEntity();
|
||||
user2.setId(2L);
|
||||
user2.setUsername("李四");
|
||||
when(adminUserMapper.selectBatchIds(any())).thenReturn(List.of(user1, user2));
|
||||
UserGroupRef ref = new UserGroupRef();
|
||||
ref.setUserId(1L);
|
||||
ref.setGroupName("一组");
|
||||
when(adminGroupMapper.selectGroupNamesByUserIds(any())).thenReturn(List.of(ref));
|
||||
|
||||
var page = service.adminPage(new AdminUserEntity(), new AdminUserSecretUsageQuery());
|
||||
|
||||
assertThat(page.getTotal()).isEqualTo(2);
|
||||
assertThat(page.getItems()).hasSize(2);
|
||||
// 合计倒序:张三 12 > 李四 7
|
||||
var first = page.getItems().get(0);
|
||||
assertThat(first.getUserId()).isEqualTo(1L);
|
||||
assertThat(first.getUsername()).isEqualTo("张三");
|
||||
assertThat(first.getGroups()).containsExactly("一组");
|
||||
assertThat(first.getSimilarAsinCount()).isEqualTo(10);
|
||||
assertThat(first.getProxyCount()).isEqualTo(2);
|
||||
assertThat(first.getTotalCount()).isEqualTo(12);
|
||||
var second = page.getItems().get(1);
|
||||
assertThat(second.getAppearancePatentCount()).isEqualTo(7);
|
||||
assertThat(second.getTotalCount()).isEqualTo(7);
|
||||
// 汇总覆盖全量(不分页)
|
||||
assertThat(page.getSimilarAsinCalls()).isEqualTo(10);
|
||||
assertThat(page.getAppearancePatentCalls()).isEqualTo(7);
|
||||
assertThat(page.getProxyCalls()).isEqualTo(2);
|
||||
assertThat(page.getTotalCalls()).isEqualTo(19);
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminPageReturnsEmptyWhenLeaderHasNoVisibleUsers() {
|
||||
UserSecretUsageService service = newService();
|
||||
AdminUserEntity operator = new AdminUserEntity();
|
||||
operator.setId(88L);
|
||||
when(userDataScopeSupport.resolveVisibleUserIds(88L)).thenReturn(List.of());
|
||||
when(adminGroupMapper.selectLedGroups(88L)).thenReturn(List.of());
|
||||
|
||||
var page = service.adminPage(operator, new AdminUserSecretUsageQuery());
|
||||
|
||||
assertThat(page.getItems()).isEmpty();
|
||||
assertThat(page.getTotal()).isZero();
|
||||
assertThat(page.getTotalCalls()).isZero();
|
||||
// 无可见用户时不应触发用量查询
|
||||
verify(usageMapper, never()).selectList(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminPageFiltersByGroupForSuperAdmin() {
|
||||
UserSecretUsageService service = newService();
|
||||
when(adminAuthSupport.currentRole(any())).thenReturn("super_admin");
|
||||
when(adminGroupMapper.selectUserIdsByGroupId(5L)).thenReturn(List.of(2L));
|
||||
when(usageMapper.selectList(any())).thenReturn(List.of(usageRow(2L, "proxy", 3)));
|
||||
AdminUserEntity user2 = new AdminUserEntity();
|
||||
user2.setId(2L);
|
||||
user2.setUsername("李四");
|
||||
when(adminUserMapper.selectBatchIds(any())).thenReturn(List.of(user2));
|
||||
when(adminGroupMapper.selectGroupNamesByUserIds(any())).thenReturn(List.of());
|
||||
|
||||
AdminUserSecretUsageQuery query = new AdminUserSecretUsageQuery();
|
||||
query.setGroupId(5L);
|
||||
var page = service.adminPage(new AdminUserEntity(), query);
|
||||
|
||||
assertThat(page.getItems()).hasSize(1);
|
||||
assertThat(page.getItems().get(0).getProxyCount()).isEqualTo(3);
|
||||
assertThat(page.getProxyCalls()).isEqualTo(3);
|
||||
|
||||
// 空日期范围不产生额外过滤条件(由 mapper 兜底)
|
||||
verify(usageMapper).selectList(any());
|
||||
verify(adminGroupMapper).selectUserIdsByGroupId(eq(5L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminPageReturnsEmptyWhenKeywordMatchesNobody() {
|
||||
UserSecretUsageService service = newService();
|
||||
when(adminAuthSupport.currentRole(any())).thenReturn("super_admin");
|
||||
when(adminUserMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
AdminUserSecretUsageQuery query = new AdminUserSecretUsageQuery();
|
||||
query.setKeyword("不存在的人");
|
||||
var page = service.adminPage(new AdminUserEntity(), query);
|
||||
|
||||
assertThat(page.getItems()).isEmpty();
|
||||
verify(usageMapper, never()).selectList(any());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user