diff --git a/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyAuthController.java b/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyAuthController.java index 03a6f79..71bdd8a 100644 --- a/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyAuthController.java +++ b/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyAuthController.java @@ -1,5 +1,6 @@ package com.imeeting.controller.android.legacy; +import com.google.protobuf.ServiceException; import com.imeeting.dto.android.legacy.LegacyApiResponse; import com.imeeting.dto.android.legacy.LegacyLoginResponse; import com.imeeting.dto.android.legacy.LegacyLoginUserResponse; @@ -40,6 +41,11 @@ public class LegacyAuthController { } catch (Exception e) { return LegacyApiResponse.error("400",e.getMessage()); } + try { + tokenResponse.getUser().setTenantId(tokenResponse.getAvailableTenants().stream().findFirst().orElseThrow(()-> new ServiceException("未绑定租户")).getTenantId()); + } catch (ServiceException e) { + return LegacyApiResponse.error("400",e.getMessage()); + } return LegacyApiResponse.ok(new LegacyLoginResponse( tokenResponse.getAccessToken(), tokenResponse.getRefreshToken(), @@ -68,6 +74,7 @@ public class LegacyAuthController { SysRoleDTO primaryRole = resolvePrimaryRole(user); return new LegacyLoginUserResponse( user.getUserId(), + user.getTenantId(), user.getUsername(), user.getDisplayName(), user.getAvatarUrl(), diff --git a/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyLoginUserResponse.java b/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyLoginUserResponse.java index 50c541c..3f93eeb 100644 --- a/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyLoginUserResponse.java +++ b/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyLoginUserResponse.java @@ -11,6 +11,7 @@ import java.time.LocalDateTime; @AllArgsConstructor public class LegacyLoginUserResponse { private Long user_id; + private Long tenant_id; private String username; private String caption; private String avatar_url; diff --git a/backend/src/main/java/com/imeeting/grpc/push/AndroidPushGrpcService.java b/backend/src/main/java/com/imeeting/grpc/push/AndroidPushGrpcService.java index 5cc0da6..3680594 100644 --- a/backend/src/main/java/com/imeeting/grpc/push/AndroidPushGrpcService.java +++ b/backend/src/main/java/com/imeeting/grpc/push/AndroidPushGrpcService.java @@ -13,11 +13,16 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + @Slf4j @Service @RequiredArgsConstructor public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase implements BindableService { + private static final DateTimeFormatter LOG_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final AndroidAuthService androidAuthService; private final AndroidDeviceSessionService androidDeviceSessionService; private final AndroidGatewayPushService androidGatewayPushService; @@ -28,6 +33,8 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase return new StreamObserver<>() { private String connectionId; private String deviceId; + private String appVersion; + private String platform; private boolean connected; @Override @@ -37,32 +44,69 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase case CONNECT -> handleConnect(message.getConnect()); case HEARTBEAT -> handleHeartbeat(message.getHeartbeat()); case ACK -> handleAck(message.getAck()); - case PAYLOAD_NOT_SET -> sendError(responseObserver, "PUSH_BAD_REQUEST", "Missing push payload", false); + case PAYLOAD_NOT_SET -> { + log.info(buildLog("gRPC请求", "收到空的客户端消息体", deviceId, appVersion, platform)); + sendError(responseObserver, "PUSH_BAD_REQUEST", "Missing push payload", false, deviceId, appVersion, platform); + } } } catch (BusinessException ex) { + log.info(buildLog("gRPC业务拒绝", + "gRPC推送请求被业务规则拒绝,连接ID=" + safe(connectionId) + ",原因=" + safe(ex.getMessage()), + deviceId, + appVersion, + platform)); log.warn("Android push gRPC business rejection, connectionId={}", connectionId, ex); - sendError(responseObserver, ex.getCode(), ex.getMessage(), false); + sendError(responseObserver, ex.getCode(), ex.getMessage(), false, deviceId, appVersion, platform); } catch (Exception ex) { + log.info(buildLog("gRPC处理异常", + "gRPC推送请求处理失败,连接ID=" + safe(connectionId) + ",异常=" + ex.getClass().getSimpleName(), + deviceId, + appVersion, + platform)); log.warn("Android push gRPC request handling failed, connectionId={}", connectionId, ex); - sendError(responseObserver, "PUSH_PROCESSING_ERROR", ex.getMessage(), false); + sendError(responseObserver, "PUSH_PROCESSING_ERROR", ex.getMessage(), false, deviceId, appVersion, platform); } } @Override public void onError(Throwable throwable) { + log.info(buildLog("gRPC异常断开", + "gRPC推送流异常断开,连接ID=" + safe(connectionId) + ",异常=" + throwable.getClass().getSimpleName(), + deviceId, + appVersion, + platform)); log.warn("Android push gRPC stream failed, connectionId={}", connectionId, throwable); cleanup(); } @Override public void onCompleted() { + log.info(buildLog("gRPC主动完成", + "客户端正常关闭gRPC推送流,连接ID=" + safe(connectionId), + deviceId, + appVersion, + platform)); cleanup(); responseObserver.onCompleted(); } private void handleConnect(ConnectRequest request) { + String requestDeviceId = request.getDeviceId(); + String requestAppVersion = request.getAppVersion(); + String requestPlatform = resolvePlatform(request.getPlatform()); + log.info(buildLog("gRPC连接请求", + "收到Android推送连接请求,请求连接ID=" + safe(request.getConnectionId()), + requestDeviceId, + requestAppVersion, + requestPlatform)); if (connected) { - sendError(responseObserver, "PUSH_ALREADY_CONNECTED", "Push connection already established", false); + log.info(buildLog("gRPC连接拒绝", + "重复发起连接,当前连接已建立,连接ID=" + safe(connectionId), + deviceId, + appVersion, + platform)); + sendError(responseObserver, "PUSH_ALREADY_CONNECTED", "Push connection already established", false, + deviceId, appVersion, platform); return; } AndroidAuthContext authContext = androidAuthService.authenticateGrpc( @@ -75,12 +119,24 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase AndroidDeviceSessionState sessionState = androidDeviceSessionService.openSession(authContext, request.getConnectionId()); connectionId = sessionState.getConnectionId(); deviceId = sessionState.getDeviceId(); + appVersion = authContext.getAppVersion(); + platform = authContext.getPlatform(); deviceOnlineManagementService.recordConnected(authContext); connected = true; String replacedConnectionId = androidGatewayPushService.register(connectionId, deviceId, responseObserver); if (replacedConnectionId != null && !replacedConnectionId.equals(connectionId)) { + log.info(buildLog("gRPC连接替换", + "同设备旧连接被新连接替换,旧连接ID=" + replacedConnectionId + ",新连接ID=" + connectionId, + deviceId, + appVersion, + platform)); androidDeviceSessionService.closeSession(replacedConnectionId); } + log.info(buildLog("gRPC连接成功", + "Android推送连接建立成功,连接ID=" + connectionId, + deviceId, + appVersion, + platform)); responseObserver.onNext(ServerMessage.newBuilder() .setConnectAck(ConnectResponse.newBuilder() .setSuccess(true) @@ -94,11 +150,13 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase return; } if (!request.getConnectionId().isBlank() && !request.getConnectionId().equals(connectionId)) { - sendError(responseObserver, "PUSH_CONNECTION_MISMATCH", "Connection id does not match active session", false); + sendError(responseObserver, "PUSH_CONNECTION_MISMATCH", "Connection id does not match active session", false, + deviceId, appVersion, platform); return; } if (!request.getDeviceId().isBlank() && !request.getDeviceId().equals(deviceId)) { - sendError(responseObserver, "PUSH_DEVICE_MISMATCH", "Device id does not match active session", false); + sendError(responseObserver, "PUSH_DEVICE_MISMATCH", "Device id does not match active session", false, + deviceId, appVersion, platform); return; } AndroidDeviceSessionState state = androidDeviceSessionService.refreshHeartbeat(connectionId, request.getTimestamp()); @@ -115,19 +173,43 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase return; } if (!request.getConnectionId().isBlank() && !request.getConnectionId().equals(connectionId)) { - sendError(responseObserver, "PUSH_CONNECTION_MISMATCH", "Connection id does not match active session", false); + log.info(buildLog("gRPC确认拒绝", + "ACK连接ID与当前活动连接不一致,请求连接ID=" + request.getConnectionId() + ",当前连接ID=" + connectionId, + deviceId, + appVersion, + platform)); + sendError(responseObserver, "PUSH_CONNECTION_MISMATCH", "Connection id does not match active session", false, + deviceId, appVersion, platform); return; } if (!request.getDeviceId().isBlank() && !request.getDeviceId().equals(deviceId)) { - sendError(responseObserver, "PUSH_DEVICE_MISMATCH", "Device id does not match active session", false); + log.info(buildLog("gRPC确认拒绝", + "ACK设备ID与当前活动设备不一致,请求设备ID=" + request.getDeviceId() + ",当前设备ID=" + deviceId, + deviceId, + appVersion, + platform)); + sendError(responseObserver, "PUSH_DEVICE_MISMATCH", "Device id does not match active session", false, + deviceId, appVersion, platform); + return; } + log.info(buildLog("gRPC消息确认", + "收到客户端ACK确认,消息ID=" + safe(request.getMessageId()) + ",连接ID=" + connectionId, + deviceId, + appVersion, + platform)); } private boolean validateConnected() { if (connected) { return true; } - sendError(responseObserver, "PUSH_NOT_CONNECTED", "Push connection has not been established", false); + log.info(buildLog("gRPC请求拒绝", + "连接尚未建立即发送后续消息", + deviceId, + appVersion, + platform)); + sendError(responseObserver, "PUSH_NOT_CONNECTED", "Push connection has not been established", false, + deviceId, appVersion, platform); return false; } @@ -139,14 +221,32 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase androidGatewayPushService.unregister(connectionId); androidDeviceSessionService.closeSession(connectionId); deviceOnlineManagementService.recordDisconnected(deviceId, state == null ? null : state.getLastSeenAt()); + log.info(buildLog("gRPC连接关闭", + "Android推送连接已关闭,连接ID=" + connectionId, + deviceId, + state == null ? appVersion : state.getAppVersion(), + state == null ? platform : state.getPlatform())); connectionId = null; deviceId = null; + appVersion = null; + platform = null; connected = false; } }; } - private void sendError(StreamObserver responseObserver, String code, String message, boolean retryable) { + private void sendError(StreamObserver responseObserver, + String code, + String message, + boolean retryable, + String deviceId, + String appVersion, + String platform) { + log.info(buildLog("gRPC错误响应", + "向客户端返回错误,错误码=" + safe(code) + ",原因=" + safe(message), + deviceId, + appVersion, + platform)); responseObserver.onNext(ServerMessage.newBuilder() .setError(ErrorEvent.newBuilder() .setCode(code) @@ -156,6 +256,20 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase .build()); } + private String buildLog(String actionType, String action, String deviceId, String appVersion, String platform) { + return String.format("[%s] %s %s 设备=%s 版本=%s 平台=%s", + safe(actionType), + LocalDateTime.now().format(LOG_TIME_FORMATTER), + safe(action), + safe(deviceId), + safe(appVersion), + safe(platform)); + } + + private String safe(String value) { + return value == null || value.isBlank() ? "-" : value.trim(); + } + private String resolvePlatform(Platform platform) { return switch (platform) { case IOS -> "ios"; diff --git a/backend/src/main/java/com/imeeting/service/android/impl/AndroidDeviceSessionServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/impl/AndroidDeviceSessionServiceImpl.java index 9f24047..55aec55 100644 --- a/backend/src/main/java/com/imeeting/service/android/impl/AndroidDeviceSessionServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/impl/AndroidDeviceSessionServiceImpl.java @@ -12,6 +12,8 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.UUID; @@ -20,6 +22,8 @@ import java.util.UUID; @RequiredArgsConstructor public class AndroidDeviceSessionServiceImpl implements AndroidDeviceSessionService { + private static final DateTimeFormatter LOG_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper; private final GrpcServerProperties grpcServerProperties; @@ -35,6 +39,11 @@ public class AndroidDeviceSessionServiceImpl implements AndroidDeviceSessionServ state.setPlatform(nonBlank(authContext.getPlatform(), "android")); state.setTenantCode(authContext.getTenantCode()); writeState(state); + log.info(buildLog("gRPC会话创建", + "创建Android设备会话,连接ID=" + state.getConnectionId(), + state.getDeviceId(), + state.getAppVersion(), + state.getPlatform())); return state; } @@ -109,6 +118,11 @@ public class AndroidDeviceSessionServiceImpl implements AndroidDeviceSessionServ redisTemplate.delete(RedisKeys.androidDeviceOnlineKey(state.getDeviceId())); } redisTemplate.delete(RedisKeys.androidDeviceConnectionKey(connectionId)); + log.info(buildLog("gRPC会话关闭", + "关闭Android设备会话,连接ID=" + connectionId, + state.getDeviceId(), + state.getAppVersion(), + state.getPlatform())); } private void writeState(AndroidDeviceSessionState state) { @@ -126,4 +140,18 @@ public class AndroidDeviceSessionServiceImpl implements AndroidDeviceSessionServ private String nonBlank(String value, String defaultValue) { return value != null && !value.isBlank() ? value : defaultValue; } + + private String buildLog(String actionType, String action, String deviceId, String appVersion, String platform) { + return String.format("[%s] %s %s 设备=%s 版本=%s 平台=%s", + safe(actionType), + LocalDateTime.now().format(LOG_TIME_FORMATTER), + safe(action), + safe(deviceId), + safe(appVersion), + safe(platform)); + } + + private String safe(String value) { + return value == null || value.isBlank() ? "-" : value.trim(); + } } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java index bf42695..54e9d9a 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java @@ -102,7 +102,7 @@ public class MeetingSummaryPromptAssembler { .append("}\n") .append("要求:\n") .append("1. `summaryContent` 必须优先遵循模板提示词中的结构、标题层级、章节顺序和写作风格。\n") - .append("2. `analysis.keywords` 必须基于完整转写内容生成,不得脱离上下文。并且在转录中能找到对应的原文\n") + .append("2. `analysis.keywords` 必须基于完整转写内容生成,不得脱离上下文。并且在会议转写中能找到对应的原文\n") // .append("3. 若无待办事项,`todos` 返回空数组。\n") .append("3. 仅输出 JSON。\n") .append("\n") diff --git a/backend/src/test/java/com/imeeting/service/android/impl/AndroidAuthServiceImplTest.java b/backend/src/test/java/com/imeeting/service/android/impl/AndroidAuthServiceImplTest.java deleted file mode 100644 index 00885e5..0000000 --- a/backend/src/test/java/com/imeeting/service/android/impl/AndroidAuthServiceImplTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.imeeting.service.android.impl; - -import com.imeeting.config.grpc.AndroidGrpcAuthProperties; -import com.imeeting.dto.android.AndroidAuthContext; -import com.unisbase.dto.InternalAuthCheckResponse; -import com.unisbase.service.TokenValidationService; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class AndroidAuthServiceImplTest { - - @AfterEach - void clearSecurityContext() { - SecurityContextHolder.clearContext(); - } - - @Test - void authenticateHttpShouldResolveBearerTokenAndIdentity() { - AndroidGrpcAuthProperties properties = new AndroidGrpcAuthProperties(); - TokenValidationService tokenValidationService = mock(TokenValidationService.class); - AndroidAuthServiceImpl service = new AndroidAuthServiceImpl(properties, tokenValidationService); - HttpServletRequest request = mock(HttpServletRequest.class); - - when(request.getHeader("Authorization")).thenReturn("Bearer access-token"); - when(request.getHeader("X-Android-Device-Id")).thenReturn("device-01"); - when(request.getHeader("X-Android-App-Id")).thenReturn("imeeting"); - when(request.getHeader("X-Android-App-Version")).thenReturn("1.0.0"); - when(request.getHeader("X-Android-Platform")).thenReturn("android"); - - InternalAuthCheckResponse authResult = new InternalAuthCheckResponse(); - authResult.setValid(true); - authResult.setUserId(11L); - authResult.setTenantId(22L); - authResult.setUsername("alice"); - authResult.setPlatformAdmin(false); - authResult.setTenantAdmin(true); - authResult.setPermissions(Set.of("meeting:create")); - when(tokenValidationService.validateAccessToken("access-token")).thenReturn(authResult); - - AndroidAuthContext context = service.authenticateHttp(request); - - assertFalse(context.isAnonymous()); - assertEquals("USER_JWT", context.getAuthMode()); - assertEquals("device-01", context.getDeviceId()); - assertEquals(11L, context.getUserId()); - assertEquals(22L, context.getTenantId()); - assertEquals("alice", context.getUsername()); - assertEquals("alice", context.getDisplayName()); - assertTrue(context.getTenantAdmin()); - assertEquals(Set.of("meeting:create"), context.getPermissions()); - assertEquals("access-token", context.getAccessToken()); - } - - @Test - void authenticateHttpShouldAllowAnonymousWhenConfigured() { - AndroidGrpcAuthProperties properties = new AndroidGrpcAuthProperties(); - properties.setAllowAnonymous(true); - TokenValidationService tokenValidationService = mock(TokenValidationService.class); - AndroidAuthServiceImpl service = new AndroidAuthServiceImpl(properties, tokenValidationService); - HttpServletRequest request = mock(HttpServletRequest.class); - - when(request.getHeader("Authorization")).thenReturn(null); - when(request.getHeader("X-Android-Device-Id")).thenReturn("device-anon"); - when(request.getHeader("X-Android-App-Id")).thenReturn("imeeting"); - when(request.getHeader("X-Android-App-Version")).thenReturn("1.0.0"); - when(request.getHeader("X-Android-Platform")).thenReturn("android"); - - AndroidAuthContext context = service.authenticateHttp(request); - - assertTrue(context.isAnonymous()); - assertEquals("NONE", context.getAuthMode()); - assertEquals("device-anon", context.getDeviceId()); - assertNull(context.getUserId()); - assertNull(context.getTenantId()); - assertNull(context.getAccessToken()); - } -} diff --git a/frontend/src/pages/system/logs/index.tsx b/frontend/src/pages/system/logs/index.tsx index 1577aef..eb44327 100644 --- a/frontend/src/pages/system/logs/index.tsx +++ b/frontend/src/pages/system/logs/index.tsx @@ -327,7 +327,7 @@ export default function Logs() { {selectedLog && ( {isPlatformAdmin && {selectedLog.tenantName || t("logsExt.platform")}} - {selectedLog.logType === "OPERATION" && {selectedLog.moduleName || t("logsExt.uncategorized")}} + {selectedLog.logType === "OPERATION" && {selectedLog.moduleName || t("logsExt.uncategorized")}} {selectedLog.logType === "OPERATION" && {selectedLog.actionName || selectedLog.operation}} {selectedLog.operation} {selectedLog.method || "N/A"}