package com.usky.vpp.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.usky.common.core.bean.CommonPage;
import com.usky.common.core.exception.BusinessException;
import com.usky.common.security.utils.SecurityUtils;
import com.usky.vpp.domain.VppContract;
import com.usky.vpp.domain.VppCustomer;
import com.usky.vpp.domain.VppCustomerAccess;
import com.usky.vpp.domain.VppCustomerContact;
import com.usky.vpp.domain.VppDrInvitation;
import com.usky.vpp.domain.VppDrParticipation;
import com.usky.vpp.domain.VppSettlementBill;
import com.usky.vpp.domain.VppSite;
import com.usky.vpp.mapper.VppContractMapper;
import com.usky.vpp.mapper.VppCustomerAccessMapper;
import com.usky.vpp.mapper.VppCustomerContactMapper;
import com.usky.vpp.mapper.VppCustomerMapper;
import com.usky.vpp.mapper.VppDrInvitationMapper;
import com.usky.vpp.mapper.VppDrParticipationMapper;
import com.usky.vpp.mapper.VppSettlementBillMapper;
import com.usky.vpp.mapper.VppSiteMapper;
import com.usky.vpp.service.VppCustomerService;
import com.usky.vpp.service.VppSiteService;
import com.usky.vpp.service.vo.CustomerAccessAuditRequest;
import com.usky.vpp.service.vo.CustomerAccessRequestVO;
import com.usky.vpp.service.vo.CustomerAccessResponseVO;
import com.usky.vpp.service.vo.CustomerContactRequest;
import com.usky.vpp.service.vo.CustomerRequestVO;
import com.usky.vpp.service.vo.CustomerResponseVO;
import com.usky.vpp.util.VppAuditHelper;
import com.usky.vpp.util.VppContractStatusHelper;
import com.usky.vpp.util.VppFieldValidator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 客户管理业务实现
 *
 * @author fyc
 * @email yuchuan.fu@chinausky.com
 * @date 2026/7/6
 */
@Slf4j
@Service
public class VppCustomerServiceImpl implements VppCustomerService {

    private static final int ACCESS_PENDING = 0;
    private static final int ACCESS_PASSED = 1;
    private static final int ACCESS_REJECTED = 2;
    private static final int LIFECYCLE_ADMITTED = 1;

    @Autowired
    private VppCustomerMapper customerMapper;
    @Autowired
    private VppCustomerAccessMapper accessMapper;
    @Autowired
    private VppCustomerContactMapper contactMapper;
    @Autowired
    private VppContractMapper contractMapper;
    @Autowired
    private VppSiteMapper siteMapper;
    @Autowired
    private VppDrInvitationMapper invitationMapper;
    @Autowired
    private VppDrParticipationMapper participationMapper;
    @Autowired
    private VppSettlementBillMapper settlementBillMapper;

    // ==================== 准入管理 ====================

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean submitAccess(CustomerAccessRequestVO request) {
        validateAccessRequest(request);
        checkCustomerNameUnique(request.getCustomerName(), null);
        checkAccessCustomerNameUnique(request.getCustomerName(), null);

        VppCustomerAccess access = new VppCustomerAccess();
        access.setApplyNo(VppAuditHelper.nextApplyNo());
        applyAccessFromRequest(access, request);
        access.setAccessStatus(ACCESS_PENDING);
        access.setApplyAt(LocalDateTime.now());
        VppAuditHelper.fillCreate(access);

        return accessMapper.insert(access) > 0;
    }

