Commit 237d26eed5b6433d53c4de5ffff9ba3467599dc1
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)
Showing
3 changed files
with
52 additions
and
0 deletions
src/main/java/com/xly/service/ErpClient.java
| ... | ... | @@ -193,6 +193,26 @@ public class ErpClient { |
| 193 | 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 | 216 | private JsonNode doExamine(String moduleId, String billId, int iFlag, String tok) { |
| 197 | 217 | try { |
| 198 | 218 | String url = baseUrl + "/business/doExamine?sModelsId=" + moduleId; | ... | ... |
src/main/java/com/xly/web/OpController.java
| ... | ... | @@ -35,6 +35,10 @@ public class OpController { |
| 35 | 35 | private final AuditService audit; |
| 36 | 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 | 42 | public OpController(OpService ops, ErpClient erp, AuditService audit, ObjectMapper mapper) { |
| 39 | 43 | this.ops = ops; |
| 40 | 44 | this.erp = erp; |
| ... | ... | @@ -68,6 +72,11 @@ public class OpController { |
| 68 | 72 | String conv = str(op.get("sConversationId")); |
| 69 | 73 | String target = str(op.get("sTargetTable")) + "#" + str(op.get("sTargetBillId")); |
| 70 | 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 | 80 | try { |
| 72 | 81 | String opType = str(op.get("sOpType")); |
| 73 | 82 | JsonNode r; |
| ... | ... | @@ -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 | 134 | @PostMapping("/{id}/cancel") |
| 108 | 135 | public Map<String, Object> cancel(@PathVariable("id") String id) { | ... | ... |
src/main/resources/application-saaslocal.yml
| ... | ... | @@ -39,6 +39,11 @@ erp: |
| 39 | 39 | subsidiary: "1111111111" |
| 40 | 40 | username: admin |
| 41 | 41 | password: "666666" |
| 42 | + # 写入确认执行路径(架构 §10): | |
| 43 | + # false = xlyAi 直连 ERP 通用写接口执行(本地默认,已测)。 | |
| 44 | + # true = 委托 ERP 侧暂存执行器 /ai/execStaging(生产路径,以用户 token 身份执行、事务化、支持审核/预填/待办)。 | |
| 45 | + exec-staging: | |
| 46 | + enabled: false | |
| 42 | 47 | |
| 43 | 48 | # LLM 可观测(Langfuse,架构 §1)。默认关闭;自托管起来后填 key 开启: |
| 44 | 49 | # docker compose -f docker-compose.langfuse.yml up -d → http://localhost:3000 拿 public/secret key。 | ... | ... |