增加上传和获取应用图标

This commit is contained in:
2025-09-12 18:42:51 +08:00
parent 8dd351cccd
commit 28971f9b14
13 changed files with 322 additions and 52 deletions

View File

@@ -1,10 +1,8 @@
FROM eclipse-temurin:21-jdk-jammy FROM eclipse-temurin:21-jdk-jammy
MAINTAINER TongTongStudio <tongtongstudios@gmail.com> MAINTAINER TongTongStudio <tongtongstudios@gmail.com>
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak
RUN mkdir /data
RUN mkdir /data/uploads/ RUN mkdir -p /data/uploads/
RUN mkdir /data/uploads/tablet/
RUN mkdir /data/uploads/tablet/avatar/
VOLUME /tmp VOLUME /tmp
ADD target/*.jar app.jar ADD target/*.jar app.jar

View File

@@ -138,6 +138,13 @@
<version>2.10.1</version> <version>2.10.1</version>
</dependency> </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.14.0</version>
</dependency>
<!--添加数据库驱动依赖(根据实际数据库选择)--> <!--添加数据库驱动依赖(根据实际数据库选择)-->
<dependency> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>

View File

@@ -1,13 +1,42 @@
package com.onekeycall.videotablet.config; package com.onekeycall.videotablet.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import java.io.File; import java.io.File;
public class FilePath { public class FilePath {
public static final String UPLOAD_FILE_PATH = "uploadFile"; // @Value("${file.upload-dir-unix}")
private static final String unixUploadDir = "/data/uploads" ;
// @Value("${file.upload-dir-windows}")
private static final String windowsUploadDir = "uploadFile";
private static Logger logger = LoggerFactory.getLogger(FilePath.class);
public static final String TABLET_PATH = "tablet"; public static final String TABLET_PATH = "tablet";
public static final String AVATAR_PATH = "avatar"; public static final String AVATAR_PATH = "avatar";
public static final String APK_ICON_PATH = "apkIcon";
public static String getAvatarPath(){ public static String getRootPath() {
return UPLOAD_FILE_PATH + File.separator + TABLET_PATH + File.separator + AVATAR_PATH + File.separator; String osName = System.getProperty("os.name");
logger.info("osName: {}", osName);
if (osName.contains("Windows")) {
String projectPath = System.getProperty("user.dir");
logger.info("projectPath: {}", projectPath);
return projectPath + File.separator + windowsUploadDir;
} else {
return unixUploadDir;
}
}
public static String getAvatarPath() {
return getRootPath() + File.separator + TABLET_PATH + File.separator + AVATAR_PATH + File.separator;
}
public static String getApkIconPath() {
return getRootPath() + File.separator + TABLET_PATH + File.separator + APK_ICON_PATH + File.separator;
} }
} }

View File

@@ -0,0 +1,106 @@
package com.onekeycall.videotablet.controller.pub;
import com.onekeycall.videotablet.config.FilePath;
import com.onekeycall.videotablet.entity.ApkIconFileInfo;
import com.onekeycall.videotablet.entity.TabletDefaultSettings;
import com.onekeycall.videotablet.mapper.TabletDefaultSettingsMapper;
import com.onekeycall.videotablet.result.Result;
import com.onekeycall.videotablet.service.ApkIconService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/public")
public class FileController {
Logger logger = LoggerFactory.getLogger(FileController.class);
@Autowired
private TabletDefaultSettingsMapper tabletDefaultSettingsMapper;
@Autowired
private ApkIconService apkIconService;
@PostMapping("/tablet/upload_avatar")
public Result uploadAvatar(@RequestParam("file") MultipartFile multipartFile) {
String avatarPath = new FilePath().getAvatarPath();
File fileDir = new File(avatarPath);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
String originalFilename = multipartFile.getOriginalFilename();
logger.info("originalFilename:" + originalFilename);
File file = new File(avatarPath + File.separator + originalFilename);
logger.info("file path = " + file.getAbsolutePath());
try {
multipartFile.transferTo(file);
} catch (IOException e) {
logger.error(e.getMessage());
return Result.error().message("upload avatarPath failed");
}
// 检查是否存在默认设置记录
TabletDefaultSettings tabletDefaultSettings = tabletDefaultSettingsMapper.getDefaultSettings();
if (tabletDefaultSettings == null) {
// 如果不存在,则创建新记录
tabletDefaultSettings = new TabletDefaultSettings();
tabletDefaultSettings.setDefaultAvatar(originalFilename);
tabletDefaultSettingsMapper.insertDefaultSettings(tabletDefaultSettings);
} else {
// 如果存在,则更新记录
tabletDefaultSettings.setDefaultAvatar(originalFilename);
tabletDefaultSettingsMapper.updateDefaultSettings(tabletDefaultSettings);
}
return Result.ok();
}
@GetMapping("/apk_icon/get_url")
public ResponseEntity<?> getApkIconUrl(
@RequestParam("package_name") String packageName) {
List<ApkIconFileInfo> apkIconFileInfoList = apkIconService.findByPackageName(packageName);
if (apkIconFileInfoList.isEmpty()) {
return new ResponseEntity<>(Result.notFound().message("未找到图标"), HttpStatus.NOT_FOUND);
}
Optional<ApkIconFileInfo> apkIconFileInfoOptional = apkIconFileInfoList.stream().max(new Comparator<ApkIconFileInfo>() {
@Override
public int compare(ApkIconFileInfo o1, ApkIconFileInfo o2) {
return Long.compare(o1.getVersionCode(), o2.getVersionCode());
}
});
ApkIconFileInfo apkIconFileInfo = apkIconFileInfoOptional.get();
File file = new File(FilePath.getApkIconPath(), apkIconFileInfo.getFileName());
if (!file.exists()) {
return new ResponseEntity<>(Result.notFound().message("未找到图标"), HttpStatus.NOT_FOUND);
}
// 包装文件资源
Resource resource = new FileSystemResource(file);
// 设置响应头(让浏览器下载而非直接打开)
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
}

View File

@@ -35,50 +35,8 @@ public class ManageSnController {
@Autowired @Autowired
private DeviceSnService deviceSnService; private DeviceSnService deviceSnService;
@Autowired @Autowired
private TabletDefaultSettingsMapper tabletDefaultSettingsMapper;
@Autowired
private TabletDefaultSettingsRepository tabletDefaultSettingsRepository; private TabletDefaultSettingsRepository tabletDefaultSettingsRepository;
@PostMapping("/tablet/upload_avatar")
public Result uploadAvatar(@RequestParam("file") MultipartFile multipartFile) {
String projectPath = System.getProperty("user.dir");
String avatarPath = projectPath + File.separator + FilePath.getAvatarPath();
File fileDir = new File(avatarPath);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
String originalFilename = multipartFile.getOriginalFilename();
logger.info("originalFilename:" + originalFilename);
File file = new File(avatarPath + File.separator + originalFilename);
logger.info("file path = " + file.getAbsolutePath());
try {
multipartFile.transferTo(file);
} catch (IOException e) {
logger.error(e.getMessage());
return Result.error().message("upload avatarPath failed");
}
// 检查是否存在默认设置记录
TabletDefaultSettings tabletDefaultSettings = tabletDefaultSettingsMapper.getDefaultSettings();
if (tabletDefaultSettings == null) {
// 如果不存在,则创建新记录
tabletDefaultSettings = new TabletDefaultSettings();
tabletDefaultSettings.setDefaultAvatar(originalFilename);
tabletDefaultSettingsMapper.insertDefaultSettings(tabletDefaultSettings);
} else {
// 如果存在,则更新记录
tabletDefaultSettings.setDefaultAvatar(originalFilename);
tabletDefaultSettingsMapper.updateDefaultSettings(tabletDefaultSettings);
}
return Result.ok();
}
@PostMapping("/add_sn") @PostMapping("/add_sn")
public Result addSn( public Result addSn(
@RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId,

View File

@@ -1,18 +1,27 @@
package com.onekeycall.videotablet.controller.sn; package com.onekeycall.videotablet.controller.sn;
import com.onekeycall.videotablet.config.FilePath;
import com.onekeycall.videotablet.entity.ApkIconFileInfo;
import com.onekeycall.videotablet.entity.Contact; import com.onekeycall.videotablet.entity.Contact;
import com.onekeycall.videotablet.entity.DeviceInfo; import com.onekeycall.videotablet.entity.DeviceInfo;
import com.onekeycall.videotablet.entity.DeviceLocation; import com.onekeycall.videotablet.entity.DeviceLocation;
import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.result.Result;
import com.onekeycall.videotablet.service.ApkIconService;
import com.onekeycall.videotablet.service.ContactService; import com.onekeycall.videotablet.service.ContactService;
import com.onekeycall.videotablet.service.DeviceLocationService; import com.onekeycall.videotablet.service.DeviceLocationService;
import com.onekeycall.videotablet.service.DeviceSnService; import com.onekeycall.videotablet.service.DeviceSnService;
import com.onekeycall.videotablet.utils.HashUtils;
import com.onekeycall.videotablet.utils.JwtUtil; import com.onekeycall.videotablet.utils.JwtUtil;
import com.onekeycall.videotablet.utils.TextUtils; import com.onekeycall.videotablet.utils.TextUtils;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -27,7 +36,10 @@ public class DevicesController {
private DeviceLocationService deviceLocationService; private DeviceLocationService deviceLocationService;
@Autowired @Autowired
private ContactService contactService; private ContactService contactService;
@Autowired
private ApkIconService apkIconService;
Logger logger = LoggerFactory.getLogger(DevicesController.class);
@PostMapping("/update_location") @PostMapping("/update_location")
public Result updateLocation( public Result updateLocation(
@@ -98,4 +110,50 @@ public class DevicesController {
} }
@PostMapping("/upload_apk_icon")
public Result uploadApkIcon(
@RequestPart(value = "file") MultipartFile file,
@RequestParam(value = "package_name") String packageName,
@RequestParam(value = "version_code") Long versionCode,
@RequestParam(value = "md5") String md5
) throws Exception {
String iconPath = FilePath.getApkIconPath();
logger.info("uploadApkIcon, iconPath: {}", iconPath);
File fileDir = new File(iconPath);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
String fileMd5 = HashUtils.calculateMultipartFileMd5(file);
if (!fileMd5.equals(md5)) {
return Result.error().message("file md5 not match");
}
logger.info("uploadApkIcon, fileMd5: {}", fileMd5);
if (apkIconService.existsByPackageNameAndMd5(packageName, md5)) {
return Result.error().message("apk icon already exists");
}
String originName = file.getOriginalFilename();
String fileExtension = FilenameUtils.getExtension(originName);
if (TextUtils.isEmpty(fileExtension)) {
return Result.error().message("file extension is empty");
}
String fileName = packageName + "_" + md5 + "." + fileExtension;
File destFile = new File(fileDir, fileName);
file.transferTo(destFile);
ApkIconFileInfo apkIconFileInfo = new ApkIconFileInfo();
apkIconFileInfo.setPackageName(packageName);
apkIconFileInfo.setVersionCode(versionCode);
apkIconFileInfo.setMd5(md5);
apkIconFileInfo.setOriginFileName(originName);
apkIconFileInfo.setFileName(fileName);
apkIconFileInfo.setFilSize(file.getSize());
apkIconService.save(apkIconFileInfo);
return Result.ok();
}
} }

View File

@@ -0,0 +1,40 @@
package com.onekeycall.videotablet.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Entity
@Table(name = "devices_apk_icon")
public class ApkIconFileInfo {
public ApkIconFileInfo() {
}
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
@Column(name = "id", unique = true, nullable = false)
private Long id;
@NotBlank(message = "包名不能为空")
@Column(name = "package_name", nullable = false)
private String packageName;
@NotBlank(message = "版本号不能为空")
@Column(name = "version_code", nullable = false)
private Long versionCode;
@Column(name = "md5", nullable = false)
private String md5;
@Column(name = "origin_file_name", nullable = false)
private String originFileName;
@Column(name = "file_name", nullable = false)
private String fileName;
@Column(name = "fil_size", nullable = false)
private Long filSize;
}

View File

@@ -0,0 +1,18 @@
package com.onekeycall.videotablet.repository;
import com.onekeycall.videotablet.entity.ApkIconFileInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ApkIconRepository extends JpaRepository<ApkIconFileInfo, Long> {
boolean existsByPackageNameAndMd5(String packageName, String md5);
boolean existsByPackageNameAndVersionCode(String packageName, Long versionCode);
List<ApkIconFileInfo> findByPackageNameAndMd5(String packageName, String md5);
List<ApkIconFileInfo> findByPackageName(String packageName);
}

View File

@@ -0,0 +1,30 @@
package com.onekeycall.videotablet.service;
import com.onekeycall.videotablet.entity.ApkIconFileInfo;
import com.onekeycall.videotablet.repository.ApkIconRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ApkIconService {
@Autowired
private ApkIconRepository apkIconRepository;
public void save(ApkIconFileInfo apkIconFileInfo) {
apkIconRepository.save(apkIconFileInfo);
}
public boolean existsByPackageNameAndMd5(String packageName, String md5) {
return apkIconRepository.existsByPackageNameAndMd5(packageName, md5);
}
public List<ApkIconFileInfo> findByPackageNameAndMd5(String packageName, String md5) {
return apkIconRepository.findByPackageNameAndMd5(packageName, md5);
}
public List<ApkIconFileInfo> findByPackageName(String packageName) {
return apkIconRepository.findByPackageName(packageName);
}
}

View File

@@ -1,5 +1,7 @@
package com.onekeycall.videotablet.utils; package com.onekeycall.videotablet.utils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@@ -8,6 +10,21 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
public class HashUtils { public class HashUtils {
public static String calculateMultipartFileMd5(MultipartFile file) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("MD5");
// 使用try-with-resources自动管理输入流
try (InputStream inputStream = file.getInputStream()) {
byte[] buffer = new byte[4096]; // 统一缓冲区大小为4KB
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes); // 复用现有工具方法
}
public static String getFileMD5(File file) throws NoSuchAlgorithmException, IOException { public static String getFileMD5(File file) throws NoSuchAlgorithmException, IOException {
MessageDigest md = MessageDigest.getInstance("MD5"); MessageDigest md = MessageDigest.getInstance("MD5");
FileInputStream fis = new FileInputStream(file); FileInputStream fis = new FileInputStream(file);

View File

@@ -61,3 +61,6 @@ logging.level.org.springframework.security=DEBUG
mybatis.type-aliases-package=com.onekeycall.videotablet.entity mybatis.type-aliases-package=com.onekeycall.videotablet.entity
mybatis.mapperLocations=classpath:mapper/*.xml mybatis.mapperLocations=classpath:mapper/*.xml
file.upload-dir-unix=/data/uploads
file.upload-dir-windows=uploadFile

View File

@@ -61,3 +61,6 @@ logging.level.org.springframework.security=DEBUG
mybatis.type-aliases-package=com.onekeycall.videotablet.entity mybatis.type-aliases-package=com.onekeycall.videotablet.entity
mybatis.mapperLocations=classpath:mapper/*.xml mybatis.mapperLocations=classpath:mapper/*.xml
file.upload-dir-unix=/data/uploads
file.upload-dir-windows=uploadFile

View File

@@ -61,3 +61,6 @@ logging.level.org.springframework.security=DEBUG
mybatis.type-aliases-package=com.onekeycall.videotablet.entity mybatis.type-aliases-package=com.onekeycall.videotablet.entity
mybatis.mapperLocations=classpath:mapper/*.xml mybatis.mapperLocations=classpath:mapper/*.xml
file.upload-dir-unix=/data/uploads
file.upload-dir-windows=uploadFile