refactor: 移除会议完成推送逻辑并更新积分校验处理
- 移除 `AndroidMeetingPushService` 中的 `pushMeetingCompleted` 方法及相关调用 - 更新 `MeetingCommandServiceImpl` 和 `AiTaskServiceImpl`,移除对 `AndroidMeetingPushService` 的依赖 - 新增 `assertSufficientPointsBeforeAsrSubmit` 和 `assertSufficientPointsBeforeSummarySubmit` 方法到 `MeetingPointsService`,并在 `AiTaskServiceImpl` 中调用这些方法以确保在提交 ASR 和总结任务前有足够的积分 - 更新前端表单占位符和多语言文件中的文本dev_na
parent
b07bbe90a0
commit
178b920581
|
|
@ -38,7 +38,9 @@ import com.unisbase.annotation.Anonymous;
|
|||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.entity.SysTenant;
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysTenantMapper;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import com.unisbase.service.SysDictItemService;
|
||||
|
|
@ -90,6 +92,7 @@ public class AndroidMeetingController {
|
|||
private static final String STAGE_AUDIO_TRANSCRIPTION = "audio_transcription";
|
||||
private static final String STAGE_SUMMARY_GENERATION = "summary_generation";
|
||||
private static final String STAGE_COMPLETED = "completed";
|
||||
private static final String TENANT_CODE_HEADER = "X-Tenant-Code";
|
||||
|
||||
@Value("${imeeting.h5.base-url:}")
|
||||
private String h5BaseUrl;
|
||||
|
|
@ -103,6 +106,7 @@ public class AndroidMeetingController {
|
|||
private final MeetingService meetingService;
|
||||
private final AiTaskService aiTaskService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
private final SysTenantMapper sysTenantMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final AiModelService aiModelService;
|
||||
private final SysDictItemService dictItemService;
|
||||
|
|
@ -120,6 +124,7 @@ public class AndroidMeetingController {
|
|||
MeetingService meetingService,
|
||||
AiTaskService aiTaskService,
|
||||
PromptTemplateService promptTemplateService,
|
||||
SysTenantMapper sysTenantMapper,
|
||||
SysUserMapper sysUserMapper,
|
||||
AiModelService aiModelService,
|
||||
SysDictItemService dictItemService,
|
||||
|
|
@ -135,6 +140,7 @@ public class AndroidMeetingController {
|
|||
this.meetingService = meetingService;
|
||||
this.aiTaskService = aiTaskService;
|
||||
this.promptTemplateService = promptTemplateService;
|
||||
this.sysTenantMapper = sysTenantMapper;
|
||||
this.sysUserMapper = sysUserMapper;
|
||||
this.meetingProgressService = meetingProgressService;
|
||||
this.aiModelService = aiModelService;
|
||||
|
|
@ -154,9 +160,12 @@ public class AndroidMeetingController {
|
|||
@PostMapping("/create")
|
||||
@Anonymous
|
||||
@Log(value = "新增Android会议", type = "Android会议管理")
|
||||
public ApiResponse<Object> create(HttpServletRequest request, @RequestBody LegacyMeetingCreateRequest command) {
|
||||
public ApiResponse<Object> create(HttpServletRequest request,
|
||||
|
||||
@RequestBody LegacyMeetingCreateRequest command) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "创建离线会议接口", "request", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
resolvePublicDeviceTenantId(request, command, authContext);
|
||||
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
try {
|
||||
// Meeting existingMeeting = findLatestUnfinishedMeetingByDevice(authContext.getDeviceId());
|
||||
|
|
@ -468,6 +477,26 @@ public class AndroidMeetingController {
|
|||
return ApiResponse.ok(resultVo);
|
||||
}
|
||||
|
||||
private void resolvePublicDeviceTenantId(HttpServletRequest request,
|
||||
LegacyMeetingCreateRequest command,
|
||||
AndroidAuthContext authContext) {
|
||||
if (command == null || command.getTenantId() != null || authContext == null || !authContext.isAnonymous()) {
|
||||
return;
|
||||
}
|
||||
String tenantCode = request == null ? null : request.getHeader(TENANT_CODE_HEADER);
|
||||
if (!StringUtils.hasText(tenantCode)) {
|
||||
throw new IllegalArgumentException("tenantCode不能为空");
|
||||
}
|
||||
SysTenant tenant = sysTenantMapper.selectOne(new LambdaQueryWrapper<SysTenant>()
|
||||
.eq(SysTenant::getTenantCode, tenantCode.trim())
|
||||
.eq(SysTenant::getIsDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (tenant == null || tenant.getId() == null) {
|
||||
throw new IllegalArgumentException("tenantCode无效,无法获取tenantId");
|
||||
}
|
||||
command.setTenantId(tenant.getId());
|
||||
}
|
||||
|
||||
private AndroidMeetingCreateResponse buildAndroidMeetingCreateResponse(MeetingVO meeting) {
|
||||
AndroidMeetingCreateResponse response = new AndroidMeetingCreateResponse();
|
||||
if (meeting == null) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
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.MeetingSummaryFinalizeDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryPromptContextRequestDTO;
|
||||
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;
|
||||
|
|
@ -34,7 +33,6 @@ 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}")
|
||||
|
|
@ -43,12 +41,10 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -128,18 +124,6 @@ public class MeetingInternalWorkflowController {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import lombok.Getter;
|
|||
public enum MeetingPushTypeEnum {
|
||||
PUBLIC_MEETING_LOGIN_CONFIRM("PUBLIC_MEETING_LOGIN_CONFIRM", "公有设备扫码登录确认消息"),
|
||||
MEETING_PENDING("MEETING_PENDING", "待开始会议通知"),
|
||||
MEETING_COMPLETED("MEETING_COMPLETED", "会议完成通知"),
|
||||
MEETING_STATUS_CHANGED("MEETING_STATUS_CHANGED", "会议状态变更通知");
|
||||
|
||||
private final String code;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,5 @@ public interface AndroidMeetingPushService {
|
|||
|
||||
void pushPublicLoginConfirm(String deviceId, AndroidPublicLoginConfirmPayload payload);
|
||||
|
||||
void pushMeetingCompleted(Long meetingId);
|
||||
|
||||
void pushMeetingStatusChanged(Long meetingId, String statusCode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,28 +84,6 @@ public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService
|
|||
deviceId, payload.getSessionId(), pushed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pushMeetingCompleted(Long meetingId) {
|
||||
if (meetingId == null) {
|
||||
return;
|
||||
}
|
||||
Meeting meeting = meetingMapper.selectByIdIgnoreTenant(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(resolveCompletedTitle(meeting))
|
||||
.setContent(buildCompletedContent(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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pushMeetingStatusChanged(Long meetingId, String statusCode) {
|
||||
if (meetingId == null || statusCode == null || statusCode.isBlank()) {
|
||||
|
|
@ -131,23 +109,12 @@ public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService
|
|||
private String resolvePendingTitle(Meeting meeting) {
|
||||
String title = meeting.getTitle();
|
||||
if (title != null && !title.isBlank()) {
|
||||
return "待开始会议 " + title.trim();
|
||||
return "待开始会议:" + title.trim();
|
||||
}
|
||||
LocalDateTime meetingTime = meeting.getMeetingTime();
|
||||
return meetingTime == null
|
||||
? "待开始会议"
|
||||
: "待开始会议 " + TITLE_TIME_FORMATTER.format(meetingTime);
|
||||
}
|
||||
|
||||
private String resolveCompletedTitle(Meeting 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);
|
||||
: "待开始会议:" + TITLE_TIME_FORMATTER.format(meetingTime);
|
||||
}
|
||||
|
||||
private String buildPendingContent(Meeting meeting) {
|
||||
|
|
@ -161,12 +128,6 @@ public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService
|
|||
return JSONUtil.toJsonStr(result);
|
||||
}
|
||||
|
||||
private String buildCompletedContent(Meeting meeting) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("meetingId", meeting.getId());
|
||||
return JSONUtil.toJsonStr(result);
|
||||
}
|
||||
|
||||
private String buildStatusChangedContent(Long meetingId, String statusCode) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("meetingId", meetingId);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ public interface MeetingPointsService {
|
|||
|
||||
void recordSummarySuccessCharge(Meeting meeting, AiTask summaryTask);
|
||||
|
||||
void assertSufficientPointsBeforeAsrSubmit(Meeting meeting, AiTask asrTask);
|
||||
|
||||
void assertSufficientPointsBeforeSummarySubmit(Meeting meeting, AiTask summaryTask);
|
||||
|
||||
void markSummaryChargeFailed(Long summaryTaskId, String failureReason);
|
||||
|
||||
String resolveLatestBlockedReason(Long summaryTaskId);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import com.imeeting.enums.MeetingStatusEnum;
|
|||
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;
|
||||
|
|
@ -43,8 +42,6 @@ import org.springframework.scheduling.annotation.Async;
|
|||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
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;
|
||||
|
|
@ -65,6 +62,9 @@ import java.util.stream.Collectors;
|
|||
@Slf4j
|
||||
public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> implements AiTaskService {
|
||||
|
||||
private static final Duration ASR_SUBMIT_REQUEST_TIMEOUT = Duration.ofSeconds(30);
|
||||
private static final Duration ASR_QUERY_REQUEST_TIMEOUT = Duration.ofSeconds(30);
|
||||
|
||||
private final MeetingMapper meetingMapper;
|
||||
private final MeetingTranscriptMapper transcriptMapper;
|
||||
private final AiModelService aiModelService;
|
||||
|
|
@ -82,7 +82,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
||||
private final SysParamService sysParamService;
|
||||
private final AndroidMeetingPushService androidMeetingPushService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("asrTaskExecutor")
|
||||
|
|
@ -126,8 +125,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
MeetingSummaryPromptAssembler meetingSummaryPromptAssembler,
|
||||
TaskSecurityContextRunner taskSecurityContextRunner,
|
||||
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
|
||||
SysParamService sysParamService,
|
||||
AndroidMeetingPushService androidMeetingPushService) {
|
||||
SysParamService sysParamService) {
|
||||
this.meetingMapper = meetingMapper;
|
||||
this.transcriptMapper = transcriptMapper;
|
||||
this.aiModelService = aiModelService;
|
||||
|
|
@ -144,7 +142,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
this.taskSecurityContextRunner = taskSecurityContextRunner;
|
||||
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
|
||||
this.sysParamService = sysParamService;
|
||||
this.androidMeetingPushService = androidMeetingPushService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -316,6 +313,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
reconcileMeetingStatus(meetingId);
|
||||
} catch (Exception e) {
|
||||
log.error("Re-summary failed for meeting {}", meetingId, e);
|
||||
failPendingSummaryTask(sumTask, e.getMessage());
|
||||
reconcileMeetingStatus(meetingId);
|
||||
}
|
||||
}
|
||||
|
|
@ -481,6 +479,19 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
}
|
||||
|
||||
private String processAsrTask(Meeting meeting, AiTask taskRecord) throws Exception {
|
||||
try {
|
||||
return doProcessAsrTask(meeting, taskRecord);
|
||||
} catch (Exception ex) {
|
||||
if (taskRecord != null
|
||||
&& !Integer.valueOf(2).equals(taskRecord.getStatus())
|
||||
&& !Integer.valueOf(3).equals(taskRecord.getStatus())) {
|
||||
updateAiTaskFail(taskRecord, buildAsrFailureMessage(ex));
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private String doProcessAsrTask(Meeting meeting, AiTask taskRecord) throws Exception {
|
||||
updateMeetingStatus(meeting.getId(), 1);
|
||||
|
||||
taskRecord.setStatus(1);
|
||||
|
|
@ -644,6 +655,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
Map<String, Object> req = buildAsrRequest(meeting, taskRecord, asrModel);
|
||||
taskRecord.setRequestData(req);
|
||||
this.updateById(taskRecord);
|
||||
meetingPointsService.assertSufficientPointsBeforeAsrSubmit(meeting, taskRecord);
|
||||
|
||||
String respBody = postJson(submitUrl, req, asrModel.getApiKey());
|
||||
JsonNode submitNode = objectMapper.readTree(respBody);
|
||||
|
|
@ -875,6 +887,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
updateMeetingStatus(meeting.getId(), 2);
|
||||
updateProgress(meeting.getId(), 90, "正在生成智能总结纪要...", 0);
|
||||
|
||||
meetingPointsService.assertSufficientPointsBeforeSummarySubmit(meeting, taskRecord);
|
||||
taskRecord.setStatus(1);
|
||||
taskRecord.setStartedAt(LocalDateTime.now());
|
||||
Map<String, Object> initialResponseData = new HashMap<>();
|
||||
|
|
@ -954,7 +967,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
|
||||
Files.writeString(filePath, markdownContent, StandardCharsets.UTF_8);
|
||||
|
||||
boolean alreadyCompleted = MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED);
|
||||
taskRecord.setResultFilePath("meetings/" + meeting.getId() + "/summaries/" + fileName);
|
||||
Map<String, Object> responseData = objectMapper.convertValue(respNode, Map.class);
|
||||
responseData.put("summarySource", summarySource.toSnapshot());
|
||||
|
|
@ -976,7 +988,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> 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);
|
||||
}
|
||||
|
|
@ -1107,10 +1118,12 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
}
|
||||
updateMeetingStatus(meeting.getId(), 2);
|
||||
try {
|
||||
meetingPointsService.assertSufficientPointsBeforeSummarySubmit(meeting, summaryTask);
|
||||
var result = meetingExternalSummaryWebhookTrigger.trigger(meeting, summaryTask, chapterTask, triggerSource, force);
|
||||
this.updateById(summaryTask);
|
||||
updateProgress(meeting.getId(), 95, result.getMessage(), 0);
|
||||
} catch (Exception ex) {
|
||||
failPendingSummaryTask(summaryTask, ex.getMessage());
|
||||
this.updateById(summaryTask);
|
||||
updateProgress(meeting.getId(), -1, "闂佽崵鍠愰悷杈╃不閹达絻浜归柛灞剧☉缁剁偤鏌″搴″箹闁?n8n 缂傚倸鍊搁崐褰掓偋濡ゅ啯鏆滈柟鐐綑缁剁偤寮堕崼顐函鐞? " + ex.getMessage(), 0);
|
||||
log.error("Failed to trigger external n8n webhook for meeting {}", meeting.getId(), ex);
|
||||
|
|
@ -1230,6 +1243,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
private String postJson(String url, Object body, String apiKey) throws Exception {
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||
.uri(buildUri(url))
|
||||
.timeout(ASR_SUBMIT_REQUEST_TIMEOUT)
|
||||
.header("Content-Type", "application/json");
|
||||
if (apiKey != null && !apiKey.isBlank()) {
|
||||
builder.header("Authorization", "Bearer " + apiKey);
|
||||
|
|
@ -1241,7 +1255,9 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
}
|
||||
|
||||
private String get(String url, String apiKey) throws Exception {
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(buildUri(url));
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||
.uri(buildUri(url))
|
||||
.timeout(ASR_QUERY_REQUEST_TIMEOUT);
|
||||
if (apiKey != null && !apiKey.isBlank()) {
|
||||
builder.header("Authorization", "Bearer " + apiKey);
|
||||
}
|
||||
|
|
@ -1319,22 +1335,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> 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<String, Object> req) {
|
||||
AiTask task = new AiTask();
|
||||
task.setMeetingId(meetingId);
|
||||
|
|
@ -1362,6 +1362,17 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
meetingPointsService.markSummaryChargeFailed(task.getId(), error);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildAsrFailureMessage(Exception ex) {
|
||||
if (ex == null) {
|
||||
return "ASR task failed";
|
||||
}
|
||||
String message = ex.getMessage();
|
||||
if (message == null || message.isBlank()) {
|
||||
return "ASR task failed: " + ex.getClass().getSimpleName();
|
||||
}
|
||||
return "ASR task failed: " + message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import com.imeeting.entity.biz.Meeting;
|
|||
import com.imeeting.entity.biz.MeetingTranscript;
|
||||
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
|
||||
import com.imeeting.enums.MeetingStatusEnum;
|
||||
import com.imeeting.service.android.AndroidMeetingPushService;
|
||||
import com.imeeting.service.android.AndroidPendingMeetingDraftService;
|
||||
import com.imeeting.service.android.AndroidPushMessageService;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
|
|
@ -83,7 +82,6 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
private final MeetingPointsService meetingPointsService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
||||
private final AndroidMeetingPushService androidMeetingPushService;
|
||||
private final AndroidPushMessageService androidPushMessageService;
|
||||
private final AndroidPendingMeetingDraftService androidPendingMeetingDraftService;
|
||||
private final MeetingLockCache meetingLockCache;
|
||||
|
|
@ -109,7 +107,6 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
MeetingPointsService meetingPointsService,
|
||||
ObjectMapper objectMapper,
|
||||
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
|
||||
AndroidMeetingPushService androidMeetingPushService,
|
||||
AndroidPushMessageService androidPushMessageService,
|
||||
AndroidPendingMeetingDraftService androidPendingMeetingDraftService,
|
||||
MeetingLockCache meetingLockCache,
|
||||
|
|
@ -130,7 +127,6 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
this.meetingPointsService = meetingPointsService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
|
||||
this.androidMeetingPushService = androidMeetingPushService;
|
||||
this.androidPushMessageService = androidPushMessageService;
|
||||
this.androidPendingMeetingDraftService = androidPendingMeetingDraftService;
|
||||
this.meetingLockCache = meetingLockCache;
|
||||
|
|
@ -886,7 +882,6 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
aiTaskService.updateById(summaryTask);
|
||||
meetingPointsService.recordSummarySuccessCharge(meeting, summaryTask);
|
||||
|
||||
boolean alreadyCompleted = MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED);
|
||||
meeting.setLatestSummaryTaskId(summaryTask.getId());
|
||||
meetingService.updateById(meeting);
|
||||
aiTaskService.reconcileMeetingStatus(meeting.getId());
|
||||
|
|
@ -898,7 +893,6 @@ 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);
|
||||
}
|
||||
|
|
@ -1320,22 +1314,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -85,7 +85,10 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
throw new RuntimeException("分配积分必须大于0");
|
||||
}
|
||||
|
||||
MeetingPointsAccount publicAccount = getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false);
|
||||
MeetingPointsAccount publicAccount = findAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID);
|
||||
if (publicAccount == null) {
|
||||
throw new RuntimeException("公共积分账户不存在");
|
||||
}
|
||||
long publicBefore = defaultLong(publicAccount.getCurrentBalance());
|
||||
if (publicBefore < safePoints) {
|
||||
throw new RuntimeException("公共账户积分不足");
|
||||
|
|
@ -258,6 +261,33 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void assertSufficientPointsBeforeAsrSubmit(Meeting meeting, AiTask asrTask) {
|
||||
if (!shouldEnforceBalance() || meeting == null || asrTask == null || meeting.getId() == null) {
|
||||
return;
|
||||
}
|
||||
Integer durationSeconds = resolveEffectiveAudioDurationSeconds(meeting);
|
||||
if (durationSeconds == null || durationSeconds <= 0) {
|
||||
throw new RuntimeException("无法解析录音时长,不能校验积分余额");
|
||||
}
|
||||
ensureSufficientPoints(meeting, null, buildChargeSnapshot(durationSeconds).asrPoints(), "ASR_SUBMIT");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void assertSufficientPointsBeforeSummarySubmit(Meeting meeting, AiTask summaryTask) {
|
||||
if (!shouldEnforceBalance() || meeting == null || summaryTask == null || meeting.getId() == null) {
|
||||
return;
|
||||
}
|
||||
String chargeTriggerType = resolveChargeTriggerType(summaryTask);
|
||||
Integer durationSeconds = resolveEffectiveAudioDurationSeconds(meeting);
|
||||
if (durationSeconds == null || durationSeconds <= 0) {
|
||||
throw new RuntimeException("无法解析录音时长,不能校验积分余额");
|
||||
}
|
||||
ensureSufficientPoints(meeting, summaryTask, chargeTriggerType, durationSeconds, "SUMMARY_SUBMIT");
|
||||
}
|
||||
|
||||
private MeetingSummaryChargeRecord getOrCreateChargeRecord(Meeting meeting, AiTask summaryTask, String chargeTriggerType, int durationSeconds) {
|
||||
MeetingSummaryChargeRecord record = meetingSummaryChargeRecordMapper.selectForUpdateBySummaryTaskId(summaryTask.getId());
|
||||
if (record != null) {
|
||||
|
|
@ -297,6 +327,68 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
return record;
|
||||
}
|
||||
|
||||
private void ensureSufficientPoints(Meeting meeting,
|
||||
AiTask summaryTask,
|
||||
String chargeTriggerType,
|
||||
int durationSeconds,
|
||||
String submitStage) {
|
||||
MeetingSummaryChargeRecord record = getOrCreateChargeRecord(meeting, summaryTask, chargeTriggerType, durationSeconds);
|
||||
long requiredPoints = defaultLong(record.getTotalPoints()) - defaultLong(record.getChargedTotalPoints());
|
||||
if (requiredPoints <= 0L) {
|
||||
clearBlockedReason(record);
|
||||
return;
|
||||
}
|
||||
long availableBalance = resolveAvailableBalanceForCheck(meeting.getTenantId(), record.getUserId());
|
||||
if (availableBalance < requiredPoints) {
|
||||
record.setBlockedReason("INSUFFICIENT_POINTS");
|
||||
record.setFailureReason("INSUFFICIENT_POINTS at " + submitStage + ", required="
|
||||
+ requiredPoints + ", available=" + availableBalance);
|
||||
record.setBalanceBefore(availableBalance);
|
||||
record.setBalanceAfter(availableBalance);
|
||||
record.setSummaryStatus("BLOCKED");
|
||||
saveOrUpdateRecord(record);
|
||||
throw new RuntimeException("积分余额不足");
|
||||
}
|
||||
clearBlockedReason(record);
|
||||
}
|
||||
|
||||
private void ensureSufficientPoints(Meeting meeting,
|
||||
MeetingSummaryChargeRecord record,
|
||||
long requiredPoints,
|
||||
String submitStage) {
|
||||
if (requiredPoints <= 0L) {
|
||||
clearBlockedReason(record);
|
||||
return;
|
||||
}
|
||||
Long ownerUserId = record == null || record.getUserId() == null ? meeting.getCreatorId() : record.getUserId();
|
||||
long availableBalance = resolveAvailableBalanceForCheck(meeting.getTenantId(), ownerUserId);
|
||||
if (availableBalance < requiredPoints) {
|
||||
if (record != null) {
|
||||
record.setBlockedReason("INSUFFICIENT_POINTS");
|
||||
record.setFailureReason("INSUFFICIENT_POINTS at " + submitStage + ", required="
|
||||
+ requiredPoints + ", available=" + availableBalance);
|
||||
record.setBalanceBefore(availableBalance);
|
||||
record.setBalanceAfter(availableBalance);
|
||||
record.setSummaryStatus("BLOCKED");
|
||||
saveOrUpdateRecord(record);
|
||||
}
|
||||
throw new RuntimeException("积分余额不足");
|
||||
}
|
||||
clearBlockedReason(record);
|
||||
}
|
||||
|
||||
private void clearBlockedReason(MeetingSummaryChargeRecord record) {
|
||||
if (record == null || record.getId() == null) {
|
||||
return;
|
||||
}
|
||||
if (record.getBlockedReason() != null || "BLOCKED".equals(record.getSummaryStatus())) {
|
||||
record.setBlockedReason(null);
|
||||
record.setFailureReason(null);
|
||||
record.setSummaryStatus(isPointsEnabled() ? STATUS_PENDING : STATUS_DISABLED);
|
||||
saveOrUpdateRecord(record);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyChargeSnapshot(MeetingSummaryChargeRecord record, Meeting meeting, String chargeTriggerType, int durationSeconds) {
|
||||
ChargeSnapshot snapshot = buildChargeSnapshot(durationSeconds);
|
||||
String accountMode = resolveAccountMode();
|
||||
|
|
@ -328,7 +420,7 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
for (ChargeTarget target : chargeTargets) {
|
||||
totalBalanceBefore += defaultLong(target.account().getCurrentBalance());
|
||||
}
|
||||
if (isBalanceEnforced() && totalBalanceBefore < chargeAmount) {
|
||||
if (shouldEnforceBalance() && totalBalanceBefore < chargeAmount) {
|
||||
record.setBlockedReason("INSUFFICIENT_POINTS");
|
||||
saveOrUpdateRecord(record);
|
||||
throw new RuntimeException("积分余额不足");
|
||||
|
|
@ -374,7 +466,7 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
if (remaining <= 0L) {
|
||||
return 0L;
|
||||
}
|
||||
if (isBalanceEnforced()) {
|
||||
if (shouldEnforceBalance()) {
|
||||
return Math.min(Math.max(currentBalance, 0L), remaining);
|
||||
}
|
||||
if (lastTarget) {
|
||||
|
|
@ -429,6 +521,30 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private MeetingPointsAccount findAccountForMutation(Long tenantId, Long userId) {
|
||||
if (tenantId == null || userId == null) {
|
||||
return null;
|
||||
}
|
||||
return meetingPointsAccountMapper.selectForUpdate(tenantId, userId);
|
||||
}
|
||||
|
||||
private long resolveAvailableBalanceForCheck(Long tenantId, Long ownerUserId) {
|
||||
Long personalUserId = ownerUserId == null ? UNIFIED_ACCOUNT_USER_ID : ownerUserId;
|
||||
String accountMode = resolveAccountMode();
|
||||
if (ACCOUNT_MODE_PUBLIC.equals(accountMode) || personalUserId.equals(UNIFIED_ACCOUNT_USER_ID)) {
|
||||
return positiveBalance(findAccount(tenantId, UNIFIED_ACCOUNT_USER_ID));
|
||||
}
|
||||
if (ACCOUNT_MODE_PERSONAL.equals(accountMode)) {
|
||||
return positiveBalance(findAccount(tenantId, personalUserId));
|
||||
}
|
||||
return positiveBalance(findAccount(tenantId, personalUserId))
|
||||
+ positiveBalance(findAccount(tenantId, UNIFIED_ACCOUNT_USER_ID));
|
||||
}
|
||||
|
||||
private long positiveBalance(MeetingPointsAccount account) {
|
||||
return account == null ? 0L : Math.max(defaultLong(account.getCurrentBalance()), 0L);
|
||||
}
|
||||
|
||||
private MeetingPointsAccount getOrCreateAccountForMutation(Long tenantId, Long userId, long initialBalance, boolean createInitLedger) {
|
||||
MeetingPointsAccount account = meetingPointsAccountMapper.selectForUpdate(tenantId, userId);
|
||||
if (account != null) {
|
||||
|
|
@ -463,34 +579,34 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
String chargePriority = resolveChargePriority();
|
||||
List<ChargeTarget> targets = new ArrayList<>();
|
||||
if (ACCOUNT_MODE_PUBLIC.equals(accountMode)) {
|
||||
targets.add(new ChargeTarget(ACCOUNT_MODE_PUBLIC, UNIFIED_ACCOUNT_USER_ID,
|
||||
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false)));
|
||||
addChargeTargetIfAccountExists(targets, ACCOUNT_MODE_PUBLIC, tenantId, UNIFIED_ACCOUNT_USER_ID);
|
||||
return targets;
|
||||
}
|
||||
if (ACCOUNT_MODE_PERSONAL.equals(accountMode)) {
|
||||
targets.add(new ChargeTarget(ACCOUNT_MODE_PERSONAL, personalUserId,
|
||||
getOrCreateAccountForMutation(tenantId, personalUserId, 0L, false)));
|
||||
addChargeTargetIfAccountExists(targets, ACCOUNT_MODE_PERSONAL, tenantId, personalUserId);
|
||||
return targets;
|
||||
}
|
||||
if (personalUserId.equals(UNIFIED_ACCOUNT_USER_ID)) {
|
||||
targets.add(new ChargeTarget(ACCOUNT_MODE_PUBLIC, UNIFIED_ACCOUNT_USER_ID,
|
||||
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false)));
|
||||
addChargeTargetIfAccountExists(targets, ACCOUNT_MODE_PUBLIC, tenantId, UNIFIED_ACCOUNT_USER_ID);
|
||||
return targets;
|
||||
}
|
||||
if (CHARGE_PRIORITY_PUBLIC_FIRST.equals(chargePriority)) {
|
||||
targets.add(new ChargeTarget(ACCOUNT_MODE_PUBLIC, UNIFIED_ACCOUNT_USER_ID,
|
||||
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false)));
|
||||
targets.add(new ChargeTarget(ACCOUNT_MODE_PERSONAL, personalUserId,
|
||||
getOrCreateAccountForMutation(tenantId, personalUserId, 0L, false)));
|
||||
addChargeTargetIfAccountExists(targets, ACCOUNT_MODE_PUBLIC, tenantId, UNIFIED_ACCOUNT_USER_ID);
|
||||
addChargeTargetIfAccountExists(targets, ACCOUNT_MODE_PERSONAL, tenantId, personalUserId);
|
||||
return targets;
|
||||
}
|
||||
targets.add(new ChargeTarget(ACCOUNT_MODE_PERSONAL, personalUserId,
|
||||
getOrCreateAccountForMutation(tenantId, personalUserId, 0L, false)));
|
||||
targets.add(new ChargeTarget(ACCOUNT_MODE_PUBLIC, UNIFIED_ACCOUNT_USER_ID,
|
||||
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false)));
|
||||
addChargeTargetIfAccountExists(targets, ACCOUNT_MODE_PERSONAL, tenantId, personalUserId);
|
||||
addChargeTargetIfAccountExists(targets, ACCOUNT_MODE_PUBLIC, tenantId, UNIFIED_ACCOUNT_USER_ID);
|
||||
return targets;
|
||||
}
|
||||
|
||||
private void addChargeTargetIfAccountExists(List<ChargeTarget> targets, String accountMode, Long tenantId, Long userId) {
|
||||
MeetingPointsAccount account = findAccountForMutation(tenantId, userId);
|
||||
if (account != null) {
|
||||
targets.add(new ChargeTarget(accountMode, userId, account));
|
||||
}
|
||||
}
|
||||
|
||||
private ChargeSnapshot buildChargeSnapshot(int durationSeconds) {
|
||||
int chargedMinutes = toChargedMinutes(durationSeconds);
|
||||
int unitMinutes = positiveInt(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_UNIT_MINUTES, "1"), 1);
|
||||
|
|
@ -557,7 +673,11 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
}
|
||||
|
||||
private boolean isBalanceEnforced() {
|
||||
return Boolean.parseBoolean(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_ENFORCE_BALANCE, "false"));
|
||||
return Boolean.parseBoolean(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_ENFORCE_BALANCE, "true"));
|
||||
}
|
||||
|
||||
private boolean shouldEnforceBalance() {
|
||||
return isPointsEnabled() && isBalanceEnforced();
|
||||
}
|
||||
|
||||
private String resolveAccountMode() {
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@
|
|||
"kickDeviceConfirm": "Kick this device offline?",
|
||||
"kickSucceeded": "Device has been kicked offline",
|
||||
"resetStats": "Reset stats",
|
||||
"resetStatsConfirm": "Reset this device's homepage statistics?",
|
||||
"resetStatsConfirm": "Reset this device's statistics?",
|
||||
"resetStatsSucceeded": "Device statistics reset",
|
||||
"deleteDevice": "Delete this device?",
|
||||
"weatherCityName": "Weather City",
|
||||
|
|
|
|||
|
|
@ -358,7 +358,11 @@
|
|||
"searchSelectUser": "搜索并选择用户",
|
||||
"deviceCodeRequired": "请输入设备编码",
|
||||
"deviceCodePlaceholder": "输入唯一设备编码",
|
||||
"deviceNamePlaceholder": "例如:A 会议室录音设备"
|
||||
"deviceNamePlaceholder": "例如:A 会议室录音设备",
|
||||
"weatherCityName": "城市",
|
||||
"statsResetAt": "重置时间",
|
||||
"resetStatsConfirm": "确认重置?"
|
||||
|
||||
},
|
||||
"dashboardExt": {
|
||||
"processing": "处理中",
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ export default function Devices() {
|
|||
<Input placeholder={t("devicesExt.deviceNamePlaceholder")} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("devicesExt.weatherCityName")} name="weatherCityName">
|
||||
<Input placeholder={t("devicesExt.weatherCityNamePlaceholder")} />
|
||||
<Input placeholder={t("devicesExt.weatherCityName")} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("common.status")} name="status" initialValue={1}>
|
||||
<Select options={statusDict.map((item) => ({ value: Number(item.itemValue), label: item.itemLabel }))} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue