feat: 增强TMDB刮削服务并新增用户资料功能
- TMDB刮削服务增强: 添加刮削状态跟踪(scraping/success/failed), 支持横版背景图下载和存储, 优化缓存策略和错误处理 - 新增每日推荐功能: 添加DailyRecommendDO数据对象及Mapper和Repository - 新增用户资料管理: 添加ProfileController用户资料控制器, 支持头像上传(MinIO配置) - 配置优化: 移除默认加密密码使用环境变量, 调整日志级别为INFO, 移除SQL日志输出 - 数据库增强: Movie和DriveMovie实体添加背景图、刮削状态等字段, 更新相关Mapper和Repository方法
This commit is contained in:
parent
fba79655c4
commit
ab4ab5b797
@ -0,0 +1,59 @@
|
||||
package com.wangpan.common.dal.dataobject;
|
||||
|
||||
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 java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 每日推荐缓存数据对象
|
||||
* 对应数据库表:wp_daily_recommend
|
||||
*
|
||||
* 设计说明:
|
||||
* - 每个用户每天缓存一组推荐电影(3部)
|
||||
* - 当天多次请求返回相同的推荐结果
|
||||
* - 第二天自动重新随机生成
|
||||
*/
|
||||
@TableName(value = "wp_daily_recommend", autoResultMap = true)
|
||||
public class DailyRecommendDO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键,自增长 */
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 推荐日期(格式:yyyy-MM-dd) */
|
||||
private LocalDate recommendDate;
|
||||
|
||||
/** 推荐的电影ID列表(逗号分隔,如 "1,5,12") */
|
||||
private String movieIds;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
// ==================== Getter 和 Setter ====================
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public LocalDate getRecommendDate() { return recommendDate; }
|
||||
public void setRecommendDate(LocalDate recommendDate) { this.recommendDate = recommendDate; }
|
||||
|
||||
public String getMovieIds() { return movieIds; }
|
||||
public void setMovieIds(String movieIds) { this.movieIds = movieIds; }
|
||||
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
}
|
||||
@ -77,12 +77,21 @@ public class DriveMovieDO implements Serializable {
|
||||
/** 刮削类型:tmdb / manual / none */
|
||||
private String scrapeType;
|
||||
|
||||
/** 刮削状态:pending / scraping / success / failed */
|
||||
private String scrapeStatus;
|
||||
|
||||
/** 刮削失败原因 */
|
||||
private String scrapeError;
|
||||
|
||||
/** 评分(0-100) */
|
||||
private Integer rating;
|
||||
|
||||
/** 海报 URL(加密存储,DO 中为解密后的明文) */
|
||||
/** 海报 URL(竖版,加密存储,DO 中为解密后的明文) */
|
||||
private String posterUrl;
|
||||
|
||||
/** 背景图 URL(横版,加密存储,DO 中为解密后的明文) */
|
||||
private String backdropUrl;
|
||||
|
||||
/** 标签(逗号分隔) */
|
||||
private String tags;
|
||||
|
||||
@ -150,12 +159,21 @@ public class DriveMovieDO implements Serializable {
|
||||
public String getScrapeType() { return scrapeType; }
|
||||
public void setScrapeType(String scrapeType) { this.scrapeType = scrapeType; }
|
||||
|
||||
public String getScrapeStatus() { return scrapeStatus; }
|
||||
public void setScrapeStatus(String scrapeStatus) { this.scrapeStatus = scrapeStatus; }
|
||||
|
||||
public String getScrapeError() { return scrapeError; }
|
||||
public void setScrapeError(String scrapeError) { this.scrapeError = scrapeError; }
|
||||
|
||||
public Integer getRating() { return rating; }
|
||||
public void setRating(Integer rating) { this.rating = rating; }
|
||||
|
||||
public String getPosterUrl() { return posterUrl; }
|
||||
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
||||
|
||||
public String getBackdropUrl() { return backdropUrl; }
|
||||
public void setBackdropUrl(String backdropUrl) { this.backdropUrl = backdropUrl; }
|
||||
|
||||
public String getTags() { return tags; }
|
||||
public void setTags(String tags) { this.tags = tags; }
|
||||
|
||||
|
||||
@ -50,12 +50,21 @@ public class MovieDO implements Serializable {
|
||||
/** 刮削类型:auto / manual / none */
|
||||
private String scrapeType;
|
||||
|
||||
/** 刮削状态:pending / scraping / success / failed */
|
||||
private String scrapeStatus;
|
||||
|
||||
/** 刮削失败原因 */
|
||||
private String scrapeError;
|
||||
|
||||
/** 评分(0-100) */
|
||||
private Integer rating;
|
||||
|
||||
/** 海报图片URL */
|
||||
/** 海报图片URL(竖版) */
|
||||
private String posterUrl;
|
||||
|
||||
/** 背景图URL(横版) */
|
||||
private String backdropUrl;
|
||||
|
||||
/** 标签(逗号分隔) */
|
||||
private String tags;
|
||||
|
||||
@ -96,12 +105,21 @@ public class MovieDO implements Serializable {
|
||||
public String getScrapeType() { return scrapeType; }
|
||||
public void setScrapeType(String scrapeType) { this.scrapeType = scrapeType; }
|
||||
|
||||
public String getScrapeStatus() { return scrapeStatus; }
|
||||
public void setScrapeStatus(String scrapeStatus) { this.scrapeStatus = scrapeStatus; }
|
||||
|
||||
public String getScrapeError() { return scrapeError; }
|
||||
public void setScrapeError(String scrapeError) { this.scrapeError = scrapeError; }
|
||||
|
||||
public Integer getRating() { return rating; }
|
||||
public void setRating(Integer rating) { this.rating = rating; }
|
||||
|
||||
public String getPosterUrl() { return posterUrl; }
|
||||
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
||||
|
||||
public String getBackdropUrl() { return backdropUrl; }
|
||||
public void setBackdropUrl(String backdropUrl) { this.backdropUrl = backdropUrl; }
|
||||
|
||||
public String getTags() { return tags; }
|
||||
public void setTags(String tags) { this.tags = tags; }
|
||||
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
package com.wangpan.common.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.wangpan.common.dal.dataobject.DailyRecommendDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
/**
|
||||
* 每日推荐缓存 Mapper 接口
|
||||
*
|
||||
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||
*/
|
||||
@Mapper
|
||||
public interface DailyRecommendMapper extends BaseMapper<DailyRecommendDO> {
|
||||
|
||||
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||
String CREATE_TABLE_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS wp_daily_recommend (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
recommend_date DATE NOT NULL COMMENT '推荐日期(yyyy-MM-dd)',
|
||||
movie_ids VARCHAR(200) NOT NULL COMMENT '推荐电影ID列表(逗号分隔)',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
UNIQUE INDEX uk_user_date (user_id, recommend_date),
|
||||
INDEX idx_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日推荐缓存表'
|
||||
""";
|
||||
|
||||
/**
|
||||
* 根据用户ID和日期查询当日推荐缓存
|
||||
*/
|
||||
@Select("SELECT * FROM wp_daily_recommend WHERE user_id = #{userId} AND recommend_date = #{recommendDate}")
|
||||
DailyRecommendDO findByUserAndDate(@Param("userId") Long userId, @Param("recommendDate") String recommendDate);
|
||||
}
|
||||
@ -38,8 +38,11 @@ public interface DriveMovieMapper extends BaseMapper<DriveMovieDO> {
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active/deleted',
|
||||
category VARCHAR(50) DEFAULT NULL COMMENT '分类',
|
||||
scrape_type VARCHAR(20) DEFAULT 'none' COMMENT '刮削类型:tmdb/manual/none',
|
||||
scrape_status VARCHAR(20) DEFAULT 'pending' COMMENT '刮削状态:pending/scraping/success/failed',
|
||||
scrape_error VARCHAR(500) DEFAULT NULL COMMENT '刮削失败原因',
|
||||
rating INT DEFAULT NULL COMMENT '评分(0-100)',
|
||||
poster_url TEXT DEFAULT NULL COMMENT '海报URL(加密存储)',
|
||||
poster_url TEXT DEFAULT NULL COMMENT '海报URL(竖版,加密存储)',
|
||||
backdrop_url TEXT DEFAULT NULL COMMENT '背景图URL(横版,加密存储)',
|
||||
tags VARCHAR(500) DEFAULT NULL COMMENT '标签(逗号分隔)',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
@ -48,6 +48,12 @@ public interface MovieMapper extends BaseMapper<MovieDO> {
|
||||
@Update("UPDATE wp_drive_movie SET poster_url = #{posterUrl}, update_time = NOW() WHERE id = #{id}")
|
||||
int updatePosterUrl(@Param("id") Long id, @Param("posterUrl") String posterUrl);
|
||||
|
||||
/**
|
||||
* 更新电影背景图URL(横版)
|
||||
*/
|
||||
@Update("UPDATE wp_drive_movie SET backdrop_url = #{backdropUrl}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateBackdropUrl(@Param("id") Long id, @Param("backdropUrl") String backdropUrl);
|
||||
|
||||
/**
|
||||
* 更新电影标签
|
||||
*/
|
||||
@ -59,4 +65,10 @@ public interface MovieMapper extends BaseMapper<MovieDO> {
|
||||
*/
|
||||
@Update("UPDATE wp_drive_movie SET scrape_type = #{scrapeType}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateScrapeType(@Param("id") Long id, @Param("scrapeType") String scrapeType);
|
||||
|
||||
/**
|
||||
* 更新刮削状态和失败原因
|
||||
*/
|
||||
@Update("UPDATE wp_drive_movie SET scrape_status = #{scrapeStatus}, scrape_error = #{scrapeError}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateScrapeStatus(@Param("id") Long id, @Param("scrapeStatus") String scrapeStatus, @Param("scrapeError") String scrapeError);
|
||||
}
|
||||
|
||||
@ -49,8 +49,10 @@ public interface TmdbScraperLogMapper extends BaseMapper<TmdbScraperLogDO> {
|
||||
/**
|
||||
* 分页查询最近的执行记录
|
||||
* 复杂SQL,在XML中实现
|
||||
* @param offset 偏移量(从0开始)
|
||||
* @param pageSize 每页条数
|
||||
*/
|
||||
Map<String, Object> queryRecent(@Param("pageNo") int pageNo, @Param("pageSize") int pageSize);
|
||||
List<Map<String, Object>> queryRecent(@Param("offset") int offset, @Param("pageSize") int pageSize);
|
||||
|
||||
/**
|
||||
* 查询最近的N条执行记录
|
||||
|
||||
@ -98,4 +98,16 @@ public interface UserMapper extends BaseMapper<UserDO> {
|
||||
*/
|
||||
@Update("UPDATE wp_user SET is_deleted = 1, update_time = NOW() WHERE id = #{userId} AND is_deleted = 0")
|
||||
int softDelete(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 更新用户名(需先检查唯一性)
|
||||
*/
|
||||
@Update("UPDATE wp_user SET username = #{username}, update_time = NOW() WHERE id = #{userId} AND is_deleted = 0")
|
||||
int updateUsername(@Param("userId") Long userId, @Param("username") String username);
|
||||
|
||||
/**
|
||||
* 更新用户头像URL
|
||||
*/
|
||||
@Update("UPDATE wp_user SET avatar_url = #{avatarUrl}, update_time = NOW() WHERE id = #{userId} AND is_deleted = 0")
|
||||
int updateAvatarUrl(@Param("userId") Long userId, @Param("avatarUrl") String avatarUrl);
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
package com.wangpan.common.dal.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.wangpan.common.dal.dataobject.DailyRecommendDO;
|
||||
import com.wangpan.common.dal.mapper.DailyRecommendMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 每日推荐缓存 Repository 层
|
||||
*
|
||||
* 职责:
|
||||
* 1. 查询用户当日推荐缓存
|
||||
* 2. 保存/更新用户当日推荐缓存
|
||||
*/
|
||||
@Repository
|
||||
public class DailyRecommendRepository extends ServiceImpl<DailyRecommendMapper, DailyRecommendDO> {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
/**
|
||||
* 查询用户当日推荐缓存
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 缓存记录,不存在返回 null
|
||||
*/
|
||||
public DailyRecommendDO findTodayRecommend(Long userId) {
|
||||
String today = LocalDate.now().format(DATE_FORMATTER);
|
||||
return baseMapper.findByUserAndDate(userId, today);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户当日推荐缓存
|
||||
* 如果当天已有缓存则更新,否则插入新记录
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param movieIds 推荐电影ID列表(逗号分隔)
|
||||
*/
|
||||
public void saveOrUpdateTodayRecommend(Long userId, String movieIds) {
|
||||
String today = LocalDate.now().format(DATE_FORMATTER);
|
||||
var existing = baseMapper.findByUserAndDate(userId, today);
|
||||
|
||||
if (existing != null) {
|
||||
// 更新已有缓存
|
||||
existing.setMovieIds(movieIds);
|
||||
updateById(existing);
|
||||
} else {
|
||||
// 插入新缓存
|
||||
var record = new DailyRecommendDO();
|
||||
record.setUserId(userId);
|
||||
record.setRecommendDate(LocalDate.now());
|
||||
record.setMovieIds(movieIds);
|
||||
save(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,6 +91,62 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
||||
.eq(DriveMovieDO::getStatus, "active"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户所有电影的文件夹ID列表(去重,分页)
|
||||
* 使用 MyBatis-Plus LambdaQueryWrapper 构建查询
|
||||
*/
|
||||
public List<Long> queryFolderIdsByUser(Long userId, int pageNo, int pageSize) {
|
||||
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||
.select(DriveMovieDO::getFolderId)
|
||||
.eq(DriveMovieDO::getUserId, userId)
|
||||
.eq(DriveMovieDO::getStatus, "active")
|
||||
.groupBy(DriveMovieDO::getFolderId)
|
||||
.orderByAsc(DriveMovieDO::getFolderId)
|
||||
.last("LIMIT " + pageSize + " OFFSET " + (pageNo - 1) * pageSize);
|
||||
|
||||
var list = list(wrapper);
|
||||
var folderIds = new ArrayList<Long>();
|
||||
for (var movieDO : list) {
|
||||
if (movieDO.getFolderId() != null) {
|
||||
folderIds.add(movieDO.getFolderId());
|
||||
}
|
||||
}
|
||||
return folderIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户所有电影的不同文件夹数量
|
||||
*/
|
||||
public int countFoldersByUser(Long userId) {
|
||||
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||
.select(DriveMovieDO::getFolderId)
|
||||
.eq(DriveMovieDO::getUserId, userId)
|
||||
.eq(DriveMovieDO::getStatus, "active")
|
||||
.groupBy(DriveMovieDO::getFolderId);
|
||||
|
||||
return (int) count(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定文件夹下的电影列表(限制数量)
|
||||
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询
|
||||
*/
|
||||
public List<Map<String, Object>> queryByFolder(Long userId, Long folderId, int limit) {
|
||||
var page = new Page<DriveMovieDO>(1, limit);
|
||||
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||
.eq(DriveMovieDO::getUserId, userId)
|
||||
.eq(DriveMovieDO::getStatus, "active")
|
||||
.eq(DriveMovieDO::getFolderId, folderId)
|
||||
.orderByDesc(DriveMovieDO::getCreateTime);
|
||||
|
||||
var result = page(page, wrapper);
|
||||
var list = new ArrayList<Map<String, Object>>();
|
||||
for (var movieDO : result.getRecords()) {
|
||||
list.add(toMap(movieDO));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户所有网盘下的电影列表(分页)
|
||||
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询
|
||||
@ -249,8 +305,11 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
||||
movieDO.setStatus(movie.getStatus());
|
||||
movieDO.setCategory(movie.getCategory());
|
||||
movieDO.setScrapeType(movie.getScrapeType());
|
||||
movieDO.setScrapeStatus(movie.getScrapeStatus());
|
||||
movieDO.setScrapeError(movie.getScrapeError());
|
||||
movieDO.setRating(movie.getRating());
|
||||
movieDO.setPosterUrl(movie.getPosterUrl());
|
||||
movieDO.setBackdropUrl(movie.getBackdropUrl());
|
||||
movieDO.setTags(movie.getTags());
|
||||
movieDO.setUpdateTime(movie.getUpdateTime());
|
||||
return movieDO;
|
||||
@ -279,8 +338,11 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
||||
movie.setStatus(movieDO.getStatus());
|
||||
movie.setCategory(movieDO.getCategory());
|
||||
movie.setScrapeType(movieDO.getScrapeType());
|
||||
movie.setScrapeStatus(movieDO.getScrapeStatus());
|
||||
movie.setScrapeError(movieDO.getScrapeError());
|
||||
movie.setRating(movieDO.getRating());
|
||||
movie.setPosterUrl(movieDO.getPosterUrl());
|
||||
movie.setBackdropUrl(movieDO.getBackdropUrl());
|
||||
movie.setTags(movieDO.getTags());
|
||||
movie.setUpdateTime(movieDO.getUpdateTime());
|
||||
movie.setCreateTime(movieDO.getCreateTime());
|
||||
@ -307,9 +369,12 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
||||
map.put("status", movieDO.getStatus());
|
||||
map.put("category", movieDO.getCategory());
|
||||
map.put("scrapeType", movieDO.getScrapeType());
|
||||
map.put("scrapeStatus", movieDO.getScrapeStatus());
|
||||
map.put("scrapeError", movieDO.getScrapeError());
|
||||
map.put("rating", movieDO.getRating());
|
||||
map.put("tags", movieDO.getTags());
|
||||
map.put("posterUrl", movieDO.getPosterUrl());
|
||||
map.put("backdropUrl", movieDO.getBackdropUrl());
|
||||
map.put("minioUrl", movieDO.getMinioUrl());
|
||||
map.put("createTime", movieDO.getCreateTime() != null ? movieDO.getCreateTime().toString() : null);
|
||||
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
||||
|
||||
@ -11,6 +11,7 @@ import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 电影 Repository 层(Dao)
|
||||
@ -69,6 +70,27 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
return movieDO != null ? toMovie(movieDO) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据电影ID列表批量查询电影(用于每日推荐缓存回查)
|
||||
*
|
||||
* @param movieIds 电影ID列表
|
||||
* @return 电影Map列表(保持传入ID的顺序)
|
||||
*/
|
||||
public List<Map<String, Object>> findByIds(List<Long> movieIds) {
|
||||
if (movieIds == null || movieIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
var wrapper = new LambdaQueryWrapper<MovieDO>()
|
||||
.in(MovieDO::getId, movieIds);
|
||||
var movieDOs = list(wrapper);
|
||||
// 按传入的ID顺序排序
|
||||
var movieMap = movieDOs.stream().collect(Collectors.toMap(MovieDO::getId, m -> m));
|
||||
return movieIds.stream()
|
||||
.filter(movieMap::containsKey)
|
||||
.map(id -> toMap(movieMap.get(id)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询用户的电影列表
|
||||
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询,支持动态条件
|
||||
@ -254,6 +276,14 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
return baseMapper.updatePosterUrl(id, posterUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电影背景图URL(横版)
|
||||
* 委托给 MovieMapper 中定义的 @Update 注解方法
|
||||
*/
|
||||
public int updateBackdropUrl(Long id, String backdropUrl) {
|
||||
return baseMapper.updateBackdropUrl(id, backdropUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电影标签
|
||||
* 委托给 MovieMapper 中定义的 @Update 注解方法
|
||||
@ -270,6 +300,14 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
return baseMapper.updateScrapeType(id, scrapeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新刮削状态和失败原因
|
||||
* 委托给 MovieMapper 中定义的 @Update 注解方法
|
||||
*/
|
||||
public int updateScrapeStatus(Long id, String scrapeStatus, String scrapeError) {
|
||||
return baseMapper.updateScrapeStatus(id, scrapeStatus, scrapeError);
|
||||
}
|
||||
|
||||
// ==================== Entity ↔ DO 转换 ====================
|
||||
|
||||
/**
|
||||
@ -286,8 +324,11 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
movieDO.setStatus(movie.getStatus());
|
||||
movieDO.setCategory(movie.getCategory());
|
||||
movieDO.setScrapeType(movie.getScrapeType());
|
||||
movieDO.setScrapeStatus(movie.getScrapeStatus());
|
||||
movieDO.setScrapeError(movie.getScrapeError());
|
||||
movieDO.setRating(movie.getRating());
|
||||
movieDO.setPosterUrl(movie.getPosterUrl());
|
||||
movieDO.setBackdropUrl(movie.getBackdropUrl());
|
||||
movieDO.setTags(movie.getTags());
|
||||
movieDO.setUpdateTime(movie.getUpdateTime());
|
||||
movieDO.setCreateTime(movie.getCreateTime());
|
||||
@ -308,8 +349,11 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
movie.setStatus(movieDO.getStatus());
|
||||
movie.setCategory(movieDO.getCategory());
|
||||
movie.setScrapeType(movieDO.getScrapeType());
|
||||
movie.setScrapeStatus(movieDO.getScrapeStatus());
|
||||
movie.setScrapeError(movieDO.getScrapeError());
|
||||
movie.setRating(movieDO.getRating());
|
||||
movie.setPosterUrl(movieDO.getPosterUrl());
|
||||
movie.setBackdropUrl(movieDO.getBackdropUrl());
|
||||
movie.setTags(movieDO.getTags());
|
||||
movie.setUpdateTime(movieDO.getUpdateTime());
|
||||
movie.setCreateTime(movieDO.getCreateTime());
|
||||
@ -330,8 +374,11 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
map.put("status", movieDO.getStatus());
|
||||
map.put("category", movieDO.getCategory());
|
||||
map.put("scrapeType", movieDO.getScrapeType());
|
||||
map.put("scrapeStatus", movieDO.getScrapeStatus());
|
||||
map.put("scrapeError", movieDO.getScrapeError());
|
||||
map.put("rating", movieDO.getRating());
|
||||
map.put("posterUrl", movieDO.getPosterUrl());
|
||||
map.put("backdropUrl", movieDO.getBackdropUrl());
|
||||
map.put("tags", movieDO.getTags());
|
||||
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
||||
map.put("createTime", movieDO.getCreateTime() != null ? movieDO.getCreateTime().toString() : null);
|
||||
|
||||
@ -34,9 +34,12 @@ public class TmdbScraperLogRepository extends ServiceImpl<TmdbScraperLogMapper,
|
||||
/**
|
||||
* 查询最近的执行记录(分页)
|
||||
* 委托给 TmdbScraperLogMapper 中定义的 XML 查询
|
||||
* @param pageNo 页码(从1开始)
|
||||
* @param pageSize 每页条数
|
||||
*/
|
||||
public Map<String, Object> queryRecent(int pageNo, int pageSize) {
|
||||
return baseMapper.queryRecent(pageNo, pageSize);
|
||||
public List<Map<String, Object>> queryRecent(int pageNo, int pageSize) {
|
||||
int offset = (pageNo - 1) * pageSize;
|
||||
return baseMapper.queryRecent(offset, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -203,6 +203,22 @@ public class UserRepository extends ServiceImpl<UserMapper, UserDO> {
|
||||
return baseMapper.softDelete(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户名
|
||||
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||
*/
|
||||
public int updateUsername(Long userId, String username) {
|
||||
return baseMapper.updateUsername(userId, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户头像URL
|
||||
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||
*/
|
||||
public int updateAvatarUrl(Long userId, String avatarUrl) {
|
||||
return baseMapper.updateAvatarUrl(userId, avatarUrl);
|
||||
}
|
||||
|
||||
// ==================== Entity ↔ DO 转换 ====================
|
||||
|
||||
/**
|
||||
|
||||
@ -97,12 +97,27 @@ public class DriveMovie implements Serializable {
|
||||
*/
|
||||
private String scrapeType;
|
||||
|
||||
/**
|
||||
* 刮削状态:
|
||||
* - pending - 待刮削(默认)
|
||||
* - scraping - 刮削中
|
||||
* - success - 刮削成功
|
||||
* - failed - 刮削失败
|
||||
*/
|
||||
private String scrapeStatus;
|
||||
|
||||
/** 刮削失败原因 */
|
||||
private String scrapeError;
|
||||
|
||||
/** 评分:0-100 */
|
||||
private Integer rating;
|
||||
|
||||
/** 海报 URL(加密存储) */
|
||||
/** 海报 URL(竖版,加密存储) */
|
||||
private String posterUrl;
|
||||
|
||||
/** 背景图 URL(横版,加密存储) */
|
||||
private String backdropUrl;
|
||||
|
||||
/** 标签:逗号分隔,如 "科幻,IMAX,杜比视界" */
|
||||
private String tags;
|
||||
|
||||
@ -136,6 +151,7 @@ public class DriveMovie implements Serializable {
|
||||
this.originalName = originalName;
|
||||
this.status = "active";
|
||||
this.scrapeType = "none";
|
||||
this.scrapeStatus = "pending";
|
||||
this.createTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@ -195,12 +211,21 @@ public class DriveMovie implements Serializable {
|
||||
public String getScrapeType() { return scrapeType; }
|
||||
public void setScrapeType(String scrapeType) { this.scrapeType = scrapeType; }
|
||||
|
||||
public String getScrapeStatus() { return scrapeStatus; }
|
||||
public void setScrapeStatus(String scrapeStatus) { this.scrapeStatus = scrapeStatus; }
|
||||
|
||||
public String getScrapeError() { return scrapeError; }
|
||||
public void setScrapeError(String scrapeError) { this.scrapeError = scrapeError; }
|
||||
|
||||
public Integer getRating() { return rating; }
|
||||
public void setRating(Integer rating) { this.rating = rating; }
|
||||
|
||||
public String getPosterUrl() { return posterUrl; }
|
||||
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
||||
|
||||
public String getBackdropUrl() { return backdropUrl; }
|
||||
public void setBackdropUrl(String backdropUrl) { this.backdropUrl = backdropUrl; }
|
||||
|
||||
public String getTags() { return tags; }
|
||||
public void setTags(String tags) { this.tags = tags; }
|
||||
|
||||
|
||||
@ -67,14 +67,29 @@ public class Movie implements Serializable {
|
||||
*/
|
||||
private String scrapeType;
|
||||
|
||||
/**
|
||||
* 刮削状态:
|
||||
* - pending - 待刮削(默认)
|
||||
* - scraping - 刮削中
|
||||
* - success - 刮削成功
|
||||
* - failed - 刮削失败
|
||||
*/
|
||||
private String scrapeStatus;
|
||||
|
||||
/** 刮削失败原因 */
|
||||
private String scrapeError;
|
||||
|
||||
// ==================== 评分与展示属性 ====================
|
||||
|
||||
/** 评分:0-100分制 */
|
||||
private Integer rating;
|
||||
|
||||
/** 海报图片URL地址 */
|
||||
/** 海报图片URL地址(竖版) */
|
||||
private String posterUrl;
|
||||
|
||||
/** 背景图URL地址(横版) */
|
||||
private String backdropUrl;
|
||||
|
||||
/** 标签:逗号分隔,如"科幻, IMAX, 杜比视界" */
|
||||
private String tags;
|
||||
|
||||
@ -134,12 +149,21 @@ public class Movie implements Serializable {
|
||||
public String getScrapeType() { return scrapeType; }
|
||||
public void setScrapeType(String scrapeType) { this.scrapeType = scrapeType; }
|
||||
|
||||
public String getScrapeStatus() { return scrapeStatus; }
|
||||
public void setScrapeStatus(String scrapeStatus) { this.scrapeStatus = scrapeStatus; }
|
||||
|
||||
public String getScrapeError() { return scrapeError; }
|
||||
public void setScrapeError(String scrapeError) { this.scrapeError = scrapeError; }
|
||||
|
||||
public Integer getRating() { return rating; }
|
||||
public void setRating(Integer rating) { this.rating = rating; }
|
||||
|
||||
public String getPosterUrl() { return posterUrl; }
|
||||
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
||||
|
||||
public String getBackdropUrl() { return backdropUrl; }
|
||||
public void setBackdropUrl(String backdropUrl) { this.backdropUrl = backdropUrl; }
|
||||
|
||||
public String getTags() { return tags; }
|
||||
public void setTags(String tags) { this.tags = tags; }
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<!-- 分页查询最近的执行记录 -->
|
||||
<select id="queryRecent" resultType="java.util.Map">
|
||||
SELECT * FROM wp_tmdb_scraper_log ORDER BY start_time DESC
|
||||
LIMIT #{pageSize} OFFSET #{pageNo}
|
||||
LIMIT #{pageSize} OFFSET #{offset}
|
||||
</select>
|
||||
|
||||
<!-- 查询最近的N条执行记录 -->
|
||||
|
||||
@ -74,6 +74,18 @@ public class DatabaseInitializer {
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive", "logo_url",
|
||||
"TEXT DEFAULT NULL COMMENT 'drive logo url'");
|
||||
|
||||
// 检查 wp_drive_movie 表是否有 scrape_status 字段(刮削状态)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "scrape_status",
|
||||
"VARCHAR(20) DEFAULT 'pending' COMMENT '刮削状态:pending/scraping/success/failed'");
|
||||
|
||||
// 检查 wp_drive_movie 表是否有 scrape_error 字段(刮削失败原因)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "scrape_error",
|
||||
"VARCHAR(500) DEFAULT NULL COMMENT '刮削失败原因'");
|
||||
|
||||
// 检查 wp_drive_movie 表是否有 backdrop_url 字段(横版背景图)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "backdrop_url",
|
||||
"TEXT DEFAULT NULL COMMENT '背景图URL(横版,加密存储)'");
|
||||
|
||||
// 检查 wp_icon 表是否有 logo_url 字段
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_icon", "logo_url",
|
||||
"VARCHAR(500) DEFAULT NULL COMMENT 'Logo 图片 URL(MinIO 存储)'");
|
||||
|
||||
@ -700,6 +700,8 @@ public class DriveController {
|
||||
map.put("status", movie.getStatus());
|
||||
map.put("category", movie.getCategory());
|
||||
map.put("scrapeType", movie.getScrapeType());
|
||||
map.put("scrapeStatus", movie.getScrapeStatus());
|
||||
map.put("scrapeError", movie.getScrapeError());
|
||||
map.put("rating", movie.getRating());
|
||||
map.put("tags", movie.getTags());
|
||||
map.put("minioUrl", movie.getMinioUrl());
|
||||
|
||||
@ -428,17 +428,10 @@ public class IconController {
|
||||
iconRepository.updateLogoUrl(id, newLogoUrl);
|
||||
log.info("图标更新成功,新 URL: {}", newLogoUrl);
|
||||
|
||||
// 异步删除旧的 MinIO 图片(防止硬盘过满)
|
||||
// 注意:旧图片不立即删除,由 IconCleanupScheduler 定时任务在 3 天后自动清理
|
||||
// 这样可以避免前端仍在使用旧 URL 时图片被删除导致 403/404
|
||||
if (oldLogoUrl != null && !oldLogoUrl.isEmpty()) {
|
||||
String oldObjectName = extractObjectName(oldLogoUrl);
|
||||
if (oldObjectName != null) {
|
||||
boolean deleted = MinioUtil.deleteFile(oldObjectName);
|
||||
if (deleted) {
|
||||
log.info("旧图标删除成功: {}", oldObjectName);
|
||||
} else {
|
||||
log.warn("旧图标删除失败: {}", oldObjectName);
|
||||
}
|
||||
}
|
||||
log.info("旧图标将在 3 天后自动清理: {}", oldLogoUrl);
|
||||
}
|
||||
|
||||
// 返回更新后的 Icon 信息
|
||||
|
||||
@ -10,7 +10,11 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
@ -22,12 +26,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
* 功能:
|
||||
* 1. 定时扫描 MinIO 中 icons/ 目录下的所有图片
|
||||
* 2. 对比数据库中在用的 logoUrl,找出无用的图片
|
||||
* 3. 删除无用图片,释放硬盘空间
|
||||
* 3. 只删除超过 3 天的无用图片(保留期,避免前端仍在使用旧 URL)
|
||||
* 4. 删除过期无用图片,释放硬盘空间
|
||||
*
|
||||
* 调度策略:
|
||||
* - 首次延迟:启动后 10 分钟
|
||||
* - 执行间隔:每 24 小时执行一次
|
||||
* - 防止并发:同一时间只允许一个清理任务运行
|
||||
* - 保留期:无用图片创建/修改后 3 天才删除
|
||||
*/
|
||||
@Component
|
||||
public class IconCleanupScheduler {
|
||||
@ -47,6 +53,9 @@ public class IconCleanupScheduler {
|
||||
/** MinIO 中 icon 图片的前缀路径 */
|
||||
private static final String ICONS_PREFIX = "icons/";
|
||||
|
||||
/** 无用图片保留期(天):超过此天数的无用图片才会被删除 */
|
||||
private static final int RETENTION_DAYS = 3;
|
||||
|
||||
public IconCleanupScheduler(IconRepository iconRepository) {
|
||||
this.iconRepository = iconRepository;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.wangpan.config;
|
||||
|
||||
import com.wangpan.common.dal.mapper.DailyRecommendMapper;
|
||||
import com.wangpan.common.dal.mapper.TmdbCacheMapper;
|
||||
import com.wangpan.common.dal.mapper.TmdbScraperLogMapper;
|
||||
import com.wangpan.common.util.DBUtil;
|
||||
@ -76,9 +77,11 @@ public class DatabaseInitializer {
|
||||
// wp_drive_movie 表由 drive-service 的 DatabaseInitializer 负责创建,此处不再重复创建
|
||||
SchemaUtil.createTableIfNotExists(conn, "wp_tmdb_cache", TmdbCacheMapper.CREATE_TABLE_SQL);
|
||||
SchemaUtil.createTableIfNotExists(conn, "wp_tmdb_scraper_log", TmdbScraperLogMapper.CREATE_TABLE_SQL);
|
||||
SchemaUtil.createTableIfNotExists(conn, "wp_daily_recommend", DailyRecommendMapper.CREATE_TABLE_SQL);
|
||||
|
||||
System.out.println(" ✓ [wp_tmdb_cache] 表创建完成");
|
||||
System.out.println(" ✓ [wp_tmdb_scraper_log] 表创建完成");
|
||||
System.out.println(" ✓ [wp_daily_recommend] 表创建完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,9 @@ package com.wangpan.controller;
|
||||
|
||||
import com.wangpan.common.Result;
|
||||
import com.wangpan.common.annotation.RequireAuth;
|
||||
import com.wangpan.common.dal.dataobject.DailyRecommendDO;
|
||||
import com.wangpan.common.dal.repository.DailyRecommendRepository;
|
||||
import com.wangpan.common.dal.repository.DriveMovieRepository;
|
||||
import com.wangpan.common.dal.repository.MovieRepository;
|
||||
import com.wangpan.common.entity.Movie;
|
||||
import com.wangpan.common.entity.User;
|
||||
@ -15,6 +18,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 电影管理控制器
|
||||
@ -30,12 +34,18 @@ public class MovieController {
|
||||
private static final Logger log = LoggerFactory.getLogger(MovieController.class);
|
||||
|
||||
private final MovieRepository movieRepository;
|
||||
private final DriveMovieRepository driveMovieRepository;
|
||||
private final DailyRecommendRepository dailyRecommendRepository;
|
||||
|
||||
/**
|
||||
* 构造函数注入 Repository 依赖
|
||||
*/
|
||||
public MovieController(MovieRepository movieRepository) {
|
||||
public MovieController(MovieRepository movieRepository,
|
||||
DriveMovieRepository driveMovieRepository,
|
||||
DailyRecommendRepository dailyRecommendRepository) {
|
||||
this.movieRepository = movieRepository;
|
||||
this.driveMovieRepository = driveMovieRepository;
|
||||
this.dailyRecommendRepository = dailyRecommendRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,13 +56,15 @@ public class MovieController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询电影列表
|
||||
* 按文件夹分组查询电影列表(从 wp_drive_movie 表查询当前用户所有电影)
|
||||
* GET /api/movies
|
||||
*
|
||||
* 返回格式:按文件夹分组,每个文件夹最多返回 10 条电影数据
|
||||
* 支持分页:pageNo 控制文件夹分页,每个文件夹固定返回最多 10 条
|
||||
*/
|
||||
@GetMapping
|
||||
public Result<?> listMovies(@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String category,
|
||||
@RequestParam(required = false) String scrapeType,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false) String sortOrder,
|
||||
@ -61,12 +73,30 @@ public class MovieController {
|
||||
HttpServletRequest request) {
|
||||
User user = getCurrentUser(request);
|
||||
|
||||
log.info("用户 [{}] 查询电影列表 | 关键词={} 状态={} 页码={}", user.getUsername(), keyword, status, pageNo);
|
||||
log.info("用户 [{}] 按文件夹查询电影列表 | 关键词={} 页码={}", user.getUsername(), keyword, pageNo);
|
||||
|
||||
Map<String, Object> resultData = movieRepository.queryByPage(
|
||||
user.getId(), keyword, status, category, scrapeType,
|
||||
sortBy, sortOrder, pageNo, pageSize
|
||||
);
|
||||
// 1. 查询用户的文件夹ID列表(分页)
|
||||
List<Long> folderIds = driveMovieRepository.queryFolderIdsByUser(user.getId(), pageNo, pageSize);
|
||||
int totalFolders = driveMovieRepository.countFoldersByUser(user.getId());
|
||||
|
||||
// 2. 对每个文件夹查询最多 10 条电影数据
|
||||
List<Map<String, Object>> folderGroups = new ArrayList<>();
|
||||
for (Long folderId : folderIds) {
|
||||
List<Map<String, Object>> movies = driveMovieRepository.queryByFolder(user.getId(), folderId, 10);
|
||||
|
||||
Map<String, Object> group = new HashMap<>();
|
||||
group.put("folderId", folderId);
|
||||
group.put("movies", movies);
|
||||
group.put("movieCount", movies.size());
|
||||
folderGroups.add(group);
|
||||
}
|
||||
|
||||
// 3. 构建分页结果
|
||||
Map<String, Object> resultData = new HashMap<>();
|
||||
resultData.put("totalFolders", totalFolders);
|
||||
resultData.put("folderGroups", folderGroups);
|
||||
resultData.put("pageNo", pageNo);
|
||||
resultData.put("pageSize", pageSize);
|
||||
|
||||
return Result.success("查询成功", resultData);
|
||||
}
|
||||
@ -246,9 +276,14 @@ public class MovieController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 今日推荐(随机推荐3部电影)
|
||||
* 今日推荐(每天固定推荐3部电影)
|
||||
* GET /api/movies/recommend
|
||||
* 从当前用户的电影库中随机抽取3部电影用于首页展示
|
||||
*
|
||||
* 推荐规则:
|
||||
* - 从 00:00 到 23:59 返回固定的3部电影
|
||||
* - 当天不管查询多少次都返回相同的结果
|
||||
* - 第二天 00:00 后重新随机挑选3部
|
||||
* - 推荐结果缓存在 wp_daily_recommend 表中
|
||||
*/
|
||||
@GetMapping("/recommend")
|
||||
public Result<?> getRecommendations(HttpServletRequest request) {
|
||||
@ -257,8 +292,34 @@ public class MovieController {
|
||||
log.info("用户 [{}] 请求今日推荐", user.getUsername());
|
||||
|
||||
try {
|
||||
// 随机查询3部电影
|
||||
List<Map<String, Object>> recommendList = movieRepository.findRandomMovies(user.getId(), 3);
|
||||
List<Map<String, Object>> recommendList;
|
||||
|
||||
// 第1步:查询当日推荐缓存
|
||||
DailyRecommendDO cached = dailyRecommendRepository.findTodayRecommend(user.getId());
|
||||
|
||||
if (cached != null && cached.getMovieIds() != null && !cached.getMovieIds().isBlank()) {
|
||||
// 有缓存:根据缓存的电影ID查询电影详情
|
||||
log.info("用户 [{}] 使用今日推荐缓存", user.getUsername());
|
||||
List<Long> movieIds = parseMovieIds(cached.getMovieIds());
|
||||
recommendList = movieRepository.findByIds(movieIds);
|
||||
} else {
|
||||
// 无缓存:随机查询3部电影并缓存
|
||||
log.info("用户 [{}] 生成新的今日推荐", user.getUsername());
|
||||
List<Map<String, Object>> randomMovies = movieRepository.findRandomMovies(user.getId(), 3);
|
||||
|
||||
if (randomMovies.isEmpty()) {
|
||||
recommendList = new ArrayList<>();
|
||||
} else {
|
||||
// 提取电影ID并缓存
|
||||
List<Long> movieIds = randomMovies.stream()
|
||||
.map(m -> ((Number) m.get("id")).longValue())
|
||||
.collect(Collectors.toList());
|
||||
String movieIdsStr = movieIds.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||
dailyRecommendRepository.saveOrUpdateTodayRecommend(user.getId(), movieIdsStr);
|
||||
|
||||
recommendList = randomMovies;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("list", recommendList);
|
||||
@ -271,6 +332,21 @@ public class MovieController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析逗号分隔的电影ID字符串
|
||||
*/
|
||||
private List<Long> parseMovieIds(String movieIdsStr) {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (String id : movieIdsStr.split(",")) {
|
||||
try {
|
||||
ids.add(Long.parseLong(id.trim()));
|
||||
} catch (NumberFormatException e) {
|
||||
// 忽略无效ID
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
* GET /api/movies/stats
|
||||
@ -307,6 +383,7 @@ public class MovieController {
|
||||
map.put("scrapeType", movie.getScrapeType());
|
||||
map.put("rating", movie.getRating());
|
||||
map.put("posterUrl", movie.getPosterUrl());
|
||||
map.put("backdropUrl", movie.getBackdropUrl());
|
||||
map.put("tags", movie.getTags());
|
||||
|
||||
if (movie.getUpdateTime() != null) {
|
||||
|
||||
@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -212,9 +213,16 @@ public class TmdbScraperController {
|
||||
|
||||
pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, pageSize));
|
||||
|
||||
Map<String, Object> logData = scraperLogRepository.queryRecent(pageNo, pageSize);
|
||||
List<Map<String, Object>> logs = scraperLogRepository.queryRecent(pageNo, pageSize);
|
||||
|
||||
return Result.success("查询成功", logData);
|
||||
// 构建分页响应
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("list", logs);
|
||||
result.put("pageNo", pageNo);
|
||||
result.put("pageSize", pageSize);
|
||||
result.put("total", logs.size());
|
||||
|
||||
return Result.success("查询成功", result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -53,8 +53,8 @@ public class TmdbScraperService {
|
||||
|
||||
// ==================== TMDB API 配置常量 ====================
|
||||
|
||||
/** TMDB API v3 基础地址 */
|
||||
public static final String TMDB_API_BASE = "https://api.themoviedb.org/3";
|
||||
/** TMDB API 基础地址(通过 Cloudflare Worker 代理访问,解决国内网络无法直连的问题) */
|
||||
public static final String TMDB_API_BASE = "https://tmdb-proxy.naihun782.workers.dev/tmdb-proxy/3";
|
||||
|
||||
/** TMDB 图片服务器基础地址(原始尺寸) */
|
||||
public static final String TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/original";
|
||||
@ -128,45 +128,57 @@ public class TmdbScraperService {
|
||||
|
||||
// 检查电影标题是否为空
|
||||
if (movie.getTitle() == null || movie.getTitle().isBlank()) {
|
||||
// 更新刮削状态为失败
|
||||
movieRepository.updateScrapeStatus(movieId, "failed", "电影标题为空,无法刮削");
|
||||
return errorResult("电影标题为空,无法刮削");
|
||||
}
|
||||
|
||||
// ========== 第2步:检查缓存有效性(7天内已刮削则跳过) ==========
|
||||
var existingCache = tmdbCacheRepository.findByMovieId(movieId);
|
||||
if (existingCache != null && !isCacheExpired(existingCache.getCacheTime().toString())) {
|
||||
// 使用缓存数据,更新刮削状态为成功
|
||||
movieRepository.updateScrapeStatus(movieId, "success", null);
|
||||
return successResult("使用缓存数据(" + existingCache.getTitle() + ")", tmdbCacheRepository.toMap(existingCache));
|
||||
}
|
||||
|
||||
// ========== 第3步:获取用户的TMDB API Key ==========
|
||||
var user = userRepository.findById(userId);
|
||||
if (user == null) {
|
||||
movieRepository.updateScrapeStatus(movieId, "failed", "用户不存在");
|
||||
return errorResult("用户不存在");
|
||||
}
|
||||
|
||||
var encryptedKey = user.getTmdbApiKey();
|
||||
if (encryptedKey == null || encryptedKey.isBlank()) {
|
||||
movieRepository.updateScrapeStatus(movieId, "failed", "用户未配置TMDB API Key");
|
||||
return errorResult("用户未配置TMDB API Key,请先设置");
|
||||
}
|
||||
|
||||
// 解密API Key
|
||||
var apiKey = EncryptUtil.decrypt(encryptedKey);
|
||||
|
||||
// 更新刮削状态为刮削中
|
||||
movieRepository.updateScrapeStatus(movieId, "scraping", null);
|
||||
|
||||
// ========== 第4步:向TMDB搜索电影 ==========
|
||||
LOGGER.info("[TmdbScraper] 开始搜索电影: " + movie.getTitle());
|
||||
var searchResult = searchMovie(movie.getTitle(), apiKey);
|
||||
|
||||
if (searchResult == null || !searchResult.has("results")) {
|
||||
movieRepository.updateScrapeStatus(movieId, "failed", "TMDB搜索无结果");
|
||||
return errorResult("TMDB搜索无结果: " + movie.getTitle());
|
||||
}
|
||||
|
||||
var results = searchResult.get("results");
|
||||
if (results.isEmpty() || results.size() == 0) {
|
||||
movieRepository.updateScrapeStatus(movieId, "failed", "TMDB未找到匹配的电影");
|
||||
return errorResult("TMDB未找到匹配的电影: " + movie.getTitle());
|
||||
}
|
||||
|
||||
// ========== 第5步:匹配最佳结果 ==========
|
||||
var bestMatch = findBestMatch(movie.getTitle(), results);
|
||||
if (bestMatch == null) {
|
||||
movieRepository.updateScrapeStatus(movieId, "failed", "未找到匹配度足够高的结果");
|
||||
return errorResult("未找到匹配度足够高的结果: " + movie.getTitle());
|
||||
}
|
||||
|
||||
@ -177,28 +189,40 @@ public class TmdbScraperService {
|
||||
rateLimitDelay(); // 遵守速率限制
|
||||
var detailResult = getMovieDetail(tmdbId, apiKey);
|
||||
|
||||
// ========== 第7步:下载海报图片 ==========
|
||||
// ========== 第7步:下载海报图片(竖版) ==========
|
||||
String localPosterPath = null;
|
||||
if (detailResult != null && detailResult.has("poster_path")
|
||||
&& !detailResult.get("poster_path").isNull()
|
||||
&& !detailResult.get("poster_path").asText().isBlank()) {
|
||||
|
||||
var posterPath = detailResult.get("poster_path").asText();
|
||||
localPosterPath = downloadPoster(posterPath, movieId, tmdbId);
|
||||
localPosterPath = downloadPoster(posterPath, movieId, tmdbId, "poster");
|
||||
}
|
||||
|
||||
// ========== 第7.5步:下载背景图(横版) ==========
|
||||
String localBackdropPath = null;
|
||||
if (detailResult != null && detailResult.has("backdrop_path")
|
||||
&& !detailResult.get("backdrop_path").isNull()
|
||||
&& !detailResult.get("backdrop_path").asText().isBlank()) {
|
||||
|
||||
var backdropPath = detailResult.get("backdrop_path").asText();
|
||||
localBackdropPath = downloadPoster(backdropPath, movieId, tmdbId, "backdrop");
|
||||
}
|
||||
|
||||
// ========== 第8步:构建缓存数据并保存 ==========
|
||||
var cacheData = buildCacheData(detailResult != null ? detailResult : bestMatch, movieId, userId, localPosterPath);
|
||||
var cacheData = buildCacheData(detailResult != null ? detailResult : bestMatch, movieId, userId, localPosterPath, localBackdropPath);
|
||||
tmdbCacheRepository.saveOrUpdate(cacheData);
|
||||
|
||||
// ========== 第9步:更新wp_drive_movie表的相关字段 ==========
|
||||
updateMovieFromScrape(movie, cacheData, localPosterPath);
|
||||
updateMovieFromScrape(movie, cacheData, localPosterPath, localBackdropPath);
|
||||
|
||||
LOGGER.info("[TmdbScraper] 刮削完成: " + cacheData.get("title"));
|
||||
return successResult("刮削成功", cacheData);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.log(Level.SEVERE, "[TmdbScraper] 刮削异常: movieId=" + movieId, e);
|
||||
// 更新刮削状态为失败
|
||||
movieRepository.updateScrapeStatus(movieId, "failed", "刮削异常: " + e.getMessage());
|
||||
return errorResult("刮削异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
@ -334,22 +358,23 @@ public class TmdbScraperService {
|
||||
/**
|
||||
* 从TMDB下载海报图片并上传到MinIO对象存储
|
||||
*
|
||||
* 存储路径规则:{POSTER_PREFIX}/{movieId}_{tmdbId}.jpg
|
||||
* 例如:posters/123_45678.jpg
|
||||
* 存储路径规则:{POSTER_PREFIX}/{imageType}_{movieId}_{tmdbId}.jpg
|
||||
* 例如:posters/poster_123_45678.jpg 或 posters/backdrop_123_45678.jpg
|
||||
*
|
||||
* 上传成功后,返回的 URL 会使用 EncryptUtil 加密后存储到数据库。
|
||||
*
|
||||
* @param posterPath TMDB返回的poster_path(如 /xxx.jpg)
|
||||
* @param imagePath TMDB返回的图片路径(如 /xxx.jpg)
|
||||
* @param movieId 本地电影ID
|
||||
* @param tmdbId TMDB电影ID
|
||||
* @param imageType 图片类型:poster(竖版海报)或 backdrop(横版背景图)
|
||||
* @return MinIO 访问 URL(已加密),上传失败返回 null
|
||||
*/
|
||||
private String downloadPoster(String posterPath, Long movieId, Long tmdbId) {
|
||||
private String downloadPoster(String imagePath, Long movieId, Long tmdbId, String imageType) {
|
||||
// 构建完整的TMDB图片URL
|
||||
var imageUrl = TMDB_IMAGE_BASE + posterPath;
|
||||
var imageUrl = TMDB_IMAGE_BASE + imagePath;
|
||||
|
||||
// 构建 MinIO 存储路径
|
||||
var objectName = POSTER_PREFIX + "/" + movieId + "_" + tmdbId + ".jpg";
|
||||
// 构建 MinIO 存储路径,区分竖版和横版
|
||||
var objectName = POSTER_PREFIX + "/" + imageType + "_" + movieId + "_" + tmdbId + ".jpg";
|
||||
|
||||
try {
|
||||
// 下载图片到内存
|
||||
@ -484,7 +509,7 @@ public class TmdbScraperService {
|
||||
/**
|
||||
* 构建缓存数据Map(从TMDB响应提取关键字段)
|
||||
*/
|
||||
private Map<String, Object> buildCacheData(JsonNode tmdbData, Long movieId, Long userId, String localPosterPath) {
|
||||
private Map<String, Object> buildCacheData(JsonNode tmdbData, Long movieId, Long userId, String localPosterPath, String localBackdropPath) {
|
||||
// 提取基本信息
|
||||
var data = new java.util.LinkedHashMap<String, Object>();
|
||||
|
||||
@ -499,7 +524,8 @@ public class TmdbScraperService {
|
||||
? tmdbData.get("poster_path").asText() : "");
|
||||
data.put("backdropPath", tmdbData.has("backdrop_path") && !tmdbData.get("backdrop_path").isNull()
|
||||
? tmdbData.get("backdrop_path").asText() : "");
|
||||
data.put("localPosterUrl", localPosterPath); // 本地路径
|
||||
data.put("localPosterUrl", localPosterPath); // 本地竖版海报路径
|
||||
data.put("localBackdropUrl", localBackdropPath); // 本地横版背景图路径
|
||||
|
||||
// 评分信息(TMDB 10分制转换为百分制)
|
||||
double voteAvg = tmdbData.has("vote_average") ? tmdbData.get("vote_average").asDouble() : 0;
|
||||
@ -569,7 +595,7 @@ public class TmdbScraperService {
|
||||
/**
|
||||
* 根据刮削结果更新wp_drive_movie表的相关字段
|
||||
*/
|
||||
private void updateMovieFromScrape(Movie movie, Map<String, Object> cacheData, String encryptedPosterUrl) {
|
||||
private void updateMovieFromScrape(Movie movie, Map<String, Object> cacheData, String encryptedPosterUrl, String encryptedBackdropUrl) {
|
||||
// 更新评分(如果之前没有或更低)
|
||||
Integer tmdbRating = cacheData.get("tmdbRating") != null
|
||||
? ((Number) cacheData.get("tmdbRating")).intValue() : null;
|
||||
@ -578,7 +604,7 @@ public class TmdbScraperService {
|
||||
movieRepository.updateRating(movie.getId(), tmdbRating);
|
||||
}
|
||||
|
||||
// 更新海报URL(优先使用 MinIO 存储的加密 URL)
|
||||
// 更新海报URL(竖版,优先使用 MinIO 存储的加密 URL)
|
||||
if (encryptedPosterUrl != null) {
|
||||
// 解密后存储到 wp_drive_movie 表(前端可直接使用)
|
||||
try {
|
||||
@ -596,6 +622,23 @@ public class TmdbScraperService {
|
||||
movieRepository.updatePosterUrl(movie.getId(), TMDB_IMAGE_W500 + cacheData.get("posterPath"));
|
||||
}
|
||||
|
||||
// 更新背景图URL(横版,优先使用 MinIO 存储的加密 URL)
|
||||
if (encryptedBackdropUrl != null) {
|
||||
try {
|
||||
var decryptedUrl = EncryptUtil.decrypt(encryptedBackdropUrl);
|
||||
movieRepository.updateBackdropUrl(movie.getId(), decryptedUrl);
|
||||
} catch (Exception e) {
|
||||
// 解密失败时使用 TMDB 远程地址作为降级方案
|
||||
System.err.println("[TmdbScraper] MinIO URL 解密失败,使用 TMDB 远程地址: " + e.getMessage());
|
||||
if (cacheData.get("backdropPath") != null && !cacheData.get("backdropPath").toString().isBlank()) {
|
||||
movieRepository.updateBackdropUrl(movie.getId(), TMDB_IMAGE_W500 + cacheData.get("backdropPath"));
|
||||
}
|
||||
}
|
||||
} else if (cacheData.get("backdropPath") != null && !cacheData.get("backdropPath").toString().isBlank()) {
|
||||
// MinIO 上传失败时使用 TMDB 远程地址
|
||||
movieRepository.updateBackdropUrl(movie.getId(), TMDB_IMAGE_W500 + cacheData.get("backdropPath"));
|
||||
}
|
||||
|
||||
// 更新标签(添加TMDB类型信息)
|
||||
var genres = cacheData.get("genres") != null ? cacheData.get("genres").toString() : "";
|
||||
if (!genres.isBlank()) {
|
||||
@ -609,6 +652,9 @@ public class TmdbScraperService {
|
||||
|
||||
// 更新刮削类型标记
|
||||
movieRepository.updateScrapeType(movie.getId(), "auto");
|
||||
|
||||
// 更新刮削状态为成功
|
||||
movieRepository.updateScrapeStatus(movie.getId(), "success", null);
|
||||
}
|
||||
|
||||
// ==================== 缓存过期判断 ====================
|
||||
|
||||
@ -0,0 +1,226 @@
|
||||
package com.wangpan.controller;
|
||||
|
||||
import com.wangpan.common.Result;
|
||||
import com.wangpan.common.dal.repository.UserRepository;
|
||||
import com.wangpan.common.entity.User;
|
||||
import com.wangpan.common.interceptor.AuthInterceptor;
|
||||
import com.wangpan.common.util.MinioUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户个人资料控制器
|
||||
*
|
||||
* 功能:允许每个登录用户修改自己的个人资料
|
||||
* - GET /api/profile 获取当前用户个人资料
|
||||
* - PUT /api/profile/username 修改用户名
|
||||
* - PUT /api/profile/avatar 修改头像(Base64图片上传到MinIO)
|
||||
*
|
||||
* 与 UserController 的区别:
|
||||
* - UserController 是管理员专用的用户管理接口(CRUD所有用户)
|
||||
* - ProfileController 是普通用户修改自己资料的接口
|
||||
*/
|
||||
@RestController
|
||||
public class ProfileController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ProfileController.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* 构造函数注入(Spring 自动装配)
|
||||
*/
|
||||
public ProfileController(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求属性中获取当前登录用户(由 AuthInterceptor 注入)
|
||||
*/
|
||||
private User getCurrentUser(HttpServletRequest request) {
|
||||
return (User) request.getAttribute(AuthInterceptor.CURRENT_USER_ATTR);
|
||||
}
|
||||
|
||||
// ==================== API 接口 ====================
|
||||
|
||||
/**
|
||||
* 获取当前用户个人资料
|
||||
* GET /api/profile
|
||||
*
|
||||
* 返回当前登录用户的基本信息(不含密码等敏感字段)
|
||||
*/
|
||||
@GetMapping("/api/profile")
|
||||
public Result<?> getProfile(HttpServletRequest request) {
|
||||
User currentUser = getCurrentUser(request);
|
||||
log.info("用户 [{}] 查询个人资料", currentUser.getUsername());
|
||||
|
||||
try {
|
||||
// 从数据库重新查询最新数据
|
||||
User user = userRepository.findById(currentUser.getId());
|
||||
if (user == null) {
|
||||
return Result.error(404, "用户不存在");
|
||||
}
|
||||
|
||||
// 构建返回数据(排除敏感字段)
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("id", user.getId());
|
||||
data.put("username", user.getUsername());
|
||||
data.put("email", user.getEmail());
|
||||
data.put("phone", user.getPhone());
|
||||
data.put("realName", user.getRealName());
|
||||
data.put("avatarUrl", user.getAvatarUrl());
|
||||
data.put("userType", user.getUserType());
|
||||
data.put("status", user.getStatus());
|
||||
data.put("createTime", user.getCreateTime() != null ? user.getCreateTime().toString() : null);
|
||||
|
||||
return Result.success("查询成功", data);
|
||||
} catch (Exception e) {
|
||||
log.error("查询个人资料失败", e);
|
||||
return Result.error("系统繁忙,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户名
|
||||
* PUT /api/profile/username
|
||||
*
|
||||
* 请求体:{"username": "新用户名"}
|
||||
*
|
||||
* 校验规则:
|
||||
* 1. 用户名不能为空
|
||||
* 2. 用户名不能与当前用户名相同
|
||||
* 3. 用户名不能被其他用户占用
|
||||
*/
|
||||
@PutMapping("/api/profile/username")
|
||||
public Result<?> updateUsername(HttpServletRequest request,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
|
||||
User currentUser = getCurrentUser(request);
|
||||
log.info("用户 [{}] 请求修改用户名", currentUser.getUsername());
|
||||
|
||||
try {
|
||||
// 1. 参数校验
|
||||
String newUsername = (String) body.get("username");
|
||||
if (newUsername == null || newUsername.isBlank()) {
|
||||
return Result.error(400, "用户名不能为空");
|
||||
}
|
||||
newUsername = newUsername.trim();
|
||||
|
||||
// 2. 检查是否与当前用户名相同
|
||||
if (newUsername.equals(currentUser.getUsername())) {
|
||||
return Result.error(400, "新用户名与当前用户名相同");
|
||||
}
|
||||
|
||||
// 3. 检查用户名是否已被其他用户占用
|
||||
User existingUser = userRepository.findByUsername(newUsername);
|
||||
if (existingUser != null) {
|
||||
return Result.error(409, "用户名已被占用");
|
||||
}
|
||||
|
||||
// 4. 执行更新
|
||||
int rows = userRepository.updateUsername(currentUser.getId(), newUsername);
|
||||
if (rows > 0) {
|
||||
log.info("用户 [{}] 用户名修改为 [{}]", currentUser.getUsername(), newUsername);
|
||||
return Result.success("用户名修改成功", null);
|
||||
} else {
|
||||
return Result.error(400, "用户名修改失败");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("修改用户名失败", e);
|
||||
return Result.error("系统繁忙,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改头像
|
||||
* PUT /api/profile/avatar
|
||||
*
|
||||
* 请求体:{"avatarData": "data:image/png;base64,iVBOR..."}
|
||||
*
|
||||
* 流程:
|
||||
* 1. 接收 Base64 编码的图片数据
|
||||
* 2. 解析 MIME 类型和 Base64 内容
|
||||
* 3. 上传到 MinIO(路径:avatar/{userId}_{timestamp}.{ext})
|
||||
* 4. 更新数据库中的 avatar_url 字段
|
||||
*/
|
||||
@PutMapping("/api/profile/avatar")
|
||||
public Result<?> updateAvatar(HttpServletRequest request,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
|
||||
User currentUser = getCurrentUser(request);
|
||||
log.info("用户 [{}] 请求修改头像", currentUser.getUsername());
|
||||
|
||||
try {
|
||||
// 1. 参数校验
|
||||
String avatarData = (String) body.get("avatarData");
|
||||
if (avatarData == null || avatarData.isBlank()) {
|
||||
return Result.error(400, "头像数据不能为空");
|
||||
}
|
||||
|
||||
// 2. 解析 Base64 图片数据
|
||||
String base64Content = avatarData;
|
||||
String mimeType = "image/png";
|
||||
|
||||
if (avatarData.startsWith("data:")) {
|
||||
int semicolonIndex = avatarData.indexOf(';');
|
||||
int commaIndex = avatarData.indexOf(',');
|
||||
if (semicolonIndex > 0 && commaIndex > semicolonIndex) {
|
||||
mimeType = avatarData.substring(5, semicolonIndex);
|
||||
base64Content = avatarData.substring(commaIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 解码 Base64
|
||||
byte[] imageBytes = Base64.getDecoder().decode(base64Content);
|
||||
|
||||
// 4. 确定文件扩展名
|
||||
String extension = switch (mimeType) {
|
||||
case "image/jpeg", "image/jpg" -> ".jpg";
|
||||
case "image/gif" -> ".gif";
|
||||
case "image/webp" -> ".webp";
|
||||
default -> ".png";
|
||||
};
|
||||
|
||||
// 5. 生成 MinIO 对象名(路径:avatar/{userId}_{timestamp}.{ext})
|
||||
String objectName = "avatar/" + currentUser.getId() + "_"
|
||||
+ System.currentTimeMillis() + extension;
|
||||
|
||||
// 6. 上传到 MinIO
|
||||
try (var inputStream = new ByteArrayInputStream(imageBytes)) {
|
||||
String uploadedName = MinioUtil.upload(inputStream, objectName, mimeType);
|
||||
if (uploadedName == null) {
|
||||
return Result.error(500, "头像上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 获取文件访问 URL
|
||||
String avatarUrl = MinioUtil.getFileUrl(objectName);
|
||||
|
||||
// 8. 更新数据库
|
||||
int rows = userRepository.updateAvatarUrl(currentUser.getId(), avatarUrl);
|
||||
if (rows > 0) {
|
||||
log.info("用户 [{}] 头像更新成功: {}", currentUser.getUsername(), avatarUrl);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("avatarUrl", avatarUrl);
|
||||
return Result.success("头像更新成功", data);
|
||||
} else {
|
||||
return Result.error(400, "头像更新失败");
|
||||
}
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Base64解码失败", e);
|
||||
return Result.error(400, "图片数据格式错误");
|
||||
} catch (Exception e) {
|
||||
log.error("修改头像失败", e);
|
||||
return Result.error("系统繁忙,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,3 +38,10 @@ mybatis-plus:
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# MinIO 对象存储配置(头像上传用)
|
||||
minio:
|
||||
endpoint: ENC(nDpSqjv62odavtIjInqIzcjmWua20QGTKKfKsJlxgCTkWGWj/JJsIAEfsLIzKjpbeAMl/mxVJuPQv8o58b46gQ==)
|
||||
accessKey: ENC(YM+d/P5KM/QqGFcbbxcZlM+jH0dTgaYlGiYOJ04dIyBX+Z4fu+h05tdylTy0SroK)
|
||||
secretKey: ENC(BKqQFlceb8mGmAMWbmXFVV1US1Ib+24VApZCci0xyz+xAqL9MQetHJKsrWW1Vib2)
|
||||
bucketName: wangpan
|
||||
|
||||
Loading…
Reference in New Issue
Block a user