# init_project_data 现状分析

## 1. 范围

本文档只分析 backend 里的 `ProjectService.init_project_data)` 当前到底做了什么。

重点回答 4 个问题：

- 这个函数当前创建了哪些业务数据。
- 它直接读取了哪些 `config` 和 `dict`。
- 哪些 `dict` 是真的在负责初始化，哪些只是历史残留。
- 如果把这里 `dict` 负责的部分迁到 `config`，复杂度到底高不高。

## 2. 触发时机

`init_project_data)` 在项目审批通过后执行。

调用链是：

- `ProjectService.audit_project()`
- `ProjectService._audit_approval()`
- `ProjectService.init_project_data)`

也就是说，这不是“创建项目主表时立即执行”的逻辑，而是“审批通过后批量灌初始化数据”的逻辑。

## 3. 函数当前做的 6 件事

按执行顺序，当前函数会做下面 6 件事：

1. 读取当前 `project_type` 整个 category 下的全部 config。
2. 读取 6 组 dict 数据。
3. 初始化 `preparation` 里程碑节点。
4. 初始化 `production` 投产节点。
5. 初始化 `contract` 合同，并在合同初始化内部继续初始化 `contract_settlement` 和 `material`。
6. 初始化 `power` 力能配置、`construction` 施工任务、`archive` 电子档案。

注意：

- 这个函数表面上不长，但它本质上是一个“项目初始化编排器”。
- 真正复杂的地方不只在它自己，而在它调用出去的各个 `init_from_*`。

## 4. 直接读取了什么

### 4.1 直接读取的 config

函数一开始会调用：

- `config_service.get_category_config(category=project.project_type)`

这一步不是只读某一个 key，而是把当前 `project_type` 这个 category 下的全部 config 都读出来，再做：

- `configs = {config_key: decoded_json}`

但在 `init_project_data)` 自己内部，真正直接用到的 config 只有 1 个：

- `contract_config`

也就是说：

- 直接读取：整类 config
- 直接消费：只有 `contract_config`
- 其余 config 在这个函数里只是被一并装进了 `configs`，并没有直接使用

### 4.2 直接读取的 dict

函数会一次性读取 6 个 dict key：

- `preparation_{project_type}`
- `production_{project_type}`
- `construction_contract_{project_type}`
- `service_contract_{project_type}`
- `material_contract_{project_type}`
- `power_{project_type}`

但这 6 个 key 不是都真正被当前函数消费了。

## 5. 哪些数据是怎么创建出来的

### 5.1 Preparation

创建来源：

- 直接使用 dict：`preparation_{project_type}`

创建方式：

- 读取 dict 项的 `code`、`name`
- 先删项目下旧的 `Preparation`
- 再批量插入新的 `Preparation`

涉及配置数量：

- `config`: 0
- `dict`: 1 组

当前种子数据条目数：

- `preparation_photovoltaic`: 12 条
- `preparation_onshore_wind`: 13 条
- `preparation_offshore_wind`: 14 条

结论：

- 这是最典型的“dict 直接承担初始化模板”场景。
- 结构非常简单，迁到 `config` 不复杂。

### 5.2 Production

创建来源：

- 直接使用 dict：`production_{project_type}`

创建方式：

- 读取 dict 项的 `code`、`name`
- 先删项目下旧的 `Production`
- 再批量插入新的 `Production`

涉及配置数量：

- `config`: 0
- `dict`: 1 组

当前种子数据条目数：

- `production_photovoltaic`: 9 条
- `production_onshore_wind`: 9 条
- `production_offshore_wind`: 9 条

结论：

- 和 `preparation` 一样，是平铺型 code/name 列表。
- 迁到 `config` 也不复杂。

### 5.3 Contract

创建来源：

- 直接使用 config：`contract_config`

创建方式：

- `init_project_data)` 从 `configs["contract_config"]` 取出列表
- `ContractService.init_from_project()` 根据每项的 `code`、`name`、`contract_type` 创建合同
- 先删项目下旧合同，再批量插入新合同

后续联动：

- 初始化 `contract_settlement`
- 初始化 `material`

涉及配置数量：

- `config`: 1 个 key
- `dict`: 0 组直接参与合同创建

