TechnicianMomentServiceImpl.java 42 KB

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