修改费用计算代码

This commit is contained in:
chentianrui
2025-08-22 18:13:09 +08:00
parent 8d595b339c
commit be848c3e78
20 changed files with 2569 additions and 360 deletions
View File
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"Codegeex.RepoIndex": true "Codegeex.RepoIndex": true,
"python.languageServer": "None"
} }
-4
View File
@@ -1,4 +0,0 @@
[neo4j]
uri = bolt://10.1.6.34:7687
user = neo4j
password = password
+3 -1
View File
@@ -33,7 +33,9 @@ def convert_json_to_readable(input_file, output_file=None):
if __name__ == "__main__": if __name__ == "__main__":
# 指定输入文件路径 # 指定输入文件路径
input_file = r"project2json/outputs/json/变电技改国网.json" input_file = (
r"E:/文件/LLM_model/RAG/code/Engineering_data_KG-1/equipment_dataset/数据工程/技改/预算/通信线路检修国网.json"
)
# 调用转换函数 # 调用转换函数
convert_json_to_readable(input_file) convert_json_to_readable(input_file)
+2 -2
View File
@@ -170,10 +170,10 @@ def save_comparison_to_txt(comparison, output_txt_path):
def main(): def main():
# ================== 配置路径 ================== # ================== 配置路径 ==================
# 存放所有 calculation_results.json 的文件夹 # 存放所有 calculation_results.json 的文件夹
calc_results_folder = "project2json/outputs/bclresults/变电技改国网" calc_results_folder = "project2json/outputs/bclresults/变电检修国网"
# 主 project_data.json 路径(参考数据源) # 主 project_data.json 路径(参考数据源)
project_data_json_path = "project2json/outputs/json/变电技改国网.json" project_data_json_path = "project2json/outputs/json/变电检修国网.json"
# 输出对比结果的文件夹 # 输出对比结果的文件夹
output_folder = "project2json/outputs/comparison_results" output_folder = "project2json/outputs/comparison_results"
+88 -3
View File
@@ -1,16 +1,19 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from memory_profiler import profile
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Dict, Callable, Any, Optional from typing import Dict, Callable, Any, Optional
import os import os
from enum import Enum, auto from enum import Enum, auto
from equipment_calculation.expressioncalculator import ExpressionCalculator from equipment_calculation.expressioncalculator import ExpressionCalculator
import copy import copy
import re
from xml.dom import minidom
# 配置logging # 配置logging
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG, level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.FileHandler("bcl_calculator.log"), logging.StreamHandler()], handlers=[logging.FileHandler("bcl_calculator.log"), logging.StreamHandler()],
) )
@@ -744,6 +747,86 @@ def strfind_func(funcname: str, claculate, context: BCLContext, *args):
return BCLVariant(position) return BCLVariant(position)
def _is_in_range(value, range_expression):
"""判断值是否在范围表达式中
Args:
value (str): 要检查的值
range_expression (str): 范围表达式,支持以下格式:
- 单个值: "JYT19-71"
- 范围: "JYT17-1~189"
- 范围(后半省略前缀且带前导零): "C09032001~09032315"
- 多个范围用逗号分隔: "JYT17-1~189,JYT18-1~100"
Returns:
bool: 如果值在范围中返回True,否则返回False
"""
# 统一转为字符串
if value is None or range_expression is None:
return False
value = str(value).strip()
range_expression = str(range_expression).strip()
# 拆分为(前缀, 尾部数字)——前缀为末尾数字前的全部,数字为末尾一段数字
def split_prefix_num(s: str) -> tuple[str, Optional[int]]:
s = str(s)
m = re.match(r"^(.*?)(\d+)$", s)
if not m:
return s, None
prefix, num_str = m.group(1), m.group(2)
try:
return prefix, int(num_str)
except ValueError:
return prefix, None
value_prefix, value_num = split_prefix_num(value)
# 处理多个范围表达式(用逗号分隔)
for seg in range_expression.split(","):
seg = seg.strip()
if not seg:
continue
# 范围:start~end
if "~" in seg:
try:
start, end = [x.strip() for x in seg.split("~", 1)]
s_pref, s_num = split_prefix_num(start)
e_pref, e_num = split_prefix_num(end)
# 允许 end 省略前缀(如 JYT17-1~189 或 C09032001~09032315),
# 即只要 end 全为数字,则继承 start 的前缀,并按数值比较(忽略前导零差异)
if e_pref == "" and end.isdigit():
e_pref = s_pref
try:
e_num = int(end)
except ValueError:
e_num = None
# 要求前缀完全一致
if s_pref != e_pref:
continue
# 值必须与范围前缀完全一致
if value_prefix != s_pref:
continue
# 三者数字齐备方可比较
if value_num is None or s_num is None or e_num is None:
continue
if s_num <= value_num <= e_num:
return True
except Exception:
continue
else:
# 单值:要求完全相等
if value == seg:
return True
return False
def in_func(funcname: str, claculate, context: BCLContext, *args): def in_func(funcname: str, claculate, context: BCLContext, *args):
# 参数数量校验 # 参数数量校验
if len(args) != 2: if len(args) != 2:
@@ -780,8 +863,8 @@ def in_func(funcname: str, claculate, context: BCLContext, *args):
else param2.value else param2.value
) )
# 判断value1是否在value2中 # 使用自定义的范围判断函数
result = value1 in value2 result = _is_in_range(value1, value2)
# 返回布尔类型的BCLVariant对象 # 返回布尔类型的BCLVariant对象
return BCLVariant(result) return BCLVariant(result)
@@ -1173,6 +1256,8 @@ class BCLCalculator:
return False return False
name = expr.get("name") name = expr.get("name")
if name in self.expressions:
logging.warning(f"BCLExpression名称重复: {name},后来的定义将覆盖之前的。")
self.expressions[name] = expr self.expressions[name] = expr
return True return True
+171 -15
View File
@@ -1,4 +1,5 @@
import json import json
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Tuple from typing import Dict, List, Any, Optional, Tuple
from equipment_calculation.bcl_calculator import ( from equipment_calculation.bcl_calculator import (
BCLCalculator, BCLCalculator,
@@ -17,6 +18,48 @@ import logging
# 全局变量 # 全局变量
calculator = BCLCalculator() calculator = BCLCalculator()
# 项目类型到需加载XML文件名的映射(按软件分类预留)。
# 仅在 calculation_type == "工程量" 时生效;人材机仍按目录全部加载。
# 注意:文件名需与目录中文件名完全匹配。
PROJECT_TYPE_XML_MAP: Dict[str, Dict[str, List[str]]] = {
# 主网软件分类
"主网": {
# 示例:变电需要读取以下文件(用户后续可补充/修改)
# 其他项目类型占位
# "输电": [],
# "线路": [],
},
# 配网软件分类
"配网": {
# 预留:用户后续填充
# "配网项目类型示例": ["xxx.xml"],
},
# 技改软件分类
"技改": {
"变电": [
"变量计算配置.xml",
"变量计算配置(变电).xml",
"定额基本信息费用计算.xml",
"工程量统计配置.xml",
"材机分析配置.xml",
],
"线路": [
"变量计算配置.xml",
"变量计算配置(线路).xml",
"定额基本信息费用计算.xml",
"工程量统计配置.xml",
"材机分析配置.xml",
],
"配网": [
"变量计算配置.xml",
"变量计算配置(配网).xml",
"定额基本信息费用计算.xml",
"工程量统计配置.xml",
"材机分析配置.xml",
],
},
}
def load_json_data(json_file_path: str, json_path: str = None) -> Dict[str, Any]: def load_json_data(json_file_path: str, json_path: str = None) -> Dict[str, Any]:
""" """
@@ -52,6 +95,9 @@ def load_json_data(json_file_path: str, json_path: str = None) -> Dict[str, Any]
return {} return {}
import logging
def create_node_from_type(node: dict[str, any]): def create_node_from_type(node: dict[str, any]):
""" """
根据项目划分子节点类型动态创建对应类型的对象 根据项目划分子节点类型动态创建对应类型的对象
@@ -60,23 +106,44 @@ def create_node_from_type(node: dict[str, any]):
node: 项目划分子节点数据 node: 项目划分子节点数据
Returns: Returns:
对应类型的对象:Ration、Material或Equipment 对应类型的对象:Ration、Material或Equipment;不支持的类型返回None
""" """
# 获取节点类型 # 获取节点类型
node_type = node.get("类型", "") node_type = node.get("类型", "")
type_code = node.get("type", "") type_code = node.get("type", "")
# 获取配件类型的附加判断字段(仅当是配件时使用)
peijian_type = node.get("配件类型", "")
# 判断是否为“定额”类型node_type 或 type_code 是 "定额" 或 "0" # 判断是否为“定额”类型
if node_type in ["定额", "0"] or type_code in ["定额", "0"]: if node_type in ["定额", "0"] or type_code in ["定额", "0"]:
return create_ration_from_node(node) return create_ration_from_node(node)
# 判断是否为“主材”类型
elif node_type in ["主材", "1"] or type_code in ["主材", "1"]: elif node_type in ["主材", "1"] or type_code in ["主材", "1"]:
return create_material_from_node(node) return create_material_from_node(node)
elif node_type in ["设备", "配件"] or type_code in ["设备", "配件"]:
# 判断是否为“设备”类型
elif node_type in ["设备", "5"] or type_code in ["设备", "5"]:
return create_equipment_from_node(node) return create_equipment_from_node(node)
else:
logging.warning(f"未知节点类型: 类型={node_type}, type={type_code},默认创建Ration对象") # 判断是否为“一笔性费用”类型
elif node_type in ["一笔性费用", ""] or type_code in ["一笔性费用", ""]:
return create_ration_from_node(node) return create_ration_from_node(node)
# 特殊处理:“配件”需要根据“配件类型”进一步判断
elif node_type == "配件" or type_code == "配件":
if peijian_type == "主材":
return create_material_from_node(node)
elif peijian_type == "配件":
return create_equipment_from_node(node)
else:
logging.warning(f"配件类型未识别: 配件类型={peijian_type}, 节点类型={node_type}, type={type_code},跳过")
return None
else:
logging.warning(f"未知节点类型: 类型={node_type}, type={type_code},跳过")
return None
def create_list_from_node(node: dict[str, any]) -> bill_node: def create_list_from_node(node: dict[str, any]) -> bill_node:
""" """
@@ -164,6 +231,7 @@ def create_ration_from_node(node: dict[str, any]) -> Ration:
ration.所属定额库 = node.get("所属定额库") ration.所属定额库 = node.get("所属定额库")
ration.专业属性 = node.get("专业属性") ration.专业属性 = node.get("专业属性")
ration.地形费计算方式 = node.get("地形费计算方式") ration.地形费计算方式 = node.get("地形费计算方式")
ration.专业类型 = node.get("调差类型")
# 遍历节点的所有属性,确保所有可能的字段都被设置 # 遍历节点的所有属性,确保所有可能的字段都被设置
for key, value in node.items(): for key, value in node.items():
@@ -172,9 +240,32 @@ def create_ration_from_node(node: dict[str, any]) -> Ration:
setattr(ration, key, str(value) if value != "" else "") setattr(ration, key, str(value) if value != "" else "")
ration.乙供材料费不含税 = node.get("材料费") ration.乙供材料费不含税 = node.get("材料费")
ration.children = node.get("children")
return ration return ration
def _derive_moe_type(node: dict[str, any]) -> str:
"""
基于节点字段推导人/材/机对象在BCL中的 type,不修改原始节点:
- 若 类型/type 为数值代码,映射到中文
- 若 类型/type 为“配件”,根据 配件类型:主材->主材;配件->设备
- 其他返回原值
"""
t = node.get("类型") or node.get("type")
pj = node.get("配件类型")
if t in ("1", "主材"):
return "主材"
if t in ("5", "设备"):
return "设备"
if t == "配件":
if pj == "主材":
return "主材"
if pj == "配件":
return "设备"
return t or ""
def create_material_from_node(node: dict[str, any]) -> Material: def create_material_from_node(node: dict[str, any]) -> Material:
""" """
创建主材对象 创建主材对象
@@ -190,7 +281,8 @@ def create_material_from_node(node: dict[str, any]) -> Material:
# 设置主材相关属性 # 设置主材相关属性
material.id = node.get("id") material.id = node.get("id")
material.name = node.get("项目名称") material.name = node.get("项目名称")
material.type = node.get("类型", "主材") # 对象层的 type 使用派生类型,不覆盖原始字典
material.type = _derive_moe_type(node) or "主材"
material.供货方 = node.get("供货方") material.供货方 = node.get("供货方")
material.关联父级量 = node.get("关联父级量") material.关联父级量 = node.get("关联父级量")
material.制造长度 = node.get("制造长度") material.制造长度 = node.get("制造长度")
@@ -236,6 +328,7 @@ def create_material_from_node(node: dict[str, any]) -> Material:
setattr(material, key, str(value) if value != "" else "") setattr(material, key, str(value) if value != "" else "")
material.预算价不含税 = material.单价不含税 material.预算价不含税 = material.单价不含税
material.children = node.get("children")
return material return material
@@ -255,7 +348,8 @@ def create_equipment_from_node(node: dict[str, any]) -> Equipment:
# 设置设备相关属性 # 设置设备相关属性
equipment.id = node.get("id") equipment.id = node.get("id")
equipment.name = node.get("项目名称") equipment.name = node.get("项目名称")
equipment.type = node.get("类型", "设备") # 对象层的 type 使用派生类型,不覆盖原始字典
equipment.type = _derive_moe_type(node) or "设备"
equipment.供货方 = node.get("供货方") equipment.供货方 = node.get("供货方")
equipment.关联父级量 = node.get("关联父级量") equipment.关联父级量 = node.get("关联父级量")
equipment.制造长度 = node.get("制造长度") equipment.制造长度 = node.get("制造长度")
@@ -296,11 +390,12 @@ def create_equipment_from_node(node: dict[str, any]) -> Equipment:
if hasattr(equipment, key) and not key.startswith("_"): if hasattr(equipment, key) and not key.startswith("_"):
if value is not None: if value is not None:
setattr(equipment, key, str(value) if value != "" else "") setattr(equipment, key, str(value) if value != "" else "")
equipment.children = node.get("children")
return equipment return equipment
def init_bcl_calculator(software_category, engineering_type, calculation_type): def init_bcl_calculator(software_category, engineering_type, calculation_type, project_type: Optional[str] = None):
""" """
初始化BCL计算器 初始化BCL计算器
@@ -308,6 +403,7 @@ def init_bcl_calculator(software_category, engineering_type, calculation_type):
software_category: 软件类别(主网/配网/技改) software_category: 软件类别(主网/配网/技改)
engineering_type: 工程类型(预算/清单) engineering_type: 工程类型(预算/清单)
calculation_type: 计算类型(工程量/人材机) calculation_type: 计算类型(工程量/人材机)
project_type: 项目类型(如 变电/线路 等);仅用于工程量时筛选要加载的XML文件
Returns: Returns:
bool: 是否成功初始化 bool: 是否成功初始化
@@ -352,7 +448,46 @@ def init_bcl_calculator(software_category, engineering_type, calculation_type):
config_path = default_path config_path = default_path
# 加载脚本 # 加载脚本
result = calculator.load_scripts_dir(config_path) # 对于工程量,若提供了project_type且存在映射,则仅加载映射中指定的XML
result = True
use_filtered_files = False
try:
import os
if calculation_type == "工程量" and project_type:
target_list = PROJECT_TYPE_XML_MAP.get(software_category, {}).get(project_type) or []
if target_list:
use_filtered_files = True
print(
f"根据项目类型筛选加载XML: 软件={software_category}, 项目类型={project_type}, 目标文件={target_list}"
)
all_ok = True
missing_files = []
for filename in target_list:
xml_path = os.path.join(config_path, filename)
if not os.path.exists(xml_path):
missing_files.append(filename)
all_ok = False
continue
if not calculator.load_script(xml_path):
print(f"加载脚本失败: {xml_path}, 错误: {calculator.get_last_error()}")
all_ok = False
else:
print(f"成功加载脚本: {xml_path}")
if missing_files:
print(f"警告: 以下映射指定的文件在目录中未找到: {missing_files}")
result = all_ok
else:
print(f"未找到映射项,按目录全部加载: 软件={software_category}, 项目类型={project_type}")
except Exception as _e:
# 映射/筛选流程异常时,退回目录加载
print(f"按项目类型筛选加载发生异常,退回目录加载: {_e}")
use_filtered_files = False
if not use_filtered_files:
result = calculator.load_scripts_dir(config_path)
if False == result: if False == result:
print(f"加载脚本错误: {calculator.get_last_error()}") print(f"加载脚本错误: {calculator.get_last_error()}")
# 尝试使用默认配置 # 尝试使用默认配置
@@ -404,12 +539,12 @@ def create_material_or_equipment_from_node(node: dict[str, any]) -> MaterialOrEq
""" """
me = MaterialOrEquipment() me = MaterialOrEquipment()
# 设置基本属性 # 设置基本属性(type 使用派生值,不改写原始字典)
me.id = node.get("id") me.id = node.get("id")
me.编码 = node.get("编码") me.编码 = node.get("编码")
me.名称 = node.get("名称") me.名称 = node.get("名称")
me.单位 = node.get("单位") me.单位 = node.get("单位")
me.type = node.get("类型") me.type = _derive_moe_type(node)
me.供货方 = node.get("供货方", "") me.供货方 = node.get("供货方", "")
me.预算价不含税 = node.get("预算价不含税") me.预算价不含税 = node.get("预算价不含税")
me.市场价不含税 = node.get("市场价不含税") me.市场价不含税 = node.get("市场价不含税")
@@ -426,12 +561,13 @@ def create_material_or_equipment_from_node(node: dict[str, any]) -> MaterialOrEq
me.商品砼 = node.get("商品砼", "") me.商品砼 = node.get("商品砼", "")
me.数量 = node.get("数量") me.数量 = node.get("数量")
me.是否未计价 = node.get("是否未计价") me.是否未计价 = node.get("是否未计价")
# 遍历节点的所有属性,确保所有可能的字段都被设置 # 遍历节点的所有属性,确保所有可能的字段都被设置
for key, value in node.items(): for key, value in node.items():
if hasattr(me, key) and not key.startswith("_"): if hasattr(me, key) and not key.startswith("_"):
setattr(me, key, str(value)) setattr(me, key, str(value))
me.children = node.get("children")
return me return me
@@ -550,7 +686,11 @@ class PrefixConfig:
self.field_mappings = field_mappings or {} self.field_mappings = field_mappings or {}
def create_project_contexts(json_file_path: str, prefix_configs: List[PrefixConfig] = None) -> BCLContext: def create_project_contexts(
json_file_path: str,
prefix_configs: List[PrefixConfig] = None,
json_data: Optional[Dict[str, Any]] = None,
) -> BCLContext:
""" """
从JSON文件创建多个前缀上下文,并将它们链接在一起 从JSON文件创建多个前缀上下文,并将它们链接在一起
@@ -577,10 +717,26 @@ def create_project_contexts(json_file_path: str, prefix_configs: List[PrefixConf
# 创建根上下文 # 创建根上下文
root_context = None root_context = None
# 简单路径解析器:从已有 json_data 中按路径提取
def _get_from_data(data_src: Dict[str, Any], path: Optional[str]):
if not path:
return data_src or {}
parts = path.split(".")
cur = data_src or {}
for p in parts:
if isinstance(cur, dict) and p in cur:
cur = cur.get(p, {})
else:
return {}
return cur
# 为每个前缀创建上下文并链接 # 为每个前缀创建上下文并链接
for config in prefix_configs: for config in prefix_configs:
# 加载数据 # 加载数据:优先使用传入的 json_data,否则从文件按路径读取
data = load_json_data(json_file_path, config.json_path) if json_data is not None:
data = _get_from_data(json_data, config.json_path)
else:
data = load_json_data(json_file_path, config.json_path)
# 创建上下文 # 创建上下文
context = ZjProjectBCLContext(prefix=config.prefix, prevContext=root_context) context = ZjProjectBCLContext(prefix=config.prefix, prevContext=root_context)
+22 -4
View File
@@ -1,4 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Set, Tuple from typing import Dict, List, Any, Optional, Set, Tuple
@@ -117,6 +118,7 @@ class CalculationStrategy(ABC):
cost_table: Dict[str, Any], cost_table: Dict[str, Any],
json_file_path: Optional[str] = None, json_file_path: Optional[str] = None,
engineering_type: Optional[str] = None, engineering_type: Optional[str] = None,
json_data: Optional[Dict[str, Any]] = None,
) -> Dict[str, float]: ) -> Dict[str, float]:
""" """
计算所有费用 计算所有费用
@@ -138,6 +140,7 @@ class CalculationStrategy(ABC):
rcj_nodes: List[Tuple[Dict[str, Any], str]], rcj_nodes: List[Tuple[Dict[str, Any], str]],
project_children: List[Dict[str, Any]], project_children: List[Dict[str, Any]],
json_file_path: Optional[str] = None, json_file_path: Optional[str] = None,
json_data: Optional[Dict[str, Any]] = None,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
""" """
计算人材机节点的数量,考虑父级消耗量 计算人材机节点的数量,考虑父级消耗量
@@ -322,12 +325,20 @@ class DefaultCalculationStrategy(CalculationStrategy):
cost_table: Dict[str, Any], cost_table: Dict[str, Any],
json_file_path: Optional[str] = None, json_file_path: Optional[str] = None,
engineering_type: Optional[str] = None, engineering_type: Optional[str] = None,
json_data: Optional[Dict[str, Any]] = None,
) -> Dict[str, float]: ) -> Dict[str, float]:
"""计算所有费用""" """计算所有费用"""
from equipment_calculation.quantity_fee_calculator import calculate_all_fees as original_calculate_all_fees from equipment_calculation.quantity_fee_calculator import calculate_all_fees as original_calculate_all_fees
# 传递自身作为计算策略 # 传递自身作为计算策略
return original_calculate_all_fees(project_node, cost_table, json_file_path, engineering_type, self) return original_calculate_all_fees(
project_node,
cost_table,
json_file_path,
engineering_type,
self,
json_data=json_data,
)
def calculate_rcj_count( def calculate_rcj_count(
self, self,
@@ -338,7 +349,7 @@ class DefaultCalculationStrategy(CalculationStrategy):
"""计算人材机节点的数量""" """计算人材机节点的数量"""
from equipment_calculation.resource_fee_calculator import calc_rcj_count as original_calc_rcj_count from equipment_calculation.resource_fee_calculator import calc_rcj_count as original_calc_rcj_count
return original_calc_rcj_count(rcj_nodes, project_children, json_file_path) return original_calc_rcj_count(rcj_nodes, project_children, json_file_path, json_data=json_data)
def cat_rcj_count(self, rcj_nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def cat_rcj_count(self, rcj_nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""汇总人材机节点的数量""" """汇总人材机节点的数量"""
@@ -359,7 +370,14 @@ class DefaultCalculationStrategy(CalculationStrategy):
return original_format_rcj_output(rcj_nodes, node_type) return original_format_rcj_output(rcj_nodes, node_type)
# 修复 calculate_rcj_fees 未定义的错误 # 修复 calculate_rcj_fees 未定义的错误
def calculate_rcj_fees(self, json_file_path, project_name, project_guid=None): def calculate_rcj_fees(
self,
json_file_path,
project_name,
project_guid=None,
json_data: Optional[Dict[str, Any]] = None,
engineering_type: Optional[str] = None,
):
""" """
计算项目划分节点下所有人材机节点的合价 计算项目划分节点下所有人材机节点的合价
@@ -375,7 +393,7 @@ class DefaultCalculationStrategy(CalculationStrategy):
from equipment_calculation.resource_fee_calculator import calculate_rcj_fees as original_calculate_rcj_fees from equipment_calculation.resource_fee_calculator import calculate_rcj_fees as original_calculate_rcj_fees
# 调用原始函数 # 调用原始函数
return original_calculate_rcj_fees(json_file_path, project_name, project_guid) return original_calculate_rcj_fees(json_file_path, project_name, project_guid, json_data=json_data)
def post_process_quantity_fees(self, output_file: str, project_name: str) -> None: def post_process_quantity_fees(self, output_file: str, project_name: str) -> None:
"""对工程量取费表进行后处理""" """对工程量取费表进行后处理"""
+87 -13
View File
@@ -1,5 +1,6 @@
import os import os
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Tuple from typing import Dict, List, Any, Optional, Tuple
from equipment_calculation.software_types import SoftwareType from equipment_calculation.software_types import SoftwareType
from equipment_calculation.find_project_nodes import get_project_divisions_list from equipment_calculation.find_project_nodes import get_project_divisions_list
@@ -7,6 +8,8 @@ from equipment_calculation.quantity_fee_calculator import calculate_quantity_fee
from equipment_calculation.resource_fee_calculator import calculate_resource_fees as base_calculate_resource_fees from equipment_calculation.resource_fee_calculator import calculate_resource_fees as base_calculate_resource_fees
from equipment_calculation.calculation_strategy import CalculationStrategy, DefaultCalculationStrategy from equipment_calculation.calculation_strategy import CalculationStrategy, DefaultCalculationStrategy
import json import json
import psutil
from datetime import datetime
class CalculatorBase(ABC): class CalculatorBase(ABC):
@@ -57,10 +60,45 @@ class CalculatorBase(ABC):
if hasattr(self.calculation_strategy, "set_output_dir"): if hasattr(self.calculation_strategy, "set_output_dir"):
self.calculation_strategy.set_output_dir(output_dir) self.calculation_strategy.set_output_dir(output_dir)
def _append_log(self, text: str, filename: str = "performance_memory_log.txt") -> str:
"""将文本即时追加写入输出目录下的日志文件,并返回日志路径。"""
try:
out_dir = self.get_output_dir()
os.makedirs(out_dir, exist_ok=True)
log_path = os.path.join(out_dir, filename)
with open(log_path, "a", encoding="utf-8") as f:
f.write(text + "\n")
f.flush()
return log_path
except Exception as e:
# 即使写日志失败,也不要影响主流程
print(f"写入性能日志失败: {e}")
return ""
def _print_memory_usage(self, prefix: str = "") -> None:
"""记录当前进程的内存占用情况(MB)到输出目录日志文件。"""
try:
process = psutil.Process(os.getpid())
mem_info = process.memory_info()
rss = mem_info.rss / (1024**2) # 物理内存(MB
vms = mem_info.vms / (1024**2) # 虚拟内存(MB
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
header = f"[{ts}]" + (f" [{prefix}]" if prefix else "")
lines = [
f"{header} 当前进程占用的物理内存: {rss:.2f} MB",
f"{header} 当前进程占用的虚拟内存: {vms:.2f} MB",
]
for line in lines:
self._append_log(line)
except Exception as e:
# 日志失败不影响主流程
print(f"记录内存信息失败: {e}")
def calculate_quantity_fee_tables( def calculate_quantity_fee_tables(
self, self,
json_file_path: str, json_file_path: str,
project_name: str = None, project_name: str = None,
project_type: Optional[str] = None,
) -> None: ) -> None:
""" """
计算工程量取费表 计算工程量取费表
@@ -76,12 +114,30 @@ class CalculatorBase(ABC):
if callable(preprocess_func): # 检查返回值是否可调用 if callable(preprocess_func): # 检查返回值是否可调用
preprocess_func(json_file_path) # 调用预处理函数 preprocess_func(json_file_path) # 调用预处理函数
# 外层只读取一次 JSON
with open(json_file_path, "r", encoding="utf-8") as _f:
_json_data = json.load(_f)
# 外层只初始化一次 BCL 计算器
from equipment_calculation.bcl_utils import init_bcl_calculator
init_bcl_calculator(
software_category=self.software_type.category.value,
engineering_type=self.software_type.engineering_type.value,
calculation_type="工程量",
project_type=project_type,
)
if project_name: if project_name:
# 处理单个项目划分 # 处理单个项目划分
print(f"处理单个项目划分: {project_name}") print(f"处理单个项目划分: {project_name}")
output_file = self._calculate_quantity_fees(json_file_path, project_name, engineering_type) output_file = self._calculate_quantity_fees(
json_file_path, project_name, engineering_type, project_guid=None, json_data=_json_data
)
if output_file: if output_file:
print(f"已完成 {project_name} 的工程量取费表计算,结果保存在 {output_file}") print(f"已完成 {project_name} 的工程量取费表计算,结果保存在 {output_file}")
# 性能打印:每个项目划分完成后打印内存占用
self._print_memory_usage(prefix=f"工程量-{project_name}")
else: else:
# 处理所有项目划分 # 处理所有项目划分
project_divisions = get_project_divisions_list(json_file_path) project_divisions = get_project_divisions_list(json_file_path)
@@ -90,9 +146,13 @@ class CalculatorBase(ABC):
for i, (proj_name, proj_guid) in enumerate(project_divisions, 1): for i, (proj_name, proj_guid) in enumerate(project_divisions, 1):
# 使用命令行参数中指定的调差类型,而不是从JSON文件中获取的调差类型 # 使用命令行参数中指定的调差类型,而不是从JSON文件中获取的调差类型
print(f"处理 {i}/{len(project_divisions)}: {proj_name} (GUID: {proj_guid})") print(f"处理 {i}/{len(project_divisions)}: {proj_name} (GUID: {proj_guid})")
output_file = self._calculate_quantity_fees(json_file_path, proj_name, engineering_type, proj_guid) output_file = self._calculate_quantity_fees(
json_file_path, proj_name, engineering_type, proj_guid, json_data=_json_data
)
if output_file: if output_file:
print(f"已完成 {proj_name} (GUID: {proj_guid}) 的工程量取费表计算") print(f"已完成 {proj_name} (GUID: {proj_guid}) 的工程量取费表计算")
# 性能打印:每个项目划分完成后打印内存占用
self._print_memory_usage(prefix=f"工程量-{proj_name}")
print(f"所有项目划分节点的工程量取费表计算完成,结果保存在 {self.get_output_dir()} 目录") print(f"所有项目划分节点的工程量取费表计算完成,结果保存在 {self.get_output_dir()} 目录")
@@ -110,14 +170,30 @@ class CalculatorBase(ABC):
# 在计算前应用软件特定的规则 # 在计算前应用软件特定的规则
self.apply_resource_fee_rules() self.apply_resource_fee_rules()
# 外层只读取一次 JSON
with open(json_file_path, "r", encoding="utf-8") as _f:
_json_data = json.load(_f)
# 外层只初始化一次 BCL 计算器(人材机)
from equipment_calculation.bcl_utils import init_bcl_calculator
init_bcl_calculator(
software_category=self.software_type.category.value,
engineering_type=self.software_type.engineering_type.value,
calculation_type="人材机",
)
if project_name: if project_name:
# 处理单个项目划分 # 处理单个项目划分
output_file = self._calculate_resource_fees( output_file = self._calculate_resource_fees(
json_file_path, json_file_path,
project_name, project_name,
json_data=_json_data,
) )
if output_file: if output_file:
print(f"已完成 {project_name} 的人材机合价计算,结果保存在 {output_file}") print(f"已完成 {project_name} 的人材机合价计算,结果保存在 {output_file}")
# 性能打印:每个项目划分完成后打印内存占用
self._print_memory_usage(prefix=f"人材机-{project_name}")
else: else:
# 处理所有项目划分 # 处理所有项目划分
project_divisions = get_project_divisions_list(json_file_path) project_divisions = get_project_divisions_list(json_file_path)
@@ -125,9 +201,11 @@ class CalculatorBase(ABC):
for i, (proj_name, proj_guid) in enumerate(project_divisions, 1): for i, (proj_name, proj_guid) in enumerate(project_divisions, 1):
print(f"处理 {i}/{len(project_divisions)}: {proj_name} (GUID: {proj_guid})") print(f"处理 {i}/{len(project_divisions)}: {proj_name} (GUID: {proj_guid})")
output_file = self._calculate_resource_fees(json_file_path, proj_name, proj_guid) output_file = self._calculate_resource_fees(json_file_path, proj_name, proj_guid, json_data=_json_data)
if output_file: if output_file:
print(f"已完成 {proj_name} (GUID: {proj_guid}) 的人材机合价计算") print(f"已完成 {proj_name} (GUID: {proj_guid}) 的人材机合价计算")
# 性能打印:每个项目划分完成后打印内存占用
self._print_memory_usage(prefix=f"人材机-{proj_name}")
print(f"所有项目划分节点的人材机合价计算完成,结果保存在 {self.get_output_dir()} 目录") print(f"所有项目划分节点的人材机合价计算完成,结果保存在 {self.get_output_dir()} 目录")
@@ -137,6 +215,7 @@ class CalculatorBase(ABC):
project_name: str, project_name: str,
engineering_type: str, engineering_type: str,
project_guid: str = None, project_guid: str = None,
json_data: dict | None = None,
) -> str: ) -> str:
""" """
计算工程量取费表,并应用软件特定的后处理规则 计算工程量取费表,并应用软件特定的后处理规则
@@ -153,17 +232,8 @@ class CalculatorBase(ABC):
# 确保输出目录存在 # 确保输出目录存在
os.makedirs(self.get_output_dir(), exist_ok=True) os.makedirs(self.get_output_dir(), exist_ok=True)
# 初始化BCL计算器,传递软件类型和计算类型
from equipment_calculation.bcl_utils import init_bcl_calculator
init_bcl_calculator(
software_category=self.software_type.category.value,
engineering_type=self.software_type.engineering_type.value,
calculation_type="工程量",
)
# 打印计算策略信息 # 打印计算策略信息
print(f"使用计算策略: {self.calculation_strategy.__class__.__name__}") # print(f"使用计算策略: {self.calculation_strategy.__class__.__name__}")
# 调用基础计算函数,传入计算策略和项目GUID # 调用基础计算函数,传入计算策略和项目GUID
output_file = base_calculate_quantity_fees( output_file = base_calculate_quantity_fees(
@@ -172,6 +242,8 @@ class CalculatorBase(ABC):
engineering_type, engineering_type,
project_guid=project_guid, project_guid=project_guid,
calculation_strategy=self.calculation_strategy, calculation_strategy=self.calculation_strategy,
software_type=self.software_type.category.value,
json_data=json_data,
) )
# 应用软件特定的后处理规则 # 应用软件特定的后处理规则
@@ -185,6 +257,7 @@ class CalculatorBase(ABC):
json_file_path: str, json_file_path: str,
project_name: str, project_name: str,
project_guid: str = None, project_guid: str = None,
json_data: dict | None = None,
) -> str: ) -> str:
""" """
计算人材机合价,并应用软件特定的后处理规则 计算人材机合价,并应用软件特定的后处理规则
@@ -216,6 +289,7 @@ class CalculatorBase(ABC):
project_name, project_name,
project_guid=project_guid, project_guid=project_guid,
calculation_strategy=self.calculation_strategy, calculation_strategy=self.calculation_strategy,
json_data=json_data,
) )
# 应用软件特定的后处理规则 # 应用软件特定的后处理规则
@@ -125,7 +125,7 @@ class ExpressionCalculator:
# 处理一元运算符 # 处理一元运算符
if token_type == "OPERATOR" and value in ("+", "-"): if token_type == "OPERATOR" and value in ("+", "-"):
self.current_token += 1 self.current_token += 1
node = self.parse_factor() node = self._parse_factor()
return ("UNARYOP", value, node) return ("UNARYOP", value, node)
# 处理变量 # 处理变量
+414 -88
View File
@@ -74,6 +74,22 @@ def map_quantity_node_types(node):
# 复制节点以避免修改原始数据 # 复制节点以避免修改原始数据
node_copy = deepcopy(node) node_copy = deepcopy(node)
# 先根据“配件判定规则”标准化类型:
# 若 节点的 type/类型 == "配件" 且 配型 == "配件",则检查 配件类型:
# - 配件类型 == "主材" -> 作为 主材 节点
# - 配件类型 == "配件" -> 作为 设备 节点
raw_type = node_copy.get("类型") if node_copy.get("类型") is not None else node_copy.get("type")
raw_peixing = node_copy.get("配型")
# 放宽条件:当 类型/type 为“配件”,且(配型为“配件”或缺省),并且存在“配件类型”时,按配件类型映射
if str(raw_type) == "配件" and (raw_peixing in (None, "", "配件")):
pj_type = node_copy.get("配件类型")
if pj_type == "主材":
node_copy["类型"] = "主材"
node_copy["type"] = "主材"
elif pj_type == "配件":
node_copy["类型"] = "设备"
node_copy["type"] = "设备"
# 映射类型字段 # 映射类型字段
if "类型" in node_copy: if "类型" in node_copy:
type_mapping = {"0": "定额", "1": "主材", "5": "设备"} type_mapping = {"0": "定额", "1": "主材", "5": "设备"}
@@ -86,9 +102,9 @@ def map_quantity_node_types(node):
node_copy["费用类型"] = "取费" node_copy["费用类型"] = "取费"
# 如果已经是"取费"则保持不变 # 如果已经是"取费"则保持不变
# 递归处理子节点 # 不在此处递归 children,避免对子节点进行类型标准化,交由各层独立处理
if "children" in node_copy and node_copy["children"]: # if isinstance(node_copy.get("children"), list):
node_copy["children"] = [map_quantity_node_types(child) for child in node_copy["children"]] # node_copy["children"] = [map_quantity_node_types(child) for child in node_copy["children"]]
return node_copy return node_copy
@@ -101,15 +117,15 @@ def map_resource_node_types(node):
# 复制节点以避免修改原始数据 # 复制节点以避免修改原始数据
node_copy = deepcopy(node) node_copy = deepcopy(node)
# 映射类型字段 # 统一标准化类型字段:同时规范化 '类型' 与 'type',输出为中文(人工/材料/机械)
if "类型" in node_copy: type_mapping = {"2": "人工", "3": "材料", "4": "机械"}
type_mapping = { # 取原始值(可能存在任一字段)
"2": "人工", raw_t = node_copy.get("类型") if node_copy.get("类型") is not None else node_copy.get("type")
"3": "材料", if raw_t is not None:
"4": "机械", # 若为代码,转换为中文;若已为中文则保持
} normalized = type_mapping.get(str(raw_t), raw_t)
if node_copy["类型"] in type_mapping: node_copy["类型"] = normalized
node_copy["类型"] = type_mapping[node_copy["类型"]] node_copy["type"] = normalized
# 递归处理子节点 # 递归处理子节点
if "children" in node_copy and node_copy["children"]: if "children" in node_copy and node_copy["children"]:
@@ -132,50 +148,72 @@ def extract_resource_nodes(node, parent_id=None):
if not isinstance(node, dict): if not isinstance(node, dict):
return [] # 返回空列表而不是None,便于后续处理 return [] # 返回空列表而不是None,便于后续处理
# 类型工具
def _is_resource_type(t):
return t in ("人工", "材料", "机械", "2", "3", "4")
str2code = {"人工": "2", "材料": "3", "机械": "4"}
code2str = {v: k for k, v in str2code.items()}
# 获取当前节点ID # 获取当前节点ID
current_id = node.get("id") current_id = node.get("id") or node.get("GUID") or node.get("guid")
resource_nodes = [] resource_nodes = []
# 检查是否是人材机节点(支持字符串和数字类型) # 检查是否是人材机节点(支持字符串和数字类型、双字段
node_type = node.get("类型") node_type = node.get("类型") or node.get("type")
is_resource_node = False is_resource_node = _is_resource_type(node_type)
# 同时支持数字类型和字符串类型 # 合并处理材机列表和children字段 # 处理逻辑:
if node_type in ["人工", "材料", "机械", "2", "3", "4"]: # - 若为资源节点且存在children:不返回父节点;其children继承父类型后继续递归提取
is_resource_node = True # - 若为资源节点且无children:返回该节点
# - 若非资源节点:常规递归处理children/材机列表
# 复制节点但不包含children和材机列表 # 优先处理“材机列表
node_copy = {k: v for k, v in node.items() if k not in ["children", "材机列表"]}
# 添加父级ID
if parent_id:
node_copy["parent_id"] = parent_id
# 添加到资源列表
resource_nodes.append(node_copy)
# 合并处理材机列表和children字段 - 只处理一种来源的子节点,优先处理材机列表
child_nodes = [] child_nodes = []
if "材机列表" in node and node["材机列表"]: if "材机列表" in node and node["材机列表"]:
child_nodes = node["材机列表"] child_nodes = node["材机列表"]
elif "children" in node and node["children"] and not is_resource_node: elif "children" in node and node["children"]:
# 只有在不是资源节点时才处理children
child_nodes = node["children"] child_nodes = node["children"]
# 处理子节点 if is_resource_node:
for child in child_nodes: if child_nodes:
child_copy = deepcopy(child) # 父为资源且有子:上提子项,子项继承父类型(同时设置中英文字段),并递归
child_copy["parent_id"] = current_id parent_code = str2code.get(node_type, node_type)
parent_str = code2str.get(node_type, node_type)
# 递归处理 for child in child_nodes:
sub_resources = extract_resource_nodes(child_copy, current_id) child_copy = deepcopy(child)
if sub_resources: if isinstance(child_copy, dict):
resource_nodes.extend(sub_resources) child_copy["parent_id"] = current_id
child_copy["type"] = parent_code
child_copy["类型"] = parent_str
sub_resources = extract_resource_nodes(child_copy, current_id)
if sub_resources:
resource_nodes.extend(sub_resources)
else:
# 基本类型直接忽略
continue
else:
# 父为资源且无子:直接计入
node_copy = deepcopy(node)
node_copy["parent_id"] = parent_id
# 统一双字段
node_copy["type"] = str2code.get(node_type, node_type)
node_copy["类型"] = code2str.get(node_type, node_type)
resource_nodes.append(node_copy)
else:
# 非资源节点:递归其子节点
for child in child_nodes:
child_copy = deepcopy(child)
if isinstance(child_copy, dict):
child_copy["parent_id"] = current_id
sub_resources = extract_resource_nodes(child_copy, current_id)
if sub_resources:
resource_nodes.extend(sub_resources)
return resource_nodes return resource_nodes
def process_project_children(children): def process_project_children(children, software_type=None):
"""处理项目子节点,分离工程量节点和人材机节点,支持嵌套的工程量节点""" """处理项目子节点,分离工程量节点和人材机节点,支持嵌套的工程量节点"""
if not children: if not children:
return None, None return None, None
@@ -183,15 +221,135 @@ def process_project_children(children):
quantity_nodes = [] quantity_nodes = []
resource_nodes = [] resource_nodes = []
# 内部工具:在人材机存在拆分时展开其子节点,删除中间父级人材机节点
def _flatten_resource_splits_in_place(node: dict):
"""
递归+迭代地展开任意深度的人材机拆分:
- 如果某个子节点是人材机(人工/材料/机械/2/3/4)且其自身有children
* 将其children上提为当前层的children
* 被上提的每个子节点的 类型/type 设为与父节点相同;
* 移除该父级人材机节点本身;
- 对非被移除的子节点递归处理;
通过 while 循环保证多层嵌套被完全展开。
"""
if not isinstance(node, dict):
return
if "children" not in node or not node["children"]:
return
def _is_resource_type(t):
return t in ("人工", "材料", "机械", "2", "3", "4")
# 中英文类型映射
str2code = {"人工": "2", "材料": "3", "机械": "4"}
code2str = {v: k for k, v in str2code.items()}
changed = True
while changed:
changed = False
new_children = []
for ch in node["children"]:
if isinstance(ch, dict):
t = ch.get("类型") or ch.get("type")
# 命中“人材机且存在children”,则将其子节点上提并继承类型
if _is_resource_type(t) and ch.get("children"):
parent_code = str2code.get(t, t) # 若 t 已是代码则保持
parent_str = code2str.get(t, t) # 若 t 已是中文则保持
for gc in ch.get("children", []) or []:
if isinstance(gc, dict):
gc["type"] = parent_code
gc["类型"] = parent_str
new_children.append(gc)
else:
new_children.append(gc)
# 丢弃父级人材机节点
changed = True
continue
else:
# 对未被移除的子节点继续递归展开
_flatten_resource_splits_in_place(ch)
new_children.append(ch)
else:
new_children.append(ch)
node["children"] = new_children
for child in children: for child in children:
# 对于工程量节点,深拷贝用于工程量树 # 先复制两份:一份用于工程量树(quantity_node),一份用于资源提取(resource_node_src)
quantity_node = deepcopy(child) quantity_node = deepcopy(child)
resource_node_src = deepcopy(child)
# 额外保留一份原始工程量节点用于最终输出,保持原样不作修改
original_quantity_node = deepcopy(child)
# 在两份结构中都先执行“人材机拆分展开”,以便:
# 1) 工程量树不保留中间的人材机父节点
# 2) 资源提取时直接提取到展开后的子资源,且不包含中间父节点
_flatten_resource_splits_in_place(quantity_node)
_flatten_resource_splits_in_place(resource_node_src)
# 应用工程量节点类型映射 # 应用工程量节点类型映射
quantity_node = map_quantity_node_types(quantity_node) quantity_node = map_quantity_node_types(quantity_node)
# 提取人材机节点 - extract_resource_nodes会递归提取所有层级的人材机节点 # 在“技改/主网”下,对定额节点做嵌套检测(children 或 材机列表 中包含 定额/主材/设备 或 配件类型为 主材/配件)
resources = extract_resource_nodes(child) if software_type in ("技改", "主网"):
def _type_is_quota_material_equipment(t):
# 支持代码与中文类型名
return t in ("定额", "主材", "设备", "0", "1", "5")
def _collect_nested_matches(node):
matches = []
# 检查 children
for ch in node.get("children", []) or []:
if isinstance(ch, dict):
t = ch.get("类型") or ch.get("type")
pj_t = ch.get("配件类型")
if _type_is_quota_material_equipment(t) or pj_t in ("主材", "配件"):
matches.append(
{
"来源": "children",
"名称": ch.get("项目名称") or ch.get("清单名称") or ch.get("name"),
"类型": t,
"配件类型": pj_t,
"id": ch.get("GUID") or ch.get("guid") or ch.get("id"),
}
)
# 递归继续检查更深层的嵌套
sub = _collect_nested_matches(ch)
if sub:
matches.extend(sub)
# 检查 材机列表(如存在)
for it in node.get("材机列表", []) or []:
if isinstance(it, dict):
t = it.get("类型") or it.get("type")
pj_t = it.get("配件类型")
if _type_is_quota_material_equipment(t) or pj_t in ("主材", "配件"):
matches.append(
{
"来源": "材机列表",
"名称": it.get("项目名称")
or it.get("清单名称")
or it.get("name")
or it.get("材料名称"),
"类型": t,
"配件类型": pj_t,
"id": it.get("GUID") or it.get("guid") or it.get("id"),
}
)
return matches
node_type = quantity_node.get("类型") or quantity_node.get("type")
if node_type in ("定额", "0"):
nested_matches = _collect_nested_matches(quantity_node)
if nested_matches:
quantity_node["嵌套检测"] = {
"存在嵌套": True,
"命中数量": len(nested_matches),
"命中项": nested_matches,
}
# 提取人材机节点 - 基于已展开的 resource_node_src 提取
resources = extract_resource_nodes(resource_node_src)
if resources: if resources:
# 应用人材机节点类型映射 # 应用人材机节点类型映射
resources = [map_resource_node_types(resource) for resource in resources] resources = [map_resource_node_types(resource) for resource in resources]
@@ -204,24 +362,146 @@ def process_project_children(children):
# 清理子节点中的人材机节点 # 清理子节点中的人材机节点
clean_resource_nodes_from_quantity(quantity_node) clean_resource_nodes_from_quantity(quantity_node)
# 递归处理剩余的工程量节点 # 🔥 核心修改:仅当当前节点是"定额"时,才递归处理其children并提取嵌套的工程量节点
if "children" in quantity_node and quantity_node["children"]: current_node_type = quantity_node.get("类型") or quantity_node.get("type")
sub_quantity_nodes, sub_resource_nodes = process_project_children(quantity_node["children"]) is_quota_node = current_node_type in ("定额", "0")
# 更新quantity_node的children sub_quantity_nodes = None
sub_resource_nodes = None
if is_quota_node and "children" in quantity_node and quantity_node["children"]:
# 处理定额的人材机节点
# 关键:递归输入改为原始副本的 children,避免上一层标准化对子层“原始类型字段”的影响
sub_quantity_nodes, sub_resource_nodes = process_project_children(
(original_quantity_node.get("children") or []), software_type
)
# 更新当前节点的 children(保留结构)
if sub_quantity_nodes: if sub_quantity_nodes:
quantity_node["children"] = sub_quantity_nodes quantity_node["children"] = sub_quantity_nodes
# 关键:将子层的工程量节点(如主材/设备)直接并入当前层输出,
# 并确保类型为标准化后的中文值(主材/设备),避免后续 BCL 过滤不到
# 不修改原始子节点类型,仅做深拷贝并并入
normalized_children = [deepcopy(n) for n in sub_quantity_nodes]
quantity_nodes.extend(normalized_children)
# 调试:打印并入的子工程量节点类型
try:
for ch in normalized_children:
nm = (
ch.get("项目名称")
or ch.get("清单名称")
or ch.get("name")
or ch.get("材料名称")
or "<未命名>"
)
tp = ch.get("类型") or ch.get("type")
# print(f"[DEBUG] 并入子工程量节点: 名称={nm} | 类型={tp}")
except Exception:
pass
else: else:
quantity_node.pop("children", None) # 使用pop安全删除 quantity_node.pop("children", None)
# 合并子节点中提取的资源节点 # 合并资源节点
if sub_resource_nodes: if sub_resource_nodes:
resource_nodes.extend(sub_resource_nodes) resource_nodes.extend(sub_resource_nodes)
# else:
# 非定额节点:不递归提取子节点作为工程量,不处理 sub_quantity_nodes
# 添加到工程量节点列表 # 对于定额节点:将“材机列表”里符合条件的项(主材/设备或配件类型为主材/配件)也作为工程量节点并入
quantity_nodes.append(quantity_node) if is_quota_node:
mj_items = (quantity_node.get("材机列表") or []) if isinstance(quantity_node, dict) else []
for it in mj_items:
if not isinstance(it, dict):
continue
t = it.get("类型") or it.get("type")
pj_t = it.get("配件类型")
# 类型代码转中文
if t in ("1", "5"):
t = {"1": "主材", "5": "设备"}[t]
new_type = None
if t in ("主材", "设备"):
new_type = t
elif pj_t == "主材":
new_type = "主材"
elif pj_t == "配件":
new_type = "设备"
if new_type:
# 不改写原始字段,直接将条目拷贝加入,类型由下游对象构造派生
new_node = deepcopy(it)
quantity_nodes.append(new_node)
try:
nm = (
new_node.get("项目名称")
or new_node.get("清单名称")
or new_node.get("name")
or new_node.get("材料名称")
or "<未命名>"
)
# print(f"[DEBUG] 从材机列表并入工程量节点: 名称={nm} | 类型={t} | 派生类型={new_type}")
except Exception:
pass
return (quantity_nodes if quantity_nodes else None, resource_nodes if resource_nodes else None) # 🔥 最后:将当前节点本身加入 quantity_nodes
# 为满足“工程量节点输出保持原样”,此处使用 original_quantity_node 代替处理后的 quantity_node
# 规则:
# - 若当前节点为“主材/设备(1/5)”,直接计入工程量节点;
# - 若为“定额(0)”,沿用既有两个条件之一成立时计入;
def _children_have_no_children(n: dict) -> bool:
ch = (n.get("children") or []) if isinstance(n, dict) else []
# 若无 children,则视作“没有更深层 children”
if not ch:
return True
for c in ch:
if isinstance(c, dict) and (c.get("children") or []):
return False
return True
def _mj_has_hit(n: dict) -> bool:
items = (n.get("材机列表") or []) if isinstance(n, dict) else []
for it in items:
if not isinstance(it, dict):
continue
t = it.get("类型") or it.get("type")
pj_t = it.get("配件类型")
if t in ("定额", "主材", "设备", "0", "1", "5") or pj_t in ("主材", "配件"):
return True
return False
# 主材/设备直接计入(以标准化类型入列,避免后续 BCL 过滤不到)
if current_node_type in ("主材", "设备", "1", "5"):
to_append = deepcopy(original_quantity_node)
quantity_nodes.append(to_append)
try:
nm = (
to_append.get("项目名称")
or to_append.get("清单名称")
or to_append.get("name")
or to_append.get("材料名称")
or "<未命名>"
)
tp = to_append.get("类型") or to_append.get("type")
# print(f"[DEBUG] 入列工程量节点(主材/设备): 名称={nm} | 类型={tp}")
except Exception:
pass
# 定额按条件计入
elif is_quota_node:
if _children_have_no_children(quantity_node) or _mj_has_hit(quantity_node):
quantity_nodes.append(original_quantity_node)
try:
nm = (
original_quantity_node.get("项目名称")
or original_quantity_node.get("清单名称")
or original_quantity_node.get("name")
or original_quantity_node.get("材料名称")
or "<未命名>"
)
tp = original_quantity_node.get("类型") or original_quantity_node.get("type")
# print(f"[DEBUG] 入列工程量节点(定额): 名称={nm} | 类型={tp}")
except Exception:
pass
# 始终返回列表,避免上层出现 None 导致判定困难
return (quantity_nodes or [], resource_nodes or [])
def clean_resource_nodes_from_quantity(node): def clean_resource_nodes_from_quantity(node):
@@ -249,12 +529,16 @@ def clean_resource_nodes_from_quantity(node):
del node["children"] del node["children"]
def load_project_data(json_file_path, project_name, project_guid=None): def load_project_data(json_file_path, project_name, project_guid=None, json_data=None):
"""加载JSON数据并获取目标项目节点""" """加载JSON数据并获取目标项目节点"""
try: try:
# 读取JSON文件 # 支持透传已加载的 JSON 数据,避免重复读取
with open(json_file_path, "r", encoding="utf-8") as f: if json_data is not None:
data = json.load(f) data = json_data
else:
# 读取JSON文件
with open(json_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 获取projectData中的costSetting和projectDivision # 获取projectData中的costSetting和projectDivision
project_data = data.get("projectData", {}) project_data = data.get("projectData", {})
@@ -280,12 +564,12 @@ def load_project_data(json_file_path, project_name, project_guid=None):
return None, None, None, None, None return None, None, None, None, None
def get_cost_table_children(json_file_path, project_name, project_guid=None): def get_cost_table_children(json_file_path, project_name, project_guid=None, json_data=None):
"""获取取费表子节点""" """获取取费表子节点"""
try: try:
# 加载项目数据,传递project_guid参数 # 加载项目数据,传递project_guid参数
data, project_data, cost_setting, project_division, target_node = load_project_data( data, project_data, cost_setting, project_division, target_node = load_project_data(
json_file_path, project_name, project_guid json_file_path, project_name, project_guid, json_data=json_data
) )
if not target_node: if not target_node:
@@ -307,7 +591,7 @@ def get_cost_table_children(json_file_path, project_name, project_guid=None):
return None return None
def get_quantity_nodes(json_file_path, project_name, engineering_type, project_guid=None): def get_quantity_nodes(json_file_path, project_name, engineering_type, project_guid=None, software_type=None):
""" """
获取工程量节点 获取工程量节点
@@ -394,7 +678,7 @@ def get_quantity_nodes(json_file_path, project_name, engineering_type, project_g
if bill_children: if bill_children:
# 处理清单节点的子节点,获取工程量节点 # 处理清单节点的子节点,获取工程量节点
bill_quantity_nodes, _ = process_project_children(bill_children) bill_quantity_nodes, _ = process_project_children(bill_children, software_type)
if bill_quantity_nodes: if bill_quantity_nodes:
print(f"清单节点 '{bill_name}' 下找到 {len(bill_quantity_nodes)} 个工程量节点") print(f"清单节点 '{bill_name}' 下找到 {len(bill_quantity_nodes)} 个工程量节点")
@@ -495,7 +779,7 @@ def get_quantity_nodes(json_file_path, project_name, engineering_type, project_g
return quantity_nodes return quantity_nodes
else: else:
# 预算工程 - 直接获取工程量节点 # 预算工程 - 直接获取工程量节点
quantity_nodes, _ = process_project_children(project_children) quantity_nodes, _ = process_project_children(project_children, software_type)
# 如果没有工程量节点,返回空列表而不是None # 如果没有工程量节点,返回空列表而不是None
if not quantity_nodes: if not quantity_nodes:
@@ -754,35 +1038,77 @@ def get_bill_node_by_id(bill_id: str) -> Dict[str, Any]:
return {} return {}
# # 测试代码
# if __name__ == "__main__": # if __name__ == "__main__":
# json_file_path = os.path.join("技改预算", "架线.json") # # 测试参数(使用新合并的JSON文件)
# project_name = "基础工程材料工地运输" # json_file_path = "project2json/outputs/merged/变电检修国网-副本.json"
# adjustment_type = "拆除" # project_name = "1.12新增卸车保管"
# project_guid = "{0952D46D-E5C7-4412-88FF-93517EF2633C}"
# # 获取并输出取费表子节点 # def _node_name(n: dict):
# cost_children = get_cost_table_children(json_file_path, project_name) # # 尽可能稳健地取名称字段
# print("\n取费表子节点:") # for k in ("名称", "name", "材料名称", "定额名称", "项目名称", "title"):
# print(json.dumps(cost_children, ensure_ascii=False, indent=2) if cost_children else None) # if isinstance(n, dict) and n.get(k):
# return n.get(k)
# return n.get("newGuid") or n.get("GUID") or n.get("id") or "<无名称>"
# # 获取并输出工程量节点 # def _node_type(n: dict):
# quantity_nodes = get_quantity_nodes(json_file_path, project_name, adjustment_type) # return n.get("类型") or n.get("type") or "<无类型>"
# print("\n工程量节点:")
# print(json.dumps(quantity_nodes, ensure_ascii=False, indent=2) if quantity_nodes else None)
# # 获取并输出分类后的人材机节点 # def _node_id(n: dict):
# labor_nodes, material_nodes, machine_nodes = get_classified_resource_nodes( # return n.get("GUID") or n.get("guid") or n.get("id") or n.get("parent_id") or "<无ID>"
# json_file_path, project_name, adjustment_type
# )
# print("\n人工节点列表:") # def _print_nodes(nodes, title):
# for node, parent_id in labor_nodes: # print(f"{title} 共 {len(nodes) if nodes else 0} 个:")
# print(f"节点名称: {node.get('名称')}, 父级ID: {parent_id}") # if not nodes:
# return
# for i, item in enumerate(nodes, 1):
# n = item[0] if isinstance(item, tuple) and item and isinstance(item[0], dict) else item
# try:
# print(f" {i:02d}. 名称={_node_name(n)} | 类型={_node_type(n)} | ID={_node_id(n)}")
# except Exception:
# print(f" {i:02d}. {item}")
# print("\n材料节点列表:") # def _check_resource_parents_with_children(nodes):
# for node, parent_id in material_nodes: # """统计是否存在类型为人/材/机且仍带children的父节点"""
# print(f"节点名称: {node.get('名称')}, 父级ID: {parent_id}")
# print("\n机械节点列表:") # def _is_res(t):
# for node, parent_id in machine_nodes: # return t in ("人工", "材料", "机械", "2", "3", "4")
# print(f"节点名称: {node.get('名称')}, 父级ID: {parent_id}")
# bad = []
# for item in nodes or []:
# n = item[0] if isinstance(item, tuple) and item and isinstance(item[0], dict) else item
# if isinstance(n, dict) and _is_res(n.get("类型") or n.get("type")) and n.get("children"):
# bad.append(n)
# if bad:
# print("[警告] 发现仍存在带children的人材机父节点,这些父节点应被跳过,子节点应上提:")
# _print_nodes(bad, "异常父级人材机节点")
# else:
# print("[校验] 未发现带children的人材机父节点,扁平化正常。")
# print(f"测试项目: {project_name}")
# print(f"文件路径: {json_file_path}")
# print(f"项目GUID: {project_guid}")
# print("-" * 50)
# # 获取工程量节点
# print("获取工程量节点...")
# quantity_nodes = get_quantity_nodes(json_file_path, project_name, "预算工程", project_guid, software_type="技改")
# _print_nodes(quantity_nodes or [], "工程量节点")
# # 获取人材机节点(应为:父为人材机且有children时,父不计,子上提并继承父类型)
# print("获取人材机节点...")
# resource_nodes = get_resource_nodes(json_file_path, project_name, project_guid)
# _print_nodes(resource_nodes or [], "人材机节点")
# _check_resource_parents_with_children(resource_nodes or [])
# # 获取分类后的人材机节点
# print("获取分类后的人材机节点...")
# labor_nodes, material_nodes, machinery_nodes = get_classified_resource_nodes(
# json_file_path, project_name, project_guid
# )
# _print_nodes(labor_nodes or [], "人工节点")
# _print_nodes(material_nodes or [], "材料节点")
# _print_nodes(machinery_nodes or [], "机械节点")
# print("-" * 50)
# print("测试完成!")
+56 -80
View File
@@ -1,5 +1,6 @@
import os import os
import json import json
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Tuple from typing import Dict, List, Any, Optional, Tuple
from equipment_calculation.software_types import ( from equipment_calculation.software_types import (
get_software_type, get_software_type,
@@ -29,101 +30,65 @@ CATEGORY_MAPPING = {
} }
def parse_json_content(json_file_path: str) -> Tuple[Optional[str], Optional[str]]: # 项目类型名称映射字典,将各种变体映射到标准类型(预算/清单)
PROJECT_TYPE_MAPPING = {
# 预算类变体
"概预算工程": "预算",
}
####应该是加在json后没释放
def parse_json_content(json_file_path: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
""" """
从JSON文件内容中解析软件类别和工程类型 从JSON文件内容中解析
- 软件类别: 来自 basicData["软件类别"](若无则尝试 basicData["软件名称"] 作为兜底)
- 项目类型: 来自 basicData["项目类型"](期望为 "预算""清单"
- 工程类型: 来自 projectData.projectInfo["工程类型"]
:param json_file_path: JSON文件路径 :param json_file_path: JSON文件路径
:return: (category, engineering_type) 元组,如果解析失败返回 (None, None) :return: (category, project_type, engineering_type) 元组,解析失败对应位置返回 None
""" """
try: try:
with open(json_file_path, "r", encoding="utf-8") as f: with open(json_file_path, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
# 定义阶段类型映射表 # 提取 basicData
budget_types = ["概预算", "定额", "定额计价", "概算", "概预算工程"] basic_data = data.get("basicData", {}) if isinstance(data, dict) else {}
list_types = ["清单", "结算", "招标控制价", "招投标工程", "清单计价"] # 软件类别(优先 软件类别,其次 软件名称)
category = basic_data.get("软件名称") or basic_data.get("软件名称")
# 从division字段获取软件名称和阶段类型 engineering_type = basic_data.get("项目类型")
if "division" in data: # 规范化项目类型为 预算/清单
division = data["division"] if engineering_type:
print(f"找到division字段: {division}") mapped_pt = PROJECT_TYPE_MAPPING.get(engineering_type)
if mapped_pt:
# 使用-分割division字段 engineering_type = mapped_pt
parts = division.split("-")
if len(parts) == 2:
category = parts[0].strip()
stage_type = parts[1].strip()
elif len(parts) == 3:
category = parts[0].strip()
stage_type = parts[2].strip()
else: else:
print(f"警告: division字段 '{division}' 格式不正确,无法分割") print(f"警告: basicData.项目类型 '{engineering_type}' 不是有效值,将使用默认值 '清单'")
# 可以选择返回默认值或抛出异常,这里以返回默认值为例
category = "主网"
engineering_type = "清单" engineering_type = "清单"
print(f"使用默认值: 软件名称={category}, 阶段类型={engineering_type}")
return category, engineering_type
# 使用映射字典规范化软件类别 # 规范化软件类别
if category:
if category in CATEGORY_MAPPING: if category in CATEGORY_MAPPING:
category = CATEGORY_MAPPING[category] category = CATEGORY_MAPPING[category]
else: else:
print(f"警告: division中的软件名称 '{category}' 不是有效值,将使用默认值 '主网'") print(f"警告: basicData中的软件名称/名称 '{category}' 不是有效值,将使用默认值 '主网'")
category = "主网" category = "主网"
# 映射阶段类型 # 提取工程类型:projectData.projectInfo.工程类型
if any(budget_type in stage_type for budget_type in budget_types): project_info = data.get("projectData", {}).get("projectInfo", {}) if isinstance(data, dict) else {}
engineering_type = "预算" project_type = project_info.get("工程类型")
elif any(list_type in stage_type for list_type in list_types):
engineering_type = "清单"
else:
print(f"警告: division中的阶段类型 '{stage_type}' 无法映射到预算或清单,将使用默认值 '清单'")
engineering_type = "清单"
print(f"从division解析: 软件名称={category}, 阶段类型={engineering_type} (原始值: {stage_type})") # 打印解析结果(便于调试)
return category, engineering_type print(f"解析完成: 软件类别={category}, 项目类型={engineering_type}, 工程类型={project_type}")
else: return category, engineering_type, project_type
print(f"警告: JSON文件中未找到division字段,尝试从basicData中解析")
# 作为备选,尝试从basicData中获取
if "basicData" in data:
basic_data = data["basicData"]
category = basic_data.get("软件名称")
engineering_type = basic_data.get("阶段类型")
# 验证解析结果
if category and engineering_type:
# 使用映射字典规范化软件类别
if category in CATEGORY_MAPPING:
category = CATEGORY_MAPPING[category]
else:
print(f"警告: basicData中的软件名称 '{category}' 不是有效值,将使用默认值 '主网'")
category = "主网"
# 确保engineering_type是有效值
if engineering_type not in ["预算", "清单"]:
print(f"警告: basicData中的阶段类型 '{engineering_type}' 不是有效值,将使用默认值 '清单'")
engineering_type = "清单"
print(f"从basicData解析: 软件名称={category}, 阶段类型={engineering_type}")
return category, engineering_type
else:
print(f"警告: basicData中未找到软件名称或阶段类型")
else:
print(f"警告: JSON文件中未找到basicData部分")
return None, None
except Exception as e: except Exception as e:
print(f"解析JSON文件内容时出错: {str(e)}") print(f"解析JSON文件内容时出错: {str(e)}")
return None, None return None, None, None
def process_json_file( def process_json_file(json_file_path: str, output_dir: str, calculate_type: str, project_name: str = None) -> bool:
json_file_path: str, output_dir: str, calculate_type: str = "all", project_name: str = None
) -> bool:
""" """
处理单个JSON文件 处理单个JSON文件
@@ -137,20 +102,27 @@ def process_json_file(
bool: 处理是否成功 bool: 处理是否成功
""" """
try: try:
# 从JSON文件内容中解析软件类别和工程类型 # 从JSON文件内容中解析软件类别、项目类型(预算/清单) 和 工程类型(如 变电/线路/配网)
category, engineering_type = parse_json_content(json_file_path) # parse_json_content 返回顺序为: (category, project_type, engineering_type)
category, project_type, engineering_type = parse_json_content(json_file_path)
# 如果解析失败,使用默认值 # 如果解析失败,使用默认值
if category is None: if category is None:
category = "主网" category = "主网"
print(f"无法从文件中解析软件类别,使用默认值: {category}") print(f"无法从文件中解析软件类别,使用默认值: {category}")
if engineering_type is None: # 项目类型(预算/清单) 兜底
engineering_type = "预算" if project_type is None:
print(f"无法从文件中解析工程类型,使用默认值: {engineering_type}") project_type = "预算"
print(f"无法从文件中解析项目类型(预算/清单),使用默认值: {project_type}")
# 可选:记录工程类型
if engineering_type:
print(f"工程类型: {engineering_type}")
# 获取软件类型 # 获取软件类型
software_type = get_software_type(category, engineering_type) # 这里的第二个参数应为项目类型(预算/清单)
software_type = get_software_type(category, project_type)
print(f"使用软件类型: {software_type.name}") print(f"使用软件类型: {software_type.name}")
# 获取计算器 # 获取计算器
@@ -190,6 +162,8 @@ def process_json_file(
calculator.calculate_quantity_fee_tables( calculator.calculate_quantity_fee_tables(
json_file_path=json_file_path, json_file_path=json_file_path,
project_name=project_name, project_name=project_name,
# 这里的 project_type 传递给计算器用于BCL筛选,应为 工程类型,例如"变电/线路/配网"
project_type=engineering_type,
) )
if calculate_type in ["all", "resource"]: if calculate_type in ["all", "resource"]:
@@ -217,6 +191,8 @@ def process_json_file(
custom_calculator.calculate_quantity_fee_tables( custom_calculator.calculate_quantity_fee_tables(
json_file_path=json_file_path, json_file_path=json_file_path,
project_name=project_name, project_name=project_name,
# 这里的 project_type 传递给计算器用于BCL筛选,应为 工程类型,例如"变电/线路/配网"
project_type=engineering_type,
) )
if calculate_type in ["all", "resource"]: if calculate_type in ["all", "resource"]:
@@ -236,7 +212,7 @@ def process_json_file(
return False return False
def process_directory(input_dir: str, output_dir: str, calculate_type: str = "all") -> None: def process_directory(input_dir: str, output_dir: str, calculate_type: str = "quantity") -> None:
""" """
批量处理目录中的JSON文件 批量处理目录中的JSON文件
@@ -269,7 +245,7 @@ def process_directory(input_dir: str, output_dir: str, calculate_type: str = "al
print(f"处理完成,成功: {success_count}/{len(json_files)}") print(f"处理完成,成功: {success_count}/{len(json_files)}")
def bcl_calculate(input_dir: str, output_dir: str, calculate_type: str = "all") -> None: def bcl_calculate(input_dir: str, output_dir: str, calculate_type: str = "quantity") -> None:
""" """
主函数,处理指定目录中的所有JSON文件 主函数,处理指定目录中的所有JSON文件
File diff suppressed because it is too large Load Diff
+3
View File
@@ -24,6 +24,7 @@ class ProjectQuantity:
self.cost_set = None # xsd:CostSet self.cost_set = None # xsd:CostSet
self.设备类型 = None # xsd:string self.设备类型 = None # xsd:string
self.供货方 = None # xsd:string self.供货方 = None # xsd:string
self.children = None
# (定额) # (定额)
self.综合地形类型 = None # xsd:string self.综合地形类型 = None # xsd:string
@@ -32,6 +33,8 @@ class ProjectQuantity:
self.机械费不含税 = None # xsd:string self.机械费不含税 = None # xsd:string
self.地形费计算方式 = None # xsd:string self.地形费计算方式 = None # xsd:string
self.专业属性 = None # xsd:string self.专业属性 = None # xsd:string
self.专业类型 = None # xsd:string
self.浇捣方式 = None # xsd:string
# (主材) # (主材)
self.拆分 = None # xsd:string self.拆分 = None # xsd:string
+154 -27
View File
@@ -1,8 +1,10 @@
import json import json
import os import os
import sys import sys
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Tuple, Set from typing import Dict, List, Any, Optional, Tuple, Set
from equipment_calculation.expressioncalculator import ExpressionCalculator from equipment_calculation.expressioncalculator import ExpressionCalculator
from equipment_calculation.bcl_utils import ( from equipment_calculation.bcl_utils import (
ZjQuantityBCLContext, ZjQuantityBCLContext,
ZjProjectBCLContext, ZjProjectBCLContext,
@@ -15,6 +17,7 @@ from equipment_calculation.bcl_utils import (
BCLDataSourceContext, BCLDataSourceContext,
create_list_from_node, create_list_from_node,
create_node_from_type, create_node_from_type,
create_material_or_equipment_from_node,
) )
from equipment_calculation.item_acquisition import ( from equipment_calculation.item_acquisition import (
get_cost_table_children, get_cost_table_children,
@@ -23,6 +26,7 @@ from equipment_calculation.item_acquisition import (
find_cost_table, find_cost_table,
get_bill_node_by_id, get_bill_node_by_id,
load_project_data, load_project_data,
get_classified_resource_nodes,
) )
# 缓存已计算过的费用 # 缓存已计算过的费用
@@ -223,7 +227,7 @@ def calculate_external_variable(var_name: str, context: ZjQuantityBCLContext, ca
try: try:
# 如果有计算策略,使用计算策略的方法 # 如果有计算策略,使用计算策略的方法
if calculation_strategy: if calculation_strategy:
print(f"使用计算策略 {calculation_strategy.__class__.__name__} 计算表外变量: {var_name}") # print(f"使用计算策略 {calculation_strategy.__class__.__name__} 计算表外变量: {var_name}")
# 直接调用计算策略的方法,而不是递归调用自己 # 直接调用计算策略的方法,而不是递归调用自己
return calculation_strategy.calculate_external_variable(var_name, context) return calculation_strategy.calculate_external_variable(var_name, context)
@@ -447,17 +451,21 @@ def calculate_all_fees(
json_file_path: str = None, json_file_path: str = None,
engineering_type: str = None, engineering_type: str = None,
calculation_strategy=None, calculation_strategy=None,
json_data: dict | None = None,
) -> Dict[str, float]: ) -> Dict[str, float]:
""" """
计算项目节点的所有费用,确保清单数量正确传递 计算项目节点的所有费用,确保清单数量正确传递
""" """
results = {} results = {}
# 1. 工程信息上下文 # 1. 工程信息上下文(优先使用传入的 json_data
project_context = create_project_contexts(json_file_path=json_file_path) project_context = create_project_contexts(json_file_path=json_file_path, json_data=json_data)
with open(json_file_path, "r", encoding="utf-8") as file: if json_data is not None:
data = json.load(file) data = json_data
else:
with open(json_file_path, "r", encoding="utf-8") as file:
data = json.load(file)
processed_data = process_DXdata(data) processed_data = process_DXdata(data)
@@ -497,8 +505,37 @@ def calculate_all_fees(
billItem.bill_quantity = bill_obj.quantity billItem.bill_quantity = bill_obj.quantity
print(f"设置清单数据源项数量: {billItem.bill_quantity}") print(f"设置清单数据源项数量: {billItem.bill_quantity}")
# 如果无法创建工程量对象,则跳过并返回当前已计算结果,避免未定义的 QuantityItem
if not project_obj:
print(
f"警告: 无法从项目节点创建工程量对象,跳过该节点: "
f"{project_node.get('项目名称', project_node.get('name', '未知节点'))}"
)
return results
QuantityItem = BCLDataSourceItem(project_obj, billItem) QuantityItem = BCLDataSourceItem(project_obj, billItem)
childItems = []
if project_obj.children:
for moe in project_obj.children:
childItems.append(BCLDataSourceItem(moe, QuantityItem))
QuantityItem.set_childs(childItems)
bill_context = BCLDataSourceContext([billItem], dxitem_context) bill_context = BCLDataSourceContext([billItem], dxitem_context)
# # 构建人材机数据源项(若存在)并接入上下文
# rcj_nodes_for_parent = project_node.get("_rcj_nodes", [])
# moe_items = []
# if rcj_nodes_for_parent:
# try:
# for rcj_node in rcj_nodes_for_parent:
# node_obj = create_material_or_equipment_from_node(rcj_node)
# moe_items.append(BCLDataSourceItem(node_obj, QuantityItem))
# except Exception as e:
# print(f"构建人材机数据源项出错: {e}")
# items_chain = [QuantityItem] + moe_items if moe_items else [QuantityItem]
context = BCLDataSourceContext([QuantityItem], bill_context) context = BCLDataSourceContext([QuantityItem], bill_context)
# 如果计算策略存在,设置清单数量 # 如果计算策略存在,设置清单数量
@@ -513,7 +550,35 @@ def calculate_all_fees(
dxitem_context = BCLDataSourceContext(DXITEM, project_context) dxitem_context = BCLDataSourceContext(DXITEM, project_context)
dxitem_context.variables["@特征段地形系数"] = BCLVariant(DXITEM) dxitem_context.variables["@特征段地形系数"] = BCLVariant(DXITEM)
# 如果无法创建工程量对象,则跳过并返回当前已计算结果,避免未定义的 QuantityItem
if not project_obj:
print(
f"警告: 无法从项目节点创建工程量对象,跳过该节点: "
f"{project_node.get('项目名称', project_node.get('name', '未知节点'))}"
)
return results
QuantityItem = BCLDataSourceItem(project_obj) QuantityItem = BCLDataSourceItem(project_obj)
childItems = []
if project_obj.children:
for moe in project_obj.children:
childItems.append(BCLDataSourceItem(moe, QuantityItem))
QuantityItem.set_childs(childItems)
# # 构建人材机数据源项(若存在)并接入上下文
# rcj_nodes_for_parent = project_node.get("_rcj_nodes", [])
# moe_items = []
# if rcj_nodes_for_parent:
# try:
# for rcj_node in rcj_nodes_for_parent:
# node_obj = create_material_or_equipment_from_node(rcj_node)
# moe_items.append(BCLDataSourceItem(node_obj, QuantityItem))
# except Exception as e:
# print(f"构建人材机数据源项出错: {e}")
# items_chain = [QuantityItem] + moe_items if moe_items else [QuantityItem]
context = BCLDataSourceContext([QuantityItem], dxitem_context) context = BCLDataSourceContext([QuantityItem], dxitem_context)
# 查找包含取费基数的节点 # 查找包含取费基数的节点
@@ -696,6 +761,8 @@ def calculate_quantity_fees(
engineering_type: str = None, engineering_type: str = None,
project_guid: str = None, project_guid: str = None,
calculation_strategy=None, calculation_strategy=None,
software_type: str = None,
json_data: dict | None = None,
) -> str: ) -> str:
""" """
计算工程量取费表,不使用全局变量,并按清单节点组织结果 计算工程量取费表,不使用全局变量,并按清单节点组织结果
@@ -769,7 +836,9 @@ def calculate_quantity_fees(
if engineering_type == "预算工程": if engineering_type == "预算工程":
# 预算工程 - 获取项目划分节点的取费表,传递project_guid参数 # 预算工程 - 获取项目划分节点的取费表,传递project_guid参数
cost_table_children = get_cost_table_children(json_file_path, project_name, project_guid) cost_table_children = get_cost_table_children(json_file_path, project_name, project_guid)
project_children = get_quantity_nodes(json_file_path, project_name, engineering_type, project_guid) project_children = get_quantity_nodes(
json_file_path, project_name, engineering_type, project_guid, software_type
)
if not cost_table_children or not project_children: if not cost_table_children or not project_children:
print(f"未找到项目 '{project_name}' (GUID: {project_guid}) 的取费表或项目划分节点") print(f"未找到项目 '{project_name}' (GUID: {project_guid}) 的取费表或项目划分节点")
@@ -777,7 +846,9 @@ def calculate_quantity_fees(
elif engineering_type == "清单工程": elif engineering_type == "清单工程":
# 清单工程 - 获取清单节点的取费表,传递project_guid参数 # 清单工程 - 获取清单节点的取费表,传递project_guid参数
project_children = get_quantity_nodes(json_file_path, project_name, engineering_type, project_guid) project_children = get_quantity_nodes(
json_file_path, project_name, engineering_type, project_guid, software_type
)
if not project_children: if not project_children:
print(f"未找到项目 '{project_name}' (GUID: {project_guid}) 的项目划分节点") print(f"未找到项目 '{project_name}' (GUID: {project_guid}) 的项目划分节点")
return None return None
@@ -785,6 +856,16 @@ def calculate_quantity_fees(
# 为每个工程量节点找到对应的清单节点取费表 # 为每个工程量节点找到对应的清单节点取费表
cost_tables = {} cost_tables = {}
# 准备一次性 JSON 数据(清单工程需要从 projectData 中获取 costSetting 与 bill
if json_data is not None:
_data_all = json_data
else:
with open(json_file_path, "r", encoding="utf-8") as f:
_data_all = json.load(f)
_project_data_all = _data_all.get("projectData", {})
_cost_setting_all = _project_data_all.get("costSetting", {})
_bill_data_all = _project_data_all.get("bill", {})
for project_node in project_children: for project_node in project_children:
node_name = project_node.get("项目名称", project_node.get("name", "未知节点")) node_name = project_node.get("项目名称", project_node.get("name", "未知节点"))
@@ -813,15 +894,8 @@ def calculate_quantity_fees(
if bill_id not in cost_tables: if bill_id not in cost_tables:
print(f"查找清单节点 '{bill_name}' 的取费表: {fee_table_name}") print(f"查找清单节点 '{bill_name}' 的取费表: {fee_table_name}")
# 查找取费表 # 查找取费表:使用已加载的数据
with open(json_file_path, "r", encoding="utf-8") as f: cost_table = find_cost_table(_cost_setting_all, fee_table_name)
data = json.load(f)
project_data = data.get("projectData", {})
cost_setting = project_data.get("costSetting", {})
# 查找取费表
cost_table = find_cost_table(cost_setting, fee_table_name)
if not cost_table: if not cost_table:
print(f"未找到取费表 '{fee_table_name}'") print(f"未找到取费表 '{fee_table_name}'")
@@ -835,12 +909,8 @@ def calculate_quantity_fees(
cost_tables[bill_id] = cost_table_children cost_tables[bill_id] = cost_table_children
print(f"成功获取清单节点 '{bill_name}' 的取费表") print(f"成功获取清单节点 '{bill_name}' 的取费表")
# 在读取JSON文件后,遍历项目中的清单节点,获取并保存清单数量 # 遍历项目中的清单节点,获取并保存清单数量(使用已加载的数据)
with open(json_file_path, "r", encoding="utf-8") as f: bill_data = _bill_data_all
data = json.load(f)
project_data = data.get("projectData", {})
bill_data = project_data.get("bill", {})
# 递归函数获取所有清单节点 # 递归函数获取所有清单节点
def get_bill_nodes(node): def get_bill_nodes(node):
@@ -885,6 +955,55 @@ def calculate_quantity_fees(
print(f"不支持的工程类型: {engineering_type}") print(f"不支持的工程类型: {engineering_type}")
return None return None
# 预取并分派人材机节点到各工程量节点上,供后续上下文使用
try:
labor_nodes, material_nodes, machine_nodes = get_classified_resource_nodes(
json_file_path, project_name, project_guid
)
# 合并三类节点,形成父ID到节点列表的映射
rcj_all = []
if labor_nodes:
rcj_all.extend([n for n, _ in labor_nodes])
if material_nodes:
rcj_all.extend([n for n, _ in material_nodes])
if machine_nodes:
rcj_all.extend([n for n, _ in machine_nodes])
rcj_by_parent: Dict[str, List[Dict[str, Any]]] = {}
# 注意:get_classified_resource_nodes 返回的是 (node, parent_id) 元组列表
def add_to_map(pairs):
if not pairs:
return
for node, parent_id in pairs:
if parent_id is None:
continue
key = str(parent_id)
rcj_by_parent.setdefault(key, []).append(node)
add_to_map(labor_nodes)
add_to_map(material_nodes)
add_to_map(machine_nodes)
# 将对应的人材机列表写入每个工程量节点
if project_children:
for node in project_children:
pid_candidates = [
node.get("GUID"),
node.get("guid"),
node.get("id"),
]
rcj_list = []
for pid in pid_candidates:
if pid and str(pid) in rcj_by_parent:
rcj_list = rcj_by_parent.get(str(pid), [])
break
if rcj_list:
node["_rcj_nodes"] = rcj_list
except Exception as e:
print(f"预取人材机节点失败: {e}")
# 在获取 project_children 后添加预处理代码 # 在获取 project_children 后添加预处理代码
if project_children and calculation_strategy and hasattr(calculation_strategy, "preprocess_quantity_fee_node"): if project_children and calculation_strategy and hasattr(calculation_strategy, "preprocess_quantity_fee_node"):
print(f"对项目节点进行预处理...") print(f"对项目节点进行预处理...")
@@ -946,7 +1065,11 @@ def calculate_quantity_fees(
if engineering_type == "预算工程": if engineering_type == "预算工程":
# 预算工程 - 使用项目划分节点的取费表 # 预算工程 - 使用项目划分节点的取费表
node_results = calculation_strategy.calculate_all_fees( node_results = calculation_strategy.calculate_all_fees(
project_node, {"children": cost_table_children}, json_file_path, engineering_type project_node,
{"children": cost_table_children},
json_file_path,
engineering_type,
json_data=json_data,
) )
# 直接使用节点键作为结果键 # 直接使用节点键作为结果键
@@ -958,7 +1081,11 @@ def calculate_quantity_fees(
bill_id = project_node.get("bill_guid") or project_node.get("bill_id") bill_id = project_node.get("bill_guid") or project_node.get("bill_id")
if bill_id and bill_id in cost_tables: if bill_id and bill_id in cost_tables:
node_results = calculation_strategy.calculate_all_fees( node_results = calculation_strategy.calculate_all_fees(
project_node, {"children": cost_tables[bill_id]}, json_file_path, engineering_type project_node,
{"children": cost_tables[bill_id]},
json_file_path,
engineering_type,
json_data=json_data,
) )
# 获取清单信息 # 获取清单信息
@@ -987,9 +1114,9 @@ def calculate_quantity_fees(
continue continue
# 输出计算结果 # 输出计算结果
print(f"费用计算结果:") # print(f"费用计算结果:")
for fee_name, fee_value in node_results.items(): # for fee_name, fee_value in node_results.items():
print(f" {fee_name}: {fee_value}") # print(f" {fee_name}: {fee_value}")
# 保存计算结果到JSON文件 # 保存计算结果到JSON文件
with open(output_file, "w", encoding="utf-8") as f: with open(output_file, "w", encoding="utf-8") as f:
@@ -3,6 +3,7 @@ import os
import math import math
from typing import Dict, List, Any, Tuple from typing import Dict, List, Any, Tuple
from copy import deepcopy from copy import deepcopy
from memory_profiler import profile
from equipment_calculation.item_acquisition import get_quantity_nodes, get_classified_resource_nodes, load_project_data from equipment_calculation.item_acquisition import get_quantity_nodes, get_classified_resource_nodes, load_project_data
@@ -140,7 +141,10 @@ def process_DXdata(json_data):
# 在 resource_fee_calculator.py 文件中修改 calc_rcj_count 函数中的相关代码 # 在 resource_fee_calculator.py 文件中修改 calc_rcj_count 函数中的相关代码
def calc_rcj_count( def calc_rcj_count(
rcj_nodes: List[Tuple[Dict[str, Any], str]], project_children: List[Dict[str, Any]], json_file_path: str = None rcj_nodes: List[Tuple[Dict[str, Any], str]],
project_children: List[Dict[str, Any]],
json_file_path: str = None,
json_data: dict | None = None,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
""" """
计算人材机节点的数量,考虑父级消耗量 计算人材机节点的数量,考虑父级消耗量
@@ -231,8 +235,8 @@ def calc_rcj_count(
result_nodes.append(node_copy) result_nodes.append(node_copy)
return result_nodes return result_nodes
# 创建工程信息上下文(顶层上下文) # 创建工程信息上下文(顶层上下文,优先使用 json_data
project_context = create_project_contexts(json_file_path=json_file_path) project_context = create_project_contexts(json_file_path=json_file_path, json_data=json_data)
# 遍历所有节点,计算数量 # 遍历所有节点,计算数量
for node, parent_id in rcj_nodes: for node, parent_id in rcj_nodes:
@@ -684,6 +688,7 @@ def calculate_rcj_fees(
json_file_path: str, json_file_path: str,
project_name: str, project_name: str,
project_guid: str = None, project_guid: str = None,
json_data: dict | None = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
计算项目划分节点下所有人材机节点的合价 计算项目划分节点下所有人材机节点的合价
@@ -698,7 +703,7 @@ def calculate_rcj_fees(
""" """
# 加载项目数据 # 加载项目数据
data, project_data, cost_setting, project_division, target_node = load_project_data( data, project_data, cost_setting, project_division, target_node = load_project_data(
json_file_path, project_name, project_guid json_file_path, project_name, project_guid, json_data=json_data
) )
# 如果没有找到项目划分节点 # 如果没有找到项目划分节点
@@ -717,7 +722,56 @@ def calculate_rcj_fees(
material_nodes = [] # 材料节点 (节点, 父级节点) 元组列表 material_nodes = [] # 材料节点 (节点, 父级节点) 元组列表
machine_nodes = [] # 机械节点 (节点, 父级节点) 元组列表 machine_nodes = [] # 机械节点 (节点, 父级节点) 元组列表
# 递归处理工程量节点,提取人材机节点 # 工具:类型判断与资源拆分上提
def _is_resource_type(t):
return t in ("人工", "材料", "机械", "2", "3", "4")
def _normalize_type(t):
return {"2": "人工", "3": "材料", "4": "机械"}.get(t, t)
def _flatten_resource_splits(n, inherited_type=None, qty_factor: float = 1.0):
"""
将可能带 children 的人材机节点递归拆平:
- 若是人材机且有children:不计入父,children 继承本节点类型继续拆分;
- 若是人材机且无children:作为叶子返回;
- 若非人材机:递归其children以发现资源叶子。
返回叶子资源节点列表(dict)。
"""
results = []
if not isinstance(n, dict):
return results
n_type = _normalize_type(n.get("类型") or n.get("type"))
children = n.get("children") or []
if _is_resource_type(n_type):
# 当前节点是人材机
current_type = _normalize_type(inherited_type or n_type)
# 当前资源节点自身数量,默认1
try:
self_qty = float((n.get("数量") or "1").strip())
except Exception:
self_qty = 1.0
new_factor = qty_factor * self_qty
if children:
for ch in children:
results.extend(_flatten_resource_splits(ch, current_type, new_factor))
else:
leaf = deepcopy(n)
leaf["类型"] = current_type
# 叶子数量 = 父链乘积 * 自身(若叶子也有数量则已计入 new_factor)
try:
# 如果叶子本身还有数量字段,已包含在 new_factor 中,这里直接覆盖为乘积
leaf["数量"] = str(new_factor)
except Exception:
leaf["数量"] = str(new_factor)
results.append(leaf)
else:
# 非资源,继续向下查找
for ch in children:
results.extend(_flatten_resource_splits(ch, inherited_type, qty_factor))
return results
# 递归处理工程量节点,提取人材机节点(对children内的人材机执行拆分上提)
def process_nodes(nodes): def process_nodes(nodes):
for node in nodes: for node in nodes:
# 如果是定额节点,处理其材机列表和children # 如果是定额节点,处理其材机列表和children
@@ -756,22 +810,25 @@ def calculate_rcj_fees(
f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 有子节点,数量: {len(node['children'])}" f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 有子节点,数量: {len(node['children'])}"
) )
for child in node["children"]: for child in node["children"]:
child_type = child.get("类型", child.get("type")) # 将children中的资源节点拆分为叶子资源,排除中间父级
if child_type in ["人工", "2"]: flattened = _flatten_resource_splits(child)
labor_nodes.append((child, node)) for leaf in flattened:
print( leaf_type = _normalize_type(leaf.get("类型") or leaf.get("type"))
f"从children找到人工节点: {child.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}" if leaf_type == "人工":
) labor_nodes.append((leaf, node))
elif child_type in ["材料", "3"]: print(
material_nodes.append((child, node)) f"从children(拆分后)找到人工节点: {leaf.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
print( )
f"从children找到材料节点: {child.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}" elif leaf_type == "材料":
) material_nodes.append((leaf, node))
elif child_type in ["机械", "4"]: print(
machine_nodes.append((child, node)) f"从children(拆分后)找到材料节点: {leaf.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
print( )
f"从children找到机械节点: {child.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}" elif leaf_type == "机械":
) machine_nodes.append((leaf, node))
print(
f"从children(拆分后)找到机械节点: {leaf.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
)
else: else:
print(f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 没有子节点") print(f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 没有子节点")
@@ -820,11 +877,14 @@ def calculate_rcj_fees(
# result_nodes.append(node_copy) # result_nodes.append(node_copy)
# return result_nodes # return result_nodes
# 创建工程信息上下文(顶层上下文) # 创建工程信息上下文(顶层上下文,优先使用 json_data
project_context = create_project_contexts(json_file_path=json_file_path) project_context = create_project_contexts(json_file_path=json_file_path, json_data=json_data)
with open(json_file_path, "r", encoding="utf-8") as file: if json_data is not None:
data = json.load(file) data = json_data
else:
with open(json_file_path, "r", encoding="utf-8") as file:
data = json.load(file)
processed_data = process_DXdata(data) processed_data = process_DXdata(data)
@@ -984,6 +1044,7 @@ def calculate_resource_fees(
project_name: str, project_name: str,
project_guid: str = None, project_guid: str = None,
calculation_strategy=None, calculation_strategy=None,
json_data: dict | None = None,
) -> str: ) -> str:
""" """
计算人材机合价 计算人材机合价
@@ -1004,7 +1065,7 @@ def calculate_resource_fees(
calculation_strategy = DefaultCalculationStrategy() calculation_strategy = DefaultCalculationStrategy()
# 获取项目划分节点的GUID # 获取项目划分节点的GUID
_, _, _, _, target_node = load_project_data(json_file_path, project_name, project_guid) _, _, _, _, target_node = load_project_data(json_file_path, project_name, project_guid, json_data=json_data)
# 如果传入了GUID但与节点的GUID不一致,优先使用传入的GUID # 如果传入了GUID但与节点的GUID不一致,优先使用传入的GUID
node_guid = target_node.get("GUID") or target_node.get("guid", "") if target_node else "" node_guid = target_node.get("GUID") or target_node.get("guid", "") if target_node else ""
project_guid = project_guid if project_guid else node_guid project_guid = project_guid if project_guid else node_guid
@@ -1055,10 +1116,16 @@ def calculate_resource_fees(
# 计算人材机节点的合价,传递project_guid参数 # 计算人材机节点的合价,传递project_guid参数
# 这里使用 calculation_strategy 的 calculate_rcj_fees 方法 # 这里使用 calculation_strategy 的 calculate_rcj_fees 方法
if hasattr(calculation_strategy, "calculate_rcj_fees"): if hasattr(calculation_strategy, "calculate_rcj_fees"):
rcj_results = calculation_strategy.calculate_rcj_fees(json_file_path, project_name, project_guid) try:
rcj_results = calculation_strategy.calculate_rcj_fees(
json_file_path, project_name, project_guid, json_data=json_data
)
except TypeError:
# 向后兼容:旧策略不支持 json_data 形参
rcj_results = calculation_strategy.calculate_rcj_fees(json_file_path, project_name, project_guid)
else: else:
# 如果计算策略没有实现 calculate_rcj_fees 方法,使用原始函数 # 如果计算策略没有实现 calculate_rcj_fees 方法,使用原始函数
rcj_results = calculate_rcj_fees(json_file_path, project_name, project_guid) rcj_results = calculate_rcj_fees(json_file_path, project_name, project_guid, json_data=json_data)
# 检查是否有人材机数据 # 检查是否有人材机数据
has_data = False has_data = False
+19 -17
View File
@@ -1,6 +1,7 @@
import re import re
import codecs import codecs
def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats_path="warning_statistics.txt"): def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats_path="warning_statistics.txt"):
""" """
从日志文件中提取 WARNING 和 ERROR 及其 Traceback 堆栈信息,保存到新文件 从日志文件中提取 WARNING 和 ERROR 及其 Traceback 堆栈信息,保存到新文件
@@ -8,15 +9,15 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
同时统计WARNING信息并输出到单独文件 同时统计WARNING信息并输出到单独文件
""" """
# 正则匹配日志行开头(时间戳格式) # 正则匹配日志行开头(时间戳格式)
log_pattern = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})') log_pattern = re.compile(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})")
# 尝试多种编码格式读取文件 # 尝试多种编码格式读取文件
encodings = ['utf-8', 'gbk', 'gb2312', 'ascii'] encodings = ["utf-8", "gbk", "gb2312", "ascii"]
lines = [] lines = []
for encoding in encodings: for encoding in encodings:
try: try:
with open(input_log_path, 'r', encoding=encoding) as f: with open(input_log_path, "r", encoding=encoding) as f:
lines = f.readlines() lines = f.readlines()
print(f"✅ 成功使用 {encoding} 编码读取文件") print(f"✅ 成功使用 {encoding} 编码读取文件")
break break
@@ -26,10 +27,10 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
# 如果所有编码都失败,则使用二进制模式读取并尝试解码 # 如果所有编码都失败,则使用二进制模式读取并尝试解码
if not lines: if not lines:
try: try:
with open(input_log_path, 'rb') as f: with open(input_log_path, "rb") as f:
content = f.read() content = f.read()
# 尝试解码,忽略错误 # 尝试解码,忽略错误
lines = content.decode('utf-8', errors='ignore').splitlines(True) lines = content.decode("utf-8", errors="ignore").splitlines(True)
print("⚠️ 使用二进制模式读取文件,可能有字符丢失") print("⚠️ 使用二进制模式读取文件,可能有字符丢失")
except Exception as e: except Exception as e:
print(f"❌ 无法读取文件: {e}") print(f"❌ 无法读取文件: {e}")
@@ -46,10 +47,10 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
if is_new_log: if is_new_log:
# 判断是否为 WARNING 或 ERROR # 判断是否为 WARNING 或 ERROR
if ' - WARNING - ' in line or ' - ERROR - ' in line: if " - WARNING - " in line or " - ERROR - " in line:
error_lines.append(line.rstrip()) error_lines.append(line.rstrip())
# 如果是 ERROR,捕获后续的 Traceback 信息 # 如果是 ERROR,捕获后续的 Traceback 信息
if ' - ERROR - ' in line: if " - ERROR - " in line:
i += 1 i += 1
# 继续读取后续行,直到遇到下一个时间戳行或文件结束 # 继续读取后续行,直到遇到下一个时间戳行或文件结束
while i < len(lines): while i < len(lines):
@@ -65,7 +66,7 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
error_lines.append(next_line.rstrip()) error_lines.append(next_line.rstrip())
i += 1 i += 1
# 如果是DEBUG/INFO行,检查是否包含Traceback # 如果是DEBUG/INFO行,检查是否包含Traceback
elif ' - DEBUG - ' in line and i + 1 < len(lines) and 'Traceback' in lines[i + 1]: elif " - DEBUG - " in line and i + 1 < len(lines) and "Traceback" in lines[i + 1]:
# 这是一个包含Traceback的DEBUG信息,也提取 # 这是一个包含Traceback的DEBUG信息,也提取
error_lines.append(line.rstrip()) error_lines.append(line.rstrip())
i += 1 i += 1
@@ -92,35 +93,36 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
i += 1 i += 1
# 写入输出文件 # 写入输出文件
with open(output_error_path, 'w', encoding='utf-8') as f: with open(output_error_path, "w", encoding="utf-8") as f:
for err_line in error_lines: for err_line in error_lines:
f.write(err_line + '\n') f.write(err_line + "\n")
# 统计WARNING信息 # 统计WARNING信息
warning_dict = {} warning_dict = {}
for line in error_lines: for line in error_lines:
if ' - WARNING - ' in line: if " - WARNING - " in line:
# 提取WARNING后的内容作为键 # 提取WARNING后的内容作为键
warning_content = line.split(' - WARNING - ', 1)[1] warning_content = line.split(" - WARNING - ", 1)[1]
if warning_content in warning_dict: if warning_content in warning_dict:
warning_dict[warning_content] += 1 warning_dict[warning_content] += 1
else: else:
warning_dict[warning_content] = 1 warning_dict[warning_content] = 1
# 写入统计结果到文件 # 写入统计结果到文件
with open(warning_stats_path, 'w', encoding='utf-8') as f: with open(warning_stats_path, "w", encoding="utf-8") as f:
f.write("WARNING统计结果:\n") f.write("WARNING统计结果:\n")
f.write(f"共找到 {len(warning_dict)} 种不同的WARNING信息\n\n") f.write(f"共找到 {len(warning_dict)} 种不同的WARNING信息\n\n")
for warning_content, count in warning_dict.items(): for warning_content, count in warning_dict.items():
f.write(f"{count}次: {warning_content}\n") f.write(f"{warning_content}\n")
print(f"✅ 提取完成!共找到 {len(error_lines)} 行错误/警告信息。") print(f"✅ 提取完成!共找到 {len(error_lines)} 行错误/警告信息。")
print(f"📁 已保存到: {output_error_path}") print(f"📁 已保存到: {output_error_path}")
print(f"📊 WARNING统计已保存到: {warning_stats_path}") print(f"📊 WARNING统计已保存到: {warning_stats_path}")
# ============ 使用示例 ============ # ============ 使用示例 ============
if __name__ == "__main__": if __name__ == "__main__":
input_file = "bcl_calculator.log" # 替换为你的日志文件路径 input_file = "bcl_calculator.log" # 替换为你的日志文件路径
output_file = "error_report.txt" # 输出的错误报告文件 output_file = "error_report.txt" # 输出的错误报告文件
warning_stats_file = "warning_statistics.txt" # WARNING统计结果文件 warning_stats_file = "warning_statistics.txt" # WARNING统计结果文件
extract_errors_and_warnings(input_file, output_file, warning_stats_file) extract_errors_and_warnings(input_file, output_file, warning_stats_file)
+62
View File
@@ -57,6 +57,37 @@ def transform_expense_preview(input_file, output_file):
print(f"原始expensePreview中的顶级分类: {list(original_expense_preview.keys())}") print(f"原始expensePreview中的顶级分类: {list(original_expense_preview.keys())}")
print(f"projectDivision中的顶级分类: {list(project_division.keys())}") print(f"projectDivision中的顶级分类: {list(project_division.keys())}")
# 先清理 projectDivision:递归删除任意带有 "删除": "1" 或 1 的节点
def _filter_deleted_nodes(obj):
# 若当前对象本身标记了删除,则直接丢弃
if isinstance(obj, dict):
flag = obj.get("删除")
if flag == "1" or flag == 1:
return None
new_obj = {}
for k, v in obj.items():
filtered = _filter_deleted_nodes(v)
if filtered is not None:
new_obj[k] = filtered
return new_obj
elif isinstance(obj, list):
new_list = []
for item in obj:
filtered = _filter_deleted_nodes(item)
if filtered is not None:
new_list.append(filtered)
return new_list
else:
return obj
cleaned_project_division = _filter_deleted_nodes(project_division) or {}
if cleaned_project_division != project_division:
print("已根据 '删除' 标记清理 projectDivision 中的节点")
project_division = cleaned_project_division
# 回写清理后的结构,确保后续流程与落盘一致
if "projectData" in data:
data["projectData"]["projectDivision"] = project_division
# 创建新的expensePreview结构 # 创建新的expensePreview结构
new_expense_preview = {} new_expense_preview = {}
@@ -478,6 +509,37 @@ def transform_json_types(input_file_path, output_file_path=None):
with open(input_file_path, "r", encoding="utf-8") as f: with open(input_file_path, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
# 在主网流程中,同样先清理 projectDivision:递归删除任意带有 "删除": "1" 或 1 的节点
def _filter_deleted_nodes(obj):
if isinstance(obj, dict):
flag = obj.get("删除")
if flag == "1" or flag == 1:
return None
new_obj = {}
for k, v in obj.items():
filtered = _filter_deleted_nodes(v)
if filtered is not None:
new_obj[k] = filtered
return new_obj
elif isinstance(obj, list):
new_list = []
for item in obj:
filtered = _filter_deleted_nodes(item)
if filtered is not None:
new_list.append(filtered)
return new_list
else:
return obj
try:
pd = data.get("projectData", {}).get("projectDivision", {})
cleaned_pd = _filter_deleted_nodes(pd) or {}
if cleaned_pd != pd and "projectData" in data:
data["projectData"]["projectDivision"] = cleaned_pd
print("[主网] 已根据 '删除' 标记清理 projectDivision 中的节点")
except Exception:
pass
# 递归处理函数 # 递归处理函数
def traverse(obj): def traverse(obj):
if isinstance(obj, dict): if isinstance(obj, dict):