第一章:FFmpeg + Go 字幕提取技术全景概览
现代音视频处理中,字幕提取是内容分析、无障碍适配与多语言分发的关键环节。FFmpeg 作为业界最成熟的多媒体框架,原生支持从 MP4、MKV、AVI 等容器中解析嵌入式字幕流(如 WebVTT、ASS、SRT、MOV_TEXT),而 Go 语言凭借其高并发能力、静态编译特性和简洁的 C 绑定机制,成为构建高效字幕提取服务的理想后端语言。
核心能力组合优势
- FFmpeg 提供底层解复用与解码能力:可精准识别字幕轨道索引、编码格式、时间戳范围及原始字节流;
- Go 实现安全可控的进程调用与内存管理:避免 Cgo 内存泄漏风险,同时通过
os/exec或github.com/moonfdd/ffmpeg-go等封装库实现低开销集成; - 跨平台一致性保障:Go 编译的二进制可直接携带 FFmpeg 静态链接库(如使用
ffmpeg-static),消除环境依赖差异。
典型提取流程示意
以下命令可快速导出 MKV 文件中第一条字幕流为独立 SRT 文件:
ffmpeg -i video.mkv -map 0:s:0 -c:s:srt subtitle.srt
其中 -map 0:s:0 表示选取第 0 个输入文件中的第 0 条字幕流(s 代表 subtitle),-c:s:srt 指定字幕编码器为 SRT 封装器(无需解码,仅复用时间戳与文本)。
支持的字幕类型与容器兼容性
| 容器格式 | 原生嵌入字幕类型 | FFmpeg 提取方式 |
|---|---|---|
| MKV | ASS, SRT, WebVTT, VobSub, PGS | 直接 -map 0:s 提取 |
| MP4 | TTML, WebVTT(需 ISO/IEC 14496-30) | 需 -c:s copy 保持原始编码 |
| MOV | QT Text, UTF-8 Plain Text | 可能需 -f srt -c:s:srt 转换 |
在 Go 中调用时,推荐使用结构化参数构造命令,例如:
cmd := exec.Command("ffmpeg",
"-i", "video.mkv",
"-map", "0:s:0",
"-c:s:srt",
"-y", // 覆盖输出
"output.srt")
该调用将阻塞执行并返回标准错误流,便于实时捕获轨道不存在或编码不支持等异常。
第二章:音轨分离与媒体流解析
2.1 FFmpeg 命令行原理与 Go 封装设计
FFmpeg 的命令行本质是构建一个参数驱动的管道拓扑:输入(-i)、滤镜链(-vf/-af)、编码器(-c:v libx264)与输出(out.mp4)通过进程标准流隐式连接。
核心执行模型
- 解析命令行 → 构建
AVFormatContext/AVCodecContext链表 - 按时序拉取帧、送入滤镜图(
avfilter_graph_run)、编码写入复用器 - 错误由
av_strerror()统一映射为可读码
Go 封装设计原则
- 零拷贝桥接:
C.avcodec_send_frame直接传递*C.AVFrame,避免 Go runtime GC 干预 - 上下文隔离:每个转码任务独占
*C.struct_AVFormatContext,规避全局状态污染
// 示例:安全启动 FFmpeg 子进程
cmd := exec.Command("ffmpeg",
"-i", input,
"-c:v", "libvpx-vp9",
"-b:v", "2M",
"-f", "webm",
output)
cmd.Stderr = &logWriter{} // 捕获详细错误日志
该调用等价于手动调用
avformat_open_input()+avcodec_parameters_to_context()流程。-b:v 2M被解析为codecCtx.bit_rate = 2_000_000,由 VP9 编码器在avcodec_encode_video2中生效。
| 封装层级 | 关键职责 | 安全边界 |
|---|---|---|
| CLI 包装层 | 参数校验、日志聚合 | 防止注入空格/分号 |
| C API 绑定层 | 内存生命周期管理(C.free 配对) |
禁止跨 goroutine 共享 *C.AVFrame |
graph TD
A[Go App] -->|构建参数| B[exec.Command]
B --> C[FFmpeg 进程]
C -->|stderr| D[结构化日志解析]
C -->|exit code| E[状态映射:AVERROR_EIO → os.ErrInvalid]
2.2 使用 goav 实现多格式视频元信息提取
goav 是基于 FFmpeg C API 的 Go 语言绑定库,支持跨平台、低开销地解析 MP4、AVI、MKV、MOV 等主流容器格式的元数据。
核心依赖与初始化
需启用 avformat 和 avcodec 模块,并调用 av.RegisterAll() 完成全局注册:
import "github.com/giorgisio/goav/avformat"
func extractMetadata(path string) {
avformat.AvRegisterAll() // 必须先注册所有格式与解码器
ctx := avformat.AvformatAllocContext()
defer ctx.AvformatCloseInput()
if ret := ctx.AvformatOpenInput(&path, nil, nil); ret < 0 {
panic("无法打开文件")
}
}
AvformatOpenInput自动探测容器格式并填充ctx.Ic中的流、时长、码率等字段;nil参数表示使用默认探测逻辑,适合通用场景。
支持格式对比
| 格式 | 容器识别 | 视频流解析 | 音频流解析 | 字幕流 |
|---|---|---|---|---|
| MP4 | ✅ | ✅ | ✅ | ✅ |
| MKV | ✅ | ✅ | ✅ | ✅ |
| AVI | ✅ | ✅ | ✅ | ❌ |
| FLV | ✅ | ✅ | ✅ | ❌ |
元信息提取流程
graph TD
A[打开输入上下文] --> B[读取流信息]
B --> C[遍历每个AVStream]
C --> D[获取codecpar参数]
D --> E[提取codec_type/width/height/duration]
2.3 音频轨道精准识别与单轨抽取实战
在多轨音视频文件中,音频流常混杂对话、背景音、音效等,需先定位目标轨道再精准剥离。
核心识别策略
- 通过
ffprobe提取流元数据,依据codec_type、tags:language、disposition:default综合判定主对白轨; - 优先匹配
language=zh且disposition:default=1的 AAC 流。
轨道索引确认(命令行)
ffprobe -v quiet -show_entries stream=index,codec_type,tags=language,disposition=default -of csv=p=0 input.mp4
输出示例:
0,video,,0、1,audio,zh,1、2,audio,en,0→ 确认音频轨索引为1。参数-of csv=p=0精简输出,避免解析干扰。
单轨抽取流程
ffmpeg -i input.mp4 -map 0:a:1 -c:a copy -y output_zh.aac
-map 0:a:1显式指定第0个输入文件的第2个音频流(索引从0起);-c:a copy启用无损直拷贝,规避重编码失真。
| 流类型 | 索引 | 语言 | 默认轨 | 用途 |
|---|---|---|---|---|
| video | 0 | — | — | 视频主轨 |
| audio | 1 | zh | ✓ | 中文对白 |
| audio | 2 | en | ✗ | 英文配音 |
graph TD
A[ffprobe元数据扫描] --> B{匹配zh+default=1?}
B -->|是| C[提取索引]
B -->|否| D[回退至language=zh最高quality流]
C --> E[ffmpeg -map 0:a:X -c:a copy]
2.4 采样率/位深标准化处理与缓冲区管理
音频流在混音、编解码或跨设备传输前,必须统一采样率与位深,否则将引发失真、爆音或同步断裂。
标准化策略选择
- 优先采用重采样+位深转换流水线(如
libsamplerate+libswresample) - 实时场景下启用动态缓冲区自适应:依据输入抖动自动伸缩环形缓冲区大小
环形缓冲区核心实现(C伪代码)
typedef struct {
int16_t *buf;
size_t head, tail, size; // size = 2^N,便于位运算取模
} ring_buffer_t;
// 写入时自动丢弃旧数据以维持实时性
void rb_write(ring_buffer_t *rb, const int16_t *data, size_t len) {
for (size_t i = 0; i < len; ++i) {
rb->buf[rb->head & (rb->size - 1)] = data[i];
rb->head++;
if (rb->head - rb->tail > rb->size) rb->tail++; // 溢出裁剪
}
}
逻辑说明:head & (size-1) 利用幂次缓冲区尺寸实现零开销取模;head-tail > size 保障写入不阻塞,适用于低延迟音频链路。
常见配置对照表
| 场景 | 推荐采样率 | 位深 | 缓冲区帧数 |
|---|---|---|---|
| VoIP通话 | 16 kHz | 16 | 64 |
| 音乐播放 | 48 kHz | 24 | 512 |
| ASR预处理 | 16 kHz | 16 | 128 |
graph TD
A[原始音频流] --> B{采样率匹配?}
B -->|否| C[重采样器 SRC]
B -->|是| D[位深对齐]
C --> D
D --> E[环形缓冲区写入]
E --> F[消费者线程读取]
2.5 并发安全的音轨切片与临时文件生命周期控制
音轨切片需在高并发场景下保证原子性与资源可回收性,核心挑战在于多 goroutine 同时写入、读取及清理同一临时目录。
数据同步机制
使用 sync.RWMutex 保护切片元数据映射,配合 time.AfterFunc 延迟触发清理:
var (
tempFiles = make(map[string]*tempTrack)
mu sync.RWMutex
)
func registerTempSlice(path string, duration time.Duration) {
mu.Lock()
tempFiles[path] = &tempTrack{createdAt: time.Now()}
mu.Unlock()
// 延迟清理,避免过早删除正在读取的文件
time.AfterFunc(duration*2, func() {
mu.Lock()
delete(tempFiles, path)
os.Remove(path) // 实际清理由 defer 或专用 GC 协程接管更稳妥
mu.Unlock()
})
}
逻辑说明:
registerTempSlice在注册切片时加写锁,确保map操作安全;AfterFunc中的清理需重新加锁,防止竞态删除。duration*2提供读取缓冲窗口,避免“文件已删但仍在解码”的典型 race。
生命周期管理策略
| 阶段 | 控制方式 | 安全保障 |
|---|---|---|
| 创建 | ioutil.TempFile |
唯一路径 + 0600 权限 |
| 使用中 | 引用计数 + 读锁 | 多协程可并发读,不可删 |
| 过期 | TTL + 定时扫描协程 | 避免 AfterFunc 漏删 |
graph TD
A[新音轨到达] --> B{并发切片请求}
B --> C[获取写锁 → 注册元数据]
C --> D[生成唯一临时路径]
D --> E[异步启动TTL监控]
E --> F[到期 → 加锁清理]
第三章:语音转写引擎集成与优化
3.1 Whisper.cpp 绑定与 CGO 调用链深度剖析
Whisper.cpp 的 Go 封装依赖 CGO 实现零拷贝跨语言调用,核心在于 whisper.h C API 与 Go runtime 的内存生命周期协同。
CGO 导入与符号可见性
// #include "whisper.h"
// #cgo LDFLAGS: -L./lib -lwhisper -lm -ldl -lpthread
// #cgo CFLAGS: -I./include -O2
import "C"
#cgo LDFLAGS 指定静态链接路径,CFLAGS 启用头文件搜索与优化;-lm 等为 whisper.cpp 依赖的数学与线程库。
关键调用链:Go → C → WASM/AVX 内核
graph TD
A[Go: whisper.NewContext] --> B[C: whisper_init_from_file]
B --> C[whisper_ctx_load_model → mmap()]
C --> D[whisper_encode/decode → AVX2 dispatch]
内存管理约束(表格)
| 对象类型 | 分配方 | 释放责任 | 备注 |
|---|---|---|---|
*C.struct_whisper_context |
C (whisper_init_*) |
Go 调用 C.whisper_free |
不可由 Go GC 回收 |
Audio buffer ([]float32) |
Go | Go GC | 需 C.CBytes 转为 *C.float 并手动 C.free |
CGO 调用需严格匹配 whisper.cpp 的 ABI 版本,否则触发段错误。
3.2 模型量化加载与内存映射式推理加速
模型量化将权重从 FP16/FP32 映射至 INT4/INT8,显著降低显存占用;内存映射(mmap)则绕过常规 I/O 缓存,直接将模型权重页按需加载至虚拟地址空间,避免全量加载延迟。
量化加载流程
- 加载
.safetensors量化权重文件(如qwen2-7b-int4.safetensors) - 解析
quantization_config.json获取bits=4、group_size=128、sym=True等参数 - 使用 AWQ/GPTQ 校准缩放因子(
scales)与零点(zeros)进行 dequantize-on-the-fly
内存映射推理示例
import mmap
import numpy as np
with open("model_q4_k.gguf", "rb") as f:
mmapped = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
# 仅在 tensor 访问时触发缺页中断,加载对应页
weight_slice = np.frombuffer(mmapped[0x1A000:0x1A800], dtype=np.int8) # 按需读取
mmap 参数说明:access=mmap.ACCESS_READ 启用只读共享映射;切片 [0x1A000:0x1A800] 触发内核按 4KB 页粒度加载,避免冗余 IO。
性能对比(7B 模型单卡推理)
| 加载方式 | 显存占用 | 首token延迟 | 吞吐(tokens/s) |
|---|---|---|---|
| 全量 FP16 加载 | 14.2 GB | 1280 ms | 18.3 |
| INT4 + mmap | 3.9 GB | 410 ms | 52.7 |
graph TD
A[读取量化权重文件] --> B[建立 mmap 虚拟地址映射]
B --> C[推理时访问 weight[i]]
C --> D{是否已加载该页?}
D -- 否 --> E[内核触发缺页中断]
E --> F[从磁盘异步加载 4KB 页到物理内存]
D -- 是 --> G[直接计算]
F --> G
3.3 中文语音上下文建模与标点恢复策略
中文语音识别输出常为无标点、无空格的连续文本流,上下文建模需兼顾语义连贯性与句法边界感知。
标点恢复的三阶段范式
- 边界检测:基于BiLSTM-CRF识别潜在句末位置(如“。”“?”“!”前的字)
- 标点分类:对候选位置输入BERT-wwm-ext微调,区分句号/问号/逗号/无标点
- 上下文重排序:引入n-gram语言模型对Top-3标点候选进行全局置信度重打分
模型输入特征设计
| 特征类型 | 示例值 | 说明 |
|---|---|---|
| 字符级上下文 | ['他','说','完','了'] |
前后各2字构成滑动窗口 |
| 语音停顿时长 | 1240ms |
ASR解码器输出的静音间隔 |
| 语义角色标注 | ARG0:他, PRED:说, ARG1:完 |
依存句法增强语义约束 |
# 标点候选生成模块(简化版)
def generate_punctuation_candidates(tokens, pause_durations):
candidates = []
for i, token in enumerate(tokens):
if i > 0 and pause_durations[i] > 800: # 长停顿触发候选
candidates.append({
'pos': i,
'context': tokens[max(0,i-2):i+3], # 5字窗口
'pause_ms': pause_durations[i]
})
return candidates # 返回结构化候选集供后续分类器使用
该函数以语音停顿时长为硬阈值触发候选,结合局部字符窗口保留上下文语义;pause_durations[i] > 800 经A/B测试验证在中文新闻播报场景下F1最优,兼顾召回与精度。
graph TD
A[原始ASR文本流] --> B{停顿时长 > 800ms?}
B -->|是| C[生成标点候选位置]
B -->|否| D[跳过]
C --> E[BERT标点分类器]
E --> F[语言模型重排序]
F --> G[带标点的结构化文本]
第四章:时间戳对齐与字幕结构重建
4.1 ASR 输出时间轴与原始音轨帧率对齐算法
数据同步机制
ASR模型输出的时间戳通常基于其内部采样率(如16kHz),而原始音轨可能为44.1kHz、48kHz等。直接映射会导致毫秒级累积偏移。
对齐核心公式
将ASR时间戳 $t{\text{asr}}$(单位:秒)映射至音轨帧索引:
$$
\text{frame_idx} = \left\lfloor t{\text{asr}} \times f{\text{audio}} + 0.5 \right\rfloor
$$
其中 $f{\text{audio}}$ 为音轨采样率(Hz)。
Python 实现示例
def asr_to_frame_index(asr_timestamps: list, audio_sr: int) -> list:
"""将ASR秒级时间戳转为整数帧索引,四舍五入取整"""
return [round(t * audio_sr) for t in asr_timestamps] # round()避免截断误差
逻辑说明:
round()确保0.5ms内对齐精度;audio_sr必须与原始WAV/MP3解码后实际帧率严格一致,否则引入系统性偏移。
| ASR 时间戳 (s) | 音轨采样率 (Hz) | 计算帧索引 |
|---|---|---|
| 1.234 | 48000 | 59232 |
| 2.789 | 48000 | 133872 |
graph TD
A[ASR输出秒级时间戳] --> B[乘以音轨采样率]
B --> C[四舍五入取整]
C --> D[得到精确音频帧索引]
4.2 基于VAD的静音段裁剪与语义分句合并
语音预处理中,VAD(Voice Activity Detection)是关键前置环节。它精准定位语音起止,为后续分句提供时序锚点。
静音段裁剪策略
采用 WebRTC VAD(mode=3)进行帧级检测,每20ms一帧,结合滑动窗口平滑决策:
import webrtcvad
vad = webrtcvad.Vad(3) # 最激进模式,抑制短时静音误判
# 输入需为16-bit PCM、16kHz单声道音频帧(长度320字节)
mode=3提升对弱语音/噪声环境鲁棒性;输入必须严格满足采样率与位深要求,否则触发断言异常。
语义分句合并逻辑
裁剪后仍存在因停顿过短导致的碎片化句子。需融合ASR置信度与标点预测结果:
| 合并条件 | 触发阈值 | 作用 |
|---|---|---|
| 相邻片段间隔 ≤ 300ms | 硬性时间约束 | 防止语义断裂 |
| 前句末尾标点置信度 | 软性语义约束 | 允许“因为…所以…”跨段合并 |
流程协同示意
graph TD
A[原始音频] --> B[VAD逐帧检测]
B --> C[静音段剔除]
C --> D[生成语音片段序列]
D --> E{间隔≤300ms?}
E -->|是| F[查标点置信度]
E -->|否| G[独立成句]
F -->|<0.7| G
F -->|≥0.7| H[保留分句边界]
4.3 SRT/ASS/TTML 多格式时间码精度校准(毫秒级)
字幕格式间的时间码解析精度差异常导致播放偏移。SRT 仅支持 HH:MM:SS,mmm(逗号分隔毫秒),ASS 支持 HH:MM:SS.mm(点分隔厘秒,实际按毫秒截断),TTML 则采用 ISO 8601 hh:mm:ss.SSS 或分数形式(如 PT1.234S)。
数据同步机制
统一将各格式时间戳归一化为整数毫秒(int64),规避浮点误差:
def parse_srt_time(s: str) -> int:
# 示例:'00:01:23,456' → 83456 ms
h, m, s_ms = s.split(':')
s, ms = s_ms.split(',')
return int(h)*3600000 + int(m)*60000 + int(s)*1000 + int(ms)
逻辑:严格按千分位拆分,ms 部分直接转 int,避免 float('0.456') 引入 IEEE 754 误差(如 0.456 实际存储为 0.45599999999999996)。
格式对齐策略
| 格式 | 原生精度 | 解析建议 | 典型偏差风险 |
|---|---|---|---|
| SRT | 毫秒 | 直接截取三位数字 | 无 |
| ASS | 厘秒 | 取前三位并舍入 | .1234 → 123 ms |
| TTML | 微秒级 | round(float*1000) |
需防浮点累积误差 |
graph TD
A[原始时间字符串] --> B{格式识别}
B -->|SRT| C[逗号分割→整数拼接]
B -->|ASS| D[点分割→取前3位+舍入]
B -->|TTML| E[ISO解析→乘1000→round]
C & D & E --> F[统一int64毫秒]
4.4 字幕样式注入与双语嵌套结构生成(含字体/位置/颜色)
字幕渲染需在单个 <div> 容器内实现中英双语分层叠加,同时支持独立样式控制。
样式注入机制
通过 CSSStyleSheet.insertRule() 动态注入带命名空间的 CSS 规则,避免全局污染:
/* 注入规则示例 */
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.subtitle-zh {
font-family: "PingFang SC", sans-serif;
color: #333;
top: 75%;
}
.subtitle-en {
font-family: "Helvetica Neue", Arial, sans-serif;
color: #666;
top: 82%;
}
逻辑分析:
top值以视口百分比设定,确保响应式定位;font-family分设中西文字体栈,兼顾渲染一致性与 fallback 安全性。
双语嵌套 DOM 结构
<div class="subtitle-track">
<div class="subtitle-zh" style="font-size:18px;">你好,世界</div>
<div class="subtitle-en" style="font-size:16px;">Hello, world</div>
</div>
| 属性 | 中文层 | 英文层 |
|---|---|---|
font-size |
18px(主视觉) | 16px(辅助层) |
color |
#333 |
#666 |
z-index |
2 | 1 |
渲染流程
graph TD
A[解析SRT/ASS时间轴] --> B[构建双语DOM节点]
B --> C[注入动态CSS规则]
C --> D[应用transform过渡动画]
D --> E[挂载至video父容器]
第五章:工程化落地与未来演进方向
工程化落地的典型实践路径
某头部电商平台在2023年Q4完成大模型推理服务的规模化部署,将LLM能力嵌入订单智能审核、客服话术生成、营销文案辅助三大核心场景。其工程化关键动作包括:构建统一模型服务网关(基于Triton Inference Server + Kubernetes Operator),实现模型热更新与AB测试灰度发布;通过ONNX Runtime + TensorRT量化将Llama-3-8B推理延迟从1.2s压降至380ms(P95);建立模型性能看板,实时监控GPU显存占用率、请求成功率、token吞吐量(TPS)等17项SLO指标。该平台日均处理230万次推理请求,错误率稳定低于0.017%。
持续交付流水线设计
以下为实际运行的CI/CD流水线关键阶段:
| 阶段 | 工具链 | 自动化动作 | SLA |
|---|---|---|---|
| 模型验证 | MLflow + Great Expectations | 数据漂移检测、输出分布校验、对抗样本鲁棒性测试 | ≤4min |
| 推理压测 | Locust + Prometheus | 模拟5000并发QPS下的P99延迟与OOM概率 | ≤2.1s |
| 安全扫描 | Snyk + Hugging Face Hub Policy Engine | 检测模型权重中恶意代码、训练数据版权风险、PII泄露 | 100%覆盖 |
多模态协同推理架构
采用分层路由策略应对异构计算负载:文本理解模块部署于A100集群(FP16精度),图像特征提取模块运行于Jetson AGX Orin边缘节点(INT8量化),语音转写服务则调度至专用vLLM实例池。Mermaid流程图展示请求流转逻辑:
graph LR
A[HTTP请求] --> B{Content-Type}
B -->|text/plain| C[Text Router]
B -->|image/jpeg| D[Image Router]
C --> E[A100 Cluster<br/>Llama-3-8B]
D --> F[Orin Edge Node<br/>CLIP-ViT-L]
E --> G[融合结果组装]
F --> G
G --> H[JSON响应]
模型即代码的生产就绪方案
团队将模型配置、提示模板、后处理规则全部纳入GitOps管理,示例YAML片段定义电商客服场景的推理参数:
model: huggingface://meta-llama/Llama-3-8B-Instruct
temperature: 0.3
max_new_tokens: 256
stop_sequences: ["\n用户:", "<|eot_id|>"]
prompt_template: |
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
你是一名专业电商客服助手,请用中文回答,禁止虚构促销政策。
<|eot_id|><|start_header_id|>user<|end_header_id|>
{{query}}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
边缘-云协同演进趋势
2024年已启动“轻量模型联邦学习”试点:在12万台IoT设备端部署TinyLlama-1.1B(仅280MB),每台设备本地微调后上传梯度而非原始数据;云端聚合服务器采用Secure Aggregation协议,在不暴露终端行为的前提下完成模型迭代。首轮实验显示退货原因识别准确率提升11.3%,且终端平均带宽消耗降低至87KB/日。
可观测性深度集成
将LangChain Tracer与OpenTelemetry Collector对接,实现跨服务链路追踪:从用户点击“智能推荐”按钮开始,完整记录向量检索耗时、RAG重排序延迟、大模型生成token序列及后处理耗时。在Prometheus中构建llm_inference_duration_seconds_bucket直方图,按model_name、prompt_length_bucket、output_length_bucket三维度打标,支持分钟级定位长尾请求根因。
合规性工程化约束
依据《生成式AI服务管理暂行办法》,所有对外服务接口强制启用内容安全网关:集成百度文心ERNIE-4.0审核模型(本地化部署)+ 规则引擎(正则匹配+关键词倒排索引),对输出文本实施三级拦截——实时阻断涉政敏感词、异步标记潜在歧视表述、离线审计生成逻辑偏差。审计日志留存周期严格遵循90天法定要求,并通过Hash链上存证确保不可篡改。
