ErpClient.java
22.1 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
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
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><b>token 解析(架构 §7)</b>:每次调用优先用调用方**透传的用户 ERP 会话 token**
* (per-request、不长期存、绝不进 prompt);只有在完全没有用户 token 时才退回配置的 dev 账号
* {@code /checklogin} 登录并缓存。<b>用户 token 过期不会用 dev 账号重登</b>(见 {@code canRelogin}),
* 避免静默提权;只有 dev-login 自己的会话过期(code=-2)才自动重登一次重试。</li>
* <li><b>读</b>:通用表单读接口 {@code getBusinessDataByFormcustomId}(只传分页/过滤参数)。</li>
* <li><b>写</b>:{@code addUpdateDelBusinessData}(handleType=add/update/del,含多表主-从一次提交)、
* {@code updatebInvalid}(作废/复原)、{@code doExamine}(审核/销审)、{@code getUuid}(取主键)。</li>
* <li><b>暂存执行器</b>:{@code /ai/execStaging/{sId}}(生产路径,由 {@code erp.exec-staging.enabled} 开关)。</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 {@code /checkModel/updatebInvalid} → 存储过程 {@code Sp_Invalidation})。
* {@code cancel=false} → 作废(handleType=toVoid);{@code cancel=true} → 取消作废/复原(handleType=cancel)。
* 业务单据的“删除”应走这里(软作废,可复原),而非物理删。会话过期自动重登重试。
*/
public JsonNode invalidForm(String authToken, String moduleId, String table, String billId, boolean cancel) {
JsonNode root = doInvalid(moduleId, table, billId, cancel, resolveToken(authToken));
if (root.path("code").asInt() == -2 && canRelogin(authToken)) {
login();
root = doInvalid(moduleId, table, billId, cancel, resolveToken(authToken));
}
return root;
}
private JsonNode doInvalid(String moduleId, String table, String billId, boolean cancel, String tok) {
try {
String url = baseUrl + "/checkModel/updatebInvalid?sModelsId=" + moduleId;
Map<String, Object> body = new LinkedHashMap<>();
body.put("sId", List.of(billId)); // 后端 sId 期望 List<String>
body.put("sTableName", table);
body.put("handleType", cancel ? "cancel" : "toVoid");
body.put("sClientType", "PC");
body.put("sComputeName", "AI-Agent");
body.put("sIpAddress", "127.0.0.1");
body.put("sLanguage", "chinese");
String json = mapper.writeValueAsString(body);
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(json, 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);
}
}
/**
* 委托 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));
JsonNode root = mapper.readTree(resp.body());
if (root.has("status")) {
return root;
}
// ERP 统一响应包装:真实结果 {status,msg,billId} 在 dataset.rows[0]
JsonNode row = root.path("dataset").path("rows").path(0);
return row.isObject() ? row : root;
} 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);
}
}
}