feat: 增强会议总结和积分管理功能

- 添加 `MeetingSummaryPromptAssembler` 依赖并更新相关方法
- 优化 `resolveHostName` 方法为 `resolveMeetingUserName`,并调整相关调用
- 更新 `updateMeetingBasic` 方法,支持更新摘要模型 ID 和提示 ID
- 在 `retrySummary` 和 `retryChapter` 方法中添加对摘要模型 ID 和提示 ID 的处理
- 优化前端积分管理页面,新增个人账户余额画廊和统计卡片
- 调整积分管理页面的布局和样式,提升用户体验
dev_na
chenhao 2026-06-10 20:43:35 +08:00
parent 178b920581
commit 2e05a25e63
19 changed files with 727 additions and 152 deletions

View File

@ -6,6 +6,7 @@ import com.imeeting.common.MeetingConstants;
import com.imeeting.common.SysParamKeys; import com.imeeting.common.SysParamKeys;
import com.imeeting.common.exception.ExistingOfflineMeetingException; import com.imeeting.common.exception.ExistingOfflineMeetingException;
import com.imeeting.dto.android.AndroidAuthContext; import com.imeeting.dto.android.AndroidAuthContext;
import com.imeeting.dto.android.AndroidOfflineMeetingCreateCommand;
import com.imeeting.dto.android.AndroidMeetingCreateResponse; import com.imeeting.dto.android.AndroidMeetingCreateResponse;
import com.imeeting.dto.android.AndroidMeetingConfigVo; import com.imeeting.dto.android.AndroidMeetingConfigVo;
import com.imeeting.dto.android.AndroidMeetingListItemVO; 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.AndroidUnifiedMeetingStatusResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest; import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest;
import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse; 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.LegacyMeetingPreviewDataResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult; import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult;
import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse; import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse;
@ -161,8 +161,7 @@ public class AndroidMeetingController {
@Anonymous @Anonymous
@Log(value = "新增Android会议", type = "Android会议管理") @Log(value = "新增Android会议", type = "Android会议管理")
public ApiResponse<Object> create(HttpServletRequest request, public ApiResponse<Object> create(HttpServletRequest request,
@RequestBody AndroidOfflineMeetingCreateCommand command) {
@RequestBody LegacyMeetingCreateRequest command) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "创建离线会议接口", "request", command); AndroidRequestLogHelper.logRequest(log, "Android会议", "创建离线会议接口", "request", command);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request); AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
resolvePublicDeviceTenantId(request, command, authContext); resolvePublicDeviceTenantId(request, command, authContext);
@ -478,7 +477,7 @@ public class AndroidMeetingController {
} }
private void resolvePublicDeviceTenantId(HttpServletRequest request, private void resolvePublicDeviceTenantId(HttpServletRequest request,
LegacyMeetingCreateRequest command, AndroidOfflineMeetingCreateCommand command,
AndroidAuthContext authContext) { AndroidAuthContext authContext) {
if (command == null || command.getTenantId() != null || authContext == null || !authContext.isAnonymous()) { if (command == null || command.getTenantId() != null || authContext == null || !authContext.isAnonymous()) {
return; return;

View File

@ -569,6 +569,7 @@ public class MeetingController {
LoginUser loginUser = currentLoginUser(); LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(id); Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanEditMeeting(meeting, loginUser); meetingAccessService.assertCanEditMeeting(meeting, loginUser);
assertPromptAvailable(command.getPromptId(), loginUser);
command.setMeetingId(id); command.setMeetingId(id);
meetingCommandService.updateMeetingBasic(command); meetingCommandService.updateMeetingBasic(command);
return ApiResponse.ok(true); return ApiResponse.ok(true);

View File

@ -33,7 +33,9 @@ public class MeetingPointsManagementController {
@GetMapping("/overview") @GetMapping("/overview")
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
public ApiResponse<MeetingPointsOverviewVO> getOverview() { public ApiResponse<MeetingPointsOverviewVO> 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 = "分页查询积分消耗流水") @Operation(summary = "分页查询积分消耗流水")

View File

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

View File

@ -3,6 +3,8 @@ package com.imeeting.dto.biz;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.util.List;
@Data @Data
@Schema(description = "积分管理总览视图") @Schema(description = "积分管理总览视图")
public class MeetingPointsOverviewVO { public class MeetingPointsOverviewVO {
@ -18,10 +20,10 @@ public class MeetingPointsOverviewVO {
@Schema(description = "公共账户累计消耗积分") @Schema(description = "公共账户累计消耗积分")
private Long publicTotalPointsUsed; private Long publicTotalPointsUsed;
@Schema(description = "个人账户余额汇总") @Schema(description = "个人账户余额")
private Long personalBalance; private Long personalBalance;
@Schema(description = "个人账户累计消耗积分汇总") @Schema(description = "个人账户累计消耗积分")
private Long personalTotalPointsUsed; private Long personalTotalPointsUsed;
@Schema(description = "当前模式下可用总积分") @Schema(description = "当前模式下可用总积分")
@ -29,4 +31,10 @@ public class MeetingPointsOverviewVO {
@Schema(description = "累计消耗次数") @Schema(description = "累计消耗次数")
private Long totalChargeCount; private Long totalChargeCount;
@Schema(description = "当前用户是否管理员")
private Boolean admin;
@Schema(description = "管理员可见的个人账户列表")
private List<MeetingPointsPersonalAccountVO> personalAccounts;
} }

View File

@ -3,6 +3,7 @@ package com.imeeting.dto.biz;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -12,16 +13,22 @@ import java.util.Map;
public class MeetingVO { public class MeetingVO {
@Schema(description = "会议 ID") @Schema(description = "会议 ID")
private Long id; private Long id;
@Schema(description = "租户 ID") @Schema(description = "租户 ID")
private Long tenantId; private Long tenantId;
@Schema(description = "创建人用户 ID")
@Schema(description = "创建人用户ID")
private Long creatorId; private Long creatorId;
@Schema(description = "创建人名称") @Schema(description = "创建人名称")
private String creatorName; private String creatorName;
@Schema(description = "主持人用户 ID")
@Schema(description = "主持人用户ID")
private Long hostUserId; private Long hostUserId;
@Schema(description = "主持人名称") @Schema(description = "主持人名称")
private String hostName; private String hostName;
@Schema(description = "会议标题") @Schema(description = "会议标题")
private String title; private String title;
@ -29,58 +36,90 @@ public class MeetingVO {
@Schema(description = "会议时间") @Schema(description = "会议时间")
private LocalDateTime meetingTime; private LocalDateTime meetingTime;
@Schema(description = "参会人 ID 串,逗号分隔") @Schema(description = "参会人ID串逗号分隔")
private String participants; private String participants;
@Schema(description = "参会人 ID 列表")
@Schema(description = "参会人ID列表")
private List<Long> participantIds; private List<Long> participantIds;
@Schema(description = "标签串") @Schema(description = "标签串")
private String tags; private String tags;
@Schema(description = "音频地址") @Schema(description = "音频地址")
private String audioUrl; private String audioUrl;
@Schema(description = "浏览器播放音频地址") @Schema(description = "浏览器播放音频地址")
private String playbackAudioUrl; private String playbackAudioUrl;
@Schema(description = "会议类型") @Schema(description = "会议类型")
private String meetingType; private String meetingType;
@Schema(description = "会议来源") @Schema(description = "会议来源")
private String meetingSource; private String meetingSource;
@Schema(description = "来源设备编码") @Schema(description = "来源设备编码")
private String sourceDeviceCode; private String sourceDeviceCode;
@Schema(description = "来源设备模式") @Schema(description = "来源设备模式")
private String sourceDeviceMode; private String sourceDeviceMode;
@Schema(description = "离线录音阶段ACTIVE / PRE_END / UPLOAD_FINISHED") @Schema(description = "离线录音阶段ACTIVE / PRE_END / UPLOAD_FINISHED")
private String offlineRecordingStatus; private String offlineRecordingStatus;
@Schema(description = "总结详细程度") @Schema(description = "总结详细程度")
private String summaryDetailLevel; private String summaryDetailLevel;
@Schema(description = "总结模型ID")
private Long summaryModelId;
@Schema(description = "总结模板ID")
private Long promptId;
@Schema(description = "音频保存状态") @Schema(description = "音频保存状态")
private String audioSaveStatus; private String audioSaveStatus;
@Schema(description = "音频保存说明") @Schema(description = "音频保存说明")
private String audioSaveMessage; private String audioSaveMessage;
@Schema(description = "访问密码") @Schema(description = "访问密码")
private String accessPassword; private String accessPassword;
@Schema(description = "音频时长,单位秒") @Schema(description = "音频时长,单位秒")
private Integer duration; private Integer duration;
@Schema(description = "会议最终有效录音时长,单位秒") @Schema(description = "会议最终有效录音时长,单位秒")
private Integer effectiveAudioDurationSeconds; private Integer effectiveAudioDurationSeconds;
@Schema(description = "会议摘要内容") @Schema(description = "会议摘要内容")
private String summaryContent; private String summaryContent;
@Schema(description = "最后一次用户补充提示词") @Schema(description = "最后一次用户补充提示词")
private String lastUserPrompt; private String lastUserPrompt;
@Schema(description = "分析结果") @Schema(description = "分析结果")
private Map<String, Object> analysis; private Map<String, Object> analysis;
@Schema(description = "最近一次总结尝试任务 ID") @Schema(description = "最近一次总结尝试任务 ID")
private Long latestSummaryAttemptTaskId; private Long latestSummaryAttemptTaskId;
@Schema(description = "最近一次总结尝试任务状态") @Schema(description = "最近一次总结尝试任务状态")
private Integer latestSummaryAttemptStatus; private Integer latestSummaryAttemptStatus;
@Schema(description = "最近一次总结尝试错误信息") @Schema(description = "最近一次总结尝试错误信息")
private String latestSummaryAttemptErrorMsg; private String latestSummaryAttemptErrorMsg;
@Schema(description = "最近一次总结尝试阻塞原因") @Schema(description = "最近一次总结尝试阻塞原因")
private String latestSummaryAttemptBlockedReason; private String latestSummaryAttemptBlockedReason;
@Schema(description = "最近一次章节尝试任务 ID") @Schema(description = "最近一次章节尝试任务 ID")
private Long latestChapterAttemptTaskId; private Long latestChapterAttemptTaskId;
@Schema(description = "最近一次章节尝试任务状态") @Schema(description = "最近一次章节尝试任务状态")
private Integer latestChapterAttemptStatus; private Integer latestChapterAttemptStatus;
@Schema(description = "最近一次章节尝试错误信息") @Schema(description = "最近一次章节尝试错误信息")
private String latestChapterAttemptErrorMsg; private String latestChapterAttemptErrorMsg;
@Schema(description = "会议状态") @Schema(description = "会议状态")
private Integer status; private Integer status;

View File

@ -1,6 +1,8 @@
package com.imeeting.dto.biz; package com.imeeting.dto.biz;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.imeeting.common.MeetingConstants;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -14,6 +16,17 @@ public class UpdateMeetingBasicCommand {
private LocalDateTime meetingTime; private LocalDateTime meetingTime;
private String tags; private String tags;
private String accessPassword; 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;
} }

View File

@ -53,6 +53,12 @@ public class Meeting extends BaseEntity {
@Schema(description = "总结详细程度") @Schema(description = "总结详细程度")
private String summaryDetailLevel; private String summaryDetailLevel;
@Schema(description = "总结模型ID")
private Long summaryModelId;
@Schema(description = "总结模板ID")
private Long promptId;
@Schema(description = "会议最终有效录音时长(秒)") @Schema(description = "会议最终有效录音时长(秒)")
private Integer effectiveAudioDurationSeconds; private Integer effectiveAudioDurationSeconds;
@ -63,7 +69,6 @@ public class Meeting extends BaseEntity {
private String audioSaveMessage; private String audioSaveMessage;
@Schema(description = "访问密码") @Schema(description = "访问密码")
private String accessPassword; private String accessPassword;
@Schema(description = "创建人ID") @Schema(description = "创建人ID")

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.imeeting.common.MeetingConstants; import com.imeeting.common.MeetingConstants;
import com.imeeting.common.exception.ExistingOfflineMeetingException; import com.imeeting.common.exception.ExistingOfflineMeetingException;
import com.imeeting.dto.android.AndroidAuthContext; 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.LegacyMeetingCreateRequest;
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse; import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.MeetingVO;
@ -99,6 +100,31 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
throw new ExistingOfflineMeetingException(existingMeeting.getId()); 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( Meeting meeting = meetingDomainSupport.initMeeting(
request.getTitle().trim(), request.getTitle().trim(),
meetingTime, meetingTime,
@ -109,10 +135,12 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
MeetingConstants.SOURCE_ANDROID, MeetingConstants.SOURCE_ANDROID,
tenantId, tenantId,
creatorUserId, creatorUserId,
creatorName, resolvedCreatorName,
creatorUserId, creatorUserId,
creatorName, resolvedCreatorName,
MeetingConstants.SUMMARY_DETAIL_STANDARD, runtimeProfile.getResolvedSummaryModelId(),
runtimeProfile.getResolvedPromptId(),
requestedSummaryDetailLevel,
0, 0,
authContext == null ? null : authContext.getDeviceId(), authContext == null ? null : authContext.getDeviceId(),
sourceDeviceMode sourceDeviceMode
@ -152,8 +180,12 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
if (transcriptCount > 0) { if (transcriptCount > 0) {
throw new RuntimeException("当前会议已存在转录内容,旧版 Android 上传不支持替换已生成的转录"); throw new RuntimeException("当前会议已存在转录内容,旧版 Android 上传不支持替换已生成的转录");
} }
if (promptId != null && !promptTemplateService.isTemplateEnabledForUser( Long effectivePromptId = promptId != null ? promptId : meeting.getPromptId();
promptId, Long effectiveSummaryModelId = modelCode != null && !modelCode.isBlank()
? resolveSummaryModelId(modelCode, loginUser.getTenantId())
: meeting.getSummaryModelId();
if (effectivePromptId != null && !promptTemplateService.isTemplateEnabledForUser(
effectivePromptId,
loginUser.getTenantId(), loginUser.getTenantId(),
loginUser.getUserId(), loginUser.getUserId(),
loginUser.getIsPlatformAdmin(), loginUser.getIsPlatformAdmin(),
@ -164,8 +196,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve( RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve(
loginUser.getTenantId(), loginUser.getTenantId(),
null, null,
resolveSummaryModelId(modelCode, loginUser.getTenantId()), effectiveSummaryModelId,
promptId, effectivePromptId,
null, null,
null, null,
null, null,
@ -181,6 +213,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl); String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl);
taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> { taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> {
meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl); meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl);
meeting.setSummaryModelId(profile.getResolvedSummaryModelId());
meeting.setPromptId(profile.getResolvedPromptId());
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS); meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS);
meeting.setAudioSaveMessage(null); meeting.setAudioSaveMessage(null);
meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED); meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED);
@ -188,8 +222,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
meetingService.updateById(meeting); meetingService.updateById(meeting);
resetOrCreateAsrTask(meetingId, profile); resetOrCreateAsrTask(meetingId, profile);
resetOrCreateChapterTask(meetingId, profile); resetOrCreateChapterTask(meetingId, profile, null, meeting.getSummaryDetailLevel());
resetOrCreateSummaryTask(meetingId, profile); resetOrCreateSummaryTask(meetingId, profile, null, meeting.getSummaryDetailLevel());
dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
}); });
return new LegacyUploadAudioResponse(meetingId, relocatedUrl); return new LegacyUploadAudioResponse(meetingId, relocatedUrl);
@ -223,8 +257,12 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
throw new RuntimeException("当前会议已存在转录内容,不支持替换已生成的转录"); throw new RuntimeException("当前会议已存在转录内容,不支持替换已生成的转录");
} }
LoginUser loginUser = toMeetingOwnerLoginUser(meeting); LoginUser loginUser = toMeetingOwnerLoginUser(meeting);
if (promptId != null && !promptTemplateService.isTemplateEnabledForUser( Long effectivePromptId = promptId != null ? promptId : meeting.getPromptId();
promptId, Long effectiveSummaryModelId = modelCode != null && !modelCode.isBlank()
? resolveSummaryModelId(modelCode, meeting.getTenantId())
: meeting.getSummaryModelId();
if (effectivePromptId != null && !promptTemplateService.isTemplateEnabledForUser(
effectivePromptId,
loginUser.getTenantId(), loginUser.getTenantId(),
loginUser.getUserId(), loginUser.getUserId(),
Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()), Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()),
@ -234,8 +272,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve( RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve(
meeting.getTenantId(), meeting.getTenantId(),
null, null,
resolveSummaryModelId(modelCode, meeting.getTenantId()), effectiveSummaryModelId,
promptId, effectivePromptId,
null, null,
null, null,
null, null,
@ -250,6 +288,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl); String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl);
taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> { taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> {
meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl); meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl);
meeting.setSummaryModelId(profile.getResolvedSummaryModelId());
meeting.setPromptId(profile.getResolvedPromptId());
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS); meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS);
meeting.setAudioSaveMessage(null); meeting.setAudioSaveMessage(null);
meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED); meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED);
@ -257,8 +297,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
meetingService.updateById(meeting); meetingService.updateById(meeting);
resetOrCreateAsrTask(meetingId, profile); resetOrCreateAsrTask(meetingId, profile);
resetOrCreateChapterTask(meetingId, profile); resetOrCreateChapterTask(meetingId, profile, null, meeting.getSummaryDetailLevel());
resetOrCreateSummaryTask(meetingId, profile); resetOrCreateSummaryTask(meetingId, profile, null, meeting.getSummaryDetailLevel());
dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
}); });
return new LegacyUploadAudioResponse(meetingId, relocatedUrl); return new LegacyUploadAudioResponse(meetingId, relocatedUrl);

View File

@ -8,7 +8,7 @@ import com.unisbase.dto.PageResult;
import java.util.List; import java.util.List;
public interface MeetingPointsQueryService { public interface MeetingPointsQueryService {
MeetingPointsOverviewVO getOverview(Long tenantId); MeetingPointsOverviewVO getOverview(Long tenantId, Long userId, boolean isAdmin);
PageResult<List<MeetingPointsLedgerListItemVO>> pageLedgers(Long tenantId, PageResult<List<MeetingPointsLedgerListItemVO>> pageLedgers(Long tenantId,
Integer current, Integer current,

View File

@ -80,6 +80,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService; private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService;
private final MeetingProgressService meetingProgressService; private final MeetingProgressService meetingProgressService;
private final MeetingPointsService meetingPointsService; private final MeetingPointsService meetingPointsService;
private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger; private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
private final AndroidPushMessageService androidPushMessageService; private final AndroidPushMessageService androidPushMessageService;
@ -105,6 +106,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService, RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService,
MeetingProgressService meetingProgressService, MeetingProgressService meetingProgressService,
MeetingPointsService meetingPointsService, MeetingPointsService meetingPointsService,
MeetingSummaryPromptAssembler meetingSummaryPromptAssembler,
ObjectMapper objectMapper, ObjectMapper objectMapper,
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger, MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
AndroidPushMessageService androidPushMessageService, AndroidPushMessageService androidPushMessageService,
@ -125,6 +127,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
this.realtimeMeetingAudioStorageService = realtimeMeetingAudioStorageService; this.realtimeMeetingAudioStorageService = realtimeMeetingAudioStorageService;
this.meetingProgressService = meetingProgressService; this.meetingProgressService = meetingProgressService;
this.meetingPointsService = meetingPointsService; this.meetingPointsService = meetingPointsService;
this.meetingSummaryPromptAssembler = meetingSummaryPromptAssembler;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger; this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
this.androidPushMessageService = androidPushMessageService; 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) { public MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource) {
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId); RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); 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()); String summaryDetailLevel = resolveSummaryDetailLevel(command.getSummaryDetailLevel());
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(), 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); meetingService.save(meeting);
AiTask asrTask = new AiTask(); 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) { public MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource) {
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId); RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); 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()); String summaryDetailLevel = resolveSummaryDetailLevel(command.getSummaryDetailLevel());
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(), 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); meetingService.save(meeting);
Long chapterModelId = command.getChapterModelId() != null ? command.getChapterModelId() : runtimeProfile.getResolvedSummaryModelId(); Long chapterModelId = command.getChapterModelId() != null ? command.getChapterModelId() : runtimeProfile.getResolvedSummaryModelId();
meetingDomainSupport.createChapterTask( meetingDomainSupport.createChapterTask(
@ -278,7 +285,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
command.getHotWords() command.getHotWords()
); );
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); 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()); String summaryDetailLevel = resolveSummaryDetailLevel(command.getSummaryDetailLevel());
Meeting meeting = meetingDomainSupport.initMeeting( Meeting meeting = meetingDomainSupport.initMeeting(
command.getTitle(), command.getTitle(),
@ -290,9 +298,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
MeetingConstants.SOURCE_ANDROID, MeetingConstants.SOURCE_ANDROID,
tenantId, tenantId,
creatorId, creatorId,
creatorName, resolvedCreatorName,
hostUserId, hostUserId,
hostName, hostName,
runtimeProfile.getResolvedSummaryModelId(),
runtimeProfile.getResolvedPromptId(),
summaryDetailLevel, summaryDetailLevel,
0, 0,
deviceCode, deviceCode,
@ -691,12 +701,36 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void updateMeetingBasic(UpdateMeetingBasicCommand command) { 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<Meeting>() meetingService.update(new LambdaUpdateWrapper<Meeting>()
.eq(Meeting::getId, command.getMeetingId()) .eq(Meeting::getId, command.getMeetingId())
.set(command.getTitle() != null, Meeting::getTitle, command.getTitle()) .set(command.getTitle() != null, Meeting::getTitle, command.getTitle())
.set(command.getMeetingTime() != null, Meeting::getMeetingTime, command.getMeetingTime()) .set(command.getMeetingTime() != null, Meeting::getMeetingTime, command.getMeetingTime())
.set(command.getTags() != null, Meeting::getTags, command.getTags()) .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) { private String normalizeAccessPassword(String accessPassword) {
@ -997,15 +1031,33 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
throw new RuntimeException("会议不存在"); throw new RuntimeException("会议不存在");
} }
String effectiveSummaryDetailLevel = resolveSummaryDetailLevel(summaryDetailLevel != null ? summaryDetailLevel : meeting.getSummaryDetailLevel()); AiTask latestSummaryTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
.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( meetingDomainSupport.createSummaryTask(
meetingId, meetingId,
summaryModelId, effectiveSummaryModelId,
promptId, effectiveChapterModelId,
userPrompt, effectivePromptId,
effectiveUserPrompt,
effectiveSummaryDetailLevel, effectiveSummaryDetailLevel,
"RESUMMARY" "RESUMMARY"
); );
meeting.setSummaryModelId(effectiveSummaryModelId);
meeting.setPromptId(effectivePromptId);
meeting.setSummaryDetailLevel(effectiveSummaryDetailLevel); meeting.setSummaryDetailLevel(effectiveSummaryDetailLevel);
meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode()); meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode());
meetingService.updateById(meeting); meetingService.updateById(meeting);
@ -1057,31 +1109,48 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
resetAiTask(asrTask, new HashMap<>(asrTask.getTaskConfig())); resetAiTask(asrTask, new HashMap<>(asrTask.getTaskConfig()));
aiTaskService.updateById(asrTask); 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<AiTask>() AiTask chapterTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
.eq(AiTask::getMeetingId, meetingId) .eq(AiTask::getMeetingId, meetingId)
.eq(AiTask::getTaskType, "CHAPTER") .eq(AiTask::getTaskType, "CHAPTER")
.orderByDesc(AiTask::getId) .orderByDesc(AiTask::getId)
.last("LIMIT 1")); .last("LIMIT 1"));
if (chapterTask == null) { 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( chapterTask = meetingDomainSupport.createChapterTask(
meetingId, meetingId,
summaryModelId, effectiveSummaryModelId,
chapterModelId != null ? chapterModelId : summaryModelId, effectiveChapterModelId,
promptId, effectivePromptId,
userPrompt, effectiveUserPrompt,
meeting.getSummaryDetailLevel() effectiveSummaryDetailLevel
); );
} else { } else {
resetAiTask(chapterTask, new HashMap<>(chapterTask.getTaskConfig())); resetAiTask(chapterTask, meetingSummaryPromptAssembler.buildTaskConfig(
effectiveSummaryModelId,
effectiveChapterModelId,
effectivePromptId,
effectiveUserPrompt,
effectiveSummaryDetailLevel
));
aiTaskService.updateById(chapterTask); aiTaskService.updateById(chapterTask);
} }
resetAiTask(summaryTask, new HashMap<>(summaryTask.getTaskConfig())); resetAiTask(summaryTask, buildSummaryTaskConfigForRetry(
summaryTask,
effectiveSummaryModelId,
effectiveChapterModelId,
effectivePromptId,
effectiveUserPrompt,
effectiveSummaryDetailLevel
));
aiTaskService.updateById(summaryTask); aiTaskService.updateById(summaryTask);
meeting.setSummaryModelId(effectiveSummaryModelId);
meeting.setPromptId(effectivePromptId);
meeting.setSummaryDetailLevel(effectiveSummaryDetailLevel);
meeting.setStatus(MeetingStatusEnum.TRANSCRIBING.getCode()); meeting.setStatus(MeetingStatusEnum.TRANSCRIBING.getCode());
meetingService.updateById(meeting); meetingService.updateById(meeting);
clearLegacyDispatchState(meetingId); clearLegacyDispatchState(meetingId);
@ -1115,8 +1184,19 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
if (!Integer.valueOf(3).equals(summaryTask.getStatus())) { if (!Integer.valueOf(3).equals(summaryTask.getStatus())) {
throw new RuntimeException("当前总结环节未失败,无需重试"); 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); aiTaskService.updateById(summaryTask);
meeting.setSummaryModelId(effectiveSummaryModelId);
meeting.setPromptId(resolveMeetingPromptId(meeting, summaryTask));
meeting.setSummaryDetailLevel(resolveMeetingSummaryDetailLevel(meeting, summaryTask));
meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode()); meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode());
meetingService.updateById(meeting); meetingService.updateById(meeting);
updateMeetingProgress(meetingId, 90, "已重新提交总结任务,正在生成总结...", 0); updateMeetingProgress(meetingId, 90, "已重新提交总结任务,正在生成总结...", 0);
@ -1149,8 +1229,18 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
if (!Integer.valueOf(3).equals(chapterTask.getStatus())) { if (!Integer.valueOf(3).equals(chapterTask.getStatus())) {
throw new RuntimeException("当前 AI 目录环节未失败,无需重试"); 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); aiTaskService.updateById(chapterTask);
meeting.setSummaryModelId(effectiveSummaryModelId);
meeting.setPromptId(resolveMeetingPromptId(meeting, chapterTask));
meeting.setSummaryDetailLevel(resolveMeetingSummaryDetailLevel(meeting, chapterTask));
meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode()); meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode());
meetingService.updateById(meeting); meetingService.updateById(meeting);
updateMeetingProgress(meetingId, 85, "已重新提交 AI 目录任务,正在生成目录...", 0); updateMeetingProgress(meetingId, 85, "已重新提交 AI 目录任务,正在生成目录...", 0);
@ -1222,6 +1312,58 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
return value == null ? null : String.valueOf(value); 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<String, Object> buildSummaryTaskConfigForRetry(AiTask summaryTask,
Long summaryModelId,
Long chapterModelId,
Long promptId,
String userPrompt,
String summaryDetailLevel) {
Map<String, Object> 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) { private AiTask resolveOwnedTask(Long taskId, Long meetingId, String expectedType) {
if (taskId == null) { if (taskId == null) {
return null; return null;
@ -1424,14 +1566,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
return requestedHostUserId != null ? requestedHostUserId : creatorId; return requestedHostUserId != null ? requestedHostUserId : creatorId;
} }
private String resolveHostName(String requestedHostName, String creatorName, Long creatorId, Long hostUserId) { private String resolveMeetingUserName(Long userId, String fallbackName) {
if (requestedHostName != null && !requestedHostName.isBlank()) { return meetingDomainSupport.resolveUserDisplayName(userId, fallbackName);
return requestedHostName.trim();
}
if (hostUserId != null && Objects.equals(hostUserId, creatorId)) {
return creatorName;
}
return null;
} }
private String resolveSummaryDetailLevel(String requestedSummaryDetailLevel) { private String resolveSummaryDetailLevel(String requestedSummaryDetailLevel) {

View File

@ -64,16 +64,20 @@ public class MeetingDomainSupport {
public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags, public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags,
String audioUrl, String meetingType, String meetingSource, String audioUrl, String meetingType, String meetingSource,
Long tenantId, Long creatorId, String creatorName, 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, 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); null, null);
} }
public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags, public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags,
String audioUrl, String meetingType, String meetingSource, String audioUrl, String meetingType, String meetingSource,
Long tenantId, Long creatorId, String creatorName, 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) { String sourceDeviceCode, String sourceDeviceMode) {
Meeting meeting = new Meeting(); Meeting meeting = new Meeting();
meeting.setTitle(title); meeting.setTitle(title);
@ -90,6 +94,8 @@ public class MeetingDomainSupport {
meeting.setAudioUrl(audioUrl); meeting.setAudioUrl(audioUrl);
meeting.setSourceDeviceCode(sourceDeviceCode); meeting.setSourceDeviceCode(sourceDeviceCode);
meeting.setSourceDeviceMode(sourceDeviceMode); meeting.setSourceDeviceMode(sourceDeviceMode);
meeting.setSummaryModelId(summaryModelId);
meeting.setPromptId(promptId);
meeting.setSummaryDetailLevel(normalizeSummaryDetailLevel(summaryDetailLevel)); meeting.setSummaryDetailLevel(normalizeSummaryDetailLevel(summaryDetailLevel));
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_NONE); meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_NONE);
if (MeetingConstants.TYPE_OFFLINE.equalsIgnoreCase(meetingType)) { if (MeetingConstants.TYPE_OFFLINE.equalsIgnoreCase(meetingType)) {
@ -99,16 +105,21 @@ public class MeetingDomainSupport {
return meeting; return meeting;
} }
public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long promptId, String userPrompt) { public String resolveUserDisplayName(Long userId, String fallbackName) {
return createSummaryTask( if (userId == null) {
meetingId, return fallbackName;
summaryModelId, }
summaryModelId, SysUser user = sysUserMapper.selectById(userId);
promptId, if (user == null) {
userPrompt, return fallbackName;
MeetingConstants.SUMMARY_DETAIL_STANDARD, }
"AUTO_SUMMARY" 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, 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, public AiTask createChapterTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId,
String userPrompt, String summaryDetailLevel) { String userPrompt, String summaryDetailLevel) {
@ -166,17 +167,7 @@ public class MeetingDomainSupport {
return chapterTask; 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, public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId,
String userPrompt, String summaryDetailLevel) { String userPrompt, String summaryDetailLevel) {
@ -406,6 +397,8 @@ public class MeetingDomainSupport {
vo.setSourceDeviceCode(meeting.getSourceDeviceCode()); vo.setSourceDeviceCode(meeting.getSourceDeviceCode());
vo.setSourceDeviceMode(meeting.getSourceDeviceMode()); vo.setSourceDeviceMode(meeting.getSourceDeviceMode());
vo.setOfflineRecordingStatus(meeting.getOfflineRecordingStatus()); vo.setOfflineRecordingStatus(meeting.getOfflineRecordingStatus());
vo.setSummaryModelId(meeting.getSummaryModelId());
vo.setPromptId(meeting.getPromptId());
vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel())); vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel()));
vo.setAudioSaveStatus(meeting.getAudioSaveStatus()); vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
vo.setAudioSaveMessage(meeting.getAudioSaveMessage()); vo.setAudioSaveMessage(meeting.getAudioSaveMessage());

View File

@ -7,6 +7,7 @@ import com.imeeting.dto.biz.MeetingPointsChargeItemVO;
import com.imeeting.dto.biz.MeetingPointsLedgerDetailVO; import com.imeeting.dto.biz.MeetingPointsLedgerDetailVO;
import com.imeeting.dto.biz.MeetingPointsLedgerListItemVO; import com.imeeting.dto.biz.MeetingPointsLedgerListItemVO;
import com.imeeting.dto.biz.MeetingPointsOverviewVO; import com.imeeting.dto.biz.MeetingPointsOverviewVO;
import com.imeeting.dto.biz.MeetingPointsPersonalAccountVO;
import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.MeetingPointsAccount; import com.imeeting.entity.biz.MeetingPointsAccount;
import com.imeeting.entity.biz.MeetingPointsLedger; import com.imeeting.entity.biz.MeetingPointsLedger;
@ -71,22 +72,28 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
} }
@Override @Override
public MeetingPointsOverviewVO getOverview(Long tenantId) { public MeetingPointsOverviewVO getOverview(Long tenantId, Long userId, boolean isAdmin) {
String accountMode = resolveAccountMode(); String accountMode = resolveAccountMode();
String chargePriority = resolveChargePriority(); String chargePriority = resolveChargePriority();
MeetingPointsAccount publicAccount = findAccount(tenantId, PUBLIC_ACCOUNT_USER_ID); MeetingPointsAccount publicAccount = findAccount(tenantId, PUBLIC_ACCOUNT_USER_ID);
long publicBalance = publicAccount == null ? 0L : defaultLong(publicAccount.getCurrentBalance()); long publicBalance = publicAccount == null ? 0L : defaultLong(publicAccount.getCurrentBalance());
long publicTotalUsed = publicAccount == null ? 0L : defaultLong(publicAccount.getTotalPointsUsed()); long publicTotalUsed = publicAccount == null ? 0L : defaultLong(publicAccount.getTotalPointsUsed());
long personalBalance = 0L;
long personalTotalUsed = 0L;
List<MeetingPointsAccount> personalAccounts = meetingPointsAccountService.list(new LambdaQueryWrapper<MeetingPointsAccount>() List<MeetingPointsAccount> personalAccounts = meetingPointsAccountService.list(new LambdaQueryWrapper<MeetingPointsAccount>()
.eq(MeetingPointsAccount::getTenantId, tenantId) .eq(MeetingPointsAccount::getTenantId, tenantId)
.ne(MeetingPointsAccount::getUserId, PUBLIC_ACCOUNT_USER_ID)); .ne(MeetingPointsAccount::getUserId, PUBLIC_ACCOUNT_USER_ID));
long personalBalance = 0L;
long personalTotalUsed = 0L;
if (isAdmin) {
for (MeetingPointsAccount account : personalAccounts) { for (MeetingPointsAccount account : personalAccounts) {
personalBalance += defaultLong(account.getCurrentBalance()); personalBalance += defaultLong(account.getCurrentBalance());
personalTotalUsed += defaultLong(account.getTotalPointsUsed()); 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<Long> scopedOwnerUserIds = resolveScopedOwnerUserIds(tenantId); List<Long> scopedOwnerUserIds = resolveScopedOwnerUserIds(tenantId);
long totalChargeCount = 0L; long totalChargeCount = 0L;
@ -110,6 +117,8 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
vo.setPersonalTotalPointsUsed(personalTotalUsed); vo.setPersonalTotalPointsUsed(personalTotalUsed);
vo.setTotalAvailableBalance(resolveVisibleTotalBalance(accountMode, publicBalance, personalBalance)); vo.setTotalAvailableBalance(resolveVisibleTotalBalance(accountMode, publicBalance, personalBalance));
vo.setTotalChargeCount(totalChargeCount); vo.setTotalChargeCount(totalChargeCount);
vo.setAdmin(isAdmin);
vo.setPersonalAccounts(buildPersonalAccountOverview(accountMode, isAdmin, personalAccounts));
return vo; return vo;
} }
@ -422,6 +431,39 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
return publicBalance + personalBalance; return publicBalance + personalBalance;
} }
private List<MeetingPointsPersonalAccountVO> buildPersonalAccountOverview(String accountMode,
boolean isAdmin,
List<MeetingPointsAccount> 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<Long> userIds = personalAccounts.stream()
.map(MeetingPointsAccount::getUserId)
.filter(Objects::nonNull)
.distinct()
.toList();
Map<Long, SysUser> 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) { private long defaultLong(Long value) {
return value == null ? 0L : value; return value == null ? 0L : value;
} }

View File

@ -146,9 +146,21 @@ public class MeetingQueryServiceImpl implements MeetingQueryService {
throw new RuntimeException("缺少可用的总结任务配置"); throw new RuntimeException("缺少可用的总结任务配置");
} }
Long summaryModelId = firstLong(request == null ? null : request.getSummaryModelId(), latestSummaryTask.getTaskConfig().get("summaryModelId")); Long summaryModelId = firstLong(
Long chapterModelId = firstLong(request == null ? null : request.getChapterModelId(), latestSummaryTask.getTaskConfig().get("chapterModelId"), summaryModelId); request == null ? null : request.getSummaryModelId(),
Long promptId = firstLong(request == null ? null : request.getPromptId(), latestSummaryTask.getTaskConfig().get("promptId")); 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 String userPrompt = request != null && request.getUserPrompt() != null
? request.getUserPrompt() ? request.getUserPrompt()
: stringValue(latestSummaryTask.getTaskConfig().get("userPrompt")); : stringValue(latestSummaryTask.getTaskConfig().get("userPrompt"));

View File

@ -33,6 +33,8 @@ export interface MeetingVO {
sourceDeviceCode?: string; sourceDeviceCode?: string;
sourceDeviceMode?: "PUBLIC" | "PRIVATE"; sourceDeviceMode?: "PUBLIC" | "PRIVATE";
summaryDetailLevel?: SummaryDetailLevel; summaryDetailLevel?: SummaryDetailLevel;
summaryModelId: number;
promptId?: number;
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED"; audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
audioSaveMessage?: string; audioSaveMessage?: string;
accessPassword?: string; accessPassword?: string;
@ -150,6 +152,9 @@ export interface UpdateMeetingBasicCommand {
meetingTime?: string; meetingTime?: string;
tags?: string; tags?: string;
accessPassword?: string | null; accessPassword?: string | null;
summaryModelId: number;
promptId?: number;
summaryDetailLevel?: SummaryDetailLevel;
} }
export type MeetingUpdateBasicDTO = UpdateMeetingBasicCommand; export type MeetingUpdateBasicDTO = UpdateMeetingBasicCommand;

View File

@ -9,6 +9,16 @@ export interface MeetingPointsOverviewVO {
personalTotalPointsUsed: number; personalTotalPointsUsed: number;
totalAvailableBalance: number; totalAvailableBalance: number;
totalChargeCount: number; totalChargeCount: number;
admin?: boolean;
personalAccounts?: MeetingPointsPersonalAccountVO[];
}
export interface MeetingPointsPersonalAccountVO {
userId: number;
username?: string;
displayName?: string;
currentBalance: number;
totalPointsUsed: number;
} }
export interface MeetingPointsChargeItemVO { export interface MeetingPointsChargeItemVO {

View File

@ -1799,9 +1799,13 @@ const MeetingDetail: React.FC = () => {
summaryForm.setFieldsValue({ summaryForm.setFieldsValue({
summaryModelId: summaryModelId:
summaryForm.getFieldValue('summaryModelId') ?? summaryForm.getFieldValue('summaryModelId') ??
meeting?.summaryModelId ??
llmModels.find((model) => model.isDefault === 1)?.id ?? llmModels.find((model) => model.isDefault === 1)?.id ??
llmModels[0]?.id, llmModels[0]?.id,
promptId: summaryForm.getFieldValue('promptId') ?? prompts[0]?.id, promptId:
summaryForm.getFieldValue('promptId') ??
meeting?.promptId ??
prompts[0]?.id,
userPrompt: meeting?.lastUserPrompt ?? '', userPrompt: meeting?.lastUserPrompt ?? '',
summaryDetailLevel: summaryDetailLevel:
summaryForm.getFieldValue('summaryDetailLevel') ?? summaryForm.getFieldValue('summaryDetailLevel') ??

View File

@ -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 { listUsers } from "@/api";
import { import {
Button, Button,
Card, Card,
Col, Col,
Descriptions, Descriptions,
Empty,
Form, Form,
Input, Input,
InputNumber, InputNumber,
@ -31,10 +32,11 @@ import {
type MeetingPointsLedgerDetailVO, type MeetingPointsLedgerDetailVO,
type MeetingPointsLedgerListItemVO, type MeetingPointsLedgerListItemVO,
type MeetingPointsOverviewVO, type MeetingPointsOverviewVO,
type MeetingPointsPersonalAccountVO,
} from "@/api/business/meetingPoints"; } from "@/api/business/meetingPoints";
import type { SysUser } from "@/types"; import type { SysUser } from "@/types";
const { Text } = Typography; const { Text, Title } = Typography;
const POINTS_TYPE_OPTIONS = [ const POINTS_TYPE_OPTIONS = [
{ label: "全部类型", value: "" }, { label: "全部类型", value: "" },
@ -42,9 +44,13 @@ const POINTS_TYPE_OPTIONS = [
{ label: "总结", value: "LLM" }, { label: "总结", value: "LLM" },
]; ];
const ACCOUNT_MODE_PUBLIC = "PUBLIC";
const ACCOUNT_MODE_PERSONAL = "PERSONAL";
const ACCOUNT_MODE_BOTH = "BOTH";
function getAccountModeLabel(mode?: string) { function getAccountModeLabel(mode?: string) {
if (mode === "PERSONAL") return "个人账户"; if (mode === ACCOUNT_MODE_PERSONAL) return "个人账户";
if (mode === "BOTH") return "公共和个人共存"; if (mode === ACCOUNT_MODE_BOTH) return "公共 + 个人";
return "公共账户"; return "公共账户";
} }
@ -83,11 +89,79 @@ function formatDateTime(value?: string) {
return value ? value.replace("T", " ").substring(0, 19) : "-"; 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() { export default function MeetingPointsManagement() {
const [overview, setOverview] = useState<MeetingPointsOverviewVO | null>(null); const [overview, setOverview] = useState<MeetingPointsOverviewVO | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
const [transferLoading, setTransferLoading] = useState(false); const [transferLoading, setTransferLoading] = useState(false);
const [usersLoading, setUsersLoading] = useState(false);
const [records, setRecords] = useState<MeetingPointsLedgerListItemVO[]>([]); const [records, setRecords] = useState<MeetingPointsLedgerListItemVO[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [detailOpen, setDetailOpen] = useState(false); const [detailOpen, setDetailOpen] = useState(false);
@ -102,14 +176,26 @@ export default function MeetingPointsManagement() {
}); });
const [transferForm] = Form.useForm(); 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 loadOverview = async () => {
const data = await getMeetingPointsOverview(); const data = await getMeetingPointsOverview();
setOverview(data); setOverview(data);
}; };
const loadUsers = async () => { const loadUsers = async () => {
setUsersLoading(true);
try {
const data = await listUsers(); const data = await listUsers();
setUsers(data || []); setUsers(data || []);
} finally {
setUsersLoading(false);
}
}; };
const loadPage = async (nextParams = params) => { const loadPage = async (nextParams = params) => {
@ -124,7 +210,7 @@ export default function MeetingPointsManagement() {
}; };
useEffect(() => { useEffect(() => {
void Promise.all([loadOverview(), loadPage(), loadUsers()]); void Promise.all([loadOverview(), loadPage()]);
}, []); }, []);
const handleSearch = () => { const handleSearch = () => {
@ -145,7 +231,7 @@ export default function MeetingPointsManagement() {
}; };
const handleRefresh = async () => { const handleRefresh = async () => {
await Promise.all([loadOverview(), loadPage(), loadUsers()]); await Promise.all([loadOverview(), loadPage()]);
message.success("已刷新积分数据"); message.success("已刷新积分数据");
}; };
@ -160,6 +246,13 @@ export default function MeetingPointsManagement() {
} }
}; };
const handleOpenTransfer = async () => {
setTransferOpen(true);
if (!users.length) {
await loadUsers();
}
};
const handleTransferSubmit = async () => { const handleTransferSubmit = async () => {
const values = await transferForm.validateFields(); const values = await transferForm.validateFields();
setTransferLoading(true); setTransferLoading(true);
@ -281,14 +374,16 @@ export default function MeetingPointsManagement() {
return ( return (
<PageContainer <PageContainer
title="积分管理" title="积分管理"
subtitle="查看公共账户、个人账户和总结扣费流水" subtitle="根据当前扣费模式查看公共账户、个人账户和会议积分消耗轨迹"
headerExtra={ headerExtra={
<Space> <Space>
<Tag color="processing">{getAccountModeLabel(overview?.accountMode)}</Tag> <Tag color="processing">{getAccountModeLabel(overview?.accountMode)}</Tag>
<Tag color="blue">{getChargePriorityLabel(overview?.chargePriority)}</Tag> <Tag color="blue">{getChargePriorityLabel(overview?.chargePriority)}</Tag>
<Button icon={<PlusOutlined />} onClick={() => setTransferOpen(true)}> {showTransferButton ? (
<Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}>
</Button> </Button>
) : null}
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}> <Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button> </Button>
@ -317,41 +412,176 @@ export default function MeetingPointsManagement() {
</Space> </Space>
} }
> >
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}> <Card
<Col xs={24} md={6}> bordered={false}
<Card> style={{
<Statistic title="当前模式可用总积分" value={overview?.totalAvailableBalance ?? 0} /> marginBottom: 16,
</Card> borderRadius: 24,
</Col> background:
<Col xs={24} md={6}> "linear-gradient(180deg, rgba(248,250,252,0.96) 0%, rgba(255,255,255,0.98) 100%)",
<Card> boxShadow: "0 18px 40px rgba(15, 23, 42, 0.06)",
<Statistic title="公共账户余额" value={overview?.publicBalance ?? 0} /> }}
</Card> >
</Col> <Space direction="vertical" size={20} style={{ width: "100%" }}>
<Col xs={24} md={6}> <div style={{ display: "flex", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
<Card> <div>
<Statistic title="个人账户余额汇总" value={overview?.personalBalance ?? 0} /> <Text type="secondary" style={{ letterSpacing: 1.4 }}>
</Card> POINTS OPERATIONS BOARD
</Col> </Text>
<Col xs={24} md={6}> <Title level={4} style={{ margin: "8px 0 0" }}>
<Card> {isPublicOnly
<Statistic title="累计消耗次数" value={overview?.totalChargeCount ?? 0} /> ? "当前为公共账户结算视图"
</Card> : isPersonalOnly
</Col> ? "当前为个人账户结算视图"
</Row> : "当前为公共与个人混合结算视图"}
</Title>
</div>
<div style={{ textAlign: "right" }}>
<Text type="secondary"></Text>
<div style={{ marginTop: 6 }}>
<Tag color={isAdmin ? "gold" : "default"}>{isAdmin ? "管理员视角" : "当前用户视角"}</Tag>
</div>
</div>
</div>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}> <Row gutter={[16, 16]}>
<Col xs={24} md={12}> {summaryCards.map((item) => (
<Card> <Col xs={24} md={12} xl={24 / Math.min(summaryCards.length, 5)} key={item.key}>
<Statistic title="公共账户累计消耗积分" value={overview?.publicTotalPointsUsed ?? 0} /> <Card
</Card> size="small"
</Col> style={{
<Col xs={24} md={12}> borderRadius: 20,
<Card> border: "1px solid rgba(15, 23, 42, 0.06)",
<Statistic title="个人账户累计消耗积分汇总" value={overview?.personalTotalPointsUsed ?? 0} /> background: item.accent,
minHeight: 142,
}}
styles={{ body: { padding: 18 } }}
>
<Space direction="vertical" size={10} style={{ width: "100%" }}>
<Text type="secondary">{item.title}</Text>
<Statistic value={item.value} valueStyle={{ fontSize: 30, fontWeight: 700, color: "#111827" }} />
<Text type="secondary" style={{ fontSize: 12 }}>
{item.note}
</Text>
</Space>
</Card> </Card>
</Col> </Col>
))}
</Row> </Row>
</Space>
</Card>
{showPersonalGallery ? (
<Card
bordered={false}
style={{
marginBottom: 16,
borderRadius: 24,
background: "linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(245,247,250,0.96) 100%)",
boxShadow: "0 16px 34px rgba(15, 23, 42, 0.05)",
}}
>
<Space direction="vertical" size={18} style={{ width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
<div>
<Title level={5} style={{ margin: 0 }}>
</Title>
<Text type="secondary">
</Text>
</div>
<Tag color="purple">{overview?.personalAccounts?.length ?? 0} </Tag>
</div>
{overview?.personalAccounts?.length ? (
<Row gutter={[16, 16]}>
{overview.personalAccounts.map((account: MeetingPointsPersonalAccountVO, index) => (
<Col xs={24} sm={12} xl={8} xxl={6} key={account.userId}>
<Card
size="small"
style={{
borderRadius: 20,
border: "1px solid rgba(124, 58, 237, 0.08)",
background:
index < 3
? "linear-gradient(145deg, rgba(255,255,255,1) 0%, rgba(245,241,255,0.98) 100%)"
: "linear-gradient(145deg, rgba(255,255,255,1) 0%, rgba(249,250,251,0.98) 100%)",
minHeight: 158,
}}
styles={{ body: { padding: 18 } }}
>
<Space direction="vertical" size={14} style={{ width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12 }}>
<Space size={12} align="start">
<div
style={{
width: 38,
height: 38,
borderRadius: 14,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(124, 58, 237, 0.10)",
color: "#7c3aed",
flexShrink: 0,
}}
>
<UserOutlined />
</div>
<div>
<Text strong style={{ display: "block" }}>
{account.displayName || account.username || `用户 #${account.userId}`}
</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
{account.username ? `@${account.username}` : `ID ${account.userId}`}
</Text>
</div>
</Space>
{index < 3 ? <Tag color="gold">TOP {index + 1}</Tag> : null}
</div>
<div>
<Text type="secondary"></Text>
<div style={{ fontSize: 30, fontWeight: 700, lineHeight: 1.2, color: "#111827" }}>
{account.currentBalance ?? 0}
</div>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gap: 10,
padding: 12,
borderRadius: 16,
background: "rgba(15, 23, 42, 0.03)",
}}
>
<div>
<Text type="secondary" style={{ fontSize: 12 }}>
</Text>
<div style={{ fontWeight: 600 }}>{account.totalPointsUsed ?? 0}</div>
</div>
<div>
<Text type="secondary" style={{ fontSize: 12 }}>
</Text>
<div style={{ fontWeight: 600 }}></div>
</div>
</div>
</Space>
</Card>
</Col>
))}
</Row>
) : (
<Empty description="当前没有可展示的个人账户数据" />
)}
</Space>
</Card>
) : null}
<Card <Card
className="app-page__content-card" className="app-page__content-card"
@ -365,7 +595,7 @@ export default function MeetingPointsManagement() {
dataSource={records} dataSource={records}
loading={loading} loading={loading}
totalCount={total} totalCount={total}
scroll={{ y: "calc(100vh - 470px)", x: 1200 }} scroll={{ y: "calc(100vh - 510px)", x: 1200 }}
pagination={false} pagination={false}
/> />
</div> </div>
@ -447,6 +677,7 @@ export default function MeetingPointsManagement() {
<Form.Item name="targetUserId" label="目标用户" rules={[{ required: true, message: "请选择目标用户" }]}> <Form.Item name="targetUserId" label="目标用户" rules={[{ required: true, message: "请选择目标用户" }]}>
<Select <Select
showSearch showSearch
loading={usersLoading}
optionFilterProp="label" optionFilterProp="label"
placeholder="请选择用户" placeholder="请选择用户"
options={users options={users

View File

@ -44,6 +44,9 @@ export interface MeetingVO {
meetingSource?: "WEB" | "ANDROID"; meetingSource?: "WEB" | "ANDROID";
sourceDeviceCode?: string; sourceDeviceCode?: string;
sourceDeviceMode?: "PUBLIC" | "PRIVATE"; sourceDeviceMode?: "PUBLIC" | "PRIVATE";
summaryDetailLevel?: "DETAILED" | "STANDARD" | "BRIEF";
summaryModelId?: number;
promptId?: number;
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED"; audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
audioSaveMessage?: string; audioSaveMessage?: string;
accessPassword?: string | null; accessPassword?: string | null;
@ -103,6 +106,9 @@ export interface UpdateMeetingBasicCommand {
meetingTime?: string; meetingTime?: string;
tags?: string; tags?: string;
accessPassword?: string | null; accessPassword?: string | null;
summaryModelId?: number;
promptId?: number;
summaryDetailLevel?: "DETAILED" | "STANDARD" | "BRIEF";
} }
export interface MeetingPageResult { export interface MeetingPageResult {