feat(controlled, controller): 新增配对码生成与兑换功能

被控端:新增生成一次性配对码并弹窗展示,支持倒计时与复制;控制端:新增输入配对码兑换绑定,刷新设备列表,并在设备列表中显示在线状态和点击连接。同时添加统一 401 拦截器,令牌失效时触发重新激活。
This commit is contained in:
TongTongStudio
2026-08-03 01:57:42 +08:00
parent ded6fe5ab7
commit e689b999e6
29 changed files with 1096 additions and 86 deletions

View File

@@ -447,6 +447,25 @@ public class AdminController {
"total", deviceIdentityService.getAllowlist().size());
}
// ==================== 用户绑定关系 ====================
/** 列出某用户账号绑定的全部终端设备(含已撤销),并附在线状态。 */
@GetMapping("/users/{userId}/bindings")
public List<Map<String, Object>> userBindings(@PathVariable String userId) {
return bindingService.listByUser(userId).stream().map(b -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("bindingId", b.getBindingId());
item.put("deviceUid", b.getDeviceUid());
item.put("userId", b.getUserId());
item.put("role", b.getRole().name());
item.put("alias", b.getAlias());
item.put("status", b.getStatus().name());
item.put("online", sessionManager.isDeviceOnline(b.getDeviceUid()));
item.put("boundAt", b.getBoundAt() == null ? null : b.getBoundAt().toEpochMilli());
return item;
}).toList();
}
// ==================== 绑定 / 黑名单管理 ====================
/** 列出某设备的全部绑定关系(含已撤销)。 */

View File

@@ -1,6 +1,7 @@
package com.ttstd.signaling.controller;
import com.ttstd.signaling.config.BearerAuthFilter;
import com.ttstd.signaling.manager.SessionManager;
import com.ttstd.signaling.model.AccountStatus;
import com.ttstd.signaling.model.AuthPrincipal;
import com.ttstd.signaling.model.DeviceAccount;
@@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
@@ -50,6 +52,7 @@ public class ClientController {
private final PairingService pairingService;
private final TurnCredentialService turnCredentialService;
private final AbuseReportService abuseReportService;
private final SessionManager sessionManager;
public ClientController(JwtService jwtService,
SecurityProperties properties,
@@ -57,7 +60,8 @@ public class ClientController {
BindingService bindingService,
PairingService pairingService,
TurnCredentialService turnCredentialService,
AbuseReportService abuseReportService) {
AbuseReportService abuseReportService,
SessionManager sessionManager) {
this.jwtService = jwtService;
this.properties = properties;
this.deviceIdentityService = deviceIdentityService;
@@ -65,6 +69,7 @@ public class ClientController {
this.pairingService = pairingService;
this.turnCredentialService = turnCredentialService;
this.abuseReportService = abuseReportService;
this.sessionManager = sessionManager;
}
// ==================== 令牌校验 ====================
@@ -336,6 +341,32 @@ public class ClientController {
return r;
}
/**
* 主控端查看当前「已绑定且在线」的设备列表,便于快速发起连接。
* 仅返回当前在信令网络中有活跃 WebSocket 会话的被控端。
*/
@GetMapping("/devices/online")
public Map<String, Object> onlineDevices(HttpServletRequest request) {
AuthPrincipal principal = requirePrincipal(request);
if (!principal.isUser()) {
throw AuthException.forbidden("仅用户令牌可调用");
}
List<Map<String, Object>> online = bindingService.listByUser(principal.principalId()).stream()
.filter(b -> b.getStatus() == DeviceBinding.BindingStatus.ACTIVE)
.filter(b -> sessionManager.isDeviceOnline(b.getDeviceUid()))
.map(b -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("deviceUid", b.getDeviceUid());
m.put("alias", b.getAlias());
m.put("online", true);
return m;
})
.toList();
Map<String, Object> r = new LinkedHashMap<>();
r.put("devices", online);
return r;
}
private Map<String, Object> bindingView(DeviceBinding b) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("bindingId", b.getBindingId());
@@ -344,6 +375,7 @@ public class ClientController {
m.put("role", b.getRole().name());
m.put("alias", b.getAlias());
m.put("status", b.getStatus().name());
m.put("online", sessionManager.isDeviceOnline(b.getDeviceUid()));
return m;
}

View File

@@ -140,6 +140,8 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
Map<String, Object> response = new HashMap<>();
response.put("type", "REGISTER_SUCCESS");
response.put("deviceId", principal.deviceId());
// 与既有信令消息协议一致下发的设备ID也写入 fromDeviceId便于客户端统一解析。
response.put("fromDeviceId", principal.deviceId());
response.put("deviceType", principal.deviceType().name());
response.put("displayName", principal.displayName());
sendToSession(session, response);
@@ -286,6 +288,7 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
Map<String, Object> response = new HashMap<>();
response.put("type", "REGISTER_SUCCESS");
response.put("deviceId", principal.deviceId());
response.put("fromDeviceId", principal.deviceId());
response.put("deviceType", principal.deviceType().name());
response.put("displayName", principal.displayName());
sendToSession(session, response);