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
+265
View File
@@ -0,0 +1,265 @@
package service
import (
"crypto/rand"
"encoding/hex"
"fmt"
"sync"
"time"
"github.com/tcs-iptv/tcs/internal/model"
)
// AdminService 用户管理/组织管理服务(系统管理模块补足)。
// MVP 使用内存存储,生产环境可替换为 PostgreSQL 实现。
type AdminService struct {
mu sync.RWMutex
users map[string]*model.User // userID -> User
orgs map[string]*model.Organization // orgID -> Organization
keys map[string]*model.User // apiKey -> User(快速鉴权查找)
names map[string]bool // username 唯一性校验
}
// NewAdminService 创建用户/组织管理服务。
func NewAdminService() *AdminService {
return &AdminService{
users: make(map[string]*model.User),
orgs: make(map[string]*model.Organization),
keys: make(map[string]*model.User),
names: make(map[string]bool),
}
}
// ---- 组织管理 ----
// CreateOrg 创建组织。
func (a *AdminService) CreateOrg(org model.Organization) (model.Organization, error) {
if org.Name == "" {
return model.Organization{}, fmt.Errorf("admin: 组织名称不能为空")
}
a.mu.Lock()
defer a.mu.Unlock()
org.ID = a.nextID("org")
org.Status = model.OrgStatusActive
if org.CreatedAt.IsZero() {
org.CreatedAt = time.Now()
}
org.UpdatedAt = time.Now()
a.orgs[org.ID] = &org
return org, nil
}
// UpdateOrg 更新组织信息。
func (a *AdminService) UpdateOrg(orgID string, updates model.Organization) (model.Organization, error) {
a.mu.Lock()
defer a.mu.Unlock()
org, ok := a.orgs[orgID]
if !ok {
return model.Organization{}, fmt.Errorf("admin: 组织 %s 不存在", orgID)
}
if updates.Name != "" {
org.Name = updates.Name
}
if updates.OrgNode != "" {
org.OrgNode = updates.OrgNode
}
if updates.Province != "" {
org.Province = updates.Province
}
if updates.Type != "" {
org.Type = updates.Type
}
if updates.Status != "" {
org.Status = updates.Status
}
org.UpdatedAt = time.Now()
return *org, nil
}
// GetOrg 查询组织详情。
func (a *AdminService) GetOrg(orgID string) (model.Organization, error) {
a.mu.RLock()
defer a.mu.RUnlock()
org, ok := a.orgs[orgID]
if !ok {
return model.Organization{}, fmt.Errorf("admin: 组织 %s 不存在", orgID)
}
return *org, nil
}
// ListOrgs 列出全部组织(可按类型过滤)。
func (a *AdminService) ListOrgs(orgType string) []model.Organization {
a.mu.RLock()
defer a.mu.RUnlock()
var out []model.Organization
for _, org := range a.orgs {
if orgType == "" || org.Type == orgType {
out = append(out, *org)
}
}
return out
}
// DisableOrg 禁用组织。
func (a *AdminService) DisableOrg(orgID string) error {
a.mu.Lock()
defer a.mu.Unlock()
org, ok := a.orgs[orgID]
if !ok {
return fmt.Errorf("admin: 组织 %s 不存在", orgID)
}
org.Status = model.OrgStatusDisabled
org.UpdatedAt = time.Now()
return nil
}
// ---- 用户管理 ----
// CreateUser 创建用户并自动生成 API Key/Secret。
func (a *AdminService) CreateUser(user model.User) (model.User, error) {
if user.Username == "" {
return model.User{}, fmt.Errorf("admin: 用户名不能为空")
}
if user.OrgID == "" {
return model.User{}, fmt.Errorf("admin: 所属组织不能为空")
}
a.mu.Lock()
defer a.mu.Unlock()
if a.names[user.Username] {
return model.User{}, fmt.Errorf("admin: 用户名 %s 已存在", user.Username)
}
if _, ok := a.orgs[user.OrgID]; !ok {
return model.User{}, fmt.Errorf("admin: 组织 %s 不存在", user.OrgID)
}
user.ID = a.nextID("user")
user.APIKey = a.generateAPIKey()
user.APISecret = a.generateAPISecret()
user.Status = model.UserStatusActive
if user.CreatedAt.IsZero() {
user.CreatedAt = time.Now()
}
user.UpdatedAt = time.Now()
a.users[user.ID] = &user
a.keys[user.APIKey] = &user
a.names[user.Username] = true
return user, nil
}
// UpdateUser 更新用户信息(不支持修改用户名和 API Key)。
func (a *AdminService) UpdateUser(userID string, updates model.User) (model.User, error) {
a.mu.Lock()
defer a.mu.Unlock()
user, ok := a.users[userID]
if !ok {
return model.User{}, fmt.Errorf("admin: 用户 %s 不存在", userID)
}
if updates.FullName != "" {
user.FullName = updates.FullName
}
if updates.OrgID != "" {
if _, ok := a.orgs[updates.OrgID]; !ok {
return model.User{}, fmt.Errorf("admin: 组织 %s 不存在", updates.OrgID)
}
user.OrgID = updates.OrgID
}
if updates.Role != "" {
user.Role = updates.Role
}
if updates.Status != "" {
user.Status = updates.Status
}
user.UpdatedAt = time.Now()
return *user, nil
}
// GetUser 查询用户详情。
func (a *AdminService) GetUser(userID string) (model.User, error) {
a.mu.RLock()
defer a.mu.RUnlock()
user, ok := a.users[userID]
if !ok {
return model.User{}, fmt.Errorf("admin: 用户 %s 不存在", userID)
}
return *user, nil
}
// ListUsers 列出全部用户(可按组织或角色过滤)。
func (a *AdminService) ListUsers(orgID, role string) []model.User {
a.mu.RLock()
defer a.mu.RUnlock()
var out []model.User
for _, user := range a.users {
if orgID != "" && user.OrgID != orgID {
continue
}
if role != "" && user.Role != role {
continue
}
out = append(out, *user)
}
return out
}
// DisableUser 禁用用户。
func (a *AdminService) DisableUser(userID string) error {
a.mu.Lock()
defer a.mu.Unlock()
user, ok := a.users[userID]
if !ok {
return fmt.Errorf("admin: 用户 %s 不存在", userID)
}
user.Status = model.UserStatusDisabled
user.UpdatedAt = time.Now()
return nil
}
// ResetAPIKey 重置用户的 API Key 和 Secret。
func (a *AdminService) ResetAPIKey(userID string) (model.User, error) {
a.mu.Lock()
defer a.mu.Unlock()
user, ok := a.users[userID]
if !ok {
return model.User{}, fmt.Errorf("admin: 用户 %s 不存在", userID)
}
// 移除旧 key
delete(a.keys, user.APIKey)
// 生成新 key
user.APIKey = a.generateAPIKey()
user.APISecret = a.generateAPISecret()
user.UpdatedAt = time.Now()
a.keys[user.APIKey] = user
return *user, nil
}
// LookupByAPIKey 根据 API Key 查询用户(供 httpx.KeyStore 使用)。
func (a *AdminService) LookupByAPIKey(apiKey string) (secret string, role string, ok bool) {
a.mu.RLock()
defer a.mu.RUnlock()
user, exists := a.keys[apiKey]
if !exists || user.Status != model.UserStatusActive {
return "", "", false
}
return user.APISecret, user.Role, true
}
// ---- 辅助方法 ----
func (a *AdminService) nextID(prefix string) string {
// 使用时间戳+随机数生成唯一 ID
ts := time.Now().Format("20060102")
b := make([]byte, 4)
rand.Read(b)
return fmt.Sprintf("%s-%s-%s", prefix, ts, hex.EncodeToString(b))
}
func (a *AdminService) generateAPIKey() string {
b := make([]byte, 16)
rand.Read(b)
return "tcs-" + hex.EncodeToString(b)
}
func (a *AdminService) generateAPISecret() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
+110
View File
@@ -0,0 +1,110 @@
package service
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tcs-iptv/tcs/internal/model"
)
func TestAdminService_OrgCRUD(t *testing.T) {
a := NewAdminService()
// 创建组织
org, err := a.CreateOrg(model.Organization{
Name: "陕西IPTV运营公司", OrgNode: "6101", Province: "陕西", Type: model.OrgTypeRegulator,
})
require.NoError(t, err)
assert.NotEmpty(t, org.ID)
assert.Equal(t, model.OrgStatusActive, org.Status)
// 查询组织
got, err := a.GetOrg(org.ID)
require.NoError(t, err)
assert.Equal(t, "陕西IPTV运营公司", got.Name)
// 更新组织
updated, err := a.UpdateOrg(org.ID, model.Organization{Name: "陕西IPTV"})
require.NoError(t, err)
assert.Equal(t, "陕西IPTV", updated.Name)
// 列出组织
orgs := a.ListOrgs(model.OrgTypeRegulator)
assert.Len(t, orgs, 1)
// 禁用组织
require.NoError(t, a.DisableOrg(org.ID))
got, _ = a.GetOrg(org.ID)
assert.Equal(t, model.OrgStatusDisabled, got.Status)
}
func TestAdminService_UserCRUD(t *testing.T) {
a := NewAdminService()
// 先创建组织
org, err := a.CreateOrg(model.Organization{
Name: "测试CP公司", Type: model.OrgTypeCP,
})
require.NoError(t, err)
// 创建用户
user, err := a.CreateUser(model.User{
Username: "testuser", FullName: "测试用户", OrgID: org.ID, Role: "cp",
})
require.NoError(t, err)
assert.NotEmpty(t, user.ID)
assert.NotEmpty(t, user.APIKey)
assert.NotEmpty(t, user.APISecret)
assert.Equal(t, model.UserStatusActive, user.Status)
// 用户名唯一性校验
_, err = a.CreateUser(model.User{
Username: "testuser", OrgID: org.ID, Role: "cp",
})
assert.Error(t, err)
// 不存在的组织
_, err = a.CreateUser(model.User{
Username: "user2", OrgID: "nonexistent", Role: "cp",
})
assert.Error(t, err)
// 查询用户
got, err := a.GetUser(user.ID)
require.NoError(t, err)
assert.Equal(t, "testuser", got.Username)
// 更新用户
updated, err := a.UpdateUser(user.ID, model.User{FullName: "更新用户名"})
require.NoError(t, err)
assert.Equal(t, "更新用户名", updated.FullName)
// 列出用户
users := a.ListUsers(org.ID, "")
assert.Len(t, users, 1)
// API Key 查找
secret, role, ok := a.LookupByAPIKey(user.APIKey)
require.True(t, ok)
assert.Equal(t, user.APISecret, secret)
assert.Equal(t, "cp", role)
// 重置 API Key
newUser, err := a.ResetAPIKey(user.ID)
require.NoError(t, err)
assert.NotEqual(t, user.APIKey, newUser.APIKey)
// 旧 Key 失效
_, _, ok = a.LookupByAPIKey(user.APIKey)
assert.False(t, ok)
// 新 Key 生效
_, _, ok = a.LookupByAPIKey(newUser.APIKey)
assert.True(t, ok)
// 禁用用户
require.NoError(t, a.DisableUser(user.ID))
_, _, ok = a.LookupByAPIKey(newUser.APIKey)
assert.False(t, ok)
}
+73
View File
@@ -0,0 +1,73 @@
package service
import (
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/model"
)
// ---- 多维度标识查询(方案第一阶段标识查询接口补足)----
// QueryByHash 根据内容哈希反查标识信息及映射关系。
func (s *Service) QueryByHash(fileHash string) (model.ContentQueryResult, error) {
return s.chain.QueryByHash(fileHash)
}
// QueryByProvincialCode 根据省级内容编码(CP MediaID)反查标识信息。
func (s *Service) QueryByProvincialCode(provincialCode string) (model.ContentQueryResult, error) {
return s.chain.QueryByProvincialCode(provincialCode)
}
// QueryByLibraryFileID 根据片库文件 ID(媒资库 ID)反查标识信息。
func (s *Service) QueryByLibraryFileID(libraryFileID string) (model.ContentQueryResult, error) {
return s.chain.QueryByLibraryFileID(libraryFileID)
}
// ---- MA 合并/拆分(方案 MA 管理模块补足)----
// MergeMACodes 将多个 MA 码合并为一个主 MA 码(仅监管主体)。
// 被合并的 MA 码的哈希绑定和映射迁移至主 MA 码,原 MA 码状态标记为 merged。
// 全链路存证记录合并操作。
func (s *Service) MergeMACodes(role chain.Role, req model.MergeRequest) (model.MergeResult, error) {
res, err := s.chain.MergeMA(role, req)
if err != nil {
return res, err
}
// 记录存证
s.prov.Record(model.ProvenanceEvent{
MACode: req.PrimaryMACode,
Node: model.NodeIssue,
Operator: req.Operator,
Detail: "MA 码合并:将 " + joinMACodes(req.SecondaryMACodes) + " 合并入 " + req.PrimaryMACode + ",原因:" + req.Reason,
})
return res, nil
}
// SplitMACode 将一个 MA 码拆分为多个独立 MA 码(仅监管主体)。
// 按集号将哈希绑定和映射迁移至新 MA 码,源 MA 码状态标记为 split。
// 全链路存证记录拆分操作。
func (s *Service) SplitMACode(role chain.Role, req model.SplitRequest) (model.SplitResult, error) {
res, err := s.chain.SplitMA(role, req)
if err != nil {
return res, err
}
// 记录存证
s.prov.Record(model.ProvenanceEvent{
MACode: req.SourceMACode,
Node: model.NodeIssue,
Operator: req.Operator,
Detail: "MA 码拆分:将 " + req.SourceMACode + " 拆分为 " + joinMACodes(res.NewMACodes) + ",原因:" + req.Reason,
})
return res, nil
}
// joinMACodes 将 MA 码列表拼接为逗号分隔的字符串。
func joinMACodes(codes []string) string {
if len(codes) == 0 {
return ""
}
out := codes[0]
for i := 1; i < len(codes); i++ {
out += ", " + codes[i]
}
return out
}