MaTechnicianServiceImpl.java 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438
  1. package com.ylx.massage.service.impl;
  2. import java.math.BigDecimal;
  3. import java.text.SimpleDateFormat;
  4. import java.time.Duration;
  5. import java.time.LocalDate;
  6. import java.time.LocalDateTime;
  7. import java.time.ZoneId;
  8. import java.time.format.DateTimeFormatter;
  9. import java.util.*;
  10. import java.util.function.Function;
  11. import java.util.stream.Collectors;
  12. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  13. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  14. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  15. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  16. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  17. import com.ylx.attendanceconfig.domain.AttendanceRule;
  18. import com.ylx.attendanceconfig.mapper.AttendanceRuleMapper;
  19. import com.ylx.common.core.domain.AjaxResult;
  20. import com.ylx.common.core.domain.model.LoginUser;
  21. import com.ylx.common.exception.ServiceException;
  22. import com.ylx.common.utils.DateUtils;
  23. import com.ylx.common.utils.StringUtils;
  24. import com.ylx.massage.controller.CityOperationApplicationController;
  25. import com.ylx.massage.domain.*;
  26. import com.ylx.massage.domain.dto.*;
  27. import com.ylx.massage.domain.vo.*;
  28. import com.ylx.massage.enums.OrderTipEnum;
  29. import com.ylx.massage.enums.ProjectCategoryEnum;
  30. import com.ylx.massage.enums.TechnicianStatusEnum;
  31. import com.ylx.massage.mapper.*;
  32. import com.ylx.massage.domain.ContractRecord;
  33. import com.ylx.massage.domain.MaProject;
  34. import com.ylx.massage.domain.MaTeProject;
  35. import com.ylx.massage.domain.dto.MaProjectSaveDto;
  36. import com.ylx.massage.domain.dto.MaTechnicianAuditQueryDTO;
  37. import com.ylx.massage.domain.dto.MaTechnicianAuditSubmitDTO;
  38. import com.ylx.massage.domain.dto.MaTechnicianMerchantAddDTO;
  39. import com.ylx.massage.domain.dto.MaTechnicianMerchantQueryDTO;
  40. import com.ylx.massage.domain.dto.MassageMerchantRecommendDto;
  41. import com.ylx.massage.domain.vo.MaTechnicianAppAddVo;
  42. import com.ylx.massage.domain.vo.MaTechnicianAuditListVO;
  43. import com.ylx.massage.domain.vo.MaTechnicianCertificateVO;
  44. import com.ylx.massage.domain.vo.MaTechnicianMerchantDetailVO;
  45. import com.ylx.massage.domain.vo.MaTechnicianMerchantListVO;
  46. import com.ylx.massage.domain.vo.MerchantVo;
  47. import com.ylx.massage.mapper.ContractRecordMapper;
  48. import com.ylx.massage.mapper.MaProjectMapper;
  49. import com.ylx.massage.mapper.MaTeProjectMapper;
  50. import com.ylx.massage.service.TbFileService;
  51. import com.ylx.order.domain.TOrder;
  52. import com.ylx.order.mapper.TOrderMapper;
  53. import com.ylx.project.domain.Project;
  54. import com.ylx.project.mapper.ProjectMapper;
  55. import lombok.Data;
  56. import org.springframework.beans.BeanUtils;
  57. import org.springframework.beans.factory.annotation.Autowired;
  58. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  59. import org.springframework.stereotype.Service;
  60. import com.ylx.massage.service.IMaTechnicianService;
  61. import org.springframework.transaction.annotation.Transactional;
  62. import org.springframework.util.CollectionUtils;
  63. import org.springframework.web.multipart.MultipartFile;
  64. import com.ylx.massage.enums.OrderStatusEnum;
  65. import javax.annotation.Resource;
  66. import static com.ylx.massage.enums.FileTypeEnum.*;
  67. /**
  68. * 技师Service业务层处理
  69. *
  70. * @author ylx
  71. * @date 2024-03-22
  72. */
  73. @Service
  74. public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaTechnician> implements IMaTechnicianService {
  75. private static final int SERVICE_STATE_AVAILABLE = 1;
  76. private static final int POST_STATE_OFFLINE = 0;
  77. private static final int ENABLED = 1;
  78. private static final int DEFAULT_AGE = 18;
  79. private static final int AUDIT_APPROVED = 2;
  80. private static final int AUDIT_WAIT_ENTER = 0;
  81. private static final int AUDIT_WAIT_REVIEW = 1;
  82. private static final int AUDIT_REJECTED = 3;
  83. private static final int AUDIT_REMARK_MAX_LENGTH = 500;
  84. private static final int NS_STATUS_NOT_ON_DUTY = -1;
  85. private static final int DEFAULT_STAT_VALUE = 0;
  86. private static final Integer NOT_DELETED = 0;
  87. private static final String MERCHANT_STATUS_NORMAL = "0";
  88. private static final String PASSWORD = "123456";
  89. @Resource
  90. private MaTechnicianMapper maTechnicianMapper;
  91. @Resource
  92. private MaTeProjectMapper maTeProjectMapper;
  93. @Resource
  94. private MaProjectMapper maProjectMapper;
  95. @Autowired
  96. private ProjectMapper projectMapper;
  97. @Autowired
  98. private TbFileService fileService;
  99. @Resource
  100. private ContractRecordMapper contractRecordMapper;
  101. @Resource
  102. private MerchantDailyAttendanceMapper merchantDailyAttendanceMapper;
  103. @Resource
  104. private AttendanceRuleMapper attendanceRuleMapper;
  105. @Resource
  106. private TAddressMapper addressMapper;
  107. @Resource
  108. private MerchantApplyFileMapper merchantApplyFileMapper;
  109. @Resource
  110. private TOrderMapper orderMapper;
  111. @Autowired
  112. private IMaTechnicianService maTechnicianService;
  113. @Resource
  114. private CityOperationApplicationMapper cityOperationApplicationMapper;
  115. /**
  116. * 商户入驻申请注册
  117. *
  118. * @param req 申请参数
  119. */
  120. @Override
  121. @Transactional(rollbackFor = Exception.class)
  122. public void apply(MaTechnicianAppAddVo req) {
  123. // 初始化加密工具
  124. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  125. String phone = req.getTePhone();
  126. //商户入住前置条件校验
  127. getMaTechnician(req, phone);
  128. MaTechnician maTechnician = new MaTechnician();
  129. BeanUtils.copyProperties(req, maTechnician);
  130. //技师类型默认为真实商户
  131. maTechnician.setTechType(0);
  132. maTechnician.setCreateBy("admin");
  133. maTechnician.setAuditStatus(AUDIT_WAIT_ENTER);
  134. maTechnician.setTePassword(encoder.encode(PASSWORD));
  135. LambdaQueryWrapper<MaTechnician> queryWrapper = new LambdaQueryWrapper<>();
  136. queryWrapper.eq(MaTechnician::getCOpenid, req.getCOpenid());
  137. MaTechnician maTechnician1 = maTechnicianMapper.selectOne(queryWrapper);
  138. if (maTechnician1 == null) {
  139. throw new RuntimeException("商户不存在");
  140. }
  141. LambdaUpdateWrapper<MaTechnician> updateWrapper = new LambdaUpdateWrapper<>();
  142. updateWrapper.eq(MaTechnician::getId, maTechnician1.getId());
  143. maTechnicianMapper.update(maTechnician, updateWrapper);
  144. CityOperationApplication cityOperationApplication = new CityOperationApplication();
  145. cityOperationApplication.setMerchantId(maTechnician1.getId());
  146. cityOperationApplication.setOperationCenterId(req.getOperationCenterId());
  147. cityOperationApplication.setProvinceCode(req.getProvinceCode());
  148. cityOperationApplication.setProvinceName(req.getProvinceName());
  149. cityOperationApplication.setCityCode(req.getCityCode());
  150. cityOperationApplication.setCityName(req.getCityName());
  151. cityOperationApplication.setDistrictCode(req.getDistrictCode());
  152. cityOperationApplication.setDistrictName(req.getDistrictName());
  153. cityOperationApplication.setOperationCenterName(req.getOperationCenterName());
  154. cityOperationApplication.setCreateBy(maTechnician1.getId().toString());
  155. cityOperationApplication.setUpdateBy(maTechnician1.getId().toString());
  156. cityOperationApplicationMapper.insert(cityOperationApplication);
  157. }
  158. /**
  159. * 商户入驻申请文件上传
  160. *
  161. * @param req
  162. */
  163. @Override
  164. public void applyFile(MerchantApplyFileRequestDto req) {
  165. if (req == null || req.getReq().isEmpty()) {
  166. }
  167. for (MerchantApplyFileDto re : req.getReq()) {
  168. LambdaQueryWrapper<MerchantApplyFile> queryWrapper = new LambdaQueryWrapper<>();
  169. queryWrapper.eq(MerchantApplyFile::getMerchantId, re.getMerchantId());
  170. queryWrapper.eq(MerchantApplyFile::getFileType, re.getFileType());
  171. MerchantApplyFile merchantApplyFile = merchantApplyFileMapper.selectOne(queryWrapper);
  172. if (merchantApplyFile != null) {
  173. // 删除原有文件
  174. merchantApplyFileMapper.deleteById(merchantApplyFile);
  175. } else {
  176. //插入文件信息
  177. MerchantApplyFile maTechnician = new MerchantApplyFile();
  178. BeanUtils.copyProperties(re, maTechnician);
  179. maTechnician.setCreateBy(re.getMerchantId().toString());
  180. maTechnician.setUpdateBy(re.getMerchantId().toString());
  181. merchantApplyFileMapper.insert(maTechnician);
  182. }
  183. }
  184. LambdaUpdateWrapper<MaTechnician> updateWrapper = new LambdaUpdateWrapper<>();
  185. updateWrapper.eq(MaTechnician::getId, req.getTechnician().getId());
  186. updateWrapper.set(MaTechnician::getTeNickName, req.getTechnician().getTeNickName());
  187. updateWrapper.set(MaTechnician::getTeBrief, req.getTechnician().getTeBrief());
  188. maTechnicianService.update(updateWrapper);
  189. }
  190. /**
  191. * 商户入住前置条件校验
  192. *
  193. * @param req
  194. * @param phone
  195. */
  196. private void getMaTechnician(MaTechnicianAppAddVo req, String phone) {
  197. // 1. 判断当前用户是否已入驻
  198. MaTechnician userProfile = getMaTechnician(req);
  199. if (userProfile != null) {
  200. throw new RuntimeException("当前用户已入驻,请勿重复提交");
  201. }
  202. // 2. 判断手机号是否已存在
  203. LambdaQueryWrapper<MaTechnician> queryPhoneWrapper = new LambdaQueryWrapper<>();
  204. queryPhoneWrapper.eq(MaTechnician::getTePhone, phone);
  205. queryPhoneWrapper.eq(MaTechnician::getIsDelete, 0);
  206. MaTechnician maTechnicianPhone = maTechnicianMapper.selectOne(queryPhoneWrapper);
  207. if (maTechnicianPhone != null) {
  208. throw new RuntimeException("手机号已存在,请更换手机号");
  209. }
  210. //3、判断手机号是否已绑定其他用户
  211. LambdaQueryWrapper<MaTechnician> queryTePhoneWrapper = new LambdaQueryWrapper<>();
  212. queryTePhoneWrapper.eq(MaTechnician::getTePhone, phone);
  213. queryTePhoneWrapper.eq(MaTechnician::getIsDelete, 0);
  214. queryTePhoneWrapper.eq(MaTechnician::getAuditStatus, 2);
  215. MaTechnician maTechnicianTePhone = maTechnicianMapper.selectOne(queryTePhoneWrapper);
  216. if (maTechnicianTePhone != null) {
  217. throw new RuntimeException("手机号已被其他用户绑定,请更换手机号");
  218. }
  219. }
  220. /**
  221. * 判断当前用户是否已入驻
  222. *
  223. * @return
  224. */
  225. private MaTechnician getMaTechnician(MaTechnicianAppAddVo req) {
  226. LambdaQueryWrapper<MaTechnician> queryWrapper = new LambdaQueryWrapper<>();
  227. queryWrapper.eq(MaTechnician::getTePhone, req.getTePhone());
  228. queryWrapper.eq(MaTechnician::getIsDelete, 0);
  229. queryWrapper.eq(MaTechnician::getAuditStatus, 2);
  230. //queryWrapper.eq(MaTechnician::getOpenService, req.getOpenService());
  231. queryWrapper.eq(MaTechnician::getServiceTag, req.getServiceTag());
  232. MaTechnician userProfile = maTechnicianMapper.selectOne(queryWrapper);
  233. return userProfile;
  234. }
  235. /**
  236. * 查询商户服务项目列表
  237. *
  238. * @param userId 商户id
  239. * @param auditStatus 审核状态
  240. * @return 技师列表
  241. */
  242. @Override
  243. public List<MaProject> selectMaTechnicianListBy(String userId, String auditStatus) {
  244. LambdaQueryWrapper<MaProject> queryWrapper = new LambdaQueryWrapper<>();
  245. queryWrapper.eq(MaProject::getMerchantId, userId);
  246. queryWrapper.eq(MaProject::getAuditStatus, auditStatus);
  247. return maProjectMapper.selectList(queryWrapper);
  248. }
  249. /**
  250. * 查询服务分类项目列表
  251. *
  252. * @param typeId 技师类型
  253. * @return 技师列表
  254. */
  255. @Override
  256. public List<Project> selectTechnicianListBy(String typeId) {
  257. LambdaQueryWrapper<Project> queryWrapper = new LambdaQueryWrapper<>();
  258. queryWrapper.eq(Project::getType, typeId);
  259. return projectMapper.selectList(queryWrapper);
  260. }
  261. /**
  262. * 查询技师
  263. *
  264. * @param id 技师主键
  265. * @return 技师
  266. */
  267. @Override
  268. public MaTechnician selectMaTechnicianById(Long id) {
  269. return maTechnicianMapper.selectMaTechnicianById(id);
  270. }
  271. /**
  272. * 查询技师列表
  273. *
  274. * @param maTechnician 技师
  275. * @return 技师
  276. */
  277. @Override
  278. public List<MaTechnician> selectMaTechnicianList(MaTechnician maTechnician) {
  279. return maTechnicianMapper.selectMaTechnicianList(maTechnician);
  280. }
  281. /**
  282. * 新增技师
  283. *
  284. * @param maTechnicianAppAddVo 技师
  285. * @return 结果
  286. */
  287. @Override
  288. @Transactional(rollbackFor = Exception.class)
  289. public int insertMaTechnician(MaTechnicianAppAddVo maTechnicianAppAddVo) {
  290. MaTechnician maTechnician = new MaTechnician();
  291. BeanUtils.copyProperties(maTechnicianAppAddVo, maTechnician);
  292. int rows = maTechnicianMapper.insertMaTechnician(maTechnician);
  293. if (maTechnicianAppAddVo.getProjectIds() != null && !maTechnicianAppAddVo.getProjectIds().isEmpty()) {
  294. insertProjectRelations(maTechnician.getId(), new LinkedHashSet<>(maTechnicianAppAddVo.getProjectIds()));
  295. }
  296. return rows;
  297. }
  298. /**
  299. * 后台新增商户
  300. *
  301. * @param dto 新增商户参数
  302. * @param loginUser 当前登录用户
  303. * @return 结果
  304. */
  305. @Override
  306. @Transactional(rollbackFor = Exception.class)
  307. public int insertMerchant(MaTechnicianMerchantAddDTO dto, LoginUser loginUser) {
  308. MerchantProjectSelection selection = checkMerchantAddParam(dto);
  309. String userName = loginUser.getUser().getUserName();
  310. MaTechnician maTechnician = new MaTechnician();
  311. maTechnician.setTeName(dto.getTeName().trim());
  312. maTechnician.setTeNickName(dto.getTeNickName().trim());
  313. maTechnician.setTeSex(dto.getTeSex());
  314. maTechnician.setTePhone(dto.getTePhone().trim());
  315. maTechnician.setOpenService(joinIds(selection.getCategoryIds()));
  316. maTechnician.setTeProject(joinProjectTitles(selection.getProjectIds(), selection.getProjectMap()));
  317. maTechnician.setTechType(dto.getTechType());
  318. maTechnician.setIsRecommend(normalizeSwitchValue(dto.getIsRecommend(), "是否推荐"));
  319. maTechnician.setServiceState(SERVICE_STATE_AVAILABLE);
  320. maTechnician.setPostState(POST_STATE_OFFLINE);
  321. maTechnician.setTeIsEnable(ENABLED);
  322. //上岗状态:默认-1 未上岗
  323. maTechnician.setNStatus2(NS_STATUS_NOT_ON_DUTY);
  324. maTechnician.setMerchantStatus(MERCHANT_STATUS_NORMAL);
  325. //审核状态
  326. maTechnician.setAuditStatus(AUDIT_APPROVED);
  327. maTechnician.setTeAddress("");
  328. maTechnician.setNStar(DEFAULT_STAT_VALUE);
  329. maTechnician.setNNum(DEFAULT_STAT_VALUE);
  330. maTechnician.setCreateBy(userName);
  331. maTechnician.setUpdateBy(userName);
  332. maTechnician.setCreateTime(DateUtils.getNowDate());
  333. maTechnician.setUpdateTime(DateUtils.getNowDate());
  334. int rows = maTechnicianMapper.insert(maTechnician);
  335. if (rows <= 0) {
  336. throw new ServiceException("新增商户失败");
  337. }
  338. insertProjectRelations(maTechnician.getId(), selection.getProjectIds());
  339. return rows;
  340. }
  341. /**
  342. * 后台编辑商户
  343. *
  344. * @param id 商户ID
  345. * @param dto 编辑商户参数
  346. * @param loginUser 当前登录用户
  347. * @return 结果
  348. */
  349. @Override
  350. @Transactional(rollbackFor = Exception.class)
  351. public int updateMerchant(Integer id, MaTechnicianMerchantAddDTO dto, LoginUser loginUser) {
  352. if (id == null) {
  353. throw new ServiceException("商户ID不能为空");
  354. }
  355. MaTechnician existsMerchant = maTechnicianMapper.selectMerchantById(id.intValue());
  356. if (existsMerchant == null || !NOT_DELETED.equals(existsMerchant.getIsDelete())) {
  357. throw new ServiceException("商户不存在或已删除");
  358. }
  359. MerchantProjectSelection selection = checkMerchantAddParam(dto);
  360. String userName = loginUser.getUser().getUserName();
  361. MaTechnician maTechnician = new MaTechnician();
  362. maTechnician.setId(id);
  363. maTechnician.setTeName(dto.getTeName().trim());
  364. maTechnician.setTeNickName(dto.getTeNickName().trim());
  365. maTechnician.setTeSex(dto.getTeSex());
  366. maTechnician.setTePhone(dto.getTePhone().trim());
  367. maTechnician.setOpenService(joinIds(selection.getCategoryIds()));
  368. maTechnician.setTeProject(joinProjectTitles(selection.getProjectIds(), selection.getProjectMap()));
  369. maTechnician.setTechType(dto.getTechType());
  370. maTechnician.setIsRecommend(normalizeSwitchValue(dto.getIsRecommend(), "是否推荐"));
  371. maTechnician.setUpdateBy(userName);
  372. maTechnician.setUpdateTime(DateUtils.getNowDate());
  373. int rows = maTechnicianMapper.updateMerchantById(maTechnician);
  374. if (rows <= 0) {
  375. throw new ServiceException("编辑商户失败");
  376. }
  377. replaceProjectRelations(id, selection.getProjectIds());
  378. return rows;
  379. }
  380. /**
  381. * 后台上传商户合同文件
  382. *
  383. * @param id 商户ID
  384. * @param
  385. * @param loginUser 当前登录用户
  386. * @return 上传结果
  387. */
  388. @Override
  389. @Transactional(rollbackFor = Exception.class)
  390. public Integer uploadMerchantContract(Integer id, Map<String, Object> map, LoginUser loginUser) {
  391. if (id == null) {
  392. throw new ServiceException("商户ID不能为空");
  393. }
  394. MaTechnician existsMerchant = maTechnicianMapper.selectMerchantById(id);
  395. if (existsMerchant == null || !NOT_DELETED.equals(existsMerchant.getIsDelete())) {
  396. throw new ServiceException("商户不存在或已删除");
  397. }
  398. // 合同的名称
  399. String contractName = String.valueOf(map.get("contractName"));
  400. // 合同文件的URL
  401. String url = String.valueOf(map.get("url"));
  402. if (StringUtils.isBlank(url)) {
  403. throw new ServiceException("合同文件上传失败,未返回文件地址");
  404. }
  405. ContractRecord contractRecord = new ContractRecord();
  406. contractRecord.setMerchantId(id);
  407. contractRecord.setContractName(contractName);
  408. contractRecord.setFileUrl(url);
  409. contractRecord.setSignTime(DateUtils.getNowDate());
  410. contractRecord.setSignerName(existsMerchant.getTeName());
  411. contractRecord.setCreateTime(DateUtils.getNowDate());
  412. int rows = contractRecordMapper.insert(contractRecord);
  413. if (rows <= 0) {
  414. throw new ServiceException("保存合同记录失败");
  415. }
  416. return rows;
  417. }
  418. /**
  419. * 商户入驻审核。
  420. *
  421. * @param id 商户ID
  422. * @param dto 审核提交参数
  423. * @param loginUser 当前登录用户
  424. * @return 结果
  425. */
  426. @Override
  427. @Transactional(rollbackFor = Exception.class)
  428. public int submitMerchantAudit(Integer id, MaTechnicianAuditSubmitDTO dto, LoginUser loginUser) {
  429. if (id == null) {
  430. throw new ServiceException("商户ID不能为空");
  431. }
  432. if (dto == null) {
  433. throw new ServiceException("审核参数不能为空");
  434. }
  435. checkEnumValue(dto.getAuditStatus(), "审核意见", AUDIT_APPROVED, AUDIT_REJECTED);
  436. String auditRemark = dto.getAuditRemark() == null ? "" : dto.getAuditRemark().trim();
  437. if (dto.getAuditStatus() == AUDIT_REJECTED && StringUtils.isBlank(auditRemark)) {
  438. throw new ServiceException("审核驳回时审核备注不能为空");
  439. }
  440. if (auditRemark.length() > AUDIT_REMARK_MAX_LENGTH) {
  441. throw new ServiceException("审核备注长度不能超过" + AUDIT_REMARK_MAX_LENGTH + "个字符");
  442. }
  443. MaTechnician existsMerchant = maTechnicianMapper.selectMerchantById(id);
  444. if (existsMerchant == null || !NOT_DELETED.equals(existsMerchant.getIsDelete())) {
  445. throw new ServiceException("商户不存在或已删除");
  446. }
  447. Integer currentAuditStatus = existsMerchant.getAuditStatus();
  448. if (currentAuditStatus == null || (currentAuditStatus != AUDIT_WAIT_ENTER)) {
  449. throw new ServiceException("当前商户待入驻已审核,不用重复审核");
  450. }
  451. MaTechnician maTechnician = new MaTechnician();
  452. maTechnician.setId(id);
  453. // 修改为1:待审核
  454. maTechnician.setAuditStatus(1);
  455. maTechnician.setAuditRemark(auditRemark);
  456. maTechnician.setApproveTime(DateUtils.getNowDate());
  457. if (loginUser != null && loginUser.getUser() != null) {
  458. maTechnician.setUpdateBy(loginUser.getUser().getUserName());
  459. }
  460. maTechnician.setUpdateTime(DateUtils.getNowDate());
  461. int rows = maTechnicianMapper.submitMerchantAuditById(maTechnician);
  462. if (rows <= 0) {
  463. throw new ServiceException("提交商户审核失败");
  464. }
  465. return rows;
  466. }
  467. /**
  468. * 后台查询商户证照
  469. *
  470. * @param id 商户ID
  471. * @return 商户证照
  472. */
  473. @Override
  474. public MaTechnicianCertificateVO selectMerchantCertificate(Integer id) {
  475. if (id == null) {
  476. throw new ServiceException("商户ID不能为空");
  477. }
  478. LambdaQueryWrapper<MerchantApplyFile> queryWrapper = Wrappers.lambdaQuery();
  479. queryWrapper.eq(MerchantApplyFile::getMerchantId, id);
  480. List<MerchantApplyFile> merchantApplyFiles = merchantApplyFileMapper.selectList(queryWrapper);
  481. if (merchantApplyFiles == null) {
  482. throw new ServiceException("商户不存在或已删除");
  483. }
  484. MaTechnicianCertificateVO certificate = new MaTechnicianCertificateVO();
  485. certificate.setMerchantId(merchantApplyFiles.get(0).getMerchantId());
  486. merchantApplyFiles.forEach(merchant -> {
  487. certificate.setAvatar(typeFIleUrl(merchant, PORTRAIT.getCode()));
  488. certificate.setLifePhotos(typeFIleUrl(merchant, LIFE_PHOTO.getCode()));
  489. certificate.setIdCardFrout(typeFIleUrl(merchant, ID_CARD_FRONT.getCode()));
  490. certificate.setIdCardBack(typeFIleUrl(merchant, ID_CARD_BACK.getCode()));
  491. certificate.setIdCardHandheld(typeFIleUrl(merchant, ID_CARD_HANDHELD.getCode()));
  492. certificate.setHealthCertificate(typeFIleUrl(merchant, HEALTH_CERT.getCode()));
  493. certificate.setQualificationCertificate(typeFIleUrl(merchant, QUALIFICATION_CERT.getCode()));
  494. certificate.setNoCrimeRecord(typeFIleUrl(merchant, NO_CRIME_RECORD.getCode()));
  495. certificate.setCommitmentPdf(typeFIleUrl(merchant, COMMITMENT_LETTER.getCode()));
  496. certificate.setCommitmentVideo(typeFIleUrl(merchant, COMMITMENT_VIDEO.getCode()));
  497. certificate.setCommitmentAudio(typeFIleUrl(merchant, COMMITMENT_AUDIO.getCode()));
  498. });
  499. return certificate;
  500. }
  501. private String typeFIleUrl(MerchantApplyFile merchant, String type) {
  502. LambdaQueryWrapper<MerchantApplyFile> queryWrapper1 = Wrappers.lambdaQuery();
  503. queryWrapper1.eq(MerchantApplyFile::getMerchantId, merchant.getMerchantId());
  504. queryWrapper1.eq(MerchantApplyFile::getFileType, type);
  505. MerchantApplyFile merchantApplyFiles = merchantApplyFileMapper.selectOne(queryWrapper1);
  506. if (merchantApplyFiles == null) {
  507. return null;
  508. }
  509. return merchantApplyFiles.getFileUrl();
  510. }
  511. /**
  512. * 全量替换商户与服务项目关联关系。
  513. *
  514. * @param technicianId 商户ID
  515. * @param projectIds 服务项目ID集合
  516. */
  517. private void replaceProjectRelations(Integer technicianId, Set<Integer> projectIds) {
  518. if (technicianId == null) {
  519. throw new ServiceException("商户ID不能为空");
  520. }
  521. maTeProjectMapper.deleteByTechnicianId(technicianId);
  522. for (Integer projectId : projectIds) {
  523. MaTeProject relation = new MaTeProject();
  524. relation.setTeId(technicianId);
  525. relation.setProjectId(projectId);
  526. int rows = maTeProjectMapper.insert(relation);
  527. if (rows <= 0) {
  528. throw new ServiceException("编辑商户服务项目失败");
  529. }
  530. }
  531. }
  532. /**
  533. * 后台查询商户入驻审核列表
  534. *
  535. * @param page 分页参数
  536. * @param dto 查询条件
  537. * @return 商户入驻审核分页列表
  538. */
  539. @Override
  540. public Page<MaTechnicianAuditListVO> selectMerchantAuditList(Page<MaTechnicianAuditListVO> page, MaTechnicianAuditQueryDTO dto) {
  541. if (dto != null && dto.getAuditStatus() != null) {
  542. checkEnumValue(dto.getAuditStatus(), "审核状态", 0, 1, 2, 3);
  543. }
  544. Page<MaTechnicianAuditListVO> pageParam = page == null ? new Page<>(1, 10) : page;
  545. return maTechnicianMapper.selectMerchantAuditList(pageParam, dto);
  546. }
  547. /**
  548. * 后台查询商户列表
  549. *
  550. * @param page 分页参数
  551. * @param dto 查询条件
  552. * @return 商户分页列表
  553. */
  554. @Override
  555. public Page<MaTechnicianMerchantListVO> selectMerchantList(Page<MaTechnicianMerchantListVO> page,
  556. MaTechnicianMerchantQueryDTO dto) {
  557. Page<MaTechnicianMerchantListVO> pageParam = page == null ? new Page<>(1, 10) : page;
  558. return maTechnicianMapper.selectMerchantList(pageParam, dto);
  559. }
  560. /**
  561. * 后台查询商户详情
  562. *
  563. * @param id 商户ID
  564. * @return 商户详情
  565. */
  566. @Override
  567. public MaTechnicianMerchantDetailVO selectMerchantDetail(Long id) {
  568. if (id == null) {
  569. throw new ServiceException("商户ID不能为空");
  570. }
  571. MaTechnicianMerchantDetailVO detail = maTechnicianMapper.selectMerchantDetailById(id);
  572. if (detail == null) {
  573. throw new ServiceException("商户不存在或已删除");
  574. }
  575. return detail;
  576. }
  577. /**
  578. * 修改技师
  579. *
  580. * @param maTechnicianAppAddVo
  581. * @return 结果
  582. */
  583. @Override
  584. public int updateMaTechnician(MaTechnicianAppAddVo maTechnicianAppAddVo) {
  585. MaTechnician maTechnician = new MaTechnician();
  586. BeanUtils.copyProperties(maTechnicianAppAddVo, maTechnician);
  587. return maTechnicianMapper.updateMaTechnician(maTechnician);
  588. }
  589. /**
  590. * 批量删除技师
  591. *
  592. * @param ids 需要删除的技师主键
  593. * @return 结果
  594. */
  595. @Override
  596. public int deleteMaTechnicianByIds(Long[] ids) {
  597. return maTechnicianMapper.deleteMaTechnicianByIds(ids);
  598. }
  599. /**
  600. * 删除技师信息
  601. *
  602. * @param id 技师主键
  603. * @return 结果
  604. */
  605. @Override
  606. public int deleteMaTechnicianById(Long id) {
  607. return maTechnicianMapper.deleteMaTechnicianById(id);
  608. }
  609. /**
  610. * 首页选中的城市是否有开通服务
  611. *
  612. * @param areaCode
  613. * @return
  614. */
  615. @Override
  616. public Boolean isHasMerchantCity(String areaCode) {
  617. return maTechnicianMapper.isHasMerchantCity(areaCode);
  618. }
  619. /**
  620. * 首页按摩推荐
  621. *
  622. * @param dto
  623. * @return
  624. */
  625. @Override
  626. public List<MerchantVo> getMerchantRecommend(MassageMerchantRecommendDto dto) {
  627. return maTechnicianMapper.getMerchantRecommend(dto);
  628. }
  629. private MerchantProjectSelection checkMerchantAddParam(MaTechnicianMerchantAddDTO dto) {
  630. if (dto == null) {
  631. throw new ServiceException("商户参数不能为空");
  632. }
  633. checkRequiredText(dto.getTeName(), "姓名", 10);
  634. checkRequiredText(dto.getTeNickName(), "昵称", 10);
  635. checkRequiredText(dto.getTePhone(), "电话", 11);
  636. checkEnumValue(dto.getTeSex(), "性别", 0, 1);
  637. Set<Integer> categoryIds = checkOpenServiceIds(dto.getOpenService());
  638. checkEnumValue(dto.getTechType(), "商户类型", 0, 1);
  639. if (dto.getIsRecommend() != null) {
  640. checkEnumValue(dto.getIsRecommend(), "是否推荐", 0, 1);
  641. }
  642. return checkProjectIds(dto.getProjectIds(), categoryIds);
  643. }
  644. private void checkRequiredText(String value, String fieldName, int maxLength) {
  645. if (StringUtils.isBlank(value)) {
  646. throw new ServiceException(fieldName + "不能为空");
  647. }
  648. if (value.trim().length() > maxLength) {
  649. throw new ServiceException(fieldName + "长度不能超过" + maxLength + "个字符");
  650. }
  651. }
  652. private void checkEnumValue(Integer value, String fieldName, int... allowedValues) {
  653. if (value == null) {
  654. throw new ServiceException(fieldName + "不能为空");
  655. }
  656. for (int allowedValue : allowedValues) {
  657. if (value == allowedValue) {
  658. return;
  659. }
  660. }
  661. throw new ServiceException(fieldName + "值不正确");
  662. }
  663. private Integer normalizeSwitchValue(Integer value, String fieldName) {
  664. if (value == null) {
  665. return 0;
  666. }
  667. checkEnumValue(value, fieldName, 0, 1);
  668. return value;
  669. }
  670. /**
  671. * 校验服务项目ID集合
  672. *
  673. * @param projectIds 服务项目ID集合
  674. * @param categoryIds 服务类目ID集合
  675. * @return 有效服务项目ID集合
  676. */
  677. private MerchantProjectSelection checkProjectIds(List<Integer> projectIds, Set<Integer> categoryIds) {
  678. if (projectIds == null || projectIds.isEmpty()) {
  679. throw new ServiceException("服务项目不能为空");
  680. }
  681. Set<Integer> distinctProjectIds = new LinkedHashSet<>();
  682. for (Integer projectId : projectIds) {
  683. if (projectId == null) {
  684. throw new ServiceException("服务项目ID不能为空");
  685. }
  686. distinctProjectIds.add(projectId);
  687. }
  688. List<Project> projects = projectMapper.selectList(new LambdaQueryWrapper<Project>()
  689. .in(Project::getId, distinctProjectIds)
  690. .eq(Project::getIsDelete, 0));
  691. if (projects.size() != distinctProjectIds.size()) {
  692. throw new ServiceException("服务项目不存在或已删除");
  693. }
  694. Map<Integer, Project> projectMap = projects.stream()
  695. .collect(Collectors.toMap(project -> project.getId(), Function.identity(), (left, right) -> left));
  696. Set<Integer> projectCategoryIds = new LinkedHashSet<>();
  697. for (Integer projectId : distinctProjectIds) {
  698. Project project = projectMap.get(projectId);
  699. if (project == null || project.getCategoryId() == null) {
  700. throw new ServiceException("服务项目类目不能为空");
  701. }
  702. if (!categoryIds.contains(project.getCategoryId())) {
  703. throw new ServiceException("服务项目不属于所选服务类目");
  704. }
  705. projectCategoryIds.add(project.getCategoryId());
  706. }
  707. if (!projectCategoryIds.containsAll(categoryIds)) {
  708. throw new ServiceException("每个服务类目至少选择一个服务项目");
  709. }
  710. return new MerchantProjectSelection(categoryIds, distinctProjectIds, projectMap);
  711. }
  712. /**
  713. * 校验服务类目ID集合
  714. *
  715. * @param openService 服务类目ID集合
  716. * @return 去重后的服务类目ID集合
  717. */
  718. private Set<Integer> checkOpenServiceIds(List<Integer> openService) {
  719. if (openService == null || openService.isEmpty()) {
  720. throw new ServiceException("服务类目不能为空");
  721. }
  722. Set<Integer> categoryIds = new LinkedHashSet<>();
  723. for (Integer categoryId : openService) {
  724. if (categoryId == null) {
  725. throw new ServiceException("服务类目ID不能为空");
  726. }
  727. categoryIds.add(categoryId);
  728. }
  729. return categoryIds;
  730. }
  731. private String joinIds(Set<Integer> ids) {
  732. return ids.stream()
  733. .map(String::valueOf)
  734. .collect(Collectors.joining(","));
  735. }
  736. private String joinProjectTitles(Set<Integer> projectIds, Map<Integer, Project> projectMap) {
  737. return projectIds.stream()
  738. .map(projectMap::get)
  739. .map(Project::getTitle)
  740. .filter(StringUtils::isNotBlank)
  741. .collect(Collectors.joining(","));
  742. }
  743. /**
  744. * 新增商户与服务项目关联关系
  745. *
  746. * @param technicianId
  747. * @param projectIds
  748. */
  749. private void insertProjectRelations(Integer technicianId, Set<Integer> projectIds) {
  750. if (technicianId == null) {
  751. throw new ServiceException("商户ID不能为空");
  752. }
  753. List<MaTeProject> relations = new ArrayList<>();
  754. for (Integer projectId : projectIds) {
  755. MaTeProject relation = new MaTeProject();
  756. relation.setTeId(technicianId);
  757. relation.setProjectId(projectId);
  758. relations.add(relation);
  759. }
  760. int rows = maTeProjectMapper.insertBatch(relations);
  761. if (rows != relations.size()) {
  762. throw new ServiceException("新增商户服务项目失败");
  763. }
  764. }
  765. private static class MerchantProjectSelection {
  766. private final Set<Integer> categoryIds;
  767. private final Set<Integer> projectIds;
  768. private final Map<Integer, Project> projectMap;
  769. private MerchantProjectSelection(Set<Integer> categoryIds, Set<Integer> projectIds, Map<Integer, Project> projectMap) {
  770. this.categoryIds = categoryIds;
  771. this.projectIds = projectIds;
  772. this.projectMap = projectMap;
  773. }
  774. private Set<Integer> getCategoryIds() {
  775. return categoryIds;
  776. }
  777. private Set<Integer> getProjectIds() {
  778. return projectIds;
  779. }
  780. private Map<Integer, Project> getProjectMap() {
  781. return projectMap;
  782. }
  783. }
  784. /**
  785. * 获取未申请技能列表
  786. *
  787. * @param userId
  788. * @param typeId
  789. * @return
  790. */
  791. @Override
  792. public List<Project> getNotApplyList(String userId, String typeId) {
  793. LambdaQueryWrapper<MaProject> query = new LambdaQueryWrapper<>();
  794. query.eq(MaProject::getMerchantId, userId);
  795. query.eq(MaProject::getMerchantType, typeId);
  796. List<MaProject> maProjectList = maProjectMapper.selectList(query);
  797. // 获取已申请技能ID集合
  798. List<Long> projectIdList = maProjectList.stream().map(MaProject::getProjectId).collect(Collectors.toList());
  799. if (projectIdList.size() == 0) {
  800. LambdaQueryWrapper<Project> query1 = new LambdaQueryWrapper<>();
  801. query1.eq(Project::getType, typeId);
  802. return projectMapper.selectList(query1);
  803. }
  804. LambdaQueryWrapper<Project> query2 = new LambdaQueryWrapper<>();
  805. query2.eq(Project::getType, typeId);
  806. query2.notIn(Project::getId, projectIdList);
  807. return projectMapper.selectList(query2);
  808. }
  809. /**
  810. * 申请开通新服务
  811. *
  812. * @param dto
  813. * @return
  814. */
  815. @Transactional(rollbackFor = Exception.class)
  816. public int applyForService(MaProjectSaveDto dto) {
  817. if (Objects.isNull(dto)) {
  818. return 0;
  819. }
  820. if (dto.getProjectIdList().size() > 0) {
  821. // 插入商户技能
  822. extracted(dto);
  823. } else {
  824. return 0;
  825. }
  826. return 1;
  827. }
  828. /**
  829. * 商户入住信息
  830. *
  831. * @param userId
  832. * @return
  833. */
  834. @Override
  835. public MerchantAuditFile getTechnicianList(Long userId) {
  836. LambdaQueryWrapper<MaTechnician> query = new LambdaQueryWrapper<>();
  837. query.eq(MaTechnician::getId, userId);
  838. MaTechnician merchant = maTechnicianMapper.selectOne(query);
  839. LambdaQueryWrapper<MerchantApplyFile> query1 = new LambdaQueryWrapper<>();
  840. query1.eq(MerchantApplyFile::getMerchantId, userId);
  841. List<MerchantApplyFile> merchantApplyFile = merchantApplyFileMapper.selectList(query1);
  842. MerchantAuditFile merchantAuditFile = new MerchantAuditFile();
  843. merchantAuditFile.setMerchant(merchant);
  844. merchantAuditFile.setMerchantAuditFile(merchantApplyFile);
  845. return merchantAuditFile;
  846. }
  847. /**
  848. * 查询商户合同记录信息
  849. *
  850. * @param userId
  851. * @return
  852. */
  853. @Override
  854. public List<ContractRecord> getContractRecords(Long userId){
  855. LambdaQueryWrapper<ContractRecord> query = new LambdaQueryWrapper<>();
  856. query.eq(ContractRecord::getMerchantId, userId);
  857. List<ContractRecord> contractRecordList = contractRecordMapper.selectList(query);
  858. if(contractRecordList.size() == 0) {
  859. return new ArrayList<>();
  860. }else {
  861. Set<String> seen = new HashSet<>();
  862. contractRecordList = contractRecordList.stream()
  863. .filter(record -> record.getContractName() != null && seen.add(record.getContractName()))
  864. .collect(Collectors.toList());
  865. }
  866. return contractRecordList;
  867. }
  868. private void extracted(MaProjectSaveDto dto) {
  869. LambdaQueryWrapper<Project> query = new LambdaQueryWrapper<>();
  870. query.in(Project::getId, dto.getProjectIdList());
  871. List<Project> projectList = projectMapper.selectList(query);
  872. for (Project project : projectList) {
  873. MaProject maProject = new MaProject();
  874. maProject.setProjectId(project.getId().longValue());
  875. maProject.setProjectName(project.getTitle());
  876. maProject.setProjectDescribe(project.getDetail());
  877. maProject.setProjectDuration(project.getStandardDuration());
  878. maProject.setProjectOriginalPrice(project.getPrice());
  879. maProject.setProjectMaxPrice(project.getPriceMax());
  880. maProject.setProjectLowestPrice(project.getPriceMin());
  881. maProject.setCreateBy(dto.getUserId());
  882. maProject.setUpdateBy(dto.getUserId());
  883. maProject.setMerchantId(dto.getUserId());
  884. maProject.setApplyTime(DateUtils.getNowDate());
  885. maProject.setMerchantPhone(dto.getMerchantPhone());
  886. maProjectMapper.insert(maProject);
  887. }
  888. }
  889. /**
  890. * 状态切换
  891. *
  892. * @param userId
  893. * @param forceConfirm
  894. * @return
  895. */
  896. @Override
  897. public Result switchToOffline(Long userId, Boolean forceConfirm) {
  898. MaTechnician technician = getTechnician(userId);
  899. // 1. 基础权限校验 (对应流程图左下角)
  900. if (!hasActiveSkills(userId)) {
  901. throw new RuntimeException("请先申请开通技能");
  902. }
  903. if (!hasHomeAddress(userId)) {
  904. // 这里通常会触发前端弹窗要求添加地址,或者直接阻断
  905. throw new RuntimeException("请完善家庭地址");
  906. }
  907. if (ProjectCategoryEnum.MASSAGE.getCode().equals(technician.getServiceTag())) {
  908. // 2. 状态判断逻辑 (对应流程图中间的菱形判断)
  909. // 只有处于“在线接单”状态才需要检查疲劳度/时长限制
  910. if (TechnicianStatusEnum.ONLINE.getCode().equals(technician.getPostState())) {
  911. // 获取今日的商户考勤记录
  912. MerchantDailyAttendance attendance = getTodayAttendance(userId);
  913. long minutesOnline = 0;
  914. if (attendance != null && attendance.getAttendanceStartTime() != null) {
  915. LocalDateTime localDateTime = attendance.getAttendanceStartTime().toInstant()
  916. .atZone(ZoneId.systemDefault())
  917. .toLocalDateTime();
  918. //计算截止现在的时长,单位为分钟
  919. minutesOnline = Duration.between(localDateTime, LocalDateTime.now()).toMinutes();
  920. // 计算今日的累加在线时长
  921. minutesOnline = minutesOnline + getWorkDuration(userId);
  922. }
  923. AttendanceRule rule = getAttendanceRule();
  924. if (rule != null) {
  925. // 将小时转换为分钟进行比较
  926. BigDecimal minutes = rule.getBasicWorkHours().multiply(new BigDecimal(60));
  927. // 2. 精确转换成 long(无小数、无溢出才成功)
  928. long requiredMinutes = minutes.longValueExact();
  929. // 判断是否超过了平台规定的在线时间 (X小时)
  930. if (minutesOnline < requiredMinutes) {
  931. // 情况 A: 超时了,且用户还没有点击“确认下线”(forceConfirm=false)
  932. if (!forceConfirm) {
  933. // 返回特定错误码或数据结构,告诉前端弹出“我在想想/确认下线”的模态框
  934. return Result.ok("平台对您的在线时间做了约定,每日在线需满足"
  935. + (requiredMinutes / 60) + "小时,距离您下线时间还剩余" + ((requiredMinutes / 60) - minutesOnline) + "小时"
  936. + "不满足在线时间将收到平台处罚,是否确认下线?");
  937. }
  938. // 情况 B: 超时了,但用户已经点击了“确认下线”,允许通过
  939. }
  940. }
  941. }
  942. // 3. 执行状态更新 (更新为休息中状态)
  943. updateStatus(userId, TechnicianStatusEnum.RESTING);
  944. if (!forceConfirm) {
  945. // 格式化成 yyyy-MM-dd 纯日期字符串
  946. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  947. String today = sdf.format(new Date());
  948. // 查询商户今日的最近考勤记录
  949. LambdaQueryWrapper<MerchantDailyAttendance> query = new LambdaQueryWrapper<>();
  950. query.eq(MerchantDailyAttendance::getMerchantId, userId)
  951. .eq(MerchantDailyAttendance::getAttendanceDate, today)
  952. .orderByDesc(MerchantDailyAttendance::getCreateTime);
  953. MerchantDailyAttendance update = merchantDailyAttendanceMapper.selectOne(query);
  954. if (update != null) {
  955. LocalDateTime localDateTime = update.getAttendanceStartTime().toInstant()
  956. .atZone(ZoneId.systemDefault())
  957. .toLocalDateTime();
  958. LambdaUpdateWrapper<MerchantDailyAttendance> updateWrapper = new LambdaUpdateWrapper<>();
  959. updateWrapper.eq(MerchantDailyAttendance::getId, update.getId())
  960. .set(MerchantDailyAttendance::getAttendanceEndTime, DateUtils.getNowDate())
  961. .set(MerchantDailyAttendance::getTotalWorkMinutes, Duration.between(localDateTime, LocalDateTime.now()).toMinutes())
  962. .set(MerchantDailyAttendance::getUpdateTime, DateUtils.getNowDate());
  963. merchantDailyAttendanceMapper.update(update, updateWrapper);
  964. }
  965. }
  966. } else {
  967. //更新状态为在线接单
  968. updateStatus(userId, TechnicianStatusEnum.ONLINE);
  969. // 插入今日考勤记录
  970. MerchantDailyAttendance merchantDailyAttendance = new MerchantDailyAttendance()
  971. .setMerchantId(userId.intValue())
  972. .setAttendanceDate(DateUtils.getNowDate())
  973. .setMerchantName(technician.getTeName())
  974. .setAttendanceStartTime(DateUtils.getNowDate())
  975. .setCreateBy(technician.getTeName())
  976. .setCreateTime(LocalDateTime.now());
  977. merchantDailyAttendanceMapper.insert(merchantDailyAttendance);
  978. }
  979. return Result.ok("状态已切换成功");
  980. }
  981. /**
  982. * 获取今天商户的考勤记录
  983. *
  984. * @param userId 技师ID
  985. * @return 考勤记录
  986. */
  987. private MerchantDailyAttendance getTodayAttendance(Long userId) {
  988. LambdaQueryWrapper<MerchantDailyAttendance> wrapper = new LambdaQueryWrapper<>();
  989. wrapper.eq(MerchantDailyAttendance::getMerchantId, userId)
  990. .eq(MerchantDailyAttendance::getAttendanceDate, DateUtils.getNowDate())
  991. .orderByDesc(MerchantDailyAttendance::getCreateTime)
  992. .last("LIMIT 1");
  993. return merchantDailyAttendanceMapper.selectOne(wrapper);
  994. }
  995. /**
  996. * 获取商户的累计工作时长
  997. *
  998. * @param userId 技师ID
  999. * @return 工作时长(分钟)
  1000. */
  1001. private long getWorkDuration(Long userId) {
  1002. LambdaQueryWrapper<MerchantDailyAttendance> wrapper = new LambdaQueryWrapper<>();
  1003. wrapper.eq(MerchantDailyAttendance::getMerchantId, userId)
  1004. .eq(MerchantDailyAttendance::getAttendanceDate, DateUtils.getNowDate());
  1005. List<MerchantDailyAttendance> attendances = merchantDailyAttendanceMapper.selectList(wrapper);
  1006. if (attendances == null || attendances.isEmpty()) return 0;
  1007. // 计算指定日期的总分钟数
  1008. long totalMinutes = attendances.stream()
  1009. .filter(attendance -> DateUtils.getNowDate().toString().equals(attendance.getAttendanceDate().toString()))
  1010. .mapToLong(MerchantDailyAttendance::getTotalWorkMinutes)
  1011. .sum();
  1012. return totalMinutes;
  1013. }
  1014. /**
  1015. * 获取商户的考勤规则
  1016. *
  1017. * @return 考勤规则
  1018. */
  1019. private AttendanceRule getAttendanceRule() {
  1020. LambdaQueryWrapper<AttendanceRule> wrapper = new LambdaQueryWrapper<>();
  1021. wrapper.eq(AttendanceRule::getIsDelete, 0);
  1022. wrapper.eq(AttendanceRule::getWorkDurationRuleEnabled, 1);
  1023. wrapper.last("LIMIT 1");
  1024. return attendanceRuleMapper.selectOne(wrapper);
  1025. }
  1026. /**
  1027. * 判断用户是否有生效中的技能
  1028. *
  1029. * @param userId 技师ID
  1030. * @return true: 有可用技能, false: 无
  1031. */
  1032. public boolean hasActiveSkills(Long userId) {
  1033. if (userId == null) return false;
  1034. // 构建查询条件:用户ID匹配 AND 状态为已发布/生效中
  1035. LambdaQueryWrapper<MaProject> wrapper = new LambdaQueryWrapper<>();
  1036. wrapper.eq(MaProject::getMerchantId, userId)
  1037. .eq(MaProject::getAuditStatus, 1); // 假设 1 代表 "生效/已审核"
  1038. // 只要查到一条记录即返回 true
  1039. return maProjectMapper.selectCount(wrapper) > 0;
  1040. }
  1041. /**
  1042. * 判断用户是否有家庭地址(通常指设置为默认的地址)
  1043. *
  1044. * @param userId 技师ID
  1045. * @return true: 有地址, false: 无
  1046. */
  1047. public boolean hasHomeAddress(Long userId) {
  1048. if (userId == null) return false;
  1049. // 构建查询条件:用户ID匹配 AND 是默认地址(可选) AND 未删除
  1050. LambdaQueryWrapper<TAddress> wrapper = new LambdaQueryWrapper<>();
  1051. wrapper.eq(TAddress::getMerchantId, userId)
  1052. .eq(TAddress::getUserType, 2) // 商户类型
  1053. .eq(TAddress::getIsDelete, 0); // 逻辑未删除
  1054. // 统计数量是否大于0
  1055. long count = addressMapper.selectCount(wrapper);
  1056. return count > 0;
  1057. }
  1058. // 辅助方法:模拟获取用户
  1059. private MaTechnician getTechnician(Long userId) {
  1060. // ... DB查询逻辑
  1061. LambdaQueryWrapper<MaTechnician> query = new LambdaQueryWrapper<>();
  1062. query.eq(MaTechnician::getId, userId);
  1063. return maTechnicianMapper.selectOne(query);
  1064. }
  1065. // 辅助方法:更新状态
  1066. private void updateStatus(Long userId, TechnicianStatusEnum status) {
  1067. LambdaUpdateWrapper<MaTechnician> update = new LambdaUpdateWrapper<>();
  1068. update.eq(MaTechnician::getId, userId);
  1069. update.set(MaTechnician::getPostState, status.getCode());
  1070. // ... DB Update逻辑,同时记录上线/下线时间
  1071. maTechnicianMapper.update(null, update);
  1072. }
  1073. /**
  1074. * 后台待审核页面审核通过商户。
  1075. *
  1076. * @param id 商户ID
  1077. * @param dto 待审核通过参数
  1078. * @param loginUser 当前登录用户
  1079. * @return 结果
  1080. */
  1081. @Override
  1082. @Transactional(rollbackFor = Exception.class)
  1083. public int approvePendingMerchantAudit(Integer id, MaTechnicianPendingAuditSubmitDTO dto, LoginUser loginUser) {
  1084. if (id == null) {
  1085. throw new ServiceException("商户ID不能为空");
  1086. }
  1087. if (dto == null) {
  1088. throw new ServiceException("审核参数不能为空");
  1089. }
  1090. String idCardFront = checkRequiredFileUrl(dto.getIdCardFront(), "身份证正面加密图片");
  1091. String idCardBack = checkRequiredFileUrl(dto.getIdCardBack(), "身份证反面加密图片");
  1092. String healthCertificate = checkRequiredFileUrl(dto.getHealthCertificate(), "健康证加密图片");
  1093. String qualificationCertificate = checkRequiredFileUrl(dto.getQualificationCertificate(), "资格证加密图片");
  1094. checkRequiredExpirationDate(dto.getIdCardExpirationDate(), "身份证到期时间");
  1095. checkRequiredExpirationDate(dto.getHealthCertificateExpirationDate(), "健康证到期时间");
  1096. checkRequiredExpirationDate(dto.getQualificationCertificateExpirationDate(), "资格证到期时间");
  1097. String auditRemark = dto.getAuditRemark() == null ? "" : dto.getAuditRemark().trim();
  1098. if (auditRemark.length() > AUDIT_REMARK_MAX_LENGTH) {
  1099. throw new ServiceException("审核备注长度不能超过" + AUDIT_REMARK_MAX_LENGTH + "个字符");
  1100. }
  1101. MaTechnician existsMerchant = maTechnicianMapper.selectMerchantById(id);
  1102. if (existsMerchant == null || !NOT_DELETED.equals(existsMerchant.getIsDelete())) {
  1103. throw new ServiceException("商户不存在或已删除");
  1104. }
  1105. if (!Integer.valueOf(AUDIT_WAIT_REVIEW).equals(existsMerchant.getAuditStatus())) {
  1106. throw new ServiceException("当前商户不是待审核状态,不能审核通过");
  1107. }
  1108. MaTechnician maTechnician = new MaTechnician();
  1109. maTechnician.setId(id);
  1110. /*maTechnician.setIdCard(String.join(",", idCardFront, idCardBack));
  1111. maTechnician.setHealthCertificate(healthCertificate);
  1112. maTechnician.setQualificationCertificate(qualificationCertificate);*/
  1113. maTechnician.setIdCardExpirationDate(dto.getIdCardExpirationDate());
  1114. maTechnician.setHealthCertificateExpirationDate(dto.getHealthCertificateExpirationDate());
  1115. maTechnician.setQualificationCertificateExpirationDate(dto.getQualificationCertificateExpirationDate());
  1116. maTechnician.setAuditStatus(AUDIT_APPROVED);
  1117. maTechnician.setAuditRemark(auditRemark);
  1118. maTechnician.setApproveTime(DateUtils.getNowDate());
  1119. if (loginUser != null && loginUser.getUser() != null) {
  1120. maTechnician.setUpdateBy(loginUser.getUser().getUserName());
  1121. }
  1122. maTechnician.setUpdateTime(DateUtils.getNowDate());
  1123. int rows = maTechnicianMapper.approvePendingMerchantAuditById(maTechnician);
  1124. if (rows <= 0) {
  1125. throw new ServiceException("待审核商户审核通过失败");
  1126. }
  1127. return rows;
  1128. }
  1129. private String checkRequiredFileUrl(String value, String fieldName) {
  1130. if (StringUtils.isBlank(value)) {
  1131. throw new ServiceException(fieldName + "不能为空");
  1132. }
  1133. return value.trim();
  1134. }
  1135. private void checkRequiredExpirationDate(LocalDate value, String fieldName) {
  1136. if (value == null) {
  1137. throw new ServiceException(fieldName + "不能为空");
  1138. }
  1139. if (value.isBefore(LocalDate.now())) {
  1140. throw new ServiceException(fieldName + "不能早于当前日期");
  1141. }
  1142. }
  1143. /**
  1144. * 技师待处理订单列表
  1145. *
  1146. * @param query
  1147. * @return
  1148. */
  1149. @Override
  1150. public List<WaitOrderDTO> listWaitOrder(WaitOrderQueryDTO query) {
  1151. LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
  1152. queryWrapper.eq(TOrder::getStatus, OrderStatusEnum.WAIT_JD.getCode());
  1153. // 1.查询所有待派未接单订单(status=待接单)
  1154. List<TOrder> allWaitOrder = orderMapper.selectList(queryWrapper);
  1155. if (CollectionUtils.isEmpty(allWaitOrder)) {
  1156. return Collections.emptyList();
  1157. }
  1158. BigDecimal techLat = query.getTechLat();
  1159. BigDecimal techLng = query.getTechLng();
  1160. // 2.逐个计算两点距离(Haversine公式)
  1161. List<WaitOrderDTO> dtoList = allWaitOrder.stream().map(order -> {
  1162. WaitOrderDTO dto = new WaitOrderDTO();
  1163. dto.setOrderId(order.getId());
  1164. dto.setProjectName(getShortProjectName(order.getProjectName()));
  1165. dto.setCustomerType("新客户");//无历史绑定默认新客,接单后再统计老客
  1166. dto.setOrderCreateTime(order.getCreateTime());
  1167. dto.setAppointTime(order.getAppointmentStartTime());
  1168. dto.setTargetAddress(order.getContactAddressInfo());
  1169. dto.setOrderLat(order.getUserLatitude());
  1170. dto.setOrderLng(order.getUserLongitude());
  1171. // 计算两点距离 单位:米
  1172. BigDecimal disMeter = calcDistance(techLat, techLng, order.getUserLatitude(), order.getUserLongitude());
  1173. dto.setDistanceMeter(disMeter);
  1174. dto.setDistanceDesc(formatDistance(disMeter));
  1175. return dto;
  1176. }).collect(Collectors.toList());
  1177. // 3.距离升序:近的排在最前面
  1178. return dtoList.stream()
  1179. .sorted(Comparator.comparing(WaitOrderDTO::getDistanceMeter))
  1180. .collect(Collectors.toList());
  1181. }
  1182. /**
  1183. * 接单
  1184. *
  1185. * @param req
  1186. * @return
  1187. */
  1188. @Override
  1189. public String acceptOrder(AcceptOrderReqDTO req) {
  1190. Long techId = req.getTechId();
  1191. Long orderId = req.getOrderId();
  1192. //【校验1:订单是否已被其他技师接单】
  1193. TOrder order = orderMapper.selectById(orderId);
  1194. if (OrderStatusEnum.RECEIVED_ORDER.getCode().equals(order.getStatus())) {
  1195. return OrderTipEnum.REPEAT_ORDER.getTip();
  1196. }
  1197. //【校验2:时间冲突校验(该技师已有已接单订单)】
  1198. boolean isTimeConflict = checkOrderTimeConflict(techId, order);
  1199. if (isTimeConflict) {
  1200. String tip = String.format(OrderTipEnum.TIME_CONFLICT.getTip(),
  1201. order.getStartTime().format(DateTimeFormatter.ofPattern("MM月dd日HH:mm")),
  1202. order.getCompletedTime().format(DateTimeFormatter.ofPattern("MM月dd日HH:mm")));
  1203. return tip;
  1204. }
  1205. //【校验3:技师休息状态】
  1206. MaTechnician tech = maTechnicianMapper.selectById(techId);
  1207. if (TechnicianStatusEnum.RESTING.getCode().equals(tech.getPostState())) {
  1208. return OrderTipEnum.REST_CONFIRM.getTip();
  1209. }
  1210. // 正常接单,绑定技师ID到订单
  1211. doAcceptOrder(techId, orderId);
  1212. return OrderTipEnum.ALREADY_ACCEPT.getTip();
  1213. }
  1214. /**
  1215. * 技师接单确认接单
  1216. *
  1217. * @param techId
  1218. * @param orderId
  1219. * @return
  1220. */
  1221. @Override
  1222. public String confirmRestAccept(Long techId, Long orderId){
  1223. LambdaUpdateWrapper<MaTechnician> update = new LambdaUpdateWrapper<>();
  1224. update.eq(MaTechnician::getId, techId);
  1225. update.set(MaTechnician::getPostState, TechnicianStatusEnum.ONLINE.getCode());
  1226. maTechnicianMapper.update(null, update);
  1227. doAcceptOrder(techId, orderId);
  1228. return OrderTipEnum.ALREADY_ACCEPT.getTip();
  1229. }
  1230. /**
  1231. * 技师拒绝接单
  1232. *
  1233. * @param req
  1234. * @return
  1235. */
  1236. @Override
  1237. public void refuseOrder(RefuseOrderReqDTO req){
  1238. LambdaUpdateWrapper<TOrder> update = new LambdaUpdateWrapper<>();
  1239. update.eq(TOrder::getId, req.getOrderId());
  1240. update.set(TOrder::getStatus, OrderStatusEnum.REFUSE.getCode());
  1241. update.set(TOrder::getRejectedReason, req.getRefuseReason());
  1242. orderMapper.update(null, update);
  1243. //拒单后订单重回待接单池,其他技师可刷到
  1244. LambdaUpdateWrapper<TOrder> update2 = new LambdaUpdateWrapper<>();
  1245. update2.eq(TOrder::getId, req.getOrderId());
  1246. update2.set(TOrder::getStatus, OrderStatusEnum.WAIT_JD.getCode());
  1247. update2.set(TOrder::getMerchantId, "");
  1248. update2.set(TOrder::getRejectedReason, "");
  1249. orderMapper.update(null, update2);
  1250. }
  1251. // =================工具方法=================
  1252. /**
  1253. * Haversine 计算经纬度距离 返回米
  1254. */
  1255. private BigDecimal calcDistance(BigDecimal lat1, BigDecimal lng1, BigDecimal lat2, BigDecimal lng2) {
  1256. // 球面距离计算公式,地球半径6371000米
  1257. // 可使用BigDecimal三角函数或数据库函数优化
  1258. double latRad1 = Math.toRadians(lat1.doubleValue());
  1259. double latRad2 = Math.toRadians(lat2.doubleValue());
  1260. double lngRad1 = Math.toRadians(lng1.doubleValue());
  1261. double lngRad2 = Math.toRadians(lng2.doubleValue());
  1262. double dLat = latRad2 - latRad1;
  1263. double dLng = lngRad2 - lngRad1;
  1264. double a = Math.pow(Math.sin(dLat / 2), 2)
  1265. + Math.cos(latRad1) * Math.cos(latRad2)
  1266. * Math.pow(Math.sin(dLng / 2), 2);
  1267. double dis = 2 * 6371000 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  1268. return BigDecimal.valueOf(dis).setScale(2, BigDecimal.ROUND_HALF_UP);
  1269. }
  1270. private String getShortProjectName(String name) {
  1271. if (name != null && name.length() > 10) {
  1272. return name.substring(0, 8) + "...";
  1273. }
  1274. return name;
  1275. }
  1276. private String formatDistance(BigDecimal distanceMeter) {
  1277. BigDecimal km = distanceMeter.divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
  1278. if (km.compareTo(BigDecimal.ONE) > 0) {
  1279. return km + "km";
  1280. } else {
  1281. return distanceMeter.intValue() + "m";
  1282. }
  1283. }
  1284. /**
  1285. * 校验技师已有订单时间冲突(只查该技师已接单数据)
  1286. */
  1287. private boolean checkOrderTimeConflict(Long techId, TOrder newOrder) {
  1288. LambdaQueryWrapper<TOrder> query = new LambdaQueryWrapper<>();
  1289. query.eq(TOrder::getMerchantId, techId);
  1290. query.eq(TOrder::getStatus, OrderStatusEnum.RECEIVED_ORDER.getCode());
  1291. query.eq(TOrder::getStartTime, newOrder.getStartTime());
  1292. List<TOrder> acceptedOrders = orderMapper.selectList(query);
  1293. LocalDate newOrderDate = newOrder.getStartTime().toLocalDate();
  1294. LocalDateTime newStart = newOrder.getStartTime();
  1295. for (TOrder old : acceptedOrders) {
  1296. if (!newOrderDate.isEqual(old.getCompletedTime().toLocalDate())) {
  1297. continue;
  1298. }
  1299. LocalDateTime oldEnd = old.getCompletedTime();
  1300. if (newStart.isBefore(oldEnd) || newStart.isEqual(oldEnd)) {
  1301. return true;
  1302. }
  1303. }
  1304. return false;
  1305. }
  1306. /**
  1307. * 接单:给订单赋值技师ID
  1308. */
  1309. private void doAcceptOrder(Long techId, Long orderId) {
  1310. TOrder update = new TOrder();
  1311. update.setId(orderId);
  1312. update.setMerchantId(techId);
  1313. update.setStatus(OrderStatusEnum.RECEIVED_ORDER.getCode());
  1314. orderMapper.updateById(update);
  1315. }
  1316. }