package com.ylx.massage.service.impl; import java.math.BigDecimal; import java.text.SimpleDateFormat; 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 com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ylx.attendanceconfig.domain.AttendanceRule; import com.ylx.attendanceconfig.mapper.AttendanceRuleMapper; import com.ylx.common.core.domain.AjaxResult; 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.StringUtils; import com.ylx.massage.controller.CityOperationApplicationController; 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.domain.ContractRecord; import com.ylx.massage.domain.MaProject; import com.ylx.massage.domain.MaTeProject; import com.ylx.massage.domain.dto.MaProjectSaveDto; import com.ylx.massage.domain.dto.MaTechnicianAuditQueryDTO; import com.ylx.massage.domain.dto.MaTechnicianAuditSubmitDTO; import com.ylx.massage.domain.dto.MaTechnicianMerchantAddDTO; import com.ylx.massage.domain.dto.MaTechnicianMerchantQueryDTO; import com.ylx.massage.domain.dto.MassageMerchantRecommendDto; import com.ylx.massage.domain.vo.MaTechnicianAppAddVo; import com.ylx.massage.domain.vo.MaTechnicianAuditListVO; import com.ylx.massage.domain.vo.MaTechnicianCertificateVO; import com.ylx.massage.domain.vo.MaTechnicianMerchantDetailVO; import com.ylx.massage.domain.vo.MaTechnicianMerchantListVO; import com.ylx.massage.domain.vo.MerchantVo; import com.ylx.massage.mapper.ContractRecordMapper; import com.ylx.massage.mapper.MaProjectMapper; import com.ylx.massage.mapper.MaTeProjectMapper; import com.ylx.massage.service.TbFileService; import com.ylx.order.domain.TOrder; import com.ylx.order.mapper.TOrderMapper; import com.ylx.project.domain.Project; import com.ylx.project.mapper.ProjectMapper; import lombok.Data; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import com.ylx.massage.service.IMaTechnicianService; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; 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 DEFAULT_AGE = 18; 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 String MERCHANT_STATUS_NORMAL = "0"; private static final String PASSWORD = "123456"; @Resource private MaTechnicianMapper maTechnicianMapper; @Resource private MaTeProjectMapper maTeProjectMapper; @Resource private MaProjectMapper maProjectMapper; @Autowired private ProjectMapper projectMapper; @Autowired private TbFileService fileService; @Resource private TFareFreeRuleMapper tFareFreeRuleMapper; @Resource private ContractRecordMapper contractRecordMapper; @Resource private MerchantDailyAttendanceMapper merchantDailyAttendanceMapper; @Resource private AttendanceRuleMapper attendanceRuleMapper; @Resource private TAddressMapper addressMapper; @Resource private MerchantApplyFileMapper merchantApplyFileMapper; @Resource private TOrderMapper orderMapper; @Autowired private IMaTechnicianService maTechnicianService; @Resource private CityOperationApplicationMapper cityOperationApplicationMapper; /** * 商户入驻申请注册 * * @param req 申请参数 */ @Override @Transactional(rollbackFor = Exception.class) public void apply(MaTechnicianAppAddVo req) { // 初始化加密工具 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String phone = req.getTePhone(); //商户入住前置条件校验 getMaTechnician(req, phone); MaTechnician maTechnician = new MaTechnician(); BeanUtils.copyProperties(req, maTechnician); //技师类型默认为真实商户 maTechnician.setTechType(0); maTechnician.setCreateBy("admin"); maTechnician.setAuditStatus(AUDIT_WAIT_ENTER); maTechnician.setTePassword(encoder.encode(PASSWORD)); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(MaTechnician::getCOpenid, req.getCOpenid()); MaTechnician maTechnician1 = maTechnicianMapper.selectOne(queryWrapper); if (maTechnician1 == null) { throw new RuntimeException("商户不存在"); } LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(MaTechnician::getId, maTechnician1.getId()); maTechnicianMapper.update(maTechnician, updateWrapper); CityOperationApplication cityOperationApplication = new CityOperationApplication(); cityOperationApplication.setMerchantId(maTechnician1.getId()); cityOperationApplication.setOperationCenterId(req.getOperationCenterId()); cityOperationApplication.setProvinceCode(req.getProvinceCode()); cityOperationApplication.setProvinceName(req.getProvinceName()); cityOperationApplication.setCityCode(req.getCityCode()); cityOperationApplication.setCityName(req.getCityName()); cityOperationApplication.setDistrictCode(req.getDistrictCode()); cityOperationApplication.setDistrictName(req.getDistrictName()); cityOperationApplication.setOperationCenterName(req.getOperationCenterName()); cityOperationApplication.setCreateBy(maTechnician1.getId().toString()); cityOperationApplication.setUpdateBy(maTechnician1.getId().toString()); cityOperationApplicationMapper.insert(cityOperationApplication); } /** * 商户入驻申请文件上传 * * @param req */ @Override public void applyFile(MerchantApplyFileRequestDto req) { if (req == null || req.getReq().isEmpty()) { } for (MerchantApplyFileDto re : req.getReq()) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(MerchantApplyFile::getMerchantId, re.getMerchantId()); queryWrapper.eq(MerchantApplyFile::getFileType, re.getFileType()); MerchantApplyFile merchantApplyFile = merchantApplyFileMapper.selectOne(queryWrapper); if (merchantApplyFile != null) { // 删除原有文件 merchantApplyFileMapper.deleteById(merchantApplyFile); } else { //插入文件信息 MerchantApplyFile maTechnician = new MerchantApplyFile(); BeanUtils.copyProperties(re, maTechnician); maTechnician.setCreateBy(re.getMerchantId().toString()); maTechnician.setUpdateBy(re.getMerchantId().toString()); merchantApplyFileMapper.insert(maTechnician); } } LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(MaTechnician::getId, req.getTechnician().getId()); updateWrapper.set(MaTechnician::getTeNickName, req.getTechnician().getTeNickName()); updateWrapper.set(MaTechnician::getTeBrief, req.getTechnician().getTeBrief()); maTechnicianService.update(updateWrapper); } /** * 商户入住前置条件校验 * * @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); queryPhoneWrapper.eq(MaTechnician::getIsDelete, 0); MaTechnician maTechnicianPhone = maTechnicianMapper.selectOne(queryPhoneWrapper); if (maTechnicianPhone != null) { throw new RuntimeException("手机号已存在,请更换手机号"); } //3、判断手机号是否已绑定其他用户 LambdaQueryWrapper queryTePhoneWrapper = new LambdaQueryWrapper<>(); queryTePhoneWrapper.eq(MaTechnician::getTePhone, phone); queryTePhoneWrapper.eq(MaTechnician::getIsDelete, 0); queryTePhoneWrapper.eq(MaTechnician::getAuditStatus, 2); MaTechnician maTechnicianTePhone = maTechnicianMapper.selectOne(queryTePhoneWrapper); if (maTechnicianTePhone != null) { throw new RuntimeException("手机号已被其他用户绑定,请更换手机号"); } } /** * 判断当前用户是否已入驻 * * @return */ private MaTechnician getMaTechnician(MaTechnicianAppAddVo req) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(MaTechnician::getTePhone, req.getTePhone()); queryWrapper.eq(MaTechnician::getIsDelete, 0); queryWrapper.eq(MaTechnician::getAuditStatus, 2); //queryWrapper.eq(MaTechnician::getOpenService, req.getOpenService()); queryWrapper.eq(MaTechnician::getServiceTag, req.getServiceTag()); MaTechnician userProfile = maTechnicianMapper.selectOne(queryWrapper); return userProfile; } /** * 查询商户服务项目列表 * * @param userId 商户id * @param auditStatus 审核状态 * @return 技师列表 */ @Override public List selectMaTechnicianListBy(String userId, String auditStatus) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(MaProject::getMerchantId, userId); queryWrapper.eq(MaProject::getAuditStatus, auditStatus); return maProjectMapper.selectList(queryWrapper); } /** * 查询服务分类项目列表 * * @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 结果 */ @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); // 修改为1:待审核 maTechnician.setAuditStatus(1); 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 商户证照 */ @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); if (merchantApplyFiles == null) { throw new ServiceException("商户不存在或已删除"); } MaTechnicianCertificateVO certificate = new MaTechnicianCertificateVO(); certificate.setMerchantId(merchantApplyFiles.get(0).getMerchantId()); merchantApplyFiles.forEach(merchant -> { certificate.setAvatar(typeFIleUrl(merchant, PORTRAIT.getCode())); certificate.setLifePhotos(typeFIleUrl(merchant, LIFE_PHOTO.getCode())); certificate.setIdCardFrout(typeFIleUrl(merchant, ID_CARD_FRONT.getCode())); certificate.setIdCardBack(typeFIleUrl(merchant, ID_CARD_BACK.getCode())); certificate.setIdCardHandheld(typeFIleUrl(merchant, ID_CARD_HANDHELD.getCode())); certificate.setHealthCertificate(typeFIleUrl(merchant, HEALTH_CERT.getCode())); certificate.setQualificationCertificate(typeFIleUrl(merchant, QUALIFICATION_CERT.getCode())); certificate.setNoCrimeRecord(typeFIleUrl(merchant, NO_CRIME_RECORD.getCode())); certificate.setCommitmentPdf(typeFIleUrl(merchant, COMMITMENT_LETTER.getCode())); certificate.setCommitmentVideo(typeFIleUrl(merchant, COMMITMENT_VIDEO.getCode())); certificate.setCommitmentAudio(typeFIleUrl(merchant, COMMITMENT_AUDIO.getCode())); }); return certificate; } private String typeFIleUrl(MerchantApplyFile merchant, String type) { LambdaQueryWrapper queryWrapper1 = Wrappers.lambdaQuery(); queryWrapper1.eq(MerchantApplyFile::getMerchantId, merchant.getMerchantId()); queryWrapper1.eq(MerchantApplyFile::getFileType, type); MerchantApplyFile merchantApplyFiles = merchantApplyFileMapper.selectOne(queryWrapper1); if (merchantApplyFiles == null) { return null; } return merchantApplyFiles.getFileUrl(); } /** * 全量替换商户与服务项目关联关系。 * * @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 userId * @param typeId * @return */ @Override public List getNotApplyList(String userId, String typeId) { LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(MaProject::getMerchantId, userId); query.eq(MaProject::getServiceTag, typeId); 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, typeId); return projectMapper.selectList(query1); } LambdaQueryWrapper query2 = new LambdaQueryWrapper<>(); query2.eq(Project::getType, typeId); 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; } /** * 商户入住信息 * * @param userId * @return */ @Override public MerchantAuditFile getTechnicianList(Long userId) { LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(MaTechnician::getId, userId); MaTechnician merchant = maTechnicianMapper.selectOne(query); LambdaQueryWrapper query1 = new LambdaQueryWrapper<>(); query1.eq(MerchantApplyFile::getMerchantId, userId); List merchantApplyFile = merchantApplyFileMapper.selectList(query1); MerchantAuditFile merchantAuditFile = new MerchantAuditFile(); merchantAuditFile.setMerchant(merchant); merchantAuditFile.setMerchantAuditFile(merchantApplyFile); return merchantAuditFile; } /** * 查询商户合同记录信息 * * @param userId * @return */ @Override public List getContractRecords(Long userId){ LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(ContractRecord::getMerchantId, userId); List contractRecordList = contractRecordMapper.selectList(query); if(contractRecordList.size() == 0) { return new ArrayList<>(); }else { Set seen = new HashSet<>(); contractRecordList = contractRecordList.stream() .filter(record -> record.getContractName() != null && seen.add(record.getContractName())) .collect(Collectors.toList()); } return contractRecordList; } 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().longValue()); 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.setCreateBy(dto.getUserId()); maProject.setUpdateBy(dto.getUserId()); maProject.setMerchantId(dto.getUserId()); maProject.setApplyTime(DateUtils.getNowDate()); maProject.setMerchantPhone(dto.getMerchantPhone()); maProjectMapper.insert(maProject); } } /** * 状态切换 * * @param userId * @param forceConfirm * @return */ @Override public Result switchToOffline(Long userId, Boolean forceConfirm) { MaTechnician technician = getTechnician(userId); // 1. 基础权限校验 (对应流程图左下角) if (!hasActiveSkills(userId)) { throw new RuntimeException("请先申请开通技能"); } if (!hasHomeAddress(userId)) { // 这里通常会触发前端弹窗要求添加地址,或者直接阻断 throw new RuntimeException("请完善家庭地址"); } if (ProjectCategoryEnum.MASSAGE.getCode().equals(technician.getServiceTag())) { // 2. 状态判断逻辑 (对应流程图中间的菱形判断) // 只有处于“在线接单”状态才需要检查疲劳度/时长限制 if (TechnicianStatusEnum.ONLINE.getCode().equals(technician.getPostState())) { if (technician.getServiceState().equals(JsStatusEnum.JS_SERVICE.getCode())) { throw new ServiceException("您有服务中的订单,不能下岗"); } // 获取今日的商户考勤记录 MerchantDailyAttendance attendance = getTodayAttendance(userId); long minutesOnline = 0; if (attendance != null && attendance.getAttendanceStartTime() != null) { LocalDateTime localDateTime = attendance.getAttendanceStartTime().toInstant() .atZone(ZoneId.systemDefault()) .toLocalDateTime(); //计算截止现在的时长,单位为分钟 minutesOnline = Duration.between(localDateTime, LocalDateTime.now()).toMinutes(); // 计算今日的累加在线时长 minutesOnline = minutesOnline + getWorkDuration(userId); } AttendanceRule rule = getAttendanceRule(); if (rule != null) { // 将小时转换为分钟进行比较 BigDecimal minutes = rule.getBasicWorkHours().multiply(new BigDecimal(60)); // 2. 精确转换成 long(无小数、无溢出才成功) long requiredMinutes = minutes.longValueExact(); // 判断是否超过了平台规定的在线时间 (X小时) if (minutesOnline < requiredMinutes) { // 情况 A: 超时了,且用户还没有点击“确认下线”(forceConfirm=false) if (!forceConfirm) { // 返回特定错误码或数据结构,告诉前端弹出“我在想想/确认下线”的模态框 return Result.ok("平台对您的在线时间做了约定,每日在线需满足" + (requiredMinutes / 60) + "小时,距离您下线时间还剩余" + ((requiredMinutes / 60) - minutesOnline) + "小时" + "不满足在线时间将收到平台处罚,是否确认下线?"); } // 情况 B: 超时了,但用户已经点击了“确认下线”,允许通过 } } } // 3. 执行状态更新 (更新为休息中状态) updateStatus(userId, TechnicianStatusEnum.RESTING); if (!forceConfirm) { // 格式化成 yyyy-MM-dd 纯日期字符串 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String today = sdf.format(new Date()); // 查询商户今日的最近考勤记录 LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(MerchantDailyAttendance::getMerchantId, userId) .eq(MerchantDailyAttendance::getAttendanceDate, today) .orderByDesc(MerchantDailyAttendance::getCreateTime); MerchantDailyAttendance update = merchantDailyAttendanceMapper.selectOne(query); if (update != null) { LocalDateTime localDateTime = update.getAttendanceStartTime().toInstant() .atZone(ZoneId.systemDefault()) .toLocalDateTime(); LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(MerchantDailyAttendance::getId, update.getId()) .set(MerchantDailyAttendance::getAttendanceEndTime, DateUtils.getNowDate()) .set(MerchantDailyAttendance::getTotalWorkMinutes, Duration.between(localDateTime, LocalDateTime.now()).toMinutes()) .set(MerchantDailyAttendance::getUpdateTime, DateUtils.getNowDate()); merchantDailyAttendanceMapper.update(update, updateWrapper); } } } else { //更新状态为在线接单 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); } return Result.ok("状态已切换成功"); } /** * 获取今天商户的考勤记录 * * @param userId 技师ID * @return 考勤记录 */ 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 * @return 工作时长(分钟) */ private long getWorkDuration(Long userId) { 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; // 计算指定日期的总分钟数 long totalMinutes = attendances.stream() .filter(attendance -> DateUtils.getNowDate().toString().equals(attendance.getAttendanceDate().toString())) .mapToLong(MerchantDailyAttendance::getTotalWorkMinutes) .sum(); return totalMinutes; } /** * 获取商户的考勤规则 * * @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 true: 有可用技能, false: 无 */ public boolean hasActiveSkills(Long userId) { if (userId == null) return false; // 构建查询条件:用户ID匹配 AND 状态为已发布/生效中 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(MaProject::getMerchantId, userId) .eq(MaProject::getAuditStatus, 1); // 假设 1 代表 "生效/已审核" // 只要查到一条记录即返回 true return maProjectMapper.selectCount(wrapper) > 0; } /** * 判断用户是否有家庭地址(通常指设置为默认的地址) * * @param userId 技师ID * @return true: 有地址, false: 无 */ public boolean hasHomeAddress(Long userId) { if (userId == null) return false; // 构建查询条件:用户ID匹配 AND 是默认地址(可选) AND 未删除 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(TAddress::getMerchantId, userId) .eq(TAddress::getUserType, 2) // 商户类型 .eq(TAddress::getIsDelete, 0); // 逻辑未删除 // 统计数量是否大于0 long count = addressMapper.selectCount(wrapper); return count > 0; } // 辅助方法:模拟获取用户 private MaTechnician getTechnician(Long userId) { // ... DB查询逻辑 LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(MaTechnician::getId, userId); return maTechnicianMapper.selectOne(query); } // 辅助方法:更新状态 private void updateStatus(Long userId, TechnicianStatusEnum status) { LambdaUpdateWrapper update = new LambdaUpdateWrapper<>(); update.eq(MaTechnician::getId, userId); update.set(MaTechnician::getPostState, status.getCode()); // ... DB Update逻辑,同时记录上线/下线时间 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); } }