该文件是整个系统的核心计算引擎,负责DXF文件解析、几何图形面积/长度/体积计算、钢筋重量计算。以下为逐行核验结果:
calc.html 第382行let dxfUnitFactor = 0.001; // 硬编码为mm→m的转换因子
$INSUNITS 决定,可以是毫米(13)、厘米(14)、米(15)、英寸(1)等。硬编码为0.001假设所有DXF均以毫米为单位。若图纸以米为单位绘制(市政工程常见),则所有面积将缩小1,000,000倍,体积将缩小1,000,000,000倍。$INSUNITS 变量,动态确定单位转换因子:// 修正:从DXF header读取单位 function getDxfUnitFactor(dxf) { const insUnits = dxf.header?.$INSUNITS || 13; // 默认毫米 const unitMap = { 1: 0.0254, 2: 0.3048, 4: 0.001, 13: 0.001, 14: 0.01, 15: 1.0 }; return unitMap[insUnits] || 0.001; }
calc.html 第878行const sw = cfg.sectionWidth || Math.round((cfg.thickness || 0) * 0.5); // 截面宽度 = 厚度×0.5,无任何工程依据 volumeCubicM = totalLengthM * (sw * dxfUnitFactor) * thicknessM;
calc.html 第385-394行const COMPONENT_TYPES = { 'floor': { defaultThickness: 150 }, // 楼板150mm 'wall': { defaultThickness: 240 }, // 墙240mm 'column': { defaultThickness: 3000 }, // 柱3000mm=层高?这不是"厚度" 'beam': { defaultThickness: 600 }, // 梁600mm=梁高? 'foundation': { defaultThickness: 500 }, // 基础500mm 'earth': { defaultThickness: 1000 }, // 土方1000mm=开挖深度? };
calc.html 第797行、第804行weightPerM: 0.00617 * r.diameter * r.diameter, // 理论重量 // ... rebar.totalLength = rebar.count * (rebar.lengthPerPiece || 1); // 默认每根1米! rebar.totalWeight = rebar.totalLength * rebar.weightPerM;
lengthPerPiece初始值为0,在未从图纸识别到钢筋长度时,|| 1默认每根钢筋长度为1米。实际工程中钢筋单根长度通常为6-12米,此默认值导致总长度严重偏小。0.00617 × d² 是近似值。国标公式为 π/4 × d² × 7850 / 10⁶ = 0.006165 × d²,使用0.00617引入约0.08%的正偏差。calc.html 第868-875行case 'area_times_thickness': volumeCubicM = totalAreaSqM * thicknessM; break; case 'area_times_height': volumeCubicM = totalAreaSqM * thicknessM; // 与上面完全相同! break;
calc.html 第917-927行function calculatePolygonArea(vertices) { if (!vertices || vertices.length < 3) return 0; const n = vertices.length; let area = 0; for (let i = 0; i < n; i++) { const j = (i + 1) % n; area += vertices[i].x * vertices[j].y; area -= vertices[j].x * vertices[i].y; } return Math.abs(area) / 2; }
ent.closed === true && vertices.length >= 3 才归入closedPolys,这是正确的。但如果DXF中多段线标记为非闭合但实际首尾点重合,会被归入openPolys只计算长度calc.html 第860-864行group.openPolys.forEach(function(poly) { const rawLength = calculatePolylineLength(poly.vertices); totalLengthM += rawLength * dxfUnitFactor; if (poly.vertices.length >= 2 && poly.vertices.length < 3) shapeCount++; // 只计2点线段 });
if (poly.vertices.length >= 2) shapeCount++;calc.html 第887行case 'count_only': volumeCubicM = shapeCount; // 门窗数量被赋值给体积字段 break;
calc.html 第856-857行const rawArea = Math.PI * circ.radius * circ.radius; totalAreaSqM += rawArea * dxfUnitFactor * dxfUnitFactor; // 面积用平方因子
if (!circ.radius || circ.radius <= 0) return; 防御性检查。calc.html 第881-884行if (totalAreaSqM > 0 && totalLengthM === 0) { volumeCubicM = totalAreaSqM * thicknessM; // 回退到面积×厚度 note = '面积×截面高'; }
DrawingParseServiceImpl.java 第151-163行Pattern textPattern = Pattern.compile(
"0\\s+TEXT[\\s\\S]*?1\\s+(.+?)[\\s\\S]*?10\\s+([\\d.\\-]+)[\\s\\S]*?20\\s+([\\d.\\-]+)",
Pattern.MULTILINE
);
[\s\S]*? 是非贪婪匹配,但仍可能跨越多个TEXT实体边界,将两个不同实体的数据拼接在一起DrawingParseServiceImpl.java 第304行、第315行// 识别混凝土标注 if (content.contains("C25") || content.contains("C30") || content.contains("混凝土")) { item.setQuantity(BigDecimal.valueOf(10.0)); // 示例值,实际应从图纸计算 }// 识别钢筋标注 if (content.contains("钢筋") || content.contains("HRB") || content.contains("Φ")) { item.setQuantity(BigDecimal.valueOf(2.5)); // 示例值 }
DrawingParseServiceImpl.java 第247-250行private void extractPolylines(String dxfContent, Listgeometries) { // 简化处理,实际需要更复杂的解析逻辑 // 这里仅作为示例 } // 方法体为空!
DrawingParseServiceImpl.java 第196-212行(extractLines)、第218-241行(extractCircles)// extractLines方法中: geometry.setType("LINE"); geometry.setCoordinates(Arrays.asList(x1, y1, x2, y2)); geometry.setLength(length); // 缺少: geometry.setLayer(layerName); // extractCircles方法中: geometry.setType("CIRCLE"); geometry.setCoordinates(Arrays.asList(centerX, centerY)); geometry.setRadius(radius); // 缺少: geometry.setLayer(layerName);
DrawingParseServiceImpl.java 第340-368行if (itemName.contains("混凝土")) { MaterialPrice concrete = materialMap.get("C25混凝土"); // 精确匹配 } if (itemName.contains("钢筋")) { MaterialPrice steel = materialMap.get("螺纹钢"); // 精确匹配 }
Collectors.toMap(MaterialPrice::getMaterialName, ...) 构建,key是材料全名。但:QuotationServiceImpl.java 第60-74行if (quantityItem.getItemName().contains("混凝土")) { item.setLaborUnitPrice(new BigDecimal("45.0")); // 硬编码 item.setMaterialUnitPrice(new BigDecimal("385.0")); // 硬编码 } else if (quantityItem.getItemName().contains("钢筋")) { item.setLaborUnitPrice(new BigDecimal("120.0")); // 硬编码 item.setMaterialUnitPrice(new BigDecimal("4250.0")); // 硬编码 } else if (quantityItem.getItemName().contains("管线")) { item.setLaborUnitPrice(new BigDecimal("35.0")); // 硬编码 item.setMaterialUnitPrice(new BigDecimal("150.0")); // 硬编码 }
// QuotationServiceImpl.java 第148行 — 管理费基数=直接费 item.setManagementFee(directCost.multiply(item.getManagementFeeRate()).divide(...)); // QuotationCalcController.java 第353行 — 管理费基数=人工费 BigDecimal managementFee = laborInMeasure.multiply(rates.get("managementRate")).divide(...);
| 计算位置 | 管理费基数 | 利润基数 | 管理费率 | 利润率 |
|---|---|---|---|---|
| QuotationServiceImpl | 直接费(人+材+机) | 直接费+管理费 | 5.0% | 8.0% |
| QuotationCalcController | 人工费 | 人工费 | 25.97% | 21.56% |
QuotationServiceImpl.java 第161-164行// 税金 = (直接费 + 管理费 + 利润) × 税率 BigDecimal baseForTax = directCost.add(item.getManagementFee()).add(item.getProfit()); // 缺少措施费、材差 item.setTax(baseForTax.multiply(item.getTaxRate()).divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP));
QuotationServiceImpl.java 第54-56行item.setManagementFeeRate(new BigDecimal("5.0")); // 管理费5% item.setProfitRate(new BigDecimal("8.0")); // 利润8% item.setTaxRate(new BigDecimal("9.0")); // 税金9%(VAT正确)
QuotationServiceImpl.java 第62行、第67行、第72行// 混凝土:工时 = 工程量 × 0.5 item.setLaborHours(quantityItem.getQuantity().multiply(new BigDecimal("0.5"))); // 钢筋:工时 = 工程量 × 2.0 item.setLaborHours(quantityItem.getQuantity().multiply(new BigDecimal("2.0"))); // 管线:工时 = 工程量 × 0.3 item.setLaborHours(quantityItem.getQuantity().multiply(new BigDecimal("0.3")));
QuotationServiceImpl.java 第148、156、164、170-176行// 管理费:四舍五入到2位 item.setManagementFee(directCost.multiply(rate).divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP)); // 利润:四舍五入到2位 item.setProfit(baseForProfit.multiply(rate).divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP)); // 税金:四舍五入到2位 item.setTax(baseForTax.multiply(rate).divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP)); // 总价:再次四舍五入 item.setTotalCost(directCost.add(mgmt).add(profit).add(tax).setScale(2, BigDecimal.ROUND_HALF_UP));
QuotationCalcController.java 第318-333行feeRates.put("construction", Map.of( "managementRate", new BigDecimal("25.97"), // 硬编码 "profitRate", new BigDecimal("21.56"), // 硬编码 "taxRate", new BigDecimal("9.00") )); feeRates.put("decoration", Map.of( "managementRate", new BigDecimal("9.12"), "profitRate", new BigDecimal("9.88"), ... ));
Map.of()创建不可变Map,运行时无法修改QuotationCalcController.java 第353-358行BigDecimal laborInMeasure = toBigDecimal(project.get("laborCostInMeasure")); // 管理费 = 人工费 × 管理费率 BigDecimal managementFee = laborInMeasure.multiply(rates.get("managementRate")) .divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP); // 利润 = 人工费 × 利润率 BigDecimal profit = laborInMeasure.multiply(rates.get("profitRate")) .divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP);
laborCostInMeasure 来自前端传入的JSON,未校验其值是否合理QuotationCalcController.java 第361-369行// 税前造价 BigDecimal preTax = directCost.add(professionalMeasure).add(generalMeasure) .add(managementFee).add(profit).add(dynamicAdjustment); // 缺少mainMaterialCost? // 增值税 BigDecimal tax = preTax.multiply(rates.get("taxRate")) .divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP); // 工程造价(含税) BigDecimal totalAmount = preTax.add(tax);
main_material_cost(主材费)字段,但税前造价计算未包含QuotationCalcController.java 第311-395行projectType 是否为 "construction"/"decoration"/"municipal" 之一feeRates.getOrDefault(projectType, feeRates.get("construction"))),可能掩盖用户输入错误DrawingParseController.java 第112-151行// 步骤2:从COS下载到临时目录 File localFile = cosUploadService.downloadToTemp(objectKey); // 步骤3:如果是DWG,转换为DXF if (isDwg) { tempDxfFile = convertDwgToDxf(localFile, filename); if (tempDxfFile != null && tempDxfFile.exists()) { localFile = tempDxfFile; // localFile指向转换后的DXF } } // 步骤4:解析DXF文件 DrawingParseResultDTO result = drawingParseService.parseDxfFile(file, taskId); // 传入的是原始MultipartFile!
file(MultipartFile对象),而非下载/转换后的 localFile。这意味着:parseDxfFile(file, taskId) 改为 parseDxfFile(new FileInputStream(localFile), localFile.getName(), taskId),或重构parseDxfFile方法接受File参数。DrawingParseController.java 第194-197行cosUploadService.cleanupAfterParse(objectKey) 删除COS上的原始文件。如果用户需要重新解析(如修改了图层映射配置后),原始文件已不存在,需要重新上传。DwgToDxfServiceImpl.java 第132-145行、第157-208行private boolean performConversion(String dwgPath, String dxfPath) { // 当前:模拟转换(仅用于测试) createSampleDxf(dxfPath); // 创建固定的示例DXF return true; } private void createSampleDxf(String dxfPath) { // 硬编码生成一条直线、一个圆、一段文字"C25混凝土" // 无论输入什么DWG文件,输出都是这个固定内容 }
// DashboardServiceImpl.java 第64行 doneWrapper.eq(ConversionTask::getStatus, 2); // 查询status=2的任务 // DrawingParseController.java 第174行 jdbcTemplate.update("UPDATE conversion_task SET status = 1, progress = 100 WHERE id = ?", taskId); // 设置status=1
DashboardServiceImpl.java 第133-135行BigDecimal total = concrete.add(steel).add(brickwork)
.add(waterChannel).add(trench).add(culvert)
.add(slopeProtection).add(earthwork).add(riverLining); // 单位不同!
quotation.html 第396-401行const summaryData = [ { id: 1, type: 'construction', direct: 115128.27, ... total: 250338.81 }, { id: 2, type: 'decoration', direct: 29300.46, ... total: 39307.96 }, { id: 3, type: 'municipal', ... total: 13427.44 }, ]; // 硬编码北屯村项目数据
/api/quotation/summary/list 等API获取实际数据;如果没有报价记录,显示空状态引导用户创建。使用北屯村安置房项目成本测算表中的实际数据,代入系统计算公式,验证结果是否与人工造价核算一致。
| # | 等级 | 文件 | 问题摘要 | 影响 |
|---|---|---|---|---|
| 01 | P0 | calc.html:382 | DXF单位转换因子硬编码0.001 | 非毫米单位图纸面积/体积偏差百万倍 |
| 02 | P0 | calc.html:878 | 梁截面宽度=厚度×0.5无工程依据 | 梁体积偏差最高+44% |
| 03 | P0 | calc.html:385-394 | 构件默认厚度硬编码,柱thickness概念混淆 | 柱体积偏差-16.7% |
| 04 | P1 | calc.html:797,804 | 钢筋默认长度1m,理论重量系数有偏差 | 钢筋重量偏差-83.3% |
| 05 | P1 | calc.html:873-875 | area_times_height与area_times_thickness代码相同 | 计算模式冗余,概念不清 |
| 06 | P1 | calc.html:917-927 | 多边形面积未校验自交和精度 | 自交多段线面积错误 |
| 07 | P2 | calc.html:863 | 3+顶点开放多段线不计入shapeCount | 门窗数量统计偏小 |
| 08 | P2 | calc.html:887 | count_only数量写入volume字段 | 总体积虚高 |
| 09 | P2 | calc.html:852,857 | 圆面积未校验负半径/零半径 | 损坏数据导致NaN |
| 10 | P2 | calc.html:881-884 | length_times_section回退逻辑混乱 | 梁体积可能误用面积公式 |
| 11 | P0 | DrawingParseServiceImpl:151 | 正则表达式解析DXF根本性错误 | 跨实体匹配/丢图层/不支持多段线 |
| 12 | P0 | DrawingParseServiceImpl:304,315 | 混凝土/钢筋工程量硬编码10.0m³/2.5t | 工程量与图纸内容完全无关 |
| 13 | P0 | DrawingParseServiceImpl:247 | 多段线提取方法为空 | 最重要的几何实体被完全忽略 |
| 14 | P1 | DrawingParseServiceImpl:196-241 | 几何实体未提取图层信息 | 无法按图层分类构件 |
| 15 | P1 | DrawingParseServiceImpl:340-368 | 材料匹配使用精确字符串 | 材料名称变体无法匹配 |
| 16 | P0 | QuotationServiceImpl:60-74 | 人工/材料单价全部硬编码 | 价格不随市场波动,偏差-14%+ |
| 17 | P1 | QuotationServiceImpl:148 vs Controller:353 | 管理费计算基数不一致(直接费vs人工费) | 同一系统两套报价,偏差+56% |
| 18 | P1 | QuotationServiceImpl:163 | 税金基数缺少措施费和材差 | 税金少计-23.6% |
| 19 | P1 | QuotationServiceImpl:54-56 | 费率硬编码5%/8%/9%不符合定额 | 管理费偏差-78% |
| 20 | P1 | QuotationServiceImpl:62,67,72 | 人工工时=工程量×固定系数无依据 | 人工费偏差-58% |
| 21 | P2 | QuotationServiceImpl:176 | 中间过程精度处理可能导致累计偏差 | 偏差≤0.03元 |
| 22 | P1 | QuotationCalcController:319-333 | 费率硬编码在控制器中 | 修改费率需重新编译 |
| 23 | P1 | QuotationCalcController:353-358 | 人工费基数缺少来源校验 | 人工费=0时管理费/利润为0 |
| 24 | P2 | QuotationCalcController:361 | 税前造价缺少主材费字段 | 可能漏算主材费 |
| 25 | P2 | QuotationCalcController:311-395 | 缺少输入参数校验 | 非法输入产生错误结果 |
| 26 | P0 | DrawingParseController:151 | 解析传入原始MultipartFile而非转换后文件 | DWG文件解析必然失败 |
| 27 | P2 | DrawingParseController:194 | COS文件在返回前已清理 | 无法重新解析 |
| 28 | P0 | DwgToDxfServiceImpl:132 | DWG转DXF为模拟实现 | 所有DWG输出固定假DXF |
| 29 | P2 | DashboardServiceImpl:64 | 已完成任务状态值不一致 | 仪表盘完成数永远为0 |
| 30 | P2 | DashboardServiceImpl:133 | 不同单位工程量直接相加 | 总量无物理意义 |
| 31 | P0 | quotation.html:396-401 | 前端报价数据全部硬编码 | 报价页面永远显示固定数据 |
经过架构师、造价工程师、测试工程师三方联合核验,工程量测算系统共发现 31个问题,其中 P0致命错误13个、P1严重偏差11个、P2一般问题7个。
核心问题汇总:
建议优先修复顺序: