package com.ylx.massage.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ylx.address.domain.TAddress;
import com.ylx.address.mapper.TAddressMapper;
import com.ylx.address.service.TAddressService;
import com.ylx.attendanceconfig.domain.AttendanceRule;
import com.ylx.attendanceconfig.mapper.AttendanceRuleMapper;
import com.ylx.collect.service.CollectService;
import com.ylx.common.core.domain.entity.SysDictData;
import com.ylx.common.core.domain.model.LoginUser;
import com.ylx.common.exception.ServiceException;
import com.ylx.common.utils.*;
import com.ylx.fareSetting.service.IMaProjectFareSettingService;
import com.ylx.home.category.domain.dto.MerchantRecommendDto;
import com.ylx.massage.domain.*;
import com.ylx.massage.domain.dto.*;
import com.ylx.massage.domain.vo.*;
import com.ylx.massage.enums.*;
import com.ylx.massage.mapper.*;
import com.ylx.massage.service.IMaTechnicianService;
import com.ylx.merchant.domain.dto.MerchantDetailDTO;
import com.ylx.merchant.domain.dto.MerchantListDTO;
import com.ylx.merchant.domain.dto.MerchantProjectDTO;
import com.ylx.merchant.domain.vo.MerchantDetailVO;
import com.ylx.merchant.domain.vo.MerchantListVO;
import com.ylx.merchant.domain.vo.MerchantWithAddressVO;
import com.ylx.order.domain.TOrder;
import com.ylx.order.mapper.TOrderMapper;
import com.ylx.project.domain.Project;
import com.ylx.project.domain.bookMerchant.vo.ProjectInfoVO;
import com.ylx.project.mapper.ProjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.ylx.massage.enums.FileTypeEnum.*;
import static com.ylx.order.constant.OrderConstant.DICT_UNIT_TYPE;

/**
 * 技师Service业务层处理
 *
 * @author ylx
 * @date 2024-03-22
 */