    @Override
    public CommonPage<CustomerAccessResponseVO> pageAccess(Long id, Integer accessStatus, String customerName,
                                                           LocalDateTime startTime, LocalDateTime endTime,
                                                           Integer pageNum, Integer pageSize) {
        IPage<VppCustomerAccess> page = new Page<>(pageNum, pageSize);

        LambdaQueryWrapper<VppCustomerAccess> wrapper = new LambdaQueryWrapper<VppCustomerAccess>()
                .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
                .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(id != null, VppCustomerAccess::getId, id)
                .eq(accessStatus != null, VppCustomerAccess::getAccessStatus, accessStatus)
                .like(StringUtils.isNotBlank(customerName), VppCustomerAccess::getCustomerName, customerName)
                .ge(startTime != null, VppCustomerAccess::getApplyAt, startTime)
                .le(endTime != null, VppCustomerAccess::getApplyAt, endTime)
                .orderByDesc(VppCustomerAccess::getApplyAt);

        page = accessMapper.selectPage(page, wrapper);

        List<VppCustomerAccess> records = page.getRecords();
        if (records == null || records.isEmpty()) {
            return new CommonPage<>(Collections.emptyList(), page.getTotal(), pageSize, pageNum);
        }

        Map<Long, VppCustomer> customerMap = loadCustomerMap(records.stream()
                .map(VppCustomerAccess::getCustomerId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet()));
        Map<Long, VppCustomerContact> primaryContactMap = loadPrimaryContactMap(customerMap.keySet());

        List<CustomerAccessResponseVO> list = records.stream().map(entity -> {
            CustomerAccessResponseVO vo = new CustomerAccessResponseVO();
            BeanUtils.copyProperties(entity, vo);
            fillAccessContactInfo(vo, customerMap.get(entity.getCustomerId()),
                    primaryContactMap.get(entity.getCustomerId()));
            return vo;
        }).collect(Collectors.toList());

        return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void auditAccess(CustomerAccessAuditRequest request) {
        if (request.getId() == null) {
            throw new BusinessException("准入申请ID不能为空！");
        }
        if (request.getResult() == null) {
            throw new BusinessException("审核结果不能为空！");
        }
        Long id = request.getId();
        VppCustomerAccess access = findAccessById(id);

        if (access.getAccessStatus() != ACCESS_PENDING) {
            throw new BusinessException("当前状态不允许审核，仅待审核状态可操作！");
        }

        access.setAuditOpinion(request.getComment());
        access.setAuditAt(LocalDateTime.now());

        if (request.getResult() == ACCESS_PASSED) {
            if (access.getCustomerId() == null) {
                checkCustomerNameUnique(access.getCustomerName(), null);
                VppCustomer customer = createCustomerFromAccess(access);
                access.setCustomerId(customer.getId());
            } else {
                checkCustomerNameUnique(access.getCustomerName(), access.getCustomerId());
            }
            access.setAccessStatus(ACCESS_PASSED);
        } else {
            access.setAccessStatus(ACCESS_REJECTED);
        }

        VppAuditHelper.fillUpdate(access);
        accessMapper.updateById(access);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean updateAccess(CustomerAccessRequestVO request) {
        if (request.getId() == null) {
            throw new BusinessException("准入申请ID不能为空！");
        }
        validateAccessRequest(request);

        VppCustomerAccess access = findAccessById(request.getId());
        if (access.getAccessStatus() != ACCESS_REJECTED) {
            throw new BusinessException("仅已驳回的准入申请允许修改！");
        }
        checkCustomerNameUnique(request.getCustomerName(), access.getCustomerId());
        checkAccessCustomerNameUnique(request.getCustomerName(), access.getId());

        applyAccessFromRequest(access, request);
        access.setAccessStatus(ACCESS_PENDING);
        access.setAuditOpinion(null);
        access.setAuditAt(null);
        access.setApplyAt(LocalDateTime.now());

        if (access.getCustomerId() != null) {
            VppCustomer customer = findCustomerById(access.getCustomerId());
            applyCustomerFromAccessRequest(customer, request);
            VppAuditHelper.fillUpdate(customer);
            customerMapper.updateById(customer);
            upsertPrimaryContact(customer.getId(), request);
        }

        VppAuditHelper.fillUpdate(access);
        return accessMapper.updateById(access) > 0;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean deleteAccess(Long id) {
        VppCustomerAccess access = findAccessById(id);
        if (access.getAccessStatus() == ACCESS_PENDING) {
            throw new BusinessException("待审核的准入申请无法删除，请先审核或驳回后再操作！");
        }
        VppAuditHelper.fillSoftDelete(access);
        int result = accessMapper.updateById(access);
        return result > 0;
    }

    @Override
    public void exportAccess(String scope, String fileName, Long id, Integer accessStatus,
                             String customerName, LocalDateTime startTime, LocalDateTime endTime,
                             Integer current, Integer size, HttpServletResponse response) {
        List<VppCustomerAccess> accessList;

        switch (scope) {
            case "current":
                if (current == null || size == null) {
                    throw new BusinessException("current 模式下分页参数 current 和 size 不能为空！");
                }
                IPage<VppCustomerAccess> page = new Page<>(current, size);
                LambdaQueryWrapper<VppCustomerAccess> currWrapper = new LambdaQueryWrapper<VppCustomerAccess>()
                        .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
                        .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .orderByDesc(VppCustomerAccess::getApplyAt);
                page = accessMapper.selectPage(page, currWrapper);
                accessList = page.getRecords();
                break;

            case "all":
                LambdaQueryWrapper<VppCustomerAccess> allWrapper = new LambdaQueryWrapper<VppCustomerAccess>()
                        .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
                        .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .orderByDesc(VppCustomerAccess::getApplyAt);
                accessList = accessMapper.selectList(allWrapper);
                break;

            case "filtered":
                boolean hasFilter = id != null || accessStatus != null
                        || StringUtils.isNotBlank(customerName)
                        || startTime != null || endTime != null;
                if (!hasFilter) {
                    throw new BusinessException("filtered 模式下请至少提供一个筛选条件（id / accessStatus / customerName / startTime / endTime）！");
                }
                LambdaQueryWrapper<VppCustomerAccess> filteredWrapper = new LambdaQueryWrapper<VppCustomerAccess>()
                        .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
                        .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .eq(id != null, VppCustomerAccess::getId, id)
                        .eq(accessStatus != null, VppCustomerAccess::getAccessStatus, accessStatus)
                        .like(StringUtils.isNotBlank(customerName), VppCustomerAccess::getCustomerName, customerName)
                        .ge(startTime != null, VppCustomerAccess::getApplyAt, startTime)
                        .le(endTime != null, VppCustomerAccess::getApplyAt, endTime)
                        .orderByDesc(VppCustomerAccess::getApplyAt);
                accessList = accessMapper.selectList(filteredWrapper);
                break;

            default:
                throw new BusinessException("不支持的导出范围：" + scope + "，请使用 current / all / filtered");
        }

        if (accessList == null) {
            accessList = Collections.emptyList();
        }

        // 生成 Excel
        try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
            org.apache.poi.xssf.usermodel.XSSFSheet sheet = workbook.createSheet("准入申请数据");

            // 表头样式
            org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
            headerStyle.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_25_PERCENT.getIndex());
            headerStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
            org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
            headerFont.setBold(true);
            headerStyle.setFont(headerFont);

            // 表头
            String[] headers = {
                    "序号", "申请编号", "电力户号", "客户名称", "客户类型",
                    "供电公司", "用电容量(kW)", "信用状态", "审核状态",
                    "审核意见", "营业执照",
                    //"签约日期", "联系人姓名", "联系人电话", "联系人邮箱",
                    "申请时间", "审核时间", "创建时间"
            };
            org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
            for (int i = 0; i < headers.length; i++) {
                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(i);
                cell.setCellValue(headers[i]);
                cell.setCellStyle(headerStyle);
            }

            // 数据行
            int rowIdx = 1;
            for (VppCustomerAccess acc : accessList) {
                org.apache.poi.xssf.usermodel.XSSFRow row = sheet.createRow(rowIdx++);
                setCell(row, 0, rowIdx - 1);
                setCell(row, 1, acc.getApplyNo());
                setCell(row, 2, acc.getAccountNo());
                setCell(row, 3, acc.getCustomerName());
                setCell(row, 4, typeLabel(acc.getCustomerType(), "中高压", "低压商用", "居民充电桩", "自有资产"));
                setCell(row, 5, acc.getPowerCompany());
                setCell(row, 6, numberStr(acc.getContractCapacity()));
                setCell(row, 7, typeLabel(acc.getCreditStatus(), "正常", "关注", "不良"));
                setCell(row, 8, accessStatusLabel(acc.getAccessStatus()));
                setCell(row, 9, acc.getAuditOpinion());
                insertLicenseImage(workbook, sheet, row, 10, acc.getBusinessLicenseUrl());
                // setCell(row, 11, dateStr(acc.getSigningDate()));
                // setCell(row, 12, acc.getContactName());
                // setCell(row, 13, acc.getContactPhone());
                // setCell(row, 14, acc.getContactEmail());
                setCell(row, 11, dateTimeStr(acc.getApplyAt()));
                setCell(row, 12, dateTimeStr(acc.getAuditAt()));
                setCell(row, 13, dateTimeStr(acc.getCreateTime()));
            }

            // 营业执照列宽度
            sheet.setColumnWidth(10, 28 * 256);

            // 自动调整列宽
            for (int i = 0; i < headers.length; i++) {
                if (i == 10) {
                    continue;
                }
                sheet.autoSizeColumn(i);
                int width = sheet.getColumnWidth(i);
                if (width > 15000) {
                    sheet.setColumnWidth(i, 15000);
                }
            }

            // 写入响应
            String encodedFileName = java.net.URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8.name())
                    .replace("+", "%20");
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName
                    + "; filename*=UTF-8''" + encodedFileName);
            workbook.write(response.getOutputStream());
            response.getOutputStream().flush();

        } catch (IOException e) {
            throw new BusinessException("导出准入申请数据失败: " + e.getMessage());
        }
    }

    // ==================== 客户管理 ====================

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean createCustomer(CustomerRequestVO vo) {
        // 必填校验
        // if (StringUtils.isBlank(vo.getAccountNo())) {
        //     throw new BusinessException("电力户号不能为空！");
        // }
        if (StringUtils.isBlank(vo.getCustomerName())) {
            throw new BusinessException("客户名称不能为空！");
        }
        if (vo.getCustomerType() == null) {
            throw new BusinessException("客户类型不能为空！");
        }
        if (StringUtils.isBlank(vo.getPowerCompany())) {
            throw new BusinessException("供电公司不能为空！");
        }
        if (vo.getContractCapacity() == null) {
            throw new BusinessException("用电容量不能为空！");
        }

        // 电力户号全局唯一校验
        // checkAccountNoUnique(vo.getAccountNo(), null);
        checkCustomerNameUnique(vo.getAccountNo(), null);

        // 构建客户
        VppCustomer customer = new VppCustomer();
        customer.setAccountNo(vo.getAccountNo());
        customer.setCustomerName(vo.getCustomerName());
        customer.setCustomerType(vo.getCustomerType());
        customer.setPowerCompany(vo.getPowerCompany());
        customer.setContractCapacity(vo.getContractCapacity());
        customer.setCreditStatus(vo.getCreditStatus());
        customer.setBusinessLicenseUrl(vo.getBusinessLicenseUrl());
        customer.setLifecycleStatus(vo.getLifecycleStatus());
        customer.setDrNotifyMinutes(vo.getDrNotifyMinutes());
        customer.setDrUpCapacityKw(vo.getDrUpCapacityKw());
        customer.setDrDownCapacityKw(vo.getDrDownCapacityKw());
        customer.setProvince(vo.getProvince());
        customer.setIsGroupUser(vo.getIsGroupUser());
        customer.setIsGreenPower(vo.getIsGreenPower());
        customer.setIsCoopDevUser(vo.getIsCoopDevUser());
        customer.setCoopPartnerName(vo.getCoopPartnerName());
        customer.setCity(vo.getCity());
        customer.setDistrict(vo.getDistrict());
        customer.setAddress(vo.getAddress());
        customer.setRemark(vo.getRemark());
        customer.setIsVppResource(vo.getIsVppResource());
        customer.setVppCategory(vo.getVppCategory());
        customer.setDrResourceType(vo.getDrResourceType());
        customer.setVppProxyCode(vo.getVppProxyCode());
        customer.setIndustry(vo.getIndustry());
        customer.setIndustrySector(vo.getIndustrySector());
        customer.setRunningCapacity(vo.getRunningCapacity());
        customer.setProductionStartDate(vo.getProductionStartDate());
        customer.setProductionEndDate(vo.getProductionEndDate());
        customer.setPowerAddress(vo.getPowerAddress());
        customer.setSigningDate(vo.getSigningDate());
        VppAuditHelper.fillCreate(customer);
        customerMapper.insert(customer);

        // 若有联系人信息，创建主联系人
        if (StringUtils.isNotBlank(vo.getContactName())) {
            VppFieldValidator.validatePhone(vo.getContactPhone());
            if (StringUtils.isNotBlank(vo.getContactEmail())) {
                VppFieldValidator.validateEmail(vo.getContactEmail());
            }
            VppCustomerContact contact = new VppCustomerContact();
            contact.setCustomerId(customer.getId());
            contact.setContactName(vo.getContactName());
            contact.setContactPhone(vo.getContactPhone());
            contact.setContactEmail(vo.getContactEmail());
            contact.setIsPrimary(1);
            VppAuditHelper.fillCreate(contact);
            contactMapper.insert(contact);
        }

        return true;
    }

    @Override
    public CommonPage<CustomerResponseVO> pageCustomer(Long id, String customerName, String accountNo,
                                                       Integer lifecycleStatus, Integer customerType,
                                                       Integer pageNum, Integer pageSize) {
        IPage<VppCustomer> page = new Page<>(pageNum, pageSize);

        LambdaQueryWrapper<VppCustomer> wrapper = new LambdaQueryWrapper<VppCustomer>()
                .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
                .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(id != null, VppCustomer::getId, id)
                .like(StringUtils.isNotBlank(customerName), VppCustomer::getCustomerName, customerName)
                .eq(StringUtils.isNotBlank(accountNo), VppCustomer::getAccountNo, accountNo)
                .eq(lifecycleStatus != null, VppCustomer::getLifecycleStatus, lifecycleStatus)
                .eq(customerType != null, VppCustomer::getCustomerType, customerType)
                .orderByDesc(VppCustomer::getCreateTime);

        page = customerMapper.selectPage(page, wrapper);
        List<VppCustomer> records = page.getRecords();

        if (records == null || records.isEmpty()) {
            return new CommonPage<>(Collections.emptyList(), page.getTotal(), pageSize, pageNum);
        }

        // 批量查询关联合同
        final Set<Long> customerIds = records.stream()
                .map(VppCustomer::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());

        final Map<Long, List<VppContract>> contractMap;
        if (!customerIds.isEmpty()) {
            LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
            contractWrapper.in(VppContract::getCustomerId, customerIds)
                    .eq(VppContract::getDeleteFlag, 0)
                    .orderByDesc(VppContract::getCreateTime);
            List<VppContract> allContracts = contractMapper.selectList(contractWrapper);
            contractMap = allContracts.stream()
                    .collect(Collectors.groupingBy(VppContract::getCustomerId));
        } else {
            contractMap = Collections.emptyMap();
        }

        // 批量查询关联联系人（取主联系人 is_primary=1）
        final Map<Long, VppCustomerContact> contactMap;
        if (!customerIds.isEmpty()) {
            LambdaQueryWrapper<VppCustomerContact> contactWrapper = new LambdaQueryWrapper<>();
            contactWrapper.in(VppCustomerContact::getCustomerId, customerIds)
                    .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
                    .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                    .eq(VppCustomerContact::getIsPrimary, 1);
            List<VppCustomerContact> allContacts = contactMapper.selectList(contactWrapper);
            contactMap = allContacts.stream()
                    .collect(Collectors.toMap(
                            VppCustomerContact::getCustomerId,
                            Function.identity(),
                            (a, b) -> a));
        } else {
            contactMap = Collections.emptyMap();
        }

        // 组装响应VO
        List<CustomerResponseVO> list = records.stream().map(entity -> {
            CustomerResponseVO vo = new CustomerResponseVO();
            BeanUtils.copyProperties(entity, vo);
            // 设置关联合同
            List<VppContract> contracts = contractMap.getOrDefault(entity.getId(), Collections.emptyList());
            vo.setContracts(contracts.stream().map(this::toContractBrief).collect(Collectors.toList()));
            // 设置关联联系人
            VppCustomerContact contact = contactMap.get(entity.getId());
            if (contact != null) {
                vo.setContactName(contact.getContactName());
                vo.setContactPhone(contact.getContactPhone());
                vo.setContactEmail(contact.getContactEmail());
            }
            return vo;
        }).collect(Collectors.toList());

        return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
    }

    @Override
    public CustomerResponseVO getCustomer(Long id) {
        VppCustomer customer = findCustomerById(id);

        CustomerResponseVO vo = new CustomerResponseVO();
        BeanUtils.copyProperties(customer, vo);

        // 关联查询合同
        LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
        contractWrapper.eq(VppContract::getCustomerId, id)
                .eq(VppContract::getDeleteFlag, 0)
                .orderByDesc(VppContract::getCreateTime);
        List<VppContract> contracts = contractMapper.selectList(contractWrapper);
        vo.setContracts(contracts.stream().map(this::toContractBrief).collect(Collectors.toList()));

        // 关联查询主联系人
        LambdaQueryWrapper<VppCustomerContact> contactWrapper = new LambdaQueryWrapper<>();
        contactWrapper.eq(VppCustomerContact::getCustomerId, id)
                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(VppCustomerContact::getIsPrimary, 1);
        VppCustomerContact contact = contactMapper.selectOne(contactWrapper);
        if (contact != null) {
            vo.setContactName(contact.getContactName());
            vo.setContactPhone(contact.getContactPhone());
            vo.setContactEmail(contact.getContactEmail());
        }

        return vo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean updateCustomer(CustomerRequestVO vo) {
        if (vo.getId() == null) {
            throw new BusinessException("客户ID不能为空！");
        }
        Long id = vo.getId();
        VppCustomer existing = findCustomerById(id);

        // 仅更新非空字段
        if (StringUtils.isNotBlank(vo.getAccountNo())) {
            if (!vo.getAccountNo().equals(existing.getAccountNo())) {
                // checkAccountNoUnique(vo.getAccountNo(), id);
            }
            existing.setAccountNo(vo.getAccountNo());
        }
        if (StringUtils.isNotBlank(vo.getCustomerName())) {
            existing.setCustomerName(vo.getCustomerName());
        }
        if (vo.getCustomerType() != null) {
            existing.setCustomerType(vo.getCustomerType());
        }
        if (StringUtils.isNotBlank(vo.getPowerCompany())) {
            existing.setPowerCompany(vo.getPowerCompany());
        }
        if (vo.getContractCapacity() != null) {
            existing.setContractCapacity(vo.getContractCapacity());
        }
        if (vo.getCreditStatus() != null) {
            existing.setCreditStatus(vo.getCreditStatus());
        }
        if (vo.getLifecycleStatus() != null) {
            existing.setLifecycleStatus(vo.getLifecycleStatus());
        }
        if (vo.getDrNotifyMinutes() != null) {
            existing.setDrNotifyMinutes(vo.getDrNotifyMinutes());
        }
        if (vo.getDrUpCapacityKw() != null) {
            existing.setDrUpCapacityKw(vo.getDrUpCapacityKw());
        }
        if (vo.getDrDownCapacityKw() != null) {
            existing.setDrDownCapacityKw(vo.getDrDownCapacityKw());
        }
        if (vo.getProvince() != null) {
            existing.setProvince(vo.getProvince());
        }
        if (vo.getIsGroupUser() != null) {
            existing.setIsGroupUser(vo.getIsGroupUser());
        }
        if (vo.getIsGreenPower() != null) {
            existing.setIsGreenPower(vo.getIsGreenPower());
        }
        if (vo.getIsCoopDevUser() != null) {
            existing.setIsCoopDevUser(vo.getIsCoopDevUser());
        }
        if (vo.getCoopPartnerName() != null) {
            existing.setCoopPartnerName(vo.getCoopPartnerName());
        }
        if (vo.getCity() != null) {
            existing.setCity(vo.getCity());
        }
        if (vo.getDistrict() != null) {
            existing.setDistrict(vo.getDistrict());
        }
        if (vo.getAddress() != null) {
            existing.setAddress(vo.getAddress());
        }
        if (vo.getBusinessLicenseUrl() != null) {
            existing.setBusinessLicenseUrl(vo.getBusinessLicenseUrl());
        }
        if (vo.getRemark() != null) {
            existing.setRemark(vo.getRemark());
        }
        if (vo.getIsVppResource() != null) {
            existing.setIsVppResource(vo.getIsVppResource());
        }
        if (vo.getVppCategory() != null) {
            existing.setVppCategory(vo.getVppCategory());
        }
        if (vo.getDrResourceType() != null) {
            existing.setDrResourceType(vo.getDrResourceType());
        }
        if (vo.getVppProxyCode() != null) {
            existing.setVppProxyCode(vo.getVppProxyCode());
        }
        if (vo.getIndustry() != null) {
            existing.setIndustry(vo.getIndustry());
        }
        if (vo.getIndustrySector() != null) {
            existing.setIndustrySector(vo.getIndustrySector());
        }
        if (vo.getRunningCapacity() != null) {
            existing.setRunningCapacity(vo.getRunningCapacity());
        }
        if (vo.getProductionStartDate() != null) {
            existing.setProductionStartDate(vo.getProductionStartDate());
        }
        if (vo.getProductionEndDate() != null) {
            existing.setProductionEndDate(vo.getProductionEndDate());
        }
        if (vo.getPowerAddress() != null) {
            existing.setPowerAddress(vo.getPowerAddress());
        }
        if (vo.getSigningDate() != null) {
            existing.setSigningDate(vo.getSigningDate());
        }

        // 同步维护主联系人：如果传了联系人姓名，则 upsert 主联系人
        if (StringUtils.isNotBlank(vo.getContactName())) {
            // 校验联系人信息
            VppFieldValidator.validatePhone(vo.getContactPhone());
            if (StringUtils.isNotBlank(vo.getContactEmail())) {
                VppFieldValidator.validateEmail(vo.getContactEmail());
            }

            LambdaQueryWrapper<VppCustomerContact> contactWrapper = new LambdaQueryWrapper<>();
            contactWrapper.eq(VppCustomerContact::getCustomerId, id)
                    .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
                    .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                    .eq(VppCustomerContact::getIsPrimary, 1);
            VppCustomerContact contact = contactMapper.selectOne(contactWrapper);

            if (contact != null) {
                // 更新已有主联系人
                contact.setContactName(vo.getContactName());
                contact.setContactPhone(vo.getContactPhone());
                contact.setContactEmail(vo.getContactEmail());
                VppAuditHelper.fillUpdate(contact);
                contactMapper.updateById(contact);
            } else {
                // 新增主联系人
                contact = new VppCustomerContact();
                contact.setCustomerId(id);
                contact.setContactName(vo.getContactName());
                contact.setContactPhone(vo.getContactPhone());
                contact.setContactEmail(vo.getContactEmail());
                contact.setIsPrimary(1);
                VppAuditHelper.fillCreate(contact);
                contactMapper.insert(contact);
            }
        }

        VppAuditHelper.fillUpdate(existing);
        int result = customerMapper.updateById(existing);
        return result > 0;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean deleteCustomer(Long id) {
        VppCustomer customer = findCustomerById(id);
        assertCustomerDeletable(id);

        VppAuditHelper.fillSoftDelete(customer);
        int result = customerMapper.updateById(customer);
        return result > 0;
    }

    /**
     * 客户下存在合同、站点、邀约、参与记录等业务数据时不允许删除。
     */
    private void assertCustomerDeletable(Long customerId) {
        Integer contractCount = contractMapper.selectCount(new LambdaQueryWrapper<VppContract>()
                .eq(VppContract::getCustomerId, customerId)
                .eq(VppContract::getDeleteFlag, VppAuditHelper.NOT_DELETED));
        if (contractCount != null && contractCount > 0) {
            throw new BusinessException("该客户下存在合同，无法删除！");
        }

        Integer siteCount = siteMapper.selectCount(new LambdaQueryWrapper<VppSite>()
                .eq(VppSite::getCustomerId, customerId)
                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED));
        if (siteCount != null && siteCount > 0) {
            throw new BusinessException("该客户下存在站点，无法删除！");
        }

        Integer invitationCount = invitationMapper.selectCount(new LambdaQueryWrapper<VppDrInvitation>()
                .eq(VppDrInvitation::getCustomerId, customerId)
                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
        if (invitationCount != null && invitationCount > 0) {
            throw new BusinessException("该客户下存在需求响应邀约，无法删除！");
        }

        Integer participationCount = participationMapper.selectCount(new LambdaQueryWrapper<VppDrParticipation>()
                .eq(VppDrParticipation::getCustomerId, customerId)
                .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
        if (participationCount != null && participationCount > 0) {
            throw new BusinessException("该客户下存在需求响应参与记录，无法删除！");
        }

        Integer billCount = settlementBillMapper.selectCount(new LambdaQueryWrapper<VppSettlementBill>()
                .eq(VppSettlementBill::getCustomerId, customerId)
                .eq(VppSettlementBill::getDeleteFlag, VppAuditHelper.NOT_DELETED));
        if (billCount != null && billCount > 0) {
            throw new BusinessException("该客户下存在结算账单，无法删除！");
        }
    }

    @Override
    public void exportCustomers(String scope, String fileName, String customerName,
                                Integer lifecycleStatus, Integer current, Integer size,
                                HttpServletResponse response) {
        List<VppCustomer> customers;

        switch (scope) {
            case "current":
                // current 模式：必须传分页参数
                if (current == null || size == null) {
                    throw new BusinessException("current 模式下分页参数 current 和 size 不能为空！");
                }
                IPage<VppCustomer> page = new Page<>(current, size);
                LambdaQueryWrapper<VppCustomer> currWrapper = new LambdaQueryWrapper<VppCustomer>()
                        .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .orderByDesc(VppCustomer::getCreateTime);
                page = customerMapper.selectPage(page, currWrapper);
                customers = page.getRecords();
                break;

            case "all":
                // all 模式：当前租户下所有未删除客户
                LambdaQueryWrapper<VppCustomer> allWrapper = new LambdaQueryWrapper<VppCustomer>()
                        .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .orderByDesc(VppCustomer::getCreateTime);
                customers = customerMapper.selectList(allWrapper);
                break;

            case "filtered":
                // filtered 模式：必须至少传一个筛选条件
                boolean hasFilter = StringUtils.isNotBlank(customerName) || lifecycleStatus != null;
                if (!hasFilter) {
                    throw new BusinessException("filtered 模式下请至少提供一个筛选条件（customerName / lifecycleStatus）！");
                }
                LambdaQueryWrapper<VppCustomer> filteredWrapper = new LambdaQueryWrapper<VppCustomer>()
                        .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .like(StringUtils.isNotBlank(customerName), VppCustomer::getCustomerName, customerName)
                        .eq(lifecycleStatus != null, VppCustomer::getLifecycleStatus, lifecycleStatus)
                        .orderByDesc(VppCustomer::getCreateTime);
                customers = customerMapper.selectList(filteredWrapper);
                break;

            default:
                throw new BusinessException("不支持的导出范围：" + scope + "，请使用 current / all / filtered");
        }

        if (customers == null) {
            customers = Collections.emptyList();
        }

        // 批量查询关联主联系人
        final Set<Long> customerIds = customers.stream()
                .map(VppCustomer::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        final Map<Long, VppCustomerContact> contactMap = loadPrimaryContactMap(customerIds);

        // 生成 Excel
        try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
            org.apache.poi.xssf.usermodel.XSSFSheet sheet = workbook.createSheet("客户数据");

            // 创建表头样式
            org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
            headerStyle.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_25_PERCENT.getIndex());
            headerStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
            org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
            headerFont.setBold(true);
            headerStyle.setFont(headerFont);

            // 表头
            String[] headers = {
                    "序号", "客户名称", "客户类型", "联系人", "联系电话",
                    "供电单位", "用电容量", "信用状况", "企业地址", "营业执照"
            };
            org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
            for (int i = 0; i < headers.length; i++) {
                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(i);
                cell.setCellValue(headers[i]);
                cell.setCellStyle(headerStyle);
            }

            // 数据行
            int rowIdx = 1;
            for (VppCustomer c : customers) {
                org.apache.poi.xssf.usermodel.XSSFRow row = sheet.createRow(rowIdx++);
                VppCustomerContact contact = contactMap.get(c.getId());

                setCell(row, 0, rowIdx - 1);                       // 序号
                setCell(row, 1, c.getCustomerName());                // 客户名称
                setCell(row, 2, typeLabel(c.getCustomerType(), "虚拟电厂", "售电客户")); // 客户类型
                setCell(row, 3, contact != null ? contact.getContactName() : "");   // 联系人
                setCell(row, 4, contact != null ? contact.getContactPhone() : "");  // 联系电话
                setCell(row, 5, c.getPowerCompany());                // 供电单位
                setCell(row, 6, numberStr(c.getContractCapacity())); // 用电容量
                setCell(row, 7, typeLabel(c.getCreditStatus(), "正常", "关注", "不良")); // 信用状况
                setCell(row, 8, c.getAddress());                     // 企业地址
                insertLicenseImage(workbook, sheet, row, 9, c.getBusinessLicenseUrl()); // 营业执照
            }

            // 设置营业执照列宽度（固定宽度以适配图片）
            sheet.setColumnWidth(9, 28 * 256); // 约 28 字符宽

            // 自动调整列宽
            for (int i = 0; i < headers.length; i++) {
                if (i == 9) {
                    continue; // 营业执照列已手动设置宽度
                }
                sheet.autoSizeColumn(i);
                // 限制最大宽度
                int width = sheet.getColumnWidth(i);
                if (width > 15000) {
                    sheet.setColumnWidth(i, 15000);
                }
            }

            // 写入响应
            String encodedFileName = java.net.URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8.name())
                    .replace("+", "%20");
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName
                    + "; filename*=UTF-8''" + encodedFileName);
            workbook.write(response.getOutputStream());
            response.getOutputStream().flush();

        } catch (IOException e) {
            throw new BusinessException("导出客户数据失败: " + e.getMessage());
        }
    }

    // ==================== 联系人管理 ====================

    @Override
    public CommonPage<VppCustomerContact> listContact(Long customerId, Integer pageNum, Integer pageSize) {
        // 校验客户是否存在（租户内）
        findCustomerById(customerId);

        IPage<VppCustomerContact> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<VppCustomerContact> wrapper = new LambdaQueryWrapper<VppCustomerContact>()
                .eq(VppCustomerContact::getCustomerId, customerId)
                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .orderByDesc(VppCustomerContact::getIsPrimary)
                .orderByAsc(VppCustomerContact::getId);

        page = contactMapper.selectPage(page, wrapper);

        return new CommonPage<>(page.getRecords(), page.getTotal(), pageSize, pageNum);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public VppCustomerContact addContact(Long customerId, CustomerContactRequest request) {
        // 校验客户是否存在（租户内）
        findCustomerById(customerId);

        validateContactRequest(request);

        VppCustomerContact contact = new VppCustomerContact();
        contact.setCustomerId(customerId);
        contact.setContactName(request.getContactName());
        contact.setContactPhone(request.getContactPhone());
        contact.setContactEmail(request.getContactEmail());
        contact.setIsPrimary(request.getIsPrimary() == null ? 0 : request.getIsPrimary());
        contact.setPosition(request.getPosition());
        VppAuditHelper.fillCreate(contact);
        contactMapper.insert(contact);
        return contact;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateContact(Long customerId, Long contactId, CustomerContactRequest request) {
        VppCustomerContact contact = getContact(customerId, contactId);
        validateContactRequest(request);

        contact.setContactName(request.getContactName());
        contact.setContactPhone(request.getContactPhone());
        contact.setContactEmail(request.getContactEmail());
        if (request.getIsPrimary() != null) {
            contact.setIsPrimary(request.getIsPrimary());
        }
        contact.setPosition(request.getPosition());

        VppAuditHelper.fillUpdate(contact);
        contactMapper.updateById(contact);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteContact(Long customerId, Long contactId) {
        VppCustomerContact contact = getContact(customerId, contactId);
        VppAuditHelper.fillSoftDelete(contact);
        contactMapper.updateById(contact);
    }

    // ==================== 内部辅助方法 ====================

    private void applyAccessFromRequest(VppCustomerAccess access, CustomerAccessRequestVO request) {
        access.setAccountNo(request.getAccountNo());
        access.setCustomerName(request.getCustomerName());
        access.setCustomerType(request.getCustomerType());
        access.setPowerCompany(request.getPowerCompany());
        access.setContractCapacity(request.getContractCapacity());
        access.setBusinessLicenseUrl(request.getBusinessLicenseUrl());
        access.setCreditStatus(request.getCreditStatus());
        access.setContactName(request.getContactName());
        access.setContactPhone(request.getContactPhone());
        access.setContactEmail(request.getContactEmail());
        access.setAddress(request.getAddress());
    }

    private CustomerAccessRequestVO toAccessRequest(VppCustomerAccess access) {
        CustomerAccessRequestVO request = new CustomerAccessRequestVO();
        request.setAccountNo(access.getAccountNo());
        request.setCustomerName(access.getCustomerName());
        request.setCustomerType(access.getCustomerType());
        request.setPowerCompany(access.getPowerCompany());
        request.setContractCapacity(access.getContractCapacity());
        request.setBusinessLicenseUrl(access.getBusinessLicenseUrl());
        request.setCreditStatus(access.getCreditStatus());
        request.setContactName(access.getContactName());
        request.setContactPhone(access.getContactPhone());
        request.setContactEmail(access.getContactEmail());
        request.setAddress(access.getAddress());
        return request;
    }

    private VppCustomer createCustomerFromAccess(VppCustomerAccess access) {
        CustomerAccessRequestVO request = toAccessRequest(access);
        VppCustomer customer = buildCustomerFromAccessRequest(request);
        VppAuditHelper.fillCreate(customer);
        customerMapper.insert(customer);
        savePrimaryContact(customer.getId(), request);
        return customer;
    }

    private VppCustomer buildCustomerFromAccessRequest(CustomerAccessRequestVO request) {
        VppCustomer customer = new VppCustomer();
        applyCustomerFromAccessRequest(customer, request);
        customer.setLifecycleStatus(LIFECYCLE_ADMITTED);
        customer.setDrNotifyMinutes(30);
        customer.setSigningDate(LocalDateTime.now());
        return customer;
    }

    private void applyCustomerFromAccessRequest(VppCustomer customer, CustomerAccessRequestVO request) {
        customer.setAccountNo(request.getAccountNo());
        customer.setCustomerName(request.getCustomerName());
        customer.setCustomerType(request.getCustomerType());
        customer.setPowerCompany(request.getPowerCompany());
        customer.setContractCapacity(request.getContractCapacity());
        customer.setCreditStatus(request.getCreditStatus());
        customer.setBusinessLicenseUrl(request.getBusinessLicenseUrl());
        customer.setAddress(request.getAddress());
    }

    private void savePrimaryContact(Long customerId, CustomerAccessRequestVO request) {
        VppCustomerContact contact = new VppCustomerContact();
        contact.setCustomerId(customerId);
        contact.setContactName(request.getContactName());
        contact.setContactPhone(request.getContactPhone());
        contact.setContactEmail(request.getContactEmail());
        contact.setIsPrimary(1);
        VppAuditHelper.fillCreate(contact);
        contactMapper.insert(contact);
    }

    private void upsertPrimaryContact(Long customerId, CustomerAccessRequestVO request) {
        LambdaQueryWrapper<VppCustomerContact> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(VppCustomerContact::getCustomerId, customerId)
                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(VppCustomerContact::getIsPrimary, 1);
        VppCustomerContact contact = contactMapper.selectOne(wrapper);
        if (contact == null) {
            savePrimaryContact(customerId, request);
            return;
        }
        contact.setContactName(request.getContactName());
        contact.setContactPhone(request.getContactPhone());
        contact.setContactEmail(request.getContactEmail());
        VppAuditHelper.fillUpdate(contact);
        contactMapper.updateById(contact);
    }

    private Map<Long, VppCustomer> loadCustomerMap(Set<Long> customerIds) {
        if (customerIds == null || customerIds.isEmpty()) {
            return Collections.emptyMap();
        }
        return customerMapper.selectBatchIds(customerIds).stream()
                .filter(customer -> !VppAuditHelper.isDeleted(customer.getDeleteFlag()))
                .collect(Collectors.toMap(VppCustomer::getId, customer -> customer, (a, b) -> a));
    }

    private Map<Long, VppCustomerContact> loadPrimaryContactMap(Set<Long> customerIds) {
        if (customerIds == null || customerIds.isEmpty()) {
            return Collections.emptyMap();
        }
        LambdaQueryWrapper<VppCustomerContact> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(VppCustomerContact::getCustomerId, customerIds)
                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(VppCustomerContact::getIsPrimary, 1);
        return contactMapper.selectList(wrapper).stream()
                .collect(Collectors.toMap(VppCustomerContact::getCustomerId, contact -> contact, (a, b) -> a));
    }

    private void fillAccessContactInfo(CustomerAccessResponseVO vo, VppCustomer customer,
                                       VppCustomerContact contact) {
        if (StringUtils.isBlank(vo.getAddress()) && customer != null) {
            vo.setAddress(customer.getAddress());
        }
        if (StringUtils.isBlank(vo.getContactName()) && contact != null) {
            vo.setContactName(contact.getContactName());
            vo.setContactPhone(contact.getContactPhone());
            vo.setContactEmail(contact.getContactEmail());
        }
    }

    private CustomerResponseVO.ContractBriefVO toContractBrief(VppContract contract) {
        CustomerResponseVO.ContractBriefVO brief = new CustomerResponseVO.ContractBriefVO();
        brief.setId(contract.getId());
        brief.setContractNo(contract.getContractNo());
        brief.setContractName(contract.getContractName());
        brief.setContractType(contract.getContractType());
        brief.setContractStatus(VppContractStatusHelper.resolve(
                contract.getContractStatus(), contract.getEffectiveDate(), contract.getExpireDate()));
        brief.setSignDate(contract.getSignDate());
        brief.setEffectiveDate(contract.getEffectiveDate());
        brief.setExpireDate(contract.getExpireDate());
        return brief;
    }

    /**
     * 租户内查询客户（含删除检查）
     */
    private VppCustomer findCustomerById(Long id) {
        LambdaQueryWrapper<VppCustomer> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(VppCustomer::getId, id)
                .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId());
        VppCustomer customer = customerMapper.selectOne(wrapper);
        if (customer == null || VppAuditHelper.isDeleted(customer.getDeleteFlag())) {
            throw new BusinessException("客户不存在或已被删除！");
        }
        return customer;
    }

    /**
     * 租户内查询准入申请（含删除检查）
     */
    private VppCustomerAccess findAccessById(Long id) {
        LambdaQueryWrapper<VppCustomerAccess> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(VppCustomerAccess::getId, id)
                .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId());
        VppCustomerAccess access = accessMapper.selectOne(wrapper);
        if (access == null || VppAuditHelper.isDeleted(access.getDeleteFlag())) {
            throw new BusinessException("准入申请不存在或已被删除！");
        }
        return access;
    }

    private VppCustomerContact getContact(Long customerId, Long contactId) {
        LambdaQueryWrapper<VppCustomerContact> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(VppCustomerContact::getId, contactId)
                .eq(VppCustomerContact::getCustomerId, customerId)
                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId());
        VppCustomerContact contact = contactMapper.selectOne(wrapper);
        if (contact == null || VppAuditHelper.isDeleted(contact.getDeleteFlag())) {
            throw new BusinessException("联系人不存在或已被删除！");
        }
        return contact;
    }

    /**
     * 电力户号全局唯一校验（不限租户）
     */
    private void checkAccountNoUnique(String accountNo, Long excludeId) {
        LambdaQueryWrapper<VppCustomer> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(VppCustomer::getAccountNo, accountNo)
                .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .ne(excludeId != null, VppCustomer::getId, excludeId);
        Integer count = customerMapper.selectCount(wrapper);
        if (count != null && count > 0) {
            throw new BusinessException("电力户号已存在，请更换！");
        }
    }

    /**
     * 客户名称全局唯一校验（当前租户）
     */
    private void checkCustomerNameUnique(String customerName, Long excludeId) {
        LambdaQueryWrapper<VppCustomer> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(VppCustomer::getCustomerName, customerName)
                .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
                .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .ne(excludeId != null, VppCustomer::getId, excludeId);
        Integer count = customerMapper.selectCount(wrapper);
        if (count != null && count > 0) {
            throw new BusinessException("客户名称已存在，请更换！");
        }
    }

    /**
     * 待审核/已驳回准入申请的客户名称唯一校验（当前租户）
     */
    private void checkAccessCustomerNameUnique(String customerName, Long excludeAccessId) {
        LambdaQueryWrapper<VppCustomerAccess> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(VppCustomerAccess::getCustomerName, customerName)
                .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
                .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .in(VppCustomerAccess::getAccessStatus, ACCESS_PENDING, ACCESS_REJECTED)
                .ne(excludeAccessId != null, VppCustomerAccess::getId, excludeAccessId);
        Integer count = accessMapper.selectCount(wrapper);
        if (count != null && count > 0) {
            throw new BusinessException("客户名称已存在待处理准入申请，请更换！");
        }
    }

    private void validateAccessRequest(CustomerAccessRequestVO request) {
        if (request == null) {
            throw new BusinessException("准入申请参数不能为空！");
        }
        if (StringUtils.isBlank(request.getCustomerName())) {
            throw new BusinessException("客户名称不能为空！");
        }
        if (request.getCustomerType() == null) {
            throw new BusinessException("客户类型不能为空！");
        }
        if (StringUtils.isBlank(request.getPowerCompany())) {
            throw new BusinessException("供电公司不能为空！");
        }
        if (request.getContractCapacity() == null) {
            throw new BusinessException("用电容量不能为空！");
        }
        if (StringUtils.isBlank(request.getContactName())) {
            throw new BusinessException("联系人不能为空！");
        }
        if (StringUtils.isBlank(request.getContactPhone())) {
            throw new BusinessException("联系电话不能为空！");
        }
        VppFieldValidator.validatePhone(request.getContactPhone());
        if (StringUtils.isBlank(request.getAddress())) {
            throw new BusinessException("地址不能为空！");
        }
        if (StringUtils.isNotBlank(request.getContactEmail())) {
            VppFieldValidator.validateEmail(request.getContactEmail());
        }
    }

    private void validateContactRequest(CustomerContactRequest request) {
        if (request == null || StringUtils.isBlank(request.getContactName())) {
            throw new BusinessException("联系人姓名不能为空！");
        }
        if (StringUtils.isBlank(request.getContactPhone())) {
            throw new BusinessException("联系电话不能为空！");
        }
        VppFieldValidator.validatePhone(request.getContactPhone());
        if (StringUtils.isNotBlank(request.getContactEmail())) {
            VppFieldValidator.validateEmail(request.getContactEmail());
        }
    }

    // ==================== Excel 导出辅助方法 ====================

    private static void setCell(org.apache.poi.xssf.usermodel.XSSFRow row, int col, Object value) {
        if (value != null) {
            row.createCell(col).setCellValue(String.valueOf(value));
        } else {
            row.createCell(col).setCellValue("");
        }
    }

    /**
     * 下载营业执照图片并嵌入到 Excel 单元格中
     *
     * @param workbook 当前工作簿
     * @param sheet    当前工作表
     * @param row      当前行
     * @param col      目标列索引
     * @param imageUrl 营业执照图片 URL，为空或无效时写入"无"
     */
    private void insertLicenseImage(org.apache.poi.xssf.usermodel.XSSFWorkbook workbook,
                                    org.apache.poi.xssf.usermodel.XSSFSheet sheet,
                                    org.apache.poi.xssf.usermodel.XSSFRow row,
                                    int col, String imageUrl) {
        if (org.apache.commons.lang3.StringUtils.isBlank(imageUrl)) {
            row.createCell(col).setCellValue("无");
            return;
        }

        log.info("开始下载营业执照图片, URL: {}", imageUrl);

        // 如果是相对路径（以 / 开头），尝试补全为本地文件路径或跳过
        if (imageUrl.startsWith("/")) {
            log.warn("营业执照URL是相对路径，无法通过HTTP下载: {}", imageUrl);
            row.createCell(col).setCellValue("无");
            return;
        }

        // 校验 URL 格式
        if (!imageUrl.startsWith("http://") && !imageUrl.startsWith("https://")) {
            log.warn("营业执照URL格式不正确: {}", imageUrl);
            row.createCell(col).setCellValue("无");
            return;
        }

        byte[] imageBytes = null;
        try {
            imageBytes = downloadImage(imageUrl);
            log.info("图片下载成功, 大小: {} bytes, URL: {}", imageBytes.length, imageUrl);
        } catch (Exception e) {
            log.error("下载营业执照图片失败, URL: {}, 错误: {}", imageUrl, e.getMessage(), e);
            row.createCell(col).setCellValue("加载失败");
            return;
        }

        if (imageBytes == null || imageBytes.length == 0) {
            row.createCell(col).setCellValue("无");
            return;
        }

        try {
            int pictureType = detectImageType(imageBytes);
            log.info("图片类型检测结果: {}, URL: {}", pictureType, imageUrl);
            int pictureIdx = workbook.addPicture(imageBytes, pictureType);

            org.apache.poi.xssf.usermodel.XSSFDrawing drawing = sheet.createDrawingPatriarch();
            int emuPerPixel = org.apache.poi.util.Units.EMU_PER_PIXEL;
            // 全参构造：dx1, dy1, dx2, dy2, col1, row1, col2, row2
            // dx/dy 设 2 像素微边距，图片紧贴单元格边界
            org.apache.poi.xssf.usermodel.XSSFClientAnchor anchor =
                    new org.apache.poi.xssf.usermodel.XSSFClientAnchor(
                            2 * emuPerPixel, 2 * emuPerPixel,   // dx1, dy1 左/上边距
                            2 * emuPerPixel, 2 * emuPerPixel,   // dx2, dy2 右/下边距
                            col, row.getRowNum(),                // col1, row1
                            col + 1, row.getRowNum() + 1         // col2, row2
                    );
            anchor.setAnchorType(org.apache.poi.ss.usermodel.ClientAnchor.AnchorType.MOVE_AND_RESIZE);

            drawing.createPicture(anchor, pictureIdx);

            if (row.getHeight() < 2000) {
                row.setHeight((short) 2000);
            }
        } catch (Exception e) {
            log.error("营业执照图片插入Excel失败, URL: {}, 错误: {}", imageUrl, e.getMessage(), e);
            row.createCell(col).setCellValue("图片插入失败");
        }
    }

    /**
     * 通过 HTTP 下载图片，返回字节数组
     */
    private static byte[] downloadImage(String imageUrl) throws IOException {
        URL url = new URL(imageUrl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(10000);
        conn.setReadTimeout(30000);
        conn.setRequestMethod("GET");
        conn.setDoInput(true);
        conn.setInstanceFollowRedirects(true);
        conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
        conn.connect();

        int responseCode = conn.getResponseCode();
        if (responseCode != HttpURLConnection.HTTP_OK) {
            conn.disconnect();
            throw new IOException("下载图片失败，HTTP " + responseCode + ", URL: " + imageUrl);
        }

        String contentType = conn.getContentType();
        if (contentType != null && !contentType.startsWith("image/")) {
            conn.disconnect();
            throw new IOException("返回的不是图片类型: " + contentType + ", URL: " + imageUrl);
        }

        try (InputStream is = conn.getInputStream();
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[8192];
            int len;
            while ((len = is.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            return bos.toByteArray();
        } finally {
            conn.disconnect();
        }
    }

    /**
     * 根据图片字节前几个魔术字节判断图片类型
     */
    private static int detectImageType(byte[] bytes) {
        if (bytes == null || bytes.length < 3) {
            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG;
        }
        // PNG: 89 50 4E 47
        if (bytes[0] == (byte) 0x89 && bytes[1] == (byte) 0x50
                && bytes[2] == (byte) 0x4E && bytes[3] == (byte) 0x47) {
            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_PNG;
        }
        // JPEG: FF D8 FF
        if (bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD8 && bytes[2] == (byte) 0xFF) {
            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG;
        }
        // GIF: 47 49 46
        if (bytes[0] == (byte) 0x47 && bytes[1] == (byte) 0x49 && bytes[2] == (byte) 0x46) {
            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG; // POI 4.x 用 JPEG 兜底
        }
        // BMP: 42 4D
        if (bytes[0] == (byte) 0x42 && bytes[1] == (byte) 0x4D) {
            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG; // POI 4.x 用 JPEG 兜底
        }
        return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG;
    }

    private static String typeLabel(Integer code, String... labels) {
        if (code == null || code < 1 || code > labels.length) {
            return "";
        }
        return labels[code - 1];
    }

    private static String accessStatusLabel(Integer code) {
        if (code == null || code < 0 || code > 2) {
            return "";
        }
        switch (code) {
            case 0:
                return "待审核";
            case 1:
                return "已通过";
            case 2:
                return "已驳回";
            default:
                return "";
        }
    }

    private static String numberStr(Object value) {
        return value == null ? "" : value.toString();
    }

    private static String timeStr(Object value) {
        return value == null ? "" : value.toString();
    }

    private static String dateStr(Object value) {
        return value == null ? "" : value.toString();
    }

    private static String dateTimeStr(Object value) {
        return value == null ? "" : value.toString();
    }
}
