HarmonyOS宠物邻里实战第12篇:账号注销集成测试、关联数据清理与隐私闭环
HarmonyOS宠物邻里实战第12篇账号注销集成测试、关联数据清理与隐私闭环摘要账号注销不是删除一条用户记录。对宠物邻里这种包含宠物档案、社区帖子、评论互动、寄养需求、寄养申请、寄养记录、评价、通知和关注关系的项目来说注销账号如果清理不完整其他页面就会留下“幽灵数据”帖子还在但作者不存在、通知指向已删除用户、寄养记录找不到宠物、关注数不准确。本文基于宠物邻里 HarmonyOS Express MongoDB 项目复盘账号注销的完整工程闭环后端/auth/delete-account为什么必须要求密码二次确认deleteAccountData()如何按集合清理关联数据宠物、帖子、评论、互动、寄养、通知、关注、Session 分别怎么处理MongoDB 删除顺序为什么会影响数据一致性HarmonyOS 端AuthService.deleteAccount()如何清理本地登录态如何设计账号注销集成测试证明注销后没有残留最后给出后端、前端和数据库三层验收清单。这篇文章重点是隐私和数据一致性用户选择注销后系统必须给出可信的清理结果。工程背景与源码定位文件作用houduan/test/routes/api.js账号注销接口和关联数据清理houduan/test/test/integration.js后端集成测试入口houduan/test/db.jsMongoDB 连接MyApp/entry/src/main/ets/services/AuthService.etsHarmonyOS 账号注销和本地态清理MyApp/entry/src/main/ets/services/BackendService.ets调用/auth/delete-accountMyApp/entry/src/main/ets/pages/user/AccountDeletionPage.ets注销账号页面MyApp/entry/src/main/ets/common/MockStore.ets当前用户和本地快照验证命令cd D:\APP\chong_wu_guan_li\houduan\test npm run check npm run test:integrationnpm run check保证后端脚本没有语法错误npm run test:integration负责跑完整业务链路。账号注销测试可以作为集成测试的一部分也可以独立写一个test/delete-account.js。一、账号注销的风险点宠物邻里项目里用户数据不只在accounts集合里。一个活跃用户可能关联很多业务对象数据类型示例账号资料accounts、profiles、sessions宠物档案pets社区内容posts、comments社区互动postInteractions、commentInteractions社交关系follows寄养业务fosterRequests、fosterApplications、fosterRecords、fosterMessages、fosterReviews消息通知notices反馈记录feedback如果只删除accounts其余集合还会引用这个用户 ID。前端通过/bootstrap拉快照时就可能遇到作者为空、头像取不到、记录无法打开等问题。二、后端注销接口要求密码确认后端接口会再次校验密码router.post(/auth/delete-account,requireAuth,asyncfunction(req,res,next){try{constpasswordString(req.body.password||);constconfirmationString(req.body.confirmation||);if(confirmation!注销账号){returnfail(res,400,请输入正确的确认文本);}constdbgetDatabase();constaccountawaitdb.collection(accounts).findOne({username:req.auth.username});if(!account||!passwordMatches(password,account)){returnfail(res,401,密码错误);}awaitdeleteAccountData(db,req.auth.username,req.auth.profileId);ok(res,true);}catch(error){next(error);}});这类高风险操作不能只依赖当前登录态。即使用户已经登录也需要密码二次确认和明确文本确认避免误触。三、前端 BackendService 调用ArkTS 端通过BackendService调用注销接口classDeleteAccountPayload{password:string;confirmation:string;}staticasyncdeleteAccount(password:string):PromiseBackendAuthResult{try{constpayload:DeleteAccountPayloadnewDeleteAccountPayload();payload.passwordpassword;payload.confirmation注销账号;constresponse:ApiEnvelopeawaitApiClient.post(/auth/delete-account,payload);if(response.code0)returnBackendAuthResult.Ok;if(response.code401)returnBackendAuthResult.WrongPassword;returnBackendAuthResult.Error;}catch(e){console.error([BackendService] delete account failed: JSON.stringify(e));returnBackendAuthResult.Error;}}页面不直接拼接口也不直接写confirmation字段。确认文本由服务层统一发送页面只负责收集密码和用户确认。四、AuthService 清理本地登录态后端注销成功后AuthService.deleteAccount()清理本地状态staticasyncdeleteAccount(password:string):PromiseAuthResult{constresult:BackendAuthResultawaitBackendService.deleteAccount(password);if(resultBackendAuthResult.WrongPassword){returnAuthResult.WrongPassword;}if(result!BackendAuthResult.Ok){returnAuthResult.Error;}constusername:stringAuthService.activeUser;AuthService.usersAuthService.users.filter((item:AuthUserCredential)item.username!username);awaitAuthService.persistUsers();AuthService.activeUser;ApiClient.setAuthToken();MockStore.setCurrentUserId();AppStorage.setOrCreatestring(AppKeys.CURRENT_USER,);AppStorage.setOrCreatestring(AppKeys.CURRENT_PROFILE,);awaitAuthService.persistActive();returnAuthResult.Ok;}这里做了六件事从本地 users 列表移除账号持久化本地账号列表清空 activeUser清空ApiClientToken清空MockStore当前用户清空AppStorage当前用户和 profile。这一步不能省。后端数据删了如果前端还拿旧 Token 请求就会不断遇到 401。五、deleteAccountData 的清理范围后端核心是deleteAccountData()。它先找到和当前用户相关的 IDconstpetsawaitdb.collection(pets).find({ownerId:profileId}).project({id:1}).toArray();constpetIdspets.map((item)item.id);constrequestsawaitdb.collection(fosterRequests).find({authorId:profileId}).project({id:1}).toArray();constrequestIdsrequests.map((item)item.id);constrecordsawaitdb.collection(fosterRecords).find({$or:[{hostId:profileId},{petId:{$in:petIds}}]}).project({id:1}).toArray();constrecordIdsrecords.map((item)item.id);constpostsawaitdb.collection(posts).find({authorId:profileId}).project({id:1}).toArray();constpostIdsposts.map((item)item.id);先收集 ID 的原因是删除主数据之前要知道哪些评论、互动、记录和评价需要一起删。六、通知和反馈清理通知和反馈是最直接的用户关联数据awaitPromise.all([db.collection(feedback).deleteMany({userId:profileId}),db.collection(notices).deleteMany({$or:[{targetUserId:profileId},{fromUserId:profileId}]})]);通知要按两个方向删targetUserId是当前用户这是发给他的通知fromUserId是当前用户这是由他触发的通知。否则别人通知列表里可能还残留一个已注销用户触发的点赞、评论或关注消息。七、社区内容和互动清理社区相关集合包括帖子、评论、点赞收藏、评论点赞awaitPromise.all([db.collection(postInteractions).deleteMany({$or:[{userId:profileId},{postId:{$in:postIds}}]}),db.collection(commentInteractions).deleteMany({$or:[{userId:profileId},{commentId:{$in:deletedCommentIds}}]}),db.collection(comments).deleteMany({$or:[{authorId:profileId},{postId:{$in:postIds}}]}),db.collection(posts).deleteMany({authorId:profileId})]);这里要注意两类清理清理类型示例用户主动产生的互动用户给别人帖子点赞别人作用在用户内容上的互动别人给该用户帖子点赞如果删除用户帖子却不删postInteractions互动集合里会留下指向不存在帖子的记录。八、寄养业务清理寄养业务链路更复杂awaitPromise.all([db.collection(fosterMessages).deleteMany({$or:[{authorId:profileId},{reply.authorId:profileId},{requestId:{$in:requestIds}}]}),db.collection(fosterReviews).deleteMany({$or:[{authorId:profileId},{hostId:profileId},{recordId:{$in:recordIds}}]}),db.collection(fosterRecords).deleteMany({id:{$in:recordIds}}),db.collection(fosterApplications).deleteMany({$or:[{applicantId:profileId},{ownerId:profileId},{requestId:{$in:requestIds}}]}),db.collection(fosterRequests).deleteMany({authorId:profileId})]);寄养数据要考虑三种身份用户是需求发布者用户是申请人或寄养人用户是评价作者或被评价寄养人。只按authorId删除是不够的。九、关注关系和计数修正关注关系会影响两个用户的数字constfollowsawaitdb.collection(follows).find({$or:[{followerId:profileId},{followingId:profileId}]}).toArray();constaffectedProfiles[...newSet(follows.flatMap((item)[item.followerId,item.followingId]))].filter((id)id!profileId);删除关注关系后需要修正其他用户的关注数和粉丝数for(constidofaffectedProfiles){constfollowCountawaitdb.collection(follows).countDocuments({followerId:id});constfanCountawaitdb.collection(follows).countDocuments({followingId:id});awaitdb.collection(profiles).updateOne({id},{$set:{followCount,fanCount}});}这一步很容易被忽略。账号注销后如果不修正计数其他用户主页会显示错误的粉丝数。十、最后清理账号基础数据业务数据清理后才删除账号基础数据awaitdb.collection(sessions).deleteMany({profileId});awaitdb.collection(profiles).deleteOne({id:profileId});awaitdb.collection(accounts).deleteOne({username});awaitdb.collection(pets).deleteMany({ownerId:profileId});顺序上建议先收集关联 ID再删业务集合最后删账号和资料。否则先删 profile后面某些清理逻辑就很难根据 profile 找到关联数据。十一、集成测试如何设计账号注销测试可以分成五步注册 owner、follower、host 三个账号owner 创建宠物、帖子、寄养需求follower 点赞、评论、关注 ownerhost 申请寄养并生成记录owner 注销账号检查所有关联数据。伪代码如下asyncfunctionassertAccountDeleted(db,profileId,petIds,postIds,requestIds,recordIds){assert.strictEqual(awaitdb.collection(accounts).countDocuments({profileId}),0);assert.strictEqual(awaitdb.collection(profiles).countDocuments({id:profileId}),0);assert.strictEqual(awaitdb.collection(sessions).countDocuments({profileId}),0);assert.strictEqual(awaitdb.collection(pets).countDocuments({ownerId:profileId}),0);assert.strictEqual(awaitdb.collection(posts).countDocuments({authorId:profileId}),0);assert.strictEqual(awaitdb.collection(comments).countDocuments({$or:[{authorId:profileId},{postId:{$in:postIds}}]}),0);assert.strictEqual(awaitdb.collection(fosterRequests).countDocuments({authorId:profileId}),0);assert.strictEqual(awaitdb.collection(fosterRecords).countDocuments({id:{$in:recordIds}}),0);assert.strictEqual(awaitdb.collection(notices).countDocuments({$or:[{targetUserId:profileId},{fromUserId:profileId}]}),0);}测试重点不是接口返回 200而是数据库真的没有残留。十二、错误密码不能注销集成测试还要覆盖错误密码letresponseawaitapi(/auth/delete-account,POST,owner.token,{password:WrongPass123,confirmation:注销账号});assert.strictEqual(response.status,401);conststillExistsawaitdb.collection(accounts).findOne({username:owner.username});assert.ok(stillExists);这条断言证明错误密码不会误删账号。十三、确认文本也要测试确认文本错误时也应该失败responseawaitapi(/auth/delete-account,POST,owner.token,{password,confirmation:删除账号});assert.strictEqual(response.status,400);前端页面可以要求用户输入“注销账号”后端也必须校验。不要把确认文本只作为 UI 装饰。十四、注销后 Token 应失效账号注销成功后旧 Token 不能再访问接口responseawaitapi(/bootstrap,GET,owner.token);assert.strictEqual(response.status,401);这是因为sessions已经被删除。这个断言很重要它能证明后端没有留下可继续访问的会话。十五、HarmonyOS 前端验收前端注销成功后要验证验收点期望当前用户AuthService.currentUser()返回空登录态AuthService.isLoggedIn()为 falseTokenApiClient不再携带旧 TokenAppStorageCURRENT_USER和CURRENT_PROFILE被清空MockStore当前用户 ID 被清空页面跳转返回登录页或未登录首页再进通知/宠物页不显示已注销账号的数据前端不要等下一次启动再清理。注销成功后当前运行时状态就要清干净。十六、常见问题排查问题可能原因排查方式注销后还能访问接口Session 没删检查sessions.deleteMany({ profileId })其他人仍看到用户帖子posts或评论未删查posts.authorId和comments.postId通知列表残留已注销用户notices.fromUserId未清理检查通知删除条件粉丝数不对关注关系删除后未重算检查affectedProfiles寄养记录打不开记录引用的宠物已删但记录未删检查recordIds前端还显示已登录本地 activeUser 未清空检查AuthService.deleteAccount()错误密码也删了账号后端未校验 password检查passwordMatches()十七、发布前验收清单删除前先收集petIds、postIds、requestIds、recordIds错误密码返回 401确认文本错误返回 400删除账号后accounts、profiles、sessions都为空用户宠物、帖子、评论、互动全部清理寄养需求、申请、记录、留言、评价全部清理通知按 target 和 from 两个方向清理关注关系删除后其他用户计数重算旧 Token 访问/bootstrap返回 401HarmonyOS 本地 Token、当前用户、AppStorage 全部清空集成测试结束后不留下测试账号。总结账号注销是一个容易被低估的功能。它不是“删账号”这么简单而是一次全链路隐私清理后端要清账号、资料、会话、宠物、社区、寄养、通知、关注和互动前端要清 Token、当前用户、本地账号列表和页面状态。宠物邻里项目用deleteAccountData()集中处理 MongoDB 关联集合用BackendService.deleteAccount()封装接口语义用AuthService.deleteAccount()清理 HarmonyOS 本地登录态。再配合集成测试验证错误密码、确认文本、旧 Token 失效和数据库残留才能让账号注销真正可信。这类功能平时不显眼但一旦出错就是严重隐私问题。越早把注销链路做成可测试闭环后续业务扩展越安心。