Files
youlai-boot/src/main/java/com/youlai/boot/client/controller/ClientController.java
TongTongStudio af2b9846f1 feat(device): 添加设备策略信息管理功能
新增设备策略信息表 sys_sn_device_policy 及对应实体、Mapper、Converter、VO、Req 模型,并在设备端、家属端、管理端分别提供策略查询、全量更新和单独开关设置接口。同时修正设备菜单按钮权限命名(add/edit → create/update),并调整删除设备与解绑接口的权限和返回值。
2026-08-27 11:48:45 +08:00

402 lines
17 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.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;
/**
* 客户端(家属端)控制层
*
* <p>提供家属端获取已绑定设备的信息、定位、应用安装列表等能力。
* 所有接口均需要先校验当前登录的 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。
* <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 = "获取已绑定设备基本信息")
@PreAuthorize("isAuthenticated()")
@GetMapping("/device-info")
public Result<DeviceSystemInfoVO> 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<DeviceLocationVO> 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<List<DeviceApkInfoVO>> getApks(
@Parameter(description = "设备序列号") @RequestParam("sn") String sn
) {
try {
checkBound(sn);
List<DeviceApkInfoVO> 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<List<ScreenshotVO>> uploadScreenshot(
@RequestParam(value = "sn", required = false) String sn
) {
try {
if (!StringUtils.hasText(sn)) {
return Result.failed("设备序列号不能为空");
}
List<ScreenshotVO> 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<List<DeviceBriefVO>> getMyDevices() {
try {
Long userId = SecurityUtils.getUserId();
ClientUser clientUser = clientUserService.getById(userId);
if (clientUser == null || !StringUtils.hasText(clientUser.getMobile())) {
return Result.failed("当前用户未绑定手机号,无法查询设备");
}
List<SnDeviceInfo> devices = deviceService.list(
new LambdaQueryWrapper<SnDeviceInfo>()
.eq(SnDeviceInfo::getSnMobile, clientUser.getMobile())
.orderByDesc(SnDeviceInfo::getCreateTime)
);
List<DeviceBriefVO> 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<SnDeviceInfo>().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<SnDeviceInfo>().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<DevicePolicyVO> 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());
}
}