From a61040e2bce669a33657e1c53749dd3ea39c981e Mon Sep 17 00:00:00 2001 From: my_local_kimi_dev2 Date: Tue, 22 Sep 2026 14:38:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(es):=20paperuselog=20state=20terms=20?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E7=BB=93=E6=9E=84=E4=BF=AE=E6=AD=A3=E2=80=94?= =?UTF-8?q?=E2=80=94{"terms":{"state":[2,4]}}=20=E8=AF=AF=E5=86=99?= =?UTF-8?q?=E6=88=90=E5=B5=8C=E5=A5=97=20terms=20=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E8=A2=AB=20ES=207.7.1=20=E4=BB=A5=20400=20=E6=8B=92=E7=BB=9D?= =?UTF-8?q?=EF=BC=88=E7=94=9F=E4=BA=A7=E5=AE=9E=E6=B5=8B=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E8=A1=A5=20Gateway=20impl=20=E5=B1=82=E5=8D=95=E6=B5=8B?= =?UTF-8?q?=E9=94=81=E8=AF=B7=E6=B1=82=E4=BD=93=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent/es/PaperUseLogEsGatewayImpl.java | 3 +- .../es/PaperUseLogEsGatewayImplTest.java | 138 ++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 xiaoqu-agent-service/src/test/java/me/iiv/agent/es/PaperUseLogEsGatewayImplTest.java diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/es/PaperUseLogEsGatewayImpl.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/es/PaperUseLogEsGatewayImpl.java index 5c69d6f..51ee598 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/es/PaperUseLogEsGatewayImpl.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/es/PaperUseLogEsGatewayImpl.java @@ -45,7 +45,8 @@ public class PaperUseLogEsGatewayImpl implements PaperUseLogEsGateway { Long fromMillis, Long toMillis, Integer deviceType, String positionMode) { ArrayNode filters = baseFilters(districtId, gender, fromMillis, toMillis, deviceType, positionMode); // 上纸(2)+撤纸(4) 合并后 rfid 去重(老 :302-342) - ArrayNode stateTerms = filters.addObject().putObject("terms").putObject("state").putArray("terms"); + // 正确结构是 {"terms":{"state":[2,4]}};写成 {"terms":{"state":{"terms":[...]}}} 会被 ES 以 400 拒绝(2026-09-22 生产实测) + ArrayNode stateTerms = filters.addObject().putObject("terms").putArray("state"); stateTerms.add(2).add(4); try { ObjectNode body = mapper.createObjectNode(); diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/es/PaperUseLogEsGatewayImplTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/es/PaperUseLogEsGatewayImplTest.java new file mode 100644 index 0000000..184b60d --- /dev/null +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/es/PaperUseLogEsGatewayImplTest.java @@ -0,0 +1,138 @@ +package me.iiv.agent.es; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PaperUseLogEsGatewayImplTest { + + @Mock + RestClient cloudRestClient; + + @Mock + RestClient localRestClient; + + PaperUseLogEsGatewayImpl gateway; + + private final ObjectMapper mapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + gateway = new PaperUseLogEsGatewayImpl(cloudRestClient, localRestClient); + } + + private static Response esResponse(String jsonBody) throws IOException { + Response response = org.mockito.Mockito.mock(Response.class); + when(response.getEntity()).thenReturn(new StringEntity(jsonBody, ContentType.APPLICATION_JSON)); + return response; + } + + private JsonNode captureRequestBody(String responseJson, boolean countOnly) throws Exception { + Response response = esResponse(responseJson); + when(localRestClient.performRequest(any(Request.class))).thenReturn(response); + if (countOnly) { + gateway.countState2("paperuselog", true, 2578L, 0, + 1789392000000L, 1789478399000L, null, null); + } else { + gateway.distinctRfidCount("paperuselog", true, 2578L, 0, + 1789392000000L, 1789478399000L, null, null); + } + ArgumentCaptor captor = ArgumentCaptor.forClass(Request.class); + verify(localRestClient).performRequest(captor.capture()); + Request captured = captor.getValue(); + assertEquals("POST", captured.getMethod()); + assertEquals(countOnly ? "/paperuselog/_count" : "/paperuselog/_search", captured.getEndpoint()); + return mapper.readTree(EntityUtils.toString(captured.getEntity())); + } + + @Test + void distinctRfidBuildsFlatStateTermsArray() throws Exception { + String response = "{\"aggregations\":{\"rfids\":{\"buckets\":[{\"key\":\"a\"},{\"key\":\"b\"}]}}}"; + JsonNode body = captureRequestBody(response, false); + + assertEquals(0, body.get("size").asInt()); + assertTrue(!body.get("track_total_hits").asBoolean()); + // 回归:必须是扁平数组 {"terms":{"state":[2,4]}}; + // 嵌套 {"terms":{"state":{"terms":[...]}}} 会被 ES 7.7.1 以 400 "terms_lookup unknown field [terms]" 拒绝(2026-09-22 生产实测) + JsonNode stateTerms = body.at("/query/bool/filter").findValue("state"); + assertTrue(stateTerms != null && stateTerms.isArray()); + assertEquals(2, stateTerms.size()); + assertEquals(2, stateTerms.get(0).asInt()); + assertEquals(4, stateTerms.get(1).asInt()); + // rfid 是 text 类型,terms 聚合必须走 rfid.keyword(直接聚合 text 报 400) + assertEquals("rfid.keyword", body.at("/aggs/rfids/terms/field").asText()); + assertEquals(10000, body.at("/aggs/rfids/terms/size").asInt()); + // 基础过滤:districtId/gender term + createTime 闭区间 range(epoch 毫秒) + assertTrue(body.at("/query/bool/filter").toString().contains("\"districtId\":{\"value\":2578}")); + assertTrue(body.at("/query/bool/filter").toString().contains("\"gender\":{\"value\":0}")); + assertEquals(1789392000000L, body.at("/query/bool/filter").findValue("gte").asLong()); + assertEquals(1789478399000L, body.at("/query/bool/filter").findValue("lte").asLong()); + } + + @Test + void distinctRfidMapsBucketCount() throws Exception { + String response = "{\"aggregations\":{\"rfids\":{\"buckets\":[{\"key\":\"a\"},{\"key\":\"b\"},{\"key\":\"c\"}]}}}"; + Response esResponse = esResponse(response); + when(localRestClient.performRequest(any(Request.class))).thenReturn(esResponse); + + long count = gateway.distinctRfidCount("paperuselog", true, 2578L, 0, + 1789392000000L, 1789478399000L, null, null); + + assertEquals(3L, count); + } + + @Test + void countState2BuildsTermFilterAndMapsCount() throws Exception { + JsonNode body = captureRequestBody("{\"count\":42}", true); + + assertTrue(body.at("/query/bool/filter").toString().contains("\"state\":{\"value\":2}")); + Response countResponse = esResponse("{\"count\":42}"); + when(localRestClient.performRequest(any(Request.class))).thenReturn(countResponse); + long count = gateway.countState2("paperuselog", true, 2578L, 0, + 1789392000000L, 1789478399000L, null, null); + assertEquals(42L, count); + } + + @Test + void routesToCloudClientWhenNotLocalCluster() throws Exception { + Response esResponse = esResponse("{\"aggregations\":{\"rfids\":{\"buckets\":[]}}}"); + when(cloudRestClient.performRequest(any(Request.class))).thenReturn(esResponse); + + gateway.distinctRfidCount("paperuselog", false, 2578L, 0, + 1789392000000L, 1789478399000L, null, null); + + verify(cloudRestClient).performRequest(any(Request.class)); + } + + @Test + void wrapsEsFailureInIllegalState() throws Exception { + when(localRestClient.performRequest(any(Request.class))) + .thenThrow(new IOException("connection reset")); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> gateway.distinctRfidCount("paperuselog", true, 2578L, 0, + 1789392000000L, 1789478399000L, null, null)); + assertTrue(exception.getMessage().startsWith("ES 查询换纸 rfid 去重失败")); + } +}