24 KiB
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 件事:
- 读取当前
project_type整个 category 下的全部 config。 - 读取 6 组 dict 数据。
- 初始化
preparation里程碑节点。 - 初始化
production投产节点。 - 初始化
contract合同,并在合同初始化内部继续初始化contract_settlement和material。 - 初始化
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: 0dict: 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: 0dict: 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 个 keydict: 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 个上游 keycontract_configdict: 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: 0dict: 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
并依赖:
BuildMethodConstructionTypeTreeConstructionTypeMapping
实际创建内容:
- 按
construction_section_config.sections[*].project_root_nodes创建项目根单位工程 - 基于这些根节点再创建主施工任务
- 对质量验收类施工继续创建施工树
- 对主施工继续自动补次级施工
涉及配置数量:
config: 至少 1 个 keyconstruction_section_configdict: 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_configpower更像一个小型初始化模板,也可以直接放到 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 看,当前明确有初始化种子的主要是:
photovoltaiconshore_windoffshore_wind
而 project_type 枚举里其实还有:
pumped_storagehydropowerthermal_powergas_turbine
也就是说,project_type 枚举范围和初始化种子覆盖范围并不一致。
8.4 海风配置目前看起来不完整
当前能直接看到:
offshore_wind有contract_config- 也有
preparation_*、production_*、power_*、material_contract_*dict
但当前 config.csv 里没看到海风的:
construction_section_configconstruction_node_configmaterial_typessupply_chain_group
这意味着海风初始化链路当前很可能并不完整,至少配置层不齐。
9. 结论
可以把 init_project_data) 当前的配置来源,拆成 4 类理解:
- 纯 dict 初始化:
preparation、production、power - 纯 config 初始化:
contract - config + dict 混合初始化:
material - config + 类型树 + 模板文件初始化:
construction、archive
所以“把这里 dict 负责的东西迁到 config 会不会太复杂”的答案不是一个统一结论:
preparation、production、power:不复杂,可以迁。material_contract:中等复杂,先统一真源再迁。construction:复杂点不在 dict,本来就不是 dict 驱动。
10. 建议落地顺序
- 先把
init_project_data)里“取了但没直接用”的construction_contract_*、service_contract_*从函数里清出来,确认是否还能删掉。 - 再把
preparation_{project_type}、production_{project_type}、power_{project_type}迁到 config。 - 再处理
material_contract_*,但前提是先统一contract_config和 material 初始化所需的单位/展示信息来源。 - 施工初始化不要和这批 dict 迁移混做,单独处理。
10.1 按本轮改造目标,初始化链路具体要改什么
如果把这次目标收敛成“初始化统一走配置工具”,那么 init_project_data) 不应该再只是分析对象,而是明确要往下面的形态改。
A. 保留并整理的 config
初始化链路应继续保留并整理这几类 config:
contract_config:合同初始化候选项construction_section_config:施工区域 / 单位工程 / 任务入口配置material_types:物资类型、展示名、单位、parts 等字段配置- 后续新增的字段级配置 key,例如:
preparation.codeproduction.codepower.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初始化不再依赖 dictmaterial初始化不再依赖 dictremark补单位- 初始化候选项与前端下拉、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_dictcontract_type必须在contarct_type_dictservice合同再限制为JL、KY、E、SCYWproduct合同再限制为一组硬编码产品 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_listapproval_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_configsection_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_dictget_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_typesconfig - 再按当前 material 的
material_type找到对应 kit 和 parts
它的数据来源也是:
- config:
material_types
但同时,物资模块里还保留了老的代码常量路径:
MaterialService.get_material_info_by_code()直接读contract_product_type_listMaterialDetailService.get_kit_options()也直接读contract_product_type_list
和初始化是否一致:
- 也不一致。
- 前端下拉主要已经走
material_typesconfig。 - 但初始化物资时,候选项来源还是
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_configsection_sub_type_list通过config_helper.section_type_project_root_node_mapping()读取construction_section_config.project_root_nodestask_type_list则继续基于construction_section_config和ConstructionTypeTree生成
和初始化是否一致:
- 基本一致。
- 施工初始化本来就是
construction_section_config + 类型树驱动。 - 前端下拉接口也是同一条配置/类型树路径。
所以施工是目前 3 类里最接近“初始化与前端下拉同源”的。
12.4 总结
从前端下拉接口角度看,当前 3 类字段的真实状态可以概括成:
- Contract:前端下拉走代码字典,和初始化
contract_config不一致。 - Material:前端下拉主要走
material_typesconfig,但初始化和创建内部仍分别依赖contract_config、material_contract_*dict、contract_product_type_list代码常量,分叉最严重。 - Construction:前端下拉和初始化都主要走
construction_section_config + 类型树,相对一致。
所以如果后续要做收口,优先级也比较明确:
- 合同:先统一
contract_config与前端type/sub-type list。 - 物资:先统一
material_types、contract_config、material_contract_*、contract_product_type_list四套来源。 - 施工:优先补 schema/创建校验与配置同源,配置来源本身相对已经收敛。