From 9f1f7fc014f4df7aa8f65590135ffd1fe4471b6d Mon Sep 17 00:00:00 2001 From: zichun <26684461+reporkey@users.noreply.github.com> Date: Wed, 29 Jul 2026 10:17:48 +0800 Subject: [PATCH] op-queue status protocol: 3-state sStatus (pending/success/fail) + iStatusCode (100/101, 200/201, 4xx/5xx error taxonomy); xlyAi writes pending+100 only; legacy values read-compat; migration maps existing rows --- docs/agent-architecture.md | 6 +++++- sql/ai_op_queue.sql | 14 ++++++++++++-- sql/migrate_rearch3.sql | 11 ++++++++++- src/main/java/com/xly/service/OpService.java | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/docs/agent-architecture.md b/docs/agent-architecture.md index d62f1fd..1dc4e66 100644 --- a/docs/agent-architecture.md +++ b/docs/agent-architecture.md @@ -485,8 +485,12 @@ ai_op_queue( - **确定性保存端点**(不经 LLM):`POST /api/agent/preview/{id}/save`(fields=技术名→值, 空值=放弃该字段的改动)与 `POST /api/agent/form/submit`(create)。流程:归属校验 → 权限重查 → **重读记录重校验**(值/状态被他人改过 → 拒绝,"所见即所写")→ **原子 claim previewId**(防并发/ - 重复提交)→ 写 `ai_op_queue` 一行 `sStatus='confirmed'`(多字段=多行、单事务)→ 落 `queued` 事件。 + 重复提交)→ 写 `ai_op_queue` 一行 `pending+100`(多字段=多行、单事务)→ 落 `queued` 事件。 **xlyAi 到此为止**:执行/审计/权属全部移交 ERP 侧(执行器读本表;现状=报价自动执行、其余进待办)。 + **状态协议 = sStatus 三态 + iStatusCode**(2026-07-29 定,码表见 `sql/ai_op_queue.sql` 头注): + pending(100 已提交/101 ERP 处理中)、success(200/201 幂等命中)、fail(400 载荷/401 权限/404 不存在/ + 409 状态冲突/422 业务校验/423 锁定/450 已取消/500 异常/504 超时);xlyAi 只写 pending+100, + 其余全由 ERP 回写;旧值 draft/confirmed/executed/failed/cancelled 兼容读不再写。 - 退役:OpController(confirm/cancel/pending)、AuditService、ErpClient 全部写方法 + execStaging。 ErpClient 只剩 whoami / readForm / getUuid / checkBusinessData(dry-run 接口位)。 - **状态合法性硬检查**(预览+保存两次,防 TOCTOU;依据已核实的存储过程行为):审核要求未审核、 diff --git a/sql/ai_op_queue.sql b/sql/ai_op_queue.sql index e1a384e..4c3fbc9 100644 --- a/sql/ai_op_queue.sql +++ b/sql/ai_op_queue.sql @@ -1,7 +1,16 @@ -- ai_op_queue:AI 写操作队列(rearch3 §3:**AI 侧唯一的写入口**)。 --- 用户在预览卡/表单上点按钮(保存/审核/作废/…)→ xlyAi 校验后落一行 sStatus='confirmed'。 +-- 用户在预览卡/表单上点按钮(保存/审核/作废/…)→ xlyAi 校验后落一行 pending+100。 -- xlyAi 工作到此为止:执行/审计由 ERP 侧负责(暂存执行器 /ai/execStaging 读本表; -- 现状=报价自动执行、其余进待办)。覆盖 update / create / delete / examine / invalid(sOpType 区分)。 +-- +-- 状态协议(2026-07-29 定)= sStatus 三态 + iStatusCode 状态码: +-- pending:100=已提交等待 ERP 处理(xlyAi 写入的唯一状态)|101=ERP 处理中(已领取,防并发重复执行) +-- success:200=执行成功|201=幂等命中(此前已执行,本次返回既有结果) +-- fail :400=载荷/参数非法|401=身份/权限不足|404=目标记录不存在|409=状态冲突(重复审核/ +-- 重复作废/未审核销审/记录被他人改过)|422=业务校验未通过|423=记录被锁定(SysLocking)| +-- 450=已取消(ERP 端取消待办)|500=执行异常|504=执行超时 +-- pending 由 xlyAi 写入(100)与 ERP 领取时更新(101);success/fail 只由 ERP 回写。 +-- 旧值 draft/confirmed/executing/executed/failed/cancelled 已废弃(xlyAi 兼容读,不再写)。 -- update:sTargetBillId + sField/sNewValue(单字段;一次保存多字段 = 多行)。 -- invalid:sNewValue 存 handleType(toVoid=作废 / cancel=复原);examine:sNewValue 存 iFlag(1/0)。 -- create:整表 payload 存 sPayload(列->值 JSON,多表用 __tables__)。 @@ -26,7 +35,8 @@ CREATE TABLE IF NOT EXISTS ai_op_queue ( sSourceRef text, -- generate:源表单 + 选中明细 sId 集合 JSON bAutoExecute tinyint NOT NULL DEFAULT 0, -- 1=ERP 确认后自动执行;0=预填表单等用户提交 sDescription varchar(500), -- 人类可读的改动描述 - sStatus varchar(16), -- draft | confirmed | executing | executed | failed | cancelled | expired + sStatus varchar(16), -- pending | success | fail(状态码见头部注释) + iStatusCode int NULL, -- 状态码(100/101/200/201/4xx/5xx) sResultBillId varchar(64), -- 执行后生成/影响的单据 id sResultMsg varchar(500), sErrorMsg varchar(1000), diff --git a/sql/migrate_rearch3.sql b/sql/migrate_rearch3.sql index 3b7246a..a973db7 100644 --- a/sql/migrate_rearch3.sql +++ b/sql/migrate_rearch3.sql @@ -1,7 +1,16 @@ -- rearch3 部署迁移(对**已有** ai_op_queue 的库执行;新库直接跑 ai_op_queue.sql 即可)。 -- 与部署同批执行的完整清单: --- 1) 本文件(ai_op_queue 加 sMakePerson 双写列——ERP 执行器仍读 sUserId,切换后可删旧列) +-- 1) 本文件(ai_op_queue 加 sMakePerson 双写列 + 状态协议改 状态三态+状态码) -- 2) sql/ai_chat_event.sql (会话事件账本,新表) -- 3) sql/ai_skill.sql (DROP 旧草稿表重建;⚠️ 建表瞬间即切断 war 包技能读取) -- 4) sql/ai_skill_seed.sql (默认技能种子,必须与 3 同批执行) ALTER TABLE ai_op_queue ADD COLUMN sMakePerson varchar(64) NULL AFTER sUserId; + +-- 状态协议(状态码表见 ai_op_queue.sql 头部注释):sStatus 三态 pending|success|fail + iStatusCode +ALTER TABLE ai_op_queue ADD COLUMN iStatusCode int NULL AFTER sStatus; +-- 存量行按旧→新映射(cancelled→fail+450;draft/confirmed→pending+100) +UPDATE ai_op_queue SET sStatus='pending', iStatusCode=100 WHERE sStatus IN ('draft','confirmed'); +UPDATE ai_op_queue SET sStatus='pending', iStatusCode=101 WHERE sStatus='executing'; +UPDATE ai_op_queue SET sStatus='success', iStatusCode=200 WHERE sStatus='executed'; +UPDATE ai_op_queue SET sStatus='fail', iStatusCode=500 WHERE sStatus='failed'; +UPDATE ai_op_queue SET sStatus='fail', iStatusCode=450 WHERE sStatus='cancelled'; diff --git a/src/main/java/com/xly/service/OpService.java b/src/main/java/com/xly/service/OpService.java index 641e951..d2d98d5 100644 --- a/src/main/java/com/xly/service/OpService.java +++ b/src/main/java/com/xly/service/OpService.java @@ -10,11 +10,20 @@ import java.util.Map; /** * ai_op_queue 队列写入 —— **AI 侧唯一的写入口**(rearch3 §3 写路径收权)。 * - *

用户在预览卡/表单上点按钮(保存/审核/作废…)→ 确定性端点校验后经本类落一行 - * {@code sStatus='confirmed'}(用户已当面授权)。**xlyAi 工作到此为止**:是否/何时执行、 - * 执行权属、审计留痕全部由 ERP 侧负责(暂存执行器读本表;现状=报价自动执行、其余进待办)。 + *

用户在预览卡/表单上点按钮(保存/审核/作废…)→ 确定性端点校验后经本类落一行队列。 + * **xlyAi 工作到此为止**:是否/何时执行、执行权属、审计留痕全部由 ERP 侧负责 + * (暂存执行器读本表;现状=报价自动执行、其余进待办)。 * xlyAi 侧只保留 {@link #statusLabel} 只读展示处理进度(流程卡)。 * + *

状态协议 = sStatus 三态 + iStatusCode 状态码(2026-07-29 定): + *

+ * 旧值(draft/confirmed/executing/executed/failed/cancelled)兼容读(见 statusLabel),不再写。 + * *

列名兼容:ERP 执行器仍读 {@code sUserId} → 新列 {@code sMakePerson}(ERP 惯例)双写, * ERP 侧切换后可删 sUserId。 */ @@ -67,6 +76,7 @@ public class OpService { null, null, null, null, payload, description); } + /** xlyAi 写入的唯一状态:pending+100(已提交,等待 ERP 处理)。 */ private String insert(AgentIdentity who, String convId, String opType, String formId, String moduleId, String table, String billId, String field, String fieldLabel, String oldValue, String newValue, String payload, String description) { @@ -74,8 +84,8 @@ public class OpService { jdbc.update( "INSERT INTO ai_op_queue(sId,sUserId,sMakePerson,sBrandsId,sSubsidiaryId,sConversationId,sOpType," + "sTargetFormId,sTargetModuleId,sTargetTable,sTargetBillId,sField,sFieldLabel," + - "sOldValue,sNewValue,sPayload,sDescription,sStatus,tCreateDate,tConfirmDate) " + - "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'confirmed',NOW(),NOW())", + "sOldValue,sNewValue,sPayload,sDescription,sStatus,iStatusCode,tCreateDate,tConfirmDate) " + + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'pending',100,NOW(),NOW())", sId, who.userId(), who.userId(), who.brandsId(), who.subsidiaryId(), convId, opType, formId, moduleId, table, billId, field, trunc(fieldLabel, 128), trunc(oldValue, 500), trunc(newValue, 500), payload, trunc(description, 500)); @@ -91,19 +101,27 @@ public class OpService { return r.isEmpty() ? null : r.get(0); } - /** sStatus → 人类可读进度(流程卡只读展示;不做任何处理动作)。查不到返回 null。 */ + /** 状态+状态码 → 人类可读进度(流程卡只读展示;不做任何处理动作)。查不到返回 null。 */ public String statusLabel(String sId) { try { List> r = jdbc.queryForList( - "SELECT sStatus, sResultMsg, sErrorMsg FROM ai_op_queue WHERE sId=?", sId); + "SELECT sStatus, iStatusCode, sResultMsg, sErrorMsg FROM ai_op_queue WHERE sId=?", sId); if (r.isEmpty()) { return null; } String st = String.valueOf(r.get(0).get("sStatus")); + Object codeObj = r.get(0).get("iStatusCode"); + int code = codeObj instanceof Number n ? n.intValue() : 0; String msg = str(r.get(0).get("sResultMsg")); String err = str(r.get(0).get("sErrorMsg")); return switch (st) { - case "confirmed" -> "已提交,等待 ERP 处理"; + case "pending" -> code == 101 ? "ERP 处理中" : "已提交,等待 ERP 处理"; + case "success" -> (code == 201 ? "ERP 已执行成功(此前已执行)" : "ERP 已执行成功") + + (msg.isBlank() ? "" : ":" + msg); + case "fail" -> "ERP 执行失败(" + failReason(code) + ")" + + (err.isBlank() ? (msg.isBlank() ? "" : ":" + msg) : ":" + err); + // 旧协议兼容读(历史行) + case "draft", "confirmed" -> "已提交,等待 ERP 处理"; case "executing" -> "ERP 处理中"; case "executed" -> "ERP 已执行成功" + (msg.isBlank() ? "" : ":" + msg); case "failed" -> "ERP 执行失败" + (err.isBlank() ? (msg.isBlank() ? "" : ":" + msg) : ":" + err); @@ -115,6 +133,22 @@ public class OpService { } } + /** fail 状态码 → 错误类型(与 ERP 侧约定的状态码表同源)。 */ + private static String failReason(int code) { + return switch (code) { + case 400 -> "载荷非法"; + case 401 -> "身份或权限不足"; + case 404 -> "目标记录不存在"; + case 409 -> "状态冲突"; + case 422 -> "业务校验未通过"; + case 423 -> "记录被锁定"; + case 450 -> "已取消"; + case 504 -> "执行超时"; + case 500 -> "执行异常"; + default -> "code=" + code; + }; + } + private static String str(Object o) { return o == null ? "" : o.toString(); } -- libgit2 0.22.2