doc:电子用印接口文档(草稿)

This commit is contained in:
FCL
2025-10-30 18:06:28 +08:00
parent b75c389b84
commit 3782b3a7c6
5 changed files with 534 additions and 14 deletions

Binary file not shown.

View File

@@ -0,0 +1,290 @@
# 检验检测模块 IWork电子用印后回调
**简介**:检测报告在用印环节会调用iwork接口发起用印流程。iwork在用印完毕后调用接口返回结果。
**基础地址**
测试系统地址http://172.16.46.63:30081/admin-api/databus/api/portal
生产系统地址:待定
**版本**:V1
## 1.总体说明
先调用【上传附件】接口,上传盖章后的文件。然后调用【用印回调】接口,返回用印结果。接口调用时需要进行签名和加密,详见【接口签名】章节。
## 2.上传附件接口
**接口地址**:`/qms_uploadAtt/v1`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**请求参数**:
| 参数名称 | 参数说明 | 数据类型 | 是否必须 |
|---------------|---------------|--------|------|
| base64Content | 文件内容 | string | 是 |
| fileName | 文件名称 | string | 是 |
| directory | 文件目录 | long | 否 |
| encrypt | 是否加密默认false | bool | 否 |
**响应参数**:
| 参数名称 | 参数说明 | 类型 |
|----------------------|---------|----------------|
| code | 返回代码 | integer(int32) |
| msg | 返回处理消息 | string |
| data | 返回数据对象 | object |
| $\qquad$ id | id | string |
| $\qquad$ path | 文件存储路径 | string |
| $\qquad$ name | 文件名 | string |
| $\qquad$ url | 文件访问url | string |
| $\qquad$ previewUrl | 文件预览url | string |
| $\qquad$ isEncrypted | 是否加密 | bool |
| $\qquad$ type | 类型 | string |
| $\qquad$ size | 大小 | long |
| $\qquad$ createTime | 创建时间 | 时间戳 |
## 3.用印回调接口
**接口地址**:`/qms_sealReply/v1`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 |
|------------|----------------|--------|------|
| mainId | 报告id | string | 是 |
| fileIds | 附件id多值用半角逗号分隔 | string | 是 |
**响应参数**:
| 参数名称 | 参数说明 | 类型 |
|----------------------|-----------------|----------------|
| code | 错误码0-成功,其他值-失败 | integer(int32) |
| msg | 返回处理消息 | string |
## 4.接口签名
接口签名和加密参考以下代码:
APP_ID和APP_SECRET请联系相关人员获取
```java
public class ApiForIworkExample {
public static final String TIMESTAMP = Long.toString(System.currentTimeMillis());
private static final String APP_ID = "";
private static final String APP_SECRET = "";
private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES;
private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/";
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final PrintStream OUT = buildConsolePrintStream();
public static final String ZT_APP_ID = "ZT-App-Id";
public static final String ZT_TIMESTAMP = "ZT-Timestamp";
public static final String ZT_NONCE = "ZT-Nonce";
public static final String ZT_SIGNATURE = "ZT-Signature";
public static final String ZT_AUTH_TOKEN = "ZT-Auth-Token";
public static final String CONTENT_TYPE = "Content-Type";
private ApiForIworkExample() {}
public static void main(String[] args) throws Exception {
executePostExample();
}
private static void executePostExample() throws Exception {
Map<String, Object> queryParams = new LinkedHashMap<>();
JSONObject jsonObject = new JSONObject();
jsonObject.put("mainId", "1983446576685900000");
jsonObject.put("fileIds", "1983446576685900001,1983446576685900002");
String bodyJson = jsonObject.toJSONString();
Map<String, Object> bodyParams = parseBodyJson(bodyJson);
String signature = generateSignature(queryParams, bodyParams);
String url = TARGET_API + "qms_sealReply/v1";
URI requestUri = buildUri(url, queryParams);
String nonce = randomNonce();
String cipherBody = encryptPayload(bodyJson);
OUT.println("原始 Request Body: " + bodyJson);
OUT.println("加密 Request Body: " + cipherBody);
HttpRequest request = HttpRequest.newBuilder(requestUri)
.timeout(Duration.ofSeconds(10))
.header(ZT_APP_ID, APP_ID)
.header(ZT_TIMESTAMP, TIMESTAMP)
.header(ZT_NONCE, nonce)
.header(ZT_SIGNATURE, signature)
// .header(ZT_AUTH_TOKEN, "82e5c281ddfa4386988fa4074e8794d7")
.header(CONTENT_TYPE, "application/json")
.POST(HttpRequest.BodyPublishers.ofString(cipherBody, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
printResponse(response);
}
private static String encryptPayload(String plaintext) {
try {
return CryptoSignatureUtils.encrypt(plaintext, APP_SECRET, ENCRYPTION_TYPE);
} catch (Exception ex) {
throw new IllegalStateException("Failed to encrypt request body", ex);
}
}
private static void printResponse(HttpResponse<String> response) {
OUT.println("HTTP Status: " + response.statusCode());
String cipherText = response.body();
OUT.println("加密 Response: " + cipherText);
String plain = tryDecrypt(cipherText);
OUT.println("原始 Response: " + normalizePotentialMojibake(plain));
}
private static String randomNonce() {
return UUID.randomUUID().toString().replace("-", "");
}
private static URI buildUri(String baseUrl, Map<String, Object> queryParams) {
if (queryParams == null || 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(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8));
builder.append('=');
builder.append(URLEncoder.encode(String.valueOf(entry.getValue()), StandardCharsets.UTF_8));
}
return URI.create(builder.toString());
}
private static String generateSignature(Map<String, Object> queryParams, Map<String, Object> bodyParams) {
TreeMap<String, Object> sorted = new TreeMap<>();
if (queryParams != null) {
queryParams.forEach((key, value) -> sorted.put(key, normalizeValue(value)));
}
if (bodyParams != null) {
bodyParams.forEach((key, value) -> sorted.put(key, normalizeValue(value)));
}
sorted.put(ZT_APP_ID, APP_ID);
sorted.put(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);
});
OUT.println("原始 签名串: " + canonical);
String md5Hex = md5Hex(canonical.toString());
OUT.println("原始签名: " + md5Hex);
return md5Hex;
}
private static Object normalizeValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof Map || value instanceof Iterable) {
try {
return OBJECT_MAPPER.writeValueAsString(value);
} catch (JsonProcessingException ignored) {
return value.toString();
}
}
return value;
}
private static Map<String, Object> parseBodyJson(String bodyJson) {
if (bodyJson == null || bodyJson.isBlank()) {
return Map.of();
}
try {
return OBJECT_MAPPER.readValue(bodyJson, new TypeReference<Map<String, Object>>() { });
} catch (IOException ex) {
throw new IllegalArgumentException("Failed to parse request body JSON", ex);
}
}
private static String md5Hex(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
String segment = Integer.toHexString(b & 0xFF);
if (segment.length() == 1) {
hex.append('0');
}
hex.append(segment);
}
return hex.toString();
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("MD5 algorithm not available", ex);
}
}
private static String tryDecrypt(String cipherText) {
if (cipherText == null || cipherText.isBlank()) {
return cipherText;
}
try {
// Databus 会在凭证开启加密时返回密文,这里做一次解密展示真实响应。
return CryptoSignatureUtils.decrypt(cipherText, APP_SECRET, ENCRYPTION_TYPE);
} catch (Exception ex) {
return "<unable to decrypt> " + ex.getMessage();
}
}
// 解决控制台打印 乱码问题
private static String normalizePotentialMojibake(String value) {
if (value == null || value.isEmpty()) {
return value;
}
long suspectCount = value.chars().filter(ch -> ch >= 0x80 && ch <= 0xFF).count();
long highCount = value.chars().filter(ch -> ch > 0xFF).count();
if (suspectCount > 0 && highCount == 0) {
try {
byte[] decoded = value.getBytes(StandardCharsets.ISO_8859_1);
String converted = new String(decoded, StandardCharsets.UTF_8);
if (converted.chars().anyMatch(ch -> ch > 0xFF)) {
return converted;
}
} catch (Exception ignored) {
return value;
}
}
return value;
}
/**
* 输出流编码与当前控制台保持一致,避免中文字符再次出现编码差异。
*/
private static PrintStream buildConsolePrintStream() {
try {
String consoleEncoding = System.getProperty("sun.stdout.encoding");
if (consoleEncoding != null && !consoleEncoding.isBlank()) {
return new PrintStream(System.out, true, Charset.forName(consoleEncoding));
}
return new PrintStream(System.out, true, Charset.defaultCharset());
} catch (Exception ignored) {
return System.out;
}
}
}
```

