# 项目侧字段级配置重构计划

## 1. 目标

本次重构的目标，不是先统一项目侧全部 config / enum / dict 的历史实现，而是先补一套项目侧自己的“字段级配置总表”和配套工具，使下面这类问题有统一解法：

- 某个字段在某个 `project_type` 下有哪些可选项。
- 这些可选项的中文名是什么。
- 业务代码如何方便地做手动校验。
- 初始化流程如何从同一套字段级配置中读取候选项。
- 哪些字段应该继续在代码里用 enum 处理。
- 哪些字段应该迁到配置文件处理。

这套能力对齐集团侧 `enum_config.json` 的思路，但项目侧不照搬集团侧的存储形式，而是基于现有 `config` 表和 CSV 数据维护方式落地。

因此，本次重构除了建设字段级配置总表本身，还需要同步完成一件事：

- 梳理字段边界，明确哪些字段属于“固定语义、固定值集合、适合保留在代码 enum 中”，哪些字段属于“随 `project_type` 或初始化流程变化、应迁到字段级配置中”。

也就是说，这次不是单纯补一个配置表，而是要同时建立一套可执行的字段分流规则：

- enum 字段：继续留在代码中，由 enum 表达固定约束。
- field_config 字段：迁到配置中，由字段级配置表达候选项。

## 1.1 本轮实际要改的内容

这次文档和后续代码改造，不再停留在“抽象上应该统一”的层面，而是直接围绕下面 5 件事展开。

### 0. 通用能力

要补两类公共能力：

- 增强现有配置类，在 `ConfigService` 上新增字段级配置读取、映射、校验工具。
- 增强基于 JSON 导出 CSV 的工具，使字段级配置和现有 config 维护流程能直接联动。

### 1. 优化项目初始化过程

目标不是只分析 `init_project_data)`，而是把初始化链路改成统一从配置工具取候选项：

- 整理项目初始化当前实际使用到的 config。
- 把适合迁移的初始化候选项改为通过新的 config 工具获取后创建。
- 去掉不适当的 dict 用法，例如把单位塞在 dict `remark` 里。

### 2. 优化创建数据的验证

这次不改 schema 目录类型系统，但要改 service 层入口。

要求是：

- 在 create 类接口对应的 service 业务入口开头，基于 config 工具校验 code 类字段。
- 不再继续新增代码字典白名单、硬编码 if/else 白名单。

### 3. 统一下拉列表来源

这次不仅要梳理下拉列表，还要把它们收口到新的 config 工具：

- 梳理所有 code 类下拉列表当前返回路径。
- 能迁移的下拉统一改成通过 config 工具返回。
- 少量现有写死的下拉列表，例如合同结算类型列表，也要改成动态获取。

### 4. 删除废弃 enum / 硬编码字典

完成上述迁移后，要清掉已经不再适合作为真源的：

- 废弃 enum
- 废弃硬编码 dict
- 只用于旧下拉或旧创建校验的代码常量

### 5. 自动化测试脚本

这次不是只改文档和代码，还要补自动化测试，覆盖至少 3 类集成场景：

- 初始化脚本 / 初始化 service 集成测试
- 各业务下拉列表获取集成测试
- 各业务 create service 集成测试

测试脚本写在 `tests` 下，不写到项目根目录。

## 2. 重构范围

本次重构只覆盖一类字段：

- 对应集团侧 `enum_config.json` 相应字段的项目侧字段。
- 和 `project_type` 相关。
- 和项目初始化过程相关。

也就是说，本次优先处理的是“初始化候选项字段”，而不是全部字符串字段。

优先关注的业务范围包括：

- project 初始化时需要的字段候选项
- contract 初始化相关字段
- construction 初始化相关字段
- material 初始化相关字段
- power / preparation / production 中与项目类型初始化直接相关的字段

从现状代码看，这些初始化来源当前混杂在：

- Python enum
- Python 硬编码 dict
- `dict_data`
- `config` 表中的自由 JSON
- 外部业务大表或类型树

本次要做的不是一次性清理所有来源，而是为这些字段补一个统一的字段级配置出口。

对本轮来说，这里的“统一出口”不是泛指，而是明确服务于 3 条业务链路：

