Files
MAcode/tcs-iptv/internal/service/distribution.go
T
selfrelease f44c53c5bb feat(phase2): 数据回传聚合与可信分账(F09/F18)
- internal/playback: 播放事件存储/MA码维度聚合/分账结算(CP60/平台34/服务费6)
- service: ReportPlayback(链上状态门禁)/PlaybackSummary/ComputeSettlement
- api: /data/playback, /data/playback-summary, /settlement/compute
- 分账取余兜底无丢分; 未知/已下架MA码回传被拒
- 13项新测试通过; 端到端验证: 回传3条→聚合40元→分账24/13.6/2.4
2026-06-14 17:00:57 +08:00

208 lines
7.3 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
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)
}
// ---- 二期 F09/F18:数据回传聚合与分账(需求9/需求21) ----
// ReportPlayback 运营商以 MA 码为维度批量回传播放/消费事件(需求9-AC1)。
// 仅当 MA 码存在且处于流通状态时接收,保证数据归属可信。
func (s *Service) ReportPlayback(events []model.PlaybackEvent) (accepted int, rejected int) {
valid := make([]model.PlaybackEvent, 0, len(events))
for _, e := range events {
c, err := s.chain.QueryContent(e.MACode)
if err != nil || c.Status == model.StatusRevoked {
rejected++
continue
}
valid = append(valid, e)
}
accepted = s.pb.Ingest(valid)
return accepted, rejected
}
// PlaybackSummary 查询按 MA 码聚合的可信播放数据(需求9-AC2/AC3)。
func (s *Service) PlaybackSummary(maCode string) model.PlaybackSummary {
return s.pb.Summary(maCode)
}
// ComputeSettlement 基于可信播放数据计算分账(需求21-AC3)。
func (s *Service) ComputeSettlement(maCode, period string) (model.Settlement, error) {
if _, err := s.chain.QueryContent(maCode); err != nil {
return model.Settlement{}, err
}
return s.pb.ComputeSettlement(maCode, period, model.DefaultShareConfig())
}