OpService.java 7.94 KB
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>列名兼容:ERP 执行器仍读 {@code sUserId} → 新列 {@code sMakePerson}(ERP 惯例)双写,
 * ERP 侧切换后可删 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,sUserId,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.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();
    }
}