海南大学交友平台开发实战 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>
<!-- 头像预览区(Base64临时展示,不存数据库) -->
<div class="avatar-preview">
<img id="previewImg" src="" alt="头像预览">
</div>
<!-- 隐藏文件选择框,通过按钮触发 -->
<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 前后端联调的实战技巧。