156 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}