ErpClient.java
19 KB
1
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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
package com.xly.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
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.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* ERP 后端(xlyEntry)薄 HTTP 客户端。
*
* <p>xlyAi 的读写业务逻辑在 ERP 后端,xlyAi 只是像前端一样发同样的 API 请求。本类负责:
* <ul>
* <li>取得可用的 ERP 会话 token —— 本地开发用配置的 dev 账号 {@code /checklogin} 登录并缓存;
* 生产应改为透传用户浏览器里的 ERP 登录 token(per-request,never stored)。</li>
* <li>调用通用表单读接口 {@code getBusinessDataByFormcustomId};会话过期(code=-2)自动重登一次重试。</li>
* </ul>
*/
@Service
public class ErpClient {
private static final Logger log = LoggerFactory.getLogger(ErpClient.class);
private final ObjectMapper mapper;
private final HttpClient http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
@Value("${erp.baseurl}")
private String baseUrl;
@Value("${erp.dev-login.brand:1111111111}")
private String brand;
@Value("${erp.dev-login.subsidiary:1111111111}")
private String subsidiary;
@Value("${erp.dev-login.username:admin}")
private String username;
@Value("${erp.dev-login.password:666666}")
private String password;
private volatile String cachedToken;
public ErpClient(ObjectMapper mapper) {
this.mapper = mapper;
}
/** 用配置的 dev 账号登录 ERP,缓存返回的 Authorization token。 */
private synchronized String login() {
try {
String url = baseUrl + "/checklogin/" + brand + "/" + subsidiary;
String body = mapper.writeValueAsString(Map.of("username", username, "password", password));
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json;charset=UTF-8")
.timeout(Duration.ofSeconds(20))
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
JsonNode root = mapper.readTree(resp.body());
if (root.path("code").asInt() != 1) {
throw new IllegalStateException("ERP 登录失败: " + root.path("msg").asText());
}
String token = root.path("dataset").path("rows").path(0).path("token").asText(null);
if (token == null || token.isBlank()) {
throw new IllegalStateException("ERP 登录未返回 token");
}
cachedToken = token;
log.info("ERP dev-login ok (user={}), token cached", username);
return token;
} catch (Exception e) {
throw new RuntimeException("ERP 登录异常: " + e.getMessage(), e);
}
}
private String token() {
String t = cachedToken;
return (t != null && !t.isBlank()) ? t : login();
}
/** 解析本次调用要用的 token:优先透传的用户 token,否则 dev-login。 */
private String resolveToken(String override) {
return (override != null && !override.isBlank()) ? override : token();
}
/**
* 是否允许在 code=-2(会话过期)时重登重试。
* <p><b>安全</b>:只有 dev-login(override 为空)才允许重登;透传的用户 token 过期时
* <b>绝不</b>用 dev(admin) 重登——否则会把某用户静默提权成管理员。用户 token 过期直接把 -2 返回,
* 由上层向对话推「登录过期」。
*/
private boolean canRelogin(String override) {
return override == null || override.isBlank();
}
/** 读取某表单一页数据(dev-login token,兼容旧调用)。 */
public JsonNode readForm(String formId, String moduleId, int page, int pageSize,
String filterField, String filterValue) {
return readForm(null, formId, moduleId, page, pageSize, filterField, filterValue);
}
/**
* 读取某表单一页数据,返回整个响应根节点(含 code / msg / dataset)。
* {@code authToken} 为透传的用户 token(可空 → dev-login)。dev-login 会话过期(code=-2)时自动重登重试;
* 用户 token 过期不重登(见 {@link #canRelogin})。
*/
public JsonNode readForm(String authToken, String formId, String moduleId, int page, int pageSize,
String filterField, String filterValue) {
JsonNode root = doRead(formId, moduleId, page, pageSize, filterField, filterValue, resolveToken(authToken));
if (root.path("code").asInt() == -2 && canRelogin(authToken)) {
login();
root = doRead(formId, moduleId, page, pageSize, filterField, filterValue, resolveToken(authToken));
}
return root;
}
private JsonNode doRead(String formId, String moduleId, int page, int pageSize,
String filterField, String filterValue, String tok) {
try {
String url = baseUrl + "/business/getBusinessDataByFormcustomId/" + formId
+ "?sModelsId=" + moduleId + "&sName=";
List<Map<String, Object>> bFilter = new ArrayList<>();
if (filterField != null && !filterField.isBlank() && filterValue != null && !filterValue.isBlank()) {
Map<String, Object> f = new LinkedHashMap<>();
f.put("bFilterName", filterField);
f.put("bFilterCondition", "like");
f.put("bFilterValue", filterValue.trim());
bFilter.add(f);
}
String body = mapper.writeValueAsString(Map.of(
"pageNum", page, "pageSize", pageSize, "bFilter", bFilter));
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json;charset=UTF-8")
.header("Authorization", tok)
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return mapper.readTree(resp.body());
} catch (Exception e) {
throw new RuntimeException("ERP 读取异常: " + e.getMessage(), e);
}
}
/**
* 执行一次字段更新(addUpdateDelBusinessData, handleType=update)。会话过期自动重登重试。
* 请求体格式与 ERP 前端一致:{@code {data:[{sTable, name:"master", column:[{handleType:"update", sId, field:value}]}]}}。
*/
public JsonNode updateForm(String moduleId, String table, String billId, String field, String value) {
return updateForm(null, moduleId, table, billId, field, value);
}
public JsonNode updateForm(String authToken, String moduleId, String table, String billId, String field, String value) {
JsonNode root = doUpdate(moduleId, table, billId, field, value, resolveToken(authToken));
if (root.path("code").asInt() == -2 && canRelogin(authToken)) {
login();
root = doUpdate(moduleId, table, billId, field, value, resolveToken(authToken));
}
return root;
}
/** 删除一条记录(addUpdateDelBusinessData, handleType=del)。会话过期自动重登重试。 */
public JsonNode deleteForm(String moduleId, String table, String billId) {
return deleteForm(null, moduleId, table, billId);
}
public JsonNode deleteForm(String authToken, String moduleId, String table, String billId) {
JsonNode root = doDelete(moduleId, table, billId, resolveToken(authToken));
if (root.path("code").asInt() == -2 && canRelogin(authToken)) {
login();
root = doDelete(moduleId, table, billId, resolveToken(authToken));
}
return root;
}
/**
* 审核 / 反审核一条单据(ERP {@code /business/doExamine})。{@code iFlag}=1 审核、0 反审核(消审)。
* 审核逻辑由 ERP 按表单数据驱动的存储过程执行({@code gdsmodule.sProcName})。会话过期自动重登重试。
*/
public JsonNode examineForm(String authToken, String moduleId, String billId, int iFlag) {
JsonNode root = doExamine(moduleId, billId, iFlag, resolveToken(authToken));
if (root.path("code").asInt() == -2 && canRelogin(authToken)) {
login();
root = doExamine(moduleId, billId, iFlag, resolveToken(authToken));
}
return root;
}
/**
* 委托 ERP 侧暂存执行器执行一条暂存写操作(架构 §10)。ERP 读共享库 {@code ai_op_queue} 行、以用户身份
* 执行并回写状态,返回 {@code {status, msg, billId}}。用户 token 必传(执行以用户身份进行)。
*/
public JsonNode execStaging(String authToken, String opId) {
try {
String url = baseUrl + "/ai/execStaging/" + opId;
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json;charset=UTF-8")
.header("Authorization", resolveToken(authToken))
.timeout(Duration.ofSeconds(120))
.POST(HttpRequest.BodyPublishers.ofString("{}", StandardCharsets.UTF_8))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return mapper.readTree(resp.body());
} catch (Exception e) {
throw new RuntimeException("ERP 暂存执行异常: " + e.getMessage(), e);
}
}
private JsonNode doExamine(String moduleId, String billId, int iFlag, String tok) {
try {
String url = baseUrl + "/business/doExamine?sModelsId=" + moduleId;
Map<String, Object> paramsMap = new LinkedHashMap<>();
paramsMap.put("sFormGuid", moduleId);
paramsMap.put("sGuid", billId);
paramsMap.put("iFlag", iFlag);
paramsMap.put("sSlaveId", "");
String body = mapper.writeValueAsString(Map.of("paramsMap", paramsMap));
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json;charset=UTF-8")
.header("Authorization", tok)
.timeout(Duration.ofSeconds(60))
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return mapper.readTree(resp.body());
} catch (Exception e) {
throw new RuntimeException("ERP 审核异常: " + e.getMessage(), e);
}
}
private JsonNode doDelete(String moduleId, String table, String billId, String tok) {
try {
String url = baseUrl + "/business/addUpdateDelBusinessData?sModelsId=" + moduleId;
Map<String, Object> col = new LinkedHashMap<>();
col.put("handleType", "del");
col.put("sId", billId);
Map<String, Object> dataItem = new LinkedHashMap<>();
dataItem.put("sTable", table);
dataItem.put("name", "master");
dataItem.put("column", List.of(col));
String body = mapper.writeValueAsString(Map.of("data", List.of(dataItem)));
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json;charset=UTF-8")
.header("Authorization", tok)
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return mapper.readTree(resp.body());
} catch (Exception e) {
throw new RuntimeException("ERP 删除异常: " + e.getMessage(), e);
}
}
/** 取一个新主键 uuid(ERP `/getUuid`)。 */
public String newUuid() {
return newUuid(null);
}
public String newUuid(String authToken) {
try {
HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + "/getUuid"))
.header("Authorization", resolveToken(authToken))
.timeout(Duration.ofSeconds(15))
.GET()
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
JsonNode root = mapper.readTree(resp.body());
return root.path("dataset").path("rows").path(0).asText(null);
} catch (Exception e) {
throw new RuntimeException("取 uuid 失败: " + e.getMessage(), e);
}
}
/**
* 新增一条记录。走 {@code addUpdateDelBusinessData?sModelsId=}(handleType=add)——与 ERP 前端一致,
* 复用校验/单号/租户;{@code columns} 里已由 ProposeWrite 备好 sId/sFormId/sBillNo 与类型化字段值。
* 会话过期自动重登重试(仅 dev-login)。
*/
public JsonNode createForm(String authToken, String moduleId, String table, Map<String, Object> columns) {
JsonNode root = doCreate(moduleId, table, columns, resolveToken(authToken));
if (root.path("code").asInt() == -2 && canRelogin(authToken)) {
login();
root = doCreate(moduleId, table, columns, resolveToken(authToken));
}
return root;
}
/**
* 多表联动新增(主-从:报价 = 主表 + 印刷/部件从表 + 多数量)。{@code tables} 每项 = {@code {sTable, name, column(map)}},
* 本方法给每行补 handleType=add 并一次性 POST addUpdateDelBusinessData,从表靠 sParentId 关联主表。
*/
public JsonNode createMulti(String authToken, String moduleId, List<Map<String, Object>> tables) {
JsonNode root = doCreateMulti(moduleId, tables, resolveToken(authToken));
if (root.path("code").asInt() == -2 && canRelogin(authToken)) {
login();
root = doCreateMulti(moduleId, tables, resolveToken(authToken));
}
return root;
}
@SuppressWarnings("unchecked")
private JsonNode doCreateMulti(String moduleId, List<Map<String, Object>> tables, String tok) {
try {
String url = baseUrl + "/business/addUpdateDelBusinessData?sModelsId=" + moduleId;
List<Map<String, Object>> data = new ArrayList<>();
for (Map<String, Object> t : tables) {
Map<String, Object> col = new LinkedHashMap<>((Map<String, Object>) t.get("column"));
col.put("handleType", "add");
Map<String, Object> item = new LinkedHashMap<>();
item.put("sTable", t.get("sTable"));
item.put("name", t.get("name"));
item.put("column", List.of(col));
data.add(item);
}
Map<String, Object> body = new LinkedHashMap<>();
body.put("sModelsId", moduleId);
body.put("data", data);
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json;charset=UTF-8")
.header("Authorization", tok)
.timeout(Duration.ofSeconds(40))
.POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body), StandardCharsets.UTF_8))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return mapper.readTree(resp.body());
} catch (Exception e) {
throw new RuntimeException("ERP 多表新增异常: " + e.getMessage(), e);
}
}
private JsonNode doCreate(String moduleId, String table, Map<String, Object> columns, String tok) {
try {
String url = baseUrl + "/business/addUpdateDelBusinessData?sModelsId=" + moduleId;
Map<String, Object> col = new LinkedHashMap<>(columns);
col.put("handleType", "add");
Map<String, Object> dataItem = new LinkedHashMap<>();
dataItem.put("sTable", table);
dataItem.put("name", "master");
dataItem.put("column", List.of(col));
Map<String, Object> body = new LinkedHashMap<>();
body.put("sModelsId", moduleId);
body.put("data", List.of(dataItem));
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json;charset=UTF-8")
.header("Authorization", tok)
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body), StandardCharsets.UTF_8))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return mapper.readTree(resp.body());
} catch (Exception e) {
throw new RuntimeException("ERP 新增异常: " + e.getMessage(), e);
}
}
private JsonNode doUpdate(String moduleId, String table, String billId, String field, String value, String tok) {
try {
String url = baseUrl + "/business/addUpdateDelBusinessData?sModelsId=" + moduleId;
Map<String, Object> col = new LinkedHashMap<>();
col.put("handleType", "update");
col.put("sId", billId);
col.put(field, value);
Map<String, Object> dataItem = new LinkedHashMap<>();
dataItem.put("sTable", table);
dataItem.put("name", "master");
dataItem.put("column", List.of(col));
String body = mapper.writeValueAsString(Map.of("data", List.of(dataItem)));
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json;charset=UTF-8")
.header("Authorization", tok)
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return mapper.readTree(resp.body());
} catch (Exception e) {
throw new RuntimeException("ERP 更新异常: " + e.getMessage(), e);
}
}
}