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.converter.DevicePolicyConverter; import com.youlai.boot.device.model.entity.SnDeviceInfo; import com.youlai.boot.device.model.entity.SnDevicePolicy; import com.youlai.boot.device.model.req.DevicePolicyReq; import com.youlai.boot.device.model.req.DevicePolicyToggleReq; import com.youlai.boot.device.model.vo.DeviceApkInfoVO; import com.youlai.boot.device.model.vo.DeviceBriefVO; import com.youlai.boot.device.model.vo.DeviceLocationVO; import com.youlai.boot.device.model.vo.DevicePolicyVO; import com.youlai.boot.device.model.vo.DeviceSystemInfoVO; import com.youlai.boot.device.model.vo.ScreenshotVO; import com.youlai.boot.device.service.ApkInstallService; import com.youlai.boot.device.service.DevicePolicyService; 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * 客户端(家属端)控制层 * *

提供家属端获取已绑定设备的信息、定位、应用安装列表等能力。 * 所有接口均需要先校验当前登录的 client 用户是否绑定了传入的设备序列号(SN)。 * * @author TTSTD * @since 2026/08/17 */ @Tag(name = "客户端-设备管理") @RestController @RequestMapping("/api/v1/client/sn") @RequiredArgsConstructor @Slf4j public class ClientController { private final ClientUserService clientUserService; private final DeviceService deviceService; private final LocationService locationService; private final ApkInstallService apkInstallService; private final ScreenshotService screenshotService; private final DevicePolicyService devicePolicyService; private final DevicePolicyConverter devicePolicyConverter; /** * 校验当前登录的 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 = "获取已绑定设备基本信息") @PreAuthorize("isAuthenticated()") @GetMapping("/device-info") public Result getDeviceInfo( @Parameter(description = "设备序列号") @RequestParam("sn") String sn ) { try { checkBound(sn); DeviceSystemInfoVO vo = deviceService.getDeviceBasicInfo(sn); return Result.success(vo); } catch (BusinessException be) { return Result.failed(be.getMessage()); } catch (Exception e) { log.error("getDeviceInfo error, sn: {}", sn, e); return Result.failed("获取设备信息失败: " + e.getMessage()); } } @Operation(summary = "获取已绑定设备最新定位信息") @PreAuthorize("isAuthenticated()") @GetMapping("/location") public Result getLocation( @Parameter(description = "设备序列号") @RequestParam("sn") String sn ) { try { checkBound(sn); DeviceLocationVO vo = locationService.getLatestBySn(sn); return Result.success(vo); } 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 = "获取已绑定设备已安装应用列表") @PreAuthorize("isAuthenticated()") @GetMapping("/apks") public Result> getApks( @Parameter(description = "设备序列号") @RequestParam("sn") String sn ) { try { checkBound(sn); List list = apkInstallService.getDeviceApkInfo(sn); return Result.success(list); } catch (BusinessException be) { return Result.failed(be.getMessage()); } catch (Exception e) { log.error("getApks error, sn: {}", sn, e); return Result.failed("获取应用列表失败: " + e.getMessage()); } } @Operation(summary = "获取设备最近截图列表(家属端)") @Parameter(name = "sn", description = "设备序列号", required = true) @PreAuthorize("isAuthenticated()") @GetMapping("/upload-screenshot") public Result> uploadScreenshot( @RequestParam(value = "sn", required = false) String sn ) { try { if (!StringUtils.hasText(sn)) { return Result.failed("设备序列号不能为空"); } List list = screenshotService.listBySn(sn); return Result.success(list); } catch (BusinessException be) { return Result.failed(be.getMessage()); } catch (Exception e) { log.error("getScreenshots error, sn: {}", sn, e); return Result.failed("获取截图失败: " + e.getMessage()); } } @Operation(summary = "获取当前用户已绑定的设备列表") @PreAuthorize("isAuthenticated()") @GetMapping("/my-devices") public Result> getMyDevices() { try { Long userId = SecurityUtils.getUserId(); ClientUser clientUser = clientUserService.getById(userId); if (clientUser == null || !StringUtils.hasText(clientUser.getMobile())) { return Result.failed("当前用户未绑定手机号,无法查询设备"); } List devices = deviceService.list( new LambdaQueryWrapper() .eq(SnDeviceInfo::getSnMobile, clientUser.getMobile()) .orderByDesc(SnDeviceInfo::getCreateTime) ); List list = devices.stream().map(d -> { DeviceBriefVO vo = new DeviceBriefVO(); vo.setSerialno(d.getSerialno()); vo.setSnName(d.getSnName()); vo.setSnModel(d.getSnModel()); vo.setSnMobile(d.getSnMobile()); vo.setStatus(d.getStatus()); vo.setActivateTime(d.getActivateTime()); return vo; }).toList(); return Result.success(list); } catch (Exception e) { log.error("getMyDevices error", e); return Result.failed("获取设备列表失败: " + e.getMessage()); } } @Operation(summary = "绑定设备(家属端)", description = "将当前登录的 client 用户手机号写入设备 snMobile,建立绑定关系") @PreAuthorize("isAuthenticated()") @PostMapping("/bind") public Result bindDevice( @Parameter(description = "设备序列号") @RequestParam("sn") String sn ) { try { if (!StringUtils.hasText(sn)) { return Result.failed("设备序列号不能为空"); } Long userId = SecurityUtils.getUserId(); ClientUser clientUser = clientUserService.getById(userId); if (clientUser == null || !StringUtils.hasText(clientUser.getMobile())) { return Result.failed("当前用户未绑定手机号,无法绑定设备"); } SnDeviceInfo deviceInfo = deviceService.getOne( new LambdaQueryWrapper().eq(SnDeviceInfo::getSerialno, sn) ); if (deviceInfo == null) { return Result.failed("设备不存在"); } // 已绑定给其他用户则拒绝 if (StringUtils.hasText(deviceInfo.getSnMobile()) && !deviceInfo.getSnMobile().equals(clientUser.getMobile())) { return Result.failed("该设备已被其他用户绑定"); } // 建立/更新绑定关系 SnDeviceInfo update = new SnDeviceInfo(); update.setId(deviceInfo.getId()); update.setSnMobile(clientUser.getMobile()); boolean result = deviceService.updateById(update); return Result.judge(result); } catch (BusinessException be) { return Result.failed(be.getMessage()); } catch (Exception e) { log.error("bindDevice error, sn: {}", sn, e); return Result.failed("绑定设备失败: " + e.getMessage()); } } @Operation(summary = "解绑设备(家属端)", description = "解除当前登录的 client 用户与设备的绑定关系,仅允许本人解绑") @PreAuthorize("isAuthenticated()") @PostMapping("/unbind") public Result unbindDevice( @Parameter(description = "设备序列号") @RequestParam("sn") String sn ) { try { if (!StringUtils.hasText(sn)) { return Result.failed("设备序列号不能为空"); } Long userId = SecurityUtils.getUserId(); ClientUser clientUser = clientUserService.getById(userId); if (clientUser == null || !StringUtils.hasText(clientUser.getMobile())) { return Result.failed("当前用户未绑定手机号,无法解绑设备"); } SnDeviceInfo deviceInfo = deviceService.getOne( new LambdaQueryWrapper().eq(SnDeviceInfo::getSerialno, sn) ); if (deviceInfo == null) { return Result.failed("设备不存在"); } // 仅允许本人解绑 if (!StringUtils.hasText(deviceInfo.getSnMobile()) || !deviceInfo.getSnMobile().equals(clientUser.getMobile())) { return Result.failed("当前用户未绑定该设备,无法解绑"); } SnDeviceInfo update = new SnDeviceInfo(); update.setId(deviceInfo.getId()); update.setSnMobile(null); boolean result = deviceService.updateById(update); return Result.judge(result); } catch (BusinessException be) { return Result.failed(be.getMessage()); } catch (Exception e) { log.error("unbindDevice error, sn: {}", sn, e); return Result.failed("解绑设备失败: " + e.getMessage()); } } // ===================== 设备策略信息(家属端) ===================== @Operation(summary = "获取已绑定设备策略信息", description = "SN 合法且无记录时自动新建默认策略(默认全开)") @PreAuthorize("isAuthenticated()") @GetMapping("/policy") public Result getPolicy( @Parameter(description = "设备序列号") @RequestParam("sn") String sn ) { try { checkBound(sn); SnDevicePolicy policy = devicePolicyService.getOrInitBySn(sn); if (policy == null) { return Result.failed("设备SN不合法或不存在"); } return Result.success(devicePolicyConverter.toVo(policy)); } catch (BusinessException be) { return Result.failed(be.getMessage()); } catch (Exception e) { log.error("getPolicy error, sn: {}", sn, e); return Result.failed("获取策略信息失败: " + e.getMessage()); } } @Operation(summary = "全量设置已绑定设备策略信息", description = "SN 合法且无记录时新建,存在时更新(空字段保持原值)") @PreAuthorize("isAuthenticated()") @PutMapping("/policy") public Result updatePolicy( @Parameter(description = "设备序列号") @RequestParam("sn") String sn, @RequestBody DevicePolicyReq req ) { try { checkBound(sn); if (req == null) { return Result.failed("策略信息不能为空"); } SnDevicePolicy policy = new SnDevicePolicy(); policy.setSerialno(sn); copyPolicy(req, policy); boolean success = devicePolicyService.saveOrUpdateBySn(sn, policy); if (!success) { return Result.failed("保存策略信息失败(设备SN不合法或参数异常)"); } // 全量保存后推送策略状态到对应设备 devicePolicyService.pushPolicyToDevice(sn); return Result.success(); } catch (BusinessException be) { return Result.failed(be.getMessage()); } catch (Exception e) { log.error("updatePolicy error, sn: {}", sn, e); return Result.failed("保存策略信息失败: " + e.getMessage()); } } @Operation(summary = "单独设置已绑定设备策略开关", description = "通过 field 指定策略字段,value 设置 0/1") @PreAuthorize("isAuthenticated()") @PutMapping("/policy/toggle") public Result togglePolicy( @Parameter(description = "设备序列号") @RequestParam("sn") String sn, @RequestBody DevicePolicyToggleReq req ) { try { checkBound(sn); if (req == null || req.getField() == null || req.getValue() == null) { return Result.failed("field 与 value 不能为空"); } boolean success = devicePolicyService.updateSingleBySn(sn, req.getField(), req.getValue()); if (!success) { return Result.failed("设置策略开关失败(设备SN不合法或字段不支持)"); } // 单独保存后推送策略状态到对应设备 devicePolicyService.pushPolicyToDevice(sn); return Result.success(); } catch (BusinessException be) { return Result.failed(be.getMessage()); } catch (Exception e) { log.error("togglePolicy error, sn: {}", sn, e); return Result.failed("设置策略开关失败: " + e.getMessage()); } } private void copyPolicy(DevicePolicyReq src, SnDevicePolicy target) { target.setUsbData(src.getUsbData()); target.setTimeSetting(src.getTimeSetting()); target.setStorageCard(src.getStorageCard()); target.setFactoryReset(src.getFactoryReset()); target.setWifiHotspot(src.getWifiHotspot()); target.setBluetoothSwitch(src.getBluetoothSwitch()); target.setWallpaper(src.getWallpaper()); target.setNotificationBar(src.getNotificationBar()); target.setStatusBarPullDown(src.getStatusBarPullDown()); target.setSystemNavBarShow(src.getSystemNavBarShow()); target.setSystemNavBarSetting(src.getSystemNavBarSetting()); target.setNavBarOptions(src.getNavBarOptions()); target.setBluetoothFunction(src.getBluetoothFunction()); target.setOtaUpgrade(src.getOtaUpgrade()); target.setAppInstall(src.getAppInstall()); target.setAutoRotate(src.getAutoRotate()); target.setAutoBrightness(src.getAutoBrightness()); target.setEyeProtection(src.getEyeProtection()); target.setDarkMode(src.getDarkMode()); } }