feat: 添加设备自注册接口并优化设备在线管理逻辑

- 新增 `AndroidDeviceController` 用于设备自注册
- 优化 `DeviceOnlineManagementServiceImpl` 和 `AndroidDeviceBindingServiceImpl` 中的设备在线管理逻辑
- 更新 `DeviceInfoMapper` 添加 `updateBaseInfoByIdIgnoreTenant` 方法
- 在 `AndroidPublicMeetingController` 中添加设备未注册时的异常处理
- 更新 `AndroidAuthService` 和 `AndroidAuthServiceImpl` 支持设备是否需要注册的验证
dev_na
chenhao 2026-06-02 19:35:17 +08:00
parent 8716608afa
commit e7659b1e31
8 changed files with 108 additions and 35 deletions

View File

@ -1,6 +1,7 @@
package com.imeeting.controller.android; package com.imeeting.controller.android;
import com.imeeting.service.android.AndroidDeviceBindingService; import com.imeeting.service.android.AndroidDeviceBindingService;
import com.imeeting.service.android.AndroidDeviceRegistrationService;
import com.imeeting.support.AndroidRequestLogHelper; import com.imeeting.support.AndroidRequestLogHelper;
import com.unisbase.auth.JwtTokenProvider; import com.unisbase.auth.JwtTokenProvider;
import com.unisbase.common.ApiResponse; import com.unisbase.common.ApiResponse;
@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
public class AndroidAuthController { public class AndroidAuthController {
private final AuthService authService; private final AuthService authService;
private final AndroidDeviceBindingService androidDeviceBindingService; private final AndroidDeviceBindingService androidDeviceBindingService;
private final AndroidDeviceRegistrationService androidDeviceRegistrationService;
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
@Operation(summary = "Android登录") @Operation(summary = "Android登录")
@ -52,6 +54,10 @@ public class AndroidAuthController {
"deviceId", deviceId, "deviceId", deviceId,
"appVersion", appVersion, "appVersion", appVersion,
"platform", platform); "platform", platform);
if (!StringUtils.hasText(deviceId)) {
throw new IllegalArgumentException("X-Android-Device-Id不能为空");
}
androidDeviceRegistrationService.requireRegistered(deviceId.trim());
TokenResponse response = authService.login(request, true); TokenResponse response = authService.login(request, true);
if (response != null && response.getUser() != null && response.getCurrentTenantId() != null && StringUtils.hasText(deviceId)) { if (response != null && response.getUser() != null && response.getCurrentTenantId() != null && StringUtils.hasText(deviceId)) {
androidDeviceBindingService.bindPrivateDevice( androidDeviceBindingService.bindPrivateDevice(

View File

@ -0,0 +1,53 @@
package com.imeeting.controller.android;
import com.imeeting.dto.android.AndroidAuthContext;
import com.imeeting.dto.android.AndroidDeviceRegisterRequest;
import com.imeeting.dto.android.AndroidDeviceRegisterResponse;
import com.imeeting.service.android.AndroidAuthService;
import com.imeeting.service.android.AndroidDeviceRegistrationService;
import com.imeeting.support.AndroidRequestLogHelper;
import com.unisbase.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "Android设备接口")
@RestController
@RequestMapping("/api/android/devices")
@RequiredArgsConstructor
@Slf4j
public class AndroidDeviceController {
private final AndroidAuthService androidAuthService;
private final AndroidDeviceRegistrationService androidDeviceRegistrationService;
@Operation(summary = "设备自注册")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "返回设备注册后的基础信息",
content = @Content(schema = @Schema(implementation = AndroidDeviceRegisterResponse.class))
)
})
@PostMapping("/register")
public ApiResponse<AndroidDeviceRegisterResponse> register(HttpServletRequest request,
@RequestBody(required = false) AndroidDeviceRegisterRequest command) {
AndroidRequestLogHelper.logRequest(log, "Android设备", "设备自注册", "request", command);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request, false);
AndroidDeviceRegisterResponse response = androidDeviceRegistrationService.register(
authContext.getDeviceId(),
command == null ? null : command.getDeviceName(),
command == null ? authContext.getPlatform() : command.getTerminalType(),
command == null ? authContext.getAppVersion() : command.getTerminalVersion()
);
return ApiResponse.ok(response);
}
}

