feat: 添加租户业务数据逻辑删除和优化会议状态处理

- 在多个 Mapper 中添加 `logicalDeleteByTenantId` 方法,支持按租户 ID 逻辑删除数据
- 优化 `MeetingUnifiedStatusServiceImpl` 中的会议状态处理逻辑,调整 `isAndroidOfflineMeetingWaitingUpload` 的调用位置
- 更新 `Meetings.tsx` 中的会议状态判断逻辑,新增 `isUnifiedTerminalProgress` 方法
- 优化 `AndroidChunkUploadServiceImpl` 中的文件上传逻辑,移除不必要的 `try-finally` 块
- 在 `TenantManagementServicePrimaryImpl` 中添加逻辑删除租户业务数据的方法,并更新相关依赖注入
dev_na
chenhao 2026-06-12 09:09:44 +08:00
parent 146b31b809
commit fd9ef5c885
9 changed files with 133 additions and 88 deletions

View File

@ -220,4 +220,14 @@ public interface LicenseMapper extends BaseMapper<LicenseEntity> {
AND is_deleted = 0
""")
int invalidateById(@Param("id") Long id);
@InterceptorIgnore(tenantLine = "true")
@Update("""
UPDATE biz_license
SET is_deleted = 1,
updated_at = CURRENT_TIMESTAMP
WHERE tenant_id = #{tenantId}
AND is_deleted = 0
""")
int logicalDeleteByTenantId(@Param("tenantId") Long tenantId);
}

View File

@ -5,6 +5,7 @@ import com.imeeting.entity.biz.MeetingPointsAccount;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface MeetingPointsAccountMapper extends BaseMapper<MeetingPointsAccount> {
@ -18,4 +19,13 @@ public interface MeetingPointsAccountMapper extends BaseMapper<MeetingPointsAcco
FOR UPDATE
""")
MeetingPointsAccount selectForUpdate(@Param("tenantId") Long tenantId, @Param("userId") Long userId);
@Update("""
UPDATE biz_meeting_points_accounts
SET is_deleted = 1,
updated_at = CURRENT_TIMESTAMP
WHERE tenant_id = #{tenantId}
AND is_deleted = 0
""")
int logicalDeleteByTenantId(@Param("tenantId") Long tenantId);
}

View File

@ -3,7 +3,17 @@ package com.imeeting.mapper.biz;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imeeting.entity.biz.MeetingPointsLedger;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface MeetingPointsLedgerMapper extends BaseMapper<MeetingPointsLedger> {
@Update("""
UPDATE biz_meeting_points_ledgers
SET is_deleted = 1,
updated_at = CURRENT_TIMESTAMP
WHERE tenant_id = #{tenantId}
AND is_deleted = 0
""")
int logicalDeleteByTenantId(@Param("tenantId") Long tenantId);
}

View File

@ -5,6 +5,7 @@ import com.imeeting.entity.biz.MeetingSummaryChargeRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface MeetingSummaryChargeRecordMapper extends BaseMapper<MeetingSummaryChargeRecord> {
@ -17,4 +18,13 @@ public interface MeetingSummaryChargeRecordMapper extends BaseMapper<MeetingSumm
FOR UPDATE
""")
MeetingSummaryChargeRecord selectForUpdateBySummaryTaskId(@Param("summaryTaskId") Long summaryTaskId);
@Update("""
UPDATE biz_meeting_summary_charge_records
SET is_deleted = 1,
updated_at = CURRENT_TIMESTAMP
WHERE tenant_id = #{tenantId}
AND is_deleted = 0
""")
int logicalDeleteByTenantId(@Param("tenantId") Long tenantId);
}

View File

@ -1,10 +1,12 @@
package com.imeeting.mapper.biz;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.imeeting.entity.biz.TenantMeetingPointsSetting;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface TenantMeetingPointsSettingMapper extends BaseMapper<TenantMeetingPointsSetting> {
@ -17,4 +19,14 @@ public interface TenantMeetingPointsSettingMapper extends BaseMapper<TenantMeeti
FOR UPDATE
""")
TenantMeetingPointsSetting selectForUpdate(@Param("tenantId") Long tenantId);
@InterceptorIgnore(tenantLine = "true")
@Update("""
UPDATE biz_meeting_points_tenant_settings
SET is_deleted = 1,
updated_at = CURRENT_TIMESTAMP
WHERE tenant_id = #{tenantId}
AND is_deleted = 0
""")
int logicalDeleteByTenantId(@Param("tenantId") Long tenantId);
}

