Files
digital-psychology/apps/api/internal/service/admin/system.go
T
jackyu66gitandCursor 5ceb3ce749
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s
feat(ECR-010): Ops-E 系统运营;修复登出解绑;P2 Complete
落地管理员 RBAC/封禁/推送任务 stub,logout 解绑 device 并统一各页 ensureAccount,同时收口 P2 生日生成与状态文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 18:54:59 +08:00

155 lines
4.2 KiB
Go

package admin
import (
"context"
"encoding/json"
"errors"
"unicode/utf8"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
)
const (
RoleSuper = "super"
RoleOps = "ops"
pushTitleMaxRunes = 128
)
var (
ErrForbidden = errString("forbidden")
ErrInvalidRole = errString("invalid role")
ErrPushNotFound = errString("push job not found")
ErrInvalidPush = errString("invalid push job")
ErrAdminNotFound = errString("admin not found")
ErrLastSuper = errString("cannot demote last super")
)
// RequireSuper returns ErrForbidden unless admin role is super.
func (s *Service) RequireSuper(ctx context.Context, adminID uuid.UUID) error {
acc, err := s.Repo.FindAccountByID(ctx, adminID)
if err != nil || acc == nil {
return ErrAdminNotFound
}
if acc.Role != RoleSuper {
return ErrForbidden
}
return nil
}
// BanUser sets users.status=banned.
func (s *Service) BanUser(ctx context.Context, adminID, userID uuid.UUID) error {
ok, err := s.Repo.UserExists(ctx, userID)
if err != nil {
return err
}
if !ok {
return ErrUserNotFound
}
meta, _ := json.Marshal(map[string]any{"status": "banned"})
err = s.Repo.SetUserStatusWithAudit(ctx, adminID, userID, "banned", "user.ban", meta)
if errors.Is(err, repository.ErrUserStatusNotFound) {
return ErrUserNotFound
}
return err
}
// UnbanUser sets users.status=active.
func (s *Service) UnbanUser(ctx context.Context, adminID, userID uuid.UUID) error {
ok, err := s.Repo.UserExists(ctx, userID)
if err != nil {
return err
}
if !ok {
return ErrUserNotFound
}
meta, _ := json.Marshal(map[string]any{"status": "active"})
err = s.Repo.SetUserStatusWithAudit(ctx, adminID, userID, "active", "user.unban", meta)
if errors.Is(err, repository.ErrUserStatusNotFound) {
return ErrUserNotFound
}
return err
}
// ListAdmins returns admin accounts (super only caller).
func (s *Service) ListAdmins(ctx context.Context, actorID uuid.UUID) ([]repository.AdminListItem, error) {
if err := s.RequireSuper(ctx, actorID); err != nil {
return nil, err
}
return s.Repo.ListAdmins(ctx)
}
// UpdateAdminRole changes an admin role (super only).
func (s *Service) UpdateAdminRole(ctx context.Context, actorID, targetID uuid.UUID, role string) error {
if err := s.RequireSuper(ctx, actorID); err != nil {
return err
}
if role != RoleSuper && role != RoleOps {
return ErrInvalidRole
}
target, err := s.Repo.FindAccountByID(ctx, targetID)
if err != nil || target == nil {
return ErrAdminNotFound
}
if target.Role == RoleSuper && role != RoleSuper {
n, err := s.Repo.CountAdminsByRole(ctx, RoleSuper)
if err != nil {
return err
}
if n <= 1 {
return ErrLastSuper
}
}
meta, _ := json.Marshal(map[string]any{"role": role})
err = s.Repo.UpdateAdminRoleWithAudit(ctx, actorID, targetID, role, meta)
if errors.Is(err, repository.ErrAdminNotFound) {
return ErrAdminNotFound
}
return err
}
// ListPushJobs lists push stubs.
func (s *Service) ListPushJobs(ctx context.Context, limit, offset int) ([]repository.PushJob, error) {
return s.Repo.ListPushJobs(ctx, limit, offset)
}
// CreatePushJob creates a draft push stub.
func (s *Service) CreatePushJob(ctx context.Context, adminID uuid.UUID, title, body, audience string) (*repository.PushJob, error) {
if err := validatePushTitle(title); err != nil {
return nil, err
}
meta, _ := json.Marshal(map[string]any{"title": title})
return s.Repo.CreatePushJobWithAudit(ctx, adminID, title, body, audience, meta)
}
// UpdatePushJob updates a push stub.
func (s *Service) UpdatePushJob(
ctx context.Context,
adminID, jobID uuid.UUID,
title, body, status *string,
) (*repository.PushJob, error) {
if status != nil && *status != "draft" && *status != "cancelled" {
return nil, ErrInvalidPush
}
if title != nil {
if err := validatePushTitle(*title); err != nil {
return nil, err
}
}
meta, _ := json.Marshal(map[string]any{})
job, err := s.Repo.UpdatePushJobWithAudit(ctx, adminID, jobID, title, body, status, meta)
if errors.Is(err, repository.ErrPushJobNotFound) {
return nil, ErrPushNotFound
}
return job, err
}
func validatePushTitle(title string) error {
if title == "" || utf8.RuneCountInString(title) > pushTitleMaxRunes {
return ErrInvalidPush
}
return nil
}