Binary file not shown.

View File

@@ -0,0 +1,232 @@
package com.zt.plat.module.qms;
import com.alibaba.fastjson.JSONObject;
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 java.io.IOException;
import java.io.PrintStream;
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.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
public class ApiForIworkExample {
public static final String TIMESTAMP = Long.toString(System.currentTimeMillis());
private static final String APP_ID = "iwork";
private static final String APP_SECRET = "lpGXiNe/GMLk0vsbYGLa8eYxXq8tGhTbuu3/D4MJzIk=";
private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES;
private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/";
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final PrintStream OUT = buildConsolePrintStream();
public static final String ZT_APP_ID = "ZT-App-Id";
public static final String ZT_TIMESTAMP = "ZT-Timestamp";
public static final String ZT_NONCE = "ZT-Nonce";
public static final String ZT_SIGNATURE = "ZT-Signature";
public static final String ZT_AUTH_TOKEN = "ZT-Auth-Token";
public static final String CONTENT_TYPE = "Content-Type";
private ApiForIworkExample() {
}
public static void main(String[] args) throws Exception {
executePostExample();
}
private static void executePostExample() throws Exception {
Map<String, Object> queryParams = new LinkedHashMap<>();
JSONObject jsonObject = new JSONObject();
jsonObject.put("id", "1983446576685985794");
jsonObject.put("remark", "");
String bodyJson = jsonObject.toJSONString();
Map<String, Object> bodyParams = parseBodyJson(bodyJson);
String signature = generateSignature(queryParams, bodyParams);
String url = TARGET_API + "qms_sealReply/v1";
URI requestUri = buildUri(url, queryParams);
String nonce = randomNonce();
String cipherBody = encryptPayload(bodyJson);
OUT.println("原始 Request Body: " + bodyJson);
OUT.println("加密 Request Body: " + cipherBody);
HttpRequest request = HttpRequest.newBuilder(requestUri)
.timeout(Duration.ofSeconds(10))
.header(ZT_APP_ID, APP_ID)
.header(ZT_TIMESTAMP, TIMESTAMP)
.header(ZT_NONCE, nonce)
.header(ZT_SIGNATURE, signature)
.header(ZT_AUTH_TOKEN, "82e5c281ddfa4386988fa4074e8794d7")
.header(CONTENT_TYPE, "application/json")
.POST(HttpRequest.BodyPublishers.ofString(cipherBody, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
printResponse(response);
}
private static String encryptPayload(String plaintext) {
try {
return CryptoSignatureUtils.encrypt(plaintext, APP_SECRET, ENCRYPTION_TYPE);
} catch (Exception ex) {
throw new IllegalStateException("Failed to encrypt request body", ex);
}
}
private static void printResponse(HttpResponse<String> response) {
OUT.println("HTTP Status: " + response.statusCode());
String cipherText = response.body();
OUT.println("加密 Response: " + cipherText);
String plain = tryDecrypt(cipherText);
OUT.println("原始 Response: " + normalizePotentialMojibake(plain));
}
private static String randomNonce() {
return UUID.randomUUID().toString().replace("-", "");
}
private static URI buildUri(String baseUrl, Map<String, Object> queryParams) {
if (queryParams == null || 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(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8));
builder.append('=');
builder.append(URLEncoder.encode(String.valueOf(entry.getValue()), StandardCharsets.UTF_8));
}
return URI.create(builder.toString());
}
private static String generateSignature(Map<String, Object> queryParams, Map<String, Object> bodyParams) {
TreeMap<String, Object> sorted = new TreeMap<>();
if (queryParams != null) {
queryParams.forEach((key, value) -> sorted.put(key, normalizeValue(value)));
}
if (bodyParams != null) {
bodyParams.forEach((key, value) -> sorted.put(key, normalizeValue(value)));
}
sorted.put(ZT_APP_ID, APP_ID);
sorted.put(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);
});
OUT.println("原始 签名串: " + canonical);
String md5Hex = md5Hex(canonical.toString());
OUT.println("原始签名: " + md5Hex);
return md5Hex;
}
private static Object normalizeValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof Map || value instanceof Iterable) {
try {
return OBJECT_MAPPER.writeValueAsString(value);
} catch (JsonProcessingException ignored) {
return value.toString();
}
}
return value;
}
private static Map<String, Object> parseBodyJson(String bodyJson) {
if (bodyJson == null || bodyJson.isBlank()) {
return Map.of();
}
try {
return OBJECT_MAPPER.readValue(bodyJson, new TypeReference<Map<String, Object>>() { });
} catch (IOException ex) {
throw new IllegalArgumentException("Failed to parse request body JSON", ex);
}
}
private static String md5Hex(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
String segment = Integer.toHexString(b & 0xFF);
if (segment.length() == 1) {
hex.append('0');
}
hex.append(segment);
}
return hex.toString();
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("MD5 algorithm not available", ex);
}
}
private static String tryDecrypt(String cipherText) {
if (cipherText == null || cipherText.isBlank()) {
return cipherText;
}
try {
// Databus 会在凭证开启加密时返回密文,这里做一次解密展示真实响应。
return CryptoSignatureUtils.decrypt(cipherText, APP_SECRET, ENCRYPTION_TYPE);
} catch (Exception ex) {
return "<unable to decrypt> " + ex.getMessage();
}
}
// 解决控制台打印 乱码问题
private static String normalizePotentialMojibake(String value) {
if (value == null || value.isEmpty()) {
return value;
}
long suspectCount = value.chars().filter(ch -> ch >= 0x80 && ch <= 0xFF).count();
long highCount = value.chars().filter(ch -> ch > 0xFF).count();
if (suspectCount > 0 && highCount == 0) {
try {
byte[] decoded = value.getBytes(StandardCharsets.ISO_8859_1);
String converted = new String(decoded, StandardCharsets.UTF_8);
if (converted.chars().anyMatch(ch -> ch > 0xFF)) {
return converted;
}
} catch (Exception ignored) {
return value;
}
}
return value;
}
/**
* 输出流编码与当前控制台保持一致,避免中文字符再次出现编码差异。
*/
private static PrintStream buildConsolePrintStream() {
try {
String consoleEncoding = System.getProperty("sun.stdout.encoding");
if (consoleEncoding != null && !consoleEncoding.isBlank()) {
return new PrintStream(System.out, true, Charset.forName(consoleEncoding));
}
return new PrintStream(System.out, true, Charset.defaultCharset());
} catch (Exception ignored) {
return System.out;
}
}
}

