Commit 96dcd92f6cf464c5108d64356a5dfab109e9c948

Authored by zichun
1 parent 576ed951

feat: ERP 侧 rearch3 对接验收后的三处接轨

- create 载荷 sFormId 改写模块 id(与 ERP 原生同构;表单 id 无
  SysBillNoSettings 编号规则,执行期重生成单号必撞已用号)
- ai_op_queue 停写 sUserId(ERP 执行器已切读 sMakePerson;列暂
  不可删——ERP myTodos 仍 COALESCE 引用)
- saaslocal 开启 erp.dry-run.enabled(ERP /business/checkBusinessData
  已上线,保存前真实校验链 validate-only)

联合 e2e 实测:建报价 BJD202607091(sFormId=模块id、主从落库)→
审核 success+200 → 重复审核 fail+409 → 改字段 success+200 →
取消 fail+450 → 幂等 success+201;dry-run 保存时真实调用(ERP 日志
时刻对齐)且零落库。
docs/agent-architecture.md
@@ -495,8 +495,9 @@ ai_op_queue( @@ -495,8 +495,9 @@ ai_op_queue(
495 ErpClient 只剩 whoami / readForm / getUuid / checkBusinessData(dry-run 接口位)。 495 ErpClient 只剩 whoami / readForm / getUuid / checkBusinessData(dry-run 接口位)。
496 - **状态合法性硬检查**(预览+保存两次,防 TOCTOU;依据已核实的存储过程行为):审核要求未审核、 496 - **状态合法性硬检查**(预览+保存两次,防 TOCTOU;依据已核实的存储过程行为):审核要求未审核、
497 销审要求已审核、作废要求未作废、复原要求已作废;表无对应状态列则跳过。已审核单物理删除仅警告。 497 销审要求已审核、作废要求未作废、复原要求已作废;表无对应状态列则跳过。已审核单物理删除仅警告。
498 -- **ai_op_queue 列名兼容**:新增 `sMakePerson`(ERP 惯例)与 `sUserId` 双写(ERP 执行器仍读  
499 - sUserId,切换后可删);迁移见 `sql/migrate_rearch3.sql`。 498 +- **ai_op_queue 归属列**:只写 `sMakePerson`(ERP 惯例);旧列 `sUserId` 已停写(2026-07-29,
  499 + ERP 执行器切读完成后),但暂不可删——ERP 侧 myTodos/归属校验仍以 `COALESCE(sMakePerson, sUserId)`
  500 + 兼容历史行;迁移见 `sql/migrate_rearch3.sql`。
500 - 护栏:反编造重试与 `maxSequentialToolsInvocations(8)` 原样保留;WRITE_CLAIM 正则补 501 - 护栏:反编造重试与 `maxSequentialToolsInvocations(8)` 原样保留;WRITE_CLAIM 正则补
501 保存/录入并用负向前瞻排除「已审核状态」类状态陈述;"出过卡"标志改为 change_preview/form_collect。 502 保存/录入并用负向前瞻排除「已审核状态」类状态陈述;"出过卡"标志改为 change_preview/form_collect。
502 503
@@ -542,8 +543,10 @@ ai_op_queue( @@ -542,8 +543,10 @@ ai_op_queue(
542 543
543 ### 21.5 待 ERP 侧(D 阶段对接) 544 ### 21.5 待 ERP 侧(D 阶段对接)
544 545
545 -- `/business/checkBusinessData` dry-run 端点在 ERP 仓库**未提交**(他人负责);xlyAi 已留  
546 - `erp.dry-run.enabled` 开关 + `ErpClient.checkBusinessData`,完工后开启即接入(update 载荷  
547 - 与入队同口径;__tables__ 多行分组形状需与 ERP 对一次)。  
548 -- doExamine/updatebInvalid 的 ERP 侧校验补做中;xlyAi 预览暂以直读 bCheck/bInvalid 实现硬检查。  
549 -- ai_op_queue `sUserId` 列:ERP 执行器切到 sMakePerson 后删除。 546 +- ~~`/business/checkBusinessData` 未提交~~ **已上线**(ERP 仓库 feature/ai @1555f0c,2026-07-29 验收):
  547 + validate-only(REQUIRES_NEW 强制回滚)、真写端点剥 bDryRun、__tables__ 两种分组形状均支持;
  548 + `erp.dry-run.enabled` 已在 saaslocal 开启(生产随 ERP 部署后开)。
  549 +- ~~doExamine/updatebInvalid 校验补做中~~ **已上线**:执行时刻按 bCheck/bInvalid 校验,冲突回 fail+409
  550 + (xlyAi 预览侧的硬检查保留,双层防 TOCTOU)。
  551 +- ai_op_queue `sUserId` 列:xlyAi 已停写;**删列需等 ERP 侧去掉 COALESCE 兼容读**(其 myTodos SQL
  552 + 直接引用该列,先删会把查询打挂)。
src/main/java/com/xly/service/FormRenderService.java
@@ -423,7 +423,8 @@ public class FormRenderService { @@ -423,7 +423,8 @@ public class FormRenderService {
423 col.put(rc, resolver.typeDefault(rc, types.get(rc))); 423 col.put(rc, resolver.typeDefault(rc, types.get(rc)));
424 } 424 }
425 col.put("sId", erp.newUuid(identity.token())); 425 col.put("sId", erp.newUuid(identity.token()));
426 - col.put("sFormId", out.formId); 426 + // 记录的 sFormId 按 ERP 原生惯例存模块 id(SysBillNoSettings 编号规则按它配置;存表单 id 会撞不上规则)
  427 + col.put("sFormId", out.moduleId);
427 String billNo = resolver.nextBillNo(out.table, identity.brandsId()); 428 String billNo = resolver.nextBillNo(out.table, identity.brandsId());
428 if (billNo != null) { 429 if (billNo != null) {
429 col.put("sBillNo", billNo); 430 col.put("sBillNo", billNo);
@@ -542,7 +543,7 @@ public class FormRenderService { @@ -542,7 +543,7 @@ public class FormRenderService {
542 } 543 }
543 String masterId = erp.newUuid(identity.token()); 544 String masterId = erp.newUuid(identity.token());
544 masterCol.put("sId", masterId); 545 masterCol.put("sId", masterId);
545 - masterCol.put("sFormId", out.formId); 546 + masterCol.put("sFormId", out.moduleId);
546 String billNo = resolver.nextBillNo(out.table, identity.brandsId()); 547 String billNo = resolver.nextBillNo(out.table, identity.brandsId());
547 if (billNo != null) { 548 if (billNo != null) {
548 masterCol.put("sBillNo", billNo); 549 masterCol.put("sBillNo", billNo);
src/main/java/com/xly/service/OpService.java
@@ -24,8 +24,8 @@ import java.util.Map; @@ -24,8 +24,8 @@ import java.util.Map;
24 * </ul> 24 * </ul>
25 * 旧值(draft/confirmed/executing/executed/failed/cancelled)兼容读(见 statusLabel),不再写。 25 * 旧值(draft/confirmed/executing/executed/failed/cancelled)兼容读(见 statusLabel),不再写。
26 * 26 *
27 - * <p>列名兼容:ERP 执行器仍读 {@code sUserId} → 新列 {@code sMakePerson}(ERP 惯例)双写,  
28 - * ERP 侧切换后可删 sUserId。 27 + * <p>归属列只写 {@code sMakePerson};旧列 {@code sUserId} 已停写但暂不可删——ERP 侧
  28 + * {@code myTodos}/归属校验仍以 {@code COALESCE(sMakePerson, sUserId)} 兼容历史行。
29 */ 29 */
30 @Service 30 @Service
31 public class OpService { 31 public class OpService {
@@ -82,11 +82,11 @@ public class OpService { @@ -82,11 +82,11 @@ public class OpService {
82 String newValue, String payload, String description) { 82 String newValue, String payload, String description) {
83 String sId = "op-" + System.currentTimeMillis() + "-" + Integer.toHexString((int) (Math.random() * 0xFFFFF)); 83 String sId = "op-" + System.currentTimeMillis() + "-" + Integer.toHexString((int) (Math.random() * 0xFFFFF));
84 jdbc.update( 84 jdbc.update(
85 - "INSERT INTO ai_op_queue(sId,sUserId,sMakePerson,sBrandsId,sSubsidiaryId,sConversationId,sOpType," + 85 + "INSERT INTO ai_op_queue(sId,sMakePerson,sBrandsId,sSubsidiaryId,sConversationId,sOpType," +
86 "sTargetFormId,sTargetModuleId,sTargetTable,sTargetBillId,sField,sFieldLabel," + 86 "sTargetFormId,sTargetModuleId,sTargetTable,sTargetBillId,sField,sFieldLabel," +
87 "sOldValue,sNewValue,sPayload,sDescription,sStatus,iStatusCode,tCreateDate,tConfirmDate) " + 87 "sOldValue,sNewValue,sPayload,sDescription,sStatus,iStatusCode,tCreateDate,tConfirmDate) " +
88 - "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'pending',100,NOW(),NOW())",  
89 - sId, who.userId(), who.userId(), who.brandsId(), who.subsidiaryId(), convId, opType, 88 + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'pending',100,NOW(),NOW())",
  89 + sId, who.userId(), who.brandsId(), who.subsidiaryId(), convId, opType,
90 formId, moduleId, table, billId, field, trunc(fieldLabel, 128), 90 formId, moduleId, table, billId, field, trunc(fieldLabel, 128),
91 trunc(oldValue, 500), trunc(newValue, 500), payload, trunc(description, 500)); 91 trunc(oldValue, 500), trunc(newValue, 500), payload, trunc(description, 500));
92 return sId; 92 return sId;
src/main/resources/application-saaslocal.yml
@@ -33,9 +33,9 @@ erp: @@ -33,9 +33,9 @@ erp:
33 subsidiary: "1111111111" 33 subsidiary: "1111111111"
34 username: admin 34 username: admin
35 password: "666666" 35 password: "666666"
36 - # Phase D 接口位:ERP dry-run 校验(/business/checkBusinessData)。ERP 侧完工前保持 false 36 + # ERP dry-run 校验(/business/checkBusinessData)。ERP 侧 2026-07-29 上线(feature/ai @1555f0c)后开启
37 dry-run: 37 dry-run:
38 - enabled: false 38 + enabled: true
39 39
40 # LLM 可观测(Langfuse,架构 §1)。默认关闭;自托管起来后填 key 开启: 40 # LLM 可观测(Langfuse,架构 §1)。默认关闭;自托管起来后填 key 开启:
41 # docker compose -f docker-compose.langfuse.yml up -d → http://localhost:3000 拿 public/secret key。 41 # docker compose -f docker-compose.langfuse.yml up -d → http://localhost:3000 拿 public/secret key。