refactor: 移除会议完成推送逻辑并更新积分校验处理

- 移除 `AndroidMeetingPushService` 中的 `pushMeetingCompleted` 方法及相关调用
- 更新 `MeetingCommandServiceImpl` 和 `AiTaskServiceImpl`,移除对 `AndroidMeetingPushService` 的依赖
- 新增 `assertSufficientPointsBeforeAsrSubmit` 和 `assertSufficientPointsBeforeSummarySubmit` 方法到 `MeetingPointsService`,并在 `AiTaskServiceImpl` 中调用这些方法以确保在提交 ASR 和总结任务前有足够的积分
- 更新前端表单占位符和多语言文件中的文本
dev_na
chenhao 2026-06-10 18:23:04 +08:00
parent b07bbe90a0
commit 178b920581
12 changed files with 219 additions and 131 deletions

View File

@ -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) {

View File

@ -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) {

View File

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

View File

@ -7,7 +7,5 @@ public interface AndroidMeetingPushService {
void pushPublicLoginConfirm(String deviceId, AndroidPublicLoginConfirmPayload payload);
void pushMeetingCompleted(Long meetingId);
void pushMeetingStatusChanged(Long meetingId, String statusCode);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

@ -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",

View File

@ -358,7 +358,11 @@
"searchSelectUser": "搜索并选择用户",
"deviceCodeRequired": "请输入设备编码",
"deviceCodePlaceholder": "输入唯一设备编码",
"deviceNamePlaceholder": "例如A 会议室录音设备"
"deviceNamePlaceholder": "例如A 会议室录音设备",
"weatherCityName": "城市",
"statsResetAt": "重置时间",
"resetStatsConfirm": "确认重置?"
},
"dashboardExt": {
"processing": "处理中",

View File

@ -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 }))} />