# 操作插件场景检查表

## 🔴 P0 级问题

### 1. onPreparePropertys 未声明使用字段
**检查点**:
- 操作插件中 `beginOperationTransaction` / `endOperationTransaction` 等方法使用了实体字段，但 `onPreparePropertys` 未通过 `e.getFieldKeys().add()` 声明？
- 校验器 `validate` 中使用了字段，但关联的 `preparePropertys` 未返回对应字段？

**风险**: 未声明的字段在操作执行时不会被预加载，运行时值为 null，导致业务逻辑静默失效

**修正方案**:
```java
// ❌ 错误写法 - 使用了字段但未声明
@Override
public void beginOperationTransaction(BeginOperationTransactionArgs e) {
    for (DynamicObject obj : e.getDataEntities()) {
        String status = obj.getString("billstatus"); // 未声明，可能为 null
    }
}

// ✅ 正确写法 - 在 onPreparePropertys 中声明
@Override
public void onPreparePropertys(PreparePropertysEventArgs e) {
    super.onPreparePropertys(e);
    e.getFieldKeys().add("billstatus");
    e.getFieldKeys().add("entryentity.material");
    e.getFieldKeys().add("entryentity.qty");
}
```

---

### 2. beforeExecuteOperationTransaction 中独立保存
**检查点**:
- `beforeExecuteOperationTransaction` 中是否调用了 `SaveServiceHelper.save()` 或 `SaveServiceHelper.update()`？
- 是否在事务钩子内创建了独立的保存操作？

**风险**: 事务钩子内独立保存会脱离当前事务，导致数据不一致；若主事务回滚，独立保存的数据不会回滚

**修正方案**:
```java
// ❌ 错误写法 - 事务钩子内独立保存
@Override
public void beforeExecuteOperationTransaction(BeforeOperationArgs e) {
    for (DynamicObject obj : e.getDataEntities()) {
        obj.set("amount", calculateAmount(obj));
        SaveServiceHelper.save(new DynamicObject[]{obj}); // 脱离事务！
    }
}

// ✅ 正确写法 - 直接修改数据实体，由框架统一保存
@Override
public void beforeExecuteOperationTransaction(BeforeOperationArgs e) {
    for (DynamicObject obj : e.getDataEntities()) {
        obj.set("amount", calculateAmount(obj));
        // 不需要手动保存，框架会在事务内统一处理
    }
}
```

---

### 3. 自定义校验器字段未声明
**检查点**:
- 自定义校验器（继承 `AbstractValidator`）的 `validate` 方法中使用了字段，但 `preparePropertys` 未返回？
- 关联的操作插件 `onPreparePropertys` 是否也声明了校验器需要的字段？

**风险**: 校验器拿到的字段值为默认值（null/0），校验逻辑基于错误数据静默通过

**修正方案**:
```java
// ❌ 错误写法 - 校验器未声明字段
public class MyValidator extends AbstractValidator {
    @Override
    public void validate() {
        for (ExtendedDataEntity entity : getDataEntities()) {
            BigDecimal qty = entity.getDataEntity().getBigDecimal("qty"); // 未声明
        }
    }
}

// ✅ 正确写法
public class MyValidator extends AbstractValidator {
    @Override
    public Set<String> preparePropertys() {
        Set<String> fields = super.preparePropertys();
        if (fields == null) {
            fields = new HashSet<>();
        }
        fields.add("qty");
        fields.add("entryentity.material");
        return fields;
    }

    @Override
    public void validate() {
        for (ExtendedDataEntity entity : getDataEntities()) {
            BigDecimal qty = entity.getDataEntity().getBigDecimal("qty");
            if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
                addErrorMessage(entity, "数量必须大于零");
            }
        }
    }
}
```

---

### 4. 交互式操作死循环（KDInteractionException）
**检查点**:
- 抛出 `KDInteractionException` 前是否先检查了交互确认结果（`InteractionConfirmResult`）？
- 交互标识（sponsor）是否唯一且固定？

