更新财务接口

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 天。
- `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 条;服务会读取全部页面后返回合计。
## 响应

View File

@ -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<String, Object> salesPostBillRequestBody(String beginTime, String endTime, int pageIndex) {
@ -78,15 +78,15 @@ public class NgpApiClient {
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<>();
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;
}

View File

@ -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;

View File

@ -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) {

View File

@ -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<String, Object> body = client.performanceBillRequestBody(
Map<String, Object> 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");
}
}

View File

@ -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<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(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 {
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("""