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
+10
View File
@@ -92,4 +92,14 @@ type Client interface {
RestoreEpisode(role Role, maCode string, episode int) error
// SetContentStatus 更新内容状态。
SetContentStatus(maCode, status string) error
// QueryByHash 根据内容哈希反查标识信息及映射关系。
QueryByHash(fileHash string) (model.ContentQueryResult, error)
// QueryByProvincialCode 根据省级内容编码(CP MediaID)反查标识信息。
QueryByProvincialCode(provincialCode string) (model.ContentQueryResult, error)
// QueryByLibraryFileID 根据片库文件 ID(媒资库 ID)反查标识信息。
QueryByLibraryFileID(libraryFileID string) (model.ContentQueryResult, error)
// MergeMA 将多个 MA 码合并为一个主 MA 码(仅监管主体)。
MergeMA(role Role, req model.MergeRequest) (model.MergeResult, error)
// SplitMA 将一个 MA 码拆分为多个独立 MA 码(仅监管主体)。
SplitMA(role Role, req model.SplitRequest) (model.SplitResult, error)
}
+64
View File
@@ -292,3 +292,67 @@ func (c *ChainMakerClient) QueryMappings(maCode string) (MappingsResult, error)
out.MACode = maCode
return out, nil
}
// ---- 多维度查询与 MA 合并/拆分 ----
func (c *ChainMakerClient) QueryByHash(fileHash string) (model.ContentQueryResult, error) {
res, err := c.query(RoleRegulator, "QueryByHash", map[string][]byte{"file_hash": []byte(fileHash)})
if err != nil {
return model.ContentQueryResult{Found: false}, err
}
var out model.ContentQueryResult
if err := json.Unmarshal(res, &out); err != nil {
return model.ContentQueryResult{Found: false}, err
}
return out, nil
}
func (c *ChainMakerClient) QueryByProvincialCode(provincialCode string) (model.ContentQueryResult, error) {
res, err := c.query(RoleRegulator, "QueryByProvincialCode", map[string][]byte{"provincial_code": []byte(provincialCode)})
if err != nil {
return model.ContentQueryResult{Found: false}, err
}
var out model.ContentQueryResult
if err := json.Unmarshal(res, &out); err != nil {
return model.ContentQueryResult{Found: false}, err
}
return out, nil
}
func (c *ChainMakerClient) QueryByLibraryFileID(libraryFileID string) (model.ContentQueryResult, error) {
res, err := c.query(RoleRegulator, "QueryByLibraryFileID", map[string][]byte{"library_file_id": []byte(libraryFileID)})
if err != nil {
return model.ContentQueryResult{Found: false}, err
}
var out model.ContentQueryResult
if err := json.Unmarshal(res, &out); err != nil {
return model.ContentQueryResult{Found: false}, err
}
return out, nil
}
func (c *ChainMakerClient) MergeMA(role Role, req model.MergeRequest) (model.MergeResult, error) {
reqJSON, _ := json.Marshal(req)
resp, err := c.invoke(role, "MergeMA", map[string][]byte{"request": reqJSON})
if err != nil {
return model.MergeResult{}, err
}
var out model.MergeResult
if err := json.Unmarshal(resp.ContractResult.Result, &out); err != nil {
return model.MergeResult{}, err
}
return out, nil
}
func (c *ChainMakerClient) SplitMA(role Role, req model.SplitRequest) (model.SplitResult, error) {
reqJSON, _ := json.Marshal(req)
resp, err := c.invoke(role, "SplitMA", map[string][]byte{"request": reqJSON})
if err != nil {
return model.SplitResult{}, err
}
var out model.SplitResult
if err := json.Unmarshal(resp.ContractResult.Result, &out); err != nil {
return model.SplitResult{}, err
}
return out, nil
}
+108
View File
@@ -150,6 +150,114 @@ func RunClientConformance(t *testing.T, newClient func(t *testing.T) Client) {
require.NoError(t, err)
assert.Len(t, pub, 1)
})
t.Run("多维度查询_按Hash反查", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
// 注册 CP 映射(省级内容编码)
_, err = c.RegisterMapping(RoleCP, model.Mapping{
ContentTwinID: ctid, Party: model.PartyCP, PartyID: "PROV-001", PartyName: "陕西CP",
})
require.NoError(t, err)
// 注册媒资库映射(片库文件 ID
_, err = c.RegisterMapping(RoleReviewer, model.Mapping{
ContentTwinID: ctid, Party: model.PartyReviewer, PartyID: "LIB-001", PartyName: "陕西媒资库",
})
require.NoError(t, err)
// 按 Hash 查询
res, err := c.QueryByHash(fh)
require.NoError(t, err)
assert.True(t, res.Found)
assert.Equal(t, ma, res.Content.MACode)
assert.Equal(t, "契约测试剧", res.Content.Title)
// 按省级内容编码查询
res2, err := c.QueryByProvincialCode("PROV-001")
require.NoError(t, err)
assert.True(t, res2.Found)
assert.Equal(t, ma, res2.Content.MACode)
// 按片库文件 ID 查询
res3, err := c.QueryByLibraryFileID("LIB-001")
require.NoError(t, err)
assert.True(t, res3.Found)
assert.Equal(t, ma, res3.Content.MACode)
// 不存在的 Hash
_, err = c.QueryByHash("nonexistent")
assert.ErrorIs(t, err, ErrNotFound)
})
t.Run("MA合并_仅监管且迁移绑定", func(t *testing.T) {
c := newClient(t)
// 发码两个 MA
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
ma2 := "MA.156.8531.6101/WD/20260000002"
ctid2 := "ctid-conf-002"
fh2 := "fh-conf-002"
_, err = c.IssueMA(RoleRegulator, issueReq(ma2, ctid2, fh2))
require.NoError(t, err)
// 非监管不可合并
_, err = c.MergeMA(RoleCP, model.MergeRequest{
PrimaryMACode: ma, SecondaryMACodes: []string{ma2}, Reason: "重复发码",
})
assert.ErrorIs(t, err, ErrPermissionDenied)
// 监管合并
res, err := c.MergeMA(RoleRegulator, model.MergeRequest{
PrimaryMACode: ma, SecondaryMACodes: []string{ma2}, Reason: "重复发码", Operator: "监管局",
})
require.NoError(t, err)
assert.Equal(t, ma, res.PrimaryMACode)
assert.Contains(t, res.MergedMACodes, ma2)
assert.Greater(t, res.MigratedBindings, 0)
// 被合并的 MA 状态为 merged
got, _ := c.QueryContent(ma2)
assert.Equal(t, model.StatusMerged, got.Status)
// 按 fh2 查询应指向主 MA 码
qr, err := c.QueryByHash(fh2)
require.NoError(t, err)
assert.Equal(t, ma, qr.Content.MACode)
})
t.Run("MA拆分_仅监管且按集迁移", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
// 非监管不可拆分
_, err = c.SplitMA(RoleCP, model.SplitRequest{
SourceMACode: ma, Splits: []model.SplitTarget{{NewMACode: "MA.156.8531.6101/WD/20260000010", Title: "拆分剧A", Episodes: []int{1, 2}}},
})
assert.ErrorIs(t, err, ErrPermissionDenied)
// 监管拆分
newMA := "MA.156.8531.6101/WD/20260000011"
res, err := c.SplitMA(RoleRegulator, model.SplitRequest{
SourceMACode: ma,
Splits: []model.SplitTarget{{NewMACode: newMA, Title: "拆分剧A", Episodes: []int{1, 2}}},
Reason: "合集拆分",
Operator: "监管局",
})
require.NoError(t, err)
assert.Contains(t, res.NewMACodes, newMA)
assert.Greater(t, res.MigratedBindings, 0)
// 源 MA 状态为 split
got, _ := c.QueryContent(ma)
assert.Equal(t, model.StatusSplit, got.Status)
// 新 MA 可查询
newContent, err := c.QueryContent(newMA)
require.NoError(t, err)
assert.Equal(t, "拆分剧A", newContent.Title)
})
}
// TestMemoryChain_Conformance 让内存实现跑契约套件(始终运行)。
+200
View File
@@ -393,4 +393,204 @@ func (m *MemoryChain) maCodeByCTID(ctid string) string {
return ""
}
// QueryByHash 根据内容哈希反查标识信息及映射关系。
func (m *MemoryChain) QueryByHash(fileHash string) (model.ContentQueryResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
maCode, ok := m.hashIndex[fileHash]
if !ok {
return model.ContentQueryResult{Found: false}, ErrNotFound
}
return model.ContentQueryResult{
Found: true,
Content: m.contents[maCode],
Bindings: m.bindings[maCode],
Mappings: m.mappings[maCode],
}, nil
}
// QueryByProvincialCode 根据省级内容编码(CP MediaID)反查标识信息。
func (m *MemoryChain) QueryByProvincialCode(provincialCode string) (model.ContentQueryResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
for maCode, maps := range m.mappings {
for _, mp := range maps {
if mp.Party == model.PartyCP && mp.PartyID == provincialCode {
return model.ContentQueryResult{
Found: true,
Content: m.contents[maCode],
Bindings: m.bindings[maCode],
Mappings: maps,
}, nil
}
}
}
return model.ContentQueryResult{Found: false}, ErrNotFound
}
// QueryByLibraryFileID 根据片库文件 ID(媒资库 ID)反查标识信息。
func (m *MemoryChain) QueryByLibraryFileID(libraryFileID string) (model.ContentQueryResult, error) {
m.mu.RLock()
defer m.mu.RUnlock()
for maCode, maps := range m.mappings {
for _, mp := range maps {
if mp.Party == model.PartyReviewer && mp.PartyID == libraryFileID {
return model.ContentQueryResult{
Found: true,
Content: m.contents[maCode],
Bindings: m.bindings[maCode],
Mappings: maps,
}, nil
}
}
}
return model.ContentQueryResult{Found: false}, ErrNotFound
}
// MergeMA 将多个 MA 码合并为一个主 MA 码(仅监管主体)。
// 被合并的 MA 码的哈希绑定和映射迁移至主 MA 码,原 MA 码状态标记为 merged。
func (m *MemoryChain) MergeMA(role Role, req model.MergeRequest) (model.MergeResult, error) {
if role != RoleRegulator {
return model.MergeResult{}, ErrPermissionDenied
}
m.mu.Lock()
defer m.mu.Unlock()
primary, ok := m.contents[req.PrimaryMACode]
if !ok {
return model.MergeResult{}, ErrNotFound
}
_ = primary
migratedBindings := 0
migratedMappings := 0
for _, secMA := range req.SecondaryMACodes {
secContent, exists := m.contents[secMA]
if !exists {
return model.MergeResult{}, fmt.Errorf("%w: secondary MA %s not found", ErrNotFound, secMA)
}
if secContent.Status == model.StatusMerged {
return model.MergeResult{}, fmt.Errorf("chain: MA %s already merged", secMA)
}
// 迁移哈希绑定至主 MA 码
for _, b := range m.bindings[secMA] {
b.ContentTwinID = primary.ContentTwinID
m.bindings[req.PrimaryMACode] = append(m.bindings[req.PrimaryMACode], b)
migratedBindings++
}
// 迁移映射至主 MA 码
for _, mp := range m.mappings[secMA] {
mp.ContentTwinID = primary.ContentTwinID
m.mappings[req.PrimaryMACode] = append(m.mappings[req.PrimaryMACode], mp)
migratedMappings++
}
// 更新哈希索引指向主 MA 码
for _, b := range m.bindings[secMA] {
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
m.hashIndex[b.HashValue] = req.PrimaryMACode
}
}
// 清空被合并 MA 码的绑定和映射,状态标记为 merged
m.bindings[secMA] = nil
m.mappings[secMA] = nil
secContent.Status = model.StatusMerged
m.contents[secMA] = secContent
}
txID := m.nextTx("mergeMA")
return model.MergeResult{
PrimaryMACode: req.PrimaryMACode,
MergedMACodes: req.SecondaryMACodes,
MigratedBindings: migratedBindings,
MigratedMappings: migratedMappings,
TxID: txID,
}, nil
}
// SplitMA 将一个 MA 码拆分为多个独立 MA 码(仅监管主体)。
// 按集号将哈希绑定和映射迁移至新 MA 码,源 MA 码状态标记为 split。
func (m *MemoryChain) SplitMA(role Role, req model.SplitRequest) (model.SplitResult, error) {
if role != RoleRegulator {
return model.SplitResult{}, ErrPermissionDenied
}
m.mu.Lock()
defer m.mu.Unlock()
srcContent, ok := m.contents[req.SourceMACode]
if !ok {
return model.SplitResult{}, ErrNotFound
}
if srcContent.Status == model.StatusMerged || srcContent.Status == model.StatusSplit {
return model.SplitResult{}, fmt.Errorf("chain: MA %s already %s", req.SourceMACode, srcContent.Status)
}
migratedBindings := 0
migratedMappings := 0
newMACodes := make([]string, 0, len(req.Splits))
for _, split := range req.Splits {
// 创建新内容记录
newContent := srcContent
newContent.MACode = split.NewMACode
newContent.ContentTwinID = split.NewMACode + "-ctid"
newContent.Title = split.Title
newContent.Status = model.StatusApproved
newContent.CreatedAt = time.Now()
m.contents[split.NewMACode] = newContent
// 按集号迁移哈希绑定
for _, b := range m.bindings[req.SourceMACode] {
shouldMigrate := false
if len(split.Episodes) == 0 {
// 空集号列表:迁移全部(整剧拆分场景)
shouldMigrate = true
} else {
for _, ep := range split.Episodes {
if b.Episode == ep {
shouldMigrate = true
break
}
}
}
if shouldMigrate {
newB := b
newB.ContentTwinID = newContent.ContentTwinID
m.bindings[split.NewMACode] = append(m.bindings[split.NewMACode], newB)
// 更新哈希索引
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
m.hashIndex[b.HashValue] = split.NewMACode
}
migratedBindings++
}
}
// 迁移映射(全部映射复制到新 MA 码)
for _, mp := range m.mappings[req.SourceMACode] {
newMP := mp
newMP.ContentTwinID = newContent.ContentTwinID
m.mappings[split.NewMACode] = append(m.mappings[split.NewMACode], newMP)
migratedMappings++
}
newMACodes = append(newMACodes, split.NewMACode)
}
// 源 MA 码状态标记为 split
srcContent.Status = model.StatusSplit
m.contents[req.SourceMACode] = srcContent
txID := m.nextTx("splitMA")
return model.SplitResult{
SourceMACode: req.SourceMACode,
NewMACodes: newMACodes,
MigratedBindings: migratedBindings,
MigratedMappings: migratedMappings,
TxID: txID,
}, nil
}
var _ Client = (*MemoryChain)(nil)
+51
View File
@@ -131,6 +131,47 @@ func (p *PersistentChain) SetContentStatus(maCode, status string) error {
return nil
}
// MergeMA 合并 MA 码:内存合并成功后写穿 PG 镜像。
func (p *PersistentChain) MergeMA(role Role, req model.MergeRequest) (model.MergeResult, error) {
res, err := p.MemoryChain.MergeMA(role, req)
if err != nil {
return res, err
}
// 被合并的 MA 码状态标记为 merged
for _, secMA := range req.SecondaryMACodes {
p.updateStatus(secMA, model.StatusMerged)
}
// 主 MA 码的绑定和映射已在内存中追加,写穿最新状态
for _, b := range p.snapshotBindings(req.PrimaryMACode) {
p.persistBinding(b)
}
p.persistTx(req.PrimaryMACode, res.TxID, "mergeMA")
return res, nil
}
// SplitMA 拆分 MA 码:内存拆分成功后写穿 PG 镜像。
func (p *PersistentChain) SplitMA(role Role, req model.SplitRequest) (model.SplitResult, error) {
res, err := p.MemoryChain.SplitMA(role, req)
if err != nil {
return res, err
}
// 源 MA 码状态标记为 split
p.updateStatus(req.SourceMACode, model.StatusSplit)
// 新 MA 码的内容记录和绑定写穿
for _, newMA := range res.NewMACodes {
c, _ := p.MemoryChain.QueryContent(newMA)
p.persistContent(c)
for _, b := range p.snapshotBindings(newMA) {
p.persistBinding(b)
}
for _, mp := range p.snapshotMappings(newMA) {
p.persistMapping(mp)
}
}
p.persistTx(req.SourceMACode, res.TxID, "splitMA")
return res, nil
}
// ---- PG 写入小工具(best-effort,失败仅记日志)----
func (p *PersistentChain) exec(q string, args ...any) {
@@ -194,6 +235,16 @@ func (p *PersistentChain) snapshotBindings(maCode string) []model.HashBinding {
return out
}
// snapshotMappings 复制某 MA 码当前的内存映射(同包访问,读锁保护)。
func (p *PersistentChain) snapshotMappings(maCode string) []model.Mapping {
p.mu.RLock()
defer p.mu.RUnlock()
src := p.mappings[maCode]
out := make([]model.Mapping, len(src))
copy(out, src)
return out
}
// ---- 启动水合:从 PG 镜像恢复内存状态 ----
func (p *PersistentChain) hydrate() error {