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;