package com.yhy.module.core.service.workbench.impl;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_RESOURCE_CHILD_CONSUME_QTY_READONLY;
import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_RESOURCE_DIVISION_NOT_QUOTA;
import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_RESOURCE_MANUAL_CONSUME_QTY_READONLY;
import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_RESOURCE_NOT_EXISTS;

import cn.hutool.core.collection.CollUtil;
import com.yhy.module.core.controller.admin.quota.vo.QuotaResourceRespVO;
import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceCategorySummaryVO;
import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceRespVO;
import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceSaveReqVO;
import com.yhy.module.core.convert.workbench.WbBoqResourceConvert;
import com.yhy.module.core.dal.dataobject.resource.ResourceCategoryDO;
import com.yhy.module.core.dal.dataobject.resource.ResourceItemDO;
import com.yhy.module.core.dal.dataobject.workbench.WbAdjustmentSettingDO;
import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO;
import com.yhy.module.core.dal.dataobject.workbench.WbBoqResourceDO;
import com.yhy.module.core.dal.mysql.resource.ResourceItemMapper;
import com.yhy.module.core.dal.mysql.workbench.WbAdjustmentSettingMapper;
import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper;
import com.yhy.module.core.dal.mysql.workbench.WbBoqResourceMapper;
import com.yhy.module.core.service.quota.QuotaResourceService;
import com.yhy.module.core.service.workbench.WbBoqResourceService;
import com.yhy.module.core.util.AdjustmentFormulaCalculator;
import com.yhy.module.core.util.FormulaEvaluator;
import com.yhy.module.core.util.ResourcePriceCalculator;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

/**
 * 工作台工料机消耗 Service 实现类
 *
 * @author yhy
 */
@Service
@Validated
@Slf4j
public class WbBoqResourceServiceImpl implements WbBoqResourceService {

    @Resource
    private WbBoqResourceMapper wbBoqResourceMapper;

    @Resource
    private WbBoqDivisionMapper wbBoqDivisionMapper;

    @Resource
    private QuotaResourceService quotaResourceService;

    @Resource
    private ResourceItemMapper resourceItemMapper;

    @Resource
    private WbAdjustmentSettingMapper wbAdjustmentSettingMapper;

    @Resource
    private com.yhy.module.core.dal.mysql.resource.ResourceInfoPriceMappingMapper resourceInfoPriceMappingMapper;
    
    @Resource
    private com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourcePriceMapper infoPriceResourcePriceMapper;
    
    @Resource
    private com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourceMapper infoPriceResourceMapper;
    
    @Resource
    private com.yhy.module.core.dal.mysql.infoprice.InfoPriceCategoryTreeMapper infoPriceCategoryTreeMapper;
    
    @Resource
    private com.yhy.module.core.dal.mysql.infoprice.InfoPriceBookMapper infoPriceBookMapper;
    
    @Resource
    private com.yhy.module.core.dal.mysql.infoprice.InfoPriceTreeMapper infoPriceTreeMapper;
    
    @Resource
    private com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper wbCompileTreeMapper;
    
    @Resource
    private com.yhy.module.core.dal.mysql.resource.ResourceCategoryMapper resourceCategoryMapper;
    
    @Resource
    private com.yhy.module.core.service.quota.QuotaItemService quotaItemService;
    
    @Resource
    private com.yhy.module.core.service.workbench.WbSnapshotReadService wbSnapshotReadService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long create(WbBoqResourceSaveReqVO createReqVO) {
        // 1. 校验定额节点
        validateDivisionIsQuota(createReqVO.getDivisionId());

        // 2. 构建DO
        WbBoqResourceDO resource = WbBoqResourceConvert.INSTANCE.convert(createReqVO);

        // 3. 租户添加工料机时，双表写入逻辑
        if (createReqVO.getSourceResourceItemId() == null && 
            createReqVO.getName() != null && createReqVO.getUnit() != null) {
            // 查找或创建后台工料机
            Long resourceItemId = findOrCreateResourceItem(
                createReqVO.getName(), 
                createReqVO.getSpec(), 
                createReqVO.getUnit(),
                createReqVO.getResourceType()
            );
            resource.setSourceResourceItemId(resourceItemId);
            resource.setSourceType(WbBoqResourceDO.SOURCE_TYPE_TENANT);
        }

        // 4. 设置默认值
        if (resource.getConsumeQty() == null) {
            resource.setConsumeQty(BigDecimal.ZERO);
        }
        if (resource.getAdjustRate() == null) {
            resource.setAdjustRate(BigDecimal.ONE);
        }
        if (resource.getSourceType() == null) {
            resource.setSourceType(WbBoqResourceDO.SOURCE_TYPE_SYSTEM);
        }

        // 5. 设置排序号
        Integer maxSortOrder = wbBoqResourceMapper.selectMaxSortOrderByDivisionId(createReqVO.getDivisionId());
        resource.setSortOrder(maxSortOrder + 1);

        // 6. 插入数据库
        wbBoqResourceMapper.insert(resource);

        return resource.getId();
    }
    
    /**
     * 查找或创建后台工料机（根据名称、型号、单位全局唯一）
     */
    private Long findOrCreateResourceItem(String name, String spec, String unit, String resourceType) {
        return findOrCreateResourceItem(name, spec, unit, resourceType, null);
    }
    
