# 表单插件场景检查表

## 🔴 P0 级问题

### 1. propertyChanged 循环触发
**检查点**:
- `propertyChanged` 中是否存在 A 字段改 B、B 字段改 A 的循环联动？
- 批量 `setValue` 是否未用 `beginInit/endInit` 包裹导致递归触发？

**风险**: 无限递归导致栈溢出或界面卡死

**修正方案**:
```java
// ❌ 错误写法 - 循环触发
@Override
public void propertyChanged(PropertyChangedArgs e) {
    String key = e.getProperty().getName();
    if ("fieldA".equals(key)) {
        getModel().setValue("fieldB", newValue); // 触发 fieldB 的 propertyChanged
    }
    if ("fieldB".equals(key)) {
        getModel().setValue("fieldA", newValue); // 又触发 fieldA，死循环！
    }
}

// ✅ 正确写法 - 使用 beginInit 避免递归
@Override
public void propertyChanged(PropertyChangedArgs e) {
    String key = e.getProperty().getName();
    if ("fieldA".equals(key)) {
        getModel().beginInit();
        try {
            getModel().setValue("fieldB", newValue);
            getModel().setValue("fieldC", newValue2);
        } finally {
            getModel().endInit();
        }
        getView().updateView("fieldB");
        getView().updateView("fieldC");
    }
}
```

---

### 2. beforeDoOperation 校验失败未 setCancel
**检查点**:
- `beforeDoOperation` 中校验不通过时是否调用了 `args.setCancel(true)`？
- 是否只显示了提示但未取消操作？

**风险**: 校验失败但操作仍然继续执行，数据被错误保存

**修正方案**:
```java
// ❌ 错误写法 - 校验失败但未取消操作
@Override
public void beforeDoOperation(BeforeDoOperationEventArgs args) {
    super.beforeDoOperation(args);
    if (getModel().getValue("requiredField") == null) {
        getView().showTipNotification("字段不能为空");
        // 缺少 args.setCancel(true)，操作仍会继续！
    }
}

// ✅ 正确写法
@Override
public void beforeDoOperation(BeforeDoOperationEventArgs args) {
    super.beforeDoOperation(args);
    FormOperate formOperate = (FormOperate) args.getSource();
    if ("save".equals(formOperate.getOperateKey())) {
        if (getModel().getValue("requiredField") == null) {
            args.setCancel(true);
            getView().showTipNotification("字段不能为空");
            return;
        }
    }
}
```

---

### 3. 交互式操作未检查回调标识（死循环）
**检查点**:
- 操作插件中抛出 `KDInteractionException` 前是否先检查了交互确认结果？
- 交互标识（sponsor）是否唯一且固定？

**风险**: 用户确认后再次进入操作，再次抛出交互异常，形成死循环

**修正方案**:
```java
// ❌ 错误写法 - 未检查回调，死循环
@Override
public void beforeExecuteOperationTransaction(BeforeOperationArgs e) {
    InteractionContext ctx = new InteractionContext();
    ctx.setSimpleMessage("确认继续？");
    throw new KDInteractionException("sponsor", ctx); // 每次都抛，死循环！
}

// ✅ 正确写法 - 先检查是否为回调
private static final String INTERACTION_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(INTERACTION_SPONSOR)) {
        // 已经是回调，根据用户选择处理
        MessageBoxResult result = MessageBoxResult.valueOf(
            confirmResult.getResults().get(INTERACTION_SPONSOR));
        if (result == MessageBoxResult.Yes) {
            return; // 用户确认，继续执行
        } else {
            e.cancel = true;
            return;
        }
    }
    // 首次进入，抛出交互
    InteractionContext ctx = new InteractionContext();
    ctx.setSimpleMessage("确认继续？");
    throw new KDInteractionException(INTERACTION_SPONSOR, ctx);
}
```

---

## 🟠 P1 级问题

### 4. registerListener 未调用 super
**检查点**:
- `registerListener` 方法是否调用了 `super.registerListener(e)`？
- 其他生命周期方法（`afterBindData`、`propertyChanged` 等）是否调用了 super？

**风险**: 框架默认行为被跳过，可能导致其他插件的监听器失效

**修正方案**:
```java
// ❌ 错误写法
@Override
public void registerListener(EventObject e) {
    addItemClickListeners("tbmain"); // 缺少 super 调用
}

// ✅ 正确写法
@Override
public void registerListener(EventObject e) {
    super.registerListener(e);
    addItemClickListeners("tbmain");
}
```

---

### 5. F7 监听未注册
**检查点**:
- 使用 `beforeF7Select` / `afterF7Select` 事件但未在 `registerListener` 中注册监听？
- 插件类是否实现了 `BeforeF7SelectListener` / `AfterF7SelectListener` 接口？

**风险**: F7 事件不触发，过滤条件不生效

