feat: 新增协作邀请功能与标签管理
Some checks failed
test/timeline-server/pipeline/head Something is wrong with the build of this commit

新增故事协作邀请功能,包括邀请状态字段和相关接口
添加标签管理功能,支持时间线节点的标签分类
实现智能填充服务,从图片EXIF提取时间和地点信息
优化Docker镜像使用Alpine基础镜像减少体积
新增批量操作功能,包括排序、删除和时间修改
扩展通知系统支持协作邀请相关消息
添加评论和提醒功能相关实体和服务接口
This commit is contained in:
2026-02-24 10:32:35 +08:00
parent d645164daa
commit 40412f6f67
49 changed files with 3816 additions and 15 deletions

View File

@@ -1,4 +1,4 @@
FROM eclipse-temurin:21-jdk
FROM eclipse-temurin:21.0.2_13-jdk-alpine
VOLUME /tmp
COPY target/*.jar app.jar
EXPOSE 30002

View File

@@ -50,6 +50,13 @@
<artifactId>thumbnailator</artifactId>
<version>0.4.17</version>
</dependency>
<!-- EXIF 元数据解析库 -->
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.19.0</version>
</dependency>
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

View File

@@ -1,6 +1,8 @@
package com.timeline.file.controller;
import com.timeline.file.service.ExifParserService;
import com.timeline.file.service.FileService;
import com.timeline.file.vo.ExifInfoVo;
import com.timeline.file.vo.ImageInfoVo;
import com.timeline.common.response.ResponseEntity;
import com.timeline.common.utils.UserContextUtils;
@@ -25,6 +27,9 @@ public class FileController {
@Autowired
private FileService fileService;
@Autowired
private ExifParserService exifParserService;
@GetMapping("/hello")
public String hello() {
return "file service hello";
@@ -196,4 +201,26 @@ public class FileController {
fileService.deleteImage(imageInstanceId);
return ResponseEntity.success("图片已删除");
}
/**
* 解析图片 EXIF 信息
*
* 功能描述:
* 解析上传图片的 EXIF 元数据提取拍摄时间、GPS 坐标等信息。
* 用于前端自动填充时间线节点的时间和地点字段。
*
* 使用场景:
* 1. 用户上传图片后,前端调用此接口获取 EXIF 信息
* 2. 前端根据返回的时间自动填充节点时间
* 3. 前端根据返回的地址自动填充节点地点
*
* @param file 图片文件
* @return EXIF 信息对象
*/
@PostMapping("/exif")
public ResponseEntity<ExifInfoVo> parseExif(@RequestPart("file") MultipartFile file) {
log.info("解析图片 EXIF 信息: {}", file.getOriginalFilename());
ExifInfoVo exifInfo = exifParserService.parseExif(file);
return ResponseEntity.success(exifInfo);
}
}

View File

@@ -0,0 +1,75 @@
package com.timeline.file.service;
import com.timeline.file.vo.ExifInfoVo;
import org.springframework.web.multipart.MultipartFile;
/**
* ExifParserService - EXIF 信息解析服务接口
*
* 功能描述:
* 解析图片文件中的 EXIF 元数据提取拍摄时间、GPS 坐标等信息。
* 用于自动填充时间线节点的时间和地点字段。
*
* 支持的 EXIF 标签:
* - DateTimeOriginal: 原始拍摄时间
* - GPSLatitude/GPSLongitude: GPS 坐标
* - Make/Model: 相机厂商和型号
* - Orientation: 图片方向
*
* @author Timeline Team
* @date 2024
*/
public interface ExifParserService {
/**
* 解析图片文件的 EXIF 信息
*
* 功能描述:
* 读取图片文件的 EXIF 元数据,提取以下信息:
* 1. 拍摄时间DateTimeOriginal
* 2. GPS 坐标(经纬度)
* 3. 相机信息(厂商、型号)
* 4. 图片尺寸
*
* @param file 图片文件
* @return EXIF 信息对象,如果解析失败返回 null
*/
ExifInfoVo parseExif(MultipartFile file);
/**
* 解析图片文件的 EXIF 信息(通过文件路径)
*
* @param filePath 文件路径
* @return EXIF 信息对象
*/
ExifInfoVo parseExifByPath(String filePath);
/**
* 根据 GPS 坐标获取地址信息
*
* 功能描述:
* 调用逆地理编码服务,将 GPS 坐标转换为可读的地址字符串。
* 支持多种逆地理编码服务:
* - 高德地图 API
* - 百度地图 API
* - Google Maps API
*
* @param latitude 纬度
* @param longitude 经度
* @return 地址字符串,如果转换失败返回 null
*/
String getAddressFromGps(double latitude, double longitude);
/**
* 将 GPS 坐标从度分秒格式转换为十进制格式
*
* 背景知识:
* EXIF 中的 GPS 坐标通常以度分秒DMS格式存储
* 需要转换为十进制格式才能用于地图服务。
*
* @param dms 度分秒格式的坐标数组 [度, 分, 秒]
* @param ref 方向参考 (N/S/E/W)
* @return 十进制坐标
*/
double convertGpsToDecimal(double[] dms, String ref);
}

