海南大学交友平台开发实战 day9(头像上传存入 SQLite+BLOB 存储 + 前后端联调避坑全记录)

海南大学交友平台开发实战 day9(头像上传存入 SQLite+BLOB 存储 + 前后端联调避坑全记录)

大家好,欢迎来到海南大学交友平台开发实战系列的第九天!在前八天的开发中,我们已经完成了登录注册、UI 布局、Flask 后端基础搭建、SQLite 好友机制、好友申请 / 同意 / 拒绝全流程功能,实现了用户关系的持久化存储。今天的核心任务围绕用户头像上传功能展开,彻底解决「图片直存本地文件夹」的问题,实现前端上传→Flask 后端接收→二进制 BLOB 存入 SQLite→前端读取数据库展示的全闭环,全程记录实战踩坑细节、报错解决方案和最优实现方案,帮助大家避开同类问题,高效完成头像功能开发。

本博文采用「前端 HTML/JS + 后端 Python Flask + 数据库 SQLite」技术栈,全程实战落地,包含完整可运行代码、报错精准分析、一步到位修复方案、功能验证流程,适合全栈开发学习,欢迎大家在评论区交流探讨开发过程中遇到的问题。

一、开发需求与技术选型

1. 核心需求

在交友平台中,用户头像是提升交互体验、增强用户辨识度的重要功能,结合前期开发基础,今日核心需求明确,重点解决"图片直存本地"的弊端,实现以下4点核心目标:

  • 摒弃传统头像存储方式:不再将图片保存在本地文件夹,避免文件夹冗余、路径混乱、用户删除/迁移项目时头像丢失等问题;

  • 实现标准流程闭环:用户在HTML页面上传头像 → Flask后端接收并处理图片 → 将图片转为二进制格式存入SQLite数据库 → 前端通过后端接口读取数据库中的图片并正常展示;

  • 兼容性保障:完全兼容现有user表结构,不丢失历史用户数据,不破坏已完成的登录、好友申请/同意等核心功能,实现无缝集成;

  • 异常处理:添加图片格式校验、大小限制、数据库读写异常捕获,确保头像上传、展示过程稳定,避免报错导致整个项目崩溃。

2. 技术方案选型(关键避坑点)

图片存入数据库,主流有两种格式可选:SQLite BLOB二进制格式、Base64字符串格式,结合项目实际场景(轻量、高效、适配SQLite),最终选型如下,同时对比两种方案,帮大家避开选型误区:

✅ 最终选型:SQLite BLOB 二进制格式(推荐)

BLOB(Binary Large Object,二进制大对象)是SQLite专门用于存储二进制数据的字段类型,完美适配图片、音频等文件的存储,也是本次开发的核心方案,优势如下:

  • 体积最小:直接存储图片原始二进制数据,比Base64格式小33%,大幅节省数据库存储空间,避免数据库快速膨胀;

  • 读取高效:后端可直接读取数据库中的BLOB数据,转为图片格式返回给前端,无需额外转换步骤,加载速度更快;

  • 适配性强:与SQLite兼容性极佳,无需额外依赖,可直接通过SQL语句实现读写操作,契合本次Flask+SQLite的技术栈;

  • 易于维护:头像与用户数据存在同一数据库,便于备份、迁移,避免本地文件夹与数据库数据不一致的问题。

❌ 不推荐:Base64 字符串格式(避坑提醒)

很多新手会选择将图片转为Base64字符串存入数据库,看似前端可直接粘贴显示,实则存在致命弊端,完全不适合长期存储:

  • 体积冗余:Base64会将图片二进制数据转为字符串,体积比原图大33%,一张100KB的头像,转为Base64后会变成133KB,大量用户使用会导致数据库迅速膨胀;

  • 读写缓慢:字符串读写速度远低于二进制数据,查询、加载头像时会明显卡顿,影响用户体验;

  • 维护繁琐:Base64字符串过长,不利于数据库查询、调试,且无法直接与图片工具联动,后续修改头像格式、压缩图片难度较大。

