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
+65
View File
@@ -48,6 +48,7 @@ func (s *Service) IngestToLibrary(role chain.Role, maCode, ctid, mediaAssetID, l
}); err != nil {
return err
}
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeIngest, Operator: libName, Detail: "审合格入媒资库"})
return s.chain.SetContentStatus(maCode, model.StatusInLibrary)
}
@@ -111,6 +112,7 @@ func (s *Service) InjectToCDN(role chain.Role, ctid, maCode, injectFileHash, ope
}); err != nil {
return InjectResult{}, err
}
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeInject, HashValue: injectFileHash, Operator: operatorID, Detail: "CDN 注入校验通过"})
return InjectResult{Allowed: true, DistributionID: distID}, nil
}
@@ -205,3 +207,66 @@ func (s *Service) ComputeSettlement(maCode, period string) (model.Settlement, er
}
return s.pb.ComputeSettlement(maCode, period, model.DefaultShareConfig())
}
// ---- 二期 F19/F20:追责取证与确权举证(需求22/23) ----
// Provenance 返回某 MA 码的全链路存证(需求22-AC1)。
func (s *Service) Provenance(maCode string) []model.ProvenanceEvent {
return s.prov.Trail(maCode)
}
// Accountability 责任界定取证:定位首次哈希变化节点与责任方(需求22-AC2)。
func (s *Service) Accountability(maCode string) model.AccountabilityReport {
return s.prov.Accountability(maCode)
}
// CopyrightEvidence 导出版权确权证据链(需求23-AC1/AC2)。
func (s *Service) CopyrightEvidence(maCode string) (model.CopyrightEvidence, error) {
c, err := s.chain.QueryContent(maCode)
if err != nil {
return model.CopyrightEvidence{}, err
}
trail := s.prov.Trail(maCode)
ev := model.CopyrightEvidence{
MACode: maCode, Title: c.Title, Issuer: c.Issuer, IssueDate: c.IssueDate,
ContentHash: c.FileHash, ChainAnchor: "chain://" + maCode, Trail: trail,
Statement: "本证据链由 MA 码、内容哈希与上链时间戳构成,遵循『谁先锁定谁有权』,不可抵赖,可用于侵权投诉与司法举证。",
}
for _, e := range trail {
if e.Node == model.NodeSubmit {
ev.FirstSeenAt = e.Timestamp
if ev.ContentHash == "" {
ev.ContentHash = e.HashValue
}
break
}
}
return ev, nil
}
// MatchInfringement 用感知哈希在已确权内容中检索疑似侵权(需求23-AC3)。
// threshold 为汉明距离阈值(<=high 高度相似,<=medium 中度相似)。
func (s *Service) MatchInfringement(perceptual string, high, medium int) ([]model.InfringeMatch, error) {
s.mu.Lock()
entries := make(map[string]phashEntry, len(s.phash))
for k, v := range s.phash {
entries[k] = v
}
s.mu.Unlock()
var out []model.InfringeMatch
for ma, e := range entries {
d, err := hash.HammingDistance(perceptual, e.Perceptual)
if err != nil {
continue // 长度不一致跳过
}
if d <= medium {
sim := "medium"
if d <= high {
sim = "high"
}
out = append(out, model.InfringeMatch{MACode: ma, Title: e.Title, Distance: d, Similarity: sim})
}
}
return out, nil
}