You've already forked FrameTour-BE
feat(video): 新增视频评价功能及购买状态查询
- 移除TaskController上的@Deprecated注解 - 在VideoController中新增/checkBuyStatus接口用于查询视频购买状态 - 新增VideoReviewController控制器,提供评价管理功能 - 新增MapTypeHandler用于处理Map类型与JSON字段的转换 - 在VideoMapper中增加countBuyRecordByVideoId方法查询视频购买记录 - 新增视频评价相关实体类、DTO及Mapper接口 - 实现VideoReviewService服务类,支持评价新增、分页查询、统计分析和Excel导出 - 在VideoServiceImpl中实现checkVideoBuyStatus方法 - 修改VideoMapper.xml,关联task表并查询task_params字段 - 新增VideoReviewMapper.xml配置文件,实现评价相关SQL查询
This commit is contained in:
48
src/main/java/com/ycwl/basic/service/VideoReviewService.java
Normal file
48
src/main/java/com/ycwl/basic/service/VideoReviewService.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.ycwl.basic.service;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* 视频评价Service接口
|
||||
*/
|
||||
public interface VideoReviewService {
|
||||
|
||||
/**
|
||||
* 新增视频评价
|
||||
*
|
||||
* @param reqDTO 评价信息
|
||||
* @return 评价ID
|
||||
*/
|
||||
Long addReview(VideoReviewAddReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 分页查询评价列表
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageInfo<VideoReviewRespDTO> getReviewList(VideoReviewListReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 获取评价统计数据
|
||||
*
|
||||
* @return 统计结果
|
||||
*/
|
||||
VideoReviewStatisticsRespDTO getStatistics();
|
||||
|
||||
/**
|
||||
* 导出评价数据到Excel
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @param outputStream 输出流
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
void exportReviews(VideoReviewListReqDTO reqDTO, OutputStream outputStream) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.ycwl.basic.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
import com.ycwl.basic.mapper.VideoReviewMapper;
|
||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO;
|
||||
import com.ycwl.basic.model.pc.videoreview.entity.VideoReviewEntity;
|
||||
import com.ycwl.basic.service.VideoReviewService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 视频评价Service实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VideoReviewServiceImpl implements VideoReviewService {
|
||||
|
||||
@Autowired
|
||||
private VideoReviewMapper videoReviewMapper;
|
||||
|
||||
@Autowired
|
||||
private VideoMapper videoMapper;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long addReview(VideoReviewAddReqDTO reqDTO) {
|
||||
// 1. 参数校验
|
||||
if (reqDTO.getVideoId() == null) {
|
||||
throw new BizException("视频ID不能为空");
|
||||
}
|
||||
if (reqDTO.getRating() == null || reqDTO.getRating() < 1 || reqDTO.getRating() > 5) {
|
||||
throw new BizException("评分必须在1-5之间");
|
||||
}
|
||||
|
||||
// 2. 查询视频信息,获取景区ID
|
||||
VideoEntity video = videoMapper.selectById(reqDTO.getVideoId());
|
||||
if (video == null) {
|
||||
throw new BizException("视频不存在");
|
||||
}
|
||||
|
||||
// 3. 获取当前登录用户(管理员)
|
||||
String userIdStr = BaseContextHandler.getUserId();
|
||||
if (userIdStr == null || userIdStr.isEmpty()) {
|
||||
throw new BizException("未登录或登录已过期");
|
||||
}
|
||||
Long creator = Long.valueOf(userIdStr);
|
||||
|
||||
// 4. 构建评价实体
|
||||
VideoReviewEntity entity = new VideoReviewEntity();
|
||||
entity.setVideoId(reqDTO.getVideoId());
|
||||
entity.setScenicId(video.getScenicId());
|
||||
entity.setCreator(creator);
|
||||
entity.setRating(reqDTO.getRating());
|
||||
entity.setContent(reqDTO.getContent());
|
||||
entity.setCameraPositionRating(reqDTO.getCameraPositionRating());
|
||||
|
||||
// 5. 插入数据库
|
||||
videoReviewMapper.insert(entity);
|
||||
|
||||
log.info("管理员[{}]对视频[{}]添加评价成功,评价ID: {}", creator, reqDTO.getVideoId(), entity.getId());
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageInfo<VideoReviewRespDTO> getReviewList(VideoReviewListReqDTO reqDTO) {
|
||||
// 设置分页参数
|
||||
PageHelper.startPage(reqDTO.getPageNum(), reqDTO.getPageSize());
|
||||
|
||||
// 查询列表
|
||||
List<VideoReviewRespDTO> list = videoReviewMapper.selectReviewList(reqDTO);
|
||||
|
||||
// 封装分页结果
|
||||
return new PageInfo<>(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoReviewStatisticsRespDTO getStatistics() {
|
||||
VideoReviewStatisticsRespDTO statistics = new VideoReviewStatisticsRespDTO();
|
||||
|
||||
// 1. 总评价数
|
||||
Long totalCount = videoReviewMapper.countTotal();
|
||||
statistics.setTotalCount(totalCount);
|
||||
|
||||
// 2. 平均评分
|
||||
Double avgRating = videoReviewMapper.calculateAverageRating();
|
||||
if (avgRating != null) {
|
||||
statistics.setAverageRating(BigDecimal.valueOf(avgRating).setScale(2, RoundingMode.HALF_UP));
|
||||
} else {
|
||||
statistics.setAverageRating(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
// 3. 评分分布
|
||||
List<Map<String, Object>> ratingDistList = videoReviewMapper.countRatingDistribution();
|
||||
Map<Integer, Long> ratingDistribution = new HashMap<>();
|
||||
for (Map<String, Object> item : ratingDistList) {
|
||||
Integer rating = (Integer) item.get("ratingValue");
|
||||
Long count = (Long) item.get("count");
|
||||
ratingDistribution.put(rating, count);
|
||||
}
|
||||
statistics.setRatingDistribution(ratingDistribution);
|
||||
|
||||
// 4. 最近7天趋势
|
||||
List<Map<String, Object>> trendList = videoReviewMapper.countRecentTrend(7);
|
||||
Map<String, Long> recentTrend = new LinkedHashMap<>();
|
||||
for (Map<String, Object> item : trendList) {
|
||||
String date = (String) item.get("dateStr");
|
||||
Long count = (Long) item.get("count");
|
||||
recentTrend.put(date, count);
|
||||
}
|
||||
statistics.setRecentTrend(recentTrend);
|
||||
|
||||
// 5. 景区排行(前10)
|
||||
List<VideoReviewStatisticsRespDTO.ScenicReviewRank> scenicRankList = videoReviewMapper.countScenicRank(10);
|
||||
statistics.setScenicRankList(scenicRankList);
|
||||
|
||||
// 6. 机位评价维度平均值
|
||||
Map<String, BigDecimal> cameraPositionAverage = calculateCameraPositionAverage();
|
||||
statistics.setCameraPositionAverage(cameraPositionAverage);
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportReviews(VideoReviewListReqDTO reqDTO, OutputStream outputStream) throws IOException {
|
||||
// 1. 查询所有数据(不分页)
|
||||
reqDTO.setPageNum(1);
|
||||
reqDTO.setPageSize(Integer.MAX_VALUE);
|
||||
List<VideoReviewRespDTO> list = videoReviewMapper.selectReviewList(reqDTO);
|
||||
|
||||
// 2. 创建Excel工作簿
|
||||
Workbook workbook = new XSSFWorkbook();
|
||||
Sheet sheet = workbook.createSheet("视频评价数据");
|
||||
|
||||
// 3. 创建标题行样式
|
||||
CellStyle headerStyle = workbook.createCellStyle();
|
||||
Font headerFont = workbook.createFont();
|
||||
headerFont.setBold(true);
|
||||
headerStyle.setFont(headerFont);
|
||||
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
||||
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
|
||||
// 4. 创建标题行
|
||||
Row headerRow = sheet.createRow(0);
|
||||
String[] headers = {"评价ID", "视频ID", "景区ID", "景区名称", "评价人ID", "评价人名称",
|
||||
"评分", "文字评价", "机位评价", "创建时间", "更新时间"};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell cell = headerRow.createCell(i);
|
||||
cell.setCellValue(headers[i]);
|
||||
cell.setCellStyle(headerStyle);
|
||||
}
|
||||
|
||||
// 5. 填充数据
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
int rowNum = 1;
|
||||
for (VideoReviewRespDTO review : list) {
|
||||
Row row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue(review.getId());
|
||||
row.createCell(1).setCellValue(review.getVideoId());
|
||||
row.createCell(2).setCellValue(review.getScenicId());
|
||||
row.createCell(3).setCellValue(review.getScenicName());
|
||||
row.createCell(4).setCellValue(review.getCreator());
|
||||
row.createCell(5).setCellValue(review.getCreatorName());
|
||||
row.createCell(6).setCellValue(review.getRating());
|
||||
row.createCell(7).setCellValue(review.getContent());
|
||||
|
||||
// 机位评价JSON转字符串
|
||||
try {
|
||||
String cameraRatingJson = review.getCameraPositionRating() != null ?
|
||||
objectMapper.writeValueAsString(review.getCameraPositionRating()) : "";
|
||||
row.createCell(8).setCellValue(cameraRatingJson);
|
||||
} catch (Exception e) {
|
||||
row.createCell(8).setCellValue("");
|
||||
}
|
||||
|
||||
row.createCell(9).setCellValue(review.getCreateTime() != null ? sdf.format(review.getCreateTime()) : "");
|
||||
row.createCell(10).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : "");
|
||||
}
|
||||
|
||||
// 6. 自动调整列宽
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
sheet.autoSizeColumn(i);
|
||||
}
|
||||
|
||||
// 7. 写入输出流
|
||||
workbook.write(outputStream);
|
||||
workbook.close();
|
||||
|
||||
log.info("导出视频评价数据成功,共{}条", list.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算机位评价各维度的平均值
|
||||
*/
|
||||
private Map<String, BigDecimal> calculateCameraPositionAverage() {
|
||||
List<Map<String, Integer>> allRatings = videoReviewMapper.selectAllCameraPositionRatings();
|
||||
|
||||
if (allRatings == null || allRatings.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
// 统计各维度的总分和次数
|
||||
Map<String, List<Integer>> dimensionScores = new HashMap<>();
|
||||
for (Map<String, Integer> rating : allRatings) {
|
||||
if (rating == null) continue;
|
||||
for (Map.Entry<String, Integer> entry : rating.entrySet()) {
|
||||
dimensionScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// 计算平均值
|
||||
Map<String, BigDecimal> result = new HashMap<>();
|
||||
for (Map.Entry<String, List<Integer>> entry : dimensionScores.entrySet()) {
|
||||
double avg = entry.getValue().stream().mapToInt(Integer::intValue).average().orElse(0.0);
|
||||
result.put(entry.getKey(), BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -17,5 +17,12 @@ public interface VideoService {
|
||||
ApiResponse<List<VideoRespVO>> list(VideoReqQuery videoReqQuery);
|
||||
ApiResponse<VideoRespVO> getById(Long id);
|
||||
|
||||
/**
|
||||
* 查询视频是否被购买
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 是否已购买 (true-已购买, false-未购买)
|
||||
*/
|
||||
Boolean checkVideoBuyStatus(Long videoId);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package com.ycwl.basic.service.pc.impl;
|
||||
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
||||
import com.ycwl.basic.model.pc.video.req.VideoReqQuery;
|
||||
import com.ycwl.basic.model.pc.video.resp.VideoRespVO;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
@@ -79,4 +82,12 @@ public class VideoServiceImpl implements VideoService {
|
||||
return ApiResponse.success(videoMapper.getById(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean checkVideoBuyStatus(Long videoId) {
|
||||
// 查询 member_video 表中是否存在该视频的已购买记录
|
||||
int count = videoMapper.countBuyRecordByVideoId(videoId);
|
||||
// count > 0 表示存在已购买记录
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user