Files
MAcode/tcs-iptv/internal/service/phase4.go
T
selfrelease 2cd5fbec6d 四期(大小屏融合)后端可代码部分:跨域解析网关/扫码验真/跨屏权益通兑
- model/rights.go: ScreenType/ParsedMA/ResolveResult/ScanVerifyResult/UserRights/PurchaseRecord/CrossScreenRightsResult
- service/phase4.go: ParseMACode + Resolve(C.1/C.2) + ScanVerify(B.2) + RecordPurchase/VerifyCrossScreenRights(D.1)
- api/handlers.go: GET /content/resolve, POST /content/scan-verify, /rights/purchase, /rights/verify
- service/phase4_test.go: 18 单测全绿
- 同一MA码跨iptv/ott/app统一解析; 任一屏购买全屏通看不重复扣费
- OTT/移动端SDK/C2PA凭证标注需外部环境
- 更新 5-task-IPTV-四期.md 进度
2026-06-14 19:01:26 +08:00

196 lines
6.1 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 (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/tcs-iptv/tcs/internal/model"
)
// 四期(大小屏融合):跨域解析网关、扫码验真、跨屏权益通兑。
// 将 MA+哈希机制从 IPTV 扩展至 OTT、手机 APP,实现大小屏内容身份互通。
// 对应任务:C.1 跨域解析网关、C.2 身份互通、B.2 扫码验真、D.1 跨屏权益子链。
// maCodePattern 匹配六段式 MA 码(含可选集级子标识 #Exx)。
// 形如 MA.156.8531.6101/WD/20260000004 或 MA.156.8531.6101/WD/20260000004#E07
var maCodePattern = regexp.MustCompile(
`^(MA)\.(\d{3})\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)/([0-9A-Za-z]+)/(\d{4})(\d+)(?:#E(\d+))?$`)
// ParseMACode 解析六段式 MA 码(纯结构解析,不查链)。
// 返回 Valid=false 表示结构不合法(可能是伪造/损坏的码)。
func ParseMACode(maCode string) model.ParsedMA {
p := model.ParsedMA{Raw: maCode}
m := maCodePattern.FindStringSubmatch(strings.TrimSpace(maCode))
if m == nil {
p.Valid = false
return p
}
p.Prefix = m[1]
p.CountryCode = m[2]
p.IndustryNode = m[3]
p.OrgNode = m[4]
p.Category = m[5]
p.Year = m[6]
p.Sequence = m[7]
if m[8] != "" {
if ep, err := strconv.Atoi(m[8]); err == nil {
p.Episode = ep
}
}
p.Valid = true
return p
}
// baseMACode 去除集级子标识 #Exx,返回整剧 MA 码(用于查链/查权益)。
func baseMACode(maCode string) string {
if i := strings.Index(maCode, "#"); i >= 0 {
return maCode[:i]
}
return maCode
}
// Resolve 跨域解析网关(C.1/C.2):同一 MA 码在 IPTV/OTT/APP 统一解析。
// 返回解析结构 + 流通状态 + 跨屏可用性,保证大小屏解析结果一致。
func (s *Service) Resolve(maCode string) model.ResolveResult {
res := model.ResolveResult{MACode: maCode}
parsed := ParseMACode(maCode)
res.Parsed = parsed
if !parsed.Valid {
res.Resolved = false
res.Message = "MA 码结构不合法,无法解析"
return res
}
c, err := s.chain.QueryContent(baseMACode(maCode))
if err != nil {
res.Resolved = false
res.Message = "MA 码未在可信数据空间登记"
return res
}
res.Resolved = true
res.Title = c.Title
res.Status = c.Status
res.Issuer = c.Issuer
res.IssueDate = c.IssueDate
res.InCirculation = c.Status == model.StatusPublished
if res.InCirculation {
// 大小屏融合:流通中的内容在全部屏统一可用,身份一致。
res.Screens = model.AllScreens()
res.Message = "解析成功,内容流通中,大小屏统一身份可用"
} else {
res.Screens = []model.ScreenType{}
res.Message = fmt.Sprintf("解析成功,但内容当前状态为 %s,未在大小屏流通", c.Status)
}
return res
}
// ScanVerify 用户扫码验真(B.2):验证内容 MA 码真伪、合规与流通状态,防盗版。
func (s *Service) ScanVerify(maCode string) model.ScanVerifyResult {
res := model.ScanVerifyResult{MACode: maCode}
parsed := ParseMACode(maCode)
res.Parsed = parsed
if !parsed.Valid {
res.Authentic = false
res.Compliant = false
res.Message = "MA 码结构不合法,疑似伪造"
return res
}
c, err := s.chain.QueryContent(baseMACode(maCode))
if err != nil {
res.Authentic = false
res.Compliant = false
res.Message = "MA 码未登记,疑似盗版或伪造内容"
return res
}
res.Authentic = true // 链上存在且结构合法 → 真码
res.Title = c.Title
res.Status = c.Status
res.Compliant = c.Status == model.StatusPublished
if res.Compliant {
res.Screens = model.AllScreens()
res.Message = "验真通过:正版内容,合规流通中"
} else if c.Status == model.StatusRevoked {
res.Screens = []model.ScreenType{}
res.Message = "真码但内容已下架,请勿观看"
} else {
res.Screens = []model.ScreenType{}
res.Message = fmt.Sprintf("真码但当前状态为 %s,尚未正式流通", c.Status)
}
return res
}
// RecordPurchase 记录用户购买(D.1):用户在某一屏购买内容,记录跨屏权益。
// 校验 MA 码有效、屏类型合法;同一用户对同一 MA 码重复购买不覆盖首次记录。
func (s *Service) RecordPurchase(maCode, userHash string, screen model.ScreenType) (model.PurchaseRecord, error) {
if userHash == "" {
return model.PurchaseRecord{}, fmt.Errorf("service: 缺少用户标识")
}
if !model.ValidScreen(screen) {
return model.PurchaseRecord{}, fmt.Errorf("service: 非法屏类型 %q(仅 iptv/ott/app", screen)
}
base := baseMACode(maCode)
if _, err := s.chain.QueryContent(base); err != nil {
return model.PurchaseRecord{}, fmt.Errorf("service: MA 码无效或未登记: %w", err)
}
s.mu.Lock()
defer s.mu.Unlock()
acct, ok := s.rights[userHash]
if !ok {
acct = &model.UserRights{UserHash: userHash, Purchases: make(map[string]model.PurchaseRecord)}
s.rights[userHash] = acct
}
if rec, exists := acct.Purchases[base]; exists {
// 已购买:跨屏通兑,不重复扣费,返回首次购买记录。
return rec, nil
}
rec := model.PurchaseRecord{
MACode: base, UserHash: userHash, Screen: screen, PurchasedAt: time.Now(),
}
acct.Purchases[base] = rec
return rec, nil
}
// VerifyCrossScreenRights 跨屏权益核验(D.1):任一屏购买即全屏通看,不重复付费。
func (s *Service) VerifyCrossScreenRights(maCode, userHash string, requestScreen model.ScreenType) model.CrossScreenRightsResult {
base := baseMACode(maCode)
res := model.CrossScreenRightsResult{
MACode: base, UserHash: userHash, RequestScreen: requestScreen,
}
if !model.ValidScreen(requestScreen) {
res.Entitled = false
res.Message = fmt.Sprintf("非法屏类型 %q", requestScreen)
return res
}
s.mu.Lock()
acct, ok := s.rights[userHash]
var rec model.PurchaseRecord
var has bool
if ok {
rec, has = acct.Purchases[base]
}
s.mu.Unlock()
if !has {
res.Entitled = false
res.Message = "未购买该内容,需先购买"
return res
}
res.Entitled = true
res.PurchaseScreen = rec.Screen
res.PurchasedAt = rec.PurchasedAt
if rec.Screen == requestScreen {
res.Message = "本屏已购买,可观看"
} else {
res.Message = fmt.Sprintf("已在 %s 屏购买,跨屏通兑至 %s 屏观看,不重复付费", rec.Screen, requestScreen)
}
return res
}