OpService.java
7.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package com.xly.service;
import com.xly.agent.AgentIdentity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* ai_op_queue 队列写入 —— **AI 侧唯一的写入口**(rearch3 §3 写路径收权)。
*
* <p>用户在预览卡/表单上点按钮(保存/审核/作废…)→ 确定性端点校验后经本类落一行队列。
* **xlyAi 工作到此为止**:是否/何时执行、执行权属、审计留痕全部由 ERP 侧负责
* (暂存执行器读本表;现状=报价自动执行、其余进待办)。
* xlyAi 侧只保留 {@link #statusLabel} 只读展示处理进度(流程卡)。
*
* <p><b>状态协议 = sStatus 三态 + iStatusCode 状态码</b>(2026-07-29 定):
* <ul>
* <li>{@code pending}:100=已提交等待 ERP 处理(xlyAi 写入的唯一状态)、101=ERP 处理中(已领取);</li>
* <li>{@code success}:200=执行成功、201=幂等命中(此前已执行);</li>
* <li>{@code fail}:400 载荷非法 / 401 身份权限 / 404 记录不存在 / 409 状态冲突 /
* 422 业务校验未通过 / 423 记录被锁 / 450 已取消 / 500 执行异常 / 504 超时。</li>
* </ul>
* 旧值(draft/confirmed/executing/executed/failed/cancelled)兼容读(见 statusLabel),不再写。
*
* <p>归属列只写 {@code sMakePerson};旧列 {@code sUserId} 已停写但暂不可删——ERP 侧
* {@code myTodos}/归属校验仍以 {@code COALESCE(sMakePerson, sUserId)} 兼容历史行。
*/
@Service
public class OpService {
private final JdbcTemplate jdbc;
public OpService(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
/** 一次保存里的一个字段改动(批量入队用)。 */
public record FieldChange(String col, String label, String oldValue, String newValue, String description) { }
/** 单字段更新入队(update)。 */
public String queueUpdate(AgentIdentity who, String convId, String formId, String moduleId, String table,
String billId, String field, String fieldLabel, String oldValue, String newValue,
String description) {
return insert(who, convId, "update", formId, moduleId, table, billId,
field, fieldLabel, oldValue, newValue, null, description);
}
/**
* 多字段更新批量入队(单事务:全部成功或全部回滚,绝不"写一半")。
* ERP 执行器为单字段 update,故一次保存多字段 = 多行待办。
*/
@org.springframework.transaction.annotation.Transactional
public List<String> queueUpdates(AgentIdentity who, String convId, String formId, String moduleId,
String table, String billId, List<FieldChange> changes) {
List<String> opIds = new java.util.ArrayList<>(changes.size());
for (FieldChange c : changes) {
opIds.add(insert(who, convId, "update", formId, moduleId, table, billId,
c.col(), c.label(), c.oldValue(), c.newValue(), null, c.description()));
}
return opIds;
}
/** 状态操作入队:invalid(sNewValue=toVoid|cancel)/ examine(sNewValue=1|0)/ delete。 */
public String queueStateOp(AgentIdentity who, String convId, String opType, String formId, String moduleId,
String table, String billId, String sNewValue, String description) {
return insert(who, convId, opType, formId, moduleId, table, billId,
null, null, null, sNewValue, null, description);
}
/** 新增入队(create):payload = 列->值 JSON(多表用 __tables__)。 */
public String queueCreate(AgentIdentity who, String convId, String formId, String moduleId, String table,
String payload, String description) {
return insert(who, convId, "create", formId, moduleId, table, null,
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) {
String sId = "op-" + System.currentTimeMillis() + "-" + Integer.toHexString((int) (Math.random() * 0xFFFFF));
jdbc.update(
"INSERT INTO ai_op_queue(sId,sMakePerson,sBrandsId,sSubsidiaryId,sConversationId,sOpType," +
"sTargetFormId,sTargetModuleId,sTargetTable,sTargetBillId,sField,sFieldLabel," +
"sOldValue,sNewValue,sPayload,sDescription,sStatus,iStatusCode,tCreateDate,tConfirmDate) " +
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'pending',100,NOW(),NOW())",
sId, 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));
return sId;
}
private static String trunc(String s, int max) {
return s != null && s.length() > max ? s.substring(0, max) : s;
}
public Map<String, Object> get(String sId) {
List<Map<String, Object>> r = jdbc.queryForList("SELECT * FROM ai_op_queue WHERE sId=?", sId);
return r.isEmpty() ? null : r.get(0);
}
/** 状态+状态码 → 人类可读进度(流程卡只读展示;不做任何处理动作)。查不到返回 null。 */
public String statusLabel(String sId) {
try {
List<Map<String, Object>> r = jdbc.queryForList(
"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 "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);
case "cancelled" -> "已取消";
default -> st;
};
} catch (Exception e) {
return null;
}
}
/** 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();
}
}