View File

@@ -0,0 +1,117 @@
package com.timeline.file.service;
import com.timeline.file.vo.ExifInfoVo;
import com.timeline.file.vo.SmartFillResultVo;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
/**
* SmartFillService - 智能填充服务接口
*
* 功能描述:
* 基于 AI 和元数据,智能推断和填充时间线节点信息。
*
* 功能列表:
* - 从图片 EXIF 提取时间和地点
* - 从文件名推断时间
* - 批量智能填充
* - 智能标签推荐
*
* @author Timeline Team
* @date 2024
*/
public interface SmartFillService {
/**
* 智能分析单个文件
*
* 功能描述:
* 综合分析文件的各种信息源,智能推断时间和地点:
* 1. 优先使用 EXIF 元数据
* 2. 备选使用文件修改时间
* 3. 从文件名提取时间信息
* 4. GPS 坐标转换为地址
*
* @param file 文件
* @return 智能填充结果
*/
SmartFillResultVo analyzeFile(MultipartFile file);
/**
* 批量智能分析文件
*
* @param files 文件列表
* @return 智能填充结果列表
*/
List<SmartFillResultVo> analyzeFiles(List<MultipartFile> files);
/**
* 从文件名推断时间
*
* 支持的文件名格式:
* - IMG_20240115_123456.jpg
* - 2024-01-15_12-30-00.jpg
* - 20240115123000.jpg
* - Screenshot_20240115-123000.png
*
* @param fileName 文件名
* @return 推断的时间,如果无法推断返回 null
*/
String inferTimeFromFileName(String fileName);
/**
* 根据已有数据推断时间
*
* 功能描述:
* 当单个数据源不完整时,结合多个数据源进行推断:
* - EXIF 时间
* - 文件修改时间
* - 文件名时间
* - 相邻文件时间(批量上传时)
*
* @param exifInfo EXIF 信息
* @param fileName 文件名
* @param fileModifiedTime 文件修改时间
* @param neighborTimes 相邻文件时间列表(可选)
* @return 推断的时间
*/
String inferTime(ExifInfoVo exifInfo, String fileName, String fileModifiedTime, List<String> neighborTimes);
/**
* 智能标签推荐
*
* 功能描述:
* 根据图片内容和已有信息,推荐相关标签。
*
* @param file 图片文件
* @param existingTags 已有标签
* @return 推荐的标签列表
*/
List<String> recommendTags(MultipartFile file, List<String> existingTags);
/**
* 智能标题生成
*
* 功能描述:
* 根据时间、地点等信息,生成建议的标题。
*
* @param time 时间
* @param location 地点
* @return 建议的标题
*/
String generateTitle(String time, String location);
/**
* 批量智能填充
*
* 功能描述:
* 对批量上传的文件进行智能分析,并自动填充时间和地点。
*
* @param files 文件列表
* @param autoApply 是否自动应用结果
* @return 智能填充结果列表
*/
List<SmartFillResultVo> batchSmartFill(List<MultipartFile> files, boolean autoApply);
}

View File