    /**
     * 查找或创建后台工料机（根据名称、型号、单位全局唯一）
     * @param name 名称
     * @param spec 型号规格
     * @param unit 单位
     * @param resourceType 资源类型
     * @param sourceResourceItemId 原工料机ID（用于获取 catalogItemId）
     */
    private Long findOrCreateResourceItem(String name, String spec, String unit, String resourceType, Long sourceResourceItemId) {
        // 1. 查找是否已存在
        ResourceItemDO existItem = resourceItemMapper.selectByNameSpecUnit(name, spec, unit);
        if (existItem != null) {
            return existItem.getId();
        }
        
        // 2. 不存在则创建新记录
        ResourceItemDO newItem = new ResourceItemDO();
        newItem.setName(name);
        newItem.setSpec(spec);
        newItem.setUnit(unit);
        newItem.setType(resourceType);
        newItem.setSourceType(ResourceItemDO.SOURCE_TYPE_TENANT);
        
        // 3. 设置 catalogItemId（从原工料机获取，或使用默认值1）
        Long catalogItemId = 1L; // 默认值
        if (sourceResourceItemId != null) {
            ResourceItemDO sourceItem = resourceItemMapper.selectById(sourceResourceItemId);
            if (sourceItem != null && sourceItem.getCatalogItemId() != null) {
                catalogItemId = sourceItem.getCatalogItemId();
            }
        }
        newItem.setCatalogItemId(catalogItemId);
        
        resourceItemMapper.insert(newItem);
        
        log.info("租户创建新工料机：name={}, spec={}, unit={}, catalogItemId={}, id={}", 
                name, spec, unit, catalogItemId, newItem.getId());
        return newItem.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO update(WbBoqResourceSaveReqVO updateReqVO) {
        // 1. 校验存在
        WbBoqResourceDO existResource = validateResourceExists(updateReqVO.getId());

        // 2. 复合工料机的子工料机禁止修改定额消耗量
        if (existResource.getParentId() != null) {
            // 检查是否尝试修改 consumeQty
            if (updateReqVO.getConsumeQty() != null && 
                !updateReqVO.getConsumeQty().equals(existResource.getConsumeQty())) {
                throw exception(WB_BOQ_RESOURCE_CHILD_CONSUME_QTY_READONLY);
            }
        }

        // 2.1 手动新增的工料机（consumeQty为null且sourceType=tenant），禁止修改定额消耗量
        if (existResource.getConsumeQty() == null 
                && WbBoqResourceDO.SOURCE_TYPE_TENANT.equals(existResource.getSourceType())) {
            if (updateReqVO.getConsumeQty() != null) {
                throw exception(WB_BOQ_RESOURCE_MANUAL_CONSUME_QTY_READONLY);
            }
        }

        // 3. 更新
        WbBoqResourceDO updateObj = WbBoqResourceConvert.INSTANCE.convert(updateReqVO);
        updateObj.setId(existResource.getId());
        // 保留不可修改的字段
        updateObj.setDivisionId(existResource.getDivisionId());
        updateObj.setSortOrder(existResource.getSortOrder());
        updateObj.setParentId(existResource.getParentId()); // 保留父子关系，防止复合工料机结构被破坏
        updateObj.setCategoryId(existResource.getCategoryId()); // 保留机类ID，防止被覆盖为null

        // 4. 检查是否修改了唯一性字段（名称、型号规格、单位）
        boolean nameChanged = updateReqVO.getName() != null && !updateReqVO.getName().equals(existResource.getName());
        boolean specChanged = (updateReqVO.getSpec() != null && !updateReqVO.getSpec().equals(existResource.getSpec()))
                || (updateReqVO.getSpec() == null && existResource.getSpec() != null);
        boolean unitChanged = updateReqVO.getUnit() != null && !updateReqVO.getUnit().equals(existResource.getUnit());
        
        if (nameChanged || specChanged || unitChanged) {
            // 唯一性字段发生变化，需要检查后台工料机表
            String newName = updateReqVO.getName() != null ? updateReqVO.getName() : existResource.getName();
            String newSpec = updateReqVO.getSpec() != null ? updateReqVO.getSpec() : existResource.getSpec();
            String newUnit = updateReqVO.getUnit() != null ? updateReqVO.getUnit() : existResource.getUnit();
            String resourceType = updateReqVO.getResourceType() != null ? updateReqVO.getResourceType() : existResource.getResourceType();
            
            // 查找或创建后台工料机（传递原工料机ID用于获取catalogItemId）
            Long newResourceItemId = findOrCreateResourceItem(newName, newSpec, newUnit, resourceType, existResource.getSourceResourceItemId());
            updateObj.setSourceResourceItemId(newResourceItemId);
            updateObj.setSourceType(WbBoqResourceDO.SOURCE_TYPE_TENANT);
            
            log.info("工料机唯一性字段变更，更新引用：id={}, oldSourceId={}, newSourceId={}", 
                    updateReqVO.getId(), existResource.getSourceResourceItemId(), newResourceItemId);
        }

        // 5. 单位为 % 的工料机：自动清空税率和价格字段
        String finalUnit = updateReqVO.getUnit() != null ? updateReqVO.getUnit() : existResource.getUnit();
        if ("%".equals(finalUnit)) {
            updateObj.setTaxRate(null);
            updateObj.setTaxExclBasePrice(null);
            updateObj.setTaxInclBasePrice(null);
            updateObj.setTaxExclCompilePrice(null);
            updateObj.setTaxInclCompilePrice(null);
            log.info("单位为%的工料机，自动清空税率和价格字段：id={}", updateReqVO.getId());
        }

        // 6. 税率与价格联动（仅普通工料机：非%单位、非子工料机）
        if (!"%".equals(finalUnit) && existResource.getParentId() == null) {
            applyTaxRatePriceLinkage(updateObj, updateReqVO, existResource);
        }

        // 7. 用量与调整消耗量联动
        applyUsageQtyLinkage(updateObj, updateReqVO, existResource);

        wbBoqResourceMapper.updateById(updateObj);

        // 8. 项目级编制价/价格来源批量同步
        return syncCompilePriceAcrossProject(updateObj, updateReqVO, existResource);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(Long id) {
        // 1. 校验存在
        validateResourceExists(id);

        // 2. 删除
        wbBoqResourceMapper.deleteById(id);
    }

    @Override
    public WbBoqResourceRespVO get(Long id) {
        WbBoqResourceDO resource = wbBoqResourceMapper.selectById(id);
        if (resource == null) {
            return null;
        }
        return WbBoqResourceConvert.INSTANCE.convert(resource);
    }

    @Override
    public List<WbBoqResourceRespVO> getListByDivisionId(Long divisionId) {
        // 1. 获取分部分项节点
        WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(divisionId);
        if (division == null || !"quota".equals(division.getNodeType())) {
            return new ArrayList<>();
        }
        
        // 2. 从本地表查询工料机数据（复制模式）
        List<WbBoqResourceDO> resources = wbBoqResourceMapper.selectListByDivisionId(divisionId);
        
        // 3. 如果本地没有数据，尝试从后台复制
        if (CollUtil.isEmpty(resources)) {
            Long quotaItemId = division.getSourceQuotaItemId();
            if (quotaItemId != null) {
                // 从后台复制工料机数据
                copyFromQuotaResource(divisionId, quotaItemId);
                // 重新查询
                resources = wbBoqResourceMapper.selectListByDivisionId(divisionId);
            }
        }
        
        if (CollUtil.isEmpty(resources)) {
            return new ArrayList<>();
        }
        
        // 4. 转换为VO格式，处理父子关系（传入定额工程量用于计算合价）
        BigDecimal quotaQty = division.getQty() != null ? division.getQty() : BigDecimal.ONE;
        List<WbBoqResourceRespVO> result = convertToVOList(resources, quotaQty);
        
        // 5. 填充价格来源状态并虚拟替换价格（必须在%工料机计算之前，否则%工料机使用的是未替换的价格）
        fillPriceSourceStatus(result, division);
        
        // 6. 计算单位为%的工料机合价（需要先有价格来源替换后的合价数据）
        calculatePercentUnitTotalSums(result);
        
        // 7. 计算调整公式（虚拟字段）
        calculateAdjustmentFormulas(divisionId, result);
        
        // 8. 格式化数值：价格/税率保留2位小数，消耗量保留4位小数
        result.forEach(this::formatResourceVO);
        
        return result;
    }
    
    /**
     * 格式化工料机VO的数值字段
     * 价格、税率、合价：保留2位小数，四舍五入
     * 消耗量：保留4位小数，四舍五入
     */
    private void formatResourceVO(WbBoqResourceRespVO vo) {
        // 价格/税率/合价：2位小数
        vo.setTaxRate(roundToScale(vo.getTaxRate(), 2));
        vo.setTaxExclBasePrice(roundToScale(vo.getTaxExclBasePrice(), 2));
        vo.setTaxInclBasePrice(roundToScale(vo.getTaxInclBasePrice(), 2));
        vo.setTaxExclCompilePrice(roundToScale(vo.getTaxExclCompilePrice(), 2));
        vo.setTaxInclCompilePrice(roundToScale(vo.getTaxInclCompilePrice(), 2));
        vo.setTaxExclBaseTotalSum(roundToScale(vo.getTaxExclBaseTotalSum(), 2));
        vo.setTaxInclBaseTotalSum(roundToScale(vo.getTaxInclBaseTotalSum(), 2));
        vo.setTaxExclCompileTotalSum(roundToScale(vo.getTaxExclCompileTotalSum(), 2));
        vo.setTaxInclCompileTotalSum(roundToScale(vo.getTaxInclCompileTotalSum(), 2));
        
        // 消耗量：4位小数
        vo.setConsumeQty(roundToScale(vo.getConsumeQty(), 4));
        vo.setAdjustConsumeQty(roundToScale(vo.getAdjustConsumeQty(), 4));
        
        // 格式化子工料机
        if (vo.getChildren() != null) {
            vo.getChildren().forEach(this::formatResourceVO);
        }
    }
    
    /**
     * 四舍五入到指定小数位数
     */
    private BigDecimal roundToScale(BigDecimal value, int scale) {
        if (value == null) {
            return null;
        }
        return value.setScale(scale, java.math.RoundingMode.HALF_UP);
    }
    
    /**
     * 从后台定额工料机复制数据到工作台（快照模式）
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void copyFromQuotaResource(Long divisionId, Long quotaItemId) {
        // 1. 获取后台定额工料机数据
        List<QuotaResourceRespVO> quotaResources = quotaResourceService.getQuotaResourceList(quotaItemId);
        if (CollUtil.isEmpty(quotaResources)) {
            return;
        }
        
        // 2. 删除已有数据（如果有）
        wbBoqResourceMapper.deleteByDivisionId(divisionId);
        
        // 3. 复制数据到本地表
        int sortOrder = 0;
        for (QuotaResourceRespVO quotaResource : quotaResources) {
            // 复制主工料机
            WbBoqResourceDO resource = convertToDO(quotaResource, divisionId, null, ++sortOrder);
            wbBoqResourceMapper.insert(resource);
            
            // 复制子工料机（复合工料机）
            Boolean isMerged = quotaResource.getIsMerged();
            List<QuotaResourceRespVO.MergedResourceItemVO> mergedItems = quotaResource.getMergedItems();
            log.debug("复制工料机: id={}, name={}, isMerged={}, mergedItems={}", 
                quotaResource.getId(), quotaResource.getResourceName(), isMerged, 
                mergedItems != null ? mergedItems.size() : "null");
            
            if (mergedItems != null && !mergedItems.isEmpty()) {
                int childSortOrder = 0;
                for (QuotaResourceRespVO.MergedResourceItemVO mergedItem : mergedItems) {
                    WbBoqResourceDO childResource = convertMergedToDO(mergedItem, divisionId, resource.getId(), ++childSortOrder);
                    wbBoqResourceMapper.insert(childResource);
                    log.debug("复制子工料机: parentId={}, childName={}", resource.getId(), mergedItem.getResourceName());
                }
            }
        }
        
        log.info("从后台复制工料机数据完成，divisionId={}, quotaItemId={}, count={}", divisionId, quotaItemId, quotaResources.size());
    }
    
    /**
     * 将后台定额工料机转换为本地DO
     */
    private WbBoqResourceDO convertToDO(QuotaResourceRespVO quotaResource, Long divisionId, Long parentId, int sortOrder) {
        WbBoqResourceDO resource = new WbBoqResourceDO();
        resource.setDivisionId(divisionId);
        resource.setParentId(parentId);
        resource.setSourceResourceItemId(quotaResource.getResourceItemId());
        resource.setSourceQuotaResourceId(quotaResource.getId());
        resource.setResourceType(quotaResource.getResourceType());
        resource.setCode(quotaResource.getResourceCode());
        resource.setName(quotaResource.getResourceName());
        resource.setSpec(quotaResource.getResourceSpec());
        resource.setUnit(quotaResource.getResourceUnit());
        resource.setConsumeQty(quotaResource.getActualDosage());
        resource.setTaxRate(quotaResource.getResourceTaxRate());
        resource.setTaxExclBasePrice(quotaResource.getResourceTaxExclBasePrice());
        resource.setTaxInclBasePrice(quotaResource.getResourceTaxInclBasePrice());
        resource.setTaxExclCompilePrice(quotaResource.getResourceTaxExclCompilePrice());
        resource.setTaxInclCompilePrice(quotaResource.getResourceTaxInclCompilePrice());
        resource.setAdjustRate(BigDecimal.ONE);
        resource.setCategoryId(quotaResource.getResourceCategoryId());
        resource.setSortOrder(sortOrder);
        // 设置来源类型为系统（从定额复制）
        resource.setSourceType(WbBoqResourceDO.SOURCE_TYPE_SYSTEM);
        // 保存基线值（快照）
        resource.setBaseConsumeQty(quotaResource.getActualDosage());
        resource.setBaseBasePrice(quotaResource.getResourceTaxExclBasePrice());
        return resource;
    }
    
    /**
     * 将复合工料机的子项转换为本地DO
     */
    private WbBoqResourceDO convertMergedToDO(QuotaResourceRespVO.MergedResourceItemVO mergedItem, Long divisionId, Long parentId, int sortOrder) {
        WbBoqResourceDO resource = new WbBoqResourceDO();
        resource.setDivisionId(divisionId);
        resource.setParentId(parentId);
        resource.setSourceResourceItemId(mergedItem.getResourceItemId());
        resource.setSourceQuotaResourceId(mergedItem.getId());
        resource.setResourceType(mergedItem.getResourceType());
        resource.setCode(mergedItem.getResourceCode());
        resource.setName(mergedItem.getResourceName());
        resource.setSpec(mergedItem.getResourceSpec());
        resource.setUnit(mergedItem.getResourceUnit());
        resource.setConsumeQty(mergedItem.getDosage());
        resource.setTaxRate(mergedItem.getResourceTaxRate());
        resource.setTaxExclBasePrice(mergedItem.getResourceTaxExclBasePrice());
        resource.setTaxInclBasePrice(mergedItem.getResourceTaxInclBasePrice());
        resource.setTaxExclCompilePrice(mergedItem.getResourceTaxExclCompilePrice());
        resource.setTaxInclCompilePrice(mergedItem.getResourceTaxInclCompilePrice());
        resource.setAdjustRate(BigDecimal.ONE);
        resource.setCategoryId(mergedItem.getResourceCategoryId());
        resource.setSortOrder(sortOrder);
        // 设置来源类型为系统（从定额复制）
        resource.setSourceType(WbBoqResourceDO.SOURCE_TYPE_SYSTEM);
        // 保存基线值（快照）
        resource.setBaseConsumeQty(mergedItem.getDosage());
        resource.setBaseBasePrice(mergedItem.getResourceTaxExclBasePrice());
        return resource;
    }
    
    /**
     * 将DO列表转换为VO列表（处理父子关系）
     * 优化：批量预加载所有 ResourceItemDO，避免 N+1 查询
     * @param resources 工料机DO列表
     * @param quotaQty 定额工程量，用于计算合价（合价 = 用量 × 单价，用量 = 定额工程量 × 消耗量）
     */
    private List<WbBoqResourceRespVO> convertToVOList(List<WbBoqResourceDO> resources, BigDecimal quotaQty) {
        // 批量预加载所有需要的 ResourceItemDO（避免 convertToVO 中逐个查询）
        List<Long> sourceResourceItemIds = resources.stream()
                .map(WbBoqResourceDO::getSourceResourceItemId)
                .filter(java.util.Objects::nonNull)
                .distinct()
                .collect(java.util.stream.Collectors.toList());
        Map<Long, ResourceItemDO> resourceItemMap = new java.util.HashMap<>();
        if (!sourceResourceItemIds.isEmpty()) {
            List<ResourceItemDO> resourceItems = resourceItemMapper.selectBatchIds(sourceResourceItemIds);
            resourceItemMap = resourceItems.stream()
                    .collect(java.util.stream.Collectors.toMap(ResourceItemDO::getId, r -> r, (a, b) -> a));
        }
        
        // 分离父工料机和子工料机
        List<WbBoqResourceDO> parentResources = new ArrayList<>();
        Map<Long, List<WbBoqResourceDO>> childrenMap = new java.util.HashMap<>();
        
        for (WbBoqResourceDO resource : resources) {
            if (resource.getParentId() == null) {
                parentResources.add(resource);
            } else {
                childrenMap.computeIfAbsent(resource.getParentId(), k -> new ArrayList<>()).add(resource);
            }
        }
        
        // 转换为VO
        List<WbBoqResourceRespVO> result = new ArrayList<>();
        for (WbBoqResourceDO parent : parentResources) {
            WbBoqResourceRespVO vo = convertToVO(parent, resourceItemMap, quotaQty);
            // 添加子工料机
            List<WbBoqResourceDO> children = childrenMap.get(parent.getId());
            if (children != null && !children.isEmpty()) {
                // 子工料机用量计算：
                // 1. 单位为%：用量 = 消耗量（直接引用）
                // 2. 其他单位：用量 = 父工料机用量 × 子工料机消耗量
                BigDecimal parentUsageQty = vo.getUsageQty();
                List<WbBoqResourceRespVO> childVOs = new ArrayList<>();
                List<WbBoqResourceRespVO> percentChildVOs = new ArrayList<>(); // 单位为%的子工料机，需要延迟计算合价
                
                // 第一轮：计算非%单位子工料机的用量和合价
                for (WbBoqResourceDO child : children) {
                    WbBoqResourceRespVO childVO = convertToVO(child, resourceItemMap, null);
                    BigDecimal childConsumeQty = child.getAdjustConsumeQty() != null 
                            ? child.getAdjustConsumeQty() : child.getConsumeQty();
                    
                    if ("%".equals(child.getUnit())) {
                        // 单位为%：用量直接等于消耗量，合价延迟计算
                        childVO.setUsageQty(childConsumeQty);
                        percentChildVOs.add(childVO);
                    } else if (parentUsageQty != null && childConsumeQty != null) {
                        // 普通子工料机：用量 = 父用量 × 子消耗量
                        BigDecimal childUsageQty = parentUsageQty.multiply(childConsumeQty).setScale(4, java.math.RoundingMode.HALF_UP);
                        childVO.setUsageQty(childUsageQty);
                        // 基于子工料机用量计算除税合价和含税合价（合价 = 用量 × 编制价）
                        if (childVO.getTaxExclCompilePrice() != null) {
                            childVO.setTaxExclTotalSum(childUsageQty.multiply(childVO.getTaxExclCompilePrice()).setScale(2, java.math.RoundingMode.HALF_UP));
                        }
                        if (childVO.getTaxInclCompilePrice() != null) {
                            childVO.setTaxInclTotalSum(childUsageQty.multiply(childVO.getTaxInclCompilePrice()).setScale(2, java.math.RoundingMode.HALF_UP));
                        }
                        // 四个合价字段 = 单价 × 调整消耗量（非%子工料机）
                        if (childVO.getTaxExclBasePrice() != null) {
                            childVO.setTaxExclBaseTotalSum(childVO.getTaxExclBasePrice().multiply(childConsumeQty).setScale(2, java.math.RoundingMode.HALF_UP));
                        }
                        if (childVO.getTaxInclBasePrice() != null) {
                            childVO.setTaxInclBaseTotalSum(childVO.getTaxInclBasePrice().multiply(childConsumeQty).setScale(2, java.math.RoundingMode.HALF_UP));
                        }
                        if (childVO.getTaxExclCompilePrice() != null) {
                            childVO.setTaxExclCompileTotalSum(childVO.getTaxExclCompilePrice().multiply(childConsumeQty).setScale(2, java.math.RoundingMode.HALF_UP));
                        }
                        if (childVO.getTaxInclCompilePrice() != null) {
                            childVO.setTaxInclCompileTotalSum(childVO.getTaxInclCompilePrice().multiply(childConsumeQty).setScale(2, java.math.RoundingMode.HALF_UP));
                        }
                    }
                    childVOs.add(childVO);
                }
                
                // 构建子工料机的类别合价映射（用于%子工料机的calcBase公式计算）
                // 六组映射：除税基价合价、含税基价合价、除税编制合价、含税编制合价、除税合价、含税合价
                Map<Long, BigDecimal> childCategoryTaxExclBaseMap = new HashMap<>();
                Map<Long, BigDecimal> childCategoryTaxInclBaseMap = new HashMap<>();
                Map<Long, BigDecimal> childCategoryTaxExclCompileMap = new HashMap<>();
                Map<Long, BigDecimal> childCategoryTaxInclCompileMap = new HashMap<>();
                Map<Long, BigDecimal> childCategoryTaxExclMap = new HashMap<>();
                Map<Long, BigDecimal> childCategoryTaxInclMap = new HashMap<>();
                for (WbBoqResourceRespVO childVO : childVOs) {
                    if (!"%".equals(childVO.getUnit()) && childVO.getCategoryId() != null) {
                        Long catId = childVO.getCategoryId();
                        if (childVO.getTaxExclBaseTotalSum() != null) {
                            childCategoryTaxExclBaseMap.merge(catId, childVO.getTaxExclBaseTotalSum(), BigDecimal::add);
                        }
                        if (childVO.getTaxInclBaseTotalSum() != null) {
                            childCategoryTaxInclBaseMap.merge(catId, childVO.getTaxInclBaseTotalSum(), BigDecimal::add);
                        }
                        if (childVO.getTaxExclCompileTotalSum() != null) {
                            childCategoryTaxExclCompileMap.merge(catId, childVO.getTaxExclCompileTotalSum(), BigDecimal::add);
                        }
                        if (childVO.getTaxInclCompileTotalSum() != null) {
                            childCategoryTaxInclCompileMap.merge(catId, childVO.getTaxInclCompileTotalSum(), BigDecimal::add);
                        }
                        if (childVO.getTaxExclTotalSum() != null) {
                            childCategoryTaxExclMap.merge(catId, childVO.getTaxExclTotalSum(), BigDecimal::add);
                        }
                        if (childVO.getTaxInclTotalSum() != null) {
                            childCategoryTaxInclMap.merge(catId, childVO.getTaxInclTotalSum(), BigDecimal::add);
                        }
                    }
                }
                
                // 第二轮：计算%单位子工料机的合价（使用calcBase公式计算基数）
                for (WbBoqResourceRespVO percentChild : percentChildVOs) {
                    BigDecimal usageQty = percentChild.getUsageQty();
                    Map<String, Object> calcBase = percentChild.getCalcBase();
                    if (usageQty != null && calcBase != null && calcBase.containsKey("formula") && calcBase.containsKey("variables")) {
                        try {
                            String formula = (String) calcBase.get("formula");
                            @SuppressWarnings("unchecked")
                            Map<String, Object> variablesConfig = (Map<String, Object>) calcBase.get("variables");
                            
                            // 构建六组变量映射
                            Map<String, BigDecimal> taxExclBaseVars = new HashMap<>();
                            Map<String, BigDecimal> taxInclBaseVars = new HashMap<>();
                            Map<String, BigDecimal> taxExclCompileVars = new HashMap<>();
                            Map<String, BigDecimal> taxInclCompileVars = new HashMap<>();
                            Map<String, BigDecimal> taxExclVars = new HashMap<>();
                            Map<String, BigDecimal> taxInclVars = new HashMap<>();
                            
                            for (Map.Entry<String, Object> varEntry : variablesConfig.entrySet()) {
                                String varName = varEntry.getKey();
                                Long categoryId = Long.valueOf(varEntry.getValue().toString());
                                // 当公式某些条件不满足时按0处理
                                taxExclBaseVars.put(varName, childCategoryTaxExclBaseMap.getOrDefault(categoryId, BigDecimal.ZERO));
                                taxInclBaseVars.put(varName, childCategoryTaxInclBaseMap.getOrDefault(categoryId, BigDecimal.ZERO));
                                taxExclCompileVars.put(varName, childCategoryTaxExclCompileMap.getOrDefault(categoryId, BigDecimal.ZERO));
                                taxInclCompileVars.put(varName, childCategoryTaxInclCompileMap.getOrDefault(categoryId, BigDecimal.ZERO));
                                taxExclVars.put(varName, childCategoryTaxExclMap.getOrDefault(categoryId, BigDecimal.ZERO));
                                taxInclVars.put(varName, childCategoryTaxInclMap.getOrDefault(categoryId, BigDecimal.ZERO));
                            }
                            
                            // 计算六个基数值
                            BigDecimal taxExclBaseResult = FormulaEvaluator.evaluate(formula, taxExclBaseVars);
                            BigDecimal taxInclBaseResult = FormulaEvaluator.evaluate(formula, taxInclBaseVars);
                            BigDecimal taxExclCompileResult = FormulaEvaluator.evaluate(formula, taxExclCompileVars);
                            BigDecimal taxInclCompileResult = FormulaEvaluator.evaluate(formula, taxInclCompileVars);
                            BigDecimal taxExclResult = FormulaEvaluator.evaluate(formula, taxExclVars);
                            BigDecimal taxInclResult = FormulaEvaluator.evaluate(formula, taxInclVars);
                            
                            BigDecimal factor = usageQty.divide(new BigDecimal("100"), 6, java.math.RoundingMode.HALF_UP);
                            // 六个合价 = calcBase公式计算值 × 用量%
                            percentChild.setTaxExclBaseTotalSum(taxExclBaseResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                            percentChild.setTaxInclBaseTotalSum(taxInclBaseResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                            percentChild.setTaxExclCompileTotalSum(taxExclCompileResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                            percentChild.setTaxInclCompileTotalSum(taxInclCompileResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                            percentChild.setTaxExclTotalSum(taxExclResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                            percentChild.setTaxInclTotalSum(taxInclResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                        } catch (Exception e) {
                            log.error("[convertToVOList] 计算%子工料机合价失败，error={}", e.getMessage(), e);
                        }
                    }
                }
                
                vo.setChildren(childVOs);
                // 复合工料机：父级合价 = 子项合价之和
                calculateMergedParentTotalSums(vo, childVOs);
                // 复合工料机：计算单价（单价 = 合价 / 用量）
                calculateMergedParentPrices(vo, quotaQty);
            }
            result.add(vo);
        }
        return result;
    }
    
    /**
     * 将DO转换为VO（不计算合价，用于汇总场景）
     */
    private WbBoqResourceRespVO convertToVO(WbBoqResourceDO resource, Map<Long, ResourceItemDO> resourceItemMap) {
        return convertToVO(resource, resourceItemMap, null);
    }
    
    /**
     * 将DO转换为VO（使用预加载的 ResourceItemDO 映射）
     * @param quotaQty 定额工程量，用于计算合价（合价 = 用量 × 单价），为null时不计算合价
     */
    private WbBoqResourceRespVO convertToVO(WbBoqResourceDO resource, Map<Long, ResourceItemDO> resourceItemMap, BigDecimal quotaQty) {
        WbBoqResourceRespVO vo = new WbBoqResourceRespVO();
        vo.setId(resource.getId());
        vo.setDivisionId(resource.getDivisionId());
        vo.setSourceResourceItemId(resource.getSourceResourceItemId());
        vo.setSourceQuotaResourceId(resource.getSourceQuotaResourceId());
        vo.setResourceType(resource.getResourceType());
        vo.setCode(resource.getCode());
        vo.setName(resource.getName());
        vo.setSpec(resource.getSpec());
        vo.setUnit(resource.getUnit());
        vo.setConsumeQty(resource.getConsumeQty());
        vo.setAdjustConsumeQty(resource.getAdjustConsumeQty());
        vo.setAdjustRate(resource.getAdjustRate());
        
        // 单位为 % 的工料机：税率和价格字段强制为空
        if ("%".equals(resource.getUnit())) {
            vo.setTaxRate(null);
            vo.setTaxExclBasePrice(null);
            vo.setTaxInclBasePrice(null);
            vo.setTaxExclCompilePrice(null);
            vo.setTaxInclCompilePrice(null);
        } else {
            vo.setTaxRate(resource.getTaxRate());
            vo.setTaxExclBasePrice(resource.getTaxExclBasePrice());
            vo.setTaxInclBasePrice(resource.getTaxInclBasePrice());
            vo.setTaxExclCompilePrice(resource.getTaxExclCompilePrice());
            vo.setTaxInclCompilePrice(resource.getTaxInclCompilePrice());
        }
        vo.setCategoryId(resource.getCategoryId());
        vo.setSortOrder(resource.getSortOrder());
        // 用量计算逻辑：
        // 1. 单位为 % 的工料机：用量 = 消耗量（直接引用）
        // 2. 其他工料机：用量 = 定额工程量 × 消耗量
        BigDecimal consumeQtyForUsage = resource.getAdjustConsumeQty() != null ? resource.getAdjustConsumeQty() : resource.getConsumeQty();
        if ("%".equals(resource.getUnit())) {
            // 单位为%：用量直接等于消耗量
            vo.setUsageQty(consumeQtyForUsage);
        } else if (quotaQty != null && consumeQtyForUsage != null) {
            // 普通工料机：用量 = 定额工程量 × 消耗量
            vo.setUsageQty(quotaQty.multiply(consumeQtyForUsage).setScale(4, java.math.RoundingMode.HALF_UP));
        } else {
            vo.setUsageQty(resource.getUsageQty());
        }
        vo.setSourceType(resource.getSourceType());
        
        // 从预加载的映射中补充 calcBase 和 categoryId（避免逐个查询）
        if (resource.getSourceResourceItemId() != null) {
            ResourceItemDO resourceItem = resourceItemMap.get(resource.getSourceResourceItemId());
            if (resourceItem != null) {
                vo.setCalcBase(resourceItem.getCalcBase());
                // 兼容旧数据：如果 categoryId 为空，从源工料机获取
                if (vo.getCategoryId() == null && resourceItem.getCategoryId() != null) {
                    vo.setCategoryId(resourceItem.getCategoryId());
                }
            }
        }
        
        // 计算虚拟字段（合价）
        // 单位为 % 的工料机：合价需要根据 calcBase 计算，这里暂不计算（由前端或其他服务处理）
        if (!"%".equals(resource.getUnit())) {
            // 优先使用调整消耗量，否则使用原始消耗量
            BigDecimal consumeQty = resource.getAdjustConsumeQty() != null ? resource.getAdjustConsumeQty() : resource.getConsumeQty();
            if (consumeQty != null) {
                // 基价合价/编制合价 = 消耗量 × 单价（原有逻辑，保持不变）
                if (resource.getTaxExclBasePrice() != null) {
                    vo.setTaxExclBaseTotalSum(consumeQty.multiply(resource.getTaxExclBasePrice()));
                }
                if (resource.getTaxInclBasePrice() != null) {
                    vo.setTaxInclBaseTotalSum(consumeQty.multiply(resource.getTaxInclBasePrice()));
                }
                if (resource.getTaxExclCompilePrice() != null) {
                    vo.setTaxExclCompileTotalSum(consumeQty.multiply(resource.getTaxExclCompilePrice()));
                }
                if (resource.getTaxInclCompilePrice() != null) {
                    vo.setTaxInclCompileTotalSum(consumeQty.multiply(resource.getTaxInclCompilePrice()));
                }
                // 除税合价/含税合价 = 用量 × 单价（用量 = 定额工程量 × 消耗量）
                if (quotaQty != null) {
                    BigDecimal usageQty = quotaQty.multiply(consumeQty);
                    if (resource.getTaxExclCompilePrice() != null) {
                        vo.setTaxExclTotalSum(usageQty.multiply(resource.getTaxExclCompilePrice()));
                    }
                    if (resource.getTaxInclCompilePrice() != null) {
                        vo.setTaxInclTotalSum(usageQty.multiply(resource.getTaxInclCompilePrice()));
                    }
                }
            }
        }
        
        return vo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteByDivisionId(Long divisionId) {
        wbBoqResourceMapper.deleteByDivisionId(divisionId);
    }

    /**
     * 校验定额节点是否为定额类型
     */
    private void validateDivisionIsQuota(Long divisionId) {
        WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(divisionId);
        if (division == null || !WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(division.getNodeType())) {
            throw exception(WB_BOQ_RESOURCE_DIVISION_NOT_QUOTA);
        }
    }

    /**
     * 校验工料机存在
     */
    private WbBoqResourceDO validateResourceExists(Long id) {
        WbBoqResourceDO resource = wbBoqResourceMapper.selectById(id);
        if (resource == null) {
            throw exception(WB_BOQ_RESOURCE_NOT_EXISTS);
        }
        return resource;
    }

    /**
     * 计算复合工料机父级的六个合价（= 子项合价之和）
     * 包括：除税基价合价、含税基价合价、除税编制合价、含税编制合价、除税合价、含税合价
     */
    private void calculateMergedParentTotalSums(WbBoqResourceRespVO parent, List<WbBoqResourceRespVO> children) {
        if (children == null || children.isEmpty()) {
            return;
        }
        BigDecimal totalTaxExclBase = BigDecimal.ZERO;
        BigDecimal totalTaxInclBase = BigDecimal.ZERO;
        BigDecimal totalTaxExclCompile = BigDecimal.ZERO;
        BigDecimal totalTaxInclCompile = BigDecimal.ZERO;
        BigDecimal totalTaxExcl = BigDecimal.ZERO;
        BigDecimal totalTaxIncl = BigDecimal.ZERO;
        
        for (WbBoqResourceRespVO child : children) {
            if (child.getTaxExclBaseTotalSum() != null) totalTaxExclBase = totalTaxExclBase.add(child.getTaxExclBaseTotalSum());
            if (child.getTaxInclBaseTotalSum() != null) totalTaxInclBase = totalTaxInclBase.add(child.getTaxInclBaseTotalSum());
            if (child.getTaxExclCompileTotalSum() != null) totalTaxExclCompile = totalTaxExclCompile.add(child.getTaxExclCompileTotalSum());
            if (child.getTaxInclCompileTotalSum() != null) totalTaxInclCompile = totalTaxInclCompile.add(child.getTaxInclCompileTotalSum());
            if (child.getTaxExclTotalSum() != null) totalTaxExcl = totalTaxExcl.add(child.getTaxExclTotalSum());
            if (child.getTaxInclTotalSum() != null) totalTaxIncl = totalTaxIncl.add(child.getTaxInclTotalSum());
        }
        
        parent.setTaxExclBaseTotalSum(totalTaxExclBase.setScale(2, java.math.RoundingMode.HALF_UP));
        parent.setTaxInclBaseTotalSum(totalTaxInclBase.setScale(2, java.math.RoundingMode.HALF_UP));
        parent.setTaxExclCompileTotalSum(totalTaxExclCompile.setScale(2, java.math.RoundingMode.HALF_UP));
        parent.setTaxInclCompileTotalSum(totalTaxInclCompile.setScale(2, java.math.RoundingMode.HALF_UP));
        parent.setTaxExclTotalSum(totalTaxExcl.setScale(2, java.math.RoundingMode.HALF_UP));
        parent.setTaxInclTotalSum(totalTaxIncl.setScale(2, java.math.RoundingMode.HALF_UP));
    }
    
    /**
     * 计算复合工料机父级的单价和合价
     * - 单价 = 对应合价（除税基价=除税基价合价、含税基价=含税基价合价、除税编制价=除税编制合价、含税编制价=含税编制合价）
     * - 除税合价/含税合价 = 用量 × 单价（用量 = 定额工程量 × 消耗量）
     */
    private void calculateMergedParentPrices(WbBoqResourceRespVO parent, BigDecimal quotaQty) {
        // 获取消耗量（优先使用调整消耗量）
        BigDecimal consumeQty = parent.getAdjustConsumeQty() != null ? parent.getAdjustConsumeQty() : parent.getConsumeQty();
        if (consumeQty == null || consumeQty.compareTo(BigDecimal.ZERO) == 0) {
            return;
        }
        // 单价 = 对应合价
        if (parent.getTaxExclBaseTotalSum() != null) {
            parent.setTaxExclBasePrice(parent.getTaxExclBaseTotalSum().setScale(2, java.math.RoundingMode.HALF_UP));
        }
        if (parent.getTaxInclBaseTotalSum() != null) {
            parent.setTaxInclBasePrice(parent.getTaxInclBaseTotalSum().setScale(2, java.math.RoundingMode.HALF_UP));
        }
        if (parent.getTaxExclCompileTotalSum() != null) {
            parent.setTaxExclCompilePrice(parent.getTaxExclCompileTotalSum().setScale(2, java.math.RoundingMode.HALF_UP));
        }
        if (parent.getTaxInclCompileTotalSum() != null) {
            parent.setTaxInclCompilePrice(parent.getTaxInclCompileTotalSum().setScale(2, java.math.RoundingMode.HALF_UP));
        }
        // 除税合价/含税合价 = 用量 × 单价（用量 = 定额工程量 × 消耗量）
        if (quotaQty != null) {
            BigDecimal usageQty = quotaQty.multiply(consumeQty);
            if (parent.getTaxExclCompilePrice() != null) {
                parent.setTaxExclTotalSum(usageQty.multiply(parent.getTaxExclCompilePrice()));
            }
            if (parent.getTaxInclCompilePrice() != null) {
                parent.setTaxInclTotalSum(usageQty.multiply(parent.getTaxInclCompilePrice()));
            }
        }
    }
    
    /**
     * 计算顶层单位为%的工料机合价
     * 四个合价字段分别使用对应的分类合价来计算：
     * - 除税基价合价 = 计算基数（除税基价合价）× 调整消耗量%
     * - 含税基价合价 = 计算基数（含税基价合价）× 调整消耗量%
     * - 除税编制合价 = 计算基数（除税编制合价）× 调整消耗量%
     * - 含税编制合价 = 计算基数（含税编制合价）× 调整消耗量%
     * 注意：复合工料机内部的%子工料机已在 calculateMergedParentTotalSums 中处理
     */
    private void calculatePercentUnitTotalSums(List<WbBoqResourceRespVO> resources) {
        if (CollUtil.isEmpty(resources)) {
            return;
        }
        // 四个合价字段使用原来的分类合价（消耗量×单价）
        // 只处理顶层资源（复合工料机的子工料机已在convertToVOList中清空合价字段）
        ResourcePriceCalculator.calculatePercentUnitTotalSums(
                resources, resources, wbBoqResourceFieldAccessor());
        // 计算%工料机的除税合价和含税合价（使用用量×单价的分类合价）
        calculatePercentUnitUsageTotalSums(resources);
    }
    
    /**
     * 计算%工料机的四个合价字段
     * 使用四个合价（除税基价合价、含税基价合价、除税编制价合价、含税编制价合价）作为分类合价来源
     */
    private void calculatePercentUnitUsageTotalSums(List<WbBoqResourceRespVO> resources) {
        // 1. 构建类别价格映射表（存储四个合价：除税基价、含税基价、除税编制价、含税编制价）
        Map<Long, BigDecimal[]> categoryPriceMap = new HashMap<>();
        for (WbBoqResourceRespVO vo : resources) {
            if ("%".equals(vo.getUnit())) {
                continue;
            }
            Long categoryId = vo.getCategoryId();
            if (categoryId != null) {
                BigDecimal[] sums = categoryPriceMap.computeIfAbsent(categoryId, 
                        k -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO});
                // sums[0]=除税基价合价, sums[1]=含税基价合价, sums[2]=除税编制价合价, sums[3]=含税编制价合价
                if (vo.getTaxExclBaseTotalSum() != null) sums[0] = sums[0].add(vo.getTaxExclBaseTotalSum());
                if (vo.getTaxInclBaseTotalSum() != null) sums[1] = sums[1].add(vo.getTaxInclBaseTotalSum());
                if (vo.getTaxExclTotalSum() != null) sums[2] = sums[2].add(vo.getTaxExclTotalSum());
                if (vo.getTaxInclTotalSum() != null) sums[3] = sums[3].add(vo.getTaxInclTotalSum());
            }
        }
        
        // 2. 计算%工料机的四个合价字段（除税定额合价、含税定额合价、除税合价、含税合价）
        // 公式：合价 = 计算基数公式得出的值 × 用量%
        // 当公式某些条件不满足时按0处理（例如"人+机"，没有类别为人时就当"0+机"）
        for (WbBoqResourceRespVO vo : resources) {
            if (!"%".equals(vo.getUnit())) {
                continue;
            }
            Map<String, Object> calcBase = vo.getCalcBase();
            if (calcBase == null || !calcBase.containsKey("formula") || !calcBase.containsKey("variables")) {
                continue;
            }
            try {
                String formula = (String) calcBase.get("formula");
                @SuppressWarnings("unchecked")
                Map<String, Object> variablesConfig = (Map<String, Object>) calcBase.get("variables");
                
                // 构建四组变量映射（除税基价、含税基价、除税编制价、含税编制价）
                Map<String, BigDecimal> taxExclBaseVars = new HashMap<>();
                Map<String, BigDecimal> taxInclBaseVars = new HashMap<>();
                Map<String, BigDecimal> taxExclCompileVars = new HashMap<>();
                Map<String, BigDecimal> taxInclCompileVars = new HashMap<>();
                
                for (Map.Entry<String, Object> varEntry : variablesConfig.entrySet()) {
                    String varName = varEntry.getKey();
                    Long categoryId = Long.valueOf(varEntry.getValue().toString());
                    BigDecimal[] sums = categoryPriceMap.get(categoryId);
                    // 当公式某些条件不满足时按0处理
                    if (sums == null) {
                        taxExclBaseVars.put(varName, BigDecimal.ZERO);
                        taxInclBaseVars.put(varName, BigDecimal.ZERO);
                        taxExclCompileVars.put(varName, BigDecimal.ZERO);
                        taxInclCompileVars.put(varName, BigDecimal.ZERO);
                    } else {
                        // sums[0]=除税基价合价, sums[1]=含税基价合价, sums[2]=除税编制价合价, sums[3]=含税编制价合价
                        taxExclBaseVars.put(varName, sums[0] != null ? sums[0] : BigDecimal.ZERO);
                        taxInclBaseVars.put(varName, sums[1] != null ? sums[1] : BigDecimal.ZERO);
                        taxExclCompileVars.put(varName, sums.length > 2 && sums[2] != null ? sums[2] : BigDecimal.ZERO);
                        taxInclCompileVars.put(varName, sums.length > 3 && sums[3] != null ? sums[3] : BigDecimal.ZERO);
                    }
                }
                
                // 计算四个基数值
                BigDecimal taxExclBaseResult = FormulaEvaluator.evaluate(formula, taxExclBaseVars);
                BigDecimal taxInclBaseResult = FormulaEvaluator.evaluate(formula, taxInclBaseVars);
                BigDecimal taxExclCompileResult = FormulaEvaluator.evaluate(formula, taxExclCompileVars);
                BigDecimal taxInclCompileResult = FormulaEvaluator.evaluate(formula, taxInclCompileVars);
                
                // 用量% = 消耗量 / 100
                BigDecimal dosage = vo.getUsageQty(); // 用量已经等于消耗量
                if (dosage != null) {
                    BigDecimal factor = dosage.divide(new BigDecimal("100"), 6, java.math.RoundingMode.HALF_UP);
                    // 四个合价 = 计算基数 × 用量%
                    vo.setTaxExclBaseTotalSum(taxExclBaseResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                    vo.setTaxInclBaseTotalSum(taxInclBaseResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                    vo.setTaxExclTotalSum(taxExclCompileResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                    vo.setTaxInclTotalSum(taxInclCompileResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP));
                }
            } catch (Exception e) {
                log.error("[calculatePercentUnitUsageTotalSums] 计算%工料机合价失败，error={}", e.getMessage(), e);
            }
        }
    }
    
    /**
     * 构建工作台工料机的 FieldAccessor
     * 映射 WbBoqResourceRespVO 的字段名到 ResourcePriceCalculator 的通用接口
     */
    private ResourcePriceCalculator.FieldAccessor<WbBoqResourceRespVO> wbBoqResourceFieldAccessor() {
        return ResourcePriceCalculator.FieldAccessor.<WbBoqResourceRespVO>builder()
                .getUnit(WbBoqResourceRespVO::getUnit)
                .getCategoryId(WbBoqResourceRespVO::getCategoryId)
                .getEffectiveDosage(vo -> vo.getAdjustConsumeQty() != null ? vo.getAdjustConsumeQty() : vo.getConsumeQty())
                .getCalcBase(WbBoqResourceRespVO::getCalcBase)
                .getTaxExclBaseTotalSum(WbBoqResourceRespVO::getTaxExclBaseTotalSum)
                .getTaxInclBaseTotalSum(WbBoqResourceRespVO::getTaxInclBaseTotalSum)
                .getTaxExclCompileTotalSum(WbBoqResourceRespVO::getTaxExclCompileTotalSum)
                .getTaxInclCompileTotalSum(WbBoqResourceRespVO::getTaxInclCompileTotalSum)
                .setTaxExclBaseTotalSum(WbBoqResourceRespVO::setTaxExclBaseTotalSum)
                .setTaxInclBaseTotalSum(WbBoqResourceRespVO::setTaxInclBaseTotalSum)
                .setTaxExclCompileTotalSum(WbBoqResourceRespVO::setTaxExclCompileTotalSum)
                .setTaxInclCompileTotalSum(WbBoqResourceRespVO::setTaxInclCompileTotalSum)
                .build();
    }
    
    /**
     * 计算调整公式（虚拟字段）
     * 根据工作台的调整设置计算每个工料机的调整公式
     * 使用公共工具类 AdjustmentFormulaCalculator 保持与后台一致的计算逻辑
     */
    private void calculateAdjustmentFormulas(Long divisionId, List<WbBoqResourceRespVO> resources) {
        // 1. 获取工作台的调整设置
        List<WbAdjustmentSettingDO> settings = wbAdjustmentSettingMapper.selectListByDivisionId(divisionId);
        if (CollUtil.isEmpty(settings)) {
            return;
        }
        
        // 2. 展开所有工料机（包括子工料机）
        List<WbBoqResourceRespVO> allResources = new ArrayList<>();
        for (WbBoqResourceRespVO vo : resources) {
            allResources.add(vo);
            if (vo.getChildren() != null && !vo.getChildren().isEmpty()) {
                allResources.addAll(vo.getChildren());
            }
        }
        
        // 3. 使用公共工具类计算调整公式
        AdjustmentFormulaCalculator.calculateFormulas(
                allResources,
                settings,
                WbBoqResourceRespVO::getCode,
                WbBoqResourceRespVO::getCategoryId,
                WbBoqResourceRespVO::setAdjustmentFormula
        );
    }

    /**
     * 填充价格来源状态（hasPriceSource）并虚拟替换价格
     * 查询工料机是否有当前价格来源（is_current=1），如果有则用信息价的价格虚拟替换
     * 注意：价格来源绑定到项目级别（projectId + resourceItemId），影响项目下所有使用该工料机的定额
     * @param resources 工料机列表
     * @param division 分部分项节点（已加载，避免重复查询）
     */
    private void fillPriceSourceStatus(List<WbBoqResourceRespVO> resources, WbBoqDivisionDO division) {
        if (CollUtil.isEmpty(resources)) {
            return;
        }
        
        // 从已加载的 division 获取 projectId（避免重复查询）
        Long projectId = null;
        if (division != null && division.getCompileTreeId() != null) {
            com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO compileTree = 
                    wbCompileTreeMapper.selectById(division.getCompileTreeId());
            if (compileTree != null) {
                projectId = compileTree.getProjectId();
            }
        }
        
        // 收集所有工料机的sourceResourceItemId（包括子工料机）
        List<Long> resourceItemIds = new ArrayList<>();
        for (WbBoqResourceRespVO vo : resources) {
            if (vo.getSourceResourceItemId() != null) {
                resourceItemIds.add(vo.getSourceResourceItemId());
            }
            if (vo.getChildren() != null) {
                for (WbBoqResourceRespVO child : vo.getChildren()) {
                    if (child.getSourceResourceItemId() != null) {
                        resourceItemIds.add(child.getSourceResourceItemId());
                    }
                }
            }
        }
        
        if (resourceItemIds.isEmpty()) {
            return;
        }
        
        // 批量查询当前价格来源的详细信息（按项目ID和工料机ID）
        Map<Long, com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO> currentPriceMappings;
        if (projectId != null) {
            currentPriceMappings = resourceInfoPriceMappingMapper.selectCurrentPriceMappingsByProject(projectId, resourceItemIds);
        } else {
            // 兼容旧逻辑（租户级别）
            currentPriceMappings = resourceInfoPriceMappingMapper.selectCurrentPriceMappingsByResourceItemIds(resourceItemIds);
        }
        
        // 收集所有需要查询的价格历史ID
        List<Long> priceIds = currentPriceMappings.values().stream()
                .map(com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO::getSelectedPriceId)
                .filter(java.util.Objects::nonNull)
                .distinct()
                .collect(java.util.stream.Collectors.toList());
        
        // 批量查询价格历史详情
        Map<Long, com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO> priceMap = new java.util.HashMap<>();
        if (!priceIds.isEmpty()) {
            List<com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO> prices = 
                    infoPriceResourcePriceMapper.selectListByIds(priceIds);
            priceMap = prices.stream()
                    .collect(java.util.stream.Collectors.toMap(
                            com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO::getId,
                            p -> p,
                            (existing, replacement) -> existing));
        }
        
        // 收集所有信息价工料机ID，用于生成价格来源显示文本
        List<Long> infoPriceResourceIds = currentPriceMappings.values().stream()
                .map(com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO::getInfoPriceResourceId)
                .filter(java.util.Objects::nonNull)
                .distinct()
                .collect(java.util.stream.Collectors.toList());
        
        // 批量查询信息价工料机详情，用于获取 categoryTreeId
        Map<Long, com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO> infoPriceResourceMap = new java.util.HashMap<>();
        if (!infoPriceResourceIds.isEmpty()) {
            List<com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO> infoPriceResources = 
                    infoPriceResourceMapper.selectListByIds(infoPriceResourceIds);
            infoPriceResourceMap = infoPriceResources.stream()
                    .collect(java.util.stream.Collectors.toMap(
                            com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO::getId,
                            r -> r,
                            (existing, replacement) -> existing));
        }
        
        // 获取定额数量（用于计算合价，从已加载的 division 获取，避免重复查询）
        java.math.BigDecimal quotaQty = java.math.BigDecimal.ONE;
        if (division != null && division.getQty() != null && division.getQty().compareTo(java.math.BigDecimal.ZERO) > 0) {
            quotaQty = division.getQty();
        }
        
        // 填充 hasPriceSource 字段并虚拟替换价格
        for (WbBoqResourceRespVO vo : resources) {
            applyPriceSourceToResource(vo, currentPriceMappings, priceMap, infoPriceResourceMap, quotaQty);
            if (vo.getChildren() != null) {
                for (WbBoqResourceRespVO child : vo.getChildren()) {
                    applyPriceSourceToResource(child, currentPriceMappings, priceMap, infoPriceResourceMap, quotaQty);
                }
            }
        }
    }
    
    /**
     * 应用价格来源到工料机（虚拟替换价格并生成显示文本）
     * 注意：使用 sourceResourceItemId 来查找映射（项目级别的价格来源）
     * @param quotaQty 定额数量，用于计算合价（用量 = 定额数量 × 消耗量）
     */
    private void applyPriceSourceToResource(
            WbBoqResourceRespVO vo,
            Map<Long, com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO> currentPriceMappings,
            Map<Long, com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO> priceMap,
            Map<Long, com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO> infoPriceResourceMap,
            java.math.BigDecimal quotaQty) {
        
        if (vo.getSourceResourceItemId() == null) {
            return;
        }
        
        // 使用 sourceResourceItemId 来查找映射（项目级别）
        com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO mapping = 
                currentPriceMappings.get(vo.getSourceResourceItemId());
        
        if (mapping != null) {
            vo.setHasPriceSource(true);
            
            // 如果有选中的价格历史，则用信息价的价格虚拟替换
            if (mapping.getSelectedPriceId() != null) {
                com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO price = 
                        priceMap.get(mapping.getSelectedPriceId());
                if (price != null) {
                    // 获取公式
                    String formula = mapping.getFormula();
                    
                    // 虚拟替换税率
                    if (price.getTaxRate() != null) {
                        vo.setTaxRate(price.getTaxRate());
                    }
                    
                    // 计算除税编制价（应用公式）
                    java.math.BigDecimal taxExclPrice = price.getPriceTaxExcl();
                    if (taxExclPrice != null) {
                        taxExclPrice = applyFormula(taxExclPrice, formula);
                        vo.setTaxExclCompilePrice(taxExclPrice);
                    }
                    
                    // 计算含税编制价（应用公式）
                    java.math.BigDecimal taxInclPrice = price.getPriceTaxIncl();
                    if (taxInclPrice != null) {
                        taxInclPrice = applyFormula(taxInclPrice, formula);
                        vo.setTaxInclCompilePrice(taxInclPrice);
                    }
                    
                    // 计算用量 = 定额数量 × 消耗量
                    java.math.BigDecimal consumeQty = vo.getConsumeQty() != null ? vo.getConsumeQty() : java.math.BigDecimal.ZERO;
                    java.math.BigDecimal usageQty = quotaQty.multiply(consumeQty);
                    
                    // 重新计算编制价合价（用量 × 编制价）
                    if (taxExclPrice != null) {
                        vo.setTaxExclCompileTotalSum(usageQty.multiply(taxExclPrice).setScale(2, java.math.RoundingMode.HALF_UP));
                    }
                    if (taxInclPrice != null) {
                        vo.setTaxInclCompileTotalSum(usageQty.multiply(taxInclPrice).setScale(2, java.math.RoundingMode.HALF_UP));
                    }
                    
                    // 重新计算基价合价（用量 × 基价，使用新税率）
                    java.math.BigDecimal taxExclBasePrice = vo.getTaxExclBasePrice();
                    java.math.BigDecimal taxInclBasePrice = vo.getTaxInclBasePrice();
                    if (taxExclBasePrice != null) {
                        vo.setTaxExclBaseTotalSum(usageQty.multiply(taxExclBasePrice).setScale(2, java.math.RoundingMode.HALF_UP));
                    }
                    if (taxInclBasePrice != null) {
                        vo.setTaxInclBaseTotalSum(usageQty.multiply(taxInclBasePrice).setScale(2, java.math.RoundingMode.HALF_UP));
                    }
                }
            }
            
            // 生成价格来源显示文本：信*01*广东*珠海*金湾
            String priceSourceText = buildPriceSourceText(mapping.getInfoPriceResourceId(), infoPriceResourceMap);
            vo.setPriceSourceText(priceSourceText);
        } else {
            vo.setHasPriceSource(false);
        }
    }
    
    /**
     * 构建价格来源显示文本
     * 格式：信*01*广东*珠海*金湾
     * - "信" 代表信息价
     * - "01" 代表信息价专业的序号
     * - "广东*珠海*金湾" 代表地区路径
     */
    private String buildPriceSourceText(Long infoPriceResourceId, 
            Map<Long, com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO> infoPriceResourceMap) {
        if (infoPriceResourceId == null) {
            return "";
        }
        
        com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO infoPriceResource = 
                infoPriceResourceMap.get(infoPriceResourceId);
        if (infoPriceResource == null || infoPriceResource.getCategoryTreeId() == null) {
            return "信";
        }
        
        try {
            // 获取分类树节点
            com.yhy.module.core.dal.dataobject.infoprice.InfoPriceCategoryTreeDO categoryTree = 
                    infoPriceCategoryTreeMapper.selectById(infoPriceResource.getCategoryTreeId());
            if (categoryTree == null || categoryTree.getBookId() == null) {
                return "信";
            }
            
            // 获取信息价册
            com.yhy.module.core.dal.dataobject.infoprice.InfoPriceBookDO book = 
                    infoPriceBookMapper.selectById(categoryTree.getBookId());
            if (book == null || book.getTreeNodeId() == null) {
                return "信";
            }
            
            // 获取树节点（包含专业类型和地区路径）
            com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO treeNode = 
                    infoPriceTreeMapper.selectById(book.getTreeNodeId());
            if (treeNode == null) {
                return "信";
            }
            
            StringBuilder sb = new StringBuilder("信");
            
            // 添加专业序号
            String professionIndex = getProfessionIndex(treeNode.getEnumType());
            sb.append("*").append(professionIndex);
            
            // 添加地区路径
            String regionPath = buildRegionPath(treeNode);
            if (!regionPath.isEmpty()) {
                sb.append("*").append(regionPath);
            }
            
            return sb.toString();
        } catch (Exception e) {
            log.warn("构建价格来源显示文本失败: {}", e.getMessage());
            return "信";
        }
    }
    
    /**
     * 获取专业序号（01, 02, 03）
     */
    private String getProfessionIndex(String enumType) {
        if (enumType == null) {
            return "01";
        }
        switch (enumType) {
            case "region_a":
                return "01";
            case "region_b":
                return "02";
            case "region_c":
                return "03";
            default:
                return "01";
        }
    }
    
    /**
     * 构建地区路径（广东*珠海*金湾）
     */
    private String buildRegionPath(com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO treeNode) {
        if (treeNode == null) {
            return "";
        }
        
        StringBuilder pathBuilder = new StringBuilder();
        String[] pathIds = treeNode.getPath();
        
        // 遍历路径ID获取名称
        if (pathIds != null && pathIds.length > 0) {
            for (String pathId : pathIds) {
                try {
                    Long id = Long.parseLong(pathId);
                    com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO pathNode = 
                            infoPriceTreeMapper.selectById(id);
                    if (pathNode != null && pathNode.getName() != null && !pathNode.getName().isEmpty()) {
                        if (pathBuilder.length() > 0) {
                            pathBuilder.append("*");
                        }
                        pathBuilder.append(pathNode.getName());
                    }
                } catch (NumberFormatException e) {
                    // 忽略无效的ID
                }
            }
        }
        
        // 添加当前节点名称
        if (treeNode.getName() != null && !treeNode.getName().isEmpty()) {
            if (pathBuilder.length() > 0) {
                pathBuilder.append("*");
            }
            pathBuilder.append(treeNode.getName());
        }
        
        return pathBuilder.toString();
    }
    
    /**
     * 应用公式计算价格
     * 公式格式：xxj, xxj+20, xxj-10, xxj*1.1, (xxj+10)*0.9 等
     * xxj 代表信息价原价
     */
    private java.math.BigDecimal applyFormula(java.math.BigDecimal originalPrice, String formula) {
        if (originalPrice == null) {
            return null;
        }
        if (formula == null || formula.trim().isEmpty()) {
            return originalPrice;
        }
        
        try {
            // 将 xxj 替换为实际价格值
            String expression = formula.trim().replace("xxj", originalPrice.toPlainString());
            
            // 使用 JavaScript 引擎计算表达式
            javax.script.ScriptEngineManager manager = new javax.script.ScriptEngineManager();
            javax.script.ScriptEngine engine = manager.getEngineByName("JavaScript");
            if (engine == null) {
                // 如果没有 JavaScript 引擎，尝试使用 Nashorn
                engine = manager.getEngineByName("nashorn");
            }
            if (engine == null) {
                // 如果仍然没有引擎，返回原价
                return originalPrice;
            }
            
            Object result = engine.eval(expression);
            if (result instanceof Number) {
                return new java.math.BigDecimal(result.toString()).setScale(2, java.math.RoundingMode.HALF_UP);
            }
            return originalPrice;
        } catch (Exception e) {
            // 公式计算失败，返回原价
            return originalPrice;
        }
    }

    @Override
    public List<WbBoqResourceCategorySummaryVO> getCategorySummary(Long divisionId) {
        List<WbBoqResourceRespVO> resources = getListByDivisionId(divisionId);
        if (CollUtil.isEmpty(resources)) {
            return new ArrayList<>();
        }

        Map<Long, ResourceCategoryDO> categoryMap = loadCategoryMap(resources);
        Map<Long, WbBoqResourceCategorySummaryVO> summaryMap = new HashMap<>();

        for (WbBoqResourceRespVO resource : resources) {
            Long categoryId = resource.getCategoryId();
            WbBoqResourceCategorySummaryVO summary = summaryMap.computeIfAbsent(categoryId, key -> {
                WbBoqResourceCategorySummaryVO vo = new WbBoqResourceCategorySummaryVO();
                ResourceCategoryDO category = categoryMap.get(key);
                if (category != null) {
                    vo.setCategory(category.getName() != null ? category.getName() : category.getCode());
                } else if (resource.getCategoryCode() != null) {
                    vo.setCategory(resource.getCategoryCode());
                } else {
                    vo.setCategory("未分类");
                }
                vo.setTotalBasePriceExTax(BigDecimal.ZERO);
                vo.setTotalBasePriceInTax(BigDecimal.ZERO);
                vo.setTotalCompilePriceExTax(BigDecimal.ZERO);
                vo.setTotalCompilePriceInTax(BigDecimal.ZERO);
                return vo;
            });

            summary.setTotalBasePriceExTax(add(summary.getTotalBasePriceExTax(), resource.getTaxExclBaseTotalSum()));
            summary.setTotalBasePriceInTax(add(summary.getTotalBasePriceInTax(), resource.getTaxInclBaseTotalSum()));
            summary.setTotalCompilePriceExTax(add(summary.getTotalCompilePriceExTax(), resource.getTaxExclCompileTotalSum()));
            summary.setTotalCompilePriceInTax(add(summary.getTotalCompilePriceInTax(), resource.getTaxInclCompileTotalSum()));
        }

        List<Map.Entry<Long, WbBoqResourceCategorySummaryVO>> entries = new ArrayList<>(summaryMap.entrySet());
        entries.sort(Comparator.comparingInt(entry -> {
            ResourceCategoryDO category = categoryMap.get(entry.getKey());
            return category != null && category.getSortOrder() != null ? category.getSortOrder() : Integer.MAX_VALUE;
        }));

        List<WbBoqResourceCategorySummaryVO> result = new ArrayList<>(entries.size());
        for (int i = 0; i < entries.size(); i++) {
            WbBoqResourceCategorySummaryVO summary = entries.get(i).getValue();
            summary.setIndex(i + 1);
            summary.setTotalBasePriceExTax(roundToScale(summary.getTotalBasePriceExTax(), 2));
            summary.setTotalBasePriceInTax(roundToScale(summary.getTotalBasePriceInTax(), 2));
            summary.setTotalCompilePriceExTax(roundToScale(summary.getTotalCompilePriceExTax(), 2));
            summary.setTotalCompilePriceInTax(roundToScale(summary.getTotalCompilePriceInTax(), 2));
            result.add(summary);
        }
        return result;
    }

    private Map<Long, ResourceCategoryDO> loadCategoryMap(List<WbBoqResourceRespVO> resources) {
        List<Long> categoryIds = resources.stream()
                .map(WbBoqResourceRespVO::getCategoryId)
                .filter(java.util.Objects::nonNull)
                .distinct()
                .collect(java.util.stream.Collectors.toList());
        if (categoryIds.isEmpty()) {
            return new HashMap<>();
        }
        return resourceCategoryMapper.selectList("id", categoryIds).stream()
                .collect(java.util.stream.Collectors.toMap(ResourceCategoryDO::getId, category -> category, (a, b) -> a));
    }

    private BigDecimal add(BigDecimal left, BigDecimal right) {
        return (left != null ? left : BigDecimal.ZERO).add(right != null ? right : BigDecimal.ZERO);
    }

    @Override
    public List<WbBoqResourceRespVO> getListByBoqDivisionId(Long boqDivisionId) {
        // 1. 验证是清单节点
        WbBoqDivisionDO boqDivision = wbBoqDivisionMapper.selectById(boqDivisionId);
        if (boqDivision == null || !WbBoqDivisionDO.NODE_TYPE_BOQ.equals(boqDivision.getNodeType())) {
            return java.util.Collections.emptyList();
        }
        
        // 2. 获取清单下所有定额节点
        List<WbBoqDivisionDO> quotaDivisions = wbBoqDivisionMapper.selectListByParentId(boqDivisionId);
        List<Long> quotaDivisionIds = quotaDivisions.stream()
                .filter(d -> WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(d.getNodeType()))
                .map(WbBoqDivisionDO::getId)
                .collect(java.util.stream.Collectors.toList());
        
        if (quotaDivisionIds.isEmpty()) {
            return java.util.Collections.emptyList();
        }
        
        // 3. 批量查询所有定额的工料机（包含子工料机）
        List<WbBoqResourceDO> allResources = new ArrayList<>();
        for (Long quotaDivisionId : quotaDivisionIds) {
            List<WbBoqResourceDO> resources = wbBoqResourceMapper.selectListByDivisionId(quotaDivisionId);
            allResources.addAll(resources);
        }
        
        if (allResources.isEmpty()) {
            return java.util.Collections.emptyList();
        }
        
        // 4. 分离父工料机和子工料机
        List<WbBoqResourceDO> parentResources = new ArrayList<>();
        Map<Long, List<WbBoqResourceDO>> childrenMap = new java.util.HashMap<>();
        
        for (WbBoqResourceDO resource : allResources) {
            if (resource.getParentId() == null) {
                parentResources.add(resource);
            } else {
                childrenMap.computeIfAbsent(resource.getParentId(), k -> new ArrayList<>()).add(resource);
            }
        }
        
        // 4.1 批量预加载所有需要的 ResourceItemDO（避免 convertToVO 中逐个查询）
        List<Long> sourceResourceItemIds = allResources.stream()
                .map(WbBoqResourceDO::getSourceResourceItemId)
                .filter(java.util.Objects::nonNull)
                .distinct()
                .collect(java.util.stream.Collectors.toList());
        Map<Long, ResourceItemDO> resourceItemMap = new java.util.HashMap<>();
        if (!sourceResourceItemIds.isEmpty()) {
            List<ResourceItemDO> resourceItems = resourceItemMapper.selectBatchIds(sourceResourceItemIds);
            resourceItemMap = resourceItems.stream()
                    .collect(java.util.stream.Collectors.toMap(ResourceItemDO::getId, r -> r, (a, b) -> a));
        }
        
        // 5. 按 sourceResourceItemId 汇总父工料机（相同工料机合并消耗量）
        java.util.Map<Long, WbBoqResourceRespVO> summaryMap = new java.util.LinkedHashMap<>();
        // 记录每个 sourceResourceItemId 对应的子工料机
        java.util.Map<Long, List<WbBoqResourceRespVO>> childrenSummaryMap = new java.util.HashMap<>();
        int sortOrder = 1;
        
        for (WbBoqResourceDO resource : parentResources) {
            Long sourceId = resource.getSourceResourceItemId();
            WbBoqResourceRespVO vo;
            
            if (sourceId == null) {
                // 没有 sourceResourceItemId 的工料机单独显示
                vo = convertToVO(resource, resourceItemMap);
                vo.setSortOrder(sortOrder++);
                summaryMap.put(resource.getId(), vo);
            } else if (summaryMap.containsKey(sourceId)) {
                // 已存在，累加消耗量
                WbBoqResourceRespVO existing = summaryMap.get(sourceId);
                if (resource.getConsumeQty() != null) {
                    java.math.BigDecimal newConsumeQty = existing.getConsumeQty() != null 
                            ? existing.getConsumeQty().add(resource.getConsumeQty())
                            : resource.getConsumeQty();
                    existing.setConsumeQty(newConsumeQty);
                }
                vo = existing;
            } else {
                // 新工料机
                vo = convertToVO(resource, resourceItemMap);
                vo.setSortOrder(sortOrder++);
                summaryMap.put(sourceId, vo);
            }
            
            // 处理子工料机
            List<WbBoqResourceDO> children = childrenMap.get(resource.getId());
            if (children != null && !children.isEmpty()) {
                Long key = sourceId != null ? sourceId : resource.getId();
                List<WbBoqResourceRespVO> existingChildren = childrenSummaryMap.computeIfAbsent(key, k -> new ArrayList<>());
                
                for (WbBoqResourceDO child : children) {
                    Long childSourceId = child.getSourceResourceItemId();
                    // 查找是否已有相同 sourceResourceItemId 的子工料机
                    WbBoqResourceRespVO existingChild = null;
                    if (childSourceId != null) {
                        for (WbBoqResourceRespVO c : existingChildren) {
                            if (childSourceId.equals(c.getSourceResourceItemId())) {
                                existingChild = c;
                                break;
                            }
                        }
                    }
                    
                    if (existingChild != null) {
                        // 累加消耗量
                        if (child.getConsumeQty() != null) {
                            java.math.BigDecimal newConsumeQty = existingChild.getConsumeQty() != null 
                                    ? existingChild.getConsumeQty().add(child.getConsumeQty())
                                    : child.getConsumeQty();
                            existingChild.setConsumeQty(newConsumeQty);
                        }
                    } else {
                        // 新子工料机
                        WbBoqResourceRespVO childVo = convertToVO(child, resourceItemMap);
                        existingChildren.add(childVo);
                    }
                }
            }
        }
        
        // 6. 将子工料机添加到父工料机，并计算复合工料机的合价
        for (Map.Entry<Long, WbBoqResourceRespVO> entry : summaryMap.entrySet()) {
            Long key = entry.getKey();
            WbBoqResourceRespVO parent = entry.getValue();
            List<WbBoqResourceRespVO> children = childrenSummaryMap.get(key);
            if (children != null && !children.isEmpty()) {
                parent.setChildren(children);
                // 计算复合工料机的合价（子工料机合价之和）
                calculateMergedParentTotalSums(parent, children);
            }
        }
        
        // 7. 计算普通工料机的合价（消耗量 × 单价）
        List<WbBoqResourceRespVO> result = new ArrayList<>(summaryMap.values());
        for (WbBoqResourceRespVO vo : result) {
            // 跳过已计算合价的复合工料机
            if (vo.getChildren() != null && !vo.getChildren().isEmpty()) {
                continue;
            }
            // 计算普通工料机的合价
            BigDecimal consumeQty = vo.getConsumeQty();
            if (consumeQty != null) {
                if (vo.getTaxExclBasePrice() != null) {
                    vo.setTaxExclBaseTotalSum(consumeQty.multiply(vo.getTaxExclBasePrice()));
                }
                if (vo.getTaxInclBasePrice() != null) {
                    vo.setTaxInclBaseTotalSum(consumeQty.multiply(vo.getTaxInclBasePrice()));
                }
                if (vo.getTaxExclCompilePrice() != null) {
                    vo.setTaxExclCompileTotalSum(consumeQty.multiply(vo.getTaxExclCompilePrice()));
                }
                if (vo.getTaxInclCompilePrice() != null) {
                    vo.setTaxInclCompileTotalSum(consumeQty.multiply(vo.getTaxInclCompilePrice()));
                }
            }
        }
        
        return result;
    }

    @Override
    public List<WbBoqResourceRespVO> getResourcesByDivisionIds(List<Long> divisionIds) {
        if (CollUtil.isEmpty(divisionIds)) {
            return new ArrayList<>();
        }
        
        // 查询所有定额节点下的工料机
        List<WbBoqResourceDO> resources = wbBoqResourceMapper.selectListByDivisionIds(divisionIds);
        if (CollUtil.isEmpty(resources)) {
            return new ArrayList<>();
        }
        
        // 转换为VO
        List<WbBoqResourceRespVO> result = new ArrayList<>(resources.size());
        for (WbBoqResourceDO resource : resources) {
            result.add(WbBoqResourceConvert.INSTANCE.convert(resource));
        }
        return result;
    }

    // ========== 新增工料机功能 ==========

    @Override
    public List<com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO> searchByCode(Long divisionId, String code) {
        // 1. 校验定额节点
        WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(divisionId);
        if (division == null || !WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(division.getNodeType())) {
            throw exception(WB_BOQ_RESOURCE_DIVISION_NOT_QUOTA);
        }

        List<com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO> result = new ArrayList<>();

        // 2. 查询"本项目"范围：当前单位工程下所有定额的工料机
        List<WbBoqResourceDO> projectResources = wbBoqResourceMapper
                .selectByCompileTreeIdAndCodeLike(division.getCompileTreeId(), code);
        // 用于去重（按 name+spec+unit）
        java.util.Set<String> seenKeys = new java.util.HashSet<>();
        for (WbBoqResourceDO res : projectResources) {
            String key = (res.getName() == null ? "" : res.getName()) + "|"
                    + (res.getSpec() == null ? "" : res.getSpec()) + "|"
                    + (res.getUnit() == null ? "" : res.getUnit());
            if (seenKeys.add(key)) {
                com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO vo =
                        new com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO();
                vo.setId(res.getSourceResourceItemId());
                vo.setCode(res.getCode());
                vo.setName(res.getName());
                vo.setSpec(res.getSpec());
                vo.setUnit(res.getUnit());
                vo.setSource("project");
                vo.setResourceType(res.getResourceType());
                vo.setTaxRate(res.getTaxRate());
                vo.setTaxExclBasePrice(res.getTaxExclBasePrice());
                vo.setTaxInclBasePrice(res.getTaxInclBasePrice());
                vo.setTaxExclCompilePrice(res.getTaxExclCompilePrice());
                vo.setTaxInclCompilePrice(res.getTaxInclCompilePrice());
                vo.setCategoryId(res.getCategoryId());
                vo.setSourceResourceItemId(res.getSourceResourceItemId());
                result.add(vo);
            }
        }

        // 3. 查询"非项目"范围：后台标准库工料机
        Long quotaItemId = division.getSourceQuotaItemId();
        if (quotaItemId != null) {
            Long categoryTreeId = quotaItemService.getCategoryTreeIdByQuotaItem(quotaItemId);
            if (categoryTreeId != null) {
                List<ResourceItemDO> standardResources = resourceItemMapper
                        .selectByCategoryTreeIdWithFilter(categoryTreeId, code, null, null);
                for (ResourceItemDO item : standardResources) {
                    String key = (item.getName() == null ? "" : item.getName()) + "|"
                            + (item.getSpec() == null ? "" : item.getSpec()) + "|"
                            + (item.getUnit() == null ? "" : item.getUnit());
                    if (seenKeys.add(key)) {
                        com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO vo =
                                new com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO();
                        vo.setId(item.getId());
                        vo.setCode(item.getCode());
                        vo.setName(item.getName());
                        vo.setSpec(item.getSpec());
                        vo.setUnit(item.getUnit());
                        vo.setSource("standard");
                        vo.setResourceType(item.getType());
                        vo.setTaxRate(item.getTaxRate());
                        vo.setTaxExclBasePrice(item.getTaxExclBasePrice());
                        vo.setTaxInclBasePrice(item.getTaxInclBasePrice());
                        vo.setTaxExclCompilePrice(item.getTaxExclCompilePrice());
                        vo.setTaxInclCompilePrice(item.getTaxInclCompilePrice());
                        vo.setCategoryId(item.getCategoryId());
                        vo.setSourceResourceItemId(item.getId());
                        result.add(vo);
                    }
                }
            }
        }

        return result;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createBlank(com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceCreateBlankReqVO createBlankReqVO) {
        // 1. 校验定额节点
        validateDivisionIsQuota(createBlankReqVO.getDivisionId());

        // 2. 计算排序号
        int newSortOrder;
        if (createBlankReqVO.getAfterResourceId() != null) {
            // 在指定行下方插入
            WbBoqResourceDO afterResource = wbBoqResourceMapper.selectById(createBlankReqVO.getAfterResourceId());
            if (afterResource == null) {
                throw exception(WB_BOQ_RESOURCE_NOT_EXISTS);
            }
            // 将后续行的排序号+1
            wbBoqResourceMapper.incrementSortOrderAfter(createBlankReqVO.getDivisionId(), afterResource.getSortOrder());
            newSortOrder = afterResource.getSortOrder() + 1;
        } else {
            // 追加到末尾
            Integer maxSortOrder = wbBoqResourceMapper.selectMaxSortOrderByDivisionId(createBlankReqVO.getDivisionId());
            newSortOrder = maxSortOrder + 1;
        }

        // 3. 创建空白行（所有业务字段为空）
        WbBoqResourceDO blankResource = WbBoqResourceDO.builder()
                .divisionId(createBlankReqVO.getDivisionId())
                .sourceType(WbBoqResourceDO.SOURCE_TYPE_TENANT)
                .sortOrder(newSortOrder)
                .build();
        wbBoqResourceMapper.insert(blankResource);

        log.info("[createBlank] 创建空白工料机行：divisionId={}, id={}, sortOrder={}",
                createBlankReqVO.getDivisionId(), blankResource.getId(), newSortOrder);
        return blankResource.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void fillFromSearch(com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceFillReqVO fillReqVO) {
        // 1. 校验空白行存在
        WbBoqResourceDO existResource = validateResourceExists(fillReqVO.getId());

        // 2. 获取定额节点信息，用于类别校验
        WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(existResource.getDivisionId());

        // 3. 校验并解析类别ID（只能是本定额专业的类别范围）
        Long resolvedCategoryId = validateAndResolveCategoryId(division, fillReqVO.getCategoryId());

        // 4. 处理后台工料机引用（双表写入）
        Long sourceResourceItemId = fillReqVO.getSourceResourceItemId();
        if (sourceResourceItemId == null && fillReqVO.getName() != null && fillReqVO.getUnit() != null) {
            // 纯手动输入，查找或创建后台工料机
            sourceResourceItemId = findOrCreateResourceItem(
                    fillReqVO.getName(), fillReqVO.getSpec(), fillReqVO.getUnit(),
                    fillReqVO.getResourceType());
        }

        // 5. 更新空白行数据
        WbBoqResourceDO updateObj = new WbBoqResourceDO();
        updateObj.setId(fillReqVO.getId());
        updateObj.setSourceResourceItemId(sourceResourceItemId);
        updateObj.setCode(fillReqVO.getCode());
        updateObj.setName(fillReqVO.getName());
        updateObj.setSpec(fillReqVO.getSpec());
        updateObj.setUnit(fillReqVO.getUnit());
        updateObj.setResourceType(fillReqVO.getResourceType());
        updateObj.setTaxRate(fillReqVO.getTaxRate());
        updateObj.setTaxExclBasePrice(fillReqVO.getTaxExclBasePrice());
        updateObj.setTaxInclBasePrice(fillReqVO.getTaxInclBasePrice());
        updateObj.setTaxExclCompilePrice(fillReqVO.getTaxExclCompilePrice());
        updateObj.setTaxInclCompilePrice(fillReqVO.getTaxInclCompilePrice());
        updateObj.setCategoryId(resolvedCategoryId);
        updateObj.setSourceType(WbBoqResourceDO.SOURCE_TYPE_TENANT);
        // consumeQty 保持 null（不保存定额消耗量）
        // adjustConsumeQty 保持 null（等用户手动编辑）

        wbBoqResourceMapper.updateById(updateObj);

        log.info("[fillFromSearch] 填充工料机数据：id={}, code={}, name={}, categoryId={}",
                fillReqVO.getId(), fillReqVO.getCode(), fillReqVO.getName(), resolvedCategoryId);
    }

    /**
     * 用量与调整消耗量联动计算
     *
     * 规则（定额工程量为固定值，不受影响）：
     * - 修改用量 → 调整消耗量 = 用量 / 定额工程量
     * - 修改调整消耗量 → 用量 = 调整消耗量 × 定额工程量
     *
     * @param updateObj 待更新的DO对象（会被修改）
     * @param reqVO 请求参数
     * @param existResource 数据库中的现有记录
     */
    private void applyUsageQtyLinkage(WbBoqResourceDO updateObj,
                                      WbBoqResourceSaveReqVO reqVO,
                                      WbBoqResourceDO existResource) {
        boolean usageQtyChanged = reqVO.getUsageQty() != null
                && !reqVO.getUsageQty().equals(existResource.getUsageQty());
        boolean adjustConsumeQtyChanged = reqVO.getAdjustConsumeQty() != null
                && !reqVO.getAdjustConsumeQty().equals(existResource.getAdjustConsumeQty());

        if (!usageQtyChanged && !adjustConsumeQtyChanged) {
            return; // 两个字段都没变，不做联动
        }

        // 获取定额工程量（从关联的定额节点）
        WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(existResource.getDivisionId());
        if (division == null || division.getQty() == null || division.getQty().compareTo(BigDecimal.ZERO) == 0) {
            // 定额工程量为空或0，无法联动计算
            return;
        }
        BigDecimal quotaQty = division.getQty();

        if (usageQtyChanged) {
            // 修改用量 → 调整消耗量 = 用量 / 定额工程量
            BigDecimal newAdjustConsumeQty = reqVO.getUsageQty()
                    .divide(quotaQty, 6, java.math.RoundingMode.HALF_UP);
            updateObj.setAdjustConsumeQty(newAdjustConsumeQty);
            updateObj.setUsageQty(reqVO.getUsageQty());
        } else if (adjustConsumeQtyChanged) {
            // 修改调整消耗量 → 用量 = 调整消耗量 × 定额工程量
            BigDecimal newUsageQty = reqVO.getAdjustConsumeQty()
                    .multiply(quotaQty).setScale(6, java.math.RoundingMode.HALF_UP);
            updateObj.setUsageQty(newUsageQty);
            updateObj.setAdjustConsumeQty(reqVO.getAdjustConsumeQty());
        }
    }

    /**
     * 项目级编制价/价格来源批量同步
     * 当"除税编制价"或"含税编制价"变更时，同步更新同项目下所有"编码+名称+型号规格+单位"一致的工料机
     *
     * @param updateObj 已更新的DO对象
     * @param reqVO 请求参数
     * @param existResource 数据库中的原始记录
     * @return 批量更新结果，无需同步时返回null
     */
    private com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO syncCompilePriceAcrossProject(
            WbBoqResourceDO updateObj, WbBoqResourceSaveReqVO reqVO, WbBoqResourceDO existResource) {

        // 判断编制价是否变更（使用updateObj中的最终值，因为可能经过税率联动计算）
        boolean taxExclCompileChanged = updateObj.getTaxExclCompilePrice() != null
                && !updateObj.getTaxExclCompilePrice().equals(existResource.getTaxExclCompilePrice());
        boolean taxInclCompileChanged = updateObj.getTaxInclCompilePrice() != null
                && !updateObj.getTaxInclCompilePrice().equals(existResource.getTaxInclCompilePrice());

        if (!taxExclCompileChanged && !taxInclCompileChanged) {
            return null; // 编制价未变更，不需要批量同步
        }

        // 匹配条件：编码+名称+型号规格+单位 四个字段一致
        String code = existResource.getCode();
        String name = existResource.getName();
        String unit = existResource.getUnit();
        if (code == null || name == null || unit == null) {
            return null; // 关键字段为空，无法匹配
        }

        // 获取项目ID
        WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(existResource.getDivisionId());
        if (division == null) {
            return null;
        }
        com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO compileTree =
                wbCompileTreeMapper.selectById(division.getCompileTreeId());
        if (compileTree == null || compileTree.getProjectId() == null) {
            return null;
        }
        Long projectId = compileTree.getProjectId();

        // 查询同项目下匹配的工料机（排除自身）
        List<WbBoqResourceDO> matchedResources = wbBoqResourceMapper.selectSameResourceInProject(
                projectId, code, name, existResource.getSpec(), unit, existResource.getId());

        if (CollUtil.isEmpty(matchedResources)) {
            return null;
        }

        // 批量更新编制价
        java.math.BigDecimal newTaxExclCompilePrice = updateObj.getTaxExclCompilePrice();
        java.math.BigDecimal newTaxInclCompilePrice = updateObj.getTaxInclCompilePrice();

        // 按 compileTreeId 分组统计
        Map<Long, List<WbBoqResourceDO>> groupByCompileTree = new java.util.LinkedHashMap<>();
        for (WbBoqResourceDO matched : matchedResources) {
            WbBoqDivisionDO matchedDiv = wbBoqDivisionMapper.selectById(matched.getDivisionId());
            if (matchedDiv != null) {
                groupByCompileTree.computeIfAbsent(matchedDiv.getCompileTreeId(), k -> new ArrayList<>()).add(matched);
            }
        }

        int totalAffected = 0;
        List<com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO.AffectedUnitInfo> affectedUnits = new ArrayList<>();

        for (Map.Entry<Long, List<WbBoqResourceDO>> entry : groupByCompileTree.entrySet()) {
            Long compileTreeId = entry.getKey();
            List<WbBoqResourceDO> resources = entry.getValue();

            for (WbBoqResourceDO res : resources) {
                WbBoqResourceDO batchUpdate = new WbBoqResourceDO();
                batchUpdate.setId(res.getId());
                if (newTaxExclCompilePrice != null) {
                    batchUpdate.setTaxExclCompilePrice(newTaxExclCompilePrice);
                }
                if (newTaxInclCompilePrice != null) {
                    batchUpdate.setTaxInclCompilePrice(newTaxInclCompilePrice);
                }
                wbBoqResourceMapper.updateById(batchUpdate);
            }

            // 获取单位工程名称
            com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO unitTree =
                    wbCompileTreeMapper.selectById(compileTreeId);
            String unitName = unitTree != null ? unitTree.getName() : "未知";

            com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO.AffectedUnitInfo info =
                    new com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO.AffectedUnitInfo();
            info.setCompileTreeId(compileTreeId);
            info.setUnitName(unitName);
            info.setAffectedRows(resources.size());
            affectedUnits.add(info);
            totalAffected += resources.size();
        }

        com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO result =
                new com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO();
        result.setTotalAffectedRows(totalAffected);
        result.setAffectedUnits(affectedUnits);

        log.info("[syncCompilePriceAcrossProject] 项目级编制价同步：projectId={}, code={}, name={}, 影响{}个单位工程共{}行",
                projectId, code, name, affectedUnits.size(), totalAffected);

        return result;
    }

    /**
     * 税率与价格联动计算（仅普通工料机）
     * 
     * 规则：
     * - 修改税率：含税基价 = 除税基价 × (1 + 税率/100)，含税编制价 = 除税编制价 × (1 + 税率/100)
     * - 修改除税基价：含税基价 = 除税基价 × (1 + 税率/100)
     * - 修改含税基价：除税基价 = 含税基价 / (1 + 税率/100)
     * - 修改除税编制价：含税编制价 = 除税编制价 × (1 + 税率/100)
     * - 修改含税编制价：除税编制价 = 含税编制价 / (1 + 税率/100)
     *
     * @param updateObj 待更新的DO对象（会被修改）
     * @param reqVO 请求参数
     * @param existResource 数据库中的现有记录
     */
    private void applyTaxRatePriceLinkage(WbBoqResourceDO updateObj,
                                          WbBoqResourceSaveReqVO reqVO,
                                          WbBoqResourceDO existResource) {
        // 确定最终税率（优先用本次传入的，否则用数据库已有的）
        BigDecimal taxRate = reqVO.getTaxRate() != null ? reqVO.getTaxRate() : existResource.getTaxRate();
        if (taxRate == null) {
            return; // 没有税率，不做联动
        }

        // 税率因子 = 1 + 税率/100
        BigDecimal taxFactor = BigDecimal.ONE.add(taxRate.divide(new BigDecimal("100"), 10, java.math.RoundingMode.HALF_UP));

        // 判断本次修改了哪些字段
        boolean taxRateChanged = reqVO.getTaxRate() != null && !reqVO.getTaxRate().equals(existResource.getTaxRate());
        boolean taxExclBasePriceChanged = reqVO.getTaxExclBasePrice() != null 
                && !reqVO.getTaxExclBasePrice().equals(existResource.getTaxExclBasePrice());
        boolean taxInclBasePriceChanged = reqVO.getTaxInclBasePrice() != null 
                && !reqVO.getTaxInclBasePrice().equals(existResource.getTaxInclBasePrice());
        boolean taxExclCompilePriceChanged = reqVO.getTaxExclCompilePrice() != null 
                && !reqVO.getTaxExclCompilePrice().equals(existResource.getTaxExclCompilePrice());
        boolean taxInclCompilePriceChanged = reqVO.getTaxInclCompilePrice() != null 
                && !reqVO.getTaxInclCompilePrice().equals(existResource.getTaxInclCompilePrice());

        if (taxRateChanged) {
            // 税率变更：以除税价为基础重算含税价
            BigDecimal baseExcl = reqVO.getTaxExclBasePrice() != null ? reqVO.getTaxExclBasePrice() : existResource.getTaxExclBasePrice();
            BigDecimal compileExcl = reqVO.getTaxExclCompilePrice() != null ? reqVO.getTaxExclCompilePrice() : existResource.getTaxExclCompilePrice();
            if (baseExcl != null) {
                updateObj.setTaxInclBasePrice(baseExcl.multiply(taxFactor).setScale(2, java.math.RoundingMode.HALF_UP));
            }
            if (compileExcl != null) {
                updateObj.setTaxInclCompilePrice(compileExcl.multiply(taxFactor).setScale(2, java.math.RoundingMode.HALF_UP));
            }
        } else {
            // 税率未变，处理价格字段联动
            if (taxExclBasePriceChanged) {
                // 除税基价变更 → 含税基价 = 除税基价 × (1 + 税率/100)
                updateObj.setTaxInclBasePrice(reqVO.getTaxExclBasePrice().multiply(taxFactor)
                        .setScale(2, java.math.RoundingMode.HALF_UP));
            } else if (taxInclBasePriceChanged) {
                // 含税基价变更 → 除税基价 = 含税基价 / (1 + 税率/100)
                updateObj.setTaxExclBasePrice(reqVO.getTaxInclBasePrice().divide(taxFactor, 2, java.math.RoundingMode.HALF_UP));
            }

            if (taxExclCompilePriceChanged) {
                // 除税编制价变更 → 含税编制价 = 除税编制价 × (1 + 税率/100)
                updateObj.setTaxInclCompilePrice(reqVO.getTaxExclCompilePrice().multiply(taxFactor)
                        .setScale(2, java.math.RoundingMode.HALF_UP));
            } else if (taxInclCompilePriceChanged) {
                // 含税编制价变更 → 除税编制价 = 含税编制价 / (1 + 税率/100)
                updateObj.setTaxExclCompilePrice(reqVO.getTaxInclCompilePrice().divide(taxFactor, 2, java.math.RoundingMode.HALF_UP));
            }
        }
    }

    /**
     * 校验并解析类别ID：只能是本定额专业的类别范围（快照数据）
     * 跨定额专业的工料机类别超出范围时置空
     *
     * @param division 定额节点
     * @param categoryId 待校验的类别ID
     * @return 校验后的类别ID，超出范围返回null
     */
    private Long validateAndResolveCategoryId(WbBoqDivisionDO division, Long categoryId) {
        if (categoryId == null) {
            return null;
        }

        Long quotaItemId = division.getSourceQuotaItemId();
        if (quotaItemId == null) {
            // 手动创建的定额节点，没有关联后台定额，类别置空
            return null;
        }

        // 获取定额专业绑定的 categoryTreeId
        Long categoryTreeId = quotaItemService.getCategoryTreeIdByQuotaItem(quotaItemId);
        if (categoryTreeId == null) {
            return null;
        }

        // 从快照表获取允许的类别ID列表（工作台切割后的数据）
        List<com.yhy.module.core.dal.dataobject.workbench.WbCategoryTreeMappingDO> mappings =
                wbSnapshotReadService.getCategoryTreeMappings(division.getCompileTreeId(), categoryTreeId);

        java.util.Set<Long> allowedCategoryIds = mappings.stream()
                .map(com.yhy.module.core.dal.dataobject.workbench.WbCategoryTreeMappingDO::getCategoryId)
                .collect(java.util.stream.Collectors.toSet());

        // 超出范围则置空
        if (!allowedCategoryIds.contains(categoryId)) {
            log.info("[validateAndResolveCategoryId] 类别ID={}不在定额专业允许范围内，置空处理。divisionId={}", 
                    categoryId, division.getId());
            return null;
        }

        return categoryId;
    }
}