- 初始化：通过统一配置工具取候选项并创建数据
- 创建：通过统一配置工具校验 code 类字段
- 下拉：通过统一配置工具返回选项列表

对本轮来说，这里的“统一出口”不是泛指，而是明确服务于 3 条业务链路：

- 初始化：通过统一配置工具取候选项并创建数据
- 创建：通过统一配置工具校验 code 类字段
- 下拉：通过统一配置工具返回选项列表

## 3. 不在本次范围内的内容

以下内容明确不在本次重构范围：

- 接口返回层的翻译统一
- `dict` 体系的整体重构
- 所有历史自由 config 的一次性收敛
- 所有外部树和业务大表的一次性字段化
- schema 目录下的类型系统改造
- 运行时直接读取 JSON 文件作为配置源

这里补充两条边界：

- 不允许为了兼容旧逻辑继续新增 dict `remark` 之类的隐式字段承载方式。
- 不允许为了减少改动，在新工具落地后继续保留“前端下拉走 config、初始化走 dict、create 校验走代码字典”的三头并存状态。

这里补充两条边界：

- 不允许为了兼容旧逻辑继续新增 dict `remark` 之类的隐式字段承载方式。
- 不允许为了减少改动，在新工具落地后继续保留“前端下拉走 config、初始化走 dict、create 校验走代码字典”的三头并存状态。

因此，本次允许字段级配置中的 `value / label` 与现有 dict 翻译短期重复。后续如有需要，可再通过脚本生成 dict 或做统一翻译收敛。

## 4. 核心设计

### 4.1 新增“字段级配置总表”概念

项目侧需要新增一层固定格式的字段级配置。

这层配置的职责只有一个：

- 对外统一回答“某字段在某项目类型下有哪些可选项”。

它不是为了取代所有业务 config，而是为了给业务代码、初始化流程、推送系统提供统一的读取接口。

### 4.2 存储方式

字段级配置继续存放在现有 `config` 表中，并通过现有 CSV 数据维护。

建议约束如下：

- `category = project_type`
- `config_type = json`
- `config_key` 不强制按规则自动拼接
- 代码里仍允许手动传入 key 查询

虽然推荐把 key 命名为 `model_name.field_name`，例如：

- `construction.section_type_code`
- `material.material_type`
- `contract.code`

但这只是推荐命名，不作为框架自动推导前提。

也就是说：

- 本次不建设“根据 model + field 自动拼 config key”的机制
- 只建设“拿到 key 后，按统一 schema 读取字段级配置”的机制

### 4.3 固定 JSON 结构

字段级配置的 `default_value` / `config_value` 必须是一个 JSON object。

对于字段级配置，只强约束一件事：

- 顶层必须有 `field_config`

其中：

- `field_config` 必须是列表
- 列表中每个元素至少有 `value`、`label`

推荐最小结构如下：

```json
{
  "field_config": [
    {"value": "GF", "label": "光伏场区", "weight": 1.4},
    {"value": "SY", "label": "升压站", "weight": 1.4}
  ]
}
```

除 `field_config` 外，其它顶层字段不做统一规定，由各业务自己定义。

例如，某些字段配置可能会额外包含：

- `description`
- `init_config`
- 其他与该字段强相关的附属配置

这些字段允许存在，但不纳入统一字段级 schema 强约束。

### 4.4 非字段级 config 的统一要求

对于仍然保留的自由业务 config，不要求包含 `field_config`。

但建议默认增加一个顶层字段：

- `description`

这样至少能保证所有 config 在文档和工具层面具备最低限度的语义说明。

## 5. `field_config` Schema 设计

### 5.1 字段候选项结构

字段级配置中的 `field_config` 列表项，最少包含：

- `value: str`
- `label: str`

允许附加字段，例如：

- `weight`
- `unit`
- `section`
- `legacy`
- `flag`
- `skip_init`
- `skip_create`

这些附加字段不要求在所有字段中统一，但它们必须和值本身强相关。

### 5.2 建议的 Pydantic 模型

建议新增专门的 Pydantic schema，用于字段级配置校验。

