package user import ( "context" "strings" "hyapp/pkg/appcode" "hyapp/pkg/xerr" userdomain "hyapp/services/user-service/internal/domain/user" ) const ( AdminUserBanStatusActive = "active" AdminUserBanStatusReleased = "released" AdminUserBanStatusExpired = "expired" adminUserBanReasonPrefix = "admin_user_ban:" ) // AdminUserBanCommand 是后台管理员永久或限时封禁命令;ExpiresAtMs=0 明确表示永久。 type AdminUserBanCommand struct { CommandID string TargetUserID int64 ExpiresAtMs int64 OperatorAdminID int64 Reason string RequestID string NowMs int64 } // ReleaseAdminUserBanCommand 只释放指定 ban_id,避免一个管理员操作覆盖其他管理员的独立封禁事实。 type ReleaseAdminUserBanCommand struct { BanID string TargetUserID int64 OperatorAdminID int64 Status string Reason string RequestID string NowMs int64 } // AdminUserBan 是后台管理员封禁事实;TargetOldStatus 用于审计,不作为自动恢复的唯一判断依据。 type AdminUserBan struct { BanID string CommandID string TargetUserID int64 TargetOldStatus userdomain.Status ExpiresAtMs int64 Status string Reason string OperatorAdminID int64 CreatedAtMs int64 UpdatedAtMs int64 ReleasedByAdminID int64 ReleasedReason string ReleasedAtMs int64 } // AdminUserBanPersistenceResult 表达创建封禁和账号准入状态在同一事务提交后的事实。 type AdminUserBanPersistenceResult struct { Ban AdminUserBan Status UserStatusPersistenceResult StatusChanged bool } // AdminUserBanRelease 表达一条管理员封禁释放后的用户状态;仍有其他来源时 Restored=false。 type AdminUserBanRelease struct { Ban AdminUserBan User userdomain.User Restored bool } // AdminBanUser 创建永久或限时管理员封禁。 // 已被其他管理员/经理封禁的用户允许叠加新事实,但不会重复吊销 session 或重复驱逐实时连接。 func (s *Service) AdminBanUser(ctx context.Context, command AdminUserBanCommand) (AdminUserBan, UserStatusResult, error) { if s.moderationRepository == nil { return AdminUserBan{}, UserStatusResult{}, xerr.New(xerr.Unavailable, "moderation repository is not configured") } command.CommandID = strings.TrimSpace(command.CommandID) command.Reason = strings.TrimSpace(command.Reason) if command.CommandID == "" || command.TargetUserID <= 0 || command.OperatorAdminID <= 0 || command.ExpiresAtMs < 0 { return AdminUserBan{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "admin ban command is incomplete") } // 封禁期限由 user-service owner 时钟判定;调用方 sent_at_ms 仅是链路元数据,延迟或伪造值不能创建已过期事实。 command.NowMs = s.now().UTC().UnixMilli() if command.ExpiresAtMs > 0 && command.ExpiresAtMs <= command.NowMs { return AdminUserBan{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "expires_at_ms must be in the future or zero") } if command.Reason == "" { command.Reason = "admin_user_ban" } if command.RequestID == "" { command.RequestID = command.CommandID } persisted, err := s.moderationRepository.CreateAdminUserBan(ctx, command) if err != nil { return AdminUserBan{}, UserStatusResult{}, err } statusCommand := UserStatusCommand{ AppCode: appcode.FromContext(ctx), TargetUserID: command.TargetUserID, Status: userdomain.StatusBanned, OperatorType: OperatorTypeAdmin, OperatorUserID: command.OperatorAdminID, Reason: adminUserBanReasonPrefix + persisted.Ban.BanID, RequestID: command.RequestID, NowMs: command.NowMs, } return persisted.Ban, s.statusResultAfterPersistence(ctx, statusCommand, persisted.Status, persisted.StatusChanged), nil } // AdminUnbanUser 手动释放一条管理员封禁。 // repository 在同一事务内重新检查其他管理员和经理封禁;没有满足恢复条件时仅释放事实,不改 users.status。 func (s *Service) AdminUnbanUser(ctx context.Context, command ReleaseAdminUserBanCommand) (AdminUserBan, UserStatusResult, error) { if s.moderationRepository == nil { return AdminUserBan{}, UserStatusResult{}, xerr.New(xerr.Unavailable, "moderation repository is not configured") } command.BanID = strings.TrimSpace(command.BanID) command.Reason = strings.TrimSpace(command.Reason) if command.TargetUserID <= 0 || command.OperatorAdminID <= 0 { return AdminUserBan{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "admin unban command is incomplete") } // 解封审计和状态恢复统一使用 owner 时钟,避免跨服务时钟偏差污染事实顺序。 command.NowMs = s.now().UTC().UnixMilli() command.RequestID = strings.TrimSpace(command.RequestID) if command.RequestID == "" { return AdminUserBan{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "admin unban request_id is required") } if command.Reason == "" { command.Reason = "admin_user_unban" } command.Status = AdminUserBanStatusReleased release, err := s.moderationRepository.ReleaseAdminUserBan(ctx, command) if err != nil { return AdminUserBan{}, UserStatusResult{}, err } return release.Ban, UserStatusResult{User: release.User, AccessTokenRevoked: true}, nil } // ExpireAdminUserBans 处理一批已到期的限时管理员封禁;永久封禁的 expires_at_ms=0 永远不会被 claim。 func (s *Service) ExpireAdminUserBans(ctx context.Context, limit int32) (int, error) { if s.moderationRepository == nil { return 0, xerr.New(xerr.Unavailable, "moderation repository is not configured") } if limit <= 0 || limit > 500 { limit = 100 } releases, err := s.moderationRepository.ExpireAdminUserBans(ctx, s.now().UTC().UnixMilli(), limit) if err != nil { return 0, err } return len(releases), nil }