当前 `contract_config` 条目数：

- `photovoltaic`: 21 条
- `onshore_wind`: 20 条
- `offshore_wind`: 20 条

结论：

- 合同初始化本体已经是 config 驱动，不是 dict 驱动。
- 所以这里真正要迁移的不是“合同本身”，而是合同初始化后还依赖的那些配套 dict。

### 5.4 Material

创建来源：

- 不是 `init_project_data)` 直接创建。
- 它是 `contract.init_from_project()` 内部继续调用 `material.init_from_contract()` 创建。

当前来源拆分：

- 合同列表来源：`contract_config`
- 物资单位来源：`material_contract_{project_type}` dict

也就是说，当前 `material` 初始化其实是两段拼起来的：

- 先由 `contract_config` 决定会创建哪些货物类合同
- 再由 `material_contract_*` dict 给这些物资补 `unit`

涉及配置数量：

- `config`: 1 个上游 key `contract_config`
- `dict`: 1 组 `material_contract_{project_type}`

当前种子数据条目数：

- `material_contract_photovoltaic`: 14 条
- `material_contract_onshore_wind`: 12 条
- `material_contract_offshore_wind`: 7 条

这里有一个关键现状：

- `material` 实际创建多少条，取决于 `contract_config` 里有多少 `product` 合同。
- `material_contract_*` dict 只负责补充单位和名称，不负责决定是否创建。
- 因此这两套来源当前不是同一个真源。

已看到的分叉：

- 光伏 `contract_config` 里的货物类候选项和 `material_contract_photovoltaic` 并不完全一致。
- 陆风也存在类似分叉。
- 海风更明显：当前能看到 `contract_config` 已有较完整货物类合同，但 `dict` 侧条目明显更少。

结论：

- `material` 不是简单的“把一个 dict 搬到 config”。
- 真正要做的是先合并“合同货物候选项”和“物资单位/展示信息”两套来源。
- 这块复杂度中等，不是最高，但已经不是平铺搬运。

### 5.5 Power

创建来源：

- 直接使用 dict：`power_{project_type}`

创建方式：

- 读取 dict 项的 `code`、`name`
- 统一绑定到 `contract_data.get("EPC")`
- 先删项目下旧 `Power`
- 再批量插入新 `Power`

涉及配置数量：

- `config`: 0
- `dict`: 1 组

当前种子数据条目数：

- `power_photovoltaic`: 1 条
- `power_onshore_wind`: 2 条
- `power_offshore_wind`: 2 条

结论：

- 也是平铺型初始化模板，迁到 `config` 不复杂。
- 但它和合同存在一条强耦合：默认绑定 `EPC` 合同。

### 5.6 Construction

创建来源：

- `init_project_data)` 自己没有直接给施工传 dict 或 config 数据
- 只调用：`create_constructions_for_project(tenant_id, project_id)`

但施工初始化内部会继续读取：

- `construction_section_config`

并依赖：

- `BuildMethod`
- `ConstructionTypeTree`
- `ConstructionTypeMapping`

实际创建内容：

- 按 `construction_section_config.sections[*].project_root_nodes` 创建项目根单位工程
- 基于这些根节点再创建主施工任务
- 对质量验收类施工继续创建施工树
- 对主施工继续自动补次级施工

涉及配置数量：

- `config`: 至少 1 个 key `construction_section_config`
- `dict`: 0
- 另外强依赖数据库里的施工类型树，不是简单 JSON 配置

结论：

- 施工初始化本来就不是 dict 驱动，已经是 config + 类型树驱动。
- 这里真正复杂的不是“dict 要不要迁 config”，而是施工本身就已经是结构化初始化。

### 5.7 Archive

创建来源：

- 不依赖 dict
- 也不依赖 `init_project_data)` 读出来的 config

当前方式：

- 按 `template/{project_type}_archive.xlsx` 模板文件初始化电子档案树

涉及配置数量：

- `config`: 0（对 `_init_project_data` 而言）
- `dict`: 0
- 外部模板文件: 1 份 Excel

结论：

- 电子档案初始化不属于这次 dict/config 收敛的核心问题。

## 6. 这 6 组 dict 里，哪些真的在负责事情

### 6.1 直接被 `init_project_data)` 消费的 dict