View File

@ -99,24 +99,24 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService
}
Path mergedFile = mergeChunks(state);
try {
MultipartFile mergedMultipart = new LocalMultipartFile(
buildMergedOriginalFilename(state, mergedFile),
state.getContentType(),
Files.readAllBytes(mergedFile)
MultipartFile mergedMultipart = new LocalMultipartFile(
buildMergedOriginalFilename(state, mergedFile),
state.getContentType(),
Files.readAllBytes(mergedFile)
);
LegacyUploadAudioResponse response;
if (authContext.isAnonymous()) {
response = legacyMeetingAdapterService.uploadAndTriggerOfflineProcessForPublicDevice(
meetingId,
null,
null,
false,
mergedMultipart,
authContext
);
if (authContext.isAnonymous()) {
return legacyMeetingAdapterService.uploadAndTriggerOfflineProcessForPublicDevice(
meetingId,
null,
null,
false,
mergedMultipart,
authContext
);
}
} else {
LoginUser loginUser = toLoginUser(authContext);
return legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
response = legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
meetingId,
null,
null,
@ -125,9 +125,11 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService
authContext,
loginUser
);
} finally {
}
if (response != null) {
cleanup(meetingId);
}
return response;
}
private AndroidChunkUploadSessionState getOrCreateState(Long meetingId,
@ -140,7 +142,7 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService
AndroidChunkUploadSessionState state = new AndroidChunkUploadSessionState();
state.setMeetingId(meetingId);
state.setDeviceId(authContext.getDeviceId());
state.setFileName(chunkFile.getOriginalFilename());
state.setFileName(normalizeChunkSourceFileName(chunkFile.getOriginalFilename()));
state.setContentType(chunkFile.getContentType());
saveState(meetingId, state);
return state;

View File

@ -84,15 +84,15 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ
if (MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED)) {
return UnifiedMeetingStatusStage.COMPLETED;
}
if (isAndroidOfflineMeetingWaitingUpload(meeting)) {
return UnifiedMeetingStatusStage.WAITING_UPLOAD;
}
UnifiedMeetingStatusStage stageFromSnapshot = resolveStageFromSnapshot(snapshot);
if (stageFromSnapshot != null) {
return stageFromSnapshot;
}
MeetingUnifiedStageContext context = buildStageContext(meeting.getId(), snapshot);
if (isAndroidOfflineMeetingWaitingUpload(meeting)) {
return UnifiedMeetingStatusStage.WAITING_UPLOAD;
}
if (isTranscribing(context)) {
return UnifiedMeetingStatusStage.TRANSCRIBING;
}
@ -173,6 +173,9 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ
}
private String resolveMessage(MeetingVO meeting, MeetingProgressSnapshot snapshot, UnifiedMeetingStatusStage stage) {
if (stage == UnifiedMeetingStatusStage.WAITING_UPLOAD) {
return "待上传录音文件";
}
if (snapshot != null && snapshot.getMessage() != null && !snapshot.getMessage().isBlank() && !Objects.equals(snapshot.getMessage(), "Waiting...")) {
return snapshot.getMessage();
}

View File

@ -1,14 +1,18 @@
package com.imeeting.service.biz.impl;
import com.imeeting.mapper.LicenseMapper;
import com.imeeting.mapper.biz.MeetingPointsAccountMapper;
import com.imeeting.mapper.biz.MeetingPointsLedgerMapper;
import com.imeeting.mapper.biz.MeetingSummaryChargeRecordMapper;
import com.imeeting.mapper.biz.TenantMeetingPointsSettingMapper;
import com.imeeting.service.biz.LicenseService;
import com.imeeting.service.biz.MeetingPointsService;
import com.imeeting.service.biz.TenantMeetingPointsSettingService;
import com.unisbase.config.properties.UnisBaseProperties;
import com.unisbase.dto.CreateTenantDTO;
import com.unisbase.dto.PageResult;
import com.unisbase.dto.SysTenantDTO;
import com.unisbase.service.SysTenantService;
import com.unisbase.service.TenantManagementService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -18,48 +22,51 @@ import java.util.List;
@Service
@Primary
public class TenantManagementServicePrimaryImpl implements TenantManagementService {
private static final long DEFAULT_TENANT_ID = 1L;
private final SysTenantService sysTenantService;
private final UnisBaseProperties unisBaseProperties;
private final TenantManagementService delegate;
private final LicenseService licenseService;
private final MeetingPointsService meetingPointsService;
private final TenantMeetingPointsSettingService tenantMeetingPointsSettingService;
private final LicenseMapper licenseMapper;
private final MeetingPointsAccountMapper meetingPointsAccountMapper;
private final MeetingPointsLedgerMapper meetingPointsLedgerMapper;
private final MeetingSummaryChargeRecordMapper meetingSummaryChargeRecordMapper;
private final TenantMeetingPointsSettingMapper tenantMeetingPointsSettingMapper;
public TenantManagementServicePrimaryImpl(SysTenantService sysTenantService,
UnisBaseProperties unisBaseProperties,
public TenantManagementServicePrimaryImpl(@Qualifier("tenantManagementServiceImpl") TenantManagementService delegate,
LicenseService licenseService,
MeetingPointsService meetingPointsService,
TenantMeetingPointsSettingService tenantMeetingPointsSettingService) {
this.sysTenantService = sysTenantService;
this.unisBaseProperties = unisBaseProperties;
TenantMeetingPointsSettingService tenantMeetingPointsSettingService,
LicenseMapper licenseMapper,
MeetingPointsAccountMapper meetingPointsAccountMapper,
MeetingPointsLedgerMapper meetingPointsLedgerMapper,
MeetingSummaryChargeRecordMapper meetingSummaryChargeRecordMapper,
TenantMeetingPointsSettingMapper tenantMeetingPointsSettingMapper) {
this.delegate = delegate;
this.licenseService = licenseService;
this.meetingPointsService = meetingPointsService;
this.tenantMeetingPointsSettingService = tenantMeetingPointsSettingService;
this.licenseMapper = licenseMapper;
this.meetingPointsAccountMapper = meetingPointsAccountMapper;
this.meetingPointsLedgerMapper = meetingPointsLedgerMapper;
this.meetingSummaryChargeRecordMapper = meetingSummaryChargeRecordMapper;
this.tenantMeetingPointsSettingMapper = tenantMeetingPointsSettingMapper;
}
@Override
public PageResult<List<SysTenantDTO>> listTenants(Integer current, Integer size, String name, String code) {
if (isSingleTenantMode()) {
SysTenantDTO defaultTenant = sysTenantService.findById(getDefaultTenantId());
PageResult<List<SysTenantDTO>> result = new PageResult<>();
result.setRecords(defaultTenant == null ? List.of() : List.of(defaultTenant));
result.setTotal(defaultTenant == null ? 0 : 1);
return result;
}
return sysTenantService.page(current, size, name, code);
return delegate.listTenants(current, size, name, code);
}
@Override
public SysTenantDTO getTenant(Long tenantId) {
return sysTenantService.findById(tenantId);
return delegate.getTenant(tenantId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createTenant(CreateTenantDTO tenant) {
assertTenantLifecycleAllowed();
Long tenantId = sysTenantService.createTenantWithAdmin(tenant);
Long tenantId = delegate.createTenant(tenant);
licenseService.initializeTemporaryLicenses(tenantId);
meetingPointsService.initializeTenantPointsAccount(tenantId);
tenantMeetingPointsSettingService.initializeTenantSetting(tenantId);
@ -68,55 +75,25 @@ public class TenantManagementServicePrimaryImpl implements TenantManagementServi
@Override
public boolean updateTenant(Long tenantId, SysTenantDTO tenant) {
assertDefaultTenantCanBeUpdated(tenantId, tenant == null ? null : tenant.getStatus());
if (tenant == null) {
throw new IllegalArgumentException("租户信息不能为空");
}
tenant.setId(tenantId);
return sysTenantService.update(tenant);
return delegate.updateTenant(tenantId, tenant);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteTenant(Long tenantId) {
assertTenantLifecycleAllowed();
return sysTenantService.deleteById(tenantId);
boolean deleted = delegate.deleteTenant(tenantId);
if (!deleted || tenantId == null) {
return deleted;
}
logicalDeleteTenantBizData(tenantId);
return true;
}
private boolean isSingleTenantMode() {
if (unisBaseProperties == null || unisBaseProperties.getTenant() == null) {
return false;
}
String mode = unisBaseProperties.getTenant().getMode();
if (mode != null && !mode.isBlank()) {
return "single".equalsIgnoreCase(mode.trim());
}
return !unisBaseProperties.getTenant().isEnabled();
}
private Long getDefaultTenantId() {
if (unisBaseProperties == null || unisBaseProperties.getTenant() == null) {
return DEFAULT_TENANT_ID;
}
Long configured = unisBaseProperties.getTenant().getDefaultTenantId();
return configured == null || configured <= 0 ? DEFAULT_TENANT_ID : configured;
}
private void assertTenantLifecycleAllowed() {
if (isSingleTenantMode()) {
throw new IllegalArgumentException("当前为单租户模式,不支持租户生命周期操作");
}
}
private void assertDefaultTenantCanBeUpdated(Long tenantId, Integer status) {
if (!isSingleTenantMode()) {
return;
}
Long defaultTenantId = getDefaultTenantId();
if (!defaultTenantId.equals(tenantId)) {
throw new IllegalArgumentException("当前为单租户模式,只允许维护默认租户");
}
if (status != null && status != 1) {
throw new IllegalArgumentException("当前为单租户模式,不允许禁用默认租户");
}
private void logicalDeleteTenantBizData(Long tenantId) {
tenantMeetingPointsSettingMapper.logicalDeleteByTenantId(tenantId);
meetingPointsLedgerMapper.logicalDeleteByTenantId(tenantId);
meetingSummaryChargeRecordMapper.logicalDeleteByTenantId(tenantId);
meetingPointsAccountMapper.logicalDeleteByTenantId(tenantId);
licenseMapper.logicalDeleteByTenantId(tenantId);
}
}

View File

@ -115,7 +115,18 @@ const shouldTrackGenerationProgress = (item: MeetingVO) =>
!hasLatestGenerationFailure(item) && (item.status === 0 || item.status === 1 || item.status === 2);
const isTerminalMeetingProgress = (progress?: MeetingProgress | null) =>
!!progress && (progress.percent === 100 || progress.percent < 0);
!!progress && (
progress.percent === 100
|| progress.percent < 0
|| progress.unifiedStatus?.statusCode === "COMPLETED"
|| progress.unifiedStatus?.statusCode?.startsWith("FAILED_")
);
const isUnifiedTerminalProgress = (progress?: MeetingProgress | null) =>
!!progress && (
progress.unifiedStatus?.statusCode === "COMPLETED"
|| progress.unifiedStatus?.statusCode?.startsWith("FAILED_")
);
const shouldPollMeetingCard = (item: MeetingVO) =>
shouldTrackGenerationProgress(item)
@ -212,7 +223,7 @@ const IntegratedStatusTag: React.FC<{ meeting: MeetingVO; progress: MeetingProgr
const displayConfig = progress?.unifiedStatus?.statusText
? { ...config, text: progress.unifiedStatus.statusText }
: config;
const isProcessing = shouldTrackGenerationProgress(meeting);
const isProcessing = shouldTrackGenerationProgress(meeting) && !isUnifiedTerminalProgress(progress);
const percent = isProcessing ? progress?.percent || 0 : 0;
return (