跳转到主要内容

crayonxiaoxin

Flutter 应用中的用户级成人内容过滤:从拦截器到后端集中管理的架构演进

前言

Yam TV 作为一个多源视频聚合平台,需要对成人内容做双重过滤——系统级管理员配置的全局过滤 + 用户级个人偏好过滤。两个开关只要有一个开启,就应该触发过滤。本文记录这个功能从设计到实现、从复杂到简化的演进过程。

需求定义

  • 用户可以在设置中开启「成人内容过滤」开关
  • 过滤逻辑与系统级配置的过滤一致——同样的关键词、同样的源拦截规则
  • 用户级和系统级只要有一个开启就生效(OR 逻辑)
  • 每个用户/设备的偏好独立存储

方案一:Flutter 端 Dio 拦截器(失败)

第一个想法:在 Dio 拦截器中拦截所有请求,自动添加 ?adult=false 参数。

class AdultFilterInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    if (enabled && _targetPaths.any(...)) {
      options.queryParameters['adult'] = 'false';
    }
    handler.next(options);
  }
}

这个方案有三个问题:

  1. 竞态条件:拦截器在 App 启动时注册,但用户偏好从 SharedPreferences 异步加载,存在时间窗口内拦截器读不到正确值。
  2. 路径覆盖不全:需要维护一个 _targetPaths 列表,漏掉一个路径过滤就不生效。
  3. 参数传递冗余:每个请求都要携带 ?adult=false,增加带宽和日志噪音。

方案二:后端存储 + Flutter 同步 API(最终方案)

更好的方案:把用户偏好存到后端,后端在处理 API 请求时自动读取并过滤。Flutter 不需要在每个请求上传参。

// Flutter:开关变化时同步到后端
onChanged: (v) async {
  final dio = ref.read(dioProvider);
  await dio.put('/sync/adult-filter', data: {'enabled': v});
}

// 后端:存储到 KV
WatchService.setAdultFilter(ns, enabled);

// 后端:处理请求时读取
AdultFilterService.getEnabled(user, clientId);
// 在 sourceFeed/search 中调用 filterAdultContent()

后端过滤逻辑

新增 isAdultContent() 函数,不依赖 content_filter_enabled

function isAdultContent(item, cfg) {
  // 1. 源被标记为 adult → 过滤
  if (site.adult === true) return true;
  // 2. type_id 在黑名单 → 过滤
  if (blockedTypeIds.includes(typeId)) return true;
  // 3. 标题/名称匹配成人关键词 → 过滤
  if (blockedKeywords.some(kw => name.includes(kw))) return true;
  return false;
}

用户级和系统级的统一门逻辑(在所有接入点保持一致):

const shouldFilter = adultFilterEnabled || cfg.content_filter_enabled;
if (shouldFilter) {
  items = filterAdultContent(items, cfg);
}

架构图

用户开关 → PUT /sync/adult-filter
                ↓
         KV 存储 (adult-filter:{clientId})
                ↓
  处理请求时 → AdultFilterService.getEnabled()
                ↓
        OR 逻辑:系统级 || 用户级 → 过滤

接入点

  • 搜索(/search)
  • 流式搜索(/search/stream)
  • 源站列表(/search/source-feed)
  • 源列表(/search/sources)
  • 分类列表(/search/source-categories)

所有接入点使用完全相同的 || 门和完全相同的 filterAdultContent() 函数,保证过滤结果一致。

总结

最初打算在 Flutter 端用 Dio 拦截器在每个请求上加参数,但这引入了竞态条件和维护成本。最终方案将偏好存储在后端,后端自动读取并过滤——更简洁、更可靠、更易维护。这也符合「功能归属后端」的设计原则,避免前端与逻辑耦合。

讨论

还没有留言,来留下第一条评论吧!

留下足迹