```python
from pydantic import BaseModel, ConfigDict, Field


class FieldOptionItem(BaseModel):
    model_config = ConfigDict(extra="allow")

    value: str = Field(..., description="候选值")
    label: str = Field(..., description="显示名")
```

说明：

- 只需要识别 `field_config` 这个字段
- 只需要校验 `field_config` 是 `list[FieldOptionItem]`
- 其它顶层字段不在统一 schema 里写死

## 6. `ConfigService` 统一承载

字段级配置相关的辅助方法统一放到 `ConfigService`，不再额外拆新的独立 service。

也就是说，这次应增强的是：

- `app/modules/system/services/config.py`

而不是在别处再起一套并行 helper。

### 6.1 建议新增的方法

```python
async def get_field_config(project_type: str, key: str, tenant_id: str | None = None) -> dict[str, Any]:
    ...


async def get_field_options(project_type: str, key: str, tenant_id: str | None = None) -> list[dict[str, Any]]:
    ...


async def get_field_option_map(project_type: str, key: str, tenant_id: str | None = None) -> dict[str, dict[str, Any]]:
    ...


async def get_field_label(project_type: str, key: str, value: str, tenant_id: str | None = None) -> str | None:
    ...


async def validate_field_value(
    project_type: str,
    key: str,
    value: str,
    tenant_id: str | None = None,
    *,
    error_prefix: str | None = None,
) -> None:
    ...
```

如果后续需要覆盖“批量校验多个字段”或“直接返回下拉结构”的场景，可以继续补：

```python
async def get_field_options_for_frontend(project_type: str, key: str, tenant_id: str | None = None) -> list[dict[str, Any]]:
    ...


async def validate_field_values(project_type: str, items: list[tuple[str, str]], tenant_id: str | None = None) -> None:
    ...
```

如果后续需要覆盖“批量校验多个字段”或“直接返回下拉结构”的场景，可以继续补：

```python
async def get_field_options_for_frontend(project_type: str, key: str, tenant_id: str | None = None) -> list[dict[str, Any]]:
    ...


async def validate_field_values(project_type: str, items: list[tuple[str, str]], tenant_id: str | None = None) -> None:
    ...
```

### 6.2 各接口职责

`get_field_config()`：

- 读取指定 `project_type + key` 的 JSON 配置
- 找出顶层 `field_config`
- 校验 `field_config` 是否满足 `list[FieldOptionItem]`
- 返回完整配置对象

`get_field_options()`：

- 返回 `field_config` 列表
- 供初始化、下拉、候选项获取直接使用

`get_field_option_map()`：

- 返回 `{value: option}` 的映射
- 供快速查找、补充属性读取使用

`get_field_label()`：

- 根据 `value` 返回 `label`
- 本次主要服务于业务内部使用，不承担接口翻译统一职责

`validate_field_value()`：

- 手动调用
- 校验给定值是否在当前项目类型该字段的候选项中
- 校验失败时抛出业务异常

另外，本轮会直接用到两种典型调用方式：

- 初始化时：`get_field_options()` / `get_field_option_map()`
- 创建和编辑时：`validate_field_value()`
- 下拉接口时：`get_field_options_for_frontend()` 或 `get_field_options()`

另外，本轮会直接用到两种典型调用方式：

- 初始化时：`get_field_options()` / `get_field_option_map()`
- 创建和编辑时：`validate_field_value()`
- 下拉接口时：`get_field_options_for_frontend()` 或 `get_field_options()`

这里特别强调：

- `validate` 是手动调用的
- 本次不做自动注入所有模型或 schema 的统一校验框架
- 目标是让业务层“方便调用”，不是一次性接管所有校验入口

### 6.3 `ConfigService` 需要补齐的基础能力

`ConfigService` 当前除了读取外，还需要补齐两类基础能力：

#### 1. JSON 序列化 / 反序列化统一承载

不应继续让调用方自己到处 `msgspec.json.decode(...)`。

`ConfigService` 应统一负责：

- 读取 JSON 配置时反序列化
- 更新 JSON 配置时序列化
- 在字段级配置方法中复用这一套逻辑

也就是说，配置的 JSON 处理应沉到 `ConfigService` 本身，而不是散落在各业务模块里。

#### 2. 关键读取方法加缓存

