MaTechnicianServiceImpl.java 62 KB

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