- `preparation_{project_type}`
- `production_{project_type}`
- `power_{project_type}`

这 3 组 dict 是真的在当前函数里直接承担初始化模板职责。

### 6.2 被 `init_project_data)` 取了，但当前函数里没有直接消费的 dict

- `construction_contract_{project_type}`
- `service_contract_{project_type}`
- `material_contract_{project_type}`

其中：

- `construction_contract_*`：当前只看到被 `init_project_data)` 取出，但没有实际使用。
- `service_contract_*`：当前也只看到被 `init_project_data)` 取出，但没有实际使用。
- `material_contract_*`：当前函数本身没用，但会在 `MaterialService.init_from_contract()` 里被重新读取一次，用于补单位。

这说明当前 `init_project_data)` 至少存在两类历史包袱：

- 取了但没用的 dict key
- 同一套语义在上游合同 config 和下游 material dict 之间分叉

## 7. 把 dict 迁到 config，会不会太复杂

### 7.1 不复杂的部分

下面 3 组迁移到 config，复杂度不高：

- `preparation_{project_type}`
- `production_{project_type}`
- `power_{project_type}`

原因很简单：

- 它们当前就是平铺的 `code/name` 列表
- 服务接口已经接受 `list[dict]`
- 初始化逻辑只做“删旧 + 批量重建”
- 不依赖复杂树结构，也不依赖额外业务联动

如果迁移：

- `preparation`、`production` 很适合直接改成 `field_config`
- `power` 更像一个小型初始化模板，也可以直接放到 config，但不一定要走 `field_config` 这个名字

### 7.2 中等复杂的部分

`material_contract_{project_type}` 迁移到 `config`，复杂度中等。

原因不是数据量大，而是来源分叉：

- 哪些货物类记录会被创建，取决于 `contract_config`
- 每种物资的显示名、单位，又取决于 `material_contract_*` dict

所以这块不是“dict 搬家”，而是“先统一真源”。

更稳妥的做法是：

- 先把 `contract_config` 和 `material_contract_*` 合并成一套正式结构
- 再决定是否把单位、显示名、parts 一并并到 `material.field_config` 或其它 config key

### 7.3 高复杂的部分其实不在 dict

真正复杂的初始化不是 dict，而是施工：

- 它已经依赖 `construction_section_config`
- 又依赖 `ConstructionTypeTree`
- 又依赖 `ConstructionTypeMapping`
- 还会继续生成施工树和次级施工

所以如果目标是“降低 `init_project_data` 初始化复杂度”，优先清理 dict 只能解决一部分表层问题，不能解决施工初始化本身的结构复杂度。

## 8. 额外现状问题

### 8.1 `init_project_data)` 读了整类 config，但只直接用了 `contract_config`

这会导致：

- 函数看起来像“config 驱动”
- 实际上只有合同初始化是直接 config 驱动

### 8.2 有 3 个 dict key 在当前函数里不是实际输入

- `construction_contract_*`
- `service_contract_*`
- `material_contract_*`

其中前 2 个当前更像历史残留。

### 8.3 当前只明显覆盖了 3 类项目类型的初始化种子

从现有 `dict.csv` / `config.csv` 看，当前明确有初始化种子的主要是：

- `photovoltaic`
- `onshore_wind`
- `offshore_wind`

而 `project_type` 枚举里其实还有：

- `pumped_storage`
- `hydropower`
- `thermal_power`
- `gas_turbine`

也就是说，`project_type` 枚举范围和初始化种子覆盖范围并不一致。

### 8.4 海风配置目前看起来不完整

当前能直接看到：

- `offshore_wind` 有 `contract_config`
- 也有 `preparation_*`、`production_*`、`power_*`、`material_contract_*` dict

但当前 `config.csv` 里没看到海风的：

- `construction_section_config`
- `construction_node_config`
- `material_types`
- `supply_chain_group`

这意味着海风初始化链路当前很可能并不完整，至少配置层不齐。

## 9. 结论

可以把 `init_project_data)` 当前的配置来源，拆成 4 类理解：

1. 纯 dict 初始化：`preparation`、`production`、`power`
2. 纯 config 初始化：`contract`
3. config + dict 混合初始化：`material`
4. config + 类型树 + 模板文件初始化：`construction`、`archive`

