Files
youlai-boot/src/main/java/com/youlai/boot/client/controller/ClientDeviceOpsController.java
TongTongStudio 2eb14103a1 feat(device): 新增远程拍照摄像头类型及家属端设备控制指令
- 拍照接口支持指定前置/后置摄像头,截图记录增加摄像头类型字段
- 新增文件传输、短信同步、清理、屏幕镜像、屏幕锁定指令下发
2026-08-24 08:41:12 +08:00

534 lines
21 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.client.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.youlai.boot.client.model.entity.ClientUser;
import com.youlai.boot.client.service.ClientUserService;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.device.model.entity.SnDeviceInfo;
import com.youlai.boot.device.model.form.AppActionForm;
import com.youlai.boot.device.model.form.DeveloperForm;
import com.youlai.boot.device.model.vo.DeviceLocationVO;
import com.youlai.boot.device.model.vo.DevicePageVO;
import com.youlai.boot.device.model.vo.ScreenshotVO;
import com.youlai.boot.device.service.DeviceService;
import com.youlai.boot.device.service.LocationService;
import com.youlai.boot.device.service.ScreenshotService;
import com.youlai.boot.framework.security.util.SecurityUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 客户端(家属端)设备操作控制层
*
* <p>将后台管理端 {@code DeviceOpsController} 的设备操作能力移植到客户端:
* 家属端用户登录后可对已绑定设备执行刷新、截图、重启、关机、定位、恢复、
* 开发者模式、应用操作以及截图管理等能力,路径统一挂在 {@code /api/v1/client/sn/ops} 下。
*
* <p>与 {@link ClientController} 一致:所有接口先通过 {@link #checkBound(String)}
* 校验当前登录的 client 用户是否绑定传入的设备 SN再执行操作。
* SN 的新增/删除属于平台管理职责,不在家属端开放。
*
* @author TTSTD
* @since 2026/08/18
*/
@Tag(name = "客户端-设备操作")
@RestController
@RequestMapping("/api/v1/client/sn/ops")
@RequiredArgsConstructor
@Slf4j
public class ClientDeviceOpsController {
private final ClientUserService clientUserService;
private final DeviceService deviceService;
private final LocationService locationService;
private final ScreenshotService screenshotService;
/**
* 校验当前登录的 client 用户是否绑定了传入的设备 SN。
* <p>
* 绑定关系:设备表 {@code sys_sn.snMobile} 与 client 用户 {@code app_user.mobile} 一致即视为已绑定。
* 校验未通过时抛出 {@link BusinessException},由调用方转换为失败结果返回。
*
* @param sn 设备序列号
* @return 已绑定的设备记录
*/
private SnDeviceInfo checkBound(String sn) {
if (!StringUtils.hasText(sn)) {
throw new BusinessException("设备序列号不能为空");
}
Long userId = SecurityUtils.getUserId();
ClientUser clientUser = clientUserService.getById(userId);
if (clientUser == null || !StringUtils.hasText(clientUser.getMobile())) {
throw new BusinessException("当前用户未绑定手机号,无法校验设备");
}
SnDeviceInfo deviceInfo = deviceService.getOne(
new LambdaQueryWrapper<SnDeviceInfo>().eq(SnDeviceInfo::getSerialno, sn)
);
if (deviceInfo == null) {
throw new BusinessException("设备不存在");
}
if (!StringUtils.hasText(deviceInfo.getSnMobile())
|| !deviceInfo.getSnMobile().equals(clientUser.getMobile())) {
log.warn("client用户 {} 未绑定设备 sn: {}", userId, sn);
throw new BusinessException("当前用户未绑定该设备");
}
return deviceInfo;
}
@Operation(summary = "获取SN绑定激活信息家属端")
@PreAuthorize("isAuthenticated()")
@GetMapping("/info")
public Result<DevicePageVO> getSnBindInfo(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
DevicePageVO detail = deviceService.getSnBindInfo(sn);
return Result.success(detail);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("getSnBindInfo error, sn: {}", sn, e);
return Result.failed("获取设备绑定信息失败: " + e.getMessage());
}
}
@Operation(summary = "设备刷新(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/refresh")
public Result<?> deviceRefresh(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.deviceRefresh(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("deviceRefresh error, sn: {}", sn, e);
return Result.failed("设备刷新失败: " + e.getMessage());
}
}
@Operation(summary = "设备截图(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/screenshot")
public Result<?> screenSnapshot(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.screenSnapshot(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("screenSnapshot error, sn: {}", sn, e);
return Result.failed("设备截图失败: " + e.getMessage());
}
}
@Operation(summary = "远程拍照(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/take_photo")
public Result<?> takePhoto(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn,
@Parameter(description = "摄像头类型0=后置摄像头1=前置摄像头)") @RequestParam("camera") int camera
) {
if (camera != 0 && camera != 1) {
return Result.failed("摄像头类型非法,仅支持 0后置或 1前置");
}
try {
checkBound(sn);
boolean result = deviceService.takePhoto(sn, camera);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("takePhoto error, sn: {}, camera: {}", sn, camera, e);
return Result.failed("远程拍照失败: " + e.getMessage());
}
}
@Operation(summary = "设备重启(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/reboot")
public Result<?> reboot(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.deviceReboot(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("reboot error, sn: {}", sn, e);
return Result.failed("设备重启失败: " + e.getMessage());
}
}
@Operation(summary = "设备关机(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/shutdown")
public Result<?> shutdown(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.deviceShutdown(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("shutdown error, sn: {}", sn, e);
return Result.failed("设备关机失败: " + e.getMessage());
}
}
@Operation(summary = "设备定位(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/locate")
public Result<?> deviceLocate(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.deviceLocate(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("deviceLocate error, sn: {}", sn, e);
return Result.failed("设备定位失败: " + e.getMessage());
}
}
@Operation(summary = "获取设备最新定位信息(家属端)", description = "返回设备最近一次上报的定位信息")
@PreAuthorize("isAuthenticated()")
@GetMapping("/location")
public Result<DeviceLocationVO> getLocation(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
DeviceLocationVO location = locationService.getLatestBySn(sn);
return Result.success(location);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("getLocation error, sn: {}", sn, e);
return Result.failed("获取定位信息失败: " + e.getMessage());
}
}
@Operation(summary = "获取设备截图列表(家属端)", description = "按上传时间倒序返回设备已上传的截图列表")
@PreAuthorize("isAuthenticated()")
@GetMapping("/screenshots")
public Result<List<ScreenshotVO>> listScreenshots(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
List<ScreenshotVO> list = screenshotService.listBySn(sn);
return Result.success(list);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("listScreenshots error, sn: {}", sn, e);
return Result.failed("获取截图列表失败: " + e.getMessage());
}
}
@Operation(summary = "删除单张设备截图(家属端)", description = "删除指定截图(数据库记录与磁盘文件)")
@PreAuthorize("isAuthenticated()")
@DeleteMapping("/screenshots/{id}")
public Result<Boolean> deleteScreenshot(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn,
@Parameter(description = "截图ID", required = true) @PathVariable Long id
) {
try {
checkBound(sn);
boolean result = screenshotService.deleteById(id);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("deleteScreenshot error, sn: {}, id: {}", sn, id, e);
return Result.failed("删除截图失败: " + e.getMessage());
}
}
@Operation(summary = "清空设备全部截图(家属端)", description = "删除该设备的所有截图(数据库记录与磁盘文件)")
@PreAuthorize("isAuthenticated()")
@DeleteMapping("/screenshots")
public Result<Integer> clearScreenshots(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
int count = screenshotService.clearBySn(sn);
return Result.success(count);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("clearScreenshots error, sn: {}", sn, e);
return Result.failed("清空截图失败: " + e.getMessage());
}
}
@Operation(summary = "设备重置(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/restore")
public Result<?> deviceRestore(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.restore(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("deviceRestore error, sn: {}", sn, e);
return Result.failed("设备重置失败: " + e.getMessage());
}
}
@Operation(summary = "开发者模式(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/developer")
public Result<?> deviceDeveloper(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.setDeviceDeveloper(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("deviceDeveloper error, sn: {}", sn, e);
return Result.failed("设置开发者模式失败: " + e.getMessage());
}
}
@Operation(summary = "新增开发者选项配置(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/developer/config")
public Result<?> addDeveloperConfig(
@Parameter(description = "开发者选项配置") @Valid @RequestBody DeveloperForm developerForm
) {
String sn = developerForm.getSn();
try {
checkBound(sn);
boolean result = deviceService.addDeveloperConfig(sn, developerForm.getDeveloperOptions());
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("addDeveloperConfig error, sn: {}", sn, e);
return Result.failed("新增开发者选项配置失败: " + e.getMessage());
}
}
@Operation(summary = "删除开发者选项配置(家属端)")
@PreAuthorize("isAuthenticated()")
@DeleteMapping("/developer/config")
public Result<?> deleteDeveloperConfig(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.deleteDeveloperConfig(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("deleteDeveloperConfig error, sn: {}", sn, e);
return Result.failed("删除开发者选项配置失败: " + e.getMessage());
}
}
@Operation(summary = "打开应用(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/app/launch")
public Result<?> launchApp(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn,
@Parameter(description = "应用操作表单") @Valid @RequestBody AppActionForm form
) {
try {
checkBound(sn);
boolean result = deviceService.launchApp(sn, form.getPackageName());
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("launchApp error, sn: {}, packageName: {}", sn, form.getPackageName(), e);
return Result.failed("打开应用失败: " + e.getMessage());
}
}
@Operation(summary = "清除应用数据(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/app/clear_data")
public Result<?> clearAppData(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn,
@Parameter(description = "应用操作表单") @Valid @RequestBody AppActionForm form
) {
try {
checkBound(sn);
boolean result = deviceService.clearAppData(sn, form.getPackageName());
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("clearAppData error, sn: {}, packageName: {}", sn, form.getPackageName(), e);
return Result.failed("清除应用数据失败: " + e.getMessage());
}
}
@Operation(summary = "卸载应用(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/app/uninstall")
public Result<?> uninstallApp(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn,
@Parameter(description = "应用操作表单") @Valid @RequestBody AppActionForm form
) {
try {
checkBound(sn);
boolean result = deviceService.uninstallApp(sn, form.getPackageName());
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("uninstallApp error, sn: {}, packageName: {}", sn, form.getPackageName(), e);
return Result.failed("卸载应用失败: " + e.getMessage());
}
}
@Operation(summary = "停止应用(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/app/stop")
public Result<?> stopApp(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn,
@Parameter(description = "应用操作表单") @Valid @RequestBody AppActionForm form
) {
try {
checkBound(sn);
boolean result = deviceService.stopApp(sn, form.getPackageName());
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("stopApp error, sn: {}, packageName: {}", sn, form.getPackageName(), e);
return Result.failed("停止应用失败: " + e.getMessage());
}
}
@Operation(summary = "文件传输(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/file_transfer")
public Result<?> fileTransfer(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.fileTransfer(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("fileTransfer error, sn: {}", sn, e);
return Result.failed("文件传输指令下发失败: " + e.getMessage());
}
}
@Operation(summary = "短信同步(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/msg_sync")
public Result<?> msgSync(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.msgSync(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("msgSync error, sn: {}", sn, e);
return Result.failed("短信同步指令下发失败: " + e.getMessage());
}
}
@Operation(summary = "清理(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/clean")
public Result<?> clean(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.clean(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("clean error, sn: {}", sn, e);
return Result.failed("清理指令下发失败: " + e.getMessage());
}
}
@Operation(summary = "屏幕镜像(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/mirror")
public Result<?> mirror(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.mirror(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("mirror error, sn: {}", sn, e);
return Result.failed("屏幕镜像指令下发失败: " + e.getMessage());
}
}
@Operation(summary = "屏幕锁定(家属端)")
@PreAuthorize("isAuthenticated()")
@PostMapping("/screen_lock")
public Result<?> screenLock(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
boolean result = deviceService.screenLock(sn);
return Result.judge(result);
} catch (BusinessException be) {
return Result.failed(be.getMessage());
} catch (Exception e) {
log.error("screenLock error, sn: {}", sn, e);
return Result.failed("屏幕锁定指令下发失败: " + e.getMessage());
}
}
}