feat: 方案100%匹配 — RBAC动态权限+Grafana监控+目录库独立微服务+全国目录中心+标识同步
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user