补充说明:Base64仅适合"前端上传前预览"(临时使用),绝对不适合长期存入数据库,这是本次开发的第一个核心避坑点。

二、前期准备与数据库改造(避坑核心)

本次开发无需新建数据库,只需在现有user表中新增2个字段,用于存储头像二进制数据和图片格式(便于前端正确渲染),重点解决"字段缺失导致的报错",全程实操,避免踩坑。

1. 现有user表结构(已完成,无需修改)

前八天开发中,我们已创建user表,用于存储用户基本信息,核心字段如下(完整SQL可直接复用,确保与你的项目一致):

sql 复制代码
-- 现有user表结构(无需修改)
CREATE TABLE IF NOT EXISTS user (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT NOT NULL UNIQUE,  -- 用户名(唯一,用于登录)
    password TEXT NOT NULL,         -- 密码(简化存储,实际开发需加密)
    nickname TEXT NOT NULL,         -- 用户昵称(展示用)
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP  -- 账号创建时间
);

2. 新增头像相关字段(关键步骤)

在user表中新增2个字段,用于存储头像BLOB数据和图片格式,避免后续读取时出现"图片无法渲染"的问题:

sql 复制代码
-- 给现有user表新增头像相关字段(无需删表、不丢数据)
ALTER TABLE user ADD COLUMN avatar BLOB;  -- 存储头像二进制数据
ALTER TABLE user ADD COLUMN avatar_type TEXT;  -- 存储头像格式(如image/png、image/jpeg)

踩坑点1:直接运行ALTER语句报错,提示"no such column: avatar"

【报错现象】:直接在SQLite工具中运行上述ALTER语句,或在Flask中执行时,报错"no such column: avatar",误以为字段未添加成功;

【报错原因】:有两个核心原因,一是数据库连接路径错误,未连接到项目实际使用的hainanu.db;二是多次执行ALTER语句,字段已存在,重复添加导致报错;

【解决方法】:编写Python脚本,先校验字段是否存在,不存在再添加,避免重复执行,脚本可直接复制运行(命名为fix_db.py):

python 复制代码
# fix_db.py:修复数据库字段,避免重复添加报错
import sqlite3

# 连接项目实际使用的数据库(路径务必正确,默认在instance文件夹下)
conn = sqlite3.connect("instance/hainanu.db")
cursor = conn.cursor()

# 校验并添加avatar字段(BLOB类型)
cursor.execute("PRAGMA table_info(user);")  # 查询user表所有字段
columns = [col[1] for col in cursor.fetchall()]  # 提取字段名列表
if 'avatar' not in columns:
    cursor.execute("ALTER TABLE user ADD COLUMN avatar BLOB;")
    print("✅ 成功添加 avatar 字段(BLOB类型)")
else:
    print("ℹ️ avatar 字段已存在,无需重复添加")

# 校验并添加avatar_type字段(TEXT类型)
if 'avatar_type' not in columns:
    cursor.execute("ALTER TABLE user ADD COLUMN avatar_type TEXT;")
    print("✅ 成功添加 avatar_type 字段(TEXT类型)")
else:
    print("ℹ️ avatar_type 字段已存在,无需重复添加")

conn.commit()
conn.close()
print("🎉 数据库字段修复完成,重启Flask服务即可使用!")

【运行方法】:在终端执行"python fix_db.py",运行一次即可,无需重复执行,所有历史用户数据不会丢失,完美兼容现有表结构。

3. 数据库连接优化(避免外键失效、读写异常)

延续前期开发的数据库连接逻辑,新增"启用外键约束"和"异常捕获",确保后续头像读写时数据库连接稳定,避免因连接问题导致的报错:

python 复制代码
# Flask数据库连接函数(优化版,可直接复制到app.py)
import sqlite3
from flask import Flask, g

