Files
ai_site/platform/internal/rolestore/store.go
2026-07-31 10:31:17 +08:00

420 lines
11 KiB
Go

package rolestore
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"sync"
"time"
"aijianzhan/platform/internal/authx"
)
type Role struct {
RoleID int64 `json:"role_id"`
TenantID int64 `json:"tenant_id"`
Code string `json:"code"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []string `json:"permissions"`
CreatedAt time.Time `json:"created_at"`
}
type CreateInput struct {
Code string
Name string
Description string
Permissions []string
}
type UpdateInput struct {
Name *string
Description *string
Permissions *[]string
}
type Store interface {
List(ctx context.Context, tenantID int64) ([]Role, error)
Get(ctx context.Context, tenantID, roleID int64) (*Role, error)
Create(ctx context.Context, tenantID int64, in CreateInput) (*Role, error)
Update(ctx context.Context, tenantID, roleID int64, in UpdateInput) (*Role, error)
Delete(ctx context.Context, tenantID, roleID int64) error
EnsureDefaults(ctx context.Context, tenantID int64) error
}
var defaultRoles = []CreateInput{
{
Code: authx.AgentRole生成发布, Name: "生成发布", Description: "生成蓝图、发布应用并导入业务数据(智能体常用最低权限)",
Permissions: []string{
"读取模块", "发布模块",
"导入数据", "查询数据",
"上传文件", "下载文件",
},
},
{
Code: authx.AgentRole只读, Name: "只读", Description: "读应用与数据,可导出,不可改写",
Permissions: []string{"读取模块", "查询数据", "导出数据", "下载文件", "查看审计"},
},
{
Code: authx.AgentRole读写, Name: "读写", Description: "读写业务数据与导入导出,不可发布模块",
Permissions: []string{
"读取模块", "写入模块",
"新增数据", "查询数据", "更新数据", "导出数据", "导入数据",
"上传文件", "下载文件",
},
},
{
Code: authx.AgentRole运维, Name: "运维", Description: "含发布、删除及全量行操作,适合运维类智能体",
Permissions: []string{
"读取模块", "写入模块", "发布模块",
"新增数据", "查询数据", "更新数据", "删除数据", "导出数据", "导入数据",
"上传文件", "下载文件", "查看审计",
},
},
}
type memRole struct{ Role }
type MemoryStore struct {
mu sync.Mutex
seq int64
by map[int64]*memRole
}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{by: map[int64]*memRole{}}
}
func findDefaultRole(byCode map[string]Role, d CreateInput) (Role, bool) {
if r, ok := byCode[d.Code]; ok {
return r, true
}
legacy := map[string]string{
"publisher": authx.AgentRole生成发布,
"viewer": authx.AgentRole只读,
"editor": authx.AgentRole读写,
"operator": authx.AgentRole运维,
}
for eng, zh := range legacy {
if zh == d.Code {
if r, ok := byCode[eng]; ok {
return r, true
}
}
}
return Role{}, false
}
func (s *MemoryStore) EnsureDefaults(ctx context.Context, tenantID int64) error {
list, err := s.List(ctx, tenantID)
if err != nil {
return err
}
byCode := map[string]Role{}
for _, r := range list {
byCode[r.Code] = r
}
for _, d := range defaultRoles {
if existing, ok := findDefaultRole(byCode, d); ok {
if d.Code == authx.AgentRole生成发布 {
perms := append([]string{}, d.Permissions...)
if _, err := s.Update(ctx, tenantID, existing.RoleID, UpdateInput{
Name: &d.Name,
Description: &d.Description,
Permissions: &perms,
}); err != nil {
return err
}
}
continue
}
if _, err := s.Create(ctx, tenantID, d); err != nil {
return err
}
}
return nil
}
func (s *MemoryStore) List(_ context.Context, tenantID int64) ([]Role, error) {
s.mu.Lock()
defer s.mu.Unlock()
out := []Role{}
for _, r := range s.by {
if r.TenantID == tenantID {
out = append(out, clone(r.Role))
}
}
return out, nil
}
func (s *MemoryStore) Get(_ context.Context, tenantID, roleID int64) (*Role, error) {
s.mu.Lock()
defer s.mu.Unlock()
r, ok := s.by[roleID]
if !ok || r.TenantID != tenantID {
return nil, fmt.Errorf("role not found")
}
cp := clone(r.Role)
return &cp, nil
}
func (s *MemoryStore) Create(_ context.Context, tenantID int64, in CreateInput) (*Role, error) {
s.mu.Lock()
defer s.mu.Unlock()
code := strings.TrimSpace(in.Code)
name := strings.TrimSpace(in.Name)
if code == "" || name == "" {
return nil, fmt.Errorf("code and name required")
}
for _, r := range s.by {
if r.TenantID == tenantID && r.Code == code {
return nil, fmt.Errorf("role code already exists")
}
}
s.seq++
r := &memRole{Role: Role{
RoleID: s.seq,
TenantID: tenantID,
Code: code,
Name: name,
Description: strings.TrimSpace(in.Description),
Permissions: uniq(in.Permissions),
CreatedAt: time.Now().UTC(),
}}
s.by[r.RoleID] = r
cp := clone(r.Role)
return &cp, nil
}
func (s *MemoryStore) Update(_ context.Context, tenantID, roleID int64, in UpdateInput) (*Role, error) {
s.mu.Lock()
defer s.mu.Unlock()
r, ok := s.by[roleID]
if !ok || r.TenantID != tenantID {
return nil, fmt.Errorf("role not found")
}
if in.Name != nil {
r.Name = strings.TrimSpace(*in.Name)
}
if in.Description != nil {
r.Description = strings.TrimSpace(*in.Description)
}
if in.Permissions != nil {
r.Permissions = uniq(*in.Permissions)
}
cp := clone(r.Role)
return &cp, nil
}
func (s *MemoryStore) Delete(_ context.Context, tenantID, roleID int64) error {
s.mu.Lock()
defer s.mu.Unlock()
r, ok := s.by[roleID]
if !ok || r.TenantID != tenantID {
return fmt.Errorf("role not found")
}
delete(s.by, roleID)
return nil
}
type PostgresStore struct{ DB *sql.DB }
func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db} }
func (s *PostgresStore) EnsureDefaults(ctx context.Context, tenantID int64) error {
list, err := s.List(ctx, tenantID)
if err != nil {
return err
}
byCode := map[string]Role{}
for _, r := range list {
byCode[r.Code] = r
}
for _, d := range defaultRoles {
if existing, ok := findDefaultRole(byCode, d); ok {
// 英文旧编码升级为中文
if existing.Code != d.Code {
if _, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.roles SET code=$1, name=$2 WHERE role_id=$3 AND tenant_id=$4`,
d.Code, d.Name, existing.RoleID, tenantID); err != nil {
return err
}
existing.Code = d.Code
existing.Name = d.Name
byCode[d.Code] = existing
}
if d.Code == authx.AgentRole生成发布 {
perms := append([]string{}, d.Permissions...)
if _, err := s.Update(ctx, tenantID, existing.RoleID, UpdateInput{
Name: &d.Name,
Description: &d.Description,
Permissions: &perms,
}); err != nil {
return err
}
}
continue
}
if _, err := s.Create(ctx, tenantID, d); err != nil {
return err
}
}
return nil
}
func (s *PostgresStore) List(ctx context.Context, tenantID int64) ([]Role, error) {
rows, err := s.DB.QueryContext(ctx, `
SELECT role_id, tenant_id, code, name, description, created_at
FROM platform_meta.roles WHERE tenant_id=$1 ORDER BY role_id`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Role
for rows.Next() {
var r Role
if err := rows.Scan(&r.RoleID, &r.TenantID, &r.Code, &r.Name, &r.Description, &r.CreatedAt); err != nil {
return nil, err
}
perms, err := s.loadPerms(ctx, r.RoleID)
if err != nil {
return nil, err
}
r.Permissions = perms
out = append(out, r)
}
return out, rows.Err()
}
func (s *PostgresStore) Get(ctx context.Context, tenantID, roleID int64) (*Role, error) {
var r Role
err := s.DB.QueryRowContext(ctx, `
SELECT role_id, tenant_id, code, name, description, created_at
FROM platform_meta.roles WHERE role_id=$1 AND tenant_id=$2`, roleID, tenantID,
).Scan(&r.RoleID, &r.TenantID, &r.Code, &r.Name, &r.Description, &r.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("role not found")
}
if err != nil {
return nil, err
}
perms, err := s.loadPerms(ctx, r.RoleID)
if err != nil {
return nil, err
}
r.Permissions = perms
return &r, nil
}
func (s *PostgresStore) Create(ctx context.Context, tenantID int64, in CreateInput) (*Role, error) {
code := strings.TrimSpace(in.Code)
name := strings.TrimSpace(in.Name)
if code == "" || name == "" {
return nil, fmt.Errorf("code and name required")
}
var r Role
err := s.DB.QueryRowContext(ctx, `
INSERT INTO platform_meta.roles(tenant_id, code, name, description)
VALUES($1,$2,$3,$4)
RETURNING role_id, tenant_id, code, name, description, created_at`,
tenantID, code, name, strings.TrimSpace(in.Description),
).Scan(&r.RoleID, &r.TenantID, &r.Code, &r.Name, &r.Description, &r.CreatedAt)
if err != nil {
return nil, err
}
perms := uniq(in.Permissions)
if err := s.replacePerms(ctx, r.RoleID, perms); err != nil {
return nil, err
}
r.Permissions = perms
return &r, nil
}
func (s *PostgresStore) Update(ctx context.Context, tenantID, roleID int64, in UpdateInput) (*Role, error) {
r, err := s.Get(ctx, tenantID, roleID)
if err != nil {
return nil, err
}
name, desc := r.Name, r.Description
if in.Name != nil {
name = strings.TrimSpace(*in.Name)
}
if in.Description != nil {
desc = strings.TrimSpace(*in.Description)
}
if _, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.roles SET name=$1, description=$2 WHERE role_id=$3 AND tenant_id=$4`,
name, desc, roleID, tenantID); err != nil {
return nil, err
}
if in.Permissions != nil {
if err := s.replacePerms(ctx, roleID, uniq(*in.Permissions)); err != nil {
return nil, err
}
}
return s.Get(ctx, tenantID, roleID)
}
func (s *PostgresStore) Delete(ctx context.Context, tenantID, roleID int64) error {
_, _ = s.DB.ExecContext(ctx, `
UPDATE platform_meta.agent_accounts SET role_id=NULL WHERE role_id=$1 AND tenant_id=$2`, roleID, tenantID)
res, err := s.DB.ExecContext(ctx, `
DELETE FROM platform_meta.roles WHERE role_id=$1 AND tenant_id=$2`, roleID, tenantID)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return fmt.Errorf("role not found")
}
return nil
}
func (s *PostgresStore) loadPerms(ctx context.Context, roleID int64) ([]string, error) {
rows, err := s.DB.QueryContext(ctx, `SELECT perm FROM platform_meta.role_permissions WHERE role_id=$1`, roleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []string
for rows.Next() {
var p string
if err := rows.Scan(&p); err != nil {
return nil, err
}
out = append(out, p)
}
if err := rows.Err(); err != nil {
return nil, err
}
return authx.NormalizePerms(out), nil
}
func (s *PostgresStore) replacePerms(ctx context.Context, roleID int64, perms []string) error {
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, `DELETE FROM platform_meta.role_permissions WHERE role_id=$1`, roleID); err != nil {
return err
}
for _, p := range authx.NormalizePerms(perms) {
if _, err := tx.ExecContext(ctx, `INSERT INTO platform_meta.role_permissions(role_id, perm) VALUES($1,$2)`, roleID, p); err != nil {
return err
}
}
return tx.Commit()
}
func clone(r Role) Role {
cp := r
cp.Permissions = append([]string{}, r.Permissions...)
return cp
}
func uniq(in []string) []string {
return authx.NormalizePerms(in)
}