管家婆接口更新
This commit is contained in:
parent
c07e85ce39
commit
f0f74489ec
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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\"}");
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"}
|
||||||
]}
|
]}
|
||||||
]}}
|
]}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user