View File

@ -113,6 +113,9 @@ public class AndroidPublicMeetingController {
throw new RuntimeException("设备ID不能为空"); throw new RuntimeException("设备ID不能为空");
} }
var device = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId); var device = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId);
if (device == null) {
throw new RuntimeException("设备未注册,请先完成设备注册");
}
if (device != null && device.getUserId() != null) { if (device != null && device.getUserId() != null) {
throw new RuntimeException("当前设备为私有设备,请走私有设备发会流程"); throw new RuntimeException("当前设备为私有设备,请走私有设备发会流程");
} }

View File

@ -41,6 +41,20 @@ public interface DeviceInfoMapper extends BaseMapper<DeviceInfoEntity> {
""") """)
int updateConnectionInfoByIdIgnoreTenant(DeviceInfoEntity deviceInfoEntity); int updateConnectionInfoByIdIgnoreTenant(DeviceInfoEntity deviceInfoEntity);
@InterceptorIgnore(tenantLine = "true")
@Update("""
<script>
UPDATE biz_device_info
SET device_name = #{deviceName},
terminal_type = #{terminalType},
terminal_version = #{terminalVersion},
updated_at = CURRENT_TIMESTAMP
WHERE device_id = #{deviceId}
AND is_deleted = 0
</script>
""")
int updateBaseInfoByIdIgnoreTenant(DeviceInfoEntity deviceInfoEntity);
@InterceptorIgnore(tenantLine = "true") @InterceptorIgnore(tenantLine = "true")
@Update(""" @Update("""
UPDATE biz_device_info UPDATE biz_device_info

View File

@ -7,4 +7,6 @@ public interface AndroidAuthService {
AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform); AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform);
AndroidAuthContext authenticateHttp(HttpServletRequest request); AndroidAuthContext authenticateHttp(HttpServletRequest request);
AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered);
} }

View File

