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;
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.VideoPurchaseCheckRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
@@ -55,6 +57,20 @@ public class VideoReviewController {
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;
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.VideoReviewRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewStatisticsRespDTO;
@@ -25,6 +27,14 @@ public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
*/
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);
/**
* 查询所有机位评价数据(用于后端计算平均值)
* 查询所有问题机位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 java.util.Map;
import java.util.List;
/**
* 新增视频评价请求DTO
@@ -26,9 +26,28 @@ public class VideoReviewAddReqDTO {
private String content;
/**
* 机位评价JSON(可选)
* 格式: {"12345": 5, "12346": 4}
* key为机位ID,value为该机位的评分(1-5)
* 有问题的机位ID列表(可选)
* 格式: [12345, 12346, 12347]
* 选择有问题的机位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;
/**
* 问题机位ID(可选,筛选包含该机位ID的评价)
* 任意一个问题机位匹配即可
*/
private Long problemDeviceId;
/**
* 问题标签(可选,筛选包含该标签的评价)
* 任意一个标签匹配即可
*/
private String problemTag;
/**
* 来源(可选,筛选指定来源的评价)
* 固定值: ORDER(订单), RENDER(渲染)
*/
private String source;
/**
* 页码(必填,默认1)
*/

View File

@@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
import java.util.Map;
import java.util.List;
/**
* 视频评价详情响应DTO
@@ -68,11 +68,28 @@ public class VideoReviewRespDTO {
private String content;
/**
* 机位评价JSON
* 格式: {"12345": 5, "12346": 4}
* key为机位ID,value为该机位的评分(1-5)
* 有问题的机位ID列表
* 格式: [12345, 12346, 12347]
*/
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;
/**
* 机位评价统计
* 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.TableId;
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 org.apache.ibatis.type.JdbcType;
import java.util.Date;
import java.util.Map;
import java.util.List;
/**
* 视频评价实体类
@@ -50,12 +51,32 @@ public class VideoReviewEntity {
private String content;
/**
* 机位评价JSON
* 格式: {"12345": 5, "12346": 4}
* key为机位ID,value为该机位的评分(1-5)
* 有问题的机位ID列表
* 格式: [12345, 12346, 12347]
* 存储被标记为有问题的机位ID
*/
@TableField(typeHandler = MapTypeHandler.class, jdbcType = JdbcType.VARCHAR)
private Map<String, Integer> cameraPositionRating;
@TableField(typeHandler = LongListTypeHandler.class, jdbcType = JdbcType.VARCHAR)
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;
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.VideoPurchaseCheckRespDTO;
import com.ycwl.basic.model.pc.videoreview.dto.VideoReviewAddReqDTO;
@@ -32,6 +34,14 @@ public interface VideoReviewService {
*/
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.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.enums.VideoReviewSourceEnum;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.exception.BizException;
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.task.entity.TaskEntity;
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.VideoPurchaseCheckRespDTO;
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) {
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
VideoEntity video = videoMapper.getEntity(reqDTO.getVideoId());
@@ -93,12 +102,16 @@ public class VideoReviewServiceImpl implements VideoReviewService {
entity.setCreator(creator);
entity.setRating(reqDTO.getRating());
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. 插入数据库
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();
}
@@ -114,6 +127,18 @@ public class VideoReviewServiceImpl implements VideoReviewService {
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
public VideoReviewStatisticsRespDTO getStatistics() {
VideoReviewStatisticsRespDTO statistics = new VideoReviewStatisticsRespDTO();
@@ -154,9 +179,9 @@ public class VideoReviewServiceImpl implements VideoReviewService {
List<VideoReviewStatisticsRespDTO.ScenicReviewRank> scenicRankList = videoReviewMapper.countScenicRank(10);
statistics.setScenicRankList(scenicRankList);
// 6. 机位评价维度平均值
Map<String, BigDecimal> cameraPositionAverage = calculateCameraPositionAverage();
statistics.setCameraPositionAverage(cameraPositionAverage);
// 6. 问题机位统计
Map<Long, Long> problemDeviceStatistics = calculateProblemDeviceStatistics();
statistics.setProblemDeviceStatistics(problemDeviceStatistics);
return statistics;
}
@@ -168,37 +193,11 @@ public class VideoReviewServiceImpl implements VideoReviewService {
reqDTO.setPageSize(Integer.MAX_VALUE);
List<VideoReviewRespDTO> list = videoReviewMapper.selectReviewList(reqDTO);
// 2. 收集所有机位ID并批量查询机位名称
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工作簿
// 2. 创建Excel工作簿
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("视频评价数据");
// 4. 创建标题行样式
// 3. 创建标题行样式
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(true);
@@ -206,7 +205,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 5. 生成动态表头 - 使用机位名称作为表头
// 4. 生成表头
Row headerRow = sheet.createRow(0);
List<String> headerList = new ArrayList<>();
headerList.add("评价ID");
@@ -216,14 +215,8 @@ public class VideoReviewServiceImpl implements VideoReviewService {
headerList.add("评价人名称");
headerList.add("评分");
headerList.add("文字评价");
// 添加机位列 - 表头直接使用机位名称
Map<Long, String> finalDeviceNames = deviceNames;
for (Long deviceId : sortedDeviceIds) {
String deviceName = finalDeviceNames.getOrDefault(deviceId, "未知设备(ID:" + deviceId + ")");
headerList.add(deviceName);
}
headerList.add("问题机位ID列表");
headerList.add("问题标签");
headerList.add("创建时间");
headerList.add("更新时间");
@@ -234,7 +227,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
cell.setCellStyle(headerStyle);
}
// 6. 填充数据
// 5. 填充数据
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
int rowNum = 1;
@@ -249,41 +242,37 @@ public class VideoReviewServiceImpl implements VideoReviewService {
row.createCell(colIndex++).setCellValue(review.getScenicName());
row.createCell(colIndex++).setCellValue(review.getCreatorName());
row.createCell(colIndex++).setCellValue(review.getRating());
row.createCell(colIndex++).setCellValue(review.getContent());
row.createCell(colIndex++).setCellValue(review.getContent() != null ? review.getContent() : "");
// 机位评价列 - 按表头顺序填充
Map<String, Integer> cameraRating = review.getCameraPositionRating();
for (Long deviceId : sortedDeviceIds) {
String deviceIdStr = String.valueOf(deviceId);
Integer rating = null;
// 问题机位ID列表
List<Long> problemDeviceIds = review.getProblemDeviceIds();
String problemDeviceIdsStr = (problemDeviceIds != null && !problemDeviceIds.isEmpty())
? problemDeviceIds.toString()
: "";
row.createCell(colIndex++).setCellValue(problemDeviceIdsStr);
if (cameraRating != null && cameraRating.containsKey(deviceIdStr)) {
rating = cameraRating.get(deviceIdStr);
}
Cell cell = row.createCell(colIndex++);
if (rating != null) {
cell.setCellValue(rating);
} else {
cell.setCellValue("");
}
}
// 问题标签
List<String> problemTags = review.getProblemTags();
String problemTagsStr = (problemTags != null && !problemTags.isEmpty())
? String.join(", ", problemTags)
: "";
row.createCell(colIndex++).setCellValue(problemTagsStr);
// 时间列
row.createCell(colIndex++).setCellValue(review.getCreateTime() != null ? sdf.format(review.getCreateTime()) : "");
row.createCell(colIndex).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : "");
}
// 7. 自动调整列宽
// 6. 自动调整列宽
for (int i = 0; i < headerList.size(); i++) {
sheet.autoSizeColumn(i);
}
// 8. 写入输出流
// 7. 写入输出流
workbook.write(outputStream);
workbook.close();
log.info("导出视频评价数据成功,共{}条,机位数:{}", list.size(), sortedDeviceIds.size());
log.info("导出视频评价数据成功,共{}条", list.size());
}
@Override
@@ -338,32 +327,25 @@ public class VideoReviewServiceImpl implements VideoReviewService {
}
/**
* 计算各机位的平均评分
* 统计问题机位
* 统计每个机位被标记为问题的次数
*/
private Map<String, BigDecimal> calculateCameraPositionAverage() {
List<Map<String, Integer>> allRatings = videoReviewMapper.selectAllCameraPositionRatings();
private Map<Long, Long> calculateProblemDeviceStatistics() {
List<List<Long>> allProblemDeviceIds = videoReviewMapper.selectAllProblemDeviceIds();
if (allRatings == null || allRatings.isEmpty()) {
if (allProblemDeviceIds == null || allProblemDeviceIds.isEmpty()) {
return new HashMap<>();
}
// 统计各机位的总分和次数
Map<String, List<Integer>> deviceScores = new HashMap<>();
for (Map<String, Integer> rating : allRatings) {
if (rating == null) continue;
// 遍历每个机位的评分
for (Map.Entry<String, Integer> entry : rating.entrySet()) {
deviceScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue());
// 统计各机位被标记为问题的次数
Map<Long, Long> deviceProblemCount = new HashMap<>();
for (List<Long> problemDeviceIds : allProblemDeviceIds) {
if (problemDeviceIds == null || problemDeviceIds.isEmpty()) continue;
for (Long deviceId : problemDeviceIds) {
deviceProblemCount.put(deviceId, deviceProblemCount.getOrDefault(deviceId, 0L) + 1);
}
}
// 计算平均值
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;
return deviceProblemCount;
}
}