所以“把这里 dict 负责的东西迁到 config 会不会太复杂”的答案不是一个统一结论：

- `preparation`、`production`、`power`：不复杂，可以迁。
- `material_contract`：中等复杂，先统一真源再迁。
- `construction`：复杂点不在 dict，本来就不是 dict 驱动。

## 10. 建议落地顺序

1. 先把 `init_project_data)` 里“取了但没直接用”的 `construction_contract_*`、`service_contract_*` 从函数里清出来，确认是否还能删掉。
2. 再把 `preparation_{project_type}`、`production_{project_type}`、`power_{project_type}` 迁到 config。
3. 再处理 `material_contract_*`，但前提是先统一 `contract_config` 和 material 初始化所需的单位/展示信息来源。
4. 施工初始化不要和这批 dict 迁移混做，单独处理。

## 10.1 按本轮改造目标，初始化链路具体要改什么

如果把这次目标收敛成“初始化统一走配置工具”，那么 `init_project_data)` 不应该再只是分析对象，而是明确要往下面的形态改。

### A. 保留并整理的 config

初始化链路应继续保留并整理这几类 config：

- `contract_config`：合同初始化候选项
- `construction_section_config`：施工区域 / 单位工程 / 任务入口配置
- `material_types`：物资类型、展示名、单位、parts 等字段配置
- 后续新增的字段级配置 key，例如：
	- `preparation.code`
	- `production.code`
	- `power.code`

这里的关键不是 config 数量多少，而是把“初始化真的要用的候选项”收口到 config 工具统一读取。

### B. 应从 dict 迁出的初始化候选项

下面这几类当前属于平铺型初始化模板，适合迁出 dict：

- `preparation_{project_type}`
- `production_{project_type}`
- `power_{project_type}`

迁移后的目标形态是：

- `init_project_data)` 不再直接读这些 dict key
- 改由 `ConfigService.get_field_options()` 或等价工具读取字段候选项
- service 仍只负责“删旧 + 批量重建”，不再关心候选项来自 dict 还是 config

### C. 应去掉的不适当 dict 用法

本轮最典型的不适当 dict 用法是：

- `material_contract_{project_type}` 用 `remark` 存物资单位

这类做法的问题不是“它现在不能工作”，而是：

- 含义不显式
- 初始化、创建、下拉三条链路都没法自然共用
- 后续导出 CSV / 校验时也不容易发现结构错误

迁移方向应是：

- 把 `unit`、`label`、`parts` 等字段并回正式 config 结构
- 不再让单位这类业务字段挂在 dict `remark` 上

### D. `init_project_data)` 迁移后的读取原则

迁移后，`init_project_data)` 本身应只做 2 件事：

- 找到当前项目类型下所需的候选项配置
- 把候选项列表传给各领域初始化 service

它不应该继续承担：

- 现场拼 JSON 结构
- 自己手工区分 dict / config / remark
- 同时取一堆历史 key 再由下游各自二次分叉读取

### E. 初始化改造完成后的判定标准

初始化侧改造完成后，至少应满足：

- `preparation`、`production`、`power` 初始化不再依赖 dict
- `material` 初始化不再依赖 dict `remark` 补单位
- 初始化候选项与前端下拉、create 校验使用同一套 config 真源
- `init_project_data)` 中不再保留只读取不用的 dict key

## 11. 从接口创建视角反看这些字段是不是已经“主要 config 化”

只看 `init_project_data)`，容易得出“系统已经比较 config 驱动”的印象。

但如果再抽样看接口创建路径，会发现当前真实情况是：

- 初始化层里，确实有一部分已经走 `config`
- 但创建接口里，很多 enum-like 字段仍然主要靠代码常量或 service 层校验
- schema 层大多还是普通 `str`，并没有把这些候选值正式收口到统一配置来源

### 11.1 Contract 创建

接口入参：

- `ContractCreate` 里的 `contract_type`、`code` 都只是普通字符串

创建时字段来源：

- `contract_type`、`code` 直接来自接口传参
- 合同名称不是前端传，而是 service 根据 `contract_code_dict` 动态拼出来

当前校验方式：

