@@ -519,9 +519,10 @@ class BitwardenRepository(private val context: Context) {
519519 * 执行完整同步
520520 *
521521 * 同步流程:
522- * 1. 从服务器拉取最新数据
523- * 2. 上传本地创建的条目到服务器
524- * 3. 上传本地修改的条目到服务器
522+ * 1. 先处理本地待删除操作(delete)
523+ * 2. 上传本地创建的条目到服务器(create)
524+ * 3. 上传本地修改的条目到服务器(update)
525+ * 4. 从服务器拉取最新数据(pull)
525526 */
526527 suspend fun sync (vaultId : Long ): SyncResult = withContext(Dispatchers .IO ) {
527528 syncMutex.withLock {
@@ -547,21 +548,24 @@ class BitwardenRepository(private val context: Context) {
547548 }
548549 }
549550
550- // 1. 先上传本地创建的条目到服务器
551+ // 1. 先处理本地待删除操作(delete)
552+ val processedDeleteCount = syncService.processPendingOperations(vault, accessToken, symmetricKey)
553+
554+ // 2. 再上传本地创建的条目到服务器(create)
551555 val uploadResult = syncService.uploadLocalEntries(vault, accessToken, symmetricKey)
552556 val uploadedCount = when (uploadResult) {
553557 is takagi.ru.monica.bitwarden.service.UploadResult .Success -> uploadResult.uploaded
554558 else -> 0
555559 }
556560
557- // 2. 再上传本地已修改的条目到服务器
561+ // 3. 上传本地已修改的条目到服务器(update)
558562 val modifiedUploadResult = syncService.uploadModifiedEntries(vault, accessToken, symmetricKey)
559563 val modifiedUploadedCount = when (modifiedUploadResult) {
560564 is takagi.ru.monica.bitwarden.service.UploadResult .Success -> modifiedUploadResult.uploaded
561565 else -> 0
562566 }
563567
564- // 3 . 执行同步(从服务器拉取 )
568+ // 4 . 执行同步(pull )
565569 val result = syncService.fullSync(vault, accessToken, symmetricKey)
566570
567571 // 更新最后同步时间
@@ -570,7 +574,7 @@ class BitwardenRepository(private val context: Context) {
570574 when (result) {
571575 is ServiceSyncResult .Success -> {
572576 SyncResult .Success (
573- syncedCount = result.ciphersAdded + result.ciphersUpdated + uploadedCount + modifiedUploadedCount,
577+ syncedCount = result.ciphersAdded + result.ciphersUpdated + uploadedCount + modifiedUploadedCount + processedDeleteCount ,
574578 conflictCount = result.conflictsDetected
575579 )
576580 }
@@ -777,6 +781,103 @@ class BitwardenRepository(private val context: Context) {
777781 }
778782 }
779783
784+ suspend fun queueCipherDelete (
785+ vaultId : Long ,
786+ cipherId : String ,
787+ entryId : Long? = null,
788+ itemType : String = BitwardenPendingOperation .ITEM_TYPE_PASSWORD
789+ ): Result <Unit > = withContext(Dispatchers .IO ) {
790+ try {
791+ val vault = vaultDao.getVaultById(vaultId)
792+ ? : return @withContext Result .failure(IllegalStateException (" Vault 不存在" ))
793+
794+ // 已有待删除操作时直接复用,保证幂等。
795+ val existingDelete = pendingOpDao.findActiveDeleteByCipher(vaultId, cipherId)
796+ if (existingDelete != null ) {
797+ return @withContext Result .success(Unit )
798+ }
799+
800+ pendingOpDao.cancelActiveRestoreByCipher(vaultId, cipherId)
801+
802+ entryId?.let { id ->
803+ pendingOpDao.deletePendingForEntryAndType(id, itemType)
804+ }
805+
806+ pendingOpDao.insert(
807+ BitwardenPendingOperation (
808+ vaultId = vault.id,
809+ entryId = entryId,
810+ bitwardenCipherId = cipherId,
811+ itemType = itemType,
812+ operationType = BitwardenPendingOperation .OP_DELETE ,
813+ targetType = BitwardenPendingOperation .TARGET_CIPHER ,
814+ payloadJson = " {}" ,
815+ status = BitwardenPendingOperation .STATUS_PENDING
816+ )
817+ )
818+
819+ Result .success(Unit )
820+ } catch (e: Exception ) {
821+ Log .e(TAG , " 加入 Bitwarden 删除队列失败" , e)
822+ Result .failure(e)
823+ }
824+ }
825+
826+ suspend fun queueCipherRestore (
827+ vaultId : Long ,
828+ cipherId : String ,
829+ entryId : Long? = null,
830+ itemType : String = BitwardenPendingOperation .ITEM_TYPE_PASSWORD
831+ ): Result <Unit > = withContext(Dispatchers .IO ) {
832+ try {
833+ val vault = vaultDao.getVaultById(vaultId)
834+ ? : return @withContext Result .failure(IllegalStateException (" Vault 不存在" ))
835+
836+ val existingRestore = pendingOpDao.findActiveRestoreByCipher(vaultId, cipherId)
837+ if (existingRestore != null ) {
838+ return @withContext Result .success(Unit )
839+ }
840+
841+ val existingDelete = pendingOpDao.findActiveDeleteByCipher(vaultId, cipherId)
842+ if (existingDelete != null ) {
843+ pendingOpDao.cancelActiveDeleteByCipher(vaultId, cipherId)
844+ return @withContext Result .success(Unit )
845+ }
846+
847+ entryId?.let { id ->
848+ pendingOpDao.deletePendingForEntryAndType(id, itemType)
849+ }
850+
851+ pendingOpDao.insert(
852+ BitwardenPendingOperation (
853+ vaultId = vault.id,
854+ entryId = entryId,
855+ bitwardenCipherId = cipherId,
856+ itemType = itemType,
857+ operationType = BitwardenPendingOperation .OP_RESTORE ,
858+ targetType = BitwardenPendingOperation .TARGET_CIPHER ,
859+ payloadJson = " {}" ,
860+ status = BitwardenPendingOperation .STATUS_PENDING
861+ )
862+ )
863+
864+ Result .success(Unit )
865+ } catch (e: Exception ) {
866+ Log .e(TAG , " 加入 Bitwarden 恢复队列失败" , e)
867+ Result .failure(e)
868+ }
869+ }
870+
871+ suspend fun cancelPendingCipherDelete (vaultId : Long , cipherId : String ): Result <Unit > = withContext(Dispatchers .IO ) {
872+ try {
873+ pendingOpDao.cancelActiveDeleteByCipher(vaultId, cipherId)
874+ Result .success(Unit )
875+ } catch (e: Exception ) {
876+ Log .e(TAG , " 取消 Bitwarden 删除队列失败" , e)
877+ Result .failure(e)
878+ }
879+ }
880+
780881 suspend fun deleteCipher (vaultId : Long , cipherId : String ): Result <Unit > = withContext(Dispatchers .IO ) {
781882 try {
782883 val vault = vaultDao.getVaultById(vaultId)
@@ -815,6 +916,44 @@ class BitwardenRepository(private val context: Context) {
815916 }
816917 }
817918
919+ suspend fun permanentDeleteCipher (vaultId : Long , cipherId : String ): Result <Unit > = withContext(Dispatchers .IO ) {
920+ try {
921+ val vault = vaultDao.getVaultById(vaultId)
922+ ? : return @withContext Result .failure(IllegalStateException (" Vault 不存在" ))
923+
924+ if (! isVaultUnlocked(vaultId)) {
925+ return @withContext Result .failure(IllegalStateException (" Vault 未解锁" ))
926+ }
927+
928+ var accessToken = accessTokenCache[vaultId]
929+ ? : return @withContext Result .failure(IllegalStateException (" 令牌不可用" ))
930+
931+ val expiresAt = vault.accessTokenExpiresAt ? : 0
932+ if (expiresAt <= System .currentTimeMillis() + 60000 ) {
933+ val refreshed = refreshToken(vault)
934+ if (refreshed != null ) {
935+ accessToken = refreshed
936+ accessTokenCache[vaultId] = refreshed
937+ } else {
938+ return @withContext Result .failure(IllegalStateException (" Token 刷新失败,请重新登录" ))
939+ }
940+ }
941+
942+ val vaultApi = apiManager.getVaultApi(vault.apiUrl)
943+ val response = vaultApi.permanentDeleteCipher(" Bearer $accessToken " , cipherId)
944+ if (! response.isSuccessful && response.code() != 404 ) {
945+ return @withContext Result .failure(
946+ IllegalStateException (" 永久删除失败: ${response.code()} ${response.message()} " )
947+ )
948+ }
949+
950+ Result .success(Unit )
951+ } catch (e: Exception ) {
952+ Log .e(TAG , " 永久删除 Bitwarden Cipher 失败" , e)
953+ Result .failure(e)
954+ }
955+ }
956+
818957 suspend fun getSends (vaultId : Long ): List <BitwardenSend > = withContext(Dispatchers .IO ) {
819958 sendDao.getSendsByVault(vaultId)
820959 }
0 commit comments