feat(video-review): 优化视频评价导出功能,支持机位名称动态表头

- 引入DeviceRepository用于批量查询机位名称
- 在导出逻辑中收集并排序机位ID,确保表头顺序一致
- 动态生成Excel表头,使用实际机位名称替代原始JSON字段
- 调整单元格样式以支持自动换行,提升可读性
- 更新mapper配置,关联template表获取模板名称
- 优化列宽自适应逻辑,为机位列设置最小宽度保障显示效果
- 日志记录中增加导出机位数量统计信息
This commit is contained in:
2025-11-20 11:00:29 +08:00
parent d387f11173
commit 90cf0d44c9
2 changed files with 121 additions and 29 deletions

View File

@@ -10,6 +10,7 @@ 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.repository.DeviceRepository;
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;
@@ -45,6 +46,9 @@ public class VideoReviewServiceImpl implements VideoReviewService {
@Autowired
private VideoMapper videoMapper;
@Autowired
private DeviceRepository deviceRepository;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
@@ -153,11 +157,37 @@ public class VideoReviewServiceImpl implements VideoReviewService {
reqDTO.setPageSize(Integer.MAX_VALUE);
List<VideoReviewRespDTO> list = videoReviewMapper.selectReviewList(reqDTO);
// 2. 创建Excel工作簿
// 2. 收集所有机位ID并批量查询机位名称
Set<Long> allDeviceIds = new LinkedHashSet<>();
for (VideoReviewRespDTO review : list) {
Map<String, 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();
Sheet sheet = workbook.createSheet("视频评价数据");
// 3. 创建标题行样式
// 4. 创建标题行样式
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(true);
@@ -165,53 +195,110 @@ public class VideoReviewServiceImpl implements VideoReviewService {
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 4. 创建标题行
// 5. 创建单元格自动换行样式
CellStyle wrapStyle = workbook.createCellStyle();
wrapStyle.setWrapText(true);
wrapStyle.setVerticalAlignment(VerticalAlignment.TOP);
// 6. 生成动态表头 - 使用机位名称作为表头
Row headerRow = sheet.createRow(0);
String[] headers = {"评价ID", "视频ID", "景区ID", "景区名称", "评价人ID", "评价人名称",
"评分", "文字评价", "机位评价", "创建时间", "更新时间"};
for (int i = 0; i < headers.length; i++) {
List<String> headerList = new ArrayList<>();
headerList.add("评价ID");
headerList.add("视频ID");
headerList.add("视频模板名称");
headerList.add("景区名称");
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("创建时间");
headerList.add("更新时间");
// 设置表头
for (int i = 0; i < headerList.size(); i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellValue(headerList.get(i));
cell.setCellStyle(headerStyle);
}
// 5. 填充数据
// 7. 填充数据
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());
int colIndex = 0;
// 机位评价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(colIndex++).setCellValue(review.getId());
row.createCell(colIndex++).setCellValue(review.getVideoId());
row.createCell(colIndex++).setCellValue(review.getTemplateName() != null ? review.getTemplateName() : "");
row.createCell(colIndex++).setCellValue(review.getScenicName());
row.createCell(colIndex++).setCellValue(review.getCreatorName());
row.createCell(colIndex++).setCellValue(review.getRating());
row.createCell(colIndex++).setCellValue(review.getContent());
// 机位评价列 - 按表头顺序填充
Map<String, Map<String, Integer>> cameraRating = review.getCameraPositionRating();
for (Long deviceId : sortedDeviceIds) {
String deviceIdStr = String.valueOf(deviceId);
Map<String, Integer> dimensions = null;
if (cameraRating != null && cameraRating.containsKey(deviceIdStr)) {
dimensions = cameraRating.get(deviceIdStr);
}
// 构建单元格内容: 只显示评分维度(不再重复机位名称)
StringBuilder cellContent = new StringBuilder();
if (dimensions != null && !dimensions.isEmpty()) {
// 按维度名排序,保证一致性
List<Map.Entry<String, Integer>> sortedDimensions = dimensions.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.collect(Collectors.toList());
boolean first = true;
for (Map.Entry<String, Integer> dimEntry : sortedDimensions) {
if (!first) {
cellContent.append("\n");
}
cellContent.append(dimEntry.getKey()).append("").append(dimEntry.getValue());
first = false;
}
}
Cell cell = row.createCell(colIndex++);
cell.setCellValue(cellContent.toString());
cell.setCellStyle(wrapStyle);
}
row.createCell(9).setCellValue(review.getCreateTime() != null ? sdf.format(review.getCreateTime()) : "");
row.createCell(10).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : "");
// 时间列
row.createCell(colIndex++).setCellValue(review.getCreateTime() != null ? sdf.format(review.getCreateTime()) : "");
row.createCell(colIndex).setCellValue(review.getUpdateTime() != null ? sdf.format(review.getUpdateTime()) : "");
}
// 6. 自动调整列宽
for (int i = 0; i < headers.length; i++) {
// 8. 自动调整列宽
for (int i = 0; i < headerList.size(); i++) {
sheet.autoSizeColumn(i);
// 对于机位列,设置最小宽度以便换行内容显示完整
if (i >= 7 && i < 7 + sortedDeviceIds.size()) {
int currentWidth = sheet.getColumnWidth(i);
sheet.setColumnWidth(i, Math.max(currentWidth, 5000)); // 最小25个字符宽度
}
}
// 7. 写入输出流
// 9. 写入输出流
workbook.write(outputStream);
workbook.close();
log.info("导出视频评价数据成功,共{}条", list.size());
log.info("导出视频评价数据成功,共{}条,机位数:{}", list.size(), sortedDeviceIds.size());
}
/**

View File

@@ -7,6 +7,8 @@
<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"/>
@@ -33,10 +35,13 @@
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
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>