**风险**: 用户确认后再次进入操作，再次抛出交互异常，形成死循环

**修正方案**:
```java
// ❌ 错误写法 - 未检查回调，死循环
@Override
public void beforeExecuteOperationTransaction(BeforeOperationArgs e) {
    throw new KDInteractionException("sponsor", ctx); // 每次都抛！
}

// ✅ 正确写法 - 先检查是否为回调
private static final String SPONSOR = "com.xxx.MyPlugin";

@Override
public void beforeExecuteOperationTransaction(BeforeOperationArgs e) {
    String confirmStr = this.getOption().getVariableValue(
        OperateOptionConst.INTERACTIONCONFIRMRESULT, "");
    InteractionConfirmResult confirmResult =
        InteractionConfirmResult.fromJsonString(confirmStr);
    if (confirmResult.getResults().containsKey(SPONSOR)) {
        MessageBoxResult result = MessageBoxResult.valueOf(
            confirmResult.getResults().get(SPONSOR));
        if (result == MessageBoxResult.Yes) {
            return; // 用户已确认，继续执行
        } else {
            e.cancel = true;
            return;
        }
    }
    // 首次进入，抛出交互
    InteractionContext ctx = new InteractionContext();
    ctx.setSimpleMessage("确认继续？");
    throw new KDInteractionException(SPONSOR, ctx);
}
```

---

## 🟠 P1 级问题

### 5. OperationResult 未检查即使用
**检查点**:
- 调用 `OperationServiceHelper.executeOperate()` 后是否检查了 `OperationResult.isSuccess()`？
- 操作失败时是否有错误处理逻辑？

**风险**: 操作失败但代码继续执行后续逻辑，导致数据不一致

**修正方案**:
```java
// ❌ 错误写法 - 未检查操作结果
OperationServiceHelper.executeOperate("submit", "demo_bill", ids, option);
// 直接执行后续逻辑...

// ✅ 正确写法
OperationResult result = OperationServiceHelper.executeOperate(
    "submit", "demo_bill", ids, option);
if (!result.isSuccess()) {
    String errorMsg = result.getAllErrorInfo();
    logger.error("提交操作失败: {}", errorMsg);
    throw new KDException(errorMsg);
}
Object[] successIds = result.getSuccessPkIds().toArray();
```

---

### 6. 操作错误提示级别不当
**检查点**:
- 校验器中使用 `addErrorMessage` 时是否区分了错误级别（Error/Warning）？
- Warning 级别的提示是否允许用户忽略继续？

**风险**: 所有问题都用 Error 级别会阻断操作；都用 Warning 级别则无法阻止严重问题

**修正方案**:
```java
// ✅ 正确写法 - 区分错误级别
@Override
public void validate() {
    for (ExtendedDataEntity entity : getDataEntities()) {
        BigDecimal qty = entity.getDataEntity().getBigDecimal("qty");
        if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
            // Error 级别 - 阻断操作
            addErrorMessage(entity, "数量必须大于零");
        }
        if (qty != null && qty.compareTo(new BigDecimal("10000")) > 0) {
            // Warning 级别 - 允许用户忽略
            addMessage(entity, "数量超过10000，请确认是否正确",
                ErrorLevel.Warning);
        }
    }
}
```

---

### 7. afterExecuteOperationTransaction 误用 getOperationResult
**检查点**:
- `afterExecuteOperationTransaction` 的参数类型是 `AfterOperationArgs`，是否错误调用了 `getOperationResult()`？

**风险**: `AfterOperationArgs` 没有 `getOperationResult()` 方法，编译通过但运行时报错

**修正方案**:
```java
// ❌ 错误写法 - AfterOperationArgs 无此方法
@Override
public void afterExecuteOperationTransaction(AfterOperationArgs e) {
    e.getOperationResult(); // 方法不存在！
}

// ✅ 正确写法 - 使用 AfterOperationArgs 提供的方法
@Override
public void afterExecuteOperationTransaction(AfterOperationArgs e) {
    DynamicObject[] entities = e.getDataEntities();
    for (DynamicObject entity : entities) {
        // 直接处理数据实体
    }
}
```