`ConfigService` 中的关键读取方法应增加缓存。

优先包括：

- `get_config_json()`
- `get_field_config()`
- `get_field_options()`
- `get_field_option_map()`
- `get_field_label()`

要求：

- 读接口走缓存
- 更新配置后显式失效相关缓存
- 不允许缓存让旧配置长期滞留

`validate_field_value()` 自身不需要单独缓存，但应复用已经缓存过的字段选项读取结果。

### 6.4 实现要求

工具类内部应统一处理：

- 读取 `config` 表 JSON
- 用 Pydantic 校验 `field_config`
- 构建 options / map / label
- 输出清晰错误，便于发现配置格式问题

不允许的处理方式：

- 遇到 schema 错误时静默降级
- 遇到缺字段时自动猜测兼容成其他结构

如果配置不满足 `field_config` schema，应直接报错。

## 7. JSON / CSV 工具重整

本次不要求运行时直接读取 JSON 文件。

但为了方便调试和维护，应把现有 `scripts/tool/csv_data_tools/` 按新的字段级配置目标重新整理。

目标是：

- 一个 key 对应一个 JSON 文件
- JSON 文件仍然采用 config 行列表格式
- 工具负责 JSON 和 `config.csv`、数据库之间的转换

这块和本轮目标直接对应：

- 字段级配置调整后，可以直接导出为 `config.csv`
- 初始化改造前后，可以快速对比 config 差异
- 下拉列表和创建校验依赖的 config，可以单独导出检查，不再手工翻长 CSV

这块和本轮目标直接对应：

- 字段级配置调整后，可以直接导出为 `config.csv`
- 初始化改造前后，可以快速对比 config 差异
- 下拉列表和创建校验依赖的 config，可以单独导出检查，不再手工翻长 CSV

建议目录：

- `scripts/initdata/json_config/`

建议能力：

- `validate-json-config`: 校验 JSON 文件是否满足 config 行结构，以及 `field_config` 约束
- `json-to-csv`: 从 `json_config` 目录批量生成 `config.csv`
- `json-to-db`: 从 `json_config` 目录批量导入数据库
- `csv-to-json`: 从 `config.csv` 按 key 导出 JSON 文件
- `db-to-json`: 从数据库导出 JSON 文件用于调试

当前已有的 `scripts/tool/csv_data_tools/csv_data_tools.py` 可以复用思路，但需要围绕“字段级配置总表 + 一 key 一文件”的目标重新整理。

## 8. 领域重构指南

主计划只定义系统级方案。具体领域的梳理应拆到独立文件，例如：

- [construction.md](/Users/zyl/my_projects/ecm_sync_system/docs/enum_refactor/construction.md)
- [project.md](/Users/zyl/my_projects/ecm_sync_system/docs/enum_refactor/project.md)

后续 contract / material 等领域也应按同样方式分别整理。

每个领域文档都不是只看“集团侧已有字段”或者“准备迁到 `field_config` 的字段”。

每个领域都必须同时覆盖两部分字段：

- depm 中所有已经使用以下来源之一的字段：代码 enum、代码中的初始化列表、硬编码 dict、类 enum、`config` 表、`dict` 表
- 集团侧 `enum_config` 中该领域涉及的全部字段

两部分做并集，不做交集。

也就是说：

- `enum_config` 中出现的字段，必须全部纳入领域文档
- 即使某字段暂时没有出现在 `enum_config` 中，只要 depm 已经在用代码 enum / 类 enum / 初始化列表 / 硬编码 dict / `config` 表 / `dict` 表承载它，并且它影响初始化、手动创建、编辑、校验、展示，也必须纳入领域文档

这里要特别避免一种错误写法：

- 不能把领域范围写成“只讨论和集团侧 `enum_config` 直接对应的字段”
- 不能因为某字段暂时只有 depm 本地 enum / 类 enum，而集团侧还没有对应项，就把它排除在领域文档之外

每个领域文档都应至少覆盖以下内容：

### 8.1 先做字段总表

每个领域文档开头都应先列出字段总表，至少回答：

