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:
@@ -0,0 +1,94 @@
|
||||
package com.ycwl.basic.controller;
|
||||
|
||||
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 com.ycwl.basic.service.VideoReviewService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
/**
|
||||
* 视频评价Controller
|
||||
* 管理端使用,通过token角色控制权限
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/video-review/v1")
|
||||
public class VideoReviewController {
|
||||
|
||||
@Autowired
|
||||
private VideoReviewService videoReviewService;
|
||||
|
||||
/**
|
||||
* 新增视频评价
|
||||
*
|
||||
* @param reqDTO 评价信息
|
||||
* @return 评价ID
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public ApiResponse<Long> addReview(@RequestBody VideoReviewAddReqDTO reqDTO) {
|
||||
log.info("新增视频评价,videoId: {}", reqDTO.getVideoId());
|
||||
Long reviewId = videoReviewService.addReview(reqDTO);
|
||||
return ApiResponse.success(reviewId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询评价列表
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public ApiResponse<PageInfo<VideoReviewRespDTO>> getReviewList(VideoReviewListReqDTO reqDTO) {
|
||||
log.info("查询视频评价列表,pageNum: {}, pageSize: {}", reqDTO.getPageNum(), reqDTO.getPageSize());
|
||||
PageInfo<VideoReviewRespDTO> pageInfo = videoReviewService.getReviewList(reqDTO);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评价统计数据
|
||||
*
|
||||
* @return 统计结果
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public ApiResponse<VideoReviewStatisticsRespDTO> getStatistics() {
|
||||
log.info("获取视频评价统计数据");
|
||||
VideoReviewStatisticsRespDTO statistics = videoReviewService.getStatistics();
|
||||
return ApiResponse.success(statistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出评价数据到Excel
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
public void exportReviews(VideoReviewListReqDTO reqDTO, HttpServletResponse response) {
|
||||
log.info("导出视频评价数据");
|
||||
|
||||
try {
|
||||
// 设置响应头
|
||||
String fileName = "video_reviews_" + System.currentTimeMillis() + ".xlsx";
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
|
||||
|
||||
// 导出数据
|
||||
videoReviewService.exportReviews(reqDTO, response.getOutputStream());
|
||||
|
||||
response.getOutputStream().flush();
|
||||
} catch (IOException e) {
|
||||
log.error("导出视频评价数据失败", e);
|
||||
throw new RuntimeException("导出失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/task/v1")
|
||||
@Deprecated
|
||||
// 任务列表管理
|
||||
public class TaskController {
|
||||
|
||||
|
||||
@@ -40,4 +40,16 @@ public class VideoController {
|
||||
return videoService.getById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询视频是否被购买
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 是否已购买
|
||||
*/
|
||||
@GetMapping("/checkBuyStatus")
|
||||
public ApiResponse<Boolean> checkBuyStatus(@RequestParam("videoId") Long videoId) {
|
||||
Boolean isBuy = videoService.checkVideoBuyStatus(videoId);
|
||||
return ApiResponse.success(isBuy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
70
src/main/java/com/ycwl/basic/handler/MapTypeHandler.java
Normal file
70
src/main/java/com/ycwl/basic/handler/MapTypeHandler.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package com.ycwl.basic.handler;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Map类型的TypeHandler,用于处理JSON字段与Map<String, Integer>的互转
|
||||
* 主要用于机位快速评价等自由格式的JSON存储
|
||||
*/
|
||||
@Slf4j
|
||||
public class MapTypeHandler extends BaseTypeHandler<Map<String, Integer>> {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final TypeReference<Map<String, Integer>> typeReference = new TypeReference<Map<String, Integer>>() {};
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, Map<String, Integer> parameter, JdbcType jdbcType) throws SQLException {
|
||||
try {
|
||||
String json = objectMapper.writeValueAsString(parameter);
|
||||
ps.setString(i, json);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("序列化Map为JSON失败", e);
|
||||
throw new SQLException("序列化Map为JSON失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Integer> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String json = rs.getString(columnName);
|
||||
return parseJson(json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Integer> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String json = rs.getString(columnIndex);
|
||||
return parseJson(json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Integer> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String json = cs.getString(columnIndex);
|
||||
return parseJson(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析JSON字符串为Map
|
||||
*/
|
||||
private Map<String, Integer> parseJson(String json) {
|
||||
if (json == null || json.trim().isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, typeReference);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("解析JSON为Map失败, json={}", json, e);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,4 +58,11 @@ public interface VideoMapper {
|
||||
int deleteUselessVideo();
|
||||
|
||||
int updateMemberIdByFaceId(Long faceId, Long memberId);
|
||||
|
||||
/**
|
||||
* 查询指定视频是否存在已购买记录
|
||||
* @param videoId 视频ID
|
||||
* @return 已购买记录数量
|
||||
*/
|
||||
int countBuyRecordByVideoId(Long videoId);
|
||||
}
|
||||
|
||||
71
src/main/java/com/ycwl/basic/mapper/VideoReviewMapper.java
Normal file
71
src/main/java/com/ycwl/basic/mapper/VideoReviewMapper.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.ycwl.basic.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
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 org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 视频评价Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询评价列表(带关联查询)
|
||||
*
|
||||
* @param reqDTO 查询条件
|
||||
* @return 评价列表
|
||||
*/
|
||||
List<VideoReviewRespDTO> selectReviewList(VideoReviewListReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 统计总评价数
|
||||
*
|
||||
* @return 总数
|
||||
*/
|
||||
Long countTotal();
|
||||
|
||||
/**
|
||||
* 计算平均评分
|
||||
*
|
||||
* @return 平均评分
|
||||
*/
|
||||
Double calculateAverageRating();
|
||||
|
||||
/**
|
||||
* 统计评分分布
|
||||
*
|
||||
* @return key: 评分, value: 数量
|
||||
*/
|
||||
List<Map<String, Object>> countRatingDistribution();
|
||||
|
||||
/**
|
||||
* 统计最近N天的评价趋势
|
||||
*
|
||||
* @param days 天数
|
||||
* @return key: 日期, value: 数量
|
||||
*/
|
||||
List<Map<String, Object>> countRecentTrend(@Param("days") int days);
|
||||
|
||||
/**
|
||||
* 统计景区评价排行(前N名)
|
||||
*
|
||||
* @param limit 限制数量
|
||||
* @return 景区排行列表
|
||||
*/
|
||||
List<VideoReviewStatisticsRespDTO.ScenicReviewRank> countScenicRank(@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 查询所有机位评价数据(用于后端计算平均值)
|
||||
*
|
||||
* @return 机位评价列表
|
||||
*/
|
||||
List<Map<String, Integer>> selectAllCameraPositionRatings();
|
||||
}
|
||||
@@ -45,6 +45,10 @@ public class VideoRespVO {
|
||||
*/
|
||||
// 任务id
|
||||
private Long taskId;
|
||||
/**
|
||||
* 任务参数,JSON字符串
|
||||
*/
|
||||
private String taskParams;
|
||||
/**
|
||||
* 执行任务的机器ID,render_worker.id
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ycwl.basic.model.pc.videoreview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 新增视频评价请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class VideoReviewAddReqDTO {
|
||||
|
||||
/**
|
||||
* 视频ID(必填)
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 购买评分 1-5(必填)
|
||||
*/
|
||||
private Integer rating;
|
||||
|
||||
/**
|
||||
* 文字评价内容(可选)
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 机位快速评价JSON(可选)
|
||||
* 格式: {"角度":5,"清晰度":4,"构图":5}
|
||||
*/
|
||||
private Map<String, Integer> cameraPositionRating;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.ycwl.basic.model.pc.videoreview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 视频评价列表查询请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class VideoReviewListReqDTO {
|
||||
|
||||
/**
|
||||
* 视频ID(可选)
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 景区ID(可选)
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 评价人ID(可选)
|
||||
*/
|
||||
private Long creator;
|
||||
|
||||
/**
|
||||
* 评分(可选,精确匹配)
|
||||
*/
|
||||
private Integer rating;
|
||||
|
||||
/**
|
||||
* 最小评分(可选,范围查询)
|
||||
*/
|
||||
private Integer minRating;
|
||||
|
||||
/**
|
||||
* 最大评分(可选,范围查询)
|
||||
*/
|
||||
private Integer maxRating;
|
||||
|
||||
/**
|
||||
* 开始时间(可选,格式: yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
private String startTime;
|
||||
|
||||
/**
|
||||
* 结束时间(可选,格式: yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
private String endTime;
|
||||
|
||||
/**
|
||||
* 关键词搜索(可选,搜索评价内容)
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 页码(必填,默认1)
|
||||
*/
|
||||
private Integer pageNum = 1;
|
||||
|
||||
/**
|
||||
* 每页数量(必填,默认10)
|
||||
*/
|
||||
private Integer pageSize = 10;
|
||||
|
||||
/**
|
||||
* 排序字段(可选,默认create_time)
|
||||
* 可选值: create_time, rating, update_time
|
||||
*/
|
||||
private String orderBy = "create_time";
|
||||
|
||||
/**
|
||||
* 排序方向(可选,默认DESC)
|
||||
* 可选值: ASC, DESC
|
||||
*/
|
||||
private String orderDirection = "DESC";
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.ycwl.basic.model.pc.videoreview.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 视频评价详情响应DTO
|
||||
*/
|
||||
@Data
|
||||
public class VideoReviewRespDTO {
|
||||
|
||||
/**
|
||||
* 评价ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 视频URL(关联查询)
|
||||
*/
|
||||
private String videoUrl;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 景区名称(关联查询)
|
||||
*/
|
||||
private String scenicName;
|
||||
|
||||
/**
|
||||
* 评价人ID(管理员ID)
|
||||
*/
|
||||
private Long creator;
|
||||
|
||||
/**
|
||||
* 评价人名称(关联查询)
|
||||
*/
|
||||
private String creatorName;
|
||||
|
||||
/**
|
||||
* 购买评分 1-5
|
||||
*/
|
||||
private Integer rating;
|
||||
|
||||
/**
|
||||
* 文字评价内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 机位快速评价JSON
|
||||
*/
|
||||
private Map<String, Integer> cameraPositionRating;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ycwl.basic.model.pc.videoreview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 视频评价统计分析响应DTO
|
||||
*/
|
||||
@Data
|
||||
public class VideoReviewStatisticsRespDTO {
|
||||
|
||||
/**
|
||||
* 总评价数
|
||||
*/
|
||||
private Long totalCount;
|
||||
|
||||
/**
|
||||
* 平均评分
|
||||
*/
|
||||
private BigDecimal averageRating;
|
||||
|
||||
/**
|
||||
* 评分分布
|
||||
* key: 评分(1-5), value: 该评分的评价数量
|
||||
*/
|
||||
private Map<Integer, Long> ratingDistribution;
|
||||
|
||||
/**
|
||||
* 最近7天评价趋势
|
||||
* key: 日期(yyyy-MM-dd), value: 该日期的评价数量
|
||||
*/
|
||||
private Map<String, Long> recentTrend;
|
||||
|
||||
/**
|
||||
* 景区评价排行(前10)
|
||||
*/
|
||||
private List<ScenicReviewRank> scenicRankList;
|
||||
|
||||
/**
|
||||
* 机位评价维度统计
|
||||
* key: 维度名称, value: 平均分
|
||||
*/
|
||||
private Map<String, BigDecimal> cameraPositionAverage;
|
||||
|
||||
/**
|
||||
* 景区评价排行内部类
|
||||
*/
|
||||
@Data
|
||||
public static class ScenicReviewRank {
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 景区名称
|
||||
*/
|
||||
private String scenicName;
|
||||
|
||||
/**
|
||||
* 评价数量
|
||||
*/
|
||||
private Long reviewCount;
|
||||
|
||||
/**
|
||||
* 平均评分
|
||||
*/
|
||||
private BigDecimal averageRating;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.ycwl.basic.model.pc.videoreview.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.ycwl.basic.handler.MapTypeHandler;
|
||||
import lombok.Data;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 视频评价实体类
|
||||
*/
|
||||
@Data
|
||||
@TableName("video_review")
|
||||
public class VideoReviewEntity {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 评价人ID(管理员ID)
|
||||
*/
|
||||
private Long creator;
|
||||
|
||||
/**
|
||||
* 购买评分 1-5
|
||||
*/
|
||||
private Integer rating;
|
||||
|
||||
/**
|
||||
* 文字评价内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 机位快速评价JSON
|
||||
* 格式: {"角度":5,"清晰度":4,"构图":5}
|
||||
*/
|
||||
@TableField(typeHandler = MapTypeHandler.class, jdbcType = JdbcType.VARCHAR)
|
||||
private Map<String, Integer> cameraPositionRating;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -77,9 +77,11 @@
|
||||
</delete>
|
||||
<select id="list" resultType="com.ycwl.basic.model.pc.video.resp.VideoRespVO">
|
||||
select v.id, v.scenic_id, template_id, task_id, worker_id, video_url, v.create_time, v.update_time,
|
||||
t.name templateName, t.price templatePrice,t.cover_url templateCoverUrl
|
||||
t.name templateName, t.price templatePrice,t.cover_url templateCoverUrl,
|
||||
tk.task_params taskParams
|
||||
from video v
|
||||
left join template t on v.template_id = t.id
|
||||
left join task tk on v.task_id = tk.id
|
||||
<where>
|
||||
<if test="scenicId!= null">and v.scenic_id = #{scenicId} </if>
|
||||
<if test="templateId!= null">and template_id = #{templateId} </if>
|
||||
@@ -98,9 +100,11 @@
|
||||
<select id="getById" resultType="com.ycwl.basic.model.pc.video.resp.VideoRespVO">
|
||||
select v.id, v.scenic_id, template_id, task_id, worker_id, video_url, v.create_time, v.update_time,
|
||||
t.name templateName,t.price templatePrice, t.cover_url templateCoverUrl, t.slash_price slashPrice,
|
||||
v.height, v.width, v.duration
|
||||
v.height, v.width, v.duration,
|
||||
tk.task_params taskParams
|
||||
from video v
|
||||
left join template t on v.template_id = t.id
|
||||
left join task tk on v.task_id = tk.id
|
||||
where v.id = #{id}
|
||||
</select>
|
||||
<select id="findByTaskId" resultType="com.ycwl.basic.model.pc.video.entity.VideoEntity">
|
||||
@@ -108,10 +112,12 @@
|
||||
</select>
|
||||
<select id="queryByRelation" resultType="com.ycwl.basic.model.pc.video.resp.VideoRespVO">
|
||||
select v.id, mv.scenic_id, v.template_id, mv.task_id, mv.face_id, worker_id, video_url, v.create_time, v.update_time,
|
||||
t.name templateName, t.price templatePrice,t.cover_url templateCoverUrl, mv.is_buy
|
||||
t.name templateName, t.price templatePrice,t.cover_url templateCoverUrl, mv.is_buy,
|
||||
tk.task_params taskParams
|
||||
from member_video mv
|
||||
left join video v on mv.video_id = v.id
|
||||
left join template t on mv.template_id = t.id
|
||||
left join task tk on mv.task_id = tk.id
|
||||
<where>
|
||||
<if test="scenicId!= null">and mv.scenic_id = #{scenicId} </if>
|
||||
<if test="memberId!= null">and mv.member_id = #{memberId} </if>
|
||||
@@ -175,4 +181,11 @@
|
||||
set member_id = #{memberId}
|
||||
where face_id = #{faceId}
|
||||
</update>
|
||||
|
||||
<!-- 查询指定视频是否存在已购买记录 -->
|
||||
<select id="countBuyRecordByVideoId" resultType="int">
|
||||
select count(*)
|
||||
from member_video
|
||||
where video_id = #{videoId} and is_buy = 1
|
||||
</select>
|
||||
</mapper>
|
||||
145
src/main/resources/mapper/VideoReviewMapper.xml
Normal file
145
src/main/resources/mapper/VideoReviewMapper.xml
Normal file
@@ -0,0 +1,145 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ycwl.basic.mapper.VideoReviewMapper">
|
||||
|
||||
<!-- 结果映射 -->
|
||||
<resultMap id="VideoReviewRespMap" type="com.ycwl.basic.model.pc.videoreview.dto.VideoReviewRespDTO">
|
||||
<id property="id" column="id"/>
|
||||
<result property="videoId" column="video_id"/>
|
||||
<result property="videoUrl" column="video_url"/>
|
||||
<result property="scenicId" column="scenic_id"/>
|
||||
<result property="scenicName" column="scenic_name"/>
|
||||
<result property="creator" column="creator"/>
|
||||
<result property="creatorName" column="creator_name"/>
|
||||
<result property="rating" column="rating"/>
|
||||
<result property="content" column="content"/>
|
||||
<result property="cameraPositionRating" column="camera_position_rating"
|
||||
typeHandler="com.ycwl.basic.handler.MapTypeHandler"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 分页查询评价列表 -->
|
||||
<select id="selectReviewList" parameterType="com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO"
|
||||
resultMap="VideoReviewRespMap">
|
||||
SELECT
|
||||
vr.id,
|
||||
vr.video_id,
|
||||
vr.scenic_id,
|
||||
vr.creator,
|
||||
vr.rating,
|
||||
vr.content,
|
||||
vr.camera_position_rating,
|
||||
vr.create_time,
|
||||
vr.update_time,
|
||||
v.video_url,
|
||||
s.name AS scenic_name,
|
||||
u.name AS creator_name
|
||||
FROM video_review vr
|
||||
LEFT JOIN video v ON vr.video_id = v.id
|
||||
LEFT JOIN scenic s ON vr.scenic_id = s.id
|
||||
LEFT JOIN sys_user u ON vr.creator = u.id
|
||||
<where>
|
||||
<if test="videoId != null">
|
||||
AND vr.video_id = #{videoId}
|
||||
</if>
|
||||
<if test="scenicId != null">
|
||||
AND vr.scenic_id = #{scenicId}
|
||||
</if>
|
||||
<if test="creator != null">
|
||||
AND vr.creator = #{creator}
|
||||
</if>
|
||||
<if test="rating != null">
|
||||
AND vr.rating = #{rating}
|
||||
</if>
|
||||
<if test="minRating != null">
|
||||
AND vr.rating >= #{minRating}
|
||||
</if>
|
||||
<if test="maxRating != null">
|
||||
AND vr.rating <= #{maxRating}
|
||||
</if>
|
||||
<if test="startTime != null and startTime != ''">
|
||||
AND vr.create_time >= #{startTime}
|
||||
</if>
|
||||
<if test="endTime != null and endTime != ''">
|
||||
AND vr.create_time <= #{endTime}
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND vr.content LIKE CONCAT('%', #{keyword}, '%')
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY
|
||||
<choose>
|
||||
<when test="orderBy == 'rating'">
|
||||
vr.rating
|
||||
</when>
|
||||
<when test="orderBy == 'update_time'">
|
||||
vr.update_time
|
||||
</when>
|
||||
<otherwise>
|
||||
vr.create_time
|
||||
</otherwise>
|
||||
</choose>
|
||||
<choose>
|
||||
<when test="orderDirection == 'ASC'">
|
||||
ASC
|
||||
</when>
|
||||
<otherwise>
|
||||
DESC
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
|
||||
<!-- 统计总评价数 -->
|
||||
<select id="countTotal" resultType="java.lang.Long">
|
||||
SELECT COUNT(*) FROM video_review
|
||||
</select>
|
||||
|
||||
<!-- 计算平均评分 -->
|
||||
<select id="calculateAverageRating" resultType="java.lang.Double">
|
||||
SELECT AVG(rating) FROM video_review
|
||||
</select>
|
||||
|
||||
<!-- 统计评分分布 -->
|
||||
<select id="countRatingDistribution" resultType="java.util.Map">
|
||||
SELECT
|
||||
rating AS ratingValue,
|
||||
COUNT(*) AS count
|
||||
FROM video_review
|
||||
GROUP BY rating
|
||||
ORDER BY rating
|
||||
</select>
|
||||
|
||||
<!-- 统计最近N天的评价趋势 -->
|
||||
<select id="countRecentTrend" resultType="java.util.Map">
|
||||
SELECT
|
||||
DATE_FORMAT(create_time, '%Y-%m-%d') AS dateStr,
|
||||
COUNT(*) AS count
|
||||
FROM video_review
|
||||
WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)
|
||||
GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')
|
||||
ORDER BY dateStr
|
||||
</select>
|
||||
|
||||
<!-- 统计景区评价排行 -->
|
||||
<select id="countScenicRank" resultType="com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO$ScenicReviewRank">
|
||||
SELECT
|
||||
vr.scenic_id AS scenicId,
|
||||
s.name AS scenicName,
|
||||
COUNT(*) AS reviewCount,
|
||||
AVG(vr.rating) AS averageRating
|
||||
FROM video_review vr
|
||||
LEFT JOIN scenic s ON vr.scenic_id = s.id
|
||||
GROUP BY vr.scenic_id, s.name
|
||||
ORDER BY reviewCount DESC, averageRating DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 查询所有机位评价数据 -->
|
||||
<select id="selectAllCameraPositionRatings" resultType="java.util.Map">
|
||||
SELECT camera_position_rating
|
||||
FROM video_review
|
||||
WHERE camera_position_rating IS NOT NULL AND camera_position_rating != ''
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user