Files
youlai-boot/src/main/java/com/youlai/boot/device/controller/MobileController.java

314 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.youlai.boot.device.controller;
import cn.hutool.core.util.IdUtil;
import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.config.FilePath;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.common.util.HashUtils;
import com.youlai.boot.device.model.entity.SnDeviceHardware;
import com.youlai.boot.device.model.req.ApkInstallInfoReq;
import com.youlai.boot.device.model.req.SnHardwareInfoReq;
import com.youlai.boot.device.model.req.SnLocationReq;
import com.youlai.boot.device.model.entity.SnDeveloper;
import com.youlai.boot.device.model.entity.SnLocation;
import com.youlai.boot.device.model.entity.SnScreenshot;
import com.youlai.boot.device.model.vo.DeveloperOptionsVO;
import com.youlai.boot.device.service.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 设备控制层
* @author TTSTD
* @since 2026/04/05
*/
@Tag(name = "16.移动设备管理")
@RestController
@RequestMapping("/api/v1/sn")
@RequiredArgsConstructor
public class MobileController {
private static final String DEVICE_SECRET_PREFIX = "device:secret:";
private final RedisTemplate<String, Object> redisTemplate;
private final DeviceService deviceService;
private final ScreenshotService screenshotService;
private final LocationService locationService;
private final DeveloperService developerService;
private final ApkInstallService apkInstallService;
private final HardwareService hardwareService;
private final Logger logger = LoggerFactory.getLogger(MobileController.class);
@Operation(summary = "注册设备")
@PostMapping("/register")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.REGISTER)
public Map<String, Object> registerDevice(@RequestParam() String sn) {
// 生成设备密钥
String deviceSecret = IdUtil.fastSimpleUUID();
// 存储到Redis可根据需要设置过期时间
redisTemplate.opsForValue().set(DEVICE_SECRET_PREFIX + sn, deviceSecret, 365, TimeUnit.DAYS);
Map<String, Object> result = new HashMap<>();
result.put("deviceId", sn);
result.put("deviceSecret", deviceSecret);
result.put("message", "请妥善保管设备密钥用于生成API签名");
return result;
}
@Operation(summary = "上传设备硬件信息")
@PostMapping("/update_hardware_info")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
public Result<Void> updateHardwareInfo(
@RequestHeader(value = "X-Device-SN") String sn,
@RequestBody SnHardwareInfoReq hardwareInfoReq
) {
try {
if (sn == null || sn.trim().isEmpty()) {
return Result.failed("设备序列号不能为空");
}
if (hardwareInfoReq == null) {
return Result.failed("硬件信息不能为空");
}
SnDeviceHardware hardware = new SnDeviceHardware();
BeanUtils.copyProperties(hardwareInfoReq, hardware);
hardware.setSerialno(sn);
hardware.setUpdateTime(LocalDateTime.now());
SnDeviceHardware existingHardware = hardwareService.lambdaQuery()
.eq(SnDeviceHardware::getSerialno, sn)
.one();
boolean saved;
if (existingHardware != null) {
hardware.setId(existingHardware.getId());
saved = hardwareService.updateById(hardware);
} else {
hardware.setCreateTime(LocalDateTime.now());
saved = hardwareService.save(hardware);
}
if (!saved) {
return Result.failed("保存硬件信息失败");
}
logger.info("硬件信息上传成功, sn: {}", sn);
return Result.success();
} catch (Exception e) {
logger.error("updateHardwareInfo error, sn: {}", sn, e);
return Result.failed("上传硬件信息失败: " + e.getMessage());
}
}
@Operation(summary = "上传设备截图")
@PostMapping("/upload_screenshot")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.SCREENSHOT)
public Result<Void> uploadScreenshot(
@RequestPart(value = "file") MultipartFile file,
@RequestHeader(value = "X-Device-SN") String sn
) {
try {
if (file.isEmpty()) {
return Result.failed("上传文件不能为空");
}
if (sn == null || sn.trim().isEmpty()) {
return Result.failed("设备序列号不能为空");
}
String screenshotPath = FilePath.getScreenshotPath();
logger.info("uploadScreenshot, screenshotPath: {}", screenshotPath);
File fileDir = new File(screenshotPath);
if (!fileDir.exists()) {
boolean created = fileDir.mkdirs();
if (!created) {
logger.error("创建目录失败: {}", screenshotPath);
return Result.failed("创建目录失败");
}
}
String originName = file.getOriginalFilename();
if (originName == null || originName.isEmpty()) {
return Result.failed("文件名无效");
}
String fileExtension = FilenameUtils.getExtension(originName);
String md5 = HashUtils.calculateMultipartFileMd5(file);
String sha1 = HashUtils.calculateMultipartFileSha1(file);
String sha256 = HashUtils.calculateMultipartFileSha256(file);
String fileName = sn + "_" + System.currentTimeMillis() + "_" + md5 + "." + fileExtension;
File destFile = new File(fileDir, fileName);
file.transferTo(destFile);
SnScreenshot screenshotInfo = new SnScreenshot();
screenshotInfo.setSn(sn);
screenshotInfo.setFileName(fileName);
screenshotInfo.setFilePath(screenshotPath + fileName);
screenshotInfo.setFileSize(file.getSize());
screenshotInfo.setFileMd5(md5);
screenshotInfo.setFileSha1(sha1);
screenshotInfo.setFileSha256(sha256);
screenshotInfo.setUploadTime(LocalDateTime.now());
screenshotService.save(screenshotInfo);
logger.info("截图上传成功, sn: {}, fileName: {}", sn, fileName);
return Result.success();
} catch (Exception e) {
logger.error("uploadScreenshot error, sn: {}", sn, e);
return Result.failed("上传截图失败: " + e.getMessage());
}
}
@Operation(summary = "上传设备定位信息")
@PostMapping("/upload_location")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.LOCATE)
public Result<Void> uploadLocation(
@RequestHeader(value = "X-Device-SN") String sn,
@RequestBody SnLocationReq locationReq
) {
try {
if (sn == null || sn.trim().isEmpty()) {
return Result.failed("设备序列号不能为空");
}
if (locationReq == null) {
return Result.failed("定位信息不能为空");
}
SnLocation location = new SnLocation();
BeanUtils.copyProperties(locationReq, location);
location.setSn(sn);
SnLocation existingLocation = locationService.lambdaQuery()
.eq(SnLocation::getSn, sn)
.one();
boolean saved;
if (existingLocation != null) {
location.setId(existingLocation.getId());
saved = locationService.updateById(location);
} else {
saved = locationService.save(location);
}
if (!saved) {
return Result.failed("保存定位信息失败");
}
logger.info("定位信息上传成功, sn: {}, location: {}", sn, locationReq);
return Result.success();
} catch (Exception e) {
logger.error("uploadLocation error, sn: {}", sn, e);
return Result.failed("上传定位信息失败: " + e.getMessage());
}
}
@Operation(summary = "获取开发者选项开关")
@GetMapping("/get_developer_options")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
public Result<DeveloperOptionsVO> getDeveloperOptions(@RequestHeader(value = "X-Device-SN") String sn) {
try {
if (sn == null || sn.trim().isEmpty()) {
return Result.failed("设备序列号不能为空");
}
// 查询该设备的开发者选项配置
SnDeveloper developerConfig = developerService.lambdaQuery()
.eq(SnDeveloper::getSn, sn)
.one();
DeveloperOptionsVO vo = new DeveloperOptionsVO();
if (developerConfig != null) {
vo.setDeveloperOptions(developerConfig.getDeveloperOptions());
} else {
vo.setDeveloperOptions(0);
}
// 返回开发者选项开关状态
return Result.success(vo);
} catch (Exception e) {
logger.error("getDeveloperOptions error, sn: {}", sn, e);
return Result.failed("获取开发者选项失败: " + e.getMessage());
}
}
@Operation(summary = "上传设备已安装应用列表")
@PostMapping("/upload_install_apks")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
public Result<Void> uploadInstallApks(
@RequestHeader(value = "X-Device-SN") String sn,
@RequestBody List<ApkInstallInfoReq> apkInfos
) {
try {
if (sn == null || sn.trim().isEmpty()) {
return Result.failed("设备序列号不能为空");
}
if (apkInfos == null || apkInfos.isEmpty()) {
return Result.failed("应用列表不能为空");
}
boolean saved = apkInstallService.saveOrUpdateDeviceApkInfo(sn, apkInfos);
if (!saved) {
return Result.failed("保存应用信息失败");
}
logger.info("应用列表上传成功, sn: {}, count: {}", sn, apkInfos.size());
return Result.success();
} catch (Exception e) {
logger.error("uploadInstallApks error, sn: {}", sn, e);
return Result.failed("上传应用列表失败: " + e.getMessage());
}
}
@Operation(summary = "获取设备已安装应用列表")
@GetMapping("/get_install_apks")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
public Result<List<ApkInstallInfoReq>> getInstallApks(@RequestHeader(value = "X-Device-SN") String sn) {
try {
if (sn == null || sn.trim().isEmpty()) {
return Result.failed("设备序列号不能为空");
}
List<ApkInstallInfoReq> apkInfos = apkInstallService.getDeviceApkInfo(sn);
logger.info("获取应用列表成功, sn: {}, count: {}", sn, apkInfos.size());
return Result.success(apkInfos);
} catch (Exception e) {
logger.error("getInstallApks error, sn: {}", sn, e);
return Result.failed("获取应用列表失败: " + e.getMessage());
}
}
}