Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class ProjectService {
private final com.checkba.version.ProjectRepoService projectRepoService;
private final com.checkba.version.memory.MemoryRepoService memoryRepoService;
private final com.checkba.repository.MemoryRemoteRepository memoryRemoteRepository;
private final com.checkba.repository.CompletionEntryRepository completionEntryRepository;
private final org.springframework.context.ApplicationEventPublisher eventPublisher;

@jakarta.persistence.PersistenceContext
Expand Down Expand Up @@ -276,7 +277,8 @@ public Project getProject(Long id) {
*/
@Transactional
public void deleteProject(Long id) {
if (!projectRepository.existsById(id)) {
Project project = entityManager.find(Project.class, id, jakarta.persistence.LockModeType.PESSIMISTIC_WRITE);
if (project == null) {
throw new IllegalArgumentException(LangText.of("项目不存在: ", "Project not found: ") + id);
}

Expand All @@ -303,6 +305,7 @@ public void deleteProject(Long id) {
// memory_remote 按 repoKey 建索引、没有 projectId 列,上面那套
// 「delete from E where e.projectId = :pid」批量语句吃不到它。
memoryRemoteRepository.findByRepoKey(memoryRepoKey).ifPresent(memoryRemoteRepository::delete);
completionEntryRepository.deleteByScopeKey("p:" + id);
projectRepository.deleteById(id);
storageResolver.invalidate(id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.checkba.model.entity.MobileMediaInbox;
import com.checkba.repository.AccountBindingRepository;
import com.checkba.repository.DeviceTokenRepository;
import com.checkba.repository.CompletionEntryRepository;
import com.checkba.repository.MobileDeviceStateRepository;
import com.checkba.repository.MobileMediaInboxRepository;
import com.checkba.repository.MobileProjectDirRepository;
Expand All @@ -18,6 +19,8 @@
import com.checkba.service.mobile.MobileBillingFailureException;
import com.checkba.service.mobile.MobileBillingKind;
import com.checkba.service.mobile.MobileRelayBlobStore;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -79,6 +82,8 @@ public class AccountDeletionService {
private final MobileTransferRequestRepository transferRepository;
private final AccountBindingRepository bindingRepository;
private final DeviceTokenRepository deviceTokenRepository;
private final CompletionEntryRepository completionEntryRepository;
private final EntityManager entityManager;
private final MobileRelayBlobStore blobStore;
/** 官网统一账户的内部记账口,注销传导用(dev-board#434)。 */
private final MobileBillingClient billing;
Expand All @@ -91,6 +96,8 @@ public AccountDeletionService(UserRepository userRepository,
MobileTransferRequestRepository transferRepository,
AccountBindingRepository bindingRepository,
DeviceTokenRepository deviceTokenRepository,
CompletionEntryRepository completionEntryRepository,
EntityManager entityManager,
MobileRelayBlobStore blobStore,
MobileBillingClient billing) {
this.userRepository = userRepository;
Expand All @@ -101,6 +108,8 @@ public AccountDeletionService(UserRepository userRepository,
this.transferRepository = transferRepository;
this.bindingRepository = bindingRepository;
this.deviceTokenRepository = deviceTokenRepository;
this.completionEntryRepository = completionEntryRepository;
this.entityManager = entityManager;
this.blobStore = blobStore;
this.billing = billing;
}
Expand All @@ -117,6 +126,12 @@ public Result deleteAccount(Long userId) {
// 官网侧先删(dev-board#434):失败即中止,本地一行都不动
propagateToUnifiedAccount(userId);

// 与 CompletionService.learn 的 scope 父行锁一致:等在途学习完成,再清词库与账号;
// 此锁释放前新的学习也无法越过父行校验留下 u:<id> 孤儿。
if (entityManager.find(com.checkba.model.entity.User.class, userId, LockModeType.PESSIMISTIC_WRITE) == null) {
throw new IllegalArgumentException("账号不存在或已注销");
}

List<MobileMediaInbox> items = inboxRepository.findByUserId(userId);
for (MobileMediaInbox item : items) {
if (item.getStoragePath() != null) {
Expand All @@ -130,6 +145,7 @@ public Result deleteAccount(Long userId) {
long sessions = sessionRepository.deleteByUserId(userId);
bindingRepository.deleteByUserId(userId);
deviceTokenRepository.deleteByUserId(userId);
completionEntryRepository.deleteByScopeKey("u:" + userId);
userRepository.deleteById(userId);

log.info("账号已注销 userId={}:影像 {}、项目目录 {}、设备 {}、传输请求 {}、会话 {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import com.checkba.repository.ProjectTaskRepository;
import com.checkba.repository.ProjectVariableRepository;
import com.checkba.model.entity.MemoryRemote;
import com.checkba.model.entity.CompletionEntry;
import com.checkba.repository.CompletionEntryRepository;
import com.checkba.repository.MemoryRemoteRepository;
import com.checkba.storage.ProjectStorageResolver;
import com.checkba.version.ProjectRepoService;
Expand Down Expand Up @@ -82,6 +84,7 @@ class ProjectDeleteCascadeTest {
@Autowired private WorkSessionRepository workSessionRepository;
@Autowired private MemoryRemoteRepository memoryRemoteRepository;
@Autowired private MemoryRepoService memoryRepoService;
@Autowired private CompletionEntryRepository completionEntryRepository;

@Test
void deleteProject_clearsEveryProjectScopedTableAndOnDiskDirectory() throws IOException {
Expand Down Expand Up @@ -184,6 +187,31 @@ void deleteProject_keepsUserOwnedLocalRootDirectory() throws IOException {
assertTrue(Files.exists(userFolder.resolve("用户自己的文件.txt")), "用户自己的文件夹被误删");
}

@Test
void deleteProject_clearsOnlyItsCompletionScope() {
Long projectId = seedProject();
Long otherProjectId = seedProject();
saveCompletion("p:" + projectId, "待删除项目词条");
saveCompletion("p:" + otherProjectId, "其他项目词条");
saveCompletion("u:9000", "个人词条");

projectService.deleteProject(projectId);

assertTrue(completionEntryRepository.findByScopeKeyOrderByLastUsedAtDescIdDesc("p:" + projectId).isEmpty());
assertFalse(completionEntryRepository.findByScopeKeyOrderByLastUsedAtDescIdDesc("p:" + otherProjectId).isEmpty());
assertFalse(completionEntryRepository.findByScopeKeyOrderByLastUsedAtDescIdDesc("u:9000").isEmpty());
}

private void saveCompletion(String scopeKey, String text) {
CompletionEntry entry = new CompletionEntry();
entry.setScopeKey(scopeKey);
entry.setText(text);
entry.setKind("PHRASE");
entry.setUses(1L);
entry.setLastUsedAt(LocalDateTime.now());
completionEntryRepository.save(entry);
}

private Long seededDocFileId;
private Long seededEvidenceFileId;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import com.checkba.service.mobile.MobileBillingFailureException;
import com.checkba.service.mobile.MobileBillingKind;
import com.checkba.service.mobile.MobileRelayBlobStore;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
Expand All @@ -35,6 +37,8 @@ private record Fixture(AccountDeletionService svc, UserRepository users,
MobileDeviceStateRepository devices,
MobileTransferRequestRepository transfers,
AccountBindingRepository bindings, DeviceTokenRepository tokens,
CompletionEntryRepository completions,
EntityManager entityManager,
MobileBillingClient billing) {}

private Fixture fixture(List<MobileMediaInbox> items) {
Expand All @@ -54,6 +58,9 @@ private Fixture fixture(List<MobileMediaInbox> items, String externalAccountId)
MobileTransferRequestRepository transfers = mock(MobileTransferRequestRepository.class);
AccountBindingRepository bindings = mock(AccountBindingRepository.class);
DeviceTokenRepository tokens = mock(DeviceTokenRepository.class);
CompletionEntryRepository completions = mock(CompletionEntryRepository.class);
EntityManager entityManager = mock(EntityManager.class);
when(entityManager.find(User.class, 7L, LockModeType.PESSIMISTIC_WRITE)).thenReturn(new User());
if (externalAccountId != null) {
AccountBinding row = new AccountBinding();
row.setUserId(7L);
Expand All @@ -64,8 +71,8 @@ private Fixture fixture(List<MobileMediaInbox> items, String externalAccountId)
}
MobileBillingClient billing = mock(MobileBillingClient.class);
return new Fixture(new AccountDeletionService(users, sessions, inbox, dirs, devices,
transfers, bindings, tokens, blobs, billing),
users, inbox, blobs, sessions, dirs, devices, transfers, bindings, tokens, billing);
transfers, bindings, tokens, completions, entityManager, blobs, billing),
users, inbox, blobs, sessions, dirs, devices, transfers, bindings, tokens, completions, entityManager, billing);
}

private static MobileMediaInbox item(String path) {
Expand All @@ -90,7 +97,13 @@ void deletesEverythingOwnedByTheUser() {
verify(f.sessions()).deleteByUserId(7L);
verify(f.bindings()).deleteByUserId(7L);
verify(f.tokens()).deleteByUserId(7L);
verify(f.completions()).deleteByScopeKey("u:7");
verify(f.completions(), never()).deleteByScopeKey("p:7");
verify(f.users()).deleteById(7L);
InOrder cleanupOrder = inOrder(f.entityManager(), f.completions(), f.users());
cleanupOrder.verify(f.entityManager()).find(User.class, 7L, LockModeType.PESSIMISTIC_WRITE);
cleanupOrder.verify(f.completions()).deleteByScopeKey("u:7");
cleanupOrder.verify(f.users()).deleteById(7L);
}

@Test
Expand Down Expand Up @@ -156,6 +169,8 @@ void blockedUpstreamDeletionAbortsLocalDeletion() {
verify(f.users(), never()).deleteById(any());
verify(f.bindings(), never()).deleteByUserId(any());
verify(f.blobs(), never()).deleteQuietly(any());
verifyNoInteractions(f.completions());
verifyNoInteractions(f.entityManager());
}

@Test
Expand All @@ -172,6 +187,8 @@ void unreachableUpstreamAbortsLocalDeletion() {
assertEquals(MobileBillingKind.UNAVAILABLE, e.getKind());
verify(f.users(), never()).deleteById(any());
verify(f.bindings(), never()).deleteByUserId(any());
verifyNoInteractions(f.completions());
verifyNoInteractions(f.entityManager());
}

@Test
Expand Down
Loading