- 集团侧 `enum_config` 覆盖了哪些字段
- depm 侧当前还额外维护了哪些相关字段
- 每个字段当前使用的来源是什么：代码 enum、初始化列表、硬编码 dict、类 enum、`config` 表、`dict` 表，或它们的混用
- 每个字段分别影响哪些链路：项目初始化、手动创建、手动编辑、接口展示、历史数据兼容、业务校验

如果同一个字段在多个来源重复定义，也必须明确记出来，而不是只记录“最终准备保留哪个”。

### 8.2 整理不查表的 enum 类数据

不是只有查表型数据才需要整理。

如果某领域存在不依赖字典表、也不依赖 `config` 表的纯 enum / 纯硬编码候选项，同样要梳理：

- 当前定义位置在哪
- 是否重复定义
- 是否和集团侧 `enum_config` 对应字段有关
- 是否参与初始化、手动创建、手动编辑、展示翻译或历史校验
- 是否需要继续保留为代码 enum
- 是否应该迁到新的 `field_config`

也就是说，纯 enum 数据如果和初始化字段、手动创建字段有关，也在本轮视野内。

### 8.3 梳理哪些功能需要迁移到新的 `field_config`

每个领域都需要明确列出：

- 哪些初始化功能依赖字段候选项
- 哪些手动创建 / 编辑功能依赖字段候选项
- 哪些历史白名单校验需要迁移到 `validate_field_value()`
- 哪些直接遍历旧 JSON 的逻辑需要迁移到 `get_field_options()` / `get_field_option_map()`
- 哪些字段虽然目前由 `dict` 表或代码 enum 承载，但实际上已经在承担“按项目类型变化的候选项字段”职责

并且必须给出示例。

示例类型包括：

- 初始化时从旧 config 直接遍历候选项，改为从 `field_config` 读取
- 手动创建时从硬编码 dict 判断合法值，改为调用 `validate_field_value()`
- 展示名称时从字段选项里直接拿 `label`

对本轮来说，每个领域文档都至少要把下面 3 条链路写清楚：

- 初始化：这个字段初始化时从哪里取候选项，迁移后改成哪个 config key
- 创建：这个字段 create service 当前怎么校验，迁移后改成哪个 `validate_field_value()` 调用
- 下拉：这个字段前端当前通过哪个接口取值，迁移后改成哪个 config key 统一返回

对本轮来说，每个领域文档都至少要把下面 3 条链路写清楚：

- 初始化：这个字段初始化时从哪里取候选项，迁移后改成哪个 config key
- 创建：这个字段 create service 当前怎么校验，迁移后改成哪个 `validate_field_value()` 调用
- 下拉：这个字段前端当前通过哪个接口取值，迁移后改成哪个 config key 统一返回

### 8.4 必须完整对齐集团侧 `enum_config`

领域梳理时，不能只挑 depm 里已经处理过的字段，也不能只挑“看起来最容易改”的字段。

每个领域都必须把集团侧 `enum_config` 中该领域涉及的字段逐一对齐到 depm，明确说明它现在处于哪一种状态：

- depm 已有同名或同语义字段，并且来源清晰
- depm 有近似字段，但命名、语义、来源或取值范围不完全一致
- depm 还没有正式落点，只能先记录缺口

对齐 `enum_config` 时，每个字段都应继续检查两条链路：

- 项目初始化时，取值从哪里来，如何展开
- 手动创建 / 更新时，取值如何校验，是否和初始化使用同一来源

如果两条链路来源不一致，应明确记录并给出迁移方案。

## 9. 实施步骤

### 第一阶段：公共能力
### 第一阶段：公共能力

- 明确字段级配置固定 JSON 结构
- 新增 `FieldOptionItem`
- 在 `ConfigService` 中新增字段级配置方法
- 给关键读取方法加缓存
- 补齐 JSON 序列化 / 反序列化统一处理
- 明确错误处理规则：配置结构不合法时直接报错
- 增强 JSON / CSV 工具，支持字段级配置导入导出

### 第二阶段：初始化改造

- 逐项整理 `init_project_data)` 当前真实依赖的 config / dict
- 把 `preparation`、`production`、`power` 这类平铺型初始化模板迁到 config
- 整理 `material` 初始化，把单位等附属属性从不适当的 dict 备注迁出
- 清理 `init_project_data)` 中取了但不该继续作为真源的 dict 依赖

