“揭秘假闺蜜!5大好友挑战测出谁是真朋友”,听起来是一个偏娱乐向的视频选题。但如果把它当作一个开发需求,其实很有意思:你需要做一个“好友挑战测试”线上小应用,让用户创建问卷、邀请好友参与、收集评分数据,然后通过算法判断谁更符合“真朋友”特征。
这次我们不依赖现成的第三方平台,直接用 Python + Flask + SQLite 从零搭一个可本地运行的“好友挑战测试”小项目。它麻雀虽小,但包含了 Web 后端、问卷逻辑、数据存储、接口 API、批量邀请链接生成、结果聚合统计这些常见工程点。跑通之后,你可以继续扩展到小程序、公众号或企业内部团建工具。
1. 核心能力速览
先看整个项目的功能边界和运行门槛。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 轻量 Web 应用:好友问卷挑战 / 社交关系测试 |
| 核心玩法 | 创建“5大好友挑战”,邀请好友匿名/实名作答,系统打分并给出“真朋友指数” |
| 技术栈 | Python 3 + Flask + SQLite + HTML/CSS/JS |
| 硬件要求 | 普通电脑即可,CPU 占用低,内存约 200MB 以内 |
| 显存要求 | 无,纯 CPU 应用 |
| 启动方式 | 命令行启动 Flask 服务,浏览器访问 |
| 是否支持 API | 支持,提供创建问卷、提交答案、查询结果、批量名单导入接口 |
| 是否支持批量任务 | 支持批量导入好友名单、批量生成邀请链接 |
| 数据存储 | SQLite,单文件存储,无需额外安装数据库 |
| 适合场景 | 社群运营、团队破冰、朋友聚会、Web 入门练习、Flask 项目模板参考 |
这个项目不需要 GPU,不需要模型文件,只要你电脑上有 Python 3.8 以上环境就能跑。它更适合用来练习 Web 后端、问卷系统设计和接口封装,也可以作为二次开发的起点。
2. 适用场景与使用边界
2.1 适合谁
- 刚学完 Python 基础,想做一个完整 Web 项目的开发者。
- 需要给社群、班级、公司团建做“好友默契/信任测试”活动的运营同学。
- 想把玩具项目从小程序、公众号等载体扩展的前端或全栈开发者。
2.2 能解决什么问题
- 省去手工统计问卷结果的麻烦。
- 把“5大好友挑战”这类玩法变成一个可复用、可配置的线上工具。
- 通过接口批量生成邀请链接,方便在群里分发。
- 通过结果聚合页,快速看到谁的平均分最高、谁被标记为“需要观察”。
2.3 不适合什么场景
- 不适合真正用于情感判断、人品鉴定或人际关系决策,它本质上是一个趣味游戏。
- 不适合大规模高并发场景,SQLite 在小流量下可用,高并发场景需要切换 PostgreSQL/MySQL。
- 不适合完全不懂 Python 的用户,因为本文给出的是源码级别的搭建方案,不是一键安装包。
2.4 合规与边界提醒
这个应用涉及人际关系测试,必须注意以下几点:
- 邀请好友参与前,需要明确告知这是趣味测试,不采集身份证号、住址、银行卡等敏感信息。
- 如果设置为实名模式,默认只能保存昵称;如果设置为匿名模式,不要允许填写真名。
- 不要让测试结果成为公开嘲讽、攻击他人的素材。
- 如果后续接入人脸识别、声纹识别、聊天记录分析等能力,需要取得明确授权,并遵守相关法律法规。
3. 环境准备与部署前检查
这是一个标准 Flask 项目,环境准备非常简单。下面是通用检查清单,按顺序执行即可。
3.1 安装 Python
建议使用 Python 3.8 至 3.11 之间的版本,过低或过高版本都可能导致依赖解析问题。安装完成后,在终端确认版本:
python --version pip --version如果电脑里同时存在 Python 2,可能需要使用python3和pip3。
3.2 创建项目目录
建议把项目隔离在独立目录中,方便管理和后续打包。
mkdir friend-challenge cd friend-challenge3.3 准备虚拟环境(可选但推荐)
虚拟环境可以避免把 Flask 等依赖装到全局 Python 中。
python -m venv venvWindows 激活方式:
venv\Scripts\activatemacOS / Linux 激活方式:
source venv/bin/activate激活后,终端提示符前会出现(venv)。
3.4 编写依赖文件
在项目根目录创建requirements.txt,内容如下:
flask>=2.3.0,<3.0这个项目只用 Flask,不需要其他重量级框架。后续如果要用 MySQL、Redis、大模型 API,可以再按需添加。
3.5 安装依赖
pip install -r requirements.txt安装完成后,可以查看 Flask 版本确认成功:
pip show flask3.6 检查端口占用
Flask 默认使用 5000 端口。启动前建议检查端口是否被占用。
Windows:
netstat -ano | findstr :5000macOS / Linux:
lsof -i :5000如果端口被占用,可以在启动命令中指定其他端口,例如--port 8000。
4. 项目结构设计
在动手写代码前,先规划好目录结构。推荐使用下面的组织结构:
friend-challenge/ ├── app.py # Flask 主应用,包含路由和 API ├── models.py # 数据库模型初始化与查询函数 ├── templates/ │ ├── create.html # 创建挑战页面 │ ├── fill.html # 好友作答页面 │ └── result.html # 结果展示页面 ├── static/ │ ├── style.css # 简单样式 │ └── app.js # 前端交互 ├── requirements.txt └── data/ └── challenge.db # SQLite 数据库文件,自动生成对于小项目来说,这样拆分已经足够清晰。如果后续功能增多,可以把路由拆成blueprints,把数据库操作拆成repository层。
5. 数据库设计与核心逻辑
5.1 数据表设计
这个应用需要存储三类数据:挑战问卷、挑战题目、作答记录。
challenge表:存储一次挑战的基本信息。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER 主键 | 挑战ID |
| creator | TEXT | 创建者昵称 |
| title | TEXT | 挑战名称 |
| created_at | TEXT | 创建时间 |
| status | INTEGER | 0 进行中,1 已关闭 |
question表:存储“5大好友挑战”中的具体题目和分值。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER 主键 | 题目ID |
| challenge_id | INTEGER | 所属挑战ID |
| content | TEXT | 题目内容 |
| score | INTEGER | 该题满分,默认 10 |
answer表:存储每个好友对其他人的评价。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER 主键 | 作答ID |
| challenge_id | INTEGER | 所属挑战ID |
| voter | TEXT | 评价者昵称 |
| target | TEXT | 被评价者昵称 |
| question_id | INTEGER | 题目ID |
| score | INTEGER | 本题打分 |
这里的关键逻辑是:每个参与者既是评价者,也是被评价者。好友 A 给好友 B 打分,同时也会收到其他好友的打分。最终“真朋友指数”通过平均分聚合计算得出。
5.2 Flask 应用核心代码
创建app.py,先写基础框架,包括数据库初始化和首页路由。
import os import sqlite3 from flask import Flask, render_template, request, jsonify, redirect, url_for, g BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "data", "challenge.db") app = Flask(__name__) def get_db(): """获取数据库连接,每个请求独立连接""" if "db" not in g: os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) g.db = sqlite3.connect(DB_PATH) g.db.row_factory = sqlite3.Row return g.db @app.teardown_appcontext def close_db(exception): db = g.pop("db", None) if db is not None: db.close() def init_db(): """初始化数据库表结构""" db = sqlite3.connect(DB_PATH) db.executescript( """ CREATE TABLE IF NOT EXISTS challenge ( id INTEGER PRIMARY KEY AUTOINCREMENT, creator TEXT NOT NULL, title TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, status INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS question ( id INTEGER PRIMARY KEY AUTOINCREMENT, challenge_id INTEGER NOT NULL, content TEXT NOT NULL, score INTEGER DEFAULT 10 ); CREATE TABLE IF NOT EXISTS answer ( id INTEGER PRIMARY KEY AUTOINCREMENT, challenge_id INTEGER NOT NULL, voter TEXT NOT NULL, target TEXT NOT NULL, question_id INTEGER NOT NULL, score INTEGER NOT NULL ); """ ) db.commit() db.close() @app.route("/") def index(): return render_template("create.html") if __name__ == "__main__": init_db() app.run(host="0.0.0.0", port=5000, debug=True)这个基础版本已经可以启动,但还没有业务路由。接下来我们补齐创建挑战、提交答案、查看结果三个核心功能。
6. 功能测试与效果验证
6.1 创建好友挑战
创建挑战的页面需要填写创建者昵称、挑战标题,以及 5 道题目。这里把 5 道题作为默认模板,用户可以自行修改。
在后端增加一个创建挑战的接口,接收创建者信息、标题和题目列表,写入数据库后跳转到邀请页面。
@app.route("/challenge/create", methods=["POST"]) def create_challenge(): creator = request.form.get("creator", "").strip() title = request.form.get("title", "").strip() if not creator or not title: return "创建者昵称和挑战标题不能为空", 400 question_contents = request.form.getlist("questions") question_clean = [q.strip() for q in question_contents if q.strip()] if len(question_clean) < 1: return "至少需要 1 道题目", 400 db = get_db() cursor = db.execute( "INSERT INTO challenge (creator, title) VALUES (?, ?)", (creator, title), ) challenge_id = cursor.lastrowid for q in question_clean[:5]: db.execute( "INSERT INTO question (challenge_id, content, score) VALUES (?, ?, 10)", (challenge_id, q), ) db.commit() return redirect(url_for("detail", challenge_id=challenge_id))创建成功后,需要展示一个带链接的邀约页面。邀请链接就是/challenge/<id>的访问地址,任何人打开后都可以填写。
6.2 好友作答页面
作答页面的核心设计是:每个参与者先填自己的昵称,然后选择“给谁打分”,再逐题打分。
前端表单可以一次性展示所有好友名单,让参与者依次为每个人打分。更轻量的做法是:参与者先选目标好友,再显示 5 道题。
后端接收作答数据的接口如下:
@app.route("/challenge/<int:challenge_id>/submit", methods=["POST"]) def submit_answer(challenge_id): voter = request.form.get("voter", "").strip() target = request.form.get("target", "").strip() if not voter or not target: return "评价者和被评价者昵称不能为空", 400 db = get_db() questions = db.execute( "SELECT id FROM question WHERE challenge_id = ?", (challenge_id,) ).fetchall() for q in questions: score_key = f"q_{q['id']}" score_str = request.form.get(score_key, "0") try: score = max(0, min(10, int(score_str))) except ValueError: score = 0 db.execute( "INSERT INTO answer (challenge_id, voter, target, question_id, score) VALUES (?, ?, ?, ?, ?)", (challenge_id, voter, target, q["id"], score), ) db.commit() return "提交成功"前台页面在提交成功后,提示参与者继续为下一位好友打分,或者返回到挑战首页。
6.3 结果聚合与真朋友指数
当所有好友都完成打分后,需要聚合每个人被其他好友评价的平均分。
@app.route("/challenge/<int:challenge_id>/result") def result(challenge_id): db = get_db() rows = db.execute( """ SELECT target, COUNT(*) as answer_count, SUM(score) as score_sum, AVG(score) as score_avg FROM answer WHERE challenge_id = ? GROUP BY target ORDER BY score_avg DESC """, (challenge_id,), ).fetchall() results = [] for r in rows: avg_score = round(r["score_avg"], 2) if r["score_avg"] else 0 level = "真朋友倾向明显" if avg_score >= 8 else "表现不错,继续观察" if avg_score >= 6 else "这次挑战得分偏低,建议聊一聊" results.append( { "target": r["target"], "answer_count": r["answer_count"], "score_avg": avg_score, "level": level, } ) return render_template("result.html", results=results)这里用平均分作为“真朋友指数”的简化计算方式。实际使用中,可以给每题设置不同的权重,或者加入“极端评分去除”的防刷机制。
6.4 测试用例示例
| 测试步骤 | 输入/操作 | 预期结果 | 判断标准 |
|---|---|---|---|
| 创建挑战 | 填写创建者昵称、标题、5道题 | 跳转到邀请详情页 | URL 中出现挑战ID,页面显示邀请链接 |
| 好友提交打分 | 选择目标好友,给5道题打分并提交 | 提示“提交成功” | 数据库 answer 表新增5条记录 |
| 查看结果 | 打开结果页 | 显示参与好友按平均分排序 | 平均分最高的人排第一位 |
| 重复提交 | 同一位好友重复提交同一目标 | 数据库记录重复 | 可接受,实际部署建议加“每人对同一目标只评一次”的限制 |
需要特别说明的是,这个示例项目没有做“同一用户只能提交一次”的身份校验,生产环境建议引入微信群昵称绑定、邀请码一次性使用等机制,避免刷分。
7. 接口 API 与批量任务设计
如果不想在浏览器上点击,而是把邀请链接批量分发给好友,或者把统计数据接到已有的运营后台,就需要提供一套简单的 JSON API。
7.1 API 接口说明
| 接口 | 方法 | 功能 |
|---|---|---|
/api/challenge | POST | 创建挑战,返回挑战ID和邀请链接 |
/api/challenge/<id> | GET | 获取挑战详情、题目列表 |
/api/answer | POST | 提交一份作答 |
/api/result/<id> | GET | 获取挑战结果聚合数据 |
/api/import | POST | 批量导入好友名单,返回生成结果 |
这些接口和页面路由共用同一个数据库,所以接入成本很低。
7.2 创建挑战接口示例
使用curl调用创建接口:
curl -X POST http://127.0.0.1:5000/api/challenge \ -H "Content-Type: application/json" \ -d '{ "creator": "小明", "title": "谁是真正的朋友", "questions": ["经常主动联系我", "愿意在我困难时帮忙", "聊天时认真听我讲", "不会背后说我", "愿意陪我一起做无聊的事"] }'为了让这个接口生效,需要修改创建逻辑,同时支持 JSON 输入:
@app.route("/api/challenge", methods=["POST"]) def api_create_challenge(): data = request.get_json(force=True) creator = data.get("creator", "").strip() title = data.get("title", "").strip() questions = data.get("questions", []) if not creator or not title or not questions: return jsonify({"error": "参数不完整"}), 400 db = get_db() cursor = db.execute( "INSERT INTO challenge (creator, title) VALUES (?, ?)", (creator, title), ) challenge_id = cursor.lastrowid for q in questions: db.execute( "INSERT INTO question (challenge_id, content, score) VALUES (?, ?, 10)", (challenge_id, q), ) db.commit() invite_link = f"http://127.0.0.1:5000/challenge/{challenge_id}" return jsonify({"challenge_id": challenge_id, "invite_link": invite_link}), 2007.3 批量导入好友名单
批量任务的核心是:避免人工一条一条发送链接,而是提供一个名单文件,程序自动为每位好友生成唯一的问候链接。
这里做一种简化设计:批量导入好友昵称列表,为每个昵称生成一条带参数voter=昵称的邀请链接,并在结果页通过target参数自动定位到对应好友的数据。
示例接口逻辑如下:
- 接收一个
names数组。 - 遍历每个昵称,生成邀请链接
http://127.0.0.1:5000/challenge/<id>?voter=<昵称>。 - 将链接列表返回,方便运营人员批量复制。
Python 调用示例:
import requests url = "http://127.0.0.1:5000/api/import" payload = { "challenge_id": 1, "names": ["阿花", "小绿", "大熊"] } res = requests.post(url, json=payload, timeout=10) print(res.json())如果接口返回每个好友对应的专属链接,就可以直接粘贴到聊天群中,并告之每位好友点击自己的专属链接作答。这样可以避免数据串号。
7.4 API 调用注意事项
- 生产环境必须给 API 加身份验证,最简单的方案是请求头带一个静态 Token。
- 批量导入接口要控制单次数量,建议每次不超过 50 个昵称。
- 接口返回的邀请链接要使用真实域名,不能写死为
127.0.0.1。
8. 资源占用与性能观察
这个项目是纯 CPU 应用,不涉及 GPU 和显存。启动后可以通过任务管理器或top命令观察内存占用。
8.1 本地资源占用参考
- 空闲状态:Flask 进程内存约 30MB 到 80MB。
- 有并发请求时:内存小幅上升,SQLite 读写成为主要性能瓶颈。
- 磁盘占用:项目本身不到 5MB,数据库文件根据参与人数增长,1000 条作答记录大概占用不到 2MB。
需要注意,不同操作系统和 Python 版本下资源占用会有差异,以上仅是参考范围。
8.2 性能瓶颈在哪里
- SQLite 的写入锁:同时大量提交作答时,可能会出现
database is locked错误。 - 结果页的
AVG聚合查询:数据量在几万条以内通常没有问题,超过后需要加索引。 - 模板渲染:如果参与人数很多,结果页一次性展示所有好友会比较慢,建议分批加载或只展示前 20 名。
8.3 降低占用与优化方向
- 给
answer表添加联合索引,例如(challenge_id, target)。 - 把 SQLite 连接的超时时间调高。
- 如果是在服务器部署,建议增加
threaded=True参数。 - 后续如果人数增长,可以把数据库切换为 MySQL,或者使用 Redis 缓存结果页。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动后页面打不开 | 端口被占用,或服务未启动 | 查看终端日志,检查端口占用 | 更换端口:python app.py --port 8000,在代码中传入端口 |
pip install失败 | 网络问题或 Python 版本不兼容 | 查看完整报错信息 | 更换 pip 镜像源;确认 Python 版本在 3.8 到 3.11 之间 |
| 创建挑战后看不到题目 | 题目列表没有正确写入数据库 | 打开 SQLite 查看 question 表 | 检查getlist("questions")表单字段名是否和前端一致 |
| 提交答案后结果为空 | 没有给任何目标好友打分 | 查看 answer 表记录 | 确保前端提交了target字段 |
| 中文显示乱码 | 终端编码问题 | 打印请求数据观察 | Windows 使用chcp 65001切换 UTF-8 编码 |
| 批量导入接口返回 400 | 没有传names数组或challenge_id不存在 | 打印请求体 | 检查 JSON 字段名和挑战ID |
database is locked报错 | 多并发写入 SQLite | 查看日志中的具体位置 | 增加连接超时,或改用 MySQL/PostgreSQL |
| 邀请链接打开后提示 404 | 挑战ID不存在 | 查看数据库 challenge 表 | 检查链接中的ID是否正确 |
| 数据被别人任意修改 | 未做权限控制 | 观察异常提交记录 | 为邀请链接加入一次性 token,或限制每个昵称每天只能提交一次 |
10. 最佳实践与使用建议
10.1 第一次先小范围测试
建议不要直接发给全部好友。先自己创建一个挑战,打开无痕浏览器窗口,用两个虚拟昵称各提交一次,确认结果页正常后再分享。
10.2 保持最小可运行配置
这个项目的核心依赖只有 Flask,不要在一开始就加入大型前端框架、数据库中间件和大模型 API。先把基础流程跑通,再逐步扩展。
10.3 目录与数据管理
建议把data/challenge.db加入.gitignore,避免把测试数据提交到代码仓库。如果要在服务器部署,定时备份数据库文件即可。
10.4 批量任务必须加日志
批量导入接口建议打印日志,记录每次导入的挑战ID、数量、生成链接数。这样出了问题可以快速定位。
10.5 权限与安全
如果部署到公网,一定要做访问控制。最简单的做法是在app.py中增加一个请求前置检查,只有带正确 Token 的请求才能访问 API 接口。
10.6 合规提醒
- 只收集昵称和打分数据,不收集手机号、身份证号、通讯录等个人敏感信息。
- 参与测试前,明确告知这是趣味互动。
- 不把测试结果用于公开排名、恶意嘲讽或任何商业决策。
- 如果接入任何 AI 情感分析、人脸识别、声音识别能力,必须取得参与者明确授权。
11. 可以继续扩展的方向
这个骨架项目可以继续往上加东西:
- 接入大模型 API,对好友提交的开放性问题进行情绪倾向分析。
- 做成小程序:前端用微信小程序,后端仍用 Flask API。
- 增加可视化图表:用 ECharts 展示所有好友的得分雷达图。
- 加入邀请码机制:每个挑战生成唯一码,好友凭码参与。
- 改成匿名模式:系统随机生成代号,保护参与者隐私。
- 增加定时关闭:超过设定时间自动停止收集答案。
对你来说,最快验证整个功能的方式是:先跑通app.py,创建一个包含 5 个经典题目的挑战,然后自己提交几组答案,最后打开结果页看排名是否正确。这个流程只要能顺利走通,剩下的事情就是按自己的具体场景去改界面和逻辑了。