1. 启动默认调度,定时请求 databus api

2. 修复 databus 单元测试
3. 调整 iwork 回调业务编号
This commit is contained in:
chenbowen
2025-12-02 17:45:58 +08:00
parent 2e0b0a5e83
commit e11065a596
22 changed files with 662 additions and 407 deletions

View File

@@ -2,6 +2,7 @@ package com.zt.plat.module.template;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 项目的启动类
@@ -9,6 +10,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* @author 周迪
*/
@SpringBootApplication
@EnableScheduling
public class TemplateServerApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,126 @@
package com.zt.plat.module.template.dal.dataobject.databus;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.mybatis.core.dataobject.BaseDO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* Databus 请求与响应日志表。
*/
@TableName("template_databus_request_log")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TemplateDatabusRequestLogDO extends BaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 请求唯一标识。
*/
@TableField("REQUEST_ID")
private String requestId;
/**
* 实际请求 URL。
*/
@TableField("TARGET_URL")
private String targetUrl;
/**
* HTTP 方法。
*/
@TableField("HTTP_METHOD")
private String httpMethod;
/**
* Query 参数 JSON。
*/
@TableField("QUERY_PARAMS")
private String queryParams;
/**
* 请求头 JSON。
*/
@TableField("REQUEST_HEADERS")
private String requestHeaders;
/**
* 原始请求体。
*/
@TableField("REQUEST_BODY")
private String requestBody;
/**
* 加密后的请求体。
*/
@TableField("ENCRYPTED_REQUEST_BODY")
private String encryptedRequestBody;
/**
* 签名。
*/
@TableField("SIGNATURE")
private String signature;
/**
* 随机串。
*/
@TableField("NONCE")
private String nonce;
/**
* 时间戳。
*/
@TableField("ZT_TIMESTAMP")
private String ztTimestamp;
/**
* HTTP 返回状态码。
*/
@TableField("RESPONSE_STATUS")
private Integer responseStatus;
/**
* 加密响应体。
*/
@TableField("ENCRYPTED_RESPONSE_BODY")
private String encryptedResponseBody;
/**
* 解密后的响应体。
*/
@TableField("RESPONSE_BODY")
private String responseBody;
/**
* 是否调用成功。
*/
@TableField("SUCCESS")
private Boolean success;
/**
* 错误信息。
*/
@TableField("ERROR_MESSAGE")
private String errorMessage;
/**
* 调用耗时(毫秒)。
*/
@TableField("DURATION_MS")
private Long durationMs;
}

View File

@@ -0,0 +1,12 @@
package com.zt.plat.module.template.dal.mysql.databus;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.module.template.dal.dataobject.databus.TemplateDatabusRequestLogDO;
import org.apache.ibatis.annotations.Mapper;
/**
* Databus 请求日志 Mapper。
*/
@Mapper
public interface TemplateDatabusRequestLogMapper extends BaseMapperX<TemplateDatabusRequestLogDO> {
}

View File

@@ -0,0 +1,21 @@
package com.zt.plat.module.template.job.databus;
import com.zt.plat.module.template.service.databus.TemplateDatabusInvokeService;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 基于 Spring 的 Databus 调度任务。
*/
@Component
@RequiredArgsConstructor
public class TemplateDatabusScheduler {
private final TemplateDatabusInvokeService invokeService;
@Scheduled(cron = "0 0/10 * * * ?")
public void execute() {
invokeService.invokeAndRecord();
}
}

View File

