You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Active Storage为Ruby on Rails用户添加头像功能?

问题概述

首次在Ruby on Rails中使用Active Storage实现用户头像上传功能,按官方文档操作但未成功。需求为:用户仅通过姓名、用户名、密码、邮箱完成注册;登录后可在个人页上传并查看头像。以下是相关代码及问题排查、解决方案:


我的代码

1. User模型(User.rb)

class User < ApplicationRecord
    has_secure_password
    has_many :games, dependent: :destroy
    
    validates :first_name, presence: true
    validates :last_name, presence: true
    validates :email, presence: true
    validates :password, presence: true

    has_one_attached :avatar
end

2. Users控制器(users_controller.rb)

class UsersController < ApplicationController
    skip_before_action :authorized
    wrap_parameters format: []

    def index
        users = User.all
        render json: users
    end

    def find_user
        user = User.find_by(id: params[:id])
        if user
            render json: user, status: :not_found
        else
            render json: { error: "Not found" }, status: :not_found
        end
    end

    def show
        user = User.find_by(id: session[:user_id])
        if user
            render json: user
        else
            render json: { error: "Not authorized" }, status: :unauthorized
        end
    end

    def create
        user = User.create(user_params)
        if user.valid?
            session[:user_id] ||= user.id
            render json: user, status: :created
        else
            render json: {error: user.errors.full_messages }, status: :unprocessable_entity
        end
    end

    def update
        currentUser = User.find_by(id: params[:id])
        if currentUser
            currentUser.update(user_update_params)
            render json: currentUser, status: :accepted
        else
            render json: {error: currentUser.errors.full_messages }, status: :unprocessable_entity
        end
    end

    def destroy
        user = User.find_by(id: params[:id])
        if user
            user.destroy
            head :no_content
        else
            render json: {error: "user not found"}, status: :not_found
        end
    end

    def set_avatar
        user = User.find_by(id: params[:id])
        if user
            user.avatar.attach(params[:avatar])
        else
            render json: {error: "Profile image upload failed"}
        end
    end

    private
    def user_params
        params.permit(:first_name, :last_name, :email, :username, :password, :avatar)
    end

    def user_update_params
        params.permit(:first_name, :last_name, :email, :username, :password)
    end
end

3. 路由配置(routes.rb)

Rails.application.routes.draw do
  resources :sessions
  
  #--------------USER------------------------------
  get '/login', to: "sessions#create"
  get '/users', to: "users#index"
  post '/users', to: "users#create"
  post "/login", to: "sessions#create"
  delete '/logout', to: "sessions#destroy"
  patch '/users/:id', to: "users#update"
  get '/me', to: "users#show"
  get '/users/:id', to: "users#find_user"
  post '/users/:id', to: "users#set_avatar"

  # -------------GAMES Routes----------------------
  get '/games', to: 'games#index'
  get '/users/:id/games', to: "games#show"
  post '/games', to: "games#create"
  delete '/users/:id/games/:id', to: "games#destroy"

  #-------------Memo Routes------------------------
  get '/games/:id/memos', to: "memos#show"
  post '/games/:id/memos', to: "memos#create"
end

4. React上传组件

import React, { useState } from "react";
import IMAGES from '../images/Image';

export default function ProfilePicture({currentUser}){
    const[profileAvatar, setAvatar] = useState([])
    
    const fileTypes = [
        "image/apng",
        "image/bmp",
        "image/gif",
        "image/jpeg",
        "image/pjpeg",
        "image/png",
        "image/svg+xml",
        "image/tiff",
        "image/webp",
        "image/x-icon"
      ];
   
      function handleSubmit(e){
    //     fetch(`/users/${currentUser.id}` ,{
    //         method: "POST",
    //         headers: {"Content-Type": "application/json"},
    //         body: JSON.stringify({avatar:profileAvatar})
    //     })
    //     .then((r) => r.json())
    //     .then(data => {
    //         if (data.errors) {
    //             alert(data.errors)
    //          }
    //          else {
    //             setAvatar(data)
    //          }
    //     })
      }

    return(
        <div> 
            <img src={IMAGES.defaultProfile} alt="default_profile_image" className="profile_avatar"/>       
            <form onSubmit={handleSubmit}>
                <input type="file" id="avatar" name="avatar" accept={fileTypes} onChange={(e)=>setAvatar(e.target.value)}/>
                <button type="submit">Submit</button>
            </form>       
        </div>
    )
}

