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
@@ -12,13 +12,13 @@ import (
// 全链路一致:发码→入库→正确注入,追责判定审播一致。
func TestAccountability_Consistent(t *testing.T) {
s := newService(t)
maCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, maCode, ctid, "M", "陕西IPTV媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{MACode: maCode, Certificate: cert}))
_, err := s.InjectToCDN(chain.RoleOperator, ctid, maCode, "filehash-abc", "CT-SX", "cdn://x")
ccCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, ccCode, ctid, "M", "陕西IPTV媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{CCCode: ccCode, Certificate: cert}))
_, err := s.InjectToCDN(chain.RoleOperator, ctid, ccCode, "filehash-abc", "CT-SX", "cdn://x")
require.NoError(t, err)
rep := s.Accountability(maCode)
rep := s.Accountability(ccCode)
assert.True(t, rep.Consistent, "全链路应一致")
assert.Nil(t, rep.FirstChange)
assert.Equal(t, "filehash-abc", rep.BaselineHash)
@@ -28,19 +28,19 @@ func TestAccountability_Consistent(t *testing.T) {
// 注入环节偷换:追责定位到 cdn_inject 节点与运营商。
func TestAccountability_TamperLocated(t *testing.T) {
s := newService(t)
maCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, maCode, ctid, "M", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{MACode: maCode, Certificate: cert}))
ccCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, ccCode, ctid, "M", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{CCCode: ccCode, Certificate: cert}))
// 直接往存证里记录一次"被偷换"的注入(绕过校验模拟违规留痕)
// 实际中 InjectToCDN 会拒绝不匹配,但运营商侧若绕过校验偷换,存证仍会暴露
_, _ = s.InjectToCDN(chain.RoleOperator, ctid, maCode, "filehash-abc", "CT-SX", "cdn://x")
_, _ = s.InjectToCDN(chain.RoleOperator, ctid, ccCode, "filehash-abc", "CT-SX", "cdn://x")
s.prov.Record(model.ProvenanceEvent{
MACode: maCode, Node: model.NodeInject, HashValue: "TAMPERED",
CCCode: ccCode, Node: model.NodeInject, HashValue: "TAMPERED",
Operator: "CT-SX-违规", Detail: "疑似偷换",
})
rep := s.Accountability(maCode)
rep := s.Accountability(ccCode)
assert.False(t, rep.Consistent)
require.NotNil(t, rep.FirstChange)
assert.Equal(t, model.NodeInject, rep.FirstChange.Node)
@@ -50,22 +50,22 @@ func TestAccountability_TamperLocated(t *testing.T) {
// 转码版哈希不同属合法,不应误判为篡改。
func TestAccountability_TranscodeNotFlagged(t *testing.T) {
s := newService(t)
maCode, ctid, _ := issueOne(t, s)
ccCode, ctid, _ := issueOne(t, s)
_, err := s.BindTranscoded(chain.RoleReviewer, ctid, "filehash-abc", "h265-4k", "H.265", "4K", "v1-4k")
require.NoError(t, err)
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeTranscode, HashValue: "h265-4k", Operator: "转码中心"})
s.prov.Record(model.ProvenanceEvent{CCCode: ccCode, Node: model.NodeTranscode, HashValue: "h265-4k", Operator: "转码中心"})
rep := s.Accountability(maCode)
rep := s.Accountability(ccCode)
assert.True(t, rep.Consistent, "转码版哈希不同属合法,不应判为篡改")
}
func TestCopyrightEvidence(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s)
ccCode, _, _ := issueOne(t, s)
ev, err := s.CopyrightEvidence(maCode)
ev, err := s.CopyrightEvidence(ccCode)
require.NoError(t, err)
assert.Equal(t, maCode, ev.MACode)
assert.Equal(t, ccCode, ev.CCCode)
assert.NotEmpty(t, ev.Trail)
assert.False(t, ev.FirstSeenAt.IsZero(), "应有最早登记时间戳")
assert.Contains(t, ev.Statement, "谁先锁定谁有权")
@@ -86,7 +86,7 @@ func TestMatchInfringement(t *testing.T) {
matches, err := s.MatchInfringement("a1b2c3d4e5f60718", 5, 10)
require.NoError(t, err)
require.NotEmpty(t, matches)
assert.Equal(t, issued.MACode, matches[0].MACode)
assert.Equal(t, issued.CCCode, matches[0].CCCode)
assert.Equal(t, "high", matches[0].Similarity)
assert.Equal(t, 0, matches[0].Distance)
+58 -58
View File
@@ -32,8 +32,8 @@ func (s *Service) BindTranscoded(role chain.Role, ctid, parentFileHash, transcod
// ---- 工作包8:媒体资源库入库、发布与映射(需求6) ----
// IngestToLibrary 审核合格内容入媒资库,建立媒资编码映射(需求6-AC1/AC2/AC3)。
func (s *Service) IngestToLibrary(role chain.Role, maCode, ctid, mediaAssetID, libName string) error {
c, err := s.chain.QueryContent(maCode)
func (s *Service) IngestToLibrary(role chain.Role, ccCode, ctid, mediaAssetID, libName string) error {
c, err := s.chain.QueryContent(ccCode)
if err != nil {
return err
}
@@ -49,19 +49,19 @@ func (s *Service) IngestToLibrary(role chain.Role, maCode, ctid, mediaAssetID, l
}); err != nil {
return err
}
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeIngest, Operator: libName, Detail: "审合格入媒资库"})
return s.chain.SetContentStatus(maCode, model.StatusInLibrary)
s.prov.Record(model.ProvenanceEvent{CCCode: ccCode, Node: model.NodeIngest, Operator: libName, Detail: "审合格入媒资库"})
return s.chain.SetContentStatus(ccCode, model.StatusInLibrary)
}
// PublishRequest 从媒资库向运营商发布的请求(需求6-AC4)。
type PublishRequest struct {
MACode string
Certificate string // 必须携带 MA码+哈希证书
CCCode string
Certificate string // 必须携带 CC码+哈希证书
}
// PublishToOperator 校验证书后将内容置为已发布(需求6-AC4/AC5、需求3-AC8)。
func (s *Service) PublishToOperator(req PublishRequest) error {
c, err := s.chain.QueryContent(req.MACode)
c, err := s.chain.QueryContent(req.CCCode)
if err != nil {
return err
}
@@ -69,10 +69,10 @@ func (s *Service) PublishToOperator(req PublishRequest) error {
return ErrNotApproved
}
// 发布必须携带证书(含 MA 码)
if req.Certificate == "" || !certContainsMA(req.Certificate, req.MACode) {
if req.Certificate == "" || !certContainsMA(req.Certificate, req.CCCode) {
return ErrNoCertificate
}
return s.chain.SetContentStatus(req.MACode, model.StatusPublished)
return s.chain.SetContentStatus(req.CCCode, model.StatusPublished)
}
// ---- 工作包9CDN 注入校验(需求7) ----
@@ -85,9 +85,9 @@ type InjectResult struct {
}
// InjectToCDN 运营商注入 CDN 前校验哈希;匹配则放行并注册运营商映射(需求7-AC1~AC4)。
func (s *Service) InjectToCDN(role chain.Role, ctid, maCode, injectFileHash, operatorID, cdnEndpoint string) (InjectResult, error) {
func (s *Service) InjectToCDN(role chain.Role, ctid, ccCode, injectFileHash, operatorID, cdnEndpoint string) (InjectResult, error) {
// 内容须处于已发布状态
c, err := s.chain.QueryContent(maCode)
c, err := s.chain.QueryContent(ccCode)
if err != nil {
return InjectResult{}, err
}
@@ -95,7 +95,7 @@ func (s *Service) InjectToCDN(role chain.Role, ctid, maCode, injectFileHash, ope
return InjectResult{Allowed: false, Reason: "内容已下架"}, ErrNotApproved
}
res, err := s.chain.VerifyHash(maCode, injectFileHash)
res, err := s.chain.VerifyHash(ccCode, injectFileHash)
if err != nil {
return InjectResult{Allowed: false, Reason: err.Error()}, err
}
@@ -105,7 +105,7 @@ func (s *Service) InjectToCDN(role chain.Role, ctid, maCode, injectFileHash, ope
}
// 授权核验(需求25-AC2/AC3):若已登记授权,校验该运营商是否在授权平台内
if authRes := s.CheckAuthorization(maCode, "", operatorID); !authRes.Allowed {
if authRes := s.CheckAuthorization(ccCode, "", operatorID); !authRes.Allowed {
return InjectResult{Allowed: false, Reason: authRes.Reason}, ErrNotApproved
}
@@ -118,7 +118,7 @@ func (s *Service) InjectToCDN(role chain.Role, ctid, maCode, injectFileHash, ope
}); err != nil {
return InjectResult{}, err
}
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeInject, HashValue: injectFileHash, Operator: operatorID, Detail: "CDN 注入校验通过"})
s.prov.Record(model.ProvenanceEvent{CCCode: ccCode, Node: model.NodeInject, HashValue: injectFileHash, Operator: operatorID, Detail: "CDN 注入校验通过"})
return InjectResult{Allowed: true, DistributionID: distID}, nil
}
@@ -159,28 +159,28 @@ func (s *Service) ReportVersionChange(ctid, reason, prevHash, newHash string, ol
// ---- 工作包14:违规应急下架(需求11) ----
// Takedown 监管主体一键下架:解析 MA 码绑定的三方编码与 CDN 端点(需求11-AC1/AC2/AC4)。
func (s *Service) Takedown(role chain.Role, maCode, reason string) (chain.MappingsResult, error) {
return s.chain.Revoke(role, maCode, reason)
func (s *Service) Takedown(role chain.Role, ccCode, reason string) (chain.MappingsResult, error) {
return s.chain.Revoke(role, ccCode, reason)
}
// TakedownEpisode 集级下架:只下架指定集,整剧其他集继续流通(仅监管主体)。
func (s *Service) TakedownEpisode(role chain.Role, maCode string, episode int, reason string) error {
return s.chain.RevokeEpisode(role, maCode, episode, reason)
func (s *Service) TakedownEpisode(role chain.Role, ccCode string, episode int, reason string) error {
return s.chain.RevokeEpisode(role, ccCode, episode, reason)
}
// Restore 恢复上架整剧(仅监管主体)。
func (s *Service) Restore(role chain.Role, maCode string) error {
return s.chain.Restore(role, maCode)
func (s *Service) Restore(role chain.Role, ccCode string) error {
return s.chain.Restore(role, ccCode)
}
// RestoreEpisode 恢复上架指定集(仅监管主体)。
func (s *Service) RestoreEpisode(role chain.Role, maCode string, episode int) error {
return s.chain.RestoreEpisode(role, maCode, episode)
func (s *Service) RestoreEpisode(role chain.Role, ccCode string, episode int) error {
return s.chain.RestoreEpisode(role, ccCode, episode)
}
// certContainsMA 校验证书是否包含指定 MA 码。
func certContainsMA(cert, maCode string) bool {
return cert != "" && maCode != "" && strings.Contains(cert, maCode)
func certContainsMA(cert, ccCode string) bool {
return cert != "" && ccCode != "" && strings.Contains(cert, ccCode)
}
// ---- 二期 F09/F18:数据回传聚合与分账(需求9/需求21) ----
@@ -190,7 +190,7 @@ func certContainsMA(cert, maCode string) bool {
func (s *Service) ReportPlayback(events []model.PlaybackEvent) (accepted int, rejected int) {
valid := make([]model.PlaybackEvent, 0, len(events))
for _, e := range events {
c, err := s.chain.QueryContent(e.MACode)
c, err := s.chain.QueryContent(e.CCCode)
if err != nil || c.Status == model.StatusRevoked {
rejected++
continue
@@ -202,40 +202,40 @@ func (s *Service) ReportPlayback(events []model.PlaybackEvent) (accepted int, re
}
// PlaybackSummary 查询按 MA 码聚合的可信播放数据(需求9-AC2/AC3)。
func (s *Service) PlaybackSummary(maCode string) model.PlaybackSummary {
return s.pb.Summary(maCode)
func (s *Service) PlaybackSummary(ccCode string) model.PlaybackSummary {
return s.pb.Summary(ccCode)
}
// ComputeSettlement 基于可信播放数据计算分账(需求21-AC3)。
func (s *Service) ComputeSettlement(maCode, period string) (model.Settlement, error) {
if _, err := s.chain.QueryContent(maCode); err != nil {
func (s *Service) ComputeSettlement(ccCode, period string) (model.Settlement, error) {
if _, err := s.chain.QueryContent(ccCode); err != nil {
return model.Settlement{}, err
}
return s.pb.ComputeSettlement(maCode, period, model.DefaultShareConfig())
return s.pb.ComputeSettlement(ccCode, period, model.DefaultShareConfig())
}
// ---- 二期 F19/F20:追责取证与确权举证(需求22/23) ----
// Provenance 返回某 MA 码的全链路存证(需求22-AC1)。
func (s *Service) Provenance(maCode string) []model.ProvenanceEvent {
return s.prov.Trail(maCode)
func (s *Service) Provenance(ccCode string) []model.ProvenanceEvent {
return s.prov.Trail(ccCode)
}
// Accountability 责任界定取证:定位首次哈希变化节点与责任方(需求22-AC2)。
func (s *Service) Accountability(maCode string) model.AccountabilityReport {
return s.prov.Accountability(maCode)
func (s *Service) Accountability(ccCode string) model.AccountabilityReport {
return s.prov.Accountability(ccCode)
}
// CopyrightEvidence 导出版权确权证据链(需求23-AC1/AC2)。
func (s *Service) CopyrightEvidence(maCode string) (model.CopyrightEvidence, error) {
c, err := s.chain.QueryContent(maCode)
func (s *Service) CopyrightEvidence(ccCode string) (model.CopyrightEvidence, error) {
c, err := s.chain.QueryContent(ccCode)
if err != nil {
return model.CopyrightEvidence{}, err
}
trail := s.prov.Trail(maCode)
trail := s.prov.Trail(ccCode)
ev := model.CopyrightEvidence{
MACode: maCode, Title: c.Title, Issuer: c.Issuer, IssueDate: c.IssueDate,
ContentHash: c.FileHash, ChainAnchor: "chain://" + maCode, Trail: trail,
CCCode: ccCode, Title: c.Title, Issuer: c.Issuer, IssueDate: c.IssueDate,
ContentHash: c.FileHash, ChainAnchor: "chain://" + ccCode, Trail: trail,
Statement: "本证据链由 MA 码、内容哈希与上链时间戳构成,遵循『谁先锁定谁有权』,不可抵赖,可用于侵权投诉与司法举证。",
}
for _, e := range trail {
@@ -271,7 +271,7 @@ func (s *Service) MatchInfringement(perceptual string, high, medium int) ([]mode
if d <= high {
sim = "high"
}
out = append(out, model.InfringeMatch{MACode: ma, Title: e.Title, Distance: d, Similarity: sim})
out = append(out, model.InfringeMatch{CCCode: ma, Title: e.Title, Distance: d, Similarity: sim})
}
}
return out, nil
@@ -280,13 +280,13 @@ func (s *Service) MatchInfringement(perceptual string, high, medium int) ([]mode
// ---- 二期 F22:授权链与发布前核验(需求25) ----
// RecordAuthorization 登记信息网络传播权授权(需求25-AC1)。
func (s *Service) RecordAuthorization(maCode string, regions, platforms []string, expiry time.Time) error {
if _, err := s.chain.QueryContent(maCode); err != nil {
func (s *Service) RecordAuthorization(ccCode string, regions, platforms []string, expiry time.Time) error {
if _, err := s.chain.QueryContent(ccCode); err != nil {
return err
}
s.mu.Lock()
s.auths[maCode] = model.Authorization{
MACode: maCode, Regions: regions, Platforms: platforms,
s.auths[ccCode] = model.Authorization{
CCCode: ccCode, Regions: regions, Platforms: platforms,
ExpiryAt: expiry, GrantedAt: time.Now(),
}
s.mu.Unlock()
@@ -295,9 +295,9 @@ func (s *Service) RecordAuthorization(maCode string, regions, platforms []string
// CheckAuthorization 核验某地域/平台是否在授权范围内(需求25-AC2/AC3)。
// 未登记授权时默认放行(向后兼容);登记后超地域/过期/非授权平台拦截。
func (s *Service) CheckAuthorization(maCode, region, platform string) model.AuthCheckResult {
func (s *Service) CheckAuthorization(ccCode, region, platform string) model.AuthCheckResult {
s.mu.Lock()
a, ok := s.auths[maCode]
a, ok := s.auths[ccCode]
s.mu.Unlock()
if !ok {
return model.AuthCheckResult{Allowed: true, Reason: "未登记授权限制"}
@@ -317,8 +317,8 @@ func (s *Service) CheckAuthorization(maCode, region, platform string) model.Auth
// ---- 二期 F21:追更与增量哈希更新(需求24) ----
// AddEpisodes 追更:为已发码剧追加新集哈希,不触发存量重审、不重新发码(需求24-AC4)。
func (s *Service) AddEpisodes(role chain.Role, maCode string, episodes []model.EpisodeHash) error {
c, err := s.chain.QueryContent(maCode)
func (s *Service) AddEpisodes(role chain.Role, ccCode string, episodes []model.EpisodeHash) error {
c, err := s.chain.QueryContent(ccCode)
if err != nil {
return err
}
@@ -336,7 +336,7 @@ func (s *Service) AddEpisodes(role chain.Role, maCode string, episodes []model.E
}
}
s.prov.Record(model.ProvenanceEvent{
MACode: maCode, Node: model.NodeSubmit,
CCCode: ccCode, Node: model.NodeSubmit,
Operator: "追更", Detail: "追加新集,增量赋码",
})
return nil
@@ -345,26 +345,26 @@ func (s *Service) AddEpisodes(role chain.Role, maCode string, episodes []model.E
// ---- 二期 F13:跨省复用快速准入(需求13) ----
// Blacklist 将 MA 码加入黑名单(用于跨省校验)。
func (s *Service) Blacklist(maCode string) {
func (s *Service) Blacklist(ccCode string) {
s.mu.Lock()
s.black[maCode] = true
s.black[ccCode] = true
s.mu.Unlock()
}
// CrossProvinceAdmit B 省凭 MA 码+哈希证书快速准入(需求13)。
// 三重校验:MA 码有效 + 哈希与原过审版一致 + 非黑名单。
func (s *Service) CrossProvinceAdmit(maCode, fileHash, province string) model.CrossProvinceResult {
func (s *Service) CrossProvinceAdmit(ccCode, fileHash, province string) model.CrossProvinceResult {
res := model.CrossProvinceResult{}
// 1. MA 码有效
if _, err := s.chain.QueryContent(maCode); err != nil {
if _, err := s.chain.QueryContent(ccCode); err != nil {
res.Reason = "MA 码无效或不存在"
return res
}
res.MACodeValid = true
res.CCCodeValid = true
// 2. 哈希与原过审版一致
vr, err := s.chain.VerifyHash(maCode, fileHash)
vr, err := s.chain.VerifyHash(ccCode, fileHash)
if err != nil || !vr.Match {
res.Reason = "哈希与原过审版不一致"
return res
@@ -373,7 +373,7 @@ func (s *Service) CrossProvinceAdmit(maCode, fileHash, province string) model.Cr
// 3. 非黑名单
s.mu.Lock()
bl := s.black[maCode]
bl := s.black[ccCode]
s.mu.Unlock()
if bl {
res.Reason = "内容在黑名单中"
@@ -391,8 +391,8 @@ func (s *Service) CrossProvinceAdmit(maCode, fileHash, province string) model.Cr
// ---- 二期 F08:终端片段抽检(需求8) ----
// TerminalVerifySegment 终端按集抽检:校验某集哈希,不匹配则提示断流(需求8-AC1/AC2)。
func (s *Service) TerminalVerifySegment(maCode string, episode int, segHash string) (bool, string) {
res, err := s.chain.VerifyEpisodeHash(maCode, episode, segHash)
func (s *Service) TerminalVerifySegment(ccCode string, episode int, segHash string) (bool, string) {
res, err := s.chain.VerifyEpisodeHash(ccCode, episode, segHash)
if err != nil {
return false, "无法校验:" + err.Error()
}
+24 -24
View File
@@ -9,7 +9,7 @@ import (
"github.com/tcs-iptv/tcs/internal/hash"
)
// issueOne 完成一次"送审→CSPS审核→发码签发",返回 maCode、ctid、证书。
// issueOne 完成一次"送审→CSPS审核→发码签发",返回 ccCode、ctid、证书。
func issueOne(t *testing.T, s *Service) (string, string, string) {
t.Helper()
sub, err := s.SubmitForReview(sampleSub())
@@ -17,19 +17,19 @@ func issueOne(t *testing.T, s *Service) (string, string, string) {
require.NoError(t, s.ReviewCSPS(sub.ReviewID, true, "reviewer-1")) // 审核在前
issued, err := s.ApproveAndIssue(chain.RoleRegulator, sub.ReviewID, "北京市广播电视局")
require.NoError(t, err)
return issued.MACode, issued.ContentTwinID, issued.Certificate
return issued.CCCode, issued.ContentTwinID, issued.Certificate
}
func TestCSPSAndTranscode(t *testing.T) {
s := newService(t)
maCode, ctid, _ := issueOne(t, s)
ccCode, ctid, _ := issueOne(t, s)
_, err := s.BindTranscoded(chain.RoleReviewer, ctid, "filehash-abc",
"transcoded-h265-4k", "H.265", "3840x2160", "v1.0-4k")
require.NoError(t, err)
// 转码版也能验真通过
res, err := s.Verify(maCode, "transcoded-h265-4k")
res, err := s.Verify(ccCode, "transcoded-h265-4k")
require.NoError(t, err)
assert.True(t, res.Match)
}
@@ -55,33 +55,33 @@ func TestIssueRequiresCSPSApproval(t *testing.T) {
func TestIngestAndPublish(t *testing.T) {
s := newService(t)
maCode, ctid, cert := issueOne(t, s)
ccCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, maCode, ctid, "MEDIA-001", "广东IPTV媒资库"))
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, ccCode, ctid, "MEDIA-001", "广东IPTV媒资库"))
// 无证书发布被拒
err := s.PublishToOperator(PublishRequest{MACode: maCode, Certificate: ""})
err := s.PublishToOperator(PublishRequest{CCCode: ccCode, Certificate: ""})
assert.ErrorIs(t, err, ErrNoCertificate)
// 携带证书发布成功
require.NoError(t, s.PublishToOperator(PublishRequest{MACode: maCode, Certificate: cert}))
require.NoError(t, s.PublishToOperator(PublishRequest{CCCode: ccCode, Certificate: cert}))
}
func TestInjectToCDN_MatchAndMismatch(t *testing.T) {
s := newService(t)
maCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, maCode, ctid, "MEDIA-001", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{MACode: maCode, Certificate: cert}))
ccCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, ccCode, ctid, "MEDIA-001", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{CCCode: ccCode, Certificate: cert}))
// 哈希匹配 → 允许注入
res, err := s.InjectToCDN(chain.RoleOperator, ctid, maCode, "filehash-abc",
res, err := s.InjectToCDN(chain.RoleOperator, ctid, ccCode, "filehash-abc",
"CT-IPTV-GD", "cdn://ct-gd/iptv/vod/008923")
require.NoError(t, err)
assert.True(t, res.Allowed)
assert.NotEmpty(t, res.DistributionID)
// 哈希不匹配 → 拒绝注入
res, err = s.InjectToCDN(chain.RoleOperator, ctid, maCode, "tampered-hash",
res, err = s.InjectToCDN(chain.RoleOperator, ctid, ccCode, "tampered-hash",
"CT-IPTV-GD", "cdn://x")
assert.ErrorIs(t, err, ErrHashMismatch)
assert.False(t, res.Allowed)
@@ -89,30 +89,30 @@ func TestInjectToCDN_MatchAndMismatch(t *testing.T) {
func TestInjectToCDN_RevokedBlocked(t *testing.T) {
s := newService(t)
maCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, maCode, ctid, "MEDIA-001", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{MACode: maCode, Certificate: cert}))
ccCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, ccCode, ctid, "MEDIA-001", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{CCCode: ccCode, Certificate: cert}))
// 下架后不得注入
_, err := s.Takedown(chain.RoleRegulator, maCode, "违规")
_, err := s.Takedown(chain.RoleRegulator, ccCode, "违规")
require.NoError(t, err)
_, err = s.InjectToCDN(chain.RoleOperator, ctid, maCode, "filehash-abc", "OP", "cdn://x")
_, err = s.InjectToCDN(chain.RoleOperator, ctid, ccCode, "filehash-abc", "OP", "cdn://x")
assert.ErrorIs(t, err, ErrNotApproved)
}
func TestTakedown_ResolvesMappings(t *testing.T) {
s := newService(t)
maCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, maCode, ctid, "MEDIA-001", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{MACode: maCode, Certificate: cert}))
_, _ = s.InjectToCDN(chain.RoleOperator, ctid, maCode, "filehash-abc", "CT-IPTV-GD", "cdn://ct-gd/vod/1")
ccCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, ccCode, ctid, "MEDIA-001", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{CCCode: ccCode, Certificate: cert}))
_, _ = s.InjectToCDN(chain.RoleOperator, ctid, ccCode, "filehash-abc", "CT-IPTV-GD", "cdn://ct-gd/vod/1")
// 非监管主体不得下架
_, err := s.Takedown(chain.RoleOperator, maCode, "越权")
_, err := s.Takedown(chain.RoleOperator, ccCode, "越权")
assert.ErrorIs(t, err, chain.ErrPermissionDenied)
// 监管下架,解析出 CDN 端点
res, err := s.Takedown(chain.RoleRegulator, maCode, "违规")
res, err := s.Takedown(chain.RoleRegulator, ccCode, "违规")
require.NoError(t, err)
assert.Contains(t, res.CDNEndpoints, "cdn://ct-gd/vod/1")
}
+16 -16
View File
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/cccode"
"github.com/tcs-iptv/tcs/internal/model"
)
@@ -24,7 +24,7 @@ func TestEpisodeLevel_OneSeriesOneCodeMultiEpisodeHash(t *testing.T) {
})
}
sub := Submission{
Title: "长安少年行", EpisodeCount: 24, Category: macode.CategoryMicroDrama,
Title: "长安少年行", EpisodeCount: 24, Category: cccode.CategoryMicroDrama,
FileHash: "series-root-hash", MerkleRoot: "series-merkle-root",
Episodes: eps,
CPMediaID: "XAQJSL-2026-001", CPName: "西安曲江丝路文化传播有限公司",
@@ -36,20 +36,20 @@ func TestEpisodeLevel_OneSeriesOneCodeMultiEpisodeHash(t *testing.T) {
require.NoError(t, err)
// 一剧一码
assert.True(t, macode.IsValid(issued.MACode))
assert.True(t, cccode.IsValid(issued.CCCode))
// 24 集哈希全部绑定在同一 MA 码下
list, err := s.ListEpisodes(issued.MACode)
list, err := s.ListEpisodes(issued.CCCode)
require.NoError(t, err)
assert.Len(t, list, 24)
// 按集验真:第 7 集正确哈希匹配
res, err := s.VerifyEpisode(issued.MACode, 7, "ep-hash-"+string(rune('a'+7)))
res, err := s.VerifyEpisode(issued.CCCode, 7, "ep-hash-"+string(rune('a'+7)))
require.NoError(t, err)
assert.True(t, res.Match)
// 第 7 集错误哈希 → 不匹配(疑似该集被替换)
_, err = s.VerifyEpisode(issued.MACode, 7, "tampered-ep7")
_, err = s.VerifyEpisode(issued.CCCode, 7, "tampered-ep7")
assert.ErrorIs(t, err, ErrHashMismatch)
}
@@ -62,7 +62,7 @@ func TestEpisodeTakedown(t *testing.T) {
{Episode: 3, FileSHA256: "h3"}, {Episode: 4, FileSHA256: "h4"},
}
sub := Submission{
Title: "多集剧", EpisodeCount: 4, Category: macode.CategoryMicroDrama,
Title: "多集剧", EpisodeCount: 4, Category: cccode.CategoryMicroDrama,
FileHash: "series-h", MerkleRoot: "series-mr", Episodes: eps,
}
r, err := s.SubmitForReview(sub)
@@ -72,13 +72,13 @@ func TestEpisodeTakedown(t *testing.T) {
require.NoError(t, err)
// 运营商无权集级下架
err = s.TakedownEpisode(chain.RoleOperator, issued.MACode, 3, "第3集违规")
err = s.TakedownEpisode(chain.RoleOperator, issued.CCCode, 3, "第3集违规")
assert.ErrorIs(t, err, chain.ErrPermissionDenied)
// 监管下架第3集
require.NoError(t, s.TakedownEpisode(chain.RoleRegulator, issued.MACode, 3, "第3集违规"))
require.NoError(t, s.TakedownEpisode(chain.RoleRegulator, issued.CCCode, 3, "第3集违规"))
list, err := s.ListEpisodes(issued.MACode)
list, err := s.ListEpisodes(issued.CCCode)
require.NoError(t, err)
for _, b := range list {
if b.Episode == 3 {
@@ -89,18 +89,18 @@ func TestEpisodeTakedown(t *testing.T) {
}
}
// 集级子标识:MA码#E07 解析与生成。
// 集级子标识:CC码#E07 解析与生成。
func TestEpisodeSubID(t *testing.T) {
ma := "MA.156.8531.6101/WD/20260000001"
sub := macode.EpisodeSubID(ma, 7)
sub := cccode.EpisodeSubID(ma, 7)
assert.Equal(t, "MA.156.8531.6101/WD/20260000001#E07", sub)
parsedMA, ep := macode.ParseEpisodeSubID(sub)
parsedMA, ep := cccode.ParseEpisodeSubID(sub)
assert.Equal(t, ma, parsedMA)
assert.Equal(t, 7, ep)
// 无后缀 → 整剧(episode 0
parsedMA2, ep2 := macode.ParseEpisodeSubID(ma)
parsedMA2, ep2 := cccode.ParseEpisodeSubID(ma)
assert.Equal(t, ma, parsedMA2)
assert.Equal(t, 0, ep2)
}
@@ -116,12 +116,12 @@ func TestSingleContent_NoEpisodes(t *testing.T) {
issued, err := s.ApproveAndIssue(chain.RoleRegulator, r.ReviewID, "陕西IPTV运营公司")
require.NoError(t, err)
list, err := s.ListEpisodes(issued.MACode)
list, err := s.ListEpisodes(issued.CCCode)
require.NoError(t, err)
assert.Empty(t, list, "单体内容无集级绑定")
// 整剧验真仍可用
res, err := s.Verify(issued.MACode, sub.FileHash)
res, err := s.Verify(issued.CCCode, sub.FileHash)
require.NoError(t, err)
assert.True(t, res.Match)
}
+28 -28
View File
@@ -14,38 +14,38 @@ import (
func TestAuthorization_PlatformGate(t *testing.T) {
s := newService(t)
maCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, maCode, ctid, "M", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{MACode: maCode, Certificate: cert}))
ccCode, ctid, cert := issueOne(t, s)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, ccCode, ctid, "M", "媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{CCCode: ccCode, Certificate: cert}))
// 仅授权 CT-SX
require.NoError(t, s.RecordAuthorization(maCode, nil, []string{"CT-SX"}, time.Time{}))
require.NoError(t, s.RecordAuthorization(ccCode, nil, []string{"CT-SX"}, time.Time{}))
// 授权平台注入通过
_, err := s.InjectToCDN(chain.RoleOperator, ctid, maCode, "filehash-abc", "CT-SX", "cdn://ct")
_, err := s.InjectToCDN(chain.RoleOperator, ctid, ccCode, "filehash-abc", "CT-SX", "cdn://ct")
require.NoError(t, err)
// 非授权平台注入被拒
_, err = s.InjectToCDN(chain.RoleOperator, ctid, maCode, "filehash-abc", "CM-SX", "cdn://cm")
_, err = s.InjectToCDN(chain.RoleOperator, ctid, ccCode, "filehash-abc", "CM-SX", "cdn://cm")
assert.ErrorIs(t, err, ErrNotApproved)
}
func TestAuthorization_Expiry(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s)
ccCode, _, _ := issueOne(t, s)
// 已过期授权
require.NoError(t, s.RecordAuthorization(maCode, nil, nil, time.Now().Add(-time.Hour)))
res := s.CheckAuthorization(maCode, "", "")
require.NoError(t, s.RecordAuthorization(ccCode, nil, nil, time.Now().Add(-time.Hour)))
res := s.CheckAuthorization(ccCode, "", "")
assert.False(t, res.Allowed)
assert.Contains(t, res.Reason, "过期")
}
func TestAuthorization_RegionGate(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s)
require.NoError(t, s.RecordAuthorization(maCode, []string{"610000"}, nil, time.Time{}))
assert.True(t, s.CheckAuthorization(maCode, "610000", "").Allowed) // 陕西
assert.False(t, s.CheckAuthorization(maCode, "440000", "").Allowed) // 广东,超域
ccCode, _, _ := issueOne(t, s)
require.NoError(t, s.RecordAuthorization(ccCode, []string{"610000"}, nil, time.Time{}))
assert.True(t, s.CheckAuthorization(ccCode, "610000", "").Allowed) // 陕西
assert.False(t, s.CheckAuthorization(ccCode, "440000", "").Allowed) // 广东,超域
}
// ---- F21 追更 ----
@@ -61,16 +61,16 @@ func TestAddEpisodes_NoReissue(t *testing.T) {
require.NoError(t, err)
// 追更第 3、4 集(不重新发码)
require.NoError(t, s.AddEpisodes(chain.RoleReviewer, issued.MACode, []model.EpisodeHash{
require.NoError(t, s.AddEpisodes(chain.RoleReviewer, issued.CCCode, []model.EpisodeHash{
{Episode: 3, FileSHA256: "e3"}, {Episode: 4, FileSHA256: "e4"},
}))
list, err := s.ListEpisodes(issued.MACode)
list, err := s.ListEpisodes(issued.CCCode)
require.NoError(t, err)
assert.Len(t, list, 4, "应有 4 集")
// 新集可独立验真
res, err := s.VerifyEpisode(issued.MACode, 3, "e3")
res, err := s.VerifyEpisode(issued.CCCode, 3, "e3")
require.NoError(t, err)
assert.True(t, res.Match)
}
@@ -79,28 +79,28 @@ func TestAddEpisodes_NoReissue(t *testing.T) {
func TestCrossProvince_Admit(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s) // sampleSub FileHash=filehash-abc
ccCode, _, _ := issueOne(t, s) // sampleSub FileHash=filehash-abc
res := s.CrossProvinceAdmit(maCode, "filehash-abc", "610000")
res := s.CrossProvinceAdmit(ccCode, "filehash-abc", "610000")
assert.True(t, res.Admitted)
assert.True(t, res.MACodeValid && res.HashConsistent && res.NotBlacklisted)
assert.True(t, res.CCCodeValid && res.HashConsistent && res.NotBlacklisted)
assert.NotEmpty(t, res.ProvinceFlowNo)
}
func TestCrossProvince_HashMismatch(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s)
res := s.CrossProvinceAdmit(maCode, "tampered", "610000")
ccCode, _, _ := issueOne(t, s)
res := s.CrossProvinceAdmit(ccCode, "tampered", "610000")
assert.False(t, res.Admitted)
assert.True(t, res.MACodeValid)
assert.True(t, res.CCCodeValid)
assert.False(t, res.HashConsistent)
}
func TestCrossProvince_Blacklisted(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s)
s.Blacklist(maCode)
res := s.CrossProvinceAdmit(maCode, "filehash-abc", "610000")
ccCode, _, _ := issueOne(t, s)
s.Blacklist(ccCode)
res := s.CrossProvinceAdmit(ccCode, "filehash-abc", "610000")
assert.False(t, res.Admitted)
assert.False(t, res.NotBlacklisted)
assert.Contains(t, res.Reason, "黑名单")
@@ -110,7 +110,7 @@ func TestCrossProvince_UnknownMA(t *testing.T) {
s := newService(t)
res := s.CrossProvinceAdmit("MA.156.8531.6101/WD/不存在", "h", "610000")
assert.False(t, res.Admitted)
assert.False(t, res.MACodeValid)
assert.False(t, res.CCCodeValid)
}
// ---- F08 终端抽检 ----
@@ -124,10 +124,10 @@ func TestTerminalVerifySegment(t *testing.T) {
issued, err := s.ApproveAndIssue(chain.RoleRegulator, r.ReviewID, "陕西IPTV")
require.NoError(t, err)
ok, _ := s.TerminalVerifySegment(issued.MACode, 1, "seg1")
ok, _ := s.TerminalVerifySegment(issued.CCCode, 1, "seg1")
assert.True(t, ok)
ok, msg := s.TerminalVerifySegment(issued.MACode, 1, "tampered")
ok, msg := s.TerminalVerifySegment(issued.CCCode, 1, "tampered")
assert.False(t, ok)
assert.Contains(t, msg, "断流")
}
+25 -25
View File
@@ -14,16 +14,16 @@ import (
// 将 MA+哈希机制从 IPTV 扩展至 OTT、手机 APP,实现大小屏内容身份互通。
// 对应任务:C.1 跨域解析网关、C.2 身份互通、B.2 扫码验真、D.1 跨屏权益子链。
// maCodePattern 匹配六段式 MA 码(含可选集级子标识 #Exx)。
// ccCodePattern 匹配六段式 MA 码(含可选集级子标识 #Exx)。
// 形如 MA.156.8531.6101/WD/20260000004 或 MA.156.8531.6101/WD/20260000004#E07
var maCodePattern = regexp.MustCompile(
var ccCodePattern = regexp.MustCompile(
`^(MA)\.(\d{3})\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)/([0-9A-Za-z]+)/(\d{4})(\d+)(?:#E(\d+))?$`)
// ParseMACode 解析六段式 MA 码(纯结构解析,不查链)。
// ParseCCCode 解析六段式 MA 码(纯结构解析,不查链)。
// 返回 Valid=false 表示结构不合法(可能是伪造/损坏的码)。
func ParseMACode(maCode string) model.ParsedMA {
p := model.ParsedMA{Raw: maCode}
m := maCodePattern.FindStringSubmatch(strings.TrimSpace(maCode))
func ParseCCCode(ccCode string) model.ParsedMA {
p := model.ParsedMA{Raw: ccCode}
m := ccCodePattern.FindStringSubmatch(strings.TrimSpace(ccCode))
if m == nil {
p.Valid = false
return p
@@ -44,19 +44,19 @@ func ParseMACode(maCode string) model.ParsedMA {
return p
}
// baseMACode 去除集级子标识 #Exx,返回整剧 MA 码(用于查链/查权益)。
func baseMACode(maCode string) string {
if i := strings.Index(maCode, "#"); i >= 0 {
return maCode[:i]
// baseCCCode 去除集级子标识 #Exx,返回整剧 MA 码(用于查链/查权益)。
func baseCCCode(ccCode string) string {
if i := strings.Index(ccCode, "#"); i >= 0 {
return ccCode[:i]
}
return maCode
return ccCode
}
// Resolve 跨域解析网关(C.1/C.2):同一 MA 码在 IPTV/OTT/APP 统一解析。
// 返回解析结构 + 流通状态 + 跨屏可用性,保证大小屏解析结果一致。
func (s *Service) Resolve(maCode string) model.ResolveResult {
res := model.ResolveResult{MACode: maCode}
parsed := ParseMACode(maCode)
func (s *Service) Resolve(ccCode string) model.ResolveResult {
res := model.ResolveResult{CCCode: ccCode}
parsed := ParseCCCode(ccCode)
res.Parsed = parsed
if !parsed.Valid {
res.Resolved = false
@@ -64,7 +64,7 @@ func (s *Service) Resolve(maCode string) model.ResolveResult {
return res
}
c, err := s.chain.QueryContent(baseMACode(maCode))
c, err := s.chain.QueryContent(baseCCCode(ccCode))
if err != nil {
res.Resolved = false
res.Message = "MA 码未在可信数据空间登记"
@@ -89,9 +89,9 @@ func (s *Service) Resolve(maCode string) model.ResolveResult {
}
// ScanVerify 用户扫码验真(B.2):验证内容 MA 码真伪、合规与流通状态,防盗版。
func (s *Service) ScanVerify(maCode string) model.ScanVerifyResult {
res := model.ScanVerifyResult{MACode: maCode}
parsed := ParseMACode(maCode)
func (s *Service) ScanVerify(ccCode string) model.ScanVerifyResult {
res := model.ScanVerifyResult{CCCode: ccCode}
parsed := ParseCCCode(ccCode)
res.Parsed = parsed
if !parsed.Valid {
res.Authentic = false
@@ -100,7 +100,7 @@ func (s *Service) ScanVerify(maCode string) model.ScanVerifyResult {
return res
}
c, err := s.chain.QueryContent(baseMACode(maCode))
c, err := s.chain.QueryContent(baseCCCode(ccCode))
if err != nil {
res.Authentic = false
res.Compliant = false
@@ -127,14 +127,14 @@ func (s *Service) ScanVerify(maCode string) model.ScanVerifyResult {
// RecordPurchase 记录用户购买(D.1):用户在某一屏购买内容,记录跨屏权益。
// 校验 MA 码有效、屏类型合法;同一用户对同一 MA 码重复购买不覆盖首次记录。
func (s *Service) RecordPurchase(maCode, userHash string, screen model.ScreenType) (model.PurchaseRecord, error) {
func (s *Service) RecordPurchase(ccCode, userHash string, screen model.ScreenType) (model.PurchaseRecord, error) {
if userHash == "" {
return model.PurchaseRecord{}, fmt.Errorf("service: 缺少用户标识")
}
if !model.ValidScreen(screen) {
return model.PurchaseRecord{}, fmt.Errorf("service: 非法屏类型 %q(仅 iptv/ott/app", screen)
}
base := baseMACode(maCode)
base := baseCCCode(ccCode)
if _, err := s.chain.QueryContent(base); err != nil {
return model.PurchaseRecord{}, fmt.Errorf("service: MA 码无效或未登记: %w", err)
}
@@ -151,17 +151,17 @@ func (s *Service) RecordPurchase(maCode, userHash string, screen model.ScreenTyp
return rec, nil
}
rec := model.PurchaseRecord{
MACode: base, UserHash: userHash, Screen: screen, PurchasedAt: time.Now(),
CCCode: base, UserHash: userHash, Screen: screen, PurchasedAt: time.Now(),
}
acct.Purchases[base] = rec
return rec, nil
}
// VerifyCrossScreenRights 跨屏权益核验(D.1):任一屏购买即全屏通看,不重复付费。
func (s *Service) VerifyCrossScreenRights(maCode, userHash string, requestScreen model.ScreenType) model.CrossScreenRightsResult {
base := baseMACode(maCode)
func (s *Service) VerifyCrossScreenRights(ccCode, userHash string, requestScreen model.ScreenType) model.CrossScreenRightsResult {
base := baseCCCode(ccCode)
res := model.CrossScreenRightsResult{
MACode: base, UserHash: userHash, RequestScreen: requestScreen,
CCCode: base, UserHash: userHash, RequestScreen: requestScreen,
}
if !model.ValidScreen(requestScreen) {
res.Entitled = false
+37 -37
View File
@@ -10,20 +10,20 @@ import (
)
// issueAndPublish 走完送审→审核→发码→入库→发布,返回已流通内容的 MA 码与 ctid。
func issueAndPublish(t *testing.T, s *Service) (maCode, ctid string) {
func issueAndPublish(t *testing.T, s *Service) (ccCode, ctid string) {
t.Helper()
sub, err := s.SubmitForReview(sampleSub())
require.NoError(t, err)
require.NoError(t, s.ReviewCSPS(sub.ReviewID, true, "rv-1"))
issued, err := s.ApproveAndIssue(chain.RoleRegulator, sub.ReviewID, "陕西省广播电视局")
require.NoError(t, err)
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, issued.MACode, issued.ContentTwinID, "MA-001", "省媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{MACode: issued.MACode, Certificate: issued.Certificate}))
return issued.MACode, issued.ContentTwinID
require.NoError(t, s.IngestToLibrary(chain.RoleReviewer, issued.CCCode, issued.ContentTwinID, "MA-001", "省媒资库"))
require.NoError(t, s.PublishToOperator(PublishRequest{CCCode: issued.CCCode, Certificate: issued.Certificate}))
return issued.CCCode, issued.ContentTwinID
}
func TestParseMACode_Valid(t *testing.T) {
p := ParseMACode("MA.156.8531.6101/WD/20260000004")
func TestParseCCCode_Valid(t *testing.T) {
p := ParseCCCode("MA.156.8531.6101/WD/20260000004")
assert.True(t, p.Valid)
assert.Equal(t, "156", p.CountryCode)
assert.Equal(t, "8531", p.IndustryNode)
@@ -33,24 +33,24 @@ func TestParseMACode_Valid(t *testing.T) {
assert.Equal(t, 0, p.Episode)
}
func TestParseMACode_WithEpisode(t *testing.T) {
p := ParseMACode("MA.156.8531.6101/WD/20260000004#E07")
func TestParseCCCode_WithEpisode(t *testing.T) {
p := ParseCCCode("MA.156.8531.6101/WD/20260000004#E07")
assert.True(t, p.Valid)
assert.Equal(t, 7, p.Episode)
}
func TestParseMACode_Invalid(t *testing.T) {
func TestParseCCCode_Invalid(t *testing.T) {
for _, bad := range []string{"", "not-a-code", "MA.156", "XX.156.8531.6101/WD/20260000004"} {
p := ParseMACode(bad)
p := ParseCCCode(bad)
assert.False(t, p.Valid, "应判定非法: %q", bad)
}
}
func TestResolve_PublishedAvailableAllScreens(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
ccCode, _ := issueAndPublish(t, s)
r := s.Resolve(maCode)
r := s.Resolve(ccCode)
assert.True(t, r.Resolved)
assert.True(t, r.InCirculation)
assert.Equal(t, model.StatusPublished, r.Status)
@@ -59,9 +59,9 @@ func TestResolve_PublishedAvailableAllScreens(t *testing.T) {
func TestResolve_EpisodeSubIDResolvesToBase(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
ccCode, _ := issueAndPublish(t, s)
r := s.Resolve(maCode + "#E03")
r := s.Resolve(ccCode + "#E03")
assert.True(t, r.Resolved)
assert.Equal(t, 3, r.Parsed.Episode)
assert.True(t, r.InCirculation)
@@ -83,11 +83,11 @@ func TestResolve_UnregisteredCode(t *testing.T) {
func TestResolve_RevokedNotInCirculation(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
_, err := s.Takedown(chain.RoleRegulator, maCode, "违规")
ccCode, _ := issueAndPublish(t, s)
_, err := s.Takedown(chain.RoleRegulator, ccCode, "违规")
require.NoError(t, err)
r := s.Resolve(maCode)
r := s.Resolve(ccCode)
assert.True(t, r.Resolved)
assert.False(t, r.InCirculation)
assert.Empty(t, r.Screens)
@@ -95,9 +95,9 @@ func TestResolve_RevokedNotInCirculation(t *testing.T) {
func TestScanVerify_AuthenticAndCompliant(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
ccCode, _ := issueAndPublish(t, s)
r := s.ScanVerify(maCode)
r := s.ScanVerify(ccCode)
assert.True(t, r.Authentic)
assert.True(t, r.Compliant)
}
@@ -111,23 +111,23 @@ func TestScanVerify_FakeCode(t *testing.T) {
func TestScanVerify_RevokedNotCompliant(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
_, err := s.Takedown(chain.RoleRegulator, maCode, "违规")
ccCode, _ := issueAndPublish(t, s)
_, err := s.Takedown(chain.RoleRegulator, ccCode, "违规")
require.NoError(t, err)
r := s.ScanVerify(maCode)
r := s.ScanVerify(ccCode)
assert.True(t, r.Authentic, "下架仍是真码")
assert.False(t, r.Compliant, "下架内容不合规")
}
func TestRecordPurchase_InvalidScreenRejected(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
_, err := s.RecordPurchase(maCode, "user-1", "smartwatch")
ccCode, _ := issueAndPublish(t, s)
_, err := s.RecordPurchase(ccCode, "user-1", "smartwatch")
assert.Error(t, err)
}
func TestRecordPurchase_UnknownMACodeRejected(t *testing.T) {
func TestRecordPurchase_UnknownCCCodeRejected(t *testing.T) {
s := newService(t)
_, err := s.RecordPurchase("MA.156.8531.6101/WD/20269999999", "user-1", model.ScreenIPTV)
assert.Error(t, err)
@@ -135,38 +135,38 @@ func TestRecordPurchase_UnknownMACodeRejected(t *testing.T) {
func TestCrossScreenRights_BuyOnceWatchEverywhere(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
ccCode, _ := issueAndPublish(t, s)
// 电视端购买
rec, err := s.RecordPurchase(maCode, "user-1", model.ScreenIPTV)
rec, err := s.RecordPurchase(ccCode, "user-1", model.ScreenIPTV)
require.NoError(t, err)
assert.Equal(t, model.ScreenIPTV, rec.Screen)
// 手机端通看,不重复付费
r := s.VerifyCrossScreenRights(maCode, "user-1", model.ScreenApp)
r := s.VerifyCrossScreenRights(ccCode, "user-1", model.ScreenApp)
assert.True(t, r.Entitled)
assert.Equal(t, model.ScreenIPTV, r.PurchaseScreen)
// OTT 端通看
r2 := s.VerifyCrossScreenRights(maCode, "user-1", model.ScreenOTT)
r2 := s.VerifyCrossScreenRights(ccCode, "user-1", model.ScreenOTT)
assert.True(t, r2.Entitled)
}
func TestCrossScreenRights_NotPurchased(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
r := s.VerifyCrossScreenRights(maCode, "user-x", model.ScreenApp)
ccCode, _ := issueAndPublish(t, s)
r := s.VerifyCrossScreenRights(ccCode, "user-x", model.ScreenApp)
assert.False(t, r.Entitled)
}
func TestRecordPurchase_IdempotentNoDoubleCharge(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
ccCode, _ := issueAndPublish(t, s)
first, err := s.RecordPurchase(maCode, "user-1", model.ScreenIPTV)
first, err := s.RecordPurchase(ccCode, "user-1", model.ScreenIPTV)
require.NoError(t, err)
// 在手机端再次"购买"同内容 → 返回首次记录,不新建(跨屏通兑)
second, err := s.RecordPurchase(maCode, "user-1", model.ScreenApp)
second, err := s.RecordPurchase(ccCode, "user-1", model.ScreenApp)
require.NoError(t, err)
assert.Equal(t, first.Screen, second.Screen, "重复购买应返回首次记录的购买屏")
assert.Equal(t, first.PurchasedAt, second.PurchasedAt)
@@ -174,10 +174,10 @@ func TestRecordPurchase_IdempotentNoDoubleCharge(t *testing.T) {
func TestCrossScreenRights_EpisodeSubIDSharesEntitlement(t *testing.T) {
s := newService(t)
maCode, _ := issueAndPublish(t, s)
_, err := s.RecordPurchase(maCode, "user-1", model.ScreenIPTV)
ccCode, _ := issueAndPublish(t, s)
_, err := s.RecordPurchase(ccCode, "user-1", model.ScreenIPTV)
require.NoError(t, err)
// 用集级子标识核验权益应归一到整剧 MA 码
r := s.VerifyCrossScreenRights(maCode+"#E05", "user-1", model.ScreenApp)
r := s.VerifyCrossScreenRights(ccCode+"#E05", "user-1", model.ScreenApp)
assert.True(t, r.Entitled)
}
+10 -10
View File
@@ -24,44 +24,44 @@ func (s *Service) QueryByLibraryFileID(libraryFileID string) (model.ContentQuery
// ---- MA 合并/拆分(方案 MA 管理模块补足)----
// MergeMACodes 将多个 MA 码合并为一个主 MA 码(仅监管主体)。
// MergeCCCodes 将多个 MA 码合并为一个主 MA 码(仅监管主体)。
// 被合并的 MA 码的哈希绑定和映射迁移至主 MA 码,原 MA 码状态标记为 merged。
// 全链路存证记录合并操作。
func (s *Service) MergeMACodes(role chain.Role, req model.MergeRequest) (model.MergeResult, error) {
func (s *Service) MergeCCCodes(role chain.Role, req model.MergeRequest) (model.MergeResult, error) {
res, err := s.chain.MergeMA(role, req)
if err != nil {
return res, err
}
// 记录存证
s.prov.Record(model.ProvenanceEvent{
MACode: req.PrimaryMACode,
CCCode: req.PrimaryCCCode,
Node: model.NodeIssue,
Operator: req.Operator,
Detail: "MA 码合并:将 " + joinMACodes(req.SecondaryMACodes) + " 合并入 " + req.PrimaryMACode + ",原因:" + req.Reason,
Detail: "MA 码合并:将 " + joinCCCodes(req.SecondaryCCCodes) + " 合并入 " + req.PrimaryCCCode + ",原因:" + req.Reason,
})
return res, nil
}
// SplitMACode 将一个 MA 码拆分为多个独立 MA 码(仅监管主体)。
// SplitCCCode 将一个 MA 码拆分为多个独立 MA 码(仅监管主体)。
// 按集号将哈希绑定和映射迁移至新 MA 码,源 MA 码状态标记为 split。
// 全链路存证记录拆分操作。
func (s *Service) SplitMACode(role chain.Role, req model.SplitRequest) (model.SplitResult, error) {
func (s *Service) SplitCCCode(role chain.Role, req model.SplitRequest) (model.SplitResult, error) {
res, err := s.chain.SplitMA(role, req)
if err != nil {
return res, err
}
// 记录存证
s.prov.Record(model.ProvenanceEvent{
MACode: req.SourceMACode,
CCCode: req.SourceCCCode,
Node: model.NodeIssue,
Operator: req.Operator,
Detail: "MA 码拆分:将 " + req.SourceMACode + " 拆分为 " + joinMACodes(res.NewMACodes) + ",原因:" + req.Reason,
Detail: "MA 码拆分:将 " + req.SourceCCCode + " 拆分为 " + joinCCCodes(res.NewCCCodes) + ",原因:" + req.Reason,
})
return res, nil
}
// joinMACodes 将 MA 码列表拼接为逗号分隔的字符串。
func joinMACodes(codes []string) string {
// joinCCCodes 将 MA 码列表拼接为逗号分隔的字符串。
func joinCCCodes(codes []string) string {
if len(codes) == 0 {
return ""
}
+12 -12
View File
@@ -3,7 +3,7 @@ package service
import (
"time"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/cccode"
"github.com/tcs-iptv/tcs/internal/model"
)
@@ -16,29 +16,29 @@ var orgNodeProvince = map[string]string{
}
// BindFiling 关联备案号/网标号至 MA 码(三期 A.1,对接广电总局备案系统)。
func (s *Service) BindFiling(maCode, licenseNo, filingNo string) (model.FilingRecord, error) {
if _, err := s.chain.QueryContent(maCode); err != nil {
func (s *Service) BindFiling(ccCode, licenseNo, filingNo string) (model.FilingRecord, error) {
if _, err := s.chain.QueryContent(ccCode); err != nil {
return model.FilingRecord{}, err
}
rec := model.FilingRecord{
MACode: maCode, LicenseNo: licenseNo, FilingNo: filingNo,
CCCode: ccCode, LicenseNo: licenseNo, FilingNo: filingNo,
BoundAt: time.Now().Format(time.RFC3339),
}
s.mu.Lock()
s.filings[maCode] = rec
s.filings[ccCode] = rec
s.mu.Unlock()
s.prov.Record(model.ProvenanceEvent{
MACode: maCode, Node: model.NodeIssue, Operator: "广电总局备案系统",
CCCode: ccCode, Node: model.NodeIssue, Operator: "广电总局备案系统",
Detail: "关联网标号 " + licenseNo + " / 备案号 " + filingNo,
})
return rec, nil
}
// QueryFiling 查询备案关联。
func (s *Service) QueryFiling(maCode string) (model.FilingRecord, bool) {
func (s *Service) QueryFiling(ccCode string) (model.FilingRecord, bool) {
s.mu.Lock()
defer s.mu.Unlock()
r, ok := s.filings[maCode]
r, ok := s.filings[ccCode]
return r, ok
}
@@ -56,7 +56,7 @@ func (s *Service) NationalStats() (model.NationalStats, error) {
st.TotalContents = len(all)
for _, c := range all {
st.ByStatus[c.Status]++
p, perr := macode.Parse(c.MACode)
p, perr := cccode.Parse(c.CCCode)
if perr == nil {
st.ByCategory[p.Category]++
prov := orgNodeProvince[p.OrgNode]
@@ -80,12 +80,12 @@ func (s *Service) NationalStats() (model.NationalStats, error) {
}
// ListSegments 列出已登记号段及使用情况(三期 B.1)。
func (s *Service) ListSegments() []macode.SegmentInfo {
func (s *Service) ListSegments() []cccode.SegmentInfo {
return s.gen.Segments()
}
// RegisterSegment 登记新号段(三期 B.1,与发码机构对接后配置)。
func (s *Service) RegisterSegment(seg macode.Segment) error {
func (s *Service) RegisterSegment(seg cccode.Segment) error {
return s.gen.RegisterSegment(seg)
}
@@ -101,7 +101,7 @@ func (s *Service) DailyRegulatoryReport(date string) (model.RegulatoryReport, er
}
for _, c := range all {
rep.TotalNew++
if p, perr := macode.Parse(c.MACode); perr == nil {
if p, perr := cccode.Parse(c.CCCode); perr == nil {
rep.LevelDist[p.Category]++
}
if c.Status == model.StatusRevoked {
+5 -5
View File
@@ -6,15 +6,15 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/cccode"
)
func newServiceSX(t *testing.T) *Service {
t.Helper()
gen := macode.NewGenerator(macode.NewMemoryStore())
require.NoError(t, gen.RegisterSegment(macode.Segment{
gen := cccode.NewGenerator(cccode.NewMemoryStore())
require.NoError(t, gen.RegisterSegment(cccode.Segment{
IndustryNode: "8531", OrgNode: "6101", // 陕西
Category: macode.CategoryMicroDrama, Start: 1, End: 100, SeqWidth: 7,
Category: cccode.CategoryMicroDrama, Start: 1, End: 100, SeqWidth: 7,
}))
return New(chain.NewMemoryChain(), gen)
}
@@ -26,7 +26,7 @@ func issueSX(t *testing.T, s *Service) string {
require.NoError(t, s.ReviewCSPS(r.ReviewID, true, "rv"))
iss, err := s.ApproveAndIssue(chain.RoleRegulator, r.ReviewID, "陕西IPTV运营公司")
require.NoError(t, err)
return iss.MACode
return iss.CCCode
}
func TestBindFiling(t *testing.T) {
+35 -35
View File
@@ -10,7 +10,7 @@ import (
"time"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/cccode"
"github.com/tcs-iptv/tcs/internal/model"
"github.com/tcs-iptv/tcs/internal/playback"
"github.com/tcs-iptv/tcs/internal/provenance"
@@ -30,7 +30,7 @@ var (
type Submission struct {
Title string
EpisodeCount int
Category string // 内容类目(macode.CategoryXxx),决定发码号段
Category string // 内容类目(cccode.CategoryXxx),决定发码号段
FileHash string
MerkleRoot string
Perceptual string
@@ -50,13 +50,13 @@ type SubmissionResult struct {
// Service 业务编排器。
type Service struct {
chain chain.Client
gen *macode.Generator
gen *cccode.Generator
pb *playback.Store
prov *provenance.Store
phash map[string]phashEntry // maCode -> 感知哈希条目(确权侵权比对)
auths map[string]model.Authorization // maCode -> 授权(F22
black map[string]bool // maCode -> 黑名单(跨省复用校验)
filings map[string]model.FilingRecord // maCode -> 备案/网标关联(三期 A.1)
phash map[string]phashEntry // ccCode -> 感知哈希条目(确权侵权比对)
auths map[string]model.Authorization // ccCode -> 授权(F22
black map[string]bool // ccCode -> 黑名单(跨省复用校验)
filings map[string]model.FilingRecord // ccCode -> 备案/网标关联(三期 A.1)
rights map[string]*model.UserRights // userHash -> 跨屏权益账户(四期 D.1)
mu sync.Mutex
seqMu sync.Mutex
@@ -69,7 +69,7 @@ type reviewItem struct {
ContentTwinID string
Sub Submission
Status string
MACode string
CCCode string
}
// phashEntry 感知哈希注册项,用于确权侵权比对。
@@ -79,7 +79,7 @@ type phashEntry struct {
}
// New 创建业务服务。
func New(c chain.Client, gen *macode.Generator) *Service {
func New(c chain.Client, gen *cccode.Generator) *Service {
return &Service{
chain: c, gen: gen,
pb: playback.NewStore(),
@@ -107,10 +107,10 @@ func (s *Service) SubmitForReview(sub Submission) (SubmissionResult, error) {
return SubmissionResult{}, ErrIncompleteHashPkg
}
// 防换壳重发(需求2-AC3、需求15-AC5
if maCode, exists := s.chain.HashExists(sub.FileHash); exists {
if ccCode, exists := s.chain.HashExists(sub.FileHash); exists {
return SubmissionResult{
Status: "rejected",
Message: fmt.Sprintf("内容哈希已存在,关联原 MA 码: %s", maCode),
Message: fmt.Sprintf("内容哈希已存在,关联原 MA 码: %s", ccCode),
}, ErrDuplicateContent
}
@@ -133,14 +133,14 @@ func (s *Service) SubmitForReview(sub Submission) (SubmissionResult, error) {
// IssueResult 签发结果。
type IssueResult struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
ContentTwinID string `json:"content_twin_id"`
TxID string `json:"tx_id"`
Certificate string `json:"certificate"` // MA码+哈希证书(MVP 简化为字符串)
Certificate string `json:"certificate"` // CC码+哈希证书(MVP 简化为字符串)
}
// ReviewCSPS CSPS 合规审核(发码前)。审核通过后方可发码,体现"审过才发证发码"。
// 对应需求5(CSPS审核)+ 需求3-AC2(审核通过后生成MA码)。
// 对应需求5(CSPS审核)+ 需求3-AC2(审核通过后生成CC码)。
func (s *Service) ReviewCSPS(reviewID string, approved bool, reviewerID string) error {
s.mu.Lock()
defer s.mu.Unlock()
@@ -158,7 +158,7 @@ func (s *Service) ReviewCSPS(reviewID string, approved bool, reviewerID string)
// ApproveAndIssue 在 CSPS 审核通过后**生成 MA 码**并强绑定哈希(需求3,模式B 自行发码)。
// 前置:该送审必须已通过 CSPS 审核(审过才发码)。
// MA 码由 macode.Generator 按内容类目从号段中原子分配。仅监管主体可调用。
// MA 码由 cccode.Generator 按内容类目从号段中原子分配。仅监管主体可调用。
func (s *Service) ApproveAndIssue(role chain.Role, reviewID, issuer string) (IssueResult, error) {
s.mu.Lock()
item, ok := s.reviews[reviewID]
@@ -179,10 +179,10 @@ func (s *Service) ApproveAndIssue(role chain.Role, reviewID, issuer string) (Iss
if err != nil {
return IssueResult{}, fmt.Errorf("service: allocate MA code: %w", err)
}
maCode := issued.MACode
ccCode := issued.CCCode
txID, err := s.chain.IssueMA(role, chain.IssueRequest{
MACode: maCode,
CCCode: ccCode,
ContentTwinID: item.ContentTwinID,
MerkleRoot: item.Sub.MerkleRoot,
FileHash: item.Sub.FileHash,
@@ -202,7 +202,7 @@ func (s *Service) ApproveAndIssue(role chain.Role, reviewID, issuer string) (Iss
s.mu.Lock()
item.Status = model.StatusIssued // 已发码,移出"待发码"队列
item.MACode = maCode
item.CCCode = ccCode
s.mu.Unlock()
// CP 注册本方映射
@@ -214,18 +214,18 @@ func (s *Service) ApproveAndIssue(role chain.Role, reviewID, issuer string) (Iss
})
// 记录全链路存证(送审→审核→发码)+ 注册感知哈希供确权比对
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeSubmit, HashValue: item.Sub.FileHash, Operator: item.Sub.CPName, Detail: "CP 送审"})
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeCSPSReview, Operator: "CSPS", Detail: "合规审核通过"})
s.prov.Record(model.ProvenanceEvent{MACode: maCode, Node: model.NodeIssue, HashValue: item.Sub.FileHash, Operator: issuer, Detail: "发码签发,绑定基准哈希"})
s.prov.Record(model.ProvenanceEvent{CCCode: ccCode, Node: model.NodeSubmit, HashValue: item.Sub.FileHash, Operator: item.Sub.CPName, Detail: "CP 送审"})
s.prov.Record(model.ProvenanceEvent{CCCode: ccCode, Node: model.NodeCSPSReview, Operator: "CSPS", Detail: "合规审核通过"})
s.prov.Record(model.ProvenanceEvent{CCCode: ccCode, Node: model.NodeIssue, HashValue: item.Sub.FileHash, Operator: issuer, Detail: "发码签发,绑定基准哈希"})
if item.Sub.Perceptual != "" {
s.mu.Lock()
s.phash[maCode] = phashEntry{Title: item.Sub.Title, Perceptual: item.Sub.Perceptual}
s.phash[ccCode] = phashEntry{Title: item.Sub.Title, Perceptual: item.Sub.Perceptual}
s.mu.Unlock()
}
cert := fmt.Sprintf("CERT|%s|%s|%s", maCode, item.Sub.FileHash, item.Sub.MerkleRoot)
cert := fmt.Sprintf("CERT|%s|%s|%s", ccCode, item.Sub.FileHash, item.Sub.MerkleRoot)
return IssueResult{
MACode: maCode,
CCCode: ccCode,
ContentTwinID: item.ContentTwinID,
TxID: txID,
Certificate: cert,
@@ -233,8 +233,8 @@ func (s *Service) ApproveAndIssue(role chain.Role, reviewID, issuer string) (Iss
}
// Verify 送审文件验真 / CDN 注入校验通用入口(需求4、需求7)。
func (s *Service) Verify(maCode, fileHash string) (chain.VerifyResult, error) {
res, err := s.chain.VerifyHash(maCode, fileHash)
func (s *Service) Verify(ccCode, fileHash string) (chain.VerifyResult, error) {
res, err := s.chain.VerifyHash(ccCode, fileHash)
if err != nil {
return res, err
}
@@ -245,13 +245,13 @@ func (s *Service) Verify(maCode, fileHash string) (chain.VerifyResult, error) {
}
// QueryMappings 查询 MA 码绑定的三方映射与 CDN 端点(需求11/17)。
func (s *Service) QueryMappings(maCode string) (chain.MappingsResult, error) {
return s.chain.QueryMappings(maCode)
func (s *Service) QueryMappings(ccCode string) (chain.MappingsResult, error) {
return s.chain.QueryMappings(ccCode)
}
// VerifyEpisode 按集级子标识(MA码#E07)或 MA码+集号 验真单集。
func (s *Service) VerifyEpisode(maCode string, episode int, fileHash string) (chain.VerifyResult, error) {
res, err := s.chain.VerifyEpisodeHash(maCode, episode, fileHash)
// VerifyEpisode 按集级子标识(CC码#E07)或 CC码+集号 验真单集。
func (s *Service) VerifyEpisode(ccCode string, episode int, fileHash string) (chain.VerifyResult, error) {
res, err := s.chain.VerifyEpisodeHash(ccCode, episode, fileHash)
if err != nil {
return res, err
}
@@ -262,8 +262,8 @@ func (s *Service) VerifyEpisode(maCode string, episode int, fileHash string) (ch
}
// ListEpisodes 列出某剧的全部集级哈希绑定。
func (s *Service) ListEpisodes(maCode string) ([]model.HashBinding, error) {
return s.chain.ListEpisodes(maCode)
func (s *Service) ListEpisodes(ccCode string) ([]model.HashBinding, error) {
return s.chain.ListEpisodes(ccCode)
}
// ReviewSummary 送审待办摘要(发码前阶段)。
@@ -275,7 +275,7 @@ type ReviewSummary struct {
EpisodeCount int `json:"episode_count"`
Status string `json:"status"`
CPName string `json:"cp_name"`
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
}
// ListReviews 列出指定状态的送审待办(用于审核台/发码台队列)。
@@ -292,7 +292,7 @@ func (s *Service) ListReviews(status string) []ReviewSummary {
ReviewID: id, ContentTwinID: item.ContentTwinID,
Title: item.Sub.Title, Category: item.Sub.Category,
EpisodeCount: item.Sub.EpisodeCount, Status: item.Status,
CPName: item.Sub.CPName, MACode: item.MACode,
CPName: item.Sub.CPName, CCCode: item.CCCode,
})
}
return out
+13 -13
View File
@@ -7,22 +7,22 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/cccode"
)
func newService(t *testing.T) *Service {
t.Helper()
gen := macode.NewGenerator(macode.NewMemoryStore())
require.NoError(t, gen.RegisterSegment(macode.Segment{
gen := cccode.NewGenerator(cccode.NewMemoryStore())
require.NoError(t, gen.RegisterSegment(cccode.Segment{
IndustryNode: "8531", OrgNode: "4401",
Category: macode.CategoryMicroDrama, Start: 1, End: 100, SeqWidth: 7,
Category: cccode.CategoryMicroDrama, Start: 1, End: 100, SeqWidth: 7,
}))
return New(chain.NewMemoryChain(), gen)
}
func sampleSub() Submission {
return Submission{
Title: "示例微短剧", EpisodeCount: 24, Category: macode.CategoryMicroDrama,
Title: "示例微短剧", EpisodeCount: 24, Category: cccode.CategoryMicroDrama,
FileHash: "filehash-abc", MerkleRoot: "merkle-abc", Perceptual: "phash-abc",
CPMediaID: "FS-MEDIA-77821", CPName: "飞翮信息",
}
@@ -30,7 +30,7 @@ func sampleSub() Submission {
func TestSubmit_IncompleteHashRejected(t *testing.T) {
s := newService(t)
_, err := s.SubmitForReview(Submission{Title: "无哈希", Category: macode.CategoryMicroDrama})
_, err := s.SubmitForReview(Submission{Title: "无哈希", Category: cccode.CategoryMicroDrama})
assert.ErrorIs(t, err, ErrIncompleteHashPkg)
}
@@ -43,7 +43,7 @@ func TestSubmit_Success(t *testing.T) {
assert.Equal(t, "pending", res.Status)
}
func TestApproveAndIssue_GeneratesMACode(t *testing.T) {
func TestApproveAndIssue_GeneratesCCCode(t *testing.T) {
s := newService(t)
sub, err := s.SubmitForReview(sampleSub())
require.NoError(t, err)
@@ -52,10 +52,10 @@ func TestApproveAndIssue_GeneratesMACode(t *testing.T) {
issued, err := s.ApproveAndIssue(chain.RoleRegulator, sub.ReviewID, "北京市广播电视局")
require.NoError(t, err)
// 模式BMA 码由系统按号段生成
assert.True(t, macode.IsValid(issued.MACode), "应生成合法 MA 码: %s", issued.MACode)
assert.True(t, strings.HasPrefix(issued.MACode, "MA.156.8531.4401/WD/"), "前缀应匹配号段: %s", issued.MACode)
assert.True(t, cccode.IsValid(issued.CCCode), "应生成合法 MA 码: %s", issued.CCCode)
assert.True(t, strings.HasPrefix(issued.CCCode, "MA.156.8531.4401/WD/"), "前缀应匹配号段: %s", issued.CCCode)
assert.NotEmpty(t, issued.TxID)
assert.Contains(t, issued.Certificate, issued.MACode)
assert.Contains(t, issued.Certificate, issued.CCCode)
}
func TestApproveAndIssue_OnlyRegulator(t *testing.T) {
@@ -84,11 +84,11 @@ func TestVerify_MatchAndMismatch(t *testing.T) {
require.NoError(t, s.ReviewCSPS(sub.ReviewID, true, "rv-1"))
issued, _ := s.ApproveAndIssue(chain.RoleRegulator, sub.ReviewID, "issuer")
res, err := s.Verify(issued.MACode, "filehash-abc")
res, err := s.Verify(issued.CCCode, "filehash-abc")
require.NoError(t, err)
assert.True(t, res.Match)
_, err = s.Verify(issued.MACode, "tampered")
_, err = s.Verify(issued.CCCode, "tampered")
assert.ErrorIs(t, err, ErrHashMismatch)
}
@@ -107,5 +107,5 @@ func TestApproveAndIssue_TwoContentsUniqueCodes(t *testing.T) {
i2, err := s.ApproveAndIssue(chain.RoleRegulator, sub2.ReviewID, "issuer")
require.NoError(t, err)
assert.NotEqual(t, i1.MACode, i2.MACode, "两条内容应分配不同 MA 码")
assert.NotEqual(t, i1.CCCode, i2.CCCode, "两条内容应分配不同 MA 码")
}
+10 -10
View File
@@ -11,24 +11,24 @@ import (
func TestReportPlaybackAndSettle(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s)
ccCode, _, _ := issueOne(t, s)
// 运营商回传播放/购买事件
acc, rej := s.ReportPlayback([]model.PlaybackEvent{
{MACode: maCode, PlatformID: "CT-SX", EventType: model.EventPlay},
{MACode: maCode, PlatformID: "CT-SX", EventType: model.EventPurchase, RevenueCent: 1500},
{MACode: maCode, PlatformID: "CM-SX", EventType: model.EventPurchase, RevenueCent: 2500},
{CCCode: ccCode, PlatformID: "CT-SX", EventType: model.EventPlay},
{CCCode: ccCode, PlatformID: "CT-SX", EventType: model.EventPurchase, RevenueCent: 1500},
{CCCode: ccCode, PlatformID: "CM-SX", EventType: model.EventPurchase, RevenueCent: 2500},
})
assert.Equal(t, 3, acc)
assert.Equal(t, 0, rej)
// 聚合可信播放数据
sum := s.PlaybackSummary(maCode)
sum := s.PlaybackSummary(ccCode)
assert.Equal(t, int64(4000), sum.TotalRevenue)
assert.Equal(t, int64(1), sum.TotalPlays)
// 分账:CP60/平台34/服务费6
st, err := s.ComputeSettlement(maCode, "2026-06")
st, err := s.ComputeSettlement(ccCode, "2026-06")
require.NoError(t, err)
assert.Equal(t, int64(4000), st.TotalRevenue)
assert.Equal(t, int64(2400), st.CPShare)
@@ -39,20 +39,20 @@ func TestReportPlaybackAndSettle(t *testing.T) {
func TestReportPlayback_RejectsUnknownOrRevoked(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s)
ccCode, _, _ := issueOne(t, s)
// 未知 MA 码被拒
acc, rej := s.ReportPlayback([]model.PlaybackEvent{
{MACode: "MA.156.8531.6101/WD/不存在", PlatformID: "P", EventType: model.EventPlay},
{CCCode: "MA.156.8531.6101/WD/不存在", PlatformID: "P", EventType: model.EventPlay},
})
assert.Equal(t, 0, acc)
assert.Equal(t, 1, rej)
// 下架后回传被拒(数据归属不可信)
_, err := s.Takedown(chain.RoleRegulator, maCode, "违规")
_, err := s.Takedown(chain.RoleRegulator, ccCode, "违规")
require.NoError(t, err)
acc, rej = s.ReportPlayback([]model.PlaybackEvent{
{MACode: maCode, PlatformID: "P", EventType: model.EventPlay},
{CCCode: ccCode, PlatformID: "P", EventType: model.EventPlay},
})
assert.Equal(t, 0, acc)
assert.Equal(t, 1, rej)