package user import ( "context" "database/sql" "hyapp/pkg/appcode" "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" }