# 通用契约：访问位 · 生命周期 · API 面

> 适用于**任意**单据/字段/插件需求。不按「销售订单数量」「改备注」等单场景背样例。  
> 企业版 C# / IPY；苍穹见文末。

---

## 1. 先定三条轴（任何需求同一流程）

```
需求口语
  → ① 产品线（enterprise | cosmic 族）
  → ② 生命周期位：谁跑这段逻辑（界面表单 / 列表 / 操作服务 / 转换 / 外部 API）
  → ③ 访问位：每个字段字符串落在哪一类 API 上
  → 打开对应 assets 手册，只使用该文件中出现的类型与成员
```

不确定时：注释写清假设，**不编造**手册中不存在的方法名。  
字段真值：按 `metadata-db-query.md` **Agent 引导只读 SQL + LLM 解析**（非 MCP）；无连接信息时**先问用户**（企业版与旗舰/苍穹相同）。
---

## 2. 访问位矩阵（企业版 · 任意字段）

对**每一个**字段字符串，先判定访问位，再选层：

| 访问位（代码形态） | 必须用 | 禁止用 |
|--------------------|--------|--------|
| `GetValue` / `SetValue` / `e.Field.Key` / 控件 Key / OpenAPI `FieldKeys` / `OnPreparePropertys.FieldKeys.Add` | **Key** | PropertyName、FieldName |
| `billObj[...]` / `entry[...]` / `entity[...]` / `DataEntity[...]` / `DynamicObject` 索引 | **PropertyName** | Key、FieldName |
| SQL / KSQL 列 | **FieldName** | Key、PropertyName |

**铁律（与字段名无关）：**

- 同一字段在不同访问位上的字符串**可以不同**；禁止用「去掉 F」「默认同形」口算换层。  
- 三列只来自元数据（只读库 XML 或设计器）。无证据时：`// 假设 PropertyName=… Key=… source=assumption`。  
- **禁止**因为某个样例写了 `Qty` 就只在数量字段上小心；**任何** `entity["…"]` 都是 PropertyName 位。

**红旗（任意字段）：** 在 `entity[` / `entry[` / `billObj[` / `DataEntity[` 中写以 `F` 开头的串（如 `FQty`、`FAmount`、`FMaterialId`）——**极大概率把 Key 当了 PropertyName**。

**无元数据时的默认假设（可推翻）：**

| 访问位 | 默认假设 | 注释 |
|--------|----------|------|
| GetValue / FieldKeys | 沿用口语/设计器常见 **Key**（常带 `F`） | `// Key 假设` |
| 数据包索引 | **不要**默认等于 Key；优先按「非 Key 形态」占位并标注，或只写步骤不写死字符串 | `// 假设 PropertyName=MaterialId，非 FMaterialId；待库验证` |
| SQL | 常见大写库列 | `// FieldName 假设` |

禁止：把 `FMaterialId` 写进 `entry[...]` 却注释「这就是 PropertyName」——无三列证据时这是**混层**，不是合法假设。

```csharp
// 访问位 → 层
e.FieldKeys.Add("FMaterialId");     // Key 位
Model.GetValue("FMaterialId", row); // Key 位
entry["MaterialId"] = x;            // PropertyName 位（假设，待库验证）
// entry["FMaterialId"]             // 红旗：Key 形态进了数据包位
```



纠错口诀（反问同事时用）：

| 说法 | 判 |
|------|-----|
| GetValue 用 Key | 对 |
| 数据包 `entity[...]` 用 PropertyName | 对 |
| GetValue 用 PropertyName / 数据包用 Key | **错** |

完整卡：`three-identifiers.md`。

---

## 3. 生命周期位矩阵（任意业务动词）

先把需求归到**生命周期位**，再选基类与事件族。不背「保存样例 / 审核样例」清单，用下表归类：

| 生命周期位 | 典型口语线索 | 基类族 | 事件族 | 失败路径 |
|------------|--------------|--------|--------|----------|
| 界面录入联动 | 改了…跟着变 | 表单 Bill | `DataChanged` | UI 提示可选 |
| 界面打开后 UI | 打开后…只读/提示/显隐 | 表单 Bill | `AfterBindData` | UI |
| 界面点保存前校验 | 保存时…别过 | 表单 Bill | `BeforeDoOperation`(Save) 或 `BeforeSave` | `e.Cancel` + `ShowErrMessage` |
| 列表勾选批处理 | 列表勾几行…按钮 | 列表 List | 仅 List 手册事件 | 白名单消息 API |
| 操作服务（含审核/提交/无界面/接口同路径） | 审核/提交/接口也要拦 | 操作 Operation | 事务前/中事件 | **抛业务异常**，无 View 主路径 |
| 下推/转换 | 下推…带过去 | Convert | Convert 手册 | 手册约定 |
| 外部系统推单 | 外面推…组报文 | OpenAPI 辅助 | 报文+开关 | 非表单插件 |

**分流规则（通用）：**

- 有**界面单据编辑上下文**且用户说「保存」→ 表单事件族，**禁止**表单类实现操作事务事件。  
- 无界面、或审核/提交/WebAPI 与 UI **同一拦截** → 操作插件。  
- 列表工具栏 → 列表插件，**禁止**单据插件冒充。

详表：`enterprise-plugin-shape.md`。

---

## 4. API 面约束（任意插件类型）

```
选定基类
  → 打开该类型唯一手册 + Template（assets 表）
  → 生成代码中的类型/方法/属性 ⊆ 手册或同仓已有实现
  → 不在面内 → 不编造；写挂载步骤或搜同仓
```

| 插件类型 | API 面真源 |
|----------|------------|
| 表单 | `FormPlugin.md` + `FormPluginTemplate.cs` |
| 列表 | `ListPlugin.md` + `ListPluginTemplate.cs` |
| 操作 | `OperationPlugin.md` + `OperationPluginTemplate.cs` |
| 转换 | `ConvertPlugin.md` + Template |
| 其它 | 对应 assets 行 |

snippets（如 SaveValidate / ListBatch）是**面内示例**，不是唯一合法场景；新场景仍遵守：访问位矩阵 + 生命周期矩阵 + 只抄本类型手册。

---

## 5. 苍穹 / 旗舰 / 星瀚

- **不套**企业版三层拆分；字段用 Cosmic field key。  
- entityId = **元数据实体标识**，禁止物理表名（`t_…` / 无依据的表名串）。  
- API 面：`ok-cosmic` assets + cheat-sheet；禁止 `queryAll` / `setReadOnly` 等面外方法。

---

## 6. 写码前自检（任意场景 5 秒）

1. 产品线？  
2. 生命周期位 → 基类/事件族是否表内？  
3. 每个字段字符串的访问位 → 层是否表内？  
4. 每个调用是否落在当前类型 API 面？  
5. 无证据的字符串是否标了 `assumption`？
