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; /** * 客户端(家属端)设备操作控制层 * *

将后台管理端 {@code DeviceOpsController} 的设备操作能力移植到客户端: * 家属端用户登录后可对已绑定设备执行刷新、截图、重启、关机、定位、恢复、 * 开发者模式、应用操作以及截图管理等能力,路径统一挂在 {@code /api/v1/client/sn/ops} 下。 * *

与 {@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。 *

* 绑定关系:设备表 {@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().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 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 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> listScreenshots( @Parameter(description = "设备序列号") @RequestParam("sn") String sn ) { try { checkBound(sn); List 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 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 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()); } } }