feat: 添加平台管理员权限和优化会议状态处理逻辑
- 在 `usePermission` 钩子中添加 `isPlatformAdmin` 权限检查 - 在 `TenantMeetingPointsSettings` 页面中根据权限控制按钮显示 - 优化 `MeetingCommandServiceImpl` 和 `MeetingUnifiedStatusServiceImpl` 中的会议状态处理逻辑 - 更新 `Meetings.tsx` 中的会议状态判断逻辑 - 在 `AiTaskServiceImpl` 中添加 `AndroidMeetingPushService` 依赖,并在任务完成或失败时推送会议状态变化dev_na
parent
4572d15bea
commit
3c299aaf3b
|
|
@ -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<AiTaskMapper, AiTask> 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<AiTaskMapper, AiTask> 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<AiTaskMapper, AiTask> 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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<MeetingTranscript>()
|
||||
.eq(MeetingTranscript::getMeetingId, meetingId)) > 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +34,16 @@ export const usePermissionStore = create<PermissionState>((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<PermissionState>((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<PermissionState>((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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<UserProfile | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [switchingTenantId, setSwitchingTenantId] = useState<number | null>(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) => (
|
||||
<Button
|
||||
type="link"
|
||||
loading={switchingTenantId === record.tenantId}
|
||||
onClick={() => confirmSwitch(record, !record.balanceCheckEnabled)}
|
||||
>
|
||||
{record.balanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
|
||||
</Button>
|
||||
),
|
||||
render: (_: unknown, record: TenantMeetingPointsSettingVO) => {
|
||||
if (!canUpdateBalanceCheck) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
loading={switchingTenantId === record.tenantId}
|
||||
onClick={() => confirmSwitch(record, !record.balanceCheckEnabled)}
|
||||
>
|
||||
{record.balanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -231,13 +240,15 @@ export default function TenantMeetingPointsSettings() {
|
|||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={switchingTenantId === currentTenantSetting.tenantId}
|
||||
onClick={() => confirmSwitch(currentTenantSetting, !currentTenantSetting.balanceCheckEnabled)}
|
||||
>
|
||||
{currentTenantSetting.balanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
|
||||
</Button>
|
||||
{canUpdateBalanceCheck ? (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={switchingTenantId === currentTenantSetting.tenantId}
|
||||
onClick={() => confirmSwitch(currentTenantSetting, !currentTenantSetting.balanceCheckEnabled)}
|
||||
>
|
||||
{currentTenantSetting.balanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
|
||||
刷新
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue