feat(phase3): 备案对接/全国统计/号段管理/BFF安全化/链合约源码
- A.1 备案对接: BindFiling/QueryFiling 关联网标号+备案号 - A.2 监管上报: DailyRegulatoryReport 日报 - B.1 号段管理: ListSegments + /admin/segments - C.1/C.2 全国统计按省聚合 + 跨省协同(单一可信源天然联动) - F.2 全国监管大屏: NationalStats(按省/类目/状态) - B(遗留) 监管大屏BFF: internal/bff + cmd/console-bff, 密钥仅存后端浏览器只用会话令牌 - G 真实链合约源码: contracts/tcs_registry/registry.go (ChainMaker Go) - 新增9个API+BFF服务; 5项新测试; 端到端BFF验证 - D/E(压测/等保/HSM)/F.1(标准)/真实链部署 标注需外部环境
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tcs-iptv/tcs/internal/macode"
|
||||
"github.com/tcs-iptv/tcs/internal/model"
|
||||
)
|
||||
|
||||
// 三期:备案对接、全国统计、号段管理、监管上报。
|
||||
|
||||
// 机构节点 → 省份映射(与发码机构号段分配一致;示例覆盖部分省)。
|
||||
var orgNodeProvince = map[string]string{
|
||||
"6101": "陕西", "4401": "广东", "3301": "浙江", "3201": "江苏",
|
||||
"1101": "北京", "3101": "上海", "5101": "四川", "4201": "湖北",
|
||||
}
|
||||
|
||||
// BindFiling 关联备案号/网标号至 MA 码(三期 A.1,对接广电总局备案系统)。
|
||||
func (s *Service) BindFiling(maCode, licenseNo, filingNo string) (model.FilingRecord, error) {
|
||||
if _, err := s.chain.QueryContent(maCode); err != nil {
|
||||
return model.FilingRecord{}, err
|
||||
}
|
||||
rec := model.FilingRecord{
|
||||
MACode: maCode, LicenseNo: licenseNo, FilingNo: filingNo,
|
||||
BoundAt: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.filings[maCode] = rec
|
||||
s.mu.Unlock()
|
||||
s.prov.Record(model.ProvenanceEvent{
|
||||
MACode: maCode, Node: model.NodeIssue, Operator: "广电总局备案系统",
|
||||
Detail: "关联网标号 " + licenseNo + " / 备案号 " + filingNo,
|
||||
})
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
// QueryFiling 查询备案关联。
|
||||
func (s *Service) QueryFiling(maCode string) (model.FilingRecord, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
r, ok := s.filings[maCode]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
// NationalStats 全国监管统计(三期 A.2/F.2)。
|
||||
func (s *Service) NationalStats() (model.NationalStats, error) {
|
||||
all, err := s.chain.ListContents("")
|
||||
if err != nil {
|
||||
return model.NationalStats{}, err
|
||||
}
|
||||
st := model.NationalStats{
|
||||
ByStatus: map[string]int{},
|
||||
ByCategory: map[string]int{},
|
||||
ByProvince: map[string]model.ProvinceStat{},
|
||||
}
|
||||
st.TotalContents = len(all)
|
||||
for _, c := range all {
|
||||
st.ByStatus[c.Status]++
|
||||
p, perr := macode.Parse(c.MACode)
|
||||
if perr == nil {
|
||||
st.ByCategory[p.Category]++
|
||||
prov := orgNodeProvince[p.OrgNode]
|
||||
if prov == "" {
|
||||
prov = "其他(" + p.OrgNode + ")"
|
||||
}
|
||||
ps := st.ByProvince[prov]
|
||||
ps.Province = prov
|
||||
ps.OrgNode = p.OrgNode
|
||||
ps.Total++
|
||||
switch c.Status {
|
||||
case model.StatusPublished:
|
||||
ps.Published++
|
||||
case model.StatusRevoked:
|
||||
ps.Revoked++
|
||||
}
|
||||
st.ByProvince[prov] = ps
|
||||
}
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// ListSegments 列出已登记号段及使用情况(三期 B.1)。
|
||||
func (s *Service) ListSegments() []macode.SegmentInfo {
|
||||
return s.gen.Segments()
|
||||
}
|
||||
|
||||
// RegisterSegment 登记新号段(三期 B.1,与发码机构对接后配置)。
|
||||
func (s *Service) RegisterSegment(seg macode.Segment) error {
|
||||
return s.gen.RegisterSegment(seg)
|
||||
}
|
||||
|
||||
// DailyRegulatoryReport 生成监管数据上报日报(三期 A.2)。
|
||||
func (s *Service) DailyRegulatoryReport(date string) (model.RegulatoryReport, error) {
|
||||
all, err := s.chain.ListContents("")
|
||||
if err != nil {
|
||||
return model.RegulatoryReport{}, err
|
||||
}
|
||||
rep := model.RegulatoryReport{
|
||||
ReportType: "daily_summary", Date: date,
|
||||
LevelDist: map[string]int{},
|
||||
}
|
||||
for _, c := range all {
|
||||
rep.TotalNew++
|
||||
if p, perr := macode.Parse(c.MACode); perr == nil {
|
||||
rep.LevelDist[p.Category]++
|
||||
}
|
||||
if c.Status == model.StatusRevoked {
|
||||
rep.RevokedCnt++
|
||||
}
|
||||
}
|
||||
s.mu.Lock()
|
||||
rep.BlacklistCnt = len(s.black)
|
||||
s.mu.Unlock()
|
||||
return rep, nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tcs-iptv/tcs/internal/chain"
|
||||
"github.com/tcs-iptv/tcs/internal/macode"
|
||||
)
|
||||
|
||||
func newServiceSX(t *testing.T) *Service {
|
||||
t.Helper()
|
||||
gen := macode.NewGenerator(macode.NewMemoryStore())
|
||||
require.NoError(t, gen.RegisterSegment(macode.Segment{
|
||||
IndustryNode: "8531", OrgNode: "6101", // 陕西
|
||||
Category: macode.CategoryMicroDrama, Start: 1, End: 100, SeqWidth: 7,
|
||||
}))
|
||||
return New(chain.NewMemoryChain(), gen)
|
||||
}
|
||||
|
||||
func issueSX(t *testing.T, s *Service) string {
|
||||
t.Helper()
|
||||
sub := sampleSub()
|
||||
r, _ := s.SubmitForReview(sub)
|
||||
require.NoError(t, s.ReviewCSPS(r.ReviewID, true, "rv"))
|
||||
iss, err := s.ApproveAndIssue(chain.RoleRegulator, r.ReviewID, "陕西IPTV运营公司")
|
||||
require.NoError(t, err)
|
||||
return iss.MACode
|
||||
}
|
||||
|
||||
func TestBindFiling(t *testing.T) {
|
||||
s := newServiceSX(t)
|
||||
ma := issueSX(t, s)
|
||||
rec, err := s.BindFiling(ma, "(陕)网微剧审字(2026)第001号", "备案2026001")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "(陕)网微剧审字(2026)第001号", rec.LicenseNo)
|
||||
|
||||
got, ok := s.QueryFiling(ma)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "备案2026001", got.FilingNo)
|
||||
}
|
||||
|
||||
func TestNationalStats(t *testing.T) {
|
||||
s := newServiceSX(t)
|
||||
ma1 := issueSX(t, s)
|
||||
sub2 := sampleSub()
|
||||
sub2.FileHash = "fh2"
|
||||
sub2.MerkleRoot = "mr2"
|
||||
r2, _ := s.SubmitForReview(sub2)
|
||||
require.NoError(t, s.ReviewCSPS(r2.ReviewID, true, "rv"))
|
||||
_, _ = s.ApproveAndIssue(chain.RoleRegulator, r2.ReviewID, "陕西IPTV")
|
||||
_, _ = s.Takedown(chain.RoleRegulator, ma1, "违规")
|
||||
|
||||
st, err := s.NationalStats()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, st.TotalContents)
|
||||
assert.Equal(t, 1, st.ByStatus["revoked"])
|
||||
// 陕西省统计
|
||||
sx := st.ByProvince["陕西"]
|
||||
assert.Equal(t, "6101", sx.OrgNode)
|
||||
assert.Equal(t, 2, sx.Total)
|
||||
assert.Equal(t, 1, sx.Revoked)
|
||||
// 类目统计
|
||||
assert.Equal(t, 2, st.ByCategory["WD"])
|
||||
}
|
||||
|
||||
func TestListSegments(t *testing.T) {
|
||||
s := newServiceSX(t)
|
||||
segs := s.ListSegments()
|
||||
require.NotEmpty(t, segs)
|
||||
assert.Equal(t, "6101", segs[0].OrgNode)
|
||||
assert.Equal(t, uint64(100), segs[0].Capacity)
|
||||
}
|
||||
|
||||
func TestDailyRegulatoryReport(t *testing.T) {
|
||||
s := newServiceSX(t)
|
||||
_ = issueSX(t, s)
|
||||
rep, err := s.DailyRegulatoryReport("2026-06-14")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, rep.TotalNew)
|
||||
assert.Equal(t, 1, rep.LevelDist["WD"])
|
||||
}
|
||||
@@ -49,16 +49,17 @@ type SubmissionResult struct {
|
||||
|
||||
// Service 业务编排器。
|
||||
type Service struct {
|
||||
chain chain.Client
|
||||
gen *macode.Generator
|
||||
pb *playback.Store
|
||||
prov *provenance.Store
|
||||
phash map[string]phashEntry // maCode -> 感知哈希条目(确权侵权比对)
|
||||
auths map[string]model.Authorization // maCode -> 授权(F22)
|
||||
black map[string]bool // maCode -> 黑名单(跨省复用校验)
|
||||
mu sync.Mutex
|
||||
seqMu sync.Mutex
|
||||
seqs map[string]int // 按前缀独立计数(REV/ctid/DIST 各自从 1 递增)
|
||||
chain chain.Client
|
||||
gen *macode.Generator
|
||||
pb *playback.Store
|
||||
prov *provenance.Store
|
||||
phash map[string]phashEntry // maCode -> 感知哈希条目(确权侵权比对)
|
||||
auths map[string]model.Authorization // maCode -> 授权(F22)
|
||||
black map[string]bool // maCode -> 黑名单(跨省复用校验)
|
||||
filings map[string]model.FilingRecord // maCode -> 备案/网标关联(三期 A.1)
|
||||
mu sync.Mutex
|
||||
seqMu sync.Mutex
|
||||
seqs map[string]int // 按前缀独立计数(REV/ctid/DIST 各自从 1 递增)
|
||||
// reviewStore 暂存送审申报(MVP 内存;生产落 PG)
|
||||
reviews map[string]*reviewItem
|
||||
}
|
||||
@@ -80,12 +81,13 @@ type phashEntry struct {
|
||||
func New(c chain.Client, gen *macode.Generator) *Service {
|
||||
return &Service{
|
||||
chain: c, gen: gen,
|
||||
pb: playback.NewStore(),
|
||||
prov: provenance.NewStore(),
|
||||
phash: make(map[string]phashEntry),
|
||||
auths: make(map[string]model.Authorization),
|
||||
black: make(map[string]bool),
|
||||
seqs: make(map[string]int), reviews: make(map[string]*reviewItem),
|
||||
pb: playback.NewStore(),
|
||||
prov: provenance.NewStore(),
|
||||
phash: make(map[string]phashEntry),
|
||||
auths: make(map[string]model.Authorization),
|
||||
black: make(map[string]bool),
|
||||
filings: make(map[string]model.FilingRecord),
|
||||
seqs: make(map[string]int), reviews: make(map[string]*reviewItem),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user