314 lines
12 KiB
Java
314 lines
12 KiB
Java
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());
|
||
}
|
||
}
|
||
}
|