app = Flask(__name__)
app.secret_key = "hainanu_avatar_platform"  # 与你的项目秘钥保持一致

# 数据库连接上下文管理(避免重复连接,提升效率)
def get_db():
    if 'db' not in g:
        # 连接数据库(路径务必正确)
        g.db = sqlite3.connect("instance/hainanu.db")
        g.db.row_factory = sqlite3.Row  # 支持用字段名取值(如user['avatar'])
        g.db.execute("PRAGMA foreign_keys = ON;")  # 启用外键约束,避免脏数据
    return g.db

# 关闭数据库连接
@app.teardown_appcontext
def teardown_db(e):
    db = g.pop('db', None)
    if db is not None:
        db.close()

# 初始化数据库(首次运行执行,后续无需重复)
def init_db():
    with app.app_context():
        db = get_db()
        # 此处可添加表结构初始化代码(若未创建user表可执行)
        print("✅ 数据库初始化完成")

三、Flask后端接口开发(核心代码+全踩坑修复)

后端核心开发3个接口,分别实现"头像上传""头像读取""头像修改",全程遵循"最小改动原则",不破坏现有登录、好友功能,所有代码可直接复制到app.py中使用,重点标注踩坑点和修复思路。

1. 接口1:头像上传(/upload_avatar)

功能:接收前端上传的头像文件,校验文件格式、大小,将图片转为二进制数据,存入user表的avatar字段,同时存储图片格式(avatar_type),返回上传结果。

python 复制代码
# 1. 头像上传接口(POST请求)
from flask import request, jsonify, session, g
import io

@app.route("/upload_avatar", methods=["POST"])
def upload_avatar():
    # 1. 校验用户是否登录(避免未登录用户上传头像)
    user_id = session.get("user_id")
    if not user_id:
        return jsonify({"code": 401, "msg": "请先登录再上传头像"}), 401
    
    # 2. 校验是否上传文件(踩坑点2:未校验文件导致报错)
    if "avatar" not in request.files:
        return jsonify({"code": 400, "msg": "未选择头像文件,请重新上传"}), 400
    file = request.files["avatar"]
    if file.filename == "":
        return jsonify({"code": 400, "msg": "未选择头像文件,请重新上传"}), 400
    
    # 3. 校验文件格式(仅允许png、jpg、jpeg,避免恶意文件)
    allowed_extensions = {"png", "jpg", "jpeg"}
    file_ext = file.filename.rsplit(".", 1)[1].lower()  # 获取文件后缀
    if file_ext not in allowed_extensions:
        return jsonify({"code": 400, "msg": "仅支持png、jpg、jpeg格式的图片"}), 400
    
    # 4. 校验文件大小(限制10MB以内,避免数据库过大)
    max_size = 10 * 1024 * 1024  # 10MB
    if file.content_length > max_size:
        return jsonify({"code": 400, "msg": "头像大小不能超过10MB,请压缩后上传"}), 400
    
    try:
        # 5. 读取图片二进制数据(核心步骤)
        avatar_data = file.read()
        # 6. 获取图片格式(用于前端渲染)
        avatar_type = file.content_type  # 如:image/png、image/jpeg
        
        # 7. 更新数据库(将BLOB数据存入user表)
        db = get_db()
        db.execute(
            "UPDATE user SET avatar = ?, avatar_type = ? WHERE id = ?",
            (avatar_data, avatar_type, user_id)
        )
        db.commit()
        return jsonify({"code": 200, "msg": "头像上传成功"}), 200
    
    except Exception as e:
        # 异常捕获:避免数据库读写失败导致的项目崩溃
        db.rollback()
        return jsonify({"code": 500, "msg": f"头像上传失败:{str(e)}"}), 500

踩坑点2:未校验上传文件,导致"request.files['avatar']"报错

【报错现象】:前端未选择文件,直接点击上传,后端报错"KeyError: 'avatar'",导致接口崩溃;