5. 用户表迁移文件

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :first_name
      t.string :last_name
      t.string :email
      t.string :username
      t.string :password_digest
      t.string :avatar
      t.timestamps
    end
  end
end

问题排查

  1. 数据库字段冗余:Active Storage不需要在users表中添加avatar字段,它通过独立的active_storage_blobs和active_storage_attachments表存储文件信息,手动添加的字段会引发冲突。
  2. 路由冲突:post '/users/:id', to: "users#set_avatar"与用户创建路由语义混淆,且未做权限校验,存在安全风险。
  3. 控制器缺陷:
    • set_avatar方法无成功响应返回,也未验证当前登录用户是否为目标用户;
    • user_params中包含avatar,但注册流程不需要上传头像,会引发参数错误。
  4. 前端逻辑错误:
    • 提交方法被注释,无法发起请求;
    • 用e.target.value获取文件(实际是文件路径字符串),而非文件对象;
    • 用JSON.stringify提交文件,正确方式应使用FormData;
    • 未处理上传后头像的展示逻辑。

解决方案

1. 修复数据库迁移

  • 生成移除avatar字段的迁移:
rails generate migration RemoveAvatarFromUsers avatar:string
  • 运行迁移:
rails db:migrate
  • 确保已执行Active Storage安装(未执行过的话):
rails active_storage:install
rails db:migrate

2. 调整路由

修改头像上传路由,语义更清晰且避免冲突:

post '/users/:id/avatar', to: "users#set_avatar"

3. 优化Users控制器

  • 完善set_avatar方法,添加权限校验和响应:
def set_avatar
  user = User.find_by(id: params[:id])
  # 验证当前登录用户为目标用户
  unless user&.id == session[:user_id]
    return render json: { error: "Not authorized" }, status: :unauthorized
  end

  if params[:avatar].present?
    user.avatar.attach(params[:avatar])
    # 返回包含头像URL的用户数据
    render json: {
      id: user.id,
      first_name: user.first_name,
      last_name: user.last_name,
      email: user.email,
      username: user.username,
      avatar_url: user.avatar.attached? ? rails_blob_url(user.avatar) : nil
    }, status: :ok
  else
    render json: { error: "No avatar file provided" }, status: :bad_request
  end
rescue => e
  render json: { error: e.message }, status: :unprocessable_entity
end
  • 修改user_params,移除avatar参数:
def user_params
  params.permit(:first_name, :last_name, :email, :username, :password)
end
  • 添加权限过滤器,确保敏感接口仅登录用户可访问:
before_action :authorized, only: [:show, :update, :set_avatar, :destroy]

4. 修复React上传组件

import React, { useState } from "react";
import IMAGES from '../images/Image';

export default function ProfilePicture({currentUser, onAvatarUpdate}){
    const[selectedFile, setSelectedFile] = useState(null)
    
    const fileTypes = [
        "image/apng", "image/bmp", "image/gif", "image/jpeg",
        "image/pjpeg", "image/png", "image/svg+xml", "image/tiff",
        "image/webp", "image/x-icon"
      ];
   
    function handleFileChange(e){
        const file = e.target.files[0];
        if(file && fileTypes.includes(file.type)){
            setSelectedFile(file);
        } else {
            alert("请选择有效的图片文件");
        }
    }

    function handleSubmit(e){
        e.preventDefault();
        if(!selectedFile) return alert("请选择图片");

        const formData = new FormData();
        formData.append('avatar', selectedFile);

        fetch(`/users/${currentUser.id}/avatar`, {
            method: "POST",
            body: formData
        })
        .then((r) => {
            if(!r.ok) throw new Error("上传失败");
            return r.json();
        })
        .then(data => {
            onAvatarUpdate(data.avatar_url); // 通知父组件更新头像显示
            alert("头像上传成功");
        })
        .catch(err => alert(err.message));
    }

    return(
        <div> 
            <img src={currentUser.avatar_url || IMAGES.defaultProfile} alt="profile" className="profile_avatar"/>       
            <form onSubmit={handleSubmit}>
                <input 
                    type="file" 
                    id="avatar" 
                    name="avatar" 
                    accept={fileTypes.join(',')} 
                    onChange={handleFileChange}
                />
                <button type="submit" disabled={!selectedFile}>Submit</button>
            </form>       
        </div>
    )
}
  • 父组件需传递onAvatarUpdate函数,用于更新用户的avatar_url状态,实现上传后实时显示新头像。

内容的提问来源于stack exchange,提问作者Jeffrey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 12:54:18