### 第三阶段：创建校验改造

- 为 contract / material / construction 等 create service 入口补基于 config 的 code 校验
- 把当前分散在 service 内的硬编码白名单逐步迁到 config 工具
- 保留真正应该继续存在的代码 enum，不把固定语义状态误迁到 config

### 第四阶段：下拉列表改造
### 第四阶段：下拉列表改造

- 梳理 code 类下拉接口
- 下拉统一改成通过 config 工具返回
- 少量仍写死的下拉改为动态获取
- 保证前端下拉、初始化候选项、create 校验三者同源

### 第五阶段：清理和测试

- 删除废弃 enum 和废弃代码字典
- 删除不再适合作为真源的 dict 依赖
- 补 service 层集成测试和初始化测试
- 梳理 code 类下拉接口
- 下拉统一改成通过 config 工具返回
- 少量仍写死的下拉改为动态获取
- 保证前端下拉、初始化候选项、create 校验三者同源

### 第五阶段：清理和测试

- 删除废弃 enum 和废弃代码字典
- 删除不再适合作为真源的 dict 依赖
- 补 service 层集成测试和初始化测试

## 10. 产出要求

本次重构最终至少要产出：

- 一套明确的字段级配置 schema
- 一套明确的 `ConfigService` 字段级配置接口
- 一套明确的缓存与缓存失效规则
- 一套 JSON / CSV / DB 三者之间的转换与维护规范
- 一组领域级重构文档
- 一组初始化 / 下拉 / create 的 service 层集成测试
- 一组初始化 / 下拉 / create 的 service 层集成测试

判断本次重构是否成功的标准不是“历史实现全部统一”，而是：

- 后续新增一个与项目类型初始化相关的字段时，知道应优先写入字段级配置总表
- 业务代码可以通过 `ConfigService` 统一获取 options / map / label / validate
- 初始化流程和手动创建校验可以逐步收敛到同一来源
- 前端下拉、初始化、create 校验不再各自读不同真源

## 11. 自动化测试要求

这次测试不是只补 unit test，而是以 service 层集成测试为主。

至少覆盖下面 3 组：

### 11.1 初始化测试

- 覆盖项目初始化入口
- 校验初始化创建出的 preparation / production / contract / material / power / construction 数据是否来自目标 config
- 校验错误配置时直接报错，不做静默降级

### 11.2 下拉列表测试

- 覆盖 contract / material / construction 等核心下拉接口或对应 service
- 校验返回项是否来自预期 config key
- 校验下拉返回和初始化候选项是否一致

### 11.3 Create 校验测试

- 直接走 service 层 create
- 对合法 code 校验通过
- 对非法 code 明确抛错
- 校验不再依赖旧硬编码字典或 dict `remark`

### 11.4 测试组织方式

- 测试代码写在 `tests/` 下
- 优先写 service 层集成测试，不在项目根目录放临时脚本
- 如果需要初始化测试数据，应复用现有测试体系，不改 `schemas/`、不改 `filtered_datasource/`
- 前端下拉、初始化、create 校验不再各自读不同真源

## 11. 自动化测试要求

这次测试不是只补 unit test，而是以 service 层集成测试为主。

至少覆盖下面 3 组：

### 11.1 初始化测试

- 覆盖项目初始化入口
- 校验初始化创建出的 preparation / production / contract / material / power / construction 数据是否来自目标 config
- 校验错误配置时直接报错，不做静默降级

### 11.2 下拉列表测试

- 覆盖 contract / material / construction 等核心下拉接口或对应 service
- 校验返回项是否来自预期 config key
- 校验下拉返回和初始化候选项是否一致

### 11.3 Create 校验测试

- 直接走 service 层 create
- 对合法 code 校验通过
- 对非法 code 明确抛错
- 校验不再依赖旧硬编码字典或 dict `remark`

### 11.4 测试组织方式

- 测试代码写在 `tests/` 下
- 优先写 service 层集成测试，不在项目根目录放临时脚本
- 如果需要初始化测试数据，应复用现有测试体系，不改 `schemas/`、不改 `filtered_datasource/`