- schema 层：只有长度约束，没有枚举白名单
- service 层：
	- `code` 必须在 `contract_code_dict`
	- `contract_type` 必须在 `contarct_type_dict`
	- `service` 合同再限制为 `JL`、`KY`、`E`、`SCYW`
	- `product` 合同再限制为一组硬编码产品 code

结论：

- 合同初始化是 `contract_config` 驱动
- 但合同接口创建仍然主要依赖代码字典和硬编码白名单
- 这说明“初始化层已 config 化”并不等于“合同域已经完成 config 收口”

### 11.2 Material 创建

接口入参：

- `MaterialCreate.material_type`、`approval_party` 也都是普通字符串

创建时字段来源：

- `material_type` 直接来自接口传参
- `show_name`、`unit` 不是前端传，而是 `MaterialService.get_material_info_by_code()` 从 `contract_product_type_list` 里反查
- 创建物资时还会同步创建一个 `product` 合同，合同 code 直接复用 `material_type`

当前校验方式：

- schema 层：
	- `material_type` 只是示例值来自 `contract_product_type_list`
	- `approval_party` 只有示例，没有正式白名单
- service 层：
	- 强校验的是 `contract_quantity > 0`、项目存在、tenant 存在
	- `material_type` 本身没有显式抛错白名单
	- 如果 code 不在 `contract_product_type_list`，`get_material_info_by_code()` 会退回默认值 `支架/组`
	- 随后又会去调用合同创建，而合同创建对 `code` 有一层更严格的白名单校验

结论：

- material 创建不是 config 驱动，而是代码常量驱动
- 而且 `material_type` 的直接校验位置并不清晰，存在“本服务默认值兜底，合同服务再拦截”的现状
- 这正是还需要整理 config/真源的证据，不适合直接认为已经收口完成

### 11.3 Construction 创建

接口入参：

- `ConstructionCreate.section_type_code`、`section_sub_type_code`、`type_id` 都是普通字符串

创建时字段来源：

- `section_type_code`、`section_sub_type_code`、`type_id` 直接来自接口传参
- `section_type_name`、`section_sub_type_name`、`task_code`、`task_type` 则由 service 根据配置和类型树反查生成

当前校验方式：

- schema 层：只有字符串长度和数值范围，没有枚举白名单
- service 层：
	- `section_type_code` 必须存在于 `construction_section_config`
	- `section_sub_type_code` 必须能映射到统一类型节点
	- `type_id` 必须能映射到施工类型树节点
	- `SCDW` 分支还会单独校验专用任务编码列表

结论：

- 施工创建是这 3 个抽样域里最接近“配置/类型树驱动”的
- 但即便如此，接口 schema 本身仍未把候选值正式声明出来，真正校验仍落在 service 里

### 11.4 总判断

如果只问“初始化层面能不能看出 enum_config 类字段怎么来的”，答案是：

- 能看出一部分。
- `preparation`、`production`、`power` 明显还来自 dict。
- `contract` 初始化候选项明显来自 `contract_config`。
- `material` 初始化候选项来自 `contract_config`，单位补充来自 `material_contract_*` dict。
- `construction` 初始化候选项来自 config + 类型树。

但如果进一步问“那是不是已经主要用 config 写了，所以没必要再整理 config”，答案是否定的。

原因是当前系统仍然存在 3 个没有收口的问题：

- 初始化来源和接口创建来源不是同一套真源。
- schema 层没有把这些字段正式建模成受控候选值。
- service 层仍保留大量代码字典、硬编码白名单和默认兜底。

所以现在做 config 整理仍然有必要，而且必要性不在“把所有东西都搬到 config”，而在：

- 明确每个字段的唯一真源
- 消除 dict/config/代码常量三头并存
- 让初始化和接口创建共用同一套候选值与校验逻辑

## 12. 前端下拉选项当前是通过什么路径返回的

除了创建接口本身，前端通常会先调一些 list 接口拿可选项。

这部分如果和初始化来源不一致，就会出现：

- 初始化能创建出来的值，不一定和前端可选项完全一致
- 前端下拉看到的值，也不一定就是创建 service 实际校验使用的那套真源

下面按合同、物资、施工 3 类抽样。

### 12.1 Contract 的前端下拉路径

