|
|
@@ -11,12 +11,13 @@ import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
import java.time.LocalDate;
|
|
|
+import java.util.List;
|
|
|
|
|
|
/**
|
|
|
* 调用 Python 抓取服务(xg_material_scraper)的 HTTP 客户端。
|
|
|
*
|
|
|
* 前端永远不直连 Python:只有 Java 通过内网调用本客户端,
|
|
|
- * XXL 定时任务与管理员「立即采集」共用同一条链路。
|
|
|
+ * XXL 定时任务(最新数据)与管理员区间采集(补历史)共用同一条链路。
|
|
|
* 配置项在 Nacos 的 material-price.scraper.* 下,未配置时使用本地默认值。
|
|
|
*/
|
|
|
@Slf4j
|
|
|
@@ -31,13 +32,8 @@ public class MaterialPriceScraperClient {
|
|
|
@Value("${material-price.scraper.api-key:dev-key-change-me}")
|
|
|
private String apiKey;
|
|
|
|
|
|
- /** HTTP 读超时,必须大于 Python 侧整批超时(默认 120s 的请求超时要小于它) */
|
|
|
- @Value("${material-price.scraper.timeout-ms:50000}")
|
|
|
- private int timeoutMs;
|
|
|
-
|
|
|
- /** 开关:关闭后同步服务调用会直接抛错,便于临时停用抓取 */
|
|
|
- @Value("${material-price.scraper.enabled:true}")
|
|
|
- private boolean enabled;
|
|
|
+ /** HTTP 读超时,必须大于 Python 侧整批超时 */
|
|
|
+ private int timeoutMs=600000;
|
|
|
|
|
|
/**
|
|
|
* 触发一次抓取。
|
|
|
@@ -46,14 +42,46 @@ public class MaterialPriceScraperClient {
|
|
|
* @return 抓取服务响应;网络错误、HTTP 非 200、内容无法解析时抛 IllegalStateException
|
|
|
*/
|
|
|
public MaterialPriceScrapeResponse scrape(LocalDate date) {
|
|
|
- if (!enabled) {
|
|
|
- throw new IllegalStateException("material-price.scraper.enabled=false");
|
|
|
- }
|
|
|
-
|
|
|
MaterialPriceScrapeRequest request = new MaterialPriceScrapeRequest();
|
|
|
request.setDate(date == null ? null : date.toString());
|
|
|
// Python 侧整批超时略小于 Java 读超时,保证 Java 先收到结构化响应而不是先超时
|
|
|
request.setTimeoutSeconds(Math.max(60, timeoutMs / 1000));
|
|
|
+ return execute(request, timeoutMs, "single");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 触发一次历史区间抓取(区间采集专用)。
|
|
|
+ *
|
|
|
+ * @param start 区间开始日
|
|
|
+ * @param end 区间结束日
|
|
|
+ * @param scraperKeys 只抓指定抓取器;区间采集按站点分批调用
|
|
|
+ * @param readTimeoutMs 本次调用的读超时,不同站点耗时差异很大,由调用方指定
|
|
|
+ * @param sliceIndex 分片下标(从 0 开始),不分片传 0
|
|
|
+ * @param sliceCount 分片总数,不分片传 1
|
|
|
+ */
|
|
|
+ public MaterialPriceScrapeResponse scrapeRange(LocalDate start, LocalDate end,
|
|
|
+ List<String> scraperKeys,
|
|
|
+ int readTimeoutMs,
|
|
|
+ int sliceIndex, int sliceCount) {
|
|
|
+ MaterialPriceScrapeRequest request = new MaterialPriceScrapeRequest();
|
|
|
+ // Python 侧约定:date 是区间结束日,startDate 是区间开始日
|
|
|
+ request.setDate(end == null ? null : end.toString());
|
|
|
+ request.setStartDate(start == null ? null : start.toString());
|
|
|
+ request.setScraperKeys(scraperKeys);
|
|
|
+ // 废废网按「指数 id 取模」分片,Python 只抓本片指数,避免单次响应过大
|
|
|
+ request.setSliceIndex(sliceIndex);
|
|
|
+ request.setSliceCount(sliceCount);
|
|
|
+ // Python 侧整批超时上限 3600s(废废网分片后每片约 200 个指数),
|
|
|
+ // 且必须小于 Java 读超时,留 10s 余量
|
|
|
+ request.setTimeoutSeconds(Math.max(60, Math.min(3600, readTimeoutMs / 1000 - 10)));
|
|
|
+ return execute(request, readTimeoutMs, "range");
|
|
|
+ }
|
|
|
+
|
|
|
+ private MaterialPriceScrapeResponse execute(MaterialPriceScrapeRequest request,
|
|
|
+ int readTimeoutMs, String scene) {
|
|
|
+ long begin = System.currentTimeMillis();
|
|
|
+ log.info("调用抓取服务 scene={} date={} startDate={} keys={} timeoutMs={}",
|
|
|
+ scene, request.getDate(), request.getStartDate(), request.getScraperKeys(), readTimeoutMs);
|
|
|
|
|
|
String url = StrUtil.removeSuffix(baseUrl, "/") + "/api/v1/scrape";
|
|
|
HttpResponse response;
|
|
|
@@ -63,13 +91,17 @@ public class MaterialPriceScraperClient {
|
|
|
.header("Content-Type", "application/json")
|
|
|
.body(JSON.toJSONString(request), "application/json")
|
|
|
.setConnectionTimeout(5000)
|
|
|
- .setReadTimeout(timeoutMs)
|
|
|
+ .setReadTimeout(readTimeoutMs)
|
|
|
.execute();
|
|
|
} catch (Exception e) {
|
|
|
+ log.error("调用抓取服务失败 scene={} elapsedMs={}", scene, System.currentTimeMillis() - begin, e);
|
|
|
throw new IllegalStateException("调用抓取服务失败: " + e.getMessage(), e);
|
|
|
}
|
|
|
|
|
|
+ long elapsed = System.currentTimeMillis() - begin;
|
|
|
if (!response.isOk()) {
|
|
|
+ log.error("抓取服务返回异常 scene={} status={} elapsedMs={} body={}",
|
|
|
+ scene, response.getStatus(), elapsed, StrUtil.maxLength(response.body(), 500));
|
|
|
throw new IllegalStateException("抓取服务返回 HTTP " + response.getStatus());
|
|
|
}
|
|
|
try {
|
|
|
@@ -77,10 +109,13 @@ public class MaterialPriceScraperClient {
|
|
|
if (result == null) {
|
|
|
throw new IllegalStateException("抓取服务返回空内容");
|
|
|
}
|
|
|
+ log.info("抓取服务返回 scene={} total={} partial={} elapsedMs={}",
|
|
|
+ scene, result.getTotal(), result.getPartial(), elapsed);
|
|
|
return result;
|
|
|
} catch (IllegalStateException e) {
|
|
|
throw e;
|
|
|
} catch (Exception e) {
|
|
|
+ log.error("抓取服务返回内容无法解析 scene={} elapsedMs={}", scene, elapsed, e);
|
|
|
throw new IllegalStateException("抓取服务返回内容无法解析", e);
|
|
|
}
|
|
|
}
|