feat(video-review): 支持机位多维度评价功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good

- 新增NestedMapTypeHandler处理嵌套Map与JSON互转
- 修改VideoReview相关实体类和DTO以支持嵌套Map结构
- 更新数据库查询逻辑以适配新的评价数据结构
- 优化平均分计算方法以处理多机位多维度评分
- 完善MyBatis配置中的typeHandler引用
- 补充视频查询接口返回任务开始结束时间字段
- 修正SQL关联查询条件确保数据准确性
This commit is contained in:
2025-11-18 10:14:42 +08:00
parent 3d361200b0
commit bb2367c5a6
9 changed files with 118 additions and 28 deletions

View File

@@ -0,0 +1,77 @@
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, Map<String, Integer>>的互转
* 主要用于机位评价功能:外层key为机位ID,内层Map为该机位的各维度评分
*
* 数据格式示例:
* {
* "12345": {"清晰度": 5, "构图": 4, "色彩": 5, "整体效果": 4},
* "12346": {"清晰度": 4, "构图": 5, "色彩": 4, "整体效果": 5}
* }
*/
@Slf4j
public class NestedMapTypeHandler extends BaseTypeHandler<Map<String, Map<String, Integer>>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final TypeReference<Map<String, Map<String, Integer>>> typeReference =
new TypeReference<Map<String, Map<String, Integer>>>() {};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Map<String, 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, Map<String, Integer>> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json);
}
@Override
public Map<String, Map<String, Integer>> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json);
}
@Override
public Map<String, Map<String, Integer>> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json);
}
/**
* 解析JSON字符串为嵌套Map
*/
private Map<String, 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<>();
}
}
}

View File

@@ -65,7 +65,7 @@ public interface VideoReviewMapper extends BaseMapper<VideoReviewEntity> {
/** /**
* 查询所有机位评价数据(用于后端计算平均值) * 查询所有机位评价数据(用于后端计算平均值)
* *
* @return 机位评价列表 * @return 机位评价列表(嵌套Map结构)
*/ */
List<Map<String, Integer>> selectAllCameraPositionRatings(); List<Map<String, Map<String, Integer>>> selectAllCameraPositionRatings();
} }

View File

@@ -63,6 +63,10 @@ public class VideoRespVO {
private Date createTime; private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime; private Date updateTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date startTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date endTime;
private Integer height; private Integer height;
private Integer width; private Integer width;
private BigDecimal duration; private BigDecimal duration;

View File

@@ -26,8 +26,10 @@ public class VideoReviewAddReqDTO {
private String content; private String content;
/** /**
* 机位快速评价JSON(可选) * 机位评价JSON(可选)
* 格式: {"角度":5,"清晰度":4,"构图":5} * 格式: {"12345": {"清晰度":5,"构图":4,"色彩":5,"整体效果":4}, "12346": {...}}
* 外层key为机位ID,内层Map为该机位的各维度评分
* 评分维度: 清晰度, 构图, 色彩, 整体效果
*/ */
private Map<String, Integer> cameraPositionRating; private Map<String, Map<String, Integer>> cameraPositionRating;
} }

View File

