// 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" "github.com/tcs-iptv/tcs/internal/playback" ) // 业务错误。 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 pb *playback.Store 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, pb: playback.NewStore(), 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) }