diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index 0df4634..f276c37 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -10,6 +10,7 @@ import com.imeeting.common.MeetingProgressStage; import com.imeeting.dto.biz.AiModelVO; import com.imeeting.dto.biz.MeetingSummarySource; import com.imeeting.dto.biz.MeetingTranscriptSourceVO; +import com.imeeting.dto.biz.UnifiedMeetingStatusStage; import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.HotWord; import com.imeeting.entity.biz.Meeting; @@ -18,6 +19,7 @@ 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; @@ -107,8 +109,10 @@ public class AiTaskServiceImpl extends ServiceImpl impleme .connectTimeout(Duration.ofSeconds(300)) .version(HttpClient.Version.HTTP_1_1) .build(); + @Autowired + private AndroidMeetingPushService androidMeetingPushService; - @Autowired + @Autowired public AiTaskServiceImpl(MeetingMapper meetingMapper, MeetingTranscriptMapper transcriptMapper, AiModelService aiModelService, @@ -323,10 +327,12 @@ public class AiTaskServiceImpl extends ServiceImpl impleme chapterTask == null ? null : chapterTask.getId(), chapterTask == null ? null : chapterTask.getStatus()); reconcileMeetingStatus(meetingId); + androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.FAILED_SUMMARIZING.getCode()); return; } executeChapterFlow(meeting, chapterTask); reconcileMeetingStatus(meetingId); + androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.COMPLETED.getCode()); log.info("[CHAPTER-FLOW] 章节任务流程结束: meetingId={}, chapterTaskId={}, costMs={}", meetingId, chapterTask.getId(), System.currentTimeMillis() - startMillis); } @@ -365,11 +371,13 @@ public class AiTaskServiceImpl extends ServiceImpl impleme sumTask == null ? null : sumTask.getStatus()); } reconcileMeetingStatus(meetingId); + androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.COMPLETED.getCode()); } catch (Exception e) { log.error("[SUMMARY-FLOW] 总结任务执行异常: meetingId={}, sumTaskId={}", meetingId, sumTask == null ? null : sumTask.getId(), e); failPendingSummaryTask(sumTask, e.getMessage()); reconcileMeetingStatus(meetingId); + androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.FAILED_SUMMARIZING.getCode()); } finally { log.info("[SUMMARY-FLOW] 总结任务流程结束: meetingId={}, costMs={}", meetingId, System.currentTimeMillis() - startMillis); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java index 16e3b99..7aa55c0 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java @@ -1047,7 +1047,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { if (effectivePromptId == null) { throw new RuntimeException("缺少 promptId,无法创建总结任务"); } - meetingDomainSupport.createSummaryTask( + AiTask summaryTask = meetingDomainSupport.createSummaryTask( meetingId, effectiveSummaryModelId, effectiveChapterModelId, @@ -1060,11 +1060,12 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { meeting.setPromptId(effectivePromptId); meeting.setSummaryDetailLevel(effectiveSummaryDetailLevel); meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode()); + meeting.setLatestSummaryTaskId(summaryTask.getId()); meetingService.updateById(meeting); if ("EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) { - updateMeetingProgress(meetingId, 95, "等待外部总结编排...", 0); + updateMeetingProgress(meetingId, summaryTask, 95, "等待外部总结编排...", 0); } else { - updateMeetingProgress(meetingId, 90, "重新总结已提交,正在生成总结...", 0); + updateMeetingProgress(meetingId, summaryTask, 90, "重新总结已提交,正在生成总结...", 0); } dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); } @@ -1457,6 +1458,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { } private void updateMeetingProgress(Long meetingId, int percent, String message, int eta) { + updateMeetingProgress(meetingId, null, percent, message, eta); + } + + private void updateMeetingProgress(Long meetingId, AiTask task, int percent, String message, int eta) { com.imeeting.common.MeetingProgressStage stage; int meetingStatus; if (percent < 0) { @@ -1478,7 +1483,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { stage = com.imeeting.common.MeetingProgressStage.QUEUED; meetingStatus = MeetingStatusEnum.TRANSCRIBING.getCode(); } - meetingProgressService.markStageAfterCommitOrNow(meetingId, null, meetingStatus, stage, percent, message, eta); + meetingProgressService.markStageAfterCommitOrNow(meetingId, task, meetingStatus, stage, percent, message, eta); } private RealtimeMeetingResumeConfig buildRealtimeResumeConfig(CreateRealtimeMeetingCommand command, diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingUnifiedStatusServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingUnifiedStatusServiceImpl.java index 22bd4db..4a64add 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingUnifiedStatusServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingUnifiedStatusServiceImpl.java @@ -133,6 +133,9 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ if (isTaskFailed(chapterTask)) { return UnifiedMeetingStatusStage.FAILED_SUMMARIZING; } + if (isTaskCompleted(asrTask) || hasTranscript(meeting.getId()) || summaryTask != null || chapterTask != null) { + return UnifiedMeetingStatusStage.FAILED_SUMMARIZING; + } return UnifiedMeetingStatusStage.FAILED_INITIALIZING; } @@ -210,6 +213,10 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ } private boolean canViewTranscript(Long meetingId) { + return hasTranscript(meetingId); + } + + private boolean hasTranscript(Long meetingId) { return meetingId != null && meetingTranscriptMapper.selectCount(new LambdaQueryWrapper() .eq(MeetingTranscript::getMeetingId, meetingId)) > 0; } diff --git a/frontend/src/hooks/usePermission.ts b/frontend/src/hooks/usePermission.ts index 2ab24e6..3d60a38 100644 --- a/frontend/src/hooks/usePermission.ts +++ b/frontend/src/hooks/usePermission.ts @@ -8,8 +8,10 @@ const PROFILE_KEY = "userProfile"; interface PermissionState { codes: string[]; isAdmin: boolean; + isPlatformAdmin: boolean; setCodes: (codes: string[]) => void; setIsAdmin: (isAdmin: boolean) => void; + setIsPlatformAdmin: (isAdmin: boolean) => void; load: () => Promise; } @@ -32,6 +34,16 @@ export const usePermissionStore = create((set) => ({ return false; } })(), + isPlatformAdmin: (() => { + try { + const raw = sessionStorage.getItem(PROFILE_KEY); + if (!raw) return false; + const parsed = JSON.parse(raw); + return !!parsed.isPlatformAdmin; + } catch (e) { + return false; + } + })(), setCodes: (codes) => { set({ codes }); localStorage.setItem(STORAGE_KEY, JSON.stringify(codes)); @@ -39,20 +51,27 @@ export const usePermissionStore = create((set) => ({ setIsAdmin: (isAdmin) => { set({ isAdmin }); }, + setIsPlatformAdmin: (isPlatformAdmin) => { + set({isPlatformAdmin}); + }, load: async () => { try { let isAdmin = false; + let isPlatformAdmin = false; const cachedProfile = sessionStorage.getItem(PROFILE_KEY); if (cachedProfile) { const parsed = JSON.parse(cachedProfile); isAdmin = !!parsed.isAdmin; + isPlatformAdmin = !!parsed.isPlatformAdmin; } else { const profile = await getCurrentUser(); isAdmin = !!profile.isAdmin; + isPlatformAdmin = !!profile.isPlatformAdmin; sessionStorage.setItem(PROFILE_KEY, JSON.stringify(profile)); } + set({isPlatformAdmin}); set({ isAdmin }); - if (isAdmin) { + if (isPlatformAdmin) { return; } const perms = await listMyPermissions(); @@ -66,27 +85,23 @@ export const usePermissionStore = create((set) => ({ })); export function usePermission() { - const { codes, load, isAdmin } = usePermissionStore(); + const {codes, load, isAdmin, isPlatformAdmin} = usePermissionStore(); const can = (perm?: string) => { if (!perm) return true; - - if (isAdmin) return true; + + if (isPlatformAdmin) return true; if (!codes || codes.length === 0) { - return true; + return perm.startsWith("menu:"); } const hasMenuCodes = codes.some((c) => c.startsWith("menu:")); - const hasButtonCodes = codes.some((c) => !c.startsWith("menu:") && c.includes(":")); - let result = false; if (perm.startsWith("menu:")) { - result = !hasMenuCodes || codes.includes(perm); - } else { - result = !hasButtonCodes || codes.includes(perm); + return !hasMenuCodes || codes.includes(perm); } - return result; + return codes.includes(perm); }; return { codes, load, can, isAdmin }; diff --git a/frontend/src/pages/business/Meetings.tsx b/frontend/src/pages/business/Meetings.tsx index 9eac195..44a35c3 100644 --- a/frontend/src/pages/business/Meetings.tsx +++ b/frontend/src/pages/business/Meetings.tsx @@ -112,7 +112,7 @@ const hasLatestGenerationFailure = (item: MeetingVO) => item.latestChapterAttemptStatus === 3 || item.latestSummaryAttemptStatus === 3; const shouldTrackGenerationProgress = (item: MeetingVO) => - !hasLatestGenerationFailure(item) && (item.status === 0 || item.status === 1 || item.status === 2); + item.status === 0 || item.status === 1 || item.status === 2; const isTerminalMeetingProgress = (progress?: MeetingProgress | null) => !!progress && ( @@ -156,14 +156,17 @@ const getEffectiveStatus = (item: MeetingVO, progress: MeetingProgress | null) = if (unifiedStatusCode?.startsWith("FAILED_")) { return 4; } - if (hasLatestGenerationFailure(item)) { - return 4; - } const status = item.displayStatus ?? item.status; // 如果处于初始化中但已经有进度,则视为转译音频中 if (status === 0 && progress && progress.percent > 0) { return 1; } + if (status === 0 || status === 1 || status === 2) { + return status; + } + if (hasLatestGenerationFailure(item)) { + return 4; + } return status; }; diff --git a/frontend/src/pages/business/TenantMeetingPointsSettings.tsx b/frontend/src/pages/business/TenantMeetingPointsSettings.tsx index 80b3f43..8f94194 100644 --- a/frontend/src/pages/business/TenantMeetingPointsSettings.tsx +++ b/frontend/src/pages/business/TenantMeetingPointsSettings.tsx @@ -3,6 +3,7 @@ import { getCurrentUser } from "@/api"; import AppPagination from "@/components/shared/AppPagination"; import ListTable from "@/components/shared/ListTable/ListTable"; import PageContainer from "@/components/shared/PageContainer"; +import {usePermission} from "@/hooks/usePermission"; import { getCurrentTenantMeetingPointsSetting, pageTenantMeetingPointsSettings, @@ -14,6 +15,7 @@ import { Button, Card, Input, message, Modal, Select, Space, Statistic, Tag, Typ import { useEffect, useState } from "react"; const { Text } = Typography; +const BALANCE_CHECK_UPDATE_PERMISSION = "biz:tenant-meeting-points:balance-check:update"; function formatDateTime(value?: string) { return value ? value.replace("T", " ").substring(0, 19) : "-"; @@ -26,6 +28,7 @@ function renderStatusTag(enabled?: boolean) { } export default function TenantMeetingPointsSettings() { + const {can} = usePermission(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(false); const [switchingTenantId, setSwitchingTenantId] = useState(null); @@ -42,6 +45,7 @@ export default function TenantMeetingPointsSettings() { const isPlatformAdmin = Boolean(profile?.isPlatformAdmin); const isTenantAdmin = Boolean(profile?.isTenantAdmin); + const canUpdateBalanceCheck = isPlatformAdmin || (isTenantAdmin && can(BALANCE_CHECK_UPDATE_PERMISSION)); const loadPlatformPage = async (nextParams = params) => { setLoading(true); @@ -179,15 +183,20 @@ export default function TenantMeetingPointsSettings() { key: "action", width: 140, fixed: "right" as const, - render: (_: unknown, record: TenantMeetingPointsSettingVO) => ( - - ), + render: (_: unknown, record: TenantMeetingPointsSettingVO) => { + if (!canUpdateBalanceCheck) { + return null; + } + return ( + + ); + }, }, ]; @@ -231,13 +240,15 @@ export default function TenantMeetingPointsSettings() { - + {canUpdateBalanceCheck ? ( + + ) : null}