管家婆接口更新

This commit is contained in:
tanglong 2026-09-23 14:58:55 +08:00
parent c07e85ce39
commit f0f74489ec
7 changed files with 192 additions and 11 deletions

View File

@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicLong;
public class NgpApiClient { public class NgpApiClient {
static final String SALES_POST_BILL_LIST = "ngp.bill.salepostbill.list"; static final String SALES_POST_BILL_LIST = "ngp.bill.salepostbill.list";
static final String ESHOP_DELIVERY_BILL_LIST = "ngp.eshopsaleorder.bills"; static final String ESHOP_DELIVERY_BILL_LIST = "ngp.eshopsaleorder.bills";
static final String STORE_LIST = "ngp.otype.list";
static final int DELIVERY_TIME = 3; static final int DELIVERY_TIME = 3;
static final int DELIVERED = 100; static final int DELIVERED = 100;
@ -68,6 +69,10 @@ public class NgpApiClient {
eshopDeliveryBillRequestBody(beginTime, endTime, pageIndex)); eshopDeliveryBillRequestBody(beginTime, endTime, pageIndex));
} }
public JsonNode storePage(int pageIndex) {
return call(STORE_LIST, storeRequestBody(pageIndex));
}
Map<String, Object> salesPostBillRequestBody(String beginTime, String endTime, int pageIndex) { Map<String, Object> salesPostBillRequestBody(String beginTime, String endTime, int pageIndex) {
Map<String, Object> body = new LinkedHashMap<>(); Map<String, Object> body = new LinkedHashMap<>();
body.put("pageSize", Math.min(Math.max(config.getPageSize(), 1), 200)); body.put("pageSize", Math.min(Math.max(config.getPageSize(), 1), 200));
@ -90,6 +95,15 @@ public class NgpApiClient {
return body; return body;
} }
Map<String, Object> storeRequestBody(int pageIndex) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("pageSize", Math.min(Math.max(config.getPageSize(), 1), 200));
body.put("pageIndex", pageIndex);
body.put("showAll", true);
body.put("showAllEshopType", true);
return body;
}
private JsonNode call(String method, Object requestBody) { private JsonNode call(String method, Object requestBody) {
int attempts = Math.max(config.getMaxRetries(), 0) + 1; int attempts = Math.max(config.getMaxRetries(), 0) + 1;
boolean tokenRefreshed = false; boolean tokenRefreshed = false;

View File

@ -0,0 +1,85 @@
package com.company.agentfoundation.integration.ngp;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
public class NgpStoreDirectory {
private static final long CACHE_MILLIS = Duration.ofHours(1).toMillis();
private static final int MAX_PAGES = 1_000;
private final NgpApiClient apiClient;
private final Object refreshLock = new Object();
private volatile Snapshot snapshot = new Snapshot(Map.of(), 0L);
public NgpStoreDirectory(NgpApiClient apiClient) {
this.apiClient = apiClient;
}
public Map<String, String> namesById() {
long now = System.currentTimeMillis();
Snapshot current = snapshot;
if (current.fresh(now)) return current.namesById();
synchronized (refreshLock) {
current = snapshot;
if (current.fresh(now)) return current.namesById();
Map<String, String> loaded = loadAll();
snapshot = new Snapshot(loaded, now);
return loaded;
}
}
private Map<String, String> loadAll() {
Map<String, String> result = new LinkedHashMap<>();
JsonNode firstPage = apiClient.storePage(1);
collect(firstPage, result);
long total = longValue(firstPage.path("data").path("total"));
int pageSize = Math.max(firstPage.path("data").path("pageSize").asInt(200), 1);
int totalPages = Math.max(1, (int) ((total + pageSize - 1L) / pageSize));
if (totalPages > MAX_PAGES) {
throw new NgpApiException("NGP 店铺目录分页数量异常:" + totalPages);
}
for (int pageIndex = 2; pageIndex <= totalPages; pageIndex++) {
collect(apiClient.storePage(pageIndex), result);
}
if (result.isEmpty()) throw new NgpApiException("NGP 店铺目录为空,无法生成按网点业绩缓存");
return Map.copyOf(result);
}
private void collect(JsonNode payload, Map<String, String> result) {
JsonNode rows = payload == null ? null : payload.path("data").path("list");
if (rows == null || !rows.isArray()) return;
for (JsonNode store : rows) {
String id = firstText(store, "otypeId", "oTypeId", "id");
String name = firstText(store, "fullName", "fullname", "name", "ofullname");
if (!id.isBlank() && !name.isBlank()) result.put(id, name);
}
}
private long longValue(JsonNode value) {
if (value == null || value.isMissingNode() || value.isNull()) return 0;
try {
return Long.parseLong(value.asText("0"));
} catch (NumberFormatException exception) {
throw new NgpApiException("NGP 店铺目录返回了无效的总数:" + value.asText(), exception);
}
}
private String firstText(JsonNode node, String... fields) {
for (String field : fields) {
String value = node.path(field).asText("").trim();
if (!value.isBlank()) return value;
}
return "";
}
private record Snapshot(Map<String, String> namesById, long loadedAtMillis) {
private boolean fresh(long now) {
return !namesById.isEmpty() && now - loadedAtMillis <= CACHE_MILLIS;
}
}
}

View File

@ -17,7 +17,7 @@ import java.util.Optional;
@Repository @Repository
public class NgpPerformanceCacheRepository { public class NgpPerformanceCacheRepository {
static final String FILTER_SIGNATURE = "eshop-delivered-refund-state-4-v2"; static final String FILTER_SIGNATURE = "eshop-delivered-refund-state-4-store-directory-v3";
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;

View File

@ -2,6 +2,7 @@ package com.company.agentfoundation.ngp.performance;
import com.company.agentfoundation.config.FoundationProperties; import com.company.agentfoundation.config.FoundationProperties;
import com.company.agentfoundation.integration.ngp.NgpApiClient; import com.company.agentfoundation.integration.ngp.NgpApiClient;
import com.company.agentfoundation.integration.ngp.NgpStoreDirectory;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -30,15 +31,18 @@ public class NgpPerformanceDayLoader {
private static final int NO_REFUND_STATE = 4; private static final int NO_REFUND_STATE = 4;
private final NgpApiClient apiClient; private final NgpApiClient apiClient;
private final NgpStoreDirectory storeDirectory;
private final FoundationProperties.Ngp config; private final FoundationProperties.Ngp config;
private final Executor queryExecutor; private final Executor queryExecutor;
public NgpPerformanceDayLoader( public NgpPerformanceDayLoader(
NgpApiClient apiClient, NgpApiClient apiClient,
NgpStoreDirectory storeDirectory,
FoundationProperties properties, FoundationProperties properties,
@Qualifier("ngpQueryExecutor") Executor queryExecutor @Qualifier("ngpQueryExecutor") Executor queryExecutor
) { ) {
this.apiClient = apiClient; this.apiClient = apiClient;
this.storeDirectory = storeDirectory;
this.config = properties.getNgp(); this.config = properties.getNgp();
this.queryExecutor = queryExecutor; this.queryExecutor = queryExecutor;
} }
@ -50,9 +54,10 @@ public class NgpPerformanceDayLoader {
int pageSize = Math.min(Math.max(config.getPageSize(), 1), 200); int pageSize = Math.min(Math.max(config.getPageSize(), 1), 200);
int concurrency = Math.min(Math.max(config.getConcurrency(), 1), 100); int concurrency = Math.min(Math.max(config.getConcurrency(), 1), 100);
Aggregate aggregate = new Aggregate(); Aggregate aggregate = new Aggregate();
Map<String, String> storeNamesById = storeDirectory.namesById();
JsonNode firstPage = apiClient.eshopDeliveryBillPage(beginTime, endTime, 1); JsonNode firstPage = apiClient.eshopDeliveryBillPage(beginTime, endTime, 1);
collect(firstPage, 1, aggregate); collect(firstPage, 1, aggregate, storeNamesById);
long observedTotal = extractTotal(firstPage); long observedTotal = extractTotal(firstPage);
int totalPages = Math.max(1, pages(observedTotal, pageSize)); int totalPages = Math.max(1, pages(observedTotal, pageSize));
@ -67,7 +72,7 @@ public class NgpPerformanceDayLoader {
for (PageFuture future : futures) { for (PageFuture future : futures) {
try { try {
JsonNode payload = future.payload().join(); JsonNode payload = future.payload().join();
collect(payload, future.pageIndex(), aggregate); collect(payload, future.pageIndex(), aggregate, storeNamesById);
observedTotal = Math.max(observedTotal, extractTotal(payload)); observedTotal = Math.max(observedTotal, extractTotal(payload));
} catch (CompletionException exception) { } catch (CompletionException exception) {
Throwable cause = exception.getCause() == null ? exception : exception.getCause(); Throwable cause = exception.getCause() == null ? exception : exception.getCause();
@ -86,7 +91,12 @@ public class NgpPerformanceDayLoader {
return (int) pages; return (int) pages;
} }
private void collect(JsonNode payload, int pageIndex, Aggregate aggregate) { private void collect(
JsonNode payload,
int pageIndex,
Aggregate aggregate,
Map<String, String> storeNamesById
) {
JsonNode rows = payload == null ? null : payload.path("data").path("list"); JsonNode rows = payload == null ? null : payload.path("data").path("list");
if (rows == null || !rows.isArray()) return; if (rows == null || !rows.isArray()) return;
int rowIndex = 0; int rowIndex = 0;
@ -97,8 +107,14 @@ public class NgpPerformanceDayLoader {
if (!aggregate.seenBills.add(billKey)) continue; if (!aggregate.seenBills.add(billKey)) continue;
long billCents = billCents(bill); long billCents = billCents(bill);
if (billCents == 0) continue; if (billCents == 0) continue;
String storeName = firstText(bill, "eshopFullname", "ofullname"); String storeId = firstText(bill, "otypeId", "oTypeId", "eshopId", "shopId", "storeId");
if (storeName.isBlank()) storeName = "未命名网点"; String storeName = firstText(
bill, "eshopFullname", "eshopFullName", "eshopName", "ofullname", "oFullName",
"storeName", "shopName");
if (storeName.isBlank() && !storeId.isBlank()) storeName = storeNamesById.getOrDefault(storeId, "");
if (storeName.isBlank()) {
storeName = storeId.isBlank() ? "未命名网点" : "未命名网点 " + storeId;
}
String storeKey = normalize(storeName); String storeKey = normalize(storeName);
NgpStoreTotal current = aggregate.storeTotals.get(storeKey); NgpStoreTotal current = aggregate.storeTotals.get(storeKey);
long nextTotal = (current == null ? 0L : current.totalCents()) + billCents; long nextTotal = (current == null ? 0L : current.totalCents()) + billCents;

View File

@ -54,6 +54,23 @@ class NgpApiClientTest {
.doesNotContainKeys("postStateList", "refundType", "otypeIds"); .doesNotContainKeys("postStateList", "refundType", "otypeIds");
} }
@Test
void storeRequestIncludesEveryShopType() {
FoundationProperties properties = new FoundationProperties();
NgpApiClient client = new NgpApiClient(
properties,
org.mockito.Mockito.mock(NgpTokenProvider.class),
new ObjectMapper(),
WebClient.builder()
);
assertThat(client.storeRequestBody(2))
.containsEntry("pageSize", 200)
.containsEntry("pageIndex", 2)
.containsEntry("showAll", true)
.containsEntry("showAllEshopType", true);
}
@Test @Test
void recognizesNgpErrorTokenResponseAsExpiredToken() throws Exception { void recognizesNgpErrorTokenResponseAsExpiredToken() throws Exception {
JsonNode response = objectMapper.readTree("{\"code\":-1,\"message\":\"error token\"}"); JsonNode response = objectMapper.readTree("{\"code\":-1,\"message\":\"error token\"}");

View File

@ -0,0 +1,43 @@
package com.company.agentfoundation.integration.ngp;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class NgpStoreDirectoryTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void loadsEveryPageAndCachesStoreNamesById() throws Exception {
NgpApiClient apiClient = mock(NgpApiClient.class);
when(apiClient.storePage(1)).thenReturn(objectMapper.readTree("""
{"code":0,"data":{"pageSize":2,"total":3,"list":[
{"otypeId":"101","fullName":"兰教练(抖店)"},
{"otypeId":"102","fullName":"另一个网点"}
]}}
"""));
when(apiClient.storePage(2)).thenReturn(objectMapper.readTree("""
{"code":0,"data":{"pageSize":2,"total":3,"list":[
{"otypeId":"103","fullName":"第三个网点"}
]}}
"""));
NgpStoreDirectory directory = new NgpStoreDirectory(apiClient);
Map<String, String> first = directory.namesById();
Map<String, String> second = directory.namesById();
assertThat(first).containsEntry("101", "兰教练(抖店)")
.containsEntry("102", "另一个网点")
.containsEntry("103", "第三个网点");
assertThat(second).isEqualTo(first);
verify(apiClient, times(1)).storePage(1);
verify(apiClient, times(1)).storePage(2);
}
}

View File

@ -2,6 +2,7 @@ package com.company.agentfoundation.ngp.performance;
import com.company.agentfoundation.config.FoundationProperties; import com.company.agentfoundation.config.FoundationProperties;
import com.company.agentfoundation.integration.ngp.NgpApiClient; import com.company.agentfoundation.integration.ngp.NgpApiClient;
import com.company.agentfoundation.integration.ngp.NgpStoreDirectory;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -25,10 +26,15 @@ class NgpPerformanceDayLoaderTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
apiClient = mock(NgpApiClient.class); apiClient = mock(NgpApiClient.class);
NgpStoreDirectory storeDirectory = mock(NgpStoreDirectory.class);
when(storeDirectory.namesById()).thenReturn(Map.of(
"101", "兰教练(抖店)",
"102", "另一个网点"
));
FoundationProperties properties = new FoundationProperties(); FoundationProperties properties = new FoundationProperties();
properties.getNgp().setPageSize(2); properties.getNgp().setPageSize(2);
properties.getNgp().setConcurrency(2); properties.getNgp().setConcurrency(2);
loader = new NgpPerformanceDayLoader(apiClient, properties, Runnable::run); loader = new NgpPerformanceDayLoader(apiClient, storeDirectory, properties, Runnable::run);
} }
@Test @Test
@ -47,21 +53,21 @@ class NgpPerformanceDayLoaderTest {
return Map.of( return Map.of(
1, objectMapper.readTree(""" 1, objectMapper.readTree("""
{"code":0,"data":{"total":5,"list":[ {"code":0,"data":{"total":5,"list":[
{"taskId":"A","eshopFullname":"兰教练(抖店)","detailList":[ {"taskId":"A","otypeId":"101","detailList":[
{"refundState":4,"disedTaxedTotal":"11.95"}, {"refundState":4,"disedTaxedTotal":"11.95"},
{"refundState":2,"disedTaxedTotal":"99.00"} {"refundState":2,"disedTaxedTotal":"99.00"}
]}, ]},
{"taskId":"B","eshopFullname":"兰教练(抖店)","detailList":[ {"taskId":"B","otypeId":"101","detailList":[
{"refundState":4,"disedTaxedTotal":"10.00"} {"refundState":4,"disedTaxedTotal":"10.00"}
]} ]}
]}} ]}}
"""), """),
2, objectMapper.readTree(""" 2, objectMapper.readTree("""
{"code":0,"data":{"total":5,"list":[ {"code":0,"data":{"total":5,"list":[
{"taskId":"B","eshopFullname":"兰教练(抖店)","detailList":[ {"taskId":"B","otypeId":"101","detailList":[
{"refundState":4,"disedTaxedTotal":"10.00"} {"refundState":4,"disedTaxedTotal":"10.00"}
]}, ]},
{"taskId":"C","eshopFullname":"另一个网点","detailList":[ {"taskId":"C","otypeId":"102","detailList":[
{"refundState":4,"disedTaxedTotal":"-2.00"} {"refundState":4,"disedTaxedTotal":"-2.00"}
]} ]}
]}} ]}}