# 开放接口
ELCube 支持多种验证方式,如用户名密码、Basic、签名等;
对于API调用,我们推荐使用签名方式;
Basic认证方式适用于PostMan等工具进行数据调试;
# Basic认证方式
// 以java语言为例,在请求头中添加basic认证信息
// password 为 加密后的密码串
headers.put(
"Authorization",
"Basic " + Base64.getUrlEncoder().encodeToString(
(username+":"+password).getBytes()
)
);
2
3
4
5
6
7
8
# URL签名认证方式
URL签名方式是对请求URL及请求查询字符串进行SHA1签名,作为请求头进行验证
URL 签名方式需要如下请求头参数:
- elcube-appKey 用户AppKey(AppKey 和 用户密钥 需要在 系统-账号管理-账号详情-Secrets 中创建)
- elcube-timestamp 当前时间戳 单位秒
- elcube-nonce 随机字符串
- elcube-signature URL签名
headers.put("elcube-nonce", nonce);
headers.put("elcube-timestamp",timestamp);
headers.put("elcube-appKey",key);
headers.put("elcube-signature",signature);
2
3
4
具体签名步骤如下:
- 将所有查询字符串,包括时间戳、密钥、随机字符串,根据key以字符串方式排序
- 根据排序后的结果,将查询字符串(仅queryString,postdata不参与签名)以"&"作为分隔符进行拼接
- 将拼接后的字符串追加到请求URL(注意,请求URL不包含域名及容器上下文)
- 将完整的字符串进行SHA1加密
List<String> parameterStr = new ArrayList<>();
parameterStr.add(String.format("timestamp=%s",timestamp));
parameterStr.add(String.format("secret=%s",secret));
parameterStr.add(String.format("nonce=%s",nonce));
parameterMap.forEach((key, value) ->{
if(value!=null){
if(value.getClass().isArray()){
Arrays.stream((Object[]) value)
.forEach(v -> parameterStr.add(String.format("%s=%s", key, v)));
}else{
parameterStr.add(String.format("%s=%s", key, value));
}
}
});
String unsigned = url+'?'+parameterStr.stream().sorted().collect(Collectors.joining("&"));
String signature = DigestUtils.sha1Hex(unsigned);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 一个完整的DEMO
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Slf4j
public class ELCubeHttpConnector {
public static void main(String[] args){
JSONObject data = new JSONObject();
data.put("from",0);
data.put("rows",10);
new ELCubeHttpConnector().doPostUseBasicAuth("/doc/list/document",data);
new ELCubeHttpConnector().doGetUseSignatureAuth("/webapp/user/saved/query/list?source=$docs");
}
private static final String endpoint = "https://loan.elcube.cloud/api";
// for basic auth
private static final String username = "admin";
private static final String password = "7b2e9f54cdff413fcde01f330af6896c3cd7e6cd";
// for signature auth
private static final String appKey = "EF47748B18F84DF7";
private static final String secretKey = "QzlCNzA5RDU1NjM3NEM3NDhFODdBQTI2RTUwOTBBRDM=";
private static final Pattern pattern =
Pattern.compile("([/\\w]+)(\\?(\\w+(=[^&=%?]*)?(&\\w+(=[^&=%?]*)?)*))?");
public void doGetUseSignatureAuth(String targetUrl){
Matcher matcher = pattern.matcher(targetUrl);
if(!matcher.matches()){
throw new RuntimeException("URL不合法");
}
// 解析URL,提取请求路径 及 查询字符串
String requestPath = matcher.group(1);
String queryString = matcher.group(3);
Map<String, String> parameterMap = Arrays.stream(StringUtils.split(queryString, "&"))
.map(item -> StringUtils.split(item, "="))
.collect(Collectors.toMap(
e -> e[0],
e -> e.length>=2?e[1]:StringUtils.EMPTY
));
// 构建请求头
String nonce = nonce();
String timestamp = String.valueOf(System.currentTimeMillis()/1000);
String signature = signature(
requestPath,
parameterMap,
timestamp,
secretKey,
nonce
);
Map<String,String> headers = new HashMap<>();
headers.put("elcube-appKey",appKey);
headers.put("elcube-nonce", nonce);
headers.put("elcube-timestamp",timestamp);
headers.put("elcube-signature", signature);
// 发起GET请求
try (CloseableHttpClient client = getClient()) {
HttpGet get = new HttpGet(endpoint + targetUrl);
headers.forEach(get::setHeader);
if(log.isInfoEnabled())
log.info("HttpClient Post "+ endpoint + targetUrl);
CloseableHttpResponse response = client.execute(get);
int status = response.getStatusLine().getStatusCode();
if(status>=200 && status<=299){
log.info("请求成功");
JSONObject responseData = JSON.parseObject(
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8));
log.info("返回结果:"+responseData);
}else{
log.error("请求失败");
JSONObject responseData = JSON.parseObject(
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8));
log.error("错误信息:"+responseData.getString("msg"));
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void doPostUseBasicAuth(String targetUrl,Object data){
// 构建请求头
Map<String,String> headers = new HashMap<>();
headers.put("Content-Type","application/json");
headers.put("Authorization","Basic " + Base64.getUrlEncoder()
.encodeToString((username+":"+password).getBytes()));
// 发起POST请求
try (CloseableHttpClient client = getClient()) {
HttpPost post = new HttpPost(endpoint + targetUrl);
headers.forEach(post::setHeader);
post.setEntity(new StringEntity(data.toString(), StandardCharsets.UTF_8));
if(log.isInfoEnabled())
log.info("HttpClient Post "+ endpoint + targetUrl);
CloseableHttpResponse response = client.execute(post);
int status = response.getStatusLine().getStatusCode();
if(status>=200 && status<=299){
log.info("请求成功");
JSONObject responseData = JSON.parseObject(
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8));
log.info("返回结果:"+responseData);
}else{
log.error("请求失败");
JSONObject res = JSON.parseObject(
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8));
log.error("错误信息:"+res.getString("msg"));
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @return HttpClient
*/
private static CloseableHttpClient getClient() {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] arg0,String arg1) {
}
@Override
public void checkServerTrusted(X509Certificate[] arg0,String arg1) {
}
};
ctx.init(null, new TrustManager[] { tm }, null);
SSLConnectionSocketFactory ssf = new SSLConnectionSocketFactory(
ctx, NoopHostnameVerifier.INSTANCE);
return HttpClients.custom()
.setSSLSocketFactory(ssf).build();
} catch (Exception e) {
return HttpClients.createDefault();
}
}
/**
* 签名函数
* @param path 请求路径
* @param queryStringMap 请求查询字符串Map
* @param timestamp 时间戳 秒 System.currentTimeMillis()/1000
* @param secretKey appSecret
* @param nonce 随机字符串
* @return 签名
*/
private static String signature(String path, Map<String,?>
queryStringMap,
String timestamp,
String secretKey,
String nonce){
List<String> list = new ArrayList<>();
list.add(String.format("timestamp=%s",timestamp));
list.add(String.format("secret=%s",secretKey));
list.add(String.format("nonce=%s",nonce));
queryStringMap.forEach((key, value) ->{
if(value!=null){
if(value.getClass().isArray()){
Arrays.stream((Object[]) value)
.forEach(v -> list.add(String.format("%s=%s", key, v)));
}else{
list.add(String.format("%s=%s", key, value));
}
}
});
return DigestUtils.sha1Hex(path+'?'+list.stream().sorted().collect(Collectors.joining("&")));
}
/**
* @return 随机字符串
*/
private static String nonce(){
String all = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
String num = "0123456789";
String letter = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
SecureRandom secureRandom = new SecureRandom();
StringBuilder uuid = new StringBuilder();
for (int j = 0 ;j<32;j++){
int i = secureRandom.nextInt(3);
int i1 = secureRandom.nextInt(10);
int i2 = secureRandom.nextInt(52);
int i3 = secureRandom.nextInt(62);
switch (i){
case 0:
String substring1= num.substring(i1,i1+1);
uuid.append(substring1);
break;
case 1:
String substring2= letter.substring(i2,i2+1);
uuid.append(substring2);
break;
case 2:
String substring3= all.substring(i3,i3+1);
uuid.append(substring3);
break;
default:
throw new RuntimeException();
}
}
return uuid.toString();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# 查询
接口地址: /doc/list/{indexName}
调用方式:POST
请求类型:application/json
请求数据:JSONObject
{
"from": "int 起始条目",
"rows": "int 返回记录数",
"conditions": "JSONObject 查询条件对象,允许为空,默认返回全部数据",
"source": "String[] 返回的字段列表 允许为空,默认返回全部字段",
"orderField": "String 排序字段名 允许为空",
"order": "排序方式 允许为空 默认ASC 可选DESC"
}
2
3
4
5
6
7
8
conditions 结构介绍
conditions 为Map结构
key是查询条件的标识位,可以是任意名称
value是查询条件构造器,ELCube的查询底层为ElasticSearch,因此查询条件构造器参考ES的DSL语法, 如match、multi_match、term、terms、range等
多个条件为并(&)的关系
ES官方文档
中文版:https://www.elastic.co/guide/cn/elasticsearch/guide/ 中文版 (opens new window)
请注意:中文版为较老版本的文档,DQL语法可以作为参考
英文版:https://www.elastic.co/guide/en/elasticsearch/reference/7.9 英文版 (opens new window)
一个常用的conditions对象可能如下,查询单据名称匹配"我的单据"的数据
{
"单据名称包含": {
"match": {
"docName" : "我的单据"
}
}
}
2
3
4
5
6
7
{
"单据类型": {
"terms": {
"docType": [
"D001",
"ZR01"
]
}
},
"更新时间": {
"range": {
"updatedTime": {
"from": 1681833600,
"to": 1682179199
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 单据
API接口可以对单据进行查看、创建、修改、计算操作,详见后续章节
单据的数据结构如下
{
"docId": "String 单据ID",
"docType": "String 单据类型KEY",
"docTypeDesc": "String 单据类型描述",
"docState": "String 单据状态KEY",
"docStateDesc": "String 单据状态描述",
"docName": "String 单据名称",
"docNumber": "String 单据编号",
"docDesc": "String 单据描述",
"docTags": "String[] 单据tags",
"preDocId": "String 前序单据ID",
"refObjectId": "String 根单据ID,如果与docId一致,标识当前单据为根单据",
"partnerId": "String 相关方ID",
"partnerName": "String 相关方名称",
"businessKey": "String 业务主键,用户自定义的单据主键",
"createdTime": "long 创建时间戳 单位秒",
"updatedTime": "long 最后修改时间戳 单位秒",
"dynamics": "JSONObject 单据索引的动态字段,key为字段名,value为字段值",
"data": "JSONObject 单据卡片数据,key为卡片key,value为卡片数据,结构由具体卡片决定,参考单据配置",
"bpmTask": "JSONObject 当前用户的待办任务",
"classify": "系统字段:单据类型分类 TRANSACTION",
"defVersion": "系统字段:单据配置版本",
"createdProducerId": "系统字段:创建的云节点ID",
"updatedProducerId": "系统字段:修改的云节点ID",
"processInstanceId": "系统字段:当前正在运行的,工作流实例ID",
"identification": "系统字段:单据修改标识位",
"runtimeKey": "系统字段:运行时KEY,可以理解为当前单据在服务端的上下文,在单据计算时,是必须的",
"items": "系统字段:单据的原始存储数据",
"def": "系统字段:JSONObject 单据配置数据",
"writeable": "系统字段:boolean 是否允许修改,仅对UI有效,Api可以强制修改",
"editMode": "系统字段:boolean 是否为编辑模式,仅对UI有效,编辑模式下,返回的配置信息会更多",
"viewed": "系统字段:boolean 是否为显示模式",
"newCreate": "系统字段:boolean 是否为新创建的单据,如果为true,说明单据尚未持久化"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
单据数据格式参考章节:
# 单据详情
接口地址: /doc/detail/{docId}
调用方式:GET
URL参数:docId 单据ID
接口返回:单据详情 单据数据结构参考 单据结构
# 创建单据
接口地址: /doc/create
调用方式:POST
请求类型:fromData
请求参数:docType 单据类型 非空
请求参数:preDocId 前序单据ID 可选
请求数据:单据详情 单据数据结构参考 单据结构
提示
创建单据接口不会持久化到数据库,仅仅只是创建了一个单据对象返回
持久化操作需要再次调用 修改单据 接口完成
# 修改单据
接口地址: /doc/update
调用方式:POST
请求类型:application/json
请求数据:单据详情 单据数据结构参考 单据结构
接口返回:单据详情 单据数据结构参考 单据结构
# 计算单据
接口地址: /doc/calculate
调用方式:POST
请求类型:application/json
请求数据:单据详情 单据数据结构参考 单据结构
接口返回:单据详情 单据数据结构参考 单据结构
什么时候需要调用计算单据接口
如果单据配置中,配置了表达式,或者配置了具有业务逻辑处理的卡片,
那么,在前端修改了单据数据以后,就需要调用计算单据接口,
单据计算会执行卡片、或配置中的表达式,对数据一致性进行后端运算
提示
单据计算接口不会持久化到数据库,仅仅只是将单据对象根据单据配置进行数据一致性计算,并返回
持久化操作需要再次调用 修改单据 接口完成