# 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/创建校验与配置同源,配置来源本身相对已经收敛。