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
+14 -14
View File
@@ -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)反查标识信息。
+27 -27
View File
@@ -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")
+13 -13
View File
@@ -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
+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,
+6 -6
View File
@@ -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)
}
+36 -36
View File
@@ -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()