海南大学交友平台开发实战day7(实现核心匹配算法+解决JSON请求报错问题)
大家好,欢迎来到海南大学交友平台开发实战系列的第七天!在前六天的开发中,我们已经完成了登录页、注册页的UI布局、Flask后端基础搭建、数据库初始化及核心表结构创建,打通了前端与后端的基础通信链路。今天的核心任务有两个:一是实现平台核心的匹配算法,完成虚拟测试用户创建、内存过滤机制及/api/match接口开发;二是定位并解决前端点击匹配按钮时,JSON请求无法传递user_id、后端解析失败的400报错问题,全程实战落地,记录每一个踩坑细节和解决思路,帮助大家避开同类问题,高效完成开发。
本博文采用"前端HTML/JS + 后端Python Flask + 数据库SQLite"的技术栈,全程实战落地,包含关键代码、报错分析、修复步骤及功能验证,欢迎大家在评论区交流探讨开发过程中遇到的问题。
一、今日开发核心目标
明确今日开发重点,聚焦核心需求,避免开发偏离方向,具体目标如下:
-
后端实现核心匹配算法:按照"基础分50分+同年级(模拟同校区)50分+兴趣10分/个(封顶50分)"的规则,计算用户匹配度,仅推荐匹配度≥80%的用户;
-
后端实现内存过滤机制:对匹配失败的用户打exclude标签,本次会话内不再重复匹配,节省算力;
-
后端创建虚拟测试用户,用于匹配功能测试,固定测试用户信息,确保测试场景可复现;
-
后端开发/api/match接口,接收前端传递的JSON格式user_id,执行匹配逻辑并返回规范结果;
-
前端实现匹配按钮交互、匹配名片展示、交友按钮及继续匹配功能,确保与后端接口正常联动;
-
定位并解决核心报错:前端点击匹配按钮时,JSON请求未传递user_id,导致后端解析失败,出现400 Bad Request错误。
二、前期准备与核心概念梳理
在开始开发前,先明确本次涉及的核心概念和前期准备,帮助大家快速理解开发逻辑,避免因概念模糊导致的开发误区,同时验证关键函数是否已正确定义,确保开发顺利推进。
2.1 核心概念解析
-
JSON数据传输:前后端数据交互的轻量级格式,前端发送POST请求时,需设置请求头Content-Type: application/json,并将请求体转为JSON字符串,否则后端无法解析,会报400错误;
-
匹配算法:本次核心算法规则------基础分50分,同年级(模拟同校区)加50分,每有一个相同兴趣加10分,兴趣加分最高50分(总分封顶100分),仅匹配度≥80%的用户会被推荐;
-
内存过滤机制:通过全局字典存储已排除的用户ID,仅在当前服务会话内有效,重启服务后清空,避免重复匹配失败用户,提升匹配效率;
-
sessionStorage:前端存储用户信息的临时存储方式,用于保存登录后的student_id,供匹配请求时调用,页面关闭后数据清空。
2.2 前期验证:关键函数是否就绪
开发前,先通过Bash命令验证后端、前端的核心函数是否已正确定义,避免开发过程中出现"函数未找到"的问题,验证过程及结果如下:
2.2.1 验证后端关键函数(app.py)
执行Bash命令,查询匹配相关核心函数是否存在:
bash
grep -n "def calculate_match_score\|def match_user\|match_exclude_cache\|create_test_user" app.py
输出结果(验证成功):
bash
142: create_test_user()
149:match_exclude_cache = {}
153: return match_exclude_cache.get(student_id, set())
157: if student_id not in match_exclude_cache:
158: match_exclude_cache[student_id] = set()
159: match_exclude_cache[student_id].add(exclude_student_id)
161:def create_test_user():
206:def calculate_match_score(current_user, target_user):
461:def match_user():
结论:后端核心函数(匹配算法、内存过滤、虚拟用户创建、匹配接口)均已正确定义,可正常开展开发。
2.2.2 验证前端关键函数(home.html)
执行Bash命令,查询前端匹配相关交互函数是否存在:
bash
grep -n "startMatching\|displayMatchResult\|sendFriendRequest\|continueMatching" home.html
输出结果(验证成功):
bash
1153: <button class="match-button" id="matchBtn" onclick="startMatching()">
1191: <button class="add-friend-btn" id="addFriendBtn" onclick="sendFriendRequest()">
1194: <button class="close-card-btn" onclick="continueMatching()">
1704: async function startMatching() {
1734: displayMatchResult(result.data);
1755: function displayMatchResult(data) {
1788: function continueMatching() {
1797: startMatching();
1809: async function sendFriendRequest() {
结论:前端核心交互函数(匹配触发、结果展示、交友申请、继续匹配)均已定义,可正常对接后端接口。
三、核心功能开发过程(含踩坑与报错修复)
本次开发按"后端功能实现→前端交互实现→报错定位→报错修复→功能验证"的顺序推进,重点记录匹配算法实现和JSON请求报错的解决过程,所有代码可直接复制使用,踩坑细节全程同步。
3.1 后端功能实现(app.py)
后端核心实现匹配算法、内存过滤、虚拟测试用户创建及/api/match接口,逐步推进,确保每一步功能可验证。
3.1.1 实现内存过滤机制(match_exclude_cache)
核心需求:匹配失败的用户,在本次会话内不再重复匹配,通过全局字典存储已排除用户ID,仅内存存储,重启服务后清空。
实现代码(可直接复制):
python
# 全局内存缓存,存储已排除的用户,格式:{当前用户student_id: {已排除用户student_id集合}}
match_exclude_cache = {}
# 获取当前用户的已排除用户列表
def get_excluded_users(student_id):
return match_exclude_cache.get(student_id, set())
# 新增排除用户(匹配失败时调用)
def add_excluded_user(student_id, exclude_student_id):
if student_id not in match_exclude_cache:
match_exclude_cache[student_id] = set()
match_exclude_cache[student_id].add(exclude_student_id)
3.1.2 创建虚拟测试用户(create_test_user)
核心需求:创建固定虚拟用户,用于匹配功能测试,固定学号、密码、兴趣及年级,确保测试场景可复现。
实现代码(可直接复制):
python
def create_test_user():
# 检查虚拟用户是否已存在,避免重复创建
test_user = User.query.filter_by(student_id='20230010001').first()
if not test_user:
# 加密存储密码,密码:123456
hashed_password = generate_password_hash('123456')
# 虚拟用户信息:学号20230010001,兴趣游泳、PUBG、三角洲行动,年级2024级
test_user = User(
student_id='20230010001',
password=hashed_password,
nickname='李明',
grade='2024',
hobbies='游泳,PUBG,三角洲行动',
campus='主校区'
)
db.session.add(test_user)
db.session.commit()
# 项目启动时自动创建虚拟测试用户
create_test_user()
3.1.3 实现核心匹配算法(calculate_match_score)
核心需求:按规则计算用户匹配度,基础分50分,同年级加50分,每有一个相同兴趣加10分,封顶50分,返回匹配度和共同兴趣。
实现代码(可直接复制):
python
def calculate_match_score(current_user, target_user):
# 基础分50分
score = 50
# 同年级(模拟同校区)加50分
if current_user.grade == target_user.grade:
score += 50
# 计算共同兴趣,每一个加10分,封顶50分
# 将兴趣字符串转为集合,便于计算交集
current_hobbies = set(current_user.hobbies.split(',')) if current_user.hobbies else set()
target_hobbies = set(target_user.hobbies.split(',')) if target_user.hobbies else set()
common_hobbies = list(current_hobbies & target_hobbies)
# 兴趣加分,最多50分
hobby_score = len(common_hobbies) * 10
score += min(hobby_score, 50)
# 返回匹配度(百分比)和共同兴趣
return {
'percent': score,
'common_hobbies': common_hobbies
}
3.1.4 开发/api/match接口(match_user函数)
核心需求:接收前端传递的JSON格式user_id,执行匹配逻辑,返回匹配成功的用户信息和匹配度,接口返回格式固定。
初始实现代码(存在隐藏问题,后续报错根源):
python
@app.route('/api/match', methods=['POST'])
@login_required
def match_user():
try:
# 初始问题:仅从session获取student_id,未接收前端传递的user_id
data = request.get_json() or {}
current_student_id = session['student_id']
# 获取当前用户信息
current_user = User.query.filter_by(student_id=current_student_id).first()
if not current_user:
return api_response(404, '当前用户不存在')
# 获取已排除的用户列表,排除自己
excluded_ids = get_excluded_users(current_student_id)
excluded_ids.add(current_student_id)
# 获取所有未排除的用户
all_users = User.query.filter(User.student_id.notin_(list(excluded_ids))).all()
if not all_users:
return api_response(404, '暂时没有可匹配的用户,请稍后再试')
# 随机打乱用户顺序,模拟随机匹配
import random
random.shuffle(all_users)
# 遍历寻找匹配度≥80%的用户
matched_user = None
match_result = None
for target_user in all_users:
match_result = calculate_match_score(current_user, target_user)
if match_result['percent'] >= 80:
matched_user = target_user
break
else:
# 匹配失败,添加到排除列表
add_excluded_user(current_student_id, target_user.student_id)
if not matched_user:
return api_response(404, '暂时没有可匹配的用户,请稍后再试')
# 构造返回数据,适配前端渲染
return api_response(200, '匹配成功', {
'user': {
'display_name': f"{matched_user.nickname[0]}同学",
'grade': f"{int(matched_user.grade)-2024+2}年级" if matched_user.grade else '未知年级',
'campus': matched_user.campus
},
'match': match_result
})
except Exception as e:
return api_response(500, f'匹配失败:{str(e)}')
3.2 前端交互实现(home.html)
前端核心实现匹配按钮点击交互、匹配名片展示、交友按钮及继续匹配功能,重点对接后端/api/match接口,传递JSON格式user_id。
3.2.1 匹配名片UI实现(毛玻璃风格)
核心需求:匹配成功后弹出毛玻璃风格名片,圆角24px,展示匹配度、用户姓名、年级、共同兴趣,包含交友按钮和继续匹配按钮,贴合项目整体UI风格。
实现代码(可直接复制):
html
<!-- 匹配名片弹窗(毛玻璃风格) -->
<div class="match-card" id="userCard" style="display: none;">
<div class="match-percent" id="matchPercent">90%</div>
<div class="user-info">
<h3 id="userName">李同学</h3>
<p id="userGrade">大二</p>
</div>
<div class="common-hobbies" id="commonHobbies">
<span class="hobby-tag">游泳</span>
<span class="hobby-tag">PUBG</span>
</div>
<div class="btn-group">
<button class="add-friend-btn" id="addFriendBtn" onclick="sendFriendRequest()">🤝 交友</button>
<button class="close-card-btn" onclick="continueMatching()">继续匹配</button>
</div>
</div>
<style>
/* 毛玻璃风格名片 */
.match-card {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 320px;
padding: 24px;
border-radius: 24px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
color: #333;
z-index: 999;
}
/* 匹配度徽章 */
.match-percent {
position: absolute;
top: -10px;
right: -10px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #66ccff, #9966ff);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
animation: gradient 2s infinite;
}
/* 交友按钮样式 */
.add-friend-btn {
width: 120px;
height: 40px;
border: none;
border-radius: 20px;
background: linear-gradient(135deg, #66ccff, #9966ff);
color: #fff;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
}
.add-friend-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 204, 255, 0.5);
}
</style>
3.2.2 匹配按钮交互实现(startMatching函数)
核心需求:点击"开始匹配"按钮,显示"匹配中..."旋转状态,发送JSON格式user_id到后端/api/match接口,接收返回结果并渲染名片。
初始实现代码(核心报错代码):
javascript
async function startMatching() {
const btn = document.getElementById('matchBtn');
const hint = document.getElementById('matchHint');
const card = document.getElementById('userCard');
// 按钮点击动画
btn.style.transform = 'scale(0.9)';
setTimeout(() => {
btn.style.transform = '';
}, 150);
// 显示加载状态,禁用按钮防止重复点击
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>匹配中...</span>';
try {
// 核心报错点:设置了JSON请求头,但未传递任何请求体(无user_id)
const response = await fetch(`${API_BASE}/match`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
// 缺失请求体,未传递user_id
}
});
const result = await response.json();
if (result.code === 200) {
// 匹配成功,渲染名片
displayMatchResult(result.data);
hint.textContent = '';
} else {
hint.textContent = result.msg;
setTimeout(() => {
hint.textContent = '';
}, 3000);
}
} catch (error) {
console.error('匹配请求失败:', error);
hint.textContent = '匹配失败,请稍后再试';
setTimeout(() => {
hint.textContent = '';
}, 3000);
} finally {
// 恢复按钮状态
btn.disabled = false;
btn.innerHTML = '开始匹配';
}
}
3.3 核心报错:JSON请求解析失败(400 Bad Request)
当点击匹配按钮时,前端提示匹配失败,查看后端日志发现报错:400 Bad Request,JSON解析失败。这是本次开发的核心踩坑点,也是今天需要重点解决的问题。
3.3.1 报错原因定位
通过Bash命令查看前端startMatching函数和后端match_user接口,定位报错根源:
-
前端问题:设置了Content-Type: application/json请求头,但未传递任何请求体(未发送user_id),导致后端无法解析JSON数据;
-
后端问题:check_login接口未返回student_id,前端无法获取并存储到sessionStorage,即使后续传递请求体,也无法获取正确的user_id;同时match_user接口仅从session获取student_id,未兼容前端传递的user_id参数。
3.3.2 报错验证过程(Bash命令)
通过Bash命令查看前端函数和后端接口,确认报错根源:
bash
# 查看前端startMatching函数,确认是否传递请求体
grep -A20 "async function startMatching" "d:\science_discover\learn\HUN_friend\HUN_self_build version1.0.3 - 副本\home.html" | head -25
# 查看后端match_user接口,确认是否接收user_id
grep -A30 "def match_user" "d:\science_discover\learn\HUN_friend\HUN_self_build version1.0.3 - 副本\app.py" | head -35
输出结果显示:
-
前端startMatching函数仅设置了请求头,未传递请求体(无user_id);
-
后端match_user接口仅从session获取student_id,未接收前端传递的user_id;
-
后端check_login接口未返回student_id,前端无法获取并存储到sessionStorage。
3.3.3 报错修复步骤(全程可复现)
修复思路:先修复后端接口,确保check_login返回student_id、match_user接口兼容前端传递的user_id;再修复前端,存储student_id到sessionStorage,发送请求时传递JSON格式user_id。
步骤1:修复后端check_login接口(app.py)
问题:check_login接口未返回student_id,前端无法获取并存储。
修复代码(新增返回student_id):
python
def check_login():
if 'student_id' in session:
# 新增返回student_id,供前端存储
return api_response(200, '已登录', {
'is_logged_in': True,
'student_id': session['student_id']
})
return api_response(200, '未登录', {'is_logged_in': False})
步骤2:修复后端match_user接口(app.py)
问题:仅从session获取student_id,未兼容前端传递的user_id,若前端传递则无法接收。
修复代码(兼容两种获取方式):
python
@app.route('/api/match', methods=['POST'])
@login_required
def match_user():
try:
data = request.get_json() or {}
# 兼容前端传递的user_id和session中的student_id
current_student_id = data.get('user_id') or session['student_id']
# 后续匹配逻辑不变(省略,与前文一致)
current_user = User.query.filter_by(student_id=current_student_id).first()
if not current_user:
return api_response(404, '当前用户不存在')
excluded_ids = get_excluded_users(current_student_id)
excluded_ids.add(current_student_id)
all_users = User.query.filter(User.student_id.notin_(list(excluded_ids))).all()
if not all_users:
return api_response(404, '暂时没有可匹配的用户,请稍后再试')
import random
random.shuffle(all_users)
matched_user = None
match_result = None
for target_user in all_users:
match_result = calculate_match_score(current_user, target_user)
if match_result['percent'] >= 80:
matched_user = target_user
break
else:
add_excluded_user(current_student_id, target_user.student_id)
if not matched_user:
return api_response(404, '暂时没有可匹配的用户,请稍后再试')
return api_response(200, '匹配成功', {
'user': {
'display_name': f"{matched_user.nickname[0]}同学",
'grade': f"{int(matched_user.grade)-2024+2}年级" if matched_user.grade else '未知年级',
'campus': matched_user.campus
},
'match': match_result
})
except Exception as e:
return api_response(500, f'匹配失败:{str(e)}')
步骤3:修复前端存储student_id(home.html)
问题:checkLogin和getProfile函数未将student_id存储到sessionStorage,前端无法获取并传递。
修复代码(修改checkLogin和getProfile函数):
javascript
// 验证登录状态,存储student_id到sessionStorage
async function checkLogin() {
try {
const response = await fetch(`${API_BASE}/check_login`, {
credentials: 'include'
});
const result = await response.json();
if (result.code === 200 && result.data.is_logged_in) {
currentUser = result.data;
// 新增:将student_id存储到sessionStorage
sessionStorage.setItem('student_id', result.data.student_id);
return true;
} else {
// 未登录,跳转到登录页
window.location.href = '/login';
return false;
}
} catch (error) {
console.error('检查登录状态失败:', error);
return false;
}
}
// 获取用户资料,补充存储student_id
async function getProfile() {
try {
const response = await fetch(`${API_BASE}/get_profile`, {
credentials: 'include'
});
const result = await response.json();
if (result.code === 200) {
userProfile = result.data;
// 新增:将student_id存储到sessionStorage(兜底)
sessionStorage.setItem('student_id', userProfile.student_id);
}
} catch (error) {
console.error('获取用户资料失败:', error);
}
}
步骤4:修复前端startMatching函数,传递JSON格式user_id
问题:未传递请求体,未发送user_id,导致后端JSON解析失败。
修复代码(最终可用版):
javascript
async function startMatching() {
const btn = document.getElementById('matchBtn');
const hint = document.getElementById('matchHint');
const card = document.getElementById('userCard');
// 按钮点击动画
btn.style.transform = 'scale(0.9)';
setTimeout(() => {
btn.style.transform = '';
}, 150);
// 显示加载状态,禁用按钮
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>匹配中...</span>';
try {
// 核心修复:获取sessionStorage中的student_id,传递JSON格式请求体
const student_id = sessionStorage.getItem('student_id') || '';
const response = await fetch(`${API_BASE}/match`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
// 新增请求体,传递JSON格式user_id
body: JSON.stringify({
user_id: student_id
})
});
const result = await response.json();
if (result.code === 200) {
// 匹配成功,渲染名片
displayMatchResult(result.data);
hint.textContent = '';
} else {
hint.textContent = result.msg;
setTimeout(() => {
hint.textContent = '';
}, 3000);
}
} catch (error) {
console.error('匹配请求失败:', error);
hint.textContent = '匹配失败,请稍后再试';
setTimeout(() => {
hint.textContent = '';
}, 3000);
} finally {
// 恢复按钮状态
btn.disabled = false;
btn.innerHTML = '开始匹配';
}
}
3.4 其他前端辅助函数实现
补充displayMatchResult(渲染匹配名片)、sendFriendRequest(交友申请)、continueMatching(继续匹配)函数,确保交互完整:
javascript
// 渲染匹配结果到名片
function displayMatchResult(data) {
const card = document.getElementById('userCard');
const matchPercent = document.getElementById('matchPercent');
const userName = document.getElementById('userName');
const userGrade = document.getElementById('userGrade');
const commonHobbies = document.getElementById('commonHobbies');
// 填充数据
matchPercent.textContent = `${data.match.percent}%`;
userName.textContent = data.user.display_name;
userGrade.textContent = data.user.grade;
// 渲染共同兴趣标签
commonHobbies.innerHTML = '';
data.match.common_hobbies.forEach(hobby => {
const tag = document.createElement('span');
tag.className = 'hobby-tag';
tag.textContent = hobby;
commonHobbies.appendChild(tag);
});
// 显示名片
card.style.display = 'block';
}
// 发送交友申请(预留后续接口对接逻辑)
async function sendFriendRequest() {
const userId = document.getElementById('addFriendBtn').dataset.userId;
try {
const response = await fetch(`${API_BASE}/add_friend`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: sessionStorage.getItem('student_id'),
target_user_id: userId
})
});
const result = await response.json();
if (result.code === 200) {
alert('好友申请已发送,等待对方同意!');
} else {
alert(result.msg);
}
} catch (error) {
console.error('发送交友申请失败:', error);
alert('交友申请发送失败,请稍后再试');
}
}
// 继续匹配(关闭当前名片,重新触发匹配)
function continueMatching() {
document.getElementById('userCard').style.display = 'none';
startMatching();
}
四、功能测试方法与预期结果
修复所有报错后,按以下步骤进行测试,确保匹配功能正常运行,JSON请求无400报错,匹配算法符合规则。
4.1 测试准备
bash
# 1. 删除旧数据库(因新增字段,避免数据冲突)
rm instance/hainanu.db
# 2. 启动Flask服务
python app.py
# 3. 登录测试
# 注册新用户:兴趣选择游泳、PUBG、三角洲行动,年级选择2024级
# 或直接使用虚拟测试用户登录:学号20230010001,密码123456
4.2 测试步骤与预期结果(预留空白,供填写最终测试用例)
测试匹配功能(核心测试)

