feat(phase2): 追责取证与确权举证(F19/F20)

- internal/provenance: 全链路存证(送审/审核/发码/转码/入库/注入)+责任界定
- service: Provenance/Accountability(定位首次哈希变化节点)/CopyrightEvidence/MatchInfringement
- api: /content/provenance, /accountability, /evidence, /infringe-match
- 转码版哈希不误判为篡改; 感知哈希侵权比对分级(high/medium)
- 11项新测试通过; 端到端: 审播一致判定+证据链+侵权命中
This commit is contained in:
selfrelease
2026-06-14 17:13:58 +08:00
parent f44c53c5bb
commit dc3095a2d5
7 changed files with 405 additions and 15 deletions
@@ -0,0 +1,81 @@
// Package provenance 记录内容全链路存证事件,支撑追责取证与确权举证(二期 F19/F20)。
// 对应需求22(责任界定)、需求23(确权维权)。
// MVP 用内存存储;生产落审计链 + PostgreSQL,保证不可篡改。
package provenance
import (
"sync"
"time"
"github.com/tcs-iptv/tcs/internal/model"
)
// Store 全链路存证存储。
type Store struct {
mu sync.RWMutex
trails map[string][]model.ProvenanceEvent // maCode -> 时间序事件
}
// NewStore 创建存证存储。
func NewStore() *Store {
return &Store{trails: make(map[string][]model.ProvenanceEvent)}
}
// Record 追加一条存证事件(按时间序)。
func (s *Store) Record(e model.ProvenanceEvent) {
if e.Timestamp.IsZero() {
e.Timestamp = time.Now()
}
s.mu.Lock()
defer s.mu.Unlock()
s.trails[e.MACode] = append(s.trails[e.MACode], e)
}
// Trail 返回某 MA 码的全链路存证(需求22-AC1)。
func (s *Store) Trail(maCode string) []model.ProvenanceEvent {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]model.ProvenanceEvent, len(s.trails[maCode]))
copy(out, s.trails[maCode])
return out
}
// Accountability 责任界定:以发码基准哈希为准,定位首次发生哈希变化的节点(需求22-AC2)。
func (s *Store) Accountability(maCode string) model.AccountabilityReport {
trail := s.Trail(maCode)
report := model.AccountabilityReport{MACode: maCode, Trail: trail, Consistent: true}
// 基准哈希 = 发码节点(NodeIssue)的哈希
for _, e := range trail {
if e.Node == model.NodeIssue && e.HashValue != "" {
report.BaselineHash = e.HashValue
break
}
}
if report.BaselineHash == "" {
report.Conclusion = "未发码或无基准哈希,无法判定"
report.Consistent = false
return report
}
// 检查后续携带哈希的节点是否与基准一致
for i := range trail {
e := trail[i]
if e.HashValue == "" || e.Node == model.NodeIssue || e.Node == model.NodeSubmit {
continue
}
// 转码节点哈希本就不同(合法),跳过
if e.Node == model.NodeTranscode {
continue
}
if e.HashValue != report.BaselineHash {
report.Consistent = false
ev := e
report.FirstChange = &ev
report.Conclusion = "在【" + string(e.Node) + "】节点(" + e.Operator + ")检出哈希与发码基准不一致,疑似该环节偷换/篡改"
return report
}
}
report.Conclusion = "全链路哈希与发码基准一致,审播一致,无偷换"
return report
}