diff --git a/docs/enum_refactor/construction.md b/docs/enum_refactor/construction.md new file mode 100644 index 0000000..5ded210 --- /dev/null +++ b/docs/enum_refactor/construction.md @@ -0,0 +1,183 @@ +# Construction 领域重构指南 + +## 1. 范围 + +本文档只讨论 construction 领域中,与集团侧 `enum_config` 对应、且和项目类型初始化有关的字段。 + +当前本轮只聚焦一条主线: + +- `construction.section_type_code` + +以下复杂问题本轮先明确跳过,不在本文展开: + +- `task_id` +- `task_type` +- 其它施工节点层级和映射规则特别复杂的问题 + +也就是说,施工文档本轮先解决 section 这一层,不强行把 task 相关复杂问题一起收进来。 + +## 2. 当前现状 + +construction 领域当前至少有两类配置来源: + +### 2.1 `construction_section_config` + +当前按项目类型配置 section 信息,例如光伏下的: + +- `code` +- `name` +- `weight` +- `project_root_nodes` + +这份配置同时承担: + +- section 候选项来源 +- 施工初始化入口信息来源 + +### 2.2 `construction_node_config` + +当前按项目类型配置节点信息,例如: + +- `task_code` +- `type_id` +- `type_name` +- `unit` +- `weight` +- `is_primary` +- `section` + +这份配置描述的是节点补充信息,不是字段级配置。 + +## 3. 重构目标 + +construction 领域本轮的目标不是重写全部施工配置,而是: + +- 把 section 候选项迁到新的字段级配置 +- 保留 `construction_node_config` 作为自由业务配置 +- 让施工初始化和手动校验可以逐步复用统一的字段级入口 + +## 4. `construction.section_type_code` 的新结构 + +`construction_section_config` 应改造成新的字段级 key: + +- `construction.section_type_code` + +它的 JSON 中必须包含: + +- `field_config` + +并且可以额外包含业务自定义字段,例如: + +- `description` +- `init_config` + +推荐结构: + +```json +{ + "description": "施工区字段配置", + "field_config": [ + {"value": "GF", "label": "光伏场区", "weight": 1.4}, + {"value": "SY", "label": "升压站", "weight": 1.4}, + {"value": "SC", "label": "送出(自建)", "weight": 1.2}, + {"value": "SCDW", "label": "送出(电网建设)", "weight": 1.0} + ], + "init_config": { + "root_nodes_by_value": { + "GF": [ + {"type_id": "01", "type_code": "01", "type_name": "光伏区土建工程"}, + {"type_id": "03", "type_code": "03", "type_name": "光伏区安装工程"} + ], + "SY": [ + {"type_id": "02", "type_code": "02", "type_name": "升压站土建工程"} + ] + } + } +} +``` + +这里的原则是: + +- `field_config` 负责 section 候选项 +- `init_config` 负责 section 相关初始化附属数据 +- 两者可以同处一个 JSON 中,不需要再拆第二份 section 配置 + +## 5. `construction_node_config` 的定位 + +`construction_node_config` 不迁到字段级配置。 + +它仍然是自由业务配置,负责描述: + +- 可能节点有哪些种类 +- 哪些节点是主节点 +- 节点单位、权重等补充信息 + +建议只做一项统一性改造: + +- 顶层补 `description` + +## 6. 需要迁移到 `field_config` 的功能 + +construction 领域应重点梳理下面两类功能: + +### 6.1 项目初始化链路 + +需要明确: + +- 初始化 section 时,候选项是否改由 `get_field_options(project_type, "construction.section_type_code")` 提供 +- 初始化 root node 时,是否从同一配置对象中的业务自定义字段读取附属数据 + +也就是说,初始化链路应逐步从“直接解释旧 `construction_section_config`”迁移到“先读取字段级配置,再读取其业务附属字段”。 + +### 6.2 手动创建 / 编辑链路 + +需要明确: + +- 如果某接口允许手动传 `section_type_code` +- 是否应改为调用 `validate_field_value(project_type, "construction.section_type_code", value)` + +这样可以保证手动写入与初始化使用同一套候选项来源。 + +## 7. 当前迁移示例 + +### 示例 1:section 候选项读取 + +当前: + +- 通过 construction 私有 helper 直接读取并解释 `construction_section_config` + +目标: + +- 通过 `ConfigService.get_field_options(project_type, "construction.section_type_code")` 获取 section 列表 + +### 示例 2:section 合法值校验 + +当前: + +- 可能由业务代码自行判断,或者根本没有统一校验入口 + +目标: + +- 统一走 `ConfigService.validate_field_value(...)` + +### 示例 3:初始化附属数据读取 + +当前: + +- 直接从旧 `construction_section_config` 读取 `project_root_nodes` + +目标: + +- 从 `construction.section_type_code` 的完整配置对象中读取业务自定义字段 + +## 8. 梳理清单 + +construction 领域后续梳理时,至少要回答下面这些问题: + +1. 哪些字段属于集团侧 `enum_config` 已覆盖或可对应的字段。 +2. 哪些字段在初始化时使用候选项,哪些字段在手动创建时也要校验。 +3. 哪些逻辑仍在直接解释旧 JSON,而不是通过统一 helper 读取。 +4. 哪些逻辑仍在使用硬编码白名单。 +5. `construction_node_config` 中哪些数据只是节点补充信息,哪些其实已经在承担字段候选项职责。 + +如果发现某字段既参与初始化又参与手动创建,就应优先考虑迁到字段级配置。 \ No newline at end of file diff --git a/docs/enum_refactor/plan.md b/docs/enum_refactor/plan.md new file mode 100644 index 0000000..c40f4fb --- /dev/null +++ b/docs/enum_refactor/plan.md @@ -0,0 +1,440 @@ +# 项目侧字段级配置重构计划 + +## 1. 目标 + +本次重构的目标,不是先统一项目侧全部 config / enum / dict 的历史实现,而是先补一套项目侧自己的“字段级配置总表”和配套工具,使下面这类问题有统一解法: + +- 某个字段在某个 `project_type` 下有哪些可选项。 +- 这些可选项的中文名是什么。 +- 业务代码如何方便地做手动校验。 +- 初始化流程如何从同一套字段级配置中读取候选项。 +- 哪些字段应该继续在代码里用 enum 处理。 +- 哪些字段应该迁到配置文件处理。 + +这套能力对齐集团侧 `enum_config.json` 的思路,但项目侧不照搬集团侧的存储形式,而是基于现有 `config` 表和 CSV 数据维护方式落地。 + +因此,本次重构除了建设字段级配置总表本身,还需要同步完成一件事: + +- 梳理字段边界,明确哪些字段属于“固定语义、固定值集合、适合保留在代码 enum 中”,哪些字段属于“随 `project_type` 或初始化流程变化、应迁到字段级配置中”。 + +也就是说,这次不是单纯补一个配置表,而是要同时建立一套可执行的字段分流规则: + +- enum 字段:继续留在代码中,由 enum 表达固定约束。 +- field_config 字段:迁到配置中,由字段级配置表达候选项。 + +## 2. 重构范围 + +本次重构只覆盖一类字段: + +- 对应集团侧 `enum_config.json` 相应字段的项目侧字段。 +- 和 `project_type` 相关。 +- 和项目初始化过程相关。 + +也就是说,本次优先处理的是“初始化候选项字段”,而不是全部字符串字段。 + +优先关注的业务范围包括: + +- project 初始化时需要的字段候选项 +- contract 初始化相关字段 +- construction 初始化相关字段 +- material 初始化相关字段 +- power / preparation / production 中与项目类型初始化直接相关的字段 + +从现状代码看,这些初始化来源当前混杂在: + +- Python enum +- Python 硬编码 dict +- `dict_data` +- `config` 表中的自由 JSON +- 外部业务大表或类型树 + +本次要做的不是一次性清理所有来源,而是为这些字段补一个统一的字段级配置出口。 + +## 3. 不在本次范围内的内容 + +以下内容明确不在本次重构范围: + +- 接口返回层的翻译统一 +- `dict` 体系的整体重构 +- 所有历史自由 config 的一次性收敛 +- 所有外部树和业务大表的一次性字段化 +- schema 目录下的类型系统改造 +- 运行时直接读取 JSON 文件作为配置源 + +因此,本次允许字段级配置中的 `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: + ... +``` + +### 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()`: + +- 手动调用 +- 校验给定值是否在当前项目类型该字段的候选项中 +- 校验失败时抛出业务异常 + +这里特别强调: + +- `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`、数据库之间的转换 + +建议目录: + +- `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 / contract / material 等领域也应按同样方式分别整理。 + +每个领域文档都应至少覆盖以下内容: + +### 8.1 整理不查表的 enum 类数据 + +不是只有查表型数据才需要整理。 + +如果某领域存在不依赖字典表、也不依赖 config 表的纯 enum / 纯硬编码候选项,同样要梳理: + +- 当前定义位置在哪 +- 是否重复定义 +- 是否和集团侧 `enum_config` 对应字段有关 +- 是否需要继续保留为代码 enum +- 是否应该迁到新的 `field_config` + +也就是说,纯 enum 数据如果和初始化字段、手动创建字段有关,也在本轮视野内。 + +### 8.2 梳理哪些功能需要迁移到新的 `field_config` + +每个领域都需要明确列出: + +- 哪些初始化功能依赖字段候选项 +- 哪些手动创建 / 编辑功能依赖字段候选项 +- 哪些历史白名单校验需要迁移到 `validate_field_value()` +- 哪些直接遍历旧 JSON 的逻辑需要迁移到 `get_field_options()` / `get_field_option_map()` + +并且必须给出示例。 + +示例类型包括: + +- 初始化时从旧 config 直接遍历候选项,改为从 `field_config` 读取 +- 手动创建时从硬编码 dict 判断合法值,改为调用 `validate_field_value()` +- 展示名称时从字段选项里直接拿 `label` + +### 8.3 重点关注集团侧 `enum_config` 对应字段 + +领域梳理时,应优先关注集团侧 `enum_config` 中已有对应关系的字段。 + +因为这类字段本身就属于: + +- 按项目类型变化 +- 会参与初始化 +- 通常也会参与手动创建与校验 + +因此,每个领域都应围绕这类字段检查两条链路: + +- 项目初始化时,取值从哪里来,如何展开 +- 手动创建 / 更新时,取值如何校验,是否和初始化使用同一来源 + +如果两条链路来源不一致,应明确记录并给出迁移方案。 + +## 9. 实施步骤 + +### 第一阶段:定义公共 schema 和 `ConfigService` 接口 + +- 明确字段级配置固定 JSON 结构 +- 新增 `FieldOptionItem` +- 在 `ConfigService` 中新增字段级配置方法 +- 给关键读取方法加缓存 +- 补齐 JSON 序列化 / 反序列化统一处理 +- 明确错误处理规则:配置结构不合法时直接报错 + +### 第二阶段:重整 JSON / CSV 工具 + +- 建立 `scripts/initdata/json_config/` 目录规范 +- 支持一 key 一文件 +- 支持 `json_config` 批量生成 `config.csv` +- 支持 `json_config` 批量导入数据库 +- 支持从 `config.csv` / 数据库反向导出 JSON + +### 第三阶段:按领域逐个梳理并迁移 + +- 按领域编写独立重构文档 +- 明确该领域哪些字段迁到 `field_config` +- 改造初始化流程和手动创建校验流程 +- 下线对应的硬编码白名单和不再适合承担初始化来源的 dict 使用方式 + +## 10. 产出要求 + +本次重构最终至少要产出: + +- 一套明确的字段级配置 schema +- 一套明确的 `ConfigService` 字段级配置接口 +- 一套明确的缓存与缓存失效规则 +- 一套 JSON / CSV / DB 三者之间的转换与维护规范 +- 一组领域级重构文档 + +判断本次重构是否成功的标准不是“历史实现全部统一”,而是: + +- 后续新增一个与项目类型初始化相关的字段时,知道应优先写入字段级配置总表 +- 业务代码可以通过 `ConfigService` 统一获取 options / map / label / validate +- 初始化流程和手动创建校验可以逐步收敛到同一来源 \ No newline at end of file