Files
MAcode/tcs-iptv/internal/service/service.go
T
selfrelease 468c3b5daa feat(phase2): 追更/授权链/跨省复用/终端抽检/CI(F21/F22/F13/F08/K)
- F21 追更: AddEpisodes 追加新集不重新发码; Merkle定位变更集
- F22 授权链: RecordAuthorization + CheckAuthorization(地域/平台/期限), 嵌入注入前核验
- F13 跨省复用: CrossProvinceAdmit 三重校验(MA有效+哈希一致+非黑名单)快速准入
- F08 终端抽检: TerminalVerifySegment 片段校验+断流提示
- K.1 CI: .gitlab-ci.yml(后端构建/测试/前端构建)
- 新增6个API; 16项测试通过; 二期纯代码功能全部完成
- A(真实链)/B(BFF)延后至有环境/三期, MemoryChain接口已就绪可平滑替换
2026-06-14 17:24:56 +08:00

302 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
"github.com/tcs-iptv/tcs/internal/provenance"
)
// 业务错误。
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
prov *provenance.Store
phash map[string]phashEntry // maCode -> 感知哈希条目(确权侵权比对)
auths map[string]model.Authorization // maCode -> 授权(F22
black map[string]bool // maCode -> 黑名单(跨省复用校验)
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
}
// phashEntry 感知哈希注册项,用于确权侵权比对。
type phashEntry struct {
Title string
Perceptual string
}
// New 创建业务服务。
func New(c chain.Client, gen *macode.Generator) *Service {
return &Service{
chain: c, gen: gen,
pb: playback.NewStore(),
prov: provenance.NewStore(),
phash: make(map[string]phashEntry),
auths: make(map[string]model.Authorization),
black: make(map[string]bool),
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,
})
// 记录全链路存证(送审→审核→发码)+ 注册感知哈希供确权比对
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: "发码签发,绑定基准哈希"})
if item.Sub.Perceptual != "" {
s.mu.Lock()
s.phash[maCode] = 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)
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)
}