From 92a12c4c8144357d58a23687022def4f42900eeb Mon Sep 17 00:00:00 2001 From: chenhao Date: Thu, 28 May 2026 16:18:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=8E=A8=E9=80=81=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=9B=B8=E5=85=B3=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 `AndroidMeetingPushService` 用于推送会议完成通知 - 在 `MeetingCommandServiceImpl` 和 `AiTaskServiceImpl` 中添加 `pushMeetingCompletedAfterCommitIfNeeded` 方法,确保事务提交后触发推送 - 更新 `MeetingInternalWorkflowController` 以支持手动触发会议完成推送和查询 gRPC 连接详情 - 新增 `MeetingPushTypeEnum` 枚举类,定义推送类型 - 优化 `AndroidGatewayPushService` 接口,添加用户级别的推送方法和连接快照功能 - 更新 `AndroidPushGrpcService` 和 `AndroidGatewayPushServiceImpl` 以支持新的注册参数和推送逻辑 --- backend/pom.xml | 7 ++ .../MeetingInternalWorkflowController.java | 30 ++++++ .../imeeting/enums/MeetingPushTypeEnum.java | 19 ++++ .../grpc/push/AndroidPushGrpcService.java | 8 +- .../android/AndroidGatewayPushService.java | 11 ++- .../android/AndroidMeetingPushService.java | 10 ++ .../impl/AndroidGatewayPushServiceImpl.java | 97 +++++++++++++++++-- .../impl/AndroidMeetingPushServiceImpl.java | 78 +++++++++++++++ .../service/biz/impl/AiTaskServiceImpl.java | 32 +++++- .../biz/impl/MeetingCommandServiceImpl.java | 30 +++++- 10 files changed, 308 insertions(+), 14 deletions(-) create mode 100644 backend/src/main/java/com/imeeting/enums/MeetingPushTypeEnum.java create mode 100644 backend/src/main/java/com/imeeting/service/android/AndroidMeetingPushService.java create mode 100644 backend/src/main/java/com/imeeting/service/android/impl/AndroidMeetingPushServiceImpl.java diff --git a/backend/pom.xml b/backend/pom.xml index daa948e..d592004 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -87,6 +87,13 @@ easy-captcha ${easycaptcha.version} + + + cn.hutool + hutool-all + 5.8.38 + + com.belerweb pinyin4j diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingInternalWorkflowController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingInternalWorkflowController.java index 8bf2dde..76f1eef 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingInternalWorkflowController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingInternalWorkflowController.java @@ -1,5 +1,6 @@ package com.imeeting.controller.biz; +import com.imeeting.dto.android.AndroidGrpcConnectionSnapshotVO; import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO; import com.imeeting.dto.biz.MeetingExternalWorkflowFailureDTO; import com.imeeting.dto.biz.MeetingSummaryPromptContextRequestDTO; @@ -7,6 +8,8 @@ import com.imeeting.dto.biz.MeetingSummaryPromptContextVO; import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO; import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO; import com.imeeting.dto.biz.MeetingTranscriptSourceVO; +import com.imeeting.service.android.AndroidGatewayPushService; +import com.imeeting.service.android.AndroidMeetingPushService; import com.imeeting.service.biz.MeetingCommandService; import com.imeeting.service.biz.MeetingQueryService; import com.unisbase.common.ApiResponse; @@ -30,6 +33,8 @@ public class MeetingInternalWorkflowController { private final MeetingCommandService meetingCommandService; private final MeetingQueryService meetingQueryService; + private final AndroidGatewayPushService androidGatewayPushService; + private final AndroidMeetingPushService androidMeetingPushService; private final UnisBaseProperties unisBaseProperties; @Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}") @@ -37,9 +42,13 @@ public class MeetingInternalWorkflowController { public MeetingInternalWorkflowController(MeetingCommandService meetingCommandService, MeetingQueryService meetingQueryService, + AndroidGatewayPushService androidGatewayPushService, + AndroidMeetingPushService androidMeetingPushService, UnisBaseProperties unisBaseProperties) { this.meetingCommandService = meetingCommandService; this.meetingQueryService = meetingQueryService; + this.androidGatewayPushService = androidGatewayPushService; + this.androidMeetingPushService = androidMeetingPushService; this.unisBaseProperties = unisBaseProperties; } @@ -119,6 +128,27 @@ public class MeetingInternalWorkflowController { return ApiResponse.ok(true); } + @Operation(summary = "手工触发会议完成推送") + @PostMapping("/{meetingId}/push/meeting-completed") + public ApiResponse pushMeetingCompleted(HttpServletRequest request, + @PathVariable Long meetingId) { + if (!isInternalSecretValid(request)) { + return ApiResponse.error("Invalid internal secret"); + } + + androidMeetingPushService.pushMeetingCompleted(meetingId); + return ApiResponse.ok(true); + } + + @Operation(summary = "查询 Android gRPC 连接详情") + @GetMapping("/grpc/connections") + public ApiResponse listGrpcConnections(HttpServletRequest request) { + if (!isInternalSecretValid(request)) { + return ApiResponse.error("Invalid internal secret"); + } + return ApiResponse.ok(androidGatewayPushService.snapshotConnections()); + } + private boolean isExternalModeEnabled() { return "EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode); } diff --git a/backend/src/main/java/com/imeeting/enums/MeetingPushTypeEnum.java b/backend/src/main/java/com/imeeting/enums/MeetingPushTypeEnum.java new file mode 100644 index 0000000..6438775 --- /dev/null +++ b/backend/src/main/java/com/imeeting/enums/MeetingPushTypeEnum.java @@ -0,0 +1,19 @@ +package com.imeeting.enums; + +import lombok.Getter; + +@Getter +public enum MeetingPushTypeEnum { + MEETING_COMPLETED("MEETING_COMPLETED","会议完成通知"),; + private final String code; + private final String desc; + MeetingPushTypeEnum(String code,String desc){ + this.code=code; + this.desc=desc; + }; + + + + + +} 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 3680594..4812eb0 100644 --- a/backend/src/main/java/com/imeeting/grpc/push/AndroidPushGrpcService.java +++ b/backend/src/main/java/com/imeeting/grpc/push/AndroidPushGrpcService.java @@ -123,7 +123,13 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase platform = authContext.getPlatform(); deviceOnlineManagementService.recordConnected(authContext); connected = true; - String replacedConnectionId = androidGatewayPushService.register(connectionId, deviceId, responseObserver); + String replacedConnectionId = androidGatewayPushService.register( + connectionId, + deviceId, + authContext.getTenantId(), + authContext.getUserId(), + responseObserver + ); if (replacedConnectionId != null && !replacedConnectionId.equals(connectionId)) { log.info(buildLog("gRPC连接替换", "同设备旧连接被新连接替换,旧连接ID=" + replacedConnectionId + ",新连接ID=" + connectionId, diff --git a/backend/src/main/java/com/imeeting/service/android/AndroidGatewayPushService.java b/backend/src/main/java/com/imeeting/service/android/AndroidGatewayPushService.java index c4cdb6c..464d2bf 100644 --- a/backend/src/main/java/com/imeeting/service/android/AndroidGatewayPushService.java +++ b/backend/src/main/java/com/imeeting/service/android/AndroidGatewayPushService.java @@ -1,11 +1,16 @@ package com.imeeting.service.android; +import com.imeeting.dto.android.AndroidGrpcConnectionSnapshotVO; import com.imeeting.grpc.push.PushMessage; import com.imeeting.grpc.push.ServerMessage; import io.grpc.stub.StreamObserver; public interface AndroidGatewayPushService { - String register(String connectionId, String deviceId, StreamObserver observer); + String register(String connectionId, + String deviceId, + Long tenantId, + Long userId, + StreamObserver observer); void unregister(String connectionId); @@ -13,5 +18,9 @@ public interface AndroidGatewayPushService { int pushToDevice(String deviceId, PushMessage message); + int pushToUser(Long tenantId, Long userId, PushMessage message); + String disconnectDevice(String deviceId); + + AndroidGrpcConnectionSnapshotVO snapshotConnections(); } diff --git a/backend/src/main/java/com/imeeting/service/android/AndroidMeetingPushService.java b/backend/src/main/java/com/imeeting/service/android/AndroidMeetingPushService.java new file mode 100644 index 0000000..35d1321 --- /dev/null +++ b/backend/src/main/java/com/imeeting/service/android/AndroidMeetingPushService.java @@ -0,0 +1,10 @@ +package com.imeeting.service.android; + +public interface AndroidMeetingPushService { + + void pushMeetingCompleted(Long meetingId); + + + + +} diff --git a/backend/src/main/java/com/imeeting/service/android/impl/AndroidGatewayPushServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/impl/AndroidGatewayPushServiceImpl.java index 8bfee0a..b3f45f8 100644 --- a/backend/src/main/java/com/imeeting/service/android/impl/AndroidGatewayPushServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/impl/AndroidGatewayPushServiceImpl.java @@ -1,5 +1,7 @@ package com.imeeting.service.android.impl; +import com.imeeting.dto.android.AndroidGrpcConnectionDetailVO; +import com.imeeting.dto.android.AndroidGrpcConnectionSnapshotVO; import com.imeeting.grpc.push.PushMessage; import com.imeeting.grpc.push.ServerMessage; import com.imeeting.service.android.AndroidGatewayPushService; @@ -7,6 +9,8 @@ import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -16,19 +20,27 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService private final Map byConnectionId = new ConcurrentHashMap<>(); private final Map connectionByDeviceId = new ConcurrentHashMap<>(); + private final Map> connectionsByUserKey = new ConcurrentHashMap<>(); @Override - public String register(String connectionId, String deviceId, StreamObserver observer) { - Binding newBinding = new Binding(deviceId, observer); + public String register(String connectionId, + String deviceId, + Long tenantId, + Long userId, + StreamObserver observer) { + Binding newBinding = new Binding(deviceId, tenantId, userId, observer); Binding previousBinding = byConnectionId.put(connectionId, newBinding); - if (previousBinding != null && !previousBinding.deviceId().equals(deviceId)) { - connectionByDeviceId.remove(previousBinding.deviceId(), connectionId); + if (previousBinding != null) { + removeDeviceIndex(connectionId, previousBinding); + removeUserIndex(connectionId, previousBinding); } String previousConnectionId = connectionByDeviceId.put(deviceId, connectionId); + addUserIndex(connectionId, newBinding); if (previousConnectionId != null && !previousConnectionId.equals(connectionId)) { Binding replacedBinding = byConnectionId.remove(previousConnectionId); if (replacedBinding != null) { + removeUserIndex(previousConnectionId, replacedBinding); safeComplete(previousConnectionId, replacedBinding); } } @@ -41,7 +53,8 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService if (binding == null) { return; } - connectionByDeviceId.remove(binding.deviceId(), connectionId); + removeDeviceIndex(connectionId, binding); + removeUserIndex(connectionId, binding); } @Override @@ -71,6 +84,25 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService return pushToConnection(connectionId, message) ? 1 : 0; } + @Override + public int pushToUser(Long tenantId, Long userId, PushMessage message) { + String userKey = buildUserKey(tenantId, userId); + if (userKey == null) { + return 0; + } + Map bindings = connectionsByUserKey.get(userKey); + if (bindings == null || bindings.isEmpty()) { + return 0; + } + int successCount = 0; + for (String connectionId : bindings.keySet()) { + if (pushToConnection(connectionId, message)) { + successCount++; + } + } + return successCount; + } + @Override public String disconnectDevice(String deviceId) { String connectionId = connectionByDeviceId.get(deviceId); @@ -87,6 +119,18 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService return connectionId; } + @Override + public AndroidGrpcConnectionSnapshotVO snapshotConnections() { + List connections = byConnectionId.entrySet().stream() + .map(entry -> toDetail(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(AndroidGrpcConnectionDetailVO::getConnectionId, Comparator.nullsLast(String::compareTo))) + .toList(); + AndroidGrpcConnectionSnapshotVO snapshot = new AndroidGrpcConnectionSnapshotVO(); + snapshot.setConnectionCount(connections.size()); + snapshot.setConnections(connections); + return snapshot; + } + private void safeComplete(String connectionId, Binding binding) { synchronized (binding) { try { @@ -97,6 +141,47 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService } } - private record Binding(String deviceId, StreamObserver observer) { + private void addUserIndex(String connectionId, Binding binding) { + String userKey = buildUserKey(binding.tenantId(), binding.userId()); + if (userKey == null) { + return; + } + connectionsByUserKey + .computeIfAbsent(userKey, ignored -> new ConcurrentHashMap<>()) + .put(connectionId, binding); + } + + private void removeDeviceIndex(String connectionId, Binding binding) { + connectionByDeviceId.remove(binding.deviceId(), connectionId); + } + + private void removeUserIndex(String connectionId, Binding binding) { + String userKey = buildUserKey(binding.tenantId(), binding.userId()); + if (userKey == null) { + return; + } + connectionsByUserKey.computeIfPresent(userKey, (ignored, bindings) -> { + bindings.remove(connectionId); + return bindings.isEmpty() ? null : bindings; + }); + } + + private String buildUserKey(Long tenantId, Long userId) { + if (tenantId == null || userId == null) { + return null; + } + return tenantId + ":" + userId; + } + + private AndroidGrpcConnectionDetailVO toDetail(String connectionId, Binding binding) { + AndroidGrpcConnectionDetailVO detail = new AndroidGrpcConnectionDetailVO(); + detail.setConnectionId(connectionId); + detail.setDeviceId(binding.deviceId()); + detail.setTenantId(binding.tenantId()); + detail.setUserId(binding.userId()); + return detail; + } + + private record Binding(String deviceId, Long tenantId, Long userId, StreamObserver observer) { } } diff --git a/backend/src/main/java/com/imeeting/service/android/impl/AndroidMeetingPushServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/impl/AndroidMeetingPushServiceImpl.java new file mode 100644 index 0000000..557e503 --- /dev/null +++ b/backend/src/main/java/com/imeeting/service/android/impl/AndroidMeetingPushServiceImpl.java @@ -0,0 +1,78 @@ +package com.imeeting.service.android.impl; + +import cn.hutool.json.JSON; +import cn.hutool.json.JSONUtil; +import com.imeeting.dto.biz.MeetingVO; +import com.imeeting.entity.biz.Meeting; +import com.imeeting.enums.MeetingPushTypeEnum; +import com.imeeting.grpc.push.PushMessage; + +import com.imeeting.service.android.AndroidGatewayPushService; +import com.imeeting.service.android.AndroidMeetingPushService; +import com.imeeting.service.biz.MeetingQueryService; +import com.imeeting.service.biz.MeetingService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Slf4j +@Service + +public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService { + + private static final DateTimeFormatter TITLE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + @Autowired + @Lazy + private MeetingQueryService meetingService; + @Autowired + private AndroidGatewayPushService androidGatewayPushService; + + @Override + public void pushMeetingCompleted(Long meetingId) { + if (meetingId == null) { + return; + } + MeetingVO meeting = meetingService.getDetailIgnoreTenant(meetingId); + if (meeting == null || meeting.getTenantId() == null || meeting.getCreatorId() == null) { + return; + } + PushMessage message = PushMessage.newBuilder() + .setMessageId("meeting_completed:" + meetingId + ":" + UUID.randomUUID()) + .setTimestamp(System.currentTimeMillis()) + .setType(MeetingPushTypeEnum.MEETING_COMPLETED.getCode()) + .setTitle(resolveTitle(meeting)) + .setContent(resolveContent(meeting)) + .setNeedAck(false) + .build(); + int pushed = androidGatewayPushService.pushToUser(meeting.getTenantId(), meeting.getCreatorId(), message); + log.info("Android meeting completion push finished, meetingId={}, tenantId={}, creatorId={}, pushedConnections={}", + meetingId, meeting.getTenantId(), meeting.getCreatorId(), pushed); + } + + + private String resolveTitle(MeetingVO meeting) { + String title = meeting.getTitle(); + if (title != null && !title.isBlank()) { + return "会议已完成: " + title.trim(); + } + LocalDateTime meetingTime = meeting.getMeetingTime(); + return meetingTime == null + ? "会议已完成" + : "会议已完成: " + TITLE_TIME_FORMATTER.format(meetingTime); + } + + private String resolveContent(MeetingVO meeting) { + Map result=new HashMap<>(); + result.put("meetingId",meeting.getId()); + return JSONUtil.toJsonStr(result); + } +} diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index 5f88d56..07c33ae 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -18,6 +18,7 @@ import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.mapper.biz.AiTaskMapper; import com.imeeting.mapper.biz.MeetingMapper; import com.imeeting.mapper.biz.MeetingTranscriptMapper; +import com.imeeting.service.android.AndroidMeetingPushService; import com.imeeting.support.TaskSecurityContextRunner; import com.imeeting.service.biz.AiModelService; import com.imeeting.service.biz.AiTaskService; @@ -41,6 +42,8 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.net.URI; import java.net.URLEncoder; @@ -78,6 +81,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme private final TaskSecurityContextRunner taskSecurityContextRunner; private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger; private final SysParamService sysParamService; + private final AndroidMeetingPushService androidMeetingPushService; @Autowired @Qualifier("asrTaskExecutor") @@ -120,7 +124,8 @@ public class AiTaskServiceImpl extends ServiceImpl impleme MeetingSummaryPromptAssembler meetingSummaryPromptAssembler, TaskSecurityContextRunner taskSecurityContextRunner, MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger, - SysParamService sysParamService) { + SysParamService sysParamService, + AndroidMeetingPushService androidMeetingPushService) { this.meetingMapper = meetingMapper; this.transcriptMapper = transcriptMapper; this.aiModelService = aiModelService; @@ -136,6 +141,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme this.taskSecurityContextRunner = taskSecurityContextRunner; this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger; this.sysParamService = sysParamService; + this.androidMeetingPushService = androidMeetingPushService; } public AiTaskServiceImpl(MeetingMapper meetingMapper, @@ -151,7 +157,8 @@ public class AiTaskServiceImpl extends ServiceImpl impleme MeetingTranscriptChapterService meetingTranscriptChapterService, MeetingSummaryPromptAssembler meetingSummaryPromptAssembler, TaskSecurityContextRunner taskSecurityContextRunner, - MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger) { + MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger, + AndroidMeetingPushService androidMeetingPushService) { this( meetingMapper, transcriptMapper, @@ -167,7 +174,8 @@ public class AiTaskServiceImpl extends ServiceImpl impleme meetingSummaryPromptAssembler, taskSecurityContextRunner, meetingExternalSummaryWebhookTrigger, - null + null, + androidMeetingPushService ); } @@ -972,6 +980,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme Files.writeString(filePath, markdownContent, StandardCharsets.UTF_8); + boolean alreadyCompleted = Integer.valueOf(3).equals(meeting.getStatus()); taskRecord.setResultFilePath("meetings/" + meeting.getId() + "/summaries/" + fileName); Map responseData = objectMapper.convertValue(respNode, Map.class); responseData.put("summarySource", summarySource.toSnapshot()); @@ -992,6 +1001,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme AiTask latestChapterTask = findLatestTask(meeting.getId(), "CHAPTER"); if (latestChapterTask != null && Integer.valueOf(2).equals(latestChapterTask.getStatus())) { updateProgress(meeting.getId(), 100, "全流程分析完成", 0); + pushMeetingCompletedAfterCommitIfNeeded(meeting.getId(), alreadyCompleted); } else { updateProgress(meeting.getId(), 95, "总结生成完成,等待 AI 目录完成...", 0); } @@ -1335,6 +1345,22 @@ public class AiTaskServiceImpl extends ServiceImpl impleme meetingMapper.updateById(m); } + private void pushMeetingCompletedAfterCommitIfNeeded(Long meetingId, boolean alreadyCompleted) { + if (alreadyCompleted) { + return; + } + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + androidMeetingPushService.pushMeetingCompleted(meetingId); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + androidMeetingPushService.pushMeetingCompleted(meetingId); + } + }); + } + private AiTask createAiTask(Long meetingId, String type, Map req) { AiTask task = new AiTask(); task.setMeetingId(meetingId); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java index de26b01..9d4c764 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java @@ -25,6 +25,7 @@ import com.imeeting.entity.biz.HotWord; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.entity.biz.MeetingTranscriptChapterVersion; +import com.imeeting.service.android.AndroidMeetingPushService; import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.HotWordService; import com.imeeting.service.biz.MeetingCommandService; @@ -75,6 +76,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { private final MeetingProgressService meetingProgressService; private final ObjectMapper objectMapper; private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger; + private final AndroidMeetingPushService androidMeetingPushService; private StringRedisTemplate compatibilityRedisTemplate; @Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}") @@ -95,7 +97,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService, MeetingProgressService meetingProgressService, ObjectMapper objectMapper, - MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger) { + MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger, + AndroidMeetingPushService androidMeetingPushService) { this.meetingService = meetingService; this.aiTaskService = aiTaskService; this.hotWordService = hotWordService; @@ -111,6 +114,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { this.meetingProgressService = meetingProgressService; this.objectMapper = objectMapper; this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger; + this.androidMeetingPushService = androidMeetingPushService; } public MeetingCommandServiceImpl(MeetingService meetingService, @@ -127,7 +131,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService, StringRedisTemplate redisTemplate, ObjectMapper objectMapper, - MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger) { + MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger, + AndroidMeetingPushService androidMeetingPushService) { this( meetingService, aiTaskService, @@ -143,7 +148,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { realtimeMeetingAudioStorageService, new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper), objectMapper, - meetingExternalSummaryWebhookTrigger + meetingExternalSummaryWebhookTrigger, + androidMeetingPushService ); this.compatibilityRedisTemplate = redisTemplate; } @@ -790,6 +796,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { summaryTask.setCompletedAt(java.time.LocalDateTime.now()); aiTaskService.updateById(summaryTask); + boolean alreadyCompleted = Integer.valueOf(3).equals(meeting.getStatus()); meeting.setLatestSummaryTaskId(summaryTask.getId()); meetingService.updateById(meeting); aiTaskService.reconcileMeetingStatus(meeting.getId()); @@ -801,6 +808,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { .last("LIMIT 1")); if (latestChapterTask != null && Integer.valueOf(2).equals(latestChapterTask.getStatus())) { updateMeetingProgress(meeting.getId(), 100, "外部总结回填完成", 0); + pushMeetingCompletedAfterCommitIfNeeded(meeting.getId(), alreadyCompleted); } else { updateMeetingProgress(meeting.getId(), 95, "外部总结回填完成,等待 AI 目录完成...", 0); } @@ -1219,6 +1227,22 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { }); } + private void pushMeetingCompletedAfterCommitIfNeeded(Long meetingId, boolean alreadyCompleted) { + if (alreadyCompleted) { + return; + } + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + androidMeetingPushService.pushMeetingCompleted(meetingId); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + androidMeetingPushService.pushMeetingCompleted(meetingId); + } + }); + } + private void updateMeetingProgress(Long meetingId, int percent, String message, int eta) { com.imeeting.common.MeetingProgressStage stage; int meetingStatus;