feat:增加AList上传文件功能
This commit is contained in:
@@ -38,6 +38,13 @@
|
|||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-crypto</artifactId>
|
<artifactId>hutool-crypto</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jave 2 视频处理库 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>ws.schild</groupId>
|
||||||
|
<artifactId>jave-all-deps</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<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.ChunkResultDto;
|
||||||
import com.cxx.home.dto.file.FileInfoDto;
|
import com.cxx.home.dto.file.FileInfoDto;
|
||||||
import com.cxx.home.dto.file.FileRecordDto;
|
import com.cxx.home.dto.file.FileRecordDto;
|
||||||
|
import com.cxx.home.service.AListService;
|
||||||
import com.cxx.home.service.FileService;
|
import com.cxx.home.service.FileService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -20,6 +21,9 @@ public class FileController {
|
|||||||
@Resource
|
@Resource
|
||||||
private FileService fileService;
|
private FileService fileService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AListService aListService;
|
||||||
|
|
||||||
@Operation(summary = "上传文件")
|
@Operation(summary = "上传文件")
|
||||||
@PostMapping(value = "", headers = "content-type=multipart/form-data")
|
@PostMapping(value = "", headers = "content-type=multipart/form-data")
|
||||||
public FileRecordDto uploadFile(@RequestPart("file") MultipartFile file,
|
public FileRecordDto uploadFile(@RequestPart("file") MultipartFile file,
|
||||||
@@ -28,6 +32,13 @@ public class FileController {
|
|||||||
return fileService.uploadFile(file, path, md5);
|
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 = "上传文件块")
|
@Operation(summary = "上传文件块")
|
||||||
@PostMapping("/chunk")
|
@PostMapping("/chunk")
|
||||||
public Boolean uploadChunk(ChunkInfoDto chunkInfo,
|
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;
|
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.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
@@ -90,12 +91,21 @@ public class FoodServiceImpl implements FoodService {
|
|||||||
public Boolean addFoodRecord(FoodRecordDto foodRecordDto) {
|
public Boolean addFoodRecord(FoodRecordDto foodRecordDto) {
|
||||||
LambdaQueryWrapper<FoodRecipe> queryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<FoodRecipe> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
FoodRecipe foodRecipe = foodRecipeMapper.selectOne(queryWrapper.eq(FoodRecipe::getName, foodRecordDto.getName()));
|
FoodRecipe foodRecipe = foodRecipeMapper.selectOne(queryWrapper.eq(FoodRecipe::getName, foodRecordDto.getName()));
|
||||||
|
long recipeId;
|
||||||
if (foodRecipe == null) {
|
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 foodRecord = new FoodRecord();
|
||||||
foodRecord.setFoodId(foodRecipe.getId());
|
foodRecord.setFoodId(recipeId);
|
||||||
BeanUtils.copyProperties(foodRecordDto, foodRecord);
|
BeanUtils.copyProperties(foodRecordDto, foodRecord);
|
||||||
foodRecordMapper.insert(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@
|
name: @project.artifactId@
|
||||||
version: @project.version@
|
version: @project.version@
|
||||||
profiles:
|
profiles:
|
||||||
active: dev
|
active: docker
|
||||||
mail:
|
mail:
|
||||||
host: smtp.163.com
|
host: smtp.163.com
|
||||||
port: 465
|
port: 465
|
||||||
@@ -36,3 +36,7 @@ web-starter:
|
|||||||
|
|
||||||
aes:
|
aes:
|
||||||
secret-key: "sG5p7t9wBdKnPqRsUvYx2D5F8H9JmQ=2"
|
secret-key: "sG5p7t9wBdKnPqRsUvYx2D5F8H9JmQ=2"
|
||||||
|
|
||||||
|
alist:
|
||||||
|
username: admin
|
||||||
|
password: 19940822Cxx
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
a.category = #{query.category}
|
a.category = #{query.category}
|
||||||
</if>
|
</if>
|
||||||
</where>
|
</where>
|
||||||
|
ORDER BY a.create_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="queryFoodStats" resultType="com.cxx.home.dto.food.FoodStatsDto">
|
<select id="queryFoodStats" resultType="com.cxx.home.dto.food.FoodStatsDto">
|
||||||
|
|||||||
Reference in New Issue
Block a user