diff --git a/docs/ngp-performance-api.md b/docs/ngp-performance-api.md index ef0496c..e77ec6e 100644 --- a/docs/ngp-performance-api.md +++ b/docs/ngp-performance-api.md @@ -14,13 +14,15 @@ - `startDate`、`endDate` 必填,包含首尾日期,最多 31 天。 - `storeName` 选填;不传或传空字符串时查询全部网点。多个网点可用逗号分隔。 -- 后端通过 `ngp.api.bill.getSaleOrderList` 查询业绩,并固定传入 - `postStateList=[800]`(财务核算成功)和 `refundType=0`(无需退款)。 - 调用方不需要传这两个参数,也不能覆盖。 -- 该 NGP 方法需要在开放平台为当前应用开通“往来单位数据授权”。若未授权, - 接口会直接返回 NGP 的授权错误,不会降级为未筛选的全量数据。 +- 后端通过 `ngp.eshopsaleorder.bills` 查询网店发货任务,固定按发货时间查询 + (`timeType=3`)、只查询已发货任务 (`deliverStatusPackage=100`),并启用全部店铺类型 + (`showAllEshopType=true`)。 +- NGP 接口不提供退款状态请求参数;后端会在商品明细层只保留 + `refundState=4`(无需退款),并汇总 `disedTaxedTotal`(含税金额)。 +- 当前开放接口没有“财务核算成功”请求参数,不能直接复刻 ERP 页面中的该项筛选; + 最终金额应使用已知日期的页面合计进行一次对账确认。 - 业绩缓存记录固定筛选规则的版本。首次部署本规则后,旧的“全部状态”缓存会自动失效并 - 重新拉取,无需手工清空缓存表;月度每周统计也会使用同一份已筛选缓存。 + 重新拉取,无需手工清空缓存表;月度每周统计也会使用同一份新缓存。 - NGP 每页最多查询 200 条;服务会读取全部页面后返回合计。 ## 响应 diff --git a/src/main/java/com/company/agentfoundation/integration/ngp/NgpApiClient.java b/src/main/java/com/company/agentfoundation/integration/ngp/NgpApiClient.java index 8239c11..6306160 100644 --- a/src/main/java/com/company/agentfoundation/integration/ngp/NgpApiClient.java +++ b/src/main/java/com/company/agentfoundation/integration/ngp/NgpApiClient.java @@ -27,9 +27,9 @@ import java.util.concurrent.atomic.AtomicLong; @Component public class NgpApiClient { static final String SALES_POST_BILL_LIST = "ngp.bill.salepostbill.list"; - static final String PERFORMANCE_SALE_ORDER_LIST = "ngp.api.bill.getSaleOrderList"; - static final int FINANCIAL_ACCOUNTING_SUCCESS = 800; - static final int NO_REFUND_TYPE = 0; + static final String ESHOP_DELIVERY_BILL_LIST = "ngp.eshopsaleorder.bills"; + static final int DELIVERY_TIME = 3; + static final int DELIVERED = 100; private final FoundationProperties.Ngp config; private final NgpTokenProvider tokenProvider; @@ -63,9 +63,9 @@ public class NgpApiClient { return call(SALES_POST_BILL_LIST, salesPostBillRequestBody(beginTime, endTime, pageIndex)); } - public JsonNode performanceBillPage(String beginTime, String endTime, int pageIndex) { - return call(PERFORMANCE_SALE_ORDER_LIST, - performanceBillRequestBody(beginTime, endTime, pageIndex)); + public JsonNode eshopDeliveryBillPage(String beginTime, String endTime, int pageIndex) { + return call(ESHOP_DELIVERY_BILL_LIST, + eshopDeliveryBillRequestBody(beginTime, endTime, pageIndex)); } Map salesPostBillRequestBody(String beginTime, String endTime, int pageIndex) { @@ -78,15 +78,15 @@ public class NgpApiClient { return body; } - Map performanceBillRequestBody(String beginTime, String endTime, int pageIndex) { + Map eshopDeliveryBillRequestBody(String beginTime, String endTime, int pageIndex) { Map body = new LinkedHashMap<>(); body.put("pageSize", Math.min(Math.max(config.getPageSize(), 1), 200)); body.put("pageIndex", pageIndex); - body.put("startTime", beginTime); + body.put("beginTime", beginTime); body.put("endTime", endTime); - body.put("vchtypes", List.of(2000)); - body.put("postStateList", List.of(FINANCIAL_ACCOUNTING_SUCCESS)); - body.put("refundType", NO_REFUND_TYPE); + body.put("timeType", DELIVERY_TIME); + body.put("deliverStatusPackage", DELIVERED); + body.put("showAllEshopType", true); return body; } diff --git a/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceCacheRepository.java b/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceCacheRepository.java index fc8ad85..b434bd6 100644 --- a/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceCacheRepository.java +++ b/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceCacheRepository.java @@ -17,7 +17,7 @@ import java.util.Optional; @Repository public class NgpPerformanceCacheRepository { - static final String FILTER_SIGNATURE = "sale-order-post-800-refund-type-0-v1"; + static final String FILTER_SIGNATURE = "eshop-delivered-refund-state-4-v2"; private final JdbcTemplate jdbcTemplate; diff --git a/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoader.java b/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoader.java index 8ef7085..f420cf2 100644 --- a/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoader.java +++ b/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoader.java @@ -27,6 +27,7 @@ import java.util.concurrent.Executor; public class NgpPerformanceDayLoader { private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai"); private static final DateTimeFormatter API_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final int NO_REFUND_STATE = 4; private final NgpApiClient apiClient; private final FoundationProperties.Ngp config; @@ -50,7 +51,7 @@ public class NgpPerformanceDayLoader { int concurrency = Math.min(Math.max(config.getConcurrency(), 1), 100); Aggregate aggregate = new Aggregate(); - JsonNode firstPage = apiClient.performanceBillPage(beginTime, endTime, 1); + JsonNode firstPage = apiClient.eshopDeliveryBillPage(beginTime, endTime, 1); collect(firstPage, 1, aggregate); long observedTotal = extractTotal(firstPage); int totalPages = Math.max(1, pages(observedTotal, pageSize)); @@ -61,7 +62,7 @@ public class NgpPerformanceDayLoader { for (int page = batchStart; page <= batchEnd; page++) { int pageIndex = page; futures.add(new PageFuture(pageIndex, CompletableFuture.supplyAsync( - () -> apiClient.performanceBillPage(beginTime, endTime, pageIndex), queryExecutor))); + () -> apiClient.eshopDeliveryBillPage(beginTime, endTime, pageIndex), queryExecutor))); } for (PageFuture future : futures) { try { @@ -90,29 +91,35 @@ public class NgpPerformanceDayLoader { if (rows == null || !rows.isArray()) return; int rowIndex = 0; for (JsonNode bill : rows) { - String billKey = firstText(bill, "vchcode", "vchCode", "billNumber"); + String billKey = firstText(bill, "taskId", "tradeOrderId", "taskNumber"); if (billKey.isBlank()) billKey = pageIndex + ":" + rowIndex; rowIndex++; if (!aggregate.seenBills.add(billKey)) continue; - String storeName = bill.path("ofullname").asText("").trim(); - if (storeName.isBlank()) storeName = ("未命名网点 " + bill.path("otypeId").asText("")).trim(); + long billCents = billCents(bill); + if (billCents == 0) continue; + String storeName = firstText(bill, "eshopFullname", "ofullname"); + if (storeName.isBlank()) storeName = "未命名网点"; String storeKey = normalize(storeName); NgpStoreTotal current = aggregate.storeTotals.get(storeKey); - long nextTotal = (current == null ? 0L : current.totalCents()) + billCents(bill); + long nextTotal = (current == null ? 0L : current.totalCents()) + billCents; aggregate.storeTotals.put(storeKey, new NgpStoreTotal(storeName, nextTotal)); } } private long billCents(JsonNode bill) { JsonNode details = bill.path("detailList"); - if (!details.isArray() || details.isEmpty()) return Math.abs(toCents(bill.path("currencyBillTotal"))); - long result = 0; - for (JsonNode detail : details) { - JsonNode amount = firstPresent(detail, - "currencyDisedTaxedTotal", "currencyDisedTotal", "currencyTotal"); - result += Math.abs(toCents(amount)); + if (details.isArray() && !details.isEmpty()) { + long result = 0; + for (JsonNode detail : details) { + if (detail.path("refundState").asInt(-1) != NO_REFUND_STATE) continue; + JsonNode amount = firstPresent(detail, "disedTaxedTotal", "total", "tradeTotal"); + result += Math.abs(toCents(amount)); + } + return result; } - return result; + if (bill.path("refundState").asInt(-1) != NO_REFUND_STATE) return 0; + JsonNode amount = firstPresent(bill, "buyerTradeTotal", "buyerPaiedTotal", "tradeTotal"); + return Math.abs(toCents(amount)); } private JsonNode firstPresent(JsonNode node, String... fields) { diff --git a/src/test/java/com/company/agentfoundation/integration/ngp/NgpApiClientTest.java b/src/test/java/com/company/agentfoundation/integration/ngp/NgpApiClientTest.java index 5d39aaa..60d71b7 100644 --- a/src/test/java/com/company/agentfoundation/integration/ngp/NgpApiClientTest.java +++ b/src/test/java/com/company/agentfoundation/integration/ngp/NgpApiClientTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test; import org.springframework.web.reactive.function.client.WebClient; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -30,7 +29,7 @@ class NgpApiClientTest { } @Test - void salesPerformanceRequestAlwaysFiltersSuccessfulAccountingAndNoRefund() { + void eshopDeliveryRequestUsesDeliveryTimeAndAllShopTypes() { FoundationProperties properties = new FoundationProperties(); NgpApiClient client = new NgpApiClient( properties, @@ -39,16 +38,16 @@ class NgpApiClientTest { WebClient.builder() ); - Map body = client.performanceBillRequestBody( + Map body = client.eshopDeliveryBillRequestBody( "2026-09-01 00:00:00", "2026-09-01 23:59:59", 3); assertThat(body) - .containsEntry("postStateList", List.of(800)) - .containsEntry("refundType", 0) - .containsEntry("vchtypes", List.of(2000)) - .containsEntry("startTime", "2026-09-01 00:00:00") + .containsEntry("beginTime", "2026-09-01 00:00:00") .containsEntry("endTime", "2026-09-01 23:59:59") .containsEntry("pageIndex", 3) - .doesNotContainKeys("postState", "refundStatus"); + .containsEntry("timeType", 3) + .containsEntry("deliverStatusPackage", 100) + .containsEntry("showAllEshopType", true) + .doesNotContainKeys("postStateList", "refundType", "otypeIds"); } } diff --git a/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoaderTest.java b/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoaderTest.java index 7fc44be..af96728 100644 --- a/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoaderTest.java +++ b/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoaderTest.java @@ -32,29 +32,38 @@ class NgpPerformanceDayLoaderTest { } @Test - void fetchesEveryPageDeduplicatesBillsAndTotalsByStore() throws Exception { - when(apiClient.performanceBillPage(anyString(), anyString(), org.mockito.ArgumentMatchers.anyInt())) + void fetchesEveryPageDeduplicatesTasksAndTotalsNoRefundDetailsByStore() throws Exception { + when(apiClient.eshopDeliveryBillPage(anyString(), anyString(), org.mockito.ArgumentMatchers.anyInt())) .thenAnswer(invocation -> pages().get(invocation.getArgument(2, Integer.class))); Map result = loader.load(LocalDate.of(2026, 9, 1)); assertThat(result.get(NgpPerformanceDayLoader.normalize("兰教练(抖店)")).totalCents()).isEqualTo(2195); assertThat(result.get(NgpPerformanceDayLoader.normalize("另一个网点")).totalCents()).isEqualTo(200); - verify(apiClient, times(3)).performanceBillPage(anyString(), anyString(), org.mockito.ArgumentMatchers.anyInt()); + verify(apiClient, times(3)).eshopDeliveryBillPage(anyString(), anyString(), org.mockito.ArgumentMatchers.anyInt()); } private Map pages() throws Exception { return Map.of( 1, objectMapper.readTree(""" {"code":0,"data":{"total":5,"list":[ - {"vchcode":"A","ofullname":"兰教练(抖店)","currencyBillTotal":"11.95"}, - {"vchcode":"B","ofullname":"兰教练(抖店)","currencyBillTotal":"10.00"} + {"taskId":"A","eshopFullname":"兰教练(抖店)","detailList":[ + {"refundState":4,"disedTaxedTotal":"11.95"}, + {"refundState":2,"disedTaxedTotal":"99.00"} + ]}, + {"taskId":"B","eshopFullname":"兰教练(抖店)","detailList":[ + {"refundState":4,"disedTaxedTotal":"10.00"} + ]} ]}} """), 2, objectMapper.readTree(""" {"code":0,"data":{"total":5,"list":[ - {"vchcode":"B","ofullname":"兰教练(抖店)","currencyBillTotal":"10.00"}, - {"vchcode":"C","ofullname":"另一个网点","currencyBillTotal":"-2.00"} + {"taskId":"B","eshopFullname":"兰教练(抖店)","detailList":[ + {"refundState":4,"disedTaxedTotal":"10.00"} + ]}, + {"taskId":"C","eshopFullname":"另一个网点","detailList":[ + {"refundState":4,"disedTaxedTotal":"-2.00"} + ]} ]}} """), 3, objectMapper.readTree("""