feat(video): 完善视频评价功能,增加问题机位和标签管理

- 新增VideoReviewSourceEnum枚举,定义评价来源类型(订单、渲染)
- 添加LongListTypeHandler和StringListTypeHandler,处理数据库JSON字段与Java列表转换
- 修改VideoReviewEntity实体类,将机位评价改为问题机位ID列表和问题标签列表
- 创建AdminVideoReviewLogReqDTO和AdminVideoReviewLogRespDTO,实现管理后台评价日志查询
- 在VideoReviewController中增加管理后台分页查询评价日志接口
- 更新视频评价添加逻辑,验证来源参数并记录问题机位和标签信息
- 修改
This commit is contained in:
2026-01-27 21:28:33 +08:00
parent 1c0a506238
commit 93744510ec
15 changed files with 826 additions and 112 deletions

View File

@@ -1,6 +1,8 @@
package com.ycwl.basic.controller; package com.ycwl.basic.controller;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
@@ -55,6 +57,20 @@ public class VideoReviewController {
return ApiResponse.success(pageInfo); return ApiResponse.success(pageInfo);
} }
/**
* 管理后台分页查询评价日志
* 提供更详细的管理信息,包括评价人账号、机位评价统计等
*
* @param reqDTO 查询条件
* @return 分页结果
*/
@GetMapping("/admin/logs")
public ApiResponse<PageInfo<AdminVideoReviewLogRespDTO>> getAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO) {
log.info("管理后台查询评价日志,pageNum: {}, pageSize: {}", reqDTO.getPageNum(), reqDTO.getPageSize());
PageInfo<AdminVideoReviewLogRespDTO> pageInfo = videoReviewService.getAdminReviewLogList(reqDTO);
return ApiResponse.success(pageInfo);
}
/** /**
* 获取评价统计数据 * 获取评价统计数据
* *

View File

@@ -0,0 +1,63 @@
package com.ycwl.basic.enums;
import lombok.Getter;
/**
* 视频评价来源枚举
*/
@Getter
public enum VideoReviewSourceEnum {
/**
* 订单
*/
ORDER("ORDER", "订单"),
/**
* 渲染
*/
RENDER("RENDER", "渲染");
/**
* 枚举代码
*/
private final String code;
/**
* 枚举描述
*/
private final String description;
VideoReviewSourceEnum(String code, String description) {
this.code = code;
this.description = description;
}
/**
* 根据code获取枚举
*
* @param code 枚举代码
* @return 枚举对象,不存在则返回null
*/
public static VideoReviewSourceEnum fromCode(String code) {
if (code == null || code.isEmpty()) {
return null;
}
for (VideoReviewSourceEnum value : VideoReviewSourceEnum.values()) {
if (value.getCode().equals(code)) {
return value;
}
}
return null;
}
/**
* 验证code是否有效
*
* @param code 枚举代码
* @return true-有效, false-无效
*/
public static boolean isValid(String code) {
return fromCode(code) != null;
}
}

View File

@@ -0,0 +1,80 @@
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 org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Long类型列表的TypeHandler
* 用于处理数据库JSON字段与Java List<Long>之间的转换
*/
@Slf4j
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class LongListTypeHandler extends BaseTypeHandler<List<Long>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final TypeReference<List<Long>> typeReference = new TypeReference<List<Long>>() {};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<Long> parameter, JdbcType jdbcType) throws SQLException {
try {
String json = objectMapper.writeValueAsString(parameter);
ps.setString(i, json);
log.debug("序列化Long列表: {}", json);
} catch (JsonProcessingException e) {
log.error("序列化Long列表失败", e);
throw new SQLException("序列化Long列表失败", e);
}
}
@Override
public List<Long> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json, columnName);
}
@Override
public List<Long> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json, "columnIndex:" + columnIndex);
}
@Override
public List<Long> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json, "columnIndex:" + columnIndex);
}
private List<Long> parseJson(String json, String source) {
if (json == null || json.trim().isEmpty() || "null".equals(json)) {
log.debug("从{}获取的JSON为空,返回空列表", source);
return new ArrayList<>();
}
try {
List<Long> result = objectMapper.readValue(json, typeReference);
if (result == null) {
log.debug("从{}反序列化得到null,返回空列表", source);
return new ArrayList<>();
}
log.debug("从{}反序列化Long列表成功,数量: {}", source, result.size());
return result;
} catch (JsonProcessingException e) {
log.error("从{}反序列化Long列表失败,JSON: {}", source, json, e);
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,74 @@
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 org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* String列表类型处理器
* 用于将数据库中的JSON字符串转换为Java的List<String>对象
*/
@Slf4j
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class StringListTypeHandler extends BaseTypeHandler<List<String>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final TypeReference<List<String>> typeReference = new TypeReference<List<String>>() {};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
try {
String json = objectMapper.writeValueAsString(parameter);
ps.setString(i, json);
log.debug("序列化String列表: {}", json);
} catch (JsonProcessingException e) {
log.error("序列化String列表失败", e);
throw new SQLException("序列化String列表失败", e);
}
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseJson(rs.getString(columnName));
}
@Override
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseJson(rs.getString(columnIndex));
}
@Override
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseJson(cs.getString(columnIndex));
}
/**
* 解析JSON字符串为List<String>
*/
private List<String> parseJson(String json) {
if (json == null || json.isEmpty() || "null".equals(json)) {
return new ArrayList<>();
}
try {
List<String> result = objectMapper.readValue(json, typeReference);
log.debug("反序列化String列表: {}", result);
return result;
} catch (JsonProcessingException e) {
log.error("反序列化String列表失败: {}", json, e);
return new ArrayList<>();
}
}
}