@Slf4j
@Service
public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaTechnician> implements IMaTechnicianService {
    private static final int SERVICE_STATE_AVAILABLE = 1;
    private static final int POST_STATE_OFFLINE = 0;
    private static final int ENABLED = 1;
    private static final int AUDIT_APPROVED = 2;
    private static final int AUDIT_WAIT_ENTER = 0;
    private static final int AUDIT_WAIT_REVIEW = 1;
    private static final int AUDIT_REJECTED = 3;
    private static final int AUDIT_REMARK_MAX_LENGTH = 500;
    private static final int NS_STATUS_NOT_ON_DUTY = -1;
    private static final int DEFAULT_STAT_VALUE = 0;
    private static final Integer NOT_DELETED = 0;
    private static final Integer MERCHANT_STATUS_NORMAL = 0;
    private static final String PASSWORD = "123456";
    private static final int PROFILE_AUDIT_PENDING = 0;
    private static final int PROFILE_AUDIT_APPROVED = 1;
    private static final int PROFILE_AUDIT_REJECTED = 2;
    private static final Set<String> PROFILE_FILE_TYPES = new LinkedHashSet<>(Arrays.asList(
            PORTRAIT.getCode(),
            LIFE_PHOTO.getCode(),
            PROMOTION_VIDEO.getCode(),
            ID_CARD_FRONT.getCode(),
            ID_CARD_BACK.getCode(),
            ID_CARD_HANDHELD.getCode(),
            HEALTH_CERT.getCode(),
            QUALIFICATION_CERT.getCode(),
            NO_CRIME_RECORD.getCode(),
            COMMITMENT_LETTER.getCode(),
            COMMITMENT_AUDIO.getCode(),
            COMMITMENT_VIDEO.getCode()
    ));

    @Resource
    private MaTechnicianMapper maTechnicianMapper;
    @Resource
    private TWxUserMapper tWxUserMapper;
    @Resource
    private MaTeProjectMapper maTeProjectMapper;
    @Resource
    private MaProjectMapper maProjectMapper;
    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ContractRecordMapper contractRecordMapper;
    @Resource
    private MerchantDailyAttendanceMapper merchantDailyAttendanceMapper;
    @Resource
    private AttendanceRuleMapper attendanceRuleMapper;
    @Resource
    private TAddressMapper addressMapper;
    @Resource
    private MerchantApplyFileMapper merchantApplyFileMapper;
    @Resource
    private TOrderMapper orderMapper;
    @Resource
    private CityOperationApplicationMapper cityOperationApplicationMapper;
    @Resource
    private IMaProjectFareSettingService maProjectFareSettingService;
    @Resource
    private TAddressService addressService;
    @Resource
    private CollectService collectService;
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    private static final Map<String, String> unitDictCache = new HashMap<>();

    static {
        List<SysDictData> dictList = DictUtils.getSortedDictCache(DICT_UNIT_TYPE);
        if (CollUtil.isNotEmpty(dictList)) {
            dictList.forEach(dict -> unitDictCache.put(dict.getDictValue(), dict.getDictLabel()));
        } else {
            log.warn("项目计量单位字典未加载，请检查字典配置：{}", DICT_UNIT_TYPE);
        }
    }

    /**
     * 商户入驻申请注册
     *
     * @param req 申请参数
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void apply(MaTechnicianAppAddVo req) {
        String phone = req.getTePhone();
        //商户入住前置条件校验
        getMaTechnician(req, phone);
        MaTechnician maTechnician = new MaTechnician();
        BeanUtils.copyProperties(req, maTechnician);
        //通过openid校验商户是否存在
        LambdaQueryWrapper<MaTechnician> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(MaTechnician::getCOpenid, req.getCOpenid());
        MaTechnician maTechnician1 = maTechnicianMapper.selectOne(queryWrapper);
        if (maTechnician1 == null) {
            throw new RuntimeException("商户不存在");
        }
        //添加城市管理地址
        insertCity(req, maTechnician1, maTechnician);
    }

    /**
     * 添加城市管理地址
     *
     * @param req
     * @param maTechnician1
     * @param maTechnician
     */
    private void insertCity(MaTechnicianAppAddVo req, MaTechnician maTechnician1, MaTechnician maTechnician) {
        //商户类型默认为真实商户
        maTechnician.setTechType(0);
        maTechnician.setCreateBy("admin");
        // 审核状态：待入驻
        maTechnician.setAuditStatus(AUDIT_WAIT_ENTER);
        maTechnicianMapper.insert(maTechnician);

        TWxUser wxUser = new TWxUser();
        // 初始化加密工具
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        //密码默认123456
        wxUser.setCPassword(encoder.encode(PASSWORD));
        wxUser.setRole(1);
        LambdaUpdateWrapper<TWxUser> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(TWxUser::getcOpenid, req.getCOpenid());
        tWxUserMapper.update(wxUser, updateWrapper);

        /*LambdaUpdateWrapper<MaTechnician> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(MaTechnician::getId, maTechnician1.getId());
        maTechnicianMapper.update(maTechnician, updateWrapper);*/
        CityOperationApplication cityOperationApplication = new CityOperationApplication();
        cityOperationApplication.setMerchantId(maTechnician1.getId());
        cityOperationApplication.setProvinceCode(req.getProvinceCode());
        cityOperationApplication.setProvinceName(req.getProvinceName());
        cityOperationApplication.setCityCode(req.getCityCode());
        cityOperationApplication.setCityName(req.getCityName());
        cityOperationApplication.setOperationCenterId(req.getOperationCenterId());
        cityOperationApplication.setOperationCenterName(req.getOperationCenterName());
        cityOperationApplication.setCreateBy(maTechnician1.getId().toString());
        cityOperationApplication.setCreateTime(LocalDateTime.now());
        cityOperationApplicationMapper.insert(cityOperationApplication);
    }

    /**
     * 商户申请入驻
     *
     * @param req
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void applyFile(MerchantApplyFileRequestDto req) {
        if (req == null) {
            throw new ServiceException("上传参数不能为空");
        }
        Integer merchantId = resolveMerchantId(req.getTechnician());
        List<MerchantApplyFileDto> files = resolveApplyFiles(req.getReq());
        if (CollectionUtils.isEmpty(files)) {
            throw new ServiceException("请上传申请入驻文件");
        }
        replaceApplyFiles(merchantId, files);
        updateTechnicianBaseInfo(req.getTechnician());
    }

    /**
     * 将文件分组格式转换为单文件记录。
     *
     * @param reqFiles 入驻资料文件
     * @return List<MerchantApplyFileDto> 转换后的文件记录
     */
    private List<MerchantApplyFileDto> resolveApplyFiles(List<MerchantApplyFileDto> reqFiles) {
        if (CollectionUtils.isEmpty(reqFiles)) {
            return Collections.emptyList();
        }
        List<MerchantApplyFileDto> files = new ArrayList<>();
        for (MerchantApplyFileDto item : reqFiles) {
            if (item == null) {
                throw new ServiceException("入驻资料文件不能为空");
            }
            checkApplyFileGroupParam(item);
            for (MerchantApplyFileDto child : item.getFiles()) {
                files.add(buildApplyFileFromGroup(item, child));
            }
        }
        return files;
    }

    /**
     * 校验文件组参数是否符合要求。
     *
     * @param group
     */
    private void checkApplyFileGroupParam(MerchantApplyFileDto group) {
        if (StringUtils.isBlank(group.getFileType())) {
            throw new ServiceException("文件类型不能为空");
        }
        if (CollectionUtils.isEmpty(group.getFiles())) {
            throw new ServiceException("文件列表不能为空");
        }
    }

    private MerchantApplyFileDto buildApplyFileFromGroup(MerchantApplyFileDto group, MerchantApplyFileDto child) {
        if (child == null) {
            throw new ServiceException("入驻资料文件不能为空");
        }
        MerchantApplyFileDto file = new MerchantApplyFileDto();
        BeanUtils.copyProperties(child, file);
        file.setFiles(null);
        if (StringUtils.isBlank(file.getFileType())) {
            file.setFileType(group.getFileType());
        } else if (!group.getFileType().equals(file.getFileType())) {
            throw new ServiceException("同一组文件类型必须一致");
        }
        checkApplyFileParam(file);
        return file;
    }

    /**
     * 按本次提交的文件类型整组替换旧文件。
     *
     * @param merchantId 商户ID
     * @param files      入驻资料文件
     */
    private void replaceApplyFiles(Integer merchantId, List<MerchantApplyFileDto> files) {
        Map<String, List<MerchantApplyFileDto>> filesByType = files.stream()
                .collect(Collectors.groupingBy(MerchantApplyFileDto::getFileType, LinkedHashMap::new, Collectors.toList()));
        for (Map.Entry<String, List<MerchantApplyFileDto>> entry : filesByType.entrySet()) {
            LambdaQueryWrapper<MerchantApplyFile> deleteWrapper = new LambdaQueryWrapper<>();
            deleteWrapper.eq(MerchantApplyFile::getMerchantId, merchantId).eq(MerchantApplyFile::getFileType, entry.getKey());
            merchantApplyFileMapper.delete(deleteWrapper);
            for (MerchantApplyFileDto file : entry.getValue()) {
                MerchantApplyFile applyFile = new MerchantApplyFile();
                BeanUtils.copyProperties(file, applyFile);
                applyFile.setMerchantId(merchantId);
                applyFile.setCreateBy(merchantId.toString());
                applyFile.setUpdateBy(merchantId.toString());
                applyFile.setIsDelete(NOT_DELETED);
                merchantApplyFileMapper.insert(applyFile);
            }
        }
    }

    /**
     * 修改商户资料。
     * <p>
     * 基础信息只允许修改昵称和简介；入驻资料文件按本次提交的文件类型整组替换。
     * </p>
     *
     * @param req 修改参数
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateTechnician(MerchantApplyFileRequestDto req) {
        if (req == null) {
            throw new ServiceException("修改参数不能为空");
        }
        MaTechnician technician = req.getTechnician();
        MerchantProfileSubmitDTO submitDTO = new MerchantProfileSubmitDTO();
        submitDTO.setMerchantId(resolveMerchantId(technician));
        if (technician != null) {
            submitDTO.setNickName(technician.getTeNickName());
            submitDTO.setBrief(technician.getTeBrief());
        }
        // 处理入驻资料文件
        submitDTO.setFiles(req.getReq());
        submitMerchantProfile(submitDTO);
    }

    @Override
    public MerchantProfileVO getMerchantProfile(String openid) {
        MaTechnician merchant = getExistingMerchant(openid);
        List<MerchantApplyFile> files = listMerchantApplyFiles(merchant.getId());

        MerchantProfileVO profile = new MerchantProfileVO();
        profile.setMerchantId(merchant.getId());
        profile.setName(merchant.getTeName());
        profile.setSex(merchant.getTeSex());
        profile.setPhone(merchant.getTePhone());
        profile.setAddress(merchant.getTeAddress());
        profile.setAreaCode(merchant.getTeAreaCode());
        profile.setAvatar(merchant.getAvatar());
        profile.setServiceTag(merchant.getServiceTag());
        profile.setAuditStatus(merchant.getAuditStatus());

        LambdaQueryWrapper<CityOperationApplication> query1 = new LambdaQueryWrapper<>();
        query1.eq(CityOperationApplication::getMerchantId, merchant.getId());
        //query1.eq(CityOperationApplication::getStatus,1);
        query1.orderByDesc(CityOperationApplication::getCreateTime).last("limit 1");
        CityOperationApplication application = cityOperationApplicationMapper.selectOne(query1);
        profile.setProvinceCode(application.getProvinceCode());
        profile.setProvinceName(application.getProvinceName());
        profile.setCityCode(application.getCityCode());
        profile.setCityName(application.getCityName());
        profile.setOperationCenterId(application.getOperationCenterId());
        profile.setOperationCenterName(application.getOperationCenterName());

        profile.setNickName(buildTextItem(merchant.getTeNickName(), merchant.getPendingTeNickName(),
                merchant.getTeNickNameAuditStatus(), merchant.getTeNickNameAuditRemark()));
        profile.setBrief(buildTextItem(merchant.getTeBrief(), merchant.getPendingTeBrief(),
                merchant.getTeBriefAuditStatus(), merchant.getTeBriefAuditRemark()));
        profile.setFileGroups(buildFileGroups(files));
        return profile;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitMerchantProfile(MerchantProfileSubmitDTO dto) {
        if (dto == null) {
            throw new ServiceException("修改参数不能为空");
        }
        checkProfileSubmitParam(dto);
        MaTechnician merchant = getExistingMerchant1(dto.getMerchantId());
        boolean changed = submitProfileTextFields(merchant, dto);

        List<MerchantApplyFileDto> files = resolveApplyFiles(dto.getFiles());
        if (!CollectionUtils.isEmpty(files)) {
            submitProfileFiles(merchant.getId(), files);
            changed = true;
        }
        if (!changed) {
            throw new ServiceException("请至少提交一项资料修改");
        }
    }

    private void checkProfileSubmitParam(MerchantProfileSubmitDTO dto) {
        if (dto.getNickName() != null) {
            checkProfileTextValue(dto.getNickName(), "昵称");
        }
        if (dto.getBrief() != null) {
            checkProfileTextValue(dto.getBrief(), "简介");
        }
        resolveApplyFiles(dto.getFiles());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int auditMerchantProfile(Integer merchantId, MerchantProfileAuditDTO dto, LoginUser loginUser) {
        // 检查merchantId是否为空
        if (merchantId == null) {
            throw new ServiceException("商户ID不能为空");
        }
        // 检查dto参数是否为空
        if (dto == null) {
            throw new ServiceException("审核参数不能为空");
        }
        // 检查商户是否存在
        MaTechnician merchant = getExistingMerchant1(merchantId);
        if (merchant == null) {
            throw new ServiceException("商户不存在");
        }
        if ((AUDIT_APPROVED == dto.getAuditStatus() || AUDIT_REJECTED == dto.getAuditStatus())) {
            String profileAuditRemark = dto.getAuditRemark() == null ? "" : dto.getAuditRemark().trim();
            if (AUDIT_REJECTED == dto.getAuditStatus() && StringUtils.isBlank(profileAuditRemark)) {
                throw new ServiceException("审核驳回时审核备注不能为空");
            }
            if (AUDIT_APPROVED == dto.getAuditStatus()) {
                checkProfileAuditExpirationDates(dto);
                // 审核通过所有待审核资料
                return approveAllPendingProfile(merchant, dto, profileAuditRemark, loginUser);
            }
            // 审核驳回所有待审核资料
            return rejectAllPendingProfile(merchant, profileAuditRemark, loginUser);
        }
        return 0;
    }

    /**
     * 检查审核通过时，是否填写了身份证到期日期、健康证到期日期、从业资格证到期日期
     *
     * @param dto
     */
    private void checkProfileAuditExpirationDates(MerchantProfileAuditDTO dto) {
        if (dto.getIdCardExpirationDate() == null
                || dto.getHealthCertificateExpirationDate() == null
                || dto.getQualificationCertificateExpirationDate() == null) {
            throw new ServiceException("审核通过时，身份证到期日期、健康证到期日期、从业资格证到期日期不能为空");
        }
    }

    /**
     * 审核通过所有待审核资料
     *
     * @param merchant
     * @param dto
     * @param auditRemark
     * @param loginUser
     * @return int
     */
    private int approveAllPendingProfile(MaTechnician merchant, MerchantProfileAuditDTO dto, String auditRemark, LoginUser loginUser) {
        List<MerchantApplyFile> pendingFiles = listPendingProfileFiles(merchant.getId());
        boolean hasPendingText = hasPendingProfileText(merchant);
        if (!hasPendingText && CollectionUtils.isEmpty(pendingFiles)) {
            throw new ServiceException("当前商户没有待审核资料");
        }
        int rows = approveProfileTechnicianInfo(merchant, dto, auditRemark, loginUser);
        if (!CollectionUtils.isEmpty(pendingFiles)) {
            rows += approvePendingProfileFiles(merchant.getId(), pendingFiles, auditRemark, loginUser);
        }
        //同时修改ma_technician表的审核状态
        LambdaUpdateWrapper<MaTechnician> updateWrapper = new LambdaUpdateWrapper<MaTechnician>();
        updateWrapper.eq(MaTechnician::getId, merchant.getId()).set(MaTechnician::getAuditStatus, dto.getAuditStatus());
        rows += maTechnicianMapper.update(null, updateWrapper);
        return rows;
    }

    /**
     * 审核驳回所有待审核资料
     *
     * @param merchant
     * @param auditRemark
     * @param loginUser
     * @return int
     */
    private int rejectAllPendingProfile(MaTechnician merchant, String auditRemark, LoginUser loginUser) {
        List<MerchantApplyFile> pendingFiles = listPendingProfileFiles(merchant.getId());
        boolean hasPendingText = hasPendingProfileText(merchant);
        if (!hasPendingText && CollectionUtils.isEmpty(pendingFiles)) {
            throw new ServiceException("当前商户没有待审核资料");
        }
        int rows = 0;
        if (hasPendingText) {
            rows += rejectProfileTextInfo(merchant, auditRemark, loginUser);
        }
        if (!CollectionUtils.isEmpty(pendingFiles)) {
            rows += rejectPendingProfileFiles(merchant.getId(), auditRemark, loginUser);
        }
        //同时修改ma_technician表的审核状态
        LambdaUpdateWrapper<MaTechnician> updateWrapper = new LambdaUpdateWrapper<MaTechnician>();
        updateWrapper.eq(MaTechnician::getId, merchant.getId()).set(MaTechnician::getAuditStatus, AUDIT_REJECTED);
        rows += maTechnicianMapper.update(null, updateWrapper);
        return rows;
    }

    /**
     * 检查商户是否有待审核资料(昵称或简介)
     *
     * @param merchant
     * @return boolean
     */
    private boolean hasPendingProfileText(MaTechnician merchant) {
        return Integer.valueOf(PROFILE_AUDIT_PENDING).equals(merchant.getTeNickNameAuditStatus())
                || Integer.valueOf(PROFILE_AUDIT_PENDING).equals(merchant.getTeBriefAuditStatus());
    }

    /**
     * 获取待审核的申请入驻文件资料
     *
     * @param merchantId
     * @return List<MerchantApplyFile>
     */
    private List<MerchantApplyFile> listPendingProfileFiles(Integer merchantId) {
        LambdaQueryWrapper<MerchantApplyFile> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(MerchantApplyFile::getMerchantId, merchantId).eq(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_PENDING);
        return merchantApplyFileMapper.selectList(queryWrapper);
    }

    /**
     * 审核通过所有待审核资料(昵称或简介)
     *
     * @param merchant
     * @param dto
     * @param auditRemark
     * @param loginUser
     * @return int
     */
    private int approveProfileTechnicianInfo(MaTechnician merchant, MerchantProfileAuditDTO dto, String auditRemark, LoginUser loginUser) {
        LambdaUpdateWrapper<MaTechnician> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.eq(MaTechnician::getId, merchant.getId())
                .set(MaTechnician::getIdCardExpirationDate, dto.getIdCardExpirationDate())
                .set(MaTechnician::getHealthCertificateExpirationDate, dto.getHealthCertificateExpirationDate())
                .set(MaTechnician::getQualificationCertificateExpirationDate, dto.getQualificationCertificateExpirationDate())
                .set(MaTechnician::getUpdateBy, getAuditUserName(loginUser))
                .set(MaTechnician::getUpdateTime, DateUtils.getNowDate());

        // 审核通过昵称
        if (Integer.valueOf(PROFILE_AUDIT_PENDING).equals(merchant.getTeNickNameAuditStatus())) {
            updateWrapper.set(MaTechnician::getTeNickName, merchant.getPendingTeNickName())
                    .set(MaTechnician::getPendingTeNickName, null)
                    .set(MaTechnician::getTeNickNameAuditStatus, PROFILE_AUDIT_APPROVED)
                    .set(MaTechnician::getTeNickNameAuditRemark, auditRemark);
        }
        // 审核通过简介
        if (Integer.valueOf(PROFILE_AUDIT_PENDING).equals(merchant.getTeBriefAuditStatus())) {
            updateWrapper.set(MaTechnician::getTeBrief, merchant.getPendingTeBrief())
                    .set(MaTechnician::getPendingTeBrief, null)
                    .set(MaTechnician::getTeBriefAuditStatus, PROFILE_AUDIT_APPROVED)
                    .set(MaTechnician::getTeBriefAuditRemark, auditRemark);
        }
        return maTechnicianMapper.update(null, updateWrapper);
    }

    private int rejectProfileTextInfo(MaTechnician merchant, String auditRemark, LoginUser loginUser) {
        LambdaUpdateWrapper<MaTechnician> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.eq(MaTechnician::getId, merchant.getId())
                .set(MaTechnician::getUpdateBy, getAuditUserName(loginUser))
                .set(MaTechnician::getUpdateTime, DateUtils.getNowDate());
        if (Integer.valueOf(PROFILE_AUDIT_PENDING).equals(merchant.getTeNickNameAuditStatus())) {
            updateWrapper.set(MaTechnician::getTeNickNameAuditStatus, PROFILE_AUDIT_REJECTED)
                    .set(MaTechnician::getTeNickNameAuditRemark, auditRemark);
        }
        if (Integer.valueOf(PROFILE_AUDIT_PENDING).equals(merchant.getTeBriefAuditStatus())) {
            updateWrapper.set(MaTechnician::getTeBriefAuditStatus, PROFILE_AUDIT_REJECTED)
                    .set(MaTechnician::getTeBriefAuditRemark, auditRemark);
        }
        return maTechnicianMapper.update(null, updateWrapper);
    }

    /**
     * 审核通过所有待审核文件资料
     *
     * @param merchantId
     * @param pendingFiles
     * @param auditRemark
     * @param loginUser
     * @return int
     */
    private int approvePendingProfileFiles(Integer merchantId, List<MerchantApplyFile> pendingFiles, String auditRemark, LoginUser loginUser) {
        Set<String> fileTypes = pendingFiles.stream()
                .map(MerchantApplyFile::getFileType)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (fileTypes.isEmpty()) {
            return 0;
        }
        String updateBy = getAuditUserName(loginUser);
        LambdaUpdateWrapper<MerchantApplyFile> deleteOldWrapper = Wrappers.lambdaUpdate();
        deleteOldWrapper.eq(MerchantApplyFile::getMerchantId, merchantId)
                .in(MerchantApplyFile::getFileType, fileTypes)
                .eq(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_APPROVED)
                .set(MerchantApplyFile::getIsDelete, 1)
                .set(MerchantApplyFile::getUpdateBy, updateBy)
                .set(MerchantApplyFile::getUpdateTime, LocalDateTime.now());
        int rows = merchantApplyFileMapper.update(null, deleteOldWrapper);

        LambdaUpdateWrapper<MerchantApplyFile> auditWrapper = Wrappers.lambdaUpdate();
        auditWrapper.eq(MerchantApplyFile::getMerchantId, merchantId)
                .eq(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_PENDING)
                .set(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_APPROVED)
                .set(MerchantApplyFile::getAuditRemark, auditRemark)
                .set(MerchantApplyFile::getUpdateBy, updateBy)
                .set(MerchantApplyFile::getUpdateTime, LocalDateTime.now());
        return rows + merchantApplyFileMapper.update(null, auditWrapper);
    }

    private int rejectPendingProfileFiles(Integer merchantId, String auditRemark, LoginUser loginUser) {
        LambdaUpdateWrapper<MerchantApplyFile> auditWrapper = Wrappers.lambdaUpdate();
        auditWrapper.eq(MerchantApplyFile::getMerchantId, merchantId)
                .eq(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_PENDING)
                .set(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_REJECTED)
                .set(MerchantApplyFile::getAuditRemark, auditRemark)
                .set(MerchantApplyFile::getUpdateBy, getAuditUserName(loginUser))
                .set(MerchantApplyFile::getUpdateTime, LocalDateTime.now());
        return merchantApplyFileMapper.update(null, auditWrapper);
    }

    /**
     * 获取存在的商户信息
     *
     * @param merchantId
     * @return MaTechnician
     */
    private MaTechnician getExistingMerchant(String openid) {
        MaTechnician merchant = maTechnicianMapper.selectOne(Wrappers.lambdaQuery(MaTechnician.class)
                .eq(MaTechnician::getCOpenid, openid));
        if (merchant == null || (merchant.getIsDelete() != null && !NOT_DELETED.equals(merchant.getIsDelete()))) {
            throw new ServiceException("商户不存在或已删除");
        }
        return merchant;
    }

    /**
     *
     * @param merchantId
     * @return
     */
    private MaTechnician getExistingMerchant1(Integer merchantId) {
        MaTechnician merchant = maTechnicianMapper.selectById(merchantId);
        if (merchant == null || (merchant.getIsDelete() != null && !NOT_DELETED.equals(merchant.getIsDelete()))) {
            throw new ServiceException("商户不存在或已删除");
        }
        return merchant;
    }

    /**
     * 提交文本资料
     *
     * @param merchant
     * @param dto
     * @return boolean
     */
    private boolean submitProfileTextFields(MaTechnician merchant, MerchantProfileSubmitDTO dto) {
        boolean changed = false;
        LambdaUpdateWrapper<MaTechnician> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.eq(MaTechnician::getId, merchant.getId());

        // 处理昵称
        if (dto.getNickName() != null) {
            String nickName = checkProfileTextValue(dto.getNickName(), "昵称");
            if (PROFILE_AUDIT_PENDING == valueOrApproved(merchant.getTeNickNameAuditStatus())) {
                throw new ServiceException("该资料正在审核中，请勿重复提交");
            }
            if (!nickName.equals(merchant.getTeNickName()) || PROFILE_AUDIT_REJECTED == valueOrApproved(merchant.getTeNickNameAuditStatus())) {
                updateWrapper.set(MaTechnician::getPendingTeNickName, nickName)
                        .set(MaTechnician::getTeNickNameAuditStatus, PROFILE_AUDIT_PENDING)
                        .set(MaTechnician::getTeNickNameAuditRemark, null);
                changed = true;
            }
        }

        // 处理简介
        if (dto.getBrief() != null) {
            String brief = checkProfileTextValue(dto.getBrief(), "简介");
            if (PROFILE_AUDIT_PENDING == valueOrApproved(merchant.getTeBriefAuditStatus())) {
                throw new ServiceException("该资料正在审核中，请勿重复提交");
            }
            if (!brief.equals(merchant.getTeBrief()) || PROFILE_AUDIT_REJECTED == valueOrApproved(merchant.getTeBriefAuditStatus())) {
                updateWrapper.set(MaTechnician::getPendingTeBrief, brief)
                        .set(MaTechnician::getTeBriefAuditStatus, PROFILE_AUDIT_PENDING)
                        .set(MaTechnician::getTeBriefAuditRemark, null);
                changed = true;
            }
        }

        if (changed) {
            updateWrapper.set(MaTechnician::getUpdateTime, DateUtils.getNowDate());
            //  同时修改audit_status字段的值
            updateWrapper.set(MaTechnician::getAuditStatus, 1);
            int rows = maTechnicianMapper.update(null, updateWrapper);
            if (rows <= 0) {
                throw new ServiceException("提交商户资料审核失败");
            }
        }
        return changed;
    }

    /**
     * 检查文本资料是否为空
     *
     * @param value
     * @param fieldName
     * @return String
     */
    private String checkProfileTextValue(String value, String fieldName) {
        if (StringUtils.isBlank(value)) {
            throw new ServiceException(fieldName + "不能为空");
        }
        return value.trim();
    }

    private int valueOrApproved(Integer status) {
        return status == null ? PROFILE_AUDIT_APPROVED : status;
    }

    private void submitProfileFiles(Integer merchantId, List<MerchantApplyFileDto> files) {
        Map<String, List<MerchantApplyFileDto>> filesByType = files.stream()
                .collect(Collectors.groupingBy(MerchantApplyFileDto::getFileType, LinkedHashMap::new, Collectors.toList()));
        for (Map.Entry<String, List<MerchantApplyFileDto>> entry : filesByType.entrySet()) {
            String fileType = entry.getKey();
            checkProfileFileType(fileType);
            if (hasPendingProfileFile(merchantId, fileType)) {
                throw new ServiceException("该资料正在审核中，请勿重复提交");
            }
            deleteRejectedProfileFiles(merchantId, fileType);
            String batchNo = UUID.randomUUID().toString().replace("-", "");
            for (MerchantApplyFileDto file : entry.getValue()) {
                MerchantApplyFile applyFile = new MerchantApplyFile();
                BeanUtils.copyProperties(file, applyFile);
                applyFile.setMerchantId(merchantId);
                applyFile.setApplyBatchNo(batchNo);
                applyFile.setAuditStatus(PROFILE_AUDIT_PENDING);
                applyFile.setAuditRemark(null);
                applyFile.setCreateBy(merchantId.toString());
                applyFile.setUpdateBy(merchantId.toString());
                applyFile.setIsDelete(NOT_DELETED);
                merchantApplyFileMapper.insert(applyFile);
            }
        }
    }

    private void checkProfileFileType(String fileType) {
        if (!PROFILE_FILE_TYPES.contains(fileType)) {
            throw new ServiceException("资料文件类型不支持修改");
        }
    }

    private boolean hasPendingProfileFile(Integer merchantId, String fileType) {
        LambdaQueryWrapper<MerchantApplyFile> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(MerchantApplyFile::getMerchantId, merchantId)
                .eq(MerchantApplyFile::getFileType, fileType)
                .eq(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_PENDING);
        return merchantApplyFileMapper.selectCount(queryWrapper) > 0;
    }

    private void deleteRejectedProfileFiles(Integer merchantId, String fileType) {
        LambdaUpdateWrapper<MerchantApplyFile> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.eq(MerchantApplyFile::getMerchantId, merchantId)
                .eq(MerchantApplyFile::getFileType, fileType)
                .eq(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_REJECTED)
                .set(MerchantApplyFile::getIsDelete, 1)
                .set(MerchantApplyFile::getUpdateTime, LocalDateTime.now());
        merchantApplyFileMapper.update(null, updateWrapper);
    }

    private int auditProfileNickName(Integer merchantId, Integer auditStatus, String auditRemark, LoginUser loginUser) {
        MaTechnician merchant = getExistingMerchant1(merchantId);
        if (!Integer.valueOf(PROFILE_AUDIT_PENDING).equals(merchant.getTeNickNameAuditStatus())) {
            throw new ServiceException("昵称资料不是审核中状态");
        }
        LambdaUpdateWrapper<MaTechnician> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.eq(MaTechnician::getId, merchantId)
                .set(MaTechnician::getTeNickNameAuditStatus, auditStatus)
                .set(MaTechnician::getTeNickNameAuditRemark, auditRemark)
                .set(MaTechnician::getUpdateBy, getAuditUserName(loginUser))
                .set(MaTechnician::getUpdateTime, DateUtils.getNowDate());
        if (PROFILE_AUDIT_APPROVED == auditStatus) {
            updateWrapper.set(MaTechnician::getTeNickName, merchant.getPendingTeNickName())
                    .set(MaTechnician::getPendingTeNickName, null);
        }
        return maTechnicianMapper.update(null, updateWrapper);
    }

    private int auditProfileBrief(Integer merchantId, Integer auditStatus, String auditRemark, LoginUser loginUser) {
        MaTechnician merchant = getExistingMerchant1(merchantId);
        if (!Integer.valueOf(PROFILE_AUDIT_PENDING).equals(merchant.getTeBriefAuditStatus())) {
            throw new ServiceException("简介资料不是审核中状态");
        }
        LambdaUpdateWrapper<MaTechnician> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.eq(MaTechnician::getId, merchantId)
                .set(MaTechnician::getTeBriefAuditStatus, auditStatus)
                .set(MaTechnician::getTeBriefAuditRemark, auditRemark)
                .set(MaTechnician::getUpdateBy, getAuditUserName(loginUser))
                .set(MaTechnician::getUpdateTime, DateUtils.getNowDate());
        if (PROFILE_AUDIT_APPROVED == auditStatus) {
            updateWrapper.set(MaTechnician::getTeBrief, merchant.getPendingTeBrief())
                    .set(MaTechnician::getPendingTeBrief, null);
        }
        return maTechnicianMapper.update(null, updateWrapper);
    }

    private int auditProfileFile(Integer merchantId, String fileType, Integer auditStatus, String auditRemark, LoginUser loginUser) {
        if (StringUtils.isBlank(fileType)) {
            throw new ServiceException("文件类型不能为空");
        }
        checkProfileFileType(fileType);
        if (!hasPendingProfileFile(merchantId, fileType)) {
            throw new ServiceException("该文件资料不是审核中状态");
        }
        String updateBy = getAuditUserName(loginUser);
        if (PROFILE_AUDIT_APPROVED == auditStatus) {
            LambdaUpdateWrapper<MerchantApplyFile> deleteOldWrapper = Wrappers.lambdaUpdate();
            deleteOldWrapper.eq(MerchantApplyFile::getMerchantId, merchantId)
                    .eq(MerchantApplyFile::getFileType, fileType)
                    .eq(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_APPROVED)
                    .set(MerchantApplyFile::getIsDelete, 1)
                    .set(MerchantApplyFile::getUpdateBy, updateBy)
                    .set(MerchantApplyFile::getUpdateTime, LocalDateTime.now());
            merchantApplyFileMapper.update(null, deleteOldWrapper);
        }
        LambdaUpdateWrapper<MerchantApplyFile> auditWrapper = Wrappers.lambdaUpdate();
        auditWrapper.eq(MerchantApplyFile::getMerchantId, merchantId)
                .eq(MerchantApplyFile::getFileType, fileType)
                .eq(MerchantApplyFile::getAuditStatus, PROFILE_AUDIT_PENDING)
                .set(MerchantApplyFile::getAuditStatus, auditStatus)
                .set(MerchantApplyFile::getAuditRemark, auditRemark)
                .set(MerchantApplyFile::getUpdateBy, updateBy)
                .set(MerchantApplyFile::getUpdateTime, LocalDateTime.now());
        return merchantApplyFileMapper.update(null, auditWrapper);
    }

    private String getAuditUserName(LoginUser loginUser) {
        if (loginUser != null && loginUser.getUser() != null) {
            return loginUser.getUser().getUserName();
        }
        return null;
    }

    private MerchantProfileTextItemVO buildTextItem(String value, String pendingValue, Integer auditStatus, String auditRemark) {
        MerchantProfileTextItemVO item = new MerchantProfileTextItemVO();
        item.setValue(value);
        item.setPendingValue(pendingValue);
        item.setAuditStatus(shouldShowAuditStatus(auditStatus) ? auditStatus : null);
        item.setAuditStatusText(shouldShowAuditStatus(auditStatus) ? getProfileAuditStatusText(auditStatus) : null);
        item.setAuditRemark(shouldShowAuditStatus(auditStatus) ? auditRemark : null);
        item.setEditable(!Integer.valueOf(PROFILE_AUDIT_PENDING).equals(auditStatus));
        return item;
    }

    private List<MerchantProfileFileGroupVO> buildFileGroups(List<MerchantApplyFile> files) {
        Map<String, List<MerchantApplyFile>> filesByType = CollectionUtils.isEmpty(files)
                ? Collections.emptyMap()
                : files.stream()
                .filter(Objects::nonNull)
                .filter(file -> PROFILE_FILE_TYPES.contains(file.getFileType()))
                .collect(Collectors.groupingBy(MerchantApplyFile::getFileType, LinkedHashMap::new, Collectors.toList()));
        List<MerchantProfileFileGroupVO> groups = new ArrayList<>();
        for (String fileType : PROFILE_FILE_TYPES) {
            List<MerchantApplyFile> typeFiles = filesByType.getOrDefault(fileType, Collections.emptyList());
            List<MerchantApplyFile> officialFiles = typeFiles.stream()
                    .filter(file -> Integer.valueOf(PROFILE_AUDIT_APPROVED).equals(file.getAuditStatus()))
                    .collect(Collectors.toList());
            List<MerchantApplyFile> pendingFiles = typeFiles.stream()
                    .filter(file -> !Integer.valueOf(PROFILE_AUDIT_APPROVED).equals(file.getAuditStatus()))
                    .collect(Collectors.toList());
            Integer auditStatus = resolveFileGroupAuditStatus(pendingFiles);
            MerchantProfileFileGroupVO group = new MerchantProfileFileGroupVO();
            group.setFileType(fileType);
            group.setFileTypeName(FileTypeEnum.getDescByCode(fileType));
            group.setOfficialFiles(toProfileFileVOList(officialFiles));
            group.setPendingFiles(toProfileFileVOList(pendingFiles));
            group.setAuditStatus(shouldShowAuditStatus(auditStatus) ? auditStatus : null);
            group.setAuditStatusText(shouldShowAuditStatus(auditStatus) ? getProfileAuditStatusText(auditStatus) : null);
            group.setAuditRemark(shouldShowAuditStatus(auditStatus) ? resolveFileGroupAuditRemark(pendingFiles) : null);
            group.setEditable(!Integer.valueOf(PROFILE_AUDIT_PENDING).equals(auditStatus));
            groups.add(group);
        }
        return groups;
    }

    private Integer resolveFileGroupAuditStatus(List<MerchantApplyFile> pendingFiles) {
        if (CollectionUtils.isEmpty(pendingFiles)) {
            return null;
        }
        if (pendingFiles.stream().anyMatch(file -> Integer.valueOf(PROFILE_AUDIT_PENDING).equals(file.getAuditStatus()))) {
            return PROFILE_AUDIT_PENDING;
        }
        return pendingFiles.get(0).getAuditStatus();
    }

    private String resolveFileGroupAuditRemark(List<MerchantApplyFile> pendingFiles) {
        if (CollectionUtils.isEmpty(pendingFiles)) {
            return null;
        }
        return pendingFiles.stream()
                .map(MerchantApplyFile::getAuditRemark)
                .filter(StringUtils::isNotBlank)
                .findFirst()
                .orElse(null);
    }

    private List<MerchantProfileFileVO> toProfileFileVOList(List<MerchantApplyFile> files) {
        if (CollectionUtils.isEmpty(files)) {
            return Collections.emptyList();
        }
        return files.stream().map(this::toProfileFileVO).collect(Collectors.toList());
    }

    private MerchantProfileFileVO toProfileFileVO(MerchantApplyFile file) {
        MerchantProfileFileVO vo = new MerchantProfileFileVO();
        vo.setId(file.getId());
        vo.setFileName(file.getFileName());
        vo.setFileUrl(file.getFileUrl());
        vo.setFileSize(file.getFileSize());
        vo.setContentType(file.getContentType());
        vo.setApplyBatchNo(file.getApplyBatchNo());
        vo.setAuditStatus(file.getAuditStatus());
        vo.setAuditRemark(file.getAuditRemark());
        return vo;
    }

    private boolean shouldShowAuditStatus(Integer auditStatus) {
        return auditStatus != null && !Integer.valueOf(PROFILE_AUDIT_APPROVED).equals(auditStatus);
    }

    private String getProfileAuditStatusText(Integer auditStatus) {
        if (Integer.valueOf(PROFILE_AUDIT_PENDING).equals(auditStatus)) {
            return "审核中";
        }
        if (Integer.valueOf(PROFILE_AUDIT_REJECTED).equals(auditStatus)) {
            return "审核驳回";
        }
        return null;
    }

    /**
     * 修改商户基础信息，仅允许修改昵称和简介。
     *
     * @param technician 商户基础信息
     */
    private void updateTechnicianBaseInfo(MaTechnician technician) {
        Integer merchantId = technician.getId();
        if (merchantId == null) {
            throw new ServiceException("商户ID不能为空");
        }
        if (StringUtils.isBlank(technician.getTeNickName())) {
            throw new ServiceException("昵称不能为空");
        }
        if (StringUtils.isBlank(technician.getTeBrief())) {
            throw new ServiceException("简介不能为空");
        }
        LambdaUpdateWrapper<MaTechnician> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(MaTechnician::getId, merchantId);
        updateWrapper.set(MaTechnician::getTeNickName, technician.getTeNickName().trim());
        updateWrapper.set(MaTechnician::getTeBrief, technician.getTeBrief().trim());
        // 审核状态默认设为1 待审核
        updateWrapper.set(MaTechnician::getAuditStatus, 1);
        int rows = maTechnicianMapper.update(null, updateWrapper);
        if (rows <= 0) {
            throw new ServiceException("修改商户基础信息失败");
        }
    }

    /**
     * 按本次提交的文件类型整组替换商户入驻资料文件。
     *
     * @param merchantId 商户ID
     * @param files      入驻资料文件
     */
    private void replaceApplyFilesBySubmittedTypes(Integer merchantId, List<MerchantApplyFileDto> files) {
        if (CollectionUtils.isEmpty(files)) {
            return;
        }
        replaceApplyFiles(merchantId, files);
    }

    /**
     * 从商户基础信息中解析商户ID。
     *
     * @param technician 商户基础信息
     * @return Integer 商户ID
     */
    private Integer resolveMerchantId(MaTechnician technician) {
        if (technician != null && technician.getId() != null) {
            return technician.getId();
        }
        throw new ServiceException("商户ID不能为空");
    }

    private void checkApplyFileParam(MerchantApplyFileDto file) {
        if (file == null) {
            throw new ServiceException("入驻资料文件不能为空");
        }
        if (StringUtils.isBlank(file.getFileType())) {
            throw new ServiceException("文件类型不能为空");
        }
        if (StringUtils.isBlank(file.getFileUrl())) {
            throw new ServiceException("文件访问地址不能为空");
        }
    }

    /**
     * 商户入住前置条件校验
     *
     * @param req
     * @param phone
     */
    private void getMaTechnician(MaTechnicianAppAddVo req, String phone) {
        // 1. 判断当前用户是否已入驻
        MaTechnician userProfile = getMaTechnician(req);
        if (userProfile != null) {
            throw new RuntimeException("当前用户已入驻，请勿重复提交");
        }

        // 2. 判断手机号是否已存在
        LambdaQueryWrapper<MaTechnician> queryPhoneWrapper = new LambdaQueryWrapper<>();
        queryPhoneWrapper.eq(MaTechnician::getTePhone, phone);
        MaTechnician maTechnicianPhone = maTechnicianMapper.selectOne(queryPhoneWrapper);
        if (maTechnicianPhone != null) {
            throw new RuntimeException("手机号已存在，请更换手机号");
        }
        //3、判断手机号是否已绑定其他用户
        LambdaQueryWrapper<MaTechnician> queryTePhoneWrapper = new LambdaQueryWrapper<>();
        queryTePhoneWrapper.eq(MaTechnician::getTePhone, phone);
        queryTePhoneWrapper.eq(MaTechnician::getAuditStatus, 2);
        MaTechnician maTechnicianTePhone = maTechnicianMapper.selectOne(queryTePhoneWrapper);
        if (maTechnicianTePhone != null) {
            throw new RuntimeException("手机号已被其他用户绑定，请更换手机号");
        }
    }

    /**
     * 判断当前用户是否已入驻
     *
     * @param req
     * @return MaTechnician
     */
    private MaTechnician getMaTechnician(MaTechnicianAppAddVo req) {
        LambdaQueryWrapper<MaTechnician> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(MaTechnician::getTePhone, req.getTePhone());
        queryWrapper.eq(MaTechnician::getAuditStatus, 2);
        queryWrapper.eq(MaTechnician::getServiceTag, req.getServiceTag());
        MaTechnician userProfile = maTechnicianMapper.selectOne(queryWrapper);
        return userProfile;
    }

    /**
     * 查询商户服务项目列表
     *
     * @param merchantId  商户id
     * @param auditStatus 审核状态
     * @return List<MaProject>
     */
    @Override
    public List<MaProject> selectMaTechnicianListBy(Integer merchantId, Integer auditStatus) {
        LambdaQueryWrapper<MaProject> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(MaProject::getMerchantId, merchantId);
        queryWrapper.eq(MaProject::getAuditStatus, auditStatus);
        List<MaProject> maProjects = maProjectMapper.selectList(queryWrapper);
        maProjects.forEach(maProject -> {
            //根据项目ID查询项目图片
            Integer projectId = maProject.getProjectId();
            Project project = projectMapper.selectById(projectId);
            maProject.setCover(project.getCover());
            maProject.setUnitType(project.getUnitType());
            maProject.setHighlight(project.getHighlight());
            maProject.setTargetAudience(project.getTargetAudience());
        });
        return maProjects;
    }

    /**
     * 查询服务分类项目列表
     *
     * @param typeId 技师类型
     * @return 技师列表
     */
    @Override
    public List<Project> selectTechnicianListBy(String typeId) {
        LambdaQueryWrapper<Project> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Project::getType, typeId);
        return projectMapper.selectList(queryWrapper);
    }

    /**
     * 查询技师
     *
     * @param id 技师主键
     * @return 技师
     */
    @Override
    public MaTechnician selectMaTechnicianById(Long id) {
        return maTechnicianMapper.selectMaTechnicianById(id);
    }

    /**
     * 查询技师列表
     *
     * @param maTechnician 技师
     * @return 技师
     */
    @Override
    public List<MaTechnician> selectMaTechnicianList(MaTechnician maTechnician) {
        return maTechnicianMapper.selectMaTechnicianList(maTechnician);
    }

    /**
     * 新增技师
     *
     * @param maTechnicianAppAddVo 技师
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int insertMaTechnician(MaTechnicianAppAddVo maTechnicianAppAddVo) {
        MaTechnician maTechnician = new MaTechnician();
        BeanUtils.copyProperties(maTechnicianAppAddVo, maTechnician);
        int rows = maTechnicianMapper.insertMaTechnician(maTechnician);
        if (maTechnicianAppAddVo.getProjectIds() != null && !maTechnicianAppAddVo.getProjectIds().isEmpty()) {
            insertProjectRelations(maTechnician.getId(), new LinkedHashSet<>(maTechnicianAppAddVo.getProjectIds()));
        }
        return rows;
    }

    /**
     * 后台新增商户
     *
     * @param dto       新增商户参数
     * @param loginUser 当前登录用户
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int insertMerchant(MaTechnicianMerchantAddDTO dto, LoginUser loginUser) {
        MerchantProjectSelection selection = checkMerchantAddParam(dto);
        String userName = loginUser.getUser().getUserName();

        MaTechnician maTechnician = new MaTechnician();
        maTechnician.setTeName(dto.getTeName().trim());
        maTechnician.setTeNickName(dto.getTeNickName().trim());
        maTechnician.setTeSex(dto.getTeSex());
        maTechnician.setTePhone(dto.getTePhone().trim());
        maTechnician.setOpenService(joinIds(selection.getCategoryIds()));
        maTechnician.setTeProject(joinProjectTitles(selection.getProjectIds(), selection.getProjectMap()));

        maTechnician.setTechType(dto.getTechType());
        maTechnician.setIsRecommend(normalizeSwitchValue(dto.getIsRecommend(), "是否推荐"));

        maTechnician.setServiceState(SERVICE_STATE_AVAILABLE);
        maTechnician.setPostState(POST_STATE_OFFLINE);
        maTechnician.setTeIsEnable(ENABLED);
        //上岗状态：默认-1 未上岗
        maTechnician.setNStatus2(NS_STATUS_NOT_ON_DUTY);
        maTechnician.setMerchantStatus(MERCHANT_STATUS_NORMAL);
        //审核状态
        maTechnician.setAuditStatus(AUDIT_APPROVED);
        maTechnician.setTeAddress("");
        maTechnician.setNStar(DEFAULT_STAT_VALUE);
        maTechnician.setNNum(DEFAULT_STAT_VALUE);
        maTechnician.setCreateBy(userName);
        maTechnician.setUpdateBy(userName);
        maTechnician.setCreateTime(DateUtils.getNowDate());
        maTechnician.setUpdateTime(DateUtils.getNowDate());

        int rows = maTechnicianMapper.insert(maTechnician);
        if (rows <= 0) {
            throw new ServiceException("新增商户失败");
        }
        insertProjectRelations(maTechnician.getId(), selection.getProjectIds());
        return rows;
    }

    /**
     * 后台编辑商户
     *
     * @param id        商户ID
     * @param dto       编辑商户参数
     * @param loginUser 当前登录用户
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int updateMerchant(Integer id, MaTechnicianMerchantAddDTO dto, LoginUser loginUser) {
        if (id == null) {
            throw new ServiceException("商户ID不能为空");
        }
        MaTechnician existsMerchant = maTechnicianMapper.selectMerchantById(id.intValue());
        if (existsMerchant == null || !NOT_DELETED.equals(existsMerchant.getIsDelete())) {
            throw new ServiceException("商户不存在或已删除");
        }

        MerchantProjectSelection selection = checkMerchantAddParam(dto);
        String userName = loginUser.getUser().getUserName();

        MaTechnician maTechnician = new MaTechnician();
        maTechnician.setId(id);
        maTechnician.setTeName(dto.getTeName().trim());
        maTechnician.setTeNickName(dto.getTeNickName().trim());
        maTechnician.setTeSex(dto.getTeSex());
        maTechnician.setTePhone(dto.getTePhone().trim());
        maTechnician.setOpenService(joinIds(selection.getCategoryIds()));
        maTechnician.setTeProject(joinProjectTitles(selection.getProjectIds(), selection.getProjectMap()));
        maTechnician.setTechType(dto.getTechType());
        maTechnician.setIsRecommend(normalizeSwitchValue(dto.getIsRecommend(), "是否推荐"));
        maTechnician.setUpdateBy(userName);
        maTechnician.setUpdateTime(DateUtils.getNowDate());

        int rows = maTechnicianMapper.updateMerchantById(maTechnician);
        if (rows <= 0) {
            throw new ServiceException("编辑商户失败");
        }
        replaceProjectRelations(id, selection.getProjectIds());
        return rows;
    }

    /**
     * 后台变更商户管理状态。
     *
     * @param id        商户ID
     * @param dto       状态变更参数
     * @param loginUser 当前登录用户
     * @return int 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int changeMerchantStatus(Integer id, MaTechnicianStatusChangeDTO dto, LoginUser loginUser) {
        if (id == null) {
            throw new ServiceException("商户ID不能为空");
        }
        if (dto == null) {
            throw new ServiceException("状态管理参数不能为空");
        }
        checkEnumValue(dto.getMerchantStatus(), "商户状态", 0, 1, 2, 3);
        String reasonChange = dto.getReasonChange() == null ? "" : dto.getReasonChange().trim();
        if (StringUtils.isBlank(reasonChange)) {
            throw new ServiceException("变更原因不能为空");
        }
        if (reasonChange.length() > AUDIT_REMARK_MAX_LENGTH) {
            throw new ServiceException("变更原因长度不能超过" + AUDIT_REMARK_MAX_LENGTH + "个字符");
        }

        MaTechnician existsMerchant = maTechnicianMapper.selectMerchantById(id);
        if (existsMerchant == null || !NOT_DELETED.equals(existsMerchant.getIsDelete())) {
            throw new ServiceException("商户不存在或已删除");
        }

        MaTechnician maTechnician = new MaTechnician();
        maTechnician.setId(id);
        maTechnician.setMerchantStatus(dto.getMerchantStatus());
        maTechnician.setReasonChange(reasonChange);
        if (loginUser != null && loginUser.getUser() != null) {
            maTechnician.setUpdateBy(loginUser.getUser().getUserName());
        }
        maTechnician.setUpdateTime(DateUtils.getNowDate());

        int rows = maTechnicianMapper.updateMerchantStatusById(maTechnician);
        if (rows <= 0) {
            throw new ServiceException("商户状态变更失败");
        }


        // ========== 新增：商户状态变更时移除热门ZSet ==========
        Integer newMerchantStatus = maTechnician.getMerchantStatus();
        // 商户管理状态(0-正常, 1-限制接单, 2-冻结, 3-注销)
        if (ObjectUtil.notEqual(0, newMerchantStatus)) {
            redisTemplate.opsForZSet().remove("hot:merchant:rank", String.valueOf(id));
        }
        return rows;
    }

    /**
     * 后台上传商户合同文件
     *
     * @param id        商户ID
     * @param
     * @param loginUser 当前登录用户
     * @return 上传结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Integer uploadMerchantContract(Integer id, Map<String, Object> map, LoginUser loginUser) {
        if (id == null) {
            throw new ServiceException("商户ID不能为空");
        }
        MaTechnician existsMerchant = maTechnicianMapper.selectMerchantById(id);
        if (existsMerchant == null || !NOT_DELETED.equals(existsMerchant.getIsDelete())) {
            throw new ServiceException("商户不存在或已删除");
        }
        // 合同的名称
        String contractName = String.valueOf(map.get("contractName"));
        // 合同文件的URL
        String url = String.valueOf(map.get("url"));
        if (StringUtils.isBlank(url)) {
            throw new ServiceException("合同文件上传失败，未返回文件地址");
        }
        ContractRecord contractRecord = new ContractRecord();
        contractRecord.setMerchantId(id);
        contractRecord.setContractName(contractName);
        contractRecord.setFileUrl(url);
        contractRecord.setSignTime(DateUtils.getNowDate());
        contractRecord.setSignerName(existsMerchant.getTeName());
        contractRecord.setCreateTime(DateUtils.getNowDate());

        int rows = contractRecordMapper.insert(contractRecord);
        if (rows <= 0) {
            throw new ServiceException("保存合同记录失败");
        }
        return rows;
    }

    /**
     * 商户入驻审核。
     *
     * @param id        商户ID
     * @param dto       审核提交参数
     * @param loginUser 当前登录用户
     * @return int 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int submitMerchantAudit(Integer id, MaTechnicianAuditSubmitDTO dto, LoginUser loginUser) {
        if (id == null) {
            throw new ServiceException("商户ID不能为空");
        }
        if (dto == null) {
            throw new ServiceException("审核参数不能为空");
        }
        checkEnumValue(dto.getAuditStatus(), "审核意见", AUDIT_APPROVED, AUDIT_REJECTED);
        String auditRemark = dto.getAuditRemark() == null ? "" : dto.getAuditRemark().trim();
        if (dto.getAuditStatus() == AUDIT_REJECTED && StringUtils.isBlank(auditRemark)) {
            throw new ServiceException("审核驳回时审核备注不能为空");
        }
        if (auditRemark.length() > AUDIT_REMARK_MAX_LENGTH) {
            throw new ServiceException("审核备注长度不能超过" + AUDIT_REMARK_MAX_LENGTH + "个字符");
        }

        MaTechnician existsMerchant = maTechnicianMapper.selectMerchantById(id);
        if (existsMerchant == null || !NOT_DELETED.equals(existsMerchant.getIsDelete())) {
            throw new ServiceException("商户不存在或已删除");
        }
        Integer currentAuditStatus = existsMerchant.getAuditStatus();
        if (currentAuditStatus == null || (currentAuditStatus != AUDIT_WAIT_ENTER)) {
            throw new ServiceException("当前商户待入驻已审核，不用重复审核");
        }

        MaTechnician maTechnician = new MaTechnician();
        maTechnician.setId(id);
        if (dto.getAuditStatus() == AUDIT_APPROVED) {
            maTechnician.setAuditStatus(1);
        } else {
            maTechnician.setAuditStatus(3);
        }
        maTechnician.setAuditRemark(auditRemark);
        maTechnician.setApproveTime(DateUtils.getNowDate());
        if (loginUser != null && loginUser.getUser() != null) {
            maTechnician.setUpdateBy(loginUser.getUser().getUserName());
        }
        maTechnician.setUpdateTime(DateUtils.getNowDate());

        int rows = maTechnicianMapper.submitMerchantAuditById(maTechnician);
        if (rows <= 0) {
            throw new ServiceException("商户待入驻审核失败");
        }
        return rows;
    }

    /**
     * 后台查询商户证照
     *
     * @param id 商户ID
     * @return MaTechnicianCertificateVO 商户证照
     */
    @Override
    public MaTechnicianCertificateVO selectMerchantCertificate(Integer id) {
        if (id == null) {
            throw new ServiceException("商户ID不能为空");
        }
        LambdaQueryWrapper<MerchantApplyFile> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(MerchantApplyFile::getMerchantId, id);
        List<MerchantApplyFile> merchantApplyFiles = merchantApplyFileMapper.selectList(queryWrapper);
        MaTechnicianCertificateVO certificate = new MaTechnicianCertificateVO();
        certificate.setMerchantId(id);

        Map<String, List<String>> fileUrlsByType = CollectionUtils.isEmpty(merchantApplyFiles)
                ? Collections.emptyMap()
                : merchantApplyFiles.stream()
                .filter(Objects::nonNull)
                .filter(file -> StringUtils.isNotBlank(file.getFileType()) && StringUtils.isNotBlank(file.getFileUrl()))
                .collect(Collectors.groupingBy(MerchantApplyFile::getFileType,
                        Collectors.mapping(MerchantApplyFile::getFileUrl, Collectors.toList())));

        certificate.setAvatar(getCertificateFileUrls(fileUrlsByType, PORTRAIT.getCode()));
        certificate.setLifePhotos(getCertificateFileUrls(fileUrlsByType, LIFE_PHOTO.getCode()));
        certificate.setPromotionVideo(getCertificateFileUrls(fileUrlsByType, PROMOTION_VIDEO.getCode()));
        certificate.setIdCardFrout(getCertificateFileUrls(fileUrlsByType, ID_CARD_FRONT.getCode()));
        certificate.setIdCardBack(getCertificateFileUrls(fileUrlsByType, ID_CARD_BACK.getCode()));
        certificate.setIdCardHandheld(getCertificateFileUrls(fileUrlsByType, ID_CARD_HANDHELD.getCode()));
        certificate.setHealthCertificate(getCertificateFileUrls(fileUrlsByType, HEALTH_CERT.getCode()));
        certificate.setQualificationCertificate(getCertificateFileUrls(fileUrlsByType, QUALIFICATION_CERT.getCode()));
        certificate.setNoCrimeRecord(getCertificateFileUrls(fileUrlsByType, NO_CRIME_RECORD.getCode()));
        certificate.setCommitmentPdf(getCertificateFileUrls(fileUrlsByType, COMMITMENT_LETTER.getCode()));
        certificate.setCommitmentAudio(getCertificateFileUrls(fileUrlsByType, COMMITMENT_AUDIO.getCode()));
        certificate.setCommitmentVideo(getCertificateFileUrls(fileUrlsByType, COMMITMENT_VIDEO.getCode()));
        return certificate;
    }

    /**
     * 获取指定证照类型的全部文件URL
     *
     * @param fileUrlsByType 文件类型与URL列表映射
     * @param type           文件类型
     * @return List<String> 文件URL列表
     */
    private List<String> getCertificateFileUrls(Map<String, List<String>> fileUrlsByType, String type) {
        return fileUrlsByType.getOrDefault(type, Collections.emptyList());
    }

    /**
     * 全量替换商户与服务项目关联关系。
     *
     * @param technicianId 商户ID
     * @param projectIds   服务项目ID集合
     */
    private void replaceProjectRelations(Integer technicianId, Set<Integer> projectIds) {
        if (technicianId == null) {
            throw new ServiceException("商户ID不能为空");
        }
        maTeProjectMapper.deleteByTechnicianId(technicianId);
        for (Integer projectId : projectIds) {
            MaTeProject relation = new MaTeProject();
            relation.setTeId(technicianId);
            relation.setProjectId(projectId);
            int rows = maTeProjectMapper.insert(relation);
            if (rows <= 0) {
                throw new ServiceException("编辑商户服务项目失败");
            }
        }
    }

    /**
     * 后台查询商户入驻审核列表
     *
     * @param page 分页参数
     * @param dto  查询条件
     * @return 商户入驻审核分页列表
     */
    @Override
    public Page<MaTechnicianAuditListVO> selectMerchantAuditList(Page<MaTechnicianAuditListVO> page, MaTechnicianAuditQueryDTO dto) {
        if (dto != null && dto.getAuditStatus() != null) {
            checkEnumValue(dto.getAuditStatus(), "审核状态", 0, 1, 2, 3);
        }
        Page<MaTechnicianAuditListVO> pageParam = page == null ? new Page<>(1, 10) : page;
        return maTechnicianMapper.selectMerchantAuditList(pageParam, dto);
    }

    /**
     * 后台查询商户列表
     *
     * @param page 分页参数
     * @param dto  查询条件
     * @return 商户分页列表
     */
    @Override
    public Page<MaTechnicianMerchantListVO> selectMerchantList(Page<MaTechnicianMerchantListVO> page,
                                                               MaTechnicianMerchantQueryDTO dto) {
        Page<MaTechnicianMerchantListVO> pageParam = page == null ? new Page<>(1, 10) : page;
        return maTechnicianMapper.selectMerchantList(pageParam, dto);
    }

    /**
     * 后台查询商户详情
     *
     * @param id 商户ID
     * @return 商户详情
     */
    @Override
    public MaTechnicianMerchantDetailVO selectMerchantDetail(Long id) {
        if (id == null) {
            throw new ServiceException("商户ID不能为空");
        }
        MaTechnicianMerchantDetailVO detail = maTechnicianMapper.selectMerchantDetailById(id);
        if (detail == null) {
            throw new ServiceException("商户不存在或已删除");
        }
        return detail;
    }

    /**
     * 修改技师
     *
     * @param maTechnicianAppAddVo
     * @return 结果
     */
    @Override
    public int updateMaTechnician(MaTechnicianAppAddVo maTechnicianAppAddVo) {

        MaTechnician maTechnician = new MaTechnician();
        BeanUtils.copyProperties(maTechnicianAppAddVo, maTechnician);

        return maTechnicianMapper.updateMaTechnician(maTechnician);
    }

    /**
     * 批量删除技师
     *
     * @param ids 需要删除的技师主键
     * @return 结果
     */
    @Override
    public int deleteMaTechnicianByIds(Long[] ids) {
        return maTechnicianMapper.deleteMaTechnicianByIds(ids);
    }

    /**
     * 删除技师信息
     *
     * @param id 技师主键
     * @return 结果
     */
    @Override
    public int deleteMaTechnicianById(Long id) {
        return maTechnicianMapper.deleteMaTechnicianById(id);
    }

    /**
     * 首页选中的城市是否有开通服务
     *
     * @param areaCode
     * @return
     */
    @Override
    public Boolean isHasMerchantCity(String areaCode) {
        return maTechnicianMapper.isHasMerchantCity(areaCode);
    }

    /**
     * 首页按摩推荐
     *
     * @param dto
     * @return
     */
    @Override
    public List<MerchantVo> getMerchantRecommend(MerchantRecommendDto dto) {
        return maTechnicianMapper.getMerchantRecommend(dto);
    }

    private MerchantProjectSelection checkMerchantAddParam(MaTechnicianMerchantAddDTO dto) {
        if (dto == null) {
            throw new ServiceException("商户参数不能为空");
        }
        checkRequiredText(dto.getTeName(), "姓名", 10);
        checkRequiredText(dto.getTeNickName(), "昵称", 10);
        checkRequiredText(dto.getTePhone(), "电话", 11);
        checkEnumValue(dto.getTeSex(), "性别", 0, 1);
        Set<Integer> categoryIds = checkOpenServiceIds(dto.getOpenService());
        checkEnumValue(dto.getTechType(), "商户类型", 0, 1);
        if (dto.getIsRecommend() != null) {
            checkEnumValue(dto.getIsRecommend(), "是否推荐", 0, 1);
        }
        return checkProjectIds(dto.getProjectIds(), categoryIds);
    }

    private void checkRequiredText(String value, String fieldName, int maxLength) {
        if (StringUtils.isBlank(value)) {
            throw new ServiceException(fieldName + "不能为空");
        }
        if (value.trim().length() > maxLength) {
            throw new ServiceException(fieldName + "长度不能超过" + maxLength + "个字符");
        }
    }

    private void checkEnumValue(Integer value, String fieldName, int... allowedValues) {
        if (value == null) {
            throw new ServiceException(fieldName + "不能为空");
        }
        for (int allowedValue : allowedValues) {
            if (value == allowedValue) {
                return;
            }
        }
        throw new ServiceException(fieldName + "值不正确");
    }

    private Integer normalizeSwitchValue(Integer value, String fieldName) {
        if (value == null) {
            return 0;
        }
        checkEnumValue(value, fieldName, 0, 1);
        return value;
    }

    /**
     * 校验服务项目ID集合
     *
     * @param projectIds  服务项目ID集合
     * @param categoryIds 服务类目ID集合
     * @return 有效服务项目ID集合
     */
    private MerchantProjectSelection checkProjectIds(List<Integer> projectIds, Set<Integer> categoryIds) {
        if (projectIds == null || projectIds.isEmpty()) {
            throw new ServiceException("服务项目不能为空");
        }

        Set<Integer> distinctProjectIds = new LinkedHashSet<>();
        for (Integer projectId : projectIds) {
            if (projectId == null) {
                throw new ServiceException("服务项目ID不能为空");
            }
            distinctProjectIds.add(projectId);
        }

        List<Project> projects = projectMapper.selectList(new LambdaQueryWrapper<Project>()
                .in(Project::getId, distinctProjectIds)
                .eq(Project::getIsDelete, 0));
        if (projects.size() != distinctProjectIds.size()) {
            throw new ServiceException("服务项目不存在或已删除");
        }

        Map<Integer, Project> projectMap = projects.stream()
                .collect(Collectors.toMap(project -> project.getId(), Function.identity(), (left, right) -> left));
        Set<Integer> projectCategoryIds = new LinkedHashSet<>();
        for (Integer projectId : distinctProjectIds) {
            Project project = projectMap.get(projectId);
            if (project == null || project.getCategoryId() == null) {
                throw new ServiceException("服务项目类目不能为空");
            }
            if (!categoryIds.contains(project.getCategoryId())) {
                throw new ServiceException("服务项目不属于所选服务类目");
            }
            projectCategoryIds.add(project.getCategoryId());
        }
        if (!projectCategoryIds.containsAll(categoryIds)) {
            throw new ServiceException("每个服务类目至少选择一个服务项目");
        }
        return new MerchantProjectSelection(categoryIds, distinctProjectIds, projectMap);
    }

    /**
     * 校验服务类目ID集合
     *
     * @param openService 服务类目ID集合
     * @return 去重后的服务类目ID集合
     */
    private Set<Integer> checkOpenServiceIds(List<Integer> openService) {
        if (openService == null || openService.isEmpty()) {
            throw new ServiceException("服务类目不能为空");
        }
        Set<Integer> categoryIds = new LinkedHashSet<>();
        for (Integer categoryId : openService) {
            if (categoryId == null) {
                throw new ServiceException("服务类目ID不能为空");
            }
            categoryIds.add(categoryId);
        }
        return categoryIds;
    }

    private String joinIds(Set<Integer> ids) {
        return ids.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
    }

    private String joinProjectTitles(Set<Integer> projectIds, Map<Integer, Project> projectMap) {
        return projectIds.stream()
                .map(projectMap::get)
                .map(Project::getTitle)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.joining(","));
    }

    /**
     * 新增商户与服务项目关联关系
     *
     * @param technicianId
     * @param projectIds
     */
    private void insertProjectRelations(Integer technicianId, Set<Integer> projectIds) {
        if (technicianId == null) {
            throw new ServiceException("商户ID不能为空");
        }

        List<MaTeProject> relations = new ArrayList<>();
        for (Integer projectId : projectIds) {
            MaTeProject relation = new MaTeProject();
            relation.setTeId(technicianId);
            relation.setProjectId(projectId);
            relations.add(relation);
        }
        int rows = maTeProjectMapper.insertBatch(relations);
        if (rows != relations.size()) {
            throw new ServiceException("新增商户服务项目失败");
        }
    }

    private static class MerchantProjectSelection {
        private final Set<Integer> categoryIds;
        private final Set<Integer> projectIds;
        private final Map<Integer, Project> projectMap;

        private MerchantProjectSelection(Set<Integer> categoryIds, Set<Integer> projectIds, Map<Integer, Project> projectMap) {
            this.categoryIds = categoryIds;
            this.projectIds = projectIds;
            this.projectMap = projectMap;
        }

        private Set<Integer> getCategoryIds() {
            return categoryIds;
        }

        private Set<Integer> getProjectIds() {
            return projectIds;
        }

        private Map<Integer, Project> getProjectMap() {
            return projectMap;
        }
    }

    /**
     * 获取未申请技能列表
     *
     * @param merchantId
     * @param serviceTag
     * @return List<Project>
     */
    @Override
    public List<Project> getNotApplyList(Integer merchantId, Integer serviceTag) {
        LambdaQueryWrapper<MaProject> query = new LambdaQueryWrapper<>();
        query.eq(MaProject::getMerchantId, merchantId);
        query.eq(MaProject::getServiceTag, serviceTag);
        // 审核状态：审核通过
        query.eq(MaProject::getAuditStatus, 1);
        List<MaProject> maProjectList = maProjectMapper.selectList(query);
        // 获取已申请技能ID集合
        List<Integer> projectIdList = maProjectList.stream().map(MaProject::getProjectId).collect(Collectors.toList());
        if (projectIdList.size() == 0) {
            LambdaQueryWrapper<Project> query1 = new LambdaQueryWrapper<>();
            query1.eq(Project::getType, serviceTag);
            return projectMapper.selectList(query1);
        }
        LambdaQueryWrapper<Project> query2 = new LambdaQueryWrapper<>();
        query2.eq(Project::getType, serviceTag);
        query2.notIn(Project::getId, projectIdList);
        return projectMapper.selectList(query2);
    }

    /**
     * 申请开通新服务
     *
     * @param dto
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    public int applyForService(MaProjectSaveDto dto) {
        if (Objects.isNull(dto)) {
            return 0;
        }
        if (dto.getProjectIdList().size() > 0) {
            // 插入商户技能
            extracted(dto);
        } else {
            return 0;
        }
        return 1;
    }

    /**
     * 根据微信openid查询商户信息和入驻资料。
     *
     * @param openid 微信openid
     * @return MerchantAuditFile
     */
    @Override
    public MerchantAuditFile getTechnicianInfo(String openid) {
        if (StringUtils.isBlank(openid)) {
            throw new IllegalArgumentException("openid不能为空");
        }
        MaTechnician merchant = findMerchantByOpenid(openid);
        return buildMerchantAuditFile(merchant);
    }

    /**
     * 商户入驻信息
     *
     * @param userId 商户ID
     * @return MerchantAuditFile
     */
    @Override
    public MerchantAuditFile getTechnicianList(Integer userId) {
        if (userId == null) {
            throw new IllegalArgumentException("商户ID不能为空");
        }
        return buildMerchantAuditFile(findMerchantById(userId));
    }

    /**
     * 根据微信openid查询商户信息
     *
     * @param openid
     * @return MaTechnician
     */
    private MaTechnician findMerchantByOpenid(String openid) {
        LambdaQueryWrapper<MaTechnician> query = new LambdaQueryWrapper<>();
        query.eq(MaTechnician::getCOpenid, openid);
        MaTechnician merchant = maTechnicianMapper.selectOne(query);
        // 根据商户ID查询city_operation_application表
        if (merchant != null && merchant.getId() != null) {
            LambdaQueryWrapper<CityOperationApplication> query1 = new LambdaQueryWrapper<>();
            query1.eq(CityOperationApplication::getMerchantId, merchant.getId()).last("limit 1");
            CityOperationApplication application = cityOperationApplicationMapper.selectOne(query1);
            if (application != null) {
                merchant.setProvinceCode(application.getProvinceCode());
                merchant.setProvinceName(application.getProvinceName());
                merchant.setCityCode(application.getCityCode());
                merchant.setCityName(application.getCityName());
                merchant.setOperationCenterId(application.getOperationCenterId());
                merchant.setOperationCenterName(application.getOperationCenterName());
            }
        }
        return merchant;
    }

    private MaTechnician findMerchantById(Integer userId) {
        LambdaQueryWrapper<MaTechnician> query = new LambdaQueryWrapper<>();
        query.eq(MaTechnician::getId, userId);
        return maTechnicianMapper.selectOne(query);
    }

    private MerchantAuditFile buildMerchantAuditFile(MaTechnician merchant) {
        MerchantAuditFile merchantAuditFile = new MerchantAuditFile();
        merchantAuditFile.setMerchant(merchant);
        if (merchant != null && merchant.getId() != null) {
            merchantAuditFile.setMerchantAuditFile(listMerchantApplyFiles(merchant.getId()));
        } else {
            merchantAuditFile.setMerchantAuditFile(Collections.emptyList());
        }
        return merchantAuditFile;
    }

    private List<MerchantApplyFile> listMerchantApplyFiles(Integer userId) {
        LambdaQueryWrapper<MerchantApplyFile> query1 = new LambdaQueryWrapper<>();
        query1.eq(MerchantApplyFile::getMerchantId, userId);
        return merchantApplyFileMapper.selectList(query1);
    }

    /**
     * 查询商户合同记录信息
     *
     * @param userId
     * @return ContractRecordVO
     */
    @Override
    public ContractRecordVO getContractRecords(Long userId) {
        LambdaQueryWrapper<ContractRecord> query = new LambdaQueryWrapper<>();
        query.eq(ContractRecord::getMerchantId, userId);
        List<ContractRecord> contractRecordList = contractRecordMapper.selectList(query);
        ContractRecordVO result = new ContractRecordVO();
        if (CollectionUtils.isEmpty(contractRecordList)) {
            result.setMerchantId(userId == null ? null : userId.intValue());
            result.setFile(Collections.emptyList());
            return result;
        }

        Set<String> seen = new HashSet<>();
        contractRecordList = contractRecordList.stream()
                .filter(record -> record.getContractName() != null && seen.add(record.getContractName()))
                .collect(Collectors.toList());

        ContractRecord firstRecord = contractRecordList.get(0);
        result.setSignTime(firstRecord.getSignTime());
        result.setSignerName(firstRecord.getSignerName());
        result.setMerchantId(firstRecord.getMerchantId());
        result.setFile(contractRecordList.stream()
                .map(this::buildContractFileVO)
                .collect(Collectors.toList()));
        return result;
    }

    /**
     * 构建合同文件VO
     *
     * @param record
     * @return ContractFileVO.Contract
     */
    private ContractRecordVO.ContractFileVO buildContractFileVO(ContractRecord record) {
        ContractRecordVO.ContractFileVO fileVO = new ContractRecordVO.ContractFileVO();
        fileVO.setId(record.getId());
        fileVO.setContractName(record.getContractName());
        // 获取当前项目的访问路径
        String contextPath = ServletUtils.getProjectAccessPath();
        String fileUrl = contextPath + record.getFileUrl();
        fileVO.setFileUrl(fileUrl);
        return fileVO;
    }

    @Override
    public Page<MerchantListVO> getMerchantPage(MerchantListDTO dto) {

        // 1. 执行分页查询 (不带免车费过滤)
        Page<MerchantListVO> page = new Page<>(dto.getCurrent(), dto.getSize());
        page = this.baseMapper.getMerchantPage(page, dto);
        List<MerchantListVO> records = page.getRecords();
        if (CollUtil.isEmpty(records)) {
            return page;
        }

        // 2. 如果用户勾选了“免车费”，则在内存中进行精准过滤
        if (Boolean.TRUE.equals(dto.getFreeCarFee())) {
            boolean isDay = this.maProjectFareSettingService.isDayTimePeriod(LocalDateTime.now());

            // 3. 过滤列表
            Iterator<MerchantListVO> iterator = records.iterator();
            while (iterator.hasNext()) {
                MerchantListVO vo = iterator.next();
                double currentDistance = vo.getCurrentDistance(); // 数据库算出的距离(km)

                // 获取该商户的有效免费里程
                BigDecimal freeKm = this.maProjectFareSettingService.getMerchantFreeKm(Long.parseLong(vo.getId()), vo.getProjectId(), isDay);

                // 核心判断：如果没配置(为0) 或者 距离超过了免费里程，则剔除
                if (freeKm == null || freeKm.doubleValue() <= 0 || currentDistance > freeKm.doubleValue()) {
                    iterator.remove();
                }
            }
        }

        return page;
    }

    @Override
    public Page<ProjectInfoVO> getByMerchantProject(MerchantProjectDTO dto) {
        Page<ProjectInfoVO> page = new Page<>(dto.getCurrent(), dto.getSize());
        page = this.baseMapper.getByMerchantProject(page, dto);
        if(ObjectUtil.isNotNull(page) && CollUtil.isNotEmpty(page.getRecords())){
            List<ProjectInfoVO> records = page.getRecords();

            for (ProjectInfoVO vo : records) {
                Integer unitType = vo.getUnitType();
                String unitKey = ObjectUtil.isNotNull(unitType) ? unitType.toString() : "";
                String unitTypeName = unitDictCache.getOrDefault(unitKey, "未知单位");
                vo.setUnitTypeName(unitTypeName);
            }
            page.setRecords(records);
            return page;
        }
        return page;
    }

    @Override
    public MerchantDetailVO getDetail(MerchantDetailDTO dto) {

        // 1. 获取商户信息
        Long merchantId = dto.getMerchantId();
        MerchantDetailVO detail = this.baseMapper.getDetail(dto);
        if (ObjectUtil.isNull(detail)) {
            throw new ServiceException("商户不存在");
        }

        // 2. 获取商户的默认地址
        TAddress address = this.addressService.getOne(new LambdaQueryWrapper<TAddress>()
                .eq(TAddress::getMerchantId, merchantId)
                .eq(TAddress::getIsDefault, 1)
                .eq(TAddress::getIsDelete, NOT_DELETED));
        if (ObjectUtil.isNull(address)) {
            throw new ServiceException("无法获取商户的默认地址");
        }

        // 3. 计算当前用户距离商户距离
        BigDecimal distanceStr = DistanceUtil.formatDistanceInKilometers(
                dto.getLatitude(), dto.getLongitude(),
                address.getLatitude(), address.getLongitude()
        );
        detail.setDistance(distanceStr);

        // 4. 获取商户是否被当前用户收藏
        boolean collected = this.collectService.isCollected(merchantId);
        detail.setCollected(collected);

        return detail;
    }

    @Override
    public List<MerchantWithAddressVO> listValidTechWithAddress(long offset, long fetchCount) {
        return this.baseMapper.listValidTechWithAddress(offset, fetchCount);
    }

    @Override
    public List<MaTechnician> selectAllValidTech() {
        return this.baseMapper.selectAllValidTech();
    }

    @Override
    public Page<MerchantListVO> homeSearchPage(MerchantListDTO dto) {
        Page<MerchantListVO> page = new Page<>(dto.getCurrent(), dto.getSize());
        return this.baseMapper.getMerchantPage(page, dto);
    }

    @Override
    public Long countValidTech() {
        return this.baseMapper.countValidTech();
    }

    @Override
    public List<MerchantWithAddressVO> selectTechWithAddressByIds(List<Integer> techIds) {
        return this.baseMapper.selectTechWithAddressByIds(techIds);
    }

    /**
     * 申请开通新服务
     *
     * @param dto
     */
    private void extracted(MaProjectSaveDto dto) {
        LambdaQueryWrapper<Project> query = new LambdaQueryWrapper<>();
        query.in(Project::getId, dto.getProjectIdList());
        List<Project> projectList = projectMapper.selectList(query);
        for (Project project : projectList) {
            MaProject maProject = new MaProject();
            maProject.setProjectId(project.getId());
            maProject.setProjectName(project.getTitle());
            maProject.setProjectDescribe(project.getDetail());
            maProject.setProjectDuration(project.getStandardDuration());
            maProject.setProjectOriginalPrice(project.getPrice());
            maProject.setProjectMaxPrice(project.getPriceMax());
            maProject.setProjectLowestPrice(project.getPriceMin());
            maProject.setMerchantId(dto.getMerchantId());
            maProject.setApplyTime(DateUtils.getNowDate());
            maProject.setMerchantPhone(dto.getMerchantPhone());

            maProject.setCreateBy(dto.getMerchantId().longValue());
            maProject.setCreateTime(DateUtils.getNowDate());
            maProjectMapper.insert(maProject);
        }
    }

    /**
     * 状态切换
     *
     * @param userId
     * @param forceConfirm
     * @return Result
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result switchToOffline(Long userId, Boolean forceConfirm) {
        if (userId == null) {
            throw new ServiceException("商户ID不能为空");
        }
        MaTechnician technician = getTechnician(userId);
        if (technician == null) {
            throw new ServiceException("商户不存在");
        }

        if (!hasActiveSkills(userId)) {
            throw new ServiceException("请先申请开通技能");
        }
        if (!hasHomeAddress(userId)) {
            throw new ServiceException("请完善家庭地址");
        }
        // 由在线接单切换为休息状态
        if (TechnicianStatusEnum.ONLINE.getCode().equals(technician.getPostState())) {
            return switchOnlineToResting(userId, technician, Boolean.TRUE.equals(forceConfirm));
        }
        // 切换到在线接单状态
        switchRestingToOnline(userId, technician);
        return Result.ok("状态已切换成功");
    }

    /**
     * 状态切换：下线休息
     *
     * @param userId
     * @param technician
     * @param forceConfirm
     * @return Result
     */
    private Result switchOnlineToResting(Long userId, MaTechnician technician, boolean forceConfirm) {
        if (JsStatusEnum.JS_SERVICE.getCode().equals(technician.getServiceState())) {
            throw new ServiceException("您有服务中的订单，不能下岗");
        }

        MerchantDailyAttendance currentAttendance = getTodayAttendance(userId);
        if (ProjectCategoryEnum.MASSAGE.getCode().equals(technician.getServiceTag())) {
            AttendanceRule rule = getAttendanceRule();
            if (rule != null && rule.getBasicWorkHours() != null) {
                long minutesOnline = calculateTodayOnlineMinutes(userId, currentAttendance);
                long requiredMinutes = rule.getBasicWorkHours().multiply(BigDecimal.valueOf(60)).longValue();
                if (minutesOnline < requiredMinutes && !forceConfirm) {
                    return Result.ok(buildOfflineConfirmMessage(requiredMinutes, minutesOnline));
                }
            }
        }

        updateStatus(userId, TechnicianStatusEnum.RESTING);
        closeTodayAttendance(currentAttendance);
        return Result.ok("状态已切换成功");
    }

    private void switchRestingToOnline(Long userId, MaTechnician technician) {
        updateStatus(userId, TechnicianStatusEnum.ONLINE);
        MerchantDailyAttendance merchantDailyAttendance = new MerchantDailyAttendance()
                .setMerchantId(userId.intValue())
                .setAttendanceDate(DateUtils.getNowDate())
                .setMerchantName(technician.getTeName())
                .setAttendanceStartTime(DateUtils.getNowDate())
                .setCreateBy(technician.getTeName())
                .setCreateTime(LocalDateTime.now());
        merchantDailyAttendanceMapper.insert(merchantDailyAttendance);
    }

    /**
     * 计算商户今天在线时间（分钟）
     *
     * @param userId
     * @param currentAttendance
     * @return long
     */
    private long calculateTodayOnlineMinutes(Long userId, MerchantDailyAttendance currentAttendance) {
        long totalMinutes = getWorkDuration(userId, currentAttendance == null ? null : currentAttendance.getId());
        if (currentAttendance == null || currentAttendance.getAttendanceStartTime() == null) {
            return totalMinutes;
        }
        LocalDateTime startTime = toLocalDateTime(currentAttendance.getAttendanceStartTime());
        return totalMinutes + Math.max(0L, Duration.between(startTime, LocalDateTime.now()).toMinutes());
    }

    /**
     * 构建下线确认消息
     *
     * @param requiredMinutes
     * @param minutesOnline
     * @return String
     */
    private String buildOfflineConfirmMessage(long requiredMinutes, long minutesOnline) {
        long remainMinutes = Math.max(0L, requiredMinutes - minutesOnline);
        long remainHours = remainMinutes / 60;
        long remainMinutePart = remainMinutes % 60;
        String remainText = remainMinutePart == 0
                ? remainHours + "小时"
                : remainHours + "小时" + remainMinutePart + "分钟";
        return "平台对您的在线时间做了约定，每日在线需满足"
                + (requiredMinutes / 60) + "小时，距离您下线时间还剩余" + remainText
                + "不满足在线时间将收到平台处罚，是否确认下线？";
    }

    /**
     * 关闭今天商户的考勤记录
     *
     * @param attendance
     */
    private void closeTodayAttendance(MerchantDailyAttendance attendance) {
        if (attendance == null || attendance.getId() == null || attendance.getAttendanceStartTime() == null) {
            return;
        }
        LocalDateTime startTime = toLocalDateTime(attendance.getAttendanceStartTime());
        long workMinutes = Math.max(0L, Duration.between(startTime, LocalDateTime.now()).toMinutes());
        LambdaUpdateWrapper<MerchantDailyAttendance> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(MerchantDailyAttendance::getId, attendance.getId())
                .set(MerchantDailyAttendance::getAttendanceEndTime, DateUtils.getNowDate())
                .set(MerchantDailyAttendance::getTotalWorkMinutes, Math.toIntExact(workMinutes))
                .set(MerchantDailyAttendance::getUpdateTime, DateUtils.getNowDate());
        merchantDailyAttendanceMapper.update(attendance, updateWrapper);
    }

    private LocalDateTime toLocalDateTime(Date date) {
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
    }

    /**
     * 获取今天商户的考勤记录
     *
     * @param userId 技师ID
     * @return MerchantDailyAttendance 考勤记录
     */
    private MerchantDailyAttendance getTodayAttendance(Long userId) {
        LambdaQueryWrapper<MerchantDailyAttendance> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(MerchantDailyAttendance::getMerchantId, userId)
                .eq(MerchantDailyAttendance::getAttendanceDate, DateUtils.getNowDate())
                .orderByDesc(MerchantDailyAttendance::getCreateTime)
                .last("LIMIT 1");
        return merchantDailyAttendanceMapper.selectOne(wrapper);
    }

    /**
     * 获取商户的累计工作时长
     *
     * @param userId              技师ID
     * @param excludeAttendanceId 排除的考勤记录ID
     * @return 工作时长（分钟）
     */
    private long getWorkDuration(Long userId, Long excludeAttendanceId) {
        LambdaQueryWrapper<MerchantDailyAttendance> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(MerchantDailyAttendance::getMerchantId, userId)
                .eq(MerchantDailyAttendance::getAttendanceDate, DateUtils.getNowDate());
        List<MerchantDailyAttendance> attendances = merchantDailyAttendanceMapper.selectList(wrapper);
        if (attendances == null || attendances.isEmpty()) return 0;
        return attendances.stream()
                .filter(attendance -> excludeAttendanceId == null || !excludeAttendanceId.equals(attendance.getId()))
                .filter(attendance -> attendance.getTotalWorkMinutes() != null)
                .mapToLong(MerchantDailyAttendance::getTotalWorkMinutes)
                .sum();
    }

    /**
     * 获取商户的考勤规则
     *
     * @return 考勤规则
     */
    private AttendanceRule getAttendanceRule() {
        LambdaQueryWrapper<AttendanceRule> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(AttendanceRule::getIsDelete, 0);
        wrapper.eq(AttendanceRule::getWorkDurationRuleEnabled, 1);
        wrapper.last("LIMIT 1");
        return attendanceRuleMapper.selectOne(wrapper);
    }

    /**
     * 判断商户是否有开通的技能
     *
     * @param userId 商户ID
     * @return boolean true: 有可用技能, false: 无
     */
    public boolean hasActiveSkills(Long userId) {
        LambdaQueryWrapper<MaProject> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(MaProject::getMerchantId, userId).eq(MaProject::getAuditStatus, 1);
        // 只要查到一条记录即返回 true
        return maProjectMapper.selectCount(wrapper) > 0;
    }

    /**
     * 判断用户是否有家庭地址（通常指设置为默认的地址）
     *
     * @param userId 技师ID
     * @return boolean true: 有地址, false: 无
     */
    public boolean hasHomeAddress(Long userId) {
        LambdaQueryWrapper<TAddress> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(TAddress::getMerchantId, userId).eq(TAddress::getUserType, 2);
        // 统计数量是否大于0
        long count = addressMapper.selectCount(wrapper);
        return count > 0;
    }

    /**
     * 获取商户信息
     *
     * @param userId 商户ID
     * @return MaTechnician 商户信息
     */
    private MaTechnician getTechnician(Long userId) {
        LambdaQueryWrapper<MaTechnician> query = new LambdaQueryWrapper<>();
        query.eq(MaTechnician::getId, userId);
        return maTechnicianMapper.selectOne(query);
    }

    /**
     * 更新商户接单状态
     *
     * @param userId 商户ID
     * @param status 商户接单状态枚举
     */
    private void updateStatus(Long userId, TechnicianStatusEnum status) {
        LambdaUpdateWrapper<MaTechnician> update = new LambdaUpdateWrapper<>();
        update.eq(MaTechnician::getId, userId);
        update.set(MaTechnician::getPostState, status.getCode());
        maTechnicianMapper.update(null, update);
    }

    /**
     * 后台待审核页面审核通过商户。
     *
     * @param id        商户ID
     * @param dto       待审核通过参数
     * @param loginUser 当前登录用户
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int approvePendingMerchantAudit(Integer id, MaTechnicianPendingAuditSubmitDTO dto, LoginUser loginUser) {
        if (id == null) {
            throw new ServiceException("商户ID不能为空");
        }
        if (dto == null) {
            throw new ServiceException("审核参数不能为空");
        }

        String idCardFront = checkRequiredFileUrl(dto.getIdCardFront(), "身份证正面加密图片");
        String idCardBack = checkRequiredFileUrl(dto.getIdCardBack(), "身份证反面加密图片");
        String healthCertificate = checkRequiredFileUrl(dto.getHealthCertificate(), "健康证加密图片");
        String qualificationCertificate = checkRequiredFileUrl(dto.getQualificationCertificate(), "资格证加密图片");
        checkRequiredExpirationDate(dto.getIdCardExpirationDate(), "身份证到期时间");
        checkRequiredExpirationDate(dto.getHealthCertificateExpirationDate(), "健康证到期时间");
        checkRequiredExpirationDate(dto.getQualificationCertificateExpirationDate(), "资格证到期时间");

        String auditRemark = dto.getAuditRemark() == null ? "" : dto.getAuditRemark().trim();
        if (auditRemark.length() > AUDIT_REMARK_MAX_LENGTH) {
            throw new ServiceException("审核备注长度不能超过" + AUDIT_REMARK_MAX_LENGTH + "个字符");
        }

        MaTechnician existsMerchant = maTechnicianMapper.selectMerchantById(id);
        if (existsMerchant == null || !NOT_DELETED.equals(existsMerchant.getIsDelete())) {
            throw new ServiceException("商户不存在或已删除");
        }
        if (!Integer.valueOf(AUDIT_WAIT_REVIEW).equals(existsMerchant.getAuditStatus())) {
            throw new ServiceException("当前商户不是待审核状态，不能审核通过");
        }

        MaTechnician maTechnician = new MaTechnician();
        maTechnician.setId(id);
        /*maTechnician.setIdCard(String.join(",", idCardFront, idCardBack));
        maTechnician.setHealthCertificate(healthCertificate);
        maTechnician.setQualificationCertificate(qualificationCertificate);*/
        maTechnician.setIdCardExpirationDate(dto.getIdCardExpirationDate());
        maTechnician.setHealthCertificateExpirationDate(dto.getHealthCertificateExpirationDate());
        maTechnician.setQualificationCertificateExpirationDate(dto.getQualificationCertificateExpirationDate());
        maTechnician.setAuditStatus(AUDIT_APPROVED);
        maTechnician.setAuditRemark(auditRemark);
        maTechnician.setApproveTime(DateUtils.getNowDate());
        if (loginUser != null && loginUser.getUser() != null) {
            maTechnician.setUpdateBy(loginUser.getUser().getUserName());
        }
        maTechnician.setUpdateTime(DateUtils.getNowDate());

        int rows = maTechnicianMapper.approvePendingMerchantAuditById(maTechnician);
        if (rows <= 0) {
            throw new ServiceException("待审核商户审核通过失败");
        }
        return rows;
    }

    private String checkRequiredFileUrl(String value, String fieldName) {
        if (StringUtils.isBlank(value)) {
            throw new ServiceException(fieldName + "不能为空");
        }
        return value.trim();
    }

    private void checkRequiredExpirationDate(LocalDate value, String fieldName) {
        if (value == null) {
            throw new ServiceException(fieldName + "不能为空");
        }
        if (value.isBefore(LocalDate.now())) {
            throw new ServiceException(fieldName + "不能早于当前日期");
        }
    }

    /**
     * 技师待处理订单列表
     *
     * @param query
     * @return
     */
    @Override
    public List<WaitOrderDTO> listWaitOrder(WaitOrderQueryDTO query) {
        LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(TOrder::getStatus, OrderStatusEnum.WAIT_JD.getCode());
        // 1.查询所有待派未接单订单（status=待接单）
        List<TOrder> allWaitOrder = orderMapper.selectList(queryWrapper);
        if (CollectionUtils.isEmpty(allWaitOrder)) {
            return Collections.emptyList();
        }

        BigDecimal techLat = query.getTechLat();
        BigDecimal techLng = query.getTechLng();

        // 2.逐个计算两点距离（Haversine公式）
        List<WaitOrderDTO> dtoList = allWaitOrder.stream().map(order -> {
            WaitOrderDTO dto = new WaitOrderDTO();
            dto.setOrderId(order.getId());
            dto.setProjectName(getShortProjectName(order.getProjectName()));
            dto.setCustomerType("新客户");//无历史绑定默认新客，接单后再统计老客
            dto.setOrderCreateTime(order.getCreateTime());
            dto.setAppointTime(order.getAppointmentStartTime());
            dto.setTargetAddress(order.getContactAddressInfo());
            dto.setOrderLat(order.getUserLatitude());
            dto.setOrderLng(order.getUserLongitude());

            // 计算两点距离 单位：米
            BigDecimal disMeter = calcDistance(techLat, techLng, order.getUserLatitude(), order.getUserLongitude());
            dto.setDistanceMeter(disMeter);
            dto.setDistanceDesc(formatDistance(disMeter));
            return dto;
        }).collect(Collectors.toList());

        // 3.距离升序：近的排在最前面
        return dtoList.stream()
                .sorted(Comparator.comparing(WaitOrderDTO::getDistanceMeter))
                .collect(Collectors.toList());
    }

    /**
     * 接单
     *
     * @param req
     * @return
     */
    @Override
    public String acceptOrder(AcceptOrderReqDTO req) {
        Long techId = req.getTechId();
        Long orderId = req.getOrderId();
        //【校验1：订单是否已被其他技师接单】
        TOrder order = orderMapper.selectById(orderId);
        if (OrderStatusEnum.RECEIVED_ORDER.getCode().equals(order.getStatus())) {
            return OrderTipEnum.REPEAT_ORDER.getTip();
        }
        //【校验2：时间冲突校验（该技师已有已接单订单）】
        boolean isTimeConflict = checkOrderTimeConflict(techId, order);
        if (isTimeConflict) {
            String tip = String.format(OrderTipEnum.TIME_CONFLICT.getTip(),
                    order.getStartTime().format(DateTimeFormatter.ofPattern("MM月dd日HH:mm")),
                    order.getCompletedTime().format(DateTimeFormatter.ofPattern("MM月dd日HH:mm")));
            return tip;
        }
        //【校验3：技师休息状态】
        MaTechnician tech = maTechnicianMapper.selectById(techId);
        if (TechnicianStatusEnum.RESTING.getCode().equals(tech.getPostState())) {
            return OrderTipEnum.REST_CONFIRM.getTip();
        }
        // 正常接单，绑定技师ID到订单
        doAcceptOrder(techId, orderId);
        return OrderTipEnum.ALREADY_ACCEPT.getTip();
    }

    /**
     * 技师接单确认接单
     *
     * @param techId
     * @param orderId
     * @return
     */
    @Override
    public String confirmRestAccept(Long techId, Long orderId) {
        LambdaUpdateWrapper<MaTechnician> update = new LambdaUpdateWrapper<>();
        update.eq(MaTechnician::getId, techId);
        update.set(MaTechnician::getPostState, TechnicianStatusEnum.ONLINE.getCode());
        maTechnicianMapper.update(null, update);
        doAcceptOrder(techId, orderId);
        return OrderTipEnum.ALREADY_ACCEPT.getTip();
    }

    /**
     * 技师拒绝接单
     *
     * @param req
     * @return
     */
    @Override
    public void refuseOrder(RefuseOrderReqDTO req) {
        LambdaUpdateWrapper<TOrder> update = new LambdaUpdateWrapper<>();
        update.eq(TOrder::getId, req.getOrderId());
        update.set(TOrder::getStatus, OrderStatusEnum.REFUSE.getCode());
        update.set(TOrder::getRejectedReason, req.getRefuseReason());
        orderMapper.update(null, update);
        //拒单后订单重回待接单池，其他技师可刷到
        LambdaUpdateWrapper<TOrder> update2 = new LambdaUpdateWrapper<>();
        update2.eq(TOrder::getId, req.getOrderId());
        update2.set(TOrder::getStatus, OrderStatusEnum.WAIT_JD.getCode());
        update2.set(TOrder::getMerchantId, "");
        update2.set(TOrder::getRejectedReason, "");
        orderMapper.update(null, update2);
    }
    // =================工具方法=================

    /**
     * Haversine 计算经纬度距离 返回米
     */
    private BigDecimal calcDistance(BigDecimal lat1, BigDecimal lng1, BigDecimal lat2, BigDecimal lng2) {
        // 球面距离计算公式，地球半径6371000米
        // 可使用BigDecimal三角函数或数据库函数优化
        double latRad1 = Math.toRadians(lat1.doubleValue());
        double latRad2 = Math.toRadians(lat2.doubleValue());
        double lngRad1 = Math.toRadians(lng1.doubleValue());
        double lngRad2 = Math.toRadians(lng2.doubleValue());

        double dLat = latRad2 - latRad1;
        double dLng = lngRad2 - lngRad1;
        double a = Math.pow(Math.sin(dLat / 2), 2)
                + Math.cos(latRad1) * Math.cos(latRad2)
                * Math.pow(Math.sin(dLng / 2), 2);
        double dis = 2 * 6371000 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return BigDecimal.valueOf(dis).setScale(2, BigDecimal.ROUND_HALF_UP);
    }

    private String getShortProjectName(String name) {
        if (name != null && name.length() > 10) {
            return name.substring(0, 8) + "...";
        }
        return name;
    }

    private String formatDistance(BigDecimal distanceMeter) {
        BigDecimal km = distanceMeter.divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
        if (km.compareTo(BigDecimal.ONE) > 0) {
            return km + "km";
        } else {
            return distanceMeter.intValue() + "m";
        }
    }

    /**
     * 校验技师已有订单时间冲突（只查该技师已接单数据）
     */
    private boolean checkOrderTimeConflict(Long techId, TOrder newOrder) {
        LambdaQueryWrapper<TOrder> query = new LambdaQueryWrapper<>();
        query.eq(TOrder::getMerchantId, techId);
        query.eq(TOrder::getStatus, OrderStatusEnum.RECEIVED_ORDER.getCode());
        query.eq(TOrder::getStartTime, newOrder.getStartTime());

        List<TOrder> acceptedOrders = orderMapper.selectList(query);
        LocalDate newOrderDate = newOrder.getStartTime().toLocalDate();
        LocalDateTime newStart = newOrder.getStartTime();
        for (TOrder old : acceptedOrders) {
            if (!newOrderDate.isEqual(old.getCompletedTime().toLocalDate())) {
                continue;
            }
            LocalDateTime oldEnd = old.getCompletedTime();
            if (newStart.isBefore(oldEnd) || newStart.isEqual(oldEnd)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 接单：给订单赋值技师ID
     */
    private void doAcceptOrder(Long techId, Long orderId) {
        TOrder update = new TOrder();
        update.setId(orderId);
        update.setMerchantId(techId);
        update.setStatus(OrderStatusEnum.RECEIVED_ORDER.getCode());
        orderMapper.updateById(update);
    }
}
