更新财务接口

This commit is contained in:
tanglong 2026-09-23 08:59:29 +08:00
parent 2ba76809b3
commit 1b92e36bd6
6 changed files with 63 additions and 46 deletions

View File

@ -14,13 +14,15 @@
- `startDate`、`endDate` 必填,包含首尾日期,最多 31 天。 - `startDate`、`endDate` 必填,包含首尾日期,最多 31 天。
- `storeName` 选填;不传或传空字符串时查询全部网点。多个网点可用逗号分隔。 - `storeName` 选填;不传或传空字符串时查询全部网点。多个网点可用逗号分隔。
- 后端通过 `ngp.api.bill.getSaleOrderList` 查询业绩,并固定传入 - 后端通过 `ngp.eshopsaleorder.bills` 查询网店发货任务,固定按发货时间查询
`postStateList=[800]`(财务核算成功)和 `refundType=0`(无需退款)。 (`timeType=3`)、只查询已发货任务 (`deliverStatusPackage=100`),并启用全部店铺类型
调用方不需要传这两个参数,也不能覆盖。 (`showAllEshopType=true`)。
- 该 NGP 方法需要在开放平台为当前应用开通“往来单位数据授权”。若未授权, - NGP 接口不提供退款状态请求参数;后端会在商品明细层只保留
接口会直接返回 NGP 的授权错误,不会降级为未筛选的全量数据。 `refundState=4`(无需退款),并汇总 `disedTaxedTotal`(含税金额)。
- 当前开放接口没有“财务核算成功”请求参数,不能直接复刻 ERP 页面中的该项筛选;
最终金额应使用已知日期的页面合计进行一次对账确认。
- 业绩缓存记录固定筛选规则的版本。首次部署本规则后,旧的“全部状态”缓存会自动失效并 - 业绩缓存记录固定筛选规则的版本。首次部署本规则后,旧的“全部状态”缓存会自动失效并
重新拉取,无需手工清空缓存表;月度每周统计也会使用同一份已筛选缓存。 重新拉取,无需手工清空缓存表;月度每周统计也会使用同一份新缓存。
- NGP 每页最多查询 200 条;服务会读取全部页面后返回合计。 - NGP 每页最多查询 200 条;服务会读取全部页面后返回合计。
## 响应 ## 响应

View File

@ -27,9 +27,9 @@ import java.util.concurrent.atomic.AtomicLong;
@Component @Component
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 PERFORMANCE_SALE_ORDER_LIST = "ngp.api.bill.getSaleOrderList"; static final String ESHOP_DELIVERY_BILL_LIST = "ngp.eshopsaleorder.bills";
static final int FINANCIAL_ACCOUNTING_SUCCESS = 800; static final int DELIVERY_TIME = 3;
static final int NO_REFUND_TYPE = 0; static final int DELIVERED = 100;
private final FoundationProperties.Ngp config; private final FoundationProperties.Ngp config;
private final NgpTokenProvider tokenProvider; private final NgpTokenProvider tokenProvider;
@ -63,9 +63,9 @@ public class NgpApiClient {
return call(SALES_POST_BILL_LIST, salesPostBillRequestBody(beginTime, endTime, pageIndex)); return call(SALES_POST_BILL_LIST, salesPostBillRequestBody(beginTime, endTime, pageIndex));
} }
public JsonNode performanceBillPage(String beginTime, String endTime, int pageIndex) { public JsonNode eshopDeliveryBillPage(String beginTime, String endTime, int pageIndex) {
return call(PERFORMANCE_SALE_ORDER_LIST, return call(ESHOP_DELIVERY_BILL_LIST,
performanceBillRequestBody(beginTime, endTime, pageIndex)); eshopDeliveryBillRequestBody(beginTime, endTime, pageIndex));
} }
Map<String, Object> salesPostBillRequestBody(String beginTime, String endTime, int pageIndex) { Map<String, Object> salesPostBillRequestBody(String beginTime, String endTime, int pageIndex) {
@ -78,15 +78,15 @@ public class NgpApiClient {
return body; return body;
} }
Map<String, Object> performanceBillRequestBody(String beginTime, String endTime, int pageIndex) { Map<String, Object> eshopDeliveryBillRequestBody(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));
body.put("pageIndex", pageIndex); body.put("pageIndex", pageIndex);
body.put("startTime", beginTime); body.put("beginTime", beginTime);
body.put("endTime", endTime); body.put("endTime", endTime);
body.put("vchtypes", List.of(2000)); body.put("timeType", DELIVERY_TIME);
body.put("postStateList", List.of(FINANCIAL_ACCOUNTING_SUCCESS)); body.put("deliverStatusPackage", DELIVERED);
body.put("refundType", NO_REFUND_TYPE); body.put("showAllEshopType", true);
return body; return body;
} }

