|
|
@@ -0,0 +1,705 @@
|
|
|
+package shop.alien.store.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.BeanUtils;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
+import shop.alien.entity.result.R;
|
|
|
+import shop.alien.entity.store.LifeFeedback;
|
|
|
+import shop.alien.entity.store.LifeFeedbackReply;
|
|
|
+import shop.alien.entity.store.LifeImg;
|
|
|
+import shop.alien.entity.store.LifeLog;
|
|
|
+import shop.alien.entity.store.dto.*;
|
|
|
+import shop.alien.entity.store.vo.*;
|
|
|
+import shop.alien.entity.store.vo.FeedbackReplyVo;
|
|
|
+import shop.alien.mapper.LifeFeedbackMapper;
|
|
|
+import shop.alien.mapper.LifeLogMapper;
|
|
|
+import shop.alien.mapper.LifeNoticeMapper;
|
|
|
+import shop.alien.mapper.StoreUserMapper;
|
|
|
+import shop.alien.entity.store.LifeNotice;
|
|
|
+import shop.alien.entity.store.StoreUser;
|
|
|
+import shop.alien.entity.store.vo.WebSocketVo;
|
|
|
+import shop.alien.store.config.WebSocketProcess;
|
|
|
+import shop.alien.store.service.LifeFeedbackService;
|
|
|
+import shop.alien.store.service.LifeFeedbackReplyService;
|
|
|
+import shop.alien.store.service.LifeImgService;
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+import java.util.ArrayList;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 意见反馈 Service实现类
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+@Transactional(rollbackFor = Exception.class)
|
|
|
+public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, LifeFeedback> implements LifeFeedbackService {
|
|
|
+
|
|
|
+ private final LifeFeedbackMapper lifeFeedbackMapper;
|
|
|
+ private final LifeImgService lifeImgService;
|
|
|
+ private final LifeLogMapper lifeLogMapper;
|
|
|
+ private final LifeFeedbackReplyService lifeFeedbackReplyService;
|
|
|
+ private final LifeNoticeMapper lifeNoticeMapper;
|
|
|
+ private final StoreUserMapper storeUserMapper;
|
|
|
+ private final WebSocketProcess webSocketProcess;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<String> submitFeedback(LifeFeedbackDto dto) {
|
|
|
+ try {
|
|
|
+ // 1. 参数校验
|
|
|
+ if (dto.getUserId() == null) {
|
|
|
+ return R.fail("用户ID不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getFeedbackSource() == null) {
|
|
|
+ return R.fail("反馈来源不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getFeedbackType() == null) {
|
|
|
+ return R.fail("反馈类型不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getContent() == null || dto.getContent().trim().isEmpty()) {
|
|
|
+ return R.fail("反馈内容不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 创建反馈记录(使用MyBatis Plus的save方法)
|
|
|
+ LifeFeedback feedback = new LifeFeedback();
|
|
|
+ BeanUtils.copyProperties(dto, feedback);
|
|
|
+ // 如果feedbackWay为空,默认为用户主动反馈(0)
|
|
|
+ if (feedback.getFeedbackWay() == null) {
|
|
|
+ feedback.setFeedbackWay(0);
|
|
|
+ }
|
|
|
+ feedback.setFeedbackTime(new Date());
|
|
|
+ feedback.setHandleStatus(0); // 处理中
|
|
|
+ feedback.setCreateTime(new Date());
|
|
|
+
|
|
|
+ boolean saveResult = this.save(feedback);
|
|
|
+ if (!saveResult) {
|
|
|
+ return R.fail("提交反馈失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 保存附件(图片和视频)
|
|
|
+ List<LifeImg> fileList = new ArrayList<>();
|
|
|
+ // 收集所有视频的截图URL,避免重复保存为普通图片
|
|
|
+ List<String> videoThumbnailUrls = new ArrayList<>();
|
|
|
+
|
|
|
+ if (!CollectionUtils.isEmpty(dto.getFileUrlList())) {
|
|
|
+ // 先处理视频,找到所有视频及其封面图
|
|
|
+ List<String> videoUrls = new ArrayList<>();
|
|
|
+ List<String> imageUrls = new ArrayList<>();
|
|
|
+
|
|
|
+ // 分类:区分视频和图片
|
|
|
+ for (String fileUrl : dto.getFileUrlList()) {
|
|
|
+ if (isVideoUrl(fileUrl)) {
|
|
|
+ videoUrls.add(fileUrl);
|
|
|
+ } else if (isImageUrl(fileUrl)) {
|
|
|
+ imageUrls.add(fileUrl);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理视频:自动匹配封面图
|
|
|
+ for (String videoUrl : videoUrls) {
|
|
|
+ LifeImg video = new LifeImg();
|
|
|
+ video.setFeedbackId(feedback.getId());
|
|
|
+ video.setImgUrl(videoUrl);
|
|
|
+ video.setFileType(2); // 2-视频
|
|
|
+ video.setUploadTime(new Date());
|
|
|
+
|
|
|
+ // 从fileUrlList中查找对应的封面图URL(通过文件名匹配)
|
|
|
+ // 视频URL格式: .../video/xxx123456.mp4
|
|
|
+ // 封面图URL格式: .../video/xxx123456.jpg 或 .../image/xxx123456.jpg
|
|
|
+ String videoFileName = videoUrl.substring(videoUrl.lastIndexOf('/') + 1);
|
|
|
+ String videoNameWithoutExt = videoFileName.substring(0, videoFileName.lastIndexOf('.'));
|
|
|
+
|
|
|
+ // 在图片列表中查找匹配的封面图
|
|
|
+ for (String imgUrl : imageUrls) {
|
|
|
+ String imgFileName = imgUrl.substring(imgUrl.lastIndexOf('/') + 1);
|
|
|
+ if (imgFileName.contains(".")) {
|
|
|
+ String imgNameWithoutExt = imgFileName.substring(0, imgFileName.lastIndexOf('.'));
|
|
|
+ // 如果文件名(不含扩展名)相同,且是图片格式,则认为是该视频的封面图
|
|
|
+ if (videoNameWithoutExt.equals(imgNameWithoutExt) && isImageUrl(imgUrl)) {
|
|
|
+ video.setThumbnailUrl(imgUrl);
|
|
|
+ videoThumbnailUrls.add(imgUrl); // 记录已使用的封面图URL
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fileList.add(video);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理图片(排除已作为视频封面的URL)
|
|
|
+ for (String imgUrl : imageUrls) {
|
|
|
+ // 如果该URL已被用作视频封面,则跳过,不重复保存
|
|
|
+ if (!videoThumbnailUrls.contains(imgUrl)) {
|
|
|
+ LifeImg img = new LifeImg();
|
|
|
+ img.setFeedbackId(feedback.getId());
|
|
|
+ img.setImgUrl(imgUrl);
|
|
|
+ img.setFileType(1); // 1-图片
|
|
|
+ img.setUploadTime(new Date());
|
|
|
+ fileList.add(img);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!fileList.isEmpty()) {
|
|
|
+ lifeImgService.batchSave(fileList);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 记录日志(只记录详细内容)
|
|
|
+ saveLog(feedback.getId(), feedback.getContent(), "0");
|
|
|
+
|
|
|
+ return R.success("提交成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("提交反馈失败", e);
|
|
|
+ return R.fail("提交反馈失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public IPage<LifeFeedbackVo> getFeedbackList(Integer userId, Integer feedbackSource, int page, int size) {
|
|
|
+ try {
|
|
|
+ // 使用自定义SQL查询(已包含工作人员名称)
|
|
|
+ // 查询用户反馈(feedbackWay=0)和AI识别(feedbackWay=1)的记录
|
|
|
+ Page<LifeFeedbackVo> pageParam = new Page<>(page, size);
|
|
|
+ IPage<LifeFeedbackVo> voPage = lifeFeedbackMapper.selectFeedbackListWithStaff(
|
|
|
+ pageParam, userId, feedbackSource, null, null
|
|
|
+ );
|
|
|
+
|
|
|
+ // 为每条记录查询附件(图片和视频)并设置反馈类型名称
|
|
|
+ voPage.getRecords().forEach(vo -> {
|
|
|
+ List<String> imgUrls = lifeImgService.getImgUrlsByFeedbackId(vo.getId());
|
|
|
+ vo.setImgUrlList(imgUrls);
|
|
|
+ List<String> videoUrls = lifeImgService.getVideoUrlsByFeedbackId(vo.getId());
|
|
|
+ vo.setVideoUrlList(videoUrls);
|
|
|
+ // 设置反馈类型名称
|
|
|
+ vo.setFeedbackTypeName(getFeedbackTypeName(vo.getFeedbackType()));
|
|
|
+ });
|
|
|
+
|
|
|
+ return voPage;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("查询反馈列表失败", e);
|
|
|
+ return new Page<>(page, size);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<LifeFeedbackVo> getFeedbackDetail(Integer feedbackId) {
|
|
|
+ try {
|
|
|
+ // 1. 使用自定义SQL查询反馈详情(已包含工作人员名称)
|
|
|
+ LifeFeedbackVo vo = lifeFeedbackMapper.selectFeedbackDetail(feedbackId);
|
|
|
+ if (vo == null) {
|
|
|
+ return R.fail("反馈记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 查询附件(图片和视频)
|
|
|
+ // 查询所有附件,然后过滤出原始反馈的附件(排除回复附件)
|
|
|
+ List<LifeImg> allImgs = lifeImgService.getByFeedbackId(feedbackId);
|
|
|
+ List<String> imgUrls = new ArrayList<>();
|
|
|
+ List<String> videoUrls = new ArrayList<>();
|
|
|
+ Date feedbackTime = vo.getFeedbackTime();
|
|
|
+ if (feedbackTime != null) {
|
|
|
+ long feedbackTimeMs = feedbackTime.getTime();
|
|
|
+ for (LifeImg img : allImgs) {
|
|
|
+ if (img.getUploadTime() != null) {
|
|
|
+ long imgTimeMs = img.getUploadTime().getTime();
|
|
|
+ // 判断附件是否属于原始反馈(时间差在5分钟内,且早于最早的回复)
|
|
|
+ // 简化处理:如果是反馈后5分钟内的附件,认为是原始反馈的附件
|
|
|
+ List<LifeFeedbackReply> replyList = lifeFeedbackReplyService.getByFeedbackId(feedbackId);
|
|
|
+ boolean isOriginalFeedback = true;
|
|
|
+ if (!replyList.isEmpty()) {
|
|
|
+ Date firstReplyTime = replyList.get(0).getCreateTime();
|
|
|
+ // 如果附件时间在最早回复时间之后,则不属于原始反馈
|
|
|
+ if (img.getUploadTime().after(firstReplyTime)) {
|
|
|
+ isOriginalFeedback = false;
|
|
|
+ } else {
|
|
|
+ long timeDiff = Math.abs(imgTimeMs - feedbackTimeMs);
|
|
|
+ if (timeDiff > 5 * 60 * 1000) { // 超过5分钟
|
|
|
+ isOriginalFeedback = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ long timeDiff = Math.abs(imgTimeMs - feedbackTimeMs);
|
|
|
+ if (timeDiff > 5 * 60 * 1000) { // 超过5分钟
|
|
|
+ isOriginalFeedback = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isOriginalFeedback) {
|
|
|
+ if (img.getFileType() == 1) {
|
|
|
+ imgUrls.add(img.getImgUrl());
|
|
|
+ } else if (img.getFileType() == 2) {
|
|
|
+ videoUrls.add(img.getImgUrl());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果没有反馈时间,使用简单方式:只查询图片和视频
|
|
|
+ for (LifeImg img : allImgs) {
|
|
|
+ if (img.getFileType() == 1) {
|
|
|
+ imgUrls.add(img.getImgUrl());
|
|
|
+ } else if (img.getFileType() == 2) {
|
|
|
+ videoUrls.add(img.getImgUrl());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ vo.setImgUrlList(imgUrls);
|
|
|
+ vo.setVideoUrlList(videoUrls);
|
|
|
+ // 设置反馈类型名称
|
|
|
+ vo.setFeedbackTypeName(getFeedbackTypeName(vo.getFeedbackType()));
|
|
|
+
|
|
|
+ // 3. 查询回复列表(从life_feedback_reply表)
|
|
|
+ List<LifeFeedbackReply> replyList = lifeFeedbackReplyService.getByFeedbackId(feedbackId);
|
|
|
+ // 转换为VO格式(使用之前查询的allImgs)
|
|
|
+ List<LifeFeedbackVo> replyVoList = new ArrayList<>();
|
|
|
+ for (LifeFeedbackReply reply : replyList) {
|
|
|
+ LifeFeedbackVo replyVo = new LifeFeedbackVo();
|
|
|
+ replyVo.setId(reply.getId());
|
|
|
+ replyVo.setContent(reply.getReplyContent());
|
|
|
+ replyVo.setFeedbackTime(reply.getCreateTime());
|
|
|
+ // reply_type: 0-平台回复, 1-我的回复
|
|
|
+ replyVo.setStaffId(reply.getReplyType() == 0 ? 1 : null); // 平台回复有staffId,用户回复为null
|
|
|
+ // 查询回复的附件(通过时间判断:上传时间在回复创建时间前后5分钟内)
|
|
|
+ List<String> replyImgUrls = new ArrayList<>();
|
|
|
+ List<String> replyVideoUrls = new ArrayList<>();
|
|
|
+ Date replyTime = reply.getCreateTime();
|
|
|
+ long replyTimeMs = replyTime.getTime();
|
|
|
+ for (LifeImg img : allImgs) {
|
|
|
+ if (img.getUploadTime() != null) {
|
|
|
+ long imgTimeMs = img.getUploadTime().getTime();
|
|
|
+ // 判断附件是否属于该回复(时间差在5分钟内)
|
|
|
+ long timeDiff = Math.abs(imgTimeMs - replyTimeMs);
|
|
|
+ if (timeDiff <= 5 * 60 * 1000) { // 5分钟
|
|
|
+ if (img.getFileType() == 1) {
|
|
|
+ replyImgUrls.add(img.getImgUrl());
|
|
|
+ } else if (img.getFileType() == 2) {
|
|
|
+ replyVideoUrls.add(img.getImgUrl());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ replyVo.setImgUrlList(replyImgUrls);
|
|
|
+ replyVo.setVideoUrlList(replyVideoUrls);
|
|
|
+ replyVoList.add(replyVo);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 按时间升序排序回复
|
|
|
+ replyVoList.sort((a, b) -> a.getFeedbackTime().compareTo(b.getFeedbackTime()));
|
|
|
+ vo.setPlatformReplies(replyVoList);
|
|
|
+
|
|
|
+ return R.data(vo);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("查询反馈详情失败", e);
|
|
|
+ return R.fail("查询反馈详情失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<String> userReply(UserReplyDto dto) {
|
|
|
+ try {
|
|
|
+ // 1. 参数校验
|
|
|
+ if (dto.getUserId() == null) {
|
|
|
+ return R.fail("用户ID不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getFeedbackSource() == null) {
|
|
|
+ return R.fail("反馈来源不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getFeedbackId() == null) {
|
|
|
+ return R.fail("反馈ID不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getContent() == null || dto.getContent().trim().isEmpty()) {
|
|
|
+ return R.fail("回复内容不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 查询原始反馈(用于验证反馈是否存在)
|
|
|
+ LifeFeedback originalFeedback = lifeFeedbackMapper.selectById(dto.getFeedbackId());
|
|
|
+ if (originalFeedback == null) {
|
|
|
+ return R.fail("反馈记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 创建用户回复记录(保存到life_feedback_reply表)
|
|
|
+ LifeFeedbackReply userReply = new LifeFeedbackReply();
|
|
|
+ userReply.setFeedbackId(dto.getFeedbackId());
|
|
|
+ userReply.setReplyType(1); // 1-我的回复(用户回复)
|
|
|
+ userReply.setReplyContent(dto.getContent());
|
|
|
+ userReply.setCreateTime(new Date());
|
|
|
+ userReply.setUpdateTime(new Date());
|
|
|
+
|
|
|
+ boolean saveResult = lifeFeedbackReplyService.save(userReply);
|
|
|
+ if (!saveResult) {
|
|
|
+ return R.fail("回复失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 记录日志(只记录内容)
|
|
|
+ saveLog(dto.getFeedbackId(), dto.getContent(), "2");
|
|
|
+
|
|
|
+ return R.success("回复成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("用户回复失败", e);
|
|
|
+ return R.fail("用户回复失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存操作日志
|
|
|
+ * @param feedbackId 反馈ID
|
|
|
+ * @param context 日志内容
|
|
|
+ * @param type 操作类型:0-创建反馈工单,1-分配跟踪人员,2-回复用户,3-问题解决状态
|
|
|
+ */
|
|
|
+ private void saveLog(Integer feedbackId, String context, String type) {
|
|
|
+ try {
|
|
|
+ LifeLog lifeLog = new LifeLog();
|
|
|
+ lifeLog.setFeedbackId(feedbackId);
|
|
|
+ lifeLog.setContext(context);
|
|
|
+ lifeLog.setType(type);
|
|
|
+ lifeLog.setCreatedTime(new Date());
|
|
|
+ lifeLogMapper.insert(lifeLog);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("保存日志失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取反馈类型名称
|
|
|
+ * @param feedbackType 反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈
|
|
|
+ * @return 反馈类型名称
|
|
|
+ */
|
|
|
+ private String getFeedbackTypeName(Integer feedbackType) {
|
|
|
+ if (feedbackType == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ switch (feedbackType) {
|
|
|
+ case 0:
|
|
|
+ return "bug反馈";
|
|
|
+ case 1:
|
|
|
+ return "优化反馈";
|
|
|
+ case 2:
|
|
|
+ return "新增功能反馈";
|
|
|
+ default:
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断URL是否为视频
|
|
|
+ * @param url 文件URL
|
|
|
+ * @return true-视频,false-非视频
|
|
|
+ */
|
|
|
+ private boolean isVideoUrl(String url) {
|
|
|
+ if (url == null || url.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String lowerUrl = url.toLowerCase();
|
|
|
+ return lowerUrl.endsWith(".mp4") ||
|
|
|
+ lowerUrl.endsWith(".avi") ||
|
|
|
+ lowerUrl.endsWith(".flv") ||
|
|
|
+ lowerUrl.endsWith(".mkv") ||
|
|
|
+ lowerUrl.endsWith(".rmvb") ||
|
|
|
+ lowerUrl.endsWith(".wmv") ||
|
|
|
+ lowerUrl.endsWith(".3gp") ||
|
|
|
+ lowerUrl.endsWith(".mov");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断URL是否为图片
|
|
|
+ * @param url 文件URL
|
|
|
+ * @return true-图片,false-非图片
|
|
|
+ */
|
|
|
+ private boolean isImageUrl(String url) {
|
|
|
+ if (url == null || url.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String lowerUrl = url.toLowerCase();
|
|
|
+ return lowerUrl.endsWith(".jpg") ||
|
|
|
+ lowerUrl.endsWith(".jpeg") ||
|
|
|
+ lowerUrl.endsWith(".png") ||
|
|
|
+ lowerUrl.endsWith(".bmp") ||
|
|
|
+ lowerUrl.endsWith(".webp") ||
|
|
|
+ lowerUrl.endsWith(".gif") ||
|
|
|
+ lowerUrl.endsWith(".svg");
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // ==================== 中台接口实现 ====================
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<IPage<LifeFeedbackListVo>> getWebFeedbackList(LifeFeedbackQueryDto queryDto) {
|
|
|
+ try {
|
|
|
+ Page<LifeFeedbackListVo> pageParam = new Page<>(queryDto.getPage(), queryDto.getSize());
|
|
|
+ IPage<LifeFeedbackListVo> result = lifeFeedbackMapper.selectWebFeedbackList(
|
|
|
+ pageParam,
|
|
|
+ queryDto.getFeedbackType(),
|
|
|
+ queryDto.getHandleStatus(),
|
|
|
+ queryDto.getFeedbackSource(),
|
|
|
+ queryDto.getFeedbackWay()
|
|
|
+ );
|
|
|
+ return R.data(result);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("中台-查询意见反馈列表失败", e);
|
|
|
+ return R.fail("查询失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<LifeFeedbackDetailVo> getWebFeedbackDetail(Integer feedbackId) {
|
|
|
+ try {
|
|
|
+ if (feedbackId == null) {
|
|
|
+ return R.fail("反馈ID不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 查询反馈详情
|
|
|
+ LifeFeedbackDetailVo detail = lifeFeedbackMapper.selectWebFeedbackDetail(feedbackId);
|
|
|
+ if (detail == null) {
|
|
|
+ return R.fail("反馈记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 查询附件列表(图片/视频)
|
|
|
+ List<FeedbackAttachmentVo> attachments = new ArrayList<>();
|
|
|
+ List<LifeImg> imgList = lifeImgService.getByFeedbackId(feedbackId);
|
|
|
+ if (!CollectionUtils.isEmpty(imgList)) {
|
|
|
+ for (LifeImg img : imgList) {
|
|
|
+ FeedbackAttachmentVo attachment = new FeedbackAttachmentVo();
|
|
|
+ attachment.setId(img.getId());
|
|
|
+ attachment.setFileType(img.getFileType() != null ? img.getFileType() : 1);
|
|
|
+ attachment.setFileUrl(img.getImgUrl());
|
|
|
+ attachment.setThumbnailUrl(img.getThumbnailUrl());
|
|
|
+ attachments.add(attachment);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ detail.setAttachments(attachments);
|
|
|
+
|
|
|
+ // 3. 查询回复列表(平台回复和用户回复)
|
|
|
+ List<LifeFeedbackReply> replyList = lifeFeedbackReplyService.getByFeedbackId(feedbackId);
|
|
|
+ List<FeedbackReplyVo> replyVoList = new ArrayList<>();
|
|
|
+ for (LifeFeedbackReply reply : replyList) {
|
|
|
+ FeedbackReplyVo replyVo = new FeedbackReplyVo();
|
|
|
+ replyVo.setId(reply.getId());
|
|
|
+ replyVo.setFeedbackId(reply.getFeedbackId());
|
|
|
+ replyVo.setReplyType(reply.getReplyType());
|
|
|
+ replyVo.setReplyTypeName(reply.getReplyType() == 0 ? "平台回复" : "用户回复");
|
|
|
+ replyVo.setReplyContent(reply.getReplyContent());
|
|
|
+ replyVo.setCreateTime(reply.getCreateTime());
|
|
|
+
|
|
|
+ // 查询回复的附件(通过时间判断:上传时间在回复创建时间前后5分钟内)
|
|
|
+ List<String> replyImgUrls = new ArrayList<>();
|
|
|
+ List<String> replyVideoUrls = new ArrayList<>();
|
|
|
+ Date replyTime = reply.getCreateTime();
|
|
|
+ if (replyTime != null && !CollectionUtils.isEmpty(imgList)) {
|
|
|
+ long replyTimeMs = replyTime.getTime();
|
|
|
+ for (LifeImg img : imgList) {
|
|
|
+ if (img.getUploadTime() != null) {
|
|
|
+ long imgTimeMs = img.getUploadTime().getTime();
|
|
|
+ // 判断附件是否属于该回复(时间差在5分钟内)
|
|
|
+ long timeDiff = Math.abs(imgTimeMs - replyTimeMs);
|
|
|
+ if (timeDiff <= 5 * 60 * 1000) { // 5分钟
|
|
|
+ if (img.getFileType() == 1) {
|
|
|
+ replyImgUrls.add(img.getImgUrl());
|
|
|
+ } else if (img.getFileType() == 2) {
|
|
|
+ replyVideoUrls.add(img.getImgUrl());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ replyVo.setImgUrlList(replyImgUrls);
|
|
|
+ replyVo.setVideoUrlList(replyVideoUrls);
|
|
|
+ replyVoList.add(replyVo);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按时间升序排序回复
|
|
|
+ replyVoList.sort((a, b) -> a.getCreateTime().compareTo(b.getCreateTime()));
|
|
|
+ detail.setReplies(replyVoList);
|
|
|
+
|
|
|
+ return R.data(detail);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("中台-查询反馈详情失败", e);
|
|
|
+ return R.fail("查询失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public R<String> webReplyUser(LifeFeedbackReplyWebDto replyDto) {
|
|
|
+ try {
|
|
|
+ // 1. 参数校验
|
|
|
+ if (replyDto.getFeedbackId() == null) {
|
|
|
+ return R.fail("反馈ID不能为空");
|
|
|
+ }
|
|
|
+ if (replyDto.getContent() == null || replyDto.getContent().trim().isEmpty()) {
|
|
|
+ return R.fail("回复内容不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 查询原始反馈
|
|
|
+ LifeFeedback feedback = lifeFeedbackMapper.selectById(replyDto.getFeedbackId());
|
|
|
+ if (feedback == null) {
|
|
|
+ return R.fail("反馈记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 保存平台回复到life_feedback_reply表
|
|
|
+ LifeFeedbackReply platformReply = new LifeFeedbackReply();
|
|
|
+ platformReply.setFeedbackId(replyDto.getFeedbackId());
|
|
|
+ platformReply.setReplyType(0); // 0-平台回复
|
|
|
+ platformReply.setReplyContent(replyDto.getContent());
|
|
|
+ platformReply.setCreateTime(new Date());
|
|
|
+ platformReply.setUpdateTime(new Date());
|
|
|
+
|
|
|
+ boolean saveResult = lifeFeedbackReplyService.save(platformReply);
|
|
|
+ if (!saveResult) {
|
|
|
+ return R.fail("保存回复失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 记录回复日志(类型3-回复用户)
|
|
|
+ String logContent = replyDto.getContent();
|
|
|
+ if (replyDto.getUserReply() != null && !replyDto.getUserReply().trim().isEmpty()) {
|
|
|
+ logContent = replyDto.getContent() + "||用户回复:" + replyDto.getUserReply();
|
|
|
+ }
|
|
|
+ saveFeedbackLog(replyDto.getFeedbackId(), 3, logContent);
|
|
|
+
|
|
|
+ // 5. 发送通知给用户
|
|
|
+ sendFeedbackReplyNotice(feedback, replyDto.getContent());
|
|
|
+
|
|
|
+ return R.success("回复成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("中台-回复用户失败", e);
|
|
|
+ return R.fail("回复失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public R<String> updateWebFeedbackStatus(LifeFeedbackStatusDto statusDto) {
|
|
|
+ try {
|
|
|
+ // 1. 参数校验
|
|
|
+ if (statusDto.getFeedbackId() == null) {
|
|
|
+ return R.fail("反馈ID不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 更新状态为已解决
|
|
|
+ LifeFeedback updateFeedback = new LifeFeedback();
|
|
|
+ updateFeedback.setId(statusDto.getFeedbackId());
|
|
|
+ updateFeedback.setHandleStatus(1); // 已解决
|
|
|
+ updateFeedback.setUpdateTime(new Date());
|
|
|
+
|
|
|
+ boolean result = this.updateById(updateFeedback);
|
|
|
+ if (!result) {
|
|
|
+ return R.fail("更新失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 记录日志(类型0-问题解决状态)
|
|
|
+ String logContent = "问题已解决";
|
|
|
+ saveFeedbackLog(statusDto.getFeedbackId(), 0, logContent);
|
|
|
+
|
|
|
+ return R.success("更新成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("中台-更新反馈状态失败", e);
|
|
|
+ return R.fail("更新失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存反馈操作日志
|
|
|
+ * @param feedbackId 反馈ID
|
|
|
+ * @param type 操作类型:0-问题解决状态,1-分配跟踪人员,2-创建反馈工单,3-回复用户
|
|
|
+ * @param context 日志内容
|
|
|
+ */
|
|
|
+ private void saveFeedbackLog(Integer feedbackId, Integer type, String context) {
|
|
|
+ try {
|
|
|
+ LifeLog lifeLog = new LifeLog();
|
|
|
+ lifeLog.setFeedbackId(feedbackId);
|
|
|
+ lifeLog.setType(String.valueOf(type));
|
|
|
+ lifeLog.setContext(context);
|
|
|
+ lifeLog.setCreatedTime(new Date());
|
|
|
+ lifeLog.setDeleteFlag(0);
|
|
|
+ lifeLogMapper.insert(lifeLog);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("保存反馈日志失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送平台回复通知给用户
|
|
|
+ * @param feedback 反馈记录
|
|
|
+ * @param replyContent 回复内容
|
|
|
+ */
|
|
|
+ private void sendFeedbackReplyNotice(LifeFeedback feedback, String replyContent) {
|
|
|
+ try {
|
|
|
+ String receiverId = null;
|
|
|
+
|
|
|
+ // 根据反馈来源判断是用户端还是商家端
|
|
|
+ if (feedback.getFeedbackSource() == null) {
|
|
|
+ log.warn("反馈来源为空,无法发送通知,feedbackId={}", feedback.getId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // userId对应store_user表的id,统一从store_user表查询
|
|
|
+ if (feedback.getUserId() == null) {
|
|
|
+ log.warn("用户ID为空,无法发送通知,feedbackId={}", feedback.getId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ StoreUser storeUser = storeUserMapper.selectById(feedback.getUserId());
|
|
|
+ if (storeUser == null || storeUser.getPhone() == null || storeUser.getPhone().trim().isEmpty()) {
|
|
|
+ log.warn("未找到商户用户信息或手机号为空,无法发送通知,userId={}", feedback.getUserId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据feedbackSource设置不同的接收者ID格式
|
|
|
+ if (feedback.getFeedbackSource() == 0) {
|
|
|
+ // 用户端 - 使用user_手机号格式
|
|
|
+ receiverId = "user_" + storeUser.getPhone();
|
|
|
+ } else if (feedback.getFeedbackSource() == 1) {
|
|
|
+ // 商家端 - 使用store_手机号格式
|
|
|
+ receiverId = "store_" + storeUser.getPhone();
|
|
|
+ } else {
|
|
|
+ log.warn("未知的反馈来源,feedbackSource={}, feedbackId={}", feedback.getFeedbackSource(), feedback.getId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建通知消息
|
|
|
+ JSONObject messageJson = new JSONObject();
|
|
|
+ messageJson.put("feedbackId", feedback.getId()); // 添加反馈ID用于区分
|
|
|
+ messageJson.put("message", "平台已回复您的意见反馈:" + replyContent);
|
|
|
+
|
|
|
+ // 创建通知记录
|
|
|
+ LifeNotice lifeNotice = new LifeNotice();
|
|
|
+ lifeNotice.setReceiverId(receiverId);
|
|
|
+ lifeNotice.setContext(messageJson.toJSONString());
|
|
|
+ lifeNotice.setTitle("意见反馈回复通知");
|
|
|
+ lifeNotice.setSenderId("system");
|
|
|
+ lifeNotice.setIsRead(0);
|
|
|
+ lifeNotice.setNoticeType(1); // 1-系统通知
|
|
|
+ lifeNotice.setBusinessId(feedback.getId());
|
|
|
+
|
|
|
+ // 保存通知
|
|
|
+ lifeNoticeMapper.insert(lifeNotice);
|
|
|
+
|
|
|
+ // 通过WebSocket发送实时通知
|
|
|
+ WebSocketVo webSocketVo = new WebSocketVo();
|
|
|
+ webSocketVo.setSenderId("system");
|
|
|
+ webSocketVo.setReceiverId(receiverId);
|
|
|
+ webSocketVo.setCategory("notice");
|
|
|
+ webSocketVo.setNoticeType("1");
|
|
|
+ webSocketVo.setIsRead(0);
|
|
|
+ webSocketVo.setText(JSONObject.toJSONString(lifeNotice));
|
|
|
+
|
|
|
+ try {
|
|
|
+ webSocketProcess.sendMessage(receiverId, JSONObject.toJSONString(webSocketVo));
|
|
|
+ log.info("平台回复通知发送成功,feedbackId={}, receiverId={}", feedback.getId(), receiverId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("发送WebSocket通知失败,feedbackId={}, receiverId={}, error={}",
|
|
|
+ feedback.getId(), receiverId, e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("发送平台回复通知异常,feedbackId={}, error={}", feedback.getId(), e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|