feat: 更新CC码监管方案业务版文档,新增摘要、UGC专项、直播专项、存量处置、误判申诉、水印嵌入、CC码生命周期、组织保障等章节;同步macode到cccode重命名及其他代码变更

This commit is contained in:
freedakgmail
2026-07-20 22:25:40 +08:00
parent 9f1e2d0a21
commit eb4a82a877
76 changed files with 2600 additions and 2122 deletions
+92 -92
View File
@@ -12,11 +12,11 @@ import (
// 严格执行合约级业务规则:签发权限、1:1 不可解绑、映射前置签发、防重复哈希。
type MemoryChain struct {
mu sync.RWMutex
contents map[string]model.Content // maCode -> Content
bindings map[string][]model.HashBinding // maCode -> bindings
mappings map[string][]model.Mapping // maCode -> mappings
contents map[string]model.Content // ccCode -> Content
bindings map[string][]model.HashBinding // ccCode -> bindings
mappings map[string][]model.Mapping // ccCode -> mappings
versions map[string][]model.VersionChange
hashIndex map[string]string // fileHash -> maCode(防换壳重发)
hashIndex map[string]string // fileHash -> ccCode(防换壳重发)
txSeq int
}
@@ -44,7 +44,7 @@ func (m *MemoryChain) IssueMA(role Role, req IssueRequest) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.contents[req.MACode]; ok {
if _, ok := m.contents[req.CCCode]; ok {
return "", ErrMAAlreadyIssued
}
if existing, ok := m.hashIndex[req.FileHash]; ok {
@@ -52,15 +52,15 @@ func (m *MemoryChain) IssueMA(role Role, req IssueRequest) (string, error) {
}
c := req.Content
c.MACode = req.MACode
c.CCCode = req.CCCode
c.ContentTwinID = req.ContentTwinID
c.Status = model.StatusApproved
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now()
}
m.contents[req.MACode] = c
m.contents[req.CCCode] = c
m.bindings[req.MACode] = []model.HashBinding{{
m.bindings[req.CCCode] = []model.HashBinding{{
ContentTwinID: req.ContentTwinID,
HashType: model.HashFile,
HashValue: req.FileHash,
@@ -69,7 +69,7 @@ func (m *MemoryChain) IssueMA(role Role, req IssueRequest) (string, error) {
CreatedBy: string(RoleRegulator),
}}
if req.PerceptualHash != "" {
m.bindings[req.MACode] = append(m.bindings[req.MACode], model.HashBinding{
m.bindings[req.CCCode] = append(m.bindings[req.CCCode], model.HashBinding{
ContentTwinID: req.ContentTwinID,
HashType: model.HashPerceptual,
HashValue: req.PerceptualHash,
@@ -77,11 +77,11 @@ func (m *MemoryChain) IssueMA(role Role, req IssueRequest) (string, error) {
CreatedBy: string(RoleRegulator),
})
}
m.hashIndex[req.FileHash] = req.MACode
m.hashIndex[req.FileHash] = req.CCCode
// 集级哈希绑定(分集内容):每集独立哈希,挂在同一 MA 码下。
for _, ep := range req.Episodes {
m.bindings[req.MACode] = append(m.bindings[req.MACode], model.HashBinding{
m.bindings[req.CCCode] = append(m.bindings[req.CCCode], model.HashBinding{
ContentTwinID: req.ContentTwinID,
HashType: model.HashFile,
HashValue: ep.FileSHA256,
@@ -94,7 +94,7 @@ func (m *MemoryChain) IssueMA(role Role, req IssueRequest) (string, error) {
})
if ep.FileSHA256 != "" {
if _, ok := m.hashIndex[ep.FileSHA256]; !ok {
m.hashIndex[ep.FileSHA256] = req.MACode
m.hashIndex[ep.FileSHA256] = req.CCCode
}
}
}
@@ -109,14 +109,14 @@ func (m *MemoryChain) RegisterHashBinding(role Role, b model.HashBinding) (strin
m.mu.Lock()
defer m.mu.Unlock()
maCode := m.maCodeByCTID(b.ContentTwinID)
if maCode == "" {
ccCode := m.ccCodeByCTID(b.ContentTwinID)
if ccCode == "" {
return "", ErrMANotIssued
}
m.bindings[maCode] = append(m.bindings[maCode], b)
m.bindings[ccCode] = append(m.bindings[ccCode], b)
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
if _, ok := m.hashIndex[b.HashValue]; !ok {
m.hashIndex[b.HashValue] = maCode
m.hashIndex[b.HashValue] = ccCode
}
}
return m.nextTx("registerHashBinding"), nil
@@ -127,28 +127,28 @@ func (m *MemoryChain) RegisterMapping(role Role, mp model.Mapping) (string, erro
m.mu.Lock()
defer m.mu.Unlock()
maCode := m.maCodeByCTID(mp.ContentTwinID)
if maCode == "" {
ccCode := m.ccCodeByCTID(mp.ContentTwinID)
if ccCode == "" {
return "", ErrMANotIssued
}
m.mappings[maCode] = append(m.mappings[maCode], mp)
m.mappings[ccCode] = append(m.mappings[ccCode], mp)
return m.nextTx("registerMapping"), nil
}
// VerifyHash 按 MA 码校验提交哈希。
func (m *MemoryChain) VerifyHash(maCode, fileHash string) (VerifyResult, error) {
func (m *MemoryChain) VerifyHash(ccCode, fileHash string) (VerifyResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
bs, ok := m.bindings[maCode]
bs, ok := m.bindings[ccCode]
if !ok {
return VerifyResult{Valid: false, MACode: maCode, SubmittedHash: fileHash}, ErrMANotIssued
return VerifyResult{Valid: false, CCCode: ccCode, SubmittedHash: fileHash}, ErrMANotIssued
}
for _, b := range bs {
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
if b.HashValue == fileHash {
return VerifyResult{
Valid: true, MACode: maCode,
Valid: true, CCCode: ccCode,
BoundHash: b.HashValue, SubmittedHash: fileHash,
Match: true, Version: b.Version,
}, nil
@@ -163,7 +163,7 @@ func (m *MemoryChain) VerifyHash(maCode, fileHash string) (VerifyResult, error)
break
}
}
return VerifyResult{Valid: true, MACode: maCode, BoundHash: bound, SubmittedHash: fileHash, Match: false}, nil
return VerifyResult{Valid: true, CCCode: ccCode, BoundHash: bound, SubmittedHash: fileHash, Match: false}, nil
}
// HashExists 判断内容哈希是否已存在。
@@ -175,12 +175,12 @@ func (m *MemoryChain) HashExists(fileHash string) (string, bool) {
}
// VerifyEpisodeHash 按 MA 码+集号校验该集哈希。
func (m *MemoryChain) VerifyEpisodeHash(maCode string, episode int, fileHash string) (VerifyResult, error) {
func (m *MemoryChain) VerifyEpisodeHash(ccCode string, episode int, fileHash string) (VerifyResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
bs, ok := m.bindings[maCode]
bs, ok := m.bindings[ccCode]
if !ok {
return VerifyResult{Valid: false, MACode: maCode, SubmittedHash: fileHash}, ErrMANotIssued
return VerifyResult{Valid: false, CCCode: ccCode, SubmittedHash: fileHash}, ErrMANotIssued
}
var bound string
for _, b := range bs {
@@ -190,7 +190,7 @@ func (m *MemoryChain) VerifyEpisodeHash(maCode string, episode int, fileHash str
}
if b.HashValue == fileHash {
return VerifyResult{
Valid: true, MACode: maCode,
Valid: true, CCCode: ccCode,
BoundHash: b.HashValue, SubmittedHash: fileHash,
Match: true, Version: b.Version,
}, nil
@@ -198,16 +198,16 @@ func (m *MemoryChain) VerifyEpisodeHash(maCode string, episode int, fileHash str
}
}
if bound == "" {
return VerifyResult{Valid: false, MACode: maCode, SubmittedHash: fileHash}, ErrNotFound
return VerifyResult{Valid: false, CCCode: ccCode, SubmittedHash: fileHash}, ErrNotFound
}
return VerifyResult{Valid: true, MACode: maCode, BoundHash: bound, SubmittedHash: fileHash, Match: false}, nil
return VerifyResult{Valid: true, CCCode: ccCode, BoundHash: bound, SubmittedHash: fileHash, Match: false}, nil
}
// ListEpisodes 返回某 MA 码下的全部集级哈希绑定(episode > 0)。
func (m *MemoryChain) ListEpisodes(maCode string) ([]model.HashBinding, error) {
func (m *MemoryChain) ListEpisodes(ccCode string) ([]model.HashBinding, error) {
m.mu.RLock()
defer m.mu.RUnlock()
bs, ok := m.bindings[maCode]
bs, ok := m.bindings[ccCode]
if !ok {
return nil, ErrMANotIssued
}
@@ -221,10 +221,10 @@ func (m *MemoryChain) ListEpisodes(maCode string) ([]model.HashBinding, error) {
}
// QueryContent 查询内容主记录。
func (m *MemoryChain) QueryContent(maCode string) (model.Content, error) {
func (m *MemoryChain) QueryContent(ccCode string) (model.Content, error) {
m.mu.RLock()
defer m.mu.RUnlock()
c, ok := m.contents[maCode]
c, ok := m.contents[ccCode]
if !ok {
return model.Content{}, ErrNotFound
}
@@ -252,14 +252,14 @@ func (m *MemoryChain) ListContents(status string) ([]model.Content, error) {
}
// QueryMappings 查询 MA 码绑定的全部映射与 CDN 端点。
func (m *MemoryChain) QueryMappings(maCode string) (MappingsResult, error) {
func (m *MemoryChain) QueryMappings(ccCode string) (MappingsResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if _, ok := m.contents[maCode]; !ok {
if _, ok := m.contents[ccCode]; !ok {
return MappingsResult{}, ErrNotFound
}
res := MappingsResult{MACode: maCode, Mappings: m.mappings[maCode]}
for _, mp := range m.mappings[maCode] {
res := MappingsResult{CCCode: ccCode, Mappings: m.mappings[ccCode]}
for _, mp := range m.mappings[ccCode] {
if mp.CDNEndpoint != "" {
res.CDNEndpoints = append(res.CDNEndpoints, mp.CDNEndpoint)
}
@@ -271,30 +271,30 @@ func (m *MemoryChain) QueryMappings(maCode string) (MappingsResult, error) {
func (m *MemoryChain) RecordVersionChange(vc model.VersionChange) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
maCode := m.maCodeByCTID(vc.ContentTwinID)
if maCode == "" {
ccCode := m.ccCodeByCTID(vc.ContentTwinID)
if ccCode == "" {
return "", ErrMANotIssued
}
m.versions[maCode] = append(m.versions[maCode], vc)
m.versions[ccCode] = append(m.versions[ccCode], vc)
return m.nextTx("recordVersionChange"), nil
}
// Revoke 下架,仅监管主体。
func (m *MemoryChain) Revoke(role Role, maCode, reason string) (MappingsResult, error) {
func (m *MemoryChain) Revoke(role Role, ccCode, reason string) (MappingsResult, error) {
if role != RoleRegulator {
return MappingsResult{}, ErrPermissionDenied
}
m.mu.Lock()
defer m.mu.Unlock()
c, ok := m.contents[maCode]
c, ok := m.contents[ccCode]
if !ok {
return MappingsResult{}, ErrNotFound
}
c.Status = model.StatusRevoked
m.contents[maCode] = c
m.contents[ccCode] = c
res := MappingsResult{MACode: maCode, Mappings: m.mappings[maCode]}
for _, mp := range m.mappings[maCode] {
res := MappingsResult{CCCode: ccCode, Mappings: m.mappings[ccCode]}
for _, mp := range m.mappings[ccCode] {
if mp.CDNEndpoint != "" {
res.CDNEndpoints = append(res.CDNEndpoints, mp.CDNEndpoint)
}
@@ -303,13 +303,13 @@ func (m *MemoryChain) Revoke(role Role, maCode, reason string) (MappingsResult,
}
// RevokeEpisode 集级下架:只下架指定集,整剧其他集不受影响(仅监管主体)。
func (m *MemoryChain) RevokeEpisode(role Role, maCode string, episode int, reason string) error {
func (m *MemoryChain) RevokeEpisode(role Role, ccCode string, episode int, reason string) error {
if role != RoleRegulator {
return ErrPermissionDenied
}
m.mu.Lock()
defer m.mu.Unlock()
bs, ok := m.bindings[maCode]
bs, ok := m.bindings[ccCode]
if !ok {
return ErrMANotIssued
}
@@ -324,34 +324,34 @@ func (m *MemoryChain) RevokeEpisode(role Role, maCode string, episode int, reaso
if !found {
return ErrNotFound
}
m.bindings[maCode] = bs
m.bindings[ccCode] = bs
return nil
}
// Restore 恢复上架整剧:下架状态恢复为流通中(仅监管主体)。
func (m *MemoryChain) Restore(role Role, maCode string) error {
func (m *MemoryChain) Restore(role Role, ccCode string) error {
if role != RoleRegulator {
return ErrPermissionDenied
}
m.mu.Lock()
defer m.mu.Unlock()
c, ok := m.contents[maCode]
c, ok := m.contents[ccCode]
if !ok {
return ErrNotFound
}
c.Status = model.StatusPublished
m.contents[maCode] = c
m.contents[ccCode] = c
return nil
}
// RestoreEpisode 恢复上架指定集(仅监管主体)。
func (m *MemoryChain) RestoreEpisode(role Role, maCode string, episode int) error {
func (m *MemoryChain) RestoreEpisode(role Role, ccCode string, episode int) error {
if role != RoleRegulator {
return ErrPermissionDenied
}
m.mu.Lock()
defer m.mu.Unlock()
bs, ok := m.bindings[maCode]
bs, ok := m.bindings[ccCode]
if !ok {
return ErrMANotIssued
}
@@ -366,25 +366,25 @@ func (m *MemoryChain) RestoreEpisode(role Role, maCode string, episode int) erro
if !found {
return ErrNotFound
}
m.bindings[maCode] = bs
m.bindings[ccCode] = bs
return nil
}
// SetContentStatus 更新内容状态。
func (m *MemoryChain) SetContentStatus(maCode, status string) error {
func (m *MemoryChain) SetContentStatus(ccCode, status string) error {
m.mu.Lock()
defer m.mu.Unlock()
c, ok := m.contents[maCode]
c, ok := m.contents[ccCode]
if !ok {
return ErrNotFound
}
c.Status = status
m.contents[maCode] = c
m.contents[ccCode] = c
return nil
}
// maCodeByCTID 内部辅助:通过 CTID 反查 MA 码(调用方已持锁)。
func (m *MemoryChain) maCodeByCTID(ctid string) string {
// ccCodeByCTID 内部辅助:通过 CTID 反查 MA 码(调用方已持锁)。
func (m *MemoryChain) ccCodeByCTID(ctid string) string {
for ma, c := range m.contents {
if c.ContentTwinID == ctid {
return ma
@@ -397,15 +397,15 @@ func (m *MemoryChain) maCodeByCTID(ctid string) string {
func (m *MemoryChain) QueryByHash(fileHash string) (model.ContentQueryResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
maCode, ok := m.hashIndex[fileHash]
ccCode, ok := m.hashIndex[fileHash]
if !ok {
return model.ContentQueryResult{Found: false}, ErrNotFound
}
return model.ContentQueryResult{
Found: true,
Content: m.contents[maCode],
Bindings: m.bindings[maCode],
Mappings: m.mappings[maCode],
Content: m.contents[ccCode],
Bindings: m.bindings[ccCode],
Mappings: m.mappings[ccCode],
}, nil
}
@@ -413,13 +413,13 @@ func (m *MemoryChain) QueryByHash(fileHash string) (model.ContentQueryResult, er
func (m *MemoryChain) QueryByProvincialCode(provincialCode string) (model.ContentQueryResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
for maCode, maps := range m.mappings {
for ccCode, maps := range m.mappings {
for _, mp := range maps {
if mp.Party == model.PartyCP && mp.PartyID == provincialCode {
return model.ContentQueryResult{
Found: true,
Content: m.contents[maCode],
Bindings: m.bindings[maCode],
Content: m.contents[ccCode],
Bindings: m.bindings[ccCode],
Mappings: maps,
}, nil
}
@@ -432,13 +432,13 @@ func (m *MemoryChain) QueryByProvincialCode(provincialCode string) (model.Conten
func (m *MemoryChain) QueryByLibraryFileID(libraryFileID string) (model.ContentQueryResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
for maCode, maps := range m.mappings {
for ccCode, maps := range m.mappings {
for _, mp := range maps {
if mp.Party == model.PartyReviewer && mp.PartyID == libraryFileID {
return model.ContentQueryResult{
Found: true,
Content: m.contents[maCode],
Bindings: m.bindings[maCode],
Content: m.contents[ccCode],
Bindings: m.bindings[ccCode],
Mappings: maps,
}, nil
}
@@ -456,7 +456,7 @@ func (m *MemoryChain) MergeMA(role Role, req model.MergeRequest) (model.MergeRes
m.mu.Lock()
defer m.mu.Unlock()
primary, ok := m.contents[req.PrimaryMACode]
primary, ok := m.contents[req.PrimaryCCCode]
if !ok {
return model.MergeResult{}, ErrNotFound
}
@@ -464,7 +464,7 @@ func (m *MemoryChain) MergeMA(role Role, req model.MergeRequest) (model.MergeRes
migratedBindings := 0
migratedMappings := 0
for _, secMA := range req.SecondaryMACodes {
for _, secMA := range req.SecondaryCCCodes {
secContent, exists := m.contents[secMA]
if !exists {
return model.MergeResult{}, fmt.Errorf("%w: secondary MA %s not found", ErrNotFound, secMA)
@@ -476,21 +476,21 @@ func (m *MemoryChain) MergeMA(role Role, req model.MergeRequest) (model.MergeRes
// 迁移哈希绑定至主 MA 码
for _, b := range m.bindings[secMA] {
b.ContentTwinID = primary.ContentTwinID
m.bindings[req.PrimaryMACode] = append(m.bindings[req.PrimaryMACode], b)
m.bindings[req.PrimaryCCCode] = append(m.bindings[req.PrimaryCCCode], b)
migratedBindings++
}
// 迁移映射至主 MA 码
for _, mp := range m.mappings[secMA] {
mp.ContentTwinID = primary.ContentTwinID
m.mappings[req.PrimaryMACode] = append(m.mappings[req.PrimaryMACode], mp)
m.mappings[req.PrimaryCCCode] = append(m.mappings[req.PrimaryCCCode], mp)
migratedMappings++
}
// 更新哈希索引指向主 MA 码
for _, b := range m.bindings[secMA] {
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
m.hashIndex[b.HashValue] = req.PrimaryMACode
m.hashIndex[b.HashValue] = req.PrimaryCCCode
}
}
@@ -503,8 +503,8 @@ func (m *MemoryChain) MergeMA(role Role, req model.MergeRequest) (model.MergeRes
txID := m.nextTx("mergeMA")
return model.MergeResult{
PrimaryMACode: req.PrimaryMACode,
MergedMACodes: req.SecondaryMACodes,
PrimaryCCCode: req.PrimaryCCCode,
MergedCCCodes: req.SecondaryCCCodes,
MigratedBindings: migratedBindings,
MigratedMappings: migratedMappings,
TxID: txID,
@@ -520,30 +520,30 @@ func (m *MemoryChain) SplitMA(role Role, req model.SplitRequest) (model.SplitRes
m.mu.Lock()
defer m.mu.Unlock()
srcContent, ok := m.contents[req.SourceMACode]
srcContent, ok := m.contents[req.SourceCCCode]
if !ok {
return model.SplitResult{}, ErrNotFound
}
if srcContent.Status == model.StatusMerged || srcContent.Status == model.StatusSplit {
return model.SplitResult{}, fmt.Errorf("chain: MA %s already %s", req.SourceMACode, srcContent.Status)
return model.SplitResult{}, fmt.Errorf("chain: MA %s already %s", req.SourceCCCode, srcContent.Status)
}
migratedBindings := 0
migratedMappings := 0
newMACodes := make([]string, 0, len(req.Splits))
newCCCodes := make([]string, 0, len(req.Splits))
for _, split := range req.Splits {
// 创建新内容记录
newContent := srcContent
newContent.MACode = split.NewMACode
newContent.ContentTwinID = split.NewMACode + "-ctid"
newContent.CCCode = split.NewCCCode
newContent.ContentTwinID = split.NewCCCode + "-ctid"
newContent.Title = split.Title
newContent.Status = model.StatusApproved
newContent.CreatedAt = time.Now()
m.contents[split.NewMACode] = newContent
m.contents[split.NewCCCode] = newContent
// 按集号迁移哈希绑定
for _, b := range m.bindings[req.SourceMACode] {
for _, b := range m.bindings[req.SourceCCCode] {
shouldMigrate := false
if len(split.Episodes) == 0 {
// 空集号列表:迁移全部(整剧拆分场景)
@@ -559,34 +559,34 @@ func (m *MemoryChain) SplitMA(role Role, req model.SplitRequest) (model.SplitRes
if shouldMigrate {
newB := b
newB.ContentTwinID = newContent.ContentTwinID
m.bindings[split.NewMACode] = append(m.bindings[split.NewMACode], newB)
m.bindings[split.NewCCCode] = append(m.bindings[split.NewCCCode], newB)
// 更新哈希索引
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
m.hashIndex[b.HashValue] = split.NewMACode
m.hashIndex[b.HashValue] = split.NewCCCode
}
migratedBindings++
}
}
// 迁移映射(全部映射复制到新 MA 码)
for _, mp := range m.mappings[req.SourceMACode] {
for _, mp := range m.mappings[req.SourceCCCode] {
newMP := mp
newMP.ContentTwinID = newContent.ContentTwinID
m.mappings[split.NewMACode] = append(m.mappings[split.NewMACode], newMP)
m.mappings[split.NewCCCode] = append(m.mappings[split.NewCCCode], newMP)
migratedMappings++
}
newMACodes = append(newMACodes, split.NewMACode)
newCCCodes = append(newCCCodes, split.NewCCCode)
}
// 源 MA 码状态标记为 split
srcContent.Status = model.StatusSplit
m.contents[req.SourceMACode] = srcContent
m.contents[req.SourceCCCode] = srcContent
txID := m.nextTx("splitMA")
return model.SplitResult{
SourceMACode: req.SourceMACode,
NewMACodes: newMACodes,
SourceCCCode: req.SourceCCCode,
NewCCCodes: newCCCodes,
MigratedBindings: migratedBindings,
MigratedMappings: migratedMappings,
TxID: txID,