refactor: 移除 `ExistingOfflineMeetingException` 并更新相关逻辑

- 移除 `ExistingOfflineMeetingException` 类
- 更新 `AndroidMeetingController` 中的异常处理,使用 `BusinessException` 和 `BusinessErrorCodeEnum`
- 优化 `AndroidDeviceHomeServiceImpl`,添加 `TenantMeetingPointsSettingService` 依赖并更新积分校验逻辑
- 将 `AboutPage` 页面移至 `ProfilePage` 的模态框中
- 更新 `MeetingUnifiedStatusServiceImpl` 的代码格式和逻辑顺序
dev_na
chenhao 2026-06-12 14:00:45 +08:00
parent fd9ef5c885
commit c64c8b5690
11 changed files with 301 additions and 408 deletions

View File

@ -1,13 +0,0 @@
package com.imeeting.common.exception;
import lombok.Getter;
@Getter
public class ExistingOfflineMeetingException extends RuntimeException {
private final Long meetingId;
public ExistingOfflineMeetingException(Long meetingId) {
super("有未结束会议");
this.meetingId = meetingId;
}
}

View File

@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.imeeting.common.MeetingConstants;
import com.imeeting.common.SysParamKeys;
import com.imeeting.common.exception.ExistingOfflineMeetingException;
import com.imeeting.dto.android.AndroidAuthContext;
import com.imeeting.dto.android.AndroidOfflineMeetingCreateCommand;
import com.imeeting.dto.android.AndroidMeetingCreateResponse;
@ -28,6 +27,7 @@ import com.imeeting.dto.biz.UnifiedMeetingStatusVO;
import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.enums.BusinessErrorCodeEnum;
import com.imeeting.enums.MeetingStatusEnum;
import com.imeeting.service.android.AndroidAuthService;
import com.imeeting.service.android.AndroidChunkUploadService;
@ -37,6 +37,7 @@ import com.imeeting.service.biz.*;
import com.unisbase.annotation.Anonymous;
import com.unisbase.common.ApiResponse;
import com.unisbase.common.annotation.Log;
import com.unisbase.common.exception.BusinessException;
import com.unisbase.dto.PageResult;
import com.unisbase.entity.SysTenant;
import com.unisbase.entity.SysUser;
@ -166,16 +167,13 @@ public class AndroidMeetingController {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
resolvePublicDeviceTenantId(request, command, authContext);
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
try {
// Meeting existingMeeting = findLatestUnfinishedMeetingByDevice(authContext.getDeviceId());
// if (existingMeeting != null) {
// return new ApiResponse<>("409", "设备端已有会议", meetingQueryService.getDetailIgnoreTenant(existingMeeting.getId()));
// }
MeetingVO meeting = legacyMeetingAdapterService.createMeeting(command, authContext, loginUser);
return ApiResponse.ok(buildAndroidMeetingCreateResponse(meeting));
} catch (ExistingOfflineMeetingException ex) {
return new ApiResponse<>("409", "有未结束会议", new AndroidOfflineMeetingConflictVO(ex.getMeetingId()));
}
}
@Operation(summary = "上传Android会议音频")
@ -292,21 +290,6 @@ public class AndroidMeetingController {
return ApiResponse.ok(buildAndroidMeetingListPage(result));
}
@Operation(summary = "查询Android会议预览数据")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "返回会议预览结果,包含已完成摘要或处理中状态信息",
content = @Content(schema = @Schema(implementation = LegacyMeetingPreviewDataResponse.class))
)
})
@GetMapping("/{meetingId}/preview-data")
public ApiResponse<LegacyMeetingPreviewDataResponse> previewData(HttpServletRequest request, @PathVariable Long meetingId) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "查询会议预览数据接口", "meetingId", meetingId);
androidAuthService.authenticateHttp(request);
LegacyMeetingPreviewResult result = buildPreviewResult(meetingId);
return new ApiResponse<>(result.getCode(), result.getMessage(), result.getData());
}
@Operation(summary = "查询Android会议统一状态")
@ApiResponses({
@ -560,7 +543,7 @@ public class AndroidMeetingController {
private MeetingVO requireOperableOfflineMeeting(Long meetingId, AndroidAuthContext authContext, LoginUser loginUser) {
MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId);
if (meeting == null) {
throw new RuntimeException("会议不存在");
throw new BusinessException(BusinessErrorCodeEnum.MEETING_NOT_FOUND.getCode(), "会议不存在");
}
if (!MeetingConstants.TYPE_OFFLINE.equals(meeting.getMeetingType())) {
throw new RuntimeException("当前会议不是离线会议");
@ -588,107 +571,6 @@ public class AndroidMeetingController {
&& MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED.equalsIgnoreCase(command.getFinishStage());
}
private LegacyMeetingPreviewResult buildPreviewResult(Long meetingId) {
Meeting meeting = meetingService.getById(meetingId);
if (meeting == null) {
return new LegacyMeetingPreviewResult("404", "会议不存在", null);
}
AiTask asrTask = findLatestTask(meetingId, "ASR");
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
boolean summaryCompleted = summaryTask != null && Integer.valueOf(2).equals(summaryTask.getStatus());
MeetingVO detail = (MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED) || summaryCompleted)
? meetingQueryService.getDetail(meetingId)
: null;
boolean hasSummary = detail != null && detail.getSummaryContent() != null && !detail.getSummaryContent().isBlank();
if (hasSummary) {
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask));
}
if (summaryCompleted) {
return new LegacyMeetingPreviewResult(
"504",
"处理已完成,但摘要尚未同步,请稍后重试",
buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可查看详情", 100, STAGE_COMPLETED))
);
}
if (isFailed(asrTask)) {
return new LegacyMeetingPreviewResult(
"503",
buildFailureMessage(asrTask, "转写"),
buildProcessingPreview(meeting, summaryTask, processingStatus("转写或总结失败", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
if (isFailed(summaryTask)) {
return new LegacyMeetingPreviewResult(
"503",
buildFailureMessage(summaryTask, "总结"),
buildProcessingPreview(meeting, summaryTask, processingStatus("转写或总结失败", 75, STAGE_SUMMARY_GENERATION))
);
}
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
if (asrTask != null && Integer.valueOf(0).equals(asrTask.getStatus()) && realtimeProgress != null && realtimeProgress <= 0) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
);
}
if (realtimeProgress != null) {
if (realtimeProgress >= 100) {
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
boolean completedHasSummary = completedDetail != null
&& completedDetail.getSummaryContent() != null
&& !completedDetail.getSummaryContent().isBlank();
if (completedHasSummary) {
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, completedDetail, summaryTask));
}
return new LegacyMeetingPreviewResult(
"504",
"处理已完成,但摘要尚未同步,请稍后重试",
buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可查看详情", 100, STAGE_COMPLETED))
);
}
if (realtimeProgress < 90) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在转写音频", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
if (realtimeProgress >= 90) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION))
);
}
}
boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
if (!isAsrStage && !isSummaryStage) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
);
}
if (!isSummaryStage) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在转写音频", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION))
);
}
private LegacyMeetingPreviewDataResponse buildCompletedPreview(Meeting meeting, MeetingVO detail, AiTask summaryTask) {
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();

View File

@ -33,4 +33,6 @@ public class AndroidDeviceHomeStatsVO {
@Schema(description = "是否已登录")
private Boolean loggedIn;
@Schema(description = "是否开启余额校验")
private Boolean balanceCheckEnabled;
}

View File

@ -0,0 +1,18 @@
package com.imeeting.enums;
import lombok.Getter;
@Getter
public enum BusinessErrorCodeEnum {
MEETING_NOT_FOUND("40001", "会议不存在"),
;
private final String code;
private final String desc;
BusinessErrorCodeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}

View File

@ -28,6 +28,7 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
private static final String HEADER_DEVICE_ID = "X-Android-Device-Id";
private static final String HEADER_APP_ID = "X-Android-App-Id";
private static final String HEADER_TENANT_CODE = "X-Tenant-Code";
private static final String HEADER_APP_VERSION = "X-Android-App-Version";
private static final String HEADER_PLATFORM = "X-Android-Platform";
private static final String HEADER_AUTHORIZATION = "Authorization";

View File

@ -11,12 +11,14 @@ import com.imeeting.dto.android.AndroidDeviceWeatherCacheValue;
import com.imeeting.dto.biz.MeetingPointsBalanceVO;
import com.imeeting.entity.biz.DeviceInfoEntity;
import com.imeeting.entity.biz.LicenseEntity;
import com.imeeting.entity.biz.TenantMeetingPointsSetting;
import com.imeeting.mapper.DeviceInfoMapper;
import com.imeeting.mapper.DeviceLoginLogMapper;
import com.imeeting.mapper.LicenseMapper;
import com.imeeting.mapper.biz.MeetingMapper;
import com.imeeting.service.android.AndroidDeviceHomeService;
import com.imeeting.service.biz.MeetingPointsService;
import com.imeeting.service.biz.TenantMeetingPointsSettingService;
import com.imeeting.support.RedisSupport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -55,6 +57,7 @@ public class AndroidDeviceHomeServiceImpl implements AndroidDeviceHomeService {
private final RedisSupport redisSupport;
private final com.unisbase.service.SysParamService sysParamService;
private final ObjectMapper objectMapper;
private final TenantMeetingPointsSettingService tenantMeetingPointsSettingService;
@Value("${imeeting.h5.base-url:}")
private String h5BaseUrl;
@ -95,6 +98,10 @@ public class AndroidDeviceHomeServiceImpl implements AndroidDeviceHomeService {
vo.setRemainingMinutes(calculateRemainingMinutes(tenantId, authContext.getUserId(), authContext.isAnonymous()));
vo.setWeather(resolveWeather(device.getWeatherCityName()));
vo.setLoggedIn(!authContext.isAnonymous() && authContext.getUserId() != null);
TenantMeetingPointsSetting byTenantId = tenantMeetingPointsSettingService.getByTenantId(authContext.getTenantId());
vo.setBalanceCheckEnabled(byTenantId == null || byTenantId.getBalanceCheckEnabled() == 1);
return vo;
}

View File

@ -97,7 +97,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
Meeting existingMeeting = findLatestBlockingOfflineMeeting(authContext == null ? null : authContext.getDeviceId(), creatorUserId);
if (existingMeeting != null) {
throw new ExistingOfflineMeetingException(existingMeeting.getId());
existingMeeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_PRE_END);
meetingService.updateById(existingMeeting);
}
Long requestedSummaryModelId = request instanceof AndroidOfflineMeetingCreateCommand androidCommand

View File

@ -121,7 +121,10 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ
if (meeting == null || !MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.FAILED)) {
return null;
}
AiTask asrTask = findLatestTask(meeting.getId(), "ASR");
if (isTaskFailed(asrTask)) {
return UnifiedMeetingStatusStage.FAILED_TRANSCRIBING;
}
AiTask summaryTask = findLatestTask(meeting.getId(), "SUMMARY");
if (isTaskFailed(summaryTask)) {
return UnifiedMeetingStatusStage.FAILED_SUMMARIZING;
@ -130,10 +133,7 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ
if (isTaskFailed(chapterTask)) {
return UnifiedMeetingStatusStage.FAILED_SUMMARIZING;
}
AiTask asrTask = findLatestTask(meeting.getId(), "ASR");
if (isTaskFailed(asrTask)) {
return UnifiedMeetingStatusStage.FAILED_TRANSCRIBING;
}
return UnifiedMeetingStatusStage.FAILED_INITIALIZING;
}

