From 237d26eed5b6433d53c4de5ffff9ba3467599dc1 Mon Sep 17 00:00:00 2001 From: zichun <26684461+reporkey@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:24:20 +0800 Subject: [PATCH] feat: wire xlyAi confirm -> ERP staging executor (§10, config-gated) + ErpClient.execStaging --- src/main/java/com/xly/service/ErpClient.java | 20 ++++++++++++++++++++ src/main/java/com/xly/web/OpController.java | 27 +++++++++++++++++++++++++++ src/main/resources/application-saaslocal.yml | 5 +++++ 3 files changed, 52 insertions(+), 0 deletions(-) diff --git a/src/main/java/com/xly/service/ErpClient.java b/src/main/java/com/xly/service/ErpClient.java index 5009b2b..81b4204 100644 --- a/src/main/java/com/xly/service/ErpClient.java +++ b/src/main/java/com/xly/service/ErpClient.java @@ -193,6 +193,26 @@ public class ErpClient { 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 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; diff --git a/src/main/java/com/xly/web/OpController.java b/src/main/java/com/xly/web/OpController.java index 6f2ce37..784a359 100644 --- a/src/main/java/com/xly/web/OpController.java +++ b/src/main/java/com/xly/web/OpController.java @@ -35,6 +35,10 @@ public class OpController { private final AuditService audit; private final ObjectMapper mapper; + /** true=确认后委托 ERP 侧暂存执行器(/ai/execStaging,§10 生产路径);false=xlyAi 直连 ERP 通用写接口执行。 */ + @org.springframework.beans.factory.annotation.Value("${erp.exec-staging.enabled:false}") + private boolean execStagingEnabled; + public OpController(OpService ops, ErpClient erp, AuditService audit, ObjectMapper mapper) { this.ops = ops; this.erp = erp; @@ -68,6 +72,11 @@ public class OpController { String conv = str(op.get("sConversationId")); String target = str(op.get("sTargetTable")) + "#" + str(op.get("sTargetBillId")); String detail = str(op.get("sDescription")); + + // 生产路径:委托 ERP 侧暂存执行器(它以用户身份执行、事务化并自行回写 ai_op_queue 状态)。 + if (execStagingEnabled) { + return confirmViaExecutor(id, op, authToken, uid, conv, target, detail); + } try { String opType = str(op.get("sOpType")); JsonNode r; @@ -103,6 +112,24 @@ public class OpController { } } + /** 委托 ERP 侧暂存执行器执行(§10);执行器自行回写 ai_op_queue 状态,这里只映射结果 + 审计。 */ + private Map confirmViaExecutor(String id, Map op, String authToken, + String uid, String conv, String target, String detail) { + try { + JsonNode r = erp.execStaging(authToken, id); + String st = r.path("status").asText("failed"); + String msg = r.path("msg").asText(""); + boolean ok = "executed".equals(st); + audit.log(uid, conv, "confirm", target, detail, ok, "execStaging:" + st + (msg.isEmpty() ? "" : (" " + msg))); + return result(ok ? "executed" : "failed", + ok ? ("已执行:" + op.get("sDescription")) : (msg.isEmpty() ? "执行失败" : msg), op); + } catch (Exception e) { + log.warn("execStaging confirm op {} failed", id, e); + audit.log(uid, conv, "confirm", target, detail, false, e.getMessage()); + return result("failed", "执行异常:" + e.getMessage(), op); + } + } + /** 取消。 */ @PostMapping("/{id}/cancel") public Map cancel(@PathVariable("id") String id) { diff --git a/src/main/resources/application-saaslocal.yml b/src/main/resources/application-saaslocal.yml index 79c2741..24e421a 100644 --- a/src/main/resources/application-saaslocal.yml +++ b/src/main/resources/application-saaslocal.yml @@ -39,6 +39,11 @@ erp: subsidiary: "1111111111" username: admin password: "666666" + # 写入确认执行路径(架构 §10): + # false = xlyAi 直连 ERP 通用写接口执行(本地默认,已测)。 + # true = 委托 ERP 侧暂存执行器 /ai/execStaging(生产路径,以用户 token 身份执行、事务化、支持审核/预填/待办)。 + exec-staging: + enabled: false # LLM 可观测(Langfuse,架构 §1)。默认关闭;自托管起来后填 key 开启: # docker compose -f docker-compose.langfuse.yml up -d → http://localhost:3000 拿 public/secret key。 -- libgit2 0.22.2