**修正方案**:
```java
// ❌ 错误写法 - 未注册监听
public class MyPlugin extends AbstractFormPlugin implements BeforeF7SelectListener {
    @Override
    public void beforeF7Select(BeforeF7SelectEvent e) {
        // 永远不会被调用！
    }
}

// ✅ 正确写法 - 在 registerListener 中注册
public class MyPlugin extends AbstractFormPlugin implements BeforeF7SelectListener {
    @Override
    public void registerListener(EventObject e) {
        super.registerListener(e);
        BasedataEdit f7 = (BasedataEdit) getControl("fieldName");
        if (f7 != null) {
            f7.addBeforeF7SelectListener(this);
        }
    }

    @Override
    public void beforeF7Select(BeforeF7SelectEvent e) {
        // 正确触发
    }
}
```

---

### 6. 分录字段操作未传 rowIndex
**检查点**:
- `propertyChanged` 中操作分录字段时是否通过 `e.getChangeSet()[0].getRowIndex()` 获取行号？
- `setValue` 分录字段时是否传入了 rowIndex 参数？

**风险**: 修改了错误行的数据，或修改了所有行

**修正方案**:
```java
// ❌ 错误写法 - 分录字段未传 rowIndex
@Override
public void propertyChanged(PropertyChangedArgs e) {
    if ("material".equals(e.getProperty().getName())) {
        DynamicObject material = (DynamicObject) getModel().getValue("material");
        getModel().setValue("relatedField", material.get("name")); // 缺少 rowIndex！
    }
}

// ✅ 正确写法
@Override
public void propertyChanged(PropertyChangedArgs e) {
    if ("material".equals(e.getProperty().getName())) {
        int rowIndex = e.getChangeSet()[0].getRowIndex();
        DynamicObject material = (DynamicObject) getModel().getValue("material", rowIndex);
        if (material != null) {
            getModel().setValue("relatedField", material.get("name"), rowIndex);
        }
    }
}
```

---

### 7. closedCallBack 未校验 actionId
**检查点**:
- `closedCallBack` 中是否根据 `e.getActionId()` 区分不同弹窗的回调？
- 是否直接处理 `returnData` 而未判断来源？

**风险**: 多个弹窗的回调混淆，数据被错误处理

**修正方案**:
```java
// ❌ 错误写法 - 未区分回调来源
@Override
public void closedCallBack(ClosedCallBackEvent e) {
    Object data = e.getReturnData();
    getModel().setValue("field", data); // 不知道是哪个弹窗返回的！
}

// ✅ 正确写法
@Override
public void closedCallBack(ClosedCallBackEvent e) {
    if ("selectCallback".equals(e.getActionId())) {
        Object data = e.getReturnData();
        if (data != null) {
            getModel().setValue("field", data);
            getView().updateView("field");
        }
    }
}
```

---

### 8. 字符串比较方式不安全
**检查点**:
- 是否使用 `variable.equals("constant")` 而非 `"constant".equals(variable)`？
- `getItemKey()` / `getOperateKey()` 返回值是否可能为 null？

**风险**: 变量为 null 时抛出 NullPointerException

**修正方案**:
```java
// ❌ 错误写法 - 变量在前，可能 NPE
if (evt.getItemKey().equals("btnSave")) { ... }

// ✅ 正确写法 - 常量在前
if ("btnSave".equals(evt.getItemKey())) { ... }
```

---

## 🟡 P2 级问题

### 9. FormShowParameter 参数不可序列化
**检查点**:
- `getCustomParams().put()` 传递的值是否可序列化？
- 是否传递了 DynamicObject 等不可序列化的复杂对象？

**风险**: 序列化失败导致弹窗打开异常

**修正方案**:
```java
// ❌ 错误写法 - 传递不可序列化对象
param.getCustomParams().put("data", dynamicObject);

// ✅ 正确写法 - 只传基本类型或 String
param.getCustomParams().put("dataId", dynamicObject.getLong("id"));
param.getCustomParams().put("dataName", dynamicObject.getString("name"));
```

---

### 10. PageCache 值类型限制
**检查点**:
- `getPageCache().put()` 的值是否为 String 类型？
- 复杂对象是否先序列化为 JSON 再存入？

**风险**: 非 String 类型存入 PageCache 可能导致类型转换异常

**修正方案**:
```java
// ❌ 错误写法 - 存入非 String 类型
getPageCache().put("ids", idList);

// ✅ 正确写法 - 序列化后存入
getPageCache().put("ids", SerializationUtils.toJsonString(idList));
// 读取时反序列化
String idsJson = getPageCache().get("ids");
List<Long> ids = SerializationUtils.fromJsonString(idsJson, List.class);
```

---

### 11. 列表 setFilter 未调用 super
**检查点**:
- 列表插件 `setFilter` 方法是否调用了 `super.setFilter(e)`？

**风险**: 框架默认过滤逻辑被跳过，可能导致数据权限失效

**修正方案**:
```java
// ❌ 错误写法
@Override
public void setFilter(SetFilterEvent e) {
    e.getQFilters().add(new QFilter("status", QCP.equals, "A"));
    // 缺少 super 调用
}

// ✅ 正确写法
@Override
public void setFilter(SetFilterEvent e) {
    super.setFilter(e);
    e.getQFilters().add(new QFilter("status", QCP.equals, "A"));
}
```

---

