
原神共享造物的屏蔽和一键删除这类需求经常出现在做内容聚合、尘歌壶分享站或者玩家工具的后台管理里。屏蔽解决的是“不想再看到某类内容”一键删除解决的则是“收藏了太多、缓存过期、批量数据不好维护”。两个功能看起来都只是按钮加接口但要做得不出事故需要把数据结构、权限、缓存、批量执行和恢复策略一起想清楚。我按一个普通内容工具项目的角度把从需求分析到落地实现这条链路完整拆一遍。文章会覆盖共享造物管理的典型数据模型、屏蔽功能怎么设计、一键删除怎么避免误删和超时以及真实开发中容易被忽略的几个排查点。不管是自己写个小工具还是在现成系统里加功能都可以按这个思路对照着看。1. 先想清楚屏蔽和一键删除解决的不是同一个问题1.1 共享造物模块里到底有什么样的数据原神共享造物在多数玩家工具或社区模块里承载的是玩家上传的家园设计、尘歌壶布局、摆设搭配方案有时还会附带复制用的代码或设计说明。每条记录通常包含作者信息、标题、封面图、内容标签、更新时间和归属用户。这类数据有一个特点看起来每个条目都很小但数量一旦上来管理复杂度会直线上升。有的作者会连续上传很多相似造物有的造物内容里带着明显不合规的标签有的玩家可能自己一键收藏了几百个造物时间久了根本分不清哪些还要用。所以不能把“屏蔽”和“一键删除”混成同一个操作。它们面向不同诉求也对应不同数据生命周期。屏蔽的本质是“我不想在当前列表里看到它”不代表它应该从服务器上消失。删除的本质是“我认为这条数据不再有价值”或者“我有权限清理它”这时候才需要真正变更数据状态。1.2 屏蔽是做个人过滤不是做全局下架很多初学者容易犯一个错误用户点“不感兴趣”后台就把这条造物状态置为删除。这个做法的后果很直接其他用户再也看不到这条共享造物作者也会发现自己上传的作品莫名其妙没了而且用户自己并没有真的删除它。正确逻辑应该把屏蔽理解成关系数据而不是造物本身的属性。也就是说一条共享造物对所有用户是否可见是状态 A而某个用户个人是否愿意看到它是状态 B。A 是全局的B 是用户级别的。当用户选择屏蔽某条造物或某个作者时要建立的是一条“该用户与该内容源之间不再匹配”的记录。下游列表查询时只对这个用户过滤掉这些内容其他用户的列表不受任何影响。如果做的是运营后台屏蔽又会有另一层含义管理员看到违规内容要把这类共享造物隐藏甚至下架。这种屏蔽一般由后台审核体系触发操作日志必须完整。个人屏蔽和管理员屏蔽不要共用一套接口否则用户端一个行为就可能影响全部人。1.3 一键删除处理的是本地收藏、脏数据和越积越多的缓存一键删除更适合用在这些场景用户想清空自己收藏的共享造物只保留最近常用的几个。用户自己上传过一批造物现在想全部下架并清除相关附件。本地缓存里积压了很多过期的图片和布局数据。运营人员需要按规则清理掉一批违规或举报达标的造物。删除前必须明确一个边界用户只能删除自己拥有管理权的数据或者删除本地客户端保存的临时数据。谁也没有能力通过游戏外工具删除别人共享造物在官方服务器里的永久数据。如果谁声称能“一键删除所有玩家的共享造物”基本可以认定是不可信甚至带欺骗性质的宣传。2. 数据结构设计屏蔽和删除不能共用一张表2.1 造物主表不要继续塞多余状态先把共享造物本身的表拆清楚。一个最小可运行的主表大概是这样CREATE TABLE shared_items ( id BIGINT PRIMARY KEY AUTO_INCREMENT, author_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, cover_url VARCHAR(500), content_json JSON, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX idx_author (author_id), INDEX idx_status_created (status, created_at) );这里的status字段主要负责内容自身的生命周期。建议用预留位来管理0草稿只有作者和管理员可见。1正常展示。2审核中。3运营手动下架。4作者删除或逻辑删除。我不推荐用同一个status字段去表达“这个作者被张三屏蔽了”。因为status是单值字段表达不了多对多关系。你如果增加位运算来表达“某个用户屏蔽”会让代码越写越绕查询也会越来越难懂。2.2 屏蔽关系单独建表个人屏蔽是典型的多对多关系一个用户可以屏蔽多个造物或作者一个造物也可能被多个用户屏蔽。正确做法是单独记录屏蔽关系CREATE TABLE user_blocks ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, target_type TINYINT NOT NULL COMMENT 1共享造物, 2作者/用户, target_id BIGINT NOT NULL, reason VARCHAR(255), created_at DATETIME NOT NULL, UNIQUE KEY uk_user_target (user_id, target_type, target_id), INDEX idx_target (target_type, target_id) );user_id是发起屏蔽的用户target_type表示屏蔽对象是具体某条造物还是某一个作者target_id是对应的 ID。这样设计有几个好处。第一用户取消屏蔽时能精确删除不会误伤其他内容。第二列表查询时可以用NOT EXISTS直接过滤。第三后续如果要做“屏蔽原因统计”可以单独加字段再分析。运营侧的全局面板不需要进这张表建议使用主表的status 3做运营下架因为运营下架是所有访客都不可见的全局操作。2.3 删除前如果可能误删回收站得留一份一键删除最容易出事的地方就是“没有后悔药”。批量清空后一旦发现误操作能不能恢复会直接影响用户信任。如果做的是普通工具或社区后台至少应该支持软删除。最稳妥的方案是建回收站快照表CREATE TABLE shared_items_recycle_bin ( id BIGINT PRIMARY KEY AUTO_INCREMENT, original_id BIGINT NOT NULL, operator_id BIGINT NOT NULL, snapshot_json JSON, operation_type TINYINT, deleted_at DATETIME NOT NULL, expired_at DATETIME NOT NULL, INDEX idx_original (original_id), INDEX idx_expired (expired_at) );删除时先把整条原始数据原样存到snapshot_json再把主表标记为已删除。这样至少能保证短时间内还能恢复内容。图片附件不建议立刻物理删除可以延迟到回收站过期后再清理否则会出现“原本内容记录还能还原但封面图和布局图已经没了”的尴尬情况。3. 屏蔽功能如何做到“屏蔽后立刻看不到”3.1 前端交互不能只搞一个静默按钮页面上的操作路径一般是卡片右上角更多菜单 - “不感兴趣”或者“屏蔽该作者”。点击后给用户两个选项会比较稳妥屏蔽这条共享造物屏蔽该作者发布的所有造物很多用户其实分不清自己要屏蔽的是“内容”还是“人”。如果只提供“不感兴趣”他屏蔽完作者之后该作者第二天发一条新内容又会出现在列表里用户就会觉得屏蔽失效了。所以要区分明确文案尽量降低理解成本。前端拿到后端返回成功后最好在当前列表里立刻把该卡片移除或者用占位卡片替代提示“已屏蔽可在设置里恢复”。如果只是把按钮状态改成已屏蔽而不更新列表用户会以为没生效。3.2 后端接口要支持幂等屏蔽接口可以设计成POST /v1/user/blocks { target_type: 1, target_id: 10086, reason: 不感兴趣 }后端返回{ code: 0, data: { block_id: 12345, already_blocked: false } }这里最关键的是幂等。同一个用户对同一条共享造物连续提交两次屏蔽请求第二次不应该报错。因为前端点击按钮后可能发生网络重试如果用户实际在弱网环境连续点击也可能产生重复请求。后端处理逻辑可以按这个顺序写检查 target_type 和 target_id 是否合法。检查资源是否存在。追加更细的权限某些造物如果已经下架允许屏蔽但需要给出提示。查询 user_blocks 表是否已有记录有则直接返回已存在。没有则新增记录。删除该用户的内容列表缓存。3.3 列表查询过滤时要在数据层做不要拿全量数据到内存里筛个人屏蔽常见的实现错误是从数据库查出全量共享造物再在内存里用屏蔽集合过滤。开发阶段数据量小几乎看不出问题当你有一万条共享造物用户又屏蔽了五千条每次列表请求都会造成严重的资源浪费。正确做法是在 SQL 层直接排除SELECT si.id, si.title, si.cover_url FROM shared_items si WHERE si.status 1 AND NOT EXISTS ( SELECT 1 FROM user_blocks ub WHERE ub.user_id ? AND ub.target_type 1 AND ub.target_id si.id ) ORDER BY si.created_at DESC LIMIT 20;如果还有屏蔽作者的需求查询会更复杂需要同时排除“作者被屏蔽”的情况AND NOT EXISTS ( SELECT 1 FROM user_blocks ub2 WHERE ub2.user_id ? AND ub2.target_type 2 AND ub2.target_id si.author_id )不要小看这两条NOT EXISTS。如果不处理作者级屏蔽就会出现“屏蔽了某作者但他以前上传的旧造物仍然在列表里”的现象。数据库索引方面要把user_blocks表上的(user_id, target_type, target_id)唯一索引建好target_type和target_id的联合索引最好也留一个避免后台按 target 反查时出现全表扫描。3.4 缓存更新只删当前用户这一条不要全局清理屏蔽能立刻生效的关键是缓存键设计。很多系统给造物列表做了 Redis 缓存但缓存键只包含分页页码没有包含当前用户 ID。结果用户屏蔽一条内容后后端即使删了这条缓存所有其他用户也一起挤进了缓存回源流程造成缓存雪崩。更合理的做法是每个用户有独立的列表缓存或者至少缓存键包含用户维度shared_items:list:{user_id}:{page}:{page_size}屏蔽成功后删除对应用户的列表缓存而不是执行一次模糊的全量删除。列表查询时如果命中缓存就直接返回没命中则从数据库取并回填缓存。这样屏蔽动作和缓存清理能做到精确联动。还有一种情况是屏蔽作者后作者后续新发布的造物也不能再出现在该用户列表里。这部分不能只靠删缓存解决必须在查询逻辑里加入作者级过滤否则用户会频繁反馈“我屏蔽的人还在出新内容”。4. 一键删除要按任务做不能裸写一个 DELETE4.1 删除前先定义清楚“一键”的范围“一键删除”这个叫法很容易让人误以为是不加确认地清空一切。在真实业务里所谓一键删除应该是一个“按条件批量删除”的快捷入口。常见的条件定义有删除用户本人收藏夹中所有共享造物后清空本地收藏列表。删除用户本人上传但已审核失败的造物。删除某个标签下的造物。删除某个作者发布的全部造物这个通常只有管理员能做。删除一周前已经进入回收站的数据。开发时先让用户选择范围再点击一键删除比直接提供一个“清空全部”按钮要安全得多。无条件全删很容易造成不可逆损失。同时删除前要给出确认弹窗弹窗里不能只写“确定删除”这种模糊文案最好明确写明影响范围例如“将删除你收藏的 28 个共享造物删除后可在回收站保留 7 天。”4.2 后端不要一次执行整批 DELETE如果用户收藏了 2000 条共享造物后端最坏的做法是拼出这样一条 SQLDELETE FROM user_favorites WHERE user_id 1 AND shared_item_id IN (1,2,3,...2000);这条 SQL 看起来能一次清完实际上风险很大事务可能锁很多行主表和其他关联表数据多时会造成锁等待执行过程中网络抖动可能导致连接断开事务回滚时用户会看到一个很久的加载状态一旦超过数据库超时时间后台会把整个请求标记为失败但实际可能已经删除了一部分数据。更保险的是分批删除。下面是一个最小示例BATCH_SIZE 100 def delete_favorites_by_user(user_id): total 0 while True: # 每次只取 100 条待删除 ID ids select_favorite_ids(user_id, limitBATCH_SIZE) if not ids: break # 先写入回收站 backup_items(operator_iduser_id, idsids) # 分批物理删除或逻辑删除 delete_user_favorites(user_id, ids) total len(ids) # 每批之间稍微停顿避免把数据库连接池打满 time.sleep(0.05) return total如果你希望删除过程中能被中断恢复建议把待删除 ID 先放到一个待处理队列表然后逐批消费。每处理完一批就把队列中的该批标记为成功。这样中途崩溃后重新运行时只需要扫队列里仍未完成的批次。4.3 一键删除必须做成幂等任务避免重复提交用户点击“一键删除”后前端通常会显示 loading。如果接口执行时间超过几秒用户可能会刷新页面再点一次导致重复删除。如果用物理删除第二次删除可能返回“没有数据”但如果先删除再重新收藏第二次请求就可能把用户新收藏的数据也删掉。想避免这个问题最简单的方式是引入 task_id用户点击一键删除后前端先向后端请求一个任务 ID。后端创建任务记录任务状态为 pending。后台线程或队列消费任务执行分批删除。前端通过轮询查询任务进度而不是一直停留在当前页面的等待状态。任务表大概长这样CREATE TABLE delete_tasks ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_uuid VARCHAR(64) NOT NULL UNIQUE, operator_id BIGINT NOT NULL, operation_type VARCHAR(64), filter_json JSON, total_count INT DEFAULT 0, processed_count INT DEFAULT 0, failed_ids JSON, status TINYINT DEFAULT 0 COMMENT 0排队中, 1执行中, 2成功, 3部分失败, 4失败, created_at DATETIME NOT NULL, finished_at DATETIME NULL );每次执行删除前先检查这个 operator 是否已有未完成的任务。如果有直接返回任务进度避免重复下单。4.4 删除关联数据时要注意顺序共享造物不会只存在于一张主表。一条共享造物可能关联评论数、点赞数、收藏数、作者统计、标签统计。一键删除时如果只删主表其他冗余数据会一直留在服务里慢慢变成脏数据。推荐顺序是先准备回收站快照。逻辑删除主表记录或物理删除主表。删除收藏关系。更新作者的造物数量统计。触发附件清理任务延迟删除图片文件。写入操作日志。这里的“更新统计”不要用count(*)全表统计最好直接对作者表做原子扣减UPDATE user_stats SET shared_item_count shared_item_count - 1 WHERE user_id ? AND shared_item_count 0;如果一条造物被收藏了很久每个用户都把它加进收藏关系那删除主表时还需要同步清理收藏表否则后续用户收藏列表会引用一条不存在的 ID。4.5 批量删除后要刷新列表缓存和前端分页一键删除成功后如果用户的收藏列表页是本地缓存的前端要刷新当前分页数据。否则会出现“明明删除了返回列表又看到那几条”的错乱体验。可以通过任务结束的响应里返回一个版本号前端拿到后把本地缓存的列表版本号加一下次请求自动拉新数据。后端如果发现请求中的版本号低于当前版本也可以直接返回“列表已过期”让前端重新加载。对后台运营人员来说一键删除后看到的成功提示需要具体一点比如“已尝试删除 328 条成功 326 条失败 2 条”。这样能帮助运营快速判断是 bug 还是因为部分记录已被删除导致。5. 屏蔽和删除功能最容易踩的坑5.1 屏蔽了还是能看到优先查缓存和过滤条件出现这个问题时不要第一反应就去查数据库里屏蔽表有没有数据应当按顺序排查屏蔽是否真的插入成功返回的 block_id 是否存在。列表查询 SQL 是否真的带上了当前 user_id 过滤条件。缓存键是否包含用户维度如果缓存键是全局的删除缓存后另一个用户访问还可能把老数据回填。是否为旧版本客户端走了旧的接口后端做了兼容但新过滤条件没生效。是否同时存在“屏蔽作者”和“屏蔽造物”两套关系查询时只判断了其中一种。从我的经验看“已经屏蔽了但列表里还能看到”有接近一半是缓存清理问题另一半是只过滤了 target_type1 而没有过滤 target_type2。尤其是用户选择屏蔽作者之后该作者老内容仍然显示的案例特别多。5.2 一键删除后记录又出现多半是同步或者本地缓存没有清干净如果后端已经删除了数据但用户刷新页面又看到了常见原因有删除是逻辑删除但查询时没有限制 status把已删除记录也查出来了。删除任务成功但本地列表缓存没有失效前端仍展示旧数据。删除任务执行了主表但收藏关系表没删某些页面用收藏关系反查内容。用户在多个设备上操作另一台设备内存里的列表没有收到更新通知。如果做的是移动端最简单的心跳同步方案是在 App 启动或列表下拉刷新时请求一个“用户数据版本号”版本号变了就重新拉取列表。如果做的是 Web 管理后台则只需要把当前用户相关缓存键都清掉即可。5.3 批量删除超时先看 SQL 是否用了大 IN 列表和长事务批量删除一旦超时不要先去升级服务器 CPU先定位慢 SQL 和事务范围。建议排查顺序打开慢查询日志查看是否有DELETE ... WHERE id IN (...)扫描行数过大。查看事务是否包裹了过多批次的删除事务每延长一秒锁冲突概率就会增加。查看前端是不是把几千个 ID 一次性带到了请求体里请求体过大也会造成后端解析慢。查看删除接口是否在时间循环里执行了同步回调比如每删一条就通知推送、发短信、写日志。确认删除是分批执行而不是一个大循环里调几千次单条删除。比较合理的做法是把单批删除数量控制在 100 到 500 之间每批都单独提交事务。如果启用了回收站同一批写入回收站和删除主表可以放在同一个事务里保证不会“备份了但没删掉”或“删掉了但没备份”。5.4 删除失败时能不能重试得先看失败原因批量任务里总会出现部分失败。失败可能来自数据库连接超时也可能来自某些记录被别的请求锁住还可能是因为个别数据本身已经不存在。任务失败后不能无脑重试整个批次。如果有一批 100 条记录中 2 条失败应该把失败的 ID 收集起来等待用户手动重试时只处理这 2 条。如果记录已经不存在可以直接判定为“该条已删除”然后在任务结果里标记为成功忽略。我用过一种比较稳妥的方式每一条删除任务都是一个独立小事务失败后将其 ID 放进 failed_ids 数组任务结束前把 failed_ids 存到任务表。运营端展示结果时可以通过这个数组直接定位是哪几条出了问题。5.5 权限漏洞往往出现在“一键”按钮上“一键删除”最大的安全风险不是按钮误触而是权限校验不严。曾经见过一些后台接口删除操作是前端传入user_id来指定清理谁的收藏。这等于把权限判断完全交给了恶意请求。只要攻击者改一下 user_id就能删除别人的收藏记录。后端处理时永远不要信任前端传入的 target owner。要从登录态或服务端会话中获取当前操作者的真实身份再判断是否有权清理该数据。current_user get_login_user(request) if not is_admin(current_user): if resource.owner_id ! current_user.id: raise PermissionDenied(只能删除自己的共享造物)如果是管理员一键下架违规造物最好还要区分“管理员 A”和“管理员 B”。防止某个管理账号被泄露后攻击者大规模清理平台内容。6. 普通用户视角的维护建议6.1 不要相信“清理别人共享造物”的外部工具如果你不是在开发后台只是普通原神玩家需要提个醒无论哪个游戏工具能提供“屏蔽”和“一键删除”的边界通常都只停留在你自己管理的范围里。你收藏的旧的共享造物自己可以清理你本地上传备份的缓存自己可以删除。但别人辛苦上传的共享造物你能做的只有“不再展示”或“取消收藏”并不会从原作者手里消失。如果遇到某个外部工具要求登录原神账号并宣称可以一键删除别人共享造物请默认它是高风险程序。不要因为“一键清理”听起来方便就随手授权。稳妥做法是优先使用官方渠道或可靠的本地工具。绝不把账号密码直接填给非官方网页。定期清理过期截图和临时缓存。删除前用笔记或截图留下自己之后还想用的布局代码。6.2 真正好用的一键删除应该具备什么特征不管是自己实现还是使用现成工具判断“一键删除”好不好用可以看几个标准删除前有没有明确提示影响范围。删除后能不能恢复能恢复多久。批量任务是同步等待还是先返回进度再异步执行。删除失败有没有日志和重试提示。删除完列表有没有自动刷新不会出现旧数据残留。权限是否合理普通用户不能乱删别人的数据。如果这几个标准都达不到那所谓一键删除很可能只是在裸写接口功能简单但风险不低。6.3 我的建议是先让单条操作变得可靠再开放批量有不少人在做共享造物管理功能时一开始就想把“屏蔽所有”、“删除全部”这种按钮做得很炫。其实这是错误优先级。先做单条屏蔽接口幂等、列表过滤生效、取消屏蔽正常再考虑批量屏蔽。先做单条删除回收站可用、权限校验正确、缓存刷新正常再开放一键删除的大范围规则。批量功能是在单条逻辑稳定之后的自然扩展不是为了做一个“一键”按钮。如果产品要求必须先上一键删除我会建议上线时默认开启二次确认并且设置只删除 7 天内未更新的收藏。这样即使误操作也能把损失控制在可恢复范围内。最后说一点原神共享造物的屏蔽和一键删除最终要处理的并不是“按钮按下去能不能删干净”而是数据关系、用户权限、缓存一致性和异常恢复这套组合问题。屏蔽如果只用状态位硬撑后面会越来越难扩展删除如果只做物理删除一次误操作就可能劝退用户。无论是个人工具还是团队后台我都会建议先把 filter 规则、回收站、任务队列、操作日志这些底层能力做扎实。功能表面上越简单底层的兜底手段就越要齐全。等你踩过一次“一键删完才发现漏了缓存”的坑就会明白这些设计不是过度工程而是让功能值得被长期信任的基础。