View File

@ -12,7 +12,6 @@ const MeetingDetailPage = lazy(() => import("@/pages/meeting-detail"));
const MeetingPreviewPage = lazy(() => import("@/pages/meeting-preview"));
const ProfilePage = lazy(() => import("@/pages/profile"));
const PasswordPage = lazy(() => import("@/pages/password"));
const AboutPage = lazy(() => import("@/pages/about"));
const ScanConfirmPage = lazy(() => import("@/pages/scan-confirm"));
function HomeRedirect() {
@ -67,16 +66,6 @@ export default function App() {
</ProtectedRoute>
}
/>
<Route
path="/about"
element={
<ProtectedRoute>
<MainLayout>
<AboutPage />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/scan-confirm/:sessionId"
element={

View File

@ -1,31 +0,0 @@
import { Card, Typography } from "antd";
import { usePlatformConfig } from "@/components/PlatformConfigProvider";
import usePageTitle from "@/hooks/usePageTitle";
import PageHeader from "@/components/PageHeader";
const { Paragraph, Title } = Typography;
export default function AboutPage() {
const { platformConfig } = usePlatformConfig();
usePageTitle("关于我们");
return (
<div className="page-stack">
<PageHeader title="关于我们" back />
<Card className="surface-card">
<Title level={4}>{platformConfig?.projectName || "iMeeting H5"}</Title>
<Paragraph>
iMeeting H5
</Paragraph>
<Paragraph>
访
</Paragraph>
<Paragraph>
support@imeeting.example.com
</Paragraph>
<Paragraph type="secondary">{platformConfig?.copyrightInfo || "Copyright © iMeeting"}</Paragraph>
</Card>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { App, Avatar, Button, Card, Space, Typography } from "antd";
import {App, Avatar, Button, Card, Modal, Space, Typography} from "antd";
import { InfoCircleOutlined, LockOutlined, LogoutOutlined, RightOutlined } from "@ant-design/icons";
import { useEffect, useState, type ReactNode } from "react";
import { useNavigate } from "react-router-dom";
@ -6,6 +6,7 @@ import { useNavigate } from "react-router-dom";
import { getCurrentUser } from "@/api/user";
import LoadingScreen from "@/components/LoadingScreen";
import PageHeader from "@/components/PageHeader";
import {usePlatformConfig} from "@/components/PlatformConfigProvider";
import usePageTitle from "@/hooks/usePageTitle";
import type { UserProfile } from "@/types";
import { clearAuth, saveProfile } from "@/utils/auth";
@ -38,9 +39,11 @@ function ProfileAction({
export default function ProfilePage() {
const { message } = App.useApp();
const navigate = useNavigate();
const {platformConfig} = usePlatformConfig();
usePageTitle("个人中心");
const [loading, setLoading] = useState(true);
const [profile, setProfile] = useState<UserProfile | null>(null);
const [aboutVisible, setAboutVisible] = useState(false);
useEffect(() => {
const loadProfile = async () => {
@ -95,7 +98,7 @@ export default function ProfilePage() {
<Card className="surface-card">
<Space direction="vertical" size={12} style={{ width: "100%" }}>
<ProfileAction icon={<LockOutlined />} label="个人设置" onClick={() => navigate("/profile/password")} />
<ProfileAction icon={<InfoCircleOutlined />} label="关于我们" onClick={() => navigate("/about")} />
<ProfileAction icon={<InfoCircleOutlined/>} label="关于我们" onClick={() => setAboutVisible(true)}/>
<ProfileAction icon={<LogoutOutlined />} label="退出当前账号" onClick={handleLogout} danger />
</Space>
</Card>
@ -103,6 +106,40 @@ export default function ProfilePage() {
<Paragraph type="secondary" className="profile-footer">
访访
</Paragraph>
<Modal
title="关于我们"
open={aboutVisible}
onCancel={() => setAboutVisible(false)}
footer={null}
centered
width="90%"
styles={{body: {textAlign: "center", padding: "24px 16px"}}}
>
{/*<img src={platformConfig?.logoUrl || "/logo.svg"} alt="logo" style={{ width: 64, height: 64, marginBottom: 16 }} />*/}
<Title level={4} style={{marginBottom: 20}}>
{platformConfig?.projectName || "iMeeting H5"}
</Title>
{/*<Paragraph type="secondary" style={{ marginBottom: 16 }}>*/}
{/* 版本号v0.1.0*/}
{/*</Paragraph>*/}
<Paragraph style={{textAlign: "left"}}>
AI
</Paragraph>
{/*<Paragraph style={{ textAlign: "left" }}>*/}
{/* 第一版页面以快速访问和移动端阅读体验为优先,聚焦“查看”和“确认”两类核心动作,不混入后台管理功能。*/}
{/*</Paragraph>*/}
{/*<Paragraph style={{ textAlign: "left", marginBottom: 24 }}>*/}
{/* 联系邮箱support@imeeting.example.com*/}
{/*</Paragraph>*/}
<Paragraph type="secondary" style={{fontSize: "12px"}}>
{platformConfig?.copyrightInfo || "Copyright © iMeeting"}
</Paragraph>
</Modal>
</div>
);
}