init: AIGC-Hub/AVCC 方案文档 + TCS-IPTV 内容可信锁定系统 MVP

- 方案文档: AVCC 体系建设、IPTV TCS 需求(0-req)/PRD(1-prd)/任务(2-task)/二三四期任务
- tcs-iptv: Go 后端(哈希SDK/MA码生成/可信数据空间mock/业务编排/HTTP API+HMAC鉴权)
- web-console: React+AntD 监管大屏(角色工作台/全流程演示/监管片库)
- 一剧一码+集级哈希, 集级下架/恢复, 全栈测试通过
This commit is contained in:
selfrelease
2026-06-14 16:50:31 +08:00
commit a329d4906b
103 changed files with 20052 additions and 0 deletions
+270
View File
@@ -0,0 +1,270 @@
// Package service 实现 TCS-IPTV 的业务编排,
// 依赖 chain.Client(链)与哈希校验,串联送审→签发→验真→入库→发布→下架全流程。
// 对应需求:需求2/3/4/5/6/7/11/12/15。
package service
import (
"errors"
"fmt"
"sync"
"time"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/model"
)
// 业务错误。
var (
ErrIncompleteHashPkg = errors.New("service: incomplete hash package")
ErrDuplicateContent = errors.New("service: duplicate content (hash exists)")
ErrHashMismatch = errors.New("service: hash mismatch (suspected version replacement)")
ErrNotApproved = errors.New("service: content not approved")
ErrNoCertificate = errors.New("service: missing MA code or hash certificate")
ErrReauditPending = errors.New("service: reaudit pending, distribution blocked")
)
// Submission 送审申报(需求2)。
type Submission struct {
Title string
EpisodeCount int
Category string // 内容类目(macode.CategoryXxx),决定发码号段
FileHash string
MerkleRoot string
Perceptual string
Episodes []model.EpisodeHash // 分集哈希(按集提交)
CPMediaID string
CPName string
}
// SubmissionResult 送审受理结果。
type SubmissionResult struct {
ReviewID string `json:"review_id"`
ContentTwinID string `json:"content_twin_id"`
Status string `json:"status"`
Message string `json:"message"`
}
// Service 业务编排器。
type Service struct {
chain chain.Client
gen *macode.Generator
mu sync.Mutex
seqMu sync.Mutex
seqs map[string]int // 按前缀独立计数(REV/ctid/DIST 各自从 1 递增)
// reviewStore 暂存送审申报(MVP 内存;生产落 PG)
reviews map[string]*reviewItem
}
type reviewItem struct {
ContentTwinID string
Sub Submission
Status string
MACode string
}
// New 创建业务服务。
func New(c chain.Client, gen *macode.Generator) *Service {
return &Service{chain: c, gen: gen, seqs: make(map[string]int), reviews: make(map[string]*reviewItem)}
}
func (s *Service) nextID(prefix string) string {
s.seqMu.Lock()
defer s.seqMu.Unlock()
s.seqs[prefix]++
return fmt.Sprintf("%s-%s-%04d", prefix, time.Now().Format("20060102"), s.seqs[prefix])
}
// SubmitForReview 处理 CP 送审申报(需求2)。
// 校验哈希包完整性、拦截换壳重发,受理后返回送审流水号。
func (s *Service) SubmitForReview(sub Submission) (SubmissionResult, error) {
if sub.FileHash == "" || sub.MerkleRoot == "" {
return SubmissionResult{}, ErrIncompleteHashPkg
}
// 防换壳重发(需求2-AC3、需求15-AC5
if maCode, exists := s.chain.HashExists(sub.FileHash); exists {
return SubmissionResult{
Status: "rejected",
Message: fmt.Sprintf("内容哈希已存在,关联原 MA 码: %s", maCode),
}, ErrDuplicateContent
}
s.mu.Lock()
defer s.mu.Unlock()
reviewID := s.nextID("REV")
ctid := s.nextID("ctid")
s.reviews[reviewID] = &reviewItem{
ContentTwinID: ctid,
Sub: sub,
Status: model.StatusPending,
}
return SubmissionResult{
ReviewID: reviewID,
ContentTwinID: ctid,
Status: model.StatusPending,
Message: "哈希已受理,待审核签发 MA 码",
}, nil
}
// IssueResult 签发结果。
type IssueResult struct {
MACode string `json:"ma_code"`
ContentTwinID string `json:"content_twin_id"`
TxID string `json:"tx_id"`
Certificate string `json:"certificate"` // MA码+哈希证书(MVP 简化为字符串)
}
// ReviewCSPS CSPS 合规审核(发码前)。审核通过后方可发码,体现"审过才发证发码"。
// 对应需求5(CSPS审核)+ 需求3-AC2(审核通过后生成MA码)。
func (s *Service) ReviewCSPS(reviewID string, approved bool, reviewerID string) error {
s.mu.Lock()
defer s.mu.Unlock()
item, ok := s.reviews[reviewID]
if !ok {
return fmt.Errorf("service: review %s not found", reviewID)
}
if approved {
item.Status = model.StatusApproved
} else {
item.Status = model.StatusRejected
}
return nil
}
// ApproveAndIssue 在 CSPS 审核通过后**生成 MA 码**并强绑定哈希(需求3,模式B 自行发码)。
// 前置:该送审必须已通过 CSPS 审核(审过才发码)。
// MA 码由 macode.Generator 按内容类目从号段中原子分配。仅监管主体可调用。
func (s *Service) ApproveAndIssue(role chain.Role, reviewID, issuer string) (IssueResult, error) {
s.mu.Lock()
item, ok := s.reviews[reviewID]
s.mu.Unlock()
if !ok {
return IssueResult{}, fmt.Errorf("service: review %s not found", reviewID)
}
// 审核门禁:未通过 CSPS 审核不得发码
if item.Status == model.StatusRejected {
return IssueResult{}, ErrNotApproved
}
if item.Status != model.StatusApproved {
return IssueResult{}, fmt.Errorf("%w: 需先通过 CSPS 审核", ErrNotApproved)
}
// 模式B:按类目自行发码
issued, err := s.gen.Allocate(item.Sub.Category)
if err != nil {
return IssueResult{}, fmt.Errorf("service: allocate MA code: %w", err)
}
maCode := issued.MACode
txID, err := s.chain.IssueMA(role, chain.IssueRequest{
MACode: maCode,
ContentTwinID: item.ContentTwinID,
MerkleRoot: item.Sub.MerkleRoot,
FileHash: item.Sub.FileHash,
PerceptualHash: item.Sub.Perceptual,
Episodes: item.Sub.Episodes,
Content: model.Content{
Title: item.Sub.Title,
EpisodeCount: item.Sub.EpisodeCount,
MAType: item.Sub.Category,
Issuer: issuer,
IssueDate: time.Now().Format("2006-01-02"),
},
})
if err != nil {
return IssueResult{}, err
}
s.mu.Lock()
item.Status = model.StatusIssued // 已发码,移出"待发码"队列
item.MACode = maCode
s.mu.Unlock()
// CP 注册本方映射
_, _ = s.chain.RegisterMapping(role, model.Mapping{
ContentTwinID: item.ContentTwinID,
Party: model.PartyCP,
PartyID: item.Sub.CPMediaID,
PartyName: item.Sub.CPName,
})
cert := fmt.Sprintf("CERT|%s|%s|%s", maCode, item.Sub.FileHash, item.Sub.MerkleRoot)
return IssueResult{
MACode: maCode,
ContentTwinID: item.ContentTwinID,
TxID: txID,
Certificate: cert,
}, nil
}
// Verify 送审文件验真 / CDN 注入校验通用入口(需求4、需求7)。
func (s *Service) Verify(maCode, fileHash string) (chain.VerifyResult, error) {
res, err := s.chain.VerifyHash(maCode, fileHash)
if err != nil {
return res, err
}
if !res.Match {
return res, ErrHashMismatch
}
return res, nil
}
// QueryMappings 查询 MA 码绑定的三方映射与 CDN 端点(需求11/17)。
func (s *Service) QueryMappings(maCode string) (chain.MappingsResult, error) {
return s.chain.QueryMappings(maCode)
}
// 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)
if err != nil {
return res, err
}
if !res.Match {
return res, ErrHashMismatch
}
return res, nil
}
// ListEpisodes 列出某剧的全部集级哈希绑定。
func (s *Service) ListEpisodes(maCode string) ([]model.HashBinding, error) {
return s.chain.ListEpisodes(maCode)
}
// ReviewSummary 送审待办摘要(发码前阶段)。
type ReviewSummary struct {
ReviewID string `json:"review_id"`
ContentTwinID string `json:"content_twin_id"`
Title string `json:"title"`
Category string `json:"category"`
EpisodeCount int `json:"episode_count"`
Status string `json:"status"`
CPName string `json:"cp_name"`
MACode string `json:"ma_code"`
}
// ListReviews 列出指定状态的送审待办(用于审核台/发码台队列)。
// status 空则返回全部;常用 pending(待审)、approved(待发码)。
func (s *Service) ListReviews(status string) []ReviewSummary {
s.mu.Lock()
defer s.mu.Unlock()
var out []ReviewSummary
for id, item := range s.reviews {
if status != "" && item.Status != status {
continue
}
out = append(out, 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,
})
}
return out
}
// ListContentsByStatus 列出指定状态的内容(用于入库台/发布台/注入台队列)。
// 常用 approved(待入库)、in_library(待发布)、published(待注入)。
func (s *Service) ListContentsByStatus(status string) ([]model.Content, error) {
return s.chain.ListContents(status)
}