156 lines
5.8 KiB
Go
156 lines
5.8 KiB
Go
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
|
||
}
|