当前相关接口主要有：

- `/contract/type/list`
- `/contract/sub-type/list`

返回路径：

- `router -> ContractService.get_contract_type_list()`
- `router -> ContractService.get_sub_type_list(contract_type)`

数据来源：

- `get_contract_type_list()` 直接读代码里的 `contarct_type_dict`
- `get_sub_type_list()` 先根据传入 `contract_type` 选一组硬编码 `filter_key_list`
- 再去代码里的 `contract_code_dict` 组装返回值

和初始化是否一致：

- 不一致。
- 初始化合同候选项来自 `contract_config`。
- 但前端拿到的合同类型/子类型下拉，来自代码字典和硬编码列表。

这意味着：

- 前端创建合同时看到的 code/type 候选，不是直接从初始化同源配置拿出来的。
- 如果未来 `contract_config` 改了，但代码字典没改，就会分叉。

### 12.2 Material 的前端下拉路径

当前至少有两条相关路径：

- `/material/material-types`
- `/material-detail/contract-product-type/list`

第一条路径：`/material/material-types`

- `router` 先根据 `project_id` 查项目类型
- 再调用 `config_service.get_config_json(project.project_type, "material_types")`
- 返回 `material_types` 里的 `label/value`

它的数据来源是：

- config：`material_types`

第二条路径：`/material-detail/contract-product-type/list`

- `router -> MaterialDetailService.get_material_part_options()`
- service 内部同样会先读项目的 `material_types` config
- 再按当前 material 的 `material_type` 找到对应 kit 和 parts

它的数据来源也是：

- config：`material_types`

但同时，物资模块里还保留了老的代码常量路径：

- `MaterialService.get_material_info_by_code()` 直接读 `contract_product_type_list`
- `MaterialDetailService.get_kit_options()` 也直接读 `contract_product_type_list`

和初始化是否一致：

- 也不一致。
- 前端下拉主要已经走 `material_types` config。
- 但初始化物资时，候选项来源还是 `contract_config`，单位来源还是 `material_contract_*` dict。
- 而创建 service 内部补 `show_name/unit` 时，又还在读代码常量 `contract_product_type_list`。

这是当前分叉最明显的一类：

- 前端选项：config
- 初始化来源：config + dict
- 创建补充逻辑：代码常量

### 12.3 Construction 的前端下拉路径

当前相关接口主要有：

- `/construction/section/type/list`
- `/construction/section/sub/type/list`
- `/construction/task/type/list`

返回路径：

- `router -> ConstructionService.get_section_type_list(project_type)`
- `router -> ConstructionService.get_section_sub_type_list(section_type_code, project_type)`
- `router -> ConstructionService.get_task_type_list(section_type_code, section_sub_type_code, project_type)`

数据来源：

- `section_type_list` 通过 `config_helper.section_type_id_dict()` 读取 `construction_section_config`
- `section_sub_type_list` 通过 `config_helper.section_type_project_root_node_mapping()` 读取 `construction_section_config.project_root_nodes`
- `task_type_list` 则继续基于 `construction_section_config` 和 `ConstructionTypeTree` 生成

和初始化是否一致：

- 基本一致。
- 施工初始化本来就是 `construction_section_config + 类型树` 驱动。
- 前端下拉接口也是同一条配置/类型树路径。

所以施工是目前 3 类里最接近“初始化与前端下拉同源”的。

### 12.4 总结

从前端下拉接口角度看，当前 3 类字段的真实状态可以概括成：

1. Contract：前端下拉走代码字典，和初始化 `contract_config` 不一致。
2. Material：前端下拉主要走 `material_types` config，但初始化和创建内部仍分别依赖 `contract_config`、`material_contract_*` dict、`contract_product_type_list` 代码常量，分叉最严重。
3. Construction：前端下拉和初始化都主要走 `construction_section_config + 类型树`，相对一致。

所以如果后续要做收口，优先级也比较明确：

- 合同：先统一 `contract_config` 与前端 `type/sub-type list`。
- 物资：先统一 `material_types`、`contract_config`、`material_contract_*`、`contract_product_type_list` 四套来源。
- 施工：优先补 schema/创建校验与配置同源，配置来源本身相对已经收敛。