View File

@ -17,7 +17,7 @@ import java.util.Optional;
@Repository @Repository
public class NgpPerformanceCacheRepository { 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; private final JdbcTemplate jdbcTemplate;

View File

@ -27,6 +27,7 @@ import java.util.concurrent.Executor;
public class NgpPerformanceDayLoader { public class NgpPerformanceDayLoader {
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai"); 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 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 NgpApiClient apiClient;
private final FoundationProperties.Ngp config; private final FoundationProperties.Ngp config;
@ -50,7 +51,7 @@ public class NgpPerformanceDayLoader {
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();
JsonNode firstPage = apiClient.performanceBillPage(beginTime, endTime, 1); JsonNode firstPage = apiClient.eshopDeliveryBillPage(beginTime, endTime, 1);
collect(firstPage, 1, aggregate); collect(firstPage, 1, aggregate);
long observedTotal = extractTotal(firstPage); long observedTotal = extractTotal(firstPage);
int totalPages = Math.max(1, pages(observedTotal, pageSize)); int totalPages = Math.max(1, pages(observedTotal, pageSize));
@ -61,7 +62,7 @@ public class NgpPerformanceDayLoader {
for (int page = batchStart; page <= batchEnd; page++) { for (int page = batchStart; page <= batchEnd; page++) {
int pageIndex = page; int pageIndex = page;
futures.add(new PageFuture(pageIndex, CompletableFuture.supplyAsync( futures.add(new PageFuture(pageIndex, CompletableFuture.supplyAsync(
() -> apiClient.performanceBillPage(beginTime, endTime, pageIndex), queryExecutor))); () -> apiClient.eshopDeliveryBillPage(beginTime, endTime, pageIndex), queryExecutor)));
} }
for (PageFuture future : futures) { for (PageFuture future : futures) {
try { try {
@ -90,29 +91,35 @@ public class NgpPerformanceDayLoader {
if (rows == null || !rows.isArray()) return; if (rows == null || !rows.isArray()) return;
int rowIndex = 0; int rowIndex = 0;
for (JsonNode bill : rows) { for (JsonNode bill : rows) {
String billKey = firstText(bill, "vchcode", "vchCode", "billNumber"); String billKey = firstText(bill, "taskId", "tradeOrderId", "taskNumber");
if (billKey.isBlank()) billKey = pageIndex + ":" + rowIndex; if (billKey.isBlank()) billKey = pageIndex + ":" + rowIndex;
rowIndex++; rowIndex++;
if (!aggregate.seenBills.add(billKey)) continue; if (!aggregate.seenBills.add(billKey)) continue;
String storeName = bill.path("ofullname").asText("").trim(); long billCents = billCents(bill);
if (storeName.isBlank()) storeName = ("未命名网点 " + bill.path("otypeId").asText("")).trim(); if (billCents == 0) continue;
String storeName = firstText(bill, "eshopFullname", "ofullname");
if (storeName.isBlank()) storeName = "未命名网点";
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(bill); long nextTotal = (current == null ? 0L : current.totalCents()) + billCents;
aggregate.storeTotals.put(storeKey, new NgpStoreTotal(storeName, nextTotal)); aggregate.storeTotals.put(storeKey, new NgpStoreTotal(storeName, nextTotal));
} }
} }
private long billCents(JsonNode bill) { private long billCents(JsonNode bill) {
JsonNode details = bill.path("detailList"); JsonNode details = bill.path("detailList");
if (!details.isArray() || details.isEmpty()) return Math.abs(toCents(bill.path("currencyBillTotal"))); if (details.isArray() && !details.isEmpty()) {
long result = 0; long result = 0;
for (JsonNode detail : details) { for (JsonNode detail : details) {
JsonNode amount = firstPresent(detail, if (detail.path("refundState").asInt(-1) != NO_REFUND_STATE) continue;
"currencyDisedTaxedTotal", "currencyDisedTotal", "currencyTotal"); JsonNode amount = firstPresent(detail, "disedTaxedTotal", "total", "tradeTotal");
result += Math.abs(toCents(amount)); 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) { private JsonNode firstPresent(JsonNode node, String... fields) {

View File

@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -30,7 +29,7 @@ class NgpApiClientTest {
} }
@Test @Test
void salesPerformanceRequestAlwaysFiltersSuccessfulAccountingAndNoRefund() { void eshopDeliveryRequestUsesDeliveryTimeAndAllShopTypes() {
FoundationProperties properties = new FoundationProperties(); FoundationProperties properties = new FoundationProperties();
NgpApiClient client = new NgpApiClient( NgpApiClient client = new NgpApiClient(
properties, properties,
@ -39,16 +38,16 @@ class NgpApiClientTest {
WebClient.builder() WebClient.builder()
); );
Map<String, Object> body = client.performanceBillRequestBody( Map<String, Object> body = client.eshopDeliveryBillRequestBody(
"2026-09-01 00:00:00", "2026-09-01 23:59:59", 3); "2026-09-01 00:00:00", "2026-09-01 23:59:59", 3);
assertThat(body) assertThat(body)
.containsEntry("postStateList", List.of(800)) .containsEntry("beginTime", "2026-09-01 00:00:00")
.containsEntry("refundType", 0)
.containsEntry("vchtypes", List.of(2000))
.containsEntry("startTime", "2026-09-01 00:00:00")
.containsEntry("endTime", "2026-09-01 23:59:59") .containsEntry("endTime", "2026-09-01 23:59:59")
.containsEntry("pageIndex", 3) .containsEntry("pageIndex", 3)
.doesNotContainKeys("postState", "refundStatus"); .containsEntry("timeType", 3)
.containsEntry("deliverStatusPackage", 100)
.containsEntry("showAllEshopType", true)
.doesNotContainKeys("postStateList", "refundType", "otypeIds");
} }
} }

View File

@ -32,29 +32,38 @@ class NgpPerformanceDayLoaderTest {
} }
@Test @Test
void fetchesEveryPageDeduplicatesBillsAndTotalsByStore() throws Exception { void fetchesEveryPageDeduplicatesTasksAndTotalsNoRefundDetailsByStore() throws Exception {
when(apiClient.performanceBillPage(anyString(), anyString(), org.mockito.ArgumentMatchers.anyInt())) when(apiClient.eshopDeliveryBillPage(anyString(), anyString(), org.mockito.ArgumentMatchers.anyInt()))
.thenAnswer(invocation -> pages().get(invocation.getArgument(2, Integer.class))); .thenAnswer(invocation -> pages().get(invocation.getArgument(2, Integer.class)));
Map<String, NgpStoreTotal> result = loader.load(LocalDate.of(2026, 9, 1)); Map<String, NgpStoreTotal> result = loader.load(LocalDate.of(2026, 9, 1));
assertThat(result.get(NgpPerformanceDayLoader.normalize("兰教练(抖店)")).totalCents()).isEqualTo(2195); assertThat(result.get(NgpPerformanceDayLoader.normalize("兰教练(抖店)")).totalCents()).isEqualTo(2195);
assertThat(result.get(NgpPerformanceDayLoader.normalize("另一个网点")).totalCents()).isEqualTo(200); 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<Integer, JsonNode> pages() throws Exception { private Map<Integer, JsonNode> pages() throws Exception {
return Map.of( return Map.of(
1, objectMapper.readTree(""" 1, objectMapper.readTree("""
{"code":0,"data":{"total":5,"list":[ {"code":0,"data":{"total":5,"list":[
{"vchcode":"A","ofullname":"兰教练(抖店)","currencyBillTotal":"11.95"}, {"taskId":"A","eshopFullname":"兰教练(抖店)","detailList":[
{"vchcode":"B","ofullname":"兰教练(抖店)","currencyBillTotal":"10.00"} {"refundState":4,"disedTaxedTotal":"11.95"},
{"refundState":2,"disedTaxedTotal":"99.00"}
]},
{"taskId":"B","eshopFullname":"兰教练(抖店)","detailList":[
{"refundState":4,"disedTaxedTotal":"10.00"}
]}
]}} ]}}
"""), """),
2, objectMapper.readTree(""" 2, objectMapper.readTree("""
{"code":0,"data":{"total":5,"list":[ {"code":0,"data":{"total":5,"list":[
{"vchcode":"B","ofullname":"兰教练(抖店)","currencyBillTotal":"10.00"}, {"taskId":"B","eshopFullname":"兰教练(抖店)","detailList":[
{"vchcode":"C","ofullname":"另一个网点","currencyBillTotal":"-2.00"} {"refundState":4,"disedTaxedTotal":"10.00"}
]},
{"taskId":"C","eshopFullname":"另一个网点","detailList":[
{"refundState":4,"disedTaxedTotal":"-2.00"}
]}
]}} ]}}
"""), """),
3, objectMapper.readTree(""" 3, objectMapper.readTree("""