feat:增加AList上传文件功能

This commit is contained in:
2026-02-02 22:55:20 +08:00
parent fcaef1409d
commit 3e83adf4f8
12 changed files with 378 additions and 7 deletions

View File

@@ -38,6 +38,13 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<!-- Jave 2 视频处理库 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>3.5.0</version>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,15 @@
package com.cxx.home.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "alist")
@Getter
@Setter
public class AListProperties {
private String username;
private String password;
}

View File

@@ -0,0 +1,25 @@
package com.cxx.home.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestClient;
@Configuration
public class RestClientConfig {
final String baseUrl = "http://192.168.1.7:5244/";
@Bean
public RestClient aListClient() {
return RestClient.builder()
// 基础RURL
.baseUrl(baseUrl)
// 默认请求头 请求体为JSON格式
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
// 默认请求头 响应体为JSON格式
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}

View File

@@ -0,0 +1,28 @@
package com.cxx.home.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.cxx.common.ReadView;
import com.cxx.common.WriteView;
import com.cxx.common.dto.StatsChartDto;
import com.cxx.home.dto.anniversary.AnniversaryDto;
import com.cxx.home.service.AnniversaryService;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/auth")
@Tag(name = "认证")
public class AuthController {
@Operation(summary = "查询在线人数")
@GetMapping("/online")
public List<String> queryOnline() {
return StpUtil.searchSessionId("", 0, -1, true);
}
}

View File

@@ -4,6 +4,7 @@ import com.cxx.home.dto.file.ChunkInfoDto;
import com.cxx.home.dto.file.ChunkResultDto;
import com.cxx.home.dto.file.FileInfoDto;
import com.cxx.home.dto.file.FileRecordDto;
import com.cxx.home.service.AListService;
import com.cxx.home.service.FileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -20,6 +21,9 @@ public class FileController {
@Resource
private FileService fileService;
@Resource
private AListService aListService;
@Operation(summary = "上传文件")
@PostMapping(value = "", headers = "content-type=multipart/form-data")
public FileRecordDto uploadFile(@RequestPart("file") MultipartFile file,
@@ -28,6 +32,13 @@ public class FileController {
return fileService.uploadFile(file, path, md5);
}
@Operation(summary = "上传aList文件")
@PostMapping(value = "/alist", consumes = "multipart/form-data")
public void uploadAListFile(@RequestParam("file") MultipartFile file,
@RequestParam("filePath") String filePath) {
aListService.uploadFile(file, filePath);
}
@Operation(summary = "上传文件块")
@PostMapping("/chunk")
public Boolean uploadChunk(ChunkInfoDto chunkInfo,

View File

@@ -0,0 +1,11 @@
package com.cxx.home.dto.alist;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AListLogin {
private String username;
private String password;
}

View File

@@ -0,0 +1,7 @@
package com.cxx.home.service;
import org.springframework.web.multipart.MultipartFile;
public interface AListService {
void uploadFile(MultipartFile file, String filePath);
}

View File

@@ -0,0 +1,99 @@
package com.cxx.home.service.impl;
import com.cxx.framework.web.CustomException;
import com.cxx.home.config.AListProperties;
import com.cxx.home.dto.alist.AListLogin;
import com.cxx.home.service.AListService;
import com.cxx.home.util.VideoCompressor;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class AListServiceImpl implements AListService {
@Resource
private RestClient aListClient;
@Resource
private AListProperties aListProperties;
private String getAccessToken() {
AListLogin aListLogin = new AListLogin();
aListLogin.setUsername(aListProperties.getUsername());
aListLogin.setPassword(aListProperties.getPassword());
Map<String, Object> response = aListClient.post()
.uri("api/auth/login")
.body(aListLogin)
.retrieve()
.body(Map.class);
if (response == null) {
throw new CustomException("AList登录失败 请联系管理员");
}
HashMap<String, Object> data = (HashMap<String, Object>) response.get("data");
return (String) data.get("token");
}
@Override
public void uploadFile(MultipartFile file, String filePath) {
try {
String token = getAccessToken();
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
File tempFile = convertMultipartFileToFile(file);
log.info("临时文件地址:{}", tempFile.toPath());
log.info("开始压缩文件:{}", tempFile.getName());
File compressFile = VideoCompressor.compressTo720p(tempFile);
log.info("完成压缩文件:{}", tempFile.getName());
log.info("压缩文件地址:{}", tempFile.toPath());
body.add("file", new FileSystemResource(compressFile));
aListClient.put()
.uri("api/fs/form")
.header(HttpHeaders.AUTHORIZATION, token)
.header("File-Path", filePath)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(body)
.retrieve()
.body(Map.class);
Files.deleteIfExists(compressFile.toPath());
Files.deleteIfExists(tempFile.toPath());
} catch (Exception e) {
log.error("文件上传失败{}", e.getMessage());
throw new RuntimeException("文件上传失败", e);
}
}
private File convertMultipartFileToFile(MultipartFile file) throws IOException {
String suffix = (file.getOriginalFilename() != null && file.getOriginalFilename().contains("."))
? file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))
: ".tmp";
Path tempFile = Files.createTempFile(System.currentTimeMillis() + "", suffix);
file.transferTo(tempFile);
return tempFile.toFile();
}
}

View File

@@ -1,5 +1,6 @@
package com.cxx.home.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -90,12 +91,21 @@ public class FoodServiceImpl implements FoodService {
public Boolean addFoodRecord(FoodRecordDto foodRecordDto) {
LambdaQueryWrapper<FoodRecipe> queryWrapper = new LambdaQueryWrapper<>();
FoodRecipe foodRecipe = foodRecipeMapper.selectOne(queryWrapper.eq(FoodRecipe::getName, foodRecordDto.getName()));
long recipeId;
if (foodRecipe == null) {
throw new CustomException("该菜谱不存在");
FoodRecipe newRecipe = new FoodRecipe();
newRecipe.setName(foodRecordDto.getName());
newRecipe.setCategory("其他");
newRecipe.setRecommendRate(0);
newRecipe.setRemark("");
foodRecipeMapper.insert(newRecipe);
recipeId = newRecipe.getId();
} else {
recipeId = foodRecipe.getId();
}
FoodRecord foodRecord = new FoodRecord();
foodRecord.setFoodId(foodRecipe.getId());
foodRecord.setFoodId(recipeId);
BeanUtils.copyProperties(foodRecordDto, foodRecord);
foodRecordMapper.insert(foodRecord);

View File

@@ -0,0 +1,153 @@
package com.cxx.home.util;
import lombok.extern.slf4j.Slf4j;
import ws.schild.jave.Encoder;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.encode.AudioAttributes;
import ws.schild.jave.encode.EncodingAttributes;
import ws.schild.jave.encode.VideoAttributes;
import java.io.File;
/**
* 视频码率压缩工具类
* 只压缩码率,保持原始分辨率和宽高比
*/
@Slf4j
public class VideoCompressor {
// 支持的视频格式
private static final String[] SUPPORTED_FORMATS = {
".mp4", ".avi", ".mov", ".mkv", ".flv",
".wmv", ".webm", ".mpeg", ".mpg", ".3gp"
};
// 码率压缩参数
private static final int TARGET_VIDEO_BITRATE = 1500000; // 1.5 Mbps
private static final int TARGET_AUDIO_BITRATE = 128000; // 128 kbps
private static final String VIDEO_CODEC = "libx264";
private static final String AUDIO_CODEC = "aac";
private static final String OUTPUT_FORMAT = "mp4";
private static final String OUTPUT_SUFFIX = "_compressed.mp4";
// 码率判断阈值
private static final int MAX_BITRATE_THRESHOLD = 2500000; // 2.5 Mbps
private static final int MIN_BITRATE_THRESHOLD = 1000000; // 1 Mbps
/**
* 压缩视频码率,保持原始分辨率和宽高比
*
* @param inputFile 输入视频文件
* @return 压缩后的视频文件,如果不需要压缩则返回原文件
* @throws Exception 压缩过程中可能出现的异常
*/
public static File compressTo720p(File inputFile) throws Exception {
if (!isSupportedVideoFile(inputFile)) {
log.info("文件格式不支持或文件不存在,直接返回源文件");
return inputFile;
}
// 判断是否需要压缩码率
if (!needBitrateCompression(inputFile)) {
return inputFile;
}
File outputFile = createOutputFile(inputFile);
Encoder encoder = new Encoder();
// 配置编码参数
EncodingAttributes encodingAttributes = createBitrateEncodingAttributes();
log.info("开始压缩视频: {} -> {}", inputFile.getName(), outputFile.getName());
encoder.encode(new MultimediaObject(inputFile), outputFile, encodingAttributes);
log.info("视频压缩完成: {}", outputFile.getName());
return outputFile;
}
/**
* 判断视频是否需要压缩码率
*/
public static boolean needBitrateCompression(File videoFile) throws Exception {
MultimediaObject media = new MultimediaObject(videoFile);
// 获取视频基本信息
double fileSizeMB = videoFile.length() / 1048576.0;
double duration = media.getInfo().getDuration() / 1000.0;
int originalBitrate = media.getInfo().getVideo().getBitRate();
// 获取视频分辨率
int width = media.getInfo().getVideo().getSize().getWidth();
int height = media.getInfo().getVideo().getSize().getHeight();
log.info("视频分析: 分辨率 {}x{}, 大小 {}MB, 时长 {}秒, 码率 {}bps", width, height, fileSizeMB, duration, originalBitrate);
// 如果原始码率超过阈值,就需要压缩
boolean needsCompression = originalBitrate > MAX_BITRATE_THRESHOLD;
if (needsCompression) {
log.info("视频码率({}bps)超过阈值,需要压缩", originalBitrate);
} else {
log.info("视频码率({}bps)在合理范围内,无需压缩", originalBitrate);
}
return needsCompression;
}
/**
* 创建码率压缩编码配置
*/
private static EncodingAttributes createBitrateEncodingAttributes() {
// 音频配置
AudioAttributes audio = new AudioAttributes();
audio.setCodec(AUDIO_CODEC);
audio.setBitRate(TARGET_AUDIO_BITRATE);
// 视频配置
VideoAttributes video = new VideoAttributes();
video.setCodec(VIDEO_CODEC);
video.setBitRate(TARGET_VIDEO_BITRATE);
// 编码设置
EncodingAttributes attributes = new EncodingAttributes();
attributes.setOutputFormat(OUTPUT_FORMAT);
attributes.setAudioAttributes(audio);
attributes.setVideoAttributes(video);
return attributes;
}
/**
* 检查文件是否为支持的视频格式
*/
private static boolean isSupportedVideoFile(File file) {
if (file == null || !file.exists()) {
return false;
}
String filename = file.getName().toLowerCase();
for (String format : SUPPORTED_FORMATS) {
if (filename.endsWith(format)) {
return true;
}
}
return false;
}
/**
* 创建输出文件对象
*/
private static File createOutputFile(File inputFile) {
String baseName = getFileBaseName(inputFile.getName());
String outputFilename = baseName + OUTPUT_SUFFIX;
return new File(inputFile.getParent(), outputFilename);
}
/**
* 获取文件名(不含扩展名)
*/
private static String getFileBaseName(String filename) {
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
}
}

View File

@@ -6,7 +6,7 @@ spring:
name: @project.artifactId@
version: @project.version@
profiles:
active: dev
active: docker
mail:
host: smtp.163.com
port: 465
@@ -36,3 +36,7 @@ web-starter:
aes:
secret-key: "sG5p7t9wBdKnPqRsUvYx2D5F8H9JmQ=2"
alist:
username: admin
password: 19940822Cxx

View File

@@ -106,16 +106,17 @@
</select>
<select id="queryFoodSummary" resultMap="foodSummaryResultMap">
SELECT a.id AS id,
a.name AS name,
a.category AS category,
a.recommend_rate AS recommendRate
SELECT a.id AS id,
a.name AS name,
a.category AS category,
a.recommend_rate AS recommendRate
FROM food_recipe a
<where>
<if test="query.category != null and query.category != ''">
a.category = #{query.category}
</if>
</where>
ORDER BY a.create_time DESC
</select>
<select id="queryFoodStats" resultType="com.cxx.home.dto.food.FoodStatsDto">