feat: add read-only lingxing connection test
This commit is contained in:
@@ -38,6 +38,16 @@ firstleg:
|
||||
lingxing:
|
||||
# 安全默认关闭;开启后自动任务才允许调用领星新品维护接口。
|
||||
auto-sync-enabled: ${LINGXING_AUTO_SYNC_ENABLED:false}
|
||||
base-url: ${LINGXING_DIRECT_BASE_URL:https://wms.xlwms.com}
|
||||
login-origin: ${LINGXING_LOGIN_ORIGIN:https://oms.xlwms.com}
|
||||
provider-key: ${LINGXING_PROVIDER_KEY:LINGXING_01}
|
||||
browser-cdp-endpoint: ${LINGXING_BROWSER_CDP_ENDPOINT:}
|
||||
browser-session-required: ${LINGXING_BROWSER_SESSION_REQUIRED:true}
|
||||
token-ttl-seconds: ${LINGXING_TOKEN_TTL_SECONDS:172800}
|
||||
token-expiry-safety-seconds: ${LINGXING_TOKEN_EXPIRY_SAFETY_SECONDS:300}
|
||||
track-key-v2-secret-base64: ${LINGXING_TRACK_KEY_V2_SECRET_BASE64:}
|
||||
request-timeout: ${LINGXING_REQUEST_TIMEOUT:30s}
|
||||
warehouses: [US01, US02]
|
||||
inbound:
|
||||
allow-rejected-measurement: false
|
||||
default-pallet-capacity-cbm: ${DEFAULT_PALLET_CAPACITY_CBM:1.200000}
|
||||
|
||||
@@ -24,6 +24,16 @@ baiyue.carrier-schedule.scheduler.lease-duration: ${CARRIER_SCHEDULE_LEASE_DURAT
|
||||
firstleg:
|
||||
lingxing:
|
||||
auto-sync-enabled: ${LINGXING_AUTO_SYNC_ENABLED:false}
|
||||
base-url: ${LINGXING_DIRECT_BASE_URL:https://wms.xlwms.com}
|
||||
login-origin: ${LINGXING_LOGIN_ORIGIN:https://oms.xlwms.com}
|
||||
provider-key: ${LINGXING_PROVIDER_KEY:LINGXING_01}
|
||||
browser-cdp-endpoint: ${LINGXING_BROWSER_CDP_ENDPOINT:}
|
||||
browser-session-required: ${LINGXING_BROWSER_SESSION_REQUIRED:true}
|
||||
token-ttl-seconds: ${LINGXING_TOKEN_TTL_SECONDS:172800}
|
||||
token-expiry-safety-seconds: ${LINGXING_TOKEN_EXPIRY_SAFETY_SECONDS:300}
|
||||
track-key-v2-secret-base64: ${LINGXING_TRACK_KEY_V2_SECRET_BASE64:}
|
||||
request-timeout: ${LINGXING_REQUEST_TIMEOUT:30s}
|
||||
warehouses: [US01, US02]
|
||||
inbound:
|
||||
allow-rejected-measurement: false
|
||||
default-pallet-capacity-cbm: ${DEFAULT_PALLET_CAPACITY_CBM:1.200000}
|
||||
|
||||
@@ -6,6 +6,10 @@ import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** 领星新品自动同步策略;默认关闭,开关由 Nacos 注入。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -15,4 +19,14 @@ import org.springframework.stereotype.Component;
|
||||
public class LingxingSyncProperties {
|
||||
/** 安全默认值:墨家数据只入库,必须人工点击推送。 */
|
||||
private boolean autoSyncEnabled = false;
|
||||
private String baseUrl = "https://wms.xlwms.com";
|
||||
private String loginOrigin = "https://oms.xlwms.com";
|
||||
private String providerKey = "LINGXING_01";
|
||||
private String browserCdpEndpoint;
|
||||
private boolean browserSessionRequired = true;
|
||||
private long tokenTtlSeconds = 172800;
|
||||
private long tokenExpirySafetySeconds = 300;
|
||||
private String trackKeyV2SecretBase64;
|
||||
private Duration requestTimeout = Duration.ofSeconds(30);
|
||||
private List<String> warehouses = new ArrayList<>(List.of("US01", "US02"));
|
||||
}
|
||||
|
||||
+12
@@ -3,6 +3,8 @@ package com.baiyue.firstleg.controller;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.firstleg.dto.MojiaSkuSyncDtos;
|
||||
import com.baiyue.firstleg.dto.LingxingTestDtos;
|
||||
import com.baiyue.firstleg.lingxing.LingxingDirectClient;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.MojiaSkuSyncService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -21,6 +23,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
@RequestMapping("/api/platform/v1/firstleg/mojia-sku-sync")
|
||||
public class PlatformMojiaSkuSyncController {
|
||||
@Resource private MojiaSkuSyncService service;
|
||||
@Resource private LingxingDirectClient lingxing;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:mojia-sku-sync:list')")
|
||||
@@ -43,6 +46,15 @@ public class PlatformMojiaSkuSyncController {
|
||||
return ApiResponse.success(service.config());
|
||||
}
|
||||
|
||||
@PostMapping("/test-connection")
|
||||
@PreAuthorize("hasAuthority('platform:mojia-sku-sync:list')")
|
||||
@Operation(summary = "测试领星连接并查询 SKU")
|
||||
public ApiResponse<LingxingTestDtos.Response> testConnection(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody LingxingTestDtos.Request request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(lingxing.test(request.skuCode()));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('platform:mojia-sku-sync:edit')")
|
||||
@Operation(summary = "编辑墨家一件代发SKU测量数据")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public final class LingxingTestDtos {
|
||||
private LingxingTestDtos() {}
|
||||
|
||||
public record Request(@NotBlank @Size(max = 128) String skuCode) {}
|
||||
public record WarehouseResult(String whCode, boolean reachable, boolean matched,
|
||||
boolean maintained, String productSku, String barcode,
|
||||
BigDecimal length, BigDecimal width, BigDecimal height,
|
||||
BigDecimal weight, String message) {}
|
||||
public record Response(String skuCode, List<WarehouseResult> warehouses) {}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.baiyue.firstleg.lingxing;
|
||||
|
||||
import com.baiyue.firstleg.config.LingxingSyncProperties;
|
||||
import com.baiyue.firstleg.dto.LingxingTestDtos;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/** 领星新品查询只读客户端。不会调用 /measure。 */
|
||||
@Component
|
||||
public class LingxingDirectClient {
|
||||
private final ObjectMapper mapper;
|
||||
private final LingxingAuthManager authManager;
|
||||
private final LingxingSyncProperties properties;
|
||||
private final HttpClient http;
|
||||
|
||||
@Autowired
|
||||
public LingxingDirectClient(ObjectMapper mapper, LingxingAuthManager authManager,
|
||||
LingxingSyncProperties properties) {
|
||||
this.mapper = mapper; this.authManager = authManager; this.properties = properties;
|
||||
this.http = HttpClient.newBuilder().connectTimeout(properties.getRequestTimeout()).build();
|
||||
}
|
||||
|
||||
public LingxingTestDtos.Response test(String skuCode) {
|
||||
String sku = skuCode == null ? "" : skuCode.trim();
|
||||
if (sku.isBlank()) throw new IllegalArgumentException("SKU不能为空");
|
||||
LingxingAuthState auth = authManager.getAuthState(new LingxingProviderConfig(
|
||||
properties.getProviderKey(), properties.getBrowserCdpEndpoint(), properties.isBrowserSessionRequired(),
|
||||
properties.getTokenTtlSeconds(), properties.getTokenExpirySafetySeconds()), properties.getLoginOrigin()).join();
|
||||
List<LingxingTestDtos.WarehouseResult> results = new ArrayList<>();
|
||||
for (String whCode : properties.getWarehouses()) results.add(query(auth, sku, whCode));
|
||||
return new LingxingTestDtos.Response(sku, results);
|
||||
}
|
||||
|
||||
private LingxingTestDtos.WarehouseResult query(LingxingAuthState auth, String sku, String whCode) {
|
||||
try {
|
||||
ObjectNode body = mapper.createObjectNode().put("barcode", sku).put("current", 1)
|
||||
.put("customerCode", "").put("keyword", sku).put("multiKeyword", "")
|
||||
.put("searchType", "barcode").put("size", 20).put("whCode", whCode);
|
||||
String json = canonical(body);
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(strip(properties.getBaseUrl()) + "/gateway/wms/newProduct/page"))
|
||||
.timeout(properties.getRequestTimeout()).header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + auth.token()).header("Cookie", auth.cookies())
|
||||
.header("Origin", properties.getLoginOrigin()).header("Referer", strip(properties.getLoginOrigin()) + "/")
|
||||
.header("lang", "zh").header("version", "prod")
|
||||
.header("Track-Key", trackKey("POST", json)).POST(HttpRequest.BodyPublishers.ofString(json)).build();
|
||||
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
JsonNode root = mapper.readTree(response.body());
|
||||
if (response.statusCode() == 401) {
|
||||
authManager.invalidate(properties.getProviderKey(), "lingxing_test_unauthorized");
|
||||
return new LingxingTestDtos.WarehouseResult(whCode, false, false, false, null, null, null, null, null, null, "领星登录态已失效,请重新登录");
|
||||
}
|
||||
if (root.path("code").asInt(-1) != 200) return empty(whCode, false, "领星返回业务失败:" + root.path("msg").asText("未知错误"));
|
||||
JsonNode records = root.path("data").path("records");
|
||||
for (JsonNode record : records) {
|
||||
if (sku.equals(record.path("productSku").asText()) || sku.equals(record.path("barcode").asText())) {
|
||||
return new LingxingTestDtos.WarehouseResult(whCode, true, true, record.path("maintained").asInt(0) == 1,
|
||||
text(record,"productSku"), text(record,"barcode"), decimal(record,"wmsLength"), decimal(record,"wmsWidth"),
|
||||
decimal(record,"wmsHeight"), decimal(record,"wmsWeight"), "查询成功");
|
||||
}
|
||||
}
|
||||
return empty(whCode, true, "未匹配到领星 SKU");
|
||||
} catch (Exception ex) {
|
||||
return empty(whCode, false, "查询领星失败:" + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private LingxingTestDtos.WarehouseResult empty(String whCode, boolean reachable, String message) {
|
||||
return new LingxingTestDtos.WarehouseResult(whCode, reachable, false, false, null, null, null, null, null, null, message);
|
||||
}
|
||||
private static String text(JsonNode node, String key) { return node.path(key).isMissingNode() ? null : node.path(key).asText(null); }
|
||||
private static java.math.BigDecimal decimal(JsonNode node, String key) { return node.path(key).isNumber() ? node.path(key).decimalValue() : null; }
|
||||
private String trackKey(String method, String json) {
|
||||
if (properties.getTrackKeyV2SecretBase64() == null || properties.getTrackKeyV2SecretBase64().isBlank()) throw new IllegalStateException("未配置领星 Track-Key v2 密钥");
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256").digest(json.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hex = new StringBuilder(); for (byte b : digest) hex.append(String.format(Locale.ROOT, "%02x", b));
|
||||
Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(Base64.getDecoder().decode(properties.getTrackKeyV2SecretBase64()), "HmacSHA256"));
|
||||
return "v2:" + Base64.getEncoder().encodeToString(mac.doFinal((method.toUpperCase(Locale.ROOT) + hex).getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (Exception ex) { throw new IllegalStateException("领星 Track-Key 生成失败", ex); }
|
||||
}
|
||||
private String canonical(JsonNode node) throws Exception { return mapper.writeValueAsString(sort(node)); }
|
||||
private JsonNode sort(JsonNode node) {
|
||||
if (!node.isObject()) return node;
|
||||
ObjectNode out = mapper.createObjectNode(); Map<String, JsonNode> ordered = new TreeMap<>(); node.fields().forEachRemaining(e -> ordered.put(e.getKey(), e.getValue()));
|
||||
ordered.forEach((key, value) -> out.set(key, sort(value))); return out;
|
||||
}
|
||||
private static String strip(String value) { return value.endsWith("/") ? value.substring(0, value.length() - 1) : value; }
|
||||
}
|
||||
Reference in New Issue
Block a user