feat: 增加sn管理接口

This commit is contained in:
2026-06-01 08:24:02 +08:00
parent 06857f6c88
commit 33fbee9a00
73 changed files with 4591 additions and 28 deletions

View File

@@ -0,0 +1,313 @@
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());
}
}
}