8db9d33694
- A.1 备案对接: BindFiling/QueryFiling 关联网标号+备案号 - A.2 监管上报: DailyRegulatoryReport 日报 - B.1 号段管理: ListSegments + /admin/segments - C.1/C.2 全国统计按省聚合 + 跨省协同(单一可信源天然联动) - F.2 全国监管大屏: NationalStats(按省/类目/状态) - B(遗留) 监管大屏BFF: internal/bff + cmd/console-bff, 密钥仅存后端浏览器只用会话令牌 - G 真实链合约源码: contracts/tcs_registry/registry.go (ChainMaker Go) - 新增9个API+BFF服务; 5项新测试; 端到端BFF验证 - D/E(压测/等保/HSM)/F.1(标准)/真实链部署 标注需外部环境
304 lines
10 KiB
Go
304 lines
10 KiB
Go
// 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 -> 黑名单(跨省复用校验)
|
||
filings map[string]model.FilingRecord // maCode -> 备案/网标关联(三期 A.1)
|
||
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),
|
||
filings: make(map[string]model.FilingRecord),
|
||
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)
|
||
}
|