View File

@@ -1,6 +1,8 @@
package com.ycwl.basic.mapper; package com.ycwl.basic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewListReqDTO; 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.VideoReviewRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO;
@@ -25,6 +27,14 @@ public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
*/ */
List<VideoReviewRespDTO> selectReviewList(VideoReviewListReqDTO reqDTO); List<VideoReviewRespDTO> selectReviewList(VideoReviewListReqDTO reqDTO);
/**
* 管理后台分页查询评价日志(带更详细的管理信息)
*
* @param reqDTO 查询条件
* @return 评价日志列表
*/
List<AdminVideoReviewLogRespDTO> selectAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO);
/** /**
* 统计总评价数 * 统计总评价数
* *
@@ -63,9 +73,9 @@ public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
List<VideoReviewStatisticsRespDTO.ScenicReviewRank> countScenicRank(@Param("limit") int limit); List<VideoReviewStatisticsRespDTO.ScenicReviewRank> countScenicRank(@Param("limit") int limit);
/** /**
* 查询所有机位评价数据(用于后端计算平均值) * 查询所有问题机位ID列表(用于后端统计问题机位)
* *
* @return 机位评价列表(Map结构: 机位ID -> 评分) * @return 问题机位ID列表
*/ */
List<Map<String, Integer>> selectAllCameraPositionRatings(); List<List<Long>> selectAllProblemDeviceIds();
} }

View File

@@ -0,0 +1,123 @@
package com.ycwl.basic.model.pc.videoreview.dto;
import lombok.Data;
/**
* 管理后台视频评价日志查询请求DTO
*/
@Data
public class AdminVideoReviewLogReqDTO {
/**
* 评价ID(可选,精确查询)
*/
private Long id;
/**
* 视频ID(可选)
*/
private Long videoId;
/**
* 景区ID(可选)
*/
private Long scenicId;
/**
* 评价人ID(可选)
*/
private Long creator;
/**
* 评价人名称(可选,模糊查询)
*/
private String creatorName;
/**
* 评分(可选,精确匹配)
*/
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;
/**
* 模板ID(可选)
*/
private Long templateId;
/**
* 模板名称(可选,模糊查询)
*/
private String templateName;
/**
* 是否有机位评价(可选)
* true: 仅查询有机位评价的记录
* false: 仅查询无机位评价的记录
* null: 不限制
*/
private Boolean hasCameraRating;
/**
* 问题机位ID(可选,筛选包含该机位ID的评价)
* 任意一个问题机位匹配即可
*/
private Long problemDeviceId;
/**
* 问题标签(可选,筛选包含该标签的评价)
* 任意一个标签匹配即可
*/
private String problemTag;
/**
* 来源(可选,筛选指定来源的评价)
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 页码(必填,默认1)
*/
private Integer pageNum = 1;
/**
* 每页数量(必填,默认20)
*/
private Integer pageSize = 20;
/**
* 排序字段(可选,默认create_time)
* 可选值: create_time, rating, update_time, id
*/
private String orderBy = "create_time";
/**
* 排序方向(可选,默认DESC)
* 可选值: ASC, DESC
*/
private String orderDirection = "DESC";
}

