TechnicianMomentServiceImpl.java 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  1. package com.ylx.massage.service.impl;
  2. import cn.hutool.core.collection.CollUtil;
  3. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  6. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  7. import com.ylx.common.exception.ServiceException;
  8. import com.ylx.massage.domain.MaTechnician;
  9. import com.ylx.massage.domain.MomentMedia;
  10. import com.ylx.massage.domain.TJs;
  11. import com.ylx.massage.domain.TechnicianMoment;
  12. import com.ylx.massage.domain.dto.EditMomentDTO;
  13. import com.ylx.massage.domain.dto.MomentAuditDTO;
  14. import com.ylx.massage.domain.dto.MomentManageQueryDTO;
  15. import com.ylx.massage.domain.dto.PublishMomentDTO;
  16. import com.ylx.massage.domain.vo.*;
  17. import com.ylx.massage.mapper.MaTechnicianMapper;
  18. import com.ylx.massage.mapper.MomentMediaMapper;
  19. import com.ylx.massage.mapper.TJsMapper;
  20. import com.ylx.massage.mapper.TechnicianMomentMapper;
  21. import com.ylx.massage.service.ITechnicianMomentService;
  22. import com.ylx.merchant.domain.dto.MerchantMomentDTO;
  23. import com.ylx.merchant.domain.vo.MomentVO;
  24. import lombok.extern.slf4j.Slf4j;
  25. import org.apache.commons.lang3.StringUtils;
  26. import org.springframework.beans.BeanUtils;
  27. import org.springframework.beans.factory.annotation.Autowired;
  28. import org.springframework.stereotype.Service;
  29. import org.springframework.transaction.annotation.Transactional;
  30. import java.math.BigDecimal;
  31. import java.time.LocalDateTime;
  32. import java.util.*;
  33. import java.util.stream.Collectors;
  34. /**
  35. * 技师动态服务实现类
  36. */
  37. @Service
  38. @Slf4j
  39. public class TechnicianMomentServiceImpl extends ServiceImpl<TechnicianMomentMapper, TechnicianMoment> implements ITechnicianMomentService {
  40. private static final int AUDIT_STATUS_PENDING = 1;
  41. private static final int NOT_DELETED = 0;
  42. private static final int DELETED = 1;
  43. @Autowired
  44. private TechnicianMomentMapper momentMapper;
  45. @Autowired
  46. private MomentMediaMapper mediaMapper;
  47. @Autowired
  48. private TJsMapper tJsMapper;
  49. @Autowired
  50. private MaTechnicianMapper maTechnicianMapper;
  51. /**
  52. * 查询推荐动态列表(不分地区,按日期倒序,同一天按浏览量倒序)
  53. *
  54. * @param pageNum 页码,默认1
  55. * @param pageSize 每页数量,默认10
  56. * @return R<List < MomentListVO>> 动态列表
  57. */
  58. @Override
  59. public List<MomentListVO> getRecommendedMoments(Integer pageNum, Integer pageSize) {
  60. Page<TechnicianMoment> page = new Page<>(pageNum, pageSize);
  61. List<TechnicianMoment> moments = momentMapper.selectRecommendedMoments(page);
  62. if (moments == null || moments.isEmpty()) {
  63. return new ArrayList<>();
  64. }
  65. // 查询技师信息
  66. List<Integer> technicianIds = moments.stream().map(TechnicianMoment::getTechnicianId).collect(Collectors.toList());
  67. Map<Integer, MaTechnician> technicianMap = getTechnicianMap(technicianIds);
  68. // 组装VO
  69. return moments.stream().map(moment -> {
  70. MomentListVO vo = new MomentListVO();
  71. BeanUtils.copyProperties(moment, vo);
  72. MaTechnician technician = technicianMap.get(moment.getTechnicianId());
  73. if (technician != null) {
  74. vo.setTechnicianNickName(technician.getTeNickName());
  75. vo.setTechnicianAvatar(technician.getTeAvatar());
  76. vo.setTechnicianStatus(getTechnicianStatus(technician));
  77. }
  78. return vo;
  79. }).collect(Collectors.toList());
  80. }
  81. /**
  82. * 查询同城动态列表(按城市和发布时间倒序)
  83. */
  84. @Override
  85. public List<MomentListVO> getSameCityMoments(String cityCode, Integer pageNum, Integer pageSize) {
  86. Page<TechnicianMoment> page = new Page<>(pageNum, pageSize);
  87. List<TechnicianMoment> moments = momentMapper.selectSameCityMoments(page, cityCode);
  88. if (moments == null || moments.isEmpty()) {
  89. return new ArrayList<>();
  90. }
  91. // 查询技师信息
  92. List<Integer> technicianIds = moments.stream().map(TechnicianMoment::getTechnicianId).collect(Collectors.toList());
  93. Map<Integer, MaTechnician> technicianMap = getTechnicianMap(technicianIds);
  94. // 组装VO
  95. return moments.stream().map(moment -> {
  96. MomentListVO vo = new MomentListVO();
  97. BeanUtils.copyProperties(moment, vo);
  98. MaTechnician technician = technicianMap.get(moment.getTechnicianId());
  99. if (technician != null) {
  100. vo.setTechnicianNickName(technician.getTeNickName());
  101. vo.setTechnicianAvatar(technician.getTeAvatar());
  102. vo.setTechnicianStatus(getTechnicianStatus(technician));
  103. }
  104. return vo;
  105. }).collect(Collectors.toList());
  106. }
  107. /**
  108. * 查询附近动态列表(10km内,按日期倒序,同一天按距离排序)
  109. *
  110. * @param longitude 经度
  111. * @param latitude 纬度
  112. * @param pageNum 页码,默认1
  113. * @param pageSize 每页数量,默认10
  114. * @return R<List < MomentListVO>> 动态列表
  115. */
  116. @Override
  117. public List<MomentListVO> getNearbyMoments(BigDecimal longitude, BigDecimal latitude, Integer pageNum, Integer pageSize) {
  118. if (latitude == null || longitude == null) {
  119. throw new ServiceException("地理位置信息不能为空");
  120. }
  121. Page<TechnicianMoment> page = new Page<>(pageNum, pageSize);
  122. List<TechnicianMoment> moments = momentMapper.selectNearbyMoments(page, longitude, latitude);
  123. if (moments == null || moments.isEmpty()) {
  124. return new ArrayList<>();
  125. }
  126. // 查询技师信息
  127. List<Integer> technicianIds = moments.stream().map(TechnicianMoment::getTechnicianId).collect(Collectors.toList());
  128. Map<Integer, MaTechnician> technicianMap = getTechnicianMap(technicianIds);
  129. // 组装VO(需要计算距离)
  130. return moments.stream().map(moment -> {
  131. MomentListVO vo = new MomentListVO();
  132. BeanUtils.copyProperties(moment, vo);
  133. MaTechnician technician = technicianMap.get(moment.getTechnicianId());
  134. if (technician != null) {
  135. vo.setTechnicianNickName(technician.getTeNickName());
  136. vo.setTechnicianAvatar(technician.getTeAvatar());
  137. vo.setTechnicianStatus(getTechnicianStatus(technician));
  138. // 计算距离
  139. if (moment.getLatitude() != null && moment.getLongitude() != null) {
  140. double distance = calculateDistance(
  141. latitude.doubleValue(), longitude.doubleValue(),
  142. moment.getLatitude().doubleValue(), moment.getLongitude().doubleValue()
  143. );
  144. vo.setDistance(Math.round(distance * 100.0) / 100.0); // 保留两位小数
  145. }
  146. }
  147. return vo;
  148. }).collect(Collectors.toList());
  149. }
  150. /**
  151. * 查询动态详情(浏览量+1)
  152. *
  153. * @param momentId 动态ID
  154. * @param longitude 用户经度
  155. * @param latitude 用户纬度
  156. * @return MomentDetailVO 动态详情
  157. */
  158. @Override
  159. @Transactional(rollbackFor = Exception.class)
  160. public MomentDetailVO getMomentDetail(Long momentId, BigDecimal longitude, BigDecimal latitude) {
  161. // 查询动态信息
  162. TechnicianMoment moment = momentMapper.selectById(momentId);
  163. if (moment == null) {
  164. throw new ServiceException("动态不存在");
  165. }
  166. // 增加浏览量
  167. momentMapper.incrementViewCount(momentId);
  168. moment.setViewCount(moment.getViewCount() + 1);
  169. // 查询商户信息
  170. MaTechnician technician = maTechnicianMapper.selectById(moment.getTechnicianId());
  171. // 查询媒体列表
  172. List<MomentMedia> mediaList = mediaMapper.selectMediaListByMomentId(momentId);
  173. List<MomentMediaVO> mediaVOList = mediaList.stream().map(media -> {
  174. MomentMediaVO mediaVO = new MomentMediaVO();
  175. BeanUtils.copyProperties(media, mediaVO);
  176. return mediaVO;
  177. }).collect(Collectors.toList());
  178. // 组装VO
  179. MomentDetailVO vo = new MomentDetailVO();
  180. BeanUtils.copyProperties(moment, vo);
  181. if (technician != null) {
  182. vo.setTechnicianNickName(technician.getTeNickName());
  183. vo.setTechnicianAvatar(technician.getTeAvatar());
  184. vo.setTechnicianStatus(getTechnicianStatus(technician));
  185. }
  186. vo.setMediaList(mediaVOList);
  187. // 计算距离(如果有位置信息)
  188. if (moment.getLatitude() != null && moment.getLongitude() != null) {
  189. if(latitude != null && longitude != null){
  190. // 传入用户位置,计算用户与技师之间的距离
  191. double distance = calculateDistance(
  192. latitude.doubleValue(), longitude.doubleValue(),
  193. moment.getLatitude().doubleValue(), moment.getLongitude().doubleValue()
  194. );
  195. vo.setDistance(Math.round(distance * 100.0) / 100.0); // 保留两位小数
  196. }
  197. }
  198. return vo;
  199. }
  200. /**
  201. * 批量查询技师信息
  202. *
  203. * @param technicianIds 技师ID列表
  204. * @return Map<String, TJs> 技师ID-技师实体映射
  205. */
  206. private Map<Integer, MaTechnician> getTechnicianMap(List<Integer> technicianIds) {
  207. return maTechnicianMapper.selectBatchIds(technicianIds).stream().collect(Collectors.toMap(MaTechnician::getId, t -> t));
  208. }
  209. /**
  210. * 获取技师状态
  211. * 1-可服务(当前时间技师空闲)
  212. * 2-可预约(当前时间技师"已接单-服务中")
  213. */
  214. private Integer getTechnicianStatus(MaTechnician technician) {
  215. if (technician.getServiceState() != null && technician.getServiceState() == 1) {
  216. return 1; // 可服务
  217. } else if (technician.getServiceState() != null && technician.getServiceState() == 0) {
  218. return 2; // 可预约(服务中)
  219. }
  220. return null;
  221. }
  222. /**
  223. * 计算两点之间的距离(单位:km)
  224. *
  225. * @param lat1 纬度1(用户纬度)
  226. * @param lon1 经度1(用户经度)
  227. * @param lat2 纬度2(技师纬度)
  228. * @param lon2 经度2(技师经度)
  229. * @return double 距离(单位:km)
  230. * <p>
  231. * 使用Haversine公式
  232. */
  233. private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
  234. final double R = 6371; // 地球半径,单位km
  235. double dLat = Math.toRadians(lat2 - lat1);
  236. double dLon = Math.toRadians(lon2 - lon1);
  237. double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
  238. Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
  239. Math.sin(dLon / 2) * Math.sin(dLon / 2);
  240. double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  241. return R * c;
  242. }
  243. /**
  244. * 发布动态或保存草稿
  245. *
  246. * @param dto 发布动态请求对象
  247. * @param openId 技师OpenID
  248. * @return Long 动态ID
  249. */
  250. @Override
  251. @Transactional(rollbackFor = Exception.class)
  252. public Long publishMoment(PublishMomentDTO dto, String openId) {
  253. // 根据openId查询技师信息
  254. LambdaQueryWrapper<MaTechnician> queryWrapper = new LambdaQueryWrapper<>();
  255. queryWrapper.eq(MaTechnician::getCOpenid, openId);
  256. MaTechnician technician = maTechnicianMapper.selectOne(queryWrapper);
  257. if (technician == null) {
  258. throw new ServiceException("商户信息不存在");
  259. }
  260. //查询是否保存为草稿
  261. Integer isDraft = dto.getIsDraft();
  262. if (isDraft == 1) {
  263. //只校验标题
  264. if (dto.getTitle() == null || dto.getTitle().trim().isEmpty()) {
  265. throw new ServiceException("动态标题不能为空");
  266. }
  267. } else {
  268. // 校验动态标题
  269. if (dto.getTitle() == null || dto.getTitle().trim().isEmpty()) {
  270. throw new ServiceException("动态标题不能为空");
  271. }
  272. // 校验动态内容
  273. if (StringUtils.isBlank(dto.getContent())) {
  274. throw new ServiceException("动态内容不能为空");
  275. }
  276. // 校验媒体类型和媒体文件
  277. if (dto.getMediaType() == null) {
  278. throw new ServiceException("媒体类型不能为空");
  279. }
  280. if (dto.getMediaType() == 1) {
  281. // 图片类型
  282. if (dto.getImageUrls() == null || dto.getImageUrls().isEmpty()) {
  283. throw new ServiceException("图片不能为空");
  284. }
  285. if (dto.getImageUrls().size() > 9) {
  286. throw new ServiceException("最多只能上传9张图片");
  287. }
  288. } else if (dto.getMediaType() == 2) {
  289. // 视频类型
  290. if (StringUtils.isBlank(dto.getVideoUrl())) {
  291. throw new ServiceException("视频不能为空");
  292. }
  293. } else {
  294. throw new ServiceException("媒体类型不正确");
  295. }
  296. }
  297. // 创建动态对象
  298. TechnicianMoment moment = new TechnicianMoment();
  299. moment.setTechnicianId(technician.getId());
  300. moment.setTitle(dto.getTitle());
  301. moment.setContent(dto.getContent());
  302. moment.setMediaType(dto.getMediaType());
  303. moment.setLatitude(dto.getLatitude());
  304. moment.setLongitude(dto.getLongitude());
  305. moment.setLocation(dto.getLocation());
  306. moment.setAddress(dto.getAddress());
  307. if(Objects.nonNull(dto.getMediaType())){
  308. // 设置封面图
  309. if (dto.getMediaType() == 1) {
  310. // 图片类型,默认取第一张作为封面
  311. if (dto.getImageUrls() != null && !dto.getImageUrls().isEmpty()) {
  312. moment.setCoverUrl(dto.getImageUrls().get(0));
  313. }
  314. } else if (dto.getMediaType() == 2) {
  315. // 视频类型,使用视频URL作为封面
  316. moment.setCoverUrl(dto.getVideoCoverUrl());
  317. }
  318. }
  319. // 设置审核状态和发布时间
  320. if (dto.getIsDraft() != null && dto.getIsDraft() == 1) {
  321. // 保存草稿
  322. moment.setAuditStatus(0);
  323. moment.setPublishTime(null);
  324. } else {
  325. // 立即发布,进入待审核状态
  326. moment.setAuditStatus(1);
  327. moment.setPublishTime(LocalDateTime.now());
  328. }
  329. // 设置初始状态
  330. moment.setIsDelete(0);
  331. moment.setViewCount(0);
  332. moment.setCreateTime(LocalDateTime.now());
  333. moment.setUpdateTime(LocalDateTime.now());
  334. // 8. 保存动态到数据库
  335. int insertResult = momentMapper.insert(moment);
  336. if (insertResult <= 0) {
  337. throw new ServiceException("发布动态失败");
  338. }
  339. Long momentId = moment.getId();
  340. // 9. 保存媒体信息到媒体表
  341. if(Objects.nonNull(dto.getMediaType())){
  342. if (dto.getMediaType() == 1) {
  343. // 保存图片
  344. if (dto.getImageUrls() != null && !dto.getImageUrls().isEmpty()) {
  345. for (int i = 0; i < dto.getImageUrls().size(); i++) {
  346. MomentMedia media = new MomentMedia();
  347. media.setMomentId(momentId);
  348. media.setMediaUrl(dto.getImageUrls().get(i));
  349. media.setMediaType(1); // 图片
  350. media.setSortOrder(i + 1);
  351. media.setCreateTime(LocalDateTime.now());
  352. //获取文件的格式
  353. String fileFormat = dto.getImageUrls().get(i).substring(dto.getImageUrls().get(i).lastIndexOf(".") + 1);
  354. media.setFileFormat(fileFormat);
  355. mediaMapper.insert(media);
  356. }
  357. }
  358. } else if (dto.getMediaType() == 2) {
  359. // 保存视频
  360. MomentMedia media = new MomentMedia();
  361. media.setMomentId(momentId);
  362. media.setMediaUrl(dto.getVideoUrl());
  363. media.setMediaType(2); // 视频
  364. media.setSortOrder(1);
  365. media.setCreateTime(LocalDateTime.now());
  366. mediaMapper.insert(media);
  367. }
  368. }
  369. log.info("发布动态成功,动态ID:{},技师ID:{},是否草稿:{}", momentId, technician.getId(), dto.getIsDraft());
  370. return momentId;
  371. }
  372. /**
  373. * 编辑动态
  374. *
  375. * @param dto 编辑动态请求对象
  376. * @param openId 技师OpenID
  377. * @return Long 动态ID
  378. */
  379. @Override
  380. @Transactional(rollbackFor = Exception.class)
  381. public Long editMoment(EditMomentDTO dto, String openId) {
  382. // 1. 参数校验
  383. if (dto == null) {
  384. throw new ServiceException("编辑动态信息不能为空");
  385. }
  386. if (openId == null || openId.trim().isEmpty()) {
  387. throw new ServiceException("openId不能为空");
  388. }
  389. if (dto.getMomentId() == null) {
  390. throw new ServiceException("动态ID不能为空");
  391. }
  392. // 2. 根据openId查询商户信息
  393. LambdaQueryWrapper<MaTechnician> queryWrapper = new LambdaQueryWrapper<>();
  394. queryWrapper.eq(MaTechnician::getCOpenid, openId);
  395. MaTechnician technician = maTechnicianMapper.selectOne(queryWrapper);
  396. if (technician == null) {
  397. throw new ServiceException("商户信息不存在");
  398. }
  399. // 3. 查询动态信息
  400. TechnicianMoment moment = momentMapper.selectById(dto.getMomentId());
  401. if (moment == null) {
  402. throw new ServiceException("动态不存在");
  403. }
  404. // 4. 验证动态是否属于当前商户
  405. if (!moment.getTechnicianId().equals(technician.getId())) {
  406. throw new ServiceException("无权编辑此动态");
  407. }
  408. // 5. 验证动态状态是否允许编辑(只能编辑草稿和审核拒绝的动态)
  409. if (moment.getAuditStatus() == null || (moment.getAuditStatus() != 0 && moment.getAuditStatus() != 3)) {
  410. throw new ServiceException("只能编辑草稿或审核拒绝的动态");
  411. }
  412. // 6. 校验媒体类型和媒体文件
  413. if (dto.getMediaType() == null) {
  414. throw new ServiceException("媒体类型不能为空");
  415. }
  416. if (dto.getMediaType() == 1) {
  417. // 图片类型
  418. if (dto.getImageUrls() == null || dto.getImageUrls().isEmpty()) {
  419. throw new ServiceException("图片不能为空");
  420. }
  421. if (dto.getImageUrls().size() > 9) {
  422. throw new ServiceException("最多只能上传9张图片");
  423. }
  424. } else if (dto.getMediaType() == 2) {
  425. // 视频类型
  426. if (dto.getVideoUrl() == null || dto.getVideoUrl().trim().isEmpty()) {
  427. throw new ServiceException("视频不能为空");
  428. }
  429. } else {
  430. throw new ServiceException("媒体类型不正确");
  431. }
  432. // 7. 更新动态对象
  433. moment.setTitle(dto.getTitle());
  434. moment.setContent(dto.getContent());
  435. moment.setMediaType(dto.getMediaType());
  436. moment.setLongitude(dto.getLongitude());
  437. moment.setLatitude(dto.getLatitude());
  438. moment.setLocation(dto.getLocation());
  439. moment.setAddress(dto.getAddress());
  440. // 8.设置封面图
  441. if (dto.getMediaType() == 1) {
  442. // 图片类型,默认取第一张作为封面
  443. if (dto.getImageUrls() != null && !dto.getImageUrls().isEmpty()) {
  444. moment.setCoverUrl(dto.getImageUrls().get(0));
  445. }
  446. } else if (dto.getMediaType() == 2) {
  447. // 视频类型,使用视频封面URL(如果提供了)或视频URL作为封面
  448. if (dto.getVideoCoverUrl() != null && !dto.getVideoCoverUrl().trim().isEmpty()) {
  449. moment.setCoverUrl(dto.getVideoCoverUrl());
  450. } else {
  451. moment.setCoverUrl(dto.getVideoUrl());
  452. }
  453. }
  454. // 9. 设置审核状态和发布时间
  455. if (dto.getIsDraft() != null && dto.getIsDraft() == 1) {
  456. // 保存草稿
  457. moment.setAuditStatus(0); // 草稿
  458. moment.setPublishTime(null);
  459. moment.setRejectReason(null); // 清空拒绝原因
  460. } else {
  461. // 提交发布,进入待审核状态
  462. moment.setAuditStatus(1);
  463. if (moment.getPublishTime() == null) {
  464. moment.setPublishTime(LocalDateTime.now());
  465. }
  466. moment.setRejectReason(null); // 清空拒绝原因
  467. }
  468. moment.setUpdateTime(LocalDateTime.now());
  469. // 10. 更新动态到数据库
  470. int updateResult = momentMapper.updateById(moment);
  471. if (updateResult <= 0) {
  472. throw new ServiceException("编辑动态失败");
  473. }
  474. Long momentId = moment.getId();
  475. // 11. 删除旧的媒体信息
  476. LambdaQueryWrapper<MomentMedia> mediaWrapper = new LambdaQueryWrapper<>();
  477. mediaWrapper.eq(MomentMedia::getMomentId, momentId);
  478. mediaMapper.delete(mediaWrapper);
  479. // 12. 保存新的媒体信息到媒体表
  480. if (dto.getMediaType() == 1) {
  481. // 保存图片
  482. if (dto.getImageUrls() != null && !dto.getImageUrls().isEmpty()) {
  483. for (int i = 0; i < dto.getImageUrls().size(); i++) {
  484. MomentMedia media = new MomentMedia();
  485. media.setMomentId(momentId);
  486. media.setMediaUrl(dto.getImageUrls().get(i));
  487. media.setMediaType(1); // 图片
  488. media.setSortOrder(i + 1);
  489. media.setCreateTime(LocalDateTime.now());
  490. // 获取文件的格式
  491. String fileFormat = dto.getImageUrls().get(i).substring(dto.getImageUrls().get(i).lastIndexOf(".") + 1);
  492. media.setFileFormat(fileFormat);
  493. mediaMapper.insert(media);
  494. }
  495. }
  496. } else if (dto.getMediaType() == 2) {
  497. // 保存视频
  498. MomentMedia media = new MomentMedia();
  499. media.setMomentId(momentId);
  500. media.setMediaUrl(dto.getVideoUrl());
  501. media.setMediaType(2); // 视频
  502. media.setSortOrder(1);
  503. media.setCreateTime(LocalDateTime.now());
  504. mediaMapper.insert(media);
  505. }
  506. log.info("编辑动态成功,动态ID:{},技师ID:{},是否草稿:{}", momentId, technician.getId(), dto.getIsDraft());
  507. return momentId;
  508. }
  509. /**
  510. * 查询草稿箱列表
  511. *
  512. * @param openId 技师OpenID
  513. * @param pageNum 页码
  514. * @param pageSize 每页数量
  515. * @return List<MomentListVO> 草稿列表
  516. */
  517. @Override
  518. public Page<MomentListVO> getDraftMoments(String openId, Integer pageNum, Integer pageSize) {
  519. if (openId == null || openId.trim().isEmpty()) {
  520. throw new ServiceException("openId不能为空");
  521. }
  522. // 1. 根据openId查询技师信息
  523. LambdaQueryWrapper<MaTechnician> queryWrapper = new LambdaQueryWrapper<>();
  524. queryWrapper.eq(MaTechnician::getCOpenid, openId);
  525. MaTechnician technician = maTechnicianMapper.selectOne(queryWrapper);
  526. if (technician == null) {
  527. throw new ServiceException("商户信息不存在");
  528. }
  529. // 2. 查询该商户的草稿列表
  530. Page<TechnicianMoment> page = new Page<>(pageNum, pageSize);
  531. LambdaQueryWrapper<TechnicianMoment> momentWrapper = new LambdaQueryWrapper<>();
  532. momentWrapper.eq(TechnicianMoment::getTechnicianId, technician.getId())
  533. .eq(TechnicianMoment::getAuditStatus, 0) // 草稿状态
  534. .orderByDesc(TechnicianMoment::getCreateTime);
  535. Page<TechnicianMoment> momentPage = momentMapper.selectPage(page, momentWrapper);
  536. List<TechnicianMoment> moments = momentPage.getRecords();
  537. Page<MomentListVO> voPage = new Page<>(pageNum, pageSize);
  538. if (moments == null || moments.isEmpty()) {
  539. // 草稿列表为空时,返回空Page对象
  540. return voPage;
  541. }
  542. // 3. 组装VO
  543. List<MomentListVO> collect = moments.stream().map(moment -> {
  544. MomentListVO vo = new MomentListVO();
  545. BeanUtils.copyProperties(moment, vo);
  546. vo.setTechnicianId(technician.getId());
  547. vo.setTechnicianNickName(technician.getTeNickName());
  548. vo.setTechnicianAvatar(technician.getTeAvatar());
  549. vo.setTechnicianStatus(getTechnicianStatus(technician));
  550. // 草稿的发布时间使用创建时间
  551. vo.setPublishTime(moment.getCreateTime());
  552. return vo;
  553. }).collect(Collectors.toList());
  554. //返回Page对象
  555. voPage.setTotal(momentPage.getTotal());
  556. voPage.setRecords(collect);
  557. return voPage;
  558. }
  559. /**
  560. * 查询我的动态列表(已发布的动态:审核中+审核通过+审核拒绝)
  561. *
  562. * @param openId 技师OpenID
  563. * @param pageNum 页码
  564. * @param pageSize 每页数量
  565. * @return Page<MyMomentVO> 我的动态列表
  566. */
  567. @Override
  568. public Page<MyMomentVO> getMyMoments(String openId, Integer pageNum, Integer pageSize) {
  569. // 1. 参数校验
  570. if (openId == null || openId.trim().isEmpty()) {
  571. throw new ServiceException("openId不能为空");
  572. }
  573. // 2. 根据openId查询技师信息
  574. LambdaQueryWrapper<MaTechnician> queryWrapper = new LambdaQueryWrapper<>();
  575. queryWrapper.eq(MaTechnician::getCOpenid, openId);
  576. MaTechnician technician = maTechnicianMapper.selectOne(queryWrapper);
  577. if (technician == null) {
  578. throw new ServiceException("商户信息不存在");
  579. }
  580. // 3. 查询该商户的已发布动态(待审核、审核通过、审核拒绝)
  581. Page<TechnicianMoment> page = new Page<>(pageNum, pageSize);
  582. LambdaQueryWrapper<TechnicianMoment> momentWrapper = new LambdaQueryWrapper<>();
  583. momentWrapper.eq(TechnicianMoment::getTechnicianId, technician.getId())
  584. .in(TechnicianMoment::getAuditStatus, 0,1, 2,3) // 1-待审核, 2-审核通过,3-审核拒绝
  585. .orderByDesc(TechnicianMoment::getPublishTime);
  586. Page<TechnicianMoment> momentPage = momentMapper.selectPage(page, momentWrapper);
  587. // 4. 组装分页返回结果
  588. Page<MyMomentVO> voPage = new Page<>(pageNum, pageSize);
  589. voPage.setTotal(momentPage.getTotal());
  590. voPage.setCurrent(momentPage.getCurrent());
  591. voPage.setSize(momentPage.getSize());
  592. List<TechnicianMoment> moments = momentPage.getRecords();
  593. if (moments == null || moments.isEmpty()) {
  594. voPage.setRecords(new ArrayList<>());
  595. return voPage;
  596. }
  597. // 5. 组装VO列表
  598. List<MyMomentVO> voList = moments.stream().map(moment -> {
  599. MyMomentVO vo = new MyMomentVO();
  600. vo.setId(moment.getId());
  601. vo.setTitle(moment.getTitle());
  602. vo.setContent(moment.getContent());
  603. vo.setAuditStatus(moment.getAuditStatus());
  604. vo.setCoverUrl(moment.getCoverUrl());
  605. vo.setPublishTime(moment.getPublishTime());
  606. vo.setViewCount(moment.getViewCount());
  607. return vo;
  608. }).collect(Collectors.toList());
  609. voPage.setRecords(voList);
  610. return voPage;
  611. }
  612. /**
  613. * 删除我的动态。
  614. *
  615. * <p>审核中的动态不允许删除;其它属于当前商户且未删除的动态仅做逻辑删除。</p>
  616. *
  617. * @param momentId 动态ID
  618. * @param openId 当前登录商户OpenID
  619. * @return Boolean 是否删除成功
  620. */
  621. @Override
  622. @Transactional(rollbackFor = Exception.class)
  623. public Boolean deleteMyMoment(Long momentId, String openId) {
  624. if (momentId == null) {
  625. throw new ServiceException("动态ID不能为空");
  626. }
  627. if (StringUtils.isBlank(openId)) {
  628. throw new ServiceException("openId不能为空");
  629. }
  630. LambdaQueryWrapper<MaTechnician> technicianWrapper = new LambdaQueryWrapper<>();
  631. technicianWrapper.eq(MaTechnician::getCOpenid, openId);
  632. MaTechnician technician = maTechnicianMapper.selectOne(technicianWrapper);
  633. if (technician == null) {
  634. throw new ServiceException("商户信息不存在");
  635. }
  636. TechnicianMoment moment = momentMapper.selectById(momentId);
  637. if (moment == null || Objects.equals(moment.getIsDelete(), DELETED)) {
  638. throw new ServiceException("动态不存在或已删除");
  639. }
  640. if (!Objects.equals(moment.getTechnicianId(), technician.getId())) {
  641. throw new ServiceException("无权删除此动态");
  642. }
  643. if (Objects.equals(moment.getAuditStatus(), AUDIT_STATUS_PENDING)) {
  644. throw new ServiceException("审核中的动态不支持删除");
  645. }
  646. TechnicianMoment update = new TechnicianMoment();
  647. update.setIsDelete(DELETED);
  648. update.setUpdateBy(openId);
  649. update.setUpdateTime(LocalDateTime.now());
  650. LambdaUpdateWrapper<TechnicianMoment> updateWrapper = new LambdaUpdateWrapper<>();
  651. updateWrapper.eq(TechnicianMoment::getId, momentId)
  652. .eq(TechnicianMoment::getTechnicianId, technician.getId())
  653. .eq(TechnicianMoment::getIsDelete, NOT_DELETED);
  654. int rows = momentMapper.update(update, updateWrapper);
  655. if (rows <= 0) {
  656. throw new ServiceException("删除动态失败");
  657. }
  658. LambdaQueryWrapper<MomentMedia> mediaQueryWrapper = new LambdaQueryWrapper<>();
  659. mediaQueryWrapper.eq(MomentMedia::getMomentId, momentId);
  660. mediaMapper.delete(mediaQueryWrapper);
  661. log.info("删除动态成功,动态ID:{},商户ID:{}", momentId, technician.getId());
  662. return true;
  663. }
  664. /**
  665. * 根据技师ID查询动态列表(已发布的动态:审核中+审核通过)
  666. *
  667. * @param technicianId 技师ID
  668. * @return List<MomentListVO> 动态列表
  669. */
  670. @Override
  671. public List<MomentListVO> getMomentsByTechnicianId(String technicianId) {
  672. // 1. 参数校验
  673. if (technicianId == null || technicianId.trim().isEmpty()) {
  674. throw new ServiceException("技师ID不能为空");
  675. }
  676. // 2. 查询该技师的已发布动态(待审核、审核通过)
  677. LambdaQueryWrapper<TechnicianMoment> momentWrapper = new LambdaQueryWrapper<>();
  678. momentWrapper.eq(TechnicianMoment::getTechnicianId, technicianId)
  679. .in(TechnicianMoment::getAuditStatus, 1, 2) // 1-待审核, 2-审核通过
  680. .orderByDesc(TechnicianMoment::getPublishTime);
  681. List<TechnicianMoment> moments = momentMapper.selectList(momentWrapper);
  682. if (moments == null || moments.isEmpty()) {
  683. return new ArrayList<>();
  684. }
  685. // 3. 组装VO列表
  686. List<MomentListVO> voList = moments.stream().map(moment -> {
  687. MomentListVO vo = new MomentListVO();
  688. vo.setId(moment.getId());
  689. vo.setTitle(moment.getTitle());
  690. vo.setAuditStatus(moment.getAuditStatus());
  691. vo.setCoverUrl(moment.getCoverUrl());
  692. vo.setPublishTime(moment.getPublishTime());
  693. vo.setViewCount(moment.getViewCount());
  694. return vo;
  695. }).collect(Collectors.toList());
  696. log.info("根据技师ID查询动态列表成功,技师ID:{},记录数:{}", technicianId, voList.size());
  697. return voList;
  698. }
  699. /**
  700. * 查询待审核动态列表(管理后台)
  701. *
  702. * @param queryDTO 查询参数
  703. * @return Page<MomentManageVO> 分页结果
  704. */
  705. @Override
  706. public Page<MomentManageVO> getMomentManageList(MomentManageQueryDTO queryDTO) {
  707. // 1. 参数处理
  708. Integer pageNum = queryDTO.getPageNum() != null ? queryDTO.getPageNum() : 1;
  709. Integer pageSize = queryDTO.getPageSize() != null ? queryDTO.getPageSize() : 10;
  710. String technicianName = queryDTO.getTechnicianName();
  711. Integer auditStatus = queryDTO.getAuditStatus();
  712. String startTime = queryDTO.getStartTime();
  713. if (StringUtils.isNotBlank(startTime)) {
  714. startTime = startTime + " 00:00:00";
  715. }
  716. String endTime = queryDTO.getEndTime();
  717. if (StringUtils.isNotBlank(endTime)) {
  718. endTime = endTime + " 23:59:59";
  719. }
  720. // 2. 构建查询条件
  721. LambdaQueryWrapper<TechnicianMoment> queryWrapper = new LambdaQueryWrapper<>();
  722. // 如果传入了审核状态,则按照审核状态查询
  723. if (auditStatus != null) {
  724. queryWrapper.eq(TechnicianMoment::getAuditStatus, auditStatus);
  725. } else {
  726. // 默认查询全部的动态(待审核、审核通过、审核拒绝)
  727. queryWrapper.in(TechnicianMoment::getAuditStatus, 1, 2, 3);
  728. }
  729. // 技师姓名查询
  730. if (StringUtils.isNotBlank(technicianName)) {
  731. //根据技师姓名查询技师的ID
  732. LambdaQueryWrapper<MaTechnician> jsQueryWrapper = new LambdaQueryWrapper<>();
  733. jsQueryWrapper.like(MaTechnician::getTeName, technicianName);
  734. List<MaTechnician> technicians = maTechnicianMapper.selectList(jsQueryWrapper);
  735. if (CollUtil.isNotEmpty(technicians)) {
  736. // 构建查询条件,查询所有匹配商户的动态
  737. queryWrapper.in(TechnicianMoment::getTechnicianId, technicians.stream().map(MaTechnician::getId).collect(Collectors.toList()));
  738. } else {
  739. // 没有匹配的商户,查询空字符串列表
  740. queryWrapper.in(TechnicianMoment::getTechnicianId, Collections.singleton(""));
  741. }
  742. }
  743. // 时间范围查询
  744. if (startTime != null && !startTime.trim().isEmpty()) {
  745. queryWrapper.ge(TechnicianMoment::getPublishTime, startTime);
  746. }
  747. if (endTime != null && !endTime.trim().isEmpty()) {
  748. queryWrapper.le(TechnicianMoment::getPublishTime, endTime);
  749. }
  750. // 按创建时间倒序排序
  751. queryWrapper.orderByDesc(TechnicianMoment::getCreateTime);
  752. // 3. 分页查询
  753. Page<TechnicianMoment> page = new Page<>(pageNum, pageSize);
  754. Page<TechnicianMoment> momentPage = momentMapper.selectPage(page, queryWrapper);
  755. // 4. 组装VO
  756. Page<MomentManageVO> voPage = new Page<>(pageNum, pageSize);
  757. voPage.setTotal(momentPage.getTotal());
  758. voPage.setCurrent(momentPage.getCurrent());
  759. voPage.setSize(momentPage.getSize());
  760. List<TechnicianMoment> moments = momentPage.getRecords();
  761. if (moments == null || moments.isEmpty()) {
  762. voPage.setRecords(new ArrayList<>());
  763. return voPage;
  764. }
  765. // 5. 批量查询技师信息
  766. List<Integer> technicianIds = moments.stream().map(TechnicianMoment::getTechnicianId).collect(Collectors.toList());
  767. Map<Integer, MaTechnician> technicianMap = getTechnicianMap(technicianIds);
  768. // 6. 组装VO列表
  769. List<MomentManageVO> voList = moments.stream().map(moment -> {
  770. MomentManageVO vo = new MomentManageVO();
  771. BeanUtils.copyProperties(moment, vo);
  772. // 设置技师信息
  773. MaTechnician technician = technicianMap.get(moment.getTechnicianId());
  774. if (technician != null) {
  775. // 设置技师姓名
  776. vo.setTechnicianName(technician.getTeName());
  777. // 设置技师昵称
  778. vo.setTechnicianNickname(technician.getTeNickName());
  779. }
  780. return vo;
  781. }).collect(Collectors.toList());
  782. voPage.setRecords(voList);
  783. log.info("查询动态管理列表,页码:{},每页数量:{},总记录数:{}", pageNum, pageSize, voPage.getTotal());
  784. return voPage;
  785. }
  786. /**
  787. * 查询后台动态管理审核状态统计。
  788. *
  789. * @return MomentAuditStatusCountVO 审核状态统计
  790. */
  791. @Override
  792. public MomentAuditStatusCountVO getManageAuditStatusCount() {
  793. MomentAuditStatusCountVO count = momentMapper.selectManageAuditStatusCount();
  794. if (count == null) {
  795. count = new MomentAuditStatusCountVO();
  796. }
  797. if (count.getPendingCount() == null) {
  798. count.setPendingCount(0L);
  799. }
  800. if (count.getApprovedCount() == null) {
  801. count.setApprovedCount(0L);
  802. }
  803. if (count.getRejectedCount() == null) {
  804. count.setRejectedCount(0L);
  805. }
  806. return count;
  807. }
  808. /**
  809. * 审核动态(通过/拒绝)
  810. *
  811. * @param momentId 动态ID
  812. * @param auditStatus 审核状态:2-通过,3-拒绝
  813. * @param rejectReason 拒绝原因(拒绝时必填)
  814. * @return Boolean 是否成功
  815. */
  816. @Override
  817. @Transactional(rollbackFor = Exception.class)
  818. public Boolean auditMoment(MomentAuditDTO dto) {
  819. // 1. 参数校验
  820. if (dto.getMomentId() == null) {
  821. throw new ServiceException("动态ID不能为空");
  822. }
  823. if (dto.getAuditStatus() == null || (dto.getAuditStatus() != 2 && dto.getAuditStatus() != 3)) {
  824. throw new ServiceException("审核状态不正确");
  825. }
  826. if (dto.getAuditStatus() == 3 && (dto.getRejectReason() == null || dto.getRejectReason().trim().isEmpty())) {
  827. throw new ServiceException("拒绝时必须填写拒绝原因");
  828. }
  829. // 2. 查询动态信息
  830. TechnicianMoment moment = momentMapper.selectById(dto.getMomentId());
  831. if (moment == null) {
  832. throw new ServiceException("动态不存在");
  833. }
  834. // 3. 校验动态状态
  835. if (moment.getAuditStatus() != 1) {
  836. throw new ServiceException("只能审核待审核状态的动态");
  837. }
  838. // 4. 更新审核状态
  839. moment.setAuditStatus(dto.getAuditStatus());
  840. // 审核通过默认上架状态和推荐状态
  841. if(dto.getAuditStatus()==2){
  842. moment.setIsRecommend(1);
  843. moment.setIsList(1);
  844. }
  845. moment.setRejectReason(dto.getRejectReason());
  846. moment.setUpdateTime(LocalDateTime.now());
  847. int updateResult = momentMapper.updateById(moment);
  848. if (updateResult <= 0) {
  849. throw new ServiceException("审核失败");
  850. }
  851. return true;
  852. }
  853. /**
  854. * 更新动态推荐状态
  855. *
  856. * @param momentId 动态ID
  857. * @param isRecommend 是否推荐:0-否,1-是
  858. * @return 是否成功
  859. */
  860. @Override
  861. @Transactional(rollbackFor = Exception.class)
  862. public Boolean updateRecommendStatus(Long momentId, Integer isRecommend) {
  863. if (momentId == null) {
  864. throw new ServiceException("动态ID不能为空");
  865. }
  866. if (isRecommend == null || (isRecommend != 0 && isRecommend != 1)) {
  867. throw new ServiceException("是否推荐参数不正确");
  868. }
  869. int rows = momentMapper.updateRecommendStatus(momentId, isRecommend);
  870. if (rows <= 0) {
  871. throw new ServiceException("动态不存在或已删除");
  872. }
  873. log.info("更新动态推荐状态成功,动态ID:{},是否推荐:{}", momentId, isRecommend);
  874. return true;
  875. }
  876. /**
  877. * 更新动态上架状态
  878. *
  879. * @param momentId 动态ID
  880. * @param isList 是否上架:0-否,1-是
  881. * @return Boolean 是否成功
  882. */
  883. @Override
  884. @Transactional(rollbackFor = Exception.class)
  885. public Boolean updateListStatus(Long momentId, Integer isList) {
  886. if (momentId == null) {
  887. throw new ServiceException("动态ID不能为空");
  888. }
  889. if (isList == null || (isList != 0 && isList != 1)) {
  890. throw new ServiceException("上架状态参数不正确");
  891. }
  892. int rows = momentMapper.updateListStatus(momentId, isList);
  893. if (rows <= 0) {
  894. throw new ServiceException("动态不存在或已删除");
  895. }
  896. log.info("更新动态上架状态成功,动态ID:{},是否上架:{}", momentId, isList);
  897. return true;
  898. }
  899. /**
  900. * 根据动态ID查询动态简要详情(包含媒体URL列表)
  901. *
  902. * @param momentId 动态ID
  903. * @return MomentSimpleDetailVO动态简要详情
  904. */
  905. @Override
  906. public MomentSimpleDetailVO getMomentSimpleDetail(Long momentId) {
  907. // 1. 参数校验
  908. if (momentId == null) {
  909. throw new ServiceException("动态ID不能为空");
  910. }
  911. // 2. 查询动态信息
  912. Map<String, Object> result = momentMapper.selectMomentSimpleDetail(momentId);
  913. if (result == null || result.isEmpty()) {
  914. throw new ServiceException("动态不存在");
  915. }
  916. // 3. 组装VO
  917. MomentSimpleDetailVO vo = new MomentSimpleDetailVO();
  918. vo.setId(((Number) result.get("id")).longValue());
  919. vo.setTitle((String) result.get("title"));
  920. vo.setContent((String) result.get("content"));
  921. vo.setMediaType((Integer) result.get("media_type"));
  922. vo.setAuditStatus((Integer) result.get("audit_status"));
  923. vo.setRejectReason((String) result.get("reject_reason"));
  924. // 4. 处理媒体URL列表
  925. String mediaUrlsStr = (String) result.get("media_urls");
  926. if (mediaUrlsStr != null && !mediaUrlsStr.trim().isEmpty()) {
  927. // 将逗号分隔的字符串转换为List
  928. List<String> mediaUrls = Arrays.asList(mediaUrlsStr.split(","));
  929. vo.setMediaUrls(mediaUrls);
  930. } else {
  931. vo.setMediaUrls(new ArrayList<>());
  932. }
  933. log.info("查询动态简要详情成功,动态ID:{}", momentId);
  934. return vo;
  935. }
  936. @Override
  937. public Page<MomentVO> getByMerchantMoment(MerchantMomentDTO dto) {
  938. Page<MomentVO> page = new Page<>(dto.getCurrent(), dto.getSize());
  939. return this.baseMapper.getMerchantProject(page, dto);
  940. }
  941. }