Files

697 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 项目侧字段级配置重构计划
## 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/`