Commit 237d26eed5b6433d53c4de5ffff9ba3467599dc1

Authored by zichun
1 parent 7a3da4ee

feat: wire xlyAi confirm -> ERP staging executor (§10, config-gated) + ErpClient.execStaging

- ErpClient.execStaging(token, opId): POST /ai/execStaging/{opId} (user-token身份)
- OpController: erp.exec-staging.enabled -> delegate confirm to ERP executor; default false keeps tested direct path
- application-saaslocal.yml: erp.exec-staging.enabled=false (documented)
src/main/java/com/xly/service/ErpClient.java
@@ -193,6 +193,26 @@ public class ErpClient { @@ -193,6 +193,26 @@ public class ErpClient {
193 return root; 193 return root;
194 } 194 }
195 195
  196 + /**
  197 + * 委托 ERP 侧暂存执行器执行一条暂存写操作(架构 §10)。ERP 读共享库 {@code ai_op_queue} 行、以用户身份
  198 + * 执行并回写状态,返回 {@code {status, msg, billId}}。用户 token 必传(执行以用户身份进行)。
  199 + */
  200 + public JsonNode execStaging(String authToken, String opId) {
  201 + try {
  202 + String url = baseUrl + "/ai/execStaging/" + opId;
  203 + HttpRequest req = HttpRequest.newBuilder(URI.create(url))
  204 + .header("Content-Type", "application/json;charset=UTF-8")
  205 + .header("Authorization", resolveToken(authToken))
  206 + .timeout(Duration.ofSeconds(120))
  207 + .POST(HttpRequest.BodyPublishers.ofString("{}", StandardCharsets.UTF_8))
  208 + .build();
  209 + HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
  210 + return mapper.readTree(resp.body());
  211 + } catch (Exception e) {
  212 + throw new RuntimeException("ERP 暂存执行异常: " + e.getMessage(), e);
  213 + }
  214 + }
  215 +
196 private JsonNode doExamine(String moduleId, String billId, int iFlag, String tok) { 216 private JsonNode doExamine(String moduleId, String billId, int iFlag, String tok) {
197 try { 217 try {
198 String url = baseUrl + "/business/doExamine?sModelsId=" + moduleId; 218 String url = baseUrl + "/business/doExamine?sModelsId=" + moduleId;
src/main/java/com/xly/web/OpController.java
@@ -35,6 +35,10 @@ public class OpController { @@ -35,6 +35,10 @@ public class OpController {
35 private final AuditService audit; 35 private final AuditService audit;
36 private final ObjectMapper mapper; 36 private final ObjectMapper mapper;
37 37
  38 + /** true=确认后委托 ERP 侧暂存执行器(/ai/execStaging,§10 生产路径);false=xlyAi 直连 ERP 通用写接口执行。 */
  39 + @org.springframework.beans.factory.annotation.Value("${erp.exec-staging.enabled:false}")
  40 + private boolean execStagingEnabled;
  41 +
38 public OpController(OpService ops, ErpClient erp, AuditService audit, ObjectMapper mapper) { 42 public OpController(OpService ops, ErpClient erp, AuditService audit, ObjectMapper mapper) {
39 this.ops = ops; 43 this.ops = ops;
40 this.erp = erp; 44 this.erp = erp;
@@ -68,6 +72,11 @@ public class OpController { @@ -68,6 +72,11 @@ public class OpController {
68 String conv = str(op.get("sConversationId")); 72 String conv = str(op.get("sConversationId"));
69 String target = str(op.get("sTargetTable")) + "#" + str(op.get("sTargetBillId")); 73 String target = str(op.get("sTargetTable")) + "#" + str(op.get("sTargetBillId"));
70 String detail = str(op.get("sDescription")); 74 String detail = str(op.get("sDescription"));
  75 +
  76 + // 生产路径:委托 ERP 侧暂存执行器(它以用户身份执行、事务化并自行回写 ai_op_queue 状态)。
  77 + if (execStagingEnabled) {
  78 + return confirmViaExecutor(id, op, authToken, uid, conv, target, detail);
  79 + }
71 try { 80 try {
72 String opType = str(op.get("sOpType")); 81 String opType = str(op.get("sOpType"));
73 JsonNode r; 82 JsonNode r;
@@ -103,6 +112,24 @@ public class OpController { @@ -103,6 +112,24 @@ public class OpController {
103 } 112 }
104 } 113 }
105 114
  115 + /** 委托 ERP 侧暂存执行器执行(§10);执行器自行回写 ai_op_queue 状态,这里只映射结果 + 审计。 */
  116 + private Map<String, Object> confirmViaExecutor(String id, Map<String, Object> op, String authToken,
  117 + String uid, String conv, String target, String detail) {
  118 + try {
  119 + JsonNode r = erp.execStaging(authToken, id);
  120 + String st = r.path("status").asText("failed");
  121 + String msg = r.path("msg").asText("");
  122 + boolean ok = "executed".equals(st);
  123 + audit.log(uid, conv, "confirm", target, detail, ok, "execStaging:" + st + (msg.isEmpty() ? "" : (" " + msg)));
  124 + return result(ok ? "executed" : "failed",
  125 + ok ? ("已执行:" + op.get("sDescription")) : (msg.isEmpty() ? "执行失败" : msg), op);
  126 + } catch (Exception e) {
  127 + log.warn("execStaging confirm op {} failed", id, e);
  128 + audit.log(uid, conv, "confirm", target, detail, false, e.getMessage());
  129 + return result("failed", "执行异常:" + e.getMessage(), op);
  130 + }
  131 + }
  132 +
106 /** 取消。 */ 133 /** 取消。 */
107 @PostMapping("/{id}/cancel") 134 @PostMapping("/{id}/cancel")
108 public Map<String, Object> cancel(@PathVariable("id") String id) { 135 public Map<String, Object> cancel(@PathVariable("id") String id) {
src/main/resources/application-saaslocal.yml
@@ -39,6 +39,11 @@ erp: @@ -39,6 +39,11 @@ erp:
39 subsidiary: "1111111111" 39 subsidiary: "1111111111"
40 username: admin 40 username: admin
41 password: "666666" 41 password: "666666"
  42 + # 写入确认执行路径(架构 §10):
  43 + # false = xlyAi 直连 ERP 通用写接口执行(本地默认,已测)。
  44 + # true = 委托 ERP 侧暂存执行器 /ai/execStaging(生产路径,以用户 token 身份执行、事务化、支持审核/预填/待办)。
  45 + exec-staging:
  46 + enabled: false
42 47
43 # LLM 可观测(Langfuse,架构 §1)。默认关闭;自托管起来后填 key 开启: 48 # LLM 可观测(Langfuse,架构 §1)。默认关闭;自托管起来后填 key 开启:
44 # docker compose -f docker-compose.langfuse.yml up -d → http://localhost:3000 拿 public/secret key。 49 # docker compose -f docker-compose.langfuse.yml up -d → http://localhost:3000 拿 public/secret key。