220 lines
8.8 KiB
Java
Raw Normal View History

2026-09-08 15:09:52 +08:00
package com.company.agentfoundation.integration.ngp;
import com.company.agentfoundation.config.FoundationProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelOption;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.netty.http.client.HttpClient;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class NgpApiClient {
static final String SALES_POST_BILL_LIST = "ngp.bill.salepostbill.list";
2026-09-23 08:59:29 +08:00
static final String ESHOP_DELIVERY_BILL_LIST = "ngp.eshopsaleorder.bills";
2026-09-23 14:58:55 +08:00
static final String STORE_LIST = "ngp.otype.list";
2026-09-23 08:59:29 +08:00
static final int DELIVERY_TIME = 3;
static final int DELIVERED = 100;
2026-09-08 15:09:52 +08:00
private final FoundationProperties.Ngp config;
private final NgpTokenProvider tokenProvider;
private final ObjectMapper objectMapper;
private final WebClient webClient;
private final Duration readTimeout;
private final AtomicLong lastTimestamp = new AtomicLong();
public NgpApiClient(
FoundationProperties properties,
NgpTokenProvider tokenProvider,
ObjectMapper objectMapper,
WebClient.Builder builder
) {
this.config = properties.getNgp();
this.tokenProvider = tokenProvider;
this.objectMapper = objectMapper;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.max(config.getConnectTimeoutMs(), 1))
.responseTimeout(Duration.ofMillis(Math.max(config.getReadTimeoutMs(), 1)));
this.webClient = builder.clone()
.baseUrl(config.getBaseUrl())
.clientConnector(new ReactorClientHttpConnector(httpClient))
.codecs(codecs -> codecs.defaultCodecs()
.maxInMemorySize(Math.max(config.getMaxResponseBytes(), 256 * 1024)))
.build();
this.readTimeout = Duration.ofMillis(Math.max(config.getReadTimeoutMs(), 1));
}
public JsonNode salesPostBillPage(String beginTime, String endTime, int pageIndex) {
2026-09-09 13:27:11 +08:00
return call(SALES_POST_BILL_LIST, salesPostBillRequestBody(beginTime, endTime, pageIndex));
}
2026-09-23 08:59:29 +08:00
public JsonNode eshopDeliveryBillPage(String beginTime, String endTime, int pageIndex) {
return call(ESHOP_DELIVERY_BILL_LIST,
eshopDeliveryBillRequestBody(beginTime, endTime, pageIndex));
2026-09-18 13:31:39 +08:00
}
2026-09-23 14:58:55 +08:00
public JsonNode storePage(int pageIndex) {
return call(STORE_LIST, storeRequestBody(pageIndex));
}
2026-09-09 13:27:11 +08:00
Map<String, Object> salesPostBillRequestBody(String beginTime, String endTime, int pageIndex) {
2026-09-08 15:09:52 +08:00
Map<String, Object> body = new LinkedHashMap<>();
body.put("pageSize", Math.min(Math.max(config.getPageSize(), 1), 200));
body.put("pageIndex", pageIndex);
body.put("vchtypes", List.of(2000));
body.put("beginTime", beginTime);
body.put("endTime", endTime);
2026-09-18 13:31:39 +08:00
return body;
}
2026-09-23 08:59:29 +08:00
Map<String, Object> eshopDeliveryBillRequestBody(String beginTime, String endTime, int pageIndex) {
2026-09-18 13:31:39 +08:00
Map<String, Object> body = new LinkedHashMap<>();
body.put("pageSize", Math.min(Math.max(config.getPageSize(), 1), 200));
body.put("pageIndex", pageIndex);
2026-09-23 08:59:29 +08:00
body.put("beginTime", beginTime);
2026-09-18 13:31:39 +08:00
body.put("endTime", endTime);
2026-09-23 08:59:29 +08:00
body.put("timeType", DELIVERY_TIME);
body.put("deliverStatusPackage", DELIVERED);
body.put("showAllEshopType", true);
2026-09-09 13:27:11 +08:00
return body;
2026-09-08 15:09:52 +08:00
}
2026-09-23 14:58:55 +08:00
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;
}
2026-09-08 15:09:52 +08:00
private JsonNode call(String method, Object requestBody) {
int attempts = Math.max(config.getMaxRetries(), 0) + 1;
boolean tokenRefreshed = false;
String token = tokenProvider.getAccessToken();
RuntimeException lastFailure = null;
for (int attempt = 1; attempt <= attempts; attempt++) {
try {
JsonNode response = execute(method, requestBody, token);
if (!tokenRefreshed && isExpiredToken(response)) {
tokenRefreshed = true;
2026-09-23 11:30:45 +08:00
token = tokenProvider.refreshAccessToken(token);
2026-09-08 15:09:52 +08:00
attempt--;
continue;
}
validate(response, method);
return response;
} catch (WebClientResponseException exception) {
lastFailure = exception;
if (!transientStatus(exception.getStatusCode().value()) || attempt == attempts) break;
pause(attempt);
} catch (WebClientRequestException exception) {
lastFailure = exception;
if (attempt == attempts) break;
pause(attempt);
}
}
throw new NgpApiException("NGP 接口调用失败:" + method + ";" + rootMessage(lastFailure), lastFailure);
}
private JsonNode execute(String method, Object requestBody, String token) {
String body;
try {
body = objectMapper.writeValueAsString(requestBody);
} catch (JsonProcessingException exception) {
throw new NgpApiException("NGP 请求序列化失败", exception);
}
Map<String, String> systemParameters = new LinkedHashMap<>();
systemParameters.put("app_key", config.getAppKey());
systemParameters.put("method", method);
systemParameters.put("timestamp", nextTimestamp());
systemParameters.put("token", token);
String signature = sign(systemParameters, body, config.getAppSecret());
return webClient.post()
.uri(builder -> {
builder.path(config.getRestPath());
systemParameters.forEach(builder::queryParam);
builder.queryParam("sign", signature);
return builder.build();
})
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(body)
.retrieve()
.bodyToMono(JsonNode.class)
.block(readTimeout);
}
static String sign(Map<String, String> systemParameters, String body, String appSecret) {
if (appSecret == null || appSecret.isBlank()) throw new NgpApiException("NGP AppSecret 未配置");
StringBuilder source = new StringBuilder();
new TreeMap<>(systemParameters).forEach((name, value) -> source.append(name).append(value));
source.append(body);
try {
Mac mac = Mac.getInstance("HmacMD5");
mac.init(new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacMD5"));
return HexFormat.of().formatHex(mac.doFinal(source.toString().getBytes(StandardCharsets.UTF_8)));
} catch (Exception exception) {
throw new NgpApiException("NGP 签名生成失败", exception);
}
}
private String nextTimestamp() {
return String.valueOf(lastTimestamp.updateAndGet(previous -> Math.max(System.currentTimeMillis(), previous + 1)));
}
private void validate(JsonNode response, String method) {
if (response == null || response.isNull()) throw new NgpApiException("NGP 接口返回空响应:" + method);
int code = response.path("code").asInt(-1);
if (code != 0 && code != 200) {
throw new NgpApiException("NGP 接口业务失败:" + method + ",code=" + code
+ ",message=" + response.path("message").asText("未知错误"));
}
}
2026-09-23 11:30:45 +08:00
static boolean isExpiredToken(JsonNode response) {
2026-09-08 15:09:52 +08:00
if (response == null) return false;
2026-09-23 11:30:45 +08:00
String message = response.path("message").asText(response.path("msg").asText("")).toLowerCase();
2026-09-08 15:09:52 +08:00
return message.contains("token") && (message.contains("expired") || message.contains("invalid")
2026-09-23 11:30:45 +08:00
|| message.contains("error") || message.contains("失效") || message.contains("过期")
|| message.contains("错误"));
2026-09-08 15:09:52 +08:00
}
private boolean transientStatus(int status) {
return status == 429 || status >= 500;
}
private void pause(int attempt) {
try {
Thread.sleep(Math.min(1000L, 200L * attempt));
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new NgpApiException("NGP 接口重试被中断", exception);
}
}
private String rootMessage(Throwable throwable) {
if (throwable == null) return "未知错误";
Throwable current = throwable;
while (current.getCause() != null && current.getCause() != current) current = current.getCause();
return current.getMessage() == null ? current.getClass().getSimpleName() : current.getMessage();
}
}