View File

@@ -0,0 +1,120 @@
package com.ycwl.basic.model.pc.videoreview.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* 管理后台视频评价日志响应DTO
*/
@Data
public class AdminVideoReviewLogRespDTO {
/**
* 评价ID
*/
private Long id;
/**
* 视频ID
*/
private Long videoId;
/**
* 视频URL(关联查询)
*/
private String videoUrl;
/**
* 模板ID(关联查询video表)
*/
private Long templateId;
/**
* 模板名称(关联查询)
*/
private String templateName;
/**
* 景区ID
*/
private Long scenicId;
/**
* 景区名称(关联查询)
*/
private String scenicName;
/**
* 评价人ID(管理员ID)
*/
private Long creator;
/**
* 评价人名称(关联查询)
*/
private String creatorName;
/**
* 评价人账号(关联查询,管理后台显示)
*/
private String creatorAccount;
/**
* 购买评分 1-5
*/
private Integer rating;
/**
* 文字评价内容
*/
private String content;
/**
* 有问题的机位ID列表
* 格式: [12345, 12346, 12347]
*/
private List<Long> problemDeviceIds;
/**
* 问题机位数量(方便前端展示)
*/
private Integer problemDeviceCount;
/**
* 问题标签列表
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
*/
private List<String> problemTags;
/**
* 来源
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 来源ID
* 用于溯源,关联订单ID或渲染任务ID等
*/
private Long sourceId;
/**
* 创建时间
*/
@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;
/**
* 操作时长(创建到更新的时间差,单位:秒)
*/
private Long operationDuration;
}

View File

@@ -2,7 +2,7 @@ package com.ycwl.basic.model.pc.videoreview.dto;
import lombok.Data; import lombok.Data;
import java.util.Map; import java.util.List;
/** /**
* 新增视频评价请求DTO * 新增视频评价请求DTO
@@ -26,9 +26,28 @@ public class VideoReviewAddReqDTO {
private String content; private String content;
/** /**
* 机位评价JSON(可选) * 有问题的机位ID列表(可选)
* 格式: {"12345": 5, "12346": 4} * 格式: [12345, 12346, 12347]
* key为机位ID,value为该机位的评分(1-5) * 选择有问题的机位ID
*/ */
private Map<String, Integer> cameraPositionRating; private List<Long> problemDeviceIds;
/**
* 问题标签列表(可选)
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
* 可多选问题标签
*/
private List<String> problemTags;
/**
* 来源(必填)
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 来源ID(可选)
* 用于溯源,关联订单ID或渲染任务ID等
*/
private Long sourceId;
} }

View File

