优化
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|---|---|
|
||||
| 链路上下文 | `src/main/java/com/baiyue/common/web/CorrelationContext.java` |
|
||||
| 请求 ID 过滤器 | `src/main/java/com/baiyue/common/web/CorrelationIdFilter.java` |
|
||||
| W3C Trace Context | `src/main/java/com/baiyue/common/web/TraceParent.java`;严格接收 version 00,响应 `traceparent`、`X-Trace-Id` 与 MDC `traceId` 使用同一 trace ID |
|
||||
| 自动配置入口 | `src/main/java/com/baiyue/common/web/CommonWebAutoConfiguration.java`;为只扫描自身包的服务注册异常处理器和 Correlation 过滤器 |
|
||||
| 全局异常映射与业务失败指标 | `src/main/java/com/baiyue/common/web/GlobalExceptionHandler.java`;指标 `baiyue.api.business.failures` 按有限 `code` 标记 |
|
||||
| 过滤器测试 | `src/test/java/com/baiyue/common/web/CorrelationIdFilterTest.java` |
|
||||
@@ -23,5 +24,6 @@
|
||||
- 网关/Servlet Security 的真实 401/403、协议或基础设施错误、Warehouse readiness 503、Audit 202 与 Event Bridge 内部鉴权仍保留真实 HTTP 状态;完整边界见 `../docs/统一响应约定改造方案-HTTP200.md`。
|
||||
- 修改自动配置时必须验证 `audit-service`、`event-bridge-service` 等只扫描自身包的应用仍能加载 `GlobalExceptionHandler` 和 `CorrelationIdFilter`。
|
||||
- 修改 Correlation ID 时同步检查当前请求头契约、线程上下文清理和日志链路。
|
||||
- 入站 Trace 优先级固定为:合法单值 `traceparent` > 完全缺失 `traceparent` 时的 32 位非零十六进制 `X-Trace-Id` > 新 trace。只要 `traceparent` 已出现但重复、超长、格式非法或含全零 ID,就必须生成新 trace,不得回退信任 `X-Trace-Id`;请求 ID 独立按 `X-Request-Id` 校验或生成。
|
||||
- 从父目录运行 `mvn -pl common-web -am test`。
|
||||
- 所有对外 `Long/long` 值统一序列化为 JSON 字符串,避免 19 位雪花 ID 在浏览器中丢失精度;禁止业务模块重复添加局部转换器。
|
||||
|
||||
@@ -9,8 +9,11 @@ import org.slf4j.MDC;
|
||||
public final class CorrelationContext {
|
||||
public static final String REQUEST_ID = "requestId";
|
||||
public static final String TRACE_ID = "traceId";
|
||||
public static final String SPAN_ID = "spanId";
|
||||
public static final String TRACE_PARENT = "traceparent";
|
||||
public static final String REQUEST_ID_HEADER = "X-Request-Id";
|
||||
public static final String TRACE_ID_HEADER = "X-Trace-Id";
|
||||
public static final String TRACE_PARENT_HEADER = TraceParent.HEADER;
|
||||
|
||||
private CorrelationContext() {}
|
||||
|
||||
@@ -22,6 +25,14 @@ public final class CorrelationContext {
|
||||
return value(request, TRACE_ID);
|
||||
}
|
||||
|
||||
public static String spanId(HttpServletRequest request) {
|
||||
return value(request, SPAN_ID);
|
||||
}
|
||||
|
||||
public static String traceParent(HttpServletRequest request) {
|
||||
return value(request, TRACE_PARENT);
|
||||
}
|
||||
|
||||
private static String value(HttpServletRequest request, String key) {
|
||||
Object attribute = request.getAttribute(key);
|
||||
return attribute == null ? MDC.get(key) : attribute.toString();
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -25,24 +26,60 @@ public class CorrelationIdFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String requestId = acceptedOrGenerated(request.getHeader(CorrelationContext.REQUEST_ID_HEADER), false);
|
||||
String traceId = acceptedOrGenerated(request.getHeader(CorrelationContext.TRACE_ID_HEADER), true);
|
||||
String requestId = requestId(request);
|
||||
TraceParent traceParent = traceParent(request);
|
||||
String traceId = traceParent.traceId();
|
||||
request.setAttribute(CorrelationContext.REQUEST_ID, requestId);
|
||||
request.setAttribute(CorrelationContext.TRACE_ID, traceId);
|
||||
request.setAttribute(CorrelationContext.SPAN_ID, traceParent.parentId());
|
||||
request.setAttribute(CorrelationContext.TRACE_PARENT, traceParent.value());
|
||||
response.setHeader(CorrelationContext.REQUEST_ID_HEADER, requestId);
|
||||
response.setHeader(CorrelationContext.TRACE_ID_HEADER, traceId);
|
||||
response.setHeader(CorrelationContext.TRACE_PARENT_HEADER, traceParent.value());
|
||||
|
||||
try (MDC.MDCCloseable ignoredRequest = MDC.putCloseable(CorrelationContext.REQUEST_ID, requestId);
|
||||
MDC.MDCCloseable ignoredTrace = MDC.putCloseable(CorrelationContext.TRACE_ID, traceId)) {
|
||||
MDC.MDCCloseable ignoredTrace = MDC.putCloseable(CorrelationContext.TRACE_ID, traceId);
|
||||
MDC.MDCCloseable ignoredSpan = MDC.putCloseable(CorrelationContext.SPAN_ID, traceParent.parentId())) {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
private String acceptedOrGenerated(String candidate, boolean compact) {
|
||||
if (candidate != null && SAFE_ID.matcher(candidate).matches()) {
|
||||
return candidate;
|
||||
private String requestId(HttpServletRequest request) {
|
||||
HeaderValue header = singleHeader(request, CorrelationContext.REQUEST_ID_HEADER, 128);
|
||||
if (header.valid() && SAFE_ID.matcher(header.value()).matches()) {
|
||||
return header.value();
|
||||
}
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private TraceParent traceParent(HttpServletRequest request) {
|
||||
HeaderValue standard = singleHeader(request, CorrelationContext.TRACE_PARENT_HEADER,
|
||||
TraceParent.HEADER_LENGTH);
|
||||
if (standard.present()) {
|
||||
if (standard.valid()) {
|
||||
return TraceParent.parse(standard.value()).map(TraceParent::childOf)
|
||||
.orElseGet(() -> TraceParent.newRoot(null));
|
||||
}
|
||||
return TraceParent.newRoot(null);
|
||||
}
|
||||
|
||||
HeaderValue legacy = singleHeader(request, CorrelationContext.TRACE_ID_HEADER, 32);
|
||||
return TraceParent.newRoot(legacy.valid() ? legacy.value() : null);
|
||||
}
|
||||
|
||||
private HeaderValue singleHeader(HttpServletRequest request, String name, int maxLength) {
|
||||
Enumeration<String> values = request.getHeaders(name);
|
||||
if (values == null || !values.hasMoreElements()) {
|
||||
return HeaderValue.missing();
|
||||
}
|
||||
String value = values.nextElement();
|
||||
boolean duplicate = values.hasMoreElements();
|
||||
return new HeaderValue(true, !duplicate && value != null && value.length() <= maxLength, value);
|
||||
}
|
||||
|
||||
private record HeaderValue(boolean present, boolean valid, String value) {
|
||||
private static HeaderValue missing() {
|
||||
return new HeaderValue(false, false, null);
|
||||
}
|
||||
String id = UUID.randomUUID().toString();
|
||||
return compact ? id.replace("-", "") : id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Strict W3C {@code traceparent} version 00 value.
|
||||
*
|
||||
* <p>This type deliberately accepts only the currently frozen version-00 shape. Unsupported,
|
||||
* ambiguous or malformed input must start a new trace instead of being partially trusted.</p>
|
||||
*/
|
||||
public record TraceParent(String traceId, String parentId, String traceFlags) {
|
||||
public static final String HEADER = "traceparent";
|
||||
public static final int HEADER_LENGTH = 55;
|
||||
|
||||
private static final Pattern VERSION_00 = Pattern.compile(
|
||||
"00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})");
|
||||
private static final Pattern LEGACY_TRACE_ID = Pattern.compile("[0-9A-Fa-f]{32}");
|
||||
private static final String ZERO_TRACE_ID = "0".repeat(32);
|
||||
private static final String ZERO_PARENT_ID = "0".repeat(16);
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
private static final HexFormat HEX = HexFormat.of();
|
||||
|
||||
public TraceParent {
|
||||
if (!validTraceId(traceId) || !validParentId(parentId)
|
||||
|| traceFlags == null || !traceFlags.matches("[0-9a-f]{2}")) {
|
||||
throw new IllegalArgumentException("Invalid W3C traceparent components");
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses one exact, lowercase W3C version-00 header value. */
|
||||
public static Optional<TraceParent> parse(String value) {
|
||||
if (value == null || value.length() != HEADER_LENGTH) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Matcher matcher = VERSION_00.matcher(value);
|
||||
if (!matcher.matches() || !validTraceId(matcher.group(1)) || !validParentId(matcher.group(2))) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new TraceParent(matcher.group(1), matcher.group(2), matcher.group(3)));
|
||||
}
|
||||
|
||||
/** Creates the local inbound context for a valid remote parent. */
|
||||
public static TraceParent childOf(TraceParent remoteParent) {
|
||||
return new TraceParent(remoteParent.traceId(), randomNonZero(8), remoteParent.traceFlags());
|
||||
}
|
||||
|
||||
/** Creates a new root context, optionally reusing a valid legacy trace ID. */
|
||||
public static TraceParent newRoot(String legacyTraceId) {
|
||||
String traceId = validLegacyTraceId(legacyTraceId)
|
||||
? legacyTraceId.toLowerCase(java.util.Locale.ROOT) : randomNonZero(16);
|
||||
return new TraceParent(traceId, randomNonZero(8), "00");
|
||||
}
|
||||
|
||||
public static boolean validLegacyTraceId(String value) {
|
||||
return value != null && LEGACY_TRACE_ID.matcher(value).matches()
|
||||
&& !ZERO_TRACE_ID.equalsIgnoreCase(value);
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return "00-" + traceId + '-' + parentId + '-' + traceFlags;
|
||||
}
|
||||
|
||||
private static boolean validTraceId(String value) {
|
||||
return value != null && value.matches("[0-9a-f]{32}") && !ZERO_TRACE_ID.equals(value);
|
||||
}
|
||||
|
||||
private static boolean validParentId(String value) {
|
||||
return value != null && value.matches("[0-9a-f]{16}") && !ZERO_PARENT_ID.equals(value);
|
||||
}
|
||||
|
||||
private static String randomNonZero(int byteCount) {
|
||||
byte[] bytes = new byte[byteCount];
|
||||
String value;
|
||||
do {
|
||||
RANDOM.nextBytes(bytes);
|
||||
value = HEX.formatHex(bytes);
|
||||
} while (value.chars().allMatch(character -> character == '0'));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
@@ -16,6 +18,8 @@ class CommonWebAutoConfigurationTest {
|
||||
contextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(GlobalExceptionHandler.class);
|
||||
assertThat(context).hasSingleBean(CorrelationIdFilter.class);
|
||||
assertThat(CorrelationIdFilter.class.getAnnotation(Order.class).value())
|
||||
.isEqualTo(Ordered.HIGHEST_PRECEDENCE);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("请求链路标识过滤功能")
|
||||
@@ -13,27 +15,85 @@ class CorrelationIdFilterTest {
|
||||
private final CorrelationIdFilter filter = new CorrelationIdFilter();
|
||||
|
||||
@Test
|
||||
void preservesSafeIncomingIds() throws Exception {
|
||||
void validTraceParentWinsAndMapsToLegacyTraceIdAndMdc() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(CorrelationContext.REQUEST_ID_HEADER, "req-123");
|
||||
request.addHeader(CorrelationContext.TRACE_ID_HEADER, "trace-123");
|
||||
request.addHeader(CorrelationContext.TRACE_ID_HEADER, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
request.addHeader(CorrelationContext.TRACE_PARENT_HEADER,
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
CapturingFilterChain chain = new CapturingFilterChain();
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
assertThat(response.getHeader(CorrelationContext.REQUEST_ID_HEADER)).isEqualTo("req-123");
|
||||
assertThat(request.getAttribute(CorrelationContext.TRACE_ID)).isEqualTo("trace-123");
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_ID_HEADER))
|
||||
.isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736");
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_PARENT_HEADER))
|
||||
.matches("00-4bf92f3577b34da6a3ce929d0e0e4736-[0-9a-f]{16}-01");
|
||||
assertThat(chain.traceId).isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736");
|
||||
assertThat(chain.spanId).matches("[0-9a-f]{16}").isNotEqualTo("0000000000000000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacesUnsafeIncomingIds() throws Exception {
|
||||
void fallsBackToValidLegacyTraceIdOnlyWhenTraceParentIsMissing() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(CorrelationContext.REQUEST_ID_HEADER, "invalid value\n");
|
||||
request.addHeader(CorrelationContext.TRACE_ID_HEADER, "ABCDEF0123456789ABCDEF0123456789");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_ID_HEADER))
|
||||
.isEqualTo("abcdef0123456789abcdef0123456789");
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_PARENT_HEADER))
|
||||
.matches("00-abcdef0123456789abcdef0123456789-[0-9a-f]{16}-00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidTraceParentStartsNewTraceWithoutTrustingLegacyTraceId() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(CorrelationContext.TRACE_PARENT_HEADER,
|
||||
"00-00000000000000000000000000000000-00f067aa0ba902b7-01");
|
||||
request.addHeader(CorrelationContext.TRACE_ID_HEADER, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_ID_HEADER))
|
||||
.matches("[0-9a-f]{32}")
|
||||
.isNotEqualTo("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
.isNotEqualTo("00000000000000000000000000000000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateAndOversizedHeaders() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(CorrelationContext.REQUEST_ID_HEADER, "a".repeat(129));
|
||||
request.addHeader(CorrelationContext.TRACE_PARENT_HEADER,
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
|
||||
request.addHeader(CorrelationContext.TRACE_PARENT_HEADER,
|
||||
"00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-00");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
|
||||
assertThat(response.getHeader(CorrelationContext.REQUEST_ID_HEADER))
|
||||
.matches("[0-9a-f-]{36}");
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_ID_HEADER))
|
||||
.matches("[0-9a-f]{32}");
|
||||
.matches("[0-9a-f]{32}")
|
||||
.isNotEqualTo("4bf92f3577b34da6a3ce929d0e0e4736")
|
||||
.isNotEqualTo("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
}
|
||||
|
||||
private static final class CapturingFilterChain extends MockFilterChain {
|
||||
private String traceId;
|
||||
private String spanId;
|
||||
|
||||
@Override
|
||||
public void doFilter(jakarta.servlet.ServletRequest request,
|
||||
jakarta.servlet.ServletResponse response) throws IOException {
|
||||
traceId = org.slf4j.MDC.get(CorrelationContext.TRACE_ID);
|
||||
spanId = org.slf4j.MDC.get(CorrelationContext.SPAN_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TraceParentTest {
|
||||
|
||||
@Test
|
||||
void parsesCanonicalVersion00AndCreatesLocalChild() {
|
||||
TraceParent remote = TraceParent.parse(
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").orElseThrow();
|
||||
|
||||
TraceParent local = TraceParent.childOf(remote);
|
||||
|
||||
assertThat(local.traceId()).isEqualTo(remote.traceId());
|
||||
assertThat(local.parentId()).matches("[0-9a-f]{16}")
|
||||
.isNotEqualTo(remote.parentId())
|
||||
.isNotEqualTo("0000000000000000");
|
||||
assertThat(local.traceFlags()).isEqualTo("01");
|
||||
assertThat(local.value()).hasSize(TraceParent.HEADER_LENGTH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonCanonicalUnsupportedAndZeroIdentifiers() {
|
||||
assertThat(TraceParent.parse(
|
||||
"00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01")).isEmpty();
|
||||
assertThat(TraceParent.parse(
|
||||
"01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")).isEmpty();
|
||||
assertThat(TraceParent.parse(
|
||||
"00-00000000000000000000000000000000-00f067aa0ba902b7-01")).isEmpty();
|
||||
assertThat(TraceParent.parse(
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01")).isEmpty();
|
||||
assertThat(TraceParent.parse("x".repeat(10_000))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsCanonicalNonZeroRootAndNormalizesLegacyHex() {
|
||||
TraceParent root = TraceParent.newRoot("ABCDEF0123456789ABCDEF0123456789");
|
||||
|
||||
assertThat(root.traceId()).isEqualTo("abcdef0123456789abcdef0123456789");
|
||||
assertThat(root.parentId()).matches("[0-9a-f]{16}").isNotEqualTo("0000000000000000");
|
||||
assertThat(root.value()).matches(
|
||||
"00-abcdef0123456789abcdef0123456789-[0-9a-f]{16}-00");
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,20 @@ lastleg:
|
||||
smart-dispatch-api:
|
||||
enabled: true
|
||||
baiyue:
|
||||
http:
|
||||
clients:
|
||||
unload-photo-file:
|
||||
connect-timeout: ${LASTLEG_UNLOAD_PHOTO_FILE_CONNECT_TIMEOUT:3s}
|
||||
read-timeout: ${LASTLEG_UNLOAD_PHOTO_FILE_READ_TIMEOUT:30s}
|
||||
relabel-photo-file:
|
||||
connect-timeout: ${LASTLEG_RELABEL_PHOTO_FILE_CONNECT_TIMEOUT:3s}
|
||||
read-timeout: ${LASTLEG_RELABEL_PHOTO_FILE_READ_TIMEOUT:30s}
|
||||
warehouse-resource:
|
||||
connect-timeout: ${LASTLEG_WAREHOUSE_RESOURCE_CONNECT_TIMEOUT:3s}
|
||||
read-timeout: ${LASTLEG_WAREHOUSE_RESOURCE_READ_TIMEOUT:5s}
|
||||
pallet-physical:
|
||||
connect-timeout: ${LASTLEG_PALLET_PHYSICAL_CONNECT_TIMEOUT:3s}
|
||||
read-timeout: ${LASTLEG_PALLET_PHYSICAL_READ_TIMEOUT:5s}
|
||||
clients:
|
||||
domestic-warehouse:
|
||||
url: ${DOMESTIC_WAREHOUSE_URL}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const moduleRoot = path.resolve(import.meta.dirname, '..');
|
||||
const sourceRoots = {
|
||||
main: path.join(moduleRoot, 'src', 'main', 'java'),
|
||||
test: path.join(moduleRoot, 'src', 'test', 'java'),
|
||||
};
|
||||
const internalPrefix = 'com.baiyue.lastleg.';
|
||||
|
||||
const sliceRules = [
|
||||
['inbound-pda', /(?:^|\.)(?:inboundplan|palletops|pda)(?:\.|$)/],
|
||||
['inventory', /(?:^|\.)inventory(?:\.|$)/],
|
||||
['dispatch-delivery', /(?:^|\.)(?:dispatch|delivery|outbound)(?:\.|$)/],
|
||||
['relabel-transfer-discard', /(?:^|\.)(?:relabel|transfer|discard)(?:\.|$)/],
|
||||
['dropship', /(?:^|\.)dropship(?:\.|$)/],
|
||||
['procurement-trucking-looseboard', /(?:^|\.)(?:procurement|trucking|looseboard)(?:\.|$)/],
|
||||
['overseas-master-data', /(?:^|\.)masterdata(?:\.|$)/],
|
||||
['tailleg-handoff', /(?:^|\.)(?:controller|dto|entity|mapper|model|repository|service|shipping)(?:\.|$)/],
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = { sourceSet: 'main', format: 'json', output: null, check: null };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === '--source-set') options.sourceSet = argv[++index];
|
||||
else if (argument === '--format') options.format = argv[++index];
|
||||
else if (argument === '--output') options.output = argv[++index];
|
||||
else if (argument === '--check') options.check = argv[++index];
|
||||
else if (argument === '--help') {
|
||||
process.stdout.write('Usage: node scripts/analyze-lastleg-dependencies.mjs [--source-set main|test|all] [--format json|markdown|dot] [--output FILE] [--check CONTRACT.json]\n');
|
||||
process.exit(0);
|
||||
} else throw new Error(`Unknown argument: ${argument}`);
|
||||
}
|
||||
if (!['main', 'test', 'all'].includes(options.sourceSet)) throw new Error(`Invalid source set: ${options.sourceSet}`);
|
||||
if (!['json', 'markdown', 'dot'].includes(options.format)) throw new Error(`Invalid format: ${options.format}`);
|
||||
return options;
|
||||
}
|
||||
|
||||
function walkJavaFiles(root) {
|
||||
if (!fs.existsSync(root)) return [];
|
||||
const result = [];
|
||||
const pending = [root];
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const absolute = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) pending.push(absolute);
|
||||
else if (entry.isFile() && entry.name.endsWith('.java')) result.push(absolute);
|
||||
}
|
||||
}
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
function stripCommentsAndStrings(source) {
|
||||
return source
|
||||
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
||||
.replace(/\/\/[^\r\n]*/g, ' ')
|
||||
.replace(/"(?:\\.|[^"\\])*"/g, '""')
|
||||
.replace(/'(?:\\.|[^'\\])*'/g, "''");
|
||||
}
|
||||
|
||||
function classifySlice(fqcn) {
|
||||
for (const [slice, expression] of sliceRules) {
|
||||
if (expression.test(fqcn)) return slice;
|
||||
}
|
||||
return 'shared-technical';
|
||||
}
|
||||
|
||||
function classifyKind(fqcn) {
|
||||
const packageName = fqcn.slice(0, fqcn.lastIndexOf('.'));
|
||||
if (/(?:^|\.)(?:client|gateway)(?:\.|$)/.test(packageName)) return 'cross-service-adapter';
|
||||
if (/(?:^|\.)(?:adapter|controller|repository|mapper|config|security)(?:\.|$)/.test(packageName)) return 'technical-adapter';
|
||||
if (/(?:^|\.)(?:entity|dto|model)(?:\.|$)/.test(packageName)) return 'data-model';
|
||||
return 'business-application';
|
||||
}
|
||||
|
||||
function parseSource(file, sourceSet) {
|
||||
const raw = fs.readFileSync(file, 'utf8');
|
||||
const source = stripCommentsAndStrings(raw);
|
||||
const packageName = source.match(/\bpackage\s+([\w.]+)\s*;/)?.[1];
|
||||
const typeName = path.basename(file, '.java');
|
||||
if (!packageName) throw new Error(`Missing package declaration: ${file}`);
|
||||
const fqcn = `${packageName}.${typeName}`;
|
||||
const imports = [...source.matchAll(/\bimport\s+(?:static\s+)?([\w.]+)(?:\.\*)?\s*;/g)].map(match => match[1]);
|
||||
return {
|
||||
file: path.relative(moduleRoot, file).replaceAll(path.sep, '/'),
|
||||
sourceSet,
|
||||
packageName,
|
||||
typeName,
|
||||
fqcn,
|
||||
imports,
|
||||
source,
|
||||
slice: classifySlice(fqcn),
|
||||
kind: classifyKind(fqcn),
|
||||
};
|
||||
}
|
||||
|
||||
function collectSources(sourceSet) {
|
||||
const selected = sourceSet === 'all' ? ['main', 'test'] : [sourceSet];
|
||||
return selected.flatMap(set => walkJavaFiles(sourceRoots[set]).map(file => parseSource(file, set)));
|
||||
}
|
||||
|
||||
function resolveDependencies(sources) {
|
||||
const knownTypes = new Map(sources.map(source => [source.fqcn, source]));
|
||||
const typesByPackage = new Map();
|
||||
for (const source of sources) {
|
||||
if (!typesByPackage.has(source.packageName)) typesByPackage.set(source.packageName, []);
|
||||
typesByPackage.get(source.packageName).push(source);
|
||||
}
|
||||
const dependencies = [];
|
||||
const seen = new Set();
|
||||
for (const source of sources) {
|
||||
const candidates = new Set();
|
||||
for (const imported of source.imports) {
|
||||
if (knownTypes.has(imported)) candidates.add(imported);
|
||||
else if (imported.startsWith(internalPrefix)) {
|
||||
const owner = [...knownTypes.keys()].find(type => imported.startsWith(`${type}.`));
|
||||
if (owner) candidates.add(owner);
|
||||
}
|
||||
}
|
||||
for (const samePackageType of typesByPackage.get(source.packageName) ?? []) {
|
||||
if (samePackageType.fqcn !== source.fqcn && new RegExp(`\\b${samePackageType.typeName}\\b`).test(source.source)) {
|
||||
candidates.add(samePackageType.fqcn);
|
||||
}
|
||||
}
|
||||
for (const targetFqcn of candidates) {
|
||||
if (targetFqcn === source.fqcn) continue;
|
||||
const key = `${source.fqcn}->${targetFqcn}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
dependencies.push({ source: source.fqcn, target: targetFqcn });
|
||||
}
|
||||
}
|
||||
return dependencies.sort((a, b) => `${a.source}\0${a.target}`.localeCompare(`${b.source}\0${b.target}`));
|
||||
}
|
||||
|
||||
function aggregateEdges(dependencies, nodeOf) {
|
||||
const counts = new Map();
|
||||
for (const dependency of dependencies) {
|
||||
const source = nodeOf(dependency.source);
|
||||
const target = nodeOf(dependency.target);
|
||||
if (source === target) continue;
|
||||
const key = `${source}\0${target}`;
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1);
|
||||
}
|
||||
return [...counts].map(([key, count]) => {
|
||||
const [source, target] = key.split('\0');
|
||||
return { source, target, count };
|
||||
}).sort((a, b) => `${a.source}\0${a.target}`.localeCompare(`${b.source}\0${b.target}`));
|
||||
}
|
||||
|
||||
function stronglyConnectedComponents(nodes, edges) {
|
||||
const adjacency = new Map(nodes.map(node => [node, []]));
|
||||
for (const edge of edges) adjacency.get(edge.source)?.push(edge.target);
|
||||
const indexByNode = new Map();
|
||||
const lowLink = new Map();
|
||||
const stack = [];
|
||||
const onStack = new Set();
|
||||
const components = [];
|
||||
let nextIndex = 0;
|
||||
function visit(node) {
|
||||
indexByNode.set(node, nextIndex);
|
||||
lowLink.set(node, nextIndex);
|
||||
nextIndex += 1;
|
||||
stack.push(node);
|
||||
onStack.add(node);
|
||||
for (const target of adjacency.get(node) ?? []) {
|
||||
if (!indexByNode.has(target)) {
|
||||
visit(target);
|
||||
lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(target)));
|
||||
} else if (onStack.has(target)) {
|
||||
lowLink.set(node, Math.min(lowLink.get(node), indexByNode.get(target)));
|
||||
}
|
||||
}
|
||||
if (lowLink.get(node) === indexByNode.get(node)) {
|
||||
const component = [];
|
||||
let current;
|
||||
do {
|
||||
current = stack.pop();
|
||||
onStack.delete(current);
|
||||
component.push(current);
|
||||
} while (current !== node);
|
||||
components.push(component.sort());
|
||||
}
|
||||
}
|
||||
for (const node of [...nodes].sort()) if (!indexByNode.has(node)) visit(node);
|
||||
return components.filter(component => component.length > 1).sort((a, b) => b.length - a.length || a[0].localeCompare(b[0]));
|
||||
}
|
||||
|
||||
function buildReport(sourceSet) {
|
||||
const sources = collectSources(sourceSet);
|
||||
const sourceByFqcn = new Map(sources.map(source => [source.fqcn, source]));
|
||||
const dependencies = resolveDependencies(sources);
|
||||
const packages = [...new Set(sources.map(source => source.packageName))].sort();
|
||||
const slices = [...new Set(sources.map(source => source.slice))].sort();
|
||||
const packageEdges = aggregateEdges(dependencies, fqcn => sourceByFqcn.get(fqcn).packageName);
|
||||
const sliceEdges = aggregateEdges(dependencies, fqcn => sourceByFqcn.get(fqcn).slice);
|
||||
const directCrossSliceDependencies = dependencies.filter(edge => sourceByFqcn.get(edge.source).slice !== sourceByFqcn.get(edge.target).slice);
|
||||
const modelLeaks = directCrossSliceDependencies.filter(edge => sourceByFqcn.get(edge.target).kind === 'data-model');
|
||||
const mapperLeaks = directCrossSliceDependencies.filter(edge => /\.mapper(?:\.|$)/.test(sourceByFqcn.get(edge.target).packageName));
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
generatedFrom: sourceSet,
|
||||
moduleRoot: 'lastleg-service',
|
||||
summary: {
|
||||
javaFiles: sources.length,
|
||||
internalClassDependencies: dependencies.length,
|
||||
packages: packages.length,
|
||||
slices: slices.length,
|
||||
crossSliceClassDependencies: directCrossSliceDependencies.length,
|
||||
crossSliceModelLeaks: modelLeaks.length,
|
||||
crossSliceMapperLeaks: mapperLeaks.length,
|
||||
},
|
||||
sliceDefinitions: Object.fromEntries(sliceRules.map(([name, expression]) => [name, expression.source]).concat([['shared-technical', 'fallback']])),
|
||||
classes: sources.map(({ source, ...entry }) => entry),
|
||||
classDependencies: dependencies,
|
||||
packageEdges,
|
||||
sliceEdges,
|
||||
packageSccs: stronglyConnectedComponents(packages, packageEdges),
|
||||
sliceSccs: stronglyConnectedComponents(slices, sliceEdges),
|
||||
directCrossSliceDependencies,
|
||||
modelLeaks,
|
||||
mapperLeaks,
|
||||
};
|
||||
}
|
||||
|
||||
function renderMarkdown(report) {
|
||||
const leaks = [...report.modelLeaks, ...report.mapperLeaks]
|
||||
.filter((edge, index, entries) => entries.findIndex(candidate => candidate.source === edge.source && candidate.target === edge.target) === index);
|
||||
return [
|
||||
'# Lastleg internal dependency report', '',
|
||||
`- Source set: ${report.generatedFrom}`,
|
||||
`- Java files: ${report.summary.javaFiles}`,
|
||||
`- Internal class dependencies: ${report.summary.internalClassDependencies}`,
|
||||
`- Packages: ${report.summary.packages}`,
|
||||
`- Cross-slice class dependencies: ${report.summary.crossSliceClassDependencies}`,
|
||||
`- Cross-slice model leaks: ${report.summary.crossSliceModelLeaks}`,
|
||||
`- Cross-slice mapper leaks: ${report.summary.crossSliceMapperLeaks}`,
|
||||
`- Package SCCs (>1 node): ${report.packageSccs.length}`,
|
||||
`- Slice SCCs (>1 node): ${report.sliceSccs.length}`,
|
||||
'', '## Slice edges', '', '| From | To | Class edges |', '|---|---|---:|',
|
||||
...report.sliceEdges.map(edge => `| ${edge.source} | ${edge.target} | ${edge.count} |`),
|
||||
'', '## Slice SCCs', '',
|
||||
...(report.sliceSccs.length ? report.sliceSccs.map(component => `- ${component.join(' -> ')}`) : ['- None']),
|
||||
'', '## Package SCCs', '',
|
||||
...(report.packageSccs.length ? report.packageSccs.map(component => `- ${component.join(' -> ')}`) : ['- None']),
|
||||
'', '## Cross-slice model and mapper leaks', '',
|
||||
...(leaks.length ? leaks.map(edge => `- \`${edge.source}\` -> \`${edge.target}\``) : ['- None']),
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function renderDot(report) {
|
||||
const lines = ['digraph lastleg_slices {', ' rankdir=LR;'];
|
||||
for (const edge of report.sliceEdges) lines.push(` "${edge.source}" -> "${edge.target}" [label="${edge.count}"];`);
|
||||
lines.push('}', '');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function checkContract(report, contractPath) {
|
||||
const absolute = path.resolve(process.cwd(), contractPath);
|
||||
const contract = JSON.parse(fs.readFileSync(absolute, 'utf8'));
|
||||
const allowed = new Set(contract.allowedSliceEdges ?? []);
|
||||
const actual = report.sliceEdges.map(edge => `${edge.source}->${edge.target}`);
|
||||
const violations = actual.filter(edge => !allowed.has(edge)).map(edge => `new slice dependency: ${edge}`);
|
||||
for (const [slice, rule] of Object.entries(contract.sliceSpecificRules ?? {})) {
|
||||
const outgoing = report.sliceEdges.filter(edge => edge.source === slice).map(edge => edge.target).sort();
|
||||
const incoming = report.sliceEdges.filter(edge => edge.target === slice).map(edge => edge.source).sort();
|
||||
if (JSON.stringify(outgoing) !== JSON.stringify([...(rule.allowedOutgoing ?? [])].sort())) {
|
||||
violations.push(`${slice} outgoing dependencies changed: ${outgoing.join(', ')}`);
|
||||
}
|
||||
if (JSON.stringify(incoming) !== JSON.stringify([...(rule.allowedIncoming ?? [])].sort())) {
|
||||
violations.push(`${slice} incoming dependencies changed: ${incoming.join(', ')}`);
|
||||
}
|
||||
}
|
||||
if (violations.length > 0) {
|
||||
process.stderr.write(`Lastleg dependency contract violations in ${contractPath}:\n${violations.map(item => `- ${item}`).join('\n')}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const report = buildReport(options.sourceSet);
|
||||
if (options.check) checkContract(report, options.check);
|
||||
const rendered = options.format === 'json'
|
||||
? `${JSON.stringify(report, null, 2)}\n`
|
||||
: options.format === 'markdown'
|
||||
? renderMarkdown(report)
|
||||
: renderDot(report);
|
||||
if (options.output) fs.writeFileSync(path.resolve(process.cwd(), options.output), rendered, 'utf8');
|
||||
else process.stdout.write(rendered);
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.config;
|
||||
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
|
||||
@Component
|
||||
public class LastlegRestClientFactory {
|
||||
public RestClient build(RestClient.Builder builder, String baseUrl,
|
||||
LastlegRestClientProperties.ClientTimeouts timeouts) {
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(timeouts.getConnectTimeout())
|
||||
.build();
|
||||
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient);
|
||||
requestFactory.setReadTimeout(timeouts.getReadTimeout());
|
||||
return builder.requestFactory(requestFactory).baseUrl(baseUrl).build();
|
||||
}
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.baiyue.lastleg.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "baiyue.http.clients")
|
||||
public class LastlegRestClientProperties {
|
||||
private ClientTimeouts unloadPhotoFile = new ClientTimeouts(Duration.ofSeconds(3), Duration.ofSeconds(30));
|
||||
private ClientTimeouts relabelPhotoFile = new ClientTimeouts(Duration.ofSeconds(3), Duration.ofSeconds(30));
|
||||
private ClientTimeouts warehouseResource = new ClientTimeouts(Duration.ofSeconds(3), Duration.ofSeconds(5));
|
||||
private ClientTimeouts palletPhysical = new ClientTimeouts(Duration.ofSeconds(3), Duration.ofSeconds(5));
|
||||
|
||||
public ClientTimeouts getUnloadPhotoFile() { return unloadPhotoFile; }
|
||||
public void setUnloadPhotoFile(ClientTimeouts value) { unloadPhotoFile = required(value, "unload-photo-file"); }
|
||||
public ClientTimeouts getRelabelPhotoFile() { return relabelPhotoFile; }
|
||||
public void setRelabelPhotoFile(ClientTimeouts value) { relabelPhotoFile = required(value, "relabel-photo-file"); }
|
||||
public ClientTimeouts getWarehouseResource() { return warehouseResource; }
|
||||
public void setWarehouseResource(ClientTimeouts value) { warehouseResource = required(value, "warehouse-resource"); }
|
||||
public ClientTimeouts getPalletPhysical() { return palletPhysical; }
|
||||
public void setPalletPhysical(ClientTimeouts value) { palletPhysical = required(value, "pallet-physical"); }
|
||||
|
||||
private static ClientTimeouts required(ClientTimeouts value, String client) {
|
||||
if (value == null) throw new IllegalArgumentException(client + " HTTP timeouts are required");
|
||||
return value;
|
||||
}
|
||||
|
||||
public static class ClientTimeouts {
|
||||
private Duration connectTimeout;
|
||||
private Duration readTimeout;
|
||||
|
||||
public ClientTimeouts() { }
|
||||
|
||||
public ClientTimeouts(Duration connectTimeout, Duration readTimeout) {
|
||||
setConnectTimeout(connectTimeout);
|
||||
setReadTimeout(readTimeout);
|
||||
}
|
||||
|
||||
public Duration getConnectTimeout() { return connectTimeout; }
|
||||
public void setConnectTimeout(Duration value) { connectTimeout = positive(value, "connect-timeout"); }
|
||||
public Duration getReadTimeout() { return readTimeout; }
|
||||
public void setReadTimeout(Duration value) { readTimeout = positive(value, "read-timeout"); }
|
||||
|
||||
private static Duration positive(Duration value, String name) {
|
||||
if (value == null || value.isZero() || value.isNegative()) {
|
||||
throw new IllegalArgumentException(name + " must be positive");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,5 +1,7 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.gateway;
|
||||
|
||||
import com.baiyue.lastleg.config.LastlegRestClientFactory;
|
||||
import com.baiyue.lastleg.config.LastlegRestClientProperties;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -19,12 +21,17 @@ import java.io.IOException;
|
||||
public class HttpUnloadPhotoFileGateway implements UnloadPhotoFileGateway {
|
||||
private static final String BUSINESS_TYPE = "OVERSEAS_UNLOAD_PHOTO";
|
||||
@Resource private RestClient.Builder restClientBuilder;
|
||||
@Resource private LastlegRestClientFactory restClientFactory;
|
||||
@Resource private LastlegRestClientProperties restClientProperties;
|
||||
@Value("${baiyue.clients.file.url:http://127.0.0.1:9205}") private String baseUrl;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
private RestClient client;
|
||||
|
||||
@PostConstruct
|
||||
void initializeClient() { client = restClientBuilder.baseUrl(baseUrl).build(); }
|
||||
void initializeClient() {
|
||||
client = restClientFactory.build(restClientBuilder, baseUrl,
|
||||
restClientProperties.getUnloadPhotoFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadedFile upload(String customerId, long taskId, long operatorId, MultipartFile file) {
|
||||
|
||||
+6
-1
@@ -1,5 +1,7 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.gateway;
|
||||
|
||||
import com.baiyue.lastleg.config.LastlegRestClientFactory;
|
||||
import com.baiyue.lastleg.config.LastlegRestClientProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
@@ -15,6 +17,8 @@ import java.util.Map;
|
||||
@Component
|
||||
public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
||||
@Resource private RestClient.Builder restClientBuilder;
|
||||
@Resource private LastlegRestClientFactory restClientFactory;
|
||||
@Resource private LastlegRestClientProperties restClientProperties;
|
||||
|
||||
@Value("${baiyue.warehouse.base-url:http://127.0.0.1:9305}")
|
||||
private String baseUrl;
|
||||
@@ -26,7 +30,8 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
||||
|
||||
@PostConstruct
|
||||
void initializeClient() {
|
||||
this.client = restClientBuilder.baseUrl(baseUrl).build();
|
||||
this.client = restClientFactory.build(restClientBuilder, baseUrl,
|
||||
restClientProperties.getWarehouseResource());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+8
-1
@@ -1,5 +1,7 @@
|
||||
package com.baiyue.lastleg.palletops.gateway;
|
||||
|
||||
import com.baiyue.lastleg.config.LastlegRestClientFactory;
|
||||
import com.baiyue.lastleg.config.LastlegRestClientProperties;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -13,11 +15,16 @@ import java.util.Map;
|
||||
@Component
|
||||
public class HttpPalletPhysicalGateway implements PalletPhysicalGateway {
|
||||
@Resource private RestClient.Builder restClientBuilder;
|
||||
@Resource private LastlegRestClientFactory restClientFactory;
|
||||
@Resource private LastlegRestClientProperties restClientProperties;
|
||||
@Value("${baiyue.warehouse.base-url:http://127.0.0.1:9305}") private String baseUrl;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
private RestClient client;
|
||||
|
||||
@PostConstruct void initialize() { client = restClientBuilder.baseUrl(baseUrl).build(); }
|
||||
@PostConstruct void initialize() {
|
||||
client = restClientFactory.build(restClientBuilder, baseUrl,
|
||||
restClientProperties.getPalletPhysical());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PalletReference resolve(String palletCode) {
|
||||
|
||||
+8
-1
@@ -1,5 +1,7 @@
|
||||
package com.baiyue.lastleg.relabel.pda.gateway;
|
||||
|
||||
import com.baiyue.lastleg.config.LastlegRestClientFactory;
|
||||
import com.baiyue.lastleg.config.LastlegRestClientProperties;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -19,12 +21,17 @@ import java.io.IOException;
|
||||
public class HttpRelabelPhotoFileGateway implements RelabelPhotoFileGateway {
|
||||
private static final String BUSINESS_TYPE = "OVERSEAS_RELABEL_PHOTO";
|
||||
@Resource private RestClient.Builder restClientBuilder;
|
||||
@Resource private LastlegRestClientFactory restClientFactory;
|
||||
@Resource private LastlegRestClientProperties restClientProperties;
|
||||
@Value("${baiyue.clients.file.url:http://127.0.0.1:9205}") private String baseUrl;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
private RestClient client;
|
||||
|
||||
@PostConstruct
|
||||
void initializeClient() { client = restClientBuilder.baseUrl(baseUrl).build(); }
|
||||
void initializeClient() {
|
||||
client = restClientFactory.build(restClientBuilder, baseUrl,
|
||||
restClientProperties.getRelabelPhotoFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadedFile upload(String customerId, long taskId, long operatorId, MultipartFile file) {
|
||||
|
||||
@@ -41,6 +41,20 @@ lastleg:
|
||||
enabled: ${LASTLEG_SMART_DISPATCH_API_ENABLED:true}
|
||||
|
||||
baiyue:
|
||||
http:
|
||||
clients:
|
||||
unload-photo-file:
|
||||
connect-timeout: ${LASTLEG_UNLOAD_PHOTO_FILE_CONNECT_TIMEOUT:3s}
|
||||
read-timeout: ${LASTLEG_UNLOAD_PHOTO_FILE_READ_TIMEOUT:30s}
|
||||
relabel-photo-file:
|
||||
connect-timeout: ${LASTLEG_RELABEL_PHOTO_FILE_CONNECT_TIMEOUT:3s}
|
||||
read-timeout: ${LASTLEG_RELABEL_PHOTO_FILE_READ_TIMEOUT:30s}
|
||||
warehouse-resource:
|
||||
connect-timeout: ${LASTLEG_WAREHOUSE_RESOURCE_CONNECT_TIMEOUT:3s}
|
||||
read-timeout: ${LASTLEG_WAREHOUSE_RESOURCE_READ_TIMEOUT:5s}
|
||||
pallet-physical:
|
||||
connect-timeout: ${LASTLEG_PALLET_PHYSICAL_CONNECT_TIMEOUT:3s}
|
||||
read-timeout: ${LASTLEG_PALLET_PHYSICAL_READ_TIMEOUT:5s}
|
||||
clients:
|
||||
domestic-warehouse:
|
||||
url: ${DOMESTIC_WAREHOUSE_URL:}
|
||||
|
||||
Reference in New Issue
Block a user