【解决方法】:先校验"avatar"是否在request.files中,再判断文件是否为空,双重校验,避免报错,同时给前端返回清晰的提示信息。

踩坑点3:图片读取失败,提示"IOError: [Errno 2] No such file or directory"

【报错现象】:上传头像时,后端提示文件路径不存在,无法读取图片;

【报错原因】:新手误将"file.read()"写成"open(file.filename, 'rb').read()",试图读取本地文件路径,而前端上传的文件是临时文件,并非本地文件;

【解决方法】:直接使用"file.read()"读取前端上传的临时文件,无需打开本地文件,这是头像上传的核心易错点。

2. 接口2:头像读取(/get_avatar/<user_id>)

功能:前端通过用户ID,调用该接口,后端从数据库中读取对应用户的avatar(BLOB数据)和avatar_type(图片格式),转为图片格式返回给前端,用于头像展示,这是实现"数据库读取头像"的核心接口。

python 复制代码
# 2. 头像读取接口(GET请求,前端img标签直接调用)
@app.route("/get_avatar/<int:user_id>")
def get_avatar(user_id):
    try:
        db = get_db()
        # 查询当前用户的头像数据和格式
        user = db.execute(
            "SELECT avatar, avatar_type FROM user WHERE id = ?",
            (user_id,)
        ).fetchone()
        
        # 校验用户是否存在,以及是否上传头像
        if not user or not user["avatar"]:
            # 若未上传头像,可返回默认头像(此处简化,返回空,前端可自定义默认图)
            return "", 404
        
        # 核心:返回图片数据,设置正确的Content-Type,确保前端正常渲染
        response = make_response(user["avatar"])
        response.headers["Content-Type"] = user["avatar_type"] or "image/png"  # 兜底格式
        return response
    
    except Exception as e:
        return jsonify({"code": 500, "msg": f"读取头像失败:{str(e)}"}), 500

踩坑点4:前端无法渲染头像,提示"无法加载图片"

【报错现象】:后端接口返回200,但前端img标签无法渲染头像,显示"破损图片";

【报错原因】:未设置response.headers["Content-Type"],前端无法识别图片格式,将二进制数据当作普通文本处理;

【解决方法】:必须设置Content-Type为存储的avatar_type,若未获取到格式,兜底设为image/png,确保前端能正确识别图片。

3. 接口3:头像修改(/update_avatar)

功能:与上传接口逻辑类似,支持用户重新上传头像,覆盖数据库中原有BLOB数据,无需额外新增接口,可直接复用上传接口,此处单独写出,便于理解和维护。

python 复制代码
# 3. 头像修改接口(与上传逻辑一致,可单独调用,也可复用上传接口)
@app.route("/update_avatar", methods=["POST"])
def update_avatar():
    # 逻辑与upload_avatar完全一致,仅修改提示信息,可直接复制上传接口代码
    user_id = session.get("user_id")
    if not user_id:
        return jsonify({"code": 401, "msg": "请先登录再修改头像"}), 401
    
    if "avatar" not in request.files:
        return jsonify({"code": 400, "msg": "未选择新头像文件,请重新上传"}), 400
    file = request.files["avatar"]
    if file.filename == "":
        return jsonify({"code": 400, "msg": "未选择新头像文件,请重新上传"}), 400
    
    allowed_extensions = {"png", "jpg", "jpeg"}
    file_ext = file.filename.rsplit(".", 1)[1].lower()
    if file_ext not in allowed_extensions:
        return jsonify({"code": 400, "msg": "仅支持png、jpg、jpeg格式的图片"}), 400
    
    max_size = 10 * 1024 * 1024
    if file.content_length > max_size:
        return jsonify({"code": 400, "msg": "头像大小不能超过10MB,请压缩后上传"}), 400
    
    try:
        avatar_data = file.read()
        avatar_type = file.content_type
        db = get_db()
        db.execute(
            "UPDATE user SET avatar = ?, avatar_type = ? WHERE id = ?",
            (avatar_data, avatar_type, user_id)
        )
        db.commit()
        return jsonify({"code": 200, "msg": "头像修改成功"}), 200
    
    except Exception as e:
        db.rollback()
        return jsonify({"code": 500, "msg": f"头像修改失败:{str(e)}"}), 500