---

### 8. 操作事件时序错误
**检查点**:
- 数据修改是否放在了正确的事件中？
- `endOperationTransaction` 中是否执行了应在 `beginOperationTransaction` 中完成的逻辑？

**风险**: 事件时序错误导致数据修改不在事务内，或修改被后续逻辑覆盖

**事件时序说明**:
```
onPreparePropertys → onAddValidators → validate
→ beforeExecuteOperationTransaction（事务开始前，可取消操作）
→ beginOperationTransaction（事务内，修改数据实体）
→ endOperationTransaction（事务内，后置处理如写日志）
→ afterExecuteOperationTransaction（事务提交后，不可回滚）
→ rollbackOperation（事务回滚时触发）
```

**修正方案**:
```java
// ❌ 错误写法 - 在事务提交后修改数据
@Override
public void afterExecuteOperationTransaction(AfterOperationArgs e) {
    for (DynamicObject obj : e.getDataEntities()) {
        obj.set("status", "C"); // 事务已提交，修改不会被保存！
    }
}

// ✅ 正确写法 - 在事务内修改数据
@Override
public void beginOperationTransaction(BeginOperationTransactionArgs e) {
    for (DynamicObject obj : e.getDataEntities()) {
        obj.set("status", "C"); // 事务内修改，会被统一提交
    }
}
```

---

### 9. 后台调用操作未设置 IGNOREINTERACTION
**检查点**:
- 后台代码（非表单触发）调用 `OperationServiceHelper.executeOperate()` 时，操作插件可能抛出 `KDInteractionException`，是否设置了 `IGNOREINTERACTION`？

**风险**: 后台无法弹出交互对话框，操作会失败且不重试

**修正方案**:
```java
// ❌ 错误写法 - 后台调用未忽略交互
OperateOption option = OperateOption.create();
OperationServiceHelper.executeOperate("submit", "demo_bill", ids, option);

// ✅ 正确写法 - 后台调用忽略交互
OperateOption option = OperateOption.create();
option.setVariableValue(OperateOptionConst.IGNOREINTERACTION, String.valueOf(true));
option.setVariableValue(OperateOptionConst.IGNOREWARN, String.valueOf(true));
OperationServiceHelper.executeOperate("submit", "demo_bill", ids, option);
```

---

## 🟡 P2 级问题

### 10. 操作自定义参数命名冲突
**检查点**:
- `OperateOption.setVariableValue()` 的 key 是否使用了过于简单的名称（如 "flag"、"type"）？

**风险**: 与其他插件或框架内置参数冲突，导致参数被覆盖

**修正方案**:
```java
// ❌ 错误写法 - 参数名过于简单
option.setVariableValue("flag", "true");

// ✅ 正确写法 - 带业务域前缀
option.setVariableValue("scmc_ccm_skipValidation", "true");
```

---

### 11. 操作内置服务与操作插件混淆
**检查点**:
- 通用性强的操作逻辑是否仍写在操作插件中而非操作内置服务？
- 操作内置服务（`AbstractOpBizRuleAction`）是否正确使用了 `getBizRule()` 获取配置参数？

**风险**: 通用逻辑写在操作插件中无法复用，每个操作都需要重复编写

**建议**: 将通用操作逻辑（如写日志、更新状态）抽象为操作内置服务，通过配置绑定到多个操作上复用

---

### 12. endOperationTransaction 中触发其他操作
**检查点**:
- `endOperationTransaction` 中是否调用了 `OperationServiceHelper.executeOperate()` 触发另一个操作？
- 被触发的操作是否会再次触发当前操作形成递归？

**风险**: 操作链形成循环调用，事务嵌套过深导致死锁或栈溢出

