feat: 添加会议完成推送功能并优化相关服务

- 引入 `AndroidMeetingPushService` 用于推送会议完成通知
- 在 `MeetingCommandServiceImpl` 和 `AiTaskServiceImpl` 中添加 `pushMeetingCompletedAfterCommitIfNeeded` 方法,确保事务提交后触发推送
- 更新 `MeetingInternalWorkflowController` 以支持手动触发会议完成推送和查询 gRPC 连接详情
- 新增 `MeetingPushTypeEnum` 枚举类,定义推送类型
- 优化 `AndroidGatewayPushService` 接口,添加用户级别的推送方法和连接快照功能
- 更新 `AndroidPushGrpcService` 和 `AndroidGatewayPushServiceImpl` 以支持新的注册参数和推送逻辑
dev_na
chenhao 2026-05-28 16:18:41 +08:00
parent 7f9c080bf7
commit 92a12c4c81
10 changed files with 308 additions and 14 deletions

View File

@ -87,6 +87,13 @@
<artifactId>easy-captcha</artifactId> <artifactId>easy-captcha</artifactId>
<version>${easycaptcha.version}</version> <version>${easycaptcha.version}</version>
</dependency> </dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
<dependency> <dependency>
<groupId>com.belerweb</groupId> <groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId> <artifactId>pinyin4j</artifactId>

View File

@ -1,5 +1,6 @@
package com.imeeting.controller.biz; package com.imeeting.controller.biz;
import com.imeeting.dto.android.AndroidGrpcConnectionSnapshotVO;
import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO; import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO;
import com.imeeting.dto.biz.MeetingExternalWorkflowFailureDTO; import com.imeeting.dto.biz.MeetingExternalWorkflowFailureDTO;
import com.imeeting.dto.biz.MeetingSummaryPromptContextRequestDTO; 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.MeetingTranscriptChapterImportDTO;
import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO; import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO;
import com.imeeting.dto.biz.MeetingTranscriptSourceVO; 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.MeetingCommandService;
import com.imeeting.service.biz.MeetingQueryService; import com.imeeting.service.biz.MeetingQueryService;
import com.unisbase.common.ApiResponse; import com.unisbase.common.ApiResponse;
@ -30,6 +33,8 @@ public class MeetingInternalWorkflowController {
private final MeetingCommandService meetingCommandService; private final MeetingCommandService meetingCommandService;
private final MeetingQueryService meetingQueryService; private final MeetingQueryService meetingQueryService;
private final AndroidGatewayPushService androidGatewayPushService;
private final AndroidMeetingPushService androidMeetingPushService;
private final UnisBaseProperties unisBaseProperties; private final UnisBaseProperties unisBaseProperties;
@Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}") @Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}")
@ -37,9 +42,13 @@ public class MeetingInternalWorkflowController {
public MeetingInternalWorkflowController(MeetingCommandService meetingCommandService, public MeetingInternalWorkflowController(MeetingCommandService meetingCommandService,
MeetingQueryService meetingQueryService, MeetingQueryService meetingQueryService,
AndroidGatewayPushService androidGatewayPushService,
AndroidMeetingPushService androidMeetingPushService,
UnisBaseProperties unisBaseProperties) { UnisBaseProperties unisBaseProperties) {
this.meetingCommandService = meetingCommandService; this.meetingCommandService = meetingCommandService;
this.meetingQueryService = meetingQueryService; this.meetingQueryService = meetingQueryService;
this.androidGatewayPushService = androidGatewayPushService;
this.androidMeetingPushService = androidMeetingPushService;
this.unisBaseProperties = unisBaseProperties; this.unisBaseProperties = unisBaseProperties;
} }
@ -119,6 +128,27 @@ public class MeetingInternalWorkflowController {
return ApiResponse.ok(true); return ApiResponse.ok(true);
} }
@Operation(summary = "手工触发会议完成推送")
@PostMapping("/{meetingId}/push/meeting-completed")
public ApiResponse<Boolean> 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<AndroidGrpcConnectionSnapshotVO> listGrpcConnections(HttpServletRequest request) {
if (!isInternalSecretValid(request)) {
return ApiResponse.error("Invalid internal secret");
}
return ApiResponse.ok(androidGatewayPushService.snapshotConnections());
}
private boolean isExternalModeEnabled() { private boolean isExternalModeEnabled() {
return "EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode); return "EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode);
} }