View File

@@ -5,7 +5,9 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; import com.zt.plat.framework.common.util.security.CryptoSignatureUtils;
import org.springframework.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream; import java.io.PrintStream;
import java.net.URI; import java.net.URI;
@@ -15,13 +17,13 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Duration; import java.time.Duration;
import java.util.LinkedHashMap; import java.util.*;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
public class DatabusApiInvocationExample { public class DatabusApiInvocationExample {
public static final String TIMESTAMP = Long.toString(System.currentTimeMillis()); public static final String TIMESTAMP = Long.toString(System.currentTimeMillis());
@@ -85,14 +87,12 @@ public class DatabusApiInvocationExample {
JSONObject jsonObject = new JSONObject(); JSONObject jsonObject = new JSONObject();
jsonObject.put("id", "1968843568287817730"); jsonObject.put("id", "1983446576685985794");
jsonObject.put("sampleName", "545(委检样)"); jsonObject.put("remark", "");
jsonObject.put("materialName", "ff");
jsonObject.put("materialId", "123");
jsonObject.put("dictionaryBusinessId", "1965289399255388162");
long extraTimestamp = 1761556157185L; // long extraTimestamp = 1761556157185L;
String bodyJson = String.format(jsonObject.toJSONString(), extraTimestamp); // String bodyJson = String.format(jsonObject.toJSONString(), extraTimestamp);
String bodyJson = jsonObject.toJSONString();
Map<String, Object> bodyParams = parseBodyJson(bodyJson); Map<String, Object> bodyParams = parseBodyJson(bodyJson);
String signature = generateSignature(queryParams, bodyParams); String signature = generateSignature(queryParams, bodyParams);
@@ -109,10 +109,8 @@ public class DatabusApiInvocationExample {
.header(ZT_TIMESTAMP, TIMESTAMP) .header(ZT_TIMESTAMP, TIMESTAMP)
.header(ZT_NONCE, nonce) .header(ZT_NONCE, nonce)
.header(ZT_SIGNATURE, signature) .header(ZT_SIGNATURE, signature)
.header(ZT_AUTH_TOKEN, "843304444f65423f901a3efe2f252a75") .header(ZT_AUTH_TOKEN, "82e5c281ddfa4386988fa4074e8794d7")
.header(CONTENT_TYPE, "application/json") .header(CONTENT_TYPE, "application/json")
.header("visit-company-id", "101")
.header("visit-dept-id", "103")
.POST(HttpRequest.BodyPublishers.ofString(cipherBody, StandardCharsets.UTF_8)) .POST(HttpRequest.BodyPublishers.ofString(cipherBody, StandardCharsets.UTF_8))
.build(); .build();