五、今日开发总结与踩坑回顾
今日核心完成了匹配系统的全流程开发和JSON请求报错修复,实现了后端匹配算法、内存过滤、虚拟测试用户创建,以及前端匹配交互和名片渲染,成功解决了"前端JSON请求未传递user_id导致后端400解析失败"的核心问题,同时记录了完整的踩坑细节和修复步骤,为后续开发提供参考。
核心踩坑点回顾:
-
前端JSON请求报错:设置了请求头但未传递请求体,导致后端无法解析,修复方案是添加JSON格式的user_id请求体,从sessionStorage获取student_id;
-
后端接口兼容问题:仅从session获取student_id,未兼容前端传递的user_id,修复方案是优先获取前端传递的user_id,兜底使用session中的值;
-
前端student_id存储问题:checkLogin和getProfile函数未存储student_id到sessionStorage,导致无法获取并传递,修复方案是在两个函数中添加存储逻辑。
今日开发的所有代码均可直接复制使用,匹配功能已实现完整交互,后续可继续完善交友申请的后端逻辑,实现好友列表展示等功能。欢迎大家在评论区交流开发过程中遇到的问题,一起高效避坑、高效开发!
关注我,后续我也会持续更新项目开发进度,分享更多Python前端全栈开发相关的实战经验。