package com.xzzn.ems.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.xzzn.common.constant.RedisKeyConstants;
import com.xzzn.common.core.modbus.ModbusProcessor;
import com.xzzn.common.core.modbus.domain.DeviceConfig;
import com.xzzn.common.core.modbus.domain.WriteTagConfig;
import com.xzzn.common.core.redis.RedisCache;
import com.xzzn.common.enums.BusinessStatus;
import com.xzzn.common.enums.BusinessType;
import com.xzzn.common.enums.CommunicationStatus;
import com.xzzn.common.enums.DeviceCategory;
import com.xzzn.common.enums.DeviceRunningStatus;
import com.xzzn.common.enums.DeviceType;
import com.xzzn.common.enums.OperatorType;
import com.xzzn.common.enums.PointType;
import com.xzzn.common.enums.WorkStatus;
import com.xzzn.common.exception.ServiceException;
import com.xzzn.common.utils.DateUtils;
import com.xzzn.common.utils.StringUtils;
import com.xzzn.ems.domain.EmsDailyChargeData;
import com.xzzn.ems.domain.EmsDevicesSetting;
import com.xzzn.ems.domain.EmsPointEnumMatch;
import com.xzzn.ems.domain.EmsPointConfig;
import com.xzzn.ems.domain.EmsPointMatch;
import com.xzzn.ems.domain.EmsPcsSetting;
import com.xzzn.ems.domain.EmsSiteMonitorItem;
import com.xzzn.ems.domain.EmsSiteMonitorPointMatch;
import com.xzzn.ems.domain.vo.DeviceUpdateRequest;
import com.xzzn.ems.domain.vo.DevicesSettingVo;
import com.xzzn.ems.domain.vo.PointDataRequest;
import com.xzzn.ems.domain.vo.PointQueryResponse;
import com.xzzn.ems.domain.vo.SiteMonitorDataSaveItemVo;
import com.xzzn.ems.domain.vo.SiteMonitorDataSaveRequest;
import com.xzzn.ems.domain.vo.SiteMonitorDataVo;
import com.xzzn.ems.domain.vo.SiteMonitorProjectDisplayVo;
import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingSaveRequest;
import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingVo;
import com.xzzn.ems.domain.vo.SingleBatteryConfigInitRequest;
import com.xzzn.ems.domain.vo.SingleBatteryConfigInitResultVo;
import com.xzzn.ems.domain.vo.SingleBatteryMonitorImportFailureVo;
import com.xzzn.ems.domain.vo.SingleBatteryMonitorImportResultVo;
import com.xzzn.ems.domain.vo.SingleBatteryMonitorImportRowVo;
import com.xzzn.ems.domain.vo.WorkStatusEnumMappingVo;
import com.xzzn.ems.mapper.EmsBatteryDataMinutesMapper;
import com.xzzn.ems.mapper.EmsDailyChargeDataMapper;
import com.xzzn.ems.mapper.EmsDailyEnergyDataMapper;
import com.xzzn.ems.mapper.EmsDevicesSettingMapper;
import com.xzzn.ems.mapper.EmsPcsSettingMapper;
import com.xzzn.ems.mapper.EmsPointConfigMapper;
import com.xzzn.ems.mapper.EmsPointEnumMatchMapper;
import com.xzzn.ems.mapper.EmsPointMatchMapper;
import com.xzzn.ems.mapper.EmsSiteMonitorDataMapper;
import com.xzzn.ems.mapper.EmsSiteMonitorItemMapper;
import com.xzzn.ems.mapper.EmsSiteMonitorPointMatchMapper;
import com.xzzn.ems.service.IEmsDeviceSettingService;
import com.xzzn.ems.service.InfluxPointDataWriter;
import com.xzzn.ems.utils.DevicePointMatchDataProcessor;
import com.xzzn.system.domain.SysOperLog;
import com.xzzn.system.service.ISysOperLogService;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.Collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

/**
 * 站点信息 服务层实现
 *
 */
