feat(client): 新增家属端设备查询与端隔离鉴权

- 新增 ClientController,支持已绑定设备的信息、定位、应用与截图查询
- JWT 增加 clientType 标识,实现 admin/client 端鉴权隔离
- client 端支持独立配置令牌有效期,并在刷新时按端解析 TTL
- 截图列表仅返回最近 20 条并规范化访问 URL 前缀
This commit is contained in:
TongTongStudio
2026-08-19 03:43:48 +08:00
parent 7687ef4f37
commit 32921274a3
17 changed files with 901 additions and 8 deletions

View File

@@ -0,0 +1,201 @@
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.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.DeviceSystemInfoVO;
import com.youlai.boot.device.model.vo.ScreenshotVO;
import com.youlai.boot.device.service.ApkInstallService;
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.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;
/**
* 校验当前登录的 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());
}
}
}