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 客户端。 * *

xlyAi 的读写业务逻辑在 ERP 后端,xlyAi 只是像前端一样发同样的 API 请求。本类负责: *

*/ @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 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(会话过期)时重登重试。 *

安全:只有 dev-login(override 为空)才允许重登;透传的用户 token 过期时 * 绝不用 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> bFilter = new ArrayList<>(); if (filterField != null && !filterField.isBlank() && filterValue != null && !filterValue.isBlank()) { Map 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 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 body = new LinkedHashMap<>(); body.put("sId", List.of(billId)); // 后端 sId 期望 List 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 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 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 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 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 col = new LinkedHashMap<>(); col.put("handleType", "del"); col.put("sId", billId); Map 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 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 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 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> 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> tables, String tok) { try { String url = baseUrl + "/business/addUpdateDelBusinessData?sModelsId=" + moduleId; List> data = new ArrayList<>(); for (Map t : tables) { Map col = new LinkedHashMap<>((Map) t.get("column")); col.put("handleType", "add"); Map 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 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 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 columns, String tok) { try { String url = baseUrl + "/business/addUpdateDelBusinessData?sModelsId=" + moduleId; Map col = new LinkedHashMap<>(columns); col.put("handleType", "add"); Map dataItem = new LinkedHashMap<>(); dataItem.put("sTable", table); dataItem.put("name", "master"); dataItem.put("column", List.of(col)); Map 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 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 col = new LinkedHashMap<>(); col.put("handleType", "update"); col.put("sId", billId); col.put(field, value); Map 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 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); } } }