@Service
public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService
{
    private static final Logger log = LoggerFactory.getLogger(EmsDeviceSettingServiceImpl.class);

    private static final String DDS_SITE_ID = "021_DDS_01";
    private static final String MODULE_HOME = "HOME";
    private static final String MODULE_SBJK = "SBJK";
    private static final String MODULE_TJBB = "TJBB";
    private static final String MENU_SBJK_EMS = "SBJK_EMS";
    private static final Integer USE_FIXED_DISPLAY_YES = 1;
    private static final String FIELD_CURVE_PCS_ACTIVE_POWER = "SBJK_SSYX__curvePcsActivePower";
    private static final String FIELD_CURVE_PCS_REACTIVE_POWER = "SBJK_SSYX__curvePcsReactivePower";
    private static final String HISTORY_TABLE_HOME = "ems_site_monitor_data_home_his";
    private static final String HISTORY_TABLE_SBJK = "ems_site_monitor_data_sbjk_his";
    private static final String HISTORY_TABLE_TJBB = "ems_site_monitor_data_tjbb_his";
    private static final String DELETED_FIELD_MARK = "__DELETED__";
    private static final String PCS_MATCH_DEVICE_CATEGORY = "PCS";
    private static final String STACK_MATCH_DEVICE_CATEGORY = "STACK";
    private static final String CLUSTER_MATCH_DEVICE_CATEGORY = "CLUSTER";
    private static final String MATCH_FIELD_WORK_STATUS = "workStatus";
    private static final String MATCH_FIELD_GRID_STATUS = "gridStatus";
    private static final String MATCH_FIELD_DEVICE_STATUS = "deviceStatus";
    private static final String MATCH_FIELD_CONTROL_MODE = "controlMode";
    private static final String MATCH_FIELD_PCS_COMMUNICATION_STATUS = "pcsCommunicationStatus";
    private static final String MATCH_FIELD_EMS_COMMUNICATION_STATUS = "emsCommunicationStatus";
    private static final String SITE_LEVEL_CALC_DEVICE_ID = "SITE_CALC";
    private static final String BATTERY_FIELD_VOLTAGE = "SBJK_DTDC__voltage";
    private static final String BATTERY_FIELD_TEMPERATURE = "SBJK_DTDC__temperature";
    private static final String BATTERY_FIELD_SOC = "SBJK_DTDC__soc";
    private static final String BATTERY_FIELD_SOH = "SBJK_DTDC__soh";
    private static final String SINGLE_BATTERY_SCOPE_CLUSTER = "cluster";
    private static final String SINGLE_BATTERY_SCOPE_STACK = "stack";
    private static final String SINGLE_BATTERY_FIELD_VALUE_ZERO = "0";
    private static final Set<String> SINGLE_BATTERY_IMPORT_FIELD_CODES = new HashSet<>(Arrays.asList(
            BATTERY_FIELD_VOLTAGE, BATTERY_FIELD_TEMPERATURE, BATTERY_FIELD_SOC, BATTERY_FIELD_SOH
    ));
    private static final Set<String> DEVICE_DIMENSION_MENU_CODES = new HashSet<>(Arrays.asList(
            "SBJK_EMS", "SBJK_PCS", "SBJK_BMSZL", "SBJK_BMSDCC", "SBJK_DTDC", "SBJK_DB", "SBJK_YL", "SBJK_DH", "SBJK_XF",
            "TJBB_PCSQX", "TJBB_DCDQX", "TJBB_DCWD"
    ));
    private static final Set<String> DEVICE_DIMENSION_FIELD_CODES = new HashSet<>(Arrays.asList(
            FIELD_CURVE_PCS_ACTIVE_POWER,
            FIELD_CURVE_PCS_REACTIVE_POWER,
            "SBJK_SSYX__curvePcsMaxTemp",
            "SBJK_SSYX__curveBatteryAveSoc",
            "SBJK_SSYX__curveBatteryAveTemp"
    ));
    private static final Map<String, String> MENU_DEVICE_CATEGORY_MAP = buildMenuDeviceCategoryMap();
    private static final long MONITOR_ITEM_CACHE_TTL_MS = 60_000L;
    private static final long PROJECT_DISPLAY_CACHE_TTL_MS = 15_000L;
    private static final int MONITOR_POINT_MATCH_REDIS_TTL_SECONDS = 300;
    private static final int DISPLAY_DEBUG_SAMPLE_SIZE = 20;
    private static final Set<String> HOME_CHARGE_FIELD_SET = new HashSet<>(Arrays.asList(
            "dayChargedCap", "dayDisChargedCap", "yesterdayChargedCap", "yesterdayDisChargedCap", "totalChargedCap", "totalDischargedCap"
    ));
    private static final Set<String> HOME_REVENUE_FIELD_SET = new HashSet<>(Arrays.asList(
            "totalRevenue", "dayRevenue", "yesterdayRevenue"
    ));
    @Autowired
    private EmsDevicesSettingMapper emsDevicesMapper;
    @Autowired
    private EmsPointMatchMapper emsPointMatchMapper;
    @Autowired
    private EmsPcsSettingMapper emsPcsSettingMapper;
    @Autowired
    private EmsPointConfigMapper emsPointConfigMapper;
    @Autowired
    private EmsPointEnumMatchMapper emsPointEnumMatchMapper;
    @Autowired
    private RedisCache redisCache;
    @Autowired
    private EmsBatteryDataMinutesMapper emsBatteryDataMinutesMapper;
    @Autowired
    private EmsDailyChargeDataMapper emsDailyChargeDataMapper;
    @Autowired
    private EmsDailyEnergyDataMapper emsDailyEnergyDataMapper;
    @Autowired
    private EmsBatteryClusterServiceImpl emsBatteryClusterServiceImpl;
    @Autowired
    private ModbusProcessor modbusProcessor;
    @Autowired
    private EmsSiteMonitorItemMapper emsSiteMonitorItemMapper;
    @Autowired
    private EmsSiteMonitorPointMatchMapper emsSiteMonitorPointMatchMapper;
    @Autowired
    private EmsSiteMonitorDataMapper emsSiteMonitorDataMapper;
    @Autowired
    private InfluxPointDataWriter influxPointDataWriter;
    @Autowired
    private ISysOperLogService operLogService;

    private volatile List<EmsSiteMonitorItem> monitorItemCache = Collections.emptyList();
    private volatile long monitorItemCacheExpireAt = 0L;
    private final ConcurrentMap<String, DisplayCacheEntry> projectDisplayCache = new ConcurrentHashMap<>();
    private static final Map<String, String> ENUM_SCOPE_NAME_MAP = buildEnumScopeNameMap();
    private static final Set<String> MANAGED_ENUM_SCOPE_KEYS = ENUM_SCOPE_NAME_MAP.keySet();

    private static Map<String, String> buildEnumScopeNameMap() {
        Map<String, String> result = new LinkedHashMap<>();
        result.put(buildEnumScopeKey(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS), "PCS-工作状态");
        result.put(buildEnumScopeKey(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_GRID_STATUS), "PCS-并网状态");
        result.put(buildEnumScopeKey(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_DEVICE_STATUS), "PCS-设备状态");
        result.put(buildEnumScopeKey(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_CONTROL_MODE), "PCS-控制模式");
        result.put(buildEnumScopeKey(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS), "BMS总览-工作状态");
        result.put(buildEnumScopeKey(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS), "BMS总览-与PCS通信");
        result.put(buildEnumScopeKey(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS), "BMS总览-与EMS通信");
        result.put(buildEnumScopeKey(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS), "BMS电池簇-工作状态");
        result.put(buildEnumScopeKey(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS), "BMS电池簇-与PCS通信");
        result.put(buildEnumScopeKey(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS), "BMS电池簇-与EMS通信");
        return result;
    }

    private static Map<String, String> buildMenuDeviceCategoryMap() {
        Map<String, String> map = new HashMap<>();
        map.put("SBJK_SSYX", DeviceCategory.PCS.getCode());
        map.put("SBJK_EMS", DeviceCategory.EMS.getCode());
        map.put("SBJK_PCS", DeviceCategory.PCS.getCode());
        map.put("SBJK_BMSZL", DeviceCategory.STACK.getCode());
        map.put("SBJK_BMSDCC", DeviceCategory.CLUSTER.getCode());
        map.put("SBJK_DTDC", DeviceCategory.BATTERY.getCode());
        map.put("SBJK_DB", DeviceCategory.AMMETER.getCode());
        map.put("SBJK_YL", DeviceCategory.COOLING.getCode());
        map.put("SBJK_DH", DeviceCategory.DH.getCode());
        map.put("SBJK_XF", DeviceCategory.XF.getCode());
        map.put("TJBB_PCSQX", DeviceCategory.PCS.getCode());
        map.put("TJBB_DCDQX", DeviceCategory.STACK.getCode());
        map.put("TJBB_DCWD", DeviceCategory.CLUSTER.getCode());
        return map;
    }

    /**
     * 获取设备详细信息
     * @param id
     * @return
     */
    @Override
    public DevicesSettingVo getDeviceDetailInfo(Long id) {
        EmsDevicesSetting emsDevicesSetting = emsDevicesMapper.selectEmsDevicesSettingById(id);
        DevicesSettingVo devicesSettingVo = new DevicesSettingVo();
        BeanUtils.copyProperties(emsDevicesSetting, devicesSettingVo);
        EmsPcsSetting pcsSetting = emsPcsSettingMapper.selectEmsPcsSettingByDeviceId(id);
        if (pcsSetting != null) {
            devicesSettingVo.setPcsSetting(pcsSetting);
        }
        return devicesSettingVo;
    }

    /**
     * 新增设备
     * @param devicesSetting
     * @return
     */
    @Override
    public int addDevice(DevicesSettingVo devicesSetting) {
        boolean flag = checkDeviceExist(devicesSetting);
        if (flag) {
            return -1;
        }
        devicesSetting.setCreateTime(DateUtils.getNowDate());
        int result = emsDevicesMapper.insertEmsDevicesSetting(devicesSetting);
        EmsPcsSetting pcsSetting = devicesSetting.getPcsSetting();
        if (pcsSetting != null) {
            pcsSetting.setDeviceSettingId(devicesSetting.getId());
            emsPcsSettingMapper.insertEmsPcsSetting(pcsSetting);
        }
        initSiteMonitorPointMappingsForNewDevice(devicesSetting);
        return result;
    }

    private void initSiteMonitorPointMappingsForNewDevice(DevicesSettingVo devicesSetting) {
        if (devicesSetting == null) {
            return;
        }
        String siteId = StringUtils.trim(devicesSetting.getSiteId());
        String deviceId = StringUtils.trim(devicesSetting.getDeviceId());
        String deviceCategory = StringUtils.trim(devicesSetting.getDeviceCategory());
        if (StringUtils.isAnyBlank(siteId, deviceId, deviceCategory)) {
            return;
        }

        List<EmsSiteMonitorItem> itemList = getEnabledMonitorItems();
        if (CollectionUtils.isEmpty(itemList)) {
            return;
        }
        Set<String> targetFieldCodes = itemList.stream()
                .filter(Objects::nonNull)
                .filter(item -> isDeviceDimensionItem(item))
                .filter(item -> StringUtils.equals(deviceCategory, resolveDeviceCategoryByItem(item)))
                .map(EmsSiteMonitorItem::getFieldCode)
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .collect(Collectors.toSet());
        if (targetFieldCodes.isEmpty()) {
            return;
        }

        List<EmsSiteMonitorPointMatch> existMappings = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId);
        if (existMappings == null) {
            existMappings = Collections.emptyList();
        }

        Set<String> deletedFieldCodes = existMappings.stream()
                .filter(Objects::nonNull)
                .filter(item -> DELETED_FIELD_MARK.equals(item.getDataPoint()))
                .map(EmsSiteMonitorPointMatch::getFieldCode)
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .collect(Collectors.toSet());
        targetFieldCodes.removeAll(deletedFieldCodes);
        if (targetFieldCodes.isEmpty()) {
            return;
        }

        Set<String> existsFieldAndDevice = existMappings.stream()
                .filter(Objects::nonNull)
                .map(item -> buildMatchKey(StringUtils.trim(item.getFieldCode()), StringUtils.trim(item.getDeviceId())))
                .collect(Collectors.toSet());

        String operName = StringUtils.defaultIfBlank(StringUtils.trim(devicesSetting.getCreateBy()), "system");
        List<EmsSiteMonitorPointMatch> insertList = new ArrayList<>();
        for (String fieldCode : targetFieldCodes) {
            String key = buildMatchKey(fieldCode, deviceId);
            if (existsFieldAndDevice.contains(key)) {
                continue;
            }
            EmsSiteMonitorPointMatch template = findTemplateMappingForNewDevice(existMappings, fieldCode);
            EmsSiteMonitorPointMatch insertItem = new EmsSiteMonitorPointMatch();
            insertItem.setSiteId(siteId);
            insertItem.setFieldCode(fieldCode);
            insertItem.setDeviceId(deviceId);
            insertItem.setDataPoint(template == null ? "" : StringUtils.defaultString(template.getDataPoint()));
            insertItem.setFixedDataPoint(template == null ? null : StringUtils.trimToNull(template.getFixedDataPoint()));
            insertItem.setUseFixedDisplay(template == null ? 0 : (template.getUseFixedDisplay() == null ? 0 : template.getUseFixedDisplay()));
            insertItem.setCreateBy(operName);
            insertItem.setUpdateBy(operName);
            insertList.add(insertItem);
        }

        if (insertList.isEmpty()) {
            return;
        }
        emsSiteMonitorPointMatchMapper.insertBatch(insertList);
        redisCache.deleteObject(buildSiteMonitorPointMatchRedisKey(siteId));
        clearSiteMonitorLatestCache(siteId);
        projectDisplayCache.remove(siteId);
    }

    private EmsSiteMonitorPointMatch findTemplateMappingForNewDevice(List<EmsSiteMonitorPointMatch> mappings, String fieldCode) {
        if (CollectionUtils.isEmpty(mappings) || StringUtils.isBlank(fieldCode)) {
            return null;
        }
        String normalizedFieldCode = fieldCode.trim();
        EmsSiteMonitorPointMatch fallback = null;
        for (EmsSiteMonitorPointMatch item : mappings) {
            if (item == null || !normalizedFieldCode.equals(StringUtils.trim(item.getFieldCode()))) {
                continue;
            }
            if (DELETED_FIELD_MARK.equals(item.getDataPoint())) {
                continue;
            }
            if (StringUtils.isNotBlank(item.getDeviceId())) {
                return item;
            }
            if (fallback == null) {
                fallback = item;
            }
        }
        return fallback;
    }

    private String resolveDeviceCategoryByItem(EmsSiteMonitorItem item) {
        if (item == null) {
            return null;
        }
        String byMenu = StringUtils.trim(MENU_DEVICE_CATEGORY_MAP.get(StringUtils.trim(item.getMenuCode())));
        if (StringUtils.isNotBlank(byMenu)) {
            return byMenu;
        }
        String fieldCode = StringUtils.trim(item.getFieldCode());
        if (StringUtils.isBlank(fieldCode) || !fieldCode.contains("__")) {
            return null;
        }
        String menuCode = fieldCode.substring(0, fieldCode.indexOf("__"));
        return StringUtils.trim(MENU_DEVICE_CATEGORY_MAP.get(StringUtils.trim(menuCode)));
    }

    /**
     * 更新设备
     * @param devicesSetting
     * @return
     */
    @Override
    public int updateDevice(DevicesSettingVo devicesSetting) {
        boolean checkExist = false;
        if (devicesSetting.getId() != null) {
            // 根据id判断该数据是否存在
            EmsDevicesSetting existDevice = emsDevicesMapper.selectEmsDevicesSettingById(devicesSetting.getId());
            if (existDevice == null) {
                return -1;
            }
            // 存在判断是否修改deviceId和siteId
            String deviceId = existDevice.getDeviceId();
            String siteId = existDevice.getSiteId();
            if (!deviceId.equals(devicesSetting.getDeviceId())
                    || !siteId.equals(devicesSetting.getSiteId())) {
                checkExist =  checkDeviceExist(devicesSetting);
            }
        }
        // 已修改校验修改后数据是否存在
        if (checkExist) {
            return -2;
        }
        devicesSetting.setUpdateTime(DateUtils.getNowDate());
        EmsPcsSetting pcsSetting = devicesSetting.getPcsSetting();
        if (pcsSetting != null) {
            if (pcsSetting.getDeviceSettingId() == null) {
                pcsSetting.setDeviceSettingId(devicesSetting.getId());
                emsPcsSettingMapper.insertEmsPcsSetting(pcsSetting);
            } else {
                pcsSetting.setUpdateTime(DateUtils.getNowDate());
                emsPcsSettingMapper.updateByDeviceSettingId(pcsSetting);
            }
        }
        return emsDevicesMapper.updateEmsDevicesSetting(devicesSetting);
    }

    private boolean checkDeviceExist(DevicesSettingVo devicesSetting) {
        String deviceId = devicesSetting.getDeviceId();
        String siteId = devicesSetting.getSiteId();
        if (!StringUtils.isEmpty(deviceId) && !StringUtils.isEmpty(siteId)) {
            EmsDevicesSetting emsDevicesSetting = emsDevicesMapper.getDeviceBySiteAndDeviceId(deviceId, siteId);
            if (emsDevicesSetting != null) {
                return true;
            }
        }
        return false;
    }

    /**
     * 删除设备配置信息
     *
     * @param id Modbus设备配置主键
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int deleteEmsDevicesSettingById(Long id){
        if (id == null) {
            return 0;
        }
        EmsDevicesSetting existDevice = emsDevicesMapper.selectEmsDevicesSettingById(id);
        int rows = emsDevicesMapper.deleteEmsDevicesSettingById(id);
        if (rows <= 0 || existDevice == null) {
            return rows;
        }
        String siteId = StringUtils.trim(existDevice.getSiteId());
        String deviceId = StringUtils.trim(existDevice.getDeviceId());
        if (StringUtils.isAnyBlank(siteId, deviceId)) {
            return rows;
        }
        emsSiteMonitorPointMatchMapper.deleteBySiteIdAndDeviceId(siteId, deviceId);
        redisCache.deleteObject(buildSiteMonitorPointMatchRedisKey(siteId));
        clearSiteMonitorLatestCache(siteId);
        projectDisplayCache.remove(siteId);
        return rows;
    }

    @Override
    public List<DeviceCategory> getDeviceCategory() {
        List<DeviceCategory> deviceList = Arrays.asList(DeviceCategory.values());
        return deviceList;
    }

    @Override
    public List<PointQueryResponse> getSingleSiteDevicePoints(PointDataRequest request) {
        String siteId = request.getSiteId();
        String deviceId = request.getDeviceId();
        String deviceCategory = request.getDeviceCategory();

        List<PointQueryResponse> response = new ArrayList<>();

        String dataPointName = request.getDataPointName();
        String dataPoint =  request.getDataPoint();
        String parentDeviceId = request.getParentId();
        String ipAddress = request.getIpAddress();
        Integer ipPort = request.getIpPort();
        log.info("处理点位清单，deviceCategory："+ deviceCategory);
        log.info("处理点位清单，deviceId："+ deviceId);
        log.info("处理点位清单，parentDeviceId："+ parentDeviceId);

        Integer isAlarm = request.getIsAlarm() == null ? PointType.NO.getCode() : request.getIsAlarm();
        // 电动所的电池簇特殊处理-来源pcs+bmsd
        if (siteId.equals(DDS_SITE_ID) && DeviceCategory.CLUSTER.getCode().equals(deviceCategory)) {
            response = specialDealWithDDSCluster(siteId,deviceId,deviceCategory,dataPointName,dataPoint,ipAddress,ipPort,isAlarm);
        } else if (DeviceCategory.BATTERY.getCode().equals(deviceCategory)) {
            log.info("单体电池处理，parentdeviceId："+ parentDeviceId);
            response = specialDealWithBattery(siteId,deviceId,deviceCategory,
                    dataPointName,dataPoint,parentDeviceId,ipAddress,ipPort,isAlarm);
        } else {
            log.info("处理点位清单，ELSE");

            response =  emsPointMatchMapper.getSingleSiteDevicePoints(
                    siteId,deviceId,deviceCategory,dataPointName,dataPoint,ipAddress,ipPort,isAlarm);
            String redisDataKey = RedisKeyConstants.ORIGINAL_MQTT_DATA;
            if (PointType.YES.getCode().equals(isAlarm)) {
                redisDataKey = RedisKeyConstants.ORIGINAL_MQTT_DATA_ALARM;
            }
            // 从redis取最新数据
            JSONObject mqttJson = redisCache.getCacheObject(redisDataKey + siteId + "_" + deviceId);
            if(mqttJson == null){
                return response;
            }
            String deviceFromMqtt = mqttJson.get("Device").toString();
            String jsonData = mqttJson.get("Data").toString();
            Long updateTime = mqttJson.getLong("timestamp");
            Date latestUpdateTime = DateUtils.convertUpdateTime(updateTime);
            if (StringUtils.isEmpty(jsonData) || !deviceId.equals(deviceFromMqtt)) {
                return response;
            }

            Map<String, Object> obj = JSON.parseObject(jsonData, new TypeReference<Map<String, Object>>() {});
            for (PointQueryResponse pointInfo : response) {
                String dataKey = pointInfo.getDataPoint();
                int isNeedDeviceId = pointInfo.getIsNeedDeviceId();
                if (isNeedDeviceId == 1) {// 需要根据deviceId拼接point
                    dataKey = deviceId + dataKey;
                    pointInfo.setDataPoint(dataKey);
                }
                pointInfo.setPointValue(obj.get(dataKey));
                pointInfo.setUpdateTime(latestUpdateTime);
            }
        }

        if (response == null || response.isEmpty()) {
            return response;
        }
        BigDecimal lowerBound = request.getLower();
        BigDecimal upperBound = request.getUpper();
        // 数据反向筛选
        if (lowerBound != null || upperBound != null) {
            response = response.stream() .filter(point -> {
                Object dataValue = point.getPointValue();
                if (dataValue == null) {
                    return false;
                }
                // 转换为BigDecimal进行精确比较（避免double精度损失）
                BigDecimal value = parseToBigDecimal(dataValue);
                if (value == null) {
                    return false;
                }
                // 比较：使用BigDecimal的compareTo方法
                boolean meetLower = (lowerBound == null) || (value.compareTo(lowerBound) >= 0);
                boolean meetUpper = (upperBound == null) || (value.compareTo(upperBound) <= 0);
                return meetLower && meetUpper;
            }).collect(Collectors.toList());
        }

        // 确保赋值后不会被修改
        final List<PointQueryResponse> resultResponse = response;

        // 根据排序字段和排序方式进行排序
        Comparator<PointQueryResponse> comparator;
        String sortData = request.getSortData();
        if ("pointValue".equals(sortData)) {// 按pointValue排序
            comparator = Comparator.comparing(
                    PointQueryResponse::getPointValue,
                    EmsDeviceSettingServiceImpl::compareMultiType);
        } else {// 默认按updateTime排序
            comparator = Comparator.comparing(PointQueryResponse::getUpdateTime,
                    Comparator.nullsLast(Comparator.naturalOrder()));
        }
        // 排序方式
        String sortMethod = request.getSortMethod();
        boolean isAsc = sortMethod == null || sortMethod.isEmpty() || "asc".equals(sortMethod);
        Comparator<PointQueryResponse> finalComparator = Comparator.comparing(
                PointQueryResponse::getPointValue,  // 用于判断null的字段
                (v1, v2) -> {  // 核心逻辑：null值始终返回1（排在后面）
                    if (v1 == v2) return 0;
                    if (v1 == null) return 1;
                    if (v2 == null) return -1;

                    // 获取实际比较对象
                    PointQueryResponse p1 = findByValue(resultResponse, v1);
                    PointQueryResponse p2 = findByValue(resultResponse, v2);

                    // 确保比较对象不为null
                    if (p1 == null && p2 == null) return 0;
                    if (p1 == null) return 1;
                    if (p2 == null) return -1;

                    // 非null值使用基础比较器逻辑
                    return isAsc ?
                            comparator.compare(p1, p2) :
                            comparator.reversed().compare(p1, p2);
                }
        );
        return resultResponse.stream()
                .sorted(finalComparator)
                .collect(Collectors.toList());
    }

    private List<PointQueryResponse> specialDealWithBattery(String siteId, String deviceId, String deviceCategory,
                                                            String dataPointName, String dataPoint, String parentDeviceId,
                                                            String ipAddress, Integer ipPort, Integer isAlarm) {
        List<PointQueryResponse> response =  emsPointMatchMapper.getSingleSiteDevicePoints(siteId,deviceId,deviceCategory,
                dataPointName,dataPoint,ipAddress,ipPort,isAlarm);

        // 获取redis同步最新数据
        JSONObject jsonObject = new JSONObject();
        String redisDataKey = RedisKeyConstants.ORIGINAL_MQTT_DATA;
        if (PointType.YES.getCode().equals(isAlarm)) {
            redisDataKey = RedisKeyConstants.ORIGINAL_MQTT_DATA_ALARM;
        }
        //TODO 单体电池数据统一有 BMSC 提供
//        if (SiteEnum.DDS.getCode().equals(siteId)) {
//            // dds单体电池数据来源于BMSD
//            EmsDevicesSetting clusterDevice = emsDevicesMapper.getDeviceBySiteAndDeviceId(parentDeviceId,siteId);
//            String bmsdDeviceId = clusterDevice == null ? "" :clusterDevice.getParentId();
//            if (StringUtils.isEmpty(bmsdDeviceId)) {
//                return response;
//            }
//            jsonObject = redisCache.getCacheObject(redisDataKey + siteId + "_" + bmsdDeviceId);
//        } else if (SiteEnum.FX.getCode().equals(siteId)) {
            // fx单体电池数据来源于父类簇BMSC
        jsonObject = redisCache.getCacheObject(redisDataKey + siteId + "_" + parentDeviceId);
//        }
        log.info("点位 key："+redisDataKey+ "，点位数据："+jsonObject);
        if (jsonObject != null) {
            // 填充数据
            fillBatteryData(jsonObject,siteId, deviceId, response);
        }
        return response;
    }

    private void fillBatteryData(JSONObject jsonObject, String siteId, String deviceId, List<PointQueryResponse> response) {
        String jsonData = jsonObject.get("Data").toString();
        if (StringUtils.isEmpty(jsonData)) {
            return;
        }
        //TODO deviceId 获取前三位
        String xdeviceId = "";
        if (deviceId != null && deviceId.length()>3) {
            xdeviceId = deviceId.substring(0, 3);
        }
        Map<String, Object> obj = JSON.parseObject(jsonData, new TypeReference<Map<String, Object>>() {
        });
        log.info("单体电池的点位数据组合 obj:"+JSON.toJSONString(obj));
        Long updateTime = Long.valueOf(jsonObject.get("timestamp").toString());
        Date dataUpdateTime = DateUtils.convertUpdateTime(updateTime);
        // 遍历点位数据，设最新值
        for (PointQueryResponse pointInfo : response) {
            String dataKey = "";
            //TODO 统一处理单体电池逻辑
//            if (SiteEnum.DDS.getCode().equals(siteId)) {
//                dataKey = "DTDC" + xdeviceId + pointInfo.getDataPoint();
//                log.info("单体电池的点位数据组合 dataKey:"+dataKey);
//            } else if (SiteEnum.FX.getCode().equals(siteId)) {
//            dataKey = pointInfo.getDataPoint();
            log.info("单体电池的点位数据组合 dataKey:"+dataKey);
//            }
            pointInfo.setPointValue(obj.get(pointInfo.getDataPoint()));
            pointInfo.setUpdateTime(dataUpdateTime);
        }
    }

    @Override
    public List<String> getSiteAllDeviceCategory(String siteId) {
        return emsDevicesMapper.getAllDeviceCategoryBySiteId(siteId);
    }

    @Override
    public List<SiteMonitorProjectPointMappingVo> getSiteMonitorProjectPointMapping(String siteId) {
        List<SiteMonitorProjectPointMappingVo> result = new ArrayList<>();
        if (StringUtils.isBlank(siteId)) {
            return result;
        }
        List<EmsSiteMonitorItem> itemList = getEnabledMonitorItems();
        if (itemList == null || itemList.isEmpty()) {
            return result;
        }
        List<EmsSiteMonitorPointMatch> mappingList = getPointMatchesBySiteId(siteId);
        Set<String> deletedFieldCodeSet = mappingList.stream()
                .filter(item -> StringUtils.isNotBlank(item.getFieldCode()))
                .filter(item -> DELETED_FIELD_MARK.equals(item.getDataPoint()))
                .map(EmsSiteMonitorPointMatch::getFieldCode)
                .collect(Collectors.toSet());
        Map<String, EmsSiteMonitorPointMatch> mappingByFieldAndDevice = mappingList.stream()
                .filter(item -> StringUtils.isNotBlank(item.getFieldCode()))
                .filter(item -> !DELETED_FIELD_MARK.equals(item.getDataPoint()))
                .collect(Collectors.toMap(
                        item -> buildMatchKey(item.getFieldCode(), item.getDeviceId()),
                        item -> item,
                        (a, b) -> b
                ));
        Map<String, List<DeviceMeta>> deviceMetaByMenu = buildDeviceMetaByMenu(siteId, itemList);

        itemList.forEach(item -> {
            if (deletedFieldCodeSet.contains(item.getFieldCode())) {
                return;
            }
            if (isDeviceDimensionItem(item)) {
                List<DeviceMeta> deviceMetaList = deviceMetaByMenu.getOrDefault(item.getMenuCode(), Collections.emptyList());
                if (deviceMetaList.isEmpty()) {
                    result.add(buildMappingVo(item, null, null, null));
                    return;
                }
                for (DeviceMeta deviceMeta : deviceMetaList) {
                    EmsSiteMonitorPointMatch pointMatch = getMatchWithFallback(mappingByFieldAndDevice, item.getFieldCode(), deviceMeta.getDeviceId());
                    result.add(buildMappingVo(item, pointMatch, deviceMeta.getDeviceId(), deviceMeta.getDeviceName()));
                }
                return;
            }
            EmsSiteMonitorPointMatch pointMatch = getMatchWithFallback(mappingByFieldAndDevice, item.getFieldCode(), null);
            result.add(buildMappingVo(item, pointMatch, null, null));
        });
        return result;
    }

    @Override
    public int saveSiteMonitorProjectPointMapping(SiteMonitorProjectPointMappingSaveRequest request, String operName) {
        if (request == null || StringUtils.isBlank(request.getSiteId())) {
            throw new ServiceException("站点ID不能为空");
        }
        String siteId = request.getSiteId();
        List<EmsSiteMonitorItem> itemList = getEnabledMonitorItems();
        if (itemList == null || itemList.isEmpty()) {
            throw new ServiceException("单站监控配置项为空，请先初始化 ems_site_monitor_item");
        }
        Set<String> fieldCodeSet = itemList.stream().map(EmsSiteMonitorItem::getFieldCode).collect(Collectors.toSet());
        Set<String> deletedFieldCodeSet = new HashSet<>();
        if (request.getDeletedFieldCodes() != null) {
            request.getDeletedFieldCodes().stream()
                    .filter(StringUtils::isNotBlank)
                    .map(String::trim)
                    .filter(fieldCodeSet::contains)
                    .forEach(deletedFieldCodeSet::add);
        }
        int deletedRows = emsSiteMonitorPointMatchMapper.deleteBySiteId(siteId);

        List<EmsSiteMonitorPointMatch> saveList = new ArrayList<>();
        if (request.getMappings() != null) {
            for (SiteMonitorProjectPointMappingVo mapping : request.getMappings()) {
                if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) {
                    continue;
                }
                String fieldCode = mapping.getFieldCode().trim();
                if (!fieldCodeSet.contains(fieldCode) || deletedFieldCodeSet.contains(fieldCode)) {
                    continue;
                }
                String deviceId = StringUtils.isBlank(mapping.getDeviceId()) ? "" : mapping.getDeviceId().trim();
                if (isDeviceDimensionField(fieldCode) && StringUtils.isBlank(deviceId)) {
                    continue;
                }
                String dataPoint = StringUtils.isBlank(mapping.getDataPoint()) ? "" : mapping.getDataPoint().trim();
                String fixedDataPoint = StringUtils.isBlank(mapping.getFixedDataPoint()) ? null : mapping.getFixedDataPoint().trim();
                Integer useFixedDisplay = mapping.getUseFixedDisplay() == null ? 0 : mapping.getUseFixedDisplay();
                boolean hasMapping = StringUtils.isNotBlank(dataPoint) || StringUtils.isNotBlank(fixedDataPoint) || USE_FIXED_DISPLAY_YES.equals(useFixedDisplay);
                if (!hasMapping) {
                    continue;
                }
                EmsSiteMonitorPointMatch pointMatch = new EmsSiteMonitorPointMatch();
                pointMatch.setSiteId(siteId);
                pointMatch.setFieldCode(fieldCode);
                pointMatch.setDeviceId(deviceId);
                pointMatch.setDataPoint(dataPoint);
                pointMatch.setFixedDataPoint(fixedDataPoint);
                pointMatch.setUseFixedDisplay(useFixedDisplay);
                pointMatch.setCreateBy(operName);
                pointMatch.setUpdateBy(operName);
                saveList.add(pointMatch);
            }
        }

        for (String deletedFieldCode : deletedFieldCodeSet) {
            EmsSiteMonitorPointMatch pointMatch = new EmsSiteMonitorPointMatch();
            pointMatch.setSiteId(siteId);
            pointMatch.setFieldCode(deletedFieldCode);
            pointMatch.setDeviceId("");
            pointMatch.setDataPoint(DELETED_FIELD_MARK);
            pointMatch.setFixedDataPoint(null);
            pointMatch.setUseFixedDisplay(0);
            pointMatch.setCreateBy(operName);
            pointMatch.setUpdateBy(operName);
            saveList.add(pointMatch);
        }

        // 点位映射变更后清理“单站监控最新值”缓存，避免页面回退读取到旧快照
        clearSiteMonitorLatestCache(siteId);
        redisCache.deleteObject(buildSiteMonitorPointMatchRedisKey(StringUtils.trim(siteId)));

        if (saveList.isEmpty()) {
            return deletedRows;
        }
        int insertedRows = emsSiteMonitorPointMatchMapper.insertBatch(saveList);
        return deletedRows + insertedRows;
    }

    @Override
    public List<WorkStatusEnumMappingVo> getSiteWorkStatusEnumMappings(String siteId) {
        List<WorkStatusEnumMappingVo> defaults = buildDefaultEnumMappings();
        if (StringUtils.isBlank(siteId)) {
            return defaults;
        }
        String normalizedSiteId = siteId.trim();
        List<WorkStatusEnumMappingVo> result = new ArrayList<>();
        for (String scopeKey : MANAGED_ENUM_SCOPE_KEYS) {
            String[] scope = parseEnumScopeKey(scopeKey);
            List<EmsPointEnumMatch> dbList = emsPointEnumMatchMapper.selectList(normalizedSiteId, scope[0], scope[1]);
            if (CollectionUtils.isEmpty(dbList)) {
                result.addAll(defaults.stream()
                        .filter(item -> scopeKey.equals(buildEnumScopeKey(item.getDeviceCategory(), item.getMatchField())))
                        .collect(Collectors.toList()));
                continue;
            }
            for (EmsPointEnumMatch match : dbList) {
                if (match == null) {
                    continue;
                }
                WorkStatusEnumMappingVo vo = new WorkStatusEnumMappingVo();
                vo.setDeviceCategory(scope[0]);
                vo.setMatchField(scope[1]);
                vo.setMatchFieldName(getScopeName(scope[0], scope[1]));
                vo.setEnumCode(StringUtils.nvl(match.getEnumCode(), ""));
                vo.setEnumName(StringUtils.nvl(match.getEnumName(), ""));
                vo.setEnumDesc(StringUtils.nvl(match.getEnumDesc(), ""));
                vo.setDataEnumCode(StringUtils.nvl(match.getDataEnumCode(), ""));
                result.add(vo);
            }
        }
        return result;
    }

    @Override
    public int saveSiteWorkStatusEnumMappings(String siteId, List<WorkStatusEnumMappingVo> mappings, String operName) {
        if (StringUtils.isBlank(siteId)) {
            throw new ServiceException("站点ID不能为空");
        }
        String normalizedSiteId = siteId.trim();
        int deletedRows = 0;
        for (String scopeKey : MANAGED_ENUM_SCOPE_KEYS) {
            String[] scope = parseEnumScopeKey(scopeKey);
            deletedRows += emsPointEnumMatchMapper.deleteByScope(normalizedSiteId, scope[0], scope[1]);
        }
        int insertedRows = 0;
        if (mappings != null) {
            for (WorkStatusEnumMappingVo item : mappings) {
                if (item == null) {
                    continue;
                }
                String deviceCategory = StringUtils.trim(item.getDeviceCategory());
                String matchField = StringUtils.trim(item.getMatchField());
                if (!isManagedEnumScope(deviceCategory, matchField)) {
                    continue;
                }
                String enumCode = StringUtils.trim(item.getEnumCode());
                String enumName = StringUtils.trim(item.getEnumName());
                String enumDesc = StringUtils.trim(item.getEnumDesc());
                String dataEnumCode = StringUtils.trim(item.getDataEnumCode());
                if (StringUtils.isBlank(enumCode) && StringUtils.isBlank(enumName) && StringUtils.isBlank(dataEnumCode)) {
                    continue;
                }
                if (StringUtils.isBlank(enumCode)) {
                    throw new ServiceException(String.format("%s 枚举编码不能为空", getScopeName(deviceCategory, matchField)));
                }
                EmsPointEnumMatch insertItem = new EmsPointEnumMatch();
                insertItem.setSiteId(normalizedSiteId);
                insertItem.setDeviceCategory(deviceCategory);
                insertItem.setMatchField(matchField);
                insertItem.setEnumCode(enumCode);
                insertItem.setEnumName(StringUtils.defaultString(enumName));
                insertItem.setEnumDesc(StringUtils.defaultString(enumDesc));
                insertItem.setDataEnumCode(StringUtils.defaultString(dataEnumCode));
                insertItem.setCreateBy(operName);
                insertItem.setUpdateBy(operName);
                insertedRows += emsPointEnumMatchMapper.insertEmsPointEnumMatch(insertItem);
            }
        }
        syncPointEnumMatchToRedis(normalizedSiteId, PCS_MATCH_DEVICE_CATEGORY);
        syncPointEnumMatchToRedis(normalizedSiteId, STACK_MATCH_DEVICE_CATEGORY);
        syncPointEnumMatchToRedis(normalizedSiteId, CLUSTER_MATCH_DEVICE_CATEGORY);
        projectDisplayCache.remove(normalizedSiteId);
        return deletedRows + insertedRows;
    }

    private void syncPointEnumMatchToRedis(String siteId, String deviceCategory) {
        if (StringUtils.isAnyBlank(siteId, deviceCategory)) {
            return;
        }
        String cacheKey = DevicePointMatchDataProcessor.getPointEnumMacthCacheKey(siteId, deviceCategory);
        redisCache.deleteObject(cacheKey);
        List<EmsPointEnumMatch> pointEnumMatchList = emsPointEnumMatchMapper.selectList(siteId, deviceCategory, null);
        if (!CollectionUtils.isEmpty(pointEnumMatchList)) {
            redisCache.setCacheList(cacheKey, pointEnumMatchList);
        }
    }

    private List<WorkStatusEnumMappingVo> buildDefaultEnumMappings() {
        List<WorkStatusEnumMappingVo> result = new ArrayList<>();
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "0", "运行"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "1", "停机"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "2", "故障"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "3", "待机"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "4", "充电"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "5", "放电"));

        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_GRID_STATUS, "0", "并网"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_GRID_STATUS, "1", "未并网"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_DEVICE_STATUS, "0", "离线"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_DEVICE_STATUS, "1", "在线"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_CONTROL_MODE, "0", "远程"));
        result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_CONTROL_MODE, "1", "本地"));

        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "0", "静置"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "1", "充电"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "2", "放电"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "3", "浮充"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "4", "待机"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "5", "运行"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "9", "故障"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "0", "正常"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "1", "通讯中断"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "2", "异常"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "0", "正常"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "1", "通讯中断"));
        result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "2", "异常"));

        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "0", "静置"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "1", "充电"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "2", "放电"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "3", "待机"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "5", "运行"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "9", "故障"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "0", "正常"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "1", "通讯中断"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "2", "异常"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "0", "正常"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "1", "通讯中断"));
        result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "2", "异常"));
        return result;
    }

    private WorkStatusEnumMappingVo buildEnumMapping(String deviceCategory, String matchField, String enumCode, String enumName) {
        WorkStatusEnumMappingVo vo = new WorkStatusEnumMappingVo();
        vo.setDeviceCategory(deviceCategory);
        vo.setMatchField(matchField);
        vo.setMatchFieldName(getScopeName(deviceCategory, matchField));
        vo.setEnumCode(enumCode);
        vo.setEnumName(enumName);
        vo.setEnumDesc("");
        vo.setDataEnumCode("");
        return vo;
    }

    private boolean isManagedEnumScope(String deviceCategory, String matchField) {
        return MANAGED_ENUM_SCOPE_KEYS.contains(buildEnumScopeKey(deviceCategory, matchField));
    }

    private static String buildEnumScopeKey(String deviceCategory, String matchField) {
        return StringUtils.defaultString(deviceCategory).trim() + "|" + StringUtils.defaultString(matchField).trim();
    }

    private static String[] parseEnumScopeKey(String scopeKey) {
        String[] values = StringUtils.defaultString(scopeKey).split("\\|", 2);
        if (values.length < 2) {
            return new String[]{"", ""};
        }
        return values;
    }

    private String getScopeName(String deviceCategory, String matchField) {
        String name = ENUM_SCOPE_NAME_MAP.get(buildEnumScopeKey(deviceCategory, matchField));
        if (StringUtils.isNotBlank(name)) {
            return name;
        }
        return StringUtils.defaultString(deviceCategory) + "-" + StringUtils.defaultString(matchField);
    }

    private SiteMonitorProjectPointMappingVo buildMappingVo(EmsSiteMonitorItem item, EmsSiteMonitorPointMatch pointMatch,
                                                            String deviceId, String deviceName) {
        SiteMonitorProjectPointMappingVo vo = new SiteMonitorProjectPointMappingVo();
        vo.setModuleCode(item.getModuleCode());
        vo.setModuleName(item.getModuleName());
        vo.setMenuCode(item.getMenuCode());
        vo.setMenuName(item.getMenuName());
        vo.setSectionName(item.getSectionName());
        vo.setFieldCode(item.getFieldCode());
        vo.setFieldName(item.getFieldName());
        vo.setDeviceId(deviceId);
        vo.setDeviceName(deviceName);
        vo.setDataPoint(pointMatch == null ? "" : StringUtils.nvl(pointMatch.getDataPoint(), ""));
        vo.setFixedDataPoint(pointMatch == null ? "" : StringUtils.nvl(pointMatch.getFixedDataPoint(), ""));
        vo.setUseFixedDisplay(pointMatch == null ? 0 : (pointMatch.getUseFixedDisplay() == null ? 0 : pointMatch.getUseFixedDisplay()));
        return vo;
    }

    private EmsSiteMonitorPointMatch getMatchWithFallback(Map<String, EmsSiteMonitorPointMatch> mappingByFieldAndDevice,
                                                          String fieldCode, String deviceId) {
        EmsSiteMonitorPointMatch exact = mappingByFieldAndDevice.get(buildMatchKey(fieldCode, deviceId));
        if (exact != null) {
            return exact;
        }
        return mappingByFieldAndDevice.get(buildMatchKey(fieldCode, null));
    }

    private String buildMatchKey(String fieldCode, String deviceId) {
        return StringUtils.nvl(fieldCode, "") + "|" + StringUtils.nvl(deviceId, "");
    }

    private Map<String, List<DeviceMeta>> buildDeviceMetaByMenu(String siteId, List<EmsSiteMonitorItem> itemList) {
        Map<String, List<DeviceMeta>> result = new HashMap<>();
        if (StringUtils.isBlank(siteId) || itemList == null || itemList.isEmpty()) {
            return result;
        }
        Set<String> menuCodeSet = itemList.stream()
                .filter(this::isDeviceDimensionItem)
                .map(EmsSiteMonitorItem::getMenuCode)
                .collect(Collectors.toSet());
        for (String menuCode : menuCodeSet) {
            String deviceCategory = MENU_DEVICE_CATEGORY_MAP.get(menuCode);
            if (StringUtils.isBlank(deviceCategory)) {
                continue;
            }
            List<Map<String, Object>> deviceList = emsDevicesMapper.getDeviceInfosBySiteIdAndCategory(siteId, deviceCategory);
            if (deviceList == null || deviceList.isEmpty()) {
                continue;
            }
            List<DeviceMeta> deviceMetas = new ArrayList<>();
            for (Map<String, Object> item : deviceList) {
                if (item == null || item.get("id") == null) {
                    continue;
                }
                String deviceId = String.valueOf(item.get("id"));
                if (StringUtils.isBlank(deviceId)) {
                    continue;
                }
                String deviceName = item.get("deviceName") == null ? deviceId : String.valueOf(item.get("deviceName"));
                deviceMetas.add(new DeviceMeta(deviceId, deviceName));
            }
            deviceMetas.sort(Comparator.comparing(DeviceMeta::getDeviceName, Comparator.nullsLast(String::compareTo))
                    .thenComparing(DeviceMeta::getDeviceId, Comparator.nullsLast(String::compareTo)));
            result.put(menuCode, deviceMetas);
        }
        return result;
    }

    private boolean isDeviceDimensionField(String fieldCode) {
        if (StringUtils.isBlank(fieldCode)) {
            return false;
        }
        String normalizedFieldCode = fieldCode.trim();
        if (DEVICE_DIMENSION_FIELD_CODES.contains(normalizedFieldCode)) {
            return true;
        }
        if (!normalizedFieldCode.contains("__")) {
            return false;
        }
        String menuCode = normalizedFieldCode.substring(0, normalizedFieldCode.indexOf("__"));
        return isDeviceDimensionMenu(menuCode);
    }

    private boolean isDeviceDimensionItem(EmsSiteMonitorItem item) {
        if (item == null) {
            return false;
        }
        if (isDeviceDimensionField(item.getFieldCode())) {
            return true;
        }
        return isDeviceDimensionMenu(item.getMenuCode());
    }

    private boolean isDeviceDimensionMenu(String menuCode) {
        return StringUtils.isNotBlank(menuCode) && DEVICE_DIMENSION_MENU_CODES.contains(menuCode);
    }

    private static class DeviceMeta {
        private final String deviceId;
        private final String deviceName;

        private DeviceMeta(String deviceId, String deviceName) {
            this.deviceId = deviceId;
            this.deviceName = deviceName;
        }

        public String getDeviceId() {
            return deviceId;
        }

        public String getDeviceName() {
            return deviceName;
        }
    }

    private static class SingleBatteryInitScope {
        private final String normalizedSiteId;
        private final String scopeType;
        private final String scopeDeviceId;
        private final List<EmsDevicesSetting> scopeClusters;
        private final String appendClusterId;
        private final List<EmsDevicesSetting> allSiteDevices;

        private SingleBatteryInitScope(String normalizedSiteId,
                                       String scopeType,
                                       String scopeDeviceId,
                                       List<EmsDevicesSetting> scopeClusters,
                                       String appendClusterId,
                                       List<EmsDevicesSetting> allSiteDevices) {
            this.normalizedSiteId = normalizedSiteId;
            this.scopeType = scopeType;
            this.scopeDeviceId = scopeDeviceId;
            this.scopeClusters = scopeClusters;
            this.appendClusterId = appendClusterId;
            this.allSiteDevices = allSiteDevices;
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SingleBatteryConfigInitResultVo initializeSingleBatteryMonitorMappings(SingleBatteryConfigInitRequest request, String operName) {
        SingleBatteryInitScope scope = resolveSingleBatteryInitScope(request);
        SingleBatteryConfigInitResultVo result = new SingleBatteryConfigInitResultVo();
        result.setCommitted(false);
        result.setSiteId(scope.normalizedSiteId);
        result.setScopeType(scope.scopeType);
        result.setScopeDeviceId(scope.scopeDeviceId);
        result.setTargetCount(request.getTargetCount());
        result.setExistingBatteryCount(0);
        result.setInitializedBatteryCount(0);
        result.setInsertedBatteryCount(0);
        result.setPointHitCount(0);
        result.setFixedValueFallbackCount(0);
        result.setInsertedMappingCount(0);
        result.setUpdatedMappingCount(0);

        Map<String, EmsDevicesSetting> deviceMap = scope.allSiteDevices.stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.isNotBlank(item.getDeviceId()))
                .collect(Collectors.toMap(
                        item -> StringUtils.trim(item.getDeviceId()),
                        item -> item,
                        (left, right) -> right,
                        LinkedHashMap::new
                ));
        Set<String> scopeClusterIdSet = scope.scopeClusters.stream()
                .map(EmsDevicesSetting::getDeviceId)
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        List<EmsDevicesSetting> scopeBatteryList = getScopeBatteryList(scope.allSiteDevices, scopeClusterIdSet);
        result.setExistingBatteryCount(scopeBatteryList.size());
        Set<String> affectedBatteryIds = scopeBatteryList.stream()
                .map(EmsDevicesSetting::getDeviceId)
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .collect(Collectors.toSet());

        if (SINGLE_BATTERY_SCOPE_CLUSTER.equals(scope.scopeType) && !scopeBatteryList.isEmpty()) {
            deleteScopeSingleBatteries(scope.normalizedSiteId, scopeBatteryList, deviceMap);
            scopeBatteryList = new ArrayList<>();
        }

        int targetCount = request.getTargetCount();
        if (targetCount > scopeBatteryList.size()) {
            int appendCount = targetCount - scopeBatteryList.size();
            List<EmsDevicesSetting> newBatteryList = createMissingSingleBatteries(
                    scope.normalizedSiteId,
                    scope.appendClusterId,
                    appendCount,
                    deviceMap,
                    operName
            );
            scopeBatteryList.addAll(newBatteryList);
            result.setInsertedBatteryCount(newBatteryList.size());
        }

        Map<String, List<EmsDevicesSetting>> batteryByCluster = scopeBatteryList.stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.isNotBlank(item.getParentId()))
                .collect(Collectors.groupingBy(
                        item -> StringUtils.trim(item.getParentId()),
                        LinkedHashMap::new,
                        Collectors.toList()
                ));
        Set<String> pointIds = new HashSet<>();
        List<SingleBatteryFieldInitTask> taskList = new ArrayList<>();
        for (EmsDevicesSetting cluster : scope.scopeClusters) {
            if (cluster == null || StringUtils.isBlank(cluster.getDeviceId())) {
                continue;
            }
            String clusterId = StringUtils.trim(cluster.getDeviceId());
            List<EmsDevicesSetting> clusterBatteryList = batteryByCluster.getOrDefault(clusterId, Collections.emptyList())
                    .stream()
                    .sorted(Comparator.comparing(item -> StringUtils.defaultString(item.getDeviceId())))
                    .collect(Collectors.toList());
            for (int i = 0; i < clusterBatteryList.size(); i++) {
                EmsDevicesSetting battery = clusterBatteryList.get(i);
                String batteryId = StringUtils.trim(battery.getDeviceId());
                String serialNo = formatSingleBatterySerial(i + 1);
                taskList.add(buildSingleBatteryFieldInitTask(scope.normalizedSiteId, clusterId, batteryId, BATTERY_FIELD_VOLTAGE, "DY", serialNo));
                taskList.add(buildSingleBatteryFieldInitTask(scope.normalizedSiteId, clusterId, batteryId, BATTERY_FIELD_TEMPERATURE, "WD", serialNo));
                taskList.add(buildSingleBatteryFieldInitTask(scope.normalizedSiteId, clusterId, batteryId, BATTERY_FIELD_SOC, "SOC", serialNo));
                taskList.add(buildSingleBatteryFieldInitTask(scope.normalizedSiteId, clusterId, batteryId, BATTERY_FIELD_SOH, "SOH", serialNo));
                pointIds.add(clusterId + "DTDY" + serialNo);
                pointIds.add(clusterId + "DTWD" + serialNo);
                pointIds.add(clusterId + "DTSOC" + serialNo);
                pointIds.add(clusterId + "DTSOH" + serialNo);
            }
        }

        Map<String, EmsPointConfig> pointConfigMap = pointIds.isEmpty()
                ? Collections.emptyMap()
                : emsPointConfigMapper.selectBySiteIdAndPointIds(scope.normalizedSiteId, new ArrayList<>(pointIds))
                .stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.isNotBlank(item.getPointId()))
                .collect(Collectors.toMap(
                        item -> StringUtils.trim(item.getPointId()),
                        item -> item,
                        (left, right) -> right,
                        HashMap::new
                ));

        Map<String, EmsSiteMonitorPointMatch> existingPointMatchMap = emsSiteMonitorPointMatchMapper.selectBySiteId(scope.normalizedSiteId)
                .stream()
                .filter(Objects::nonNull)
                .filter(item -> SINGLE_BATTERY_IMPORT_FIELD_CODES.contains(StringUtils.trim(item.getFieldCode())))
                .filter(item -> StringUtils.isNotBlank(item.getDeviceId()))
                .collect(Collectors.toMap(
                        item -> buildMatchKey(StringUtils.trim(item.getFieldCode()), StringUtils.trim(item.getDeviceId())),
                        item -> item,
                        (left, right) -> right,
                        LinkedHashMap::new
                ));

        List<EmsSiteMonitorPointMatch> insertList = new ArrayList<>();
        affectedBatteryIds.addAll(scopeBatteryList.stream()
                .map(EmsDevicesSetting::getDeviceId)
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .collect(Collectors.toSet()));
        for (SingleBatteryFieldInitTask task : taskList) {
            boolean pointExists = pointConfigMap.containsKey(task.pointId);
            if (pointExists) {
                result.setPointHitCount(result.getPointHitCount() + 1);
            } else {
                result.setFixedValueFallbackCount(result.getFixedValueFallbackCount() + 1);
            }
            upsertSingleBatteryPointMatchForInit(insertList, existingPointMatchMap, result, task, pointExists, operName);
        }
        if (!insertList.isEmpty()) {
            emsSiteMonitorPointMatchMapper.insertBatch(insertList);
        }

        refreshSingleBatteryImportCache(scope.normalizedSiteId, affectedBatteryIds);
        result.setInitializedBatteryCount(scopeBatteryList.size());
        result.setCommitted(true);
        result.setMessage(String.format(
                "初始化完成：已初始化单体 %d 个，新增单体 %d 个，命中点位 %d 条，固定值回填 %d 条",
                result.getInitializedBatteryCount(),
                result.getInsertedBatteryCount(),
                result.getPointHitCount(),
                result.getFixedValueFallbackCount()
        ));
        return result;
    }

    private SingleBatteryInitScope resolveSingleBatteryInitScope(SingleBatteryConfigInitRequest request) {
        if (request == null) {
            throw new ServiceException("鍒濆鍖栬姹備笉鑳戒负绌?");
        }
        String normalizedSiteId = StringUtils.trim(request.getSiteId());
        String normalizedScopeType = StringUtils.trimToEmpty(request.getScopeType()).toLowerCase();
        String normalizedScopeDeviceId = StringUtils.trim(request.getScopeDeviceId());
        Integer targetCount = request.getTargetCount();
        if (StringUtils.isBlank(normalizedSiteId)) {
            throw new ServiceException("绔欑偣ID涓嶈兘涓虹┖");
        }
        if (StringUtils.isBlank(normalizedScopeType) || StringUtils.isBlank(normalizedScopeDeviceId)) {
            throw new ServiceException("鍒濆鍖栬寖鍥村拰璁惧ID涓嶈兘涓虹┖");
        }
        if (!SINGLE_BATTERY_SCOPE_CLUSTER.equals(normalizedScopeType) && !SINGLE_BATTERY_SCOPE_STACK.equals(normalizedScopeType)) {
            throw new ServiceException("鍒濆鍖栬寖鍥诲彧鏀寔 cluster 鎴?stack");
        }
        if (targetCount == null || targetCount < 1) {
            throw new ServiceException("鍗曚綋鏁伴噺蹇呴』澶т簬 0");
        }

        List<EmsDevicesSetting> allSiteDevices = emsDevicesMapper.selectEmsDevicesSettingList(buildSiteDeviceQuery(normalizedSiteId));
        Map<String, EmsDevicesSetting> deviceMap = allSiteDevices.stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.isNotBlank(item.getDeviceId()))
                .collect(Collectors.toMap(
                        item -> StringUtils.trim(item.getDeviceId()),
                        item -> item,
                        (left, right) -> right,
                        LinkedHashMap::new
                ));
        if (SINGLE_BATTERY_SCOPE_CLUSTER.equals(normalizedScopeType)) {
            EmsDevicesSetting cluster = deviceMap.get(normalizedScopeDeviceId);
            if (cluster == null || !StringUtils.equals(DeviceCategory.CLUSTER.getCode(), StringUtils.trim(cluster.getDeviceCategory()))) {
                throw new ServiceException(String.format("电池簇编号[%s]不存在", normalizedScopeDeviceId));
            }
            return new SingleBatteryInitScope(
                    normalizedSiteId,
                    normalizedScopeType,
                    normalizedScopeDeviceId,
                    Collections.singletonList(cluster),
                    normalizedScopeDeviceId,
                    allSiteDevices
            );
        }

        EmsDevicesSetting stack = deviceMap.get(normalizedScopeDeviceId);
        if (stack == null || !StringUtils.equals(DeviceCategory.STACK.getCode(), StringUtils.trim(stack.getDeviceCategory()))) {
            throw new ServiceException(String.format("电池堆编号[%s]不存在", normalizedScopeDeviceId));
        }
        List<EmsDevicesSetting> scopeClusters = allSiteDevices.stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.equals(DeviceCategory.CLUSTER.getCode(), StringUtils.trim(item.getDeviceCategory())))
                .filter(item -> StringUtils.equals(normalizedScopeDeviceId, StringUtils.trim(item.getParentId())))
                .sorted(Comparator.comparing(item -> StringUtils.defaultString(item.getDeviceId())))
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(scopeClusters)) {
            throw new ServiceException(String.format("电池堆[%s]下未配置电池簇，无法初始化单体电池", normalizedScopeDeviceId));
        }
        return new SingleBatteryInitScope(
                normalizedSiteId,
                normalizedScopeType,
                normalizedScopeDeviceId,
                scopeClusters,
                StringUtils.trim(scopeClusters.get(0).getDeviceId()),
                allSiteDevices
        );
    }

    private List<EmsDevicesSetting> getScopeBatteryList(List<EmsDevicesSetting> allSiteDevices, Set<String> scopeClusterIdSet) {
        if (CollectionUtils.isEmpty(allSiteDevices) || CollectionUtils.isEmpty(scopeClusterIdSet)) {
            return new ArrayList<>();
        }
        return allSiteDevices.stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.equals(DeviceCategory.BATTERY.getCode(), StringUtils.trim(item.getDeviceCategory())))
                .filter(item -> scopeClusterIdSet.contains(StringUtils.trim(item.getParentId())))
                .collect(Collectors.toCollection(ArrayList::new));
    }

    private void deleteScopeSingleBatteries(String siteId,
                                            List<EmsDevicesSetting> batteryList,
                                            Map<String, EmsDevicesSetting> deviceMap) {
        if (CollectionUtils.isEmpty(batteryList)) {
            return;
        }
        Long[] ids = batteryList.stream()
                .map(EmsDevicesSetting::getId)
                .filter(Objects::nonNull)
                .toArray(Long[]::new);
        for (EmsDevicesSetting battery : batteryList) {
            if (battery == null || StringUtils.isBlank(battery.getDeviceId())) {
                continue;
            }
            String batteryId = StringUtils.trim(battery.getDeviceId());
            emsSiteMonitorPointMatchMapper.deleteBySiteIdAndDeviceId(siteId, batteryId);
            if (deviceMap != null) {
                deviceMap.remove(batteryId);
            }
        }
        if (ids.length > 0) {
            emsDevicesMapper.deleteEmsDevicesSettingByIds(ids);
        }
    }

    private List<EmsDevicesSetting> createMissingSingleBatteries(String siteId,
                                                                 String clusterDeviceId,
                                                                 int appendCount,
                                                                 Map<String, EmsDevicesSetting> deviceMap,
                                                                 String operName) {
        List<EmsDevicesSetting> result = new ArrayList<>();
        if (appendCount < 1) {
            return result;
        }
        Set<String> existingIds = deviceMap.keySet().stream()
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .collect(Collectors.toSet());
        Set<String> clusterBatteryIds = deviceMap.values().stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.equals(DeviceCategory.BATTERY.getCode(), StringUtils.trim(item.getDeviceCategory())))
                .filter(item -> StringUtils.equals(clusterDeviceId, StringUtils.trim(item.getParentId())))
                .map(EmsDevicesSetting::getDeviceId)
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .collect(Collectors.toCollection(HashSet::new));
        Date now = DateUtils.getNowDate();
        int serial = 1;
        while (result.size() < appendCount) {
            String candidateId = formatSingleBatterySerial(serial);
            serial++;
            if (existingIds.contains(candidateId) || clusterBatteryIds.contains(candidateId)) {
                continue;
            }
            EmsDevicesSetting insertDevice = new EmsDevicesSetting();
            insertDevice.setSiteId(siteId);
            insertDevice.setDeviceId(candidateId);
            insertDevice.setDeviceName(candidateId);
            insertDevice.setDeviceType(DeviceType.TCP.name());
            insertDevice.setCommunicationStatus(CommunicationStatus.OK.getCode());
            insertDevice.setDeviceCategory(DeviceCategory.BATTERY.getCode());
            insertDevice.setDeviceStatus(DeviceRunningStatus.OFFLINE.getCode());
            insertDevice.setWorkStatus(WorkStatus.NORMAL.getCode());
            insertDevice.setParentId(clusterDeviceId);
            insertDevice.setDescription("single battery initialize");
            insertDevice.setCreateBy(operName);
            insertDevice.setUpdateBy(operName);
            insertDevice.setCreatedAt(now);
            insertDevice.setUpdatedAt(now);
            emsDevicesMapper.insertEmsDevicesSetting(insertDevice);
            existingIds.add(candidateId);
            clusterBatteryIds.add(candidateId);
            deviceMap.put(candidateId, insertDevice);
            result.add(insertDevice);
        }
        return result;
    }

    private String formatSingleBatterySerial(int serialNo) {
        return String.format("%03d", serialNo);
    }

    private SingleBatteryFieldInitTask buildSingleBatteryFieldInitTask(String siteId,
                                                                       String clusterDeviceId,
                                                                       String batteryDeviceId,
                                                                       String fieldCode,
                                                                       String bizCode,
                                                                       String serialNo) {
        String pointId = clusterDeviceId + "DT" + bizCode + serialNo;
        return new SingleBatteryFieldInitTask(siteId, clusterDeviceId, batteryDeviceId, fieldCode, pointId);
    }

    private void upsertSingleBatteryPointMatchForInit(List<EmsSiteMonitorPointMatch> insertList,
                                                      Map<String, EmsSiteMonitorPointMatch> existingPointMatchMap,
                                                      SingleBatteryConfigInitResultVo result,
                                                      SingleBatteryFieldInitTask task,
                                                      boolean pointExists,
                                                      String operName) {
        String key = buildMatchKey(task.fieldCode, task.batteryDeviceId);
        EmsSiteMonitorPointMatch existing = existingPointMatchMap.get(key);
        String dataPoint = pointExists ? task.pointId : "";
        String fixedDataPoint = pointExists ? null : SINGLE_BATTERY_FIELD_VALUE_ZERO;
        int useFixedDisplay = pointExists ? 0 : USE_FIXED_DISPLAY_YES;
        if (existing == null) {
            EmsSiteMonitorPointMatch insertItem = new EmsSiteMonitorPointMatch();
            insertItem.setSiteId(task.siteId);
            insertItem.setFieldCode(task.fieldCode);
            insertItem.setDeviceId(task.batteryDeviceId);
            insertItem.setDataPoint(dataPoint);
            insertItem.setFixedDataPoint(fixedDataPoint);
            insertItem.setUseFixedDisplay(useFixedDisplay);
            insertItem.setCreateBy(operName);
            insertItem.setUpdateBy(operName);
            insertList.add(insertItem);
            result.setInsertedMappingCount(result.getInsertedMappingCount() + 1);
            return;
        }
        boolean changed = !StringUtils.equals(StringUtils.trimToEmpty(existing.getDataPoint()), dataPoint)
                || !StringUtils.equals(StringUtils.trimToEmpty(existing.getFixedDataPoint()), StringUtils.trimToEmpty(fixedDataPoint))
                || (existing.getUseFixedDisplay() == null ? 0 : existing.getUseFixedDisplay()) != useFixedDisplay;
        if (!changed) {
            return;
        }
        existing.setDataPoint(dataPoint);
        existing.setFixedDataPoint(fixedDataPoint);
        existing.setUseFixedDisplay(useFixedDisplay);
        existing.setUpdateBy(operName);
        emsSiteMonitorPointMatchMapper.updateEmsSiteMonitorPointMatch(existing);
        result.setUpdatedMappingCount(result.getUpdatedMappingCount() + 1);
    }

    private static class SingleBatteryFieldInitTask {
        private final String siteId;
        private final String clusterDeviceId;
        private final String batteryDeviceId;
        private final String fieldCode;
        private final String pointId;

        private SingleBatteryFieldInitTask(String siteId,
                                           String clusterDeviceId,
                                           String batteryDeviceId,
                                           String fieldCode,
                                           String pointId) {
            this.siteId = siteId;
            this.clusterDeviceId = clusterDeviceId;
            this.batteryDeviceId = batteryDeviceId;
            this.fieldCode = fieldCode;
            this.pointId = pointId;
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SingleBatteryMonitorImportResultVo importSingleBatteryMonitorMappings(String siteId, List<SingleBatteryMonitorImportRowVo> rows, String operName) {
        SingleBatteryMonitorImportResultVo result = new SingleBatteryMonitorImportResultVo();
        String normalizedSiteId = StringUtils.trim(siteId);
        List<SingleBatteryMonitorImportRowVo> sourceRows = rows == null ? Collections.emptyList() : rows;
        result.setCommitted(false);
        result.setTotalRows(sourceRows.size());
        result.setInsertedBatteryCount(0);
        result.setUpdatedBatteryCount(0);
        result.setInsertedMappingCount(0);
        result.setUpdatedMappingCount(0);

        if (StringUtils.isBlank(normalizedSiteId)) {
            result.setFailureRows(sourceRows.size());
            result.setSuccessRows(0);
            result.setMessage("导入失败：站点ID不能为空");
            return result;
        }
        if (CollectionUtils.isEmpty(sourceRows)) {
            result.setFailureRows(0);
            result.setSuccessRows(0);
            result.setCommitted(true);
            result.setMessage("导入文件中未读取到任何数据");
            return result;
        }

        List<EmsDevicesSetting> settingList = emsDevicesMapper.selectEmsDevicesSettingList(buildSiteDeviceQuery(normalizedSiteId));
        Map<String, EmsDevicesSetting> deviceMap = settingList.stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.isNotBlank(item.getDeviceId()))
                .collect(Collectors.toMap(
                        item -> StringUtils.trim(item.getDeviceId()),
                        item -> item,
                        (left, right) -> right,
                        LinkedHashMap::new
                ));

        List<SingleBatteryMonitorImportFailureVo> failureList = new ArrayList<>();
        Set<String> seenBatteryKeys = new HashSet<>();
        Map<String, Integer> seenClusterBatteryRowMap = new HashMap<>();
        Map<String, Integer> seenSiteBatteryRowMap = new HashMap<>();
        Set<String> pointIds = new HashSet<>();
        List<SingleBatteryMonitorImportRowVo> validRows = new ArrayList<>();

        for (SingleBatteryMonitorImportRowVo row : sourceRows) {
            SingleBatteryMonitorImportRowVo normalizedRow = normalizeSingleBatteryImportRow(row);
            String duplicateKey = normalizedRow.getSiteId() + "|" + normalizedRow.getBatteryDeviceId();
            String clusterDuplicateKey = normalizedRow.getSiteId() + "|" + normalizedRow.getClusterDeviceId() + "|" + normalizedRow.getBatteryDeviceId();
            String rowError = validateSingleBatteryImportRow(normalizedSiteId, normalizedRow, deviceMap, duplicateKey, clusterDuplicateKey,
                    seenBatteryKeys, seenSiteBatteryRowMap, seenClusterBatteryRowMap);
            if (rowError != null) {
                failureList.add(buildSingleBatteryImportFailure(normalizedRow, rowError));
                continue;
            }
            collectPointIds(pointIds, normalizedRow);
            validRows.add(normalizedRow);
        }

        if (!pointIds.isEmpty()) {
            Map<String, EmsPointConfig> pointConfigMap = emsPointConfigMapper.selectBySiteIdAndPointIds(normalizedSiteId, new ArrayList<>(pointIds))
                    .stream()
                    .filter(Objects::nonNull)
                    .filter(item -> StringUtils.isNotBlank(item.getPointId()))
                    .collect(Collectors.toMap(
                            item -> StringUtils.trim(item.getPointId()),
                            item -> item,
                            (left, right) -> right
                    ));
            for (SingleBatteryMonitorImportRowVo row : validRows) {
                String missingPointMessage = validateSingleBatteryImportPoints(row, pointConfigMap);
                if (missingPointMessage != null) {
                    failureList.add(buildSingleBatteryImportFailure(row, missingPointMessage));
                }
            }
        }

        if (!failureList.isEmpty()) {
            result.setFailureDetails(failureList);
            result.setFailureRows(failureList.size());
            result.setSuccessRows(Math.max(sourceRows.size() - failureList.size(), 0));
            result.setMessage("导入校验未通过，请根据失败明细修改后重试");
            return result;
        }

        Map<String, EmsSiteMonitorPointMatch> existingPointMatchMap = emsSiteMonitorPointMatchMapper.selectBySiteId(normalizedSiteId)
                .stream()
                .filter(Objects::nonNull)
                .filter(item -> SINGLE_BATTERY_IMPORT_FIELD_CODES.contains(StringUtils.trim(item.getFieldCode())))
                .filter(item -> StringUtils.isNotBlank(item.getDeviceId()))
                .collect(Collectors.toMap(
                        item -> buildMatchKey(StringUtils.trim(item.getFieldCode()), StringUtils.trim(item.getDeviceId())),
                        item -> item,
                        (left, right) -> right,
                        LinkedHashMap::new
                ));

        List<EmsSiteMonitorPointMatch> insertList = new ArrayList<>();
        Set<String> importedBatteryIds = new HashSet<>();
        Date now = DateUtils.getNowDate();
        for (SingleBatteryMonitorImportRowVo row : validRows) {
            String batteryId = row.getBatteryDeviceId();
            importedBatteryIds.add(batteryId);
            EmsDevicesSetting batterySetting = deviceMap.get(batteryId);
            if (batterySetting == null) {
                EmsDevicesSetting insertDevice = new EmsDevicesSetting();
                insertDevice.setSiteId(normalizedSiteId);
                insertDevice.setDeviceId(batteryId);
                insertDevice.setDeviceName(StringUtils.defaultIfBlank(row.getBatteryDeviceName(), batteryId));
                insertDevice.setDeviceType(DeviceType.TCP.name());
                insertDevice.setCommunicationStatus(CommunicationStatus.OK.getCode());
                insertDevice.setDeviceCategory(DeviceCategory.BATTERY.getCode());
                insertDevice.setDeviceStatus(DeviceRunningStatus.OFFLINE.getCode());
                insertDevice.setWorkStatus(WorkStatus.NORMAL.getCode());
                insertDevice.setParentId(row.getClusterDeviceId());
                insertDevice.setDescription("single battery import");
                insertDevice.setCreatedAt(now);
                insertDevice.setUpdatedAt(now);
                emsDevicesMapper.insertEmsDevicesSetting(insertDevice);
                deviceMap.put(batteryId, insertDevice);
                result.setInsertedBatteryCount(result.getInsertedBatteryCount() + 1);
                batterySetting = insertDevice;
            } else {
                boolean shouldUpdateBattery = false;
                if (!StringUtils.equals(StringUtils.trimToEmpty(batterySetting.getParentId()), row.getClusterDeviceId())) {
                    batterySetting.setParentId(row.getClusterDeviceId());
                    shouldUpdateBattery = true;
                }
                if (StringUtils.isNotBlank(row.getBatteryDeviceName()) &&
                        !StringUtils.equals(StringUtils.trimToEmpty(batterySetting.getDeviceName()), row.getBatteryDeviceName())) {
                    batterySetting.setDeviceName(row.getBatteryDeviceName());
                    shouldUpdateBattery = true;
                }
                if (shouldUpdateBattery) {
                    batterySetting.setUpdatedAt(now);
                    emsDevicesMapper.updateEmsDevicesSetting(batterySetting);
                    result.setUpdatedBatteryCount(result.getUpdatedBatteryCount() + 1);
                }
            }

            upsertSingleBatteryPointMatch(insertList, existingPointMatchMap, result, normalizedSiteId, batteryId, BATTERY_FIELD_VOLTAGE, row.getVoltagePointId(), operName);
            upsertSingleBatteryPointMatch(insertList, existingPointMatchMap, result, normalizedSiteId, batteryId, BATTERY_FIELD_TEMPERATURE, row.getTemperaturePointId(), operName);
            upsertSingleBatteryPointMatch(insertList, existingPointMatchMap, result, normalizedSiteId, batteryId, BATTERY_FIELD_SOC, row.getSocPointId(), operName);
            upsertSingleBatteryPointMatch(insertList, existingPointMatchMap, result, normalizedSiteId, batteryId, BATTERY_FIELD_SOH, row.getSohPointId(), operName);
        }

        if (!insertList.isEmpty()) {
            emsSiteMonitorPointMatchMapper.insertBatch(insertList);
        }

        refreshSingleBatteryImportCache(normalizedSiteId, importedBatteryIds);

        result.setCommitted(true);
        result.setFailureRows(0);
        result.setSuccessRows(sourceRows.size());
        result.setMessage(String.format("导入完成：成功 %d 行，新增单体 %d 个，更新单体 %d 个，新增映射 %d 条，更新映射 %d 条",
                result.getSuccessRows(),
                result.getInsertedBatteryCount(),
                result.getUpdatedBatteryCount(),
                result.getInsertedMappingCount(),
                result.getUpdatedMappingCount()));
        return result;
    }

    private EmsDevicesSetting buildSiteDeviceQuery(String siteId) {
        EmsDevicesSetting query = new EmsDevicesSetting();
        query.setSiteId(siteId);
        return query;
    }

    private SingleBatteryMonitorImportRowVo normalizeSingleBatteryImportRow(SingleBatteryMonitorImportRowVo row) {
        SingleBatteryMonitorImportRowVo normalized = row == null ? new SingleBatteryMonitorImportRowVo() : row;
        normalized.setSiteId(StringUtils.trimToEmpty(normalized.getSiteId()));
        normalized.setStackDeviceId(StringUtils.trimToEmpty(normalized.getStackDeviceId()));
        normalized.setClusterDeviceId(StringUtils.trimToEmpty(normalized.getClusterDeviceId()));
        normalized.setBatteryDeviceId(StringUtils.trimToEmpty(normalized.getBatteryDeviceId()));
        normalized.setBatteryDeviceName(StringUtils.trimToEmpty(normalized.getBatteryDeviceName()));
        normalized.setVoltagePointId(StringUtils.trimToEmpty(normalized.getVoltagePointId()));
        normalized.setTemperaturePointId(StringUtils.trimToEmpty(normalized.getTemperaturePointId()));
        normalized.setSocPointId(StringUtils.trimToEmpty(normalized.getSocPointId()));
        normalized.setSohPointId(StringUtils.trimToEmpty(normalized.getSohPointId()));
        return normalized;
    }

    private String validateSingleBatteryImportRow(String siteId,
                                                  SingleBatteryMonitorImportRowVo row,
                                                  Map<String, EmsDevicesSetting> deviceMap,
                                                  String duplicateKey,
                                                  String clusterDuplicateKey,
                                                  Set<String> seenBatteryKeys,
                                                  Map<String, Integer> seenSiteBatteryRowMap,
                                                  Map<String, Integer> seenClusterBatteryRowMap) {
        if (row == null) {
            return "导入行为空，请检查模板内容";
        }
        if (StringUtils.isBlank(row.getSiteId())) {
            return "站点ID不能为空";
        }
        if (!StringUtils.equals(siteId, row.getSiteId())) {
            return String.format("站点ID不一致：当前导入站点为[%s]，Excel中为[%s]", siteId, row.getSiteId());
        }
        if (StringUtils.isBlank(row.getStackDeviceId())) {
            return "电池堆编号不能为空";
        }
        if (StringUtils.isBlank(row.getClusterDeviceId())) {
            return "电池簇编号不能为空";
        }
        if (StringUtils.isBlank(row.getBatteryDeviceId())) {
            return "单体编号不能为空";
        }
        if (StringUtils.isAllBlank(row.getVoltagePointId(), row.getTemperaturePointId(), row.getSocPointId(), row.getSohPointId())) {
            return "电压/温度/SOC/SOH 点位ID至少需要填写一个";
        }
        int currentRowNum = safeRowNum(row);
        Integer duplicateClusterRowNum = seenClusterBatteryRowMap.putIfAbsent(clusterDuplicateKey, currentRowNum);
        if (duplicateClusterRowNum != null) {
            return String.format("同一电池簇[%s]下的单体编号[%s]重复，首次出现在第%d行",
                    row.getClusterDeviceId(), row.getBatteryDeviceId(), duplicateClusterRowNum);
        }
        if (!seenBatteryKeys.add(duplicateKey)) {
            Integer duplicateSiteRowNum = seenSiteBatteryRowMap.get(duplicateKey);
            return String.format("单体编号[%s]在当前导入文件中重复，首次出现在第%d行",
                    row.getBatteryDeviceId(), duplicateSiteRowNum == null ? safeRowNum(row) : duplicateSiteRowNum);
        }
        seenSiteBatteryRowMap.putIfAbsent(duplicateKey, currentRowNum);
        EmsDevicesSetting stackSetting = deviceMap.get(row.getStackDeviceId());
        if (stackSetting == null || !StringUtils.equals(DeviceCategory.STACK.getCode(), StringUtils.trimToEmpty(stackSetting.getDeviceCategory()))) {
            return String.format("电池堆编号[%s]不存在，请先确认设备已创建", row.getStackDeviceId());
        }
        EmsDevicesSetting clusterSetting = deviceMap.get(row.getClusterDeviceId());
        if (clusterSetting == null || !StringUtils.equals(DeviceCategory.CLUSTER.getCode(), StringUtils.trimToEmpty(clusterSetting.getDeviceCategory()))) {
            return String.format("电池簇编号[%s]不存在，请先确认设备已创建", row.getClusterDeviceId());
        }
        if (!StringUtils.equals(StringUtils.trimToEmpty(clusterSetting.getParentId()), row.getStackDeviceId())) {
            return String.format("电池簇[%s]不属于电池堆[%s]，请检查堆簇关系", row.getClusterDeviceId(), row.getStackDeviceId());
        }
        EmsDevicesSetting batterySetting = deviceMap.get(row.getBatteryDeviceId());
        if (batterySetting != null && !StringUtils.equals(DeviceCategory.BATTERY.getCode(), StringUtils.trimToEmpty(batterySetting.getDeviceCategory()))) {
            return String.format("单体编号[%s]已存在，但对应设备类型不是单体电池", row.getBatteryDeviceId());
        }
        return null;
    }

    private void collectPointIds(Set<String> pointIds, SingleBatteryMonitorImportRowVo row) {
        if (row == null || pointIds == null) {
            return;
        }
        if (StringUtils.isNotBlank(row.getVoltagePointId())) {
            pointIds.add(row.getVoltagePointId());
        }
        if (StringUtils.isNotBlank(row.getTemperaturePointId())) {
            pointIds.add(row.getTemperaturePointId());
        }
        if (StringUtils.isNotBlank(row.getSocPointId())) {
            pointIds.add(row.getSocPointId());
        }
        if (StringUtils.isNotBlank(row.getSohPointId())) {
            pointIds.add(row.getSohPointId());
        }
    }

    private String validateSingleBatteryImportPoints(SingleBatteryMonitorImportRowVo row, Map<String, EmsPointConfig> pointConfigMap) {
        if (row == null) {
            return "导入行为空，请检查模板内容";
        }
        if (StringUtils.isNotBlank(row.getVoltagePointId()) && !pointConfigMap.containsKey(row.getVoltagePointId())) {
            return String.format("电压点位ID[%s]不存在，请确认当前站点已配置该点位", row.getVoltagePointId());
        }
        if (StringUtils.isNotBlank(row.getTemperaturePointId()) && !pointConfigMap.containsKey(row.getTemperaturePointId())) {
            return String.format("温度点位ID[%s]不存在，请确认当前站点已配置该点位", row.getTemperaturePointId());
        }
        if (StringUtils.isNotBlank(row.getSocPointId()) && !pointConfigMap.containsKey(row.getSocPointId())) {
            return String.format("SOC点位ID[%s]不存在，请确认当前站点已配置该点位", row.getSocPointId());
        }
        if (StringUtils.isNotBlank(row.getSohPointId()) && !pointConfigMap.containsKey(row.getSohPointId())) {
            return String.format("SOH点位ID[%s]不存在，请确认当前站点已配置该点位", row.getSohPointId());
        }
        return null;
    }

    private int safeRowNum(SingleBatteryMonitorImportRowVo row) {
        if (row == null || row.getRowNum() == null || row.getRowNum() < 1) {
            return 0;
        }
        return row.getRowNum();
    }

    private SingleBatteryMonitorImportFailureVo buildSingleBatteryImportFailure(SingleBatteryMonitorImportRowVo row, String message) {
        SingleBatteryMonitorImportFailureVo failure = new SingleBatteryMonitorImportFailureVo();
        if (row != null) {
            failure.setRowNum(row.getRowNum());
            failure.setSiteId(row.getSiteId());
            failure.setStackDeviceId(row.getStackDeviceId());
            failure.setClusterDeviceId(row.getClusterDeviceId());
            failure.setBatteryDeviceId(row.getBatteryDeviceId());
        }
        failure.setErrorMessage(message);
        return failure;
    }

    private void upsertSingleBatteryPointMatch(List<EmsSiteMonitorPointMatch> insertList,
                                               Map<String, EmsSiteMonitorPointMatch> existingPointMatchMap,
                                               SingleBatteryMonitorImportResultVo result,
                                               String siteId,
                                               String batteryId,
                                               String fieldCode,
                                               String pointId,
                                               String operName) {
        if (StringUtils.isBlank(pointId)) {
            return;
        }
        String key = buildMatchKey(fieldCode, batteryId);
        EmsSiteMonitorPointMatch existing = existingPointMatchMap.get(key);
        if (existing == null) {
            EmsSiteMonitorPointMatch insertItem = new EmsSiteMonitorPointMatch();
            insertItem.setSiteId(siteId);
            insertItem.setFieldCode(fieldCode);
            insertItem.setDeviceId(batteryId);
            insertItem.setDataPoint(pointId);
            insertItem.setFixedDataPoint(null);
            insertItem.setUseFixedDisplay(0);
            insertItem.setCreateBy(operName);
            insertItem.setUpdateBy(operName);
            insertList.add(insertItem);
            result.setInsertedMappingCount(result.getInsertedMappingCount() + 1);
            return;
        }
        boolean changed = !StringUtils.equals(StringUtils.trimToEmpty(existing.getDataPoint()), pointId)
                || StringUtils.isNotBlank(existing.getFixedDataPoint())
                || (existing.getUseFixedDisplay() != null && existing.getUseFixedDisplay() != 0);
        if (!changed) {
            return;
        }
        existing.setDataPoint(pointId);
        existing.setFixedDataPoint(null);
        existing.setUseFixedDisplay(0);
        existing.setUpdateBy(operName);
        emsSiteMonitorPointMatchMapper.updateEmsSiteMonitorPointMatch(existing);
        result.setUpdatedMappingCount(result.getUpdatedMappingCount() + 1);
    }

    private void refreshSingleBatteryImportCache(String siteId, Set<String> batteryIds) {
        redisCache.deleteObject(RedisKeyConstants.INIT_DEVICE_INFO);
        if (!CollectionUtils.isEmpty(batteryIds)) {
            for (String batteryId : batteryIds) {
                if (StringUtils.isBlank(batteryId)) {
                    continue;
                }
                redisCache.deleteObject(RedisKeyConstants.DEVICE_SETTING + siteId + "_" + batteryId);
            }
        }
        redisCache.deleteObject(buildSiteMonitorPointMatchRedisKey(siteId));
        clearSiteMonitorLatestCache(siteId);
        projectDisplayCache.remove(siteId);
    }

    @Override
    public List<SiteMonitorProjectDisplayVo> getSiteMonitorProjectDisplay(String siteId) {
        String normalizedSiteId = StringUtils.trim(siteId);
        if (StringUtils.isBlank(normalizedSiteId)) {
            return new ArrayList<>();
        }
        DisplayCacheEntry cacheEntry = projectDisplayCache.get(normalizedSiteId);
        long now = System.currentTimeMillis();
        if (cacheEntry != null && cacheEntry.getExpireAt() > now) {
            return cloneDisplayList(cacheEntry.getData());
        }

        List<SiteMonitorProjectPointMappingVo> mappingList = getSiteMonitorProjectPointMapping(normalizedSiteId);
        if (mappingList.isEmpty()) {
            return new ArrayList<>();
        }
        Set<String> pointIds = mappingList.stream()
                .map(SiteMonitorProjectPointMappingVo::getDataPoint)
                .filter(StringUtils::isNotBlank)
                .map(item -> item.trim().toUpperCase())
                .collect(Collectors.toSet());
        Map<String, EmsPointConfig> pointConfigByPointId = buildPointConfigByPointId(normalizedSiteId, pointIds);
        Map<String, Object> homeLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(normalizedSiteId, MODULE_HOME)));
        Map<String, Object> sbjkLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(normalizedSiteId, MODULE_SBJK)));
        Map<String, Object> tjbbLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(normalizedSiteId, MODULE_TJBB)));
        Map<String, Map<String, String>> enumDataCodeMapByScope = buildEnumDataCodeMapByScope(normalizedSiteId);
        Map<String, PointLatestSnapshot> pointSnapshotCache = new HashMap<>();
        HomeRunningSnapshot homeRunningSnapshot = buildHomeRunningSnapshot(normalizedSiteId);

        List<SiteMonitorProjectDisplayVo> result = new ArrayList<>();
        int totalCount = 0;
        int fixedHitCount = 0;
        int pointHitCount = 0;
        int cacheHitCount = 0;
        int emptyCount = 0;
        List<String> emptySamples = new ArrayList<>();
        List<String> pointHitSamples = new ArrayList<>();
        List<String> cacheHitSamples = new ArrayList<>();
        for (SiteMonitorProjectPointMappingVo mapping : mappingList) {
            totalCount++;
            SiteMonitorProjectDisplayVo vo = new SiteMonitorProjectDisplayVo();
            BeanUtils.copyProperties(mapping, vo);
            if (USE_FIXED_DISPLAY_YES.equals(mapping.getUseFixedDisplay()) && StringUtils.isNotBlank(mapping.getFixedDataPoint())) {
                vo.setFieldValue(mapping.getFixedDataPoint());
                vo.setValueTime(null);
                fixedHitCount++;
                result.add(vo);
                continue;
            }
            if (MODULE_HOME.equals(mapping.getModuleCode()) && homeRunningSnapshot != null) {
                String homeFieldKey = resolveHomeFieldKey(mapping.getFieldCode());
                if (StringUtils.isNotBlank(homeFieldKey)) {
                    String homeFieldValue = homeRunningSnapshot.getValue(homeFieldKey);
                    if (homeFieldValue != null) {
                        vo.setFieldValue(homeFieldValue);
                        vo.setValueTime(homeRunningSnapshot.getValueTime());
                        result.add(vo);
                        continue;
                    }
                }
            }
            // 与“点位配置列表最新值”一致：按 pointId -> 点位配置(dataKey/deviceId) -> MQTT 最新报文读取
            PointLatestSnapshot latestSnapshot = null;
            String dataPoint = StringUtils.trim(mapping.getDataPoint());
            if (StringUtils.isNotBlank(dataPoint)) {
                String pointCacheKey = dataPoint.toUpperCase();
                if (pointSnapshotCache.containsKey(pointCacheKey)) {
                    latestSnapshot = pointSnapshotCache.get(pointCacheKey);
                } else {
                    latestSnapshot = getLatestSnapshotByPointId(normalizedSiteId, dataPoint, pointConfigByPointId);
                    pointSnapshotCache.put(pointCacheKey, latestSnapshot);
                }
            }
            if (latestSnapshot != null && latestSnapshot.getFieldValue() != null) {
                vo.setFieldValue(latestSnapshot.getFieldValue());
                vo.setValueTime(latestSnapshot.getValueTime());
                applyEnumMappingIfNecessary(mapping, vo, enumDataCodeMapByScope);
                pointHitCount++;
                if (pointHitSamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) {
                    pointHitSamples.add(mapping.getFieldCode() + "->" + StringUtils.defaultString(mapping.getDataPoint()));
                }
                result.add(vo);
                continue;
            }
            if (isSbjkEmsMenu(mapping)) {
                if (StringUtils.isBlank(vo.getFieldValue())) {
                    emptyCount++;
                    if (emptySamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) {
                        emptySamples.add(mapping.getFieldCode() + "->" + StringUtils.defaultString(mapping.getDataPoint()));
                    }
                }
                applyEnumMappingIfNecessary(mapping, vo, enumDataCodeMapByScope);
                result.add(vo);
                continue;
            }
            Object cacheObj = null;
            if (MODULE_HOME.equals(mapping.getModuleCode())) {
                cacheObj = homeLatestMap.get(mapping.getFieldCode());
            } else if (MODULE_SBJK.equals(mapping.getModuleCode())) {
                cacheObj = sbjkLatestMap.get(mapping.getFieldCode());
            } else if (MODULE_TJBB.equals(mapping.getModuleCode())) {
                cacheObj = tjbbLatestMap.get(mapping.getFieldCode());
            }
            if (cacheObj != null) {
                JSONObject snapshot = parseFieldSnapshot(cacheObj);
                vo.setFieldValue(snapshot.getString("fieldValue"));
                vo.setValueTime(parseValueTime(snapshot.get("valueTime")));
                if (StringUtils.isNotBlank(vo.getFieldValue())) {
                    cacheHitCount++;
                    if (cacheHitSamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) {
                        cacheHitSamples.add(mapping.getFieldCode() + "->cache");
                    }
                }
            }
            if (StringUtils.isBlank(vo.getFieldValue())) {
                emptyCount++;
                if (emptySamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) {
                    emptySamples.add(mapping.getFieldCode() + "->" + StringUtils.defaultString(mapping.getDataPoint()));
                }
            }
            applyEnumMappingIfNecessary(mapping, vo, enumDataCodeMapByScope);
            result.add(vo);
        }
        log.info("SiteMonitorDisplayDebug siteId={}, total={}, fixedHit={}, pointHit={}, cacheHit={}, empty={}, emptySamples={}, pointHitSamples={}, cacheHitSamples={}",
                normalizedSiteId, totalCount, fixedHitCount, pointHitCount, cacheHitCount, emptyCount,
                emptySamples, pointHitSamples, cacheHitSamples);
        projectDisplayCache.put(normalizedSiteId, new DisplayCacheEntry(cloneDisplayList(result), now + PROJECT_DISPLAY_CACHE_TTL_MS));
        return result;
    }

    private HomeRunningSnapshot buildHomeRunningSnapshot(String siteId) {
        if (StringUtils.isBlank(siteId)) {
            return null;
        }
        HomeRunningSnapshot snapshot = new HomeRunningSnapshot();
        String today = DateUtils.getDate();
        Map<String, BigDecimal> totalChargeData = emsDailyChargeDataMapper.getAllSiteChargeData(today, siteId);
        if (totalChargeData != null) {
            snapshot.put("totalChargedCap", totalChargeData.get("totalChargedCap"));
            snapshot.put("totalDischargedCap", totalChargeData.get("totalDischargedCap"));
        }

        LocalDate now = LocalDate.now();
        LocalDate yesterday = now.minusDays(1);
        List<SiteMonitorDataVo> chargeList = emsDailyChargeDataMapper.getSingleSiteChargeData(
                siteId,
                DateUtils.toDate(yesterday),
                DateUtils.toDate(now)
        );
        if (!CollectionUtils.isEmpty(chargeList)) {
            for (SiteMonitorDataVo item : chargeList) {
                if (item == null || StringUtils.isBlank(item.getAmmeterDate())) {
                    continue;
                }
                if (today.equals(item.getAmmeterDate())) {
                    snapshot.put("dayChargedCap", item.getChargedCap());
                    snapshot.put("dayDisChargedCap", item.getDisChargedCap());
                } else if (DateUtils.getYesterdayDayString().equals(item.getAmmeterDate())) {
                    snapshot.put("yesterdayChargedCap", item.getChargedCap());
                    snapshot.put("yesterdayDisChargedCap", item.getDisChargedCap());
                }
            }
        }

        EmsDailyChargeData todayChargeData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, DateUtils.toDate(now));
        EmsDailyChargeData yesterdayChargeData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, DateUtils.toDate(yesterday));
        if (todayChargeData != null) {
            snapshot.put("totalRevenue", todayChargeData.getTotalRevenue());
            snapshot.put("dayRevenue", todayChargeData.getDayRevenue());
        } else {
            Map<String, BigDecimal> latestRevenueData = emsDailyEnergyDataMapper.getRealTimeRevenue(siteId);
            if (latestRevenueData != null) {
                snapshot.put("totalRevenue", latestRevenueData.get("totalRevenue"));
                snapshot.put("dayRevenue", latestRevenueData.get("dayRevenue"));
            }
        }
        if (yesterdayChargeData != null) {
            snapshot.put("yesterdayRevenue", yesterdayChargeData.getDayRevenue());
        }
        return snapshot;
    }

    private String resolveHomeFieldKey(String fieldCode) {
        String normalizedFieldCode = StringUtils.trim(fieldCode);
        if (StringUtils.isBlank(normalizedFieldCode)) {
            return null;
        }
        int splitIndex = normalizedFieldCode.lastIndexOf("__");
        String fieldKey = splitIndex >= 0 ? normalizedFieldCode.substring(splitIndex + 2) : normalizedFieldCode;
        if (HOME_CHARGE_FIELD_SET.contains(fieldKey) || HOME_REVENUE_FIELD_SET.contains(fieldKey)) {
            return fieldKey;
        }
        return null;
    }

    private static class HomeRunningSnapshot {
        private final Map<String, String> valueMap = new HashMap<>();
        private final Date valueTime = new Date();

        public void put(String key, BigDecimal value) {
            if (StringUtils.isBlank(key) || value == null) {
                return;
            }
            valueMap.put(key, value.stripTrailingZeros().toPlainString());
        }

        public String getValue(String key) {
            return valueMap.get(key);
        }

        public Date getValueTime() {
            return valueTime;
        }
    }

    private Map<String, Map<String, String>> buildEnumDataCodeMapByScope(String siteId) {
        Map<String, Map<String, String>> result = new HashMap<>();
        if (StringUtils.isBlank(siteId)) {
            return result;
        }
        List<String> categories = Arrays.asList(PCS_MATCH_DEVICE_CATEGORY, STACK_MATCH_DEVICE_CATEGORY, CLUSTER_MATCH_DEVICE_CATEGORY);
        for (String deviceCategory : categories) {
            String cacheKey = DevicePointMatchDataProcessor.getPointEnumMacthCacheKey(siteId, deviceCategory);
            List<EmsPointEnumMatch> enumList = redisCache.getCacheList(cacheKey);
            if (CollectionUtils.isEmpty(enumList)) {
                enumList = emsPointEnumMatchMapper.selectList(siteId, deviceCategory, null);
            }
            if (CollectionUtils.isEmpty(enumList)) {
                continue;
            }
            for (EmsPointEnumMatch item : enumList) {
                if (item == null) {
                    continue;
                }
                String matchField = StringUtils.trim(item.getMatchField());
                String enumCode = StringUtils.trim(item.getEnumCode());
                String dataEnumCode = normalizeEnumDataValue(item.getDataEnumCode());
                if (StringUtils.isAnyBlank(matchField, enumCode, dataEnumCode)) {
                    continue;
                }
                String scopeKey = buildEnumScopeKey(deviceCategory, matchField);
                if (!MANAGED_ENUM_SCOPE_KEYS.contains(scopeKey)) {
                    continue;
                }
                result.computeIfAbsent(scopeKey, key -> new HashMap<>()).put(dataEnumCode, enumCode);
            }
        }
        return result;
    }

    private void applyEnumMappingIfNecessary(SiteMonitorProjectPointMappingVo mapping,
                                             SiteMonitorProjectDisplayVo vo,
                                             Map<String, Map<String, String>> enumDataCodeMapByScope) {
        if (mapping == null || vo == null || CollectionUtils.isEmpty(enumDataCodeMapByScope)) {
            return;
        }
        String rawFieldValue = StringUtils.trim(vo.getFieldValue());
        if (StringUtils.isBlank(rawFieldValue)) {
            return;
        }
        String scopeKey = resolveEnumScopeKey(mapping.getMenuCode(), mapping.getFieldCode());
        if (StringUtils.isBlank(scopeKey)) {
            return;
        }
        Map<String, String> enumMap = enumDataCodeMapByScope.get(scopeKey);
        if (CollectionUtils.isEmpty(enumMap)) {
            return;
        }
        String mappedValue = enumMap.get(normalizeEnumDataValue(rawFieldValue));
        if (StringUtils.isNotBlank(mappedValue)) {
            vo.setFieldValue(mappedValue);
        }
    }

    private String resolveEnumScopeKey(String menuCode, String fieldCode) {
        String normalizedMenuCode = StringUtils.trim(menuCode);
        String deviceCategory = StringUtils.trim(MENU_DEVICE_CATEGORY_MAP.get(normalizedMenuCode));
        if (StringUtils.isBlank(deviceCategory)) {
            return null;
        }
        String matchField = null;
        String normalizedFieldCode = StringUtils.trim(fieldCode);
        if (StringUtils.isNotBlank(normalizedFieldCode)) {
            int splitIndex = normalizedFieldCode.lastIndexOf("__");
            matchField = splitIndex >= 0 ? normalizedFieldCode.substring(splitIndex + 2) : normalizedFieldCode;
        }
        matchField = StringUtils.trim(matchField);
        if (StringUtils.isBlank(matchField)) {
            return null;
        }
        String scopeKey = buildEnumScopeKey(deviceCategory, matchField);
        return MANAGED_ENUM_SCOPE_KEYS.contains(scopeKey) ? scopeKey : null;
    }

    private String normalizeEnumDataValue(String value) {
        String normalized = StringUtils.trim(value);
        if (StringUtils.isBlank(normalized)) {
            return normalized;
        }
        if (normalized.matches("^-?\\d+\\.0+$")) {
            return normalized.substring(0, normalized.indexOf('.'));
        }
        return normalized;
    }

    private boolean isSbjkEmsMenu(SiteMonitorProjectPointMappingVo mapping) {
        return mapping != null && MENU_SBJK_EMS.equals(mapping.getMenuCode());
    }

    private List<SiteMonitorProjectDisplayVo> cloneDisplayList(List<SiteMonitorProjectDisplayVo> source) {
        if (source == null || source.isEmpty()) {
            return new ArrayList<>();
        }
        List<SiteMonitorProjectDisplayVo> copy = new ArrayList<>(source.size());
        for (SiteMonitorProjectDisplayVo item : source) {
            if (item == null) {
                continue;
            }
            SiteMonitorProjectDisplayVo target = new SiteMonitorProjectDisplayVo();
            BeanUtils.copyProperties(item, target);
            copy.add(target);
        }
        return copy;
    }

    private Map<String, EmsPointConfig> buildPointConfigByPointId(String siteId, Set<String> pointIds) {
        Map<String, EmsPointConfig> result = new HashMap<>();
        if (StringUtils.isBlank(siteId) || pointIds == null || pointIds.isEmpty()) {
            return result;
        }
        List<EmsPointConfig> configList = emsPointConfigMapper.selectBySiteIdAndPointIds(
                siteId.trim(),
                new ArrayList<>(pointIds)
        );
        if (configList == null || configList.isEmpty()) {
            return result;
        }
        for (EmsPointConfig config : configList) {
            if (config == null || StringUtils.isBlank(config.getPointId())) {
                continue;
            }
            String pointId = config.getPointId().trim().toUpperCase();
            result.putIfAbsent(pointId, config);
        }
        return result;
    }

    private PointLatestSnapshot getLatestSnapshotByPointId(String siteId, String pointId, Map<String, EmsPointConfig> pointConfigByPointId) {
        if (StringUtils.isAnyBlank(siteId, pointId) || pointConfigByPointId == null || pointConfigByPointId.isEmpty()) {
            return null;
        }
        EmsPointConfig pointConfig = pointConfigByPointId.get(pointId.trim().toUpperCase());
        // 直接按 pointId 拉取最新值（pointId 全局唯一），不再依赖 deviceId 维度
        PointLatestSnapshot curveSnapshot = getLatestSnapshotFromCurve(siteId, pointId, pointConfig);
        if (curveSnapshot != null) {
            return curveSnapshot;
        }

        // 兼容旧逻辑：Influx 无数据时，再按点位配置回退到 MQTT 原始报文解析
        if (pointConfig != null && StringUtils.isNoneBlank(pointConfig.getDeviceId(), pointConfig.getDataKey())) {
            String redisKey = RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + pointConfig.getDeviceId().trim();
            Object raw = redisCache.getCacheObject(redisKey);
            if (raw != null) {
                JSONObject root = toJsonObject(raw);
                if (root != null) {
                    JSONObject dataObject = extractDataObject(root);
                    if (dataObject != null) {
                        Object rawValue = getValueIgnoreCase(dataObject, pointConfig.getDataKey());
                        BigDecimal sourceValue = StringUtils.getBigDecimal(rawValue);
                        if (sourceValue != null) {
                            BigDecimal pointValue = convertPointValue(sourceValue, pointConfig);
                            Long timestamp = root.getLong("timestamp");
                            Date valueTime = timestamp == null ? null : DateUtils.convertUpdateTime(timestamp);
                            return new PointLatestSnapshot(pointValue.stripTrailingZeros().toPlainString(), valueTime);
                        }
                    }
                }
            }
        }
        return null;
    }

    private PointLatestSnapshot getLatestSnapshotFromCurve(String siteId, String pointId, EmsPointConfig pointConfig) {
        if (StringUtils.isAnyBlank(siteId, pointId)) {
            return null;
        }
        String configPointId = pointConfig == null ? null : pointConfig.getPointId();
        String influxPointKey = StringUtils.isNotBlank(configPointId)
                ? configPointId.trim()
                : pointId.trim();
        Date endTime = DateUtils.getNowDate();
        // 优先近24小时快速取最新值，查不到再回退到7天窗口
        Date fastStartTime = DateUtils.addDays(endTime, -1);
        InfluxPointDataWriter.PointValue latest = influxPointDataWriter.queryLatestPointValueByPointKey(
                siteId.trim(),
                influxPointKey,
                fastStartTime,
                endTime
        );
        if (latest == null) {
            Date fallbackStartTime = DateUtils.addDays(endTime, -7);
            latest = influxPointDataWriter.queryLatestPointValueByPointKey(
                    siteId.trim(),
                    influxPointKey,
                    fallbackStartTime,
                    endTime
            );
        }
        if (latest == null || latest.getPointValue() == null) {
            return null;
        }
        return new PointLatestSnapshot(latest.getPointValue().stripTrailingZeros().toPlainString(), latest.getDataTime());
    }

    private JSONObject toJsonObject(Object raw) {
        if (raw == null) {
            return null;
        }
        if (raw instanceof JSONObject) {
            return (JSONObject) raw;
        }
        try {
            return JSON.parseObject(JSON.toJSONString(raw));
        } catch (Exception ex) {
            return null;
        }
    }

    private JSONObject extractDataObject(JSONObject root) {
        if (root == null) {
            return null;
        }
        JSONObject dataObject = root.getJSONObject("Data");
        if (dataObject != null) {
            return dataObject;
        }
        String dataJson = root.getString("Data");
        if (StringUtils.isBlank(dataJson)) {
            return null;
        }
        try {
            return JSON.parseObject(dataJson);
        } catch (Exception ex) {
            return null;
        }
    }

    private Object getValueIgnoreCase(JSONObject dataObject, String dataKey) {
        if (dataObject == null || StringUtils.isBlank(dataKey)) {
            return null;
        }
        Object directValue = dataObject.get(dataKey);
        if (directValue != null) {
            return directValue;
        }
        for (String key : dataObject.keySet()) {
            if (key != null && key.equalsIgnoreCase(dataKey)) {
                return dataObject.get(key);
            }
        }
        return null;
    }

    private BigDecimal convertPointValue(BigDecimal sourceValue, EmsPointConfig pointConfig) {
        if (sourceValue == null || pointConfig == null) {
            return sourceValue;
        }
        BigDecimal a = pointConfig.getDataA() == null ? BigDecimal.ZERO : pointConfig.getDataA();
        BigDecimal k = pointConfig.getDataK() == null ? BigDecimal.ONE : pointConfig.getDataK();
        BigDecimal b = pointConfig.getDataB() == null ? BigDecimal.ZERO : pointConfig.getDataB();
        return a.multiply(sourceValue).multiply(sourceValue)
                .add(k.multiply(sourceValue))
                .add(b);
    }

    private static class PointLatestSnapshot {
        private final String fieldValue;
        private final Date valueTime;

        private PointLatestSnapshot(String fieldValue, Date valueTime) {
            this.fieldValue = fieldValue;
            this.valueTime = valueTime;
        }

        public String getFieldValue() {
            return fieldValue;
        }

        public Date getValueTime() {
            return valueTime;
        }
    }

    private static class DisplayCacheEntry {
        private final List<SiteMonitorProjectDisplayVo> data;
        private final long expireAt;

        private DisplayCacheEntry(List<SiteMonitorProjectDisplayVo> data, long expireAt) {
            this.data = data;
            this.expireAt = expireAt;
        }

        public List<SiteMonitorProjectDisplayVo> getData() {
            return data;
        }

        public long getExpireAt() {
            return expireAt;
        }
    }

    @Override
    public int saveSiteMonitorProjectData(SiteMonitorDataSaveRequest request, String operName) {
        if (request == null || StringUtils.isBlank(request.getSiteId())) {
            throw new ServiceException("站点ID不能为空");
        }
        if (request.getItems() == null || request.getItems().isEmpty()) {
            return 0;
        }
        List<EmsSiteMonitorItem> itemList = getEnabledMonitorItems();
        if (itemList == null || itemList.isEmpty()) {
            throw new ServiceException("单站监控配置项为空，请先初始化 ems_site_monitor_item");
        }
        Set<String> fieldCodeSet = itemList.stream().map(EmsSiteMonitorItem::getFieldCode).collect(Collectors.toSet());

        Map<String, EmsSiteMonitorItem> itemMap = itemList.stream()
                .collect(Collectors.toMap(EmsSiteMonitorItem::getFieldCode, item -> item, (a, b) -> a));

        Date now = DateUtils.getNowDate();
        Map<String, JSONObject> homeLatestUpdates = new HashMap<>();
        Map<String, JSONObject> sbjkLatestUpdates = new HashMap<>();
        Map<String, JSONObject> tjbbLatestUpdates = new HashMap<>();
        Map<Date, JSONObject> homeHistoryByMinute = new HashMap<>();
        Map<Date, JSONObject> sbjkHistoryByMinute = new HashMap<>();
        Map<Date, JSONObject> tjbbHistoryByMinute = new HashMap<>();
        for (SiteMonitorDataSaveItemVo item : request.getItems()) {
            if (item == null || StringUtils.isBlank(item.getFieldCode())) {
                continue;
            }
            String fieldCode = item.getFieldCode().trim();
            if (!fieldCodeSet.contains(fieldCode)) {
                continue;
            }
            EmsSiteMonitorItem itemDef = itemMap.get(fieldCode);
            if (itemDef == null || StringUtils.isBlank(itemDef.getModuleCode())) {
                continue;
            }
            Date valueTime = item.getValueTime() == null ? now : item.getValueTime();
            JSONObject snapshot = buildFieldSnapshot(item.getFieldValue(), valueTime);
            Date statisMinute = truncateToMinute(valueTime);
            if (MODULE_HOME.equals(itemDef.getModuleCode())) {
                homeLatestUpdates.put(fieldCode, snapshot);
                mergeHistorySnapshot(homeHistoryByMinute, statisMinute, fieldCode, snapshot);
            } else if (MODULE_SBJK.equals(itemDef.getModuleCode())) {
                sbjkLatestUpdates.put(fieldCode, snapshot);
                mergeHistorySnapshot(sbjkHistoryByMinute, statisMinute, fieldCode, snapshot);
            } else if (MODULE_TJBB.equals(itemDef.getModuleCode())) {
                tjbbLatestUpdates.put(fieldCode, snapshot);
                mergeHistorySnapshot(tjbbHistoryByMinute, statisMinute, fieldCode, snapshot);
            }
        }
        if (homeLatestUpdates.isEmpty() && sbjkLatestUpdates.isEmpty() && tjbbLatestUpdates.isEmpty()) {
            return 0;
        }
        int rows = 0;
        if (!homeLatestUpdates.isEmpty()) {
            upsertLatestToRedis(request.getSiteId(), MODULE_HOME, homeLatestUpdates);
            rows += upsertHistoryByMinute(HISTORY_TABLE_HOME, request.getSiteId(), homeHistoryByMinute, operName);
        }
        if (!sbjkLatestUpdates.isEmpty()) {
            upsertLatestToRedis(request.getSiteId(), MODULE_SBJK, sbjkLatestUpdates);
            rows += upsertHistoryByMinute(HISTORY_TABLE_SBJK, request.getSiteId(), sbjkHistoryByMinute, operName);
        }
        if (!tjbbLatestUpdates.isEmpty()) {
            upsertLatestToRedis(request.getSiteId(), MODULE_TJBB, tjbbLatestUpdates);
            rows += upsertHistoryByMinute(HISTORY_TABLE_TJBB, request.getSiteId(), tjbbHistoryByMinute, operName);
        }
        projectDisplayCache.remove(StringUtils.trim(request.getSiteId()));
        return rows;
    }

    @Override
    public int syncSiteMonitorDataByMqtt(String siteId, String deviceId, String jsonData, Date valueTime) {
        if (StringUtils.isAnyBlank(siteId, jsonData)) {
            return 0;
        }
        Map<String, Object> dataMap = JSON.parseObject(jsonData, new TypeReference<Map<String, Object>>() {});
        if (dataMap == null || dataMap.isEmpty()) {
            return 0;
        }
        Map<String, Object> upperDataMap = new HashMap<>();
        dataMap.forEach((k, v) -> {
            if (StringUtils.isNotBlank(k)) {
                upperDataMap.put(k.toUpperCase(), v);
            }
        });

        List<EmsSiteMonitorPointMatch> mappingList = getPointMatchesBySiteId(siteId);
        if (mappingList == null || mappingList.isEmpty()) {
            return 0;
        }
        List<EmsSiteMonitorItem> itemList = getEnabledMonitorItems();
        if (itemList == null || itemList.isEmpty()) {
            return 0;
        }
        Map<String, EmsSiteMonitorItem> itemMap = itemList.stream()
                .filter(item -> StringUtils.isNotBlank(item.getFieldCode()))
                .collect(Collectors.toMap(EmsSiteMonitorItem::getFieldCode, item -> item, (a, b) -> a));

        Date actualValueTime = valueTime == null ? DateUtils.getNowDate() : valueTime;
        Date statisMinute = truncateToMinute(actualValueTime);
        Map<String, JSONObject> homeLatestUpdates = new HashMap<>();
        Map<String, JSONObject> sbjkLatestUpdates = new HashMap<>();
        Map<String, JSONObject> tjbbLatestUpdates = new HashMap<>();
        Map<Date, JSONObject> homeHistoryByMinute = new HashMap<>();
        Map<Date, JSONObject> sbjkHistoryByMinute = new HashMap<>();
        Map<Date, JSONObject> tjbbHistoryByMinute = new HashMap<>();
        int matchedCount = 0;
        int missCount = 0;
        List<String> missSamples = new ArrayList<>();

        for (EmsSiteMonitorPointMatch mapping : mappingList) {
            if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) {
                continue;
            }
            if (DELETED_FIELD_MARK.equals(mapping.getDataPoint())) {
                continue;
            }
            // pointId 在系统内唯一，按 pointId 匹配即可，不再按 deviceId 过滤
            EmsSiteMonitorItem itemDef = itemMap.get(mapping.getFieldCode());
            if (itemDef == null || StringUtils.isBlank(itemDef.getModuleCode())) {
                continue;
            }
            if (USE_FIXED_DISPLAY_YES.equals(mapping.getUseFixedDisplay())) {
                continue;
            }
            String point = mapping.getDataPoint();
            if (StringUtils.isBlank(point)) {
                continue;
            }
            point = point.trim();
            Object value = upperDataMap.get(point.toUpperCase());
            if (value == null && StringUtils.isNotBlank(deviceId)) {
                value = upperDataMap.get((deviceId + point).toUpperCase());
            }
            if (value == null) {
                missCount++;
                if (missSamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) {
                    missSamples.add(mapping.getFieldCode() + "->" + point);
                }
                continue;
            }
            matchedCount++;

            JSONObject snapshot = buildFieldSnapshot(String.valueOf(value), actualValueTime);

            if (MODULE_HOME.equals(itemDef.getModuleCode())) {
                homeLatestUpdates.put(mapping.getFieldCode(), snapshot);
                mergeHistorySnapshot(homeHistoryByMinute, statisMinute, mapping.getFieldCode(), snapshot);
            } else if (MODULE_SBJK.equals(itemDef.getModuleCode())) {
                sbjkLatestUpdates.put(mapping.getFieldCode(), snapshot);
                mergeHistorySnapshot(sbjkHistoryByMinute, statisMinute, mapping.getFieldCode(), snapshot);
            } else if (MODULE_TJBB.equals(itemDef.getModuleCode())) {
                tjbbLatestUpdates.put(mapping.getFieldCode(), snapshot);
                mergeHistorySnapshot(tjbbHistoryByMinute, statisMinute, mapping.getFieldCode(), snapshot);
            }
        }

        int rows = 0;
        if (!homeLatestUpdates.isEmpty()) {
            upsertLatestToRedis(siteId, MODULE_HOME, homeLatestUpdates);
            rows += upsertHistoryByMinute(HISTORY_TABLE_HOME, siteId, homeHistoryByMinute, "mqtt");
        }
        if (!sbjkLatestUpdates.isEmpty()) {
            upsertLatestToRedis(siteId, MODULE_SBJK, sbjkLatestUpdates);
            rows += upsertHistoryByMinute(HISTORY_TABLE_SBJK, siteId, sbjkHistoryByMinute, "mqtt");
        }
        if (!tjbbLatestUpdates.isEmpty()) {
            upsertLatestToRedis(siteId, MODULE_TJBB, tjbbLatestUpdates);
            rows += upsertHistoryByMinute(HISTORY_TABLE_TJBB, siteId, tjbbHistoryByMinute, "mqtt");
        }
        log.info("SiteMonitorSyncDebug siteId={}, deviceId={}, mappingTotal={}, matched={}, miss={}, homeUpdates={}, sbjkUpdates={}, tjbbUpdates={}, missSamples={}",
                siteId, deviceId, mappingList.size(), matchedCount, missCount,
                homeLatestUpdates.size(), sbjkLatestUpdates.size(), tjbbLatestUpdates.size(), missSamples);
        return rows;
    }

    private int upsertHistoryByMinute(String tableName, String siteId, Map<Date, JSONObject> minuteSnapshotMap, String operName) {
        int rows = 0;
        for (Map.Entry<Date, JSONObject> entry : minuteSnapshotMap.entrySet()) {
            Date statisMinute = entry.getKey();
            JSONObject merged = mergeHistoryRecord(tableName, siteId, statisMinute, entry.getValue());
            Map<String, String> hotColumns = extractHotColumns(merged);
            rows += emsSiteMonitorDataMapper.upsertHistoryJsonByMinute(
                    tableName,
                    siteId,
                    statisMinute,
                    merged.toJSONString(),
                    hotColumns.get("hotSoc"),
                    hotColumns.get("hotTotalActivePower"),
                    hotColumns.get("hotTotalReactivePower"),
                    hotColumns.get("hotDayChargedCap"),
                    hotColumns.get("hotDayDisChargedCap"),
                    operName
            );
        }
        return rows;
    }

    private JSONObject mergeHistoryRecord(String tableName, String siteId, Date statisMinute, JSONObject updates) {
        String existingJson = emsSiteMonitorDataMapper.selectHistoryJsonByMinute(tableName, siteId, statisMinute);
        JSONObject merged = StringUtils.isBlank(existingJson) ? new JSONObject() : JSON.parseObject(existingJson);
        if (merged == null) {
            merged = new JSONObject();
        }
        if (updates != null) {
            merged.putAll(updates);
        }
        return merged;
    }

    private void upsertLatestToRedis(String siteId, String moduleCode, Map<String, JSONObject> latestUpdates) {
        if (latestUpdates == null || latestUpdates.isEmpty()) {
            return;
        }
        String redisKey = buildSiteMonitorLatestRedisKey(siteId, moduleCode);
        Map<String, String> redisMap = new HashMap<>();
        latestUpdates.forEach((fieldCode, snapshot) -> redisMap.put(fieldCode, snapshot.toJSONString()));
        redisCache.setAllCacheMapValue(redisKey, redisMap);
        redisCache.expire(redisKey, 7, TimeUnit.DAYS);
    }

    private void mergeHistorySnapshot(Map<Date, JSONObject> historyByMinute, Date statisMinute, String fieldCode, JSONObject snapshot) {
        JSONObject minuteObj = historyByMinute.computeIfAbsent(statisMinute, k -> new JSONObject());
        minuteObj.put(fieldCode, snapshot);
    }

    private Map<String, Object> safeRedisMap(Map<String, Object> source) {
        return source == null ? new HashMap<>() : source;
    }

    private List<EmsSiteMonitorItem> getEnabledMonitorItems() {
        long now = System.currentTimeMillis();
        List<EmsSiteMonitorItem> cached = monitorItemCache;
        if (now < monitorItemCacheExpireAt && cached != null) {
            return cached;
        }
        synchronized (this) {
            now = System.currentTimeMillis();
            if (now < monitorItemCacheExpireAt && monitorItemCache != null) {
                return monitorItemCache;
            }
            List<EmsSiteMonitorItem> latest = emsSiteMonitorItemMapper.selectEnabledList();
            if (latest == null) {
                latest = Collections.emptyList();
            }
            monitorItemCache = latest;
            monitorItemCacheExpireAt = now + MONITOR_ITEM_CACHE_TTL_MS;
            return latest;
        }
    }

    private List<EmsSiteMonitorPointMatch> getPointMatchesBySiteId(String siteId) {
        if (StringUtils.isBlank(siteId)) {
            return Collections.emptyList();
        }
        String normalizedSiteId = siteId.trim();
        String redisKey = buildSiteMonitorPointMatchRedisKey(normalizedSiteId);
        Object cacheObj = redisCache.getCacheObject(redisKey);
        List<EmsSiteMonitorPointMatch> cached = parsePointMatchCache(cacheObj);
        if (cached != null) {
            return cached;
        }
        List<EmsSiteMonitorPointMatch> latest = emsSiteMonitorPointMatchMapper.selectBySiteId(normalizedSiteId);
        if (latest == null) {
            latest = Collections.emptyList();
        }
        redisCache.setCacheObject(redisKey, latest, MONITOR_POINT_MATCH_REDIS_TTL_SECONDS, TimeUnit.SECONDS);
        return latest;
    }

    private List<EmsSiteMonitorPointMatch> parsePointMatchCache(Object cacheObj) {
        if (cacheObj == null) {
            return null;
        }
        try {
            if (cacheObj instanceof String) {
                return JSON.parseArray((String) cacheObj, EmsSiteMonitorPointMatch.class);
            }
            if (cacheObj instanceof List) {
                List<?> cacheList = (List<?>) cacheObj;
                if (cacheList.isEmpty()) {
                    return Collections.emptyList();
                }
                if (cacheList.get(0) instanceof EmsSiteMonitorPointMatch) {
                    return (List<EmsSiteMonitorPointMatch>) cacheList;
                }
                return JSON.parseArray(JSON.toJSONString(cacheObj), EmsSiteMonitorPointMatch.class);
            }
            return JSON.parseArray(JSON.toJSONString(cacheObj), EmsSiteMonitorPointMatch.class);
        } catch (Exception ex) {
            log.warn("解析单站监控点位映射缓存失败，key类型={}, err={}",
                    cacheObj.getClass().getName(), ex.getMessage());
            return null;
        }
    }

    private Map<String, String> extractHotColumns(JSONObject merged) {
        Map<String, String> result = new HashMap<>();
        result.put("hotSoc", extractFieldValue(merged,
                new String[]{"SBJK_SSYX__soc", "TJBB_DCDQX__soc_stat", "HOME__avgSoc"},
                new String[]{"__soc", "__soc_stat", "__avgSoc"}));
        result.put("hotTotalActivePower", extractFieldValue(merged,
                new String[]{"SBJK_SSYX__totalActivePower"},
                new String[]{"__totalActivePower", "__activePower", "__activePower_stat"}));
        result.put("hotTotalReactivePower", extractFieldValue(merged,
                new String[]{"SBJK_SSYX__totalReactivePower"},
                new String[]{"__totalReactivePower", "__reactivePower", "__reactivePower_stat"}));
        result.put("hotDayChargedCap", extractFieldValue(merged,
                new String[]{"HOME__dayChargedCap", "SBJK_SSYX__dayChargedCap_rt", "TJBB_GLTJ__chargedCap_stat"},
                new String[]{"__dayChargedCap", "__dayChargedCap_rt", "__chargedCap_stat"}));
        result.put("hotDayDisChargedCap", extractFieldValue(merged,
                new String[]{"HOME__dayDisChargedCap", "SBJK_SSYX__dayDisChargedCap_rt", "TJBB_GLTJ__disChargedCap_stat"},
                new String[]{"__dayDisChargedCap", "__dayDisChargedCap_rt", "__disChargedCap_stat"}));
        return result;
    }

    private String extractFieldValue(JSONObject merged, String[] exactKeys, String[] suffixKeys) {
        if (merged == null || merged.isEmpty()) {
            return null;
        }
        if (exactKeys != null) {
            for (String key : exactKeys) {
                String value = getSnapshotFieldValue(merged.get(key));
                if (StringUtils.isNotBlank(value)) {
                    return value;
                }
            }
        }
        if (suffixKeys != null) {
            for (Map.Entry<String, Object> entry : merged.entrySet()) {
                String fieldCode = entry.getKey();
                if (StringUtils.isBlank(fieldCode)) {
                    continue;
                }
                for (String suffix : suffixKeys) {
                    if (StringUtils.isNotBlank(suffix) && fieldCode.endsWith(suffix)) {
                        String value = getSnapshotFieldValue(entry.getValue());
                        if (StringUtils.isNotBlank(value)) {
                            return value;
                        }
                    }
                }
            }
        }
        return null;
    }

    private String getSnapshotFieldValue(Object snapshotObj) {
        if (snapshotObj == null) {
            return null;
        }
        if (snapshotObj instanceof JSONObject) {
            return ((JSONObject) snapshotObj).getString("fieldValue");
        }
        if (snapshotObj instanceof Map) {
            Object val = ((Map<?, ?>) snapshotObj).get("fieldValue");
            return val == null ? null : String.valueOf(val);
        }
        return String.valueOf(snapshotObj);
    }

    private JSONObject buildFieldSnapshot(String fieldValue, Date valueTime) {
        JSONObject snapshot = new JSONObject();
        snapshot.put("fieldValue", fieldValue);
        snapshot.put("valueTime", valueTime == null ? null : valueTime.getTime());
        return snapshot;
    }

    private JSONObject parseFieldSnapshot(Object cacheObj) {
        if (cacheObj == null) {
            return new JSONObject();
        }
        if (cacheObj instanceof JSONObject) {
            return (JSONObject) cacheObj;
        }
        if (cacheObj instanceof Map) {
            return new JSONObject((Map) cacheObj);
        }
        try {
            JSONObject snapshot = JSON.parseObject(String.valueOf(cacheObj));
            return snapshot == null ? new JSONObject() : snapshot;
        } catch (Exception e) {
            return new JSONObject();
        }
    }

    private Date parseValueTime(Object valueTimeObj) {
        if (valueTimeObj == null) {
            return null;
        }
        if (valueTimeObj instanceof Date) {
            return (Date) valueTimeObj;
        }
        if (valueTimeObj instanceof Number) {
            return new Date(((Number) valueTimeObj).longValue());
        }
        try {
            return DateUtils.parseDate(valueTimeObj.toString());
        } catch (Exception e) {
            return null;
        }
    }

    private Date truncateToMinute(Date date) {
        Date source = date == null ? DateUtils.getNowDate() : date;
        long millis = source.getTime();
        long minuteMillis = 60_000L;
        return new Date((millis / minuteMillis) * minuteMillis);
    }

    private String buildSiteMonitorLatestRedisKey(String siteId, String moduleCode) {
        return RedisKeyConstants.SITE_MONITOR_LATEST + siteId + "_" + moduleCode;
    }

    private String buildSiteMonitorPointMatchRedisKey(String siteId) {
        return RedisKeyConstants.SITE_MONITOR_POINT_MATCH + siteId;
    }

    private void clearSiteMonitorLatestCache(String siteId) {
        if (StringUtils.isBlank(siteId)) {
            return;
        }
        redisCache.deleteObject(buildSiteMonitorLatestRedisKey(siteId, MODULE_HOME));
        redisCache.deleteObject(buildSiteMonitorLatestRedisKey(siteId, MODULE_SBJK));
        redisCache.deleteObject(buildSiteMonitorLatestRedisKey(siteId, MODULE_TJBB));
    }

    // 辅助方法：根据值查找对应的对象（用于比较器中获取完整对象）
    private PointQueryResponse findByValue(List<PointQueryResponse> list, Object value) {
        return list.stream()
                .filter(p -> {
                    Object val = p.getPointValue();
                    return Objects.equals(val, value);
                })
                .findFirst()
                .orElse(null);
    }
    // Object转BigDecimal（支持多种类型）
    private BigDecimal parseToBigDecimal(Object dataValue) {
        if (dataValue instanceof BigDecimal) {
            return (BigDecimal) dataValue;
        } else if (dataValue instanceof Number) {
            return new BigDecimal(((Number) dataValue).toString());
        } else if (dataValue instanceof String) {
            try {
                return new BigDecimal((String) dataValue);
            } catch (NumberFormatException e) {
                return null;
            }
        } else {
            return null;
        }
    }

    // 对于dds的电池簇点位最新数据获取特殊处理
    private List<PointQueryResponse> specialDealWithDDSCluster(String siteId, String deviceId, String deviceCategory,
                                                               String dataPointName, String dataPoint, String ipAddress,
                                                               Integer ipPort, Integer isAlarm) {

        // 替换为对应的父类id
        String bmsdDeviceId = deviceId.replace("BMSC","BMSD");
        List<PointQueryResponse> response = emsPointMatchMapper
                .getClusterDevicePoints(siteId,deviceId,bmsdDeviceId,deviceCategory,dataPointName,dataPoint,ipAddress,ipPort,isAlarm);


        JSONObject mergedData = new JSONObject();
        String redisDataKey = RedisKeyConstants.ORIGINAL_MQTT_DATA;
        if (PointType.YES.getCode().equals(isAlarm)) {
            redisDataKey = RedisKeyConstants.ORIGINAL_MQTT_DATA_ALARM;
        }

        // 数据来源pcs
        JSONObject pcsJson = redisCache.getCacheObject(redisDataKey + siteId + "_PCS");
        if (pcsJson != null) {
            JSONObject data = pcsJson.getJSONObject("Data");
            mergedData.putAll(data);
        }
        // 根据deviceId获取父类bmsd
        JSONObject bmsdJson = redisCache.getCacheObject(redisDataKey + siteId + "_" + bmsdDeviceId);
         if (bmsdJson != null) {
             JSONObject data = bmsdJson.getJSONObject("Data");
             mergedData.putAll(data);
        }

        String jsonData = mergedData.toString();
        Map<String, Object> obj = JSON.parseObject(jsonData, new TypeReference<Map<String, Object>>() {});
        for (PointQueryResponse pointInfo : response) {
            String dataKey = pointInfo.getDataPoint();
            Long updateTime = Long.valueOf(bmsdJson.get("timestamp").toString());
            if ("PCS".equals(pointInfo.getDataDevice())) {
                updateTime = Long.valueOf(pcsJson.get("timestamp").toString());
            }
            pointInfo.setDataPoint(dataKey);
            pointInfo.setPointValue(obj.get(dataKey));
            Date latestUpdateTime = convertUpdateTime(updateTime);
            pointInfo.setUpdateTime(latestUpdateTime);
        }
        return response;
    }

    private Date convertUpdateTime(Long updateTime) {
        if (updateTime == null) {
            return null;
        }

        // 兼容10位（秒级）和13位（毫秒级）
        int length = String.valueOf(updateTime).length();
        if (length == 10) {  // 10位秒级 -> 转换为毫秒级
            updateTime *= 1000;
        } else if (length != 13) { // 既不是10位也不是13位，视为非法
            System.err.println("时间戳格式错误，必须是10位(秒)或13位(毫秒)");
            return null;
        }

        // 3. 转换为Date
        return new Date(updateTime);
    }
    /**
     * 自定义多类型比较器，支持BigDecimal、Integer、String等类型比较
     */
    private static int compareMultiType(Object o1, Object o2) {
        // 统一转换为Comparable进行比较
        try {
            // 对于数字类型（BigDecimal、Integer等）
            if (o1 instanceof Number && o2 instanceof Number) {
                Number numObj1 = (Number) o1;
                Number numObj2 = (Number) o2;

                BigDecimal num1 = toBigDecimal(numObj1);
                BigDecimal num2 = toBigDecimal(numObj2);
                return num1.compareTo(num2);
            }

            // 对于字符串类型
            if (o1 instanceof String && o2 instanceof String) {
                String str1 = (String) o1;
                String str2 = (String) o2;

                if (isNumericString(str1) && isNumericString(str2)) {
                    return new BigDecimal(str1).compareTo(new BigDecimal(str2));
                }
                return str1.compareTo(str2);
            }

            // 其他类型默认按toString比较
            return o1.toString().compareTo(o2.toString());

        } catch (Exception e) {
            // 比较失败时按toString兜底
            return o1.toString().compareTo(o2.toString());
        }
    }

    /**
     * 将Number类型转换为BigDecimal
     */
    private static BigDecimal toBigDecimal(Number number) {
        if (number instanceof BigDecimal) {
            return (BigDecimal) number;
        }
        return new BigDecimal(number.toString());
    }

    /**
     * 判断字符串是否为数字格式
     */
    private static boolean isNumericString(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        try {
            new BigDecimal(str);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    @Override
    public Map<String, List<EmsDevicesSetting>> initDeviceInfo() {
        List<EmsDevicesSetting> settingList = emsDevicesMapper.selectEmsDevicesSettingList(null);
        // 按siteId分组
        Map<String, List<EmsDevicesSetting>> map = settingList.stream()
                .collect(Collectors.groupingBy(
                        EmsDevicesSetting::getSiteId,
                        HashMap::new,
                        Collectors.toList()
                ));
        // 存redis
        redisCache.setCacheObject(RedisKeyConstants.INIT_DEVICE_INFO,  map, 30, TimeUnit.DAYS);
        return map;
    }

    @Override
    public List<Map<String, Object>> getDeviceListBySiteAndCategory(String siteId, String deviceCategory) {
        return emsDevicesMapper.getDeviceInfosBySiteIdAndCategory(siteId, deviceCategory);
    }

    @Override
    public boolean updateDeviceStatus(DeviceUpdateRequest request) {
        EmsDevicesSetting device = emsDevicesMapper.getDeviceBySiteAndDeviceId(request.getDeviceId(), request.getSiteId());
        if (device == null) {
            throw new ServiceException("未找到对应设备配置信息");
        }
        if (DeviceType.TCP.name().equals(device.getDeviceType())) {
            if (device.getSlaveId() == null) {
                throw new ServiceException("设备未配置从站地址");
            }
            if (device.getIpPort() == null) {
                throw new ServiceException("设备未配置端口号");
            }
        }
        // 查询设备的开关机配置
        EmsPcsSetting pcsSetting = emsPcsSettingMapper.selectEmsPcsSettingByDeviceId(device.getId());
        if (pcsSetting == null) {
            throw new ServiceException("未找到对应PCS配置信息");
        }
        // 调用Modbus向设备发送指令
        DeviceConfig deviceConfig = getDeviceConfig(device);
        deviceConfig.setWriteTags(getWriteTags(request.getWorkStatus(), pcsSetting));
        log.info("设备控制指令发送数据: {}", JSON.toJSONString(deviceConfig));
        boolean result = modbusProcessor.writeDataToDevice(deviceConfig);
        if (!result) {
            recordDeviceOperationLog(request, false, "设备控制指令发送失败");
            throw new ServiceException("设备控制指令发送失败");
        }
        recordDeviceOperationLog(request, true, null);
        return true;
    }

    private void recordDeviceOperationLog(DeviceUpdateRequest request, boolean success, String errorMsg) {
        try {
            SysOperLog operLog = new SysOperLog();
            operLog.setTitle("设备控制-开关机/写功率");
            operLog.setBusinessType(BusinessType.UPDATE.ordinal());
            operLog.setMethod(this.getClass().getName() + ".updateDeviceStatus");
            operLog.setRequestMethod("SERVICE");
            operLog.setOperatorType(OperatorType.OTHER.ordinal());
            operLog.setOperName("system");
            operLog.setOperIp("127.0.0.1");
            operLog.setOperUrl("/ems/siteConfig/updateDeviceStatus");
            operLog.setOperTime(DateUtils.getNowDate());
            operLog.setOperParam(StringUtils.substring(JSON.toJSONString(request), 0, 2000));
            operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(Collections.singletonMap("success", success)), 0, 2000));
            operLog.setStatus(success ? BusinessStatus.SUCCESS.ordinal() : BusinessStatus.FAIL.ordinal());
            if (!success) {
                operLog.setErrorMsg(StringUtils.substring(errorMsg, 0, 2000));
            }
            operLogService.insertOperlog(operLog);
        } catch (Exception e) {
            log.error("记录sys_oper_log失败, siteId={}, deviceId={}", request.getSiteId(), request.getDeviceId(), e);
        }
    }

    public DeviceConfig getDeviceConfig(EmsDevicesSetting device) {
        DeviceConfig deviceConfig = new DeviceConfig();
        deviceConfig.setDeviceNumber(device.getDeviceId());
        deviceConfig.setDeviceName(device.getDeviceName());
        deviceConfig.setSlaveId(device.getSlaveId().intValue());
//        if (DeviceType.TCP.name().equals(device.getDeviceType())) {
        deviceConfig.setHost(device.getIpAddress());
        deviceConfig.setPort(device.getIpPort().intValue());
//        }

        return deviceConfig;
    }

    public List<WriteTagConfig> getWriteTags(String workStatus, EmsPcsSetting pcsSetting) {
        List<WriteTagConfig> writeTags = new ArrayList<>();
        BigDecimal power;
        BigDecimal powerMultiplier = pcsSetting.getPowerMultiplier();
        if (powerMultiplier == null || BigDecimal.ZERO.compareTo(powerMultiplier) == 0) {
            powerMultiplier = BigDecimal.ONE;
        }

        if (WorkStatus.NORMAL.getCode().equals(workStatus)) {
            // 开机先发送开机指令再发送有功功率给定值
            WriteTagConfig writeTag = new WriteTagConfig();
            writeTag.setAddress(pcsSetting.getPointAddress());
            writeTag.setValue(pcsSetting.getStartCommand());
            writeTags.add(writeTag);
            power = pcsSetting.getStartPower();
        } else if (WorkStatus.STOP.getCode().equals(workStatus)) {
            // 关机
            power = pcsSetting.getStopPower();
        } else {
            throw new ServiceException("工作状态不合法");
        }
        JSONArray array = JSON.parseArray(pcsSetting.getClusterPointAddress());
        for (int i = 0; i < pcsSetting.getClusterNum(); i++) {
            Object clusterPointAddress = array.get(i);
            WriteTagConfig clusterWriteTag = new WriteTagConfig();
            clusterWriteTag.setAddress(String.valueOf(clusterPointAddress));
            // 电池簇PCS有功功率给定默认置0
            if (power == null) {
                power = BigDecimal.ZERO;
            }
            clusterWriteTag.setValue(power.multiply(powerMultiplier));
            writeTags.add(clusterWriteTag);
        }
        if (WorkStatus.STOP.getCode().equals(workStatus)) {
            // 关机先发送有功功率给定值再发送关机指令
            WriteTagConfig writeTag = new WriteTagConfig();
            writeTag.setAddress(pcsSetting.getPointAddress());
            writeTag.setValue(pcsSetting.getStopCommand());
            writeTags.add(writeTag);
        }
        return writeTags;
    }
}
