用户头像与个人资料:集成 Gravatar 头像与上传自定义头像

📂 所属阶段:第三阶段 — 用户系统(安全篇)
🔗 相关章节:Flask-Login 实战 · 静态文件管理


1. Gravatar 头像

1.1 什么是 Gravatar?

Gravatar(Globally Recognized Avatar)让你用邮箱注册一个头像,全网通用。

1.2 获取 Gravatar URL

import hashlib

def get_gravatar_url(email, size=80):
    """生成 Gravatar URL"""
    # 1. 取邮箱 MD5(不区分大小写)
    email_hash = hashlib.md5(email.strip().lower().encode()).hexdigest()
    # 2. 拼接 URL
    return f"https://www.gravatar.com/avatar/{email_hash}?s={size}&d=identicon"

# 使用
gravatar_url = get_gravatar_url("alice@example.com")
# → https://www.gravatar.com/avatar/5aba7...d1?s=80&d=identicon

1.3 Gravatar 参数

参数说明
didenticon几何图形(默认)
dmp简单人像
dretro像素风格
dwavatar卡通脸
d404未找到时返回 404
s80头像尺寸(像素)
rg评分限制(g/pg/r/x)
def get_gravatar(email, size=80, rating="g", default="identicon"):
    h = hashlib.md5(email.strip().lower().encode()).hexdigest()
    return f"https://www.gravatar.com/avatar/{h}?s={size}&d={default}&r={rating}"

1.4 在模板中使用

<!-- 简单使用 -->
<img src="{{ get_gravatar(current_user.email, size=40) }}"
     alt="{{ current_user.username }}"
     class="rounded-circle">

<!-- 带默认图片 -->
<img src="{{ get_gravatar(user.email, 80) if user.email else '/static/img/default-avatar.png' }}"
     alt="{{ user.username }}">

1.5 Flask 中注册 Jinja2 全局函数

# app/extensions.py
import hashlib

def get_gravatar_url(email, size=80, rating="g", default="identicon"):
    h = hashlib.md5(email.strip().lower().encode()).hexdigest()
    return f"https://www.gravatar.com/avatar/{h}?s={size}&d={default}&r={rating}"

# app/__init__.py
def create_app():
    app = Flask(__name__)
    # ...
    app.jinja_env.globals["get_gravatar"] = get_gravatar_url
    return app

2. 自定义头像上传

2.1 模型扩展

# app/models/user.py
class User(UserMixin, db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(120), unique=True, nullable=False)
    username = db.Column(db.String(50))
    password_hash = db.Column(db.String(256))
    # 头像路径(相对路径)
    avatar = db.Column(db.String(200), default="")
    bio = db.Column(db.String(500))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def get_avatar(self, size=80):
        """优先使用自定义头像,否则用 Gravatar"""
        if self.avatar:
            return self.avatar
        if self.email:
            import hashlib
            h = hashlib.md5(self.email.strip().lower().encode()).hexdigest()
            return f"https://www.gravatar.com/avatar/{h}?s={size}&d=identicon"
        return "/static/img/default-avatar.png"

2.2 上传路由

# app/routes/profile.py
import os
import uuid
from flask import Blueprint, request, redirect, url_for, flash, current_app
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
from PIL import Image  # pip install Pillow

ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}

def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS

@profile_bp.route("/profile/avatar", methods=["POST"])
@login_required
def upload_avatar():
    if "avatar" not in request.files:
        flash("没有检测到文件!", "danger")
        return redirect(url_for("profile.edit_profile"))

    file = request.files["avatar"]
    if file.filename == "":
        flash("请选择一张图片!", "warning")
        return redirect(url_for("profile.edit_profile"))

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        ext = filename.rsplit(".", 1)[1].lower()
        new_filename = f"{uuid.uuid4().hex}.{ext}"

        upload_dir = os.path.join(
            current_app.root_path, "static", "uploads", "avatars"
        )
        os.makedirs(upload_dir, exist_ok=True)
        filepath = os.path.join(upload_dir, new_filename)

        # 处理图片:缩放到 200x200
        img = Image.open(file)
        img = img.convert("RGB")
        img.thumbnail((200, 200), Image.LANCZOS)
        img.save(filepath, "JPEG", quality=85)

        # 删除旧头像
        if current_user.avatar and current_user.avatar.startswith("/static"):
            old_path = os.path.join(
                current_app.root_path, current_user.avatar.lstrip("/")
            )
            if os.path.exists(old_path):
                os.remove(old_path)

        # 保存新头像路径
        current_user.avatar = f"/static/uploads/avatars/{new_filename}"
        db.session.commit()
        flash("头像已更新!", "success")

    else:
        flash("不支持的图片格式!", "danger")

    return redirect(url_for("profile.edit_profile"))

2.3 头像上传表单

<!-- templates/profile/avatar_form.html -->
<div class="avatar-upload">
    <img src="{{ current_user.get_avatar(200) }}"
         alt="当前头像" class="avatar-preview rounded-circle">

    <form method="POST" enctype="multipart/form-data"
          action="{{ url_for('profile.upload_avatar') }}">
        <input type="file" name="avatar" accept="image/*" required>
        <button type="submit" class="btn btn-primary btn-sm mt-2">上传新头像</button>
    </form>

    {% if current_user.avatar %}
    <a href="{{ url_for('profile.remove_avatar') }}"
       class="btn btn-outline-secondary btn-sm mt-2">恢复 Gravatar</a>
    {% endif %}
</div>

3. 小结

# 头像方案选择

# 1. Gravatar(零存储,全球通用)
h = hashlib.md5(email.lower().encode()).hexdigest()
avatar_url = f"https://www.gravatar.com/avatar/{h}"

# 2. 自定义上传(用户自由度更高)
# 需要:werkzeug + PIL + 文件存储

# 3. 组合方案(推荐)
# 优先自定义头像,无则用 Gravatar
def get_avatar(user):
    return user.avatar or get_gravatar(user.email)

💡 最佳实践:Gravatar 是零成本的默认方案,用户体验好。自定义上传则给用户更多选择,两者结合最佳。


🔗 扩展阅读