本篇文章会介绍从零开始构建一个基于 Flask + GPT 的小项目的过程。总共有四个版本的迭代,包括:
1、调用 GPT 接口并渲染到前端页面;
2、使用 Flask 提供的 session 来实现登录和登出功能;
3、用 SQLAlchemy 管理数据库,实现用户注册和登录;
4、记录和分页查看用户与 GPT 的对话历史。
Python 版本:建议 3.7+
Flask:最常用的 Python Web 框架之一
openai:官方 Python SDK,用于调用 GPT API
dotenv:用于在本地加载 .env 文件,存储一些敏感数据(如 OPENAI_API_KEY)
SQLAlchemy:Python ORM 框架,方便管理数据库
bcrypt:用于安全地加密与验证密码
前端:主要使用原生 HTML + JavaScript
安装依赖
pip install flask openai python-dotenv flask_sqlalchemy bcrypt
目标:能让用户在网页上输入一段文本,后端调用 GPT,直接返回结果给前端渲染即可。
关键点:
在 app.py 中直接引用 openai.ChatCompletion 接口;
前端用一个简单的 index.html 配合 AJAX (fetch) 请求。
代码如下:
app.py
import os
import openai
from flask import Flask, request, jsonify, render_template
from dotenv import load_dotenv
# 加载本地 .env 文件中的环境变量
load_dotenv()
app = Flask(__name__)
# 从环境变量中读取你的 GPT Key
openai.api_key = os.getenv("OPENAI_API_KEY")
@app.route("/")
def index():
# 返回前端页面
return render_template("index.html")
@app.route("/chat", methods=["POST"])
def chat():
# 接收JSON数据
data = request.get_json()
user_message = data.get("message", "").strip()
if not user_message:
return jsonify({"error": "message cannot be empty"}), 400
try:
response = openai.ChatCompletion.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": user_message}]
)
reply = response.choices[0].message.content.strip()
return jsonify({"reply": reply})
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
app.run(debug=True, port=5001)
前端 index.html(放在 templates/index.html 下):
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GPT 聊天title>
head>
<body style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; margin: 0;">
<h1>GPT 聊天h1>
<textarea id="userInput" rows="4" cols="50" placeholder="请输入您的问题..." style="width: 50%; resize: none;">textarea>
<div style="width: 50%; display: flex; justify-content: flex-end; margin-top: 10px;">
<button onclick="sendMessage()" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">发送给 GPTbutton>
div>
<div style="width: 50%; margin-top: 20px;">
<h2 style="margin: 0;">GPT 回复:h2>
<div id="responseBox" style="white-space: pre-wrap; margin-top: 10px; padding: 10px; width: 100%; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9; min-height: 50px;">div>
div>
<script>
async function sendMessage() {
const userInput = document.getElementById("userInput").value.trim();
if (!userInput) {
alert("输入不能为空!");
return;
}
try {
const res = await fetch("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userInput })
});
const data = await res.json();
document.getElementById("responseBox").innerText = data.reply || data.error || "未知错误";
} catch (error) {
console.error(error);
alert("请求出错了!");
}
}
script>
body>
html>
此时我们已经完成了最基本的“把 GPT 接口包装成 Flask 服务”的功能。运行代码后,在浏览器访问 http://127.0.0.1:5001/,即可使用。
这是效果图:
目标:让用户只能在登录成功后,才能访问 GPT 聊天功能;
关键点:
在后端使用 Flask 的 session 来保存登录状态;
需要增加登录页 (login.html),以及登录、登出逻辑;
聊天接口 /chat 在没有 session 的情况下,禁止访问。
代码如下:
app.py
import os
import openai
import bcrypt
from flask import Flask, request, jsonify, render_template, session, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from dotenv import load_dotenv
# =========================
# 1. 加载环境 & 基本配置
# =========================
load_dotenv()
app = Flask(__name__)
# 生成随机密钥并设置为 SECRET_KEY
app.config['SECRET_KEY'] = os.urandom(24)
# 从环境变量中读取你的 GPT Key
openai.api_key = os.getenv("OPENAI_API_KEY")
# 配置数据库,这里以 SQLite 为例
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///demo.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# =========================
# 2. 数据库模型
# =========================
class User(db.Model):
"""用户表"""
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password_hash = db.Column(db.String(60), nullable=False) # 存储哈希后的密码
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# def __init__(self, username, password_hash):
# self.username = username
# self.password_hash = password_hash
# =========================
# 3. 路由逻辑
# =========================
@app.route('/')
def index():
# 如果已登录,则在界面上显示聊天功能;否则提示登录或注册
if session.get("logged_in"):
return render_template("index.html", logged_in=True, username=session.get("username"))
else:
return render_template("index.html", logged_in=False)
@app.route('/register', methods=['GET', 'POST'])
def register():
"""
用户注册:
- GET 访问时返回注册页面
- POST 提交时写数据库
"""
if request.method == 'GET':
return render_template("register.html")
# POST 提交表单数据
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
return "用户名或密码不能为空", 400
# 检查是否存在同名用户
existing_user = User.query.filter_by(username=username).first()
if existing_user:
return "该用户名已被注册", 400
# 加密密码(bcrypt)并存储到数据库
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
new_user = User(username=username, password_hash=hashed.decode('utf-8'))
db.session.add(new_user)
db.session.commit()
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
"""
用户登录:
- GET 访问时返回登录页面
- POST 提交时校验密码
"""
if request.method == 'GET':
# 如果已经登录,就直接跳到首页
if session.get("logged_in"):
return redirect(url_for("index"))
return render_template("login.html")
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
return "用户名或密码不能为空", 400
# 从数据库查询用户
user = User.query.filter_by(username=username).first()
if not user:
return "用户名不存在", 400
# 验证密码
if bcrypt.checkpw(password.encode('utf-8'), user.password_hash.encode('utf-8')):
# 登录成功
session["logged_in"] = True
session["username"] = user.username
return redirect(url_for('index'))
else:
return "密码错误", 401
@app.route('/logout')
def logout():
"""
用户登出
"""
session.clear()
return redirect(url_for('index'))
@app.route("/chat", methods=["POST"])
def chat():
"""
聊天接口 (GPT 调用)
"""
if not session.get("logged_in"):
return jsonify({"error": "未登录,无法访问此接口"}), 401
data = request.get_json()
user_message = data.get("message", "").strip()
if not user_message:
return jsonify({"error": "message cannot be empty"}), 400
try:
response = openai.ChatCompletion.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": user_message}]
)
reply = response.choices[0].message.content.strip()
return jsonify({"reply": reply})
except Exception as e:
return jsonify({"error": str(e)}), 500
# =========================
# 4. 启动应用入口
# =========================
if __name__ == "__main__":
with app.app_context():
db.create_all() # 在应用上下文中创建表
app.run(debug=True, port=5001)
index.html
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GPT 聊天title>
<style>
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
background-color: #f9f9f9;
}
h1 {
margin-bottom: 20px;
}
.chat-container {
width: 50%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
}
textarea {
width: 100%;
resize: none;
padding: 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 5px;
}
.button-container {
display: flex;
justify-content: flex-end;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 5px;
background-color: #007BFF;
color: white;
}
button:hover {
background-color: #0056b3;
}
.response-container {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #fff;
min-height: 50px;
white-space: pre-wrap;
}
.logout-link {
margin-top: 20px;
text-decoration: none;
font-size: 16px;
color: #007BFF;
}
.logout-link:hover {
text-decoration: underline;
}
.centered {
display: flex;
justify-content: center;
align-items: center;
height: 50vh;
}
.centered p {
font-size: 18px;
}
style>
head>
<body>
<h1>GPT 聊天h1>
{% if logged_in %}
<div class="chat-container">
<p>您已登录!p>
<textarea id="userInput" rows="4" placeholder="请输入您的问题...">textarea>
<div class="button-container">
<button onclick="sendMessage()">发送给 GPTbutton>
div>
<h2>GPT 回复:h2>
<div class="response-container" id="responseBox">div>
div>
<a href="/logout" class="logout-link">退出登录a>
<script>
async function sendMessage() {
const userInput = document.getElementById("userInput").value.trim();
if (!userInput) {
alert("输入不能为空!");
return;
}
try {
const res = await fetch("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userInput })
});
const data = await res.json();
document.getElementById("responseBox").innerText = data.reply || data.error || "未知错误";
} catch (error) {
console.error(error);
alert("请求出错了!");
}
}
script>
{% else %}
<div class="centered">
<p>您尚未登录,请先 <a href="/login">登录a>。p>
div>
{% endif %}
body>
html>
login.html
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>登录title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
flex-direction: column; /* 垂直排列 */
}
h1 {
margin-bottom: 20px;
}
.input-group {
display: flex;
align-items: flex-end;
gap: 10px;
}
input {
flex: 1;
padding: 5px;
}
button {
padding: 5px 10px;
font-size: 14px;
}
form {
width: 300px; /* 限制表单宽度 */
}
style>
head>
<body>
<h1>登录页面h1>
<form action="/login" method="POST">
<div>
<label for="username">用户名:label>
<input type="text" id="username" name="username" required>
div>
<div class="input-group">
<label for="password">密码:label>
<input type="password" id="password" name="password" required>
<button type="submit">登录button>
div>
form>
body>
html>
目标:让用户可以在系统中“注册新账号”,并把密码以安全的方式存储到数据库,而不再写死用户名密码。
关键点:
使用 SQLAlchemy 管理数据库(这里示例用 SQLite);
新增 User 模型,包含 id, username, password_hash, created_at 等字段;
新增 register 路由,用户提交表单后,将数据写入数据库;
登录时从数据库读取用户信息,用 bcrypt 校验密码。
app.py:
import os
import openai
import bcrypt
from flask import Flask, request, jsonify, render_template, session, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from dotenv import load_dotenv
# =========================
# 1. 加载环境 & 基本配置
# =========================
load_dotenv()
app = Flask(__name__)
# 生成随机密钥并设置为 SECRET_KEY
app.config['SECRET_KEY'] = os.urandom(24)
# 从环境变量中读取你的 GPT Key
openai.api_key = os.getenv("OPENAI_API_KEY")
# 配置数据库,这里以 SQLite 为例
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///demo.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# =========================
# 2. 数据库模型
# =========================
class User(db.Model):
"""用户表"""
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password_hash = db.Column(db.String(60), nullable=False) # 存储哈希后的密码
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __init__(self, username, password_hash):
self.username = username
self.password_hash = password_hash
# =========================
# 3. 路由逻辑
# =========================
@app.route('/')
def index():
# 如果已登录,则在界面上显示聊天功能;否则提示登录或注册
if session.get("logged_in"):
return render_template("index.html", logged_in=True, username=session.get("username"))
else:
return render_template("index.html", logged_in=False)
@app.route('/register', methods=['GET', 'POST'])
def register():
"""
用户注册:
- GET 访问时返回注册页面
- POST 提交时写数据库
"""
if request.method == 'GET':
return render_template("register.html")
# POST 提交表单数据
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
return "用户名或密码不能为空", 400
# 检查是否存在同名用户
existing_user = User.query.filter_by(username=username).first()
if existing_user:
return "该用户名已被注册", 400
# 加密密码(bcrypt)并存储到数据库
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
new_user = User(username=username, password_hash=hashed.decode('utf-8'))
db.session.add(new_user)
db.session.commit()
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
"""
用户登录:
- GET 访问时返回登录页面
- POST 提交时校验密码
"""
if request.method == 'GET':
# 如果已经登录,就直接跳到首页
if session.get("logged_in"):
return redirect(url_for("index"))
return render_template("login.html")
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
return "用户名或密码不能为空", 400
# 从数据库查询用户
user = User.query.filter_by(username=username).first()
if not user:
return "用户名不存在", 400
# 验证密码
if bcrypt.checkpw(password.encode('utf-8'), user.password_hash.encode('utf-8')):
# 登录成功
session["logged_in"] = True
session["username"] = user.username
return redirect(url_for('index'))
else:
return "密码错误", 401
@app.route('/logout')
def logout():
"""
用户登出
"""
session.clear()
return redirect(url_for('index'))
@app.route("/chat", methods=["POST"])
def chat():
"""
聊天接口 (GPT 调用)
"""
if not session.get("logged_in"):
return jsonify({"error": "未登录,无法访问此接口"}), 401
data = request.get_json()
user_message = data.get("message", "").strip()
if not user_message:
return jsonify({"error": "message cannot be empty"}), 400
try:
response = openai.ChatCompletion.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": user_message}]
)
reply = response.choices[0].message.content.strip()
return jsonify({"reply": reply})
except Exception as e:
return jsonify({"error": str(e)}), 500
# =========================
# 4. 启动应用入口
# =========================
if __name__ == "__main__":
with app.app_context():
db.create_all() # 在应用上下文中创建表
app.run(debug=True, port=5001)
register.html:
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>注册title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
flex-direction: column; /* 垂直排列 */
}
h1 {
margin-bottom: 20px; /* 与表单的间距 */
}
.input-group {
display: flex;
align-items: flex-end;
gap: 10px;
}
input {
flex: 1;
padding: 5px;
}
button {
padding: 5px 10px;
font-size: 14px;
}
style>
head>
<body>
<h1>用户注册h1>
<form action="/register" method="POST">
<div>
<label for="username">用户名:label>
<input type="text" id="username" name="username" required>
div>
<div class="input-group">
<label for="password">密码:label>
<input type="password" id="password" name="password" required>
<button type="submit">注册button>
div>
form>
body>
html>
index.html文件需要将
<p>您尚未登录,请先 <a href="/login">登录a>。p>
改为
<p>您尚未登录,请先 <a href="/login">登录a> 或 <a href="/register">注册a>.p>
然后这是效果图
在此版本中,你就可以在“注册”页面创建自己的账户,然后凭借这个账号登录并使用 GPT 聊天功能了。
目标:除了能聊天,还希望把每一次对话都保存到数据库,并在前端提供接口查看“历史聊天”。
关键点:
新增 ChatRecord 模型,包含 user_id, question, answer, created_at;
在 /chat 路由中,成功获取回复后,先存库再返回给前端;
新增 history 接口,支持分页查询,返回前端浏览。
然后app.py需要新增和修改的代码如下:
# 1. 定义聊天记录表
class ChatRecord(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, nullable=False)
question = db.Column(db.Text, nullable=False)
answer = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 2. 聊天时保存记录
@app.route("/chat", methods=["POST"])
def chat():
if not session.get("logged_in"):
return jsonify({"error": "未登录"}), 401
data = request.get_json()
user_message = data.get("message", "").strip()
response = openai.ChatCompletion.create(...)
reply = response.choices[0].message.content.strip()
# 保存
record = ChatRecord(
user_id=session["user_id"],
question=user_message,
answer=reply
)
db.session.add(record)
db.session.commit()
return jsonify({"reply": reply})
# 3. 查询历史记录(带分页)
@app.route("/history", methods=["GET"])
def get_chat_history():
if not session.get("logged_in"):
return jsonify({"error": "未登录"}), 401
page = request.args.get("page", 1, type=int)
size = request.args.get("size", 10, type=int)
user_id = session["user_id"]
query = ChatRecord.query.filter_by(user_id=user_id).order_by(ChatRecord.created_at.desc())
total = query.count()
records = query.offset((page - 1) * size).limit(size).all()
data = []
for r in records:
data.append({
"id": r.id,
"question": r.question,
"answer": r.answer,
"created_at": r.created_at.isoformat()
})
return jsonify({
"records": data,
"total": total,
"total_pages": ceil(total / size),
"current_page": page,
"page_size": size
})
然后是修改后的index.html代码
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GPT 聊天title>
<style>
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
background-color: #f9f9f9;
}
.container {
width: 70%;
margin: 0 auto;
}
h1, h2 {
text-align: center;
margin-bottom: 20px;
}
.chat-box {
margin-bottom: 20px;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
position: relative; /* 设置父容器为相对定位 */
}
textarea {
width: calc(100% - 100px); /* 减去按钮的宽度及间距 */
resize: none;
padding: 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box; /* 确保宽度计算正确 */
}
.send-button {
position: absolute; /* 绝对定位按钮 */
bottom: 25px; /* 距离父容器底部 10px */
right: 10px; /* 距离父容器右侧 10px */
padding: 10px 20px;
font-size: 10px;
cursor: pointer;
border: none;
border-radius: 5px;
background-color: #007BFF;
color: white;
}
.send-button:hover {
background-color: #0056b3;
}
.response-box, .history-box {
margin-bottom: 20px;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}
.response-container {
min-height: 50px;
border: 1px solid #ddd;
padding: 10px;
background-color: #f5f5f5;
}
.history-content {
margin-top: 20px;
border: 1px solid #ddd;
padding: 10px;
background-color: #fff;
}
.logout-link {
display: block;
margin-top: 20px;
text-align: center;
text-decoration: none;
font-size: 16px;
color: #007BFF;
}
.logout-link:hover {
text-decoration: underline;
}
.pagination-buttons {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 10px;
}
style>
head>
<body>
{% if logged_in %}
<div class="container">
<p>用户{{ username }}已登录p>
<div class="chat-box">
<h2>GPT 聊天h2>
<p style="font-weight: bold;">输入您的问题:p>
<textarea id="userInput" rows="4" placeholder="请输入您的问题...">textarea>
<button onclick="sendMessage()" class="send-button">发送给 GPTbutton>
div>
<div class="response-box">
<h2>GPT 回复:h2>
<div id="responseBox" class="response-container">div>
div>
<div class="history-box">
<h2>聊天历史记录h2>
<button onclick="loadHistory()">查看历史记录button>
<div id="historyContent" class="history-content">div>
div>
div>
<a href="/logout" class="logout-link">退出登录a>
<script>
// 发送消息给GPT
async function sendMessage() {
const userInput = document.getElementById("userInput").value.trim();
if (!userInput) {
alert("输入不能为空!");
return;
}
try {
const res = await fetch("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userInput })
});
const data = await res.json();
document.getElementById("responseBox").innerText = data.reply || data.error || "未知错误";
} catch (error) {
console.error(error);
alert("请求出错了!");
}
}
// 加载历史记录
async function loadHistory(page=1, size=10) {
try {
const res = await fetch(`/history?page=${page}&size=${size}`);
const data = await res.json();
if (data.error) {
document.getElementById("historyContent").innerText = data.error;
return;
}
// 显示分页信息
let historyHtml = `第
${data.current_page} 页 / 共 ${data.total_pages} 页 (共 ${data.total} 条记录)`;
// 遍历记录,生成HTML
data.records.forEach(rec => {
historyHtml += `
时间: ${rec.created_at}
问题: ${rec.question}
回答: ${rec.answer}
`;
});
// 如果有多页,可以加“上一页/下一页”按钮
historyHtml += ' ';
document.getElementById("historyContent").innerHTML = historyHtml;
} catch (error) {
console.error(error);
alert("加载历史记录出错!");
}
}
script>
{% else %}
<p>您尚未登录,请先 <a href="/login">登录a> 或 <a href="/register">注册a>.p>
{% endif %}
body>
html>
至此,一套“登录 - 聊天 - 记录保存 - 历史查看”的基础应用就全部完成。
然后这是github 项目地址