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
+176
View File
@@ -0,0 +1,176 @@
package service
import (
"strings"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/hash"
"github.com/tcs-iptv/tcs/internal/model"
)
// ---- 工作包7:转码版哈希绑定(需求5) ----
// 注:CSPS 合规审核已前移至发码前(service.ReviewCSPS),此处仅处理转码。
// BindTranscoded 绑定转码版哈希,与母版建立父子关系(需求5-AC3/AC4/AC5)。
func (s *Service) BindTranscoded(role chain.Role, ctid, parentFileHash, transcodedHash, format, resolution, version string) (string, error) {
if transcodedHash == "" {
return "", ErrIncompleteHashPkg
}
return s.chain.RegisterHashBinding(role, model.HashBinding{
ContentTwinID: ctid,
HashType: model.HashTranscoded,
HashValue: transcodedHash,
ParentHash: parentFileHash,
FileFormat: format,
Resolution: resolution,
Version: version,
CreatedBy: string(role),
})
}
// ---- 工作包8:媒体资源库入库、发布与映射(需求6) ----
// IngestToLibrary 审核合格内容入媒资库,建立媒资编码映射(需求6-AC1/AC2/AC3)。
func (s *Service) IngestToLibrary(role chain.Role, maCode, ctid, mediaAssetID, libName string) error {
c, err := s.chain.QueryContent(maCode)
if err != nil {
return err
}
// 门禁:未审核通过/未绑定 MA 码不得入库可发布状态
if c.Status == model.StatusRejected || c.Status == model.StatusRevoked {
return ErrNotApproved
}
if _, err := s.chain.RegisterMapping(role, model.Mapping{
ContentTwinID: ctid,
Party: model.PartyReviewer,
PartyID: mediaAssetID,
PartyName: libName,
}); err != nil {
return err
}
return s.chain.SetContentStatus(maCode, model.StatusInLibrary)
}
// PublishRequest 从媒资库向运营商发布的请求(需求6-AC4)。
type PublishRequest struct {
MACode string
Certificate string // 必须携带 MA码+哈希证书
}
// PublishToOperator 校验证书后将内容置为已发布(需求6-AC4/AC5、需求3-AC8)。
func (s *Service) PublishToOperator(req PublishRequest) error {
c, err := s.chain.QueryContent(req.MACode)
if err != nil {
return err
}
if c.Status != model.StatusInLibrary && c.Status != model.StatusPublished {
return ErrNotApproved
}
// 发布必须携带证书(含 MA 码)
if req.Certificate == "" || !certContainsMA(req.Certificate, req.MACode) {
return ErrNoCertificate
}
return s.chain.SetContentStatus(req.MACode, model.StatusPublished)
}
// ---- 工作包9CDN 注入校验(需求7) ----
// InjectResult CDN 注入校验结果。
type InjectResult struct {
Allowed bool `json:"allowed"`
DistributionID string `json:"distribution_id,omitempty"`
Reason string `json:"reason,omitempty"`
}
// InjectToCDN 运营商注入 CDN 前校验哈希;匹配则放行并注册运营商映射(需求7-AC1~AC4)。
func (s *Service) InjectToCDN(role chain.Role, ctid, maCode, injectFileHash, operatorID, cdnEndpoint string) (InjectResult, error) {
// 内容须处于已发布状态
c, err := s.chain.QueryContent(maCode)
if err != nil {
return InjectResult{}, err
}
if c.Status == model.StatusRevoked {
return InjectResult{Allowed: false, Reason: "内容已下架"}, ErrNotApproved
}
res, err := s.chain.VerifyHash(maCode, injectFileHash)
if err != nil {
return InjectResult{Allowed: false, Reason: err.Error()}, err
}
if !res.Match {
// 不匹配:拒绝注入(需求7-AC3、需求15-AC2
return InjectResult{Allowed: false, Reason: "哈希不匹配,疑似篡改,拒绝注入并告警"}, ErrHashMismatch
}
distID := s.nextID("DIST")
if _, err := s.chain.RegisterMapping(role, model.Mapping{
ContentTwinID: ctid,
Party: model.PartyOperator,
PartyID: operatorID,
CDNEndpoint: cdnEndpoint,
}); err != nil {
return InjectResult{}, err
}
return InjectResult{Allowed: true, DistributionID: distID}, nil
}
// ---- 工作包10:版本变更与重审(需求12) ----
// ReportVersionChange 上报内容变更:哈希变化判定绑定断裂,触发重审(需求12-AC1/AC2)。
// 当提供 oldSegments/newSegments 时,定位被篡改的具体集(需求12-AC3)。
func (s *Service) ReportVersionChange(ctid, reason, prevHash, newHash string, oldSegments, newSegments []string) ([]int, error) {
var changedEpisodes []int
affected := 0
if len(oldSegments) > 0 || len(newSegments) > 0 {
changedEpisodes = hash.LocateChangedLeaves(oldSegments, newSegments)
if len(changedEpisodes) > 0 {
affected = changedEpisodes[0] + 1 // 1-based 集号
}
}
_, err := s.chain.RecordVersionChange(model.VersionChange{
ContentTwinID: ctid,
Version: "v-next",
ChangeReason: reason,
PrevHash: prevHash,
NewHash: newHash,
ReauditRequired: true,
ReauditStatus: "pending",
AffectedEpisode: affected,
})
if err != nil {
return nil, err
}
// 转为 1-based 集号返回
episodes := make([]int, len(changedEpisodes))
for i, idx := range changedEpisodes {
episodes[i] = idx + 1
}
return episodes, nil
}
// ---- 工作包14:违规应急下架(需求11) ----
// Takedown 监管主体一键下架:解析 MA 码绑定的三方编码与 CDN 端点(需求11-AC1/AC2/AC4)。
func (s *Service) Takedown(role chain.Role, maCode, reason string) (chain.MappingsResult, error) {
return s.chain.Revoke(role, maCode, reason)
}
// TakedownEpisode 集级下架:只下架指定集,整剧其他集继续流通(仅监管主体)。
func (s *Service) TakedownEpisode(role chain.Role, maCode string, episode int, reason string) error {
return s.chain.RevokeEpisode(role, maCode, episode, reason)
}
// Restore 恢复上架整剧(仅监管主体)。
func (s *Service) Restore(role chain.Role, maCode string) error {
return s.chain.Restore(role, maCode)
}
// RestoreEpisode 恢复上架指定集(仅监管主体)。
func (s *Service) RestoreEpisode(role chain.Role, maCode string, episode int) error {
return s.chain.RestoreEpisode(role, maCode, episode)
}
// certContainsMA 校验证书是否包含指定 MA 码。
func certContainsMA(cert, maCode string) bool {
return cert != "" && maCode != "" && strings.Contains(cert, maCode)
}