@@ -58,9 +58,11 @@ public class VideoReviewRespDTO {
private String content; private String content;
/** /**
* 机位快速评价JSON * 机位评价JSON
* 格式: {"12345": {"清晰度":5,"构图":4,"色彩":5,"整体效果":4}, "12346": {...}}
* 外层key为机位ID,内层Map为该机位的各维度评分
*/ */
private Map<String, Integer> cameraPositionRating; private Map<String, Map<String, Integer>> cameraPositionRating;
/** /**
* 创建时间 * 创建时间

View File

@@ -4,7 +4,7 @@ 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.NestedMapTypeHandler;
import lombok.Data; import lombok.Data;
import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.JdbcType;
@@ -50,11 +50,12 @@ public class VideoReviewEntity {
private String content; private String content;
/** /**
* 机位快速评价JSON * 机位评价JSON
* 格式: {"角度":5,"清晰度":4,"构图":5} * 格式: {"12345": {"清晰度":5,"构图":4,"色彩":5,"整体效果":4}, "12346": {...}}
* 外层key为机位ID,内层Map为该机位的各维度评分
*/ */
@TableField(typeHandler = MapTypeHandler.class, jdbcType = JdbcType.VARCHAR) @TableField(typeHandler = NestedMapTypeHandler.class, jdbcType = JdbcType.VARCHAR)
private Map<String, Integer> cameraPositionRating; private Map<String, Map<String, Integer>> cameraPositionRating;
/** /**
* 创建时间 * 创建时间

View File

@@ -218,7 +218,7 @@ public class VideoReviewServiceImpl implements VideoReviewService {
* 计算机位评价各维度的平均值 * 计算机位评价各维度的平均值
*/ */
private Map<String, BigDecimal> calculateCameraPositionAverage() { private Map<String, BigDecimal> calculateCameraPositionAverage() {
List<Map<String, Integer>> allRatings = videoReviewMapper.selectAllCameraPositionRatings(); List<Map<String, Map<String, Integer>>> allRatings = videoReviewMapper.selectAllCameraPositionRatings();
if (allRatings == null || allRatings.isEmpty()) { if (allRatings == null || allRatings.isEmpty()) {
return new HashMap<>(); return new HashMap<>();
@@ -226,10 +226,15 @@ public class VideoReviewServiceImpl implements VideoReviewService {
// 统计各维度的总分和次数 // 统计各维度的总分和次数
Map<String, List<Integer>> dimensionScores = new HashMap<>(); Map<String, List<Integer>> dimensionScores = new HashMap<>();
for (Map<String, Integer> rating : allRatings) { for (Map<String, Map<String, Integer>> rating : allRatings) {
if (rating == null) continue; if (rating == null) continue;
for (Map.Entry<String, Integer> entry : rating.entrySet()) { // 遍历每个机位
dimensionScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue()); for (Map<String, Integer> deviceRatings : rating.values()) {
if (deviceRatings == null) continue;
// 遍历该机位的每个维度
for (Map.Entry<String, Integer> entry : deviceRatings.entrySet()) {
dimensionScores.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue());
}
} }
} }

View File

@@ -76,17 +76,17 @@
) )
</delete> </delete>
<select id="list" resultType="com.ycwl.basic.model.pc.video.resp.VideoRespVO"> <select id="list" resultType="com.ycwl.basic.model.pc.video.resp.VideoRespVO">
select v.id, v.scenic_id, template_id, task_id, worker_id, video_url, v.create_time, v.update_time, select v.id, v.scenic_id, v.template_id, task_id, tk.worker_id, v.video_url, v.create_time, v.update_time,
t.name templateName, t.price templatePrice,t.cover_url templateCoverUrl, t.name templateName, t.cover_url templateCoverUrl,
tk.task_params taskParams tk.task_params taskParams, tk.start_time, tk.end_time
from video v from video v
left join template t on v.template_id = t.id left join template t on v.template_id = t.id
left join task tk on v.task_id = tk.id left join task tk on v.task_id = tk.id
<where> <where>
<if test="scenicId!= null">and v.scenic_id = #{scenicId} </if> <if test="scenicId!= null">and v.scenic_id = #{scenicId} </if>
<if test="templateId!= null">and template_id = #{templateId} </if> <if test="templateId!= null">and v.template_id = #{templateId} </if>
<if test="taskId!=null">and task_id = #{taskId}</if> <if test="taskId!=null">and task_id = #{taskId}</if>
<if test="workerId!= null">and worker_id = #{workerId} </if> <if test="workerId!= null">and t.worker_id = #{workerId} </if>
<if test="startTime!= null">and v.create_time &gt;= #{startTime} </if> <if test="startTime!= null">and v.create_time &gt;= #{startTime} </if>
<if test="endTime!= null">and v.create_time &lt;= #{endTime} </if> <if test="endTime!= null">and v.create_time &lt;= #{endTime} </if>
<if test="faceId!= null"> <if test="faceId!= null">
@@ -98,10 +98,9 @@
order by v.create_time desc order by v.create_time desc
</select> </select>
<select id="getById" resultType="com.ycwl.basic.model.pc.video.resp.VideoRespVO"> <select id="getById" resultType="com.ycwl.basic.model.pc.video.resp.VideoRespVO">
select v.id, v.scenic_id, template_id, task_id, worker_id, video_url, v.create_time, v.update_time, select v.id, v.scenic_id, v.template_id, task_id, tk.worker_id, v.video_url, v.create_time, v.update_time,
t.name templateName,t.price templatePrice, t.cover_url templateCoverUrl, t.slash_price slashPrice, t.name templateName, t.cover_url templateCoverUrl,
v.height, v.width, v.duration, tk.task_params taskParams, tk.start_time, tk.end_time
tk.task_params taskParams
from video v from video v
left join template t on v.template_id = t.id left join template t on v.template_id = t.id
left join task tk on v.task_id = tk.id left join task tk on v.task_id = tk.id
@@ -111,7 +110,7 @@
select * from video where task_id = #{taskId} limit 1 select * from video where task_id = #{taskId} limit 1
</select> </select>
<select id="queryByRelation" resultType="com.ycwl.basic.model.pc.video.resp.VideoRespVO"> <select id="queryByRelation" resultType="com.ycwl.basic.model.pc.video.resp.VideoRespVO">
select v.id, mv.scenic_id, v.template_id, mv.task_id, mv.face_id, worker_id, video_url, v.create_time, v.update_time, select v.id, mv.scenic_id, v.template_id, mv.task_id, mv.face_id, tk.worker_id, v.video_url, v.create_time, v.update_time,
t.name templateName, t.price templatePrice,t.cover_url templateCoverUrl, mv.is_buy, t.name templateName, t.price templatePrice,t.cover_url templateCoverUrl, mv.is_buy,
tk.task_params taskParams tk.task_params taskParams
from member_video mv from member_video mv

View File

@@ -14,7 +14,7 @@
<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="cameraPositionRating" column="camera_position_rating"
typeHandler="com.ycwl.basic.handler.MapTypeHandler"/> typeHandler="com.ycwl.basic.handler.NestedMapTypeHandler"/>
<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>
@@ -38,7 +38,7 @@
FROM video_review vr FROM video_review vr
LEFT JOIN video v ON vr.video_id = v.id LEFT JOIN video v ON vr.video_id = v.id
LEFT JOIN scenic s ON vr.scenic_id = s.id LEFT JOIN scenic s ON vr.scenic_id = s.id
LEFT JOIN sys_user u ON vr.creator = u.id LEFT JOIN admin_user u ON vr.creator = u.id
<where> <where>
<if test="videoId != null"> <if test="videoId != null">
AND vr.video_id = #{videoId} AND vr.video_id = #{videoId}