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.attendanceconfig.domain.AttendanceRule; import com.ylx.attendanceconfig.mapper.AttendanceRuleMapper; import com.ylx.collect.service.CollectService; import com.ylx.common.core.domain.model.LoginUser; import com.ylx.common.exception.ServiceException; import com.ylx.common.utils.DateUtils; import com.ylx.common.utils.DistanceUtil; import com.ylx.common.utils.ServletUtils; import com.ylx.common.utils.StringUtils; import com.ylx.fareSetting.service.IMaProjectFareSettingService; 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.address.service.TAddressService; 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.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 org.springframework.beans.BeanUtils; 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.*; /** * 技师Service业务层处理 * * @author ylx * @date 2024-03-22 */ @Service public class MaTechnicianServiceImpl extends ServiceImpl 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 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; /** * 商户入驻申请注册 * * @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 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 updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(TWxUser::getcOpenid, req.getCOpenid()); tWxUserMapper.update(wxUser, updateWrapper); /*LambdaUpdateWrapper 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 files = resolveApplyFiles(req.getReq()); if (CollectionUtils.isEmpty(files)) { throw new ServiceException("请上传申请入驻文件"); } replaceApplyFiles(merchantId, files); updateTechnicianBaseInfo(req.getTechnician()); } /** * 将文件分组格式转换为单文件记录。 * * @param reqFiles 入驻资料文件 * @return List 转换后的文件记录 */ private List resolveApplyFiles(List reqFiles) { if (CollectionUtils.isEmpty(reqFiles)) { return Collections.emptyList(); } List 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 files) { Map> filesByType = files.stream() .collect(Collectors.groupingBy(MerchantApplyFileDto::getFileType, LinkedHashMap::new, Collectors.toList())); for (Map.Entry> entry : filesByType.entrySet()) { LambdaQueryWrapper 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); } } } /** * 修改商户资料。 *

* 基础信息只允许修改昵称和简介;入驻资料文件按本次提交的文件类型整组替换。 *

* * @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 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 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 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 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 updateWrapper = new LambdaUpdateWrapper(); 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 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 updateWrapper = new LambdaUpdateWrapper(); 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 */ private List listPendingProfileFiles(Integer merchantId) { LambdaQueryWrapper 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 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 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 pendingFiles, String auditRemark, LoginUser loginUser) { Set fileTypes = pendingFiles.stream() .map(MerchantApplyFile::getFileType) .filter(StringUtils::isNotBlank) .collect(Collectors.toCollection(LinkedHashSet::new)); if (fileTypes.isEmpty()) { return 0; } String updateBy = getAuditUserName(loginUser); LambdaUpdateWrapper 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 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 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 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 files) { Map> filesByType = files.stream() .collect(Collectors.groupingBy(MerchantApplyFileDto::getFileType, LinkedHashMap::new, Collectors.toList())); for (Map.Entry> 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 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 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 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 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 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 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 buildFileGroups(List files) { Map> 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 groups = new ArrayList<>(); for (String fileType : PROFILE_FILE_TYPES) { List typeFiles = filesByType.getOrDefault(fileType, Collections.emptyList()); List officialFiles = typeFiles.stream() .filter(file -> Integer.valueOf(PROFILE_AUDIT_APPROVED).equals(file.getAuditStatus())) .collect(Collectors.toList()); List 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 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 pendingFiles) { if (CollectionUtils.isEmpty(pendingFiles)) { return null; } return pendingFiles.stream() .map(MerchantApplyFile::getAuditRemark) .filter(StringUtils::isNotBlank) .findFirst() .orElse(null); } private List toProfileFileVOList(List 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 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 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 queryPhoneWrapper = new LambdaQueryWrapper<>(); queryPhoneWrapper.eq(MaTechnician::getTePhone, phone); MaTechnician maTechnicianPhone = maTechnicianMapper.selectOne(queryPhoneWrapper); if (maTechnicianPhone != null) { throw new RuntimeException("手机号已存在,请更换手机号"); } //3、判断手机号是否已绑定其他用户 LambdaQueryWrapper 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 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 */ @Override public List selectMaTechnicianListBy(Integer merchantId, Integer auditStatus) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(MaProject::getMerchantId, merchantId); queryWrapper.eq(MaProject::getAuditStatus, auditStatus); List 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()); }); return maProjects; } /** * 查询服务分类项目列表 * * @param typeId 技师类型 * @return 技师列表 */ @Override public List selectTechnicianListBy(String typeId) { LambdaQueryWrapper 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 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 * @param loginUser 当前登录用户 * @return 上传结果 */ @Override @Transactional(rollbackFor = Exception.class) public Integer uploadMerchantContract(Integer id, Map 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 queryWrapper = Wrappers.lambdaQuery(); queryWrapper.eq(MerchantApplyFile::getMerchantId, id); List merchantApplyFiles = merchantApplyFileMapper.selectList(queryWrapper); MaTechnicianCertificateVO certificate = new MaTechnicianCertificateVO(); certificate.setMerchantId(id); Map> 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 文件URL列表 */ private List getCertificateFileUrls(Map> fileUrlsByType, String type) { return fileUrlsByType.getOrDefault(type, Collections.emptyList()); } /** * 全量替换商户与服务项目关联关系。 * * @param technicianId 商户ID * @param projectIds 服务项目ID集合 */ private void replaceProjectRelations(Integer technicianId, Set 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 selectMerchantAuditList(Page page, MaTechnicianAuditQueryDTO dto) { if (dto != null && dto.getAuditStatus() != null) { checkEnumValue(dto.getAuditStatus(), "审核状态", 0, 1, 2, 3); } Page pageParam = page == null ? new Page<>(1, 10) : page; return maTechnicianMapper.selectMerchantAuditList(pageParam, dto); } /** * 后台查询商户列表 * * @param page 分页参数 * @param dto 查询条件 * @return 商户分页列表 */ @Override public Page selectMerchantList(Page page, MaTechnicianMerchantQueryDTO dto) { Page 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 getMerchantRecommend(MassageMerchantRecommendDto 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 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 projectIds, Set categoryIds) { if (projectIds == null || projectIds.isEmpty()) { throw new ServiceException("服务项目不能为空"); } Set distinctProjectIds = new LinkedHashSet<>(); for (Integer projectId : projectIds) { if (projectId == null) { throw new ServiceException("服务项目ID不能为空"); } distinctProjectIds.add(projectId); } List projects = projectMapper.selectList(new LambdaQueryWrapper() .in(Project::getId, distinctProjectIds) .eq(Project::getIsDelete, 0)); if (projects.size() != distinctProjectIds.size()) { throw new ServiceException("服务项目不存在或已删除"); } Map projectMap = projects.stream() .collect(Collectors.toMap(project -> project.getId(), Function.identity(), (left, right) -> left)); Set 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 checkOpenServiceIds(List openService) { if (openService == null || openService.isEmpty()) { throw new ServiceException("服务类目不能为空"); } Set categoryIds = new LinkedHashSet<>(); for (Integer categoryId : openService) { if (categoryId == null) { throw new ServiceException("服务类目ID不能为空"); } categoryIds.add(categoryId); } return categoryIds; } private String joinIds(Set ids) { return ids.stream() .map(String::valueOf) .collect(Collectors.joining(",")); } private String joinProjectTitles(Set projectIds, Map 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 projectIds) { if (technicianId == null) { throw new ServiceException("商户ID不能为空"); } List 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 categoryIds; private final Set projectIds; private final Map projectMap; private MerchantProjectSelection(Set categoryIds, Set projectIds, Map projectMap) { this.categoryIds = categoryIds; this.projectIds = projectIds; this.projectMap = projectMap; } private Set getCategoryIds() { return categoryIds; } private Set getProjectIds() { return projectIds; } private Map getProjectMap() { return projectMap; } } /** * 获取未申请技能列表 * * @param merchantId * @param serviceTag * @return List */ @Override public List getNotApplyList(Integer merchantId, Integer serviceTag) { LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(MaProject::getMerchantId, merchantId); query.eq(MaProject::getServiceTag, serviceTag); // 审核状态:审核通过 query.eq(MaProject::getAuditStatus, 1); List maProjectList = maProjectMapper.selectList(query); // 获取已申请技能ID集合 List projectIdList = maProjectList.stream().map(MaProject::getProjectId).collect(Collectors.toList()); if (projectIdList.size() == 0) { LambdaQueryWrapper query1 = new LambdaQueryWrapper<>(); query1.eq(Project::getType, serviceTag); return projectMapper.selectList(query1); } LambdaQueryWrapper 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 query = new LambdaQueryWrapper<>(); query.eq(MaTechnician::getCOpenid, openid); MaTechnician merchant = maTechnicianMapper.selectOne(query); // 根据商户ID查询city_operation_application表 if (merchant != null && merchant.getId() != null) { LambdaQueryWrapper 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 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 listMerchantApplyFiles(Integer userId) { LambdaQueryWrapper query1 = new LambdaQueryWrapper<>(); query1.eq(MerchantApplyFile::getMerchantId, userId); return merchantApplyFileMapper.selectList(query1); } /** * 查询商户合同记录信息 * * @param userId * @return ContractRecordVO */ @Override public ContractRecordVO getContractRecords(Long userId) { LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(ContractRecord::getMerchantId, userId); List 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 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 getMerchantPage(MerchantListDTO dto) { // 1. 执行分页查询 (不带免车费过滤) Page page = new Page<>(dto.getCurrent(), dto.getSize()); page = this.baseMapper.getMerchantPage(page, dto); List 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 iterator = records.iterator(); while (iterator.hasNext()) { MerchantListVO vo = iterator.next(); double currentDistance = vo.getDistance(); // 数据库算出的距离(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 getByMerchantProject(MerchantProjectDTO dto) { Page page = new Page<>(dto.getCurrent(), dto.getSize()); return this.baseMapper.getByMerchantProject(page, dto); } @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() .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; } /** * 申请开通新服务 * * @param dto */ private void extracted(MaProjectSaveDto dto) { LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.in(Project::getId, dto.getProjectIdList()); List 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 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 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 wrapper = new LambdaQueryWrapper<>(); wrapper.eq(MerchantDailyAttendance::getMerchantId, userId) .eq(MerchantDailyAttendance::getAttendanceDate, DateUtils.getNowDate()); List 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 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 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 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 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 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 listWaitOrder(WaitOrderQueryDTO query) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(TOrder::getStatus, OrderStatusEnum.WAIT_JD.getCode()); // 1.查询所有待派未接单订单(status=待接单) List allWaitOrder = orderMapper.selectList(queryWrapper); if (CollectionUtils.isEmpty(allWaitOrder)) { return Collections.emptyList(); } BigDecimal techLat = query.getTechLat(); BigDecimal techLng = query.getTechLng(); // 2.逐个计算两点距离(Haversine公式) List 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 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 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 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 query = new LambdaQueryWrapper<>(); query.eq(TOrder::getMerchantId, techId); query.eq(TOrder::getStatus, OrderStatusEnum.RECEIVED_ORDER.getCode()); query.eq(TOrder::getStartTime, newOrder.getStartTime()); List 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); } }