diff --git a/src/main/java/com/ycwl/basic/controller/VideoReviewController.java b/src/main/java/com/ycwl/basic/controller/VideoReviewController.java new file mode 100644 index 00000000..40c2eadd --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/VideoReviewController.java @@ -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 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> getReviewList(VideoReviewListReqDTO reqDTO) { + log.info("查询视频评价列表,pageNum: {}, pageSize: {}", reqDTO.getPageNum(), reqDTO.getPageSize()); + PageInfo pageInfo = videoReviewService.getReviewList(reqDTO); + return ApiResponse.success(pageInfo); + } + + /** + * 获取评价统计数据 + * + * @return 统计结果 + */ + @GetMapping("/statistics") + public ApiResponse 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()); + } + } +} diff --git a/src/main/java/com/ycwl/basic/controller/pc/TaskController.java b/src/main/java/com/ycwl/basic/controller/pc/TaskController.java index 2009c6fd..a6fc81c9 100644 --- a/src/main/java/com/ycwl/basic/controller/pc/TaskController.java +++ b/src/main/java/com/ycwl/basic/controller/pc/TaskController.java @@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.*; */ @RestController @RequestMapping("/api/task/v1") -@Deprecated // 任务列表管理 public class TaskController { diff --git a/src/main/java/com/ycwl/basic/controller/pc/VideoController.java b/src/main/java/com/ycwl/basic/controller/pc/VideoController.java index c3f36ec7..add899d4 100644 --- a/src/main/java/com/ycwl/basic/controller/pc/VideoController.java +++ b/src/main/java/com/ycwl/basic/controller/pc/VideoController.java @@ -40,4 +40,16 @@ public class VideoController { return videoService.getById(id); } + /** + * 查询视频是否被购买 + * + * @param videoId 视频ID + * @return 是否已购买 + */ + @GetMapping("/checkBuyStatus") + public ApiResponse checkBuyStatus(@RequestParam("videoId") Long videoId) { + Boolean isBuy = videoService.checkVideoBuyStatus(videoId); + return ApiResponse.success(isBuy); + } + } diff --git a/src/main/java/com/ycwl/basic/handler/MapTypeHandler.java b/src/main/java/com/ycwl/basic/handler/MapTypeHandler.java new file mode 100644 index 00000000..bb313afa --- /dev/null +++ b/src/main/java/com/ycwl/basic/handler/MapTypeHandler.java @@ -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的互转 + * 主要用于机位快速评价等自由格式的JSON存储 + */ +@Slf4j +public class MapTypeHandler extends BaseTypeHandler> { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final TypeReference> typeReference = new TypeReference>() {}; + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Map 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 getNullableResult(ResultSet rs, String columnName) throws SQLException { + String json = rs.getString(columnName); + return parseJson(json); + } + + @Override + public Map getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String json = rs.getString(columnIndex); + return parseJson(json); + } + + @Override + public Map getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String json = cs.getString(columnIndex); + return parseJson(json); + } + + /** + * 解析JSON字符串为Map + */ + private Map 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<>(); + } + } +} diff --git a/src/main/java/com/ycwl/basic/mapper/VideoMapper.java b/src/main/java/com/ycwl/basic/mapper/VideoMapper.java index faecf379..2216e63d 100644 --- a/src/main/java/com/ycwl/basic/mapper/VideoMapper.java +++ b/src/main/java/com/ycwl/basic/mapper/VideoMapper.java @@ -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); } diff --git a/src/main/java/com/ycwl/basic/mapper/VideoReviewMapper.java b/src/main/java/com/ycwl/basic/mapper/VideoReviewMapper.java new file mode 100644 index 00000000..16c148f6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/mapper/VideoReviewMapper.java @@ -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 { + + /** + * 分页查询评价列表(带关联查询) + * + * @param reqDTO 查询条件 + * @return 评价列表 + */ + List selectReviewList(VideoReviewListReqDTO reqDTO); + + /** + * 统计总评价数 + * + * @return 总数 + */ + Long countTotal(); + + /** + * 计算平均评分 + * + * @return 平均评分 + */ + Double calculateAverageRating(); + + /** + * 统计评分分布 + * + * @return key: 评分, value: 数量 + */ + List> countRatingDistribution(); + + /** + * 统计最近N天的评价趋势 + * + * @param days 天数 + * @return key: 日期, value: 数量 + */ + List> countRecentTrend(@Param("days") int days); + + /** + * 统计景区评价排行(前N名) + * + * @param limit 限制数量 + * @return 景区排行列表 + */ + List countScenicRank(@Param("limit") int limit); + + /** + * 查询所有机位评价数据(用于后端计算平均值) + * + * @return 机位评价列表 + */ + List> selectAllCameraPositionRatings(); +} diff --git a/src/main/java/com/ycwl/basic/model/pc/video/resp/VideoRespVO.java b/src/main/java/com/ycwl/basic/model/pc/video/resp/VideoRespVO.java index 3a97fd80..860b81ac 100644 --- a/src/main/java/com/ycwl/basic/model/pc/video/resp/VideoRespVO.java +++ b/src/main/java/com/ycwl/basic/model/pc/video/resp/VideoRespVO.java @@ -45,6 +45,10 @@ public class VideoRespVO { */ // 任务id private Long taskId; + /** + * 任务参数,JSON字符串 + */ + private String taskParams; /** * 执行任务的机器ID,render_worker.id */ diff --git a/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewAddReqDTO.java b/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewAddReqDTO.java new file mode 100644 index 00000000..ace12246 --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewAddReqDTO.java @@ -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 cameraPositionRating; +} diff --git a/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewListReqDTO.java b/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewListReqDTO.java new file mode 100644 index 00000000..6dc0e6d2 --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewListReqDTO.java @@ -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"; +} diff --git a/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewRespDTO.java b/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewRespDTO.java new file mode 100644 index 00000000..b336ca3c --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewRespDTO.java @@ -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 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; +} diff --git a/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewStatisticsRespDTO.java b/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewStatisticsRespDTO.java new file mode 100644 index 00000000..fb36dc7f --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/pc/videoreview/dto/VideoReviewStatisticsRespDTO.java @@ -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 ratingDistribution; + + /** + * 最近7天评价趋势 + * key: 日期(yyyy-MM-dd), value: 该日期的评价数量 + */ + private Map recentTrend; + + /** + * 景区评价排行(前10) + */ + private List scenicRankList; + + /** + * 机位评价维度统计 + * key: 维度名称, value: 平均分 + */ + private Map cameraPositionAverage; + + /** + * 景区评价排行内部类 + */ + @Data + public static class ScenicReviewRank { + /** + * 景区ID + */ + private Long scenicId; + + /** + * 景区名称 + */ + private String scenicName; + + /** + * 评价数量 + */ + private Long reviewCount; + + /** + * 平均评分 + */ + private BigDecimal averageRating; + } +} diff --git a/src/main/java/com/ycwl/basic/model/pc/videoreview/entity/VideoReviewEntity.java b/src/main/java/com/ycwl/basic/model/pc/videoreview/entity/VideoReviewEntity.java new file mode 100644 index 00000000..36480ada --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/pc/videoreview/entity/VideoReviewEntity.java @@ -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 cameraPositionRating; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; +} diff --git a/src/main/java/com/ycwl/basic/service/VideoReviewService.java b/src/main/java/com/ycwl/basic/service/VideoReviewService.java new file mode 100644 index 00000000..7f96942f --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/VideoReviewService.java @@ -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 getReviewList(VideoReviewListReqDTO reqDTO); + + /** + * 获取评价统计数据 + * + * @return 统计结果 + */ + VideoReviewStatisticsRespDTO getStatistics(); + + /** + * 导出评价数据到Excel + * + * @param reqDTO 查询条件 + * @param outputStream 输出流 + * @throws IOException IO异常 + */ + void exportReviews(VideoReviewListReqDTO reqDTO, OutputStream outputStream) throws IOException; +} diff --git a/src/main/java/com/ycwl/basic/service/impl/VideoReviewServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/VideoReviewServiceImpl.java new file mode 100644 index 00000000..7fc17505 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/impl/VideoReviewServiceImpl.java @@ -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 getReviewList(VideoReviewListReqDTO reqDTO) { + // 设置分页参数 + PageHelper.startPage(reqDTO.getPageNum(), reqDTO.getPageSize()); + + // 查询列表 + List 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> ratingDistList = videoReviewMapper.countRatingDistribution(); + Map ratingDistribution = new HashMap<>(); + for (Map item : ratingDistList) { + Integer rating = (Integer) item.get("ratingValue"); + Long count = (Long) item.get("count"); + ratingDistribution.put(rating, count); + } + statistics.setRatingDistribution(ratingDistribution); + + // 4. 最近7天趋势 + List> trendList = videoReviewMapper.countRecentTrend(7); + Map recentTrend = new LinkedHashMap<>(); + for (Map item : trendList) { + String date = (String) item.get("dateStr"); + Long count = (Long) item.get("count"); + recentTrend.put(date, count); + } + statistics.setRecentTrend(recentTrend); + + // 5. 景区排行(前10) + List scenicRankList = videoReviewMapper.countScenicRank(10); + statistics.setScenicRankList(scenicRankList); + + // 6. 机位评价维度平均值 + Map 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 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 calculateCameraPositionAverage() { + List> allRatings = videoReviewMapper.selectAllCameraPositionRatings(); + + if (allRatings == null || allRatings.isEmpty()) { + return new HashMap<>(); + } + + // 统计各维度的总分和次数 + Map> dimensionScores = new HashMap<>(); + for (Map rating : allRatings) { + if (rating == null) continue; + for (Map.Entry entry : rating.entrySet()) { + dimensionScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue()); + } + } + + // 计算平均值 + Map result = new HashMap<>(); + for (Map.Entry> 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; + } +} diff --git a/src/main/java/com/ycwl/basic/service/pc/VideoService.java b/src/main/java/com/ycwl/basic/service/pc/VideoService.java index f06afd1d..00d325e5 100644 --- a/src/main/java/com/ycwl/basic/service/pc/VideoService.java +++ b/src/main/java/com/ycwl/basic/service/pc/VideoService.java @@ -17,5 +17,12 @@ public interface VideoService { ApiResponse> list(VideoReqQuery videoReqQuery); ApiResponse getById(Long id); + /** + * 查询视频是否被购买 + * + * @param videoId 视频ID + * @return 是否已购买 (true-已购买, false-未购买) + */ + Boolean checkVideoBuyStatus(Long videoId); } diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/VideoServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/VideoServiceImpl.java index 78c6d65b..f643ad99 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/VideoServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/VideoServiceImpl.java @@ -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; + } + } diff --git a/src/main/resources/mapper/VideoMapper.xml b/src/main/resources/mapper/VideoMapper.xml index c1713665..68a34066 100644 --- a/src/main/resources/mapper/VideoMapper.xml +++ b/src/main/resources/mapper/VideoMapper.xml @@ -77,9 +77,11 @@ 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 count(*) + from member_video + where video_id = #{videoId} and is_buy = 1 + \ No newline at end of file diff --git a/src/main/resources/mapper/VideoReviewMapper.xml b/src/main/resources/mapper/VideoReviewMapper.xml new file mode 100644 index 00000000..a88eb0cf --- /dev/null +++ b/src/main/resources/mapper/VideoReviewMapper.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +