MaTechnicianServiceImpl.java 68 KB

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