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:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user