diff --git a/src/main/java/com/petstore/controller/ReportController.java b/src/main/java/com/petstore/controller/ReportController.java
index 497f792..e0cf044 100644
--- a/src/main/java/com/petstore/controller/ReportController.java
+++ b/src/main/java/com/petstore/controller/ReportController.java
@@ -62,8 +62,19 @@ public class ReportController {
return "photo";
}
+ /**
+ * 报告 JSON 中与「服务回顾短片」相关的字段(与小程序 / H5 共用契约)。
+ *
+ * - {@code highlightVideoUrl} — 成片 MP4;库内为相对路径 {@code /api/upload/image/...} 或历史绝对 URL;接口统一为可访问绝对 URL。
+ * - {@code highlightShareCoverUrl} — 成片首帧 JPEG,用于分享卡片封面;截帧失败时为 {@code null},前端按 utils 中分享封面降级链处理。
+ * - {@code highlightPosterUrl} — 预留(画布海报等);当前恒为 {@code null},前端可用 {@code highlightShareCoverUrl}。
+ * - {@code highlightVideoStatus} — {@code processing} | {@code done} | {@code failed} 等。
+ *
+ */
private void putHighlightFields(Map target, Report r) {
target.put("highlightVideoUrl", r.getHighlightVideoUrl() != null ? fullUrl(r.getHighlightVideoUrl()) : null);
+ target.put("highlightShareCoverUrl", r.getHighlightShareCoverUrl() != null ? fullUrl(r.getHighlightShareCoverUrl()) : null);
+ target.put("highlightPosterUrl", null);
target.put("highlightVideoStatus", r.getHighlightVideoStatus());
target.put("highlightVideoError", r.getHighlightVideoError());
target.put("highlightDurationSec", r.getHighlightDurationSec());
diff --git a/src/main/java/com/petstore/entity/Report.java b/src/main/java/com/petstore/entity/Report.java
index bae7511..97469c3 100644
--- a/src/main/java/com/petstore/entity/Report.java
+++ b/src/main/java/com/petstore/entity/Report.java
@@ -75,4 +75,11 @@ public class Report {
/** 失败原因归类:material | service | network | unknown(对宠主展示,无技术细节) */
@Column(name = "highlight_fail_reason", length = 16)
private String highlightFailReason;
+
+ /**
+ * 成片首帧截图(竖屏内一帧),用于分享卡片封面等;相对路径如 {@code /api/upload/image/年/月/日/xxx_cover.jpg}。
+ * 与成片 MP4 同目录;仅当 highlight 状态为 done 且截帧成功时有值。
+ */
+ @Column(name = "highlight_share_cover_url", length = 500)
+ private String highlightShareCoverUrl;
}
diff --git a/src/main/java/com/petstore/service/ReportHighlightVideoService.java b/src/main/java/com/petstore/service/ReportHighlightVideoService.java
index f468062..142ea5a 100644
--- a/src/main/java/com/petstore/service/ReportHighlightVideoService.java
+++ b/src/main/java/com/petstore/service/ReportHighlightVideoService.java
@@ -143,6 +143,7 @@ public class ReportHighlightVideoService {
report.setHighlightVideoStatus("processing");
report.setHighlightVideoError(null);
report.setHighlightFailReason(null);
+ report.setHighlightShareCoverUrl(null);
report.setHighlightComposeMode(mode);
report.setUpdateTime(LocalDateTime.now());
reportMapper.save(report);
@@ -356,13 +357,20 @@ public class ReportHighlightVideoService {
Path finalPath = outDir.resolve(filename);
Files.copy(afterBgm, finalPath, StandardCopyOption.REPLACE_EXISTING);
- String publicBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
- String url = publicBase + "/api/upload/image/" + datePath + "/" + filename;
+ String videoRel = "/api/upload/image/" + datePath + "/" + filename;
+ String coverFilename = filename.endsWith(".mp4")
+ ? filename.substring(0, filename.length() - 4) + "_cover.jpg"
+ : filename + "_cover.jpg";
+ Path coverPath = outDir.resolve(coverFilename);
+ String coverRel = extractVideoCoverFrame(finalPath, coverPath)
+ ? "/api/upload/image/" + datePath + "/" + coverFilename
+ : null;
int durationRoundedSec = (int) Math.ceil(totalDurationSec);
synchronized (lockFor(reportId)) {
reportMapper.findByIdAndDeletedFalse(reportId).ifPresent(r -> {
- r.setHighlightVideoUrl(url);
+ r.setHighlightVideoUrl(videoRel);
+ r.setHighlightShareCoverUrl(coverRel);
r.setHighlightVideoStatus("done");
r.setHighlightVideoError(null);
r.setHighlightFailReason(null);
@@ -451,6 +459,26 @@ public class ReportHighlightVideoService {
/**
* 服务前第一张 | 服务后第一张 左右拼接为 1080×1920 竖屏对比图(左前右后)。
*/
+ /**
+ * 从成片 MP4 截取首帧为 JPEG,供分享卡片等使用。
+ *
+ * @return 是否写出有效文件
+ */
+ private boolean extractVideoCoverFrame(Path mp4, Path outJpg) {
+ try {
+ int code = runFfmpeg(List.of(
+ ffmpegBinary, "-y",
+ "-i", mp4.toString(),
+ "-map", "0:v:0",
+ "-frames:v", "1",
+ "-q:v", "2",
+ outJpg.toString()));
+ return code == 0 && Files.isRegularFile(outJpg);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
private boolean buildComparisonCover(Path beforeFile, Path afterFile, Path outJpg) throws Exception {
String fc = "[0:v]scale=540:1920:force_original_aspect_ratio=decrease,pad=540:1920:(ow-iw)/2:(oh-ih)/2,setsar=1[va];"
+ "[1:v]scale=540:1920:force_original_aspect_ratio=decrease,pad=540:1920:(ow-iw)/2:(oh-ih)/2,setsar=1[vb];"