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" ) // SetUserStatus 原子更新用户主状态,并在非 active 状态下吊销该用户所有 active refresh session。 func (r *Repository) SetUserStatus(ctx context.Context, command userservice.UserStatusCommand) (userservice.UserStatusPersistenceResult, error) { if r == nil || r.db == nil { return userservice.UserStatusPersistenceResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") } appCode := appcode.Normalize(command.AppCode) if appCode == "" { appCode = appcode.FromContext(ctx) } tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userservice.UserStatusPersistenceResult{}, err } defer 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 err == sql.ErrNoRows { return userservice.UserStatusPersistenceResult{}, xerr.New(xerr.NotFound, "user not found") } return userservice.UserStatusPersistenceResult{}, err } _, err = tx.ExecContext(ctx, ` UPDATE users SET status = ?, updated_at_ms = ? WHERE app_code = ? AND user_id = ? `, string(command.Status), command.NowMs, appCode, command.TargetUserID) if err != nil { return userservice.UserStatusPersistenceResult{}, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?) `, appCode, command.TargetUserID, oldStatus, string(command.Status), command.OperatorType, command.OperatorUserID, command.Reason, command.RequestID, command.NowMs); err != nil { return userservice.UserStatusPersistenceResult{}, err } sessionIDs := make([]string, 0) if command.Status != userdomain.StatusActive { rows, err := tx.QueryContext(ctx, ` SELECT session_id FROM auth_sessions WHERE app_code = ? AND user_id = ? AND expires_at_ms > ? FOR UPDATE `, appCode, command.TargetUserID, command.NowMs) if err != nil { return userservice.UserStatusPersistenceResult{}, err } for rows.Next() { var sessionID string if err := rows.Scan(&sessionID); err != nil { _ = rows.Close() return userservice.UserStatusPersistenceResult{}, err } sessionIDs = append(sessionIDs, sessionID) } if err := rows.Close(); err != nil { return userservice.UserStatusPersistenceResult{}, err } if _, err := tx.ExecContext(ctx, ` UPDATE auth_sessions SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ? WHERE app_code = ? AND user_id = ? AND revoked_at_ms IS NULL `, command.NowMs, userStatusRevokeReason(command.Status), command.RequestID, revokedBy(command.OperatorUserID), command.NowMs, appCode, command.TargetUserID); err != nil { return userservice.UserStatusPersistenceResult{}, err } } if err := tx.Commit(); err != nil { return userservice.UserStatusPersistenceResult{}, err } user, err := r.GetUser(appcode.WithContext(ctx, appCode), command.TargetUserID) if err != nil { return userservice.UserStatusPersistenceResult{}, err } return userservice.UserStatusPersistenceResult{User: user, RevokedSessionIDs: sessionIDs}, nil } func userStatusRevokeReason(status userdomain.Status) string { switch status { case userdomain.StatusBanned: return "USER_BANNED" case userdomain.StatusDisabled: return "USER_DISABLED" default: return "USER_STATUS_CHANGED" } } func revokedBy(operatorUserID int64) string { if operatorUserID > 0 { return "user_status_operator" } return "user_status" } // CreateManagerUserBlock 写入经理封禁记录;用户主状态修改由 service 层随后调用 SetUserStatus 完成。 func (r *Repository) CreateManagerUserBlock(ctx context.Context, command userservice.ManagerUserBlockCommand, target userdomain.User) (userservice.ManagerUserBlock, error) { if r == nil || r.db == nil { return userservice.ManagerUserBlock{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") } appCode := appcode.FromContext(ctx) blockID := idgen.New("mblock") _, err := r.db.ExecContext(ctx, ` INSERT INTO manager_user_blocks ( app_code, block_id, command_id, manager_user_id, target_user_id, target_display_user_id, target_username, target_avatar, country, target_old_status, blocked_until_ms, status, reason, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)`, appCode, blockID, command.CommandID, command.ManagerUserID, command.TargetUserID, target.CurrentDisplayUserID, target.Username, target.Avatar, target.Country, string(target.Status), command.BlockedUntilMS, command.Reason, command.NowMS, command.NowMS, ) if err != nil { if managerBlockDuplicate(err) { return r.getManagerUserBlockByCommand(ctx, command.CommandID) } return userservice.ManagerUserBlock{}, err } return r.getManagerUserBlock(ctx, blockID) } func (r *Repository) ListManagerUserBlocks(ctx context.Context, managerUserID int64, status string, pageSize int32) ([]userservice.ManagerUserBlock, error) { if r == nil || r.db == nil { return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured") } rows, err := r.db.QueryContext(ctx, managerBlockSelectSQL()+` WHERE app_code = ? AND manager_user_id = ? AND status = ? ORDER BY created_at_ms DESC, block_id DESC LIMIT ?`, appcode.FromContext(ctx), managerUserID, status, pageSize, ) if err != nil { return nil, err } defer rows.Close() return scanManagerUserBlocks(rows) } func (r *Repository) ReleaseManagerUserBlock(ctx context.Context, command userservice.ReleaseManagerUserBlockCommand) (userservice.ManagerUserBlockRelease, error) { return r.releaseManagerUserBlock(ctx, command.BlockID, command.ManagerUserID, command.Status, command.Reason, command.NowMS) } func (r *Repository) ExpireManagerUserBlocks(ctx context.Context, nowMS int64, limit int32) ([]userservice.ManagerUserBlockRelease, 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 block_id, manager_user_id FROM manager_user_blocks WHERE app_code = ? AND status = 'active' AND blocked_until_ms <= ? ORDER BY blocked_until_ms ASC, block_id ASC LIMIT ?`, appcode.FromContext(ctx), nowMS, limit, ) if err != nil { return nil, err } pending := make([]struct { blockID string managerUserID int64 }, 0) for rows.Next() { var item struct { blockID string managerUserID int64 } if err := rows.Scan(&item.blockID, &item.managerUserID); err != nil { _ = rows.Close() return nil, err } pending = append(pending, item) } if err := rows.Close(); err != nil { return nil, err } releases := make([]userservice.ManagerUserBlockRelease, 0, len(pending)) for _, item := range pending { // 逐条释放而不是批量 UPDATE,是为了给每个目标用户单独计算是否还存在其他 active 经理封禁。 release, err := r.releaseManagerUserBlock(ctx, item.blockID, item.managerUserID, userservice.ManagerUserBlockStatusExpired, "manager_center_block_expired", nowMS) if err != nil { return releases, err } releases = append(releases, release) } return releases, nil } func (r *Repository) releaseManagerUserBlock(ctx context.Context, blockID string, managerUserID int64, status string, reason string, nowMS int64) (userservice.ManagerUserBlockRelease, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return userservice.ManagerUserBlockRelease{}, err } defer func() { _ = tx.Rollback() }() block, err := r.getManagerUserBlockForUpdate(ctx, tx, blockID, managerUserID) if err != nil { return userservice.ManagerUserBlockRelease{}, err } if block.Status != userservice.ManagerUserBlockStatusActive { if err := tx.Commit(); err != nil { return userservice.ManagerUserBlockRelease{}, err } return userservice.ManagerUserBlockRelease{Block: block}, nil } _, err = tx.ExecContext(ctx, ` UPDATE manager_user_blocks SET status = ?, released_reason = ?, released_at_ms = ?, updated_at_ms = ? WHERE app_code = ? AND block_id = ?`, status, reason, nowMS, nowMS, appcode.FromContext(ctx), blockID, ) if err != nil { return userservice.ManagerUserBlockRelease{}, err } block.Status = status block.UpdatedAtMS = nowMS activeCount, err := r.countActiveManagerBlocks(ctx, tx, block.TargetUserID) if err != nil { return userservice.ManagerUserBlockRelease{}, err } // 只在“没有其他经理封禁 + 原状态是 active + 最近一次 ban 由经理封禁写入”时恢复,避免误解后台封禁。 latestManagerBan, err := r.latestBanBelongsToManagerBlock(ctx, tx, block.TargetUserID) if err != nil { return userservice.ManagerUserBlockRelease{}, err } if err := tx.Commit(); err != nil { return userservice.ManagerUserBlockRelease{}, err } return userservice.ManagerUserBlockRelease{ Block: block, ShouldRestore: activeCount == 0 && block.TargetOldStatus == userdomain.StatusActive && latestManagerBan, }, nil } func (r *Repository) getManagerUserBlock(ctx context.Context, blockID string) (userservice.ManagerUserBlock, error) { row := r.db.QueryRowContext(ctx, managerBlockSelectSQL()+`WHERE app_code = ? AND block_id = ?`, appcode.FromContext(ctx), blockID) return scanManagerUserBlock(row) } func (r *Repository) getManagerUserBlockByCommand(ctx context.Context, commandID string) (userservice.ManagerUserBlock, error) { row := r.db.QueryRowContext(ctx, managerBlockSelectSQL()+`WHERE app_code = ? AND command_id = ?`, appcode.FromContext(ctx), commandID) return scanManagerUserBlock(row) } func (r *Repository) getManagerUserBlockForUpdate(ctx context.Context, tx *sql.Tx, blockID string, managerUserID int64) (userservice.ManagerUserBlock, error) { row := tx.QueryRowContext(ctx, managerBlockSelectSQL()+` WHERE app_code = ? AND block_id = ? AND manager_user_id = ? FOR UPDATE`, appcode.FromContext(ctx), blockID, managerUserID, ) return scanManagerUserBlock(row) } func (r *Repository) countActiveManagerBlocks(ctx context.Context, tx *sql.Tx, targetUserID int64) (int64, error) { var count int64 err := tx.QueryRowContext(ctx, ` SELECT COUNT(*) FROM manager_user_blocks WHERE app_code = ? AND target_user_id = ? AND status = 'active'`, appcode.FromContext(ctx), targetUserID, ).Scan(&count) return count, err } func (r *Repository) latestBanBelongsToManagerBlock(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:"), nil } func managerBlockSelectSQL() string { return ` SELECT block_id, manager_user_id, target_user_id, target_display_user_id, target_username, target_avatar, country, target_old_status, blocked_until_ms, status, reason, created_at_ms, updated_at_ms FROM manager_user_blocks ` } func scanManagerUserBlocks(rows *sql.Rows) ([]userservice.ManagerUserBlock, error) { items := make([]userservice.ManagerUserBlock, 0) for rows.Next() { item, err := scanManagerUserBlockRows(rows) if err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } type managerBlockScanner interface { Scan(dest ...any) error } func scanManagerUserBlock(row managerBlockScanner) (userservice.ManagerUserBlock, error) { item, err := scanManagerUserBlockRows(row) if errors.Is(err, sql.ErrNoRows) { return userservice.ManagerUserBlock{}, xerr.New(xerr.NotFound, "manager block not found") } return item, err } func scanManagerUserBlockRows(row managerBlockScanner) (userservice.ManagerUserBlock, error) { var item userservice.ManagerUserBlock var oldStatus string if err := row.Scan( &item.BlockID, &item.ManagerUserID, &item.TargetUserID, &item.TargetDisplayUserID, &item.TargetUsername, &item.TargetAvatar, &item.Country, &oldStatus, &item.BlockedUntilMS, &item.Status, &item.Reason, &item.CreatedAtMS, &item.UpdatedAtMS, ); err != nil { return userservice.ManagerUserBlock{}, err } item.TargetOldStatus = userdomain.Status(oldStatus) return item, nil } func managerBlockDuplicate(err error) bool { if err == nil { return false } return strings.Contains(err.Error(), "Duplicate entry") || strings.Contains(err.Error(), "Error 1062") }