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<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,13 @@ public interface VideoMapper {
|
||||
int deleteNotBuyFaceRelations(Long userId, Long faceId);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user