四、前端页面开发(HTML/JS + 前后端联调)

前端开发2个核心页面片段(可嵌入现有个人中心、设置页面),实现"头像上传/修改"和"头像展示",采用原生JS发送请求,确保与后端接口联动,同时添加上传预览功能(使用Base64临时展示,不存数据库)。

1. 头像上传/修改页面(avatar_upload.html)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>上传/修改头像</title>
    <style>
        .container { width: 400px; margin: 50px auto; text-align: center; }
        .avatar-preview { width: 150px; height: 150px; border-radius: 50%; border: 2px dashed #ccc; margin: 0 auto 20px; overflow: hidden; }
        .avatar-preview img { width: 100%; height: 100%; object-fit: cover; }
        input[type="file"] { display: none; }
        .upload-btn { padding: 8px 20px; background: #4169E1; color: white; border: none; border-radius: 4px; cursor: pointer; }
        .submit-btn { padding: 8px 20px; background: #32CD32; color: white; border: none; border-radius: 4px; cursor: pointer; margin-left: 10px; }
        .msg { margin-top: 20px; font-size: 14px; color: red; }
    </style>
</head>
<body>
    <div class="container">
        <h2>上传/修改头像</h2&gt;
        <!-- 头像预览区(Base64临时展示,不存数据库) -->
        <div class="avatar-preview">
            <img id="previewImg" src="" alt="头像预览"&gt;
        &lt;/div&gt;
        <!-- 隐藏文件选择框,通过按钮触发 -->
        <label for="avatarFile" class="upload-btn">选择头像</label>
        <input type="file" id="avatarFile" accept="image/*" onchange="previewAvatar(event)">
        <button class="submit-btn" onclick="uploadAvatar()">确认上传</button>
        <div class="msg" id="msg"></div>
    </div>

    <script>
        // 1. 头像预览(使用Base64,临时展示,不存数据库)
        function previewAvatar(event) {
            const file = event.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            // 转为Base64字符串,用于预览
            reader.onload = function(e) {
                document.getElementById("previewImg").src = e.target.result;
            }
            reader.readAsDataURL(file);
        }

        // 2. 上传头像(调用后端接口)
        function uploadAvatar() {
            const file = document.getElementById("avatarFile").files[0];
            const msgDom = document.getElementById("msg");
            if (!file) {
                msgDom.innerText = "请先选择头像文件";
                return;
            }

            // 构建FormData,用于上传文件(踩坑点5:请求格式错误)
            const formData = new FormData();
            formData.append("avatar", file);

            // 发送POST请求
            fetch("/upload_avatar", {
                method: "POST",
                body: formData  // 注意:上传文件无需设置Content-Type,浏览器自动识别
            })
            .then(response => response.json())
            .then(data => {
                if (data.code === 200) {
                    msgDom.innerText = data.msg;
                    msgDom.style.color = "green";
                    // 上传成功后,刷新页面或重新读取头像
                    setTimeout(() => {
                        window.location.reload();
                    }, 1000);
                } else {
                    msgDom.innerText = data.msg;
                    msgDom.style.color = "red";
                }
            })
            .catch(error => {
                msgDom.innerText = "网络错误,请重试";
                console.error("上传失败:", error);
            });
        }
    </script>
</body>
</html>

2. 头像展示页面(avatar_show.html)

可嵌入个人中心、好友列表等页面,通过调用后端/get_avatar/<user_id>接口,展示用户头像,适配现有页面布局:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>个人中心</title>
    <style>
        .user-info { width: 500px; margin: 50px auto; display: flex; align-items: center; }
        .avatar { width: 120px; height: 120px; border-radius: 50%; margin-right: 30px; border: 3px solid #4169E1; }
        .info h2 { margin: 0 0 10px 0; }
        .info p { margin: 5px 0; font-size: 16px; }
        .edit-btn { padding: 6px 15px; background: #4169E1; color: white; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; }
    </style>
</head>
<body>
    <div class="user-info">
        <!-- 核心:调用后端接口,展示数据库中的头像 -->
        <img class="avatar" src="/get_avatar/1" alt="用户头像" onerror="this.src='默认头像路径'">
        <div class="info">
            <h2>我的个人中心</h2>
            <p>用户名:test_user</p>
            <p>昵称:海南大学小帅</p>
            <a href="avatar_upload.html" class="edit-btn">修改头像</a>
        </div>
    </div>

    <script>
        // 可选:头像加载失败时,显示默认头像
        document.querySelector(".avatar").addEventListener("error", function() {
            this.src = "static/default_avatar.png";  // 替换为你的默认头像路径
        });
    </script>
</body>
</html>

踩坑点5:前端上传文件请求格式错误,后端无法获取文件

【报错现象】:前端上传头像时,后端request.files为空,无法获取到avatar文件,提示"未选择头像文件";

【报错原因】:上传文件时,错误设置了"Content-Type: application/json",且未使用FormData传递文件,导致后端无法解析;

【解决方法】:上传文件必须使用FormData传递,且无需手动设置Content-Type,浏览器会自动识别为"multipart/form-data"格式,确保后端能正常获取文件。

五、今日开发总结

今日核心完成了用户头像上传功能的全闭环开发,彻底解决了"图片直存本地文件夹"的弊端,实现了前端上传→Flask后端接收→二进制BLOB存入SQLite→前端读取展示的完整流程,同时兼容现有项目结构,不丢失历史数据、不破坏已实现的登录、好友功能。

全程记录了5个核心踩坑点,从数据库字段添加、后端接口开发到前端联调,每个报错都给出了精准分析和一步到位的修复方案,所有代码均可直接复制使用,无需额外修改,大幅降低开发难度。重点掌握了SQLite BLOB格式的使用技巧,明确了Base64格式的适用场景,避开了新手常见的选型和开发误区。

今日开发的所有代码均可直接复制使用,头像功能已实现完整交互,后续可继续完善头像裁剪、压缩、默认头像设置等细节功能,进一步优化前端展示效果,适配手机端布局。欢迎大家在评论区交流开发过程中遇到的问题,一起高效避坑、高效开发!

关注我,后续我也会持续更新项目开发进度,分享更多 Python 前端全栈开发相关的实战经验,以及 SQLite 数据库操作、Flask 前后端联调的实战技巧。

相关推荐
FreakStudio3 小时前
嘉立创开源:应该是全网MicroPython教程最多的开发板
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy
上天_去_做颗惺星 EVE_BLUE3 小时前
接口自动化测试全流程:pytest 用例收集、并行执行、Allure 报告合并与上传
python·pytest
chushiyunen3 小时前
python fastapi使用、uvicorn
开发语言·python·fastapi
落魄江湖行3 小时前
基础篇六 Nuxt4 状态管理:useState 的正确用法
前端·vue.js·typescript·nuxt4
Trouvaille ~3 小时前
【MySQL篇】内置函数:数据处理的利器
数据库·mysql·面试·数据清洗·数据处理·dql·基础入门
jerrywus3 小时前
手机控制 AI 编程?Paseo 让你随时随地跑 Claude Code / Codex
前端·agent·claude
迦南的迦 亚索的索3 小时前
PYTHON_DAY20_数据库
数据库·oracle
GISer_Jing3 小时前
前端视频技术全解析:从编解码到渲染优化
前端·音视频·状态模式
咕白m6253 小时前
Python 高效添加与管理 Excel 工作表
后端·python