@@ -53,6 +53,24 @@ public class VideoReviewListReqDTO {
*/ */
private String keyword; private String keyword;
/**
* 问题机位ID(可选,筛选包含该机位ID的评价)
* 任意一个问题机位匹配即可
*/
private Long problemDeviceId;
/**
* 问题标签(可选,筛选包含该标签的评价)
* 任意一个标签匹配即可
*/
private String problemTag;
/**
* 来源(可选,筛选指定来源的评价)
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/** /**
* 页码(必填,默认1) * 页码(必填,默认1)
*/ */

View File

@@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.List;
/** /**
* 视频评价详情响应DTO * 视频评价详情响应DTO
@@ -68,11 +68,28 @@ public class VideoReviewRespDTO {
private String content; private String content;
/** /**
* 机位评价JSON * 有问题的机位ID列表
* 格式: {"12345": 5, "12346": 4} * 格式: [12345, 12346, 12347]
* key为机位ID,value为该机位的评分(1-5)
*/ */
private Map<String, Integer> cameraPositionRating; private List<Long> problemDeviceIds;
/**
* 问题标签列表
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
*/
private List<String> problemTags;
/**
* 来源
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 来源ID
* 用于溯源,关联订单ID或渲染任务ID等
*/
private Long sourceId;
/** /**
* 创建时间 * 创建时间

View File

@@ -40,10 +40,10 @@ public class VideoReviewStatisticsRespDTO {
private List<ScenicReviewRank> scenicRankList; private List<ScenicReviewRank> scenicRankList;
/** /**
* 机位评价统计 * 问题机位统计
* key: 机位ID, value: 该机位的平均评分 * key: 机位ID, value: 该机位被标记为问题的次数
*/ */
private Map<String, BigDecimal> cameraPositionAverage; private Map<Long, Long> problemDeviceStatistics;
/** /**
* 景区评价排行内部类 * 景区评价排行内部类

View File

@@ -4,12 +4,13 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.ycwl.basic.handler.MapTypeHandler; import com.ycwl.basic.handler.LongListTypeHandler;
import com.ycwl.basic.handler.StringListTypeHandler;
import lombok.Data; import lombok.Data;
import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.JdbcType;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.List;
/** /**
* 视频评价实体类 * 视频评价实体类
@@ -50,12 +51,32 @@ public class VideoReviewEntity {
private String content; private String content;
/** /**
* 机位评价JSON * 有问题的机位ID列表
* 格式: {"12345": 5, "12346": 4} * 格式: [12345, 12346, 12347]
* key为机位ID,value为该机位的评分(1-5) * 存储被标记为有问题的机位ID
*/ */
@TableField(typeHandler = MapTypeHandler.class, jdbcType = JdbcType.VARCHAR) @TableField(typeHandler = LongListTypeHandler.class, jdbcType = JdbcType.VARCHAR)
private Map<String, Integer> cameraPositionRating; private List<Long> problemDeviceIds;
/**
* 问题标签列表
* 格式: ["画面模糊", "抖动严重", "色彩异常"]
* 存储视频或机位的问题标签,可多选
*/
@TableField(typeHandler = StringListTypeHandler.class, jdbcType = JdbcType.VARCHAR)
private List<String> problemTags;
/**
* 来源
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 来源ID
* 用于溯源,关联订单ID或渲染任务ID等
*/
private Long sourceId;
/** /**
* 创建时间 * 创建时间

View File

@@ -1,6 +1,8 @@
package com.ycwl.basic.service; package com.ycwl.basic.service;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
@@ -32,6 +34,14 @@ public interface VideoReviewService {
*/ */
PageInfo<VideoReviewRespDTO> getReviewList(VideoReviewListReqDTO reqDTO); PageInfo<VideoReviewRespDTO> getReviewList(VideoReviewListReqDTO reqDTO);
/**
* 管理后台分页查询评价日志
*
* @param reqDTO 查询条件
* @return 分页结果
*/
PageInfo<AdminVideoReviewLogRespDTO> getAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO);
/** /**
* 获取评价统计数据 * 获取评价统计数据
* *

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.enums.VideoReviewSourceEnum;
import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.exception.BizException; import com.ycwl.basic.exception.BizException;
import com.ycwl.basic.mapper.OrderMapper; import com.ycwl.basic.mapper.OrderMapper;
@@ -14,6 +15,8 @@ import com.ycwl.basic.mapper.VideoReviewMapper;
import com.ycwl.basic.model.pc.order.entity.OrderEntity; import com.ycwl.basic.model.pc.order.entity.OrderEntity;
import com.ycwl.basic.model.pc.task.entity.TaskEntity; import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.video.entity.VideoEntity; import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckReqDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO; import com.ycwl.basic.model.pc.videoreview.dto.VideoPurchaseCheckRespDTO;
import com.ycwl.basic.repository.DeviceRepository; import com.ycwl.basic.repository.DeviceRepository;
@@ -72,6 +75,12 @@ public class VideoReviewServiceImpl implements VideoReviewService {
if (reqDTO.getRating() == null || reqDTO.getRating() < 1 || reqDTO.getRating() > 5) { if (reqDTO.getRating() == null || reqDTO.getRating() < 1 || reqDTO.getRating() > 5) {
throw new BaseException("评分必须在1-5之间"); throw new BaseException("评分必须在1-5之间");
} }
if (reqDTO.getSource() == null || reqDTO.getSource().isEmpty()) {
throw new BaseException("来源不能为空");
}
if (!VideoReviewSourceEnum.isValid(reqDTO.getSource())) {
throw new BaseException("来源值无效,仅支持: ORDER(订单), RENDER(渲染)");
}
// 2. 查询视频信息,获取景区ID // 2. 查询视频信息,获取景区ID
VideoEntity video = videoMapper.getEntity(reqDTO.getVideoId()); VideoEntity video = videoMapper.getEntity(reqDTO.getVideoId());
@@ -93,12 +102,16 @@ public class VideoReviewServiceImpl implements VideoReviewService {
entity.setCreator(creator); entity.setCreator(creator);
entity.setRating(reqDTO.getRating()); entity.setRating(reqDTO.getRating());
entity.setContent(reqDTO.getContent()); entity.setContent(reqDTO.getContent());
entity.setCameraPositionRating(reqDTO.getCameraPositionRating()); entity.setProblemDeviceIds(reqDTO.getProblemDeviceIds());
entity.setProblemTags(reqDTO.getProblemTags());
entity.setSource(reqDTO.getSource());
entity.setSourceId(reqDTO.getSourceId());
// 5. 插入数据库 // 5. 插入数据库
videoReviewMapper.insert(entity); videoReviewMapper.insert(entity);
log.info("管理员[{}]对视频[{}]添加评价成功,评价ID: {}", creator, reqDTO.getVideoId(), entity.getId()); log.info("管理员[{}]对视频[{}]添加评价成功,评价ID: {}, 来源: {}, 来源ID: {}",
creator, reqDTO.getVideoId(), entity.getId(), reqDTO.getSource(), reqDTO.getSourceId());
return entity.getId(); return entity.getId();
} }
@@ -114,6 +127,18 @@ public class VideoReviewServiceImpl implements VideoReviewService {
return new PageInfo<>(list); return new PageInfo<>(list);
} }
@Override
public PageInfo<AdminVideoReviewLogRespDTO> getAdminReviewLogList(AdminVideoReviewLogReqDTO reqDTO) {
// 设置分页参数
PageHelper.startPage(reqDTO.getPageNum(), reqDTO.getPageSize());
// 查询列表
List<AdminVideoReviewLogRespDTO> list = videoReviewMapper.selectAdminReviewLogList(reqDTO);
// 封装分页结果
return new PageInfo<>(list);
}
@Override @Override
public VideoReviewStatisticsRespDTO getStatistics() { public VideoReviewStatisticsRespDTO getStatistics() {
VideoReviewStatisticsRespDTO statistics = new VideoReviewStatisticsRespDTO(); VideoReviewStatisticsRespDTO statistics = new VideoReviewStatisticsRespDTO();
@@ -154,9 +179,9 @@ public class VideoReviewServiceImpl implements VideoReviewService {
List<VideoReviewStatisticsRespDTO.ScenicReviewRank> scenicRankList = videoReviewMapper.countScenicRank(10); List<VideoReviewStatisticsRespDTO.ScenicReviewRank> scenicRankList = videoReviewMapper.countScenicRank(10);
statistics.setScenicRankList(scenicRankList); statistics.setScenicRankList(scenicRankList);
// 6. 机位评价维度平均值 // 6. 问题机位统计
Map<String, BigDecimal> cameraPositionAverage = calculateCameraPositionAverage(); Map<Long, Long> problemDeviceStatistics = calculateProblemDeviceStatistics();
statistics.setCameraPositionAverage(cameraPositionAverage); statistics.setProblemDeviceStatistics(problemDeviceStatistics);
return statistics; return statistics;
} }
@@ -168,37 +193,11 @@ public class VideoReviewServiceImpl implements VideoReviewService {
reqDTO.setPageSize(Integer.MAX_VALUE); reqDTO.setPageSize(Integer.MAX_VALUE);
List<VideoReviewRespDTO> list = videoReviewMapper.selectReviewList(reqDTO); List<VideoReviewRespDTO> list = videoReviewMapper.selectReviewList(reqDTO);
// 2. 收集所有机位ID并批量查询机位名称 // 2. 创建Excel工作簿
Set<Long> allDeviceIds = new LinkedHashSet<>();
for (VideoReviewRespDTO review : list) {
Map<String, Integer> cameraRating = review.getCameraPositionRating();
if (cameraRating != null && !cameraRating.isEmpty()) {
// 收集机位ID (按顺序)
for (String deviceIdStr : cameraRating.keySet()) {
try {
allDeviceIds.add(Long.valueOf(deviceIdStr));
} catch (NumberFormatException e) {
log.warn("无效的机位ID: {}", deviceIdStr);
}
}
}
}
// 批量查询机位名称
Map<Long, String> deviceNames = new HashMap<>();
if (!allDeviceIds.isEmpty()) {
deviceNames = deviceRepository.batchGetDeviceNames(new ArrayList<>(allDeviceIds));
}
// 对机位ID按ID排序,保证表头顺序一致
List<Long> sortedDeviceIds = new ArrayList<>(allDeviceIds);
sortedDeviceIds.sort(Long::compareTo);
// 3. 创建Excel工作簿
Workbook workbook = new XSSFWorkbook(); Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("视频评价数据"); Sheet sheet = workbook.createSheet("视频评价数据");
// 4. 创建标题行样式 // 3. 创建标题行样式
CellStyle headerStyle = workbook.createCellStyle(); CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont(); Font headerFont = workbook.createFont();
headerFont.setBold(true); headerFont.setBold(true);
@@ -206,7 +205,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 5. 生成动态表头 - 使用机位名称作为表头 // 4. 生成表头
Row headerRow = sheet.createRow(0); Row headerRow = sheet.createRow(0);
List<String> headerList = new ArrayList<>(); List<String> headerList = new ArrayList<>();
headerList.add("评价ID"); headerList.add("评价ID");
@@ -216,14 +215,8 @@ public class VideoReviewServiceImpl implements VideoReviewService {
headerList.add("评价人名称"); headerList.add("评价人名称");
headerList.add("评分"); headerList.add("评分");
headerList.add("文字评价"); headerList.add("文字评价");
headerList.add("问题机位ID列表");
// 添加机位列 - 表头直接使用机位名称 headerList.add("问题标签");
Map<Long, String> finalDeviceNames = deviceNames;
for (Long deviceId : sortedDeviceIds) {
String deviceName = finalDeviceNames.getOrDefault(deviceId, "未知设备(ID:" + deviceId + ")");
headerList.add(deviceName);
}
headerList.add("创建时间"); headerList.add("创建时间");
headerList.add("更新时间"); headerList.add("更新时间");
@@ -234,7 +227,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
cell.setCellStyle(headerStyle); cell.setCellStyle(headerStyle);
} }
// 6. 填充数据 // 5. 填充数据
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
int rowNum = 1; int rowNum = 1;
@@ -249,41 +242,37 @@ public class VideoReviewServiceImpl implements VideoReviewService {
row.createCell(colIndex++).setCellValue(review.getScenicName()); row.createCell(colIndex++).setCellValue(review.getScenicName());
row.createCell(colIndex++).setCellValue(review.getCreatorName()); row.createCell(colIndex++).setCellValue(review.getCreatorName());
row.createCell(colIndex++).setCellValue(review.getRating()); row.createCell(colIndex++).setCellValue(review.getRating());
row.createCell(colIndex++).setCellValue(review.getContent()); row.createCell(colIndex++).setCellValue(review.getContent() != null ? review.getContent() : "");
// 机位评价列 - 按表头顺序填充 // 问题机位ID列表
Map<String, Integer> cameraRating = review.getCameraPositionRating(); List<Long> problemDeviceIds = review.getProblemDeviceIds();
for (Long deviceId : sortedDeviceIds) { String problemDeviceIdsStr = (problemDeviceIds != null && !problemDeviceIds.isEmpty())
String deviceIdStr = String.valueOf(deviceId); ? problemDeviceIds.toString()
Integer rating = null; : "";
row.createCell(colIndex++).setCellValue(problemDeviceIdsStr);
if (cameraRating != null && cameraRating.containsKey(deviceIdStr)) { // 问题标签
rating = cameraRating.get(deviceIdStr); List<String> problemTags = review.getProblemTags();
} String problemTagsStr = (problemTags != null && !problemTags.isEmpty())
? String.join(", ", problemTags)
Cell cell = row.createCell(colIndex++); : "";
if (rating != null) { row.createCell(colIndex++).setCellValue(problemTagsStr);
cell.setCellValue(rating);
} else {
cell.setCellValue("");
}
}
// 时间列 // 时间列
row.createCell(colIndex++).setCellValue(review.getCreateTime() != null ? sdf.format(review.getCreateTime()) : ""); row.createCell(colIndex++).setCellValue(review.getCreateTime() != null ? sdf.format(review.getCreateTime()) : "");
row.createCell(colIndex).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : ""); row.createCell(colIndex).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : "");
} }
// 7. 自动调整列宽 // 6. 自动调整列宽
for (int i = 0; i < headerList.size(); i++) { for (int i = 0; i < headerList.size(); i++) {
sheet.autoSizeColumn(i); sheet.autoSizeColumn(i);
} }
// 8. 写入输出流 // 7. 写入输出流
workbook.write(outputStream); workbook.write(outputStream);
workbook.close(); workbook.close();
log.info("导出视频评价数据成功,共{}条,机位数:{}", list.size(), sortedDeviceIds.size()); log.info("导出视频评价数据成功,共{}条", list.size());
} }
@Override @Override
@@ -338,32 +327,25 @@ public class VideoReviewServiceImpl implements VideoReviewService {
} }
/** /**
* 计算各机位的平均评分 * 统计问题机位
* 统计每个机位被标记为问题的次数
*/ */
private Map<String, BigDecimal> calculateCameraPositionAverage() { private Map<Long, Long> calculateProblemDeviceStatistics() {
List<Map<String, Integer>> allRatings = videoReviewMapper.selectAllCameraPositionRatings(); List<List<Long>> allProblemDeviceIds = videoReviewMapper.selectAllProblemDeviceIds();
if (allRatings == null || allRatings.isEmpty()) { if (allProblemDeviceIds == null || allProblemDeviceIds.isEmpty()) {
return new HashMap<>(); return new HashMap<>();
} }
// 统计各机位的总分和次数 // 统计各机位被标记为问题的次数
Map<String, List<Integer>> deviceScores = new HashMap<>(); Map<Long, Long> deviceProblemCount = new HashMap<>();
for (Map<String, Integer> rating : allRatings) { for (List<Long> problemDeviceIds : allProblemDeviceIds) {
if (rating == null) continue; if (problemDeviceIds == null || problemDeviceIds.isEmpty()) continue;
// 遍历每个机位的评分 for (Long deviceId : problemDeviceIds) {
for (Map.Entry<String, Integer> entry : rating.entrySet()) { deviceProblemCount.put(deviceId, deviceProblemCount.getOrDefault(deviceId, 0L) + 1);
deviceScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue());
} }
} }
// 计算平均值 return deviceProblemCount;
Map<String, BigDecimal> result = new HashMap<>();
for (Map.Entry<String, List<Integer>> entry : deviceScores.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;
} }
} }

