480 lines
19 KiB
Go
480 lines
19 KiB
Go
package user
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"strings"
|
||
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/idgen"
|
||
"hyapp/pkg/xerr"
|
||
userdomain "hyapp/services/user-service/internal/domain/user"
|
||
userservice "hyapp/services/user-service/internal/service/user"
|
||
)
|
||
|
||
const adminBanStatusReasonPrefix = "admin_user_ban:"
|
||
|
||
// CreateAdminUserBan 在一个事务内写封禁事实、切换 users.status、写审计并吊销 refresh session。
|
||
// 目标已经因其他事实处于 banned 时只叠加事实,不重复写 banned->banned 日志;该约束也是历史永久封禁不被误恢复的判据。
|
||
func (r *Repository) CreateAdminUserBan(ctx context.Context, command userservice.AdminUserBanCommand) (userservice.AdminUserBanPersistenceResult, error) {
|
||
if r == nil || r.db == nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||
}
|
||
appCode := appcode.FromContext(ctx)
|
||
tx, err := r.db.BeginTx(ctx, nil)
|
||
if err != nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, err
|
||
}
|
||
defer func() { _ = tx.Rollback() }()
|
||
|
||
var oldStatus string
|
||
if err := tx.QueryRowContext(ctx, `
|
||
SELECT status
|
||
FROM users
|
||
WHERE app_code = ? AND user_id = ?
|
||
FOR UPDATE`, appCode, command.TargetUserID).Scan(&oldStatus); err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return userservice.AdminUserBanPersistenceResult{}, xerr.New(xerr.NotFound, "user not found")
|
||
}
|
||
return userservice.AdminUserBanPersistenceResult{}, err
|
||
}
|
||
if userdomain.Status(oldStatus) != userdomain.StatusActive && userdomain.Status(oldStatus) != userdomain.StatusBanned {
|
||
// disabled 是独立平台停用语义,不能用封禁事实覆盖;后台应先显式恢复后再封禁。
|
||
return userservice.AdminUserBanPersistenceResult{}, xerr.New(xerr.InvalidArgument, "target user status cannot be banned")
|
||
}
|
||
|
||
banID := idgen.New("admin_ban")
|
||
_, err = tx.ExecContext(ctx, `
|
||
INSERT INTO admin_user_bans (
|
||
app_code, ban_id, command_id, target_user_id, target_old_status,
|
||
expires_at_ms, status, reason, operator_admin_id, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)`,
|
||
appCode, banID, command.CommandID, command.TargetUserID, oldStatus,
|
||
command.ExpiresAtMs, command.Reason, command.OperatorAdminID, command.NowMs, command.NowMs,
|
||
)
|
||
if err != nil {
|
||
if adminBanDuplicate(err) {
|
||
// 幂等命令可能在上一次调用中已经完整提交;回滚当前事务后读取既有事实和当前用户状态即可。
|
||
_ = tx.Rollback()
|
||
ban, lookupErr := r.getAdminUserBanByCommand(ctx, command.CommandID)
|
||
if lookupErr != nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, lookupErr
|
||
}
|
||
if ban.TargetUserID != command.TargetUserID || ban.OperatorAdminID != command.OperatorAdminID || ban.ExpiresAtMs != command.ExpiresAtMs || ban.Reason != command.Reason {
|
||
// command_id 是完整请求的幂等边界;同一 key 不能静默复用到另一位用户或另一种封禁期限。
|
||
return userservice.AdminUserBanPersistenceResult{}, xerr.New(xerr.Conflict, "admin ban command payload does not match existing fact")
|
||
}
|
||
user, lookupErr := r.GetUser(ctx, ban.TargetUserID)
|
||
if lookupErr != nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, lookupErr
|
||
}
|
||
return userservice.AdminUserBanPersistenceResult{Ban: ban, Status: userservice.UserStatusPersistenceResult{User: user}}, nil
|
||
}
|
||
return userservice.AdminUserBanPersistenceResult{}, err
|
||
}
|
||
|
||
sessionIDs := make([]string, 0)
|
||
statusChanged := userdomain.Status(oldStatus) == userdomain.StatusActive
|
||
if statusChanged {
|
||
if _, err := tx.ExecContext(ctx, `
|
||
UPDATE users SET status = 'banned', updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ?`, command.NowMs, appCode, command.TargetUserID); err != nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, err
|
||
}
|
||
if _, err := tx.ExecContext(ctx, `
|
||
INSERT INTO user_status_change_logs (
|
||
app_code, target_user_id, old_status, new_status, operator_type,
|
||
operator_user_id, reason, request_id, created_at_ms
|
||
) VALUES (?, ?, ?, 'banned', 'admin', ?, ?, ?, ?)`,
|
||
appCode, command.TargetUserID, oldStatus, command.OperatorAdminID,
|
||
adminBanStatusReasonPrefix+banID, command.RequestID, command.NowMs,
|
||
); err != nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, err
|
||
}
|
||
var revokeErr error
|
||
sessionIDs, revokeErr = revokeActiveUserSessions(ctx, tx, appCode, command.TargetUserID, command.OperatorAdminID, command.RequestID, command.NowMs)
|
||
if revokeErr != nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, revokeErr
|
||
}
|
||
}
|
||
|
||
if err := tx.Commit(); err != nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, err
|
||
}
|
||
ban, err := r.getAdminUserBan(ctx, banID)
|
||
if err != nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, err
|
||
}
|
||
user, err := r.GetUser(ctx, command.TargetUserID)
|
||
if err != nil {
|
||
return userservice.AdminUserBanPersistenceResult{}, err
|
||
}
|
||
return userservice.AdminUserBanPersistenceResult{
|
||
Ban: ban,
|
||
Status: userservice.UserStatusPersistenceResult{User: user, RevokedSessionIDs: sessionIDs},
|
||
StatusChanged: statusChanged,
|
||
}, nil
|
||
}
|
||
|
||
func (r *Repository) ReleaseAdminUserBan(ctx context.Context, command userservice.ReleaseAdminUserBanCommand) (userservice.AdminUserBanRelease, error) {
|
||
return r.releaseAdminUserBan(ctx, command)
|
||
}
|
||
|
||
func (r *Repository) ExpireAdminUserBans(ctx context.Context, nowMS int64, limit int32) ([]userservice.AdminUserBanRelease, error) {
|
||
if r == nil || r.db == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||
}
|
||
rows, err := r.db.QueryContext(ctx, `
|
||
SELECT ban_id, target_user_id
|
||
FROM admin_user_bans
|
||
WHERE app_code = ? AND status = 'active' AND expires_at_ms > 0 AND expires_at_ms <= ?
|
||
ORDER BY expires_at_ms ASC, ban_id ASC
|
||
LIMIT ?`, appcode.FromContext(ctx), nowMS, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
type pendingBan struct {
|
||
banID string
|
||
targetUserID int64
|
||
}
|
||
pending := make([]pendingBan, 0)
|
||
for rows.Next() {
|
||
var item pendingBan
|
||
if err := rows.Scan(&item.banID, &item.targetUserID); err != nil {
|
||
_ = rows.Close()
|
||
return nil, err
|
||
}
|
||
pending = append(pending, item)
|
||
}
|
||
if err := rows.Close(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
releases := make([]userservice.AdminUserBanRelease, 0, len(pending))
|
||
for _, item := range pending {
|
||
release, err := r.releaseAdminUserBan(ctx, userservice.ReleaseAdminUserBanCommand{
|
||
BanID: item.banID,
|
||
TargetUserID: item.targetUserID,
|
||
Status: userservice.AdminUserBanStatusExpired,
|
||
Reason: "admin_user_ban_expired",
|
||
RequestID: "admin_user_ban_expire:" + item.banID,
|
||
NowMs: nowMS,
|
||
})
|
||
if err != nil {
|
||
return releases, err
|
||
}
|
||
releases = append(releases, release)
|
||
}
|
||
return releases, nil
|
||
}
|
||
|
||
func (r *Repository) releaseAdminUserBan(ctx context.Context, command userservice.ReleaseAdminUserBanCommand) (userservice.AdminUserBanRelease, error) {
|
||
tx, err := r.db.BeginTx(ctx, nil)
|
||
if err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
defer func() { _ = tx.Rollback() }()
|
||
|
||
// 先锁定命令幂等事实,再选择具体 ban。空 ban_id 的重试会复用第一次解析出的 resolved_ban_id,
|
||
// 不会因为上一条已 released 而误释放下一条重叠管理员封禁。
|
||
existingCommand, resolvedBanID, err := claimAdminBanReleaseCommand(ctx, tx, command)
|
||
if err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
var ban userservice.AdminUserBan
|
||
if existingCommand {
|
||
ban, err = r.getAdminUserBanForUpdate(ctx, tx, resolvedBanID, command.TargetUserID)
|
||
} else if strings.TrimSpace(command.BanID) == "" {
|
||
// 后台摘要可能正在展示更晚到期的经理封禁;按 target 选最新管理员事实,使重叠场景仍能逐条手动释放。
|
||
ban, err = r.getLatestActiveAdminUserBanForUpdate(ctx, tx, command.TargetUserID)
|
||
} else {
|
||
ban, err = r.getAdminUserBanForUpdate(ctx, tx, command.BanID, command.TargetUserID)
|
||
}
|
||
if err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
if !existingCommand {
|
||
if _, err := tx.ExecContext(ctx, `
|
||
UPDATE admin_user_ban_release_commands
|
||
SET resolved_ban_id = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND command_id = ?`,
|
||
ban.BanID, command.NowMs, appcode.FromContext(ctx), command.RequestID,
|
||
); err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
}
|
||
if ban.Status != userservice.AdminUserBanStatusActive {
|
||
if err := tx.Commit(); err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
user, err := r.GetUser(ctx, ban.TargetUserID)
|
||
return userservice.AdminUserBanRelease{Ban: ban, User: user}, err
|
||
}
|
||
|
||
currentStatus, err := lockUserStatus(ctx, tx, appcode.FromContext(ctx), ban.TargetUserID)
|
||
if err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
if _, err := tx.ExecContext(ctx, `
|
||
UPDATE admin_user_bans
|
||
SET status = ?, released_by_admin_id = ?, released_reason = ?, released_at_ms = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND ban_id = ?`,
|
||
command.Status, command.OperatorAdminID, command.Reason, command.NowMs, command.NowMs,
|
||
appcode.FromContext(ctx), ban.BanID,
|
||
); err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
ban.Status = command.Status
|
||
ban.ReleasedByAdminID = command.OperatorAdminID
|
||
ban.ReleasedReason = command.Reason
|
||
ban.ReleasedAtMs = command.NowMs
|
||
ban.UpdatedAtMs = command.NowMs
|
||
|
||
restored, err := restoreUserAfterManagedBanRelease(ctx, tx, managedBanRestoreCommand{
|
||
TargetUserID: ban.TargetUserID,
|
||
CurrentStatus: currentStatus,
|
||
OperatorType: releaseOperatorType(command.Status),
|
||
OperatorID: command.OperatorAdminID,
|
||
Reason: "admin_user_unban:" + ban.BanID,
|
||
RequestID: command.RequestID,
|
||
NowMs: command.NowMs,
|
||
})
|
||
if err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
if err := tx.Commit(); err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
user, err := r.GetUser(ctx, ban.TargetUserID)
|
||
if err != nil {
|
||
return userservice.AdminUserBanRelease{}, err
|
||
}
|
||
return userservice.AdminUserBanRelease{Ban: ban, User: user, Restored: restored}, nil
|
||
}
|
||
|
||
// claimAdminBanReleaseCommand 把一次手动/自动释放固定为不可变命令载荷。
|
||
// INSERT IGNORE 会在同一 command_id 的并发请求间形成行级串行化,第二个请求提交后只读取首个 resolved_ban_id。
|
||
func claimAdminBanReleaseCommand(ctx context.Context, tx *sql.Tx, command userservice.ReleaseAdminUserBanCommand) (bool, string, error) {
|
||
commandID := strings.TrimSpace(command.RequestID)
|
||
if commandID == "" {
|
||
return false, "", xerr.New(xerr.InvalidArgument, "admin unban request_id is required")
|
||
}
|
||
requestedBanID := strings.TrimSpace(command.BanID)
|
||
result, err := tx.ExecContext(ctx, `
|
||
INSERT IGNORE INTO admin_user_ban_release_commands (
|
||
app_code, command_id, target_user_id, requested_ban_id, resolved_ban_id,
|
||
release_status, operator_admin_id, reason, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, '', ?, ?, ?, ?, ?)`,
|
||
appcode.FromContext(ctx), commandID, command.TargetUserID, requestedBanID,
|
||
command.Status, command.OperatorAdminID, command.Reason, command.NowMs, command.NowMs,
|
||
)
|
||
if err != nil {
|
||
return false, "", err
|
||
}
|
||
inserted, err := result.RowsAffected()
|
||
if err != nil {
|
||
return false, "", err
|
||
}
|
||
if inserted == 1 {
|
||
return false, "", nil
|
||
}
|
||
|
||
var targetUserID int64
|
||
var storedRequestedBanID, resolvedBanID, releaseStatus, reason string
|
||
var operatorAdminID int64
|
||
if err := tx.QueryRowContext(ctx, `
|
||
SELECT target_user_id, requested_ban_id, resolved_ban_id, release_status, operator_admin_id, reason
|
||
FROM admin_user_ban_release_commands
|
||
WHERE app_code = ? AND command_id = ?
|
||
FOR UPDATE`, appcode.FromContext(ctx), commandID).Scan(
|
||
&targetUserID, &storedRequestedBanID, &resolvedBanID, &releaseStatus, &operatorAdminID, &reason,
|
||
); err != nil {
|
||
return false, "", err
|
||
}
|
||
if targetUserID != command.TargetUserID || storedRequestedBanID != requestedBanID ||
|
||
releaseStatus != command.Status || operatorAdminID != command.OperatorAdminID || reason != command.Reason {
|
||
return false, "", xerr.New(xerr.Conflict, "admin unban command payload does not match existing command")
|
||
}
|
||
if strings.TrimSpace(resolvedBanID) == "" {
|
||
return false, "", xerr.New(xerr.Conflict, "admin unban command has no resolved ban")
|
||
}
|
||
return true, resolvedBanID, nil
|
||
}
|
||
|
||
type managedBanRestoreCommand struct {
|
||
TargetUserID int64
|
||
CurrentStatus userdomain.Status
|
||
OperatorType string
|
||
OperatorID int64
|
||
Reason string
|
||
RequestID string
|
||
NowMs int64
|
||
}
|
||
|
||
// restoreUserAfterManagedBanRelease 在持有 users 行锁时检查全部可管理封禁来源并恢复账号。
|
||
// 最新 banned 日志必须来自 admin/manager 事实;历史无日志或其他平台封禁即使没有 active 事实也不会被自动恢复。
|
||
func restoreUserAfterManagedBanRelease(ctx context.Context, tx *sql.Tx, command managedBanRestoreCommand) (bool, error) {
|
||
if command.CurrentStatus != userdomain.StatusBanned {
|
||
return false, nil
|
||
}
|
||
activeCount, err := countActiveManagedBans(ctx, tx, command.TargetUserID)
|
||
if err != nil || activeCount > 0 {
|
||
return false, err
|
||
}
|
||
managed, err := latestBanBelongsToManagedFact(ctx, tx, command.TargetUserID)
|
||
if err != nil || !managed {
|
||
return false, err
|
||
}
|
||
if _, err := tx.ExecContext(ctx, `
|
||
UPDATE users SET status = 'active', updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ? AND status = 'banned'`,
|
||
command.NowMs, appcode.FromContext(ctx), command.TargetUserID,
|
||
); err != nil {
|
||
return false, err
|
||
}
|
||
if _, err := tx.ExecContext(ctx, `
|
||
INSERT INTO user_status_change_logs (
|
||
app_code, target_user_id, old_status, new_status, operator_type,
|
||
operator_user_id, reason, request_id, created_at_ms
|
||
) VALUES (?, ?, 'banned', 'active', ?, ?, ?, ?, ?)`,
|
||
appcode.FromContext(ctx), command.TargetUserID, command.OperatorType,
|
||
command.OperatorID, command.Reason, command.RequestID, command.NowMs,
|
||
); err != nil {
|
||
return false, err
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
func lockUserStatus(ctx context.Context, tx *sql.Tx, appCode string, userID int64) (userdomain.Status, error) {
|
||
var status string
|
||
if err := tx.QueryRowContext(ctx, `
|
||
SELECT status FROM users WHERE app_code = ? AND user_id = ? FOR UPDATE`, appCode, userID).Scan(&status); err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return "", xerr.New(xerr.NotFound, "user not found")
|
||
}
|
||
return "", err
|
||
}
|
||
return userdomain.Status(status), nil
|
||
}
|
||
|
||
func countActiveManagedBans(ctx context.Context, tx *sql.Tx, targetUserID int64) (int64, error) {
|
||
var count int64
|
||
err := tx.QueryRowContext(ctx, `
|
||
SELECT
|
||
(SELECT COUNT(*) FROM manager_user_blocks WHERE app_code = ? AND target_user_id = ? AND status = 'active') +
|
||
(SELECT COUNT(*) FROM admin_user_bans WHERE app_code = ? AND target_user_id = ? AND status = 'active')`,
|
||
appcode.FromContext(ctx), targetUserID, appcode.FromContext(ctx), targetUserID,
|
||
).Scan(&count)
|
||
return count, err
|
||
}
|
||
|
||
func latestBanBelongsToManagedFact(ctx context.Context, tx *sql.Tx, targetUserID int64) (bool, error) {
|
||
var reason string
|
||
err := tx.QueryRowContext(ctx, `
|
||
SELECT reason
|
||
FROM user_status_change_logs
|
||
WHERE app_code = ? AND target_user_id = ? AND new_status = 'banned'
|
||
ORDER BY created_at_ms DESC, id DESC
|
||
LIMIT 1`, appcode.FromContext(ctx), targetUserID).Scan(&reason)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return false, nil
|
||
}
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return strings.HasPrefix(reason, "manager_center_block:") || strings.HasPrefix(reason, adminBanStatusReasonPrefix), nil
|
||
}
|
||
|
||
func releaseOperatorType(status string) string {
|
||
if status == userservice.AdminUserBanStatusExpired || status == userservice.ManagerUserBlockStatusExpired {
|
||
return userservice.OperatorTypeSystem
|
||
}
|
||
return userservice.OperatorTypeAdmin
|
||
}
|
||
|
||
func revokeActiveUserSessions(ctx context.Context, tx *sql.Tx, appCode string, targetUserID int64, operatorUserID int64, requestID string, nowMs int64) ([]string, error) {
|
||
rows, err := tx.QueryContext(ctx, `
|
||
SELECT session_id
|
||
FROM auth_sessions
|
||
WHERE app_code = ? AND user_id = ? AND expires_at_ms > ? AND revoked_at_ms IS NULL
|
||
FOR UPDATE`, appCode, targetUserID, nowMs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
sessionIDs := make([]string, 0)
|
||
for rows.Next() {
|
||
var sessionID string
|
||
if err := rows.Scan(&sessionID); err != nil {
|
||
_ = rows.Close()
|
||
return nil, err
|
||
}
|
||
sessionIDs = append(sessionIDs, sessionID)
|
||
}
|
||
if err := rows.Close(); err != nil {
|
||
return nil, err
|
||
}
|
||
_, err = tx.ExecContext(ctx, `
|
||
UPDATE auth_sessions
|
||
SET revoked_at_ms = ?, revoked_reason = 'USER_BANNED', revoked_request_id = ?, revoked_by = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ? AND revoked_at_ms IS NULL`,
|
||
nowMs, requestID, revokedBy(operatorUserID), nowMs, appCode, targetUserID)
|
||
return sessionIDs, err
|
||
}
|
||
|
||
func (r *Repository) getAdminUserBan(ctx context.Context, banID string) (userservice.AdminUserBan, error) {
|
||
return scanAdminUserBan(r.db.QueryRowContext(ctx, adminBanSelectSQL()+` WHERE app_code = ? AND ban_id = ?`, appcode.FromContext(ctx), banID))
|
||
}
|
||
|
||
func (r *Repository) getAdminUserBanByCommand(ctx context.Context, commandID string) (userservice.AdminUserBan, error) {
|
||
return scanAdminUserBan(r.db.QueryRowContext(ctx, adminBanSelectSQL()+` WHERE app_code = ? AND command_id = ?`, appcode.FromContext(ctx), commandID))
|
||
}
|
||
|
||
func (r *Repository) getAdminUserBanForUpdate(ctx context.Context, tx *sql.Tx, banID string, targetUserID int64) (userservice.AdminUserBan, error) {
|
||
return scanAdminUserBan(tx.QueryRowContext(ctx, adminBanSelectSQL()+`
|
||
WHERE app_code = ? AND ban_id = ? AND target_user_id = ? FOR UPDATE`,
|
||
appcode.FromContext(ctx), banID, targetUserID))
|
||
}
|
||
|
||
func (r *Repository) getLatestActiveAdminUserBanForUpdate(ctx context.Context, tx *sql.Tx, targetUserID int64) (userservice.AdminUserBan, error) {
|
||
return scanAdminUserBan(tx.QueryRowContext(ctx, adminBanSelectSQL()+`
|
||
WHERE app_code = ? AND target_user_id = ? AND status = 'active'
|
||
ORDER BY created_at_ms DESC, ban_id DESC
|
||
LIMIT 1 FOR UPDATE`, appcode.FromContext(ctx), targetUserID))
|
||
}
|
||
|
||
func adminBanSelectSQL() string {
|
||
return `SELECT ban_id, command_id, target_user_id, target_old_status, expires_at_ms,
|
||
status, reason, operator_admin_id, created_at_ms, updated_at_ms,
|
||
released_by_admin_id, released_reason, released_at_ms
|
||
FROM admin_user_bans`
|
||
}
|
||
|
||
type adminBanScanner interface {
|
||
Scan(dest ...any) error
|
||
}
|
||
|
||
func scanAdminUserBan(scanner adminBanScanner) (userservice.AdminUserBan, error) {
|
||
var ban userservice.AdminUserBan
|
||
var oldStatus string
|
||
if err := scanner.Scan(
|
||
&ban.BanID, &ban.CommandID, &ban.TargetUserID, &oldStatus, &ban.ExpiresAtMs,
|
||
&ban.Status, &ban.Reason, &ban.OperatorAdminID, &ban.CreatedAtMs, &ban.UpdatedAtMs,
|
||
&ban.ReleasedByAdminID, &ban.ReleasedReason, &ban.ReleasedAtMs,
|
||
); err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return userservice.AdminUserBan{}, xerr.New(xerr.NotFound, "admin user ban not found")
|
||
}
|
||
return userservice.AdminUserBan{}, err
|
||
}
|
||
ban.TargetOldStatus = userdomain.Status(oldStatus)
|
||
return ban, nil
|
||
}
|
||
|
||
func adminBanDuplicate(err error) bool {
|
||
if err == nil {
|
||
return false
|
||
}
|
||
return strings.Contains(err.Error(), "Duplicate entry") || strings.Contains(err.Error(), "Error 1062")
|
||
}
|