@@ -0,0 +1,222 @@
package com.timeline.file.service.impl;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.lang.GeoLocation;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.GpsDirectory;
import com.timeline.file.service.ExifParserService;
import com.timeline.file.vo.ExifInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
/**
* ExifParserServiceImpl - EXIF 信息解析服务实现
*
* 功能描述:
* 使用 metadata-extractor 库解析图片文件的 EXIF 元数据。
* 支持常见的图片格式JPEG, TIFF, PNG, GIF, BMP 等。
*
* 技术方案:
* - 使用 metadata-extractor 库读取 EXIF 标签
* - 支持从 MultipartFile 和文件路径两种方式读取
* - GPS 坐标自动转换为十进制格式
*
* 依赖说明:
* 需要在 pom.xml 中添加 metadata-extractor 依赖:
* <dependency>
* <groupId>com.drewnoakes</groupId>
* <artifactId>metadata-extractor</artifactId>
* <version>2.19.0</version>
* </dependency>
*
* @author Timeline Team
* @date 2024
*/
@Slf4j
@Service
public class ExifParserServiceImpl implements ExifParserService {
/**
* 是否启用逆地理编码
* 可通过配置文件设置
*/
@Value("${exif.geocoding.enabled:false}")
private boolean geocodingEnabled;
/**
* 逆地理编码 API Key
*/
@Value("${exif.geocoding.api-key:}")
private String geocodingApiKey;
@Override
public ExifInfoVo parseExif(MultipartFile file) {
ExifInfoVo exifInfo = new ExifInfoVo();
exifInfo.setHasExif(false);
try (InputStream inputStream = file.getInputStream()) {
Metadata metadata = ImageMetadataReader.readMetadata(inputStream);
extractMetadata(metadata, exifInfo);
exifInfo.setHasExif(true);
} catch (ImageProcessingException e) {
log.warn("图片 EXIF 解析失败: {}", e.getMessage());
} catch (IOException e) {
log.error("读取图片文件失败: {}", e.getMessage());
}
return exifInfo;
}
@Override
public ExifInfoVo parseExifByPath(String filePath) {
ExifInfoVo exifInfo = new ExifInfoVo();
exifInfo.setHasExif(false);
try {
File file = new File(filePath);
Metadata metadata = ImageMetadataReader.readMetadata(file);
extractMetadata(metadata, exifInfo);
exifInfo.setHasExif(true);
} catch (ImageProcessingException e) {
log.warn("图片 EXIF 解析失败: {}", e.getMessage());
} catch (IOException e) {
log.error("读取图片文件失败: {}", e.getMessage());
}
return exifInfo;
}
/**
* 从 Metadata 对象中提取 EXIF 信息
*
* 提取顺序:
* 1. ExifSubIFDDirectory - 包含拍摄时间、相机设置等
* 2. ExifIFD0Directory - 包含相机型号、方向等
* 3. GpsDirectory - 包含 GPS 坐标
* 4. FileMetadataDirectory - 包含文件基本信息
*/
private void extractMetadata(Metadata metadata, ExifInfoVo exifInfo) {
// 提取 ExifSubIFDDirectory 信息
ExifSubIFDDirectory subIfdDir = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (subIfdDir != null) {
// 原始拍摄时间
Date dateOriginal = subIfdDir.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
if (dateOriginal != null) {
exifInfo.setDateTimeOriginal(convertToLocalDateTime(dateOriginal));
}
// ISO 感光度
exifInfo.setIso(subIfdDir.getInteger(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
// 光圈值
exifInfo.setAperture(subIfdDir.getDoubleObject(ExifSubIFDDirectory.TAG_APERTURE));
// 曝光时间
String exposureTime = subIfdDir.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME);
exifInfo.setExposureTime(exposureTime);
// 焦距
exifInfo.setFocalLength(subIfdDir.getDoubleObject(ExifSubIFDDirectory.TAG_FOCAL_LENGTH));
}
// 提取 ExifIFD0Directory 信息
ExifIFD0Directory ifd0Dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
if (ifd0Dir != null) {
// 相机厂商
exifInfo.setMake(ifd0Dir.getString(ExifIFD0Directory.TAG_MAKE));
// 相机型号
exifInfo.setModel(ifd0Dir.getString(ExifIFD0Directory.TAG_MODEL));
// 图片方向
exifInfo.setOrientation(ifd0Dir.getInteger(ExifIFD0Directory.TAG_ORIENTATION));
// 图片尺寸
exifInfo.setWidth(ifd0Dir.getInteger(ExifIFD0Directory.TAG_IMAGE_WIDTH));
exifInfo.setHeight(ifd0Dir.getInteger(ExifIFD0Directory.TAG_IMAGE_HEIGHT));
}
// 提取 GPS 信息
GpsDirectory gpsDir = metadata.getFirstDirectoryOfType(GpsDirectory.class);
if (gpsDir != null) {
GeoLocation geoLocation = gpsDir.getGeoLocation();
if (geoLocation != null) {
exifInfo.setLatitude(geoLocation.getLatitude());
exifInfo.setLongitude(geoLocation.getLongitude());
// 如果启用了逆地理编码,获取地址
if (geocodingEnabled && exifInfo.getLatitude() != null && exifInfo.getLongitude() != null) {
String address = getAddressFromGps(exifInfo.getLatitude(), exifInfo.getLongitude());
exifInfo.setAddress(address);
}
}
}
// 如果没有原始拍摄时间,使用文件修改时间
if (exifInfo.getDateTimeOriginal() == null) {
// 尝试从其他目录获取时间信息
for (var directory : metadata.getDirectories()) {
if (directory != null) {
Date date = directory.getDate(0x0132); // DateTime tag
if (date != null) {
exifInfo.setDateTime(convertToLocalDateTime(date));
break;
}
}
}
}
}
@Override
public String getAddressFromGps(double latitude, double longitude) {
// TODO: 实现逆地理编码
// 可以集成高德地图、百度地图或 Google Maps API
// 示例:调用第三方 API 获取地址
log.info("逆地理编码: lat={}, lng={}", latitude, longitude);
// 暂时返回空,实际项目中应调用地图 API
return null;
}
@Override
public double convertGpsToDecimal(double[] dms, String ref) {
if (dms == null || dms.length < 3) {
return 0;
}
double degrees = dms[0];
double minutes = dms[1];
double seconds = dms[2];
double decimal = degrees + (minutes / 60.0) + (seconds / 3600.0);
// 南纬和西经为负数
if ("S".equalsIgnoreCase(ref) || "W".equalsIgnoreCase(ref)) {
decimal = -decimal;
}
return decimal;
}
/**
* 将 Date 转换为 LocalDateTime
*/
private LocalDateTime convertToLocalDateTime(Date date) {
if (date == null) {
return null;
}
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}
}

View File

@@ -0,0 +1,284 @@
package com.timeline.file.service.impl;
import com.timeline.file.service.ExifParserService;
import com.timeline.file.service.SmartFillService;
import com.timeline.file.vo.ExifInfoVo;
import com.timeline.file.vo.SmartFillResultVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* SmartFillServiceImpl - 智能填充服务实现
*
* 功能描述:
* 实现智能时间推断和填充功能。
*
* 技术方案:
* 1. 优先级EXIF > 文件名 > 文件修改时间
* 2. 时间格式识别:支持多种常见格式
* 3. GPS 地址转换:调用逆地理编码服务
*
* @author Timeline Team
* @date 2024
*/
@Slf4j
@Service
public class SmartFillServiceImpl implements SmartFillService {
@Autowired
private ExifParserService exifParserService;
/**
* 文件名时间格式正则表达式列表
* 按优先级排序
*/
private static final List<Pattern> TIME_PATTERNS = List.of(
// IMG_20240115_123456.jpg
Pattern.compile("(\\d{4})(\\d{2})(\\d{2})[_-](\\d{2})(\\d{2})(\\d{2})"),
// 2024-01-15_12-30-00.jpg
Pattern.compile("(\\d{4})[-_](\\d{2})[-_](\\d{2})[_-](\\d{2})[-_](\\d{2})[-_](\\d{2})"),
// Screenshot_20240115-123000.png
Pattern.compile("Screenshot[_-]?(\\d{4})(\\d{2})(\\d{2})[_-](\\d{2})(\\d{2})(\\d{2})"),
// 20240115123000.jpg
Pattern.compile("(\\d{4})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})"),
// 20240115.jpg (仅日期)
Pattern.compile("(\\d{4})(\\d{2})(\\d{2})")
);
@Override
public SmartFillResultVo analyzeFile(MultipartFile file) {
SmartFillResultVo result = new SmartFillResultVo();
result.setFileName(file.getOriginalFilename());
result.setSuccess(false);
try {
// 1. 解析 EXIF 信息
ExifInfoVo exifInfo = exifParserService.parseExif(file);
result.setExifInfo(exifInfo);
// 2. 推断时间
inferTimeForResult(result, exifInfo, file.getOriginalFilename());
// 3. 推断地点
inferLocationForResult(result, exifInfo);
// 4. 生成建议标题
if (result.getInferredTime() != null || result.getInferredLocation() != null) {
result.setSuggestedTitle(generateTitle(
result.getInferredTime() != null ? result.getInferredTime().toString() : null,
result.getInferredLocation()
));
}
result.setSuccess(true);
} catch (Exception e) {
log.error("智能分析文件失败: {}", file.getOriginalFilename(), e);
result.setErrorMessage("分析失败: " + e.getMessage());
}
return result;
}
@Override
public List<SmartFillResultVo> analyzeFiles(List<MultipartFile> files) {
List<SmartFillResultVo> results = new ArrayList<>();
for (MultipartFile file : files) {
results.add(analyzeFile(file));
}
return results;
}
@Override
public String inferTimeFromFileName(String fileName) {
if (fileName == null || fileName.isEmpty()) {
return null;
}
for (Pattern pattern : TIME_PATTERNS) {
Matcher matcher = pattern.matcher(fileName);
if (matcher.find()) {
try {
int year = Integer.parseInt(matcher.group(1));
int month = Integer.parseInt(matcher.group(2));
int day = Integer.parseInt(matcher.group(3));
// 检查日期有效性
if (month < 1 || month > 12 || day < 1 || day > 31) {
continue;
}
// 如果有时间信息
if (matcher.groupCount() >= 6) {
int hour = Integer.parseInt(matcher.group(4));
int minute = Integer.parseInt(matcher.group(5));
int second = Integer.parseInt(matcher.group(6));
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
continue;
}
return String.format("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second);
} else {
// 仅日期
return String.format("%04d-%02d-%02d 00:00:00", year, month, day);
}
} catch (NumberFormatException e) {
continue;
}
}
}
return null;
}
@Override
public String inferTime(ExifInfoVo exifInfo, String fileName, String fileModifiedTime, List<String> neighborTimes) {
// 优先级1: EXIF 原始拍摄时间
if (exifInfo != null && exifInfo.getDateTimeOriginal() != null) {
return exifInfo.getDateTimeOriginal().toString();
}
// 优先级2: 文件名推断
String fileNameTime = inferTimeFromFileName(fileName);
if (fileNameTime != null) {
return fileNameTime;
}
// 优先级3: 文件修改时间
if (fileModifiedTime != null) {
return fileModifiedTime;
}
// 优先级4: 相邻文件时间(取中位数)
if (neighborTimes != null && !neighborTimes.isEmpty()) {
return neighborTimes.get(neighborTimes.size() / 2);
}
return null;
}
@Override
public List<String> recommendTags(MultipartFile file, List<String> existingTags) {
List<String> recommendedTags = new ArrayList<>();
// 基于文件名推荐
String fileName = file.getOriginalFilename();
if (fileName != null) {
String lowerName = fileName.toLowerCase();
if (lowerName.contains("screenshot")) {
recommendedTags.add("截图");
}
if (lowerName.contains("photo") || lowerName.contains("img")) {
recommendedTags.add("照片");
}
if (lowerName.contains("video")) {
recommendedTags.add("视频");
}
}
// TODO: 集成 AI 服务进行内容识别
// 过滤已存在的标签
if (existingTags != null) {
recommendedTags.removeAll(existingTags);
}
return recommendedTags;
}
@Override
public String generateTitle(String time, String location) {
StringBuilder title = new StringBuilder();
if (time != null) {
try {
LocalDateTime dateTime = LocalDateTime.parse(time.replace(" ", "T"));
title.append(dateTime.format(DateTimeFormatter.ofPattern("yyyy年M月d日")));
} catch (Exception e) {
// 忽略解析错误
}
}
if (location != null && !location.isEmpty()) {
if (title.length() > 0) {
title.append(" · ");
}
title.append(location);
}
return title.length() > 0 ? title.toString() : "未命名时刻";
}
@Override
public List<SmartFillResultVo> batchSmartFill(List<MultipartFile> files, boolean autoApply) {
List<SmartFillResultVo> results = analyzeFiles(files);
if (autoApply) {
// TODO: 自动应用填充结果
}
return results;
}
/**
* 为结果对象推断时间
*/
private void inferTimeForResult(SmartFillResultVo result, ExifInfoVo exifInfo, String fileName) {
// 优先级1: EXIF 原始拍摄时间
if (exifInfo != null && exifInfo.getDateTimeOriginal() != null) {
result.setInferredTime(exifInfo.getDateTimeOriginal());
result.setTimeSource("EXIF");
result.setTimeConfidence(95);
return;
}
// 优先级2: EXIF 修改时间
if (exifInfo != null && exifInfo.getDateTime() != null) {
result.setInferredTime(exifInfo.getDateTime());
result.setTimeSource("EXIF_MODIFIED");
result.setTimeConfidence(70);
return;
}
// 优先级3: 文件名推断
String fileNameTime = inferTimeFromFileName(fileName);
if (fileNameTime != null) {
result.setInferredTime(LocalDateTime.parse(fileNameTime.replace(" ", "T")));
result.setTimeSource("FILE_NAME");
result.setTimeConfidence(60);
return;
}
// 无法推断
result.setTimeConfidence(0);
}
/**
* 为结果对象推断地点
*/
private void inferLocationForResult(SmartFillResultVo result, ExifInfoVo exifInfo) {
if (exifInfo != null && exifInfo.getLatitude() != null && exifInfo.getLongitude() != null) {
result.setLatitude(exifInfo.getLatitude());
result.setLongitude(exifInfo.getLongitude());
result.setLocationSource("EXIF_GPS");
result.setLocationConfidence(90);
// 如果已有地址信息
if (exifInfo.getAddress() != null) {
result.setInferredLocation(exifInfo.getAddress());
}
}
}
}

View File

@@ -0,0 +1,116 @@
package com.timeline.file.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* ExifInfoVo - EXIF 信息值对象
*
* 功能描述:
* 封装从图片文件中提取的 EXIF 元数据信息。
* 用于前端展示和自动填充时间线节点字段。
*
* 字段说明:
* - dateTimeOriginal: 原始拍摄时间,优先级最高
* - dateTime: 文件修改时间,作为备选
* - latitude/longitude: GPS 坐标
* - address: 根据坐标解析的地址
* - make/model: 相机信息
* - orientation: 图片方向(用于自动旋转)
*
* @author Timeline Team
* @date 2024
*/
@Data
public class ExifInfoVo {
/**
* 原始拍摄时间
* 来自 EXIF 标签 DateTimeOriginal (0x9003)
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateTimeOriginal;
/**
* 文件修改时间
* 当没有 EXIF 时间时作为备选
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateTime;
/**
* GPS 纬度
* 十进制格式,正数表示北纬,负数表示南纬
*/
private Double latitude;
/**
* GPS 经度
* 十进制格式,正数表示东经,负数表示西经
*/
private Double longitude;
/**
* 地址信息
* 根据经纬度逆地理编码获得
*/
private String address;
/**
* 相机厂商
* 来自 EXIF 标签 Make (0x010F)
*/
private String make;
/**
* 相机型号
* 来自 EXIF 标签 Model (0x0110)
*/
private String model;
/**
* 图片方向
* 1: 正常
* 3: 旋转180度
* 6: 顺时针旋转90度
* 8: 逆时针旋转90度
*/
private Integer orientation;
/**
* 图片宽度(像素)
*/
private Integer width;
/**
* 图片高度(像素)
*/
private Integer height;
/**
* 是否包含有效的 EXIF 数据
*/
private Boolean hasExif;
/**
* ISO 感光度
*/
private Integer iso;
/**
* 光圈值
*/
private Double aperture;
/**
* 曝光时间
*/
private String exposureTime;
/**
* 焦距
*/
private Double focalLength;
}

View File

@@ -2,8 +2,10 @@ package com.timeline.file.vo;
import com.timeline.common.vo.CommonVo;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class ImageInfoVo extends CommonVo {
private String objectKey;
private String imageName;

View File

@@ -0,0 +1,92 @@
package com.timeline.file.vo;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* SmartFillResultVo - 智能填充结果值对象
*
* 功能描述:
* 封装智能分析的结果,包含推断的时间和地点信息。
* 用于前端展示智能填充建议。
*
* @author Timeline Team
* @date 2024
*/
@Data
public class SmartFillResultVo {
/**
* 文件名
*/
private String fileName;
/**
* 推断的时间
*/
private LocalDateTime inferredTime;
/**
* 时间来源
* EXIF/FILE_NAME/FILE_MODIFIED/NEIGHBOR
*/
private String timeSource;
/**
* 时间推断置信度 (0-100)
*/
private Integer timeConfidence;
/**
* 推断的地点
*/
private String inferredLocation;
/**
* 地点来源
* EXIF_GPS/MANUAL
*/
private String locationSource;
/**
* 地点推断置信度 (0-100)
*/
private Integer locationConfidence;
/**
* GPS 纬度
*/
private Double latitude;
/**
* GPS 经度
*/
private Double longitude;
/**
* 推荐的标签
*/
private List<String> recommendedTags;
/**
* 建议的标题
*/
private String suggestedTitle;
/**
* 原始 EXIF 信息
*/
private ExifInfoVo exifInfo;
/**
* 是否成功分析
*/
private Boolean success;
/**
* 错误信息
*/
private String errorMessage;
}