View File

@@ -15,8 +15,12 @@
<result property="creatorName" column="creator_name"/> <result property="creatorName" column="creator_name"/>
<result property="rating" column="rating"/> <result property="rating" column="rating"/>
<result property="content" column="content"/> <result property="content" column="content"/>
<result property="cameraPositionRating" column="camera_position_rating" <result property="problemDeviceIds" column="problem_device_ids"
typeHandler="com.ycwl.basic.handler.MapTypeHandler"/> typeHandler="com.ycwl.basic.handler.LongListTypeHandler"/>
<result property="problemTags" column="problem_tags"
typeHandler="com.ycwl.basic.handler.StringListTypeHandler"/>
<result property="source" column="source"/>
<result property="sourceId" column="source_id"/>
<result property="createTime" column="create_time"/> <result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/> <result property="updateTime" column="update_time"/>
</resultMap> </resultMap>
@@ -31,7 +35,10 @@
vr.creator, vr.creator,
vr.rating, vr.rating,
vr.content, vr.content,
vr.camera_position_rating, vr.problem_device_ids,
vr.problem_tags,
vr.source,
vr.source_id,
vr.create_time, vr.create_time,
vr.update_time, vr.update_time,
v.video_url, v.video_url,
@@ -72,6 +79,15 @@
<if test="keyword != null and keyword != ''"> <if test="keyword != null and keyword != ''">
AND vr.content LIKE CONCAT('%', #{keyword}, '%') AND vr.content LIKE CONCAT('%', #{keyword}, '%')
</if> </if>
<if test="problemDeviceId != null">
AND JSON_CONTAINS(vr.problem_device_ids, CAST(#{problemDeviceId} AS CHAR), '$')
</if>
<if test="problemTag != null and problemTag != ''">
AND JSON_CONTAINS(vr.problem_tags, JSON_QUOTE(#{problemTag}), '$')
</if>
<if test="source != null and source != ''">
AND vr.source = #{source}
</if>
</where> </where>
ORDER BY ORDER BY
<choose> <choose>
@@ -140,11 +156,156 @@
LIMIT #{limit} LIMIT #{limit}
</select> </select>
<!-- 查询所有机位评价数据 --> <!-- 查询所有问题机位ID列表 -->
<select id="selectAllCameraPositionRatings" resultType="java.util.Map"> <select id="selectAllProblemDeviceIds" resultType="java.util.List">
SELECT camera_position_rating SELECT problem_device_ids
FROM video_review FROM video_review
WHERE camera_position_rating IS NOT NULL AND camera_position_rating != '' WHERE problem_device_ids IS NOT NULL
AND problem_device_ids != ''
AND problem_device_ids != '[]'
</select>
<!-- 管理后台评价日志结果映射 -->
<resultMap id="AdminVideoReviewLogRespMap" type="com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogRespDTO">
<id property="id" column="id"/>
<result property="videoId" column="video_id"/>
<result property="videoUrl" column="video_url"/>
<result property="templateId" column="template_id"/>
<result property="templateName" column="template_name"/>
<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="creatorAccount" column="creator_account"/>
<result property="rating" column="rating"/>
<result property="content" column="content"/>
<result property="problemDeviceIds" column="problem_device_ids"
typeHandler="com.ycwl.basic.handler.LongListTypeHandler"/>
<result property="problemDeviceCount" column="problem_device_count"/>
<result property="problemTags" column="problem_tags"
typeHandler="com.ycwl.basic.handler.StringListTypeHandler"/>
<result property="source" column="source"/>
<result property="sourceId" column="source_id"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="operationDuration" column="operation_duration"/>
</resultMap>
<!-- 管理后台分页查询评价日志 -->
<select id="selectAdminReviewLogList" parameterType="com.ycwl.basic.model.pc.videoreview.dto.AdminVideoReviewLogReqDTO"
resultMap="AdminVideoReviewLogRespMap">
SELECT
vr.id,
vr.video_id,
vr.scenic_id,
vr.creator,
vr.rating,
vr.content,
vr.problem_device_ids,
vr.problem_tags,
vr.source,
vr.source_id,
vr.create_time,
vr.update_time,
v.video_url,
v.template_id,
t.name AS template_name,
s.name AS scenic_name,
u.name AS creator_name,
u.account AS creator_account,
<!-- 计算问题机位数量 -->
CASE
WHEN vr.problem_device_ids IS NOT NULL AND vr.problem_device_ids != '' AND vr.problem_device_ids != '[]'
THEN JSON_LENGTH(vr.problem_device_ids)
ELSE 0
END AS problem_device_count,
<!-- 计算操作时长(秒) -->
TIMESTAMPDIFF(SECOND, vr.create_time, vr.update_time) AS operation_duration
FROM video_review vr
LEFT JOIN video v ON vr.video_id = v.id
LEFT JOIN template t ON v.template_id = t.id
LEFT JOIN scenic s ON vr.scenic_id = s.id
LEFT JOIN admin_user u ON vr.creator = u.id
<where>
<if test="id != null">
AND vr.id = #{id}
</if>
<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="creatorName != null and creatorName != ''">
AND u.name LIKE CONCAT('%', #{creatorName}, '%')
</if>
<if test="rating != null">
AND vr.rating = #{rating}
</if>
<if test="minRating != null">
AND vr.rating &gt;= #{minRating}
</if>
<if test="maxRating != null">
AND vr.rating &lt;= #{maxRating}
</if>
<if test="startTime != null and startTime != ''">
AND vr.create_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND vr.create_time &lt;= #{endTime}
</if>
<if test="templateId != null">
AND v.template_id = #{templateId}
</if>
<if test="templateName != null and templateName != ''">
AND t.name LIKE CONCAT('%', #{templateName}, '%')
</if>
<if test="keyword != null and keyword != ''">
AND (
vr.content LIKE CONCAT('%', #{keyword}, '%')
OR s.name LIKE CONCAT('%', #{keyword}, '%')
OR t.name LIKE CONCAT('%', #{keyword}, '%')
)
</if>
<if test="hasCameraRating != null">
<!-- hasCameraRating 参数已废弃,保留以兼容旧接口 -->
</if>
<if test="problemDeviceId != null">
AND JSON_CONTAINS(vr.problem_device_ids, CAST(#{problemDeviceId} AS CHAR), '$')
</if>
<if test="problemTag != null and problemTag != ''">
AND JSON_CONTAINS(vr.problem_tags, JSON_QUOTE(#{problemTag}), '$')
</if>
<if test="source != null and source != ''">
AND vr.source = #{source}
</if>
</where>
ORDER BY
<choose>
<when test="orderBy == 'rating'">
vr.rating
</when>
<when test="orderBy == 'update_time'">
vr.update_time
</when>
<when test="orderBy == 'id'">
vr.id
</when>
<otherwise>
vr.create_time
</otherwise>
</choose>
<choose>
<when test="orderDirection == 'ASC'">
ASC
</when>
<otherwise>
DESC
</otherwise>
</choose>
</select> </select>
</mapper> </mapper>