@ -40,18 +40,21 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
if (properties.isEnabled() && !properties.isAllowAnonymous()) { if (properties.isEnabled() && !properties.isAllowAnonymous()) {
throw new RuntimeException("Android gRPC push does not allow anonymous access"); throw new RuntimeException("Android gRPC push does not allow anonymous access");
} }
assertDeviceEnabled(deviceId); DeviceInfoEntity device = requireRegisteredDevice(deviceId);
assertDeviceEnabled(device);
AndroidAuthContext context = buildContext("NONE", true, deviceId, null, appVersion, platform, null, null, null, null); AndroidAuthContext context = buildContext("NONE", true, deviceId, null, appVersion, platform, null, null, null, null);
DeviceInfoEntity device = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId.trim()); context.setUserId(device.getUserId());
if (device != null) { context.setTenantId(device.getTenantId());
context.setUserId(device.getUserId());
context.setTenantId(device.getTenantId());
}
return context; return context;
} }
@Override @Override
public AndroidAuthContext authenticateHttp(HttpServletRequest request) { public AndroidAuthContext authenticateHttp(HttpServletRequest request) {
return authenticateHttp(request, true);
}
@Override
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered) {
LoginUser loginUser = currentLoginUser(); LoginUser loginUser = currentLoginUser();
String resolvedToken = resolveHttpToken(request); String resolvedToken = resolveHttpToken(request);
String deviceId = firstHeader(request, HEADER_DEVICE_ID); String deviceId = firstHeader(request, HEADER_DEVICE_ID);
@ -61,7 +64,8 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
requireAndroidHttpHeaders(deviceId, appVersion, platform); requireAndroidHttpHeaders(deviceId, appVersion, platform);
log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}",deviceId,appVersion,platform); log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}",deviceId,appVersion,platform);
assertDeviceEnabled(deviceId); DeviceInfoEntity device = requireRegistered ? requireRegisteredDevice(deviceId) : findDevice(deviceId);
assertDeviceEnabled(device);
if (loginUser != null) { if (loginUser != null) {
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, loginUser.getTenantId(), loginUser.getUserId()); androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, loginUser.getTenantId(), loginUser.getUserId());
return buildContext("USER_JWT", false, return buildContext("USER_JWT", false,
@ -192,16 +196,27 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
} }
} }
private void assertDeviceEnabled(String deviceId) { private void assertDeviceEnabled(DeviceInfoEntity device) {
if (!StringUtils.hasText(deviceId)) {
return;
}
DeviceInfoEntity device = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId.trim());
if (device != null && device.getStatus() != null && device.getStatus() == 0) { if (device != null && device.getStatus() != null && device.getStatus() == 0) {
throw new BusinessException("403", "设备被禁用"); throw new BusinessException("403", "设备被禁用");
} }
} }
private DeviceInfoEntity requireRegisteredDevice(String deviceId) {
DeviceInfoEntity device = findDevice(deviceId);
if (device == null) {
throw new RuntimeException("设备未注册,请先调用设备注册接口");
}
return device;
}
private DeviceInfoEntity findDevice(String deviceId) {
if (!StringUtils.hasText(deviceId)) {
return null;
}
return deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId.trim());
}
private String normalizeToken(String token) { private String normalizeToken(String token) {
if (!StringUtils.hasText(token)) { if (!StringUtils.hasText(token)) {
return null; return null;

View File

@ -21,19 +21,10 @@ public class AndroidDeviceBindingServiceImpl implements AndroidDeviceBindingServ
throw new RuntimeException("设备登录缺少绑定上下文"); throw new RuntimeException("设备登录缺少绑定上下文");
} }
DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceCode.trim()); DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceCode.trim());
LocalDateTime now = LocalDateTime.now();
if (existing == null) { if (existing == null) {
DeviceInfoEntity created = new DeviceInfoEntity(); throw new RuntimeException("设备未注册,请先完成设备注册");
created.setTenantId(tenantId);
created.setUserId(userId);
created.setDeviceCode(deviceCode.trim());
created.setTerminalType(normalize(platform));
created.setTerminalVersion(normalize(appVersion));
created.setLastOnlineAt(now);
created.setStatus(1);
deviceInfoMapper.insert(created);
return;
} }
LocalDateTime now = LocalDateTime.now();
existing.setTenantId(tenantId); existing.setTenantId(tenantId);
existing.setUserId(userId); existing.setUserId(userId);
existing.setTerminalType(normalize(platform)); existing.setTerminalType(normalize(platform));

View File

@ -34,21 +34,10 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement
} }
String deviceCode = authContext.getDeviceId().trim(); String deviceCode = authContext.getDeviceId().trim();
DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceCode); DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceCode);
LocalDateTime now = LocalDateTime.now();
if (existing == null) { if (existing == null) {
DeviceInfoEntity created = new DeviceInfoEntity();
created.setTenantId(authContext.getTenantId());
created.setUserId(authContext.getUserId());
created.setDeviceCode(deviceCode);
created.setDeviceName(null);
created.setTerminalType(normalizeTerminalType(authContext.getPlatform()));
created.setTerminalVersion(normalize(authContext.getAppVersion()));
created.setLastOnlineAt(now);
created.setStatus(1);
deviceInfoMapper.insert(created);
return; return;
} }
LocalDateTime now = LocalDateTime.now();
existing.setTerminalType(normalizeTerminalType(authContext.getPlatform())); existing.setTerminalType(normalizeTerminalType(authContext.getPlatform()));
existing.setTerminalVersion(normalize(authContext.getAppVersion())); existing.setTerminalVersion(normalize(authContext.getAppVersion()));
existing.setLastOnlineAt(now); existing.setLastOnlineAt(now);