feat:增加AList上传文件功能
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cxx.home.service;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface AListService {
|
||||
void uploadFile(MultipartFile file, String filePath);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
a.category = #{query.category}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY a.create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="queryFoodStats" resultType="com.cxx.home.dto.food.FoodStatsDto">
|
||||
|
||||
Reference in New Issue
Block a user