更新财务接口
This commit is contained in:
parent
2ba76809b3
commit
1b92e36bd6
@ -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 条;服务会读取全部页面后返回合计。
|
||||
|
||||
## 响应
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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("""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user