feat: 方案100%匹配 — RBAC动态权限+Grafana监控+目录库独立微服务+全国目录中心+标识同步

This commit is contained in:
selfrelease
2026-06-30 13:42:11 +08:00
parent 96e2393c50
commit 29a6b57533
40 changed files with 4159 additions and 7 deletions
+86
View File
@@ -0,0 +1,86 @@
// Package catalog 目录库服务(方案第一阶段 — 目录库独立查询层)。
//
// 方案定位:目录库负责存储 MA、Hash、片库文件 ID、省内内容编码等映射关系,
// 为标识查询接口提供统一入口。本包在 chain.Client 之上封装独立查询服务,
// 支持按 MA 码、Hash、省级编码、片库文件 ID 四种维度查询标识信息。
//
// 架构说明:
// - MVP 阶段委托 chain.Client 获取数据(零数据冗余,单一真相源)
// - 生产阶段可替换为独立 PostgreSQL 存储(通过 CatalogStore 接口注入)
// - 对外接口不变,业务层零改动
package catalog
import (
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/model"
)
// Catalog 目录库查询服务。
// 提供四种维度的标识信息查询:MA码、Hash、省级编码、片库文件ID。
type Catalog struct {
client chain.Client
store CatalogStore // 可选独立存储(为 nil 时委托 chain.Client
}
// CatalogStore 目录库独立存储接口(生产阶段可替换为 PG 实现)。
type CatalogStore interface {
FindByHash(fileHash string) (model.ContentQueryResult, error)
FindByProvincialCode(code string) (model.ContentQueryResult, error)
FindByLibraryFileID(libraryID string) (model.ContentQueryResult, error)
}
// New 创建目录库服务(委托 chain.Client)。
func New(c chain.Client) *Catalog {
return &Catalog{client: c}
}
// NewWithStore 创建目录库服务(使用独立存储)。
func NewWithStore(c chain.Client, s CatalogStore) *Catalog {
return &Catalog{client: c, store: s}
}
// QueryByMA 根据MA码查询标识信息及映射关系。
func (cat *Catalog) QueryByMA(maCode string) (model.ContentQueryResult, error) {
c, err := cat.client.QueryContent(maCode)
if err != nil {
return model.ContentQueryResult{Found: false}, err
}
mr, _ := cat.client.QueryMappings(maCode)
eps, _ := cat.client.ListEpisodes(maCode)
bindings := eps
return model.ContentQueryResult{
Found: true,
Content: c,
Bindings: bindings,
Mappings: mr.Mappings,
}, nil
}
// QueryByHash 根据内容哈希反查标识信息。
func (cat *Catalog) QueryByHash(fileHash string) (model.ContentQueryResult, error) {
if cat.store != nil {
return cat.store.FindByHash(fileHash)
}
return cat.client.QueryByHash(fileHash)
}
// QueryByProvincialCode 根据省级内容编码反查标识信息。
func (cat *Catalog) QueryByProvincialCode(code string) (model.ContentQueryResult, error) {
if cat.store != nil {
return cat.store.FindByProvincialCode(code)
}
return cat.client.QueryByProvincialCode(code)
}
// QueryByLibraryFileID 根据片库文件 ID 反查标识信息。
func (cat *Catalog) QueryByLibraryFileID(libraryID string) (model.ContentQueryResult, error) {
if cat.store != nil {
return cat.store.FindByLibraryFileID(libraryID)
}
return cat.client.QueryByLibraryFileID(libraryID)
}
// QueryAll 批量查询:按 MA 码返回全部关联信息(含绑定、映射、集级哈希)。
func (cat *Catalog) QueryAll(maCode string) (model.ContentQueryResult, error) {
return cat.QueryByMA(maCode)
}
+99
View File
@@ -0,0 +1,99 @@
package catalog
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/model"
)
func TestCatalog_QueryByMA(t *testing.T) {
c := chain.NewMemoryChain()
cat := New(c)
// 发码
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-cat-001",
FileHash: "fh-cat-001", MerkleRoot: "mr-cat-001",
Content: model.Content{Title: "目录库测试剧", EpisodeCount: 2, MAType: "WD", Issuer: "测试局"},
})
require.NoError(t, err)
// 按 MA 码查询
res, err := cat.QueryByMA("MA.156.8531.6101/WD/20260000001")
require.NoError(t, err)
assert.True(t, res.Found)
assert.Equal(t, "目录库测试剧", res.Content.Title)
}
func TestCatalog_QueryByHash(t *testing.T) {
c := chain.NewMemoryChain()
cat := New(c)
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-cat-002",
FileHash: "fh-cat-002", MerkleRoot: "mr-cat-002",
Content: model.Content{Title: "Hash查询测试", MAType: "WD", Issuer: "测试局"},
})
require.NoError(t, err)
// 按 Hash 查询
res, err := cat.QueryByHash("fh-cat-002")
require.NoError(t, err)
assert.True(t, res.Found)
assert.Equal(t, "MA.156.8531.6101/WD/20260000002", res.Content.MACode)
// 不存在的 Hash
_, err = cat.QueryByHash("nonexistent")
assert.Error(t, err)
}
func TestCatalog_QueryByProvincialCode(t *testing.T) {
c := chain.NewMemoryChain()
cat := New(c)
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000003", ContentTwinID: "ctid-cat-003",
FileHash: "fh-cat-003", MerkleRoot: "mr-cat-003",
Content: model.Content{Title: "省级编码查询测试", MAType: "WD", Issuer: "测试局"},
})
require.NoError(t, err)
// 注册 CP 映射
_, err = c.RegisterMapping(chain.RoleCP, model.Mapping{
ContentTwinID: "ctid-cat-003", Party: model.PartyCP, PartyID: "PROV-CAT-001", PartyName: "测试CP",
})
require.NoError(t, err)
// 按省级编码查询
res, err := cat.QueryByProvincialCode("PROV-CAT-001")
require.NoError(t, err)
assert.True(t, res.Found)
assert.Equal(t, "MA.156.8531.6101/WD/20260000003", res.Content.MACode)
}
func TestCatalog_QueryByLibraryFileID(t *testing.T) {
c := chain.NewMemoryChain()
cat := New(c)
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000004", ContentTwinID: "ctid-cat-004",
FileHash: "fh-cat-004", MerkleRoot: "mr-cat-004",
Content: model.Content{Title: "片库ID查询测试", MAType: "WD", Issuer: "测试局"},
})
require.NoError(t, err)
// 注册媒资库映射
_, err = c.RegisterMapping(chain.RoleReviewer, model.Mapping{
ContentTwinID: "ctid-cat-004", Party: model.PartyReviewer, PartyID: "LIB-CAT-001", PartyName: "测试媒资库",
})
require.NoError(t, err)
// 按片库文件 ID 查询
res, err := cat.QueryByLibraryFileID("LIB-CAT-001")
require.NoError(t, err)
assert.True(t, res.Found)
assert.Equal(t, "MA.156.8531.6101/WD/20260000004", res.Content.MACode)
}