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
+4 -4
View File
@@ -39,7 +39,7 @@ TCS-IPTV 在 **内容提供商(CP)、审核和监管部门、运营商** 三
### 一期 MVP(核心闭环)
送审 → CSPS审核 → 发码签发 → 媒资库入库 → 发布 → CDN注入校验 → 应急下架
- 哈希SDK(文件SHA-256 / 分段Merkle / 感知哈希)
- MA码生成(六段式,号段原子分配,PostgreSQL 行锁防重号)
- CC码生成(六段式,号段原子分配,PostgreSQL 行锁防重号)
- 可信数据空间(合约规则:1:1强绑定不可解绑、防换壳重发、权限控制)
- 一剧一码 + 集级哈希 + 集级下架/恢复
- HTTP API + HMAC 三角色权限 + 监管大屏(角色工作台/全流程演示/监管片库)
@@ -74,7 +74,7 @@ tcs-iptv/
│ └── console-bff/ # 监管控制台 BFF:8090,三期)
├── internal/
│ ├── hash/ # 哈希核心(SHA256/Merkle/感知哈希)覆盖率88%
│ ├── macode/ # MA码生成/解析/号段(含PG存储)覆盖率75%
│ ├── cccode/ # CC码生成/解析/号段(含PG存储)覆盖率75%
│ ├── chain/ # 可信数据空间抽象 + MemoryChain
│ ├── service/ # 业务编排(覆盖率85%)
│ ├── playback/ # 播放聚合与分账(覆盖率100%)
@@ -125,7 +125,7 @@ bash scripts/e2e_smoke.sh
| 指标 | 状况 |
|------|------|
| 测试用例 | 83 个,全部通过 |
| 核心覆盖率 | playback 100% / hash 88% / service 85% / macode 75% |
| 核心覆盖率 | playback 100% / hash 88% / service 85% / cccode 75% |
| go vet / build | 通过 |
| 前端构建 | 通过 |
| 端到端冒烟 | 一期→三期全相位通过 |
@@ -149,7 +149,7 @@ bash scripts/e2e_smoke.sh
## 八、安全说明
- ✅ 已实现:HMAC-SHA256 鉴权、三角色权限矩阵、MA码1:1不可解绑、哈希本地计算不上链原片、关键操作存证、**监管大屏 BFF 化(密钥不下发浏览器)**
- ✅ 已实现:HMAC-SHA256 鉴权、三角色权限矩阵、CC码1:1不可解绑、哈希本地计算不上链原片、关键操作存证、**监管大屏 BFF 化(密钥不下发浏览器)**
- ⚠️ 上生产前:真实链替换、等保三级测评、HSM 托管、生产凭证接 Vault/SSO
---
+1 -1
View File
@@ -1,6 +1,6 @@
# TCS-IPTV 内容可信锁定系统
> MA码(监管身份)+ 哈希码(技术指纹)双锚定,在 CP / 审核和监管部门 / 运营商 三方系统之上建立"可信身份映射层"。
> CC码(监管身份)+ 哈希码(技术指纹)双锚定,在 CP / 审核和监管部门 / 运营商 三方系统之上建立"可信身份映射层"。
>
> 上游文档:`../0-req-IPTV.md`(需求)、`../1-prd-IPTV.md`PRD)、`../2-task-IPTV.md`(任务)
+13 -13
View File
@@ -11,7 +11,7 @@ import (
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/config"
"github.com/tcs-iptv/tcs/internal/httpx"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/cccode"
"github.com/tcs-iptv/tcs/internal/monitor"
"github.com/tcs-iptv/tcs/internal/service"
)
@@ -29,13 +29,13 @@ func openDB(dsn string) *sql.DB {
}
// newAllocationStore 优先使用 PostgreSQL(持久、防重号),不可用时回退内存。
func newAllocationStore(db *sql.DB) macode.AllocationStore {
func newAllocationStore(db *sql.DB) cccode.AllocationStore {
if db != nil {
log.Printf("macode: 使用 PostgreSQL 号段存储")
return macode.NewPostgresStore(db)
log.Printf("cccode: 使用 PostgreSQL 号段存储")
return cccode.NewPostgresStore(db)
}
log.Printf("macode: PostgreSQL 不可用,回退内存号段存储(仅开发用)")
return macode.NewMemoryStore()
log.Printf("cccode: PostgreSQL 不可用,回退内存号段存储(仅开发用)")
return cccode.NewMemoryStore()
}
// newChain 按配置选择链后端:
@@ -76,20 +76,20 @@ func main() {
// 装配依赖:共享一个 PG 连接给链持久化与号段存储
db := openDB(cfg.PostgresDSN)
ch := newChain(cfg.ChainBackend, cfg.ChainMakerSDKConf, db)
gen := macode.NewGenerator(newAllocationStore(db))
gen := cccode.NewGenerator(newAllocationStore(db))
// 示例号段(生产由与发码机构对接后配置)
// 机构节点 6101 = 陕西(管理方:陕西IPTV运营公司);行业节点 8531 = IPTV视听内容
_ = gen.RegisterSegment(macode.Segment{
_ = gen.RegisterSegment(cccode.Segment{
IndustryNode: "8531", OrgNode: "6101",
Category: macode.CategoryMicroDrama, Start: 1, End: 9999999, SeqWidth: 7,
Category: cccode.CategoryMicroDrama, Start: 1, End: 9999999, SeqWidth: 7,
})
_ = gen.RegisterSegment(macode.Segment{
_ = gen.RegisterSegment(cccode.Segment{
IndustryNode: "8531", OrgNode: "6101",
Category: macode.CategoryWebSeries, Start: 1, End: 9999999, SeqWidth: 7,
Category: cccode.CategoryWebSeries, Start: 1, End: 9999999, SeqWidth: 7,
})
_ = gen.RegisterSegment(macode.Segment{
_ = gen.RegisterSegment(cccode.Segment{
IndustryNode: "8531", OrgNode: "6101",
Category: macode.CategoryWebMovie, Start: 1, End: 9999999, SeqWidth: 7,
Category: cccode.CategoryWebMovie, Start: 1, End: 9999999, SeqWidth: 7,
})
svc := service.New(ch, gen)
h := api.NewHandler(svc)
+11 -11
View File
@@ -8,14 +8,14 @@
| 方法 | 权限 | 说明 |
|------|------|------|
| `IssueMA(maCode, ctid, merkleRoot, fileHash, contentJSON)` | 仅监管节点 | 签发 MA 码并 1:1 强绑定哈希;不可重复、不可解绑 |
| `IssueMA(ccCode, ctid, merkleRoot, fileHash, contentJSON)` | 仅监管节点 | 签发 MA 码并 1:1 强绑定哈希;不可重复、不可解绑 |
| `RegisterHashBinding(ctid, bindingJSON)` | 审核/监管 | 追加哈希绑定(转码版父子关系) |
| `RegisterMapping(ctid, party, partyID, cdnEndpoint)` | 三方 | 注册编码映射;MA 必须已签发 |
| `VerifyHash(maCode, fileHash) -> bool` | 任意 | 校验提交哈希与绑定哈希是否一致 |
| `QueryContent(maCode) -> json` | 任意 | 查询内容主记录 |
| `QueryMappings(maCode) -> json` | 任意 | 查询全部三方映射与 CDN 端点 |
| `VerifyHash(ccCode, fileHash) -> bool` | 任意 | 校验提交哈希与绑定哈希是否一致 |
| `QueryContent(ccCode) -> json` | 任意 | 查询内容主记录 |
| `QueryMappings(ccCode) -> json` | 任意 | 查询全部三方映射与 CDN 端点 |
| `RecordVersionChange(ctid, vcJSON)` | 审核/监管 | 记录版本变更,触发重审 |
| `Revoke(maCode, reason)` | 仅监管节点 | 下架,返回受影响映射 |
| `Revoke(ccCode, reason)` | 仅监管节点 | 下架,返回受影响映射 |
## 权限模型(对应需求14
@@ -33,12 +33,12 @@
## 状态键设计(KV
```
content:{maCode} -> Content JSON
binding:{maCode}:{idx} -> HashBinding JSON
hashidx:{fileHash} -> maCode (防换壳重发)
mapping:{maCode}:{idx} -> Mapping JSON
version:{maCode}:{idx} -> VersionChange JSON
ctid2ma:{ctid} -> maCode
content:{ccCode} -> Content JSON
binding:{ccCode}:{idx} -> HashBinding JSON
hashidx:{fileHash} -> ccCode (防换壳重发)
mapping:{ccCode}:{idx} -> Mapping JSON
version:{ccCode}:{idx} -> VersionChange JSON
ctid2ma:{ctid} -> ccCode
```
## 构建与部署(真实链,二期接入)
+45 -45
View File
@@ -6,18 +6,18 @@
//
// 状态键设计(KV):
//
// content:{maCode} -> Content JSON(含 status
// binding:{maCode}:0 -> 整剧 file 哈希绑定 JSON
// binding:{maCode}:p -> 感知哈希绑定 JSON
// ep:{maCode}:{n} -> 集级绑定 JSON {episode,hash,revoked,reason}
// epcount:{maCode} -> 集数 N
// hashidx:{fileHash} -> maCode(防换壳重发)
// mapping:{maCode}:{idx} -> Mapping JSON
// mapcount:{maCode} -> 映射数 N
// version:{maCode}:{idx} -> VersionChange JSON
// vercount:{maCode} -> 版本变更数 N
// ctid2ma:{ctid} -> maCode
// allmacodes -> []maCode(供 ListContents 遍历)
// content:{ccCode} -> Content JSON(含 status
// binding:{ccCode}:0 -> 整剧 file 哈希绑定 JSON
// binding:{ccCode}:p -> 感知哈希绑定 JSON
// ep:{ccCode}:{n} -> 集级绑定 JSON {episode,hash,revoked,reason}
// epcount:{ccCode} -> 集数 N
// hashidx:{fileHash} -> ccCode(防换壳重发)
// mapping:{ccCode}:{idx} -> Mapping JSON
// mapcount:{ccCode} -> 映射数 N
// version:{ccCode}:{idx} -> VersionChange JSON
// vercount:{ccCode} -> 版本变更数 N
// ctid2ma:{ctid} -> ccCode
// allcccodes -> []ccCode(供 ListContents 遍历)
//
// 权限:通过 sender 组织证书判断(仅监管组织可 IssueMA/Revoke/RevokeEpisode/Restore)。
package main
@@ -76,15 +76,15 @@ type epBinding struct {
Reason string `json:"revoked_reason,omitempty"`
}
// appendMACode 把新发码加入全局列表(供 ListContents 遍历)。
func appendMACode(maCode string) {
// appendCCCode 把新发码加入全局列表(供 ListContents 遍历)。
func appendCCCode(ccCode string) {
var all []string
if v, _ := sdk.Instance.GetStateByte("allmacodes", ""); len(v) > 0 {
if v, _ := sdk.Instance.GetStateByte("allcccodes", ""); len(v) > 0 {
_ = json.Unmarshal(v, &all)
}
all = append(all, maCode)
all = append(all, ccCode)
b, _ := json.Marshal(all)
_ = sdk.Instance.PutStateByte("allmacodes", "", b)
_ = sdk.Instance.PutStateByte("allcccodes", "", b)
}
// ---- 写方法 ----
@@ -95,11 +95,11 @@ func (t *TCSRegistry) IssueMA() protogo.Response {
return sdk.Error("permission denied: only regulator can issue MA")
}
args := sdk.Instance.GetArgs()
maCode := string(args["ma_code"])
ccCode := string(args["ma_code"])
ctid := string(args["ctid"])
fileHash := string(args["file_hash"])
if existing, _ := sdk.Instance.GetStateByte("content", maCode); len(existing) > 0 {
if existing, _ := sdk.Instance.GetStateByte("content", ccCode); len(existing) > 0 {
return sdk.Error("MA already issued (1:1 binding immutable)")
}
if bound, _ := sdk.Instance.GetStateByte("hashidx", fileHash); len(bound) > 0 {
@@ -112,21 +112,21 @@ func (t *TCSRegistry) IssueMA() protogo.Response {
if content == nil {
content = map[string]interface{}{}
}
content["ma_code"] = maCode
content["ma_code"] = ccCode
content["content_twin_id"] = ctid
content["status"] = "approved"
cj, _ := json.Marshal(content)
_ = sdk.Instance.PutStateByte("content", maCode, cj)
_ = sdk.Instance.PutStateByte("content", ccCode, cj)
// 整剧 file 绑定 + 感知哈希绑定
fb, _ := json.Marshal(epBinding{Episode: 0, HashValue: fileHash})
_ = sdk.Instance.PutStateByte("binding", maCode+":0", fb)
_ = sdk.Instance.PutStateByte("binding", ccCode+":0", fb)
if ph := string(args["perceptual_hash"]); ph != "" {
pb, _ := json.Marshal(map[string]string{"hash_type": "perceptual", "hash_value": ph})
_ = sdk.Instance.PutStateByte("binding", maCode+":p", pb)
_ = sdk.Instance.PutStateByte("binding", ccCode+":p", pb)
}
_ = sdk.Instance.PutStateByte("hashidx", fileHash, []byte(maCode))
_ = sdk.Instance.PutStateByte("ctid2ma", ctid, []byte(maCode))
_ = sdk.Instance.PutStateByte("hashidx", fileHash, []byte(ccCode))
_ = sdk.Instance.PutStateByte("ctid2ma", ctid, []byte(ccCode))
// 集级哈希
var eps []map[string]interface{}
@@ -139,19 +139,19 @@ func (t *TCSRegistry) IssueMA() protogo.Response {
continue
}
eb, _ := json.Marshal(epBinding{Episode: ep, HashValue: hv})
_ = sdk.Instance.PutStateByte("ep", maCode+":"+strconv.Itoa(ep), eb)
_ = sdk.Instance.PutStateByte("ep", ccCode+":"+strconv.Itoa(ep), eb)
if exist, _ := sdk.Instance.GetStateByte("hashidx", hv); len(exist) == 0 {
_ = sdk.Instance.PutStateByte("hashidx", hv, []byte(maCode))
_ = sdk.Instance.PutStateByte("hashidx", hv, []byte(ccCode))
}
if ep > n {
n = ep
}
}
putInt("epcount", maCode, n)
appendMACode(maCode)
putInt("epcount", ccCode, n)
appendCCCode(ccCode)
sdk.Instance.EmitEvent("RegisterSuccess", []string{maCode, fileHash})
return sdk.Success([]byte(maCode))
sdk.Instance.EmitEvent("RegisterSuccess", []string{ccCode, fileHash})
return sdk.Success([]byte(ccCode))
}
func toFloat(v interface{}) float64 {
@@ -226,8 +226,8 @@ func (t *TCSRegistry) SetContentStatus() protogo.Response {
return setStatus(string(args["ma_code"]), string(args["status"]), "StatusChanged")
}
func setStatus(maCode, status, event string) protogo.Response {
cj, _ := sdk.Instance.GetStateByte("content", maCode)
func setStatus(ccCode, status, event string) protogo.Response {
cj, _ := sdk.Instance.GetStateByte("content", ccCode)
if len(cj) == 0 {
return sdk.Error("not found")
}
@@ -235,8 +235,8 @@ func setStatus(maCode, status, event string) protogo.Response {
_ = json.Unmarshal(cj, &content)
content["status"] = status
nj, _ := json.Marshal(content)
_ = sdk.Instance.PutStateByte("content", maCode, nj)
sdk.Instance.EmitEvent(event, []string{maCode, status})
_ = sdk.Instance.PutStateByte("content", ccCode, nj)
sdk.Instance.EmitEvent(event, []string{ccCode, status})
return sdk.Success([]byte("ok"))
}
@@ -266,8 +266,8 @@ func (t *TCSRegistry) Restore() protogo.Response {
return setStatus(string(sdk.Instance.GetArgs()["ma_code"]), "published", "Restored")
}
func setEpisodeRevoked(maCode, epStr string, revoked bool, reason string) protogo.Response {
key := maCode + ":" + epStr
func setEpisodeRevoked(ccCode, epStr string, revoked bool, reason string) protogo.Response {
key := ccCode + ":" + epStr
v, _ := sdk.Instance.GetStateByte("ep", key)
if len(v) == 0 {
return sdk.Error("not found")
@@ -316,11 +316,11 @@ func (t *TCSRegistry) HashExists() protogo.Response {
// ListEpisodes 返回某 MA 的集级绑定数组(JSON)。
func (t *TCSRegistry) ListEpisodes() protogo.Response {
maCode := string(sdk.Instance.GetArgs()["ma_code"])
n := getInt("epcount", maCode)
ccCode := string(sdk.Instance.GetArgs()["ma_code"])
n := getInt("epcount", ccCode)
out := make([]epBinding, 0, n)
for i := 1; i <= n; i++ {
if v, _ := sdk.Instance.GetStateByte("ep", maCode+":"+strconv.Itoa(i)); len(v) > 0 {
if v, _ := sdk.Instance.GetStateByte("ep", ccCode+":"+strconv.Itoa(i)); len(v) > 0 {
var eb epBinding
_ = json.Unmarshal(v, &eb)
out = append(out, eb)
@@ -341,12 +341,12 @@ func (t *TCSRegistry) QueryContent() protogo.Response {
// QueryMappings 返回某 MA 的全部映射(JSON {mappings:[],cdn_endpoints:[]})。
func (t *TCSRegistry) QueryMappings() protogo.Response {
maCode := string(sdk.Instance.GetArgs()["ma_code"])
n := getInt("mapcount", maCode)
ccCode := string(sdk.Instance.GetArgs()["ma_code"])
n := getInt("mapcount", ccCode)
maps := make([]map[string]interface{}, 0, n)
cdns := []string{}
for i := 1; i <= n; i++ {
if v, _ := sdk.Instance.GetStateByte("mapping", maCode+":"+strconv.Itoa(i)); len(v) > 0 {
if v, _ := sdk.Instance.GetStateByte("mapping", ccCode+":"+strconv.Itoa(i)); len(v) > 0 {
var m map[string]interface{}
_ = json.Unmarshal(v, &m)
maps = append(maps, m)
@@ -355,7 +355,7 @@ func (t *TCSRegistry) QueryMappings() protogo.Response {
}
}
}
b, _ := json.Marshal(map[string]interface{}{"ma_code": maCode, "mappings": maps, "cdn_endpoints": cdns})
b, _ := json.Marshal(map[string]interface{}{"ma_code": ccCode, "mappings": maps, "cdn_endpoints": cdns})
return sdk.Success(b)
}
@@ -363,7 +363,7 @@ func (t *TCSRegistry) QueryMappings() protogo.Response {
func (t *TCSRegistry) ListContents() protogo.Response {
status := string(sdk.Instance.GetArgs()["status"])
var all []string
if v, _ := sdk.Instance.GetStateByte("allmacodes", ""); len(v) > 0 {
if v, _ := sdk.Instance.GetStateByte("allcccodes", ""); len(v) > 0 {
_ = json.Unmarshal(v, &all)
}
out := make([]map[string]interface{}, 0, len(all))
@@ -3,12 +3,12 @@
BEGIN;
CREATE TABLE IF NOT EXISTS macode_cursor (
CREATE TABLE IF NOT EXISTS cccode_cursor (
segment_key VARCHAR(128) PRIMARY KEY, -- {industryNode}:{orgNode}:{category}
cursor BIGINT NOT NULL, -- 已分配的最大序列
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE macode_cursor IS 'MA码号段分配游标,行级原子自增';
COMMENT ON TABLE cccode_cursor IS 'CC码号段分配游标,行级原子自增';
COMMIT;
+6 -6
View File
@@ -29,12 +29,12 @@ func (h *CatalogHandler) Register(rg *gin.RouterGroup) {
}
func (h *CatalogHandler) queryByMA(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code 参数")
return
}
result, err := h.cat.QueryByMA(maCode)
result, err := h.cat.QueryByMA(ccCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
@@ -85,12 +85,12 @@ func (h *CatalogHandler) queryByLibraryID(c *gin.Context) {
}
func (h *CatalogHandler) queryAll(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code 参数")
return
}
result, err := h.cat.QueryAll(maCode)
result, err := h.cat.QueryAll(ccCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
@@ -20,7 +20,7 @@ func TestCatalogHandler_QueryByMA(t *testing.T) {
// 发码
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-cat-svc-001",
CCCode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-cat-svc-001",
FileHash: "fh-cat-svc-001", MerkleRoot: "mr-cat-svc-001",
Content: model.Content{Title: "目录库服务测试", MAType: "WD", Issuer: "测试局"},
})
@@ -55,7 +55,7 @@ func TestCatalogHandler_QueryByHash(t *testing.T) {
c := chain.NewMemoryChain()
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-cat-svc-002",
CCCode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-cat-svc-002",
FileHash: "fh-cat-svc-002", MerkleRoot: "mr-cat-svc-002",
Content: model.Content{Title: "Hash查询服务测试", MAType: "WD", Issuer: "测试局"},
})
@@ -81,7 +81,7 @@ func TestCatalogHandler_QueryByHash(t *testing.T) {
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.True(t, resp.Data.Found)
assert.Equal(t, "MA.156.8531.6101/WD/20260000002", resp.Data.Content.MACode)
assert.Equal(t, "MA.156.8531.6101/WD/20260000002", resp.Data.Content.CCCode)
// 缺少参数
w2 := httptest.NewRecorder()
+72 -72
View File
@@ -44,7 +44,7 @@ func (h *Handler) Register(rg *gin.RouterGroup) {
rg.GET("/content/reviews", h.listReviews) // 送审待办队列(待审/待发码)
rg.GET("/content/list", h.listContents) // 内容队列(待入库/待发布/待注入)
rg.POST("/data/playback", h.reportPlayback) // 播放数据回传(需求9
rg.GET("/data/playback-summary", h.playbackSummary) // 按MA码聚合可信播放数据(需求9/21
rg.GET("/data/playback-summary", h.playbackSummary) // 按CC码聚合可信播放数据(需求9/21
rg.POST("/settlement/compute", h.computeSettlement) // 基于可信播放数据分账(需求21
rg.GET("/content/provenance", h.provenance) // 全链路存证(需求22
rg.GET("/content/accountability", h.accountability) // 责任界定取证(需求22
@@ -147,7 +147,7 @@ func (h *Handler) issue(c *gin.Context) {
}
type verifyReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
FileHash string `json:"file_sha256"`
}
@@ -157,7 +157,7 @@ func (h *Handler) verify(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.Verify(req.MACode, req.FileHash)
res, err := h.svc.Verify(req.CCCode, req.FileHash)
if err != nil {
// 验真不匹配也返回结果体,便于调用方据 match 处理
httpx.Error(c, http.StatusBadRequest, "VERIFY_MISMATCH", err.Error())
@@ -210,7 +210,7 @@ func (h *Handler) bindTranscoded(c *gin.Context) {
}
type ingestReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
CTID string `json:"content_twin_id"`
MediaAssetID string `json:"media_asset_id"`
LibName string `json:"lib_name"`
@@ -222,15 +222,15 @@ func (h *Handler) ingest(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.IngestToLibrary(roleOf(c), req.MACode, req.CTID, req.MediaAssetID, req.LibName); err != nil {
if err := h.svc.IngestToLibrary(roleOf(c), req.CCCode, req.CTID, req.MediaAssetID, req.LibName); err != nil {
httpx.Error(c, http.StatusBadRequest, "INGEST_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "status": "in_library"})
httpx.OK(c, gin.H{"ma_code": req.CCCode, "status": "in_library"})
}
type publishReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Certificate string `json:"certificate"`
}
@@ -240,16 +240,16 @@ func (h *Handler) publish(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.PublishToOperator(service.PublishRequest{MACode: req.MACode, Certificate: req.Certificate}); err != nil {
if err := h.svc.PublishToOperator(service.PublishRequest{CCCode: req.CCCode, Certificate: req.Certificate}); err != nil {
httpx.Error(c, http.StatusBadRequest, "PUBLISH_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "status": "published"})
httpx.OK(c, gin.H{"ma_code": req.CCCode, "status": "published"})
}
type injectReq struct {
CTID string `json:"content_twin_id"`
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
FileHash string `json:"file_sha256"`
OperatorID string `json:"operator_id"`
CDNEndpoint string `json:"cdn_endpoint"`
@@ -261,7 +261,7 @@ func (h *Handler) inject(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.InjectToCDN(roleOf(c), req.CTID, req.MACode, req.FileHash, req.OperatorID, req.CDNEndpoint)
res, err := h.svc.InjectToCDN(roleOf(c), req.CTID, req.CCCode, req.FileHash, req.OperatorID, req.CDNEndpoint)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "INJECT_REJECTED", err.Error())
return
@@ -293,7 +293,7 @@ func (h *Handler) versionChange(c *gin.Context) {
}
type takedownReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Reason string `json:"reason"`
}
@@ -303,7 +303,7 @@ func (h *Handler) takedown(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.Takedown(roleOf(c), req.MACode, req.Reason)
res, err := h.svc.Takedown(roleOf(c), req.CCCode, req.Reason)
if err != nil {
httpx.Error(c, http.StatusForbidden, "TAKEDOWN_FAILED", err.Error())
return
@@ -312,7 +312,7 @@ func (h *Handler) takedown(c *gin.Context) {
}
type takedownEpisodeReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Episode int `json:"episode"`
Reason string `json:"reason"`
}
@@ -323,11 +323,11 @@ func (h *Handler) takedownEpisode(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.TakedownEpisode(roleOf(c), req.MACode, req.Episode, req.Reason); err != nil {
if err := h.svc.TakedownEpisode(roleOf(c), req.CCCode, req.Episode, req.Reason); err != nil {
httpx.Error(c, http.StatusForbidden, "TAKEDOWN_EPISODE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "episode": req.Episode, "revoked": true})
httpx.OK(c, gin.H{"ma_code": req.CCCode, "episode": req.Episode, "revoked": true})
}
func (h *Handler) restore(c *gin.Context) {
@@ -336,11 +336,11 @@ func (h *Handler) restore(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.Restore(roleOf(c), req.MACode); err != nil {
if err := h.svc.Restore(roleOf(c), req.CCCode); err != nil {
httpx.Error(c, http.StatusForbidden, "RESTORE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "status": "published"})
httpx.OK(c, gin.H{"ma_code": req.CCCode, "status": "published"})
}
func (h *Handler) restoreEpisode(c *gin.Context) {
@@ -349,20 +349,20 @@ func (h *Handler) restoreEpisode(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
if err := h.svc.RestoreEpisode(roleOf(c), req.MACode, req.Episode); err != nil {
if err := h.svc.RestoreEpisode(roleOf(c), req.CCCode, req.Episode); err != nil {
httpx.Error(c, http.StatusForbidden, "RESTORE_EPISODE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "episode": req.Episode, "revoked": false})
httpx.OK(c, gin.H{"ma_code": req.CCCode, "episode": req.Episode, "revoked": false})
}
func (h *Handler) mappings(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
res, err := h.svc.QueryMappings(maCode)
res, err := h.svc.QueryMappings(ccCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
@@ -371,7 +371,7 @@ func (h *Handler) mappings(c *gin.Context) {
}
type verifyEpisodeReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Episode int `json:"episode"`
FileHash string `json:"file_sha256"`
}
@@ -382,7 +382,7 @@ func (h *Handler) verifyEpisode(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.VerifyEpisode(req.MACode, req.Episode, req.FileHash)
res, err := h.svc.VerifyEpisode(req.CCCode, req.Episode, req.FileHash)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "VERIFY_MISMATCH", err.Error())
return
@@ -391,17 +391,17 @@ func (h *Handler) verifyEpisode(c *gin.Context) {
}
func (h *Handler) listEpisodes(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
eps, err := h.svc.ListEpisodes(maCode)
eps, err := h.svc.ListEpisodes(ccCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": maCode, "episodes": eps, "count": len(eps)})
httpx.OK(c, gin.H{"ma_code": ccCode, "episodes": eps, "count": len(eps)})
}
func (h *Handler) listReviews(c *gin.Context) {
@@ -422,7 +422,7 @@ func (h *Handler) listContents(c *gin.Context) {
type playbackReq struct {
PlatformID string `json:"platform_id"`
Batch []struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Episode int `json:"episode"`
UserHash string `json:"user_hash"`
EventType string `json:"event_type"`
@@ -440,7 +440,7 @@ func (h *Handler) reportPlayback(c *gin.Context) {
events := make([]model.PlaybackEvent, 0, len(req.Batch))
for _, b := range req.Batch {
events = append(events, model.PlaybackEvent{
MACode: b.MACode, Episode: b.Episode, PlatformID: req.PlatformID,
CCCode: b.CCCode, Episode: b.Episode, PlatformID: req.PlatformID,
UserHash: b.UserHash, EventType: model.PlaybackEventType(b.EventType),
DurationSec: b.DurationSec, RevenueCent: b.RevenueCent, EventTime: time.Now(),
})
@@ -450,16 +450,16 @@ func (h *Handler) reportPlayback(c *gin.Context) {
}
func (h *Handler) playbackSummary(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
httpx.OK(c, h.svc.PlaybackSummary(maCode))
httpx.OK(c, h.svc.PlaybackSummary(ccCode))
}
type settlementReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Period string `json:"period"`
}
@@ -469,7 +469,7 @@ func (h *Handler) computeSettlement(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
st, err := h.svc.ComputeSettlement(req.MACode, req.Period)
st, err := h.svc.ComputeSettlement(req.CCCode, req.Period)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "SETTLEMENT_FAILED", err.Error())
return
@@ -480,30 +480,30 @@ func (h *Handler) computeSettlement(c *gin.Context) {
// ---- 二期:追责取证与确权举证(需求22/23) ----
func (h *Handler) provenance(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
httpx.OK(c, gin.H{"ma_code": maCode, "trail": h.svc.Provenance(maCode)})
httpx.OK(c, gin.H{"ma_code": ccCode, "trail": h.svc.Provenance(ccCode)})
}
func (h *Handler) accountability(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
httpx.OK(c, h.svc.Accountability(maCode))
httpx.OK(c, h.svc.Accountability(ccCode))
}
func (h *Handler) evidence(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
ev, err := h.svc.CopyrightEvidence(maCode)
ev, err := h.svc.CopyrightEvidence(ccCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
@@ -540,7 +540,7 @@ func (h *Handler) infringeMatch(c *gin.Context) {
// ---- 二期:授权链/追更/跨省/终端抽检(需求25/24/13/8 ----
type authorizeReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Regions []string `json:"regions"`
Platforms []string `json:"platforms"`
ExpiryAt string `json:"expiry_at"` // RFC3339,空=长期
@@ -556,15 +556,15 @@ func (h *Handler) authorize(c *gin.Context) {
if req.ExpiryAt != "" {
expiry, _ = time.Parse(time.RFC3339, req.ExpiryAt)
}
if err := h.svc.RecordAuthorization(req.MACode, req.Regions, req.Platforms, expiry); err != nil {
if err := h.svc.RecordAuthorization(req.CCCode, req.Regions, req.Platforms, expiry); err != nil {
httpx.Error(c, http.StatusBadRequest, "AUTHORIZE_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "authorized": true})
httpx.OK(c, gin.H{"ma_code": req.CCCode, "authorized": true})
}
type authCheckReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Region string `json:"region"`
Platform string `json:"platform"`
}
@@ -575,11 +575,11 @@ func (h *Handler) authCheck(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
httpx.OK(c, h.svc.CheckAuthorization(req.MACode, req.Region, req.Platform))
httpx.OK(c, h.svc.CheckAuthorization(req.CCCode, req.Region, req.Platform))
}
type addEpisodesReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Episodes []struct {
Episode int `json:"episode"`
FileSHA256 string `json:"file_sha256"`
@@ -597,15 +597,15 @@ func (h *Handler) addEpisodes(c *gin.Context) {
for _, e := range req.Episodes {
eps = append(eps, model.EpisodeHash{Episode: e.Episode, FileSHA256: e.FileSHA256, MerkleRoot: e.MerkleRoot})
}
if err := h.svc.AddEpisodes(roleOf(c), req.MACode, eps); err != nil {
if err := h.svc.AddEpisodes(roleOf(c), req.CCCode, eps); err != nil {
httpx.Error(c, http.StatusBadRequest, "ADD_EPISODES_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"ma_code": req.MACode, "added": len(eps)})
httpx.OK(c, gin.H{"ma_code": req.CCCode, "added": len(eps)})
}
type crossProvinceReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
FileHash string `json:"file_sha256"`
Province string `json:"province"`
}
@@ -616,11 +616,11 @@ func (h *Handler) crossProvince(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
httpx.OK(c, h.svc.CrossProvinceAdmit(req.MACode, req.FileHash, req.Province))
httpx.OK(c, h.svc.CrossProvinceAdmit(req.CCCode, req.FileHash, req.Province))
}
type terminalVerifyReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Episode int `json:"episode"`
SegHash string `json:"segment_hash"`
}
@@ -631,14 +631,14 @@ func (h *Handler) terminalVerify(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
ok, msg := h.svc.TerminalVerifySegment(req.MACode, req.Episode, req.SegHash)
ok, msg := h.svc.TerminalVerifySegment(req.CCCode, req.Episode, req.SegHash)
httpx.OK(c, gin.H{"ok": ok, "message": msg})
}
// ---- 三期:备案对接/全国统计/监管上报/号段管理 ----
type bindFilingReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
LicenseNo string `json:"license_no"`
FilingNo string `json:"filing_no"`
}
@@ -649,7 +649,7 @@ func (h *Handler) bindFiling(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
rec, err := h.svc.BindFiling(req.MACode, req.LicenseNo, req.FilingNo)
rec, err := h.svc.BindFiling(req.CCCode, req.LicenseNo, req.FilingNo)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "BIND_FILING_FAILED", err.Error())
return
@@ -658,8 +658,8 @@ func (h *Handler) bindFiling(c *gin.Context) {
}
func (h *Handler) queryFiling(c *gin.Context) {
maCode := c.Query("ma_code")
rec, ok := h.svc.QueryFiling(maCode)
ccCode := c.Query("ma_code")
rec, ok := h.svc.QueryFiling(ccCode)
if !ok {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", "未关联备案")
return
@@ -696,16 +696,16 @@ func (h *Handler) listSegments(c *gin.Context) {
// ---- 四期:大小屏融合(跨域解析/扫码验真/跨屏权益)----
func (h *Handler) resolve(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
ccCode := c.Query("ma_code")
if ccCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code")
return
}
httpx.OK(c, h.svc.Resolve(maCode))
httpx.OK(c, h.svc.Resolve(ccCode))
}
type scanVerifyReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
}
func (h *Handler) scanVerify(c *gin.Context) {
@@ -714,11 +714,11 @@ func (h *Handler) scanVerify(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
httpx.OK(c, h.svc.ScanVerify(req.MACode))
httpx.OK(c, h.svc.ScanVerify(req.CCCode))
}
type purchaseReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
UserHash string `json:"user_hash"`
Screen string `json:"screen"` // iptv/ott/app
}
@@ -729,7 +729,7 @@ func (h *Handler) recordPurchase(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
rec, err := h.svc.RecordPurchase(req.MACode, req.UserHash, model.ScreenType(req.Screen))
rec, err := h.svc.RecordPurchase(req.CCCode, req.UserHash, model.ScreenType(req.Screen))
if err != nil {
httpx.Error(c, http.StatusBadRequest, "PURCHASE_FAILED", err.Error())
return
@@ -738,7 +738,7 @@ func (h *Handler) recordPurchase(c *gin.Context) {
}
type verifyRightsReq struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
UserHash string `json:"user_hash"`
Screen string `json:"screen"` // 当前请求屏
}
@@ -749,7 +749,7 @@ func (h *Handler) verifyRights(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
httpx.OK(c, h.svc.VerifyCrossScreenRights(req.MACode, req.UserHash, model.ScreenType(req.Screen)))
httpx.OK(c, h.svc.VerifyCrossScreenRights(req.CCCode, req.UserHash, model.ScreenType(req.Screen)))
}
// ---- 多维度标识查询 handlers ----
@@ -808,7 +808,7 @@ func (h *Handler) mergeMA(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.MergeMACodes(roleOf(c), req)
res, err := h.svc.MergeCCCodes(roleOf(c), req)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "MERGE_FAILED", err.Error())
return
@@ -823,7 +823,7 @@ func (h *Handler) splitMA(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.SplitMACode(roleOf(c), req)
res, err := h.svc.SplitCCCode(roleOf(c), req)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "SPLIT_FAILED", err.Error())
return
+20 -20
View File
@@ -14,7 +14,7 @@ import (
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/hash"
"github.com/tcs-iptv/tcs/internal/httpx"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/cccode"
"github.com/tcs-iptv/tcs/internal/service"
)
@@ -24,10 +24,10 @@ func testServer(t *testing.T) (*httptest.Server, *httpx.MemoryKeyStore) {
gin.SetMode(gin.TestMode)
ch := chain.NewMemoryChain()
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: 9999999, SeqWidth: 7,
Category: cccode.CategoryMicroDrama, Start: 1, End: 9999999, SeqWidth: 7,
}))
svc := service.New(ch, gen)
h := NewHandler(svc)
@@ -115,39 +115,39 @@ func TestE2E_FullLifecycle(t *testing.T) {
"review_id": reviewID, "issuer": "北京市广播电视局",
})
require.Equal(t, http.StatusOK, st)
maCode := dataOf(resp)["ma_code"].(string)
ccCode := dataOf(resp)["ma_code"].(string)
cert := dataOf(resp)["certificate"].(string)
assert.True(t, macode.IsValid(maCode))
assert.True(t, cccode.IsValid(ccCode))
// 4) 入媒资库
st, _ = signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/ingest", map[string]any{
"ma_code": maCode, "content_twin_id": ctid, "media_asset_id": "MEDIA-001", "lib_name": "广东IPTV媒资库",
"ma_code": ccCode, "content_twin_id": ctid, "media_asset_id": "MEDIA-001", "lib_name": "广东IPTV媒资库",
})
require.Equal(t, http.StatusOK, st)
// 5) 发布
st, _ = signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/publish", map[string]any{
"ma_code": maCode, "certificate": cert,
"ma_code": ccCode, "certificate": cert,
})
require.Equal(t, http.StatusOK, st)
// 6) CDN 注入(匹配)
st, resp = signedCall(t, b, "ak-operator", "sk-operator", "POST", "/content/inject", map[string]any{
"content_twin_id": ctid, "ma_code": maCode, "file_sha256": "fh-e2e",
"content_twin_id": ctid, "ma_code": ccCode, "file_sha256": "fh-e2e",
"operator_id": "CT-IPTV-GD", "cdn_endpoint": "cdn://ct-gd/vod/1",
})
require.Equal(t, http.StatusOK, st)
assert.Equal(t, true, dataOf(resp)["allowed"])
// 7) 映射查询
st, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "GET", "/content/mappings?ma_code="+maCode, nil)
st, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "GET", "/content/mappings?ma_code="+ccCode, nil)
require.Equal(t, http.StatusOK, st)
mappings := dataOf(resp)["mappings"].([]any)
assert.Len(t, mappings, 3) // cp + reviewer + operator
// 8) 监管下架
st, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/takedown", map[string]any{
"ma_code": maCode, "reason": "违规",
"ma_code": ccCode, "reason": "违规",
})
require.Equal(t, http.StatusOK, st)
assert.NotEmpty(t, dataOf(resp)["cdn_endpoints"])
@@ -171,14 +171,14 @@ func TestE2E_TamperRejected(t *testing.T) {
_, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/issue", map[string]any{
"review_id": reviewID, "issuer": "x",
})
maCode := dataOf(resp)["ma_code"].(string)
ccCode := dataOf(resp)["ma_code"].(string)
cert := dataOf(resp)["certificate"].(string)
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/ingest", map[string]any{"ma_code": maCode, "content_twin_id": ctid, "media_asset_id": "M", "lib_name": "L"})
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/publish", map[string]any{"ma_code": maCode, "certificate": cert})
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/ingest", map[string]any{"ma_code": ccCode, "content_twin_id": ctid, "media_asset_id": "M", "lib_name": "L"})
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/publish", map[string]any{"ma_code": ccCode, "certificate": cert})
// 篡改文件注入 → 拒绝
st, _ = signedCall(t, b, "ak-operator", "sk-operator", "POST", "/content/inject", map[string]any{
"content_twin_id": ctid, "ma_code": maCode, "file_sha256": "fh-TAMPERED",
"content_twin_id": ctid, "ma_code": ccCode, "file_sha256": "fh-TAMPERED",
"operator_id": "OP", "cdn_endpoint": "cdn://x",
})
assert.Equal(t, http.StatusBadRequest, st, "篡改注入必须被拒")
@@ -235,14 +235,14 @@ func TestE2E_PermissionMatrix(t *testing.T) {
// 正常签发
_, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/issue", map[string]any{"review_id": reviewID, "issuer": "x"})
maCode := dataOf(resp)["ma_code"].(string)
ccCode := dataOf(resp)["ma_code"].(string)
// 运营商越权下架 → 403
st, _ = signedCall(t, b, "ak-operator", "sk-operator", "POST", "/content/takedown", map[string]any{"ma_code": maCode, "reason": "越权"})
st, _ = signedCall(t, b, "ak-operator", "sk-operator", "POST", "/content/takedown", map[string]any{"ma_code": ccCode, "reason": "越权"})
assert.Equal(t, http.StatusForbidden, st, "运营商不得发起下架")
// 无效签名 → 401
req, _ := http.NewRequest("GET", b+"/api/v1/content/mappings?ma_code="+maCode, nil)
req, _ := http.NewRequest("GET", b+"/api/v1/content/mappings?ma_code="+ccCode, nil)
req.Header.Set("Authorization", "TCS ak-regulator:badsig")
r, _ := http.DefaultClient.Do(req)
assert.Equal(t, http.StatusUnauthorized, r.StatusCode, "错误签名应 401")
@@ -261,10 +261,10 @@ func TestE2E_TakedownLatency(t *testing.T) {
reviewID := dataOf(resp)["review_id"].(string)
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/csps-result", map[string]any{"review_id": reviewID, "approved": true})
_, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/issue", map[string]any{"review_id": reviewID, "issuer": "x"})
maCode := dataOf(resp)["ma_code"].(string)
ccCode := dataOf(resp)["ma_code"].(string)
start := time.Now()
st, _ := signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/takedown", map[string]any{"ma_code": maCode, "reason": "违规"})
st, _ := signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/takedown", map[string]any{"ma_code": ccCode, "reason": "违规"})
elapsed := time.Since(start)
require.Equal(t, http.StatusOK, st)
assert.Less(t, elapsed, time.Second, "下架端到端应在秒级内(目标分钟级)")
+8 -8
View File
@@ -16,7 +16,7 @@ import (
)
// Catalog 目录库查询服务。
// 提供四种维度的标识信息查询:MA码、Hash、省级编码、片库文件ID。
// 提供四种维度的标识信息查询:CC码、Hash、省级编码、片库文件ID。
type Catalog struct {
client chain.Client
store CatalogStore // 可选独立存储(为 nil 时委托 chain.Client
@@ -39,14 +39,14 @@ func NewWithStore(c chain.Client, s CatalogStore) *Catalog {
return &Catalog{client: c, store: s}
}
// QueryByMA 根据MA码查询标识信息及映射关系。
func (cat *Catalog) QueryByMA(maCode string) (model.ContentQueryResult, error) {
c, err := cat.client.QueryContent(maCode)
// QueryByMA 根据CC码查询标识信息及映射关系。
func (cat *Catalog) QueryByMA(ccCode string) (model.ContentQueryResult, error) {
c, err := cat.client.QueryContent(ccCode)
if err != nil {
return model.ContentQueryResult{Found: false}, err
}
mr, _ := cat.client.QueryMappings(maCode)
eps, _ := cat.client.ListEpisodes(maCode)
mr, _ := cat.client.QueryMappings(ccCode)
eps, _ := cat.client.ListEpisodes(ccCode)
bindings := eps
return model.ContentQueryResult{
Found: true,
@@ -81,6 +81,6 @@ func (cat *Catalog) QueryByLibraryFileID(libraryID string) (model.ContentQueryRe
}
// QueryAll 批量查询:按 MA 码返回全部关联信息(含绑定、映射、集级哈希)。
func (cat *Catalog) QueryAll(maCode string) (model.ContentQueryResult, error) {
return cat.QueryByMA(maCode)
func (cat *Catalog) QueryAll(ccCode string) (model.ContentQueryResult, error) {
return cat.QueryByMA(ccCode)
}
+7 -7
View File
@@ -15,7 +15,7 @@ func TestCatalog_QueryByMA(t *testing.T) {
// 发码
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-cat-001",
CCCode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-cat-001",
FileHash: "fh-cat-001", MerkleRoot: "mr-cat-001",
Content: model.Content{Title: "目录库测试剧", EpisodeCount: 2, MAType: "WD", Issuer: "测试局"},
})
@@ -33,7 +33,7 @@ func TestCatalog_QueryByHash(t *testing.T) {
cat := New(c)
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-cat-002",
CCCode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-cat-002",
FileHash: "fh-cat-002", MerkleRoot: "mr-cat-002",
Content: model.Content{Title: "Hash查询测试", MAType: "WD", Issuer: "测试局"},
})
@@ -43,7 +43,7 @@ func TestCatalog_QueryByHash(t *testing.T) {
res, err := cat.QueryByHash("fh-cat-002")
require.NoError(t, err)
assert.True(t, res.Found)
assert.Equal(t, "MA.156.8531.6101/WD/20260000002", res.Content.MACode)
assert.Equal(t, "MA.156.8531.6101/WD/20260000002", res.Content.CCCode)
// 不存在的 Hash
_, err = cat.QueryByHash("nonexistent")
@@ -55,7 +55,7 @@ func TestCatalog_QueryByProvincialCode(t *testing.T) {
cat := New(c)
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000003", ContentTwinID: "ctid-cat-003",
CCCode: "MA.156.8531.6101/WD/20260000003", ContentTwinID: "ctid-cat-003",
FileHash: "fh-cat-003", MerkleRoot: "mr-cat-003",
Content: model.Content{Title: "省级编码查询测试", MAType: "WD", Issuer: "测试局"},
})
@@ -71,7 +71,7 @@ func TestCatalog_QueryByProvincialCode(t *testing.T) {
res, err := cat.QueryByProvincialCode("PROV-CAT-001")
require.NoError(t, err)
assert.True(t, res.Found)
assert.Equal(t, "MA.156.8531.6101/WD/20260000003", res.Content.MACode)
assert.Equal(t, "MA.156.8531.6101/WD/20260000003", res.Content.CCCode)
}
func TestCatalog_QueryByLibraryFileID(t *testing.T) {
@@ -79,7 +79,7 @@ func TestCatalog_QueryByLibraryFileID(t *testing.T) {
cat := New(c)
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000004", ContentTwinID: "ctid-cat-004",
CCCode: "MA.156.8531.6101/WD/20260000004", ContentTwinID: "ctid-cat-004",
FileHash: "fh-cat-004", MerkleRoot: "mr-cat-004",
Content: model.Content{Title: "片库ID查询测试", MAType: "WD", Issuer: "测试局"},
})
@@ -95,5 +95,5 @@ func TestCatalog_QueryByLibraryFileID(t *testing.T) {
res, err := cat.QueryByLibraryFileID("LIB-CAT-001")
require.NoError(t, err)
assert.True(t, res.Found)
assert.Equal(t, "MA.156.8531.6101/WD/20260000004", res.Content.MACode)
assert.Equal(t, "MA.156.8531.6101/WD/20260000004", res.Content.CCCode)
}
@@ -1,6 +1,6 @@
// Package macode 实现 MA 码生成服务(模式B:自行发码)。
// Package cccode 实现 MA 码生成服务(模式B:自行发码)。
// TCS 与 MA 发码机构合作获取「码段(号段)」与「备案规则」,在本地按规则原子发码。
// 对应需求:需求3MA码签发)、需求16;与 ISO/IEC 15459 MA 标识体系对齐。
// 对应需求:需求3CC码签发)、需求16;与 ISO/IEC 15459 MA 标识体系对齐。
//
// MA 码结构(六段式,可由备案规则配置):
//
@@ -14,7 +14,7 @@
// - WD 内容类目(WD=微短剧 / WJ=网络剧 / DY=网络电影 / DH=网络动画)
// - yyyy 年份
// - sequence 号段内递增序列(按位补零)
package macode
package cccode
import (
"errors"
@@ -25,9 +25,9 @@ import (
// 错误定义。
var (
ErrSegmentExhausted = errors.New("macode: code segment exhausted")
ErrUnknownCategory = errors.New("macode: unknown content category")
ErrInvalidSegment = errors.New("macode: invalid segment range")
ErrSegmentExhausted = errors.New("cccode: code segment exhausted")
ErrUnknownCategory = errors.New("cccode: unknown content category")
ErrInvalidSegment = errors.New("cccode: invalid segment range")
)
// 内容类目码。
@@ -108,7 +108,7 @@ func segmentKey(s Segment) string {
// Issued 一次发码结果。
type Issued struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
IndustryNode string `json:"industry_node"`
OrgNode string `json:"org_node"`
Category string `json:"category"`
@@ -133,7 +133,7 @@ func (g *Generator) Allocate(category string) (Issued, error) {
year := g.clock().Year()
code := Format(seg, year, seq)
return Issued{
MACode: code,
CCCode: code,
IndustryNode: seg.IndustryNode,
OrgNode: seg.OrgNode,
Category: category,
@@ -1,4 +1,4 @@
package macode
package cccode
import (
"sync"
@@ -30,9 +30,9 @@ func TestAllocate_SequentialUnique(t *testing.T) {
for i := 0; i < 50; i++ {
issued, err := g.Allocate(CategoryMicroDrama)
require.NoError(t, err)
assert.False(t, seen[issued.MACode], "MA 码必须唯一: %s", issued.MACode)
seen[issued.MACode] = true
assert.True(t, IsValid(issued.MACode), "应符合格式: %s", issued.MACode)
assert.False(t, seen[issued.CCCode], "MA 码必须唯一: %s", issued.CCCode)
seen[issued.CCCode] = true
assert.True(t, IsValid(issued.CCCode), "应符合格式: %s", issued.CCCode)
}
assert.Len(t, seen, 50)
}
@@ -41,7 +41,7 @@ func TestAllocate_FormatCorrect(t *testing.T) {
g := newGen(t)
issued, err := g.Allocate(CategoryMicroDrama)
require.NoError(t, err)
assert.Equal(t, "MA.156.8531.4401/WD/20250000001", issued.MACode)
assert.Equal(t, "MA.156.8531.4401/WD/20250000001", issued.CCCode)
assert.Equal(t, uint64(1), issued.Sequence)
assert.Equal(t, 2025, issued.Year)
}
@@ -88,10 +88,10 @@ func TestAllocate_ConcurrentNoDuplicate(t *testing.T) {
return
}
mu.Lock()
if seen[issued.MACode] {
if seen[issued.CCCode] {
dup++
}
seen[issued.MACode] = true
seen[issued.CCCode] = true
mu.Unlock()
}()
}
@@ -1,4 +1,4 @@
package macode
package cccode
import (
"fmt"
@@ -17,21 +17,21 @@ type Parsed struct {
Sequence uint64
}
// maCodePattern 匹配六段式 MA 码:
// ccCodePattern 匹配六段式 MA 码:
// MA.156.{industry}.{org}/{category}/{yyyy}{sequence}
var maCodePattern = regexp.MustCompile(
var ccCodePattern = regexp.MustCompile(
`^(MA)\.(\d{3})\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)/([A-Z]{2})/(\d{4})(\d+)$`)
// Parse 将 MA 码字符串解析为结构化字段;格式非法返回错误(需求4 校验基础)。
func Parse(code string) (Parsed, error) {
m := maCodePattern.FindStringSubmatch(code)
m := ccCodePattern.FindStringSubmatch(code)
if m == nil {
return Parsed{}, fmt.Errorf("macode: invalid format: %s", code)
return Parsed{}, fmt.Errorf("cccode: invalid format: %s", code)
}
year, _ := strconv.Atoi(m[6])
seq, err := strconv.ParseUint(m[7], 10, 64)
if err != nil {
return Parsed{}, fmt.Errorf("macode: invalid sequence: %w", err)
return Parsed{}, fmt.Errorf("cccode: invalid sequence: %w", err)
}
return Parsed{
Root: m[1],
@@ -46,13 +46,13 @@ func Parse(code string) (Parsed, error) {
// IsValid 仅校验格式合法性。
func IsValid(code string) bool {
return maCodePattern.MatchString(code)
return ccCodePattern.MatchString(code)
}
// EpisodeSubID 生成集级子标识:{maCode}#E{NN}。
// EpisodeSubID 生成集级子标识:{ccCode}#E{NN}。
// 整剧用 MA 码,单集用子标识,便于按集验真/追更/下架。
func EpisodeSubID(maCode string, episode int) string {
return fmt.Sprintf("%s#E%02d", maCode, episode)
func EpisodeSubID(ccCode string, episode int) string {
return fmt.Sprintf("%s#E%02d", ccCode, episode)
}
// episodeSubPattern 匹配集级子标识后缀。
@@ -60,7 +60,7 @@ var episodeSubPattern = regexp.MustCompile(`^(.+)#E(\d+)$`)
// ParseEpisodeSubID 拆解集级子标识,返回主 MA 码与集号。
// 若无 #E 后缀,episode 返回 0(表示整剧)。
func ParseEpisodeSubID(subID string) (maCode string, episode int) {
func ParseEpisodeSubID(subID string) (ccCode string, episode int) {
m := episodeSubPattern.FindStringSubmatch(subID)
if m == nil {
return subID, 0
@@ -1,4 +1,4 @@
package macode
package cccode
import "sync"
@@ -1,4 +1,4 @@
package macode
package cccode
import (
"database/sql"
@@ -25,15 +25,15 @@ func NewPostgresStore(db *sql.DB) *PostgresStore {
// 单条 SQL 在行锁内完成读改写,并发安全;超过 end 返回耗尽错误。
func (s *PostgresStore) Next(segmentKey string, start, end uint64) (uint64, error) {
const q = `
INSERT INTO macode_cursor (segment_key, cursor)
INSERT INTO cccode_cursor (segment_key, cursor)
VALUES ($1, $2)
ON CONFLICT (segment_key)
DO UPDATE SET cursor = macode_cursor.cursor + 1, updated_at = NOW()
DO UPDATE SET cursor = cccode_cursor.cursor + 1, updated_at = NOW()
RETURNING cursor;`
var next uint64
if err := s.db.QueryRow(q, segmentKey, start).Scan(&next); err != nil {
return 0, fmt.Errorf("macode: pg next: %w", err)
return 0, fmt.Errorf("cccode: pg next: %w", err)
}
if next > end {
return 0, ErrSegmentExhausted
@@ -1,4 +1,4 @@
package macode
package cccode
import (
"database/sql"
@@ -31,7 +31,7 @@ func openTestDB(t *testing.T) *sql.DB {
func cleanupKey(t *testing.T, db *sql.DB, key string) {
t.Helper()
_, _ = db.Exec("DELETE FROM macode_cursor WHERE segment_key = $1", key)
_, _ = db.Exec("DELETE FROM cccode_cursor WHERE segment_key = $1", key)
}
func TestPostgresStore_Sequential(t *testing.T) {
@@ -122,8 +122,8 @@ func TestPostgresStore_WithGenerator(t *testing.T) {
for i := 0; i < 10; i++ {
issued, err := g.Allocate(CategoryAnimation)
require.NoError(t, err)
assert.False(t, seen[issued.MACode])
assert.True(t, IsValid(issued.MACode))
seen[issued.MACode] = true
assert.False(t, seen[issued.CCCode])
assert.True(t, IsValid(issued.CCCode))
seen[issued.CCCode] = true
}
}
+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()
+2 -2
View File
@@ -6,7 +6,7 @@ import "time"
// Authorization 信息网络传播权授权(需求25-AC1)。
type Authorization struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Regions []string `json:"regions"` // 授权地域(省码),空=全国
Platforms []string `json:"platforms"` // 授权平台/运营商,空=不限
ExpiryAt time.Time `json:"expiry_at"` // 授权到期;零值=长期
@@ -22,7 +22,7 @@ type AuthCheckResult struct {
// CrossProvinceResult 跨省复用准入结果(需求13)。
type CrossProvinceResult struct {
Admitted bool `json:"admitted"`
MACodeValid bool `json:"ma_code_valid"`
CCCodeValid bool `json:"ma_code_valid"`
HashConsistent bool `json:"hash_consistent"`
NotBlacklisted bool `json:"not_blacklisted"`
ProvinceFlowNo string `json:"province_flow_no"` // 本省审核流水号
+11 -11
View File
@@ -16,7 +16,7 @@ const (
// Content 内容主表(Content Registry)。
type Content struct {
ContentTwinID string `json:"content_twin_id"`
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
MAType string `json:"ma_type"`
Title string `json:"title"`
EpisodeCount int `json:"episode_count"`
@@ -95,8 +95,8 @@ const (
StatusInLibrary = "in_library" // 已入媒资库
StatusPublished = "published" // 已发布
StatusRevoked = "revoked" // 已下架
StatusMerged = "merged" // 已合并入其他MA
StatusSplit = "split" // 已拆分为多个MA
StatusMerged = "merged" // 已合并入其他CC
StatusSplit = "split" // 已拆分为多个CC
)
// ContentQueryResult 多维度标识查询的统一返回结果。
@@ -110,16 +110,16 @@ type ContentQueryResult struct {
// MergeRequest MA 合并请求(将多个 MA 码合并为一个主 MA 码)。
type MergeRequest struct {
PrimaryMACode string `json:"primary_ma_code"` // 合并后保留的主 MA 码
SecondaryMACodes []string `json:"secondary_ma_codes"` // 被合并的 MA 码列表(合并后标记为 merged)
PrimaryCCCode string `json:"primary_ma_code"` // 合并后保留的主 MA 码
SecondaryCCCodes []string `json:"secondary_ma_codes"` // 被合并的 MA 码列表(合并后标记为 merged)
Reason string `json:"reason"` // 合并原因
Operator string `json:"operator"` // 操作人
}
// MergeResult MA 合并结果。
type MergeResult struct {
PrimaryMACode string `json:"primary_ma_code"` // 主 MA 码
MergedMACodes []string `json:"merged_ma_codes"` // 已合并的 MA 码列表
PrimaryCCCode string `json:"primary_ma_code"` // 主 MA 码
MergedCCCodes []string `json:"merged_ma_codes"` // 已合并的 MA 码列表
MigratedBindings int `json:"migrated_bindings"` // 迁移的哈希绑定数
MigratedMappings int `json:"migrated_mappings"` // 迁移的映射数
TxID string `json:"tx_id"` // 链上交易 ID
@@ -127,7 +127,7 @@ type MergeResult struct {
// SplitRequest MA 拆分请求(将一个 MA 码拆分为多个独立 MA 码)。
type SplitRequest struct {
SourceMACode string `json:"source_ma_code"` // 被拆分的源 MA 码
SourceCCCode string `json:"source_ma_code"` // 被拆分的源 MA 码
Splits []SplitTarget `json:"splits"` // 拆分目标列表
Reason string `json:"reason"` // 拆分原因
Operator string `json:"operator"` // 操作人
@@ -135,15 +135,15 @@ type SplitRequest struct {
// SplitTarget 拆分目标:每集或每组内容拆分后的新 MA 码信息。
type SplitTarget struct {
NewMACode string `json:"new_ma_code"` // 拆分后新分配的 MA 码
NewCCCode string `json:"new_ma_code"` // 拆分后新分配的 MA 码
Title string `json:"title"` // 拆分后内容标题
Episodes []int `json:"episodes"` // 关联的集号列表(空表示整剧拆分)
}
// SplitResult MA 拆分结果。
type SplitResult struct {
SourceMACode string `json:"source_ma_code"` // 源 MA 码
NewMACodes []string `json:"new_ma_codes"` // 拆分后生成的新 MA 码列表
SourceCCCode string `json:"source_ma_code"` // 源 MA 码
NewCCCodes []string `json:"new_ma_codes"` // 拆分后生成的新 MA 码列表
MigratedBindings int `json:"migrated_bindings"` // 迁移的哈希绑定数
MigratedMappings int `json:"migrated_mappings"` // 迁移的映射数
TxID string `json:"tx_id"` // 链上交易 ID
+3 -3
View File
@@ -15,7 +15,7 @@ const (
// PlaybackEvent 运营商以 MA 码为维度回传的播放/消费事件(需求9-AC1)。
type PlaybackEvent struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Episode int `json:"episode"` // 0=整剧/单体
PlatformID string `json:"platform_id"` // 运营商节点
UserHash string `json:"user_hash"` // 用户标识哈希(隐私保护)
@@ -27,7 +27,7 @@ type PlaybackEvent struct {
// PlaybackSummary 按 MA 码聚合的可信播放数据(需求9-AC2、需求21-AC1)。
type PlaybackSummary struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
TotalPlays int64 `json:"total_plays"`
TotalComplete int64 `json:"total_complete"`
TotalRevenue int64 `json:"total_revenue_cent"`
@@ -55,7 +55,7 @@ func DefaultShareConfig() RevenueShareConfig {
// Settlement 基于可信播放数据的分账结算结果(需求21-AC3)。
type Settlement struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Period string `json:"period"`
TotalRevenue int64 `json:"total_revenue_cent"`
CPShare int64 `json:"cp_share_cent"`
+4 -4
View File
@@ -18,7 +18,7 @@ const (
// ProvenanceEvent 全链路存证事件(需求22-AC1/AC3):带时间戳、操作方、不可篡改。
type ProvenanceEvent struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Node ProvenanceNode `json:"node"`
HashValue string `json:"hash_value"` // 该节点经手的内容哈希(可空,如纯审核结论)
Operator string `json:"operator"` // 操作方标识
@@ -28,7 +28,7 @@ type ProvenanceEvent struct {
// AccountabilityReport 责任界定取证报告(需求22-AC2/AC4)。
type AccountabilityReport struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Trail []ProvenanceEvent `json:"trail"`
BaselineHash string `json:"baseline_hash"` // 发码时绑定的基准哈希
FirstChange *ProvenanceEvent `json:"first_change"` // 首次发生哈希变化的节点(nil=全程一致)
@@ -38,7 +38,7 @@ type AccountabilityReport struct {
// CopyrightEvidence 版权确权证据链(需求23-AC1/AC2)。
type CopyrightEvidence struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Title string `json:"title"`
ContentHash string `json:"content_hash"`
Issuer string `json:"issuer"`
@@ -51,7 +51,7 @@ type CopyrightEvidence struct {
// InfringeMatch 疑似侵权命中(需求23-AC3)。
type InfringeMatch struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Title string `json:"title"`
Distance int `json:"hamming_distance"` // 感知哈希汉明距离,越小越相似
Similarity string `json:"similarity"` // high/medium
+1 -1
View File
@@ -4,7 +4,7 @@ package model
// FilingRecord 备案/网标关联(三期 A.1,对接广电总局备案/发行许可证系统)。
type FilingRecord struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
LicenseNo string `json:"license_no"` // 网络剧片发行许可证号(网标号)
FilingNo string `json:"filing_no"` // 重点网络影视剧备案号
BoundAt string `json:"bound_at"`
+5 -5
View File
@@ -44,7 +44,7 @@ type ParsedMA struct {
// ResolveResult 跨域解析网关返回(四期 C.1/C.2,大小屏统一解析)。
type ResolveResult struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Parsed ParsedMA `json:"parsed"`
Title string `json:"title"`
Status string `json:"status"` // 流通状态(published/revoked/...
@@ -58,7 +58,7 @@ type ResolveResult struct {
// ScanVerifyResult 用户扫码验真返回(四期 B.2)。
type ScanVerifyResult struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
Authentic bool `json:"authentic"` // MA 码真伪(链上存在且结构合法)
Compliant bool `json:"compliant"` // 是否合规流通(未下架)
Status string `json:"status"`
@@ -70,7 +70,7 @@ type ScanVerifyResult struct {
// PurchaseRecord 用户一次购买记录(四期 D.1)。
type PurchaseRecord struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
UserHash string `json:"user_hash"`
Screen ScreenType `json:"screen"` // 购买时所在屏
PurchasedAt time.Time `json:"purchased_at"`
@@ -79,12 +79,12 @@ type PurchaseRecord struct {
// UserRights 用户跨屏权益账户:以 MA 码为维度记录购买,任一屏购买即全屏通看。
type UserRights struct {
UserHash string `json:"user_hash"`
Purchases map[string]PurchaseRecord `json:"purchases"` // maCode -> record
Purchases map[string]PurchaseRecord `json:"purchases"` // ccCode -> record
}
// CrossScreenRightsResult 跨屏权益核验返回(四期 D.1)。
type CrossScreenRightsResult struct {
MACode string `json:"ma_code"`
CCCode string `json:"ma_code"`
UserHash string `json:"user_hash"`
Entitled bool `json:"entitled"` // 是否有权益(任一屏购买即全屏通看)
RequestScreen ScreenType `json:"request_screen"` // 当前请求屏
@@ -25,11 +25,11 @@ import (
// 聚合多省标识数据,提供统一查询入口。
type NationalCatalog struct {
mu stdsync.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
provinces map[string]*ProvinceInfo // provinceCode -> 省级信息
auditShared map[string][]model.ProvenanceEvent // maCode -> 跨省共享审核记录
auditShared map[string][]model.ProvenanceEvent // ccCode -> 跨省共享审核记录
}
// ProvinceInfo 省级节点信息。
@@ -81,44 +81,44 @@ func (nc *NationalCatalog) ListProvinces() []ProvinceInfo {
func (nc *NationalCatalog) UpsertContent(c model.Content) error {
nc.mu.Lock()
defer nc.mu.Unlock()
if _, exists := nc.contents[c.MACode]; exists {
if _, exists := nc.contents[c.CCCode]; exists {
return syncpkg.ErrConflict
}
nc.contents[c.MACode] = c
nc.contents[c.CCCode] = c
return nil
}
// UpsertBinding 写入/更新哈希绑定。
func (nc *NationalCatalog) UpsertBinding(maCode string, b model.HashBinding) error {
func (nc *NationalCatalog) UpsertBinding(ccCode string, b model.HashBinding) error {
nc.mu.Lock()
defer nc.mu.Unlock()
nc.bindings[maCode] = append(nc.bindings[maCode], b)
nc.bindings[ccCode] = append(nc.bindings[ccCode], b)
return nil
}
// UpsertMapping 写入/更新映射。
func (nc *NationalCatalog) UpsertMapping(maCode string, m model.Mapping) error {
func (nc *NationalCatalog) UpsertMapping(ccCode string, m model.Mapping) error {
nc.mu.Lock()
defer nc.mu.Unlock()
nc.mappings[maCode] = append(nc.mappings[maCode], m)
nc.mappings[ccCode] = append(nc.mappings[ccCode], m)
return nil
}
// ---- 全国统一查询 ----
// QueryByMA 按 MA 码查询(全国维度)。
func (nc *NationalCatalog) QueryByMA(maCode string) (model.ContentQueryResult, error) {
func (nc *NationalCatalog) QueryByMA(ccCode string) (model.ContentQueryResult, error) {
nc.mu.RLock()
defer nc.mu.RUnlock()
c, ok := nc.contents[maCode]
c, ok := nc.contents[ccCode]
if !ok {
return model.ContentQueryResult{Found: false}, fmt.Errorf("national: MA 码 %s 未找到", maCode)
return model.ContentQueryResult{Found: false}, fmt.Errorf("national: MA 码 %s 未找到", ccCode)
}
return model.ContentQueryResult{
Found: true,
Content: c,
Bindings: nc.bindings[maCode],
Mappings: nc.mappings[maCode],
Bindings: nc.bindings[ccCode],
Mappings: nc.mappings[ccCode],
}, nil
}
@@ -127,25 +127,25 @@ func (nc *NationalCatalog) QueryByHash(fileHash string) (model.ContentQueryResul
nc.mu.RLock()
defer nc.mu.RUnlock()
// 先搜索 Content.FileHash(整剧主哈希)
for maCode, c := range nc.contents {
for ccCode, c := range nc.contents {
if c.FileHash == fileHash {
return model.ContentQueryResult{
Found: true,
Content: c,
Bindings: nc.bindings[maCode],
Mappings: nc.mappings[maCode],
Bindings: nc.bindings[ccCode],
Mappings: nc.mappings[ccCode],
}, nil
}
}
// 再搜索 bindings 中的 HashValue(集级/转码版哈希)
for maCode, bindings := range nc.bindings {
for ccCode, bindings := range nc.bindings {
for _, b := range bindings {
if b.HashValue == fileHash {
return model.ContentQueryResult{
Found: true,
Content: nc.contents[maCode],
Content: nc.contents[ccCode],
Bindings: bindings,
Mappings: nc.mappings[maCode],
Mappings: nc.mappings[ccCode],
}, nil
}
}
@@ -157,13 +157,13 @@ func (nc *NationalCatalog) QueryByHash(fileHash string) (model.ContentQueryResul
func (nc *NationalCatalog) QueryByProvincialCode(provincialCode string) (model.ContentQueryResult, error) {
nc.mu.RLock()
defer nc.mu.RUnlock()
for maCode, mappings := range nc.mappings {
for ccCode, mappings := range nc.mappings {
for _, mp := range mappings {
if mp.Party == model.PartyCP && mp.PartyID == provincialCode {
return model.ContentQueryResult{
Found: true,
Content: nc.contents[maCode],
Bindings: nc.bindings[maCode],
Content: nc.contents[ccCode],
Bindings: nc.bindings[ccCode],
Mappings: mappings,
}, nil
}
@@ -175,17 +175,17 @@ func (nc *NationalCatalog) QueryByProvincialCode(provincialCode string) (model.C
// ---- 跨省审核记录共享 ----
// ShareAuditRecord 省级节点上报审核记录至全国中心。
func (nc *NationalCatalog) ShareAuditRecord(maCode string, event model.ProvenanceEvent) {
func (nc *NationalCatalog) ShareAuditRecord(ccCode string, event model.ProvenanceEvent) {
nc.mu.Lock()
defer nc.mu.Unlock()
nc.auditShared[maCode] = append(nc.auditShared[maCode], event)
nc.auditShared[ccCode] = append(nc.auditShared[ccCode], event)
}
// QuerySharedAudit 查询跨省共享的审核记录。
func (nc *NationalCatalog) QuerySharedAudit(maCode string) []model.ProvenanceEvent {
func (nc *NationalCatalog) QuerySharedAudit(ccCode string) []model.ProvenanceEvent {
nc.mu.RLock()
defer nc.mu.RUnlock()
return nc.auditShared[maCode]
return nc.auditShared[ccCode]
}
// ---- 全国统计 ----
@@ -214,7 +214,7 @@ func (nc *NationalCatalog) Stats() NationalStats {
st.ByStatus[c.Status]++
st.ByCategory[c.MAType]++
// 按机构节点统计省份
for _, mp := range nc.mappings[c.MACode] {
for _, mp := range nc.mappings[c.CCCode] {
if mp.Party == model.PartyCP {
st.ByProvince[mp.PartyName]++
}
@@ -29,7 +29,7 @@ func TestNationalCatalog_SyncFromProvince(t *testing.T) {
// 源端发码
srcClient := chain.NewMemoryChain()
_, err := srcClient.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-nat-001",
CCCode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-nat-001",
FileHash: "fh-nat-001", MerkleRoot: "mr-nat-001",
Episodes: []model.EpisodeHash{
{Episode: 1, FileSHA256: "fh-nat-001-E1"},
@@ -74,7 +74,7 @@ func TestNationalCatalog_Stats(t *testing.T) {
srcClient := chain.NewMemoryChain()
_, err := srcClient.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-nat-002",
CCCode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-nat-002",
FileHash: "fh-nat-002", MerkleRoot: "mr-nat-002",
Content: model.Content{Title: "统计测试剧", MAType: "WD", Issuer: "陕西局"},
})
@@ -98,7 +98,7 @@ func TestNationalCatalog_Stats(t *testing.T) {
func TestNationalCatalog_ShareAuditRecord(t *testing.T) {
nc := New()
nc.ShareAuditRecord("MA.156.8531.6101/WD/20260000003", model.ProvenanceEvent{
MACode: "MA.156.8531.6101/WD/20260000003",
CCCode: "MA.156.8531.6101/WD/20260000003",
Node: model.NodeIssue,
Operator: "陕西局",
Detail: "跨省审核记录",
+9 -9
View File
@@ -14,7 +14,7 @@ import (
// Store 播放事件存储与聚合。
type Store struct {
mu sync.RWMutex
events map[string][]model.PlaybackEvent // maCode -> events
events map[string][]model.PlaybackEvent // ccCode -> events
}
// NewStore 创建播放数据存储。
@@ -29,21 +29,21 @@ func (s *Store) Ingest(events []model.PlaybackEvent) int {
defer s.mu.Unlock()
n := 0
for _, e := range events {
if e.MACode == "" {
if e.CCCode == "" {
continue
}
s.events[e.MACode] = append(s.events[e.MACode], e)
s.events[e.CCCode] = append(s.events[e.CCCode], e)
n++
}
return n
}
// Summary 按 MA 码聚合可信播放数据(需求9-AC2、需求21-AC1)。
func (s *Store) Summary(maCode string) model.PlaybackSummary {
func (s *Store) Summary(ccCode string) model.PlaybackSummary {
s.mu.RLock()
defer s.mu.RUnlock()
sum := model.PlaybackSummary{MACode: maCode, ByPlatform: map[string]model.PlatformMetric{}}
for _, e := range s.events[maCode] {
sum := model.PlaybackSummary{CCCode: ccCode, ByPlatform: map[string]model.PlatformMetric{}}
for _, e := range s.events[ccCode] {
pm := sum.ByPlatform[e.PlatformID]
switch e.EventType {
case model.EventPlay:
@@ -62,12 +62,12 @@ func (s *Store) Summary(maCode string) model.PlaybackSummary {
// ComputeSettlement 基于聚合的可信播放收益执行分账(需求21-AC3)。
// 分账依据明确标注为"链上可信播放数据",保证 CP 与运营商口径一致。
func (s *Store) ComputeSettlement(maCode, period string, cfg model.RevenueShareConfig) (model.Settlement, error) {
func (s *Store) ComputeSettlement(ccCode, period string, cfg model.RevenueShareConfig) (model.Settlement, error) {
if cfg.CPShareBp+cfg.PlatformShareBp+cfg.HubFeeBp != 10000 {
return model.Settlement{}, fmt.Errorf("playback: share config must sum to 10000bp, got %d",
cfg.CPShareBp+cfg.PlatformShareBp+cfg.HubFeeBp)
}
sum := s.Summary(maCode)
sum := s.Summary(ccCode)
total := sum.TotalRevenue
cp := total * int64(cfg.CPShareBp) / 10000
@@ -76,7 +76,7 @@ func (s *Store) ComputeSettlement(maCode, period string, cfg model.RevenueShareC
hub := total - cp - platform
return model.Settlement{
MACode: maCode, Period: period, TotalRevenue: total,
CCCode: ccCode, Period: period, TotalRevenue: total,
CPShare: cp, PlatformShare: platform, HubFee: hub,
DataSource: "链上可信播放数据",
}, nil
+2 -2
View File
@@ -9,7 +9,7 @@ import (
)
func ev(ma, plat string, t model.PlaybackEventType, rev int64) model.PlaybackEvent {
return model.PlaybackEvent{MACode: ma, PlatformID: plat, EventType: t, RevenueCent: rev}
return model.PlaybackEvent{CCCode: ma, PlatformID: plat, EventType: t, RevenueCent: rev}
}
func TestIngestAndSummary(t *testing.T) {
@@ -33,7 +33,7 @@ func TestIngestAndSummary(t *testing.T) {
func TestIngestSkipsEmptyMA(t *testing.T) {
s := NewStore()
n := s.Ingest([]model.PlaybackEvent{{MACode: ""}, ev("MA-1", "P", model.EventPlay, 0)})
n := s.Ingest([]model.PlaybackEvent{{CCCode: ""}, ev("MA-1", "P", model.EventPlay, 0)})
assert.Equal(t, 1, n)
}
+8 -8
View File
@@ -13,7 +13,7 @@ import (
// Store 全链路存证存储。
type Store struct {
mu sync.RWMutex
trails map[string][]model.ProvenanceEvent // maCode -> 时间序事件
trails map[string][]model.ProvenanceEvent // ccCode -> 时间序事件
}
// NewStore 创建存证存储。
@@ -28,22 +28,22 @@ func (s *Store) Record(e model.ProvenanceEvent) {
}
s.mu.Lock()
defer s.mu.Unlock()
s.trails[e.MACode] = append(s.trails[e.MACode], e)
s.trails[e.CCCode] = append(s.trails[e.CCCode], e)
}
// Trail 返回某 MA 码的全链路存证(需求22-AC1)。
func (s *Store) Trail(maCode string) []model.ProvenanceEvent {
func (s *Store) Trail(ccCode string) []model.ProvenanceEvent {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]model.ProvenanceEvent, len(s.trails[maCode]))
copy(out, s.trails[maCode])
out := make([]model.ProvenanceEvent, len(s.trails[ccCode]))
copy(out, s.trails[ccCode])
return out
}
// Accountability 责任界定:以发码基准哈希为准,定位首次发生哈希变化的节点(需求22-AC2)。
func (s *Store) Accountability(maCode string) model.AccountabilityReport {
trail := s.Trail(maCode)
report := model.AccountabilityReport{MACode: maCode, Trail: trail, Consistent: true}
func (s *Store) Accountability(ccCode string) model.AccountabilityReport {
trail := s.Trail(ccCode)
report := model.AccountabilityReport{CCCode: ccCode, Trail: trail, Consistent: true}
// 基准哈希 = 发码节点(NodeIssue)的哈希
for _, e := range trail {
@@ -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)
+16 -16
View File
@@ -23,16 +23,16 @@ import (
// SyncSource 标识同步数据源接口。
type SyncSource interface {
ListContents(status string) ([]model.Content, error)
QueryContent(maCode string) (model.Content, error)
QueryMappings(maCode string) (chain.MappingsResult, error)
ListEpisodes(maCode string) ([]model.HashBinding, error)
QueryContent(ccCode string) (model.Content, error)
QueryMappings(ccCode string) (chain.MappingsResult, error)
ListEpisodes(ccCode string) ([]model.HashBinding, error)
}
// SyncSink 标识同步数据汇接口(远端节点实现)。
type SyncSink interface {
UpsertContent(c model.Content) error
UpsertBinding(maCode string, b model.HashBinding) error
UpsertMapping(maCode string, m model.Mapping) error
UpsertBinding(ccCode string, b model.HashBinding) error
UpsertMapping(ccCode string, m model.Mapping) error
}
// ConflictResolver 同步冲突解决策略。
@@ -103,7 +103,7 @@ func (s *SyncService) Sync(req SyncRequest) (SyncResult, error) {
} else {
if err := s.sink.UpsertContent(c); err != nil {
if errors.Is(err, ErrConflict) && req.Resolver == ConflictFail {
return result, fmt.Errorf("sync: 冲突 MA 码 %s: %w", c.MACode, err)
return result, fmt.Errorf("sync: 冲突 MA 码 %s: %w", c.CCCode, err)
}
result.Failed++
continue
@@ -112,9 +112,9 @@ func (s *SyncService) Sync(req SyncRequest) (SyncResult, error) {
result.TotalContents++
// 同步哈希绑定
eps, _ := s.source.ListEpisodes(c.MACode)
eps, _ := s.source.ListEpisodes(c.CCCode)
for _, b := range eps {
if err := s.sink.UpsertBinding(c.MACode, b); err != nil {
if err := s.sink.UpsertBinding(c.CCCode, b); err != nil {
result.Failed++
continue
}
@@ -122,9 +122,9 @@ func (s *SyncService) Sync(req SyncRequest) (SyncResult, error) {
}
// 同步映射
mr, _ := s.source.QueryMappings(c.MACode)
mr, _ := s.source.QueryMappings(c.CCCode)
for _, m := range mr.Mappings {
if err := s.sink.UpsertMapping(c.MACode, m); err != nil {
if err := s.sink.UpsertMapping(c.CCCode, m); err != nil {
result.Failed++
continue
}
@@ -151,16 +151,16 @@ func (cs *ChainSource) ListContents(status string) ([]model.Content, error) {
}
// QueryContent 查询内容主记录。
func (cs *ChainSource) QueryContent(maCode string) (model.Content, error) {
return cs.Client.QueryContent(maCode)
func (cs *ChainSource) QueryContent(ccCode string) (model.Content, error) {
return cs.Client.QueryContent(ccCode)
}
// QueryMappings 查询映射。
func (cs *ChainSource) QueryMappings(maCode string) (chain.MappingsResult, error) {
return cs.Client.QueryMappings(maCode)
func (cs *ChainSource) QueryMappings(ccCode string) (chain.MappingsResult, error) {
return cs.Client.QueryMappings(ccCode)
}
// ListEpisodes 列出集级哈希。
func (cs *ChainSource) ListEpisodes(maCode string) ([]model.HashBinding, error) {
return cs.Client.ListEpisodes(maCode)
func (cs *ChainSource) ListEpisodes(ccCode string) ([]model.HashBinding, error) {
return cs.Client.ListEpisodes(ccCode)
}
+11 -11
View File
@@ -32,25 +32,25 @@ func (m *memorySink) UpsertContent(c model.Content) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.conflict {
if _, exists := m.contents[c.MACode]; exists {
if _, exists := m.contents[c.CCCode]; exists {
return ErrConflict
}
}
m.contents[c.MACode] = c
m.contents[c.CCCode] = c
return nil
}
func (m *memorySink) UpsertBinding(maCode string, b model.HashBinding) error {
func (m *memorySink) UpsertBinding(ccCode string, b model.HashBinding) error {
m.mu.Lock()
defer m.mu.Unlock()
m.bindings[maCode] = append(m.bindings[maCode], b)
m.bindings[ccCode] = append(m.bindings[ccCode], b)
return nil
}
func (m *memorySink) UpsertMapping(maCode string, mp model.Mapping) error {
func (m *memorySink) UpsertMapping(ccCode string, mp model.Mapping) error {
m.mu.Lock()
defer m.mu.Unlock()
m.mappings[maCode] = append(m.mappings[maCode], mp)
m.mappings[ccCode] = append(m.mappings[ccCode], mp)
return nil
}
@@ -60,7 +60,7 @@ func TestSyncService_FullSync(t *testing.T) {
// 在源端发码
_, err := src.Client.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-sync-001",
CCCode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-sync-001",
FileHash: "fh-sync-001", MerkleRoot: "mr-sync-001",
Episodes: []model.EpisodeHash{
{Episode: 1, FileSHA256: "fh-sync-001-E1"},
@@ -97,7 +97,7 @@ func TestSyncService_ConflictSkip(t *testing.T) {
// 源端发码
_, err := src.Client.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-sync-002",
CCCode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-sync-002",
FileHash: "fh-sync-002", MerkleRoot: "mr-sync-002",
Content: model.Content{Title: "冲突测试剧", MAType: "WD", Issuer: "测试局"},
})
@@ -105,7 +105,7 @@ func TestSyncService_ConflictSkip(t *testing.T) {
// 远端预先存在同 MA 码
sink.contents["MA.156.8531.6101/WD/20260000002"] = model.Content{
MACode: "MA.156.8531.6101/WD/20260000002", Title: "远端已有",
CCCode: "MA.156.8531.6101/WD/20260000002", Title: "远端已有",
}
// 冲突跳过策略
@@ -122,14 +122,14 @@ func TestSyncService_ConflictFail(t *testing.T) {
sink.conflict = true
_, err := src.Client.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000003", ContentTwinID: "ctid-sync-003",
CCCode: "MA.156.8531.6101/WD/20260000003", ContentTwinID: "ctid-sync-003",
FileHash: "fh-sync-003", MerkleRoot: "mr-sync-003",
Content: model.Content{Title: "冲突Fail测试", MAType: "WD", Issuer: "测试局"},
})
require.NoError(t, err)
sink.contents["MA.156.8531.6101/WD/20260000003"] = model.Content{
MACode: "MA.156.8531.6101/WD/20260000003", Title: "远端已有",
CCCode: "MA.156.8531.6101/WD/20260000003", Title: "远端已有",
}
svc := New(src, sink)
+4 -4
View File
@@ -49,7 +49,7 @@ flow() {
local iss; iss=$(call ak-regulator sk-regulator POST /content/issue \
"{\"review_id\":\"$rid\",\"issuer\":\"陕西IPTV运营公司\"}")
local ma cert; ma=$(field "$iss" ma_code); cert=$(field "$iss" certificate)
echo " MA码: $ma"
echo " CC码: $ma"
call ak-reviewer sk-reviewer POST /content/ingest \
"{\"ma_code\":\"$ma\",\"content_twin_id\":\"$ctid\",\"media_asset_id\":\"SXMEDIA-$fh\",\"lib_name\":\"陕西IPTV媒体资源库\"}" >/dev/null
@@ -58,10 +58,10 @@ flow() {
local inj; inj=$(call ak-operator sk-operator POST /content/inject \
"{\"content_twin_id\":\"$ctid\",\"ma_code\":\"$ma\",\"file_sha256\":\"$fh\",\"operator_id\":\"$opid\",\"cdn_endpoint\":\"$cdn\"}")
echo " 运营商: $opname 注入: $(field "$inj" distribution_id)"
echo "$ma" >> /tmp/tcs_demo_macodes.txt
echo "$ma" >> /tmp/tcs_demo_cccodes.txt
}
: > /tmp/tcs_demo_macodes.txt
: > /tmp/tcs_demo_cccodes.txt
flow "长安少年行" WD "fh-changan-001" \
"XAQJSL-2026-001" "西安曲江丝路文化传播有限公司" \
@@ -77,7 +77,7 @@ flow "丝路驼铃" DY "fh-silu-003" \
echo ""
echo "=== 已生成 MA 码(可复制到监管大屏查询)==="
cat /tmp/tcs_demo_macodes.txt
cat /tmp/tcs_demo_cccodes.txt
echo ""
echo "提示:在 http://localhost:5174「角色工作台 → 监管片库」点详情查看全链路三方映射与集级哈希;"
echo " 或在「大小屏融合」tab 用上述 MA 码体验跨域解析 / 扫码验真 / 跨屏权益。"
+1 -1
View File
@@ -28,7 +28,7 @@ export default function App() {
TCS-IPTV 内容可信锁定系统
</Title>
<Text style={{ color: '#b3c5ff', marginLeft: 16, whiteSpace: 'nowrap' }}>
陕西IPTV运营公司 · MA+哈希双锚定
陕西IPTV运营公司 · CC+哈希双锚定
</Text>
<Menu
mode="horizontal" theme="dark" selectedKeys={[view]}
+15 -15
View File
@@ -28,7 +28,7 @@ export default function FlowDemo() {
const [current, setCurrent] = useState(0)
const [running, setRunning] = useState(false)
const [logs, setLogs] = useState([])
const [ctx, setCtx] = useState({}) // reviewID/ctid/maCode/cert/fileHash/episodeHashes
const [ctx, setCtx] = useState({}) // reviewID/ctid/ccCode/cert/fileHash/episodeHashes
const [done, setDone] = useState(false)
const [episodes, setEpisodes] = useState([]) // 集级哈希列表
const [epVerify, setEpVerify] = useState({}) // {episode: 'match'|'mismatch'}
@@ -70,8 +70,8 @@ export default function FlowDemo() {
break
case 'issue':
res = await api.issue({ review_id: shared.reviewID, issuer: '陕西IPTV运营公司' })
if (res.ok) { shared.maCode = res.data.data.ma_code; shared.cert = res.data.data.certificate }
addLog(step.role, step.title, res.ok, res.ok ? `MA${shared.maCode}` : res.data.message)
if (res.ok) { shared.ccCode = res.data.data.ma_code; shared.cert = res.data.data.certificate }
addLog(step.role, step.title, res.ok, res.ok ? `CC${shared.ccCode}` : res.data.message)
break
case 'csps':
res = await api.csps({ review_id: shared.reviewID, approved: true, reviewer_id: 'sxiptv-审核01' })
@@ -79,18 +79,18 @@ export default function FlowDemo() {
break
case 'ingest':
res = await api.ingest({
ma_code: shared.maCode, content_twin_id: shared.ctid,
ma_code: shared.ccCode, content_twin_id: shared.ctid,
media_asset_id: 'SXMEDIA-' + fileHash, lib_name: '陕西IPTV媒体资源库',
})
addLog(step.role, step.title, res.ok, res.ok ? '已入媒资库' : res.data.message)
break
case 'publish':
res = await api.publish({ ma_code: shared.maCode, certificate: shared.cert })
res = await api.publish({ ma_code: shared.ccCode, certificate: shared.cert })
addLog(step.role, step.title, res.ok, res.ok ? '已发布' : res.data.message)
break
case 'inject':
res = await api.inject({
content_twin_id: shared.ctid, ma_code: shared.maCode, file_sha256: fileHash,
content_twin_id: shared.ctid, ma_code: shared.ccCode, file_sha256: fileHash,
operator_id: v.opId, cdn_endpoint: v.cdn,
})
addLog(step.role, step.title, res.ok,
@@ -119,11 +119,11 @@ export default function FlowDemo() {
setCurrent(STEPS.length)
setCtx(shared); setDone(true); setRunning(false)
message.success('全流程跑通:审过即锁定,锁定即通行')
await loadEpisodes(shared.maCode)
await loadEpisodes(shared.ccCode)
}
async function loadEpisodes(maCode) {
const res = await api.episodes(maCode)
async function loadEpisodes(ccCode) {
const res = await api.episodes(ccCode)
if (res.ok) setEpisodes(res.data.data.episodes || [])
}
@@ -131,7 +131,7 @@ export default function FlowDemo() {
async function verifyEp(ep, correct) {
const realHash = (ctx.episodeHashes || []).find((e) => e.episode === ep)?.hash
const submit = correct ? realHash : 'TAMPERED-' + realHash
const res = await api.verifyEpisode(ctx.maCode, ep, submit)
const res = await api.verifyEpisode(ctx.ccCode, ep, submit)
const matched = res.ok && res.data.data?.match
setEpVerify((prev) => ({ ...prev, [ep]: matched ? 'match' : 'mismatch' }))
if (matched) message.success(`${ep}集验真通过`)
@@ -139,7 +139,7 @@ export default function FlowDemo() {
}
async function doTakedown() {
const res = await api.takedown(ctx.maCode, '监管演示下架')
const res = await api.takedown(ctx.ccCode, '监管演示下架')
if (res.ok) {
addLog('regulator', '违规应急下架', true,
`秒级下架,受影响 CDN: ${(res.data.data.cdn_endpoints || []).join(', ')}`)
@@ -149,7 +149,7 @@ export default function FlowDemo() {
async function doTamperTest() {
const res = await api.inject({
content_twin_id: ctx.ctid, ma_code: ctx.maCode, file_sha256: 'TAMPERED-' + ctx.fileHash,
content_twin_id: ctx.ctid, ma_code: ctx.ccCode, file_sha256: 'TAMPERED-' + ctx.fileHash,
operator_id: 'OP-X', cdn_endpoint: 'cdn://x',
})
addLog('operator', '篡改注入测试', !res.ok, res.ok ? '⚠️ 异常:篡改竟通过' : '✅ 已拒绝:' + res.data.message)
@@ -210,7 +210,7 @@ export default function FlowDemo() {
{done && (
<Card title={<Space><SafetyCertificateOutlined />赋码结果双锚定</Space>} style={{ marginBottom: 16 }}>
<Descriptions bordered size="small" column={1}>
<Descriptions.Item label="MA 码(监管锚点)"><Text strong copyable>{ctx.maCode}</Text></Descriptions.Item>
<Descriptions.Item label="MA 码(监管锚点)"><Text strong copyable>{ctx.ccCode}</Text></Descriptions.Item>
<Descriptions.Item label="文件哈希(技术锚点)">{ctx.fileHash}</Descriptions.Item>
<Descriptions.Item label="CTID(机器主键)">{ctx.ctid}</Descriptions.Item>
</Descriptions>
@@ -226,14 +226,14 @@ export default function FlowDemo() {
<Card
title={<Space>集级面板<Tag color="purple">一剧一码 · {episodes.length} 集独立哈希</Tag></Space>}
style={{ marginBottom: 16 }}
extra={<Text type="secondary">集级子标识{ctx.maCode}#E01 </Text>}
extra={<Text type="secondary">集级子标识{ctx.ccCode}#E01 </Text>}
>
<Table
size="small" rowKey="episode" pagination={false}
dataSource={episodes}
columns={[
{ title: '集号', dataIndex: 'episode', width: 70, render: (e) => <Tag> {e} </Tag> },
{ title: '集级子标识', render: (_, r) => <Text code>{`${ctx.maCode}#E${String(r.episode).padStart(2, '0')}`}</Text> },
{ title: '集级子标识', render: (_, r) => <Text code>{`${ctx.ccCode}#E${String(r.episode).padStart(2, '0')}`}</Text> },
{ title: '该集哈希', dataIndex: 'hash_value', ellipsis: true },
{
title: '验真状态', width: 110, render: (_, r) => {
+16 -16
View File
@@ -14,16 +14,16 @@ const nodeLabel = {
}
// 分账面板
function SettlementPanel({ maCode }) {
function SettlementPanel({ ccCode }) {
const [sum, setSum] = useState(null)
const [st, setSt] = useState(null)
async function load() {
const s = await api.playbackSummary(maCode)
const s = await api.playbackSummary(ccCode)
setSum(s.data?.data)
const r = await api.settlement(maCode, '2026-06')
const r = await api.settlement(ccCode, '2026-06')
if (r.ok) setSt(r.data.data)
}
useEffect(() => { load() }, [maCode])
useEffect(() => { load() }, [ccCode])
if (!sum) return null
return (
<div>
@@ -49,9 +49,9 @@ function SettlementPanel({ maCode }) {
}
// 追责取证面板
function AccountabilityPanel({ maCode }) {
function AccountabilityPanel({ ccCode }) {
const [rep, setRep] = useState(null)
useEffect(() => { api.accountability(maCode).then((r) => setRep(r.data?.data)) }, [maCode])
useEffect(() => { api.accountability(ccCode).then((r) => setRep(r.data?.data)) }, [ccCode])
if (!rep) return null
return (
<div>
@@ -74,9 +74,9 @@ function AccountabilityPanel({ maCode }) {
}
// 确权举证面板
function EvidencePanel({ maCode }) {
function EvidencePanel({ ccCode }) {
const [ev, setEv] = useState(null)
useEffect(() => { api.evidence(maCode).then((r) => setEv(r.data?.data)) }, [maCode])
useEffect(() => { api.evidence(ccCode).then((r) => setEv(r.data?.data)) }, [ccCode])
if (!ev) return null
return (
<Card size="small" title="版权确权证据链">
@@ -94,7 +94,7 @@ function EvidencePanel({ maCode }) {
}
// 授权管理面板
function AuthPanel({ maCode }) {
function AuthPanel({ ccCode }) {
const [form] = Form.useForm()
const [checkResult, setCheckResult] = useState(null)
async function grant() {
@@ -102,13 +102,13 @@ function AuthPanel({ maCode }) {
const regions = v.regions ? v.regions.split(',').map((s) => s.trim()).filter(Boolean) : []
const platforms = v.platforms ? v.platforms.split(',').map((s) => s.trim()).filter(Boolean) : []
const expiry = v.expiry ? v.expiry.toISOString() : ''
const res = await api.authorize(maCode, regions, platforms, expiry)
const res = await api.authorize(ccCode, regions, platforms, expiry)
if (res.ok) message.success('授权已登记')
else message.error(res.data.message)
}
async function check() {
const v = form.getFieldsValue()
const res = await api.authCheck(maCode, v.checkRegion || '', v.checkPlatform || '')
const res = await api.authCheck(ccCode, v.checkRegion || '', v.checkPlatform || '')
setCheckResult(res.data?.data)
}
return (
@@ -134,13 +134,13 @@ function AuthPanel({ maCode }) {
)
}
export default function GovernancePanel({ maCode }) {
export default function GovernancePanel({ ccCode }) {
return (
<Tabs size="small" items={[
{ key: 'settle', label: '💰 分账', children: <SettlementPanel maCode={maCode} /> },
{ key: 'account', label: '⚖️ 追责取证', children: <AccountabilityPanel maCode={maCode} /> },
{ key: 'evidence', label: '📜 确权举证', children: <EvidencePanel maCode={maCode} /> },
{ key: 'auth', label: '🔑 授权管理', children: <AuthPanel maCode={maCode} /> },
{ key: 'settle', label: '💰 分账', children: <SettlementPanel ccCode={ccCode} /> },
{ key: 'account', label: '⚖️ 追责取证', children: <AccountabilityPanel ccCode={ccCode} /> },
{ key: 'evidence', label: '📜 确权举证', children: <EvidencePanel ccCode={ccCode} /> },
{ key: 'auth', label: '🔑 授权管理', children: <AuthPanel ccCode={ccCode} /> },
]} />
)
}
+7 -7
View File
@@ -268,7 +268,7 @@ function LibraryDesk({ tick, onChanged }) {
const [filter, setFilter] = useState('all')
const [rows, setRows] = useState([])
const [loading, setLoading] = useState(false)
const [detail, setDetail] = useState(null) // {maCode, mappings, episodes}
const [detail, setDetail] = useState(null) // {ccCode, mappings, episodes}
async function load() {
setLoading(true)
@@ -304,13 +304,13 @@ function LibraryDesk({ tick, onChanged }) {
})
}
function takedownEpisode(maCode, episode) {
function takedownEpisode(ccCode, episode) {
Modal.confirm({
title: `集级下架 · 第 ${episode}`,
content: `只下架《${maCode}#E${String(episode).padStart(2, '0')}》本集,整剧其他集继续流通。确认?`,
content: `只下架《${ccCode}#E${String(episode).padStart(2, '0')}》本集,整剧其他集继续流通。确认?`,
okText: '下架本集', okType: 'danger', cancelText: '取消',
onOk: async () => {
const res = await api.takedownEpisode(maCode, episode, '监管片库集级下架')
const res = await api.takedownEpisode(ccCode, episode, '监管片库集级下架')
if (res.ok) {
message.success(`${episode} 集已下架`)
await viewDetail(detail.content) // 刷新弹窗
@@ -326,8 +326,8 @@ function LibraryDesk({ tick, onChanged }) {
else message.error(res.data.message || '恢复失败')
}
async function restoreEpisode(maCode, episode) {
const res = await api.restoreEpisode(maCode, episode)
async function restoreEpisode(ccCode, episode) {
const res = await api.restoreEpisode(ccCode, episode)
if (res.ok) {
message.success('第 ' + episode + ' 集已恢复上架')
await viewDetail(detail.content)
@@ -420,7 +420,7 @@ function LibraryDesk({ tick, onChanged }) {
</Card>
</>
) },
{ key: 'gov', label: '权益与治理', children: <GovernancePanel maCode={detail.content.ma_code} /> },
{ key: 'gov', label: '权益与治理', children: <GovernancePanel ccCode={detail.content.ma_code} /> },
]} />
)}
</Modal>
+16 -16
View File
@@ -32,14 +32,14 @@ function ScreenTags({ screens }) {
// ============ 跨域解析网关(C.1/C.2============
function ResolvePanel() {
const [maCode, setMaCode] = useState('')
const [ccCode, setCcCode] = useState('')
const [res, setRes] = useState(null)
const [loading, setLoading] = useState(false)
async function doResolve() {
if (!maCode) return message.warning('请输入 MA 码(支持集级子标识 #E03)')
if (!ccCode) return message.warning('请输入 MA 码(支持集级子标识 #E03)')
setLoading(true)
const r = await api.resolve(maCode.trim())
const r = await api.resolve(ccCode.trim())
setLoading(false)
if (r.ok) setRes(r.data.data)
else { setRes(null); message.error(r.data.message || '解析失败') }
@@ -50,7 +50,7 @@ function ResolvePanel() {
<Card size="small" title={<Space><GlobalOutlined />MA 跨域解析网关 · 同一码三屏统一解析</Space>}>
<Space.Compact style={{ width: '100%', maxWidth: 640 }}>
<Input placeholder="如 MA.156.8531.6101/WD/20260000021 或 ...#E03"
value={maCode} onChange={(e) => setMaCode(e.target.value)} onPressEnter={doResolve} />
value={ccCode} onChange={(e) => setCcCode(e.target.value)} onPressEnter={doResolve} />
<Button type="primary" loading={loading} onClick={doResolve}>解析</Button>
</Space.Compact>
@@ -89,14 +89,14 @@ function ResolvePanel() {
// ============ 扫码验真(B.2============
function ScanVerifyPanel() {
const [maCode, setMaCode] = useState('')
const [ccCode, setCcCode] = useState('')
const [res, setRes] = useState(null)
const [loading, setLoading] = useState(false)
async function doScan() {
if (!maCode) return message.warning('请输入/扫描 MA 码')
if (!ccCode) return message.warning('请输入/扫描 MA 码')
setLoading(true)
const r = await api.scanVerify(maCode.trim())
const r = await api.scanVerify(ccCode.trim())
setLoading(false)
if (r.ok) setRes(r.data.data)
else { setRes(null); message.error(r.data.message || '验真失败') }
@@ -112,8 +112,8 @@ function ScanVerifyPanel() {
return (
<Card size="small" title={<Space><ScanOutlined />用户扫码验真 · 防盗版</Space>}>
<Space.Compact style={{ width: '100%', maxWidth: 640 }}>
<Input placeholder="模拟扫码:粘贴 MA 码" value={maCode}
onChange={(e) => setMaCode(e.target.value)} onPressEnter={doScan} />
<Input placeholder="模拟扫码:粘贴 MA 码" value={ccCode}
onChange={(e) => setCcCode(e.target.value)} onPressEnter={doScan} />
<Button type="primary" icon={<ScanOutlined />} loading={loading} onClick={doScan}>扫码验真</Button>
</Space.Compact>
@@ -140,7 +140,7 @@ function ScanVerifyPanel() {
// ============ 跨屏权益通兑(D.1============
function RightsPanel() {
const [maCode, setMaCode] = useState('')
const [ccCode, setCcCode] = useState('')
const [userHash, setUserHash] = useState('user-demo-001')
const [buyScreen, setBuyScreen] = useState('iptv')
const [verifyScreen, setVerifyScreen] = useState('app')
@@ -148,14 +148,14 @@ function RightsPanel() {
const [verifyRes, setVerifyRes] = useState(null)
async function doBuy() {
if (!maCode) return message.warning('请输入 MA 码')
const r = await api.purchase(maCode.trim(), userHash, buyScreen)
if (!ccCode) return message.warning('请输入 MA 码')
const r = await api.purchase(ccCode.trim(), userHash, buyScreen)
if (r.ok) { setBuyRes(r.data.data); message.success(`已在「${screenMeta[buyScreen].label}」购买`) }
else message.error(r.data.message || '购买失败')
}
async function doVerify() {
if (!maCode) return message.warning('请输入 MA 码')
const r = await api.verifyRights(maCode.trim(), userHash, verifyScreen)
if (!ccCode) return message.warning('请输入 MA 码')
const r = await api.verifyRights(ccCode.trim(), userHash, verifyScreen)
if (r.ok) setVerifyRes(r.data.data)
else message.error(r.data.message || '核验失败')
}
@@ -166,8 +166,8 @@ function RightsPanel() {
<Card size="small" title={<Space><ShoppingOutlined />跨屏权益通兑 · 一次购买全屏通看</Space>}>
<Space direction="vertical" style={{ width: '100%' }}>
<Space wrap>
<Input addonBefore="MA 码" style={{ width: 380 }} value={maCode}
onChange={(e) => setMaCode(e.target.value)} placeholder="已发布的 MA 码" />
<Input addonBefore="MA 码" style={{ width: 380 }} value={ccCode}
onChange={(e) => setCcCode(e.target.value)} placeholder="已发布的 MA 码" />
<Input addonBefore="用户" style={{ width: 220 }} value={userHash}
onChange={(e) => setUserHash(e.target.value)} />
</Space>
+19 -19
View File
@@ -53,30 +53,30 @@ export const api = {
publish: (body) => request('reviewer', 'POST', '/content/publish', body),
inject: (body) => request('operator', 'POST', '/content/inject', body),
// 监管功能
verify: (maCode, fileHash) => request('regulator', 'POST', '/content/verify', { ma_code: maCode, file_sha256: fileHash }),
mappings: (maCode) => request('regulator', 'GET', '/content/mappings?ma_code=' + encodeURIComponent(maCode)),
takedown: (maCode, reason) => request('regulator', 'POST', '/content/takedown', { ma_code: maCode, reason }),
takedownEpisode: (maCode, episode, reason) => request('regulator', 'POST', '/content/takedown-episode', { ma_code: maCode, episode, reason }),
restore: (maCode) => request('regulator', 'POST', '/content/restore', { ma_code: maCode }),
restoreEpisode: (maCode, episode) => request('regulator', 'POST', '/content/restore-episode', { ma_code: maCode, episode }),
verify: (ccCode, fileHash) => request('regulator', 'POST', '/content/verify', { ma_code: ccCode, file_sha256: fileHash }),
mappings: (ccCode) => request('regulator', 'GET', '/content/mappings?ma_code=' + encodeURIComponent(ccCode)),
takedown: (ccCode, reason) => request('regulator', 'POST', '/content/takedown', { ma_code: ccCode, reason }),
takedownEpisode: (ccCode, episode, reason) => request('regulator', 'POST', '/content/takedown-episode', { ma_code: ccCode, episode, reason }),
restore: (ccCode) => request('regulator', 'POST', '/content/restore', { ma_code: ccCode }),
restoreEpisode: (ccCode, episode) => request('regulator', 'POST', '/content/restore-episode', { ma_code: ccCode, episode }),
// 集级粒度(一剧一码 + 集级哈希)
episodes: (maCode) => request('regulator', 'GET', '/content/episodes?ma_code=' + encodeURIComponent(maCode)),
verifyEpisode: (maCode, episode, fileHash) => request('regulator', 'POST', '/content/verify-episode', { ma_code: maCode, episode, file_sha256: fileHash }),
episodes: (ccCode) => request('regulator', 'GET', '/content/episodes?ma_code=' + encodeURIComponent(ccCode)),
verifyEpisode: (ccCode, episode, fileHash) => request('regulator', 'POST', '/content/verify-episode', { ma_code: ccCode, episode, file_sha256: fileHash }),
// 工作队列(多角色工作台)
reviews: (role, status) => request(role, 'GET', '/content/reviews?status=' + status),
list: (role, status) => request(role, 'GET', '/content/list?status=' + status),
// 二期:分账/追责/确权/授权/跨省/追更/回传
playback: (platformId, batch) => request('operator', 'POST', '/data/playback', { platform_id: platformId, batch }),
playbackSummary: (maCode) => request('regulator', 'GET', '/data/playback-summary?ma_code=' + encodeURIComponent(maCode)),
settlement: (maCode, period) => request('regulator', 'POST', '/settlement/compute', { ma_code: maCode, period }),
accountability: (maCode) => request('regulator', 'GET', '/content/accountability?ma_code=' + encodeURIComponent(maCode)),
evidence: (maCode) => request('regulator', 'GET', '/content/evidence?ma_code=' + encodeURIComponent(maCode)),
authorize: (maCode, regions, platforms, expiryAt) => request('regulator', 'POST', '/content/authorize', { ma_code: maCode, regions, platforms, expiry_at: expiryAt }),
authCheck: (maCode, region, platform) => request('regulator', 'POST', '/content/auth-check', { ma_code: maCode, region, platform }),
crossProvince: (maCode, fileHash, province) => request('regulator', 'POST', '/content/cross-province', { ma_code: maCode, file_sha256: fileHash, province }),
playbackSummary: (ccCode) => request('regulator', 'GET', '/data/playback-summary?ma_code=' + encodeURIComponent(ccCode)),
settlement: (ccCode, period) => request('regulator', 'POST', '/settlement/compute', { ma_code: ccCode, period }),
accountability: (ccCode) => request('regulator', 'GET', '/content/accountability?ma_code=' + encodeURIComponent(ccCode)),
evidence: (ccCode) => request('regulator', 'GET', '/content/evidence?ma_code=' + encodeURIComponent(ccCode)),
authorize: (ccCode, regions, platforms, expiryAt) => request('regulator', 'POST', '/content/authorize', { ma_code: ccCode, regions, platforms, expiry_at: expiryAt }),
authCheck: (ccCode, region, platform) => request('regulator', 'POST', '/content/auth-check', { ma_code: ccCode, region, platform }),
crossProvince: (ccCode, fileHash, province) => request('regulator', 'POST', '/content/cross-province', { ma_code: ccCode, file_sha256: fileHash, province }),
// 四期:大小屏融合(跨域解析/扫码验真/跨屏权益)
resolve: (maCode) => request('regulator', 'GET', '/content/resolve?ma_code=' + encodeURIComponent(maCode)),
scanVerify: (maCode) => request('operator', 'POST', '/content/scan-verify', { ma_code: maCode }),
purchase: (maCode, userHash, screen) => request('operator', 'POST', '/rights/purchase', { ma_code: maCode, user_hash: userHash, screen }),
verifyRights: (maCode, userHash, screen) => request('operator', 'POST', '/rights/verify', { ma_code: maCode, user_hash: userHash, screen }),
resolve: (ccCode) => request('regulator', 'GET', '/content/resolve?ma_code=' + encodeURIComponent(ccCode)),
scanVerify: (ccCode) => request('operator', 'POST', '/content/scan-verify', { ma_code: ccCode }),
purchase: (ccCode, userHash, screen) => request('operator', 'POST', '/rights/purchase', { ma_code: ccCode, user_hash: userHash, screen }),
verifyRights: (ccCode, userHash, screen) => request('operator', 'POST', '/rights/verify', { ma_code: ccCode, user_hash: userHash, screen }),
}