@@ -0,0 +1,304 @@
package com.zt.plat.module.template.service.databus;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.util.security.CryptoSignatureUtils;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import com.zt.plat.module.template.dal.dataobject.databus.TemplateDatabusRequestLogDO;
import com.zt.plat.module.template.dal.mysql.databus.TemplateDatabusRequestLogMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.net.URI;
import java.net.URLEncoder;
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.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* 调用 Databus 接口并记录日志。
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class TemplateDatabusInvokeService {
private static final TypeReference<LinkedHashMap<String, Object>> MAP_TYPE = new TypeReference<>() {
};
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final String HEADER_ZT_APP_ID = "ZT-App-Id";
private static final String HEADER_ZT_TIMESTAMP = "ZT-Timestamp";
private static final String HEADER_ZT_NONCE = "ZT-Nonce";
private static final String HEADER_ZT_SIGNATURE = "ZT-Signature";
private static final String HEADER_ZT_AUTH_TOKEN = "ZT-Auth-Token";
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String TARGET_URL = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1";
private static final String APP_ID = "ztmy";
private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ=";
private static final String AUTH_TOKEN = "a5d7cf609c0b47038ea405c660726ee9";
private static final String DEFAULT_HTTP_METHOD = "POST";
private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES;
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
private static final Duration READ_TIMEOUT = Duration.ofSeconds(10);
private static final Map<String, String> BASE_QUERY_PARAMS = Map.of(
"businessCode", "11",
"fileId", "11"
);
private static final Map<String, String> EXTRA_HEADERS = Map.of();
private static final String DEFAULT_REQUEST_BODY = """
{
}
""";
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
.connectTimeout(CONNECT_TIMEOUT)
.build();
private final TemplateDatabusRequestLogMapper requestLogMapper;
private final ObjectMapper objectMapper;
@TenantIgnore
public void invokeAndRecord() {
TemplateDatabusRequestLogDO logDO = TemplateDatabusRequestLogDO.builder()
.requestId(generateRequestId())
.httpMethod(DEFAULT_HTTP_METHOD)
.targetUrl(TARGET_URL)
.success(Boolean.FALSE)
.build();
Instant start = Instant.now();
try {
Map<String, Object> queryParams = buildQueryParams();
Map<String, Object> bodyParams = buildBodyParams(logDO.getRequestId());
String requestBody = toJson(bodyParams);
String serializedQuery = toJson(queryParams);
logDO.setRequestBody(requestBody);
logDO.setQueryParams(serializedQuery);
String timestamp = Long.toString(System.currentTimeMillis());
logDO.setZtTimestamp(timestamp);
String nonce = generateNonce();
logDO.setNonce(nonce);
String signature = generateSignature(queryParams, bodyParams, timestamp);
logDO.setSignature(signature);
String encryptedBody = encryptPayload(requestBody);
logDO.setEncryptedRequestBody(encryptedBody);
URI uri = buildUri(TARGET_URL, queryParams);
logDO.setTargetUrl(uri.toString());
Map<String, String> headers = buildHeaders(nonce, timestamp, signature);
logDO.setRequestHeaders(toJson(headers));
HttpRequest request = buildHttpRequest(uri, headers, encryptedBody);
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
logDO.setResponseStatus(response.statusCode());
logDO.setEncryptedResponseBody(response.body());
logDO.setResponseBody(tryDecrypt(response.body()));
boolean success = response.statusCode() >= 200 && response.statusCode() < 300;
logDO.setSuccess(success);
if (!success) {
log.warn("Databus API 返回非 2xx 状态码: {}", response.statusCode());
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
logDO.setErrorMessage(truncate("Interrupted: " + ex.getMessage(), 1000));
log.warn("Databus 调度被中断: {}", ex.getMessage());
} catch (Exception ex) {
logDO.setErrorMessage(truncate(ex.getMessage(), 1000));
log.error("Databus 调度执行异常", ex);
} finally {
logDO.setDurationMs(Duration.between(start, Instant.now()).toMillis());
requestLogMapper.insert(logDO);
}
}
private Map<String, Object> buildQueryParams() {
Map<String, Object> params = new LinkedHashMap<>();
BASE_QUERY_PARAMS.forEach(params::put);
return params;
}
private Map<String, Object> buildBodyParams(String requestId) {
Map<String, Object> body = new LinkedHashMap<>(parseTemplateBody());
body.put("__requestId__", requestId);
body.put("datetime", DATETIME_FORMATTER.format(LocalDateTime.now()));
body.putIfAbsent("system", "TEMPLATE");
return body;
}
private Map<String, Object> parseTemplateBody() {
try {
return objectMapper.readValue(DEFAULT_REQUEST_BODY, MAP_TYPE);
} catch (JsonProcessingException ex) {
throw new IllegalStateException("内置 Databus 请求体 JSON 解析失败", ex);
}
}
private Map<String, String> buildHeaders(String nonce, String timestamp, String signature) {
Map<String, String> headers = new LinkedHashMap<>();
headers.put(HEADER_ZT_APP_ID, APP_ID);
headers.put(HEADER_ZT_TIMESTAMP, timestamp);
headers.put(HEADER_ZT_NONCE, nonce);
headers.put(HEADER_ZT_SIGNATURE, signature);
if (StringUtils.hasText(AUTH_TOKEN)) {
headers.put(HEADER_ZT_AUTH_TOKEN, AUTH_TOKEN);
}
headers.put(HEADER_CONTENT_TYPE, "application/json");
EXTRA_HEADERS.forEach((key, value) -> {
if (StringUtils.hasText(key) && value != null) {
headers.put(key, value);
}
});
return headers;
}
private HttpRequest buildHttpRequest(URI uri, Map<String, String> headers, String encryptedBody) {
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
.timeout(READ_TIMEOUT);
headers.forEach(builder::header);
HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofString(encryptedBody, StandardCharsets.UTF_8);
switch (DEFAULT_HTTP_METHOD) {
case "GET":
builder.GET();
break;
case "PUT":
builder.PUT(publisher);
break;
case "PATCH":
builder.method("PATCH", publisher);
break;
case "DELETE":
builder.method("DELETE", publisher);
break;
default:
builder.POST(publisher);
}
return builder.build();
}
private String encryptPayload(String plaintext) {
try {
return CryptoSignatureUtils.encrypt(plaintext, APP_SECRET, ENCRYPTION_TYPE);
} catch (Exception ex) {
throw new IllegalStateException("请求体加密失败", ex);
}
}
private String tryDecrypt(String cipherText) {
if (!StringUtils.hasText(cipherText)) {
return cipherText;
}
try {
return CryptoSignatureUtils.decrypt(cipherText, APP_SECRET, ENCRYPTION_TYPE);
} catch (Exception ex) {
return "<unable to decrypt> " + ex.getMessage();
}
}
private URI buildUri(String baseUrl, Map<String, Object> queryParams) {
if (queryParams.isEmpty()) {
return URI.create(baseUrl);
}
StringBuilder builder = new StringBuilder(baseUrl);
builder.append(baseUrl.contains("?") ? '&' : '?');
boolean first = true;
for (Map.Entry<String, Object> entry : queryParams.entrySet()) {
if (!first) {
builder.append('&');
}
first = false;
builder.append(encode(entry.getKey()))
.append('=')
.append(encode(Objects.toString(entry.getValue(), "")));
}
return URI.create(builder.toString());
}
private String generateSignature(Map<String, Object> queryParams, Map<String, Object> bodyParams, String timestamp) {
TreeMap<String, Object> sorted = new TreeMap<>();
queryParams.forEach((key, value) -> sorted.put(key, normalizeValue(value)));
bodyParams.forEach((key, value) -> sorted.put(key, normalizeValue(value)));
sorted.put(HEADER_ZT_APP_ID, APP_ID);
sorted.put(HEADER_ZT_TIMESTAMP, timestamp);
StringBuilder canonical = new StringBuilder();
sorted.forEach((key, value) -> {
if (value == null) {
return;
}
if (canonical.length() > 0) {
canonical.append('&');
}
canonical.append(key).append('=').append(value);
});
return md5Hex(canonical.toString());
}
private Object normalizeValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof Map || value instanceof Iterable) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException ex) {
return value.toString();
}
}
return value;
}
private String md5Hex(String source) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(source.getBytes(StandardCharsets.UTF_8));
StringBuilder builder = new StringBuilder(bytes.length * 2);
for (byte aByte : bytes) {
String part = Integer.toHexString(aByte & 0xFF);
if (part.length() == 1) {
builder.append('0');
}
builder.append(part);
}
return builder.toString();
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("MD5 算法不可用", ex);
}
}
private String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
private String generateRequestId() {
return UUID.randomUUID().toString().replace("-", "");
}
private String generateNonce() {
return UUID.randomUUID().toString().replace("-", "");
}
private String toJson(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException ex) {
return String.valueOf(value);
}
}
private String truncate(String value, int maxLength) {
if (!StringUtils.hasText(value) || value.length() <= maxLength) {
return value;
}
return value.substring(0, maxLength);
}
}

