新增设备策略信息表 sys_sn_device_policy 及对应实体、Mapper、Converter、VO、Req 模型,并在设备端、家属端、管理端分别提供策略查询、全量更新和单独开关设置接口。同时修正设备菜单按钮权限命名(add/edit → create/update),并调整删除设备与解绑接口的权限和返回值。
402 lines
17 KiB
Java
402 lines
17 KiB
Java
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());
|
||
}
|
||
}
|