feat: 增强会议总结和积分管理功能
- 添加 `MeetingSummaryPromptAssembler` 依赖并更新相关方法 - 优化 `resolveHostName` 方法为 `resolveMeetingUserName`,并调整相关调用 - 更新 `updateMeetingBasic` 方法,支持更新摘要模型 ID 和提示 ID - 在 `retrySummary` 和 `retryChapter` 方法中添加对摘要模型 ID 和提示 ID 的处理 - 优化前端积分管理页面,新增个人账户余额画廊和统计卡片 - 调整积分管理页面的布局和样式,提升用户体验dev_na
parent
178b920581
commit
2e05a25e63
|
|
@ -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<Object> 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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@ public class MeetingPointsManagementController {
|
|||
@GetMapping("/overview")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
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 = "分页查询积分消耗流水")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<MeetingPointsPersonalAccountVO> personalAccounts;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Long> 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<String, Object> 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<List<MeetingPointsLedgerListItemVO>> pageLedgers(Long tenantId,
|
||||
Integer current,
|
||||
|
|
|
|||
|
|
@ -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<Meeting>()
|
||||
.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<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(
|
||||
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<AiTask>()
|
||||
.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<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) {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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<MeetingPointsAccount> personalAccounts = meetingPointsAccountService.list(new LambdaQueryWrapper<MeetingPointsAccount>()
|
||||
.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<Long> 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<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) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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') ??
|
||||
|
|
|
|||
|
|
@ -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<MeetingPointsOverviewVO | null>(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<MeetingPointsLedgerListItemVO[]>([]);
|
||||
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 (
|
||||
<PageContainer
|
||||
title="积分管理"
|
||||
subtitle="查看公共账户、个人账户和总结扣费流水"
|
||||
subtitle="根据当前扣费模式查看公共账户、个人账户和会议积分消耗轨迹"
|
||||
headerExtra={
|
||||
<Space>
|
||||
<Tag color="processing">当前模式:{getAccountModeLabel(overview?.accountMode)}</Tag>
|
||||
<Tag color="blue">优先级:{getChargePriorityLabel(overview?.chargePriority)}</Tag>
|
||||
<Button icon={<PlusOutlined />} onClick={() => setTransferOpen(true)}>
|
||||
分配积分
|
||||
</Button>
|
||||
{showTransferButton ? (
|
||||
<Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}>
|
||||
分配积分
|
||||
</Button>
|
||||
) : null}
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
|
||||
刷新
|
||||
</Button>
|
||||
|
|
@ -317,41 +412,176 @@ export default function MeetingPointsManagement() {
|
|||
</Space>
|
||||
}
|
||||
>
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} md={6}>
|
||||
<Card>
|
||||
<Statistic title="当前模式可用总积分" value={overview?.totalAvailableBalance ?? 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={6}>
|
||||
<Card>
|
||||
<Statistic title="公共账户余额" value={overview?.publicBalance ?? 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={6}>
|
||||
<Card>
|
||||
<Statistic title="个人账户余额汇总" value={overview?.personalBalance ?? 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={6}>
|
||||
<Card>
|
||||
<Statistic title="累计消耗次数" value={overview?.totalChargeCount ?? 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
borderRadius: 24,
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(248,250,252,0.96) 0%, rgba(255,255,255,0.98) 100%)",
|
||||
boxShadow: "0 18px 40px rgba(15, 23, 42, 0.06)",
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={20} style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ letterSpacing: 1.4 }}>
|
||||
POINTS OPERATIONS BOARD
|
||||
</Text>
|
||||
<Title level={4} style={{ margin: "8px 0 0" }}>
|
||||
{isPublicOnly
|
||||
? "当前为公共账户结算视图"
|
||||
: isPersonalOnly
|
||||
? "当前为个人账户结算视图"
|
||||
: "当前为公共与个人混合结算视图"}
|
||||
</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 }}>
|
||||
<Col xs={24} md={12}>
|
||||
<Card>
|
||||
<Statistic title="公共账户累计消耗积分" value={overview?.publicTotalPointsUsed ?? 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Card>
|
||||
<Statistic title="个人账户累计消耗积分汇总" value={overview?.personalTotalPointsUsed ?? 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
{summaryCards.map((item) => (
|
||||
<Col xs={24} md={12} xl={24 / Math.min(summaryCards.length, 5)} key={item.key}>
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
border: "1px solid rgba(15, 23, 42, 0.06)",
|
||||
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>
|
||||
</Col>
|
||||
))}
|
||||
</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
|
||||
className="app-page__content-card"
|
||||
|
|
@ -365,7 +595,7 @@ export default function MeetingPointsManagement() {
|
|||
dataSource={records}
|
||||
loading={loading}
|
||||
totalCount={total}
|
||||
scroll={{ y: "calc(100vh - 470px)", x: 1200 }}
|
||||
scroll={{ y: "calc(100vh - 510px)", x: 1200 }}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -447,6 +677,7 @@ export default function MeetingPointsManagement() {
|
|||
<Form.Item name="targetUserId" label="目标用户" rules={[{ required: true, message: "请选择目标用户" }]}>
|
||||
<Select
|
||||
showSearch
|
||||
loading={usersLoading}
|
||||
optionFilterProp="label"
|
||||
placeholder="请选择用户"
|
||||
options={users
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ export interface MeetingVO {
|
|||
meetingSource?: "WEB" | "ANDROID";
|
||||
sourceDeviceCode?: string;
|
||||
sourceDeviceMode?: "PUBLIC" | "PRIVATE";
|
||||
summaryDetailLevel?: "DETAILED" | "STANDARD" | "BRIEF";
|
||||
summaryModelId?: number;
|
||||
promptId?: number;
|
||||
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
|
||||
audioSaveMessage?: string;
|
||||
accessPassword?: string | null;
|
||||
|
|
@ -103,6 +106,9 @@ export interface UpdateMeetingBasicCommand {
|
|||
meetingTime?: string;
|
||||
tags?: string;
|
||||
accessPassword?: string | null;
|
||||
summaryModelId?: number;
|
||||
promptId?: number;
|
||||
summaryDetailLevel?: "DETAILED" | "STANDARD" | "BRIEF";
|
||||
}
|
||||
|
||||
export interface MeetingPageResult {
|
||||
|
|
|
|||
Loading…
Reference in New Issue