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接口已就绪可平滑替换
This commit is contained in:
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tcs-iptv/tcs/internal/chain"
|
||||
"github.com/tcs-iptv/tcs/internal/hash"
|
||||
@@ -103,6 +104,11 @@ func (s *Service) InjectToCDN(role chain.Role, ctid, maCode, injectFileHash, ope
|
||||
return InjectResult{Allowed: false, Reason: "哈希不匹配,疑似篡改,拒绝注入并告警"}, ErrHashMismatch
|
||||
}
|
||||
|
||||
// 授权核验(需求25-AC2/AC3):若已登记授权,校验该运营商是否在授权平台内
|
||||
if authRes := s.CheckAuthorization(maCode, "", operatorID); !authRes.Allowed {
|
||||
return InjectResult{Allowed: false, Reason: authRes.Reason}, ErrNotApproved
|
||||
}
|
||||
|
||||
distID := s.nextID("DIST")
|
||||
if _, err := s.chain.RegisterMapping(role, model.Mapping{
|
||||
ContentTwinID: ctid,
|
||||
@@ -270,3 +276,138 @@ func (s *Service) MatchInfringement(perceptual string, high, medium int) ([]mode
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ---- 二期 F22:授权链与发布前核验(需求25) ----
|
||||
|
||||
// RecordAuthorization 登记信息网络传播权授权(需求25-AC1)。
|
||||
func (s *Service) RecordAuthorization(maCode string, regions, platforms []string, expiry time.Time) error {
|
||||
if _, err := s.chain.QueryContent(maCode); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.auths[maCode] = model.Authorization{
|
||||
MACode: maCode, Regions: regions, Platforms: platforms,
|
||||
ExpiryAt: expiry, GrantedAt: time.Now(),
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckAuthorization 核验某地域/平台是否在授权范围内(需求25-AC2/AC3)。
|
||||
// 未登记授权时默认放行(向后兼容);登记后超地域/过期/非授权平台拦截。
|
||||
func (s *Service) CheckAuthorization(maCode, region, platform string) model.AuthCheckResult {
|
||||
s.mu.Lock()
|
||||
a, ok := s.auths[maCode]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return model.AuthCheckResult{Allowed: true, Reason: "未登记授权限制"}
|
||||
}
|
||||
if !a.ExpiryAt.IsZero() && time.Now().After(a.ExpiryAt) {
|
||||
return model.AuthCheckResult{Allowed: false, Reason: "授权已过期"}
|
||||
}
|
||||
if region != "" && len(a.Regions) > 0 && !contains(a.Regions, region) {
|
||||
return model.AuthCheckResult{Allowed: false, Reason: "超出授权地域: " + region}
|
||||
}
|
||||
if platform != "" && len(a.Platforms) > 0 && !contains(a.Platforms, platform) {
|
||||
return model.AuthCheckResult{Allowed: false, Reason: "非授权平台: " + platform}
|
||||
}
|
||||
return model.AuthCheckResult{Allowed: true, Reason: "在授权范围内"}
|
||||
}
|
||||
|
||||
// ---- 二期 F21:追更与增量哈希更新(需求24) ----
|
||||
|
||||
// AddEpisodes 追更:为已发码剧追加新集哈希,不触发存量重审、不重新发码(需求24-AC4)。
|
||||
func (s *Service) AddEpisodes(role chain.Role, maCode string, episodes []model.EpisodeHash) error {
|
||||
c, err := s.chain.QueryContent(maCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ep := range episodes {
|
||||
if _, err := s.chain.RegisterHashBinding(role, model.HashBinding{
|
||||
ContentTwinID: c.ContentTwinID,
|
||||
HashType: model.HashFile,
|
||||
HashValue: ep.FileSHA256,
|
||||
MerkleRoot: ep.MerkleRoot,
|
||||
Episode: ep.Episode,
|
||||
Version: "v1.0",
|
||||
CreatedBy: string(role),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.prov.Record(model.ProvenanceEvent{
|
||||
MACode: maCode, Node: model.NodeSubmit,
|
||||
Operator: "追更", Detail: "追加新集,增量赋码",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- 二期 F13:跨省复用快速准入(需求13) ----
|
||||
|
||||
// Blacklist 将 MA 码加入黑名单(用于跨省校验)。
|
||||
func (s *Service) Blacklist(maCode string) {
|
||||
s.mu.Lock()
|
||||
s.black[maCode] = true
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// CrossProvinceAdmit B 省凭 MA 码+哈希证书快速准入(需求13)。
|
||||
// 三重校验:MA 码有效 + 哈希与原过审版一致 + 非黑名单。
|
||||
func (s *Service) CrossProvinceAdmit(maCode, fileHash, province string) model.CrossProvinceResult {
|
||||
res := model.CrossProvinceResult{}
|
||||
|
||||
// 1. MA 码有效
|
||||
if _, err := s.chain.QueryContent(maCode); err != nil {
|
||||
res.Reason = "MA 码无效或不存在"
|
||||
return res
|
||||
}
|
||||
res.MACodeValid = true
|
||||
|
||||
// 2. 哈希与原过审版一致
|
||||
vr, err := s.chain.VerifyHash(maCode, fileHash)
|
||||
if err != nil || !vr.Match {
|
||||
res.Reason = "哈希与原过审版不一致"
|
||||
return res
|
||||
}
|
||||
res.HashConsistent = true
|
||||
|
||||
// 3. 非黑名单
|
||||
s.mu.Lock()
|
||||
bl := s.black[maCode]
|
||||
s.mu.Unlock()
|
||||
if bl {
|
||||
res.Reason = "内容在黑名单中"
|
||||
return res
|
||||
}
|
||||
res.NotBlacklisted = true
|
||||
|
||||
// 准入:生成本省流水号并注册本省映射
|
||||
res.ProvinceFlowNo = s.nextID("REV-" + province)
|
||||
res.Admitted = true
|
||||
res.Reason = "三重校验通过,快速准入(审核简化为合规抽检)"
|
||||
return res
|
||||
}
|
||||
|
||||
// ---- 二期 F08:终端片段抽检(需求8) ----
|
||||
|
||||
// TerminalVerifySegment 终端按集抽检:校验某集哈希,不匹配则提示断流(需求8-AC1/AC2)。
|
||||
func (s *Service) TerminalVerifySegment(maCode string, episode int, segHash string) (bool, string) {
|
||||
res, err := s.chain.VerifyEpisodeHash(maCode, episode, segHash)
|
||||
if err != nil {
|
||||
return false, "无法校验:" + err.Error()
|
||||
}
|
||||
if !res.Match {
|
||||
return false, "片段哈希不匹配,疑似 CDN 劫持/传输篡改,建议断流切备用源"
|
||||
}
|
||||
return true, "校验通过"
|
||||
}
|
||||
|
||||
// contains 判断字符串切片是否包含目标。
|
||||
func contains(arr []string, v string) bool {
|
||||
for _, a := range arr {
|
||||
if a == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user