178 lines
6.9 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 只用于单条管理员封禁的自动到期和底层兼容释放。
// 后台人工解封必须走 AdminForceUnbanCommand保证经理封禁和重叠管理员封禁在同一事务内一起释放。
type ReleaseAdminUserBanCommand struct {
BanID string
TargetUserID int64
OperatorAdminID int64
Status string
Reason string
RequestID string
NowMs int64
}
// AdminForceUnbanCommand 是后台管理员覆盖全部用户治理封禁来源的命令。
// RequestID 是跨 gRPC 重试的幂等边界,避免一次超时重试误释放原操作完成后新产生的封禁。
type AdminForceUnbanCommand struct {
TargetUserID int64
OperatorAdminID int64
Reason string
RequestID string
NowMs int64
}
// AdminForceUnbanResult 返回本次命令实际释放的事实数量和最终用户主状态。
// StatusRestored 只描述首次执行时是否发生 banned/disabled -> active不因幂等重试重复写状态日志。
type AdminForceUnbanResult struct {
User userdomain.User
ReleasedAdminBanCount int64
ReleasedManagerBlockCount int64
StatusRestored bool
}
// 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 原子释放全部管理员/经理封禁,并覆盖没有独立事实表记录的 legacy banned/disabled 状态。
func (s *Service) AdminUnbanUser(ctx context.Context, command AdminForceUnbanCommand) (AdminForceUnbanResult, UserStatusResult, error) {
if s.moderationRepository == nil {
return AdminForceUnbanResult{}, UserStatusResult{}, xerr.New(xerr.Unavailable, "moderation repository is not configured")
}
command.Reason = strings.TrimSpace(command.Reason)
if command.TargetUserID <= 0 || command.OperatorAdminID <= 0 {
return AdminForceUnbanResult{}, 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 AdminForceUnbanResult{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "admin unban request_id is required")
}
if command.Reason == "" {
command.Reason = "admin_force_unban"
}
// release_reason 还会附带操作类型和管理员 ID给前缀预留空间避免多字节原因触发 MySQL 截断。
if len(command.Reason) > 200 {
return AdminForceUnbanResult{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "admin unban reason is too long")
}
release, err := s.moderationRepository.ForceReleaseUserBans(ctx, command)
if err != nil {
return AdminForceUnbanResult{}, UserStatusResult{}, err
}
return release, 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
}