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:
2025-11-17 23:37:04 +08:00
parent ebf05ab189
commit 755ba1153e
18 changed files with 1057 additions and 5 deletions

View File

@@ -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;
}
}