View File

@ -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;
};
}

View File

@ -123,7 +123,13 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
platform = authContext.getPlatform(); platform = authContext.getPlatform();
deviceOnlineManagementService.recordConnected(authContext); deviceOnlineManagementService.recordConnected(authContext);
connected = true; 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)) { if (replacedConnectionId != null && !replacedConnectionId.equals(connectionId)) {
log.info(buildLog("gRPC连接替换", log.info(buildLog("gRPC连接替换",
"同设备旧连接被新连接替换旧连接ID=" + replacedConnectionId + "新连接ID=" + connectionId, "同设备旧连接被新连接替换旧连接ID=" + replacedConnectionId + "新连接ID=" + connectionId,

View File

@ -1,11 +1,16 @@
package com.imeeting.service.android; package com.imeeting.service.android;
import com.imeeting.dto.android.AndroidGrpcConnectionSnapshotVO;
import com.imeeting.grpc.push.PushMessage; import com.imeeting.grpc.push.PushMessage;
import com.imeeting.grpc.push.ServerMessage; import com.imeeting.grpc.push.ServerMessage;
import io.grpc.stub.StreamObserver; import io.grpc.stub.StreamObserver;
public interface AndroidGatewayPushService { public interface AndroidGatewayPushService {
String register(String connectionId, String deviceId, StreamObserver<ServerMessage> observer); String register(String connectionId,
String deviceId,
Long tenantId,
Long userId,
StreamObserver<ServerMessage> observer);
void unregister(String connectionId); void unregister(String connectionId);
@ -13,5 +18,9 @@ public interface AndroidGatewayPushService {
int pushToDevice(String deviceId, PushMessage message); int pushToDevice(String deviceId, PushMessage message);
int pushToUser(Long tenantId, Long userId, PushMessage message);
String disconnectDevice(String deviceId); String disconnectDevice(String deviceId);
AndroidGrpcConnectionSnapshotVO snapshotConnections();
} }

View File

@ -0,0 +1,10 @@
package com.imeeting.service.android;
public interface AndroidMeetingPushService {
void pushMeetingCompleted(Long meetingId);
}

View File

@ -1,5 +1,7 @@
package com.imeeting.service.android.impl; 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.PushMessage;
import com.imeeting.grpc.push.ServerMessage; import com.imeeting.grpc.push.ServerMessage;
import com.imeeting.service.android.AndroidGatewayPushService; import com.imeeting.service.android.AndroidGatewayPushService;
@ -7,6 +9,8 @@ import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -16,19 +20,27 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService
private final Map<String, Binding> byConnectionId = new ConcurrentHashMap<>(); private final Map<String, Binding> byConnectionId = new ConcurrentHashMap<>();
private final Map<String, String> connectionByDeviceId = new ConcurrentHashMap<>(); private final Map<String, String> connectionByDeviceId = new ConcurrentHashMap<>();
private final Map<String, Map<String, Binding>> connectionsByUserKey = new ConcurrentHashMap<>();
@Override @Override
public String register(String connectionId, String deviceId, StreamObserver<ServerMessage> observer) { public String register(String connectionId,
Binding newBinding = new Binding(deviceId, observer); String deviceId,
Long tenantId,
Long userId,
StreamObserver<ServerMessage> observer) {
Binding newBinding = new Binding(deviceId, tenantId, userId, observer);
Binding previousBinding = byConnectionId.put(connectionId, newBinding); Binding previousBinding = byConnectionId.put(connectionId, newBinding);
if (previousBinding != null && !previousBinding.deviceId().equals(deviceId)) { if (previousBinding != null) {
connectionByDeviceId.remove(previousBinding.deviceId(), connectionId); removeDeviceIndex(connectionId, previousBinding);
removeUserIndex(connectionId, previousBinding);
} }
String previousConnectionId = connectionByDeviceId.put(deviceId, connectionId); String previousConnectionId = connectionByDeviceId.put(deviceId, connectionId);
addUserIndex(connectionId, newBinding);
if (previousConnectionId != null && !previousConnectionId.equals(connectionId)) { if (previousConnectionId != null && !previousConnectionId.equals(connectionId)) {
Binding replacedBinding = byConnectionId.remove(previousConnectionId); Binding replacedBinding = byConnectionId.remove(previousConnectionId);
if (replacedBinding != null) { if (replacedBinding != null) {
removeUserIndex(previousConnectionId, replacedBinding);
safeComplete(previousConnectionId, replacedBinding); safeComplete(previousConnectionId, replacedBinding);
} }
} }
@ -41,7 +53,8 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService
if (binding == null) { if (binding == null) {
return; return;
} }
connectionByDeviceId.remove(binding.deviceId(), connectionId); removeDeviceIndex(connectionId, binding);
removeUserIndex(connectionId, binding);
} }
@Override @Override
@ -71,6 +84,25 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService
return pushToConnection(connectionId, message) ? 1 : 0; 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<String, Binding> 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 @Override
public String disconnectDevice(String deviceId) { public String disconnectDevice(String deviceId) {
String connectionId = connectionByDeviceId.get(deviceId); String connectionId = connectionByDeviceId.get(deviceId);
@ -87,6 +119,18 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService
return connectionId; return connectionId;
} }
@Override
public AndroidGrpcConnectionSnapshotVO snapshotConnections() {
List<AndroidGrpcConnectionDetailVO> 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) { private void safeComplete(String connectionId, Binding binding) {
synchronized (binding) { synchronized (binding) {
try { try {
@ -97,6 +141,47 @@ public class AndroidGatewayPushServiceImpl implements AndroidGatewayPushService
} }
} }
private record Binding(String deviceId, StreamObserver<ServerMessage> 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<ServerMessage> observer) {
} }
} }

View File

@ -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<String,Object> result=new HashMap<>();
result.put("meetingId",meeting.getId());
return JSONUtil.toJsonStr(result);
}
}

View File

@ -18,6 +18,7 @@ import com.imeeting.entity.biz.MeetingTranscript;
import com.imeeting.mapper.biz.AiTaskMapper; import com.imeeting.mapper.biz.AiTaskMapper;
import com.imeeting.mapper.biz.MeetingMapper; import com.imeeting.mapper.biz.MeetingMapper;
import com.imeeting.mapper.biz.MeetingTranscriptMapper; import com.imeeting.mapper.biz.MeetingTranscriptMapper;
import com.imeeting.service.android.AndroidMeetingPushService;
import com.imeeting.support.TaskSecurityContextRunner; import com.imeeting.support.TaskSecurityContextRunner;
import com.imeeting.service.biz.AiModelService; import com.imeeting.service.biz.AiModelService;
import com.imeeting.service.biz.AiTaskService; 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.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; 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.URI;
import java.net.URLEncoder; import java.net.URLEncoder;
@ -78,6 +81,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
private final TaskSecurityContextRunner taskSecurityContextRunner; private final TaskSecurityContextRunner taskSecurityContextRunner;
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger; private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
private final SysParamService sysParamService; private final SysParamService sysParamService;
private final AndroidMeetingPushService androidMeetingPushService;
@Autowired @Autowired
@Qualifier("asrTaskExecutor") @Qualifier("asrTaskExecutor")
@ -120,7 +124,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
MeetingSummaryPromptAssembler meetingSummaryPromptAssembler, MeetingSummaryPromptAssembler meetingSummaryPromptAssembler,
TaskSecurityContextRunner taskSecurityContextRunner, TaskSecurityContextRunner taskSecurityContextRunner,
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger, MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
SysParamService sysParamService) { SysParamService sysParamService,
AndroidMeetingPushService androidMeetingPushService) {
this.meetingMapper = meetingMapper; this.meetingMapper = meetingMapper;
this.transcriptMapper = transcriptMapper; this.transcriptMapper = transcriptMapper;
this.aiModelService = aiModelService; this.aiModelService = aiModelService;
@ -136,6 +141,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
this.taskSecurityContextRunner = taskSecurityContextRunner; this.taskSecurityContextRunner = taskSecurityContextRunner;
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger; this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
this.sysParamService = sysParamService; this.sysParamService = sysParamService;
this.androidMeetingPushService = androidMeetingPushService;
} }
public AiTaskServiceImpl(MeetingMapper meetingMapper, public AiTaskServiceImpl(MeetingMapper meetingMapper,
@ -151,7 +157,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
MeetingTranscriptChapterService meetingTranscriptChapterService, MeetingTranscriptChapterService meetingTranscriptChapterService,
MeetingSummaryPromptAssembler meetingSummaryPromptAssembler, MeetingSummaryPromptAssembler meetingSummaryPromptAssembler,
TaskSecurityContextRunner taskSecurityContextRunner, TaskSecurityContextRunner taskSecurityContextRunner,
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger) { MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
AndroidMeetingPushService androidMeetingPushService) {
this( this(
meetingMapper, meetingMapper,
transcriptMapper, transcriptMapper,
@ -167,7 +174,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
meetingSummaryPromptAssembler, meetingSummaryPromptAssembler,
taskSecurityContextRunner, taskSecurityContextRunner,
meetingExternalSummaryWebhookTrigger, meetingExternalSummaryWebhookTrigger,
null null,
androidMeetingPushService
); );
} }
@ -972,6 +980,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
Files.writeString(filePath, markdownContent, StandardCharsets.UTF_8); Files.writeString(filePath, markdownContent, StandardCharsets.UTF_8);
boolean alreadyCompleted = Integer.valueOf(3).equals(meeting.getStatus());
taskRecord.setResultFilePath("meetings/" + meeting.getId() + "/summaries/" + fileName); taskRecord.setResultFilePath("meetings/" + meeting.getId() + "/summaries/" + fileName);
Map<String, Object> responseData = objectMapper.convertValue(respNode, Map.class); Map<String, Object> responseData = objectMapper.convertValue(respNode, Map.class);
responseData.put("summarySource", summarySource.toSnapshot()); responseData.put("summarySource", summarySource.toSnapshot());
@ -992,6 +1001,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
AiTask latestChapterTask = findLatestTask(meeting.getId(), "CHAPTER"); AiTask latestChapterTask = findLatestTask(meeting.getId(), "CHAPTER");
if (latestChapterTask != null && Integer.valueOf(2).equals(latestChapterTask.getStatus())) { if (latestChapterTask != null && Integer.valueOf(2).equals(latestChapterTask.getStatus())) {
updateProgress(meeting.getId(), 100, "全流程分析完成", 0); updateProgress(meeting.getId(), 100, "全流程分析完成", 0);
pushMeetingCompletedAfterCommitIfNeeded(meeting.getId(), alreadyCompleted);
} else { } else {
updateProgress(meeting.getId(), 95, "总结生成完成,等待 AI 目录完成...", 0); updateProgress(meeting.getId(), 95, "总结生成完成,等待 AI 目录完成...", 0);
} }
@ -1335,6 +1345,22 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
meetingMapper.updateById(m); 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<String, Object> req) { private AiTask createAiTask(Long meetingId, String type, Map<String, Object> req) {
AiTask task = new AiTask(); AiTask task = new AiTask();
task.setMeetingId(meetingId); task.setMeetingId(meetingId);

View File

@ -25,6 +25,7 @@ import com.imeeting.entity.biz.HotWord;
import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.entity.biz.MeetingTranscript;
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion; import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
import com.imeeting.service.android.AndroidMeetingPushService;
import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.AiTaskService;
import com.imeeting.service.biz.HotWordService; import com.imeeting.service.biz.HotWordService;
import com.imeeting.service.biz.MeetingCommandService; import com.imeeting.service.biz.MeetingCommandService;
@ -75,6 +76,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
private final MeetingProgressService meetingProgressService; private final MeetingProgressService meetingProgressService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger; private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
private final AndroidMeetingPushService androidMeetingPushService;
private StringRedisTemplate compatibilityRedisTemplate; private StringRedisTemplate compatibilityRedisTemplate;
@Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}") @Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}")
@ -95,7 +97,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService, RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService,
MeetingProgressService meetingProgressService, MeetingProgressService meetingProgressService,
ObjectMapper objectMapper, ObjectMapper objectMapper,
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger) { MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
AndroidMeetingPushService androidMeetingPushService) {
this.meetingService = meetingService; this.meetingService = meetingService;
this.aiTaskService = aiTaskService; this.aiTaskService = aiTaskService;
this.hotWordService = hotWordService; this.hotWordService = hotWordService;
@ -111,6 +114,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
this.meetingProgressService = meetingProgressService; this.meetingProgressService = meetingProgressService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger; this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
this.androidMeetingPushService = androidMeetingPushService;
} }
public MeetingCommandServiceImpl(MeetingService meetingService, public MeetingCommandServiceImpl(MeetingService meetingService,
@ -127,7 +131,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService, RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService,
StringRedisTemplate redisTemplate, StringRedisTemplate redisTemplate,
ObjectMapper objectMapper, ObjectMapper objectMapper,
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger) { MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
AndroidMeetingPushService androidMeetingPushService) {
this( this(
meetingService, meetingService,
aiTaskService, aiTaskService,
@ -143,7 +148,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
realtimeMeetingAudioStorageService, realtimeMeetingAudioStorageService,
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper), new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
objectMapper, objectMapper,
meetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
androidMeetingPushService
); );
this.compatibilityRedisTemplate = redisTemplate; this.compatibilityRedisTemplate = redisTemplate;
} }
@ -790,6 +796,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
summaryTask.setCompletedAt(java.time.LocalDateTime.now()); summaryTask.setCompletedAt(java.time.LocalDateTime.now());
aiTaskService.updateById(summaryTask); aiTaskService.updateById(summaryTask);
boolean alreadyCompleted = Integer.valueOf(3).equals(meeting.getStatus());
meeting.setLatestSummaryTaskId(summaryTask.getId()); meeting.setLatestSummaryTaskId(summaryTask.getId());
meetingService.updateById(meeting); meetingService.updateById(meeting);
aiTaskService.reconcileMeetingStatus(meeting.getId()); aiTaskService.reconcileMeetingStatus(meeting.getId());
@ -801,6 +808,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
.last("LIMIT 1")); .last("LIMIT 1"));
if (latestChapterTask != null && Integer.valueOf(2).equals(latestChapterTask.getStatus())) { if (latestChapterTask != null && Integer.valueOf(2).equals(latestChapterTask.getStatus())) {
updateMeetingProgress(meeting.getId(), 100, "外部总结回填完成", 0); updateMeetingProgress(meeting.getId(), 100, "外部总结回填完成", 0);
pushMeetingCompletedAfterCommitIfNeeded(meeting.getId(), alreadyCompleted);
} else { } else {
updateMeetingProgress(meeting.getId(), 95, "外部总结回填完成,等待 AI 目录完成...", 0); 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) { private void updateMeetingProgress(Long meetingId, int percent, String message, int eta) {
com.imeeting.common.MeetingProgressStage stage; com.imeeting.common.MeetingProgressStage stage;
int meetingStatus; int meetingStatus;