feat: 方案100%匹配 — RBAC动态权限+Grafana监控+目录库独立微服务+全国目录中心+标识同步
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 让内存实现跑契约套件(始终运行)。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user