a329d4906b
- 方案文档: AVCC 体系建设、IPTV TCS 需求(0-req)/PRD(1-prd)/任务(2-task)/二三四期任务 - tcs-iptv: Go 后端(哈希SDK/MA码生成/可信数据空间mock/业务编排/HTTP API+HMAC鉴权) - web-console: React+AntD 监管大屏(角色工作台/全流程演示/监管片库) - 一剧一码+集级哈希, 集级下架/恢复, 全栈测试通过
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
// Package macode 实现 MA 码生成服务(模式B:自行发码)。
|
|
// TCS 与 MA 发码机构合作获取「码段(号段)」与「备案规则」,在本地按规则原子发码。
|
|
// 对应需求:需求3(MA码签发)、需求16;与 ISO/IEC 15459 MA 标识体系对齐。
|
|
//
|
|
// MA 码结构(六段式,可由备案规则配置):
|
|
//
|
|
// MA.156.{industryNode}.{orgNode}/{category}/{yyyy}{sequence}
|
|
//
|
|
// 示例:MA.156.8531.4401/WD/20250000123
|
|
// - MA 固定前缀(标识体系根)
|
|
// - 156 国家码(中国)
|
|
// - 8531 行业节点(IPTV 视听内容,由发码机构分配)
|
|
// - 4401 机构节点(运营主体/省局,由发码机构分配)
|
|
// - WD 内容类目(WD=微短剧 / WJ=网络剧 / DY=网络电影 / DH=网络动画)
|
|
// - yyyy 年份
|
|
// - sequence 号段内递增序列(按位补零)
|
|
package macode
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// 错误定义。
|
|
var (
|
|
ErrSegmentExhausted = errors.New("macode: code segment exhausted")
|
|
ErrUnknownCategory = errors.New("macode: unknown content category")
|
|
ErrInvalidSegment = errors.New("macode: invalid segment range")
|
|
)
|
|
|
|
// 内容类目码。
|
|
const (
|
|
CategoryMicroDrama = "WD" // 网络微短剧
|
|
CategoryWebSeries = "WJ" // 网络剧
|
|
CategoryWebMovie = "DY" // 网络电影
|
|
CategoryAnimation = "DH" // 网络动画
|
|
)
|
|
|
|
// Segment 是 MA 发码机构分配给本运营主体的「码段(号段)」。
|
|
// 同一 (industryNode, orgNode, category) 下,序列在 [Start, End] 内递增分配。
|
|
type Segment struct {
|
|
IndustryNode string // 行业节点(发码机构分配)
|
|
OrgNode string // 机构节点(发码机构分配)
|
|
Category string // 适用内容类目
|
|
Start uint64 // 号段起始序列(含)
|
|
End uint64 // 号段结束序列(含)
|
|
SeqWidth int // 序列补零宽度,如 7 → 0000123
|
|
}
|
|
|
|
// Validate 校验号段合法性。
|
|
func (s Segment) Validate() error {
|
|
if s.IndustryNode == "" || s.OrgNode == "" || s.Category == "" {
|
|
return ErrInvalidSegment
|
|
}
|
|
if s.End < s.Start {
|
|
return ErrInvalidSegment
|
|
}
|
|
if s.SeqWidth <= 0 {
|
|
s.SeqWidth = 7
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AllocationStore 持久化号段游标,保证重启后不重号、并发下原子分配。
|
|
// MVP 提供内存实现;生产可用 PostgreSQL 行锁 / Redis INCR 实现。
|
|
type AllocationStore interface {
|
|
// Next 原子取得指定号段键的下一个序列值;超出 end 返回 ErrSegmentExhausted。
|
|
Next(segmentKey string, start, end uint64) (uint64, error)
|
|
}
|
|
|
|
// Generator MA 码生成器。
|
|
type Generator struct {
|
|
mu sync.RWMutex
|
|
segments map[string]Segment // category -> segment
|
|
store AllocationStore
|
|
clock func() time.Time
|
|
}
|
|
|
|
// NewGenerator 创建生成器。
|
|
func NewGenerator(store AllocationStore) *Generator {
|
|
return &Generator{
|
|
segments: make(map[string]Segment),
|
|
store: store,
|
|
clock: time.Now,
|
|
}
|
|
}
|
|
|
|
// RegisterSegment 登记一个码段(通常在与发码机构对接后配置)。
|
|
func (g *Generator) RegisterSegment(seg Segment) error {
|
|
if err := seg.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if seg.SeqWidth <= 0 {
|
|
seg.SeqWidth = 7
|
|
}
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
g.segments[seg.Category] = seg
|
|
return nil
|
|
}
|
|
|
|
// segmentKey 唯一标识一个号段(用于持久化游标)。
|
|
func segmentKey(s Segment) string {
|
|
return fmt.Sprintf("%s:%s:%s", s.IndustryNode, s.OrgNode, s.Category)
|
|
}
|
|
|
|
// Issued 一次发码结果。
|
|
type Issued struct {
|
|
MACode string `json:"ma_code"`
|
|
IndustryNode string `json:"industry_node"`
|
|
OrgNode string `json:"org_node"`
|
|
Category string `json:"category"`
|
|
Sequence uint64 `json:"sequence"`
|
|
Year int `json:"year"`
|
|
}
|
|
|
|
// Allocate 为指定类目原子分配并格式化一个全局唯一 MA 码。
|
|
func (g *Generator) Allocate(category string) (Issued, error) {
|
|
g.mu.RLock()
|
|
seg, ok := g.segments[category]
|
|
g.mu.RUnlock()
|
|
if !ok {
|
|
return Issued{}, ErrUnknownCategory
|
|
}
|
|
|
|
seq, err := g.store.Next(segmentKey(seg), seg.Start, seg.End)
|
|
if err != nil {
|
|
return Issued{}, err
|
|
}
|
|
|
|
year := g.clock().Year()
|
|
code := Format(seg, year, seq)
|
|
return Issued{
|
|
MACode: code,
|
|
IndustryNode: seg.IndustryNode,
|
|
OrgNode: seg.OrgNode,
|
|
Category: category,
|
|
Sequence: seq,
|
|
Year: year,
|
|
}, nil
|
|
}
|
|
|
|
// Format 按备案规则拼装 MA 码。
|
|
func Format(seg Segment, year int, seq uint64) string {
|
|
w := seg.SeqWidth
|
|
if w <= 0 {
|
|
w = 7
|
|
}
|
|
return fmt.Sprintf("MA.156.%s.%s/%s/%d%0*d",
|
|
seg.IndustryNode, seg.OrgNode, seg.Category, year, w, seq)
|
|
}
|