feat: 更新CC码监管方案业务版文档,新增摘要、UGC专项、直播专项、存量处置、误判申诉、水印嵌入、CC码生命周期、组织保障等章节;同步macode到cccode重命名及其他代码变更
This commit is contained in:
@@ -31,7 +31,7 @@ var (
|
||||
|
||||
// IssueRequest 签发 MA 码并强绑定哈希包。
|
||||
type IssueRequest struct {
|
||||
MACode string
|
||||
CCCode string
|
||||
ContentTwinID string
|
||||
MerkleRoot string
|
||||
FileHash string
|
||||
@@ -43,7 +43,7 @@ type IssueRequest struct {
|
||||
// VerifyResult 哈希验真结果(需求4-AC4)。
|
||||
type VerifyResult struct {
|
||||
Valid bool `json:"valid"`
|
||||
MACode string `json:"ma_code"`
|
||||
CCCode string `json:"ma_code"`
|
||||
BoundHash string `json:"bound_hash"`
|
||||
SubmittedHash string `json:"submitted_hash"`
|
||||
Match bool `json:"match"`
|
||||
@@ -52,7 +52,7 @@ type VerifyResult struct {
|
||||
|
||||
// MappingsResult 映射查询结果(需求11/17)。
|
||||
type MappingsResult struct {
|
||||
MACode string `json:"ma_code"`
|
||||
CCCode string `json:"ma_code"`
|
||||
Mappings []model.Mapping `json:"mappings"`
|
||||
CDNEndpoints []string `json:"cdn_endpoints"`
|
||||
}
|
||||
@@ -67,31 +67,31 @@ type Client interface {
|
||||
// RegisterMapping 注册三方编码映射(MA 必须已签发)。
|
||||
RegisterMapping(role Role, m model.Mapping) (txID string, err error)
|
||||
// VerifyHash 按 MA 码校验提交哈希是否与绑定哈希一致。
|
||||
VerifyHash(maCode, fileHash string) (VerifyResult, error)
|
||||
VerifyHash(ccCode, fileHash string) (VerifyResult, error)
|
||||
// VerifyEpisodeHash 按 MA 码+集号校验该集哈希。
|
||||
VerifyEpisodeHash(maCode string, episode int, fileHash string) (VerifyResult, error)
|
||||
VerifyEpisodeHash(ccCode string, episode int, fileHash string) (VerifyResult, error)
|
||||
// ListEpisodes 返回某 MA 码下的全部集级哈希绑定。
|
||||
ListEpisodes(maCode string) ([]model.HashBinding, error)
|
||||
ListEpisodes(ccCode string) ([]model.HashBinding, error)
|
||||
// HashExists 判断内容哈希是否已存在(防换壳重发)。
|
||||
HashExists(fileHash string) (maCode string, exists bool)
|
||||
HashExists(fileHash string) (ccCode string, exists bool)
|
||||
// QueryContent 查询内容主记录。
|
||||
QueryContent(maCode string) (model.Content, error)
|
||||
QueryContent(ccCode string) (model.Content, error)
|
||||
// ListContents 按状态列出内容(空状态返回全部)。
|
||||
ListContents(status string) ([]model.Content, error)
|
||||
// QueryMappings 查询 MA 码绑定的全部三方映射与 CDN 端点。
|
||||
QueryMappings(maCode string) (MappingsResult, error)
|
||||
QueryMappings(ccCode string) (MappingsResult, error)
|
||||
// RecordVersionChange 记录版本变更(绑定断裂触发重审)。
|
||||
RecordVersionChange(vc model.VersionChange) (txID string, err error)
|
||||
// Revoke 下架(仅监管主体),返回受影响的映射。
|
||||
Revoke(role Role, maCode, reason string) (MappingsResult, error)
|
||||
Revoke(role Role, ccCode, reason string) (MappingsResult, error)
|
||||
// RevokeEpisode 集级下架(仅监管主体):只下架指定集,整剧其他集不受影响。
|
||||
RevokeEpisode(role Role, maCode string, episode int, reason string) error
|
||||
RevokeEpisode(role Role, ccCode string, episode int, reason string) error
|
||||
// Restore 恢复上架整剧(仅监管主体):下架状态恢复为流通中。
|
||||
Restore(role Role, maCode string) error
|
||||
Restore(role Role, ccCode string) error
|
||||
// RestoreEpisode 恢复上架指定集(仅监管主体)。
|
||||
RestoreEpisode(role Role, maCode string, episode int) error
|
||||
RestoreEpisode(role Role, ccCode string, episode int) error
|
||||
// SetContentStatus 更新内容状态。
|
||||
SetContentStatus(maCode, status string) error
|
||||
SetContentStatus(ccCode, status string) error
|
||||
// QueryByHash 根据内容哈希反查标识信息及映射关系。
|
||||
QueryByHash(fileHash string) (model.ContentQueryResult, error)
|
||||
// QueryByProvincialCode 根据省级内容编码(CP MediaID)反查标识信息。
|
||||
|
||||
@@ -129,7 +129,7 @@ func (c *ChainMakerClient) IssueMA(role Role, req IssueRequest) (string, error)
|
||||
contentJSON, _ := json.Marshal(req.Content)
|
||||
epJSON, _ := json.Marshal(req.Episodes)
|
||||
resp, err := c.invoke(role, "IssueMA", map[string][]byte{
|
||||
"ma_code": []byte(req.MACode),
|
||||
"ma_code": []byte(req.CCCode),
|
||||
"ctid": []byte(req.ContentTwinID),
|
||||
"merkle_root": []byte(req.MerkleRoot),
|
||||
"file_hash": []byte(req.FileHash),
|
||||
@@ -177,66 +177,66 @@ func (c *ChainMakerClient) RecordVersionChange(vc model.VersionChange) (string,
|
||||
return resp.TxId, nil
|
||||
}
|
||||
|
||||
func (c *ChainMakerClient) Revoke(role Role, maCode, reason string) (MappingsResult, error) {
|
||||
func (c *ChainMakerClient) Revoke(role Role, ccCode, reason string) (MappingsResult, error) {
|
||||
if _, err := c.invoke(role, "Revoke", map[string][]byte{
|
||||
"ma_code": []byte(maCode), "reason": []byte(reason),
|
||||
"ma_code": []byte(ccCode), "reason": []byte(reason),
|
||||
}); err != nil {
|
||||
return MappingsResult{}, err
|
||||
}
|
||||
return c.QueryMappings(maCode)
|
||||
return c.QueryMappings(ccCode)
|
||||
}
|
||||
|
||||
func (c *ChainMakerClient) RevokeEpisode(role Role, maCode string, episode int, reason string) error {
|
||||
func (c *ChainMakerClient) RevokeEpisode(role Role, ccCode string, episode int, reason string) error {
|
||||
_, err := c.invoke(role, "RevokeEpisode", map[string][]byte{
|
||||
"ma_code": []byte(maCode), "episode": []byte(fmt.Sprint(episode)), "reason": []byte(reason),
|
||||
"ma_code": []byte(ccCode), "episode": []byte(fmt.Sprint(episode)), "reason": []byte(reason),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ChainMakerClient) Restore(role Role, maCode string) error {
|
||||
_, err := c.invoke(role, "Restore", map[string][]byte{"ma_code": []byte(maCode)})
|
||||
func (c *ChainMakerClient) Restore(role Role, ccCode string) error {
|
||||
_, err := c.invoke(role, "Restore", map[string][]byte{"ma_code": []byte(ccCode)})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ChainMakerClient) RestoreEpisode(role Role, maCode string, episode int) error {
|
||||
func (c *ChainMakerClient) RestoreEpisode(role Role, ccCode string, episode int) error {
|
||||
_, err := c.invoke(role, "RestoreEpisode", map[string][]byte{
|
||||
"ma_code": []byte(maCode), "episode": []byte(fmt.Sprint(episode)),
|
||||
"ma_code": []byte(ccCode), "episode": []byte(fmt.Sprint(episode)),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ChainMakerClient) SetContentStatus(maCode, status string) error {
|
||||
func (c *ChainMakerClient) SetContentStatus(ccCode, status string) error {
|
||||
_, err := c.invoke(RoleReviewer, "SetContentStatus", map[string][]byte{
|
||||
"ma_code": []byte(maCode), "status": []byte(status),
|
||||
"ma_code": []byte(ccCode), "status": []byte(status),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// ---- chain.Client 实现(读操作)----
|
||||
|
||||
func (c *ChainMakerClient) VerifyHash(maCode, fileHash string) (VerifyResult, error) {
|
||||
func (c *ChainMakerClient) VerifyHash(ccCode, fileHash string) (VerifyResult, error) {
|
||||
res, err := c.query(RoleOperator, "VerifyHash", map[string][]byte{
|
||||
"ma_code": []byte(maCode), "file_hash": []byte(fileHash),
|
||||
"ma_code": []byte(ccCode), "file_hash": []byte(fileHash),
|
||||
})
|
||||
if err != nil {
|
||||
return VerifyResult{MACode: maCode, SubmittedHash: fileHash}, err
|
||||
return VerifyResult{CCCode: ccCode, SubmittedHash: fileHash}, err
|
||||
}
|
||||
match := string(res) == "true"
|
||||
return VerifyResult{Valid: true, MACode: maCode, SubmittedHash: fileHash, Match: match}, nil
|
||||
return VerifyResult{Valid: true, CCCode: ccCode, SubmittedHash: fileHash, Match: match}, nil
|
||||
}
|
||||
|
||||
func (c *ChainMakerClient) VerifyEpisodeHash(maCode string, episode int, fileHash string) (VerifyResult, error) {
|
||||
func (c *ChainMakerClient) VerifyEpisodeHash(ccCode string, episode int, fileHash string) (VerifyResult, error) {
|
||||
res, err := c.query(RoleOperator, "VerifyEpisodeHash", map[string][]byte{
|
||||
"ma_code": []byte(maCode), "episode": []byte(fmt.Sprint(episode)), "file_hash": []byte(fileHash),
|
||||
"ma_code": []byte(ccCode), "episode": []byte(fmt.Sprint(episode)), "file_hash": []byte(fileHash),
|
||||
})
|
||||
if err != nil {
|
||||
return VerifyResult{MACode: maCode, SubmittedHash: fileHash}, err
|
||||
return VerifyResult{CCCode: ccCode, SubmittedHash: fileHash}, err
|
||||
}
|
||||
return VerifyResult{Valid: true, MACode: maCode, SubmittedHash: fileHash, Match: string(res) == "true"}, nil
|
||||
return VerifyResult{Valid: true, CCCode: ccCode, SubmittedHash: fileHash, Match: string(res) == "true"}, nil
|
||||
}
|
||||
|
||||
func (c *ChainMakerClient) ListEpisodes(maCode string) ([]model.HashBinding, error) {
|
||||
res, err := c.query(RoleRegulator, "ListEpisodes", map[string][]byte{"ma_code": []byte(maCode)})
|
||||
func (c *ChainMakerClient) ListEpisodes(ccCode string) ([]model.HashBinding, error) {
|
||||
res, err := c.query(RoleRegulator, "ListEpisodes", map[string][]byte{"ma_code": []byte(ccCode)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -255,8 +255,8 @@ func (c *ChainMakerClient) HashExists(fileHash string) (string, bool) {
|
||||
return string(res), true
|
||||
}
|
||||
|
||||
func (c *ChainMakerClient) QueryContent(maCode string) (model.Content, error) {
|
||||
res, err := c.query(RoleRegulator, "QueryContent", map[string][]byte{"ma_code": []byte(maCode)})
|
||||
func (c *ChainMakerClient) QueryContent(ccCode string) (model.Content, error) {
|
||||
res, err := c.query(RoleRegulator, "QueryContent", map[string][]byte{"ma_code": []byte(ccCode)})
|
||||
if err != nil {
|
||||
return model.Content{}, err
|
||||
}
|
||||
@@ -280,8 +280,8 @@ func (c *ChainMakerClient) ListContents(status string) ([]model.Content, error)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *ChainMakerClient) QueryMappings(maCode string) (MappingsResult, error) {
|
||||
res, err := c.query(RoleRegulator, "QueryMappings", map[string][]byte{"ma_code": []byte(maCode)})
|
||||
func (c *ChainMakerClient) QueryMappings(ccCode string) (MappingsResult, error) {
|
||||
res, err := c.query(RoleRegulator, "QueryMappings", map[string][]byte{"ma_code": []byte(ccCode)})
|
||||
if err != nil {
|
||||
return MappingsResult{}, err
|
||||
}
|
||||
@@ -289,7 +289,7 @@ func (c *ChainMakerClient) QueryMappings(maCode string) (MappingsResult, error)
|
||||
if err := json.Unmarshal(res, &out); err != nil {
|
||||
return MappingsResult{}, err
|
||||
}
|
||||
out.MACode = maCode
|
||||
out.CCCode = ccCode
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// 仅在 `go test -tags chainmaker` 且配置了测试链时运行:
|
||||
// - TCS_TEST_CHAINMAKER_CONF:测试链 sdk_config.yml 路径
|
||||
//
|
||||
// 注意:真实链不易"清空状态",建议每次用全新 maCode/合约实例,或对接专用测试链。
|
||||
// 注意:真实链不易"清空状态",建议每次用全新 ccCode/合约实例,或对接专用测试链。
|
||||
// 本用例提供接线骨架,实际跑通需真实 ChainMaker 测试网与已部署的 tcs_registry 合约。
|
||||
func TestChainMakerClient_Conformance(t *testing.T) {
|
||||
conf := os.Getenv("TCS_TEST_CHAINMAKER_CONF")
|
||||
|
||||
@@ -19,7 +19,7 @@ func RunClientConformance(t *testing.T, newClient func(t *testing.T) Client) {
|
||||
// 构造一条标准发码请求(集级 3 集)。
|
||||
issueReq := func(ma, ctid, fh string) IssueRequest {
|
||||
return IssueRequest{
|
||||
MACode: ma, ContentTwinID: ctid, MerkleRoot: "mr-" + fh, FileHash: fh,
|
||||
CCCode: ma, ContentTwinID: ctid, MerkleRoot: "mr-" + fh, FileHash: fh,
|
||||
PerceptualHash: "ph-" + fh,
|
||||
Episodes: []model.EpisodeHash{
|
||||
{Episode: 1, FileSHA256: fh + "-E1"},
|
||||
@@ -170,20 +170,20 @@ func RunClientConformance(t *testing.T, newClient func(t *testing.T) Client) {
|
||||
res, err := c.QueryByHash(fh)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.Found)
|
||||
assert.Equal(t, ma, res.Content.MACode)
|
||||
assert.Equal(t, ma, res.Content.CCCode)
|
||||
assert.Equal(t, "契约测试剧", res.Content.Title)
|
||||
|
||||
// 按省级内容编码查询
|
||||
res2, err := c.QueryByProvincialCode("PROV-001")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res2.Found)
|
||||
assert.Equal(t, ma, res2.Content.MACode)
|
||||
assert.Equal(t, ma, res2.Content.CCCode)
|
||||
|
||||
// 按片库文件 ID 查询
|
||||
res3, err := c.QueryByLibraryFileID("LIB-001")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res3.Found)
|
||||
assert.Equal(t, ma, res3.Content.MACode)
|
||||
assert.Equal(t, ma, res3.Content.CCCode)
|
||||
|
||||
// 不存在的 Hash
|
||||
_, err = c.QueryByHash("nonexistent")
|
||||
@@ -203,17 +203,17 @@ func RunClientConformance(t *testing.T, newClient func(t *testing.T) Client) {
|
||||
|
||||
// 非监管不可合并
|
||||
_, err = c.MergeMA(RoleCP, model.MergeRequest{
|
||||
PrimaryMACode: ma, SecondaryMACodes: []string{ma2}, Reason: "重复发码",
|
||||
PrimaryCCCode: ma, SecondaryCCCodes: []string{ma2}, Reason: "重复发码",
|
||||
})
|
||||
assert.ErrorIs(t, err, ErrPermissionDenied)
|
||||
|
||||
// 监管合并
|
||||
res, err := c.MergeMA(RoleRegulator, model.MergeRequest{
|
||||
PrimaryMACode: ma, SecondaryMACodes: []string{ma2}, Reason: "重复发码", Operator: "监管局",
|
||||
PrimaryCCCode: ma, SecondaryCCCodes: []string{ma2}, Reason: "重复发码", Operator: "监管局",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ma, res.PrimaryMACode)
|
||||
assert.Contains(t, res.MergedMACodes, ma2)
|
||||
assert.Equal(t, ma, res.PrimaryCCCode)
|
||||
assert.Contains(t, res.MergedCCCodes, ma2)
|
||||
assert.Greater(t, res.MigratedBindings, 0)
|
||||
|
||||
// 被合并的 MA 状态为 merged
|
||||
@@ -223,7 +223,7 @@ func RunClientConformance(t *testing.T, newClient func(t *testing.T) Client) {
|
||||
// 按 fh2 查询应指向主 MA 码
|
||||
qr, err := c.QueryByHash(fh2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ma, qr.Content.MACode)
|
||||
assert.Equal(t, ma, qr.Content.CCCode)
|
||||
})
|
||||
|
||||
t.Run("MA拆分_仅监管且按集迁移", func(t *testing.T) {
|
||||
@@ -233,20 +233,20 @@ func RunClientConformance(t *testing.T, newClient func(t *testing.T) Client) {
|
||||
|
||||
// 非监管不可拆分
|
||||
_, err = c.SplitMA(RoleCP, model.SplitRequest{
|
||||
SourceMACode: ma, Splits: []model.SplitTarget{{NewMACode: "MA.156.8531.6101/WD/20260000010", Title: "拆分剧A", Episodes: []int{1, 2}}},
|
||||
SourceCCCode: ma, Splits: []model.SplitTarget{{NewCCCode: "MA.156.8531.6101/WD/20260000010", Title: "拆分剧A", Episodes: []int{1, 2}}},
|
||||
})
|
||||
assert.ErrorIs(t, err, ErrPermissionDenied)
|
||||
|
||||
// 监管拆分
|
||||
newMA := "MA.156.8531.6101/WD/20260000011"
|
||||
res, err := c.SplitMA(RoleRegulator, model.SplitRequest{
|
||||
SourceMACode: ma,
|
||||
Splits: []model.SplitTarget{{NewMACode: newMA, Title: "拆分剧A", Episodes: []int{1, 2}}},
|
||||
SourceCCCode: ma,
|
||||
Splits: []model.SplitTarget{{NewCCCode: newMA, Title: "拆分剧A", Episodes: []int{1, 2}}},
|
||||
Reason: "合集拆分",
|
||||
Operator: "监管局",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, res.NewMACodes, newMA)
|
||||
assert.Contains(t, res.NewCCCodes, newMA)
|
||||
assert.Greater(t, res.MigratedBindings, 0)
|
||||
|
||||
// 源 MA 状态为 split
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,7 +12,7 @@ func newIssued(t *testing.T) *MemoryChain {
|
||||
t.Helper()
|
||||
c := NewMemoryChain()
|
||||
_, err := c.IssueMA(RoleRegulator, IssueRequest{
|
||||
MACode: "(京)网微剧审字(2025)第123号",
|
||||
CCCode: "(京)网微剧审字(2025)第123号",
|
||||
ContentTwinID: "ctid-001",
|
||||
MerkleRoot: "merkle-root-1",
|
||||
FileHash: "filehash-1",
|
||||
@@ -24,13 +24,13 @@ func newIssued(t *testing.T) *MemoryChain {
|
||||
|
||||
func TestIssueMA_OnlyRegulator(t *testing.T) {
|
||||
c := NewMemoryChain()
|
||||
_, err := c.IssueMA(RoleCP, IssueRequest{MACode: "MA-1", ContentTwinID: "ct-1", FileHash: "h1"})
|
||||
_, err := c.IssueMA(RoleCP, IssueRequest{CCCode: "MA-1", ContentTwinID: "ct-1", FileHash: "h1"})
|
||||
assert.ErrorIs(t, err, ErrPermissionDenied)
|
||||
|
||||
_, err = c.IssueMA(RoleReviewer, IssueRequest{MACode: "MA-1", ContentTwinID: "ct-1", FileHash: "h1"})
|
||||
_, err = c.IssueMA(RoleReviewer, IssueRequest{CCCode: "MA-1", ContentTwinID: "ct-1", FileHash: "h1"})
|
||||
assert.ErrorIs(t, err, ErrPermissionDenied)
|
||||
|
||||
_, err = c.IssueMA(RoleRegulator, IssueRequest{MACode: "MA-1", ContentTwinID: "ct-1", FileHash: "h1"})
|
||||
_, err = c.IssueMA(RoleRegulator, IssueRequest{CCCode: "MA-1", ContentTwinID: "ct-1", FileHash: "h1"})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestIssueMA_NoReissue(t *testing.T) {
|
||||
c := newIssued(t)
|
||||
// 同 MA 码重复签发被拒(1:1 不可解绑/不可覆盖)
|
||||
_, err := c.IssueMA(RoleRegulator, IssueRequest{
|
||||
MACode: "(京)网微剧审字(2025)第123号", ContentTwinID: "ctid-001", FileHash: "other",
|
||||
CCCode: "(京)网微剧审字(2025)第123号", ContentTwinID: "ctid-001", FileHash: "other",
|
||||
})
|
||||
assert.ErrorIs(t, err, ErrMAAlreadyIssued)
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func TestIssueMA_DuplicateHashRejected(t *testing.T) {
|
||||
c := newIssued(t)
|
||||
// 换壳重发:不同 MA 码但相同内容哈希 → 拒绝
|
||||
_, err := c.IssueMA(RoleRegulator, IssueRequest{
|
||||
MACode: "(沪)网微剧审字(2025)第999号", ContentTwinID: "ctid-002", FileHash: "filehash-1",
|
||||
CCCode: "(沪)网微剧审字(2025)第999号", ContentTwinID: "ctid-002", FileHash: "filehash-1",
|
||||
})
|
||||
assert.ErrorIs(t, err, ErrHashExists)
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ func (p *PersistentChain) IssueMA(role Role, req IssueRequest) (string, error) {
|
||||
if err != nil {
|
||||
return tx, err
|
||||
}
|
||||
c, _ := p.MemoryChain.QueryContent(req.MACode)
|
||||
c, _ := p.MemoryChain.QueryContent(req.CCCode)
|
||||
p.persistContent(c)
|
||||
for _, b := range p.snapshotBindings(req.MACode) {
|
||||
for _, b := range p.snapshotBindings(req.CCCode) {
|
||||
p.persistBinding(b)
|
||||
}
|
||||
p.persistTx(req.ContentTwinID, tx, "issueMA")
|
||||
@@ -86,48 +86,48 @@ func (p *PersistentChain) RecordVersionChange(vc model.VersionChange) (string, e
|
||||
}
|
||||
|
||||
// Revoke 整剧下架:镜像内容状态。
|
||||
func (p *PersistentChain) Revoke(role Role, maCode, reason string) (MappingsResult, error) {
|
||||
res, err := p.MemoryChain.Revoke(role, maCode, reason)
|
||||
func (p *PersistentChain) Revoke(role Role, ccCode, reason string) (MappingsResult, error) {
|
||||
res, err := p.MemoryChain.Revoke(role, ccCode, reason)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
p.updateStatus(maCode, model.StatusRevoked)
|
||||
p.updateStatus(ccCode, model.StatusRevoked)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Restore 整剧恢复上架:镜像内容状态。
|
||||
func (p *PersistentChain) Restore(role Role, maCode string) error {
|
||||
if err := p.MemoryChain.Restore(role, maCode); err != nil {
|
||||
func (p *PersistentChain) Restore(role Role, ccCode string) error {
|
||||
if err := p.MemoryChain.Restore(role, ccCode); err != nil {
|
||||
return err
|
||||
}
|
||||
p.updateStatus(maCode, model.StatusPublished)
|
||||
p.updateStatus(ccCode, model.StatusPublished)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeEpisode 集级下架:镜像该集 revoked 标记。
|
||||
func (p *PersistentChain) RevokeEpisode(role Role, maCode string, episode int, reason string) error {
|
||||
if err := p.MemoryChain.RevokeEpisode(role, maCode, episode, reason); err != nil {
|
||||
func (p *PersistentChain) RevokeEpisode(role Role, ccCode string, episode int, reason string) error {
|
||||
if err := p.MemoryChain.RevokeEpisode(role, ccCode, episode, reason); err != nil {
|
||||
return err
|
||||
}
|
||||
p.updateEpisodeRevoked(maCode, episode, true, reason)
|
||||
p.updateEpisodeRevoked(ccCode, episode, true, reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreEpisode 集级恢复:镜像该集 revoked 标记。
|
||||
func (p *PersistentChain) RestoreEpisode(role Role, maCode string, episode int) error {
|
||||
if err := p.MemoryChain.RestoreEpisode(role, maCode, episode); err != nil {
|
||||
func (p *PersistentChain) RestoreEpisode(role Role, ccCode string, episode int) error {
|
||||
if err := p.MemoryChain.RestoreEpisode(role, ccCode, episode); err != nil {
|
||||
return err
|
||||
}
|
||||
p.updateEpisodeRevoked(maCode, episode, false, "")
|
||||
p.updateEpisodeRevoked(ccCode, episode, false, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetContentStatus 状态流转(入库/发布等):镜像内容状态。
|
||||
func (p *PersistentChain) SetContentStatus(maCode, status string) error {
|
||||
if err := p.MemoryChain.SetContentStatus(maCode, status); err != nil {
|
||||
func (p *PersistentChain) SetContentStatus(ccCode, status string) error {
|
||||
if err := p.MemoryChain.SetContentStatus(ccCode, status); err != nil {
|
||||
return err
|
||||
}
|
||||
p.updateStatus(maCode, status)
|
||||
p.updateStatus(ccCode, status)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -138,14 +138,14 @@ func (p *PersistentChain) MergeMA(role Role, req model.MergeRequest) (model.Merg
|
||||
return res, err
|
||||
}
|
||||
// 被合并的 MA 码状态标记为 merged
|
||||
for _, secMA := range req.SecondaryMACodes {
|
||||
for _, secMA := range req.SecondaryCCCodes {
|
||||
p.updateStatus(secMA, model.StatusMerged)
|
||||
}
|
||||
// 主 MA 码的绑定和映射已在内存中追加,写穿最新状态
|
||||
for _, b := range p.snapshotBindings(req.PrimaryMACode) {
|
||||
for _, b := range p.snapshotBindings(req.PrimaryCCCode) {
|
||||
p.persistBinding(b)
|
||||
}
|
||||
p.persistTx(req.PrimaryMACode, res.TxID, "mergeMA")
|
||||
p.persistTx(req.PrimaryCCCode, res.TxID, "mergeMA")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -156,9 +156,9 @@ func (p *PersistentChain) SplitMA(role Role, req model.SplitRequest) (model.Spli
|
||||
return res, err
|
||||
}
|
||||
// 源 MA 码状态标记为 split
|
||||
p.updateStatus(req.SourceMACode, model.StatusSplit)
|
||||
p.updateStatus(req.SourceCCCode, model.StatusSplit)
|
||||
// 新 MA 码的内容记录和绑定写穿
|
||||
for _, newMA := range res.NewMACodes {
|
||||
for _, newMA := range res.NewCCCodes {
|
||||
c, _ := p.MemoryChain.QueryContent(newMA)
|
||||
p.persistContent(c)
|
||||
for _, b := range p.snapshotBindings(newMA) {
|
||||
@@ -168,7 +168,7 @@ func (p *PersistentChain) SplitMA(role Role, req model.SplitRequest) (model.Spli
|
||||
p.persistMapping(mp)
|
||||
}
|
||||
}
|
||||
p.persistTx(req.SourceMACode, res.TxID, "splitMA")
|
||||
p.persistTx(req.SourceCCCode, res.TxID, "splitMA")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ func (p *PersistentChain) persistContent(c model.Content) {
|
||||
(content_twin_id, ma_code, ma_type, title, episode_count, status, issuer, issue_date, created_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||
ON CONFLICT (content_twin_id) DO UPDATE SET status=EXCLUDED.status, updated_at=NOW()`,
|
||||
c.ContentTwinID, c.MACode, c.MAType, c.Title, c.EpisodeCount, c.Status, c.Issuer, issueDate, c.CreatedAt)
|
||||
c.ContentTwinID, c.CCCode, c.MAType, c.Title, c.EpisodeCount, c.Status, c.Issuer, issueDate, c.CreatedAt)
|
||||
}
|
||||
|
||||
func (p *PersistentChain) persistBinding(b model.HashBinding) {
|
||||
@@ -214,32 +214,32 @@ func (p *PersistentChain) persistTx(ctid, txID, method string) {
|
||||
ctid, txID, method)
|
||||
}
|
||||
|
||||
func (p *PersistentChain) updateStatus(maCode, status string) {
|
||||
p.exec(`UPDATE content_registry SET status=$1, updated_at=NOW() WHERE ma_code=$2`, status, maCode)
|
||||
func (p *PersistentChain) updateStatus(ccCode, status string) {
|
||||
p.exec(`UPDATE content_registry SET status=$1, updated_at=NOW() WHERE ma_code=$2`, status, ccCode)
|
||||
}
|
||||
|
||||
func (p *PersistentChain) updateEpisodeRevoked(maCode string, episode int, revoked bool, reason string) {
|
||||
func (p *PersistentChain) updateEpisodeRevoked(ccCode string, episode int, revoked bool, reason string) {
|
||||
p.exec(`UPDATE hash_binding hb SET revoked=$1, revoked_reason=$2
|
||||
FROM content_registry cr
|
||||
WHERE hb.content_twin_id = cr.content_twin_id AND cr.ma_code=$3 AND hb.episode=$4`,
|
||||
revoked, reason, maCode, episode)
|
||||
revoked, reason, ccCode, episode)
|
||||
}
|
||||
|
||||
// snapshotBindings 复制某 MA 码当前的内存绑定(同包访问,读锁保护)。
|
||||
func (p *PersistentChain) snapshotBindings(maCode string) []model.HashBinding {
|
||||
func (p *PersistentChain) snapshotBindings(ccCode string) []model.HashBinding {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
src := p.bindings[maCode]
|
||||
src := p.bindings[ccCode]
|
||||
out := make([]model.HashBinding, len(src))
|
||||
copy(out, src)
|
||||
return out
|
||||
}
|
||||
|
||||
// snapshotMappings 复制某 MA 码当前的内存映射(同包访问,读锁保护)。
|
||||
func (p *PersistentChain) snapshotMappings(maCode string) []model.Mapping {
|
||||
func (p *PersistentChain) snapshotMappings(ccCode string) []model.Mapping {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
src := p.mappings[maCode]
|
||||
src := p.mappings[ccCode]
|
||||
out := make([]model.Mapping, len(src))
|
||||
copy(out, src)
|
||||
return out
|
||||
@@ -248,7 +248,7 @@ func (p *PersistentChain) snapshotMappings(maCode string) []model.Mapping {
|
||||
// ---- 启动水合:从 PG 镜像恢复内存状态 ----
|
||||
|
||||
func (p *PersistentChain) hydrate() error {
|
||||
// 1) 内容主表 + 建立 ctid -> maCode 映射
|
||||
// 1) 内容主表 + 建立 ctid -> ccCode 映射
|
||||
ctidToMA := map[string]string{}
|
||||
rows, err := p.db.Query(`SELECT content_twin_id, ma_code, COALESCE(ma_type,''), title,
|
||||
COALESCE(episode_count,1), status, COALESCE(issuer,''),
|
||||
@@ -259,13 +259,13 @@ func (p *PersistentChain) hydrate() error {
|
||||
n := 0
|
||||
for rows.Next() {
|
||||
var c model.Content
|
||||
if err := rows.Scan(&c.ContentTwinID, &c.MACode, &c.MAType, &c.Title,
|
||||
if err := rows.Scan(&c.ContentTwinID, &c.CCCode, &c.MAType, &c.Title,
|
||||
&c.EpisodeCount, &c.Status, &c.Issuer, &c.IssueDate, &c.CreatedAt); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
p.contents[c.MACode] = c
|
||||
ctidToMA[c.ContentTwinID] = c.MACode
|
||||
p.contents[c.CCCode] = c
|
||||
ctidToMA[c.ContentTwinID] = c.CCCode
|
||||
n++
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
Reference in New Issue
Block a user