**修正方案**:
```java
// ❌ 错误写法 - 事务内触发其他操作
@Override
public void endOperationTransaction(EndOperationTransactionArgs e) {
    for (DynamicObject obj : e.getDataEntities()) {
        Object[] ids = new Object[]{obj.getLong("id")};
        // 事务内触发新操作，可能导致事务嵌套
        OperationServiceHelper.executeOperate("submit", "demo_bill", ids);
    }
}

// ✅ 正确写法 - 使用 afterExecuteOperationTransaction（事务提交后）
@Override
public void afterExecuteOperationTransaction(AfterOperationArgs e) {
    for (DynamicObject obj : e.getDataEntities()) {
        Object[] ids = new Object[]{obj.getLong("id")};
        OperateOption option = OperateOption.create();
        option.setVariableValue(OperateOptionConst.IGNOREINTERACTION, "true");
        OperationServiceHelper.executeOperate("submit", "demo_bill", ids, option);
    }
}
```

---

### 13. 批量操作数据量过大未分批
**检查点**:
- 操作插件处理的数据量是否可能超过 1000 条？
- 是否在操作插件内部做了分批处理？

**风险**: 单次操作数据量过大，事务过长导致数据库锁等待超时

**修正方案**:
```java
// ❌ 错误写法 - 一次性处理所有数据
@Override
public void beginOperationTransaction(BeginOperationTransactionArgs e) {
    for (DynamicObject obj : e.getDataEntities()) {
        // 如果有 10000 条数据，全在一个事务内处理
        processData(obj);
    }
}

// ✅ 正确写法 - 操作层面分批（调用方控制）
// 在调用方分批提交
int batchSize = 500;
List<Object> allIds = getAllIds();
for (int i = 0; i < allIds.size(); i += batchSize) {
    List<Object> batchIds = allIds.subList(i, Math.min(i + batchSize, allIds.size()));
    OperationServiceHelper.executeOperate("save", entityName,
        batchIds.toArray());
}
```

---

### 14. rollbackOperation 中执行数据库写操作
**检查点**:
- `rollbackOperation` 回调中是否执行了 `SaveServiceHelper.save()` 或其他写数据库操作？
- 回滚处理中是否尝试记录日志到数据库？

**风险**: 事务回滚后再执行写操作可能被一起回滚，或导致新的异常覆盖原始异常

**修正方案**:
```java
// ❌ 错误写法 - 回滚中写数据库
@Override
public void rollbackOperation(RollbackOperationArgs e) {
    DynamicObject log = new DynamicObject();
    log.set("message", "操作失败: " + e.getException().getMessage());
    SaveServiceHelper.save(new DynamicObject[]{log}); // 可能也被回滚！
}

// ✅ 正确写法 - 回滚中只记录内存日志
@Override
public void rollbackOperation(RollbackOperationArgs e) {
    logger.error("操作回滚，原因: {}", e.getException().getMessage(), e.getException());
    // 如果需要持久化日志，使用异步方式在事务外执行
}
```

---

### 15. 多个操作插件执行顺序依赖
**检查点**:
- 同一操作绑定了多个操作插件时，是否存在执行顺序依赖？
- 插件之间是否通过 `OperateOption` 传递状态数据？

**风险**: 插件执行顺序不确定，依赖特定顺序的逻辑可能在不同环境下表现不一致

**修正方案**:
```java
// ❌ 错误写法 - 插件A设置标记，插件B依赖该标记
// PluginA:
@Override
public void beginOperationTransaction(BeginOperationTransactionArgs e) {
    this.getOption().setVariableValue("pluginA_done", "true");
}
// PluginB:
@Override
public void beginOperationTransaction(BeginOperationTransactionArgs e) {
    String flag = this.getOption().getVariableValue("pluginA_done", "");
    if (!"true".equals(flag)) {
        return; // 假设 A 先执行，但顺序不保证！
    }
}

// ✅ 正确写法 - 合并为一个插件，或使用操作链（前置操作 → 后置操作）
// 将有依赖关系的逻辑放在同一个插件中，通过方法拆分保持清晰
```
