package com.xly.web; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.service.AuditService; import com.xly.service.ErpClient; import com.xly.service.OpService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.LinkedHashMap; import java.util.Map; /** * 写操作确认端点(**确定性、不经过 LLM**)—— 人在环写入闭环的执行侧。 * *

用户在对话框点【确认】-> {@code /confirm} 才真正调 ERP 执行;点【取消】-> {@code /cancel}。 * v1 为同步执行(update 很快):直接调 ERP 表单更新接口,回写 ai_op_queue 状态并返回结果。 */ @RestController @RequestMapping("/api/agent/op") public class OpController { private static final Logger log = LoggerFactory.getLogger(OpController.class); private final OpService ops; private final ErpClient erp; 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; this.audit = audit; this.mapper = mapper; } /** 会话里最近一条待确认操作(前端每轮结束后轮询,用于渲染确认卡片)。 */ @GetMapping("/pending") public Map pending(@RequestParam("conversationId") String conversationId) { Map op = ops.pending(conversationId); return op == null ? Map.of() : op; } /** * 确认执行:调 ERP 执行暂存的写操作,回写状态。 *

{@code Authorization} 头(可空)= 用户浏览器里的 ERP 登录 token,透传给 ERP 使执行以用户身份 * 进行;为空则回退 dev-login。绝不因用户 token 缺失而静默提权(见 ErpClient.canRelogin)。 */ @PostMapping("/{id}/confirm") public Map confirm(@PathVariable("id") String id, @RequestHeader(value = "Authorization", required = false) String authToken) { Map op = ops.get(id); if (op == null) { return result("failed", "找不到该操作", null); } if (!"draft".equals(String.valueOf(op.get("sStatus")))) { return result(String.valueOf(op.get("sStatus")), "该操作已处理过(" + op.get("sStatus") + ")", op); } String uid = str(op.get("sUserId")); 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; if ("create".equals(opType)) { @SuppressWarnings("unchecked") Map columns = mapper.readValue(str(op.get("sPayload")), Map.class); r = erp.createForm(authToken, str(op.get("sTargetTable")), columns); } else if ("delete".equals(opType)) { r = erp.deleteForm(authToken, str(op.get("sTargetModuleId")), str(op.get("sTargetTable")), str(op.get("sTargetBillId"))); } else if ("examine".equals(opType)) { int iFlag = "0".equals(str(op.get("sNewValue"))) ? 0 : 1; // 1=审核 0=反审核 r = erp.examineForm(authToken, str(op.get("sTargetModuleId")), str(op.get("sTargetBillId")), iFlag); } else { r = erp.updateForm(authToken, str(op.get("sTargetModuleId")), str(op.get("sTargetTable")), str(op.get("sTargetBillId")), str(op.get("sField")), str(op.get("sNewValue"))); } int code = r.path("code").asInt(0); if (code == 1) { ops.setStatus(id, "executed", "操作成功"); audit.log(uid, conv, "confirm", target, detail, true, "executed"); return result("executed", "已执行:" + op.get("sDescription"), op); } String msg = r.path("msg").asText("执行失败"); ops.setStatus(id, "failed", msg); audit.log(uid, conv, "confirm", target, detail, false, msg); return result("failed", msg, op); } catch (Exception e) { log.warn("confirm op {} failed", id, e); ops.setStatus(id, "failed", e.getMessage()); audit.log(uid, conv, "confirm", target, detail, false, e.getMessage()); return result("failed", "执行异常:" + e.getMessage(), op); } } /** 委托 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) { Map op = ops.get(id); if (op != null && "draft".equals(String.valueOf(op.get("sStatus")))) { ops.setStatus(id, "cancelled", "用户取消"); audit.log(str(op.get("sUserId")), str(op.get("sConversationId")), "cancel", str(op.get("sTargetTable")) + "#" + str(op.get("sTargetBillId")), str(op.get("sDescription")), true, "cancelled"); } return result("cancelled", "已取消", op); } private Map result(String status, String msg, Map op) { Map m = new LinkedHashMap<>(); m.put("status", status); m.put("msg", msg); if (op != null) { m.put("opId", op.get("sId")); m.put("description", op.get("sDescription")); } return m; } private static String str(Object o) { return o == null ? null : o.toString(); } }