View File

@@ -0,0 +1,49 @@
CREATE TABLE template_databus_request_log (
ID BIGINT NOT NULL,
REQUEST_ID VARCHAR(64) NOT NULL,
TARGET_URL VARCHAR(512) NOT NULL,
HTTP_METHOD VARCHAR(16) NOT NULL,
QUERY_PARAMS TEXT,
REQUEST_HEADERS TEXT,
REQUEST_BODY TEXT,
ENCRYPTED_REQUEST_BODY TEXT,
SIGNATURE VARCHAR(128),
NONCE VARCHAR(64),
ZT_TIMESTAMP VARCHAR(32),
RESPONSE_STATUS INT,
ENCRYPTED_RESPONSE_BODY TEXT,
RESPONSE_BODY TEXT,
SUCCESS BIT DEFAULT '0' NOT NULL,
ERROR_MESSAGE TEXT,
DURATION_MS BIGINT,
CREATE_TIME DATETIME(6) DEFAULT CURRENT_TIMESTAMP NOT NULL,
UPDATE_TIME DATETIME(6) DEFAULT CURRENT_TIMESTAMP NOT NULL,
CREATOR VARCHAR(64),
UPDATER VARCHAR(64),
DELETED BIT DEFAULT '0' NOT NULL,
PRIMARY KEY (ID)
);
COMMENT ON TABLE template_databus_request_log IS 'Databus 请求调度日志表';
COMMENT ON COLUMN template_databus_request_log.ID IS '主键';
COMMENT ON COLUMN template_databus_request_log.REQUEST_ID IS '请求唯一标识';
COMMENT ON COLUMN template_databus_request_log.TARGET_URL IS '目标地址(含 Query';
COMMENT ON COLUMN template_databus_request_log.HTTP_METHOD IS 'HTTP 方法';
COMMENT ON COLUMN template_databus_request_log.QUERY_PARAMS IS 'Query 参数 JSON';
COMMENT ON COLUMN template_databus_request_log.REQUEST_HEADERS IS '请求头 JSON';
COMMENT ON COLUMN template_databus_request_log.REQUEST_BODY IS '原始请求体';
COMMENT ON COLUMN template_databus_request_log.ENCRYPTED_REQUEST_BODY IS '加密请求体';
COMMENT ON COLUMN template_databus_request_log.SIGNATURE IS '签名';
COMMENT ON COLUMN template_databus_request_log.NONCE IS '随机串';
COMMENT ON COLUMN template_databus_request_log.ZT_TIMESTAMP IS '时间戳';
COMMENT ON COLUMN template_databus_request_log.RESPONSE_STATUS IS '响应状态码';
COMMENT ON COLUMN template_databus_request_log.ENCRYPTED_RESPONSE_BODY IS '加密响应体';
COMMENT ON COLUMN template_databus_request_log.RESPONSE_BODY IS '解密后的响应体';
COMMENT ON COLUMN template_databus_request_log.SUCCESS IS '是否成功';
COMMENT ON COLUMN template_databus_request_log.ERROR_MESSAGE IS '错误信息';
COMMENT ON COLUMN template_databus_request_log.DURATION_MS IS '耗时(毫秒)';
COMMENT ON COLUMN template_databus_request_log.CREATE_TIME IS '创建时间';
COMMENT ON COLUMN template_databus_request_log.UPDATE_TIME IS '更新时间';
COMMENT ON COLUMN template_databus_request_log.CREATOR IS '创建人';
COMMENT ON COLUMN template_databus_request_log.UPDATER IS '更新人';
COMMENT ON COLUMN template_databus_request_log.DELETED IS '逻辑删除标记';