feat(client): 新增家属端设备查询与端隔离鉴权
- 新增 ClientController,支持已绑定设备的信息、定位、应用与截图查询 - JWT 增加 clientType 标识,实现 admin/client 端鉴权隔离 - client 端支持独立配置令牌有效期,并在刷新时按端解析 TTL - 截图列表仅返回最近 20 条并规范化访问 URL 前缀
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
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("/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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user