### 12. afterBindData 中 setValue 触发 propertyChanged
**检查点**:
- `afterBindData` 中是否直接调用 `getModel().setValue()` 而未使用 `beginInit/endInit` 包裹？
- 此 setValue 是否会触发 `propertyChanged` 中的联动逻辑导致死循环或数据覆盖？

**风险**: afterBindData 触发 propertyChanged，propertyChanged 中的联动逻辑再次修改数据，导致循环或数据错误

**修正方案**:
```java
// ❌ 错误写法 - afterBindData 中直接 setValue
@Override
public void afterBindData(EventObject e) {
    super.afterBindData(e);
    getModel().setValue("calcField", computeValue()); // 触发 propertyChanged
}

// ✅ 正确写法 - 使用 beginInit 屏蔽联动
@Override
public void afterBindData(EventObject e) {
    super.afterBindData(e);
    getModel().beginInit();
    try {
        getModel().setValue("calcField", computeValue());
    } finally {
        getModel().endInit();
    }
    getView().updateView("calcField");
}
```

---

### 13. 分录行删除后仍使用原行号
**检查点**:
- `afterDeleteRow` 中是否使用了删除前缓存的行号访问分录数据？
- 删除行后是否重新获取了行数和行号？

**风险**: 删除行后行号偏移，使用旧行号访问到错误数据或数组越界

**修正方案**:
```java
// ❌ 错误写法 - 删除行后使用原行号
@Override
public void afterDeleteRow(AfterDeleteRowEventArgs e) {
    super.afterDeleteRow(e);
    int rowCount = getModel().getEntryRowCount("entryentity");
    for (int i = 0; i < rowCount; i++) {
        // 应重新计算而非依赖删除前的缓存行号
        recalcRow(i);
    }
}

// ✅ 正确写法 - 基于当前实际行重新遍历
@Override
public void afterDeleteRow(AfterDeleteRowEventArgs e) {
    super.afterDeleteRow(e);
    DynamicObjectCollection entries = getModel().getEntryEntity("entryentity");
    getModel().beginInit();
    try {
        for (int i = 0; i < entries.size(); i++) {
            recalcRow(i);
        }
    } finally {
        getModel().endInit();
    }
    getView().updateView("entryentity");
}
```

---

### 14. entryRowClick 未校验行号有效性
**检查点**:
- `entryRowClick` 事件中是否直接使用行号访问数据而未校验有效性？
- 行号是否可能为 -1（未选中任何行时）？

**风险**: 行号为 -1 时直接 getValue 导致 IndexOutOfBoundsException

**修正方案**:
```java
// ❌ 错误写法 - 未校验行号
@Override
public void entryRowClick(RowClickEvent e) {
    super.entryRowClick(e);
    int row = e.getRow();
    Object value = getModel().getValue("material", row); // row 可能为 -1
}

// ✅ 正确写法
@Override
public void entryRowClick(RowClickEvent e) {
    super.entryRowClick(e);
    int row = e.getRow();
    if (row < 0) {
        return;
    }
    Object value = getModel().getValue("material", row);
}
```

---

### 15. flexEdit 动态编辑控制遗漏
**检查点**:
- 使用 `flexEdit` 控制分录字段可编辑性时，是否在 `afterBindData` 和 `afterCreateNewData` 中都设置了？
- 新增行时是否遗漏了编辑控制？

**风险**: 新增行的字段编辑状态与预期不一致

**修正方案**:
```java
// ❌ 错误写法 - 只在 afterBindData 中设置
@Override
public void afterBindData(EventObject e) {
    super.afterBindData(e);
    setEntryFieldEditable();
}

// ✅ 正确写法 - afterBindData 和 afterCreateNewData 都设置
@Override
public void afterBindData(EventObject e) {
    super.afterBindData(e);
    setEntryFieldEditable();
}

@Override
public void afterCreateNewData(EventObject e) {
    super.afterCreateNewData(e);
    setEntryFieldEditable();
}

private void setEntryFieldEditable() {
    int rowCount = getModel().getEntryRowCount("entryentity");
    for (int i = 0; i < rowCount; i++) {
        String status = (String) getModel().getValue("linestatus", i);
        getView().setEnable("A".equals(status), i, "qty", "price");
    }
}
```

---

### 16. 多页签子页面插件通信错误
**检查点**:
- 多页签表单中，子页面插件是否通过 `getView().getParentView()` 获取父页面？
- 是否在父页面未加载时就尝试访问父页面控件？

**风险**: 父页面未加载完成时获取 parentView 为 null，导致 NPE

**修正方案**:
```java
// ❌ 错误写法 - 未判空 parentView
@Override
public void afterBindData(EventObject e) {
    super.afterBindData(e);
    IFormView parentView = getView().getParentView();
    Object parentValue = parentView.getModel().getValue("headField"); // 可能 NPE
}

// ✅ 正确写法
@Override
public void afterBindData(EventObject e) {
    super.afterBindData(e);
    IFormView parentView = getView().getParentView();
    if (parentView != null && parentView.getModel() != null) {
        Object parentValue = parentView.getModel().getValue("headField");
    }
}
```
