From 2e05a25e63356e15138f1f24105e7c5cb72f9f63 Mon Sep 17 00:00:00 2001 From: chenhao Date: Wed, 10 Jun 2026 20:43:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E6=80=BB=E7=BB=93=E5=92=8C=E7=A7=AF=E5=88=86=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 `MeetingSummaryPromptAssembler` 依赖并更新相关方法 - 优化 `resolveHostName` 方法为 `resolveMeetingUserName`,并调整相关调用 - 更新 `updateMeetingBasic` 方法,支持更新摘要模型 ID 和提示 ID - 在 `retrySummary` 和 `retryChapter` 方法中添加对摘要模型 ID 和提示 ID 的处理 - 优化前端积分管理页面,新增个人账户余额画廊和统计卡片 - 调整积分管理页面的布局和样式,提升用户体验 --- .../android/AndroidMeetingController.java | 7 +- .../controller/biz/MeetingController.java | 1 + .../MeetingPointsManagementController.java | 4 +- .../AndroidOfflineMeetingCreateCommand.java | 29 ++ .../dto/biz/MeetingPointsOverviewVO.java | 12 +- .../java/com/imeeting/dto/biz/MeetingVO.java | 47 ++- .../dto/biz/UpdateMeetingBasicCommand.java | 15 +- .../java/com/imeeting/entity/biz/Meeting.java | 7 +- .../impl/LegacyMeetingAdapterServiceImpl.java | 70 +++- .../biz/MeetingPointsQueryService.java | 2 +- .../biz/impl/MeetingCommandServiceImpl.java | 200 +++++++++-- .../biz/impl/MeetingDomainSupport.java | 61 ++-- .../impl/MeetingPointsQueryServiceImpl.java | 54 ++- .../biz/impl/MeetingQueryServiceImpl.java | 18 +- frontend/src/api/business/meeting.ts | 5 + frontend/src/api/business/meetingPoints.ts | 10 + frontend/src/pages/business/MeetingDetail.tsx | 6 +- .../business/MeetingPointsManagement.tsx | 325 +++++++++++++++--- imeeting-h5/src/types/index.ts | 6 + 19 files changed, 727 insertions(+), 152 deletions(-) create mode 100644 backend/src/main/java/com/imeeting/dto/android/AndroidOfflineMeetingCreateCommand.java diff --git a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java index 9e9fa9c..32e4960 100644 --- a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java @@ -6,6 +6,7 @@ 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; import com.imeeting.dto.android.AndroidMeetingConfigVo; import com.imeeting.dto.android.AndroidMeetingListItemVO; @@ -15,7 +16,6 @@ import com.imeeting.dto.android.AndroidUnifiedMeetingStatusRequest; import com.imeeting.dto.android.AndroidUnifiedMeetingStatusResponse; import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest; import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse; -import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest; import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse; import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult; import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse; @@ -161,8 +161,7 @@ public class AndroidMeetingController { @Anonymous @Log(value = "新增Android会议", type = "Android会议管理") public ApiResponse create(HttpServletRequest request, - - @RequestBody LegacyMeetingCreateRequest command) { + @RequestBody AndroidOfflineMeetingCreateCommand command) { AndroidRequestLogHelper.logRequest(log, "Android会议", "创建离线会议接口", "request", command); AndroidAuthContext authContext = androidAuthService.authenticateHttp(request); resolvePublicDeviceTenantId(request, command, authContext); @@ -478,7 +477,7 @@ public class AndroidMeetingController { } private void resolvePublicDeviceTenantId(HttpServletRequest request, - LegacyMeetingCreateRequest command, + AndroidOfflineMeetingCreateCommand command, AndroidAuthContext authContext) { if (command == null || command.getTenantId() != null || authContext == null || !authContext.isAnonymous()) { return; diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java index cbd4b88..8ebcdaa 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java @@ -569,6 +569,7 @@ public class MeetingController { LoginUser loginUser = currentLoginUser(); Meeting meeting = meetingAccessService.requireMeeting(id); meetingAccessService.assertCanEditMeeting(meeting, loginUser); + assertPromptAvailable(command.getPromptId(), loginUser); command.setMeetingId(id); meetingCommandService.updateMeetingBasic(command); return ApiResponse.ok(true); diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingPointsManagementController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingPointsManagementController.java index 5c5a3c1..cdff31a 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingPointsManagementController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingPointsManagementController.java @@ -33,7 +33,9 @@ public class MeetingPointsManagementController { @GetMapping("/overview") @PreAuthorize("isAuthenticated()") public ApiResponse getOverview() { - return ApiResponse.ok(meetingPointsQueryService.getOverview(currentLoginUser().getTenantId())); + LoginUser loginUser = currentLoginUser(); + boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin()); + return ApiResponse.ok(meetingPointsQueryService.getOverview(loginUser.getTenantId(), loginUser.getUserId(), isAdmin)); } @Operation(summary = "分页查询积分消耗流水") diff --git a/backend/src/main/java/com/imeeting/dto/android/AndroidOfflineMeetingCreateCommand.java b/backend/src/main/java/com/imeeting/dto/android/AndroidOfflineMeetingCreateCommand.java new file mode 100644 index 0000000..1b4a6cd --- /dev/null +++ b/backend/src/main/java/com/imeeting/dto/android/AndroidOfflineMeetingCreateCommand.java @@ -0,0 +1,29 @@ +package com.imeeting.dto.android; + +import com.imeeting.common.MeetingConstants; +import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Android离线会议创建请求") +public class AndroidOfflineMeetingCreateCommand extends LegacyMeetingCreateRequest { + + @Schema(description = "总结模型ID") + private Long summaryModelId; + + @Schema(description = "总结模板ID") + private Long promptId; + + @Schema( + description = "总结详细程度:DETAILED=详细,STANDARD=标准,BRIEF=简洁", + allowableValues = { + MeetingConstants.SUMMARY_DETAIL_DETAILED, + MeetingConstants.SUMMARY_DETAIL_STANDARD, + MeetingConstants.SUMMARY_DETAIL_BRIEF + } + ) + private String summaryDetailLevel; +} diff --git a/backend/src/main/java/com/imeeting/dto/biz/MeetingPointsOverviewVO.java b/backend/src/main/java/com/imeeting/dto/biz/MeetingPointsOverviewVO.java index 69decac..e982d8a 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/MeetingPointsOverviewVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/MeetingPointsOverviewVO.java @@ -3,6 +3,8 @@ package com.imeeting.dto.biz; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.List; + @Data @Schema(description = "积分管理总览视图") public class MeetingPointsOverviewVO { @@ -18,10 +20,10 @@ public class MeetingPointsOverviewVO { @Schema(description = "公共账户累计消耗积分") private Long publicTotalPointsUsed; - @Schema(description = "个人账户余额汇总") + @Schema(description = "个人账户余额") private Long personalBalance; - @Schema(description = "个人账户累计消耗积分汇总") + @Schema(description = "个人账户累计消耗积分") private Long personalTotalPointsUsed; @Schema(description = "当前模式下可用总积分") @@ -29,4 +31,10 @@ public class MeetingPointsOverviewVO { @Schema(description = "累计消耗次数") private Long totalChargeCount; + + @Schema(description = "当前用户是否管理员") + private Boolean admin; + + @Schema(description = "管理员可见的个人账户列表") + private List personalAccounts; } diff --git a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java index 836ecaf..66c89c5 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java @@ -3,6 +3,7 @@ package com.imeeting.dto.biz; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; + import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -12,16 +13,22 @@ import java.util.Map; public class MeetingVO { @Schema(description = "会议 ID") private Long id; + @Schema(description = "租户 ID") private Long tenantId; - @Schema(description = "创建人用户 ID") + + @Schema(description = "创建人用户ID") private Long creatorId; + @Schema(description = "创建人名称") private String creatorName; - @Schema(description = "主持人用户 ID") + + @Schema(description = "主持人用户ID") private Long hostUserId; + @Schema(description = "主持人名称") private String hostName; + @Schema(description = "会议标题") private String title; @@ -29,58 +36,90 @@ public class MeetingVO { @Schema(description = "会议时间") private LocalDateTime meetingTime; - @Schema(description = "参会人 ID 串,逗号分隔") + @Schema(description = "参会人ID串,逗号分隔") private String participants; - @Schema(description = "参会人 ID 列表") + + @Schema(description = "参会人ID列表") private List participantIds; + @Schema(description = "标签串") private String tags; + @Schema(description = "音频地址") private String audioUrl; + @Schema(description = "浏览器播放音频地址") private String playbackAudioUrl; + @Schema(description = "会议类型") private String meetingType; + @Schema(description = "会议来源") private String meetingSource; + @Schema(description = "来源设备编码") private String sourceDeviceCode; + @Schema(description = "来源设备模式") private String sourceDeviceMode; + @Schema(description = "离线录音阶段:ACTIVE / PRE_END / UPLOAD_FINISHED") private String offlineRecordingStatus; + @Schema(description = "总结详细程度") private String summaryDetailLevel; + + @Schema(description = "总结模型ID") + private Long summaryModelId; + + @Schema(description = "总结模板ID") + private Long promptId; + @Schema(description = "音频保存状态") private String audioSaveStatus; + @Schema(description = "音频保存说明") private String audioSaveMessage; + @Schema(description = "访问密码") private String accessPassword; + @Schema(description = "音频时长,单位秒") private Integer duration; + @Schema(description = "会议最终有效录音时长,单位秒") private Integer effectiveAudioDurationSeconds; + @Schema(description = "会议摘要内容") private String summaryContent; + @Schema(description = "最后一次用户补充提示词") private String lastUserPrompt; + @Schema(description = "分析结果") private Map analysis; + @Schema(description = "最近一次总结尝试任务 ID") private Long latestSummaryAttemptTaskId; + @Schema(description = "最近一次总结尝试任务状态") private Integer latestSummaryAttemptStatus; + @Schema(description = "最近一次总结尝试错误信息") private String latestSummaryAttemptErrorMsg; + @Schema(description = "最近一次总结尝试阻塞原因") private String latestSummaryAttemptBlockedReason; + @Schema(description = "最近一次章节尝试任务 ID") private Long latestChapterAttemptTaskId; + @Schema(description = "最近一次章节尝试任务状态") private Integer latestChapterAttemptStatus; + @Schema(description = "最近一次章节尝试错误信息") private String latestChapterAttemptErrorMsg; + @Schema(description = "会议状态") private Integer status; diff --git a/backend/src/main/java/com/imeeting/dto/biz/UpdateMeetingBasicCommand.java b/backend/src/main/java/com/imeeting/dto/biz/UpdateMeetingBasicCommand.java index a61a4c9..6808239 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/UpdateMeetingBasicCommand.java +++ b/backend/src/main/java/com/imeeting/dto/biz/UpdateMeetingBasicCommand.java @@ -1,6 +1,8 @@ package com.imeeting.dto.biz; import com.fasterxml.jackson.annotation.JsonFormat; +import com.imeeting.common.MeetingConstants; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @@ -14,6 +16,17 @@ public class UpdateMeetingBasicCommand { private LocalDateTime meetingTime; private String tags; - private String accessPassword; + private Long promptId; + private Long summaryModelId; + + @Schema( + description = "总结详细程度:DETAILED=详细,STANDARD=标准,BRIEF=简洁", + allowableValues = { + MeetingConstants.SUMMARY_DETAIL_DETAILED, + MeetingConstants.SUMMARY_DETAIL_STANDARD, + MeetingConstants.SUMMARY_DETAIL_BRIEF + } + ) + private String summaryDetailLevel; } diff --git a/backend/src/main/java/com/imeeting/entity/biz/Meeting.java b/backend/src/main/java/com/imeeting/entity/biz/Meeting.java index 5a91332..03dfc41 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/Meeting.java +++ b/backend/src/main/java/com/imeeting/entity/biz/Meeting.java @@ -53,6 +53,12 @@ public class Meeting extends BaseEntity { @Schema(description = "总结详细程度") private String summaryDetailLevel; + @Schema(description = "总结模型ID") + private Long summaryModelId; + + @Schema(description = "总结模板ID") + private Long promptId; + @Schema(description = "会议最终有效录音时长(秒)") private Integer effectiveAudioDurationSeconds; @@ -63,7 +69,6 @@ public class Meeting extends BaseEntity { private String audioSaveMessage; @Schema(description = "访问密码") - private String accessPassword; @Schema(description = "创建人ID") diff --git a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java index af6d07f..a4fd43b 100644 --- a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.imeeting.common.MeetingConstants; import com.imeeting.common.exception.ExistingOfflineMeetingException; import com.imeeting.dto.android.AndroidAuthContext; +import com.imeeting.dto.android.AndroidOfflineMeetingCreateCommand; import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest; import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse; import com.imeeting.dto.biz.MeetingVO; @@ -99,6 +100,31 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ throw new ExistingOfflineMeetingException(existingMeeting.getId()); } + Long requestedSummaryModelId = request instanceof AndroidOfflineMeetingCreateCommand androidCommand + ? androidCommand.getSummaryModelId() + : null; + Long requestedPromptId = request instanceof AndroidOfflineMeetingCreateCommand androidCommand + ? androidCommand.getPromptId() + : null; + String requestedSummaryDetailLevel = request instanceof AndroidOfflineMeetingCreateCommand androidCommand + ? androidCommand.getSummaryDetailLevel() + : MeetingConstants.SUMMARY_DETAIL_STANDARD; + RealtimeMeetingRuntimeProfile runtimeProfile = runtimeProfileResolver.resolve( + tenantId, + null, + requestedSummaryModelId, + requestedPromptId, + null, + null, + null, + null, + null, + null, + null, + null, + List.of() + ); + String resolvedCreatorName = meetingDomainSupport.resolveUserDisplayName(creatorUserId, creatorName); Meeting meeting = meetingDomainSupport.initMeeting( request.getTitle().trim(), meetingTime, @@ -109,10 +135,12 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ MeetingConstants.SOURCE_ANDROID, tenantId, creatorUserId, - creatorName, + resolvedCreatorName, creatorUserId, - creatorName, - MeetingConstants.SUMMARY_DETAIL_STANDARD, + resolvedCreatorName, + runtimeProfile.getResolvedSummaryModelId(), + runtimeProfile.getResolvedPromptId(), + requestedSummaryDetailLevel, 0, authContext == null ? null : authContext.getDeviceId(), sourceDeviceMode @@ -152,8 +180,12 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ if (transcriptCount > 0) { throw new RuntimeException("当前会议已存在转录内容,旧版 Android 上传不支持替换已生成的转录"); } - if (promptId != null && !promptTemplateService.isTemplateEnabledForUser( - promptId, + Long effectivePromptId = promptId != null ? promptId : meeting.getPromptId(); + Long effectiveSummaryModelId = modelCode != null && !modelCode.isBlank() + ? resolveSummaryModelId(modelCode, loginUser.getTenantId()) + : meeting.getSummaryModelId(); + if (effectivePromptId != null && !promptTemplateService.isTemplateEnabledForUser( + effectivePromptId, loginUser.getTenantId(), loginUser.getUserId(), loginUser.getIsPlatformAdmin(), @@ -164,8 +196,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve( loginUser.getTenantId(), null, - resolveSummaryModelId(modelCode, loginUser.getTenantId()), - promptId, + effectiveSummaryModelId, + effectivePromptId, null, null, null, @@ -181,6 +213,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl); taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> { meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl); + meeting.setSummaryModelId(profile.getResolvedSummaryModelId()); + meeting.setPromptId(profile.getResolvedPromptId()); meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS); meeting.setAudioSaveMessage(null); meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED); @@ -188,8 +222,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ meetingService.updateById(meeting); resetOrCreateAsrTask(meetingId, profile); - resetOrCreateChapterTask(meetingId, profile); - resetOrCreateSummaryTask(meetingId, profile); + resetOrCreateChapterTask(meetingId, profile, null, meeting.getSummaryDetailLevel()); + resetOrCreateSummaryTask(meetingId, profile, null, meeting.getSummaryDetailLevel()); dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); }); return new LegacyUploadAudioResponse(meetingId, relocatedUrl); @@ -223,8 +257,12 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ throw new RuntimeException("当前会议已存在转录内容,不支持替换已生成的转录"); } LoginUser loginUser = toMeetingOwnerLoginUser(meeting); - if (promptId != null && !promptTemplateService.isTemplateEnabledForUser( - promptId, + Long effectivePromptId = promptId != null ? promptId : meeting.getPromptId(); + Long effectiveSummaryModelId = modelCode != null && !modelCode.isBlank() + ? resolveSummaryModelId(modelCode, meeting.getTenantId()) + : meeting.getSummaryModelId(); + if (effectivePromptId != null && !promptTemplateService.isTemplateEnabledForUser( + effectivePromptId, loginUser.getTenantId(), loginUser.getUserId(), Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()), @@ -234,8 +272,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve( meeting.getTenantId(), null, - resolveSummaryModelId(modelCode, meeting.getTenantId()), - promptId, + effectiveSummaryModelId, + effectivePromptId, null, null, null, @@ -250,6 +288,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl); taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> { meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl); + meeting.setSummaryModelId(profile.getResolvedSummaryModelId()); + meeting.setPromptId(profile.getResolvedPromptId()); meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS); meeting.setAudioSaveMessage(null); meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED); @@ -257,8 +297,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ meetingService.updateById(meeting); resetOrCreateAsrTask(meetingId, profile); - resetOrCreateChapterTask(meetingId, profile); - resetOrCreateSummaryTask(meetingId, profile); + resetOrCreateChapterTask(meetingId, profile, null, meeting.getSummaryDetailLevel()); + resetOrCreateSummaryTask(meetingId, profile, null, meeting.getSummaryDetailLevel()); dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); }); return new LegacyUploadAudioResponse(meetingId, relocatedUrl); diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingPointsQueryService.java b/backend/src/main/java/com/imeeting/service/biz/MeetingPointsQueryService.java index 1279791..4847c65 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingPointsQueryService.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingPointsQueryService.java @@ -8,7 +8,7 @@ import com.unisbase.dto.PageResult; import java.util.List; public interface MeetingPointsQueryService { - MeetingPointsOverviewVO getOverview(Long tenantId); + MeetingPointsOverviewVO getOverview(Long tenantId, Long userId, boolean isAdmin); PageResult> pageLedgers(Long tenantId, Integer current, 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 5d94732..16e3b99 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 @@ -80,6 +80,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService; private final MeetingProgressService meetingProgressService; private final MeetingPointsService meetingPointsService; + private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler; private final ObjectMapper objectMapper; private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger; private final AndroidPushMessageService androidPushMessageService; @@ -105,6 +106,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService, MeetingProgressService meetingProgressService, MeetingPointsService meetingPointsService, + MeetingSummaryPromptAssembler meetingSummaryPromptAssembler, ObjectMapper objectMapper, MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger, AndroidPushMessageService androidPushMessageService, @@ -125,6 +127,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { this.realtimeMeetingAudioStorageService = realtimeMeetingAudioStorageService; this.meetingProgressService = meetingProgressService; this.meetingPointsService = meetingPointsService; + this.meetingSummaryPromptAssembler = meetingSummaryPromptAssembler; this.objectMapper = objectMapper; this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger; this.androidPushMessageService = androidPushMessageService; @@ -138,10 +141,12 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { public MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource) { RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId); Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); - String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId); + String resolvedCreatorName = resolveMeetingUserName(creatorId, creatorName); + String hostName = resolveMeetingUserName(hostUserId, resolvedCreatorName); String summaryDetailLevel = resolveSummaryDetailLevel(command.getSummaryDetailLevel()); Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(), - command.getAudioUrl(), MeetingConstants.TYPE_OFFLINE, meetingSource, tenantId, creatorId, creatorName, hostUserId, hostName, summaryDetailLevel, 0); + command.getAudioUrl(), MeetingConstants.TYPE_OFFLINE, meetingSource, tenantId, creatorId, resolvedCreatorName, + hostUserId, hostName, runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId(), summaryDetailLevel, 0); meetingService.save(meeting); AiTask asrTask = new AiTask(); @@ -215,10 +220,12 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { public MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource) { RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId); Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); - String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId); + String resolvedCreatorName = resolveMeetingUserName(creatorId, creatorName); + String hostName = resolveMeetingUserName(hostUserId, resolvedCreatorName); String summaryDetailLevel = resolveSummaryDetailLevel(command.getSummaryDetailLevel()); Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(), - null, MeetingConstants.TYPE_REALTIME, meetingSource, tenantId, creatorId, creatorName, hostUserId, hostName, summaryDetailLevel, 0); + null, MeetingConstants.TYPE_REALTIME, meetingSource, tenantId, creatorId, resolvedCreatorName, + hostUserId, hostName, runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId(), summaryDetailLevel, 0); meetingService.save(meeting); Long chapterModelId = command.getChapterModelId() != null ? command.getChapterModelId() : runtimeProfile.getResolvedSummaryModelId(); meetingDomainSupport.createChapterTask( @@ -278,7 +285,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { command.getHotWords() ); Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); - String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId); + String resolvedCreatorName = resolveMeetingUserName(creatorId, creatorName); + String hostName = resolveMeetingUserName(hostUserId, resolvedCreatorName); String summaryDetailLevel = resolveSummaryDetailLevel(command.getSummaryDetailLevel()); Meeting meeting = meetingDomainSupport.initMeeting( command.getTitle(), @@ -290,9 +298,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { MeetingConstants.SOURCE_ANDROID, tenantId, creatorId, - creatorName, + resolvedCreatorName, hostUserId, hostName, + runtimeProfile.getResolvedSummaryModelId(), + runtimeProfile.getResolvedPromptId(), summaryDetailLevel, 0, deviceCode, @@ -691,12 +701,36 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { @Override @Transactional(rollbackFor = Exception.class) public void updateMeetingBasic(UpdateMeetingBasicCommand command) { + if (command.getSummaryModelId() != null || command.getPromptId() != null) { + Meeting meeting = meetingService.getById(command.getMeetingId()); + if (meeting == null) { + throw new RuntimeException("会议不存在"); + } + meetingRuntimeProfileResolver.resolve( + meeting.getTenantId(), + null, + command.getSummaryModelId() != null ? command.getSummaryModelId() : meeting.getSummaryModelId(), + command.getPromptId() != null ? command.getPromptId() : meeting.getPromptId(), + null, + null, + null, + null, + null, + null, + null, + null, + List.of() + ); + } meetingService.update(new LambdaUpdateWrapper() .eq(Meeting::getId, command.getMeetingId()) .set(command.getTitle() != null, Meeting::getTitle, command.getTitle()) .set(command.getMeetingTime() != null, Meeting::getMeetingTime, command.getMeetingTime()) .set(command.getTags() != null, Meeting::getTags, command.getTags()) - .set(command.getAccessPassword() != null, Meeting::getAccessPassword, normalizeAccessPassword(command.getAccessPassword()))); + .set(command.getAccessPassword() != null, Meeting::getAccessPassword, normalizeAccessPassword(command.getAccessPassword())) + .set(command.getSummaryModelId() != null, Meeting::getSummaryModelId, command.getSummaryModelId()) + .set(command.getPromptId() != null, Meeting::getPromptId, command.getPromptId()) + .set(command.getSummaryDetailLevel() != null, Meeting::getSummaryDetailLevel, resolveSummaryDetailLevel(command.getSummaryDetailLevel()))); } private String normalizeAccessPassword(String accessPassword) { @@ -997,15 +1031,33 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { throw new RuntimeException("会议不存在"); } - String effectiveSummaryDetailLevel = resolveSummaryDetailLevel(summaryDetailLevel != null ? summaryDetailLevel : meeting.getSummaryDetailLevel()); + AiTask latestSummaryTask = aiTaskService.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meetingId) + .eq(AiTask::getTaskType, "SUMMARY") + .orderByDesc(AiTask::getId) + .last("LIMIT 1")); + Long effectiveSummaryModelId = summaryModelId != null ? summaryModelId : resolveMeetingSummaryModelId(meeting, latestSummaryTask); + Long effectivePromptId = promptId != null ? promptId : resolveMeetingPromptId(meeting, latestSummaryTask); + Long effectiveChapterModelId = chapterModelId != null ? chapterModelId : resolveMeetingChapterModelId(meeting, latestSummaryTask, effectiveSummaryModelId); + String effectiveSummaryDetailLevel = resolveSummaryDetailLevel(summaryDetailLevel != null ? summaryDetailLevel : resolveMeetingSummaryDetailLevel(meeting, latestSummaryTask)); + String effectiveUserPrompt = userPrompt != null ? userPrompt : resolveMeetingUserPrompt(latestSummaryTask); + if (effectiveSummaryModelId == null) { + throw new RuntimeException("缺少 summaryModelId,无法创建总结任务"); + } + if (effectivePromptId == null) { + throw new RuntimeException("缺少 promptId,无法创建总结任务"); + } meetingDomainSupport.createSummaryTask( meetingId, - summaryModelId, - promptId, - userPrompt, + effectiveSummaryModelId, + effectiveChapterModelId, + effectivePromptId, + effectiveUserPrompt, effectiveSummaryDetailLevel, "RESUMMARY" ); + meeting.setSummaryModelId(effectiveSummaryModelId); + meeting.setPromptId(effectivePromptId); meeting.setSummaryDetailLevel(effectiveSummaryDetailLevel); meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode()); meetingService.updateById(meeting); @@ -1057,31 +1109,48 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { resetAiTask(asrTask, new HashMap<>(asrTask.getTaskConfig())); aiTaskService.updateById(asrTask); + Long effectiveSummaryModelId = resolveMeetingSummaryModelId(meeting, summaryTask); + Long effectivePromptId = resolveMeetingPromptId(meeting, summaryTask); + Long effectiveChapterModelId = resolveMeetingChapterModelId(meeting, summaryTask, effectiveSummaryModelId); + String effectiveUserPrompt = resolveMeetingUserPrompt(summaryTask); + String effectiveSummaryDetailLevel = resolveMeetingSummaryDetailLevel(meeting, summaryTask); AiTask chapterTask = aiTaskService.getOne(new LambdaQueryWrapper() .eq(AiTask::getMeetingId, meetingId) .eq(AiTask::getTaskType, "CHAPTER") .orderByDesc(AiTask::getId) .last("LIMIT 1")); if (chapterTask == null) { - Long summaryModelId = longValue(summaryTask.getTaskConfig().get("summaryModelId")); - Long chapterModelId = longValue(summaryTask.getTaskConfig().get("chapterModelId")); - Long promptId = longValue(summaryTask.getTaskConfig().get("promptId")); - String userPrompt = stringValue(summaryTask.getTaskConfig().get("userPrompt")); chapterTask = meetingDomainSupport.createChapterTask( meetingId, - summaryModelId, - chapterModelId != null ? chapterModelId : summaryModelId, - promptId, - userPrompt, - meeting.getSummaryDetailLevel() + effectiveSummaryModelId, + effectiveChapterModelId, + effectivePromptId, + effectiveUserPrompt, + effectiveSummaryDetailLevel ); } else { - resetAiTask(chapterTask, new HashMap<>(chapterTask.getTaskConfig())); + resetAiTask(chapterTask, meetingSummaryPromptAssembler.buildTaskConfig( + effectiveSummaryModelId, + effectiveChapterModelId, + effectivePromptId, + effectiveUserPrompt, + effectiveSummaryDetailLevel + )); aiTaskService.updateById(chapterTask); } - resetAiTask(summaryTask, new HashMap<>(summaryTask.getTaskConfig())); + resetAiTask(summaryTask, buildSummaryTaskConfigForRetry( + summaryTask, + effectiveSummaryModelId, + effectiveChapterModelId, + effectivePromptId, + effectiveUserPrompt, + effectiveSummaryDetailLevel + )); aiTaskService.updateById(summaryTask); + meeting.setSummaryModelId(effectiveSummaryModelId); + meeting.setPromptId(effectivePromptId); + meeting.setSummaryDetailLevel(effectiveSummaryDetailLevel); meeting.setStatus(MeetingStatusEnum.TRANSCRIBING.getCode()); meetingService.updateById(meeting); clearLegacyDispatchState(meetingId); @@ -1115,8 +1184,19 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { if (!Integer.valueOf(3).equals(summaryTask.getStatus())) { throw new RuntimeException("当前总结环节未失败,无需重试"); } - resetAiTask(summaryTask, new HashMap<>(summaryTask.getTaskConfig())); + Long effectiveSummaryModelId = resolveMeetingSummaryModelId(meeting, summaryTask); + resetAiTask(summaryTask, buildSummaryTaskConfigForRetry( + summaryTask, + effectiveSummaryModelId, + resolveMeetingChapterModelId(meeting, summaryTask, effectiveSummaryModelId), + resolveMeetingPromptId(meeting, summaryTask), + resolveMeetingUserPrompt(summaryTask), + resolveMeetingSummaryDetailLevel(meeting, summaryTask) + )); aiTaskService.updateById(summaryTask); + meeting.setSummaryModelId(effectiveSummaryModelId); + meeting.setPromptId(resolveMeetingPromptId(meeting, summaryTask)); + meeting.setSummaryDetailLevel(resolveMeetingSummaryDetailLevel(meeting, summaryTask)); meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode()); meetingService.updateById(meeting); updateMeetingProgress(meetingId, 90, "已重新提交总结任务,正在生成总结...", 0); @@ -1149,8 +1229,18 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { if (!Integer.valueOf(3).equals(chapterTask.getStatus())) { throw new RuntimeException("当前 AI 目录环节未失败,无需重试"); } - resetAiTask(chapterTask, new HashMap<>(chapterTask.getTaskConfig())); + Long effectiveSummaryModelId = resolveMeetingSummaryModelId(meeting, chapterTask); + resetAiTask(chapterTask, meetingSummaryPromptAssembler.buildTaskConfig( + effectiveSummaryModelId, + resolveMeetingChapterModelId(meeting, chapterTask, effectiveSummaryModelId), + resolveMeetingPromptId(meeting, chapterTask), + resolveMeetingUserPrompt(chapterTask), + resolveMeetingSummaryDetailLevel(meeting, chapterTask) + )); aiTaskService.updateById(chapterTask); + meeting.setSummaryModelId(effectiveSummaryModelId); + meeting.setPromptId(resolveMeetingPromptId(meeting, chapterTask)); + meeting.setSummaryDetailLevel(resolveMeetingSummaryDetailLevel(meeting, chapterTask)); meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode()); meetingService.updateById(meeting); updateMeetingProgress(meetingId, 85, "已重新提交 AI 目录任务,正在生成目录...", 0); @@ -1222,6 +1312,58 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { return value == null ? null : String.valueOf(value); } + private Long resolveMeetingSummaryModelId(Meeting meeting, AiTask task) { + if (meeting != null && meeting.getSummaryModelId() != null) { + return meeting.getSummaryModelId(); + } + return task == null || task.getTaskConfig() == null ? null : longValue(task.getTaskConfig().get("summaryModelId")); + } + + private Long resolveMeetingChapterModelId(Meeting meeting, AiTask task, Long fallbackSummaryModelId) { + Long chapterModelId = task == null || task.getTaskConfig() == null ? null : longValue(task.getTaskConfig().get("chapterModelId")); + return chapterModelId != null ? chapterModelId : fallbackSummaryModelId; + } + + private Long resolveMeetingPromptId(Meeting meeting, AiTask task) { + if (meeting != null && meeting.getPromptId() != null) { + return meeting.getPromptId(); + } + return task == null || task.getTaskConfig() == null ? null : longValue(task.getTaskConfig().get("promptId")); + } + + private String resolveMeetingSummaryDetailLevel(Meeting meeting, AiTask task) { + if (meeting != null && meeting.getSummaryDetailLevel() != null && !meeting.getSummaryDetailLevel().isBlank()) { + return resolveSummaryDetailLevel(meeting.getSummaryDetailLevel()); + } + return resolveSummaryDetailLevel(task == null || task.getTaskConfig() == null ? null : stringValue(task.getTaskConfig().get("summaryDetailLevel"))); + } + + private String resolveMeetingUserPrompt(AiTask task) { + return task == null || task.getTaskConfig() == null ? null : stringValue(task.getTaskConfig().get("userPrompt")); + } + + private Map buildSummaryTaskConfigForRetry(AiTask summaryTask, + Long summaryModelId, + Long chapterModelId, + Long promptId, + String userPrompt, + String summaryDetailLevel) { + Map taskConfig = meetingSummaryPromptAssembler.buildTaskConfig( + summaryModelId, + chapterModelId, + promptId, + userPrompt, + summaryDetailLevel + ); + String chargeTriggerType = summaryTask == null || summaryTask.getTaskConfig() == null + ? null + : stringValue(summaryTask.getTaskConfig().get("chargeTriggerType")); + taskConfig.put("chargeTriggerType", chargeTriggerType == null || chargeTriggerType.isBlank() + ? "AUTO_SUMMARY" + : chargeTriggerType.trim().toUpperCase()); + return taskConfig; + } + private AiTask resolveOwnedTask(Long taskId, Long meetingId, String expectedType) { if (taskId == null) { return null; @@ -1424,14 +1566,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { return requestedHostUserId != null ? requestedHostUserId : creatorId; } - private String resolveHostName(String requestedHostName, String creatorName, Long creatorId, Long hostUserId) { - if (requestedHostName != null && !requestedHostName.isBlank()) { - return requestedHostName.trim(); - } - if (hostUserId != null && Objects.equals(hostUserId, creatorId)) { - return creatorName; - } - return null; + private String resolveMeetingUserName(Long userId, String fallbackName) { + return meetingDomainSupport.resolveUserDisplayName(userId, fallbackName); } private String resolveSummaryDetailLevel(String requestedSummaryDetailLevel) { diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java index 1b31682..9e5c6a0 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java @@ -64,16 +64,20 @@ public class MeetingDomainSupport { public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags, String audioUrl, String meetingType, String meetingSource, Long tenantId, Long creatorId, String creatorName, - Long hostUserId, String hostName, String summaryDetailLevel, int status) { + Long hostUserId, String hostName, + Long summaryModelId, Long promptId, + String summaryDetailLevel, int status) { return initMeeting(title, meetingTime, participants, tags, audioUrl, meetingType, meetingSource, - tenantId, creatorId, creatorName, hostUserId, hostName, summaryDetailLevel, status, + tenantId, creatorId, creatorName, hostUserId, hostName, summaryModelId, promptId, summaryDetailLevel, status, null, null); } public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags, String audioUrl, String meetingType, String meetingSource, Long tenantId, Long creatorId, String creatorName, - Long hostUserId, String hostName, String summaryDetailLevel, int status, + Long hostUserId, String hostName, + Long summaryModelId, Long promptId, + String summaryDetailLevel, int status, String sourceDeviceCode, String sourceDeviceMode) { Meeting meeting = new Meeting(); meeting.setTitle(title); @@ -90,6 +94,8 @@ public class MeetingDomainSupport { meeting.setAudioUrl(audioUrl); meeting.setSourceDeviceCode(sourceDeviceCode); meeting.setSourceDeviceMode(sourceDeviceMode); + meeting.setSummaryModelId(summaryModelId); + meeting.setPromptId(promptId); meeting.setSummaryDetailLevel(normalizeSummaryDetailLevel(summaryDetailLevel)); meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_NONE); if (MeetingConstants.TYPE_OFFLINE.equalsIgnoreCase(meetingType)) { @@ -99,16 +105,21 @@ public class MeetingDomainSupport { return meeting; } - public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long promptId, String userPrompt) { - return createSummaryTask( - meetingId, - summaryModelId, - summaryModelId, - promptId, - userPrompt, - MeetingConstants.SUMMARY_DETAIL_STANDARD, - "AUTO_SUMMARY" - ); + public String resolveUserDisplayName(Long userId, String fallbackName) { + if (userId == null) { + return fallbackName; + } + SysUser user = sysUserMapper.selectById(userId); + if (user == null) { + return fallbackName; + } + if (user.getDisplayName() != null && !user.getDisplayName().isBlank()) { + return user.getDisplayName().trim(); + } + if (user.getUsername() != null && !user.getUsername().isBlank()) { + return user.getUsername().trim(); + } + return fallbackName; } public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long promptId, @@ -137,16 +148,6 @@ public class MeetingDomainSupport { ); } - public AiTask createChapterTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) { - return createChapterTask( - meetingId, - summaryModelId, - chapterModelId, - promptId, - userPrompt, - MeetingConstants.SUMMARY_DETAIL_STANDARD - ); - } public AiTask createChapterTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt, String summaryDetailLevel) { @@ -166,17 +167,7 @@ public class MeetingDomainSupport { return chapterTask; } - public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) { - return createSummaryTask( - meetingId, - summaryModelId, - chapterModelId, - promptId, - userPrompt, - MeetingConstants.SUMMARY_DETAIL_STANDARD, - "AUTO_SUMMARY" - ); - } + public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt, String summaryDetailLevel) { @@ -406,6 +397,8 @@ public class MeetingDomainSupport { vo.setSourceDeviceCode(meeting.getSourceDeviceCode()); vo.setSourceDeviceMode(meeting.getSourceDeviceMode()); vo.setOfflineRecordingStatus(meeting.getOfflineRecordingStatus()); + vo.setSummaryModelId(meeting.getSummaryModelId()); + vo.setPromptId(meeting.getPromptId()); vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel())); vo.setAudioSaveStatus(meeting.getAudioSaveStatus()); vo.setAudioSaveMessage(meeting.getAudioSaveMessage()); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingPointsQueryServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingPointsQueryServiceImpl.java index d89e035..e4131b6 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingPointsQueryServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingPointsQueryServiceImpl.java @@ -7,6 +7,7 @@ import com.imeeting.dto.biz.MeetingPointsChargeItemVO; import com.imeeting.dto.biz.MeetingPointsLedgerDetailVO; import com.imeeting.dto.biz.MeetingPointsLedgerListItemVO; import com.imeeting.dto.biz.MeetingPointsOverviewVO; +import com.imeeting.dto.biz.MeetingPointsPersonalAccountVO; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.MeetingPointsAccount; import com.imeeting.entity.biz.MeetingPointsLedger; @@ -71,21 +72,27 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService } @Override - public MeetingPointsOverviewVO getOverview(Long tenantId) { + public MeetingPointsOverviewVO getOverview(Long tenantId, Long userId, boolean isAdmin) { String accountMode = resolveAccountMode(); String chargePriority = resolveChargePriority(); MeetingPointsAccount publicAccount = findAccount(tenantId, PUBLIC_ACCOUNT_USER_ID); long publicBalance = publicAccount == null ? 0L : defaultLong(publicAccount.getCurrentBalance()); long publicTotalUsed = publicAccount == null ? 0L : defaultLong(publicAccount.getTotalPointsUsed()); - long personalBalance = 0L; - long personalTotalUsed = 0L; List personalAccounts = meetingPointsAccountService.list(new LambdaQueryWrapper() .eq(MeetingPointsAccount::getTenantId, tenantId) .ne(MeetingPointsAccount::getUserId, PUBLIC_ACCOUNT_USER_ID)); - for (MeetingPointsAccount account : personalAccounts) { - personalBalance += defaultLong(account.getCurrentBalance()); - personalTotalUsed += defaultLong(account.getTotalPointsUsed()); + long personalBalance = 0L; + long personalTotalUsed = 0L; + if (isAdmin) { + for (MeetingPointsAccount account : personalAccounts) { + personalBalance += defaultLong(account.getCurrentBalance()); + personalTotalUsed += defaultLong(account.getTotalPointsUsed()); + } + } else { + MeetingPointsAccount currentUserAccount = findAccount(tenantId, userId); + personalBalance = currentUserAccount == null ? 0L : defaultLong(currentUserAccount.getCurrentBalance()); + personalTotalUsed = currentUserAccount == null ? 0L : defaultLong(currentUserAccount.getTotalPointsUsed()); } List scopedOwnerUserIds = resolveScopedOwnerUserIds(tenantId); @@ -110,6 +117,8 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService vo.setPersonalTotalPointsUsed(personalTotalUsed); vo.setTotalAvailableBalance(resolveVisibleTotalBalance(accountMode, publicBalance, personalBalance)); vo.setTotalChargeCount(totalChargeCount); + vo.setAdmin(isAdmin); + vo.setPersonalAccounts(buildPersonalAccountOverview(accountMode, isAdmin, personalAccounts)); return vo; } @@ -422,6 +431,39 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService return publicBalance + personalBalance; } + private List buildPersonalAccountOverview(String accountMode, + boolean isAdmin, + List personalAccounts) { + if (!isAdmin || (!ACCOUNT_MODE_PERSONAL.equals(accountMode) && !ACCOUNT_MODE_BOTH.equals(accountMode))) { + return Collections.emptyList(); + } + if (personalAccounts == null || personalAccounts.isEmpty()) { + return Collections.emptyList(); + } + List userIds = personalAccounts.stream() + .map(MeetingPointsAccount::getUserId) + .filter(Objects::nonNull) + .distinct() + .toList(); + Map userMap = sysUserMapper.selectBatchIds(userIds).stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(SysUser::getUserId, user -> user, (left, right) -> left, HashMap::new)); + + return personalAccounts.stream() + .sorted((left, right) -> Long.compare(defaultLong(right.getCurrentBalance()), defaultLong(left.getCurrentBalance()))) + .map(account -> { + SysUser user = userMap.get(account.getUserId()); + MeetingPointsPersonalAccountVO item = new MeetingPointsPersonalAccountVO(); + item.setUserId(account.getUserId()); + item.setUsername(user == null ? null : user.getUsername()); + item.setDisplayName(resolveOwnerName(user, account.getUserId())); + item.setCurrentBalance(defaultLong(account.getCurrentBalance())); + item.setTotalPointsUsed(defaultLong(account.getTotalPointsUsed())); + return item; + }) + .toList(); + } + private long defaultLong(Long value) { return value == null ? 0L : value; } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java index f4cdcc2..f263c0e 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java @@ -146,9 +146,21 @@ public class MeetingQueryServiceImpl implements MeetingQueryService { throw new RuntimeException("缺少可用的总结任务配置"); } - Long summaryModelId = firstLong(request == null ? null : request.getSummaryModelId(), latestSummaryTask.getTaskConfig().get("summaryModelId")); - Long chapterModelId = firstLong(request == null ? null : request.getChapterModelId(), latestSummaryTask.getTaskConfig().get("chapterModelId"), summaryModelId); - Long promptId = firstLong(request == null ? null : request.getPromptId(), latestSummaryTask.getTaskConfig().get("promptId")); + Long summaryModelId = firstLong( + request == null ? null : request.getSummaryModelId(), + meeting.getSummaryModelId(), + latestSummaryTask.getTaskConfig().get("summaryModelId") + ); + Long chapterModelId = firstLong( + request == null ? null : request.getChapterModelId(), + latestSummaryTask.getTaskConfig().get("chapterModelId"), + summaryModelId + ); + Long promptId = firstLong( + request == null ? null : request.getPromptId(), + meeting.getPromptId(), + latestSummaryTask.getTaskConfig().get("promptId") + ); String userPrompt = request != null && request.getUserPrompt() != null ? request.getUserPrompt() : stringValue(latestSummaryTask.getTaskConfig().get("userPrompt")); diff --git a/frontend/src/api/business/meeting.ts b/frontend/src/api/business/meeting.ts index 6251dc6..537dbd7 100644 --- a/frontend/src/api/business/meeting.ts +++ b/frontend/src/api/business/meeting.ts @@ -33,6 +33,8 @@ export interface MeetingVO { sourceDeviceCode?: string; sourceDeviceMode?: "PUBLIC" | "PRIVATE"; summaryDetailLevel?: SummaryDetailLevel; + summaryModelId: number; + promptId?: number; audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED"; audioSaveMessage?: string; accessPassword?: string; @@ -150,6 +152,9 @@ export interface UpdateMeetingBasicCommand { meetingTime?: string; tags?: string; accessPassword?: string | null; + summaryModelId: number; + promptId?: number; + summaryDetailLevel?: SummaryDetailLevel; } export type MeetingUpdateBasicDTO = UpdateMeetingBasicCommand; diff --git a/frontend/src/api/business/meetingPoints.ts b/frontend/src/api/business/meetingPoints.ts index 4f5bb1b..c735060 100644 --- a/frontend/src/api/business/meetingPoints.ts +++ b/frontend/src/api/business/meetingPoints.ts @@ -9,6 +9,16 @@ export interface MeetingPointsOverviewVO { personalTotalPointsUsed: number; totalAvailableBalance: number; totalChargeCount: number; + admin?: boolean; + personalAccounts?: MeetingPointsPersonalAccountVO[]; +} + +export interface MeetingPointsPersonalAccountVO { + userId: number; + username?: string; + displayName?: string; + currentBalance: number; + totalPointsUsed: number; } export interface MeetingPointsChargeItemVO { diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index 4848a9b..edcd925 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -1799,9 +1799,13 @@ const MeetingDetail: React.FC = () => { summaryForm.setFieldsValue({ summaryModelId: summaryForm.getFieldValue('summaryModelId') ?? + meeting?.summaryModelId ?? llmModels.find((model) => model.isDefault === 1)?.id ?? llmModels[0]?.id, - promptId: summaryForm.getFieldValue('promptId') ?? prompts[0]?.id, + promptId: + summaryForm.getFieldValue('promptId') ?? + meeting?.promptId ?? + prompts[0]?.id, userPrompt: meeting?.lastUserPrompt ?? '', summaryDetailLevel: summaryForm.getFieldValue('summaryDetailLevel') ?? diff --git a/frontend/src/pages/business/MeetingPointsManagement.tsx b/frontend/src/pages/business/MeetingPointsManagement.tsx index 10d735d..f6e5d62 100644 --- a/frontend/src/pages/business/MeetingPointsManagement.tsx +++ b/frontend/src/pages/business/MeetingPointsManagement.tsx @@ -1,10 +1,11 @@ -import { EyeOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons"; +import { EyeOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons"; import { listUsers } from "@/api"; import { Button, Card, Col, Descriptions, + Empty, Form, Input, InputNumber, @@ -31,10 +32,11 @@ import { type MeetingPointsLedgerDetailVO, type MeetingPointsLedgerListItemVO, type MeetingPointsOverviewVO, + type MeetingPointsPersonalAccountVO, } from "@/api/business/meetingPoints"; import type { SysUser } from "@/types"; -const { Text } = Typography; +const { Text, Title } = Typography; const POINTS_TYPE_OPTIONS = [ { label: "全部类型", value: "" }, @@ -42,9 +44,13 @@ const POINTS_TYPE_OPTIONS = [ { label: "总结", value: "LLM" }, ]; +const ACCOUNT_MODE_PUBLIC = "PUBLIC"; +const ACCOUNT_MODE_PERSONAL = "PERSONAL"; +const ACCOUNT_MODE_BOTH = "BOTH"; + function getAccountModeLabel(mode?: string) { - if (mode === "PERSONAL") return "个人账户"; - if (mode === "BOTH") return "公共和个人共存"; + if (mode === ACCOUNT_MODE_PERSONAL) return "个人账户"; + if (mode === ACCOUNT_MODE_BOTH) return "公共 + 个人"; return "公共账户"; } @@ -83,11 +89,79 @@ function formatDateTime(value?: string) { return value ? value.replace("T", " ").substring(0, 19) : "-"; } +function buildSummaryCards(overview: MeetingPointsOverviewVO | null) { + if (!overview) { + return []; + } + const isAdmin = Boolean(overview.admin); + const isPublicOnly = overview.accountMode === ACCOUNT_MODE_PUBLIC; + const isPersonalOnly = overview.accountMode === ACCOUNT_MODE_PERSONAL; + const showPublicSummary = !isPersonalOnly || isAdmin; + const showPersonalSummary = !isPublicOnly; + + const cards: Array<{ + key: string; + title: string; + value: number; + accent: string; + note: string; + }> = []; + + if (showPublicSummary) { + cards.push( + { + key: "public-balance", + title: "公共账户余额", + value: overview.publicBalance ?? 0, + accent: "linear-gradient(135deg, rgba(18, 87, 241, 0.14), rgba(18, 87, 241, 0.02))", + note: "用于统一分配和公共扣费", + }, + { + key: "public-used", + title: "公共账户累计消耗积分", + value: overview.publicTotalPointsUsed ?? 0, + accent: "linear-gradient(135deg, rgba(11, 132, 98, 0.14), rgba(11, 132, 98, 0.02))", + note: "公共账户历史累计扣减", + }, + ); + } + + if (showPersonalSummary) { + cards.push( + { + key: "personal-balance", + title: isAdmin ? "个人账户余额汇总" : "个人账户余额", + value: overview.personalBalance ?? 0, + accent: "linear-gradient(135deg, rgba(148, 77, 255, 0.14), rgba(148, 77, 255, 0.02))", + note: isAdmin ? "管理员视角下的个人账户汇总" : "当前账号可用积分", + }, + { + key: "personal-used", + title: isAdmin ? "个人账户累计消耗积分汇总" : "个人账户累计消耗积分", + value: overview.personalTotalPointsUsed ?? 0, + accent: "linear-gradient(135deg, rgba(237, 108, 2, 0.14), rgba(237, 108, 2, 0.02))", + note: isAdmin ? "管理员视角下的个人账户消耗汇总" : "当前账号历史累计扣减", + }, + ); + } + + cards.push({ + key: "charge-count", + title: "累计消耗次数", + value: overview.totalChargeCount ?? 0, + accent: "linear-gradient(135deg, rgba(48, 48, 48, 0.12), rgba(48, 48, 48, 0.02))", + note: "已发生扣费的总结记录数", + }); + + return cards; +} + export default function MeetingPointsManagement() { const [overview, setOverview] = useState(null); const [loading, setLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false); const [transferLoading, setTransferLoading] = useState(false); + const [usersLoading, setUsersLoading] = useState(false); const [records, setRecords] = useState([]); const [total, setTotal] = useState(0); const [detailOpen, setDetailOpen] = useState(false); @@ -102,14 +176,26 @@ export default function MeetingPointsManagement() { }); const [transferForm] = Form.useForm(); + const isAdmin = Boolean(overview?.admin); + const isPublicOnly = overview?.accountMode === ACCOUNT_MODE_PUBLIC; + const isPersonalOnly = overview?.accountMode === ACCOUNT_MODE_PERSONAL; + const showTransferButton = isAdmin && !isPublicOnly; + const showPersonalGallery = isAdmin && !isPublicOnly; + const summaryCards = buildSummaryCards(overview); + const loadOverview = async () => { const data = await getMeetingPointsOverview(); setOverview(data); }; const loadUsers = async () => { - const data = await listUsers(); - setUsers(data || []); + setUsersLoading(true); + try { + const data = await listUsers(); + setUsers(data || []); + } finally { + setUsersLoading(false); + } }; const loadPage = async (nextParams = params) => { @@ -124,7 +210,7 @@ export default function MeetingPointsManagement() { }; useEffect(() => { - void Promise.all([loadOverview(), loadPage(), loadUsers()]); + void Promise.all([loadOverview(), loadPage()]); }, []); const handleSearch = () => { @@ -145,7 +231,7 @@ export default function MeetingPointsManagement() { }; const handleRefresh = async () => { - await Promise.all([loadOverview(), loadPage(), loadUsers()]); + await Promise.all([loadOverview(), loadPage()]); message.success("已刷新积分数据"); }; @@ -160,6 +246,13 @@ export default function MeetingPointsManagement() { } }; + const handleOpenTransfer = async () => { + setTransferOpen(true); + if (!users.length) { + await loadUsers(); + } + }; + const handleTransferSubmit = async () => { const values = await transferForm.validateFields(); setTransferLoading(true); @@ -281,14 +374,16 @@ export default function MeetingPointsManagement() { return ( 当前模式:{getAccountModeLabel(overview?.accountMode)} 优先级:{getChargePriorityLabel(overview?.chargePriority)} - + {showTransferButton ? ( + + ) : null} @@ -317,41 +412,176 @@ export default function MeetingPointsManagement() { } > - - - - - - - - - - - - - - - - - - - - - - + + +
+
+ + POINTS OPERATIONS BOARD + + + {isPublicOnly + ? "当前为公共账户结算视图" + : isPersonalOnly + ? "当前为个人账户结算视图" + : "当前为公共与个人混合结算视图"} + +
+
+ 统计口径 +
+ {isAdmin ? "管理员视角" : "当前用户视角"} +
+
+
- - - - - - - - - - - - + + {summaryCards.map((item) => ( + + + + {item.title} + + + {item.note} + + + + + ))} + +
+
+ + {showPersonalGallery ? ( + + +
+
+ + 个人账户余额画廊 + + + 仅管理员可见,按当前余额从高到低展示全部个人账户。 + +
+ {overview?.personalAccounts?.length ?? 0} 个账户 +
+ + {overview?.personalAccounts?.length ? ( + + {overview.personalAccounts.map((account: MeetingPointsPersonalAccountVO, index) => ( + + + +
+ +
+ +
+
+ + {account.displayName || account.username || `用户 #${account.userId}`} + + + {account.username ? `@${account.username}` : `ID ${account.userId}`} + +
+
+ {index < 3 ? TOP {index + 1} : null} +
+ +
+ 当前余额 +
+ {account.currentBalance ?? 0} +
+
+ +
+
+ + 累计消耗 + +
{account.totalPointsUsed ?? 0}
+
+
+ + 账户类型 + +
个人
+
+
+
+
+ + ))} +
+ ) : ( + + )} +
+
+ ) : null} @@ -447,6 +677,7 @@ export default function MeetingPointsManagement() {