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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user