计算哈希 #
什么是策略模式 策略模式就是:把一组可以互相替换的算法或处理方式单独封装起来,在运行时根据条件选择其中一种,而不是把所有逻辑硬写死在一大坨 if/switch 里。
一句话理解: 同一个目标,不同的做法,可切换。
它的特点
- 都是在解决同一类问题,只是实现方式不同。
- 调用方只关心“我要做这件事”,不关心具体算法细节。
- 可以在运行时根据参数、配置、环境选择不同策略。
- 新增一种做法时,通常不用改主流程太多,扩展性更好。
- 能把复杂分支拆开,减少一个函数里堆太多细节。
这里为什么说用到了策略模式 在 hash.go 里:
这两个分支做的事情本质上是同一件事:计算哈希。 但它们的处理策略完全不同:
- CalcSourceMediaHash 处理的是源介质,可能是物理盘、逻辑盘、E01 镜像,disk.go
- CalcFilesHash 处理的是文件集合,要先批量取数据、并发算哈希、汇总结果、更新进度,file.go
所以这里适合用“策略”的原因是:
- 目标相同:都是哈希计算任务
- 输入相近:都基于 HashCalcParam
- 实现差异大:一个偏介质读取,一个偏文件批处理
- 运行时选择:由 ScopeType 决定用哪种做法
扫描 #
模板方法模式 #
什么是: 把整体流程骨架固定下来,把可变步骤留给具体任务实现。
特点: 主流程统一。 变化点集中在少数钩子方法。 框架负责生命周期,业务负责具体动作。
这里为什么用到了: 真正的任务启动、初始化、状态更新、退出清理,是任务框架统一调度的,最后才回调到具体任务
为什么这样做: 扫描任务很多,如果每个任务都自己写一套“初始化语言、起协程、上报状态、异常清理”,代码会非常散。用模板方法后,公共部分进框架,任务本身只写扫描逻辑。
策略模式 #
什么是: 同一目标下,准备多种可替换的处理方式,运行时根据条件选择其中一种。
特点: 目标一致,实现不同。 运行时可切换。 把大分支拆成独立策略。
这里为什么用到了: 深度扫描里并不是只有一种“扫法”。 在 deepscan.go 开始,会根据品牌、是否指定品牌、分区品牌等条件,选择不同扫描策略:
- performSingleShardmountScan
- deepByFeatureRecovery
- deepByMount
- deepByShardmount
更典型的是 deepscan.go,processPartitions() 固定了“遍历所有分区”的通用流程,而把具体扫描动作通过 action func(…) error 传进去。 这就是很典型的“骨架相同,动作可替换”。
为什么这样做: 深度扫描面对的介质和品牌差异很大,但整体目标都是“扫描分区并恢复数据”。如果不做策略拆分,这个函数会变成一大坨难维护的分支。
生产者-消费者模式 #
什么是: 一端负责生产数据,另一端负责消费数据,中间通过通道或队列解耦。
特点: 生产和消费解耦。 支持异步处理。 便于批量提交和限流。
这里为什么用到了: 扫描过程不断产出 File、VideoTapeInfo、PictureInfo、RecoveredFile 等数据,统一写入 UnifiedDataSubChannel;而 DataSub() 后台协程负责从通道中取出数据、分批提交,data.go。
任务入口里先启动消费者,再开始扫描:deepscan.go。
为什么这样做: 扫描是高频产出,写库和提交节点是相对慢操作。两者解耦后,扫描线程不用每次都等落库完成,吞吐更稳。
分析 #
策略模式 #
什么是策略模式: 把一组可以互相替换的处理方式封装起来,在运行时根据条件选择其中一种,而不是把所有逻辑硬塞进一个函数里。
特点是什么: 同一个目标,不同的实现方式。 运行时可切换。 新增一种策略时,主流程通常不用大改。 调用方关心“做什么”,不关心“具体怎么做”。
这里为什么说用了策略模式: 在 analyticsTask.go (line 127) 的事件循环里,收到 taskQueue 中的任务后,会根据 taskR.taskType 选择不同处理策略:
- Mark 走 basic.CreateRemarkTask
- Frame 走 basic.CreateFrameTask
- ObjectDetection 走 detection.CreateODTask
- SceneDetection 走 detection.CreateSDTask
- ImageDetection 走 advanced.CreateIDTask
- AdvancedDetection 走 advanced.CreateADTask
这些任务本质上都属于“视频分析任务”,但实现路径完全不同,所以这里适合做策略分发。
为什么要这样做: 因为“打标”“抽帧”“目标检测”“场景检测”“高级检测”这几类任务的参数、节点结构、调用 AI 的方式都不一样。 如果不拆开,OnParse() 最后会变成一个巨大的业务垃圾场。
工厂模式 #
什么是工厂模式: 把对象创建逻辑集中起来,外部只拿结果对象,不自己关心具体该 new 哪个类、怎么配置。
特点是什么: 创建逻辑集中。 调用方和具体实现解耦。 适合“同一种接口,多种实现”的场景。
这里为什么说用了工厂模式: 在 AI 检测任务里,真正干活的不是入口函数,而是各种 TaskProcessor。 这个接口定义在 interface.go (line 7)。 它的具体创建,由 factory.go (line 38) 的 CreateProcessor() 负责。
比如:
- ObjectDetection / SceneDetection 走 YOLO processor
- ImageDetection 会尝试 Face / ReID / MLLM+YOLO
- AdvancedDetection 走高级 processor
为什么要这样做: 因为“分析任务类型”和“底层 AI 处理器类型”不是一一简单对应的。 例如 ImageDetection 不是固定走一个实现,而是可能先试人脸,再试 ReID,再试 MLLM。 如果不把创建逻辑集中在工厂里,业务层会到处写 if/switch/new/apply options,非常难维护。
这里和策略模式的区别是: 策略模式回答的是“这次要执行哪种行为”; 工厂模式回答的是“这次要创建哪种对象”。
模板方法模式 #
什么是模板方法模式: 把一个流程的固定骨架写在父类或框架里,把可变步骤留给子类或具体实现去覆盖。
特点是什么: 流程顺序固定。 变化点受控。 框架掌握生命周期,业务只填空。
这里其实有两层模板方法。
第一层,在任务框架里: VideoAnalysisTask 嵌入了 task.ParseBase,analyticsTask.go (line 22)。 外层框架统一负责 RunTask -> Start -> initTask -> OnBeforeParse -> OnParse -> exit,见 task.go (line 55) 和 task.go (line 79)。 也就是说,这个视频分析任务只需要实现 OnParse(),生命周期由框架统一控制。
第二层,在 AI 分析子任务里: AiAnalyticsClass 定义了统一分析骨架,interface.go (line 31)。 StartTask() 里固定了“校验引擎 -> BeginTask -> 成功/失败收尾”的流程,interface.go (line 136)。 而真正变化的部分,比如 VideoProcess、SubmitData、UpdateProgress,由具体 AI 任务实现。
为什么要这样做: 因为所有分析任务都有一套共同流程:
- 找到视频
- 初始化 AI 环境
- 遍历视频
- 更新进度
- 发结果消息
- 失败时统一处理
这套骨架非常适合模板方法,否则每个 AI 子任务都要复制一份。
修复 #
装饰器模式 #
什么是: 装饰器模式是在不改变原对象接口的前提下,给它额外加功能。
特点: 保留原接口。 在外面包一层。 新增行为而不侵入原逻辑。 非常适合 I/O 流包装。
这里为什么说用了: ProgressWriter 明确就是一个装饰器,write.go (line 8)。 它内部包了一个 io.Writer,但在 Write() 的时候,除了真正写数据,还额外做了两件事:
- 检查是否取消
- 计算并更新进度
它的使用位置也很典型,比如 common.go (line 379) 的 fileCopy() 和 handle_mp4.go (line 142) 的 fixFtyp()。
为什么要这样做: 因为底层复制逻辑仍然想沿用标准的 io.CopyN。 如果不加装饰器,你就得把“取消判断”和“进度计算”散落到每次写入逻辑里。 用了 ProgressWriter 后,调用方只需要把原来的 writer 换成包装后的 writer,I/O 代码本身不用改。
导出 #
建造者模式 #
什么是建造者模式: 当一个对象构建过程比较复杂、配置项比较多时,用 Builder 分步组装,而不是把所有参数一次性塞进构造函数。
特点是什么: 适合参数多、步骤多、可选项多的对象。 调用链清晰。 避免“超长构造函数”。 构建过程和使用过程分离。
这里为什么说用了建造者模式: PDF 导出是标准的 fluent builder 写法,pdf.go (line 11):
- WithProvider(…)
- WithOutputPath(…)
- WithFonts(…)
- WithTitle(…)
- WithSubtitle(…)
- WithVersion(…)
- WithLogoPath(…)
- Build()
为什么要这样做: 因为导出报告不是“给个路径就完了”,它需要组装很多东西:
- 输出路径
- 标题、副标题
- 字体
- Logo
- 版本号
- 数据提供器
- 页面生成器
- 文件生成器
如果不用 Builder,构造函数会非常难看,也很容易传错参数。 Builder 的好处是:每一步都显式可读,后面新增配置也容易加。
组合模式 #
什么是组合模式: 把“单个对象”和“对象组合”统一成同一种结构处理,最典型的就是树结构。
特点是什么: 叶子节点和父节点结构统一。 递归处理简单。 特别适合目录树、菜单树、报告树。
这里为什么说用了: HTML 报告里定义了 TreeNode,interfaces.go (line 29),它里面有:
- Name
- Model
- Page
- Children []*TreeNode
这就是典型的组合结构。 在 tree.go (line 55) 开始,代码会把“概览”“视频扫描”“图片扫描”“日志扫描”“视频修复”“视频分析”等节点统一拼成一棵树;有的节点是叶子,有的节点下面再挂子节点,比如视频分析结果节点下面还会分基础分析和 AI 分析。
为什么要这样做: 因为报告目录本来就是层级结构。 如果不用组合模式,你就得给一级节点、二级节点、三级节点分别写不同结构,处理逻辑会非常别扭。 有了统一的 TreeNode 后,不管是叶子还是父节点,都能按同一套方式生成和渲染。