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 定): + *
列名兼容: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