package identity import ( "context" "errors" "fmt" "strings" platformid "aigateway.local/core/internal/platform/id" "aigateway.local/core/internal/platform/oplog" "github.com/jackc/pgx/v5" ) // Role 是一个自定义角色定义(内置角色在代码中,不落库)。 type Role struct { ID string `json:"id"` Code string `json:"code"` Name string `json:"name"` Description string `json:"description"` Permissions []string `json:"permissions"` Builtin bool `json:"builtin"` CreatedBy *string `json:"created_by,omitempty"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // ListRoles 返回全部角色(内置 + 自定义)。 func (r *Repository) ListRoles(ctx context.Context) ([]Role, error) { if r.pool == nil { return nil, ErrUnavailable } rows, err := r.pool.Query(ctx, `SELECT id::text,code,name,description,permissions,builtin,created_by::text,created_at,updated_at FROM gateway.roles ORDER BY builtin DESC,created_at`) if err != nil { return nil, fmt.Errorf("%w: %v", ErrUnavailable, err) } defer rows.Close() items := []Role{} for rows.Next() { var item Role var createdBy *string if err := rows.Scan(&item.ID, &item.Code, &item.Name, &item.Description, &item.Permissions, &item.Builtin, &createdBy, &item.CreatedAt, &item.UpdatedAt); err != nil { return nil, err } item.CreatedBy = createdBy items = append(items, item) } if err := rows.Err(); err != nil { return nil, err } // 合并代码内置角色(带可读名称)。 builtinNames := map[string]string{"superadmin": "超级管理员", "operator": "运维操作员", "auditor": "审计员", "member": "普通成员"} for code, permissions := range rolePermissions { items = append(items, Role{ID: "builtin:" + code, Code: code, Name: builtinNames[code], Permissions: permissions, Builtin: true}) } return items, nil } // FindRole 按 code 查询角色;builtin 角色由代码返回。 func (r *Repository) FindRole(ctx context.Context, code string) (Role, error) { code = strings.ToLower(strings.TrimSpace(code)) if permissions, ok := rolePermissions[code]; ok { return Role{Code: code, Name: code, Permissions: permissions, Builtin: true}, nil } var item Role var createdBy *string err := r.pool.QueryRow(ctx, `SELECT id::text,code,name,description,permissions,builtin,created_by::text,created_at,updated_at FROM gateway.roles WHERE code=$1`, code).Scan(&item.ID, &item.Code, &item.Name, &item.Description, &item.Permissions, &item.Builtin, &createdBy, &item.CreatedAt, &item.UpdatedAt) if errors.Is(err, pgx.ErrNoRows) { return Role{}, ErrNotFound } if err != nil { return Role{}, fmt.Errorf("%w: %v", ErrUnavailable, err) } item.CreatedBy = createdBy return item, nil } // SaveRole 创建或更新自定义角色;内置角色禁止修改。 func (r *Repository) SaveRole(ctx context.Context, id, code, name, description string, permissions []string, actorID string, create bool) (Role, error) { if r.pool == nil { return Role{}, ErrUnavailable } code = strings.ToLower(strings.TrimSpace(code)) if _, builtin := rolePermissions[code]; builtin { return Role{}, errors.New("内置角色不可修改") } if create { id, err := platformid.NewUUID() if err != nil { return Role{}, err } _, err = r.pool.Exec(ctx, `INSERT INTO gateway.roles(id,code,name,description,permissions,created_by) VALUES($1,$2,$3,$4,$5,$6)`, id, code, strings.TrimSpace(name), strings.TrimSpace(description), permissions, actorID) if err != nil { return Role{}, mapRoleError(err) } oplog.Record(ctx, r.pool, nil, actorID, "", "role.create", "role", id, map[string]any{"code": code, "name": name, "permissions": permissions}) return r.FindRole(ctx, code) } tag, err := r.pool.Exec(ctx, `UPDATE gateway.roles SET name=$2,description=$3,permissions=$4,updated_at=clock_timestamp() WHERE id=$1 AND NOT builtin`, id, strings.TrimSpace(name), strings.TrimSpace(description), permissions) if err != nil { return Role{}, fmt.Errorf("%w: %v", ErrUnavailable, err) } if tag.RowsAffected() == 0 { return Role{}, ErrNotFound } oplog.Record(ctx, r.pool, nil, actorID, "", "role.update", "role", id, map[string]any{"code": code, "name": name, "permissions": permissions}) var item Role item, err = r.FindRole(ctx, code) if err != nil { return Role{}, err } return item, nil } // DeleteRole 删除自定义角色(内置角色禁止)。 func (r *Repository) DeleteRole(ctx context.Context, id, actorID string) error { if r.pool == nil { return ErrUnavailable } tag, err := r.pool.Exec(ctx, `DELETE FROM gateway.roles WHERE id=$1 AND NOT builtin`, id) if err != nil { return fmt.Errorf("%w: %v", ErrUnavailable, err) } if tag.RowsAffected() == 0 { return ErrNotFound } oplog.Record(ctx, r.pool, nil, actorID, "", "role.delete", "role", id, nil) return nil } func mapRoleError(err error) error { var pgError interface{ Code() string } if errors.As(err, &pgError) && pgError.Code() == "23505" { return ErrIdentityConflict } return fmt.Errorf("%w: %v", ErrUnavailable, err) }