Files
2026-03-18 11:34:02 +08:00

24 KiB
Raw Permalink Blame History

init_project_data 现状分析

1. 范围

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

重点回答 4 个问题:

  • 这个函数当前创建了哪些业务数据。
  • 它直接读取了哪些 configdict
  • 哪些 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_settlementmaterial
  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

创建来源:

  • 直接使用 dictpreparation_{project_type}

创建方式:

  • 读取 dict 项的 codename
  • 先删项目下旧的 Preparation
  • 再批量插入新的 Preparation

涉及配置数量:

  • config: 0
  • dict: 1 组

当前种子数据条目数:

  • preparation_photovoltaic: 12 条
  • preparation_onshore_wind: 13 条
  • preparation_offshore_wind: 14 条

结论:

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

5.2 Production

创建来源:

  • 直接使用 dictproduction_{project_type}

创建方式:

  • 读取 dict 项的 codename
  • 先删项目下旧的 Production
  • 再批量插入新的 Production

涉及配置数量:

  • config: 0
  • dict: 1 组

当前种子数据条目数:

  • production_photovoltaic: 9 条
  • production_onshore_wind: 9 条
  • production_offshore_wind: 9 条

结论:

  • preparation 一样,是平铺型 code/name 列表。
  • 迁到 config 也不复杂。

5.3 Contract

创建来源:

  • 直接使用 configcontract_config

创建方式:

  • init_project_data)configs["contract_config"] 取出列表
  • ContractService.init_from_project() 根据每项的 codenamecontract_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

创建来源:

  • 直接使用 dictpower_{project_type}

创建方式:

  • 读取 dict 项的 codename
  • 统一绑定到 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]
  • 初始化逻辑只做“删旧 + 批量重建”
  • 不依赖复杂树结构,也不依赖额外业务联动

如果迁移:

  • preparationproduction 很适合直接改成 field_config
  • power 更像一个小型初始化模板,也可以直接放到 config,但不一定要走 field_config 这个名字

7.2 中等复杂的部分

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

原因不是数据量大,而是来源分叉:

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

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

更稳妥的做法是:

  • 先把 contract_configmaterial_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_windcontract_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 初始化:preparationproductionpower
  2. 纯 config 初始化:contract
  3. config + dict 混合初始化:material
  4. config + 类型树 + 模板文件初始化:constructionarchive

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

  • preparationproductionpower:不复杂,可以迁。
  • 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 / 校验时也不容易发现结构错误

迁移方向应是:

  • unitlabelparts 等字段并回正式 config 结构
  • 不再让单位这类业务字段挂在 dict remark

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

迁移后,init_project_data) 本身应只做 2 件事:

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

它不应该继续承担:

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

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

初始化侧改造完成后,至少应满足:

  • preparationproductionpower 初始化不再依赖 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_typecode 都只是普通字符串

创建时字段来源:

  • contract_typecode 直接来自接口传参
  • 合同名称不是前端传,而是 service 根据 contract_code_dict 动态拼出来

当前校验方式:

  • schema 层:只有长度约束,没有枚举白名单
  • service 层:
    • code 必须在 contract_code_dict
    • contract_type 必须在 contarct_type_dict
    • service 合同再限制为 JLKYESCYW
    • product 合同再限制为一组硬编码产品 code

结论:

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

11.2 Material 创建

接口入参:

  • MaterialCreate.material_typeapproval_party 也都是普通字符串

创建时字段来源:

  • material_type 直接来自接口传参
  • show_nameunit 不是前端传,而是 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_listget_material_info_by_code() 会退回默认值 支架/组
    • 随后又会去调用合同创建,而合同创建对 code 有一层更严格的白名单校验

结论:

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

11.3 Construction 创建

接口入参:

  • ConstructionCreate.section_type_codesection_sub_type_codetype_id 都是普通字符串

创建时字段来源:

  • section_type_codesection_sub_type_codetype_id 直接来自接口传参
  • section_type_namesection_sub_type_nametask_codetask_type 则由 service 根据配置和类型树反查生成

当前校验方式:

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

结论:

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

11.4 总判断

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

  • 能看出一部分。
  • preparationproductionpower 明显还来自 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

它的数据来源是:

  • configmaterial_types

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

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

它的数据来源也是:

  • configmaterial_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_configConstructionTypeTree 生成

和初始化是否一致:

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

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

12.4 总结

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

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

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

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