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
+26 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/model"
"github.com/tcs-iptv/tcs/internal/playback"
"github.com/tcs-iptv/tcs/internal/provenance"
)
// 业务错误。
@@ -51,6 +52,8 @@ type Service struct {
chain chain.Client
gen *macode.Generator
pb *playback.Store
prov *provenance.Store
phash map[string]phashEntry // maCode -> 感知哈希条目(确权侵权比对)
mu sync.Mutex
seqMu sync.Mutex
seqs map[string]int // 按前缀独立计数(REV/ctid/DIST 各自从 1 递增)
@@ -65,9 +68,21 @@ type reviewItem struct {
MACode string
}
// phashEntry 感知哈希注册项,用于确权侵权比对。
type phashEntry struct {
Title string
Perceptual string
}
// New 创建业务服务。
func New(c chain.Client, gen *macode.Generator) *Service {
return &Service{chain: c, gen: gen, pb: playback.NewStore(), seqs: make(map[string]int), reviews: make(map[string]*reviewItem)}
return &Service{
chain: c, gen: gen,
pb: playback.NewStore(),
prov: provenance.NewStore(),
phash: make(map[string]phashEntry),
seqs: make(map[string]int), reviews: make(map[string]*reviewItem),
}
}
func (s *Service) nextID(prefix string) string {
@@ -190,6 +205,16 @@ func (s *Service) ApproveAndIssue(role chain.Role, reviewID, issuer string) (Iss
PartyName: item.Sub.CPName,
})
// 记录全链路存证(送审→审核→发码)+ 注册感知哈希供确权比对
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeSubmit, HashValue: item.Sub.FileHash, Operator: item.Sub.CPName, Detail: "CP 送审"})
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeCSPSReview, Operator: "CSPS", Detail: "合规审核通过"})
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeIssue, HashValue: item.Sub.FileHash, Operator: issuer, Detail: "发码签发,绑定基准哈希"})
if item.Sub.Perceptual != "" {
s.mu.Lock()
s.phash[maCode] = phashEntry{Title: item.Sub.Title, Perceptual: item.Sub.Perceptual}
s.mu.Unlock()
}
cert := fmt.Sprintf("CERT|%s|%s|%s", maCode, item.Sub.FileHash, item.Sub.MerkleRoot)
return IssueResult{
MACode: maCode,